mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
fed3eba117
* feat(v3.7.1 Step 2): D2 audit Scope Report block + lint enforcement
Spec: docs/design/2026-04-30-ars-v3.6.8-trust-provenance-and-drift-
transparency-spec.md §3.2 (D2 — Audit scope coverage non-disclosure),
§3.7 (v3.6.7 invariant overlap audit), §4 Step 2.
Pattern D2 closes the "PASSED without disclosing 22/31 verification
split" failure mode observed in the 2026-04-30 HEEACT chapter session.
Codex round 3 reported "ADDRESSED" without surfacing that only 22 of
53 entries had retrieved original sources; the remaining 31 description-
only entries inherited the verified verdict by aggregation.
Changes (additive only — Sections 1-7 of the audit template stay byte-
equivalent in title, ordinal label, and order per Q5 amend):
- shared/templates/codex_audit_multifile_template.md: prepend Section 0
Scope Report block ahead of Section 1, plus Template-structure prose
update from "seven sections" to "eight sections" (Section 0 listed
with v3.7.1 D2 origin tag). Section 0 carries the four required
content fields (Total entries audited / Entries with retrieved
original source / Entries description-only / Audit scope warning)
and the two firm rules (must appear before any pass/fail summary;
combined-aggregate "PASSED" verb forbidden in audit summary —
aggregate verdict MUST be split into verified-against-source /
description-internally-consistent / unaudited-due-to-missing-source).
- scripts/check_v3_6_8_audit_scope_block.py: new lint enforcing five
rules (R1 Section 0 anchor + Scope Report header strictly before
Section 1; R2 four required content fields; R3 three required
aggregate-status splits; R4 forbidden combined-aggregate "PASSED"
verb in summary; R5 Section 1 byte-equivalence sentinel for the
additive-prepend invariant). Issue-#77-aware design: heading-
anchored extraction (not first-match re.search), duplicate H2
marker rejection, fenced-code-block exclusion for top-level
anchor detection.
- scripts/test_check_v3_6_8_audit_scope_block.py: 11 mutation tests
(T1-T8 covering positive baseline + 6 negative mutation classes;
T8 parametrized across the 4 required content fields). Snapshot+
mutate+restore pattern mirrors test_check_v3_6_8_pattern_protection.py.
- .github/workflows/spec-consistency.yml: wire new lint + pytest into
CI (mirroring v3.6.8 SHA gate / frontmatter trust schema pattern).
Three-lint regression gate (spec §3.7 line 367-372) PASSES on the
modified template:
python scripts/check_v3_6_7_pattern_protection.py → 12/12
pytest scripts/test_check_v3_6_7_pattern_protection.py → 58/58
python scripts/check_audit_artifact_consistency.py
--example-validation-harness → PASS
v3.7.1 lint suite full sweep: 6 lints exit 0; 113 pytest PASS
(58 v3.6.7 + 20 v3.6.8 SHA gate + 24 v3.6.8 trust schema + 11 new).
Roadmap: implements Step 2 of v3.7.1 impl PR sequence (PR #76 Step 0+1
shipped 2026-05-08; #77 SHA gate v2 hardening parked).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): render Section 0 in audit_snapshot.render_prompt (codex round-1 P1)
Codex round-1 review of PR #78 surfaced a P1 architectural finding: the
static Section 0 Scope Report block was prepended to the audit template
and lint-enforced, but `scripts/audit_snapshot.py::render_prompt` extracted
only sections [3, 6, 7] from the template. Section 0 was therefore absent
from every wrapper-dispatched audit prompt, defeating the spec §3.2
contract that the Scope Report rides verbatim every round.
Two changes close the gap:
1. render_prompt now extracts Section 0 from the template alongside
Sections 3 / 6 / 7 and emits it ahead of the rebuilt Section 1 Round
metadata block. Section 0 carries `<N_total>`, `<N_with_source>`, etc.
placeholders that codex is expected to fill from bundle inventory at
audit time, matching the spec §3.2 contract.
2. _extract_template_sections is now fence-aware: ## headings inside
fenced code blocks (```/~~~) no longer terminate section extraction.
Without this, the canonical "## Codex Audit Round N — Scope Report"
header inside Section 0's fenced sub-block was treated as the next
H2 boundary and Section 0 was truncated mid-fence.
Adds scripts/test_audit_snapshot_render_section_0.py with 5 render-side
regression tests pinning the contract:
- rendered prompt contains Section 0 H2 anchor
- rendered prompt contains canonical "## Codex Audit Round N — Scope
Report" header literal
- Section 0 appears strictly before Section 1 (spec line 146)
- all four required content fields present (spec lines 136-140)
- all three aggregate-status splits present (spec lines 147-150)
Verification:
- 6/6 lints exit 0 (v3.6.7 + audit-artifact-harness + 4 v3.6.8 lints)
- 802/802 pytest PASS across the full repo (skipping Linux-only e2e)
- scripts/test_audit_snapshot_render_section_0.py 5/5 PASS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): scope R2/R3 to Section 0 + harden --target path (codex round-2 P2)
Codex round-2 review of PR #78 surfaced two P2 correctness issues:
P2-1: R2 (required content fields) and R3 (aggregate-status splits)
checks scanned the entire template via `in text_full`. A required marker
appearing in a later appendix or documentation block could falsely
satisfy the contract while Section 0 itself was incomplete. The audit
prompt sent to codex contains Section 0, not the appendix, so the
runtime contract was unenforced.
Fix: introduce `_section_0_block(text_full, text_no_fences)` helper that
slices from the Section 0 H2 anchor up to (but not including) the next
top-level H2 boundary. Boundary detection runs on the fence-stripped
string so a fenced sub-block inside Section 0 (the canonical Scope
Report header) does not terminate the slice. Both R2 and R3 now check
membership in `section_0_only` instead of `text_full`.
P2-2: `target.relative_to(REPO_ROOT)` raised ValueError when --target
was passed a relative repo path (e.g. `shared/templates/codex_audit_
multifile_template.md`) or an absolute path outside the repo (e.g. a
pytest tmp_path fixture). Crash signature: traceback in stderr instead
of a deterministic exit code.
Fix: introduce `_format_target_for_report(target)` helper that resolves
to absolute first, then relativizes to REPO_ROOT, falling back to the
raw resolved path if not under the repo. The lint produces a clean
report line for any --target shape.
Adds 9 round-2 mutation tests (T9-T12) to `test_check_v3_6_8_audit_
scope_block.py`:
- T9 (4 parametrized): each required field dropped from Section 0 with
an appendix decoy carrying the same marker — lint must FAIL
- T10 (3 parametrized): each aggregate-status split dropped from
Section 0 with appendix decoy — lint must FAIL
- T11: --target with relative repo path must not crash
- T12: --target with absolute path outside repo (pytest tmp_path) must
not crash, lint reports a deterministic exit code
Verification:
- 7/7 lints exit 0 (including new --target relative-path invocation)
- 811/811 pytest PASS across the full repo (skipping Linux-only e2e)
- scripts/test_check_v3_6_8_audit_scope_block.py 20/20 PASS (11 round-1
+ 9 round-2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): close decoy-bypass family in lint anchors (codex round-3 P2)
Codex round-3 review of PR #78 surfaced two P2 findings in the same
decoy-bypass family — the lint accepted heading literals appearing
outside the live top-level template structure as if they were the
canonical anchors:
P2-3: `_find_section_1_position()` ran `text_full.find(SECTION_1_
HEADING_EXACT)`, so the exact Section 1 heading text appearing inside
a fenced code block (e.g. a worked example) was accepted as the anchor.
That would let a contributor rename the real top-level Section 1
heading while a fenced decoy still satisfied R5 byte-equivalence and
the Section 0/Section 1 ordering check.
P2-4: R1 SCOPE_REPORT_HEADER check ran `text_full.find()`, so the
canonical "## Codex Audit Round N — Scope Report" line appearing in
preamble documentation (or any pre-Section-0 reference) was accepted
even when Section 0 itself was missing the header. The lint scoped
the field-presence checks (R2/R3) to Section 0 in round 2 but left
this last whole-file find() unscoped.
Fixes:
1. `_find_section_1_position(text_no_fences)` now takes the fence-
stripped string so a fenced occurrence cannot match. Caller updated
to pass `text_no_fences` instead of `text_full`. Line-start anchoring
preserved.
2. R1 Scope Report header check now looks inside the extracted
`section_0_only` block instead of `text_full`. Mirrors the R2/R3
scoping fix from round 2 — ordering check via section_0 H2 anchor
was already correct; only the canonical header literal check was
still whole-file.
Adds 2 round-3 mutation tests:
- T13: Section 1 byte-equivalence sentinel rejects fenced-block decoy
(real heading title-case mutated, decoy fence carries original
bytes — lint must FAIL)
- T14: R1 header check rejects preamble decoy (canonical header
redacted in Section 0, decoy "Appendix" carries original bytes
in preamble — lint must FAIL)
Verification:
- 6/6 lints exit 0
- 813/813 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 22/22 PASS (11 round-1
+ 9 round-2 + 2 round-3)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): R4 must scan fenced prompt content (codex round-4 P2)
Codex round-4 review of PR #78 surfaced one P2 in the same decoy-bypass
family closed in rounds 2-3:
R4 forbidden combined-aggregate "PASSED" verb scan ran against
text_no_fences. Fenced code blocks were blanked by the fence-mask helper
so a `verdict: PASSED` line injected inside a fenced sub-block (e.g.
the canonical Scope Report fence) was missed. But fenced blocks ARE the
prompt content codex sees verbatim — the canonical "## Codex Audit
Round N — Scope Report" header lives inside a fence. The fence-mask was
correct for boundary detection (so headings inside fences don't
terminate sections) but wrong for content scanning.
Fix: R4 now runs FORBIDDEN_AGGREGATE_PATTERNS against text_full instead
of text_no_fences. The two patterns are both verdict-shaped
(`audit summary ... PASSED` within 80 chars, or
`^... verdict: PASSED`), so the self-explanatory spec-template prose
"The combined-aggregate 'PASSED' verb is forbidden in the audit summary"
still does not match (the 80-char window between "audit summary" and
any subsequent PASSED token does not close).
Adds T15 mutation test:
- Fenced-block decoy carrying `verdict: PASSED` injected after the
canonical Scope Report header — lint must FAIL.
Verification:
- 6/6 lints exit 0
- 814/814 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 23/23 PASS (11 round-1
+ 9 round-2 + 2 round-3 + 1 round-4)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): broaden verdict patterns + wire render-side CI (codex round-5 P2)
Codex round-5 review of PR #78 surfaced three P2 findings spanning two
families:
VERDICT-KEY BREADTH (P2-5, P2-6):
P2-5 — R4 forbidden combined-aggregate "PASSED" verb only matched
`verdict: PASSED` lines. A summary written as `Overall status: PASSED`,
`Result: PASSED`, or `Final: PASSED` slipped past the lint even though
spec line 152 forbids the verb regardless of which key precedes it.
P2-6 — R1 ordering check (Scope Report must precede any pass/fail
summary, spec line 146) only detected `## ... Summary` heading framing
or the literal `verified-against-source: PASS|FAIL` line. A bare
`verdict: PASS` or `result: FAIL` line ahead of Section 0 evaded the
check despite still being a pass/fail summary by the spec definition.
Fixes:
- Introduce shared `_VERDICT_KEY` regex covering verdict/status/result/
final/final-status/overall/overall-status. R4
FORBIDDEN_AGGREGATE_PATTERNS line-anchored verdict pattern now uses
`_VERDICT_KEY` instead of hard-coded `verdict`.
- New BARE_VERDICT_BEFORE_SECTION_0_PATTERNS list driving the R1
ordering check: heading-style, the legacy first-aggregate-split
signal, plus a bare `<key>: PASS|FAIL|PASSED|FAILED` line. The
ordering scan still runs on text_no_fences so a verdict-shaped line
inside fenced documentation does NOT trip the rule.
CI HOOKUP GAP (P2-7):
P2-7 — `scripts/test_audit_snapshot_render_section_0.py` (the round-1
P1 closure: render-side regression that proves the wrapper-dispatched
audit prompt actually contains Section 0) was never invoked by
`.github/workflows/spec-consistency.yml`. Lint covered the static
template; the runtime path that builds the codex prompt was unguarded
by CI. A future render_prompt regression that drops Section 0 again
would pass spec-consistency silently — exactly the failure mode
round-1 P1 was added to prevent.
Fix: add a dedicated `Run v3.7.1 audit_snapshot render-side Section 0
regression tests` workflow step that runs
`pytest scripts/test_audit_snapshot_render_section_0.py -v`.
Adds 7 round-5 mutation tests:
- T16 (3 parametrized): non-`verdict` key + PASSED → FAIL
- T17 (4 parametrized): bare `<key>: PASS|FAIL` line before Section 0 → FAIL
Verification:
- 6/6 lints exit 0
- 821/821 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 30/30 PASS (11 round-1
+ 9 round-2 + 2 round-3 + 1 round-4 + 7 round-5)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): line-anchor header check + multi-word verdict keys (codex round-6 P2)
Codex round-6 review of PR #78 surfaced two more P2 lint-bypass paths:
P2-8 — R1 SCOPE_REPORT_HEADER check used `in section_0_only` substring
match. A prose mention inside Section 0 like
`The required header is "## Codex Audit Round N — Scope Report"` could
satisfy R1 even when the real heading line had been removed or renamed.
The rendered audit prompt needs the actual heading line, not a
documentation reference.
Fix: replace the substring check with a line-anchored regex
(`^##\s+Codex Audit Round N\s+—\s+Scope Report\b`, MULTILINE) so only
a real heading line at start-of-line satisfies R1. Prose mentions are
no longer accepted.
P2-9 — `_VERDICT_KEY` accepted only single-word verdict tokens (with
the explicit `final[\s_-]?status` and `overall(?:[\s_-]?status)?` pair).
Common multi-word keys like `Final verdict: PASSED`, `Audit status:
PASSED`, `Overall verdict: PASSED` slipped past R4 and the bare-verdict
ordering check.
Fix: rewrite `_VERDICT_KEY` to allow 0–2 short alphabetic qualifier
words ahead of a canonical verdict token (verdict / status / result /
final / outcome / overall):
`(?:[A-Za-z]{1,15}\s+){0,2}<token>`. This catches the multi-word
combinations codex enumerated without false-positives on spec self-
explanation prose like `The combined-aggregate "PASSED" verb is
forbidden in the audit summary` (verified by smoke test on the
spec-template prose lines).
Adds 6 round-6 mutation tests:
- T18: prose mention of header literal inside Section 0 (real heading
removed) → FAIL
- T19 (5 parametrized): multi-word verdict keys + PASSED →
FAIL — `Final verdict`, `Audit status`, `Overall verdict`,
`Final status`, `Final Result`
Verification:
- 6/6 lints exit 0
- 827/827 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 36/36 PASS (11 round-1
+ 9 round-2 + 2 round-3 + 1 round-4 + 7 round-5 + 6 round-6)
- Smoke-tested 5 spec self-explanation prose lines: all OK
(no false-positive on `audit summary`, `combined-aggregate`,
`aggregate verdict ... split`, etc.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): catch multi-line audit summaries with PASSED (codex round-7 P2)
Codex round-7 review of PR #78 surfaced one P2 in the multi-line summary
shape:
P2 — R4 patterns were all same-line: `audit summary[^\n]{0,80}` and
`^\s*<key>: PASSED`. A common Markdown shape evades both:
## Audit Summary
This audit PASSED.
The H2 heading and the PASSED token live on different lines, separated
by blank lines. Same gap for `## Audit Summary\n\nPASSED` (bare verdict
token).
Fix: add a third FORBIDDEN_AGGREGATE_PATTERNS entry that matches an
H2 heading whose title carries any verdict-key token, followed within
~400 chars (≈5 lines) by a `\bPASSED\b` token. The 400-char window is
tight enough that an unrelated PASSED in a much later section does not
falsely pair with the heading.
To support this:
- `_VERDICT_TOKEN` extends to include `summary` so heading like
`## Audit Summary` is recognized as a verdict heading.
- New `_SUMMARY_HEADING` regex (built on `_VERDICT_TOKEN`) matches H2
lines whose title carries any verdict-key word in the first 4 tokens.
Adds 4 round-7 mutation tests:
- T20 (4 parametrized): `## Audit Summary\n\nThis audit PASSED.`,
`## Audit Summary\n\nPASSED`, `## Final Verdict\n\nThis run PASSED
comprehensively.`, `## Audit Summary\n\nThe audit PASSED for all
dimensions.` — all FAIL.
Verification:
- 6/6 lints exit 0
- 831/831 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 40/40 PASS (11 round-1
+ 9 round-2 + 2 round-3 + 1 round-4 + 7 round-5 + 6 round-6 + 4 round-7)
- Smoke test on baseline template: zero false-positives across all
three FORBIDDEN_AGGREGATE_PATTERNS entries (including the new
multi-line lookahead) — `## Codex Audit Round N — Scope Report`
inside Section 0 does not match because no PASSED token appears
within 400 chars; spec self-explanation prose like
`combined-aggregate "PASSED" verb is forbidden in the audit summary`
does not match because no preceding H2 verdict heading frames it.
Convergence note: rounds 5-7 progressively broadened R4 from a hard-
coded `verdict:` key to the multi-word/multi-line aggregate-PASSED
detection family. If round 8 returns yet another lexical bypass in the
same family, the next move is an architectural cap rule (deny any
`PASSED` in the post-Section-0 surface, allow only spec self-
explanation contexts) rather than continued pattern enumeration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): Section 1 sentinel must match whole line (codex round-8 P2)
Codex round-8 review of PR #78 surfaced one P2 in the Section 1 byte-
equivalence sentinel:
P2 — `_find_section_1_position()` used `text.find(SECTION_1_HEADING_
EXACT)` plus a line-start guard. That matched the canonical bytes as
a PREFIX of the heading line, so a mutation that appended text — e.g.
`## Section 1 — Round metadata (renamed)`, `## Section 1 — Round
metadata v2`, `## Section 1 — Round metadata: revised` — still anchored
the sentinel even though the actual heading title had changed. The
rendered audit prompt would carry a different Section 1 framing but
R5 would not detect the mutation.
Fix: replace the `find()` + boundary check with a line-anchored regex
that requires the canonical heading bytes to occupy the whole line:
^## Section 1 — Round metadata[ \t]*(?:\n|\Z)
Optional trailing whitespace before the newline is permitted (since
that is invisible in rendered Markdown), but anything else after the
canonical bytes — including a space + parenthesized qualifier — fails.
\Z handles the case where the heading is the very last line of the
file.
Adds 4 round-8 mutation tests:
- T21 (4 parametrized): Section 1 heading with appended text — `(renamed)`,
` v2`, `: revised`, ` — Round Metadata Override` — all FAIL.
Verification:
- 6/6 lints exit 0
- 835/835 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 44/44 PASS (11 round-1
+ 9 round-2 + 2 round-3 + 1 round-4 + 7 round-5 + 6 round-6 + 4 round-7
+ 4 round-8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): Affected-refcodes required + scope ordering to Scope Report content (codex round-9 P2)
Codex round-9 review of PR #78 surfaced two P2 holes in the Scope Report
contract enforcement:
P2a — REQUIRED_FIELDS was missing the spec line 142 entry
`**Affected refcodes (description-only):**`. The lint accepted Scope
Reports that disclosed the count of unverified entries but hid which
refcodes were actually unaudited, defeating the disclosure purpose.
Fix: extend REQUIRED_FIELDS to five entries (lines 136-142). Mirror
test fixture REQUIRED_FIELDS list also extends so T8 parametrization
covers the new field.
P2b — R1 ordering check ("no pass/fail summary ahead of Scope Report")
scoped its scan to text BEFORE the Section 0 H2 anchor only. A verdict
line inserted INSIDE Section 0 — between the H2 and the canonical
fenced `## Codex Audit Round N — Scope Report` header — slipped past:
the H2 alone is not the Scope Report content. Spec line 146 requires
the Scope Report CONTENT (the disclosure block) to precede any
pass/fail summary, not just the heading wrapper.
Fix: re-anchor the ordering boundary to the canonical Scope Report
header line (or, if absent, fall back to Section 1, then Section 0 H2).
The boundary now reflects where Scope Report content actually starts,
not where its label appears. Scan still uses text_no_fences so verdict-
shaped lines inside fenced documentation references do not trip.
Adds round-9 mutation tests:
- T22: Affected-refcodes field removed from Section 0 → FAIL
- T23 (3 parametrized): verdict line between Section 0 H2 and canonical
header → FAIL — `verdict: PASS`, `result: FAIL`, `Final status:
PASSED` (last one would already FAIL via R4 forbidden-verb scan, but
exercises ordering path explicitly)
Verification:
- 6/6 lints exit 0
- 841/841 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 50/50 PASS (11 round-1
+ 9 round-2 + 2 round-3 + 1 round-4 + 7 round-5 + 6 round-6 + 4 round-7
+ 4 round-8 + 4 round-9 + 2 newly-covered T8 parametrize: the new
Affected-refcodes field exercises the existing T8 mutation harness)
- Baseline template still PASSES — line 142 already carries the
Affected-refcodes field inside the canonical fenced Scope Report
block, so the new required-field check is satisfied.
Convergence note: rounds 5-7 broadened R4 forbidden-verb detection;
round 8 hardened R5 sentinel; round 9 closed two distinct contract
gaps. Different family per round still — no "补 counter" pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): ordering scan must include live Scope Report fence (codex round-10 P2)
Codex round-10 review of PR #78 surfaced a regression introduced by the
round-9 ordering boundary fix:
P2 — Round-9 anchored the R1 ordering scan on the canonical Scope
Report header, looked up via `text_no_fences`. But the canonical
header (`## Codex Audit Round N — Scope Report`) lives INSIDE the
Section 0 fenced block by spec design, so a fence-stripped search
could never find it. The boundary then fell back to Section 1, and
because the prefix was also `text_no_fences`, every verdict line
inserted INSIDE the live Scope Report fence (e.g. between the opening
``` and the canonical header) was masked. The lint reported PASS
despite a clear ordering violation in the rendered prompt content.
Fix: search `text_full` for both anchor discovery AND the prefix scan.
The earlier round-5 motivation for using fence-masked text was to keep
documentation references in late-template appendices from tripping the
rule — but the prefix is now bounded by the canonical header, which
sits near the top of Section 0, so appendix references (post-Section
7) fall well outside the scan range and cannot interfere. Meanwhile
the in-fence verdict that broke round-10 is captured.
Adds round-10 mutation tests:
- T24 (3 parametrized): verdict line inside fenced Scope Report block
but BEFORE canonical header — `verdict: PASS`, `result: FAIL`,
`Final status: PASSED`. The third would already FAIL via R4
forbidden-verb scan but exercises the ordering path explicitly; the
first two now correctly FAIL via R1 ordering.
Verification:
- 6/6 lints exit 0
- 844/844 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 53/53 PASS (cumulative
across rounds 1-10)
- Baseline template still PASSES — no verdict line inside the live
fence ahead of the canonical header.
Convergence note: round-10 closed a regression introduced by round-9.
That is a different convergence shape from "补 counter" — the round-9
fix had a real subtle bug (fence-strip + canonical-header-in-fence
interaction) that took an additional review pass to surface. Rounds
1-10 cumulative: 1 P1 + 17 P2 closed; no P0; cross-cutting families
covered (render-side parity / scope-to-Section-0 / decoy-bypass /
fence-mask asymmetry / verdict-key breadth / multi-line summary /
prefix-match sentinel / contract field completeness / fence ordering).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): boundary derives from Section-0 validated header (codex round-11 P2)
Codex round-11 review of PR #78 surfaced a corner-case false-negative
in the round-10 ordering boundary derivation:
P2 — Round-10 located the canonical Scope Report header via
`re.search(... text_full)` — first whole-file occurrence. A preamble
decoy line carrying the same header text (e.g. inside a documentation
block or appendix moved to the top) would short-circuit
`ordering_boundary` to the decoy's offset. Any `verdict: PASS` or
similar pass/fail summary inserted BETWEEN the decoy and the real
Scope Report block would then sit OUTSIDE the prefix scan range and
the lint would not flag it, despite a clear ordering rule violation
in the rendered prompt.
Fix: derive `ordering_boundary` from the Section-0-validated header
position. Search for the canonical header inside the already-extracted
`section_0_only` block (R1 already enforces it must live there as a
real heading line), then map back to absolute offset by adding the
Section 0 start. A preamble decoy that lives outside Section 0 cannot
short-circuit the boundary because the search is scoped to the
section_0_only slice. Fallbacks to Section 1 / Section 0 H2 retained
as coarse upper bounds when the validated header itself is missing
(those cases are already separately failed by R1).
Adds round-11 mutation test:
- T25: preamble decoy `## Codex Audit Round N — Scope Report` line +
`## Documentation reference` heading + verdict line BETWEEN decoy
and real Section 0 → FAIL.
Verification:
- 6/6 lints exit 0
- 845/845 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 54/54 PASS (cumulative
rounds 1-11)
- Baseline template still PASSES — no preamble decoy of the canonical
header exists in the live template.
Convergence note: rounds 9-11 form an "anchor-derivation" cluster —
each round refined how the ordering boundary is computed. Round 9
fixed scoping (header anchor instead of Section 0 H2 as boundary).
Round 10 fixed search domain (text_full instead of text_no_fences).
Round 11 fixed search scope (section_0_only instead of whole text).
This reflects a real difficulty: the boundary needs to be
authoritative (validated, not first occurrence), live (not fence-
stripped), and unique (anchored within the validated Section 0).
That is the genuine architectural shape, not whack-a-mole.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): Scope Report header must be line-equal not prefix (codex round-12 P2)
Codex round-12 review of PR #78 surfaced a P2 symmetric to round-8:
P2 — R1 line-anchored regex
`^##\s+Codex Audit Round N\s+—\s+Scope Report\b` accepted suffixed
forms because `\b` matches after `Report`. A header line mutated to
`## Codex Audit Round N — Scope Report (draft)` or `... — Scope Report
v2` still satisfied R1 even though the rendered prompt would carry a
non-canonical heading title.
Fix: introduce a module-level `_SCOPE_REPORT_HEADER_LINE_RE` requiring
line equality:
^## Codex Audit Round N — Scope Report[ \t]*(?:\n|\Z)
Optional trailing whitespace before the line break is tolerated
(invisible in rendered Markdown); anything else after the canonical
bytes — including a space + parenthesized suffix — fails. \Z handles
the EOF case. Both R1 (header presence inside Section 0) and the
ordering boundary derivation (round-11 P2 closure) now reuse this
single regex, so the line-equality contract holds across both checks.
This is the structural mirror of round-8's _find_section_1_position
fix (Section 1 byte-equivalence sentinel needed the same anchoring).
The two heading sentinels now share the same line-equality discipline.
Adds round-12 mutation test:
- T26 (4 parametrized): Scope Report header with suffix —
` (draft)`, ` v2`, `: revised`, ` — Updated` — all FAIL.
Verification:
- 6/6 lints exit 0
- 849/849 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 58/58 PASS (cumulative
rounds 1-12)
- Baseline template still PASSES — canonical header line carries no
trailing characters before the newline.
Convergence note: rounds 8 + 12 form a "line-equality" cluster (Section
1 sentinel; Scope Report header). Both used `find()` or `\b`-anchored
regex that accepted prefix matches. The fix is identical in shape:
require the canonical bytes to occupy the whole line, with optional
trailing whitespace. Now consistently applied across both heading
sentinels via a shared module-level regex.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.7.1 Step 2): R4 architectural cap rule for unquoted PASSED (codex round-13 P2)
Codex round-13 review of PR #78 surfaced one P2 in the R4 forbidden-
verb family — but this round triggers an architectural inflection
per `feedback_architectural_inflection_after_repeated_p1.md`:
P2 — Bare aggregate `PASSED` in output instructions, e.g.
`If zero findings, output exactly: PASSED`, evaded all three legacy
FORBIDDEN_AGGREGATE_PATTERNS because none required `audit summary`
phrasing, a `<key>:` line, or an H2 summary heading.
This is the FIFTH round of the same R4 family (rounds 4, 5, 6, 7, 13).
Each round added a new lexical pattern; each round codex returned
with another shape. Per `feedback_codex_review_vs_resume_audit_scope.md`
("补 counter, 不改 rule" stop condition), pattern enumeration is
unbounded — an attacker (or future contributor) can always coin a
new sentence carrying `PASSED` that no enumerated regex foresaw.
ARCHITECTURAL FIX: replace lexical enumeration with a CAP RULE.
The new R4 scans the post-Section-0 surface for any UNQUOTED `\bPASSED\b`
token. Quoted forms remain permitted because the canonical spec
template uses them in self-explanation prose:
- line 26: `"PASSED" verdict cannot mask un-retrieved sources`
- line 49: `combined-aggregate "PASSED" verb is forbidden`
Allowed quote characters: straight double `"PASSED"`, single `'PASSED'`,
backtick `` `PASSED` `` (Markdown inline code), and CJK/curly
"PASSED" (defensive). Any other context (instruction phrasing,
verdict assertion, output specification, prose claim) fails the cap.
Surface boundary: from the Section 0 H2 anchor onward — the part of
the template that becomes the rendered audit prompt body. Anything
ahead of Section 0 is upstream meta-prose (template structure
description) and is checked separately by the legacy patterns
(retained as supplementary diagnostics so structural pre-Section-0
violations still surface).
This is "boil the lake" applied to lexical enforcement: instead of
enumerating allowed/forbidden patterns, deny by default and allow
only the explicit self-explanation form. Future contributors who
need to discuss `PASSED` in prose use the quoted form (which is
what the spec template already does).
Adds round-13 mutation tests:
- T27 (4 parametrized): bare PASSED in instruction phrasings —
`If zero findings, output exactly: PASSED`, `On success: PASSED`,
`Mark the run PASSED in the manifest.`,
`Emit a single token: PASSED.` — all FAIL.
- T28 (3 parametrized): quoted self-explanation forms must remain
ALLOWED — straight double quotes, backtick inline code, embedded
documentation note — all PASS.
Verification:
- 6/6 lints exit 0
- 856/856 pytest PASS across the full repo
- scripts/test_check_v3_6_8_audit_scope_block.py 65/65 PASS (cumulative
rounds 1-13 plus the cap-rule allow tests)
- Baseline template PASSES — both existing `"PASSED"` mentions
(lines 26, 49) are quoted self-explanation form, allowed by cap rule.
Convergence note: this closes the R4 lexical-enumeration cluster
that ran from round 4 to round 13. Future rounds may surface non-R4
families (more decoy / boundary / contract-completeness shapes), but
cannot return to the same family — the cap rule subsumes all
permutations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
106 lines
4.1 KiB
Python
106 lines
4.1 KiB
Python
"""Render-side regression test for v3.7.1 Step 2 — Section 0 Scope Report.
|
|
|
|
Codex round-1 review of PR #78 surfaced a P1 architectural finding: the static
|
|
Section 0 block was prepended to `shared/templates/codex_audit_multifile_template.md`
|
|
and lint-enforced, but `scripts/audit_snapshot.py::render_prompt` extracted only
|
|
sections [3, 6, 7] from the template. Section 0 was therefore absent from every
|
|
wrapper-dispatched audit prompt, defeating the spec §3.2 contract that the
|
|
Scope Report rides verbatim every round.
|
|
|
|
This test pins the render-side contract: render_prompt MUST emit a Section 0
|
|
Scope Report block ahead of the Section 1 Round metadata block so the audit
|
|
target sees the scope-disclosure framing before any pass/fail summary.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scripts.audit_snapshot import render_prompt
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
TEMPLATE_PATH = (
|
|
REPO_ROOT / "shared" / "templates" / "codex_audit_multifile_template.md"
|
|
)
|
|
|
|
|
|
def _render_minimal_prompt() -> bytes:
|
|
"""Run render_prompt with minimal valid arguments."""
|
|
return render_prompt(
|
|
audit_template=TEMPLATE_PATH.read_bytes(),
|
|
primary_paths=["primary/file.md"],
|
|
primary_contents=[b"primary content"],
|
|
supporting_paths=[],
|
|
supporting_contents=[],
|
|
round_n=1,
|
|
target_rounds=3,
|
|
git_sha="0123456789ab",
|
|
stage=2,
|
|
agent="synthesis_agent",
|
|
prior_findings=None,
|
|
)
|
|
|
|
|
|
def test_render_prompt_emits_section_0_scope_report_header() -> None:
|
|
"""The rendered prompt MUST contain the Section 0 H2 anchor."""
|
|
rendered = _render_minimal_prompt()
|
|
assert b"## Section 0 \xe2\x80\x94 Scope Report" in rendered, (
|
|
"render_prompt does not emit Section 0 Scope Report header — "
|
|
"spec §3.2 contract violated (codex round-1 PR #78 P1)."
|
|
)
|
|
|
|
|
|
def test_render_prompt_emits_scope_report_codex_round_header() -> None:
|
|
"""The rendered prompt MUST contain the canonical Scope Report header."""
|
|
rendered = _render_minimal_prompt()
|
|
assert b"## Codex Audit Round N \xe2\x80\x94 Scope Report" in rendered, (
|
|
"render_prompt does not emit the canonical Scope Report header "
|
|
"literal (spec line 134)."
|
|
)
|
|
|
|
|
|
def test_render_prompt_section_0_appears_before_section_1() -> None:
|
|
"""Spec line 146 firm rule: Scope Report must appear BEFORE any pass/fail summary."""
|
|
rendered = _render_minimal_prompt()
|
|
section_0_pos = rendered.find(b"## Section 0 \xe2\x80\x94 Scope Report")
|
|
section_1_pos = rendered.find(b"## Section 1 \xe2\x80\x94 Round metadata")
|
|
assert section_0_pos != -1 and section_1_pos != -1, (
|
|
"fixture assumption violated: both Section 0 and Section 1 must be present"
|
|
)
|
|
assert section_0_pos < section_1_pos, (
|
|
f"Scope Report (offset {section_0_pos}) does not appear before Section 1 "
|
|
f"(offset {section_1_pos}); spec line 146 firm rule violated."
|
|
)
|
|
|
|
|
|
def test_render_prompt_emits_required_scope_report_fields() -> None:
|
|
"""The four required Scope Report content fields must appear (spec lines 136-140)."""
|
|
rendered = _render_minimal_prompt()
|
|
text = rendered.decode("utf-8")
|
|
required_fields = [
|
|
"**Total entries audited:**",
|
|
"**Entries with retrieved original source:**",
|
|
"**Entries description-only (no retrieved source):**",
|
|
"**Audit scope warning:**",
|
|
]
|
|
missing = [f for f in required_fields if f not in text]
|
|
assert not missing, (
|
|
f"render_prompt output missing required Scope Report fields: {missing}"
|
|
)
|
|
|
|
|
|
def test_render_prompt_emits_aggregate_status_splits() -> None:
|
|
"""The three aggregate-status splits must appear (spec lines 147-150)."""
|
|
rendered = _render_minimal_prompt()
|
|
text = rendered.decode("utf-8")
|
|
required_splits = [
|
|
"verified-against-source",
|
|
"description-internally-consistent",
|
|
"unaudited-due-to-missing-source",
|
|
]
|
|
missing = [s for s in required_splits if s not in text]
|
|
assert not missing, (
|
|
f"render_prompt output missing aggregate-status splits: {missing}"
|
|
)
|