feat(harness): add structural workflow guard gates
This commit is contained in:
@@ -321,6 +321,152 @@ Issue worktree workflow의 완료 보고에는 다음도 포함한다.
|
||||
|
||||
## Canonical external Issue verification artifacts
|
||||
|
||||
### Shared workflow guard gates
|
||||
|
||||
Workflow guard는 기존 위임·test-first·계약 보존 규칙의 구조적 완료 gate다. 쓰기를 막는 sandbox, 기록된 주장의 진실성을 인증하는 수단, 자연어 의미 판정기 또는 자동 복구 도구가 아니다. 보호 경로 밖 변경이나 변경 후 원상 복구까지 없었다고 증명하지 않는다. 실제 승인 근거·관찰·assertion 검출력은 main agent와 독립 reviewer가 확인한다.
|
||||
|
||||
지원 CLI는 아래 네 가지다. `ROOT`는 사용이 이미 승인된 exact Git checkout root이며 raw 절대경로의 alias·symlink를 허용하지 않는다. `INPUT`과 JSON input은 이미 존재하는 bounded 기록이고 snapshot 출력의 parent는 실행 UID 소유 mode `0700`이어야 한다. Helper는 directory를 만들거나 기존 권한을 repair하지 않고 새 snapshot만 mode `0600`·no-clobber로 생성한다.
|
||||
|
||||
```sh
|
||||
python3 <harness-root>/harness/lib/workflow_guard.py snapshot --repo-root ROOT --paths-json INPUT --output SNAPSHOT
|
||||
python3 <harness-root>/harness/lib/workflow_guard.py compare-source --repo-root ROOT --before SNAPSHOT --paths-json INPUT
|
||||
python3 <harness-root>/harness/lib/workflow_guard.py check-tests --input JSON
|
||||
python3 <harness-root>/harness/lib/workflow_guard.py check-contracts --input JSON
|
||||
```
|
||||
|
||||
- **기록 위치:** 제품 checkout 밖의 기존 `verification/<run-id>`에 snapshot·inventory·command/result를 보존한다. 먼저 `verification_root.py prepare`로 해당 run의 identity·lease·권한을 확인하고 기존 `evidence_manifest.py run/validate`의 command/claim 기록에 연결한다. 경로·v2 manifest schema를 바꾸거나 generic run record를 추가하지 않는다. Spec의 native-review 운영 artifact는 기존 trusted `harness/.runtime` 예외를 유지한다. Helper의 snapshot 출력은 검사 대상 Git root 밖이어야 하므로 harness 자체 유지보수에서 내부 진단 위치를 검사 대상 root의 예외로 자동 허용하지 않는다.
|
||||
- **읽기 전용 위임:** 위임 전에 보호 경로 JSON array와 HEAD, 파일 존재·종류·identity·digest를 immutable protected scope로 고정한다. 결과 채택 전에 같은 경로 집합으로 비교한다. 시작 baseline 누락·source 변경·경로 집합 drift는 검토 근거를 무효화하며 PASS를 채택하거나 자동 restore하지 않는다. 쓰기 worker는 승인된 변경 allowlist 안에서 수정하고 main agent가 실제 diff·test를 확인한다. 허용된 편집을 읽기 전용 위반으로 취급하지 않는다. 최종 독립 검토 중에는 보호 대상 mutator를 병렬 실행하지 않는다. Spec reviewer의 immutable provider/Git blob binding과 moving checkout 사용 금지는 그대로이며 이 gate가 별도 checkout·제품 test 실행 권한을 주지 않는다.
|
||||
- **기존 테스트:** 첫 편집 전에 canonical collection의 ID·skip·expected-failure 상태와 collection command/source/output 근거를 기록한다. 새 regression 추가는 허용한다. 기존 ID 삭제·rename·새 skip/xfail에는 exact old-to-current mapping, 승인 근거와 대체/의미 검토 근거가 필요하다. 변경된 assertion·fixture·mock의 의미 검토도 별도로 연결한다. 같은 개수·이름·exit 0만으로 검출력 보존을 판정하지 않는다. 수집하지 않은 framework나 case를 수집 완료라고 기록하지 않는다.
|
||||
- **Finding 재현:** 유효한 finding마다 current inventory의 named test와 구조화된 RED/GREEN 참조를 연결한다. 각 참조는 test ID, source revision/scope digest, command ID/argv, 실제·예상 exit와 evidence를 담는다. RED는 의도한 assertion/승인 동작의 nonzero 실패이고 GREEN은 exit 0이며 import·환경 오류를 RED로 인정하지 않는다. 반증된 지적에는 가짜 RED를 만들지 않는다. Helper는 참조 형식·연결·exit 조건을 확인하고 main agent는 실제 기록을 읽어 source binding과 실패 원인을 검증한다.
|
||||
- **계약·단계:** 변경 영향 범위의 기존 requirement뿐 아니라 API/CLI/응답 필드/상태 전이/cleanup 의미를 baseline inventory로 고정한다. 각 항목은 실제 current 항목과 검증 근거에 정확히 대응하거나 승인된 의도적 변경을 설명해야 한다. `missing`·`ambiguous`는 승인 문자열로 통과시킬 수 없고, 실제 public-response 관찰이 미확인이면 observed로 바꾸지 않는다. 초기화 선행 조건의 모순·미지 stage·순환 precedence를 거부하되 정상 renew 등 lifecycle loop를 초기화 precedence로 모델링해 금지하지 않는다.
|
||||
- **판정:** helper의 `status=pass`·exit 0은 제출된 구조적 gate가 통과했다는 뜻이며 제품 승인이나 evidence 인증을 대신하지 않는다. `status=block`/nonzero이면 해당 운영 근거를 채택하지 않고 누락·변경·판단 불가를 보고한다. 운영 gate 실패만으로 새로운 제품 acceptance criterion이나 자동 `REQUEST_CHANGES`를 만들지 않는다.
|
||||
|
||||
Input schema와 엄격한 type/key 규칙은 `harness/lib/workflow_guard.py`의 API 계약을 따른다. `paths-json`은 중복 없는 기존 repository-relative protected 파일 array다. `check-tests`는 `baseline_ids/current_ids`, `baseline_status/current_status`, source·collection argv·inventory digest·evidence를 가진 `baseline_collection/current_collection`, `authorized_changes`, `changed_tests`, `findings`를 연결한다. `check-contracts`는 `baseline/current/mappings`와 `stages/precedence`를 연결한다. 파일별 의미를 임의 추론하지 말고 helper의 실제 schema와 기록을 검증한다. 이 문서의 정적 contract test 통과는 agent가 실행 중 절차를 준수했다는 증명이 아니다.
|
||||
|
||||
#### Nested input contracts
|
||||
|
||||
All object keys below are required and exact; unknown/missing keys, duplicate JSON keys, duplicate list identities, incorrect scalar types and non-regular/unbounded JSON inputs block. Empty strings are allowed only where explicitly stated. References are nonempty strings identifying existing verification command/result or approval records, not their secret contents. Full source revision is lowercase 40-hex; byte/scope digests are lowercase 64-hex. Integer fields reject booleans. CLI exit codes are 0 for structural PASS, 1 for detected violations/drift, 2 for rejected input; APIs raise `GuardError` for rejected input or return a status/violations result.
|
||||
|
||||
**Test input:** exactly `baseline_ids`, `current_ids`, `baseline_status`, `current_status`, `baseline_collection`, `current_collection`, `authorized_changes`, `changed_tests`, `findings`.
|
||||
|
||||
- ID arrays contain unique nonempty strings. Each status object has exactly that inventory's ID keys; each value is `{"skip": boolean, "expected_failure": boolean}`. No omitted status is silently treated as false.
|
||||
- Each collection object is `{source_revision: full SHA, command_id: reference, argv: nonempty array of nonempty strings, inventory_sha256: digest, evidence: reference}`. Digest is SHA256 over UTF-8 bytes of `json.dumps({"ids": sorted(ids), "statuses": {id: statuses[id] for id in sorted(ids)}}, sort_keys=True, separators=(',', ':'))`; use Python's default JSON escaping. Main verifies collection actually covered the declared frameworks/cases.
|
||||
- `authorized_changes` is an array of `{old_id: baseline ID, kind: "delete" | "rename" | "status", replacement_ids: unique current ID array, reason: nonempty string, approval: reference, evidence: semantic-review reference}`. A delete requires the old ID absent and an empty replacement; a rename requires the old ID absent and at least one actual current replacement; status requires the old ID still present and exactly `[old_id]`. Each old ID has at most one approval row. No replacement is silently invented or reused by another approval. Unclassified empty mappings block. Ordinary added regression IDs do not require removal approval. Every removed ID and newly introduced skip/xfail is covered by a compatible explicit approval; new skipped tests need explicit replacement coverage, not an unrelated deletion.
|
||||
- `changed_tests` is an array of `{test_id: current ID, baseline_sha256: digest, current_sha256: digest, review_evidence: reference}`, unique by test ID. Main inventories actual assertion/fixture/mock edits; helper cannot discover an omitted changed-test declaration or prove semantic preservation.
|
||||
- `findings` is an array of `{id: unique nonempty string, test_id: current ID, classification: "assertion" | "approved_behavior", red: execution, green: execution}`. Each execution is `{test_id: same finding test, source_revision: full SHA, scope_sha256: digest, command_id: reference, argv: nonempty string array, exit_code: integer, expected_exit_code: same integer, failure_kind: string, evidence: reference}`. RED requires positive nonzero exit and failure_kind `assertion` or `approved_behavior`; GREEN requires exit 0 and failure_kind `pass`. Import/environment failure is invalid RED. Main inspects actual logs and ties each command to the named test and its exact tested source; expected/actual code equality alone does not prove that.
|
||||
|
||||
**Contract input:** exactly `baseline`, `current`, `mappings`, `stages`, `precedence`.
|
||||
|
||||
- `baseline` is a nonempty array of `{semantic_key: unique nonempty string, baseline_revision: full SHA, path: repository-relative path without empty/dot/traversal segments, meaning: nonempty string}`. It inventories old semantics, not merely current document headings.
|
||||
- `current` is an array of `{semantic_key: unique nonempty string, meaning: nonempty string, observation: "observed" | "not_applicable" | "unknown", evidence: reference}`. `unknown` blocks even with an evidence string; justified not_applicable is not a fabricated public-response observation. Main must check actual source/static contract and public response as applicable.
|
||||
- `mappings` is one row per baseline semantic key: `{semantic_key: baseline key, current_keys: unique current-key array, status: "preserved" | "intentionally_changed" | "missing" | "ambiguous", authorization: string, verification: string}`. Resolved rows require nonempty current_keys and verification; intentionally_changed additionally requires explicit authorization. Authorization may be empty for preserved rows. Missing/ambiguous always block even with authorization. New current keys and approved merges are permitted with complete old-to-current coverage: every old key retains its own row and semantic verification, and every current key is referenced. Several old rows may reference one merged current row; this is not permission to omit old meanings. Main checks preservation/substitution/merge semantics and real approvals independently.
|
||||
- `stages` is a nonempty unique string array; `precedence` is an array of unique `[prerequisite, dependent]` string pairs, both declared stages. Initialization precedence must be acyclic. Do not encode allowed renew/lifecycle loops as startup prerequisites.
|
||||
|
||||
**Binding procedure (shape-only, not evidence authentication):** collection inventory digest binds submitted ID/status data, not its truth. Helper does not open/authenticate evidence/approval references or compare their source_revision/scope_sha256 to a checkout. Before adopting PASS, main reads the referenced existing immutable command/result and approval records, verifies exact HEAD/provider blob and relevant source digests, actual argv/environment/exit/output and named RED failure cause, then independently reviews changed test/contract semantics. Missing/unreadable/wrong-source evidence is `evidence_gap`, not a product defect by itself. Product execution remains in existing `verification/<run-id>` with v2 manifest run/validate and exact-head readback; spec uses immutable authoritative blobs and its existing trusted runtime exception, never a moving implementation checkout or newly mandated product execution.
|
||||
|
||||
The following two inputs are runnable **synthetic structure examples only**; the `a` revision and synthetic evidence strings are not genuine verification records and cannot be adopted as product evidence. They demonstrate the exact nested shape and actual calculated inventory digest. For production, replace them with verified collection, source and evidence values before gate adoption.
|
||||
|
||||
```json
|
||||
{
|
||||
"baseline_ids": [
|
||||
"case_a"
|
||||
],
|
||||
"current_ids": [
|
||||
"case_a"
|
||||
],
|
||||
"baseline_status": {
|
||||
"case_a": {
|
||||
"skip": false,
|
||||
"expected_failure": false
|
||||
}
|
||||
},
|
||||
"current_status": {
|
||||
"case_a": {
|
||||
"skip": false,
|
||||
"expected_failure": false
|
||||
}
|
||||
},
|
||||
"baseline_collection": {
|
||||
"source_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"command_id": "fixture-collection",
|
||||
"argv": [
|
||||
"python3",
|
||||
"-B",
|
||||
"-m",
|
||||
"unittest",
|
||||
"case_a"
|
||||
],
|
||||
"inventory_sha256": "b2fac8d5f9e294a3d2c642955deab1bbf3bf8c9cf696526be56f2661b7eba455",
|
||||
"evidence": "synthetic-collection-record"
|
||||
},
|
||||
"current_collection": {
|
||||
"source_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"command_id": "fixture-collection",
|
||||
"argv": [
|
||||
"python3",
|
||||
"-B",
|
||||
"-m",
|
||||
"unittest",
|
||||
"case_a"
|
||||
],
|
||||
"inventory_sha256": "b2fac8d5f9e294a3d2c642955deab1bbf3bf8c9cf696526be56f2661b7eba455",
|
||||
"evidence": "synthetic-collection-record"
|
||||
},
|
||||
"authorized_changes": [],
|
||||
"changed_tests": [],
|
||||
"findings": []
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"baseline": [
|
||||
{
|
||||
"semantic_key": "ready-fields",
|
||||
"baseline_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"path": "src/broker.py",
|
||||
"meaning": "Public ready fields are handle/socket."
|
||||
}
|
||||
],
|
||||
"current": [
|
||||
{
|
||||
"semantic_key": "ready-fields",
|
||||
"meaning": "Public ready fields are handle/socket.",
|
||||
"observation": "observed",
|
||||
"evidence": "synthetic-response-record"
|
||||
}
|
||||
],
|
||||
"mappings": [
|
||||
{
|
||||
"semantic_key": "ready-fields",
|
||||
"current_keys": [
|
||||
"ready-fields"
|
||||
],
|
||||
"status": "preserved",
|
||||
"authorization": "",
|
||||
"verification": "synthetic-contract-record"
|
||||
}
|
||||
],
|
||||
"stages": [
|
||||
"runtime",
|
||||
"ready",
|
||||
"admission"
|
||||
],
|
||||
"precedence": [
|
||||
[
|
||||
"runtime",
|
||||
"ready"
|
||||
],
|
||||
[
|
||||
"ready",
|
||||
"admission"
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Invalid examples: rename with `replacement_ids: []` blocks (not an implicit deletion); `expected_exit_code: true` blocks; unknown observation or missing contract with nonempty authorization blocks; duplicate current contract rows and precedence cycles block. Static documentation tests establish documentation/schema consistency only, not agent runtime compliance.
|
||||
|
||||
Issue별 검증 실행 기록의 정본은 `<harness-root>/repositories/<owner>/<repo>/worktrees/issue-<number>/verification/<run-id>`이다. `<harness-root>`는 실제로 읽은 `harness/operations/repository-cache.md`에서 결정한다. 제품 source는 형제 `checkout/`에 있고 verification은 제품 Git tree 밖에 있다. 구현·재작업의 회차별 plan, evidence manifest, command output, RED/GREEN 결과, finding matrix와 head별 provenance를 제품 `docs/verification/`에 새로 누적하거나 evidence-only commit을 만들지 않는다. 승인된 제품 요구사항이 명시적으로 요구하는 정본 문서와 지속적인 테스트·synthetic fixture·재현 스크립트는 제품에 유지할 수 있다. 기존 파일을 자동 이동·삭제하거나 광범위 `.gitignore` 변경으로 처리하지 않는다.
|
||||
|
||||
- 경로 identity는 인증된 exact owner, repository와 양의 Issue 번호에서 결정한다. 기존 cache identifier 규칙을 따르며 `issue-<number>`는 선행 0 없는 십진수다. `<run-id>`는 `[A-Za-z0-9][A-Za-z0-9_.-]{0,127}` 형식의 새 식별자다. Stage, source revision 또는 실행을 바꾸면 새 run-id를 사용한다. Dot segment, 절대 artifact file 경로, traversal, symlink와 임의 temporary/artifact root는 거부한다. `<owner>/<repo>`를 성공한 lookup 결과에 맞춰 자동 정규화하지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user