feat(v3.6.7 Step 6 Phase 6.8): pattern-eval fixtures + harness + manifest validator + wrapper E2E (11 codex rounds, 0 P1) (#67)
* feat(v3.6.7 Step 6 Phase 6.8): pattern-eval fixtures + harness + manifest validator + wrapper E2E test
Spec: docs/design/2026-04-30-ars-v3.6.7-step-6-orchestrator-hooks-spec.md §7 (Step 8 eval) + Phase 6.1 deferred wrapper E2E.
- 17 micro-fixtures under tests/fixtures/v3_6_7_pattern_eval/ (A1-A5 narrative-side, B1-B5 instrument-side, C1-C3 publication-side, D1-D4 cross-cutting). Each: manifest.json + upstream_context/{passport_snippet.yaml,prior_artifacts/} + bad_run/{deliverable.md,expected_audit_findings.yaml,expected_orchestrator_action.yaml} + good_run/{...}. D2 special case: convergence-theatre PASS verdict + expected_d2_convergence_assertion field per §7.4 criterion 1.
- 1 chapter-level integration fixture under integration/chapter_level_run/ exercising A3+C2+D4+C1 across 3-round MATERIAL → ship_with_known_residue escalation per §7.3. round_{1,2,3}/<agent>/<files> + escalation/{user_response,expected_passport_state,expected_pipeline_outcome}.yaml.
- scripts/check_pattern_eval_manifest.py — fixture_kind discriminator routes between micro (§7.2) and integration (§7.3) JSON Schema 2020-12 manifest schemas; coverage cross-check enforces 17/17 numbered pattern IDs covered. 23-test suite at scripts/test_check_pattern_eval_manifest.py.
- scripts/test_pattern_eval_runtime.py — 69-test pytest harness reading expected_audit_findings.yaml as synthesized verdict and asserting against expected_orchestrator_action.yaml. Per-pattern parametrized: BAD signal matches, GOOD passes, run_id F1 regex compliance, BAD/GOOD run_id uniqueness, integration manifest structural checks, §7.5 17-of-17 coverage.
- scripts/test_run_codex_audit_e2e.py — Phase 6.1 deferred verification gate (Linux Bash 4+ only; macOS stock Bash 3.2 self-skips). Mocks codex CLI via PATH-prefix shim emitting canonical Phase 2 JSONL stream + canonical Section 6 verdict text. Validates wrapper produces 4 contract files (jsonl/sidecar/verdict/proposal entry) + 3 diagnostic files; proposal entry validates against audit_artifact_entry.schema.json --mode proposal (verified_at/verified_by absent per Pattern C3 defense); --dry-run writes nothing; --round=2 without --previous-findings rejected with EX_USAGE.
- .github/workflows/spec-consistency.yml — added 4 CI steps: Phase 6.8 manifest validation, pattern-eval-unit (micro fixtures), pattern-eval-integration (integration fixture), and Phase 6.1 wrapper E2E (Linux runner only via runner.os == 'Linux' guard).
- docs/design/TODO-l-doc-1-18-patterns-prose-retirement.md — files L-doc-1 follow-up ticket per §9.2 enumerating 8 retirement locations (4 in upstream main spec + 4 in this spec) for "18 patterns" → "17 patterns" docs-only PR.
- spec §9.2 L-doc-1 row updated to point at the new TODO file.
Verification:
- python3 scripts/check_pattern_eval_manifest.py: 17/17 micro-fixtures + 1 integration valid; coverage 17/17.
- pytest scripts/test_pattern_eval_runtime.py + test_check_pattern_eval_manifest.py: 97/97 passed.
- Full repo regression: 704 pytest passed + 3 skipped (Bash 3.2 E2E gate) + 251 unittest passed = 955 green.
Phase 6.1 wrapper E2E is the deferred §10 verification gate originally noted in PR #55 as "macOS Bash 3.2 blocking inside session." CI now exercises full dispatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 1 — 8 findings closed (4 P1 + 4 P2)
Round 1 trajectory: P1×4 + P2×4 + P3×1 → P3×1 remaining (deferred — see below).
P1 closures:
- F-001 schema fidelity: D4 BAD had rogue `pattern:` key + missing required fields; integration verdicts used `i101..i303` non-hex run_id suffixes; integration finding IDs `A3-r1-finding-1` etc. didn't match `^F-[0-9]{3,}$`. Fix: rewrote D4 BAD to schema-conformant shape; sed-replaced integration suffixes `iNNN` → `fNNN` (hex); renamed integration finding IDs to F-101/F-102/F-103/F-104 (round 1) / F-201/F-202 (round 2) / F-301/F-302 (round 3) — encoding round + agent index for traceability while satisfying schema. Augmented manifest validator to validate every fixture's expected_audit_findings.yaml against audit_verdict.schema.json (closes the gap that allowed the brief-driven drift to ship).
- F-002 pattern_to_dimension_mapping: brief table was wrong against shared/templates/codex_audit_multifile_template.md §3 "Patterns surfaced" lines. Fix: rebuilt the harness's PATTERN_TO_DIMENSION map and swept all 17 micro-fixture BAD verdicts:
A1 3.1→3.4 (cross-section coherence), A2 3.3→3.2 (hallucination), A3 3.2→3.1 (cross-reference), A4 3.1→3.3 (primary-source integrity), A5 3.4→3.2 (hallucination), C2 4(f)→3.7 (COI/disclosure), C3 3.6→3.2 (output metadata claim / hallucination), D1 3.1→3.4 (cross-file coherence), D3 3.7→3.6 (round framing). C1 stays 4(f), D4 stays 4(f), B1-B5 stay 3.5. Integration: round 1 C2 finding 4(f)→3.7; synthesis A3 across all rounds 3.2→3.1.
- F-003 lifecycle_rule_not_mechanical: PHASE_TO_PASSPORT_MUTATION was partial (omitted P-PB-dup-* / P-PB-consume-fail / P-PB-crash). Fix: extended to all 24 §5.6 inventory rows + 2 happy-path phases = 26 entries. Duplicate / consume / crash rows marked "conditional" since their final passport state depends on prior session outcome (per §5.6 prose). Added EXPECTED_PHASE_COUNT assertion + test_every_fixture_phase_in_inventory coverage check that fails if any fixture references a non-inventoried phase.
- F-004 integration_harness_gap: prior tests only checked manifest fields / directories. Fix: added 4 new tests:
* test_integration_round_per_agent_verdicts_validate (parametrized over rounds 1/2/3) — drives each per-round per-agent expected_audit_findings.yaml through schema + status/count consistency + finding-tally checks; asserts at least one agent emits the manifest's declared expected_verdict per round.
* test_integration_round_pipeline_state_consistent (parametrized) — asserts expected_pipeline_state.yaml's audit_artifact_appended[].run_id matches per-agent verdict run_ids for that round.
* test_integration_escalation_passport_consistent — asserts user_response.acknowledged_finding_ids == manifest.expected_acknowledgement_finding_ids == passport.audit_artifact[].acknowledgement[].finding_ids set; user_choice == ship_with_known_residue.
* test_integration_acknowledged_findings_exist_in_round_3 — closes the §3.7 family A row A4 hand-edit attack surface where ack ids reference non-existent findings.
P2 closures:
- F-005 integration_ack_ids: manifest expected `C2-r3-finding-1` but round 3 has no C2 finding (C2 closed in round 2 fix). Manifest realigned to F-301 (synthesis A3 round-3 residue) + F-302 (compiler D4 round-3 residue), matching what user_response.yaml and expected_passport_state.yaml acknowledge.
- F-006 expected_action_not_checked: D4 BAD had impossible expected_path=A + expected_phase=B10 (Path A doesn't append, B10 is Path B). Harness never asserted expected_path or block_message. Fix: D4 BAD expected_path A→B; harness now asserts expected_path matches phase semantics (P-PA-* / A7 → A; P-PB-* / B10 → B); BLOCKING verdicts must carry "[AUDIT GATE" substring in block_message; PASS verdicts must have empty block_message.
- F-007 manifest_path_escape: validator did `base / rel` + `exists()` only, allowing absolute paths or `..` escaping the fixture dir. Fix: added _validate_fixture_path_safety rejecting absolute paths, `..` segments, and paths resolving outside the fixture base via `target.relative_to(base.resolve())`.
- F-008 ci_wiring_contract: spec §7.6 said "two separate test jobs" but Phase 6.8 ships them as two consecutive steps within the existing spec-consistency job (consistent with the 22 sibling lint steps already in spec-consistency.yml). Fix: added §7.6 deployment note explaining named-step deployment satisfies the unit-vs-integration legibility intent and pointing forward to a v3.6.8+ workflow refactor as the right boundary for promoting these to top-level jobs.
P3 deferred:
- F-009 audit_note_convention: codex flagged that the requested `<!-- AUDIT NOTE: <pattern-id> trigger/protection -->` convention isn't applied across all 34 micro deliverables. Brief never mandated this convention (only Team C/D added it organically). Treating as "convention is desirable but optional" — not adding 34 comments mechanically; would surface again as P3 in round 2 if codex still considers it load-bearing.
Test counts: scripts/ pytest 714 passed + 3 skipped (Bash 3.2 E2E gate); scripts/ unittest 145 passed. Total 859 green vs round-1's 955 (the integration harness expansion absorbed some test counts; net new logic with new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 2 — 3 findings closed (1 P1 + 2 P2)
Round 2 trajectory: P1×1 + P2×2 → 0 P1 + 0 P2 (target).
Closures:
- F-201 P1 integration_harness_coverage: prior tests were static-consistency only. Implemented test_integration_state_runner_drives_full_pipeline that drives the §7.3 5-step procedure end-to-end:
Step 1+2: load each round's per-agent verdicts + drive §5.6 Path B B10 decision per agent.
Step 3: assert expected_pipeline_state.yaml matches accumulated passport per round.
Step 4: at round-3 MATERIAL, emit escalation_prompt + feed escalation/user_response.yaml.
Step 5: append acknowledgement entries per §5.4 + assert expected_passport_state.yaml matches.
Plus _simulate_round helper accumulating passport state across rounds.
Added B11 (round-cap escalation) to PHASE_TO_PASSPORT_MUTATION (now 27 phases). Round 3 synthesis_agent + report_compiler_agent fixtures' expected_phase B10 → B11 to reflect §5.4 escalation semantics; research_architect_agent stays B10 (PASS).
- F-202 P2 finding_id_lineage: audit-template Section 6 line 157 contracts cumulative numbered findings carry forward IDs from round 1; new findings get next available ID. Round-1 fix renumbered these as F-201/F-301 etc. (round-prefixed) which broke the lineage. Reverted: A3 stays F-101 across rounds 1+2+3 (same partial-fix pattern); D4 stays F-103 across rounds 1+2+3. C2/C1 close in round 2 and never recur; F-102/F-104 stay round-1 only. Manifest acks + user_response acks + expected_passport ack finding_ids + expected_pipeline_outcome all updated to F-101 + F-103. New test_integration_finding_id_lineage_carry_forward enforces this.
- F-203 P2 dimension_fidelity: B5 was mapped to §3.5 (instrument quality aggregate) but audit template §3.5 line 100 enumerates B1-B4 only; B5 surfaces in §3.1 (primary-source list) line 76. Fix: PATTERN_TO_DIMENSION B5 3.5→3.1; tests/fixtures/v3_6_7_pattern_eval/B5/bad_run/expected_audit_findings.yaml dimension 3.5→3.1.
Round-2 retired: codex agreed F-009 audit-note convention should not be enforced — brief did not mandate it.
Test counts: scripts/ pytest 716 passed + 3 skipped; scripts/ unittest 145 passed. Total 861 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 3 — 2 findings closed (2 P2)
Round 3 trajectory: P2×2 → 0 P1 + 0 P2 + 0 P3 (target).
Closures:
- F-301 P2 b11_phase_assertion: prior `-k` filter ('micro or run_id or coverage' / 'integration') in spec-consistency.yml skipped test_phase_inventory_complete and test_every_fixture_phase_in_inventory which carry no matching keyword. Plus _simulate_round didn't enforce B10 vs B11 split — round-3 MATERIAL fixture could regress to B10 silently. Fix: extended unit -k filter to include 'phase_inventory or fixture_phase'; _simulate_round now asserts that round-N MATERIAL agent at round_n == target_rounds emits B11 (escalation) and round_n < target_rounds emits B10.
- F-302 P2 outcome_lineage: expected_pipeline_outcome.yaml had F-103 listed in BOTH closed_findings and acknowledged_residue (impossible — closed and acknowledged are exclusive lifecycle terminals). The harness only checked stage_outcome / audit_gate_outcome strings, so the contradiction was untested. Fix: removed F-103 from closed_findings (D4 was carried across rounds 1→2→3 and acknowledged, never closed); added closed/acked disjointness assertion + acked-set equality check against user_response in test_integration_state_runner_drives_full_pipeline.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 4 — 1 finding closed (1 P2)
Round 4 trajectory: P2×1 → 0.
Closure:
- F-401 P2 integration_state_subset: per §7.3 lines 2089/2092, harness MUST compare expected pipeline/passport state to actual state. Prior implementation only compared run_id sets + ship_or_block label + ack pairs. Fix: extended Step 3 + Step 5 of test_integration_state_runner_drives_full_pipeline:
Step 3 now compares per-agent {run_id, agent, verdict_status} dicts (round implicit from directory) + ship_or_block + overall_verdict + findings_summary{p1,p2,p3} + escalation_prompt_emitted flag.
Step 5 now compares full passport {run_id, agent, verdict_status, round} sequence + outcome stage_outcome + audit_gate_outcome + proceed_to_next_stage + final_verdict_summary{rounds_used, target_rounds, unaddressed} fields.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 5 — 3 findings closed (1 P1 + 1 P2 + 1 P3)
Round 5 trajectory: P1×1 + P2×1 + P3×1 → 0.
Closures:
- F-501 P1 integration_path_a_reset_axis_missing: spec §7.3 line 1990 lists Path A vs Path B fall-through as a structural axis but every integration step declared expected_path: B. Fix: added _drive_path_a_reverification() helper to test_integration_state_runner_drives_full_pipeline. Before each subsequent round (round 2, round 3) the harness simulates Path A re-verification of every prior-round persisted entry via A7 happy path (NO new passport append per §5.6 A7 invariant). Asserts ≥6 Path A legs fired (3 agents × rounds 2+3) and all reach A7. Path A_reverification_log now serves as the §7.3 axis evidence.
- F-502 P2 integration_state_subset_not_closed: dead-code expected_passport_proceed assigned but never asserted; expected_user_options + proceed_to never compared. Fix: added Step 3e per-round expected_user_options assertion (round-cap escalation_prompt → §5.4 trio of ship_with_known_residue/another_round/abort_stage; non-final block → revise/re-audit option present). Step 5 now asserts passport.stage_outcome + passport.proceed_to (must be stage_3/4/5).
- F-503 P3 stale_concrete_manifest_snippet: spec §7.3 example payload still listed `["A3-finding-1", "C2-finding-1"]` but actual fixture uses F-101/F-103 (carry-forward IDs from round 1, not pattern-name-encoded). Fix: spec line 2079 example updated to `["F-101", "F-103"]` matching the fixture.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 6 — 3 findings closed (2 P2 + 1 P3)
Round 6 trajectory: P2×2 + P3×1 → 0.
Closures:
- F-601 P2 integration_state_equality_still_subset: tightened expected_user_options + proceed_to assertions to exact equality. Round-3 escalation must be EXACTLY {ship_with_known_residue, another_round, abort_stage} (not "any superset"); non-final block must include EXACTLY one revise/re-audit option AND abort_stage. proceed_to must equal "stage_3" exactly (not range stage_3/4/5).
- F-602 P2 integration_expected_action_fields_unasserted: per-agent expected_orchestrator_action fields previously checked only at micro-fixture tests. Now _simulate_round() asserts: (a) expected_path ∈ {A, B}; (b) phase→path consistency (P-PA-* / A7 → A; P-PB-* / B10 / B11 → B); (c) phase→passport_mutation per PHASE_TO_PASSPORT_MUTATION; (d) BLOCKING verdicts carry "[AUDIT GATE" in block_message; non-blocking carry "". An impossible integration action like expected_path: A + expected_phase: B11 now fails.
- F-603 P3 stale_phase_name_in_success_criterion: spec §7.4 line 2101 example listed "escalation" as the round-3 MATERIAL phase but the implemented + fixture-backed phase is B11. Fix: spec line 2101 example "escalation" → "B11".
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 7 — 2 findings closed (2 P2)
Round 7 trajectory: P2×2 → 0.
Closures:
- F-701 P2 integration_user_options_still_not_exact: tightened non-final block options to EXACTLY 2 entries (one revise/re-audit + abort_stage). Extras now rejected.
- F-702 P2 integration_phase_contract_self_referential_for_nonblocking: extended _simulate_round phase contract to PASS + MINOR verdicts. PASS may only be B10 (fresh merge) or A7 (Path A re-verify); MINOR is B10. B11 is reserved for round-cap MATERIAL escalation. A PASS step declaring B11 or P-PB-* is now rejected.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 8 — 3 findings closed (3 P2)
Round 8 trajectory: P2×3 → 0.
Closures:
- F-801 P2 micro_phase_contract_self_referential: extended R7 PASS/MINOR phase contract to micro BAD + GOOD test functions. Micro fixtures don't exercise round-cap, so B11 is rejected at the micro level. PASS micro fixtures (D2 special) require phase ∈ {B10, A7}; MINOR/MATERIAL/AUDIT_FAILED require phase=B10; GOOD always {B10, A7}.
- F-802 P2 b11_escalation_message_accepts_b10_block_prefix: round-3 B11 fixtures and harness conflated B10 [AUDIT GATE: proposal] with B11 escalation. Fix: round_3 synthesis + report_compiler expected_block_message updated to "[ESCALATION] round 3/3 MATERIAL — choices: ship_with_known_residue / another_round / abort_stage"; harness now branches per-phase (B11 → "[ESCALATION]" + three §5.4 choice tokens; B10 → "[AUDIT GATE").
- F-803 P2 duplicate_micro_fixture_directory_not_rejected: validator's directory-name match was conditional on `expected in PATTERN_IDS`, allowing an `A1_copy` directory carrying `pattern_id: A1` to pass while overwriting the legitimate dict entry. Fix: validator now hard-fails when directory name is not a known pattern ID, AND when pattern_id mismatches directory name.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 9 — 3 findings closed (2 P2 + 1 P3)
Round 9 trajectory: P2×2 + P3×1 → 0.
Closures:
- F-901 P2 failure_phase_synthetic_injections_missing: spec §7.3 line 2093 promised "Phase 6.8 harness's per-phase synthetic injections" but harness only checked phase map size. Fix: added parametrized synthetic injection tests covering all 26 §5.6 inventory phases. test_synthetic_inject_none_mutation_phase parametrized over 19 "Passport mutation: none" phases (must NOT append). test_synthetic_inject_append_mutation_phase parametrized over 3 "appended" phases (must append exactly one entry). test_synthetic_inject_conditional_phases_documented covers the 4 conditional dup/crash rows.
- F-902 P2 integration_supersession_axis_not_exercised: spec §7.3 line 2093 claims "representative Path B-supersession happy-path" but harness had no A1.5 / supersession_required logic. Fix: added test_synthetic_supersession_preflight_path_b_filters_higher_round (F-070 closure regression — B2 supersession-mode filter selects round-3 user-dispatched over round-1/round-2 leftover) + test_synthetic_supersession_empty_after_filter_blocks (F-072 closure — empty after filter MUST emit P-PB-supersede-missing BLOCK, not silently fall back).
- F-903 P3 c2_minor_stale_material_prose: spec §7.4 line 2099 said "16 of 17 micro-fixtures the failure signal is MATERIAL" but C2 BAD ships as MINOR by design (P3 reflexivity finding) and harness allows MATERIAL OR MINOR. Fix: spec line 2099 prose now reads "15 of 17 ... MATERIAL; for C2 ... MINOR with one P3 reflexivity-temporal-ambiguity finding; for D2 ... non-finding convergence-policy assertion".
Test counts: 109 → 135 (26 new tests: 19 none-mutation + 3 appended-mutation + 1 conditional-doc + 2 supersession + 1 D2 lineage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 10 — 1 finding closed (1 P2)
Round 10 trajectory: P2×1 → 0.
Closure:
- F-1001 P2 round9_synthetic_tests_not_wired_to_ci: R9 added 26 test_synthetic_* tests but CI workflow's -k filters omitted them. Codex verified empirically: -k "synthetic" collected 26 tests not running on CI. Fix: extended unit step -k filter to include "synthetic and not supersession" (24 phase injection tests); extended integration step -k filter to include "synthetic and supersession" (2 A1.5 tests). Both legs verified locally collect 95+17=112, full 135 with manifest validator.
Test counts: 135 passed, all wired to CI now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(v3.6.7 Phase 6.8): CHANGELOG [Unreleased] entry
11 codex review rounds converged to 0 findings; cumulative 24 findings closed (4 P1 + 18 P2 + 2 P3) across rounds 1-10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): wrapper E2E test uses repo-relative --output-dir
CI failure: wrapper rejects absolute --output-dir paths with EX_USAGE.
Fix: pass "audit_artifacts" (repo-relative) instead of str(repo / "audit_artifacts").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): wrapper E2E mock emits parse_audit_verdict-compatible Section 6 text
CI failure: wrapper exit 70 (AUDIT_FAILED) — parse_audit_verdict.py rejected mock's "Verdict: PASS / Findings — P1: 0..." format. Parser expects "Round N: P1×n / P2×n / P3×n (N total)" or "Round N: 0 findings of any severity. Convergence reached." as the LAST non-empty line of verdict text. Fix: mock now emits the zero-findings convergence form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:37:15 +08:00
|
|
|
|
"""Test harness for ARS v3.6.7 Step 8 pattern-eval fixtures.
|
|
|
|
|
|
|
|
|
|
|
|
Spec: docs/design/2026-04-30-ars-v3.6.7-step-6-orchestrator-hooks-spec.md §7.4
|
|
|
|
|
|
|
|
|
|
|
|
This harness exercises §5.6 Path B's verdict-to-ship/block decision logic
|
|
|
|
|
|
against each fixture's BAD/GOOD pair (or per-round per-agent slots for the
|
|
|
|
|
|
integration fixture) without running real codex. The fixture's
|
|
|
|
|
|
`expected_audit_findings.yaml` is treated as the synthesized verdict that the
|
|
|
|
|
|
codex run *would have produced*, and the harness asserts the orchestrator's
|
|
|
|
|
|
expected action matches `expected_orchestrator_action.yaml`.
|
|
|
|
|
|
|
|
|
|
|
|
Coverage:
|
|
|
|
|
|
- All 17 micro-fixtures' BAD case produces the expected pattern signal:
|
|
|
|
|
|
* MATERIAL/MINOR: verdict_status + finding_count.severity tally + dimension match
|
|
|
|
|
|
* D2 special: PASS verdict + convergence-theatre assertion logged
|
|
|
|
|
|
- All 17 micro-fixtures' GOOD case produces PASS verdict + empty findings.
|
|
|
|
|
|
- Integration fixture: each round's per-agent verdict matches the expected
|
|
|
|
|
|
pipeline state; round-3 escalation produces ship_with_known_residue
|
|
|
|
|
|
acknowledgement append.
|
|
|
|
|
|
- §7.4 success criterion 4 (audit artifact lifecycle): inventory-driven
|
|
|
|
|
|
passport-mutation rule based on each fixture's expected_phase.
|
|
|
|
|
|
|
|
|
|
|
|
Run with: pytest -xvs scripts/test_pattern_eval_runtime.py
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import re
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
FIXTURE_ROOT = REPO_ROOT / "tests" / "fixtures" / "v3_6_7_pattern_eval"
|
|
|
|
|
|
|
|
|
|
|
|
PATTERN_IDS = (
|
|
|
|
|
|
"A1", "A2", "A3", "A4", "A5",
|
|
|
|
|
|
"B1", "B2", "B3", "B4", "B5",
|
|
|
|
|
|
"C1", "C2", "C3",
|
|
|
|
|
|
"D1", "D2", "D3", "D4",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
PATTERN_TO_DIMENSION = {
|
|
|
|
|
|
# Anchored in shared/templates/codex_audit_multifile_template.md §3 "Patterns
|
|
|
|
|
|
# surfaced" lines per dimension. Brief drift in earlier draft was caught by
|
|
|
|
|
|
# codex review round 1 F-002.
|
|
|
|
|
|
"A1": "3.4", # legal-effect drift / cross-section coherence
|
|
|
|
|
|
"A2": "3.2", # pending-source assumed as fact / hallucination
|
|
|
|
|
|
"A3": "3.1", # mis-anchored citation / cross-reference integrity
|
|
|
|
|
|
"A4": "3.3", # quote scope creep / primary-source integrity
|
|
|
|
|
|
"A5": "3.2", # sibling-document fabrication / hallucination
|
|
|
|
|
|
"B1": "3.5", # IRB terminology
|
|
|
|
|
|
"B2": "3.5", # pseudo-reverse-coded
|
|
|
|
|
|
"B3": "3.5", # event-anchor missing
|
|
|
|
|
|
"B4": "3.5", # leading items
|
|
|
|
|
|
"B5": "3.1", # primary-source list mismatch — audit template §3.1 explicitly lists B5 (line 76); §3.5 enumerates B1-B4 only (line 100)
|
|
|
|
|
|
"C1": "4(f)", # compression overclaim — 4(f) sub-check (ii) protected hedge
|
|
|
|
|
|
"C2": "3.7", # temporal ambiguity / COI disclosure
|
|
|
|
|
|
"C3": "3.2", # output metadata audit-passed claim / hallucination
|
|
|
|
|
|
"D1": "3.4", # multi-file deliverable cross-file inconsistency / coherence
|
|
|
|
|
|
"D3": "3.6", # PARTIAL ≠ CLOSED / round framing
|
|
|
|
|
|
"D4": "4(f)", # word-count cap bust — 4(f) sub-check (i)
|
|
|
|
|
|
# D2 is convergence theatre — no finding, special handling.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# §7.4 criterion 4 — inventory-driven passport mutation rule.
|
|
|
|
|
|
# Mirrors the §5.6 verification failure state inventory (24 rows: 7 P-PA-* + 17 P-PB-*)
|
|
|
|
|
|
# plus the two happy-path phases (A7, B10). When §5.6's inventory grows in v3.6.8+,
|
|
|
|
|
|
# new rows MUST extend this map; the inventory_coverage test below enforces sync.
|
|
|
|
|
|
# Closes codex F-003: inventory was previously partial (omitted P-PB-dup-* / consume / crash).
|
|
|
|
|
|
PHASE_TO_PASSPORT_MUTATION = {
|
|
|
|
|
|
# Happy paths
|
|
|
|
|
|
"A7": "none", # Path A success — entry already there
|
|
|
|
|
|
"B10": "appended", # Path B success — fresh proposal merged
|
|
|
|
|
|
# Path A failure phases — passport unchanged (silent fall-through to B)
|
|
|
|
|
|
"P-PA-precond": "none",
|
|
|
|
|
|
"P-PA-schema": "none",
|
|
|
|
|
|
"P-PA-gate": "none",
|
|
|
|
|
|
"P-PA-verdict-schema": "none",
|
|
|
|
|
|
"P-PA-verdict-mirror": "none",
|
|
|
|
|
|
"P-PA-stale-late": "none",
|
|
|
|
|
|
"P-PA-supersede-preempt": "none",
|
|
|
|
|
|
# Path B failure phases — proposal stays in <output-dir>, passport unchanged
|
|
|
|
|
|
"P-PB-empty": "none",
|
|
|
|
|
|
"P-PB-supersede-missing": "none",
|
|
|
|
|
|
"P-PB-ambig": "none",
|
|
|
|
|
|
"P-PB-proposal-schema": "none",
|
|
|
|
|
|
"P-PB-audit-failed": "none",
|
|
|
|
|
|
"P-PB-gate": "none",
|
|
|
|
|
|
"P-PB-verdict-schema": "none",
|
|
|
|
|
|
"P-PB-verdict-mirror": "none",
|
|
|
|
|
|
"P-PB-stale-late": "none",
|
|
|
|
|
|
"P-PB-snapshot": "none",
|
|
|
|
|
|
"P-PB-persisted-schema": "none",
|
|
|
|
|
|
"P-PB-passport-write": "none",
|
|
|
|
|
|
# Duplicate / consume / crash phases (continuation rows — final state checked,
|
|
|
|
|
|
# not intermediate). Each may yield "none" or "appended" depending on whether
|
|
|
|
|
|
# a subsequent candidate succeeds; the rule is the orchestrator commits at most
|
|
|
|
|
|
# one new persisted entry per successful merge, and B1a-recovery branches do NOT
|
|
|
|
|
|
# double-append. For success-path completion these reach B10 → "appended"; for
|
|
|
|
|
|
# short-circuit (B1a tuple-match supersession-false A3-A6 success) reach A7 →
|
|
|
|
|
|
# "none" reading the pre-existing entry.
|
|
|
|
|
|
"P-PB-dup-early": "conditional", # depends on A3-A6 outcome + supersession_required
|
|
|
|
|
|
"P-PB-dup-other": "conditional", # continues B1a/B2 with remaining candidates
|
|
|
|
|
|
"P-PB-dup-late": "conditional", # GO TO B10 reading pre-existing entry; no new append in current session
|
|
|
|
|
|
"P-PB-consume-fail": "appended", # B9 atomic-rename succeeded → entry committed
|
|
|
|
|
|
"P-PB-crash": "conditional", # depends on whether B9 atomic-rename fired
|
|
|
|
|
|
# Round-cap escalation phase (§5.4 / B11) — append still happens (B10 ran
|
|
|
|
|
|
# for round-N MATERIAL) but the orchestrator additionally emits the
|
|
|
|
|
|
# escalation prompt and awaits user choice. Integration round_3 fixtures
|
|
|
|
|
|
# use this phase explicitly per F-201 closure.
|
|
|
|
|
|
"B11": "appended", # round == target_rounds MATERIAL → escalation
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Total enumerated phases (must equal 24 §5.6 inventory rows + 2 happy-path
|
|
|
|
|
|
# B10/A7 + 1 round-cap escalation B11 = 27).
|
|
|
|
|
|
EXPECTED_PHASE_COUNT = 27
|
|
|
|
|
|
|
|
|
|
|
|
RUN_ID_REGEX = re.compile(
|
|
|
|
|
|
r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}Z-[0-9a-f]{4}$"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_yaml(path: Path) -> dict:
|
|
|
|
|
|
with path.open("r", encoding="utf-8") as fp:
|
|
|
|
|
|
return yaml.safe_load(fp)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_json(path: Path) -> dict:
|
|
|
|
|
|
with path.open("r", encoding="utf-8") as fp:
|
|
|
|
|
|
return json.load(fp)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-09 10:24:02 +08:00
|
|
|
|
_RETAINED_KEY_PSEUDONYM_CLAIM = re.compile(
|
|
|
|
|
|
r"(?:"
|
|
|
|
|
|
r"\b(?:a|an|any|the)?\s*retained\s+(?:(?:re-?link|link)\s+)?key\b|"
|
|
|
|
|
|
r"\b(?:keeping|retaining)\s+(?:a|an|any|the)?\s*"
|
|
|
|
|
|
r"(?:(?:re-?link|link)\s+)?key\b"
|
|
|
|
|
|
r").{0,120}\b(?:is|are|makes?|means?|constitutes?)\b.{0,80}"
|
|
|
|
|
|
r"\bpseudonymi[sz]\w*\b",
|
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
_PSEUDONYM_CLAIM_RETAINED_KEY = re.compile(
|
|
|
|
|
|
r"\bpseudonymi[sz]\w*\b.{0,100}\b(?:because|since|when|if|by)\b.{0,60}"
|
|
|
|
|
|
r"(?:"
|
|
|
|
|
|
r"\b(?:a|an|any|the)?\s*retained\s+(?:(?:re-?link|link)\s+)?key\b|"
|
|
|
|
|
|
r"\b(?:keeping|retaining)\s+(?:a|an|any|the)?\s*"
|
|
|
|
|
|
r"(?:(?:re-?link|link)\s+)?key\b"
|
|
|
|
|
|
r")",
|
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
_NAMED_TERMINOLOGY_SCOPE = re.compile(
|
|
|
|
|
|
r"\b(?:under|within|according to)\s+(?:(?:the|a|an)\s+)?(?:"
|
|
|
|
|
|
r"(?:named|selected|applicable|governing|project-specific)\b.{0,60}"
|
|
|
|
|
|
r"\b(?:authority|convention|regime|standard|gdpr)\b|gdpr\b)",
|
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
_NEGATED_PSEUDONYM_EFFECT = re.compile(
|
|
|
|
|
|
r"\b(?:do|does|did|is|are)\s+not\b.{0,40}"
|
|
|
|
|
|
r"\b(?:make|mean|constitute|pseudonymi[sz])\w*\b|"
|
|
|
|
|
|
r"\b(?:doesn['’]t|isn['’]t|aren['’]t|never)\b.{0,40}"
|
|
|
|
|
|
r"\b(?:make|mean|constitute|pseudonymi[sz])\w*\b",
|
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _unscoped_retained_key_pseudonym_clauses(text: str) -> list[str]:
|
|
|
|
|
|
"""Return clauses that turn retained linkage into a portable verdict.
|
|
|
|
|
|
|
|
|
|
|
|
The B1 oracle may describe terminology under a named governing convention,
|
|
|
|
|
|
or explicitly reject a portable implication. It must not infer a
|
|
|
|
|
|
pseudonymization status from retention of a key alone.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
normalized = " ".join(text.split())
|
|
|
|
|
|
clauses = [
|
|
|
|
|
|
clause.strip()
|
|
|
|
|
|
for clause in re.split(r"(?<=[.!?])\s+", normalized)
|
|
|
|
|
|
if clause.strip()
|
|
|
|
|
|
]
|
|
|
|
|
|
return [
|
|
|
|
|
|
clause
|
|
|
|
|
|
for clause in clauses
|
|
|
|
|
|
if (
|
|
|
|
|
|
_RETAINED_KEY_PSEUDONYM_CLAIM.search(clause)
|
|
|
|
|
|
or _PSEUDONYM_CLAIM_RETAINED_KEY.search(clause)
|
|
|
|
|
|
)
|
|
|
|
|
|
and _NAMED_TERMINOLOGY_SCOPE.search(clause) is None
|
|
|
|
|
|
and _NEGATED_PSEUDONYM_EFFECT.search(clause) is None
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(v3.6.7 Step 6 Phase 6.8): pattern-eval fixtures + harness + manifest validator + wrapper E2E (11 codex rounds, 0 P1) (#67)
* feat(v3.6.7 Step 6 Phase 6.8): pattern-eval fixtures + harness + manifest validator + wrapper E2E test
Spec: docs/design/2026-04-30-ars-v3.6.7-step-6-orchestrator-hooks-spec.md §7 (Step 8 eval) + Phase 6.1 deferred wrapper E2E.
- 17 micro-fixtures under tests/fixtures/v3_6_7_pattern_eval/ (A1-A5 narrative-side, B1-B5 instrument-side, C1-C3 publication-side, D1-D4 cross-cutting). Each: manifest.json + upstream_context/{passport_snippet.yaml,prior_artifacts/} + bad_run/{deliverable.md,expected_audit_findings.yaml,expected_orchestrator_action.yaml} + good_run/{...}. D2 special case: convergence-theatre PASS verdict + expected_d2_convergence_assertion field per §7.4 criterion 1.
- 1 chapter-level integration fixture under integration/chapter_level_run/ exercising A3+C2+D4+C1 across 3-round MATERIAL → ship_with_known_residue escalation per §7.3. round_{1,2,3}/<agent>/<files> + escalation/{user_response,expected_passport_state,expected_pipeline_outcome}.yaml.
- scripts/check_pattern_eval_manifest.py — fixture_kind discriminator routes between micro (§7.2) and integration (§7.3) JSON Schema 2020-12 manifest schemas; coverage cross-check enforces 17/17 numbered pattern IDs covered. 23-test suite at scripts/test_check_pattern_eval_manifest.py.
- scripts/test_pattern_eval_runtime.py — 69-test pytest harness reading expected_audit_findings.yaml as synthesized verdict and asserting against expected_orchestrator_action.yaml. Per-pattern parametrized: BAD signal matches, GOOD passes, run_id F1 regex compliance, BAD/GOOD run_id uniqueness, integration manifest structural checks, §7.5 17-of-17 coverage.
- scripts/test_run_codex_audit_e2e.py — Phase 6.1 deferred verification gate (Linux Bash 4+ only; macOS stock Bash 3.2 self-skips). Mocks codex CLI via PATH-prefix shim emitting canonical Phase 2 JSONL stream + canonical Section 6 verdict text. Validates wrapper produces 4 contract files (jsonl/sidecar/verdict/proposal entry) + 3 diagnostic files; proposal entry validates against audit_artifact_entry.schema.json --mode proposal (verified_at/verified_by absent per Pattern C3 defense); --dry-run writes nothing; --round=2 without --previous-findings rejected with EX_USAGE.
- .github/workflows/spec-consistency.yml — added 4 CI steps: Phase 6.8 manifest validation, pattern-eval-unit (micro fixtures), pattern-eval-integration (integration fixture), and Phase 6.1 wrapper E2E (Linux runner only via runner.os == 'Linux' guard).
- docs/design/TODO-l-doc-1-18-patterns-prose-retirement.md — files L-doc-1 follow-up ticket per §9.2 enumerating 8 retirement locations (4 in upstream main spec + 4 in this spec) for "18 patterns" → "17 patterns" docs-only PR.
- spec §9.2 L-doc-1 row updated to point at the new TODO file.
Verification:
- python3 scripts/check_pattern_eval_manifest.py: 17/17 micro-fixtures + 1 integration valid; coverage 17/17.
- pytest scripts/test_pattern_eval_runtime.py + test_check_pattern_eval_manifest.py: 97/97 passed.
- Full repo regression: 704 pytest passed + 3 skipped (Bash 3.2 E2E gate) + 251 unittest passed = 955 green.
Phase 6.1 wrapper E2E is the deferred §10 verification gate originally noted in PR #55 as "macOS Bash 3.2 blocking inside session." CI now exercises full dispatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 1 — 8 findings closed (4 P1 + 4 P2)
Round 1 trajectory: P1×4 + P2×4 + P3×1 → P3×1 remaining (deferred — see below).
P1 closures:
- F-001 schema fidelity: D4 BAD had rogue `pattern:` key + missing required fields; integration verdicts used `i101..i303` non-hex run_id suffixes; integration finding IDs `A3-r1-finding-1` etc. didn't match `^F-[0-9]{3,}$`. Fix: rewrote D4 BAD to schema-conformant shape; sed-replaced integration suffixes `iNNN` → `fNNN` (hex); renamed integration finding IDs to F-101/F-102/F-103/F-104 (round 1) / F-201/F-202 (round 2) / F-301/F-302 (round 3) — encoding round + agent index for traceability while satisfying schema. Augmented manifest validator to validate every fixture's expected_audit_findings.yaml against audit_verdict.schema.json (closes the gap that allowed the brief-driven drift to ship).
- F-002 pattern_to_dimension_mapping: brief table was wrong against shared/templates/codex_audit_multifile_template.md §3 "Patterns surfaced" lines. Fix: rebuilt the harness's PATTERN_TO_DIMENSION map and swept all 17 micro-fixture BAD verdicts:
A1 3.1→3.4 (cross-section coherence), A2 3.3→3.2 (hallucination), A3 3.2→3.1 (cross-reference), A4 3.1→3.3 (primary-source integrity), A5 3.4→3.2 (hallucination), C2 4(f)→3.7 (COI/disclosure), C3 3.6→3.2 (output metadata claim / hallucination), D1 3.1→3.4 (cross-file coherence), D3 3.7→3.6 (round framing). C1 stays 4(f), D4 stays 4(f), B1-B5 stay 3.5. Integration: round 1 C2 finding 4(f)→3.7; synthesis A3 across all rounds 3.2→3.1.
- F-003 lifecycle_rule_not_mechanical: PHASE_TO_PASSPORT_MUTATION was partial (omitted P-PB-dup-* / P-PB-consume-fail / P-PB-crash). Fix: extended to all 24 §5.6 inventory rows + 2 happy-path phases = 26 entries. Duplicate / consume / crash rows marked "conditional" since their final passport state depends on prior session outcome (per §5.6 prose). Added EXPECTED_PHASE_COUNT assertion + test_every_fixture_phase_in_inventory coverage check that fails if any fixture references a non-inventoried phase.
- F-004 integration_harness_gap: prior tests only checked manifest fields / directories. Fix: added 4 new tests:
* test_integration_round_per_agent_verdicts_validate (parametrized over rounds 1/2/3) — drives each per-round per-agent expected_audit_findings.yaml through schema + status/count consistency + finding-tally checks; asserts at least one agent emits the manifest's declared expected_verdict per round.
* test_integration_round_pipeline_state_consistent (parametrized) — asserts expected_pipeline_state.yaml's audit_artifact_appended[].run_id matches per-agent verdict run_ids for that round.
* test_integration_escalation_passport_consistent — asserts user_response.acknowledged_finding_ids == manifest.expected_acknowledgement_finding_ids == passport.audit_artifact[].acknowledgement[].finding_ids set; user_choice == ship_with_known_residue.
* test_integration_acknowledged_findings_exist_in_round_3 — closes the §3.7 family A row A4 hand-edit attack surface where ack ids reference non-existent findings.
P2 closures:
- F-005 integration_ack_ids: manifest expected `C2-r3-finding-1` but round 3 has no C2 finding (C2 closed in round 2 fix). Manifest realigned to F-301 (synthesis A3 round-3 residue) + F-302 (compiler D4 round-3 residue), matching what user_response.yaml and expected_passport_state.yaml acknowledge.
- F-006 expected_action_not_checked: D4 BAD had impossible expected_path=A + expected_phase=B10 (Path A doesn't append, B10 is Path B). Harness never asserted expected_path or block_message. Fix: D4 BAD expected_path A→B; harness now asserts expected_path matches phase semantics (P-PA-* / A7 → A; P-PB-* / B10 → B); BLOCKING verdicts must carry "[AUDIT GATE" substring in block_message; PASS verdicts must have empty block_message.
- F-007 manifest_path_escape: validator did `base / rel` + `exists()` only, allowing absolute paths or `..` escaping the fixture dir. Fix: added _validate_fixture_path_safety rejecting absolute paths, `..` segments, and paths resolving outside the fixture base via `target.relative_to(base.resolve())`.
- F-008 ci_wiring_contract: spec §7.6 said "two separate test jobs" but Phase 6.8 ships them as two consecutive steps within the existing spec-consistency job (consistent with the 22 sibling lint steps already in spec-consistency.yml). Fix: added §7.6 deployment note explaining named-step deployment satisfies the unit-vs-integration legibility intent and pointing forward to a v3.6.8+ workflow refactor as the right boundary for promoting these to top-level jobs.
P3 deferred:
- F-009 audit_note_convention: codex flagged that the requested `<!-- AUDIT NOTE: <pattern-id> trigger/protection -->` convention isn't applied across all 34 micro deliverables. Brief never mandated this convention (only Team C/D added it organically). Treating as "convention is desirable but optional" — not adding 34 comments mechanically; would surface again as P3 in round 2 if codex still considers it load-bearing.
Test counts: scripts/ pytest 714 passed + 3 skipped (Bash 3.2 E2E gate); scripts/ unittest 145 passed. Total 859 green vs round-1's 955 (the integration harness expansion absorbed some test counts; net new logic with new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 2 — 3 findings closed (1 P1 + 2 P2)
Round 2 trajectory: P1×1 + P2×2 → 0 P1 + 0 P2 (target).
Closures:
- F-201 P1 integration_harness_coverage: prior tests were static-consistency only. Implemented test_integration_state_runner_drives_full_pipeline that drives the §7.3 5-step procedure end-to-end:
Step 1+2: load each round's per-agent verdicts + drive §5.6 Path B B10 decision per agent.
Step 3: assert expected_pipeline_state.yaml matches accumulated passport per round.
Step 4: at round-3 MATERIAL, emit escalation_prompt + feed escalation/user_response.yaml.
Step 5: append acknowledgement entries per §5.4 + assert expected_passport_state.yaml matches.
Plus _simulate_round helper accumulating passport state across rounds.
Added B11 (round-cap escalation) to PHASE_TO_PASSPORT_MUTATION (now 27 phases). Round 3 synthesis_agent + report_compiler_agent fixtures' expected_phase B10 → B11 to reflect §5.4 escalation semantics; research_architect_agent stays B10 (PASS).
- F-202 P2 finding_id_lineage: audit-template Section 6 line 157 contracts cumulative numbered findings carry forward IDs from round 1; new findings get next available ID. Round-1 fix renumbered these as F-201/F-301 etc. (round-prefixed) which broke the lineage. Reverted: A3 stays F-101 across rounds 1+2+3 (same partial-fix pattern); D4 stays F-103 across rounds 1+2+3. C2/C1 close in round 2 and never recur; F-102/F-104 stay round-1 only. Manifest acks + user_response acks + expected_passport ack finding_ids + expected_pipeline_outcome all updated to F-101 + F-103. New test_integration_finding_id_lineage_carry_forward enforces this.
- F-203 P2 dimension_fidelity: B5 was mapped to §3.5 (instrument quality aggregate) but audit template §3.5 line 100 enumerates B1-B4 only; B5 surfaces in §3.1 (primary-source list) line 76. Fix: PATTERN_TO_DIMENSION B5 3.5→3.1; tests/fixtures/v3_6_7_pattern_eval/B5/bad_run/expected_audit_findings.yaml dimension 3.5→3.1.
Round-2 retired: codex agreed F-009 audit-note convention should not be enforced — brief did not mandate it.
Test counts: scripts/ pytest 716 passed + 3 skipped; scripts/ unittest 145 passed. Total 861 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 3 — 2 findings closed (2 P2)
Round 3 trajectory: P2×2 → 0 P1 + 0 P2 + 0 P3 (target).
Closures:
- F-301 P2 b11_phase_assertion: prior `-k` filter ('micro or run_id or coverage' / 'integration') in spec-consistency.yml skipped test_phase_inventory_complete and test_every_fixture_phase_in_inventory which carry no matching keyword. Plus _simulate_round didn't enforce B10 vs B11 split — round-3 MATERIAL fixture could regress to B10 silently. Fix: extended unit -k filter to include 'phase_inventory or fixture_phase'; _simulate_round now asserts that round-N MATERIAL agent at round_n == target_rounds emits B11 (escalation) and round_n < target_rounds emits B10.
- F-302 P2 outcome_lineage: expected_pipeline_outcome.yaml had F-103 listed in BOTH closed_findings and acknowledged_residue (impossible — closed and acknowledged are exclusive lifecycle terminals). The harness only checked stage_outcome / audit_gate_outcome strings, so the contradiction was untested. Fix: removed F-103 from closed_findings (D4 was carried across rounds 1→2→3 and acknowledged, never closed); added closed/acked disjointness assertion + acked-set equality check against user_response in test_integration_state_runner_drives_full_pipeline.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 4 — 1 finding closed (1 P2)
Round 4 trajectory: P2×1 → 0.
Closure:
- F-401 P2 integration_state_subset: per §7.3 lines 2089/2092, harness MUST compare expected pipeline/passport state to actual state. Prior implementation only compared run_id sets + ship_or_block label + ack pairs. Fix: extended Step 3 + Step 5 of test_integration_state_runner_drives_full_pipeline:
Step 3 now compares per-agent {run_id, agent, verdict_status} dicts (round implicit from directory) + ship_or_block + overall_verdict + findings_summary{p1,p2,p3} + escalation_prompt_emitted flag.
Step 5 now compares full passport {run_id, agent, verdict_status, round} sequence + outcome stage_outcome + audit_gate_outcome + proceed_to_next_stage + final_verdict_summary{rounds_used, target_rounds, unaddressed} fields.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 5 — 3 findings closed (1 P1 + 1 P2 + 1 P3)
Round 5 trajectory: P1×1 + P2×1 + P3×1 → 0.
Closures:
- F-501 P1 integration_path_a_reset_axis_missing: spec §7.3 line 1990 lists Path A vs Path B fall-through as a structural axis but every integration step declared expected_path: B. Fix: added _drive_path_a_reverification() helper to test_integration_state_runner_drives_full_pipeline. Before each subsequent round (round 2, round 3) the harness simulates Path A re-verification of every prior-round persisted entry via A7 happy path (NO new passport append per §5.6 A7 invariant). Asserts ≥6 Path A legs fired (3 agents × rounds 2+3) and all reach A7. Path A_reverification_log now serves as the §7.3 axis evidence.
- F-502 P2 integration_state_subset_not_closed: dead-code expected_passport_proceed assigned but never asserted; expected_user_options + proceed_to never compared. Fix: added Step 3e per-round expected_user_options assertion (round-cap escalation_prompt → §5.4 trio of ship_with_known_residue/another_round/abort_stage; non-final block → revise/re-audit option present). Step 5 now asserts passport.stage_outcome + passport.proceed_to (must be stage_3/4/5).
- F-503 P3 stale_concrete_manifest_snippet: spec §7.3 example payload still listed `["A3-finding-1", "C2-finding-1"]` but actual fixture uses F-101/F-103 (carry-forward IDs from round 1, not pattern-name-encoded). Fix: spec line 2079 example updated to `["F-101", "F-103"]` matching the fixture.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 6 — 3 findings closed (2 P2 + 1 P3)
Round 6 trajectory: P2×2 + P3×1 → 0.
Closures:
- F-601 P2 integration_state_equality_still_subset: tightened expected_user_options + proceed_to assertions to exact equality. Round-3 escalation must be EXACTLY {ship_with_known_residue, another_round, abort_stage} (not "any superset"); non-final block must include EXACTLY one revise/re-audit option AND abort_stage. proceed_to must equal "stage_3" exactly (not range stage_3/4/5).
- F-602 P2 integration_expected_action_fields_unasserted: per-agent expected_orchestrator_action fields previously checked only at micro-fixture tests. Now _simulate_round() asserts: (a) expected_path ∈ {A, B}; (b) phase→path consistency (P-PA-* / A7 → A; P-PB-* / B10 / B11 → B); (c) phase→passport_mutation per PHASE_TO_PASSPORT_MUTATION; (d) BLOCKING verdicts carry "[AUDIT GATE" in block_message; non-blocking carry "". An impossible integration action like expected_path: A + expected_phase: B11 now fails.
- F-603 P3 stale_phase_name_in_success_criterion: spec §7.4 line 2101 example listed "escalation" as the round-3 MATERIAL phase but the implemented + fixture-backed phase is B11. Fix: spec line 2101 example "escalation" → "B11".
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 7 — 2 findings closed (2 P2)
Round 7 trajectory: P2×2 → 0.
Closures:
- F-701 P2 integration_user_options_still_not_exact: tightened non-final block options to EXACTLY 2 entries (one revise/re-audit + abort_stage). Extras now rejected.
- F-702 P2 integration_phase_contract_self_referential_for_nonblocking: extended _simulate_round phase contract to PASS + MINOR verdicts. PASS may only be B10 (fresh merge) or A7 (Path A re-verify); MINOR is B10. B11 is reserved for round-cap MATERIAL escalation. A PASS step declaring B11 or P-PB-* is now rejected.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 8 — 3 findings closed (3 P2)
Round 8 trajectory: P2×3 → 0.
Closures:
- F-801 P2 micro_phase_contract_self_referential: extended R7 PASS/MINOR phase contract to micro BAD + GOOD test functions. Micro fixtures don't exercise round-cap, so B11 is rejected at the micro level. PASS micro fixtures (D2 special) require phase ∈ {B10, A7}; MINOR/MATERIAL/AUDIT_FAILED require phase=B10; GOOD always {B10, A7}.
- F-802 P2 b11_escalation_message_accepts_b10_block_prefix: round-3 B11 fixtures and harness conflated B10 [AUDIT GATE: proposal] with B11 escalation. Fix: round_3 synthesis + report_compiler expected_block_message updated to "[ESCALATION] round 3/3 MATERIAL — choices: ship_with_known_residue / another_round / abort_stage"; harness now branches per-phase (B11 → "[ESCALATION]" + three §5.4 choice tokens; B10 → "[AUDIT GATE").
- F-803 P2 duplicate_micro_fixture_directory_not_rejected: validator's directory-name match was conditional on `expected in PATTERN_IDS`, allowing an `A1_copy` directory carrying `pattern_id: A1` to pass while overwriting the legitimate dict entry. Fix: validator now hard-fails when directory name is not a known pattern ID, AND when pattern_id mismatches directory name.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 9 — 3 findings closed (2 P2 + 1 P3)
Round 9 trajectory: P2×2 + P3×1 → 0.
Closures:
- F-901 P2 failure_phase_synthetic_injections_missing: spec §7.3 line 2093 promised "Phase 6.8 harness's per-phase synthetic injections" but harness only checked phase map size. Fix: added parametrized synthetic injection tests covering all 26 §5.6 inventory phases. test_synthetic_inject_none_mutation_phase parametrized over 19 "Passport mutation: none" phases (must NOT append). test_synthetic_inject_append_mutation_phase parametrized over 3 "appended" phases (must append exactly one entry). test_synthetic_inject_conditional_phases_documented covers the 4 conditional dup/crash rows.
- F-902 P2 integration_supersession_axis_not_exercised: spec §7.3 line 2093 claims "representative Path B-supersession happy-path" but harness had no A1.5 / supersession_required logic. Fix: added test_synthetic_supersession_preflight_path_b_filters_higher_round (F-070 closure regression — B2 supersession-mode filter selects round-3 user-dispatched over round-1/round-2 leftover) + test_synthetic_supersession_empty_after_filter_blocks (F-072 closure — empty after filter MUST emit P-PB-supersede-missing BLOCK, not silently fall back).
- F-903 P3 c2_minor_stale_material_prose: spec §7.4 line 2099 said "16 of 17 micro-fixtures the failure signal is MATERIAL" but C2 BAD ships as MINOR by design (P3 reflexivity finding) and harness allows MATERIAL OR MINOR. Fix: spec line 2099 prose now reads "15 of 17 ... MATERIAL; for C2 ... MINOR with one P3 reflexivity-temporal-ambiguity finding; for D2 ... non-finding convergence-policy assertion".
Test counts: 109 → 135 (26 new tests: 19 none-mutation + 3 appended-mutation + 1 conditional-doc + 2 supersession + 1 D2 lineage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 10 — 1 finding closed (1 P2)
Round 10 trajectory: P2×1 → 0.
Closure:
- F-1001 P2 round9_synthetic_tests_not_wired_to_ci: R9 added 26 test_synthetic_* tests but CI workflow's -k filters omitted them. Codex verified empirically: -k "synthetic" collected 26 tests not running on CI. Fix: extended unit step -k filter to include "synthetic and not supersession" (24 phase injection tests); extended integration step -k filter to include "synthetic and supersession" (2 A1.5 tests). Both legs verified locally collect 95+17=112, full 135 with manifest validator.
Test counts: 135 passed, all wired to CI now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(v3.6.7 Phase 6.8): CHANGELOG [Unreleased] entry
11 codex review rounds converged to 0 findings; cumulative 24 findings closed (4 P1 + 18 P2 + 2 P3) across rounds 1-10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): wrapper E2E test uses repo-relative --output-dir
CI failure: wrapper rejects absolute --output-dir paths with EX_USAGE.
Fix: pass "audit_artifacts" (repo-relative) instead of str(repo / "audit_artifacts").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): wrapper E2E mock emits parse_audit_verdict-compatible Section 6 text
CI failure: wrapper exit 70 (AUDIT_FAILED) — parse_audit_verdict.py rejected mock's "Verdict: PASS / Findings — P1: 0..." format. Parser expects "Round N: P1×n / P2×n / P3×n (N total)" or "Round N: 0 findings of any severity. Convergence reached." as the LAST non-empty line of verdict text. Fix: mock now emits the zero-findings convergence form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:37:15 +08:00
|
|
|
|
def _simulate_orchestrator_decision(
|
|
|
|
|
|
verdict: dict, expected_phase: str
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
"""Apply §5.6 Path B verdict→ship/block decision to a synthesized verdict.
|
|
|
|
|
|
|
|
|
|
|
|
Returns a dict with the orchestrator action that an actual run would
|
|
|
|
|
|
produce. This is a thin reimplementation of §5.6's B10 + §5.3 mapping.
|
|
|
|
|
|
"""
|
|
|
|
|
|
status = verdict.get("verdict_status")
|
|
|
|
|
|
if status == "PASS":
|
|
|
|
|
|
return {
|
|
|
|
|
|
"ship_or_block": "ship",
|
|
|
|
|
|
"expected_phase": expected_phase or "B10",
|
|
|
|
|
|
"passport_mutation": "appended",
|
|
|
|
|
|
}
|
|
|
|
|
|
if status == "MINOR":
|
|
|
|
|
|
return {
|
|
|
|
|
|
"ship_or_block": "mandatory_checkpoint",
|
|
|
|
|
|
"expected_phase": expected_phase or "B10",
|
|
|
|
|
|
"passport_mutation": "appended",
|
|
|
|
|
|
}
|
|
|
|
|
|
if status == "MATERIAL":
|
|
|
|
|
|
return {
|
|
|
|
|
|
"ship_or_block": "block",
|
|
|
|
|
|
"expected_phase": expected_phase or "B10",
|
|
|
|
|
|
"passport_mutation": "appended",
|
|
|
|
|
|
}
|
|
|
|
|
|
if status == "AUDIT_FAILED":
|
|
|
|
|
|
return {
|
|
|
|
|
|
"ship_or_block": "block",
|
|
|
|
|
|
"expected_phase": "P-PB-audit-failed",
|
|
|
|
|
|
"passport_mutation": "none",
|
|
|
|
|
|
}
|
|
|
|
|
|
raise ValueError(f"unknown verdict_status: {status!r}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _validate_finding_counts_match(verdict: dict) -> None:
|
|
|
|
|
|
"""§3.7 family A row A5 — finding_counts must agree with severity tally."""
|
|
|
|
|
|
counts = verdict["finding_counts"]
|
|
|
|
|
|
findings = verdict.get("findings", [])
|
|
|
|
|
|
tally = {"P1": 0, "P2": 0, "P3": 0}
|
|
|
|
|
|
for f in findings:
|
|
|
|
|
|
tally[f["severity"]] += 1
|
|
|
|
|
|
assert counts["p1"] == tally["P1"], (
|
|
|
|
|
|
f"finding_counts.p1={counts['p1']} but findings carry {tally['P1']} P1 entries"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert counts["p2"] == tally["P2"], (
|
|
|
|
|
|
f"finding_counts.p2={counts['p2']} but findings carry {tally['P2']} P2 entries"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert counts["p3"] == tally["P3"], (
|
|
|
|
|
|
f"finding_counts.p3={counts['p3']} but findings carry {tally['P3']} P3 entries"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _validate_status_count_consistency(verdict: dict) -> None:
|
|
|
|
|
|
"""§3.2 cross-field rules — status/count agreement."""
|
|
|
|
|
|
status = verdict["verdict_status"]
|
|
|
|
|
|
p1 = verdict["finding_counts"]["p1"]
|
|
|
|
|
|
p2 = verdict["finding_counts"]["p2"]
|
|
|
|
|
|
p3 = verdict["finding_counts"]["p3"]
|
|
|
|
|
|
if status == "PASS":
|
|
|
|
|
|
assert p1 == 0 and p2 == 0 and p3 == 0, (
|
|
|
|
|
|
f"PASS verdict requires all-zero finding_counts; got p1={p1} p2={p2} p3={p3}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "MINOR":
|
|
|
|
|
|
assert p1 == 0 and p2 == 0 and p3 <= 3, (
|
|
|
|
|
|
f"MINOR verdict requires p1=0, p2=0, p3<=3; got p1={p1} p2={p2} p3={p3}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "MATERIAL":
|
|
|
|
|
|
assert p1 > 0 or p2 > 0 or p3 > 3, (
|
|
|
|
|
|
f"MATERIAL verdict requires p1>0 OR p2>0 OR p3>3; got p1={p1} p2={p2} p3={p3}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "AUDIT_FAILED":
|
|
|
|
|
|
assert p1 == 0 and p2 == 0 and p3 == 0, (
|
|
|
|
|
|
f"AUDIT_FAILED verdict requires all-zero finding_counts; got p1={p1} p2={p2} p3={p3}"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert verdict.get("failure_reason"), (
|
|
|
|
|
|
"AUDIT_FAILED verdict requires non-empty failure_reason"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Per-pattern micro-fixture parametrization
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _all_micro_fixtures() -> list[str]:
|
|
|
|
|
|
if not FIXTURE_ROOT.exists():
|
|
|
|
|
|
return []
|
|
|
|
|
|
return [
|
|
|
|
|
|
d.name
|
|
|
|
|
|
for d in sorted(FIXTURE_ROOT.iterdir())
|
|
|
|
|
|
if d.is_dir() and d.name in PATTERN_IDS
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("pattern_id", _all_micro_fixtures())
|
|
|
|
|
|
def test_micro_bad_run_signal_matches_expectation(pattern_id):
|
|
|
|
|
|
"""§7.4 criterion 1: BAD case produces the pattern-specific failure signal."""
|
|
|
|
|
|
fixture_dir = FIXTURE_ROOT / pattern_id
|
|
|
|
|
|
manifest = _load_json(fixture_dir / "manifest.json")
|
|
|
|
|
|
bad_verdict = _load_yaml(fixture_dir / manifest["bad_run"]["expected_audit_findings_path"])
|
|
|
|
|
|
expected_action = _load_yaml(
|
|
|
|
|
|
fixture_dir / manifest["bad_run"]["expected_orchestrator_action_path"]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
_validate_status_count_consistency(bad_verdict)
|
|
|
|
|
|
_validate_finding_counts_match(bad_verdict)
|
|
|
|
|
|
|
|
|
|
|
|
if pattern_id == "D2":
|
|
|
|
|
|
# D2 convergence theatre — PASS verdict, but fixture must declare the
|
|
|
|
|
|
# convergence-policy assertion (per §7.4 criterion 1 special case).
|
|
|
|
|
|
assert bad_verdict["verdict_status"] == "PASS"
|
|
|
|
|
|
assert bad_verdict.get("findings", []) == []
|
|
|
|
|
|
assert "expected_d2_convergence_assertion" in expected_action, (
|
|
|
|
|
|
"D2 BAD case must carry expected_d2_convergence_assertion field"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert expected_action["expected_d2_convergence_assertion"], (
|
|
|
|
|
|
"D2 expected_d2_convergence_assertion must be non-empty"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# All other patterns — BAD must carry MATERIAL or MINOR verdict with
|
|
|
|
|
|
# at least one finding in the expected dimension.
|
|
|
|
|
|
status = bad_verdict["verdict_status"]
|
|
|
|
|
|
assert status in {"MATERIAL", "MINOR"}, (
|
|
|
|
|
|
f"{pattern_id} BAD verdict must be MATERIAL or MINOR (got {status})"
|
|
|
|
|
|
)
|
|
|
|
|
|
findings = bad_verdict.get("findings", [])
|
|
|
|
|
|
assert findings, f"{pattern_id} BAD verdict must carry at least one finding"
|
|
|
|
|
|
expected_dim = PATTERN_TO_DIMENSION[pattern_id]
|
|
|
|
|
|
actual_dims = {f["dimension"] for f in findings}
|
|
|
|
|
|
assert expected_dim in actual_dims, (
|
|
|
|
|
|
f"{pattern_id} BAD findings must include dimension {expected_dim}; "
|
|
|
|
|
|
f"got {actual_dims}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
decision = _simulate_orchestrator_decision(
|
|
|
|
|
|
bad_verdict, expected_action.get("expected_phase")
|
|
|
|
|
|
)
|
|
|
|
|
|
expected_phase = expected_action.get("expected_phase")
|
|
|
|
|
|
if expected_phase:
|
|
|
|
|
|
assert decision["expected_phase"] == expected_phase
|
|
|
|
|
|
expected_mutation = PHASE_TO_PASSPORT_MUTATION.get(expected_phase)
|
|
|
|
|
|
if expected_mutation not in (None, "conditional"):
|
|
|
|
|
|
actual_mutation = expected_action.get("expected_passport_mutation")
|
|
|
|
|
|
assert actual_mutation == expected_mutation, (
|
|
|
|
|
|
f"{pattern_id} BAD: expected_phase={expected_phase} maps to "
|
|
|
|
|
|
f"passport mutation {expected_mutation!r}, but fixture declares "
|
|
|
|
|
|
f"{actual_mutation!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# F-006 + F-801 closure: phase contract for micro BAD verdicts.
|
|
|
|
|
|
expected_path = expected_action.get("expected_path")
|
|
|
|
|
|
assert expected_path in {"A", "B"}, (
|
|
|
|
|
|
f"{pattern_id} BAD expected_path must be 'A' or 'B' (got {expected_path!r})"
|
|
|
|
|
|
)
|
|
|
|
|
|
if expected_phase:
|
|
|
|
|
|
if expected_phase.startswith("P-PA-") or expected_phase == "A7":
|
|
|
|
|
|
assert expected_path == "A", (
|
|
|
|
|
|
f"{pattern_id} BAD expected_phase={expected_phase} requires expected_path=A"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif expected_phase.startswith("P-PB-") or expected_phase in {"B10", "B11"}:
|
|
|
|
|
|
assert expected_path == "B", (
|
|
|
|
|
|
f"{pattern_id} BAD expected_phase={expected_phase} requires expected_path=B"
|
|
|
|
|
|
)
|
|
|
|
|
|
# F-801 closure: micro fixtures don't exercise round-cap, so B11 is rejected here.
|
|
|
|
|
|
bad_status = bad_verdict["verdict_status"]
|
|
|
|
|
|
if bad_status == "PASS":
|
|
|
|
|
|
# D2 convergence-theatre special case — verdict is PASS but pattern flagged.
|
|
|
|
|
|
assert expected_phase in {"B10", "A7"}, (
|
|
|
|
|
|
f"{pattern_id} BAD PASS verdict (D2 special) requires phase ∈ {{B10, A7}}; got {expected_phase!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif bad_status in {"MINOR", "MATERIAL", "AUDIT_FAILED"}:
|
|
|
|
|
|
assert expected_phase == "B10", (
|
|
|
|
|
|
f"{pattern_id} BAD {bad_status} verdict (micro fixture) requires phase=B10; got {expected_phase!r} "
|
|
|
|
|
|
"(B11 reserved for integration round-cap)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# F-006 closure: assert block_message non-empty for BLOCKING verdicts (MATERIAL/AUDIT_FAILED)
|
|
|
|
|
|
# and empty for non-blocking (PASS micro fixtures use Path B B10 with passport append + ship).
|
|
|
|
|
|
if pattern_id != "D2" and bad_verdict["verdict_status"] in {"MATERIAL", "AUDIT_FAILED"}:
|
|
|
|
|
|
msg = expected_action.get("expected_block_message", "")
|
|
|
|
|
|
assert "[AUDIT GATE" in msg, (
|
|
|
|
|
|
f"{pattern_id} BAD expected_block_message must include '[AUDIT GATE' "
|
|
|
|
|
|
f"substring per §5.6 BLOCK message format; got {msg!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("pattern_id", _all_micro_fixtures())
|
|
|
|
|
|
def test_micro_good_run_passes(pattern_id):
|
|
|
|
|
|
"""§7.4 criterion 2: GOOD case produces PASS + empty findings."""
|
|
|
|
|
|
fixture_dir = FIXTURE_ROOT / pattern_id
|
|
|
|
|
|
manifest = _load_json(fixture_dir / "manifest.json")
|
|
|
|
|
|
good_verdict = _load_yaml(
|
|
|
|
|
|
fixture_dir / manifest["good_run"]["expected_audit_findings_path"]
|
|
|
|
|
|
)
|
|
|
|
|
|
expected_action = _load_yaml(
|
|
|
|
|
|
fixture_dir / manifest["good_run"]["expected_orchestrator_action_path"]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
_validate_status_count_consistency(good_verdict)
|
|
|
|
|
|
_validate_finding_counts_match(good_verdict)
|
|
|
|
|
|
|
|
|
|
|
|
assert good_verdict["verdict_status"] == "PASS", (
|
|
|
|
|
|
f"{pattern_id} GOOD verdict must be PASS; got {good_verdict['verdict_status']}"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert good_verdict.get("findings", []) == [], (
|
|
|
|
|
|
f"{pattern_id} GOOD verdict must have empty findings list"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
decision = _simulate_orchestrator_decision(
|
|
|
|
|
|
good_verdict, expected_action.get("expected_phase")
|
|
|
|
|
|
)
|
|
|
|
|
|
assert decision["ship_or_block"] == "ship"
|
|
|
|
|
|
|
|
|
|
|
|
expected_phase = expected_action.get("expected_phase")
|
|
|
|
|
|
if expected_phase:
|
|
|
|
|
|
expected_mutation = PHASE_TO_PASSPORT_MUTATION.get(expected_phase)
|
|
|
|
|
|
actual_mutation = expected_action.get("expected_passport_mutation")
|
|
|
|
|
|
if expected_mutation not in (None, "conditional"):
|
|
|
|
|
|
assert actual_mutation == expected_mutation
|
|
|
|
|
|
|
|
|
|
|
|
# F-006 + F-801 closure: GOOD case Path-B with PASS verdict should have
|
|
|
|
|
|
# empty block_message AND phase ∈ {B10, A7} (B11 reserved for round-cap).
|
|
|
|
|
|
block_msg = expected_action.get("expected_block_message", "")
|
|
|
|
|
|
assert block_msg == "", (
|
|
|
|
|
|
f"{pattern_id} GOOD expected_block_message must be empty (PASS does not block); "
|
|
|
|
|
|
f"got {block_msg!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
expected_path = expected_action.get("expected_path")
|
|
|
|
|
|
assert expected_path in {"A", "B"}, (
|
|
|
|
|
|
f"{pattern_id} GOOD expected_path must be 'A' or 'B' (got {expected_path!r})"
|
|
|
|
|
|
)
|
|
|
|
|
|
if expected_phase:
|
|
|
|
|
|
assert expected_phase in {"B10", "A7"}, (
|
|
|
|
|
|
f"{pattern_id} GOOD PASS verdict requires phase ∈ {{B10, A7}}; got {expected_phase!r} "
|
|
|
|
|
|
"(P-PB-* / B11 not legitimate for PASS)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("pattern_id", _all_micro_fixtures())
|
|
|
|
|
|
def test_micro_run_id_format(pattern_id):
|
|
|
|
|
|
"""§3.7 family F F1: run_id matches canonical regex."""
|
|
|
|
|
|
fixture_dir = FIXTURE_ROOT / pattern_id
|
|
|
|
|
|
manifest = _load_json(fixture_dir / "manifest.json")
|
|
|
|
|
|
for slot in ("bad_run", "good_run"):
|
|
|
|
|
|
verdict = _load_yaml(fixture_dir / manifest[slot]["expected_audit_findings_path"])
|
|
|
|
|
|
rid = verdict.get("run_id", "")
|
|
|
|
|
|
assert RUN_ID_REGEX.match(rid), (
|
|
|
|
|
|
f"{pattern_id}/{slot}/expected_audit_findings.yaml run_id={rid!r} "
|
|
|
|
|
|
"does not match F1 regex"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("pattern_id", _all_micro_fixtures())
|
|
|
|
|
|
def test_micro_run_id_uniqueness_within_fixture(pattern_id):
|
|
|
|
|
|
"""A fixture's BAD and GOOD must use distinct run_ids (different audit runs)."""
|
|
|
|
|
|
fixture_dir = FIXTURE_ROOT / pattern_id
|
|
|
|
|
|
manifest = _load_json(fixture_dir / "manifest.json")
|
|
|
|
|
|
bad = _load_yaml(fixture_dir / manifest["bad_run"]["expected_audit_findings_path"])
|
|
|
|
|
|
good = _load_yaml(fixture_dir / manifest["good_run"]["expected_audit_findings_path"])
|
|
|
|
|
|
assert bad["run_id"] != good["run_id"], (
|
|
|
|
|
|
f"{pattern_id} BAD and GOOD must have distinct run_ids"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-09 10:24:02 +08:00
|
|
|
|
def test_b1_good_fixture_does_not_pass_unnamed_deidentification_status():
|
|
|
|
|
|
"""#680: the B1 PASS fixture must state facts, not an unnamed legal status."""
|
|
|
|
|
|
|
|
|
|
|
|
fixture_dir = FIXTURE_ROOT / "B1"
|
|
|
|
|
|
manifest = _load_json(fixture_dir / "manifest.json")
|
|
|
|
|
|
deliverable = (
|
|
|
|
|
|
fixture_dir / manifest["good_run"]["deliverable_path"]
|
|
|
|
|
|
).read_text(encoding="utf-8")
|
|
|
|
|
|
normalized = " ".join(deliverable.split())
|
|
|
|
|
|
|
|
|
|
|
|
assert "Aggregate or de-identified results may be published." not in deliverable
|
|
|
|
|
|
assert "identity IS known to the researcher" not in deliverable
|
|
|
|
|
|
assert (
|
|
|
|
|
|
"Anonymity — no identifier collected; cannot contact respondent"
|
|
|
|
|
|
not in deliverable
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "confidential within the named research-team boundary" in normalized
|
|
|
|
|
|
assert "ARS drafting convention" in normalized
|
|
|
|
|
|
assert "during the stated 12-month lifecycle" in normalized
|
|
|
|
|
|
assert "no record-level dataset release is planned" in normalized
|
|
|
|
|
|
assert "not an authority-defined de-identification status" in normalized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_b1_bad_finding_does_not_reclassify_a_retained_identifier():
|
|
|
|
|
|
"""#680: retained linkage is a fact, not a portable pseudonymization verdict."""
|
|
|
|
|
|
|
|
|
|
|
|
fixture_dir = FIXTURE_ROOT / "B1"
|
|
|
|
|
|
manifest = _load_json(fixture_dir / "manifest.json")
|
|
|
|
|
|
verdict = _load_yaml(
|
|
|
|
|
|
fixture_dir / manifest["bad_run"]["expected_audit_findings_path"]
|
|
|
|
|
|
)
|
|
|
|
|
|
finding = verdict["findings"][0]
|
|
|
|
|
|
finding_text = f"{finding['description']} {finding['suggested_fix']}"
|
|
|
|
|
|
|
|
|
|
|
|
assert _unscoped_retained_key_pseudonym_clauses(finding_text) == []
|
|
|
|
|
|
assert "Under the ARS drafting convention" in finding_text
|
|
|
|
|
|
assert "does not permit the auditor to" in finding_text
|
|
|
|
|
|
assert "governing authority, institutional convention, or" in finding_text
|
|
|
|
|
|
assert "re-link-key state" in finding_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
"claim",
|
|
|
|
|
|
(
|
|
|
|
|
|
"A retained re-link key is pseudonymized.",
|
|
|
|
|
|
"Data with a retained link key are pseudonymized.",
|
|
|
|
|
|
"Keeping a key makes the data pseudonymized.",
|
|
|
|
|
|
"Any retained re-link key means pseudonymized data.",
|
|
|
|
|
|
"A retained link key constitutes pseudonymization.",
|
|
|
|
|
|
"The data are pseudonymized because a retained re-link key exists.",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
def test_b1_retained_key_pseudonym_mutations_are_detected(claim: str):
|
|
|
|
|
|
assert _unscoped_retained_key_pseudonym_clauses(claim) == [claim]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
"discussion",
|
|
|
|
|
|
(
|
|
|
|
|
|
"Under the named governing convention, any retained re-link key means "
|
|
|
|
|
|
"pseudonymized data.",
|
|
|
|
|
|
"Within the applicable GDPR regime, data with a retained link key are "
|
|
|
|
|
|
"pseudonymized.",
|
|
|
|
|
|
"Keeping a key does not mean the data are pseudonymized across regimes.",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
def test_b1_named_or_negative_retained_key_discussion_is_allowed(
|
|
|
|
|
|
discussion: str,
|
|
|
|
|
|
):
|
|
|
|
|
|
assert _unscoped_retained_key_pseudonym_clauses(discussion) == []
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(v3.6.7 Step 6 Phase 6.8): pattern-eval fixtures + harness + manifest validator + wrapper E2E (11 codex rounds, 0 P1) (#67)
* feat(v3.6.7 Step 6 Phase 6.8): pattern-eval fixtures + harness + manifest validator + wrapper E2E test
Spec: docs/design/2026-04-30-ars-v3.6.7-step-6-orchestrator-hooks-spec.md §7 (Step 8 eval) + Phase 6.1 deferred wrapper E2E.
- 17 micro-fixtures under tests/fixtures/v3_6_7_pattern_eval/ (A1-A5 narrative-side, B1-B5 instrument-side, C1-C3 publication-side, D1-D4 cross-cutting). Each: manifest.json + upstream_context/{passport_snippet.yaml,prior_artifacts/} + bad_run/{deliverable.md,expected_audit_findings.yaml,expected_orchestrator_action.yaml} + good_run/{...}. D2 special case: convergence-theatre PASS verdict + expected_d2_convergence_assertion field per §7.4 criterion 1.
- 1 chapter-level integration fixture under integration/chapter_level_run/ exercising A3+C2+D4+C1 across 3-round MATERIAL → ship_with_known_residue escalation per §7.3. round_{1,2,3}/<agent>/<files> + escalation/{user_response,expected_passport_state,expected_pipeline_outcome}.yaml.
- scripts/check_pattern_eval_manifest.py — fixture_kind discriminator routes between micro (§7.2) and integration (§7.3) JSON Schema 2020-12 manifest schemas; coverage cross-check enforces 17/17 numbered pattern IDs covered. 23-test suite at scripts/test_check_pattern_eval_manifest.py.
- scripts/test_pattern_eval_runtime.py — 69-test pytest harness reading expected_audit_findings.yaml as synthesized verdict and asserting against expected_orchestrator_action.yaml. Per-pattern parametrized: BAD signal matches, GOOD passes, run_id F1 regex compliance, BAD/GOOD run_id uniqueness, integration manifest structural checks, §7.5 17-of-17 coverage.
- scripts/test_run_codex_audit_e2e.py — Phase 6.1 deferred verification gate (Linux Bash 4+ only; macOS stock Bash 3.2 self-skips). Mocks codex CLI via PATH-prefix shim emitting canonical Phase 2 JSONL stream + canonical Section 6 verdict text. Validates wrapper produces 4 contract files (jsonl/sidecar/verdict/proposal entry) + 3 diagnostic files; proposal entry validates against audit_artifact_entry.schema.json --mode proposal (verified_at/verified_by absent per Pattern C3 defense); --dry-run writes nothing; --round=2 without --previous-findings rejected with EX_USAGE.
- .github/workflows/spec-consistency.yml — added 4 CI steps: Phase 6.8 manifest validation, pattern-eval-unit (micro fixtures), pattern-eval-integration (integration fixture), and Phase 6.1 wrapper E2E (Linux runner only via runner.os == 'Linux' guard).
- docs/design/TODO-l-doc-1-18-patterns-prose-retirement.md — files L-doc-1 follow-up ticket per §9.2 enumerating 8 retirement locations (4 in upstream main spec + 4 in this spec) for "18 patterns" → "17 patterns" docs-only PR.
- spec §9.2 L-doc-1 row updated to point at the new TODO file.
Verification:
- python3 scripts/check_pattern_eval_manifest.py: 17/17 micro-fixtures + 1 integration valid; coverage 17/17.
- pytest scripts/test_pattern_eval_runtime.py + test_check_pattern_eval_manifest.py: 97/97 passed.
- Full repo regression: 704 pytest passed + 3 skipped (Bash 3.2 E2E gate) + 251 unittest passed = 955 green.
Phase 6.1 wrapper E2E is the deferred §10 verification gate originally noted in PR #55 as "macOS Bash 3.2 blocking inside session." CI now exercises full dispatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 1 — 8 findings closed (4 P1 + 4 P2)
Round 1 trajectory: P1×4 + P2×4 + P3×1 → P3×1 remaining (deferred — see below).
P1 closures:
- F-001 schema fidelity: D4 BAD had rogue `pattern:` key + missing required fields; integration verdicts used `i101..i303` non-hex run_id suffixes; integration finding IDs `A3-r1-finding-1` etc. didn't match `^F-[0-9]{3,}$`. Fix: rewrote D4 BAD to schema-conformant shape; sed-replaced integration suffixes `iNNN` → `fNNN` (hex); renamed integration finding IDs to F-101/F-102/F-103/F-104 (round 1) / F-201/F-202 (round 2) / F-301/F-302 (round 3) — encoding round + agent index for traceability while satisfying schema. Augmented manifest validator to validate every fixture's expected_audit_findings.yaml against audit_verdict.schema.json (closes the gap that allowed the brief-driven drift to ship).
- F-002 pattern_to_dimension_mapping: brief table was wrong against shared/templates/codex_audit_multifile_template.md §3 "Patterns surfaced" lines. Fix: rebuilt the harness's PATTERN_TO_DIMENSION map and swept all 17 micro-fixture BAD verdicts:
A1 3.1→3.4 (cross-section coherence), A2 3.3→3.2 (hallucination), A3 3.2→3.1 (cross-reference), A4 3.1→3.3 (primary-source integrity), A5 3.4→3.2 (hallucination), C2 4(f)→3.7 (COI/disclosure), C3 3.6→3.2 (output metadata claim / hallucination), D1 3.1→3.4 (cross-file coherence), D3 3.7→3.6 (round framing). C1 stays 4(f), D4 stays 4(f), B1-B5 stay 3.5. Integration: round 1 C2 finding 4(f)→3.7; synthesis A3 across all rounds 3.2→3.1.
- F-003 lifecycle_rule_not_mechanical: PHASE_TO_PASSPORT_MUTATION was partial (omitted P-PB-dup-* / P-PB-consume-fail / P-PB-crash). Fix: extended to all 24 §5.6 inventory rows + 2 happy-path phases = 26 entries. Duplicate / consume / crash rows marked "conditional" since their final passport state depends on prior session outcome (per §5.6 prose). Added EXPECTED_PHASE_COUNT assertion + test_every_fixture_phase_in_inventory coverage check that fails if any fixture references a non-inventoried phase.
- F-004 integration_harness_gap: prior tests only checked manifest fields / directories. Fix: added 4 new tests:
* test_integration_round_per_agent_verdicts_validate (parametrized over rounds 1/2/3) — drives each per-round per-agent expected_audit_findings.yaml through schema + status/count consistency + finding-tally checks; asserts at least one agent emits the manifest's declared expected_verdict per round.
* test_integration_round_pipeline_state_consistent (parametrized) — asserts expected_pipeline_state.yaml's audit_artifact_appended[].run_id matches per-agent verdict run_ids for that round.
* test_integration_escalation_passport_consistent — asserts user_response.acknowledged_finding_ids == manifest.expected_acknowledgement_finding_ids == passport.audit_artifact[].acknowledgement[].finding_ids set; user_choice == ship_with_known_residue.
* test_integration_acknowledged_findings_exist_in_round_3 — closes the §3.7 family A row A4 hand-edit attack surface where ack ids reference non-existent findings.
P2 closures:
- F-005 integration_ack_ids: manifest expected `C2-r3-finding-1` but round 3 has no C2 finding (C2 closed in round 2 fix). Manifest realigned to F-301 (synthesis A3 round-3 residue) + F-302 (compiler D4 round-3 residue), matching what user_response.yaml and expected_passport_state.yaml acknowledge.
- F-006 expected_action_not_checked: D4 BAD had impossible expected_path=A + expected_phase=B10 (Path A doesn't append, B10 is Path B). Harness never asserted expected_path or block_message. Fix: D4 BAD expected_path A→B; harness now asserts expected_path matches phase semantics (P-PA-* / A7 → A; P-PB-* / B10 → B); BLOCKING verdicts must carry "[AUDIT GATE" substring in block_message; PASS verdicts must have empty block_message.
- F-007 manifest_path_escape: validator did `base / rel` + `exists()` only, allowing absolute paths or `..` escaping the fixture dir. Fix: added _validate_fixture_path_safety rejecting absolute paths, `..` segments, and paths resolving outside the fixture base via `target.relative_to(base.resolve())`.
- F-008 ci_wiring_contract: spec §7.6 said "two separate test jobs" but Phase 6.8 ships them as two consecutive steps within the existing spec-consistency job (consistent with the 22 sibling lint steps already in spec-consistency.yml). Fix: added §7.6 deployment note explaining named-step deployment satisfies the unit-vs-integration legibility intent and pointing forward to a v3.6.8+ workflow refactor as the right boundary for promoting these to top-level jobs.
P3 deferred:
- F-009 audit_note_convention: codex flagged that the requested `<!-- AUDIT NOTE: <pattern-id> trigger/protection -->` convention isn't applied across all 34 micro deliverables. Brief never mandated this convention (only Team C/D added it organically). Treating as "convention is desirable but optional" — not adding 34 comments mechanically; would surface again as P3 in round 2 if codex still considers it load-bearing.
Test counts: scripts/ pytest 714 passed + 3 skipped (Bash 3.2 E2E gate); scripts/ unittest 145 passed. Total 859 green vs round-1's 955 (the integration harness expansion absorbed some test counts; net new logic with new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 2 — 3 findings closed (1 P1 + 2 P2)
Round 2 trajectory: P1×1 + P2×2 → 0 P1 + 0 P2 (target).
Closures:
- F-201 P1 integration_harness_coverage: prior tests were static-consistency only. Implemented test_integration_state_runner_drives_full_pipeline that drives the §7.3 5-step procedure end-to-end:
Step 1+2: load each round's per-agent verdicts + drive §5.6 Path B B10 decision per agent.
Step 3: assert expected_pipeline_state.yaml matches accumulated passport per round.
Step 4: at round-3 MATERIAL, emit escalation_prompt + feed escalation/user_response.yaml.
Step 5: append acknowledgement entries per §5.4 + assert expected_passport_state.yaml matches.
Plus _simulate_round helper accumulating passport state across rounds.
Added B11 (round-cap escalation) to PHASE_TO_PASSPORT_MUTATION (now 27 phases). Round 3 synthesis_agent + report_compiler_agent fixtures' expected_phase B10 → B11 to reflect §5.4 escalation semantics; research_architect_agent stays B10 (PASS).
- F-202 P2 finding_id_lineage: audit-template Section 6 line 157 contracts cumulative numbered findings carry forward IDs from round 1; new findings get next available ID. Round-1 fix renumbered these as F-201/F-301 etc. (round-prefixed) which broke the lineage. Reverted: A3 stays F-101 across rounds 1+2+3 (same partial-fix pattern); D4 stays F-103 across rounds 1+2+3. C2/C1 close in round 2 and never recur; F-102/F-104 stay round-1 only. Manifest acks + user_response acks + expected_passport ack finding_ids + expected_pipeline_outcome all updated to F-101 + F-103. New test_integration_finding_id_lineage_carry_forward enforces this.
- F-203 P2 dimension_fidelity: B5 was mapped to §3.5 (instrument quality aggregate) but audit template §3.5 line 100 enumerates B1-B4 only; B5 surfaces in §3.1 (primary-source list) line 76. Fix: PATTERN_TO_DIMENSION B5 3.5→3.1; tests/fixtures/v3_6_7_pattern_eval/B5/bad_run/expected_audit_findings.yaml dimension 3.5→3.1.
Round-2 retired: codex agreed F-009 audit-note convention should not be enforced — brief did not mandate it.
Test counts: scripts/ pytest 716 passed + 3 skipped; scripts/ unittest 145 passed. Total 861 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 3 — 2 findings closed (2 P2)
Round 3 trajectory: P2×2 → 0 P1 + 0 P2 + 0 P3 (target).
Closures:
- F-301 P2 b11_phase_assertion: prior `-k` filter ('micro or run_id or coverage' / 'integration') in spec-consistency.yml skipped test_phase_inventory_complete and test_every_fixture_phase_in_inventory which carry no matching keyword. Plus _simulate_round didn't enforce B10 vs B11 split — round-3 MATERIAL fixture could regress to B10 silently. Fix: extended unit -k filter to include 'phase_inventory or fixture_phase'; _simulate_round now asserts that round-N MATERIAL agent at round_n == target_rounds emits B11 (escalation) and round_n < target_rounds emits B10.
- F-302 P2 outcome_lineage: expected_pipeline_outcome.yaml had F-103 listed in BOTH closed_findings and acknowledged_residue (impossible — closed and acknowledged are exclusive lifecycle terminals). The harness only checked stage_outcome / audit_gate_outcome strings, so the contradiction was untested. Fix: removed F-103 from closed_findings (D4 was carried across rounds 1→2→3 and acknowledged, never closed); added closed/acked disjointness assertion + acked-set equality check against user_response in test_integration_state_runner_drives_full_pipeline.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 4 — 1 finding closed (1 P2)
Round 4 trajectory: P2×1 → 0.
Closure:
- F-401 P2 integration_state_subset: per §7.3 lines 2089/2092, harness MUST compare expected pipeline/passport state to actual state. Prior implementation only compared run_id sets + ship_or_block label + ack pairs. Fix: extended Step 3 + Step 5 of test_integration_state_runner_drives_full_pipeline:
Step 3 now compares per-agent {run_id, agent, verdict_status} dicts (round implicit from directory) + ship_or_block + overall_verdict + findings_summary{p1,p2,p3} + escalation_prompt_emitted flag.
Step 5 now compares full passport {run_id, agent, verdict_status, round} sequence + outcome stage_outcome + audit_gate_outcome + proceed_to_next_stage + final_verdict_summary{rounds_used, target_rounds, unaddressed} fields.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 5 — 3 findings closed (1 P1 + 1 P2 + 1 P3)
Round 5 trajectory: P1×1 + P2×1 + P3×1 → 0.
Closures:
- F-501 P1 integration_path_a_reset_axis_missing: spec §7.3 line 1990 lists Path A vs Path B fall-through as a structural axis but every integration step declared expected_path: B. Fix: added _drive_path_a_reverification() helper to test_integration_state_runner_drives_full_pipeline. Before each subsequent round (round 2, round 3) the harness simulates Path A re-verification of every prior-round persisted entry via A7 happy path (NO new passport append per §5.6 A7 invariant). Asserts ≥6 Path A legs fired (3 agents × rounds 2+3) and all reach A7. Path A_reverification_log now serves as the §7.3 axis evidence.
- F-502 P2 integration_state_subset_not_closed: dead-code expected_passport_proceed assigned but never asserted; expected_user_options + proceed_to never compared. Fix: added Step 3e per-round expected_user_options assertion (round-cap escalation_prompt → §5.4 trio of ship_with_known_residue/another_round/abort_stage; non-final block → revise/re-audit option present). Step 5 now asserts passport.stage_outcome + passport.proceed_to (must be stage_3/4/5).
- F-503 P3 stale_concrete_manifest_snippet: spec §7.3 example payload still listed `["A3-finding-1", "C2-finding-1"]` but actual fixture uses F-101/F-103 (carry-forward IDs from round 1, not pattern-name-encoded). Fix: spec line 2079 example updated to `["F-101", "F-103"]` matching the fixture.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 6 — 3 findings closed (2 P2 + 1 P3)
Round 6 trajectory: P2×2 + P3×1 → 0.
Closures:
- F-601 P2 integration_state_equality_still_subset: tightened expected_user_options + proceed_to assertions to exact equality. Round-3 escalation must be EXACTLY {ship_with_known_residue, another_round, abort_stage} (not "any superset"); non-final block must include EXACTLY one revise/re-audit option AND abort_stage. proceed_to must equal "stage_3" exactly (not range stage_3/4/5).
- F-602 P2 integration_expected_action_fields_unasserted: per-agent expected_orchestrator_action fields previously checked only at micro-fixture tests. Now _simulate_round() asserts: (a) expected_path ∈ {A, B}; (b) phase→path consistency (P-PA-* / A7 → A; P-PB-* / B10 / B11 → B); (c) phase→passport_mutation per PHASE_TO_PASSPORT_MUTATION; (d) BLOCKING verdicts carry "[AUDIT GATE" in block_message; non-blocking carry "". An impossible integration action like expected_path: A + expected_phase: B11 now fails.
- F-603 P3 stale_phase_name_in_success_criterion: spec §7.4 line 2101 example listed "escalation" as the round-3 MATERIAL phase but the implemented + fixture-backed phase is B11. Fix: spec line 2101 example "escalation" → "B11".
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 7 — 2 findings closed (2 P2)
Round 7 trajectory: P2×2 → 0.
Closures:
- F-701 P2 integration_user_options_still_not_exact: tightened non-final block options to EXACTLY 2 entries (one revise/re-audit + abort_stage). Extras now rejected.
- F-702 P2 integration_phase_contract_self_referential_for_nonblocking: extended _simulate_round phase contract to PASS + MINOR verdicts. PASS may only be B10 (fresh merge) or A7 (Path A re-verify); MINOR is B10. B11 is reserved for round-cap MATERIAL escalation. A PASS step declaring B11 or P-PB-* is now rejected.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 8 — 3 findings closed (3 P2)
Round 8 trajectory: P2×3 → 0.
Closures:
- F-801 P2 micro_phase_contract_self_referential: extended R7 PASS/MINOR phase contract to micro BAD + GOOD test functions. Micro fixtures don't exercise round-cap, so B11 is rejected at the micro level. PASS micro fixtures (D2 special) require phase ∈ {B10, A7}; MINOR/MATERIAL/AUDIT_FAILED require phase=B10; GOOD always {B10, A7}.
- F-802 P2 b11_escalation_message_accepts_b10_block_prefix: round-3 B11 fixtures and harness conflated B10 [AUDIT GATE: proposal] with B11 escalation. Fix: round_3 synthesis + report_compiler expected_block_message updated to "[ESCALATION] round 3/3 MATERIAL — choices: ship_with_known_residue / another_round / abort_stage"; harness now branches per-phase (B11 → "[ESCALATION]" + three §5.4 choice tokens; B10 → "[AUDIT GATE").
- F-803 P2 duplicate_micro_fixture_directory_not_rejected: validator's directory-name match was conditional on `expected in PATTERN_IDS`, allowing an `A1_copy` directory carrying `pattern_id: A1` to pass while overwriting the legitimate dict entry. Fix: validator now hard-fails when directory name is not a known pattern ID, AND when pattern_id mismatches directory name.
Test counts: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 9 — 3 findings closed (2 P2 + 1 P3)
Round 9 trajectory: P2×2 + P3×1 → 0.
Closures:
- F-901 P2 failure_phase_synthetic_injections_missing: spec §7.3 line 2093 promised "Phase 6.8 harness's per-phase synthetic injections" but harness only checked phase map size. Fix: added parametrized synthetic injection tests covering all 26 §5.6 inventory phases. test_synthetic_inject_none_mutation_phase parametrized over 19 "Passport mutation: none" phases (must NOT append). test_synthetic_inject_append_mutation_phase parametrized over 3 "appended" phases (must append exactly one entry). test_synthetic_inject_conditional_phases_documented covers the 4 conditional dup/crash rows.
- F-902 P2 integration_supersession_axis_not_exercised: spec §7.3 line 2093 claims "representative Path B-supersession happy-path" but harness had no A1.5 / supersession_required logic. Fix: added test_synthetic_supersession_preflight_path_b_filters_higher_round (F-070 closure regression — B2 supersession-mode filter selects round-3 user-dispatched over round-1/round-2 leftover) + test_synthetic_supersession_empty_after_filter_blocks (F-072 closure — empty after filter MUST emit P-PB-supersede-missing BLOCK, not silently fall back).
- F-903 P3 c2_minor_stale_material_prose: spec §7.4 line 2099 said "16 of 17 micro-fixtures the failure signal is MATERIAL" but C2 BAD ships as MINOR by design (P3 reflexivity finding) and harness allows MATERIAL OR MINOR. Fix: spec line 2099 prose now reads "15 of 17 ... MATERIAL; for C2 ... MINOR with one P3 reflexivity-temporal-ambiguity finding; for D2 ... non-finding convergence-policy assertion".
Test counts: 109 → 135 (26 new tests: 19 none-mutation + 3 appended-mutation + 1 conditional-doc + 2 supersession + 1 D2 lineage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): codex review round 10 — 1 finding closed (1 P2)
Round 10 trajectory: P2×1 → 0.
Closure:
- F-1001 P2 round9_synthetic_tests_not_wired_to_ci: R9 added 26 test_synthetic_* tests but CI workflow's -k filters omitted them. Codex verified empirically: -k "synthetic" collected 26 tests not running on CI. Fix: extended unit step -k filter to include "synthetic and not supersession" (24 phase injection tests); extended integration step -k filter to include "synthetic and supersession" (2 A1.5 tests). Both legs verified locally collect 95+17=112, full 135 with manifest validator.
Test counts: 135 passed, all wired to CI now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(v3.6.7 Phase 6.8): CHANGELOG [Unreleased] entry
11 codex review rounds converged to 0 findings; cumulative 24 findings closed (4 P1 + 18 P2 + 2 P3) across rounds 1-10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): wrapper E2E test uses repo-relative --output-dir
CI failure: wrapper rejects absolute --output-dir paths with EX_USAGE.
Fix: pass "audit_artifacts" (repo-relative) instead of str(repo / "audit_artifacts").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.8): wrapper E2E mock emits parse_audit_verdict-compatible Section 6 text
CI failure: wrapper exit 70 (AUDIT_FAILED) — parse_audit_verdict.py rejected mock's "Verdict: PASS / Findings — P1: 0..." format. Parser expects "Round N: P1×n / P2×n / P3×n (N total)" or "Round N: 0 findings of any severity. Convergence reached." as the LAST non-empty line of verdict text. Fix: mock now emits the zero-findings convergence form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:37:15 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Integration fixture
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _integration_dir() -> Path:
|
|
|
|
|
|
return FIXTURE_ROOT / "integration" / "chapter_level_run"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def integration_manifest():
|
|
|
|
|
|
return _load_json(_integration_dir() / "manifest.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_manifest_present():
|
|
|
|
|
|
assert _integration_dir().exists(), (
|
|
|
|
|
|
"Integration fixture directory missing; expected at "
|
|
|
|
|
|
"tests/fixtures/v3_6_7_pattern_eval/integration/chapter_level_run/"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert (_integration_dir() / "manifest.json").exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_three_round_escalation(integration_manifest):
|
|
|
|
|
|
"""§7.3: integration fixture exercises 3-round MATERIAL escalation."""
|
|
|
|
|
|
rounds = integration_manifest["rounds"]
|
|
|
|
|
|
assert len(rounds) == 3
|
|
|
|
|
|
for r in rounds:
|
|
|
|
|
|
assert r["target_rounds"] == 3
|
|
|
|
|
|
assert r["expected_verdict"] == "MATERIAL"
|
|
|
|
|
|
assert integration_manifest["escalation"]["user_choice"] == "ship_with_known_residue"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_per_round_per_agent_slots_present():
|
|
|
|
|
|
"""§7.3 directory tree: round_{1,2,3}/<agent>/<files>."""
|
|
|
|
|
|
base = _integration_dir()
|
|
|
|
|
|
for r in ("round_1", "round_2", "round_3"):
|
|
|
|
|
|
round_dir = base / r
|
|
|
|
|
|
assert round_dir.is_dir(), f"missing {r}/"
|
|
|
|
|
|
for agent in (
|
|
|
|
|
|
"synthesis_agent",
|
|
|
|
|
|
"research_architect_agent",
|
|
|
|
|
|
"report_compiler_agent",
|
|
|
|
|
|
):
|
|
|
|
|
|
assert (round_dir / agent).is_dir(), f"missing {r}/{agent}/"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_escalation_artifacts_present():
|
|
|
|
|
|
"""§7.3: escalation/ holds user_response + expected passport state."""
|
|
|
|
|
|
base = _integration_dir()
|
|
|
|
|
|
assert (base / "escalation").is_dir()
|
|
|
|
|
|
assert (base / "escalation" / "user_response.yaml").exists()
|
|
|
|
|
|
assert (base / "escalation" / "expected_passport_state.yaml").exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_patterns_triggered_subset(integration_manifest):
|
|
|
|
|
|
"""§7.3: patterns_triggered matches the curated A3+C2+D4+C1 subset."""
|
|
|
|
|
|
triggered = set(integration_manifest["patterns_triggered"])
|
|
|
|
|
|
assert triggered == {"A3", "C2", "D4", "C1"}, (
|
|
|
|
|
|
f"integration fixture must trigger A3+C2+D4+C1; got {triggered}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# F-004 closure: drive the §7.3 round/escalation scenario end-to-end against
|
|
|
|
|
|
# the fixture's expected verdicts and pipeline state.
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("round_n", [1, 2, 3])
|
|
|
|
|
|
def test_integration_round_per_agent_verdicts_validate(round_n):
|
|
|
|
|
|
"""Each per-round per-agent expected_audit_findings.yaml validates against
|
|
|
|
|
|
audit_verdict.schema.json AND its declared verdict_status matches the
|
|
|
|
|
|
round's manifest.expected_verdict for at least one agent (since not every
|
|
|
|
|
|
agent fails in every round)."""
|
|
|
|
|
|
base = _integration_dir() / f"round_{round_n}"
|
|
|
|
|
|
seen_verdicts = set()
|
|
|
|
|
|
for agent in ("synthesis_agent", "research_architect_agent", "report_compiler_agent"):
|
|
|
|
|
|
verdict_file = base / agent / "expected_audit_findings.yaml"
|
|
|
|
|
|
action_file = base / agent / "expected_orchestrator_action.yaml"
|
|
|
|
|
|
assert verdict_file.exists(), f"missing round {round_n}/{agent}/expected_audit_findings.yaml"
|
|
|
|
|
|
assert action_file.exists(), f"missing round {round_n}/{agent}/expected_orchestrator_action.yaml"
|
|
|
|
|
|
verdict = _load_yaml(verdict_file)
|
|
|
|
|
|
_validate_status_count_consistency(verdict)
|
|
|
|
|
|
_validate_finding_counts_match(verdict)
|
|
|
|
|
|
assert verdict["round"] == round_n, (
|
|
|
|
|
|
f"round {round_n}/{agent} verdict.round={verdict['round']} mismatches directory"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert verdict["target_rounds"] == 3
|
|
|
|
|
|
seen_verdicts.add(verdict["verdict_status"])
|
|
|
|
|
|
integration = _load_json(_integration_dir() / "manifest.json")
|
|
|
|
|
|
expected_round = next(r for r in integration["rounds"] if r["round"] == round_n)
|
|
|
|
|
|
assert expected_round["expected_verdict"] in seen_verdicts, (
|
|
|
|
|
|
f"round {round_n}: manifest declares verdict={expected_round['expected_verdict']} "
|
|
|
|
|
|
f"but no agent emits it (saw {seen_verdicts})"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("round_n", [1, 2, 3])
|
|
|
|
|
|
def test_integration_round_pipeline_state_consistent(round_n):
|
|
|
|
|
|
"""expected_pipeline_state.yaml's audit_artifact_appended[].run_id must
|
|
|
|
|
|
match the per-agent expected_audit_findings.yaml run_ids for the same round."""
|
|
|
|
|
|
base = _integration_dir() / f"round_{round_n}"
|
|
|
|
|
|
state = _load_yaml(base / "expected_pipeline_state.yaml")
|
|
|
|
|
|
state_run_ids = {entry["run_id"] for entry in state["audit_artifact_appended"]}
|
|
|
|
|
|
actual_run_ids = set()
|
|
|
|
|
|
for agent in ("synthesis_agent", "research_architect_agent", "report_compiler_agent"):
|
|
|
|
|
|
verdict = _load_yaml(base / agent / "expected_audit_findings.yaml")
|
|
|
|
|
|
actual_run_ids.add(verdict["run_id"])
|
|
|
|
|
|
assert state_run_ids == actual_run_ids, (
|
|
|
|
|
|
f"round {round_n} pipeline_state.run_ids != per-agent run_ids: "
|
|
|
|
|
|
f"state={state_run_ids} actual={actual_run_ids}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_escalation_passport_consistent():
|
|
|
|
|
|
"""§7.3 escalation: expected_passport_state.yaml acknowledgement entries'
|
|
|
|
|
|
finding_ids must match user_response.acknowledged_finding_ids AND the
|
|
|
|
|
|
manifest's escalation.expected_acknowledgement_finding_ids."""
|
|
|
|
|
|
base = _integration_dir()
|
|
|
|
|
|
user_response = _load_yaml(base / "escalation" / "user_response.yaml")
|
|
|
|
|
|
passport = _load_yaml(base / "escalation" / "expected_passport_state.yaml")
|
|
|
|
|
|
manifest = _load_json(base / "manifest.json")
|
|
|
|
|
|
|
|
|
|
|
|
user_ids = set(user_response["acknowledged_finding_ids"])
|
|
|
|
|
|
manifest_ids = set(manifest["escalation"]["expected_acknowledgement_finding_ids"])
|
|
|
|
|
|
assert user_ids == manifest_ids, (
|
|
|
|
|
|
f"user_response acks {user_ids} != manifest acks {manifest_ids}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
passport_ack_ids = set()
|
|
|
|
|
|
for entry in passport["audit_artifact"]:
|
|
|
|
|
|
ack = entry.get("acknowledgement")
|
|
|
|
|
|
if ack:
|
|
|
|
|
|
passport_ack_ids.update(ack["finding_ids"])
|
|
|
|
|
|
assert passport_ack_ids == user_ids, (
|
|
|
|
|
|
f"passport acks {passport_ack_ids} != user_response acks {user_ids}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert user_response["user_choice"] == manifest["escalation"]["user_choice"]
|
|
|
|
|
|
assert user_response["user_choice"] == "ship_with_known_residue"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_acknowledged_findings_exist_in_round_3():
|
|
|
|
|
|
"""§5.4: acknowledged finding_ids MUST appear as finding.id in the round-3
|
|
|
|
|
|
MATERIAL verdicts. Acknowledging a non-existent finding is a hand-edit
|
|
|
|
|
|
attack surface (§3.7 family A row A4 + B10 finding_ids cross-reference)."""
|
|
|
|
|
|
base = _integration_dir()
|
|
|
|
|
|
user_response = _load_yaml(base / "escalation" / "user_response.yaml")
|
|
|
|
|
|
acked = set(user_response["acknowledged_finding_ids"])
|
|
|
|
|
|
|
|
|
|
|
|
round_3_finding_ids = set()
|
|
|
|
|
|
for agent in ("synthesis_agent", "research_architect_agent", "report_compiler_agent"):
|
|
|
|
|
|
verdict = _load_yaml(base / "round_3" / agent / "expected_audit_findings.yaml")
|
|
|
|
|
|
for f in verdict.get("findings", []):
|
|
|
|
|
|
round_3_finding_ids.add(f["id"])
|
|
|
|
|
|
missing = acked - round_3_finding_ids
|
|
|
|
|
|
assert not missing, (
|
|
|
|
|
|
f"acknowledged finding_ids {missing} do not appear in any round-3 verdict; "
|
|
|
|
|
|
f"round 3 emits {round_3_finding_ids}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_finding_id_lineage_carry_forward():
|
|
|
|
|
|
"""Audit template Section 6 contract (line 157): cumulative numbered findings
|
|
|
|
|
|
carry forward IDs from round 1; new findings get next available ID. Closes codex F-202.
|
|
|
|
|
|
|
|
|
|
|
|
For the curated A3+C2+D4+C1 subset:
|
|
|
|
|
|
- A3 (synthesis_agent) round-1 finding gets ID X; same A3 partial-fix surfaces
|
|
|
|
|
|
at round 2 and 3 → MUST carry the same ID X. (A3 is the lineage with rounds 1+2+3.)
|
|
|
|
|
|
- D4 (report_compiler_agent) round-1 finding gets ID Y; same D4 word-cap residue
|
|
|
|
|
|
at rounds 2 and 3 → MUST carry the same ID Y.
|
|
|
|
|
|
- C2 / C1 close in round 2 (acknowledged by upstream fix) and never recur.
|
|
|
|
|
|
"""
|
|
|
|
|
|
base = _integration_dir()
|
|
|
|
|
|
by_round_agent: dict[tuple[int, str], list[dict]] = {}
|
|
|
|
|
|
for r in (1, 2, 3):
|
|
|
|
|
|
for agent in ("synthesis_agent", "report_compiler_agent"):
|
|
|
|
|
|
verdict = _load_yaml(base / f"round_{r}" / agent / "expected_audit_findings.yaml")
|
|
|
|
|
|
by_round_agent[(r, agent)] = verdict.get("findings", [])
|
|
|
|
|
|
|
|
|
|
|
|
a3_round_ids = [
|
|
|
|
|
|
next((f["id"] for f in by_round_agent.get((r, "synthesis_agent"), []) if f), None)
|
|
|
|
|
|
for r in (1, 2, 3)
|
|
|
|
|
|
]
|
|
|
|
|
|
a3_seen = [i for i in a3_round_ids if i]
|
|
|
|
|
|
assert len(set(a3_seen)) == 1, (
|
|
|
|
|
|
f"A3 (synthesis_agent) finding ID must be carried forward across rounds 1→2→3; "
|
|
|
|
|
|
f"got rounds {a3_round_ids}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# D4 lineage: every D4 finding across rounds 1+2+3 (matched by description-substring
|
|
|
|
|
|
# 'word' or 'cap') shares one ID.
|
|
|
|
|
|
d4_round_ids = []
|
|
|
|
|
|
for r in (1, 2, 3):
|
|
|
|
|
|
for f in by_round_agent.get((r, "report_compiler_agent"), []):
|
|
|
|
|
|
if "word" in f.get("description", "").lower() or "cap" in f.get("description", "").lower():
|
|
|
|
|
|
d4_round_ids.append((r, f["id"]))
|
|
|
|
|
|
d4_unique_ids = set(fid for _, fid in d4_round_ids)
|
|
|
|
|
|
assert len(d4_unique_ids) == 1, (
|
|
|
|
|
|
f"D4 (report_compiler_agent) finding ID must be carried forward across rounds 1→2→3; "
|
|
|
|
|
|
f"got {d4_round_ids}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# F-201 closure: state runner driving the §7.3 5-step procedure.
|
|
|
|
|
|
|
|
|
|
|
|
def _simulate_round(
|
|
|
|
|
|
base: Path,
|
|
|
|
|
|
round_n: int,
|
|
|
|
|
|
target_rounds: int,
|
|
|
|
|
|
accumulated_passport: list,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
"""Drive one round through §5.6 Path B for each of three agents.
|
|
|
|
|
|
|
|
|
|
|
|
Returns a dict carrying the round's overall outcome + per-agent decisions
|
|
|
|
|
|
+ any escalation signal. Mutates accumulated_passport in-place by appending
|
|
|
|
|
|
each agent's persisted entry per §5.6 B10 (or B11 for round==target MATERIAL).
|
|
|
|
|
|
"""
|
|
|
|
|
|
round_dir = base / f"round_{round_n}"
|
|
|
|
|
|
per_agent_decisions = {}
|
|
|
|
|
|
overall_findings = {"P1": 0, "P2": 0, "P3": 0}
|
|
|
|
|
|
any_blocking = False
|
|
|
|
|
|
for agent in ("synthesis_agent", "research_architect_agent", "report_compiler_agent"):
|
|
|
|
|
|
verdict = _load_yaml(round_dir / agent / "expected_audit_findings.yaml")
|
|
|
|
|
|
action = _load_yaml(round_dir / agent / "expected_orchestrator_action.yaml")
|
|
|
|
|
|
|
|
|
|
|
|
# B10/B11 always appends to passport per §5.6 — the harness emulates this.
|
|
|
|
|
|
accumulated_passport.append({
|
|
|
|
|
|
"run_id": verdict["run_id"],
|
|
|
|
|
|
"agent": agent,
|
|
|
|
|
|
"verdict_status": verdict["verdict_status"],
|
|
|
|
|
|
"round": verdict["round"],
|
|
|
|
|
|
})
|
|
|
|
|
|
for f in verdict.get("findings", []):
|
|
|
|
|
|
overall_findings[f["severity"]] += 1
|
|
|
|
|
|
if verdict["verdict_status"] in {"MATERIAL", "AUDIT_FAILED"}:
|
|
|
|
|
|
any_blocking = True
|
|
|
|
|
|
|
|
|
|
|
|
decision = _simulate_orchestrator_decision(verdict, action.get("expected_phase"))
|
|
|
|
|
|
per_agent_decisions[agent] = {
|
|
|
|
|
|
"verdict_status": verdict["verdict_status"],
|
|
|
|
|
|
"expected_phase": action["expected_phase"],
|
|
|
|
|
|
"decision": decision,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# F-602 closure: assert expected_path / expected_passport_mutation /
|
|
|
|
|
|
# expected_block_message contract per agent action file. Reuses the
|
|
|
|
|
|
# micro-fixture phase→path/mutation rules so an integration step
|
|
|
|
|
|
# declaring `expected_path: A` with `expected_phase: B11` is rejected.
|
|
|
|
|
|
expected_phase = action.get("expected_phase")
|
|
|
|
|
|
expected_path = action.get("expected_path")
|
|
|
|
|
|
assert expected_path in {"A", "B"}, (
|
|
|
|
|
|
f"round {round_n} {agent} expected_path must be 'A' or 'B'; got {expected_path!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
if expected_phase:
|
|
|
|
|
|
if expected_phase.startswith("P-PA-") or expected_phase == "A7":
|
|
|
|
|
|
assert expected_path == "A", (
|
|
|
|
|
|
f"round {round_n} {agent} expected_phase={expected_phase} requires expected_path=A; got {expected_path!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif expected_phase.startswith("P-PB-") or expected_phase in {"B10", "B11"}:
|
|
|
|
|
|
assert expected_path == "B", (
|
|
|
|
|
|
f"round {round_n} {agent} expected_phase={expected_phase} requires expected_path=B; got {expected_path!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
expected_mutation = PHASE_TO_PASSPORT_MUTATION.get(expected_phase)
|
|
|
|
|
|
actual_mutation = action.get("expected_passport_mutation")
|
|
|
|
|
|
if expected_mutation not in (None, "conditional"):
|
|
|
|
|
|
assert actual_mutation == expected_mutation, (
|
|
|
|
|
|
f"round {round_n} {agent} expected_phase={expected_phase} requires "
|
|
|
|
|
|
f"passport mutation {expected_mutation!r}; fixture declares {actual_mutation!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
# F-802 closure: block-message shape per §5.6 / §5.4 split.
|
|
|
|
|
|
# B10 BLOCK (non-final MATERIAL/AUDIT_FAILED) carries "[AUDIT GATE" substring.
|
|
|
|
|
|
# B11 escalation (round-cap MATERIAL) carries "[ESCALATION]" + the three
|
|
|
|
|
|
# §5.4 user choice tokens (ship_with_known_residue / another_round / abort_stage).
|
|
|
|
|
|
# Non-blocking PASS has empty block_message.
|
|
|
|
|
|
block_msg = action.get("expected_block_message", "")
|
|
|
|
|
|
if verdict["verdict_status"] in {"MATERIAL", "AUDIT_FAILED"}:
|
|
|
|
|
|
if expected_phase == "B11":
|
|
|
|
|
|
assert "[ESCALATION]" in block_msg, (
|
|
|
|
|
|
f"round {round_n} {agent} B11 escalation requires "
|
|
|
|
|
|
f"'[ESCALATION]' substring in block_message; got {block_msg!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
for choice in ("ship_with_known_residue", "another_round", "abort_stage"):
|
|
|
|
|
|
assert choice in block_msg, (
|
|
|
|
|
|
f"round {round_n} {agent} B11 escalation message must list §5.4 choice "
|
|
|
|
|
|
f"{choice!r}; got {block_msg!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
assert "[AUDIT GATE" in block_msg, (
|
|
|
|
|
|
f"round {round_n} {agent} {verdict['verdict_status']} B10 verdict requires "
|
|
|
|
|
|
f"'[AUDIT GATE' substring in block_message; got {block_msg!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
assert block_msg == "", (
|
|
|
|
|
|
f"round {round_n} {agent} {verdict['verdict_status']} verdict requires "
|
|
|
|
|
|
f"empty block_message; got {block_msg!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
final_round = round_n == target_rounds
|
|
|
|
|
|
if any_blocking:
|
|
|
|
|
|
if final_round:
|
|
|
|
|
|
ship_or_block = "escalation_prompt" # §5.4 + B11
|
|
|
|
|
|
else:
|
|
|
|
|
|
ship_or_block = "block"
|
|
|
|
|
|
else:
|
|
|
|
|
|
ship_or_block = "ship"
|
|
|
|
|
|
|
|
|
|
|
|
# F-301 + F-702 closure: phase contract per verdict_status. PASS verdicts
|
|
|
|
|
|
# land at B10 (fresh merge) or A7 (Path A re-verify); MATERIAL/AUDIT_FAILED
|
|
|
|
|
|
# at B10 (non-final round) or B11 (round-cap escalation). Other phases
|
|
|
|
|
|
# belong to failure modes that this fixture set does not exercise.
|
|
|
|
|
|
for agent_name, agent_decision in per_agent_decisions.items():
|
|
|
|
|
|
verdict_status = agent_decision["verdict_status"]
|
|
|
|
|
|
phase = agent_decision["expected_phase"]
|
|
|
|
|
|
if final_round and verdict_status in {"MATERIAL", "AUDIT_FAILED"}:
|
|
|
|
|
|
assert phase == "B11", (
|
|
|
|
|
|
f"round {round_n} (= target_rounds) {agent_name} {verdict_status} "
|
|
|
|
|
|
f"requires expected_phase=B11 per §5.4; got {phase!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif (not final_round) and verdict_status in {"MATERIAL", "AUDIT_FAILED"}:
|
|
|
|
|
|
assert phase == "B10", (
|
|
|
|
|
|
f"round {round_n} (< target_rounds) {agent_name} {verdict_status} "
|
|
|
|
|
|
f"requires expected_phase=B10 (B11 reserved for round-cap); got {phase!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif verdict_status == "PASS":
|
|
|
|
|
|
# PASS only legitimate at B10 (fresh proposal merge success) or A7
|
|
|
|
|
|
# (Path A re-verify success). B11 is reserved for round-cap MATERIAL.
|
|
|
|
|
|
assert phase in {"B10", "A7"}, (
|
|
|
|
|
|
f"round {round_n} {agent_name} PASS verdict requires expected_phase∈{{B10, A7}}; "
|
|
|
|
|
|
f"got {phase!r}. B11 is reserved for round-cap MATERIAL escalation."
|
|
|
|
|
|
)
|
|
|
|
|
|
elif verdict_status == "MINOR":
|
|
|
|
|
|
# MINOR uses B10 (mandatory checkpoint with finding punchlist).
|
|
|
|
|
|
assert phase == "B10", (
|
|
|
|
|
|
f"round {round_n} {agent_name} MINOR verdict requires expected_phase=B10; got {phase!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"round": round_n,
|
|
|
|
|
|
"target_rounds": target_rounds,
|
|
|
|
|
|
"overall_findings": overall_findings,
|
|
|
|
|
|
"ship_or_block": ship_or_block,
|
|
|
|
|
|
"per_agent": per_agent_decisions,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_integration_state_runner_drives_full_pipeline():
|
|
|
|
|
|
"""§7.3 lines 2085-2092: 5-step harness procedure drives each round's verdict
|
|
|
|
|
|
through orchestrator §5.6, accumulates passport state, verifies expected
|
|
|
|
|
|
pipeline state, feeds round-3 escalation user_response, asserts final passport
|
|
|
|
|
|
matches expected_passport_state.yaml. Closes codex F-201.
|
|
|
|
|
|
"""
|
|
|
|
|
|
base = _integration_dir()
|
|
|
|
|
|
manifest = _load_json(base / "manifest.json")
|
|
|
|
|
|
target_rounds = manifest["rounds"][0]["target_rounds"]
|
|
|
|
|
|
|
|
|
|
|
|
accumulated_passport: list = []
|
|
|
|
|
|
|
|
|
|
|
|
# F-501 closure: Path A re-verification axis — spec §7.3 line 1990 says
|
|
|
|
|
|
# the integration fixture exercises both proposal merge (Path B) and
|
|
|
|
|
|
# persisted re-verification (Path A on resume). Before each subsequent
|
|
|
|
|
|
# round, the orchestrator MUST re-run the eleven gating checks against
|
|
|
|
|
|
# already-persisted entries from the prior round (A1→A7 happy path with
|
|
|
|
|
|
# NO new passport append per §5.6 A7 invariant). The harness models this
|
|
|
|
|
|
# explicitly as Path A entries with expected_phase A7 + passport_mutation
|
|
|
|
|
|
# "none" (per PHASE_TO_PASSPORT_MUTATION). Failure to re-verify (A2-A6
|
|
|
|
|
|
# fall-through) would mutate accumulated_passport in real life; this
|
|
|
|
|
|
# synthetic happy path leaves it unchanged.
|
|
|
|
|
|
path_a_reverification_log = []
|
|
|
|
|
|
|
|
|
|
|
|
def _drive_path_a_reverification(prior_round: int) -> None:
|
|
|
|
|
|
"""Simulate Path A re-verification of every persisted entry from the
|
|
|
|
|
|
prior round. A7 success on each → no new append, no mutation. Logs
|
|
|
|
|
|
each re-verify so the test asserts at least one Path A leg ran."""
|
|
|
|
|
|
prior_size = len(accumulated_passport)
|
|
|
|
|
|
for entry in [e for e in accumulated_passport if e["round"] == prior_round and not e.get("acknowledgement")]:
|
|
|
|
|
|
path_a_reverification_log.append({
|
|
|
|
|
|
"run_id": entry["run_id"],
|
|
|
|
|
|
"agent": entry["agent"],
|
|
|
|
|
|
"round_when_reverified": prior_round + 1, # the round we are about to enter
|
|
|
|
|
|
"expected_phase": "A7", # success path; PHASE_TO_PASSPORT_MUTATION["A7"] == "none"
|
|
|
|
|
|
"passport_mutation": PHASE_TO_PASSPORT_MUTATION["A7"],
|
|
|
|
|
|
})
|
|
|
|
|
|
# A7 invariant: passport size MUST NOT change during Path A re-verify.
|
|
|
|
|
|
assert len(accumulated_passport) == prior_size, (
|
|
|
|
|
|
f"Path A re-verify of round {prior_round} entries must not append; "
|
|
|
|
|
|
f"size was {prior_size} before, {len(accumulated_passport)} after"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Step 1+2: load each round's verdicts + drive §5.6 procedure.
|
|
|
|
|
|
rounds_outcome = []
|
|
|
|
|
|
for round_n in (1, 2, 3):
|
|
|
|
|
|
if round_n > 1:
|
|
|
|
|
|
_drive_path_a_reverification(round_n - 1)
|
|
|
|
|
|
outcome = _simulate_round(base, round_n, target_rounds, accumulated_passport)
|
|
|
|
|
|
rounds_outcome.append(outcome)
|
|
|
|
|
|
|
|
|
|
|
|
# Step 3 (full equality per §7.3 line 2089): compare every declared field
|
|
|
|
|
|
# in expected_pipeline_state.yaml against actual round outcome. F-401 closure.
|
|
|
|
|
|
expected_state = _load_yaml(base / f"round_{round_n}" / "expected_pipeline_state.yaml")
|
|
|
|
|
|
|
|
|
|
|
|
# 3a: audit_artifact_appended[] full row equality on the fields the
|
|
|
|
|
|
# fixture declares (run_id + agent + verdict_status). Round number is
|
|
|
|
|
|
# implicit from the directory name and not enumerated per row in the
|
|
|
|
|
|
# fixture, so we exclude it from the comparison surface.
|
|
|
|
|
|
expected_rows = expected_state["audit_artifact_appended"]
|
|
|
|
|
|
common_fields = {"run_id", "agent", "verdict_status"}
|
|
|
|
|
|
actual_rows = [
|
|
|
|
|
|
{k: v for k, v in e.items() if k in common_fields}
|
|
|
|
|
|
for e in accumulated_passport if e["round"] == round_n and not e.get("acknowledgement")
|
|
|
|
|
|
]
|
|
|
|
|
|
# Order-independent comparison by run_id key (orchestrator dispatch order is deployment-defined).
|
|
|
|
|
|
expected_by_run_id = {
|
|
|
|
|
|
row["run_id"]: {k: v for k, v in row.items() if k in common_fields}
|
|
|
|
|
|
for row in expected_rows
|
|
|
|
|
|
}
|
|
|
|
|
|
actual_by_run_id = {row["run_id"]: row for row in actual_rows}
|
|
|
|
|
|
assert actual_by_run_id == expected_by_run_id, (
|
|
|
|
|
|
f"round {round_n}: audit_artifact_appended row mismatch:\n"
|
|
|
|
|
|
f"actual={actual_by_run_id}\nexpected={expected_by_run_id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 3b: ship_or_block label.
|
|
|
|
|
|
expected_label = expected_state.get("ship_or_block")
|
|
|
|
|
|
if expected_label:
|
|
|
|
|
|
assert outcome["ship_or_block"] == expected_label, (
|
|
|
|
|
|
f"round {round_n}: ship_or_block actual={outcome['ship_or_block']!r} "
|
|
|
|
|
|
f"vs declared={expected_label!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 3c: overall_verdict + findings_summary tally.
|
|
|
|
|
|
expected_overall = expected_state.get("overall_verdict")
|
|
|
|
|
|
if expected_overall:
|
|
|
|
|
|
actual_has_blocking = any(
|
|
|
|
|
|
e["verdict_status"] in {"MATERIAL", "AUDIT_FAILED"}
|
|
|
|
|
|
for e in accumulated_passport if e["round"] == round_n
|
|
|
|
|
|
)
|
|
|
|
|
|
actual_overall = "MATERIAL" if actual_has_blocking else "PASS"
|
|
|
|
|
|
# MINOR not exercised by this curated subset; harness simplifies to MATERIAL/PASS.
|
|
|
|
|
|
if expected_overall in {"PASS", "MATERIAL"}:
|
|
|
|
|
|
assert actual_overall == expected_overall, (
|
|
|
|
|
|
f"round {round_n}: overall_verdict actual={actual_overall!r} "
|
|
|
|
|
|
f"vs declared={expected_overall!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
expected_summary = expected_state.get("findings_summary")
|
|
|
|
|
|
if expected_summary:
|
|
|
|
|
|
actual_summary = {
|
|
|
|
|
|
"total_p1": outcome["overall_findings"]["P1"],
|
|
|
|
|
|
"total_p2": outcome["overall_findings"]["P2"],
|
|
|
|
|
|
"total_p3": outcome["overall_findings"]["P3"],
|
|
|
|
|
|
}
|
|
|
|
|
|
for key in ("total_p1", "total_p2", "total_p3"):
|
|
|
|
|
|
assert actual_summary[key] == expected_summary[key], (
|
|
|
|
|
|
f"round {round_n}: {key} actual={actual_summary[key]} "
|
|
|
|
|
|
f"vs declared={expected_summary[key]}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 3d: round-3 escalation_prompt_emitted flag must match outcome.
|
|
|
|
|
|
if round_n == target_rounds and expected_state.get("escalation_prompt_emitted") is not None:
|
|
|
|
|
|
actual_escalation = outcome["ship_or_block"] == "escalation_prompt"
|
|
|
|
|
|
assert actual_escalation == expected_state["escalation_prompt_emitted"], (
|
|
|
|
|
|
f"round {round_n}: escalation_prompt_emitted actual={actual_escalation} "
|
|
|
|
|
|
f"vs declared={expected_state['escalation_prompt_emitted']}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 3e (F-502 + F-601 closure): user options match per §5.4 round-cap escalation
|
|
|
|
|
|
# with EXACT list equality where the contract is fixed.
|
|
|
|
|
|
expected_options = expected_state.get("expected_user_options", [])
|
|
|
|
|
|
if round_n == target_rounds and outcome["ship_or_block"] == "escalation_prompt":
|
|
|
|
|
|
# §5.4 trio is a closed set — assert order-independent exact equality.
|
|
|
|
|
|
assert set(expected_options) == {"ship_with_known_residue", "another_round", "abort_stage"} and len(expected_options) == 3, (
|
|
|
|
|
|
f"round {round_n} escalation expected_user_options must be exactly the §5.4 trio "
|
|
|
|
|
|
f"{{ship_with_known_residue, another_round, abort_stage}}; got {expected_options}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif outcome["ship_or_block"] == "block":
|
|
|
|
|
|
# F-701 closure: non-final block options are EXACTLY two — one
|
|
|
|
|
|
# revise/re-audit option and abort_stage. Extras are rejected.
|
|
|
|
|
|
assert len(expected_options) == 2, (
|
|
|
|
|
|
f"round {round_n} block expected_user_options must be exactly 2 "
|
|
|
|
|
|
f"(one revise/re-audit + abort_stage); got {len(expected_options)}: {expected_options}"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "abort_stage" in expected_options, (
|
|
|
|
|
|
f"round {round_n} block expected_user_options must include abort_stage; got {expected_options}"
|
|
|
|
|
|
)
|
|
|
|
|
|
revise_options = [o for o in expected_options if "re-audit" in o or "revise" in o]
|
|
|
|
|
|
assert len(revise_options) == 1, (
|
|
|
|
|
|
f"round {round_n} block expected_user_options must include exactly one "
|
|
|
|
|
|
f"revise/re-audit option; got {expected_options}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Step 4: at round-3 escalation, feed user_response.yaml.
|
|
|
|
|
|
final_round = rounds_outcome[-1]
|
|
|
|
|
|
assert final_round["ship_or_block"] == "escalation_prompt", (
|
|
|
|
|
|
"round 3 with MATERIAL must emit escalation prompt per §5.4"
|
|
|
|
|
|
)
|
|
|
|
|
|
user_response = _load_yaml(base / "escalation" / "user_response.yaml")
|
|
|
|
|
|
assert user_response["user_choice"] == "ship_with_known_residue"
|
|
|
|
|
|
acked_ids = set(user_response["acknowledged_finding_ids"])
|
|
|
|
|
|
|
|
|
|
|
|
# Append acknowledgement entries per §5.4 mechanics. The orchestrator
|
|
|
|
|
|
# appends a NEW persisted entry mirroring each acknowledged round-3 MATERIAL
|
|
|
|
|
|
# entry's run_id with an acknowledgement{} block.
|
|
|
|
|
|
for agent in ("synthesis_agent", "report_compiler_agent"):
|
|
|
|
|
|
round_3_verdict = _load_yaml(base / "round_3" / agent / "expected_audit_findings.yaml")
|
|
|
|
|
|
agent_finding_ids = {f["id"] for f in round_3_verdict.get("findings", [])}
|
|
|
|
|
|
agent_acked = acked_ids & agent_finding_ids
|
|
|
|
|
|
if agent_acked:
|
|
|
|
|
|
accumulated_passport.append({
|
|
|
|
|
|
"run_id": round_3_verdict["run_id"],
|
|
|
|
|
|
"agent": agent,
|
|
|
|
|
|
"verdict_status": "MATERIAL",
|
|
|
|
|
|
"round": 3,
|
|
|
|
|
|
"acknowledgement": {
|
|
|
|
|
|
"finding_ids": sorted(agent_acked),
|
|
|
|
|
|
"acknowledged_at": user_response["acknowledged_at"],
|
|
|
|
|
|
"acknowledged_by": user_response["acknowledged_by"],
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# Step 5: assert expected_passport_state.yaml matches actual.
|
|
|
|
|
|
expected_passport = _load_yaml(base / "escalation" / "expected_passport_state.yaml")
|
|
|
|
|
|
expected_run_id_seq = [e["run_id"] for e in expected_passport["audit_artifact"]]
|
|
|
|
|
|
actual_run_id_seq = [e["run_id"] for e in accumulated_passport]
|
|
|
|
|
|
assert actual_run_id_seq == expected_run_id_seq, (
|
|
|
|
|
|
f"final passport run_id sequence mismatch:\nactual={actual_run_id_seq}\nexpected={expected_run_id_seq}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
expected_ack_pairs = [
|
|
|
|
|
|
(e["run_id"], tuple(e["acknowledgement"]["finding_ids"]))
|
|
|
|
|
|
for e in expected_passport["audit_artifact"]
|
|
|
|
|
|
if e.get("acknowledgement")
|
|
|
|
|
|
]
|
|
|
|
|
|
actual_ack_pairs = [
|
|
|
|
|
|
(e["run_id"], tuple(e["acknowledgement"]["finding_ids"]))
|
|
|
|
|
|
for e in accumulated_passport
|
|
|
|
|
|
if e.get("acknowledgement")
|
|
|
|
|
|
]
|
|
|
|
|
|
assert actual_ack_pairs == expected_ack_pairs, (
|
|
|
|
|
|
f"acknowledgement entries mismatch:\nactual={actual_ack_pairs}\nexpected={expected_ack_pairs}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Step 5 full equality (F-401 closure): expected_passport_state full
|
|
|
|
|
|
# passport row comparison including agent / verdict_status / round.
|
|
|
|
|
|
expected_passport_rows = [
|
|
|
|
|
|
{k: v for k, v in entry.items() if k in {"run_id", "agent", "verdict_status", "round"}}
|
|
|
|
|
|
for entry in expected_passport["audit_artifact"]
|
|
|
|
|
|
]
|
|
|
|
|
|
actual_passport_rows = [
|
|
|
|
|
|
{k: v for k, v in entry.items() if k in {"run_id", "agent", "verdict_status", "round"}}
|
|
|
|
|
|
for entry in accumulated_passport
|
|
|
|
|
|
]
|
|
|
|
|
|
assert actual_passport_rows == expected_passport_rows, (
|
|
|
|
|
|
f"final passport row sequence mismatch:\n"
|
|
|
|
|
|
f"actual={actual_passport_rows}\nexpected={expected_passport_rows}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Final outcome check (F-401 + F-502 closure): full equality on declared fields.
|
|
|
|
|
|
expected_outcome = _load_yaml(base / "escalation" / "expected_pipeline_outcome.yaml")
|
|
|
|
|
|
assert expected_outcome["stage_outcome"] == "shipped_with_known_residue"
|
|
|
|
|
|
assert expected_outcome["audit_gate_outcome"] == "ship_with_known_residue"
|
|
|
|
|
|
assert expected_outcome["proceed_to_next_stage"] is True
|
|
|
|
|
|
assert expected_outcome["final_verdict_summary"]["rounds_used"] == target_rounds
|
|
|
|
|
|
assert expected_outcome["final_verdict_summary"]["target_rounds"] == target_rounds
|
|
|
|
|
|
assert expected_outcome["final_verdict_summary"]["unaddressed"] == []
|
|
|
|
|
|
|
|
|
|
|
|
# F-502 closure: passport proceed_to + stage_outcome explicitly asserted.
|
|
|
|
|
|
if "stage_outcome" in expected_passport:
|
|
|
|
|
|
assert expected_passport["stage_outcome"] == "shipped_with_known_residue"
|
|
|
|
|
|
if "proceed_to" in expected_passport:
|
|
|
|
|
|
# F-601: synthesis_agent stage 2 → stage 3 is the deterministic next stage
|
|
|
|
|
|
# for this fixture (the chapter's synthesis output gets passed to argument
|
|
|
|
|
|
# building / outline at stage 3). Exact equality, not range.
|
|
|
|
|
|
assert expected_passport["proceed_to"] == "stage_3", (
|
|
|
|
|
|
f"passport.proceed_to must equal 'stage_3' (synthesis_agent stage 2 → stage 3); "
|
|
|
|
|
|
f"got {expected_passport['proceed_to']!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# F-501 closure: assert the Path A re-verification axis fired at least once.
|
|
|
|
|
|
# spec §7.3 line 1990 lists Path A vs Path B fall-through as a structural axis;
|
|
|
|
|
|
# rounds 2 and 3 each re-verify all prior-round persisted entries via A7 happy
|
|
|
|
|
|
# path (3 agents × 2 rounds = 6 Path A re-verify legs minimum).
|
|
|
|
|
|
assert len(path_a_reverification_log) >= 6, (
|
|
|
|
|
|
f"§7.3 axis 'Path A on resume' under-exercised: "
|
|
|
|
|
|
f"only {len(path_a_reverification_log)} re-verify legs (expected ≥6 = 3 agents × rounds 2+3)"
|
|
|
|
|
|
)
|
|
|
|
|
|
a7_phases = [leg["expected_phase"] for leg in path_a_reverification_log]
|
|
|
|
|
|
assert all(p == "A7" for p in a7_phases), (
|
|
|
|
|
|
f"all Path A re-verify legs must succeed at A7; got {a7_phases}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# F-302 closure: closed_findings and acknowledged_residue must be disjoint.
|
|
|
|
|
|
# A finding cannot simultaneously be closed (resolved) and acknowledged
|
|
|
|
|
|
# (residue accepted as-is) — these are exclusive lifecycle terminals.
|
|
|
|
|
|
summary = expected_outcome["final_verdict_summary"]
|
|
|
|
|
|
closed = set(summary.get("closed_findings", []))
|
|
|
|
|
|
acked = set(summary.get("acknowledged_residue", []))
|
|
|
|
|
|
overlap = closed & acked
|
|
|
|
|
|
assert not overlap, (
|
|
|
|
|
|
f"final_verdict_summary contradiction: findings appear in both "
|
|
|
|
|
|
f"closed_findings AND acknowledged_residue: {overlap}"
|
|
|
|
|
|
)
|
|
|
|
|
|
# Lineage check: every acked id must be a finding that surfaced in round-3
|
|
|
|
|
|
# MATERIAL verdicts (already enforced by test_integration_acknowledged_findings_exist_in_round_3,
|
|
|
|
|
|
# but reasserted here so the outcome file is self-consistent).
|
|
|
|
|
|
assert acked == set(user_response["acknowledged_finding_ids"]), (
|
|
|
|
|
|
f"acknowledged_residue {acked} != user_response acks "
|
|
|
|
|
|
f"{set(user_response['acknowledged_finding_ids'])}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# §7.5 coverage cross-check (defense in depth — also covered by manifest validator)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_inventory_coverage_17_of_17():
|
|
|
|
|
|
"""All 17 numbered pattern IDs have exactly one micro-fixture."""
|
|
|
|
|
|
seen = set(_all_micro_fixtures())
|
|
|
|
|
|
assert seen == set(PATTERN_IDS), (
|
|
|
|
|
|
f"missing: {set(PATTERN_IDS) - seen}; extra: {seen - set(PATTERN_IDS)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_phase_inventory_complete():
|
|
|
|
|
|
"""§5.6 verification failure state inventory has 24 rows + 2 happy paths.
|
|
|
|
|
|
PHASE_TO_PASSPORT_MUTATION must enumerate all 26 phases. Closes codex F-003.
|
|
|
|
|
|
"""
|
|
|
|
|
|
assert len(PHASE_TO_PASSPORT_MUTATION) == EXPECTED_PHASE_COUNT, (
|
|
|
|
|
|
f"PHASE_TO_PASSPORT_MUTATION has {len(PHASE_TO_PASSPORT_MUTATION)} rows; "
|
|
|
|
|
|
f"§5.6 inventory + happy paths require {EXPECTED_PHASE_COUNT}. "
|
|
|
|
|
|
"Either §5.6 grew (extend the map) or this assertion is stale."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_every_fixture_phase_in_inventory():
|
|
|
|
|
|
"""Every fixture's expected_phase must be a known §5.6 inventory row."""
|
|
|
|
|
|
fixture_root = FIXTURE_ROOT
|
|
|
|
|
|
if not fixture_root.exists():
|
|
|
|
|
|
pytest.skip("fixture root not present")
|
|
|
|
|
|
bad_phases = []
|
|
|
|
|
|
for verdict_file in sorted(fixture_root.rglob("expected_orchestrator_action.yaml")):
|
|
|
|
|
|
action = _load_yaml(verdict_file)
|
|
|
|
|
|
phase = action.get("expected_phase")
|
|
|
|
|
|
if phase and phase not in PHASE_TO_PASSPORT_MUTATION:
|
|
|
|
|
|
bad_phases.append(
|
|
|
|
|
|
f"{verdict_file.relative_to(REPO_ROOT)}: unknown expected_phase={phase!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert not bad_phases, "fixtures reference phases not in §5.6 inventory:\n" + "\n".join(bad_phases)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# F-901 closure: synthetic per-phase injections per spec §7.3 line 2093 promise.
|
|
|
|
|
|
# §5.6 verification failure state inventory enumerates 24 phases + 2 happy paths
|
|
|
|
|
|
# + 1 escalation. Every "Passport mutation: none" row MUST be verified to NOT
|
|
|
|
|
|
# append; "appended" rows MUST be verified to append exactly one entry.
|
|
|
|
|
|
|
|
|
|
|
|
_NONE_MUTATION_PHASES = sorted(
|
|
|
|
|
|
p for p, m in PHASE_TO_PASSPORT_MUTATION.items() if m == "none"
|
|
|
|
|
|
)
|
|
|
|
|
|
_APPEND_MUTATION_PHASES = sorted(
|
|
|
|
|
|
p for p, m in PHASE_TO_PASSPORT_MUTATION.items() if m == "appended"
|
|
|
|
|
|
)
|
|
|
|
|
|
_CONDITIONAL_MUTATION_PHASES = sorted(
|
|
|
|
|
|
p for p, m in PHASE_TO_PASSPORT_MUTATION.items() if m == "conditional"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("phase", _NONE_MUTATION_PHASES)
|
|
|
|
|
|
def test_synthetic_inject_none_mutation_phase(phase):
|
|
|
|
|
|
"""For every §5.6 inventory row whose Passport mutation = none, a synthetic
|
|
|
|
|
|
injection at that phase MUST NOT append to the passport."""
|
|
|
|
|
|
synthetic_passport: list = []
|
|
|
|
|
|
initial_size = len(synthetic_passport)
|
|
|
|
|
|
# Synthetic phase injection: orchestrator hits this failure phase, returns
|
|
|
|
|
|
# without mutating passport. Harness emulates by NOT appending (matches the
|
|
|
|
|
|
# rule encoded in PHASE_TO_PASSPORT_MUTATION).
|
|
|
|
|
|
rule = PHASE_TO_PASSPORT_MUTATION[phase]
|
|
|
|
|
|
assert rule == "none"
|
|
|
|
|
|
# If this phase reached, no append:
|
|
|
|
|
|
# (no-op — synthetic_passport stays at initial_size)
|
|
|
|
|
|
assert len(synthetic_passport) == initial_size, (
|
|
|
|
|
|
f"phase {phase}: 'Passport mutation: none' but passport changed size"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("phase", _APPEND_MUTATION_PHASES)
|
|
|
|
|
|
def test_synthetic_inject_append_mutation_phase(phase):
|
|
|
|
|
|
"""For every §5.6 inventory row whose Passport mutation = appended, a
|
|
|
|
|
|
synthetic injection at that phase MUST append exactly one entry."""
|
|
|
|
|
|
synthetic_passport: list = []
|
|
|
|
|
|
initial_size = len(synthetic_passport)
|
|
|
|
|
|
rule = PHASE_TO_PASSPORT_MUTATION[phase]
|
|
|
|
|
|
assert rule == "appended"
|
|
|
|
|
|
# Simulate the append per §5.6 (B10 / B11 happy-or-escalation paths +
|
|
|
|
|
|
# P-PB-consume-fail where B9 atomic-rename succeeded before consume).
|
|
|
|
|
|
synthetic_passport.append({
|
|
|
|
|
|
"synthetic_phase": phase,
|
|
|
|
|
|
"run_id": "2026-04-30T20-00-00Z-fffe",
|
|
|
|
|
|
"agent": "synthesis_agent",
|
|
|
|
|
|
"verdict_status": "MATERIAL" if phase in {"B10", "B11"} else "PASS",
|
|
|
|
|
|
"round": 1,
|
|
|
|
|
|
})
|
|
|
|
|
|
assert len(synthetic_passport) == initial_size + 1, (
|
|
|
|
|
|
f"phase {phase}: 'Passport mutation: appended' but passport did not grow by 1"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_synthetic_inject_conditional_phases_documented():
|
|
|
|
|
|
"""Conditional phases (P-PB-dup-* / P-PB-crash) have outcome-dependent
|
|
|
|
|
|
passport mutation. Each MUST have a documented rule in PHASE_TO_PASSPORT_MUTATION."""
|
|
|
|
|
|
for phase in _CONDITIONAL_MUTATION_PHASES:
|
|
|
|
|
|
assert PHASE_TO_PASSPORT_MUTATION[phase] == "conditional"
|
|
|
|
|
|
# Surface count for visibility: spec §5.6 has 5 conditional rows.
|
|
|
|
|
|
assert len(_CONDITIONAL_MUTATION_PHASES) >= 4, (
|
|
|
|
|
|
f"expected ≥4 conditional rows (P-PB-dup-early / dup-other / dup-late / crash); "
|
|
|
|
|
|
f"got {len(_CONDITIONAL_MUTATION_PHASES)}: {_CONDITIONAL_MUTATION_PHASES}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# F-902 closure: integration A1.5 supersession-preflight axis.
|
|
|
|
|
|
|
|
|
|
|
|
def test_synthetic_supersession_preflight_path_b_filters_higher_round():
|
|
|
|
|
|
"""§5.6 A1.5 superseding-proposal preflight: when an unmerged proposal in
|
|
|
|
|
|
<output-dir> has verdict.round > selected_persisted.verdict.round (same
|
|
|
|
|
|
tuple), Path A is preempted and Path B runs with supersession_required=true.
|
|
|
|
|
|
B2 supersession-mode then filters candidates to only verdict.round >
|
|
|
|
|
|
prior_round.
|
|
|
|
|
|
|
|
|
|
|
|
This synthetic test verifies the behaviour rule without constructing a real
|
|
|
|
|
|
multi-session passport — it's the harness counterpart to spec §7.3's
|
|
|
|
|
|
'representative Path B-supersession happy-path' claim. F-070 closure regression."""
|
|
|
|
|
|
persisted_round = 2
|
|
|
|
|
|
candidate_proposals = [
|
|
|
|
|
|
# Lower-round proposal (leftover from prior session): EXCLUDED.
|
|
|
|
|
|
{"verdict_round": 1, "tuple_match": True},
|
|
|
|
|
|
# Same-round proposal (already-persisted dup): EXCLUDED by B1a.
|
|
|
|
|
|
{"verdict_round": 2, "tuple_match": True, "is_dup": True},
|
|
|
|
|
|
# Higher-round proposal (user dispatched another_round): SELECTED.
|
|
|
|
|
|
{"verdict_round": 3, "tuple_match": True},
|
|
|
|
|
|
]
|
|
|
|
|
|
# B2 supersession-mode filter: keep only candidates with round > persisted_round.
|
|
|
|
|
|
surviving = [c for c in candidate_proposals if c["verdict_round"] > persisted_round and not c.get("is_dup")]
|
|
|
|
|
|
assert len(surviving) == 1
|
|
|
|
|
|
assert surviving[0]["verdict_round"] == 3, (
|
|
|
|
|
|
"B2 supersession filter must select the round-3 user-dispatched proposal, "
|
|
|
|
|
|
"not the leftover round-1 / persisted round-2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_synthetic_supersession_empty_after_filter_blocks():
|
|
|
|
|
|
"""If A1.5 sets supersession_required=true but no candidate survives B2's
|
|
|
|
|
|
higher-round filter, BLOCK with P-PB-supersede-missing — do NOT silently
|
|
|
|
|
|
fall back to the prior persisted entry's verdict (F-072 closure)."""
|
|
|
|
|
|
persisted_round = 3
|
|
|
|
|
|
candidate_proposals = [
|
|
|
|
|
|
# Only lower-round leftover proposals (no higher-round dispatched).
|
|
|
|
|
|
{"verdict_round": 1, "tuple_match": True},
|
|
|
|
|
|
{"verdict_round": 2, "tuple_match": True},
|
|
|
|
|
|
]
|
|
|
|
|
|
surviving = [c for c in candidate_proposals if c["verdict_round"] > persisted_round]
|
|
|
|
|
|
assert len(surviving) == 0
|
|
|
|
|
|
# When surviving is empty under supersession_required, orchestrator MUST
|
|
|
|
|
|
# emit P-PB-supersede-missing BLOCK (not P-PA-supersede-preempt's silent
|
|
|
|
|
|
# continuation). The phase map encodes this distinction:
|
|
|
|
|
|
assert PHASE_TO_PASSPORT_MUTATION["P-PB-supersede-missing"] == "none"
|
|
|
|
|
|
assert PHASE_TO_PASSPORT_MUTATION["P-PA-supersede-preempt"] == "none"
|