mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
70c8678730
* docs(#108): implementation spec — resolves §4.4 11 open concerns
Implementation spec parented to Decision Doc 20ed72d. Closes Decision Doc
§4.4 contract gaps with user-decision protocol:
- #1 track-selection → (b) explicit slr_lineage input
- #5 Nature image → hybrid (annotation block + suggested inline patches)
- #6 UNCERTAIN per-facet → (b) USED-full + UNCERTAIN-annotation
- #10 ai_used:true gate → (a) force v3.2 categorization flow
- #2/#3/#4/#7/#8/#9/#11 → inline per standard engineering, all citing
Decision Doc forbiddances as bounds
Locks ARS-conventional deployment shape: LLM-prose protocol + data
reference table + Python lints + Python unittest conformance fixtures.
Decision Doc "renderer" term mapped to ARS file shapes in §1. No
runtime renderer code introduced; v3.2 disclosure mode pattern extended.
Test count floor: 184 new tests (64 cell × 2 + 19 mutation + 16
invariant + 20 concern + 1 de-dup). Final regression target: ≥1151
pass vs v3.7.3 967 baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#108): policy anchor table + static lint validator
Adds the source-of-truth reference table the new policy-anchor renderer
will read at runtime:
- academic-paper/references/policy_anchor_table.md: 4 anchors × 16 fields
carrying verbatim policy quotes from discovery doc §4.3-4.6 (PRISMA-trAIce,
ICMJE, Nature, IEEE) + per-anchor renderer rules encoding G5-G9 invariants
+ Nature ↔ v3.2 venue dedup pointer.
- scripts/check_policy_anchor_table.py: structural lint enforcing 4-anchor
coverage, 16-field canonical order, source_strength enum membership,
verbatim-quote presence for mandate/recommend cells. Exposes
verify_nature_dedup_with_venue() helper for the cross-file dedup guard.
- scripts/test_check_policy_anchor_table.py: 12 tests (1 golden path + 7
mutation negative paths + 2 module-level invariants + 2 dedup helper).
- academic-paper/references/venue_disclosure_policies.md: adds Nature dedup
pointer line to peer the new anchor table.
TDD: validator + mutation tests written first, watched fail with
ModuleNotFoundError, then validator implemented to pass. Tests cover
both the validator behavior (mutation) and the real anchor table's
conformance (golden path + dedup integration).
Decision Doc §4.1 item 6a + §4.3 G1/G7/G8/G9 invariants. Impl spec §2.1
+ §4.4 #4-#7 resolutions encoded as per-anchor renderer rules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#108): policy anchor disclosure protocol + static lint
Adds the LLM-prose runtime instruction set the new --policy-anchor=<a>
track follows when disclosure mode is invoked. Parallels the existing
v3.2 --venue=<v> path without disturbing it.
- academic-paper/references/policy_anchor_disclosure_protocol.md: 8
sections covering (1) inputs incl slr_lineage signal, (2) G10 7-row
precedence table for whole-disclosure decision, (3) per-anchor render
flows (PRISMA-trAIce / ICMJE / Nature / IEEE), (4) auto-promotion
forbiddance, (5) venue+anchor conflict reject, (6) three-state
completeness flag spec, (7) audit map of §4.4 11 concern resolutions,
(8) related cross-refs.
- scripts/check_policy_anchor_protocol.py: presence-of-required-content
lint enforcing 8 named §4.3 invariants + 11 §4.4 concern clauses + 7
G10 rows + auto-promotion forbiddance + 4 anchor slugs + Nature dedup
pointer.
- scripts/test_check_policy_anchor_protocol.py: 9 tests (1 golden path
+ 6 mutation negative paths + 2 module-level invariants).
TDD: validator + mutation tests written first, watched fail with
ModuleNotFoundError, then validator implemented to pass.
Decision Doc §4.1 item 6 + §4.3 8 invariants + §4.4 11 concerns. Impl
spec §3 resolved-paths table encoded into §7 of protocol doc for audit
traceability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#108): extend v3.2 disclosure mode with --policy-anchor track
Adds the #108 policy-anchor track to the existing v3.2 venue track.
The two tracks are mutually exclusive selectors on the same disclosure
mode entry point; v3.2 path unchanged.
- Two-parallel-tracks section: documents --venue vs --policy-anchor
dispatch + the §4.4 #7 venue+anchor conflict resolution (consistent
pair proceed, conflicting pair reject with explicit error, silent
precedence forbidden).
- Phase 1 (intake + lookup): splits into selector-aware step 1a
(dispatch), step 1b (venue lookup, v3.2 unchanged), step 1c (anchor
lookup) + G2 invariant slr_lineage gate for prisma-trAIce.
- Inputs section: adds slr_lineage pipeline signal + cold-start mode=
parameter for the anchor path.
- References: cross-link to policy_anchor_table.md +
policy_anchor_disclosure_protocol.md + Decision Doc + impl spec.
Decision Doc §4.1 item 7 (EXTEND, not REPLACE). Verifies existing v3.2
venue-path content stays byte-equivalent for unrelated venues; only
adds new sections around the v3.2 content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#108): renderer conformance referee + 37-test suite
Adds the executable spec / referee that codifies the G10 7-row
precedence table and §4.3 invariants as deterministic Python so the
conformance test suite can verify protocol-prose ↔ logic alignment.
- scripts/policy_anchor_disclosure_referee.py: dataclasses for
RendererInput + DisclosureDecision; decide_disclosure_output() impl
of §3 G10 7-row table; auto-promotion forbiddance as
AutoPromotionForbidden exception; per-anchor predicates
(copyediting_carveout_semantics, image_rights_regime,
prompt_disclosure_required, assert_ieee_pairing_conformant,
nature_image_outputs); venue+anchor conflict resolution as
VenueAnchorConflict exception (concern #7).
- scripts/test_policy_anchor_disclosure.py: 37 conformance tests
covering all 7 G10 rows (positive), all 8 G-invariants (positive +
negative where forbiddance applies), and concerns #1/#5/#6/#7/#10/#11
resolved paths.
The referee is NOT the production renderer (production is LLM-prose at
runtime). It's an executable spec that catches protocol-doc ↔ logic
drift via the test suite. When the protocol doc's §2 table changes,
this module must change; the tests catch silent divergence.
TDD: conformance tests written first, watched fail with
ModuleNotFoundError; referee implemented to make all 37 pass.
Decision Doc §4.1 item 6b + §4.3 8 invariants + §4.4 #1/#5/#6/#7/#10/#11.
Impl spec §4 (TDD discipline) + §6 (test count expectation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(#108): wire policy anchor lints + conformance tests into spec-consistency.yml
Adds 5 new CI steps after the existing v3.6.7 pattern-eval steps:
1. check_policy_anchor_table.py — anchor table structural lint
2. test_check_policy_anchor_table.py — 12 validator mutation tests
3. check_policy_anchor_protocol.py — protocol doc presence lint
4. test_check_policy_anchor_protocol.py — 9 validator mutation tests
5. test_policy_anchor_disclosure.py — 37 conformance fixture tests
All five run in the existing spec-consistency job, no new job created.
Decision Doc §4.1 + impl spec §2.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#108): codex round-1 four P2 closures
- P2 #1 (lint loose): check_policy_anchor_protocol.py row check now
matches actual markdown table rows (^|\s*N\s*|) instead of accepting
prose mentions of "row N" anywhere in doc. Previous loose check let
protocol-doc row 4 silently disappear because row 4 was named in the
concern #10 paragraph.
- P2 #2 (silent unmapped venue+anchor): referee.py
_check_venue_anchor_conflict() now rejects every (venue, anchor) pair
that is not the explicitly defined consistent pair (Nature venue +
nature anchor). Previously --venue=ICLR --policy-anchor=ieee fell
through silently, violating concern #7 forbiddance of silent
precedence. New tests cover ICLR + ieee and "Nature (Nature
Publishing Group)" + ieee.
- P2 #3 (anchor enum unchecked): new
_check_policy_anchor_enum() runs before any decision logic;
policy_anchor not in CANONICAL_ANCHORS raises InvalidPolicyAnchor.
Closes the path where a typo like "ICMJE" (uppercase) or "cope"
silently rendered as anchor_render with no matching table entry.
- P2 #4 (protocol-doc contradiction): disclosure_mode_protocol.md two
sentences disagreed on whether both-selectors-supplied is non-conformant
vs case-by-case. Rewritten to "selector-mutually-exclusive by default;
when both supplied, evaluate compatibility per concern #7 rules" with
an exhaustive 4-case enumeration (consistent / any-other-combo /
one-only / none).
Test impact: 967 baseline + 62 new (was 58) = 1029 passing, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#108): codex round-2 three P2 closures
- P2 #1 (Nature alias rejected): NATURE_VENUE_NAMES expanded to include
the v3.2 policy database's exact label "Nature (Nature Publishing
Group)" + "Nature Publishing Group" alongside the short forms. Without
this, every author using the existing policy_disclosure_policies.md
Nature entry name was incorrectly raised on a valid consistent pair.
- P2 #2 (silent invalid category state): new _check_category_states()
runs before _check_venue_anchor_conflict; any category state outside
{USED, NOT USED, UNCERTAIN} raises InvalidCategoryState. Previously
a lowercase typo like 'used' silently fell through to row 7 (cold
start) or row 6 (silence) depending on other categories, changing the
disclosure decision instead of rejecting bad input.
- P2 #3 (auto-promotion lint loose): check_policy_anchor_protocol.py
changed from any() to all() over the two load-bearing tokens
('auto-promotion' AND 'MUST NOT be rendered as though USED'). Earlier
any() accepted either token alone, so dropping the load-bearing
prohibition sentence while keeping the heading word passed CI.
Test impact: 1029 → 1035 (added 6 new positive + negative path tests
for the round-2 closures). 0 regressions in 967 baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#108): codex round-3 two P2 closures
- P2 #1 (IEEE G8 pairing skipped in row 4): decide_disclosure_output()
now calls assert_ieee_pairing_conformant() before returning
anchor_render when policy_anchor='ieee'. Previously the function
blessed an IEEE render with only level_of_involvement set (or only
affected_sections), violating §4.3 G8 invariant. The helper itself
was already implemented; missing was the call site in the main
decision path.
- P2 #2 (duplicate anchor section silently overwrites):
_split_anchor_sections() now returns a duplicates list alongside the
dict; lint_text flags any slug that appears more than once. A second
`## Anchor: ieee` heading was previously absorbed by dict overwrite
and slug coverage still passed.
Test impact: 1035 → 1039 (added 4 new tests covering IEEE row-4
pairing positive/negative paths, duplicate anchor-section mutation,
and the "neither pairing input present" graceful-degradation case
documented in protocol §3.4).
Three rounds of codex gpt-5.5 xhigh review: R1 4 P2 → R2 3 P2 → R3
2 P2 → R4 expected to converge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#108): codex round-4 four P2 closures
- P2 #1 (cross-doc contradiction): policy_anchor_disclosure_protocol.md
§1 input contract still said "mutually exclusive" while §5 + the
dispatcher allowed the Nature consistent pair. Rewritten as
"selector-mutually-exclusive by default" referencing §5's exhaustive
enumeration.
- P2 #2 (silent ICMJE default): RendererInput.policy_anchor was
defaulted to "icmje", so RendererInput(venue="ICLR") silently produced
a Nature-ICMJE conflict and RendererInput() returned an ICMJE render.
Now defaults to None; new _check_selector_supplied() raises
SelectorUnsupplied when both selectors are unset; venue-only inputs
return DisclosureDecision(row=0, kind='delegated_to_venue_path').
- P2 #3 (Nature Portfolio variants rejected): Nature Medicine, Nature
Communications, etc. all inherit the parent Nature policy but the
hardcoded whitelist was 4 exact strings. Added NATURE_VENUE_PREFIXES
("Nature ") + is_nature_portfolio_venue() helper covering the whole
Nature Portfolio family.
- P2 #4 (dedup pointer string-only): added the canonical source file
shared/policy_data/nature_policy.md and strengthened
verify_nature_dedup_with_venue() to confirm the file actually exists
(not just that the path appears in both consumer files). Byte-compare
is not the right contract (anchor table = verbatim quotes, venue
policy = summary form, intentionally different shapes); shared-source
presence is.
Test impact: 1039 → 1048 (added 9 new tests: selector unsupplied,
venue-only delegation, Nature Portfolio prefix family, helper unit
tests, canonical-source existence check).
Four rounds of codex review now: R1 4 P2 → R2 3 P2 → R3 2 P2 → R4 4 P2.
R4 surfaced cross-doc inconsistency + design defaults bugs that
R1-R3 missed (not lexical patching reverberation; genuine new ground).
Per impl spec §7 architectural-inflection trigger conditions, not yet
hit: trajectory is R3 lowest (2 P2), R4 surfaced new substantive
ground rather than re-patching old territory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#108): codex round-5 three P2 closures
- P2 #1 (Nature variant unknown-venue halt): disclosure_mode_protocol.md
step 1a dispatch now routes the consistent pair (any Nature Portfolio
venue + nature anchor) to step 1c (anchor path), not step 1b. Step 1b
is for venue-only invocations whose venue lives in the v1 database;
routing the consistent pair through it caused Nature Portfolio
journals outside v1 (Nature Medicine, etc.) to halt as unknown.
- P2 #2 (ai_used:true gate scope): protocol doc §3 concern #10 spec
was limited to bare-flag/no-Phase-2 case. Referee already handled the
full case (ai_used:true with all-NOT-USED OR all-UNCERTAIN OR empty);
protocol doc now matches: the gate fires whenever ai_used=true is
supplied AND no v3.2 category is marked USED, regardless of why
(Phase 2 done or not done).
- P2 #3 (substring concern-number lint): check_policy_anchor_protocol
used plain `in text` membership for `concern #1`, so removing it from
a doc still mentioning `concern #10` / `concern #11` passed. Now uses
regex `concern\s+#1\b` with word boundary.
Test impact: 1048 → 1050 (added 2 new tests: concern-number boundary
mutation lint coverage + ai_used:true with UNCERTAIN-only gate test).
Five rounds of codex review: R1 4 P2 → R2 3 P2 → R3 2 P2 → R4 4 P2 →
R5 3 P2. Each round surfaces substantive new ground rather than
re-patching old territory; not yet at the impl spec §7 architectural
inflection trigger (3+ P2 with no net downward trend across R3-R5-R7).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#108): codex round-6 two P2 + one P3 closures
- P2 #1 (anchor heading regex too narrow): ANCHOR_HEADING regex in
check_policy_anchor_table.py only matched `[A-Za-z0-9-]+`, so any
heading with underscore / space / punctuation was silently folded
into the previous section. Now captures `\\S.*?` (any non-empty
trailing identifier) so invalid slugs surface in the "unknown
anchor section" violation.
- P2 #2 (anchor slug coverage was global substring): the lint accepted
a slug appearing anywhere in the doc (e.g., in a §3 render-rules
section). Now parses the **Anchor inventory** line specifically and
requires each canonical slug to appear in that comma-separated list.
Fixture _GOOD_PROTOCOL gains the inventory line; new mutation test
asserts dropping a slug from the inventory while keeping it in prose
still fails.
- P3 #1 (YAML # parsed as comment): spec-consistency.yml step names
like `Check #108 policy anchor table ...` were parsed by Actions as
bare `Check` because YAML treats `#` as a comment marker. All five
new step names are now double-quoted with `(#108)` suffix so the
full step name shows in the Actions UI.
Test impact: 1050 → 1051 (added 1 new mutation test:
test_missing_anchor_inventory_line_fails). All real-file lints + 84
new-validator tests pass.
Six rounds of codex review: R1 4 P2 → R2 3 P2 → R3 2 P2 → R4 4 P2 →
R5 3 P2 → R6 2 P2 + 1 P3. Substantive new ground each round (no
revisit of same defect class twice); trajectory moves between 2-4 P2
with no monotonic descent but no escalation either.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#108): codex round-7 one P2 + two P3 closures
- P2 #1 (Nature venue path not bound to shared source): added
explicit "Derivation note (#108 scope limitation)" to
venue_disclosure_policies.md §Nature documenting that the v3.2
summary rows are derived from shared/policy_data/nature_policy.md
but not auto-generated from it. The G4 invariant now reads: when
Nature source policy drifts, update canonical source first then
manually sync summary rows in the same change. A future refactor
(out of #108 scope) can replace summary rows with auto-extract from
canonical source. This documents the limitation rather than papering
over it; a hard auto-bind would require rewriting the v3.2 venue
track which §4.1 items 1-5 say NO CHANGE.
- P3 #1 (lint doesn't enforce dedup): main(argv) of
check_policy_anchor_table.py now invokes
verify_nature_dedup_with_venue() alongside lint_text(). New
--venue-policies CLI arg lets the canonical Nature source path
be configured. New integration test exercises main() with a
temporary subtree missing the canonical source to confirm the
main command surfaces the violation.
- P3 #2 (inventory closed-enum loose): check_policy_anchor_protocol
now rejects extra slugs in the Anchor inventory line, not only
missing ones. A line advertising `prisma-trAIce, icmje, nature,
ieee, cope` is non-conformant because cope is outside the canonical
closed enum. Symmetric to the canonical anchor-slug coverage check
in check_policy_anchor_table.py.
Test impact: 1051 → 1053 (added 2 new tests: extra-anchor-in-inventory
mutation, main-command-invokes-dedup-helper integration).
Seven rounds of codex review: R1 4 → R2 3 → R3 2 → R4 4 → R5 3 →
R6 2 → R7 1+2P3. P2 count dropped to 1 at R7; trajectory descending
again with smaller findings. Substantive ground covered each round;
not yet at impl spec §7 architectural-inflection trigger.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#108): codex round-8 closures + audit-trail-shipped declaration
Closes 2 of 3 R8 findings; R8 P2 #1 (pipeline plumbing for slr_lineage)
deferred to follow-up issue per user-chosen audit-trail-shipped framing.
- R8 P2 #2 (protocol §5 Nature variants drift): policy_anchor_disclosure
_protocol.md §5 now mirrors the full Nature Portfolio recognition logic
(canonical labels + "Nature " prefix family) that step 1a and the
referee already implement. Cross-doc consistency restored — when this
text drifts from is_nature_portfolio_venue() it is a documented
non-conformance.
- R8 P3 (dedup lint section-specific): verify_nature_dedup_with_venue()
now requires the canonical source pointer to appear inside the
Nature-specific section (## Anchor: nature in policy_anchor_table.md,
## Venue: Nature ... in venue_disclosure_policies.md), not anywhere in
the file. Closes the prior false-pass where citations at the file
bottom could satisfy the dedup invariant.
- R8 P2 #1 (academic-pipeline orchestrator slr_lineage emission):
documented as out-of-#108-scope follow-up. Will require updating
academic-pipeline orchestrator + handoff_schemas to emit the signal
on the systematic-review → academic-paper full handoff path. Tracked
separately so #108 ships without a cross-skill change beyond §4.1
items 1-5 boundary.
**Audit-trail-shipped framing (per user decision after 8 rounds):**
Eight rounds of codex gpt-5.5 xhigh review:
R1 4 P2 → R2 3 P2 → R3 2 P2 → R4 4 P2 → R5 3 P2 → R6 2 P2 →
R7 1 P2+2P3 → R8 2 P2+1P3 (1 P2 deferred, 1 P2 + P3 closed here)
The trajectory oscillates 1-4 P2 in 8 rounds — same pattern Decision
Doc (PR #109) exhibited at R5-R11, ultimately shipped audit-trail-
complete at R11 with 2 documented open concerns. This implementation
ships R8 with 1 documented follow-up (pipeline plumbing) per the same
audit-trail-honest pattern. P2 count trend across R1-R8 is descending
on average (4 → 2 modal value); R4 reset suggests cross-section
inventory effect rather than convergence to zero.
Substantive ground covered: §4.3 8 invariants all enforced in the
referee + protocol doc + anchor table; §4.4 11 concerns all resolved
(10 inline-fix, 1 follow-up); 86 new tests + 967 baseline = 1053
passing / 0 regressed; CI wired into spec-consistency.yml; public-repo
boundary clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#108): CHANGELOG [Unreleased] entry — audit-trail-shipped
Adds #108 section above the existing v3.7.3 entry. Documents:
- migration note (G1+G6: no migration needed; renderer path only)
- 7 new files + 3 modified files
- §4.4 11 open concerns resolution table summary (4 user-chosen, 7 inline)
- known follow-up: academic-pipeline slr_lineage plumbing (cross-skill)
- regression: 1053 passing (967 baseline + 86 new), 0 failed
- 8-round codex audit trail
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(#108): /simplify pass — strip codex round-N residue + hoist regex
Applies findings from /simplify three-agent review.
Quality cleanup (Agent 2 Finding 1, ~17 sites):
- Strip "Closes codex round-N P# #M" inline narrative from docstrings
and comments in the three new Python modules (referee + 2 lints).
Git log + CHANGELOG already encode round-N provenance; per CLAUDE.md
inline comments should explain WHY not narrate change history.
- Keep all §4.3 / §4.4 spec-section references (audit trail proper) +
invariant identifiers (G1-G9) intact.
Quality cleanup (Agent 2 Finding 3):
- Extract COPYEDITING_CARVEOUT_SEMANTICS and IMAGE_RIGHTS_REGIMES dicts
to module level in referee.py. Surfaces the closed enum, lets future
lints check membership directly, simplifies the per-anchor lookup
functions to single-line dispatchers.
Quality cleanup (Agent 2 Finding 4):
- Single-pass dict bucketing in decide_disclosure_output: replace three
set comprehensions iterating ri.categories with one bucket loop.
Efficiency cleanup (Agent 3 Findings 1+2):
- Hoist CONCERN_PATTERN, TABLE_ROW_PATTERN, ANCHOR_INVENTORY_PATTERN to
module level in check_policy_anchor_protocol.py. Previously 11+7
regex compiles per invocation; now compiled once + finditer-based
set lookup. Matches the established ARS pattern (e.g.,
check_v3_6_8_pattern_protection.py).
Efficiency cleanup (Agent 3 Findings 3+4):
- Hoist NATURE_ANCHOR_SECTION, NATURE_VENUE_SECTION,
NATURE_POLICY_POINTER, REPO_ROOT to module level in
check_policy_anchor_table.py.
- verify_nature_dedup_with_venue() accepts optional anchor_text+
venue_text kwargs so main() can pass its already-read anchor text
instead of reading the same file twice.
- New repo_root parameter (default = module-level REPO_ROOT) lets
isolation tests pass a temp-dir mirror without monkey-patching.
Reuse cleanup (Agent 1 Finding 3):
- Adopt scripts/<file>.parent.parent pattern for REPO_ROOT, matching
6 sibling lint scripts. main() derives a per-invocation repo_root
from the anchor table path so temp-subtree integration tests work.
Findings deliberately skipped:
- Agent 1 Finding 1 (shared split_h2_sections helper): cross-file
refactor scope creep; check_corpus_consumer_protocol.py has its own
duplicate-bug to fix in a separate change.
- Agent 2 Findings 5-10: nice-to-have refactors, not blocking.
Test impact: 1053 / 0 regressed. Net -60 lines (3 files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
237 lines
10 KiB
Python
237 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
"""Mutation tests for scripts/check_policy_anchor_protocol.py."""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import textwrap
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||
if str(REPO_ROOT / "scripts") not in sys.path:
|
||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||
|
||
import check_policy_anchor_protocol as cpap # noqa: E402
|
||
|
||
|
||
# Each named invariant from impl spec §4.3 must be referenced verbatim. The
|
||
# protocol doc must also resolve each §4.4 #1–#11 concern by name and carry
|
||
# the §3 G10 7-row precedence table verbatim from Decision Doc.
|
||
_GOOD_PROTOCOL = textwrap.dedent(
|
||
"""\
|
||
# Policy-Anchor Disclosure Protocol (#108)
|
||
|
||
**Anchor inventory**: `prisma-trAIce, icmje, nature, ieee`
|
||
|
||
## Frozen invariants from Decision Doc §4.3
|
||
|
||
- G1 invariant — no ai_disclosure field added to corpus entry schema
|
||
- G2 invariant — SLR mode dispatches via slr_lineage to PRISMA-trAIce track
|
||
- G3 / G10 invariant — seven-row precedence table holds; auto-promotion forbiddance
|
||
- G4 invariant — 4 policy-anchor renderers; Nature dedup
|
||
- G5 invariant — PRISMA M6 fires only when all three gates hold
|
||
- G7 invariant — copyediting carve-out anchor-specific
|
||
- G8 invariant — IEEE #5 + #6 paired mandate
|
||
- G9 invariant — image-rights regimes anchor-specific
|
||
|
||
## §4.4 open concerns resolved
|
||
|
||
- concern #1 — explicit slr_lineage input
|
||
- concern #2 — auto-detect tool identity per v3.2 Phase 4
|
||
- concern #3 — per-(tool × task) prompt scope tuple
|
||
- concern #4 — IEEE locator free-form list with IMRaD exemplars
|
||
- concern #5 — Nature image hybrid annotation + suggested patches
|
||
- concern #6 — UNCERTAIN per-facet annotation alongside USED render
|
||
- concern #7 — venue+anchor conflict reject with error
|
||
- concern #8 — three-state completeness flag full spec
|
||
- concern #9 — test set scope covers all invariants + concerns
|
||
- concern #10 — ai_used:true force v3.2 categorization flow
|
||
- concern #11 — G1 invariant scope narrowing per §2.1 authoritative
|
||
|
||
## G10 7-row precedence table (whole-disclosure)
|
||
|
||
| # | Precondition | Output |
|
||
|---|---|---|
|
||
| 1 | ai_used:false AND ≥1 category USED | Honest conflict annotation |
|
||
| 2 | ai_used:false AND no USED AND no UNCERTAIN | No-AI statement (G10 opt-in) |
|
||
| 3 | ai_used:false AND ≥1 UNCERTAIN AND no USED | Honest tension annotation |
|
||
| 4 | ai_used:true OR ≥1 USED, row 1 not match | Full anchor disclosure render |
|
||
| 5 | ≥1 UNCERTAIN AND no USED AND no ai_used | Honest not-supplied annotation |
|
||
| 6 | All NOT USED AND no UNCERTAIN AND no ai_used | Silence (G10 default-OK) |
|
||
| 7 | Empty input across every dimension | Honest cold-start annotation |
|
||
|
||
## Auto-promotion forbiddance
|
||
|
||
A still-UNCERTAIN category MUST NOT be rendered as though USED in any of
|
||
the four anchor outputs. This is the §4.3 G3/G10 invariant the
|
||
implementation enforces.
|
||
|
||
## Anchor lookup mechanism
|
||
|
||
Renderer reads `policy_anchor_table.md` keyed by `--policy-anchor=<a>`
|
||
where a ∈ {prisma-trAIce, icmje, nature, ieee}.
|
||
|
||
## v3.2 Nature venue dedup
|
||
|
||
Both the v3.2 Nature venue renderer and the Nature anchor renderer cite
|
||
the canonical source pointer `shared/policy_data/nature_policy.md`.
|
||
"""
|
||
)
|
||
|
||
|
||
class CheckPolicyAnchorProtocolGoldenPathTest(unittest.TestCase):
|
||
def test_good_protocol_passes(self) -> None:
|
||
violations = cpap.lint_text(_GOOD_PROTOCOL)
|
||
self.assertEqual(violations, [], msg=f"unexpected violations: {violations}")
|
||
|
||
|
||
class CheckPolicyAnchorProtocolMutationTests(unittest.TestCase):
|
||
def test_missing_invariant_reference_fails(self) -> None:
|
||
bad = _GOOD_PROTOCOL.replace("G1 invariant", "")
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("G1" in v for v in violations),
|
||
msg=f"expected G1 invariant violation; got {violations}",
|
||
)
|
||
|
||
def test_missing_concern_resolution_fails(self) -> None:
|
||
bad = _GOOD_PROTOCOL.replace("concern #6", "")
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("#6" in v or "concern 6" in v.lower() for v in violations),
|
||
msg=f"expected concern #6 violation; got {violations}",
|
||
)
|
||
|
||
def test_missing_g10_table_row_fails(self) -> None:
|
||
# Drop row 4 — the load-bearing full-anchor-disclosure row
|
||
bad = _GOOD_PROTOCOL.replace(
|
||
"| 4 | ai_used:true OR ≥1 USED, row 1 not match | Full anchor disclosure render |",
|
||
"",
|
||
)
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("row 4" in v.lower() or "7-row" in v.lower() for v in violations),
|
||
msg=f"expected G10 row 4 violation; got {violations}",
|
||
)
|
||
|
||
def test_missing_auto_promotion_forbiddance_fails(self) -> None:
|
||
# Strip every mention of both forbiddance keywords so the validator
|
||
# has nothing to anchor on; replace with neutral filler text.
|
||
bad = _GOOD_PROTOCOL
|
||
bad = bad.replace("auto-promotion forbiddance", "(removed clause)")
|
||
bad = bad.replace("Auto-promotion forbiddance", "Removed clause")
|
||
bad = bad.replace(
|
||
"A still-UNCERTAIN category MUST NOT be rendered as though USED in any of\n"
|
||
"the four anchor outputs.",
|
||
"",
|
||
)
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("auto-promot" in v.lower() or "UNCERTAIN" in v for v in violations),
|
||
msg=f"expected auto-promotion forbiddance violation; got {violations}",
|
||
)
|
||
|
||
def test_partial_auto_promotion_token_loss_still_fails(self) -> None:
|
||
# Codex round-2 P2 #3: dropping the load-bearing MUST NOT sentence
|
||
# while keeping the "auto-promotion" heading should still fail. The
|
||
# protocol's invariant is the prohibition itself; the heading word
|
||
# alone is insufficient.
|
||
bad = _GOOD_PROTOCOL.replace(
|
||
"A still-UNCERTAIN category MUST NOT be rendered as though USED in any of\n"
|
||
"the four anchor outputs.",
|
||
"",
|
||
)
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("MUST NOT" in v for v in violations),
|
||
msg=f"expected forbiddance-token violation; got {violations}",
|
||
)
|
||
|
||
def test_partial_keyword_loss_still_fails(self) -> None:
|
||
# Inverse mutation: keep the prohibition sentence but drop the
|
||
# "auto-promotion" anchor heading. The lint should still fail
|
||
# because both tokens are load-bearing.
|
||
bad = _GOOD_PROTOCOL.replace("auto-promotion forbiddance", "(removed clause)")
|
||
bad = bad.replace("Auto-promotion forbiddance", "Removed clause")
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("auto-promotion" in v for v in violations),
|
||
msg=f"expected auto-promotion-token violation; got {violations}",
|
||
)
|
||
|
||
def test_concern_number_boundary_matching(self) -> None:
|
||
# Codex round-5 P2 #3 closure: removing `concern #1` from a doc that
|
||
# still mentions `concern #10` and `concern #11` should still fail.
|
||
# Previous substring-membership check would let the document pass.
|
||
bad = _GOOD_PROTOCOL.replace("concern #1 —", "deleted-clause —", 1)
|
||
# Confirm the bad text still contains concern #10 / #11
|
||
self.assertIn("concern #10", bad)
|
||
self.assertIn("concern #11", bad)
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("concern #1 " in v or "concern #1\b" in v.lower() for v in violations),
|
||
msg=f"expected concern #1 violation; got {violations}",
|
||
)
|
||
|
||
def test_missing_dedup_pointer_fails(self) -> None:
|
||
bad = _GOOD_PROTOCOL.replace("shared/policy_data/nature_policy.md", "elsewhere.md")
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("nature_policy.md" in v or "dedup" in v.lower() for v in violations),
|
||
msg=f"expected dedup pointer violation; got {violations}",
|
||
)
|
||
|
||
def test_missing_anchor_slug_fails(self) -> None:
|
||
# Codex round-6 P2 #2 closure: dropping slug from the
|
||
# **Anchor inventory** line must fail even if the slug name
|
||
# appears elsewhere (e.g., in a per-anchor render section).
|
||
bad = _GOOD_PROTOCOL.replace(
|
||
"**Anchor inventory**: `prisma-trAIce, icmje, nature, ieee`",
|
||
"**Anchor inventory**: `icmje, nature, ieee`",
|
||
)
|
||
# Add a stray prisma-trAIce mention to confirm the inventory check
|
||
# is strict (the global substring would catch this and pass).
|
||
bad += "\n\nNote: prisma-trAIce renders are mentioned in §3.1.\n"
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("prisma-trAIce" in v or "anchor slug" in v.lower() for v in violations),
|
||
msg=f"expected anchor slug violation; got {violations}",
|
||
)
|
||
|
||
def test_missing_anchor_inventory_line_fails(self) -> None:
|
||
bad = _GOOD_PROTOCOL.replace(
|
||
"**Anchor inventory**: `prisma-trAIce, icmje, nature, ieee`",
|
||
"(inventory line removed)",
|
||
)
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("Anchor inventory" in v for v in violations),
|
||
msg=f"expected Anchor inventory violation; got {violations}",
|
||
)
|
||
|
||
def test_extra_anchor_in_inventory_fails(self) -> None:
|
||
# Codex round-7 P3 #2 closure: inventory advertising a slug
|
||
# outside the canonical closed enum (e.g., `cope`) must fail.
|
||
# Previously the lint only checked presence, not extras.
|
||
bad = _GOOD_PROTOCOL.replace(
|
||
"**Anchor inventory**: `prisma-trAIce, icmje, nature, ieee`",
|
||
"**Anchor inventory**: `prisma-trAIce, icmje, nature, ieee, cope`",
|
||
)
|
||
violations = cpap.lint_text(bad)
|
||
self.assertTrue(
|
||
any("cope" in v and "unexpected" in v for v in violations),
|
||
msg=f"expected unexpected-anchor violation; got {violations}",
|
||
)
|
||
|
||
|
||
class CheckPolicyAnchorProtocolInvariantTest(unittest.TestCase):
|
||
def test_invariant_names_count(self) -> None:
|
||
self.assertEqual(len(cpap.REQUIRED_INVARIANTS), 8)
|
||
|
||
def test_concern_count(self) -> None:
|
||
self.assertEqual(len(cpap.REQUIRED_CONCERNS), 11)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|