mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
feat(v3.6.7 Step 6 Phase 6.7): downstream agent partial inversion sweep + INV-1/2/3 lint (10 codex rounds, 0 P1) (#66)
* feat(v3.6.7 Step 6 Phase 6.7): downstream agent partial inversion sweep + INV-1/2/3 lint
Implements Phase 6.7 deliverable per spec §10 (line 2391-2403):
Prompt edits per §6.2 sweep table:
- synthesis_agent.md line 164: remove "Cross-model audit follows ..."
sentence (Clause 2(b)+(d) violation); append canonical Clause 1 line
bullet to PATTERN PROTECTION block.
- research_architect_agent.md line 190: remove "Cross-model audit covers
these via dimension §3.5 ..." sentence (Clause 2(b)+(d) violation);
append canonical Clause 1 line.
- report_compiler_agent.md line 172: remove "Cross-model audit covers
these via dimension §3.7 ..." sentence; merge prior line 177 (trim
"The orchestrator runs codex audit afterward" tail) and line 178
("Output metadata must not claim audit-passed state") into the single
canonical Clause 1 line bullet.
Manifest (new): scripts/v3_6_7_inversion_manifest.json
- Three-file scope per §6.3; locks the v3.6.7 sweep boundary so
widening to a fourth file requires explicit §9 L2 resolution.
Lint extension: scripts/check_v3_6_7_pattern_protection.py
- INV-1: canonical Clause 1 line appears exactly once in each manifest
file's PATTERN PROTECTION block (presence + uniqueness).
- INV-2: zero hits across four Clause 2 disclosure regex patterns
(a) the orchestrator + audit, (b) cross-model audit follows/covers
+ codex_audit_multifile_template, (c) audit afterwards/will be run/
is dispatched, (d) downstream audit / this output is/will be audited.
All compiled with re.IGNORECASE per §6.3.
- INV-3: scans deep-research/agents/ + academic-pipeline/agents/ for
the canonical line; any hit outside the manifest fails (defends
against accidental sweep widening, guards §9 L2 deferred question).
- C3 regex consolidation: prior C1-C3 had two separate regexes for
(anti-fake-audit pair) and (output-metadata audit-passed prohibition)
bullets. Phase 6.7 merges them into a single whole-line regex matching
the canonical Clause 1 line so all three prohibition tokens
(DO NOT × 2 + must not) sit inside the matched span — the negation
post-filter no longer flags a sibling prohibition as a weakener
(B2 R3-001 span-restricted exemption).
Tests: scripts/test_check_v3_6_7_pattern_protection.py (+12 tests)
- INV-1 negative × 4: canonical line deletion (synthesis / architect /
compiler) + duplication (synthesis).
- INV-2 negative × 5: inject one canonical violation phrase per pattern
(a)/(b)/(c) and two for (d)'s alternation.
- INV-3 negative × 3: canonical line in non-manifest agent file
(bibliography_agent), manifest shrunk to two files, manifest extra
entry pointing to a file without the canonical line.
- Removed obsolete R3MutationTests.test_r3_001_orchestrator_does_not_run_fails
whose mutation source ("The orchestrator runs codex audit afterward.")
no longer exists post-sweep.
Verification gate per spec §10 line 2403:
- check_v3_6_7_pattern_protection.py: 12/12 passed (9 existing checks +
INV-1/2/3 aggregated)
- test suite: 40/40 tests green (28 existing - 1 obsolete + 13 new INV tests
including BaselineTest)
Phase 6.6 PR #65 was shipped standalone; Phase 6.7 follows as a separate
PR per spec §10 line 2401's bundling recommendation deferred (Phase 6.6
shipped first due to budget reconciliation patch in PR #64).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 tests): update C3 audit-passed deletion mutation source post-canonical-line merge
Phase 6.7 merged line 178 ("Output metadata must not claim audit-passed
state.") into the canonical Clause 1 line bullet. The standalone bullet
form (preceded by `\n- `) no longer exists, so the prior
test_c3_audit_passed_sentence_deleted_fails mutation source string is
not found and the test errors out. Update the mutation to delete just
the audit-passed segment from within the merged canonical line bullet;
the assertion (lint must fail) is unchanged.
Verified: 40/40 tests pass after this fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R1): close 3 codex P2 INV bypass findings
Codex R1 review (model=gpt-5.5 + xhigh, --base main) surfaced three P2
findings against the initial INV-1/INV-2/INV-3 implementation. All three
allowed full lint PASS while the spec §6.3 invariant was violated.
R1-001 P2 — INV-2 wrapped disclosure bypass
- Pre-fix: 4 INV-2 patterns compiled with `re.IGNORECASE` only. `.*`
did not cross newlines, so a Markdown soft-wrap of a forbidden Clause
2 sentence (e.g. "the orchestrator" on one line, "audit" on the
next) reported PASS despite the disclosure being intact.
- Fix: compile all four INV-2 patterns with `re.IGNORECASE | re.DOTALL`
so `.` crosses newlines.
R1-002 P2 — INV-1 canonical-with-tail-weakener bypass
- Pre-fix: CANONICAL_CLAUSE_1_RE ended at `state\b`, so an agent prompt
could keep the canonical bullet's three sentences yet append a tail
weakener (` if feasible.`, `; this is recommended.`) and INV-1 still
matched the prefix. The spec §6.3 INV-1 rule requires the bullet
text to match verbatim, which means anchored to the sentence end.
- Fix: re-compile with `re.MULTILINE` and end the pattern at
`state\.\s*$` so the third sentence must be the bullet's terminal
content. Apply the parallel `state\.(?=\s|\Z)` anchor to the C3
whole-line regex inside the C1-C3 Check (defense in depth — the
pre-existing `_ALWAYS_NEGATION_PATTERNS` already lists `if feasible`
/ `when possible` / `where feasible` etc., but the regex anchor
closes the gap before negation post-filter is even consulted).
R1-003 P2 — Manifest copy-paste widening attack
- Pre-fix: `_load_inversion_manifest()` only validated that `scope ==
"v3.6.7-only"` and `files` was a list of strings. An attacker (or
well-intentioned editor) could append a fourth file to the manifest
AND copy the canonical bullet into that file's PATTERN PROTECTION
block. INV-1 then passed for all four files (canonical present
everywhere). INV-3 silently skipped the new file because it was now
in `manifest_set`. End result: full lint PASS, sweep silently
widened, spec §9 L2 deferred question marked decided without
resolution.
- Fix: introduce module-level `EXPECTED_MANIFEST_FILES` tuple frozen at
the v3.6.7 three-file scope. `_load_inversion_manifest()` now
validates `set(files) == set(EXPECTED_MANIFEST_FILES)` (exact match,
no superset / no drift) AND uniqueness (`len(files) ==
len(set(files))`). Per spec §6.3 line 1807, future v3.6.8+ widening
must land its own version-tagged manifest with its own scope tag,
not retroactively edit v3.6.7's manifest.
Tests added (4): each closure pair has at least one mutation regression
test in CodexR1MutationTests covering the bypass codex demonstrated:
- test_r1_p2_inv2_wrapped_disclosure_fails (R1-001)
- test_r1_p2_inv1_canonical_with_tail_weakener_fails (R1-002)
- test_r1_p2_manifest_widening_with_canonical_copy_fails (R1-003)
- test_r1_p2_manifest_duplicate_entry_fails (R1-003 defense in depth)
Verified: 44/44 tests pass (40 prior + 4 new R1 tests). 12/12 lint
checks pass on clean working tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R2): close 2 codex P2 INV bullet-extraction findings
Codex R2 review (model=gpt-5.5 + xhigh, --base main, after R1 fixes
landed) surfaced two more P2 findings — both about the granularity
mismatch between the spec's per-bullet semantics and the regex's
substring/cross-bullet matching.
R2-001 P2 — INV-1 false negative on bullet-prefix weakener
- Pre-R2: CANONICAL_CLAUSE_1_RE was a regex search-anywhere with
re.MULTILINE + `\.\s*$`. A bullet of the form `- When feasible, DO
NOT simulate ...` carried the canonical sentences as a substring;
the regex still matched the suffix and INV-1 reported PASS. The
spec §6.3 INV-1 rule says "exactly one bullet whose text matches
the canonical Clause 1 line verbatim" — substring match is not
verbatim.
- Pre-R2 dual: the same anchor rejected harmless soft-wraps (e.g. a
Markdown reflow line break between `run` and `codex/external`).
- Fix: introduce `_iter_bullets()` helper that walks `^- ` markers,
collapses each bullet's whitespace runs (including newlines from
soft wraps) to single spaces, and yields normalized bullet text.
INV-1 now compares each bullet's normalized text byte-for-byte
against `CANONICAL_CLAUSE_1_TEXT`. Soft-wrap tolerance preserved;
prefix weakener rejected; CANONICAL_CLAUSE_1_RE retired.
R2-002 P2 — INV-2 false positive across bullets
- Pre-R2: INV-2(a) `\bthe orchestrator\b.*\baudit\b` (and INV-2(b))
compiled with re.IGNORECASE | re.DOTALL applied to raw block text.
The unbounded `.*` would match across bullet boundaries — a benign
`the orchestrator` mention in one bullet plus the canonical bullet's
`audit step` mention in another would false-positive even though
no single sentence said the orchestrator runs an audit.
- Fix: `_inv2_check_file` now iterates `_iter_bullets(block)` and
applies each pattern against one bullet's normalized text at a
time. The canonical Clause 1 bullet is skipped (it inherently
contains "audit step" / "audit-passed state" tokens but is not a
disclosure). Patterns now compile with re.IGNORECASE only — DOTALL
is unnecessary because soft-wrap tolerance comes from the
whitespace-normalization step.
R2-derived consistency: INV-3 also now operates at bullet
granularity. Scanning the whole file as bullet sequence eliminates
false positives where a non-manifest agent prompt happens to discuss
the canonical wording in prose (a heading or paragraph mention does
not count as the canonical bullet).
Tests added (4): each closure has at least one regression test in
CodexR2MutationTests, plus one positive test for the soft-wrap
tolerance side of R2-001 and one for the cross-bullet false-positive
side of R2-002. Defense-in-depth test confirms a real in-bullet
INV-2 violation still fails after the per-bullet refactor.
Verified: 48/48 tests pass (44 prior + 4 new R2 tests). 12/12 lint
checks pass on clean working tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R3): close codex P2 INV-2 prose-paragraph regression
Codex R3 review (model=gpt-5.5 + xhigh, --base main, after R2 fixes
landed) surfaced one P2 — the R2 per-bullet refactor of INV-2 dropped
coverage of the intro paragraph, which is exactly where the Phase 6.7
sweep removed Clause 2 disclosures.
R3-001 P2 — INV-2 misses non-bullet prose disclosures
- Pre-R3: `_inv2_check_file` iterated only bullets (`_iter_bullets`).
But the three sentences removed by the §6.2 sweep
(`Cross-model audit follows ... codex_audit_multifile_template.md`)
were intro-paragraph prose, not bullets. The original forbidden
pipeline leak could be re-added to the intro paragraph and lint
silently passed all 12 checks.
- Fix: introduce `_iter_block_segments(block)` helper that yields
`(offset, kind, normalized_text)` for every matched-content
segment — both `prose` paragraphs (intro paragraph; future inline
paragraphs) AND `bullet` items. INV-2 now iterates segments,
whitespace-normalizing each (preserving R2's soft-wrap tolerance
and bounded-`.*` invariant), and applies the four patterns against
one segment at a time.
Heading line `## PATTERN PROTECTION (v3.6.7)` is excluded — it is
content marker, not contract content. The canonical Clause 1 bullet
is still skipped (it inherently carries `audit step` /
`audit-passed state` tokens; it is the required prohibition, not a
disclosure).
Tests added (1): `test_r3_p2_inv2_intro_paragraph_disclosure_fails`
re-introduces the synthesis_agent intro paragraph's original
`Cross-model audit follows ... codex_audit_multifile_template.md`
sentence and asserts INV-2 catches it as a `prose` violation. The
diagnostic includes the segment kind for debuggability.
Verified: 49/49 tests pass. 12/12 lint checks pass on clean working
tree. Manual probe confirms R3 attack vector (re-add Clause 2 prose
to intro paragraph) now fails with "[INV-2(b)] ... Clause 2
violation in prose: ...".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R4): close codex P2 INV-2 post-bullet prose regression
Codex R4 review (model=gpt-5.5 + xhigh, --base main, after R3 fixes
landed) surfaced one more P2 — R3's segmentation only walked prose
paragraphs in `block[:first_bullet]`, missing prose appended after
the canonical bullet.
R4-001 P2 — INV-2 misses post-bullet prose disclosures
- Pre-R4: `_iter_block_segments` extracted prose paragraphs from
`block[:first_bullet]` only (intro paragraph + paragraphs before
the first `- ` marker), then appended bullet items. Any prose
appended AFTER the bullet list — e.g. a trailing paragraph saying
"The orchestrator runs codex audit afterward." after the canonical
bullet — fell outside the segmentation window and silently passed
the four INV-2 patterns.
- Fix: rewrite `_iter_block_segments` to paragraph-split the ENTIRE
block (`re.split(r"\n\s*\n", block)`) and classify each non-empty
paragraph as either a bullet group (starts with `- `, walked via
`_iter_bullets`) or a prose paragraph. Bullet-vs-prose discrimination
is uniform across the whole block, so:
* intro paragraph → prose
* first bullet group → bullets (one or more)
* inserted paragraph between bullet groups → prose
* trailing paragraph after last bullet → prose
The promise is now: every non-heading-non-empty character in the
block lands in one segment of the segmentation, no carve-out.
Tests added (1): `test_r4_p2_inv2_post_bullet_prose_disclosure_fails`
appends "The orchestrator runs codex audit afterward." as a prose
paragraph immediately after the canonical bullet in synthesis_agent
and asserts lint catches it as a Clause 2 prose violation.
Verified: 50/50 tests pass. 12/12 lint checks pass on clean working
tree. Manual probe confirms R4 attack vector now fails with
"[INV-2(a)] ... Clause 2 violation in prose: 'The orchestrator runs
codex audit afterward.'".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R5): close codex P2 C3 regex soft-wrap intolerance
Codex R5 review (model=gpt-5.5 + xhigh, --base main, after R4 fixes
landed) surfaced one P2 — the C3 Check regex still used literal
spaces inside sentences while INV-1 had been refactored to
whitespace-tolerant exact compare. CI failed on harmless Markdown
soft-wraps inside the canonical bullet.
R5-001 P2 — C3 regex rejects soft-wrapped canonical bullet
- Pre-R5: C3 regex used `\s+` only between sentences but literal
single spaces inside each sentence. A soft-wrap inside a sentence
(e.g. between `run` and `codex/external`) caused C3 to FAIL even
though INV-1's bullet-text whitespace normalization correctly
accepted it. Result: CI failed on benign Markdown reflow despite
the v3.6.7 contract not being violated.
- Fix: every inter-token space inside the C3 regex now accepts
`\s+`. Soft-wrap tolerance is now uniform across:
1. INV-1 (`_iter_bullets` whitespace-normalization)
2. C3 Check regex (this fix)
The `\.(?=\s|\Z)` trailing anchor (R1 closure) is preserved so a
tail weakener like ` if feasible.` is still rejected.
Tests added (1): `test_r5_p2_compiler_softwrap_canonical_passes`
mutates report_compiler's canonical bullet to wrap between `run`
and `codex/external` and asserts lint passes (rc=0). Mirror of R2's
synthesis-side soft-wrap positive test, now covering the C3 surface.
Verified: 51/51 tests pass. 12/12 lint checks pass on clean working
tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R6): close codex P2 INV-1 weakened-duplicate bypass
Codex R6 review (model=gpt-5.5 + xhigh, --base main, after R5 fixes
landed) surfaced one P2 — INV-1's "exactly one bullet equals canonical"
contract counted ONLY exact matches, leaving room for a duplicate
weakened variant alongside the exact canonical bullet.
R6-001 P2 — INV-1 ignores weakened duplicates
- Pre-R6: `_inv1_check_file` counted bullets whose normalized text
equaled CANONICAL_CLAUSE_1_TEXT and required count == 1. An attacker
could keep the exact canonical bullet AND add a second near-canonical
variant (`- When feasible, DO NOT simulate ...` / `... audit-passed
state if feasible.`); the exact-count stayed at 1, lint passed, and
the block was left with two contradictory bullets the agent might
internalize. The spec §6.3 INV-1 wording "exactly one bullet whose
text matches the canonical Clause 1 line verbatim" implicitly forbids
weakened duplicates.
- Fix: introduce `_is_clause_1_like` helper using fragment markers
(`do not simulate`, `do not claim to have run`, `audit-passed
state`) to identify any bullet that reads as a Clause 1 variant.
`_inv1_check_file` now collects two bullet sets:
* exact: bullets whose normalized text equals canonical
* weakened: Clause 1-like bullets that are NOT exact
Lint fails if `weakened` is non-empty (any near-duplicate present)
OR if `len(exact) != 1`. Diagnostic enumerates the offending
weakened bullets so the editor sees exactly which ones to fix.
The fragment markers are load-bearing pieces of the canonical
sentence — no other v3.6.7 PATTERN PROTECTION bullet legitimately
carries them, so the heuristic does not false-positive on the A1-A5 /
B1-B5 / C1-C2 obligation bullets.
Tests added (2): `test_r6_p2_inv1_weakened_duplicate_alongside_canonical_fails`
covers the prefix-weakener variant; `test_r6_p2_inv1_tail_weakened_duplicate_fails`
covers the tail-weakener variant (defense in depth).
Verified: 53/53 tests pass. 12/12 lint checks pass on clean working
tree. Manual probe confirms both R6 attack vectors now fail with
clear diagnostic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R7): close codex P2 INV-3 weakened-Clause-1 scope-leak bypass
Codex R7 review (model=gpt-5.5 + xhigh, --base main, after R6 fixes
landed) surfaced one P2 — INV-3 had the same weakened-duplicate
asymmetry that R6 closed inside manifest files: INV-3 rejected only
exact canonical bullets in non-manifest prompts, so a near-canonical
bullet outside the manifest still widened the v3.6.7 prohibition
scope silently.
R7-001 P2 — INV-3 misses Clause 1-like bullets outside the manifest
- Pre-R7: `_inv3_check` checked `bt == CANONICAL_CLAUSE_1_TEXT` for
each bullet in non-manifest agent files. A near-canonical variant
(`- When feasible, DO NOT simulate ...`) was not equal-byte to
canonical, so INV-3 passed even though the same prohibition
vocabulary was being smuggled into a fourth file.
- Fix: replace exact equality with `_is_clause_1_like` (the helper
R6 added for INV-1's weakened-duplicate detection). The same
fragment markers (`do not simulate`, `do not claim to have run`,
`audit-passed state`) now flag any Clause 1-like bullet anywhere
in a non-manifest agent file. Diagnostic enumerates offending
bullets so the editor can identify exactly what to remove.
The asymmetry between INV-1 (R6) and INV-3 (R7) is now closed:
both use the same Clause 1-like heuristic, so neither path can be
bypassed by the weakened-duplicate attack class.
Tests added (1): `test_r7_p2_inv3_weakened_clause_1_in_non_manifest_fails`
appends a `When feasible, ...` near-canonical bullet to
bibliography_agent.md (not in manifest) and asserts INV-3 fails.
Verified: 54/54 tests pass. 12/12 lint checks pass on clean working
tree. Manual probe confirms R7 attack vector now fails with
"[INV-3] ... Offending bullet(s): ['When feasible, DO NOT simulate ...']".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R8): close 2 codex P2 INV-3 over-broad / under-broad findings
Codex R8 review (model=gpt-5.5 + xhigh, --base main, after R7 fixes
landed) surfaced two P2 findings — a false-positive on generic
anti-fabrication guidance AND a false-negative on prose-form
canonical pasting.
R8-001 P2 — `_is_clause_1_like` over-broad on generic prohibition
- Pre-R8: any bullet containing `do not simulate` or `do not claim
to have run` was flagged Clause 1-like. These are common
anti-fabrication phrases used in many agent prompts. A legitimate
bibliography guidance bullet `- Do not simulate data or sources.`
would false-positive INV-3 and prevent ordinary anti-fabrication
policies from being added to non-manifest agents.
- Fix: split fragments into two tiers:
_CLAUSE_1_AUDIT_FRAGMENTS = ("audit step", "audit-passed state",
"codex/external review")
_CLAUSE_1_GENERIC_FRAGMENTS = ("do not simulate",
"do not claim to have run")
`_is_clause_1_like` now requires at least one audit-specific
fragment. Generic fragments alone no longer trip the check, so
unrelated anti-fabrication guidance can ride elsewhere.
R8-002 P2 — INV-3 misses prose-form canonical pasting
- Pre-R8: `_inv3_check` only walked `- ` bullets. A non-manifest
prompt that pasted the exact canonical sentence as a normal
paragraph (not a Markdown list item) silently passed lint, even
though the prohibition was widened.
- Fix: add a paragraph-level scan inside `_inv3_check`. For each
non-bullet, non-heading paragraph in a non-manifest agent file,
whitespace-normalize and compare against `CANONICAL_CLAUSE_1_TEXT`
byte-for-byte. Prose match uses exact equality (not the
Clause 1-like heuristic) so a *discussion* of the canonical
wording in prose (e.g. quoting it for context) only fails when
the line is actually re-introduced as contract content.
Diagnostic enumerates offenders separately as `bullet:` vs `prose:`
so the editor sees exactly which form to fix.
Tests added (2):
- `test_r8_p2_generic_do_not_simulate_in_non_manifest_passes`
injects `- Do not simulate data or sources.` into
bibliography_agent and asserts lint passes (no false positive).
- `test_r8_p2_inv3_canonical_as_prose_in_non_manifest_fails`
injects the canonical sentence as a prose paragraph (not a
bullet) into bibliography_agent and asserts lint fails with
`prose: ...` offender diagnostic.
Verified: 56/56 tests pass. 12/12 lint checks pass on clean working
tree. Two manual probes confirm both attack vectors land correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(v3.6.7 Phase 6.7 R9): close 2 codex P2 INV-3 architectural rewind
Codex R9 review (model=gpt-5.5 + xhigh, --base main, after R8 fixes
landed) surfaced that R7+R8's extension of the Clause 1-like heuristic
to INV-3 was over-extension. Spec §6.3 INV-3 wording says "canonical
Clause 1 line found outside the manifest" — that means the actual
sentence, not Clause 1-like variants. Variants outside the manifest
are a v3.6.8+ concern, not a v3.6.7 INV-3 contract.
R9-001 P2 — INV-3 over-broad on audit-related bullets outside manifest
- Pre-R9: INV-3 applied `_is_clause_1_like` to every bullet in non-
manifest agents. A legitimate process-flow bullet like
`- Review each audit step before finalizing.` triggered the lint
even though it has no prohibition semantics. The heuristic was
designed for INV-1's manifest-internal weakened-duplicate detection
(where any `audit step` mention SHOULD equal canonical), not for
scope-widening detection in unrelated agents.
- Fix: INV-3 now matches bullets via byte-exact whitespace-normalized
equality with `CANONICAL_CLAUSE_1_TEXT`. The Clause 1-like heuristic
remains in `_is_clause_1_like` for INV-1's R6+R8 contracts.
R9-002 P2 — INV-3 prose scan missed canonical after heading with no blank line
- Pre-R9: prose scan used `re.split(r"\n\s*\n", text)` to extract
paragraphs and filtered out paragraphs starting with `## `. When
an editor pasted `## PATTERN PROTECTION (v3.6.7)\nDO NOT simulate
...` (heading immediately followed by canonical sentence with no
intervening blank line), the entire chunk was one paragraph that
started with `## ` and was filtered out — canonical sentence
silently slipped past INV-3.
- Fix: replace paragraph-split with line-level heading-strip plus
whitespace-collapse. All non-heading non-bullet lines join with
single spaces; the canonical sentence is detected as a substring.
Heading adjacency no longer bypasses the scan.
Test updates:
- Renamed `CodexR7MutationTests` → `CodexR7R9MutationTests` and
inverted the R7 test's expectation: a weakened Clause 1 variant
outside the manifest now PASSES (not fails). Documents the R7→R9
architectural rewind.
- Added `CodexR9MutationTests` with two regression tests covering
R9-001 (audit-step bullet not equal to canonical passes) and
R9-002 (canonical-after-heading-no-blank-line fails).
Verified: 57/57 tests pass. 12/12 lint checks pass on clean working
tree. Two manual probes confirm both R9 attack/non-attack vectors
land correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
2a796b9a8d
commit
e7e775a0e1
@@ -169,10 +169,9 @@ The full report in markdown with APA 7.0 formatting, plus:
|
||||
|
||||
## PATTERN PROTECTION (v3.6.7)
|
||||
|
||||
These rules apply when this agent operates in **abstract-only mode** (compiling a publisher-format abstract from a stable body draft, typically the Phase 3 hand-off after the body has been calibrated by upstream). They harden output against the three publication-side hallucination/drift patterns documented in `docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md` §3.3 (C1–C3). Cross-model audit covers these via dimension §3.7 (COI adequacy) plus the bundle-specific Section 4(f) check of `shared/templates/codex_audit_multifile_template.md`.
|
||||
These rules apply when this agent operates in **abstract-only mode** (compiling a publisher-format abstract from a stable body draft, typically the Phase 3 hand-off after the body has been calibrated by upstream). They harden output against the three publication-side hallucination/drift patterns documented in `docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md` §3.3 (C1–C3).
|
||||
|
||||
- Word budget uses whitespace-split convention (`body.split()`), not hyphenated-as-1. Reserve 3–5% buffer below hard cap. See `shared/references/word_count_conventions.md`.
|
||||
- Compression must preserve protected hedging phrases identified by upstream calibration as budget-protected (the dispatch context carries the list). See `shared/references/protected_hedging_phrases.md`.
|
||||
- Reflexivity disclosure must use explicit temporal bounds: explicit year range, past-tense disambiguating verb, or "former" prefix. Deictic temporal phrases ("during this period" / "at the time") are forbidden.
|
||||
- DO NOT simulate any audit step. DO NOT claim to have run codex/external review. The orchestrator runs codex audit afterward.
|
||||
- Output metadata must not claim audit-passed state.
|
||||
- DO NOT simulate any audit step. DO NOT claim to have run codex/external review. Output metadata must not claim audit-passed state.
|
||||
|
||||
@@ -187,10 +187,11 @@ Recommended platforms: PROSPERO for systematic reviews, OSF Registries for all o
|
||||
|
||||
## PATTERN PROTECTION (v3.6.7)
|
||||
|
||||
These rules apply when this agent operates as the **survey designer** for instrument design (Likert items, consent scripts, retrospective items, list-of-options items). They harden output against the five instrument-side hallucination/drift patterns documented in `docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md` §3.2 (B1–B5). Cross-model audit covers these via dimension §3.5 (instrument quality) of `shared/templates/codex_audit_multifile_template.md`.
|
||||
These rules apply when this agent operates as the **survey designer** for instrument design (Likert items, consent scripts, retrospective items, list-of-options items). They harden output against the five instrument-side hallucination/drift patterns documented in `docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md` §3.2 (B1–B5).
|
||||
|
||||
- Consent / privacy language must pass through `shared/references/irb_terminology_glossary.md` before output. Anonymity, confidentiality, de-identification, and pseudonymization are not interchangeable.
|
||||
- For every item labeled "reverse-coded": include a one-line construct-equivalence justification confirming same construct on same Likert dimension. True reverse vs contrast distinction is mandatory. See `shared/references/psychometric_terminology_glossary.md`.
|
||||
- Retrospective items default to event-anchored phrasing ("immediately before X happened to your unit"). Calendar-anchored phrasing only when sample shares a common event date.
|
||||
- Item phrasing must be neutral/balanced. Chapter argument vocabulary is forbidden in instrument items. Open-text prompts must invite all valences ("positive, negative, or neutral").
|
||||
- Any list-of-options item must declare its primary-source list and enumerate fully. No subsetting, no over-setting, no scope cross-contamination.
|
||||
- DO NOT simulate any audit step. DO NOT claim to have run codex/external review. Output metadata must not claim audit-passed state.
|
||||
|
||||
@@ -161,10 +161,11 @@ Gap: [ ] Theme D (0 sources)
|
||||
|
||||
## PATTERN PROTECTION (v3.6.7)
|
||||
|
||||
These rules harden the synthesis output against the five narrative-side hallucination/drift patterns documented in `docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md` §3.1 (A1–A5). Cross-model audit follows `shared/templates/codex_audit_multifile_template.md` audit dimensions §3.1, §3.2, §3.3, §3.4 and the bundle-specific Section 4(f) check.
|
||||
These rules harden the synthesis output against the five narrative-side hallucination/drift patterns documented in `docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md` §3.1 (A1–A5).
|
||||
|
||||
- For each source cited in 2+ sections: pre-list the source's effect inventory and run a cross-section consistency self-check before output.
|
||||
- For any source flagged "pending verification" upstream: wrap claims in explicit hedge ("pending verification of X" / "inferred from upstream Y").
|
||||
- For each substantive claim: include a one-line anchor justification.
|
||||
- Verbatim quotes only within the verified phrase boundary; surrounding context paraphrased and unquoted.
|
||||
- For un-provided external documents (e.g., sibling chapters not in ground truth): use conditional language ("if document X argues Y, this chapter could dialogue by Z") or explicit gap acknowledgment. Declarative claims about un-provided documents are forbidden.
|
||||
- DO NOT simulate any audit step. DO NOT claim to have run codex/external review. Output metadata must not claim audit-passed state.
|
||||
|
||||
@@ -742,37 +742,529 @@ def compiler_agent_checks() -> list[Check]:
|
||||
"C1 protected hedges non-negotiable",
|
||||
r"protected\s+hedg(?:e|ing)\s+phrases[^.\n]{0,200}\b(?:budget[- ]protected|non-negotiable|verbatim)\b",
|
||||
),
|
||||
# C3 — anti-fake-audit guard. Both DO NOT clauses must appear
|
||||
# in either order. The gap allows two short sentences (the
|
||||
# natural "DO NOT simulate ... DO NOT claim ..." wording).
|
||||
# This is itself a prohibition obligation, so allow the
|
||||
# `DO NOT` imperative through the negation post-filter
|
||||
# (R4-001 still applies — trailing "this is not required"
|
||||
# is rejected by the adjective-targeted negation rules).
|
||||
# C3 — canonical Clause 1 line per Step 6 §6.2. The line is
|
||||
# one bullet carrying three prohibitions in fixed order:
|
||||
# "DO NOT simulate any audit step. DO NOT claim to have run
|
||||
# codex/external review. Output metadata must not claim
|
||||
# audit-passed state." Phase 6.7 merged the prior two C3
|
||||
# regexes (anti-fake-audit pair + output-metadata) into a
|
||||
# single line so the bullet is whole-line verbatim — INV-1
|
||||
# below enforces presence/uniqueness across all three
|
||||
# in-scope prompts; this regex stays here to keep C1-C3
|
||||
# mutation coverage intact (R2-001 inverted-must-not, R3-001
|
||||
# trailing weakener, R4-001 advisory framing). Pattern is
|
||||
# whole-line so all three prohibition tokens (DO NOT × 2 +
|
||||
# must not) sit inside the matched span and the negation
|
||||
# post-filter does not flag a sibling prohibition as a
|
||||
# weakener (B2 R3-001 span-restricted exemption).
|
||||
(
|
||||
"C3 anti-fake-audit guard pair",
|
||||
r"DO NOT simulate[^\n]{0,300}\.[^\n]{0,100}\bDO NOT claim to have run\b"
|
||||
r"|DO NOT claim to have run[^\n]{0,300}\.[^\n]{0,100}\bDO NOT simulate\b",
|
||||
"C3 canonical Clause 1 line (whole-line verbatim)",
|
||||
# Whitespace-tolerant: every inter-token space inside
|
||||
# the canonical line accepts `\s+` so a Markdown
|
||||
# soft-wrap anywhere inside a sentence (e.g. between
|
||||
# `run` and `codex/external`) is treated as the
|
||||
# same canonical bullet — matches INV-1's
|
||||
# whitespace-normalization contract (codex R5 P2
|
||||
# closure). The trailing `\.(?=\s|\Z)` anchor still
|
||||
# rejects tail weakeners like ` if feasible.` (R1
|
||||
# P2 closure).
|
||||
r"\bDO\s+NOT\s+simulate\s+any\s+audit\s+step\.\s+"
|
||||
r"DO\s+NOT\s+claim\s+to\s+have\s+run\s+codex/external\s+review\.\s+"
|
||||
r"Output\s+metadata\s+must\s+not\s+claim\s+audit-passed\s+state\.(?=\s|\Z)",
|
||||
True, # allow_prohibition: this IS the prohibition
|
||||
),
|
||||
# C3 — output metadata prohibition. Spec §6.3 explicitly
|
||||
# closes the loop: "Output metadata must not claim
|
||||
# audit-passed state." Without this, an agent could
|
||||
# honour the DO NOT pair while still surfacing fake
|
||||
# audit-passed metadata (B2 codex R1-001). Uses `must not`
|
||||
# as the prohibition expression — exemption is per-regex
|
||||
# so it does not leak to C1's assertion-style obligation
|
||||
# (B2 R2-001).
|
||||
(
|
||||
"C3 output metadata audit-passed prohibition",
|
||||
r"\bOutput metadata must not claim audit-passed state\b",
|
||||
True, # allow_prohibition: `must not` is the obligation
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Inversion sweep checks (Phase 6.7 — spec §6 partial inversion rule)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Spec §6.3 defines INV-1/INV-2/INV-3 as the lint enforcement of the
|
||||
# §6.2 sweep. These run alongside the keyword/regex Check pipeline above
|
||||
# but operate at file-list granularity (manifest-driven) rather than
|
||||
# per-Check, so they are implemented as standalone functions returning
|
||||
# (pattern_id, description, ok, message) tuples.
|
||||
#
|
||||
# - INV-1: each manifest file's PATTERN PROTECTION block carries the
|
||||
# canonical Clause 1 line exactly once (presence + uniqueness).
|
||||
# - INV-2: each manifest file's PATTERN PROTECTION block contains zero
|
||||
# sentences matching any of the four Clause 2 violation patterns.
|
||||
# - INV-3: no agent prompt file outside the manifest carries the canonical
|
||||
# Clause 1 line (defends against accidental sweep widening per §9 L2).
|
||||
|
||||
INVERSION_MANIFEST = REPO_ROOT / "scripts" / "v3_6_7_inversion_manifest.json"
|
||||
|
||||
# Canonical Clause 1 line, byte-aligned with spec §6.2 line 1767. Whitespace
|
||||
# inside is tolerant (collapse whitespace runs to a single space) so a future
|
||||
# Markdown reflow that wraps the bullet across two lines does not break the
|
||||
# match; presence is what matters.
|
||||
CANONICAL_CLAUSE_1_TEXT = (
|
||||
"DO NOT simulate any audit step. "
|
||||
"DO NOT claim to have run codex/external review. "
|
||||
"Output metadata must not claim audit-passed state."
|
||||
)
|
||||
|
||||
# Spec §6.3 INV-2 regex set (a)-(d). Patterns are written as Python regex
|
||||
# literals here (`|` for alternation, no Markdown escaping) — the spec
|
||||
# table renders them as `\|` because raw `|` is the Markdown table-column
|
||||
# delimiter; the lint reads the underlying regex, not the rendered cell.
|
||||
# All four compile with re.IGNORECASE only — DOTALL is not needed because
|
||||
# patterns are applied to bullet text after whitespace normalization
|
||||
# (newlines collapsed to single spaces), which preserves Markdown
|
||||
# soft-wrap tolerance (codex R1 P2 closure) while bounding the `.*`
|
||||
# wildcard to a single bullet (codex R2 P2 closure: prior IGNORECASE |
|
||||
# DOTALL applied to raw block text let INV-2(a)/(b) match across
|
||||
# unrelated bullets).
|
||||
INV2_PATTERNS = [
|
||||
("INV-2(a)", re.compile(r"\bthe orchestrator\b.*\baudit\b", re.IGNORECASE)),
|
||||
(
|
||||
"INV-2(b)",
|
||||
re.compile(
|
||||
r"\bcross-model audit (?:follows|covers)\b.*codex_audit_multifile_template",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
),
|
||||
(
|
||||
"INV-2(c)",
|
||||
re.compile(
|
||||
r"\baudit (?:afterwards?|will be run|is dispatched)\b",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
),
|
||||
(
|
||||
"INV-2(d)",
|
||||
re.compile(
|
||||
r"\bdownstream audit\b|\bthis output (?:is|will be) audited\b",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
# Frozen v3.6.7 manifest contents per spec §6.3. The manifest file at
|
||||
# scripts/v3_6_7_inversion_manifest.json is the data; this constant is
|
||||
# the schema's expected value, used by `_load_inversion_manifest` to
|
||||
# refuse drifted manifests at lint time. Spec §6.3 line 1807 reserves
|
||||
# manifest widening for explicit v3.6.8+ work that lands "its own
|
||||
# version-tagged manifest rather than retroactively widening v3.6.7's";
|
||||
# this constant locks the v3.6.7 scope so a copy-paste widening
|
||||
# (codex R1 P2 closure: add fourth file + canonical bullet → INV-1
|
||||
# passes for all 4, INV-3 skips the new file because it is in
|
||||
# manifest_set, full pass) can no longer slip past lint.
|
||||
EXPECTED_MANIFEST_FILES = (
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
"deep-research/agents/research_architect_agent.md",
|
||||
"deep-research/agents/report_compiler_agent.md",
|
||||
)
|
||||
|
||||
|
||||
def _load_inversion_manifest() -> tuple[list[str], str | None]:
|
||||
"""Return (file_list, error_message). file_list paths are repo-relative.
|
||||
|
||||
Validates that the manifest carries exactly the three v3.6.7-scoped
|
||||
file paths (per spec §6.3 + EXPECTED_MANIFEST_FILES). Drift, addition,
|
||||
deletion, or duplication of entries is rejected — widening to a
|
||||
fourth file requires landing a v3.6.8+ manifest with its own scope
|
||||
tag, not retroactive edits to this manifest (per spec §6.3 line
|
||||
1807).
|
||||
"""
|
||||
if not INVERSION_MANIFEST.exists():
|
||||
return [], f"manifest missing: scripts/v3_6_7_inversion_manifest.json"
|
||||
try:
|
||||
import json
|
||||
data = json.loads(INVERSION_MANIFEST.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
return [], f"manifest unreadable: {exc}"
|
||||
if data.get("scope") != "v3.6.7-only":
|
||||
return [], f"manifest 'scope' must be 'v3.6.7-only', got {data.get('scope')!r}"
|
||||
files = data.get("files")
|
||||
if not isinstance(files, list) or not all(isinstance(p, str) for p in files):
|
||||
return [], "manifest 'files' must be a list of strings"
|
||||
if len(files) != len(set(files)):
|
||||
dupes = sorted({p for p in files if files.count(p) > 1})
|
||||
return [], f"manifest 'files' contains duplicate entries: {dupes}"
|
||||
if set(files) != set(EXPECTED_MANIFEST_FILES):
|
||||
expected = sorted(EXPECTED_MANIFEST_FILES)
|
||||
actual = sorted(files)
|
||||
return [], (
|
||||
f"manifest 'files' must match the v3.6.7 frozen scope. "
|
||||
f"Expected (sorted): {expected}. Got (sorted): {actual}. "
|
||||
f"To widen scope to additional agents, land a v3.6.8+ "
|
||||
f"manifest with its own scope tag per spec §6.3 line 1807; "
|
||||
f"do not edit v3.6.7's manifest retroactively."
|
||||
)
|
||||
return files, None
|
||||
|
||||
|
||||
def _extract_block(text: str, marker: str) -> str | None:
|
||||
"""Return PATTERN PROTECTION block text, or None if marker missing."""
|
||||
pos = text.lower().find(marker.lower())
|
||||
if pos == -1:
|
||||
return None
|
||||
rest = text[pos:]
|
||||
match = _HEADING_RE.search(rest, pos=len(marker))
|
||||
end = match.start() if match else len(rest)
|
||||
return rest[:end]
|
||||
|
||||
|
||||
# Bullet line — Markdown list item starting with `- ` at the start of a
|
||||
# line, optionally indented. Captures the bullet text including any
|
||||
# soft-wrapped continuation lines (lines indented more than the bullet
|
||||
# itself, by Markdown convention typically two spaces). Bullet text ends
|
||||
# at the next bullet, blank line, or block boundary.
|
||||
_BULLET_START_RE = re.compile(r"^(\s*)-\s+", re.MULTILINE)
|
||||
|
||||
|
||||
def _iter_bullets(block: str) -> list[tuple[int, str]]:
|
||||
"""Walk a PATTERN PROTECTION block and yield (line_offset, bullet_text)
|
||||
pairs where bullet_text is the bullet's content (without the leading
|
||||
`- ` marker) with all whitespace runs (including newlines from soft
|
||||
wraps) collapsed to a single space.
|
||||
|
||||
Bullet boundaries: each bullet runs from its `- ` marker to the next
|
||||
bullet, blank line (`\\n\\n`), or end of block. This makes INV-1
|
||||
(canonical-line-as-bullet) and INV-2 (no-Clause-2-disclosure) operate
|
||||
at bullet granularity, which is what the spec §6.3 means by "exactly
|
||||
one bullet whose text matches the canonical Clause 1 line verbatim".
|
||||
|
||||
`line_offset` is the 0-indexed character position of the bullet's
|
||||
`-` marker inside `block` (used to compute file-line diagnostics).
|
||||
"""
|
||||
starts = list(_BULLET_START_RE.finditer(block))
|
||||
bullets: list[tuple[int, str]] = []
|
||||
for i, m in enumerate(starts):
|
||||
bullet_start = m.start()
|
||||
content_start = m.end()
|
||||
if i + 1 < len(starts):
|
||||
bullet_end = starts[i + 1].start()
|
||||
else:
|
||||
bullet_end = len(block)
|
||||
# Trim at the first blank line within the bullet — a blank line
|
||||
# ends the bullet even if no further `- ` appears in the block.
|
||||
candidate = block[content_start:bullet_end]
|
||||
blank = candidate.find("\n\n")
|
||||
if blank >= 0:
|
||||
candidate = candidate[:blank]
|
||||
normalized = " ".join(candidate.split())
|
||||
bullets.append((bullet_start, normalized))
|
||||
return bullets
|
||||
|
||||
|
||||
# Audit-specific fragment markers — load-bearing pieces of the canonical
|
||||
# Clause 1 sentence that uniquely tie a bullet to the v3.6.7 audit
|
||||
# prohibition (not generic anti-fabrication guidance). Any one of these
|
||||
# is sufficient to classify a bullet as Clause 1-like.
|
||||
_CLAUSE_1_AUDIT_FRAGMENTS = (
|
||||
"audit step",
|
||||
"audit-passed state",
|
||||
"codex/external review",
|
||||
)
|
||||
|
||||
# Generic prohibition fragments that, ALONE, are common anti-fabrication
|
||||
# language and DO NOT imply the v3.6.7 audit prohibition (codex R8 P2:
|
||||
# `- Do not simulate data or sources.` is legitimate non-audit
|
||||
# guidance). A bullet carrying one of these must ALSO carry an
|
||||
# audit-specific fragment to be classified as Clause 1-like.
|
||||
_CLAUSE_1_GENERIC_FRAGMENTS = (
|
||||
"do not simulate",
|
||||
"do not claim to have run",
|
||||
)
|
||||
|
||||
|
||||
def _is_clause_1_like(bullet_text: str) -> bool:
|
||||
"""True if bullet_text reads as a (possibly weakened) variant of
|
||||
the canonical Clause 1 line.
|
||||
|
||||
A bullet is flagged Clause 1-like iff it carries at least one
|
||||
audit-specific fragment (audit step / audit-passed state /
|
||||
codex/external review). Generic prohibition fragments (`do not
|
||||
simulate`, `do not claim to have run`) alone are insufficient —
|
||||
they appear in legitimate non-audit anti-fabrication guidance.
|
||||
Codex R8 P2 closure: tighten so non-manifest prompts can carry
|
||||
`- Do not simulate data or sources.` without tripping INV-3."""
|
||||
lowered = bullet_text.lower()
|
||||
return any(frag in lowered for frag in _CLAUSE_1_AUDIT_FRAGMENTS)
|
||||
|
||||
|
||||
def _inv1_check_file(rel_path: str) -> tuple[bool, str]:
|
||||
"""INV-1: canonical Clause 1 line appears as exactly one bullet in
|
||||
the PATTERN PROTECTION block, AND no other Clause 1-like bullet
|
||||
exists. The bullet's whitespace-normalized text MUST equal the
|
||||
canonical Clause 1 text byte-for-byte; prefix weakeners (`When
|
||||
feasible, DO NOT simulate ...`), tail weakeners (`... audit-passed
|
||||
state if feasible.`), or any near-canonical duplicate all fail.
|
||||
Returns (ok, message).
|
||||
|
||||
Codex R2 P2 closure (substring match → bullet-extract + exact
|
||||
compare); R6 P2 closure (weakened-duplicate bypass — keep canonical
|
||||
bullet, add `- When feasible, DO NOT simulate ...` second bullet,
|
||||
exact-count stays 1 and lint passed). The R6 closure flags every
|
||||
Clause 1-like bullet via _CLAUSE_1_LIKE_FRAGMENTS and demands each
|
||||
one is byte-exact canonical."""
|
||||
target = REPO_ROOT / rel_path
|
||||
if not target.exists():
|
||||
return False, f"file missing: {rel_path}"
|
||||
block = _extract_block(target.read_text(encoding="utf-8"), PROTECTION_BLOCK)
|
||||
if block is None:
|
||||
return False, (
|
||||
f"{rel_path}: PATTERN PROTECTION block missing "
|
||||
f"(marker {PROTECTION_BLOCK!r} not found)"
|
||||
)
|
||||
bullets = list(_iter_bullets(block))
|
||||
exact = [t for _o, t in bullets if t == CANONICAL_CLAUSE_1_TEXT]
|
||||
weakened = [
|
||||
t for _o, t in bullets
|
||||
if _is_clause_1_like(t) and t != CANONICAL_CLAUSE_1_TEXT
|
||||
]
|
||||
if weakened:
|
||||
return False, (
|
||||
f"{rel_path}: PATTERN PROTECTION block contains "
|
||||
f"{len(weakened)} Clause 1-like bullet(s) that do not "
|
||||
f"match the canonical text byte-for-byte. Each must equal "
|
||||
f"the canonical wording verbatim or be removed. Offending "
|
||||
f"bullet(s): {weakened!r}. Expected canonical wording: "
|
||||
f"{CANONICAL_CLAUSE_1_TEXT!r}"
|
||||
)
|
||||
if len(exact) != 1:
|
||||
return False, (
|
||||
f"{rel_path}: PATTERN PROTECTION block has {len(exact)} "
|
||||
f"bullet(s) whose normalized text equals the canonical "
|
||||
f"Clause 1 line; expected exactly 1. Expected wording: "
|
||||
f"{CANONICAL_CLAUSE_1_TEXT!r}"
|
||||
)
|
||||
return True, "OK"
|
||||
|
||||
|
||||
def _iter_block_segments(block: str) -> list[tuple[int, str, str]]:
|
||||
"""Yield (offset, kind, normalized_text) tuples for every
|
||||
matched-content segment inside a PATTERN PROTECTION block. `kind`
|
||||
is `"bullet"` for `^- ` list items or `"prose"` for non-bullet
|
||||
paragraphs (intro paragraph, paragraphs interleaved between bullet
|
||||
runs, paragraphs appended after the last bullet — anywhere in the
|
||||
block).
|
||||
|
||||
Whitespace-normalised so soft-wrapped Markdown lines collapse into
|
||||
single-space sentences. Codex R3 + R4 P2 closures: scanning must
|
||||
cover ALL prose in the block, not just the intro paragraph (R3) or
|
||||
just the pre-first-bullet region (R4). The spec §6.2 sweep promises
|
||||
"zero Clause 2 violation in PATTERN PROTECTION block"; carving out
|
||||
any sub-region (post-bullet trailers, between-bullet inserts) gives
|
||||
a regression vector.
|
||||
|
||||
Algorithm: paragraph-split the block on blank lines. Each paragraph
|
||||
starting with `- ` is treated as a bullet group and walked via
|
||||
`_iter_bullets`; each non-bullet paragraph (excluding the section
|
||||
heading) is reported as `"prose"`. This handles bullets-then-prose,
|
||||
prose-then-bullets, and mixed orderings uniformly.
|
||||
"""
|
||||
segments: list[tuple[int, str, str]] = []
|
||||
cursor = 0
|
||||
paragraphs = re.split(r"\n\s*\n", block)
|
||||
for paragraph in paragraphs:
|
||||
offset = block.find(paragraph, cursor)
|
||||
if offset < 0:
|
||||
offset = cursor
|
||||
# Advance the cursor past this paragraph and its trailing
|
||||
# blank-line separator (`\n\n`) for the next find().
|
||||
cursor = offset + len(paragraph) + 2
|
||||
stripped = paragraph.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
# The block heading line (`## PATTERN PROTECTION (v3.6.7)`)
|
||||
# is content marker, not contract content.
|
||||
if stripped.startswith("## "):
|
||||
continue
|
||||
if stripped.startswith("- "):
|
||||
# Bullet group — may be a single bullet or a wrapped multi-
|
||||
# line bullet; delegate to `_iter_bullets` which knows how
|
||||
# to walk `- ` markers and collapse soft wraps.
|
||||
for bullet_local_offset, bullet_text in _iter_bullets(paragraph):
|
||||
segments.append((offset + bullet_local_offset, "bullet", bullet_text))
|
||||
else:
|
||||
normalized = " ".join(stripped.split())
|
||||
segments.append((offset, "prose", normalized))
|
||||
return segments
|
||||
|
||||
|
||||
def _inv2_check_file(rel_path: str) -> tuple[bool, list[str]]:
|
||||
"""INV-2: zero Clause 2 violation hits across the four regex
|
||||
patterns (a)-(d), evaluated per segment (intro prose paragraphs +
|
||||
bullets, both whitespace-normalized). Returns (ok, error_messages).
|
||||
|
||||
Per-segment evaluation closes codex R2 P2 (cross-bullet `.*`
|
||||
over-reach) AND codex R3 P2 (intro-paragraph regression): each
|
||||
pattern runs against a single bullet OR a single prose paragraph,
|
||||
never spanning unrelated text. Soft-wrap tolerance is preserved by
|
||||
pre-normalizing each segment's whitespace."""
|
||||
target = REPO_ROOT / rel_path
|
||||
if not target.exists():
|
||||
return False, [f"file missing: {rel_path}"]
|
||||
full = target.read_text(encoding="utf-8")
|
||||
block = _extract_block(full, PROTECTION_BLOCK)
|
||||
if block is None:
|
||||
return False, [
|
||||
f"{rel_path}: PATTERN PROTECTION block missing "
|
||||
f"(marker {PROTECTION_BLOCK!r} not found)"
|
||||
]
|
||||
errors: list[str] = []
|
||||
block_offset = full.find(block)
|
||||
for segment_offset, kind, normalized in _iter_block_segments(block):
|
||||
# Skip the canonical Clause 1 bullet — it inherently contains
|
||||
# "audit step" / "audit-passed state" tokens but is the
|
||||
# required prohibition, not a disclosure.
|
||||
if kind == "bullet" and normalized == CANONICAL_CLAUSE_1_TEXT:
|
||||
continue
|
||||
for label, pat in INV2_PATTERNS:
|
||||
m = pat.search(normalized)
|
||||
if m is None:
|
||||
continue
|
||||
absolute_pos = (block_offset + segment_offset) if block_offset >= 0 else segment_offset
|
||||
line_no = full.count("\n", 0, absolute_pos) + 1
|
||||
errors.append(
|
||||
f"{rel_path}:{line_no}: {label} Clause 2 violation in {kind}: "
|
||||
f"{normalized!r}. Sentence must be removed per "
|
||||
f"docs/design/2026-04-30-ars-v3.6.7-step-6-orchestrator-hooks-spec.md §6.2."
|
||||
)
|
||||
# One label per segment is enough; further labels on the
|
||||
# same segment would be redundant.
|
||||
break
|
||||
return (len(errors) == 0), errors
|
||||
|
||||
|
||||
# Directories scanned by INV-3. Spec §6.3 limits scope to agent prompt
|
||||
# files under deep-research/agents/ and academic-pipeline/agents/. docs/,
|
||||
# scripts/, and tests/ are excluded — the canonical line legitimately
|
||||
# appears in the spec, the manifest checker, and test fixtures, and
|
||||
# scanning those would self-fail.
|
||||
INV3_SCAN_DIRS = [
|
||||
REPO_ROOT / "deep-research" / "agents",
|
||||
REPO_ROOT / "academic-pipeline" / "agents",
|
||||
]
|
||||
|
||||
|
||||
def _inv3_check(manifest_files: list[str]) -> tuple[bool, list[str]]:
|
||||
"""INV-3: the EXACT canonical Clause 1 line MUST NOT appear in any
|
||||
agent prompt outside the manifest. Detects:
|
||||
(a) Bullets whose whitespace-normalized text equals
|
||||
`CANONICAL_CLAUSE_1_TEXT` byte-for-byte.
|
||||
(b) Prose runs whose whitespace-normalized text equals
|
||||
`CANONICAL_CLAUSE_1_TEXT` byte-for-byte. Prose runs are
|
||||
identified by sliding a 3-sentence window across non-bullet
|
||||
text after stripping `^#` heading lines.
|
||||
Returns (ok, error_messages).
|
||||
|
||||
Codex R9 P2 closures (architectural rewind on R7+R8 over-extension):
|
||||
- The Clause 1-like heuristic is a manifest-internal weakened-
|
||||
duplicate guard (INV-1), NOT a manifest-external detector.
|
||||
Applied to non-manifest agents, it false-positives any bullet
|
||||
mentioning `audit step` for unrelated reasons (e.g. process-
|
||||
flow guidance like `- Review each audit step before
|
||||
finalizing.`). Spec §6.3 INV-3 wording says "canonical Clause 1
|
||||
line found outside the manifest" — that means the actual
|
||||
sentence, not a Clause 1-like variant. Variants outside the
|
||||
manifest are a separate concern that v3.6.7 does not lint.
|
||||
- Prose detection no longer relies on `re.split(r"\\n\\s*\\n", ...)`
|
||||
paragraphs (which mis-handle a heading immediately followed by
|
||||
the canonical sentence with no blank line). Use line-level
|
||||
heading-strip + whitespace-collapse + a 3-sentence sliding
|
||||
window so the canonical sentence is detectable regardless of
|
||||
surrounding Markdown structure.
|
||||
"""
|
||||
manifest_set = {str(REPO_ROOT / p) for p in manifest_files}
|
||||
errors: list[str] = []
|
||||
for d in INV3_SCAN_DIRS:
|
||||
if not d.is_dir():
|
||||
continue
|
||||
for path in sorted(d.glob("*.md")):
|
||||
if str(path) in manifest_set:
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
offending_bullets = [
|
||||
bt
|
||||
for _o, bt in _iter_bullets(text)
|
||||
if bt == CANONICAL_CLAUSE_1_TEXT
|
||||
]
|
||||
# Line-level heading strip + whitespace collapse on the
|
||||
# rest. Heading lines (start with `#`) and bullet lines
|
||||
# (`- `) are excluded; remaining lines are joined with a
|
||||
# single space. The canonical sentence is detected as a
|
||||
# substring with proper sentence boundaries.
|
||||
non_bullet_non_heading = "\n".join(
|
||||
line for line in text.splitlines()
|
||||
if not line.lstrip().startswith("#")
|
||||
and not line.lstrip().startswith("- ")
|
||||
)
|
||||
normalized_prose = " ".join(non_bullet_non_heading.split())
|
||||
offending_prose: list[str] = []
|
||||
if CANONICAL_CLAUSE_1_TEXT in normalized_prose:
|
||||
offending_prose.append(CANONICAL_CLAUSE_1_TEXT)
|
||||
if offending_bullets or offending_prose:
|
||||
rel = path.relative_to(REPO_ROOT)
|
||||
offenders = []
|
||||
for bt in offending_bullets:
|
||||
offenders.append(f"bullet: {bt!r}")
|
||||
for pr in offending_prose:
|
||||
offenders.append(f"prose: {pr!r}")
|
||||
errors.append(
|
||||
f"{rel}: canonical Clause 1 line found outside the "
|
||||
f"v3.6.7 inversion manifest. If this is intentional "
|
||||
f"widening, land a v3.6.8+ scope-tagged manifest per "
|
||||
f"spec §6.3 line 1807 and open the §9 L2 question; "
|
||||
f"do not retroactively widen v3.6.7's manifest. "
|
||||
f"Offender(s): {offenders!r}"
|
||||
)
|
||||
return (len(errors) == 0), errors
|
||||
|
||||
|
||||
def inversion_sweep_results() -> list[tuple[str, str, bool, str]]:
|
||||
"""Run INV-1/INV-2/INV-3. Returns list of
|
||||
(pattern_id, description, ok, message) tuples — one row per check ID,
|
||||
aggregated across files for INV-2 and INV-3."""
|
||||
results: list[tuple[str, str, bool, str]] = []
|
||||
files, err = _load_inversion_manifest()
|
||||
if err is not None:
|
||||
results.append(("INV-manifest", "v3.6.7 inversion manifest readable", False, err))
|
||||
return results
|
||||
|
||||
# INV-1: per-file presence/uniqueness check, aggregated.
|
||||
inv1_errors: list[str] = []
|
||||
for f in files:
|
||||
ok, msg = _inv1_check_file(f)
|
||||
if not ok:
|
||||
inv1_errors.append(msg)
|
||||
results.append((
|
||||
"INV-1",
|
||||
f"canonical Clause 1 line present exactly once in each of {len(files)} manifest file(s)",
|
||||
len(inv1_errors) == 0,
|
||||
"OK" if not inv1_errors else "; ".join(inv1_errors),
|
||||
))
|
||||
|
||||
# INV-2: aggregate Clause 2 violation hits across all manifest files.
|
||||
inv2_errors: list[str] = []
|
||||
for f in files:
|
||||
ok, errs = _inv2_check_file(f)
|
||||
if not ok:
|
||||
inv2_errors.extend(errs)
|
||||
results.append((
|
||||
"INV-2",
|
||||
"no Clause 2 disclosure phrases (a)-(d) inside PATTERN PROTECTION blocks",
|
||||
len(inv2_errors) == 0,
|
||||
"OK" if not inv2_errors else "; ".join(inv2_errors),
|
||||
))
|
||||
|
||||
# INV-3: canonical line restricted to manifest files.
|
||||
ok, errs = _inv3_check(files)
|
||||
results.append((
|
||||
"INV-3",
|
||||
f"canonical Clause 1 line confined to {len(files)} manifest file(s)",
|
||||
ok,
|
||||
"OK" if ok else "; ".join(errs),
|
||||
))
|
||||
return results
|
||||
|
||||
|
||||
# Environment variable controlling whether agent-prompt checks run.
|
||||
#
|
||||
# Spec §9 ships v3.6.7 across multiple steps. Step 1 (this PR) shipped the
|
||||
@@ -806,21 +1298,33 @@ def all_checks() -> list[Check]:
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
checks = all_checks()
|
||||
passed: list[Check] = []
|
||||
failed: list[tuple[Check, str]] = []
|
||||
passed: list[tuple[str, str]] = []
|
||||
failed: list[tuple[str, str, str]] = []
|
||||
|
||||
for check in checks:
|
||||
ok, msg = check.run()
|
||||
entry = (check.pattern_id, check.description)
|
||||
if ok:
|
||||
passed.append(check)
|
||||
passed.append(entry)
|
||||
else:
|
||||
failed.append((check, msg))
|
||||
failed.append((check.pattern_id, check.description, msg))
|
||||
|
||||
inv_results: list[tuple[str, str, bool, str]] = []
|
||||
if _agent_checks_enabled():
|
||||
inv_results = inversion_sweep_results()
|
||||
for pid, desc, ok, msg in inv_results:
|
||||
entry = (pid, desc)
|
||||
if ok:
|
||||
passed.append(entry)
|
||||
else:
|
||||
failed.append((pid, desc, msg))
|
||||
|
||||
total = len(checks) + len(inv_results)
|
||||
deferred_note = ""
|
||||
if not _agent_checks_enabled():
|
||||
deferred_note = " (agent-prompt checks skipped — ARS_V3_6_7_AGENT_CHECKS=0)"
|
||||
summary = (
|
||||
f"v3.6.7 pattern-protection static audit: {len(passed)}/{len(checks)} "
|
||||
f"v3.6.7 pattern-protection static audit: {len(passed)}/{total} "
|
||||
f"checks passed{deferred_note}"
|
||||
)
|
||||
print(summary)
|
||||
@@ -828,8 +1332,8 @@ def main(argv: list[str]) -> int:
|
||||
|
||||
if passed:
|
||||
print("PASS:")
|
||||
for c in passed:
|
||||
print(f" [{c.pattern_id}] {c.description}")
|
||||
for pid, desc in passed:
|
||||
print(f" [{pid}] {desc}")
|
||||
print()
|
||||
|
||||
if failed:
|
||||
@@ -837,8 +1341,8 @@ def main(argv: list[str]) -> int:
|
||||
# channel (matching scripts/check_corpus_consumer_protocol.py) surface
|
||||
# the diagnostics correctly.
|
||||
print("FAIL:", file=sys.stderr)
|
||||
for c, msg in failed:
|
||||
print(f" [{c.pattern_id}] {c.description}", file=sys.stderr)
|
||||
for pid, desc, msg in failed:
|
||||
print(f" [{pid}] {desc}", file=sys.stderr)
|
||||
print(f" → {msg}", file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
print(
|
||||
@@ -849,6 +1353,10 @@ def main(argv: list[str]) -> int:
|
||||
" docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
" docs/design/2026-04-30-ars-v3.6.7-step-6-orchestrator-hooks-spec.md (INV-1/2/3)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
@@ -105,10 +105,15 @@ class R2MutationTests(_MutationTestBase):
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_c3_audit_passed_sentence_deleted_fails(self) -> None:
|
||||
# Phase 6.7 merged line 178 into the canonical Clause 1 line
|
||||
# bullet. The "audit-passed state" segment is now part of a single
|
||||
# whole-line bullet, so the mutation deletes that segment from
|
||||
# within the canonical line and asserts the now-incomplete bullet
|
||||
# fails the C3 + INV-1 regex pair.
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/report_compiler_agent.md",
|
||||
"\n- Output metadata must not claim audit-passed state.",
|
||||
" Output metadata must not claim audit-passed state.",
|
||||
"",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
@@ -127,15 +132,6 @@ class R3MutationTests(_MutationTestBase):
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_r3_001_orchestrator_does_not_run_fails(self) -> None:
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/report_compiler_agent.md",
|
||||
"The orchestrator runs codex audit afterward.",
|
||||
"The orchestrator does not run codex audit afterward.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_r3_002_a2_pending_verification_optional_fails(self) -> None:
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
@@ -365,5 +361,600 @@ class R6MutationTests(_MutationTestBase):
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class INV1MutationTests(_MutationTestBase):
|
||||
"""INV-1 (Phase 6.7 §6.3): canonical Clause 1 line MUST appear exactly
|
||||
once in each manifest file's PATTERN PROTECTION block. Deleting the
|
||||
line, duplicating it, or replacing it with a near-miss MUST fail lint."""
|
||||
|
||||
CANONICAL_BULLET = (
|
||||
"- DO NOT simulate any audit step. DO NOT claim to have run "
|
||||
"codex/external review. Output metadata must not claim audit-passed "
|
||||
"state."
|
||||
)
|
||||
|
||||
def test_inv1_synthesis_canonical_line_deleted_fails(self) -> None:
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
"\n" + self.CANONICAL_BULLET,
|
||||
"",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv1_architect_canonical_line_deleted_fails(self) -> None:
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/research_architect_agent.md",
|
||||
"\n" + self.CANONICAL_BULLET,
|
||||
"",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv1_compiler_canonical_line_deleted_fails(self) -> None:
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/report_compiler_agent.md",
|
||||
"\n" + self.CANONICAL_BULLET,
|
||||
"",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv1_synthesis_canonical_line_duplicated_fails(self) -> None:
|
||||
# Duplicating the bullet trips INV-1 uniqueness (`hits == 1`)
|
||||
# without breaking the per-file C-style positive regex.
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
self.CANONICAL_BULLET,
|
||||
self.CANONICAL_BULLET + "\n" + self.CANONICAL_BULLET,
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class INV2MutationTests(_MutationTestBase):
|
||||
"""INV-2 (Phase 6.7 §6.3): four Clause 2 disclosure regex patterns
|
||||
(a)-(d) MUST not match anything inside a PATTERN PROTECTION block.
|
||||
Each test injects one canonical violation phrase as a new bullet
|
||||
above the canonical Clause 1 line and asserts lint fails."""
|
||||
|
||||
CANONICAL_BULLET = INV1MutationTests.CANONICAL_BULLET
|
||||
|
||||
def _inject_violation(self, file_rel: str, violation: str) -> None:
|
||||
"""Insert a violation bullet immediately above the canonical line."""
|
||||
new_block = f"- {violation}\n{self.CANONICAL_BULLET}"
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
file_rel,
|
||||
self.CANONICAL_BULLET,
|
||||
new_block,
|
||||
)
|
||||
|
||||
def test_inv2_a_orchestrator_audit_fails(self) -> None:
|
||||
# Pattern (a): \bthe orchestrator\b.*\baudit\b
|
||||
self._inject_violation(
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
"The orchestrator runs codex audit afterward.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv2_b_cross_model_audit_template_fails(self) -> None:
|
||||
# Pattern (b): \bcross-model audit (?:follows|covers)\b.*codex_audit_multifile_template
|
||||
self._inject_violation(
|
||||
"deep-research/agents/research_architect_agent.md",
|
||||
"Cross-model audit covers these via dimension §3.5 of "
|
||||
"`shared/templates/codex_audit_multifile_template.md`.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv2_c_audit_will_be_run_fails(self) -> None:
|
||||
# Pattern (c): \baudit (?:afterwards?|will be run|is dispatched)\b
|
||||
self._inject_violation(
|
||||
"deep-research/agents/report_compiler_agent.md",
|
||||
"An audit will be run on the deliverable.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv2_d_downstream_audit_fails(self) -> None:
|
||||
# Pattern (d): \bdownstream audit\b
|
||||
self._inject_violation(
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
"A downstream audit covers narrative claims.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv2_d_this_output_will_be_audited_fails(self) -> None:
|
||||
# Pattern (d) second alternative: \bthis output (?:is|will be) audited\b
|
||||
self._inject_violation(
|
||||
"deep-research/agents/research_architect_agent.md",
|
||||
"This output will be audited by codex downstream.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class INV3MutationTests(_MutationTestBase):
|
||||
"""INV-3 (Phase 6.7 §6.3): canonical Clause 1 line MUST NOT appear in
|
||||
any agent prompt outside the v3.6.7 inversion manifest. Adding the
|
||||
line to a non-manifest agent prompt OR shrinking the manifest below
|
||||
the three v3.6.7 downstream agents MUST fail lint."""
|
||||
|
||||
CANONICAL_BULLET = INV1MutationTests.CANONICAL_BULLET
|
||||
|
||||
def test_inv3_canonical_in_non_manifest_agent_fails(self) -> None:
|
||||
# Add the canonical line to bibliography_agent (which is NOT in
|
||||
# the v3.6.7 inversion manifest). The §6 sweep is v3.6.7-only;
|
||||
# widening to a fourth file is the §9 L2 deferred question and
|
||||
# MUST fail lint as a guard against accidental sweep widening.
|
||||
non_manifest_path = self._repo_dir / "deep-research/agents/bibliography_agent.md"
|
||||
# Sanity-check the file exists and isn't already in the manifest.
|
||||
self.assertTrue(non_manifest_path.exists())
|
||||
original = non_manifest_path.read_text(encoding="utf-8")
|
||||
non_manifest_path.write_text(
|
||||
original + "\n\n" + self.CANONICAL_BULLET + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv3_manifest_shrunk_to_two_files_fails(self) -> None:
|
||||
# Drop one entry from the manifest. The dropped file's canonical
|
||||
# line is now "outside" the manifest from INV-3's perspective and
|
||||
# must be flagged.
|
||||
manifest_path = self._repo_dir / "scripts/v3_6_7_inversion_manifest.json"
|
||||
import json
|
||||
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(len(data["files"]), 3)
|
||||
data["files"] = data["files"][:2]
|
||||
manifest_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_inv3_manifest_extra_nonexistent_entry_fails(self) -> None:
|
||||
# Adding a fourth file to the manifest that does not carry the
|
||||
# canonical line must fail INV-1 (the manifest claims a file that
|
||||
# is not actually swept). This guards against drift where someone
|
||||
# widens the manifest without doing the corresponding prompt edit.
|
||||
manifest_path = self._repo_dir / "scripts/v3_6_7_inversion_manifest.json"
|
||||
import json
|
||||
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
data["files"].append("deep-research/agents/bibliography_agent.md")
|
||||
manifest_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class CodexR1MutationTests(_MutationTestBase):
|
||||
"""Codex R1 (Phase 6.7 review) closures: 3 P2 findings against the
|
||||
initial INV-1/INV-2/INV-3 implementation. Each test re-introduces the
|
||||
bypass codex demonstrated and asserts lint now fails."""
|
||||
|
||||
CANONICAL_BULLET = INV1MutationTests.CANONICAL_BULLET
|
||||
|
||||
def test_r1_p2_inv2_wrapped_disclosure_fails(self) -> None:
|
||||
# Codex R1 P2: INV-2 patterns compiled without re.DOTALL allow a
|
||||
# forbidden Clause 2 sentence to be Markdown-soft-wrapped across
|
||||
# newlines and slip past the regex. Inject a wrapped (a) violation
|
||||
# ("the orchestrator" on one line, "audit" on the next) and
|
||||
# assert lint fails. Phase 6.7 R1 closure compiles INV-2 with
|
||||
# IGNORECASE | DOTALL so `.` crosses newlines.
|
||||
wrapped_violation = (
|
||||
"- The orchestrator dispatches against the\n"
|
||||
" template and codex audit covers each deliverable."
|
||||
)
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
self.CANONICAL_BULLET,
|
||||
wrapped_violation + "\n" + self.CANONICAL_BULLET,
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_r1_p2_inv1_canonical_with_tail_weakener_fails(self) -> None:
|
||||
# Codex R1 P2: the prior CANONICAL_CLAUSE_1_RE stopped at
|
||||
# `state\b` so a manifest prompt could keep the canonical
|
||||
# sentence yet append ` if feasible.` and INV-1 still passed.
|
||||
# Phase 6.7 R1 closure anchors the regex to `state\.\s*$` with
|
||||
# re.MULTILINE so any tail content on the same bullet line
|
||||
# breaks the match. Both INV-1 and the C3 regex must reject this
|
||||
# mutation (defense in depth — the C3 negation post-filter also
|
||||
# catches `if feasible` via _ALWAYS_NEGATION_PATTERNS).
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/report_compiler_agent.md",
|
||||
"Output metadata must not claim audit-passed state.",
|
||||
"Output metadata must not claim audit-passed state if feasible.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_r1_p2_manifest_widening_with_canonical_copy_fails(self) -> None:
|
||||
# Codex R1 P2: a copy-paste widening attack — add a fourth file
|
||||
# to the manifest AND copy the canonical bullet into that file's
|
||||
# PATTERN PROTECTION block. Pre-fix INV-1 passed for all four
|
||||
# (canonical line present everywhere) and INV-3 silently skipped
|
||||
# the new file because it was now in `manifest_set`. Phase 6.7
|
||||
# R1 closure validates manifest['files'] against the frozen
|
||||
# EXPECTED_MANIFEST_FILES set (exact match, no superset / no
|
||||
# drift); widening triggers L2 resolution per spec §6.3 line
|
||||
# 1807, which requires landing a v3.6.8+ manifest, not editing
|
||||
# the v3.6.7 one. Construct the attack and assert lint fails.
|
||||
bibliography = self._repo_dir / "deep-research/agents/bibliography_agent.md"
|
||||
original = bibliography.read_text(encoding="utf-8")
|
||||
bibliography.write_text(
|
||||
original
|
||||
+ "\n\n## PATTERN PROTECTION (v3.6.7)\n\n"
|
||||
+ self.CANONICAL_BULLET
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
manifest_path = self._repo_dir / "scripts/v3_6_7_inversion_manifest.json"
|
||||
import json
|
||||
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
data["files"].append("deep-research/agents/bibliography_agent.md")
|
||||
manifest_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_r1_p2_manifest_duplicate_entry_fails(self) -> None:
|
||||
# Defense in depth on the same closure: a duplicate file path in
|
||||
# `files` should also fail (per uniqueness check added in the
|
||||
# R1 fix), even if the path is otherwise valid.
|
||||
manifest_path = self._repo_dir / "scripts/v3_6_7_inversion_manifest.json"
|
||||
import json
|
||||
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
data["files"].append(data["files"][0]) # duplicate first entry
|
||||
manifest_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class CodexR2MutationTests(_MutationTestBase):
|
||||
"""Codex R2 (Phase 6.7 review, after R1 fixes) closures: 2 P2
|
||||
findings against the bullet-text exactness contract and INV-2's
|
||||
cross-bullet `.*` over-reach."""
|
||||
|
||||
CANONICAL_BULLET = INV1MutationTests.CANONICAL_BULLET
|
||||
|
||||
def test_r2_p2_inv1_bullet_prefix_weakener_fails(self) -> None:
|
||||
# Codex R2 P2: a bullet of the form `- When feasible, DO NOT
|
||||
# simulate ...` carries the canonical sentences as a substring
|
||||
# but is NOT a verbatim canonical bullet. The pre-fix INV-1
|
||||
# regex search-anywhere accepted this; the bullet-extraction +
|
||||
# exact-match contract rejects it.
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/report_compiler_agent.md",
|
||||
self.CANONICAL_BULLET,
|
||||
"- When feasible, DO NOT simulate any audit step. DO NOT "
|
||||
"claim to have run codex/external review. Output metadata "
|
||||
"must not claim audit-passed state.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_r2_p2_inv1_softwrap_canonical_passes(self) -> None:
|
||||
# Codex R2 P2 dual: a soft-wrapped canonical bullet (line break
|
||||
# between `run` and `codex/external`) MUST still pass INV-1.
|
||||
# This is the false-negative side of the regex anchor problem
|
||||
# that the bullet-extraction + whitespace-normalization fix
|
||||
# closes. Asserts that this specific Markdown reflow does not
|
||||
# break baseline lint.
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
"DO NOT claim to have run codex/external review.",
|
||||
"DO NOT claim to have run\n codex/external review.",
|
||||
)
|
||||
# Mutation should leave lint passing — the soft-wrap is benign.
|
||||
rc, _stdout, stderr = _run_lint(self._repo_dir)
|
||||
self.assertEqual(
|
||||
rc,
|
||||
0,
|
||||
f"soft-wrapped canonical bullet should still pass; stderr={stderr}",
|
||||
)
|
||||
|
||||
def test_r2_p2_inv2_cross_bullet_no_false_positive(self) -> None:
|
||||
# Codex R2 P2: prior INV-2(a) `\bthe orchestrator\b.*\baudit\b`
|
||||
# with re.DOTALL applied to raw block text would match across
|
||||
# bullets — a benign `the orchestrator` mention in one bullet
|
||||
# plus the canonical bullet's `audit step` mention would
|
||||
# false-positive even though no single bullet expresses the
|
||||
# forbidden disclosure. Insert a benign orchestrator mention as
|
||||
# a NEW bullet near the canonical bullet and assert lint still
|
||||
# passes (no false positive).
|
||||
benign_bullet = (
|
||||
"- The orchestrator may supply the dispatch context for this "
|
||||
"block when running this agent."
|
||||
)
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
self.CANONICAL_BULLET,
|
||||
benign_bullet + "\n" + self.CANONICAL_BULLET,
|
||||
)
|
||||
rc, _stdout, stderr = _run_lint(self._repo_dir)
|
||||
self.assertEqual(
|
||||
rc,
|
||||
0,
|
||||
f"benign cross-bullet 'orchestrator' mention should not "
|
||||
f"false-positive INV-2; stderr={stderr}",
|
||||
)
|
||||
|
||||
def test_r2_p2_inv2_in_bullet_violation_still_fails(self) -> None:
|
||||
# Defense in depth on R2-002: the per-bullet INV-2 must STILL
|
||||
# catch a real violation that lives inside a single bullet.
|
||||
violation = "- The orchestrator runs codex audit afterward on the deliverable."
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/research_architect_agent.md",
|
||||
self.CANONICAL_BULLET,
|
||||
violation + "\n" + self.CANONICAL_BULLET,
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class CodexR3MutationTests(_MutationTestBase):
|
||||
"""Codex R3 (Phase 6.7 review, after R2 fixes) closure: 1 P2
|
||||
finding restoring INV-2 coverage of intro-paragraph prose, not
|
||||
just bullets."""
|
||||
|
||||
CANONICAL_BULLET = INV1MutationTests.CANONICAL_BULLET
|
||||
|
||||
def test_r3_p2_inv2_intro_paragraph_disclosure_fails(self) -> None:
|
||||
# Codex R3 P2: the Phase 6.7 sweep removed Clause 2 disclosure
|
||||
# sentences that lived in the intro paragraph of each PATTERN
|
||||
# PROTECTION block (e.g. "Cross-model audit follows ...
|
||||
# codex_audit_multifile_template.md ..." in synthesis_agent.md
|
||||
# line 164). After R2's per-bullet refactor, INV-2 only
|
||||
# iterated bullets and the original disclosure could be
|
||||
# silently re-added to the intro paragraph. Phase 6.7 R3
|
||||
# closure adds prose-paragraph segments to INV-2's iteration
|
||||
# via `_iter_block_segments`. Re-introduce the original
|
||||
# `Cross-model audit follows ...` sentence into synthesis's
|
||||
# intro paragraph and assert lint catches it as a prose
|
||||
# violation.
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
"documented in `docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md` §3.1 (A1–A5).",
|
||||
"documented in `docs/design/2026-04-29-ars-v3.6.7-downstream-agent-pattern-protection-spec.md` §3.1 (A1–A5). "
|
||||
"Cross-model audit follows `shared/templates/codex_audit_multifile_template.md` audit dimensions §3.1, §3.2, §3.3, §3.4.",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class CodexR4MutationTests(_MutationTestBase):
|
||||
"""Codex R4 (Phase 6.7 review, after R3 fixes) closure: 1 P2
|
||||
finding restoring INV-2 coverage of post-bullet prose."""
|
||||
|
||||
CANONICAL_BULLET = INV1MutationTests.CANONICAL_BULLET
|
||||
|
||||
def test_r4_p2_inv2_post_bullet_prose_disclosure_fails(self) -> None:
|
||||
# Codex R4 P2: R3's `_iter_block_segments` only iterated prose
|
||||
# paragraphs in `block[:first_bullet]` — pre-first-bullet
|
||||
# only. A Clause 2 disclosure appended as a paragraph AFTER
|
||||
# the canonical bullet (or anywhere after the bullet list)
|
||||
# was outside the segmentation window and silently passed
|
||||
# lint. R4 closure rewrites segmentation to paragraph-split
|
||||
# the entire block, distinguishing bullet groups (`- ...`
|
||||
# paragraphs) from prose paragraphs uniformly. Append a
|
||||
# forbidden disclosure as a trailing prose paragraph and
|
||||
# assert lint catches it.
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
"Output metadata must not claim audit-passed state.\n",
|
||||
"Output metadata must not claim audit-passed state.\n"
|
||||
"\nThe orchestrator runs codex audit afterward.\n",
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class CodexR5MutationTests(_MutationTestBase):
|
||||
"""Codex R5 (Phase 6.7 review, after R4 fixes) closure: 1 P2
|
||||
finding — C3 regex was not whitespace-tolerant inside sentences,
|
||||
causing CI to fail on harmless Markdown soft-wraps that INV-1
|
||||
correctly accepts."""
|
||||
|
||||
def test_r5_p2_compiler_softwrap_canonical_passes(self) -> None:
|
||||
# Codex R5 P2: C3 regex used literal spaces inside sentences,
|
||||
# so a soft-wrap between `run` and `codex/external` (the same
|
||||
# benign Markdown reflow R2's INV-1 test verifies for synthesis)
|
||||
# caused CI to fail on report_compiler. Phase 6.7 R5 closure
|
||||
# rewrites every inter-token space inside the canonical regex
|
||||
# to `\s+` so soft-wrap tolerance is uniform across INV-1 and
|
||||
# the C3 Check regex. Mutation should leave lint passing.
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/report_compiler_agent.md",
|
||||
"DO NOT claim to have run codex/external review.",
|
||||
"DO NOT claim to have run\n codex/external review.",
|
||||
)
|
||||
rc, _stdout, stderr = _run_lint(self._repo_dir)
|
||||
self.assertEqual(
|
||||
rc,
|
||||
0,
|
||||
f"compiler soft-wrap canonical bullet should pass; stderr={stderr}",
|
||||
)
|
||||
|
||||
|
||||
class CodexR6MutationTests(_MutationTestBase):
|
||||
"""Codex R6 (Phase 6.7 review, after R5 fixes) closure: 1 P2
|
||||
finding — INV-1 only counted exact canonical matches and ignored
|
||||
weakened near-duplicates that preserved an exact canonical bullet
|
||||
while adding a confusing variant alongside."""
|
||||
|
||||
CANONICAL_BULLET = INV1MutationTests.CANONICAL_BULLET
|
||||
|
||||
def test_r6_p2_inv1_weakened_duplicate_alongside_canonical_fails(self) -> None:
|
||||
# Codex R6 P2: an attacker keeps the exact canonical bullet
|
||||
# AND adds a second, weakened variant
|
||||
# (`- When feasible, DO NOT simulate ...`). Pre-fix INV-1 used
|
||||
# `count(exact) == 1` which stayed at 1; lint passed and the
|
||||
# block was left in a state where the agent reads two
|
||||
# contradictory bullets. Phase 6.7 R6 closure introduces
|
||||
# `_is_clause_1_like` that flags any bullet carrying canonical
|
||||
# fragment markers (`do not simulate`, `do not claim to have
|
||||
# run`, `audit-passed state`); each flagged bullet must equal
|
||||
# the canonical text byte-for-byte or lint fails.
|
||||
weakened = (
|
||||
"- When feasible, DO NOT simulate any audit step. DO NOT "
|
||||
"claim to have run codex/external review. Output metadata "
|
||||
"must not claim audit-passed state."
|
||||
)
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
self.CANONICAL_BULLET,
|
||||
self.CANONICAL_BULLET + "\n" + weakened,
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
def test_r6_p2_inv1_tail_weakened_duplicate_fails(self) -> None:
|
||||
# Defense in depth on R6: tail-weakener variant of the same
|
||||
# attack (`... audit-passed state if feasible.`) — preserves
|
||||
# canonical, adds a near-duplicate with a tail weakener.
|
||||
weakened = (
|
||||
"- DO NOT simulate any audit step. DO NOT claim to have "
|
||||
"run codex/external review. Output metadata must not "
|
||||
"claim audit-passed state if feasible."
|
||||
)
|
||||
_mutate(
|
||||
self._repo_dir,
|
||||
"deep-research/agents/research_architect_agent.md",
|
||||
self.CANONICAL_BULLET,
|
||||
self.CANONICAL_BULLET + "\n" + weakened,
|
||||
)
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class CodexR7R9MutationTests(_MutationTestBase):
|
||||
"""Codex R7 → R9 architectural rewind: R7 originally extended INV-3
|
||||
with `_is_clause_1_like` to catch weakened variants outside the
|
||||
manifest. R9 surfaced that this over-extends: spec §6.3 INV-3
|
||||
detects "the canonical Clause 1 line found outside the manifest"
|
||||
(the actual sentence), not Clause 1-like variants. v3.6.7 does not
|
||||
lint variants outside the manifest. Test updated to reflect the
|
||||
correct contract: a weakened variant in a non-manifest agent
|
||||
passes lint."""
|
||||
|
||||
def test_r9_inv3_weakened_clause_1_in_non_manifest_passes(self) -> None:
|
||||
# Codex R9 P2 architectural rewind: a weakened canonical
|
||||
# variant outside the manifest is NOT a v3.6.7 INV-3 violation.
|
||||
# Only the exact canonical sentence (whitespace-normalized
|
||||
# equal to CANONICAL_CLAUSE_1_TEXT) widens the scope. Variants
|
||||
# are a v3.6.8+ concern if/when L2 is reopened.
|
||||
bibliography = self._repo_dir / "deep-research/agents/bibliography_agent.md"
|
||||
original = bibliography.read_text(encoding="utf-8")
|
||||
weakened = (
|
||||
"\n\n- When feasible, DO NOT simulate any audit step. DO NOT "
|
||||
"claim to have run codex/external review. Output metadata "
|
||||
"must not claim audit-passed state.\n"
|
||||
)
|
||||
bibliography.write_text(original + weakened, encoding="utf-8")
|
||||
rc, _stdout, stderr = _run_lint(self._repo_dir)
|
||||
self.assertEqual(
|
||||
rc,
|
||||
0,
|
||||
f"weakened Clause 1 variant outside manifest is not a "
|
||||
f"v3.6.7 INV-3 violation per R9 architectural rewind; "
|
||||
f"stderr={stderr}",
|
||||
)
|
||||
|
||||
|
||||
class CodexR8MutationTests(_MutationTestBase):
|
||||
"""Codex R8 (Phase 6.7 review, after R7 fixes) closures: 2 P2
|
||||
findings — `_is_clause_1_like` was over-broad (rejected legitimate
|
||||
anti-fabrication guidance) AND INV-3 missed prose-form Clause 1
|
||||
copies."""
|
||||
|
||||
CANONICAL_BULLET = INV1MutationTests.CANONICAL_BULLET
|
||||
|
||||
def test_r8_p2_generic_do_not_simulate_in_non_manifest_passes(self) -> None:
|
||||
# Codex R8 P2 (a): the prior `_is_clause_1_like` flagged any
|
||||
# bullet containing `do not simulate`, which is common
|
||||
# anti-fabrication language. A legitimate non-manifest bullet
|
||||
# like `- Do not simulate data or sources.` would
|
||||
# false-positive INV-3 even though it has no audit-prohibition
|
||||
# semantics. Phase 6.7 R8 closure narrows the heuristic to
|
||||
# require an audit-specific fragment (`audit step`,
|
||||
# `audit-passed state`, or `codex/external review`). Inject
|
||||
# the generic bullet into bibliography_agent and assert lint
|
||||
# passes (no false positive).
|
||||
bibliography = self._repo_dir / "deep-research/agents/bibliography_agent.md"
|
||||
original = bibliography.read_text(encoding="utf-8")
|
||||
bibliography.write_text(
|
||||
original + "\n\n- Do not simulate data or sources.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
rc, _stdout, stderr = _run_lint(self._repo_dir)
|
||||
self.assertEqual(
|
||||
rc,
|
||||
0,
|
||||
f"generic anti-fabrication bullet should not false-positive "
|
||||
f"INV-3 after R8 audit-specific tightening; stderr={stderr}",
|
||||
)
|
||||
|
||||
def test_r8_p2_inv3_canonical_as_prose_in_non_manifest_fails(self) -> None:
|
||||
# Codex R8 P2 (b): the prior INV-3 only walked Markdown bullets.
|
||||
# A non-manifest agent prompt that pasted the exact canonical
|
||||
# sentence as a normal paragraph (not a `- ` bullet) silently
|
||||
# passed lint, even though the v3.6.7 prohibition was now
|
||||
# widened beyond the frozen manifest. Phase 6.7 R8 closure adds
|
||||
# a paragraph-level scan that compares whitespace-normalized
|
||||
# prose paragraphs against `CANONICAL_CLAUSE_1_TEXT` exactly.
|
||||
# Inject the canonical sentence as a prose paragraph in
|
||||
# bibliography_agent and assert lint fails.
|
||||
bibliography = self._repo_dir / "deep-research/agents/bibliography_agent.md"
|
||||
original = bibliography.read_text(encoding="utf-8")
|
||||
canon_prose = (
|
||||
"\n\nDO NOT simulate any audit step. DO NOT claim to have "
|
||||
"run codex/external review. Output metadata must not claim "
|
||||
"audit-passed state.\n"
|
||||
)
|
||||
bibliography.write_text(original + canon_prose, encoding="utf-8")
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
class CodexR9MutationTests(_MutationTestBase):
|
||||
"""Codex R9 (Phase 6.7 review, after R8 fixes) architectural
|
||||
rewind: R7+R8 had over-extended INV-3 with the
|
||||
Clause 1-like heuristic (false-positives on legitimate audit-step
|
||||
mentions in non-manifest agents) AND under-extended prose scan
|
||||
(missed canonical-after-heading-no-blank-line). R9 closes both."""
|
||||
|
||||
def test_r9_p2_inv3_audit_fragment_unrelated_bullet_passes(self) -> None:
|
||||
# Codex R9 P2 (a): a non-manifest agent prompt may legitimately
|
||||
# discuss audit steps in process-flow guidance, e.g.
|
||||
# `- Review each audit step before finalizing.`. R8's
|
||||
# heuristic flagged this as Clause 1-like even though the
|
||||
# bullet has no prohibition semantics. R9 closure scopes INV-3
|
||||
# to exact canonical sentence only.
|
||||
bibliography = self._repo_dir / "deep-research/agents/bibliography_agent.md"
|
||||
original = bibliography.read_text(encoding="utf-8")
|
||||
bibliography.write_text(
|
||||
original + "\n\n- Review each audit step before finalizing.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
rc, _stdout, stderr = _run_lint(self._repo_dir)
|
||||
self.assertEqual(
|
||||
rc,
|
||||
0,
|
||||
f"unrelated audit-step bullet should not trip INV-3 after "
|
||||
f"R9 architectural rewind; stderr={stderr}",
|
||||
)
|
||||
|
||||
def test_r9_p2_inv3_canonical_after_heading_no_blank_line_fails(self) -> None:
|
||||
# Codex R9 P2 (b): when the canonical sentence is pasted as
|
||||
# prose immediately after a Markdown heading with no blank
|
||||
# line, R8's `re.split(r"\n\s*\n", text)` kept the heading and
|
||||
# sentence in one paragraph, and the `startswith("## ")`
|
||||
# filter skipped the whole thing. R9 closure switches to
|
||||
# line-level heading-strip + whitespace-collapse so heading
|
||||
# adjacency does not bypass the prose scan.
|
||||
bibliography = self._repo_dir / "deep-research/agents/bibliography_agent.md"
|
||||
original = bibliography.read_text(encoding="utf-8")
|
||||
injection = (
|
||||
"\n\n## PATTERN PROTECTION (v3.6.7)\n"
|
||||
"DO NOT simulate any audit step. DO NOT claim to have run "
|
||||
"codex/external review. Output metadata must not claim "
|
||||
"audit-passed state.\n"
|
||||
)
|
||||
bibliography.write_text(original + injection, encoding="utf-8")
|
||||
self.assert_mutation_fails()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"scope": "v3.6.7-only",
|
||||
"rationale_doc": "docs/design/2026-04-30-ars-v3.6.7-step-6-orchestrator-hooks-spec.md#6-partial-inversion-rule-downstream-agent-prompt-edits",
|
||||
"files": [
|
||||
"deep-research/agents/synthesis_agent.md",
|
||||
"deep-research/agents/research_architect_agent.md",
|
||||
"deep-research/agents/report_compiler_agent.md"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user