mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
5d769b52d1
* docs(#111): slr_lineage emission design doc Pipeline plumbing design for #108 R8 follow-up. Closes the upstream signal gap so `disclosure --policy-anchor=prisma-trAIce` dispatches automatically when the documented `deep-research systematic-review → academic-paper full → disclosure` path runs. Three decisions (per impl-spec §3 row #1 frozen choice): - Transport: Schema 9 Material Passport top-level optional `slr_lineage` - Emission: pipeline_orchestrator_agent at Stage 1→2 handoff - G1 boundary: corpus entry schema frozen, passport schema permitted per v3.6.3/4/7 extension pattern Files touched: 3 docs + 1 new conformance test. 1053-baseline frozen, #108 referee/protocol/anchor table/v3.2 venue track all out-of-scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(#111): slr_lineage emission on systematic-review handoff Closes the pipeline-plumbing gap from #108 (PR #110 round-8 P2 #1): `disclosure --policy-anchor=prisma-trAIce` now dispatches automatically when `deep-research systematic-review → academic-paper full → disclosure` runs, without manual `mode=systematic-review` at cold-start. Three pieces (per design 2026-05-15): - Schema 9 Material Passport gains optional top-level `slr_lineage: bool` + dedicated subsection documenting semantics, producer, consumer, backward compat, and the G1 boundary note (passport-level vs corpus-entry-level distinction) - pipeline_orchestrator_agent §4 Transition Management gains a "Run-level lineage emission (v3.7.4+)" step computed at every handoff - New `scripts/slr_lineage.py` pure resolver + 12 conformance tests covering resolver semantics (7) + renderer integration (3) + e2e handoff (2) #108 contract frozen: referee, protocol doc, anchor table, v3.2 venue track, corpus entry schema all out-of-scope per #111 §Scope. G1 boundary honored — Decision Doc §4.4 #11 G1 scope is corpus entry schema, passport-schema extensions follow v3.6.3/4/7 precedent. Backward compat: pre-v3.7.4 passports lack the field; renderer treats absence as false (cold-start fallback identical to pre-v3.7.4 behavior). Tests: 1053-baseline frozen, +12 new tests cover acceptance #1-#3. All unittest (394) + pytest (401) + check_spec_consistency green. Closes #111 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(#111): /simplify pass — 5 findings fixed Three-agent review (reuse / quality / efficiency) findings: 1. SLR_MODES drift bait removed — slr_lineage.py now imports from policy_anchor_disclosure_referee instead of redefining + manual "keep in sync" comment. Single source of truth. 2. Design doc status updated from "pending approval" to "implemented (commit 7a2f789; pending PR)". 3. CHANGELOG entry stamped (unreleased) + provisional v3.7.4 disclaimer per feedback_version_bump_sweep_checklist. Version label confirmed at release tag, not pre-baked. 4. Duplicate test path collapsed — test_pre_111_passport_falls_back_to _cold_start first half duplicated test_non_slr_pipeline_passport_ still_blocks_prisma_track. Renamed + trimmed to cover only the unique positive cold-start path. 5. Orchestrator emission step reworded — was "every handoff transition" (monotonic flag = redundant recomputation). Now "compute once at Stage 1 → Stage 2 handoff, propagate persisted value thereafter". False positives skipped: 3 TestCase classes (justified separation), defensive .get() (real test_stage_without_mode_skipped case), all-stages scan (run-level contract, not Stage-1-only). Regression: 12 #111 tests + 394 unittest baseline + check_spec_consistency all green. Net -4 LOC. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#111): codex round-1 [P2] — monotonic OR preserves slr_lineage across resume Codex review against #108 baseline caught a real cascade bug: the emission step as worded ("compute from stages") would overwrite a persisted slr_lineage=true when called inside a `resume_from_passport= <hash>` session, because state_tracker.stages is empty (reconstructed from ledger only). That defeats the auto-dispatch goal #111 exists to achieve — exactly the gap users would hit when resuming an SLR pipeline in a fresh session and then invoking disclosure mode. Fix: - New `slr_lineage.emit(stages, incoming_slr_lineage)` wrapper applying the monotonic OR: bool(incoming) or resolve_from_stages(stages). A true value persists across resume; a false value can still flip true in-session when Stage 1 carries SLR mode. - pipeline_orchestrator_agent §"Run-level lineage emission" reworded to specify the OR semantics + cite the resume edge case explicitly. - handoff_schemas.md producer description updated to match. - Design doc §4.2 marks the OR as load-bearing per codex P2 closure. - CHANGELOG bumped to +17 tests (5 new EmitMonotonicOrTest cases). Tests: - 5 new cases (RED then GREEN) covering: resume preserves true / in- session flips false → true / no-evidence false / None incoming / default arg ergonomics. - 399 full unittest regression + spec consistency green. No #108 contract drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#111): codex round-2 [P2]×2 — reset-boundary emission + dual-path import Round 2 caught two real cascades that survived round 1: R2-1 [P2] Reset-boundary emission missed: Under ARS_PASSPORT_RESET=1 on a systematic-review run, the v3.6.3 reset protocol freezes the passport at the FULL checkpoint AND halts before the Stage 1 → Stage 2 handoff. Handoff-only emission therefore misses the only write opportunity; the resuming fresh session sees state_tracker.stages={} + flag-less incoming passport → OR resolves false → PRISMA-trAIce dispatch blocks. Fixed by reworking the orchestrator prose to specify emit() runs BEFORE any passport write — both the normal handoff and the reset-boundary FULL-checkpoint write. The flag is hashed into the boundary entry per the JCS protocol. R2-2 [P2] Module not importable as scripts.slr_lineage: The repo has both namespace-style callers (`from scripts.X import Y` in test_check_sprint_contract / _test_helpers) and sibling-style callers (sys.path-prepending in test_slr_lineage_emission). The original `from policy_anchor_disclosure_referee import SLR_MODES` only worked in the latter style. Fixed by dual-path try/except import preserving the single source of truth for SLR_MODES while supporting both styles. Files: - pipeline_orchestrator_agent.md §"Run-level lineage emission": prose reworked to specify "before any passport write" + new §"Reset- boundary interaction" subsection citing v3.6.3 protocol. - slr_lineage.py: dual-path import with try/except. - design doc §4.2: both R2-1 + R2-2 closures documented. Tests: 17 existing #111 tests still green (emit's monotonic OR covers the resume case at the helper level — R2-1 is prose-only). Verified `from scripts.slr_lineage import emit` works from repo root. 399 full unittest regression + spec consistency green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#111): codex round-3 [P2] — retract incorrect hash-coverage claim Round 3 caught a factual error introduced by round-2 fix: The round-2 prose claimed slr_lineage is "hashed into the boundary entry per the JCS protocol." That's wrong. The v3.6.3 reset_boundary hash covers only reset_boundary[] ledger entries (per the closed schema at shared/contracts/passport/reset_ledger_entry.schema.json which disallows extra fields). slr_lineage lives at passport top-level alongside origin_skill / verification_status / version_label, NOT inside the ledger. Fix: rewrite the §"Reset-boundary interaction" subsection to be honest about what protection v3.7.4 provides: - The OR ensures the field is correctly written before passport freeze (correctness-at-write). - The field is NOT hash-integrity-checked by the boundary hash (same trust model as other top-level Schema 9 fields). - The need at v3.7.4 is correctness, not integrity. Design doc §4.2 updated to mirror the honest accounting. No test changes — the test suite already covers the correctness side (EmitMonotonicOrTest's resume_preserves_true_when_stages_empty). Integrity-side guarantees were never tested because they never existed. 399 full unittest regression + spec consistency green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#111): CI lint — co-locate passport_as_reset_boundary reference + skip .codex-rounds CI's check_passport_reset_contract.py caught two real issues: 1. Design doc mentioned ARS_PASSPORT_RESET but didn't reference passport_as_reset_boundary.md per the v3.6.3 contract. Added explicit references at both mention sites (the §"Reset-boundary path" subsection now points readers at the protocol doc twice). 2. The lint scans local scratch dirs (.codex-rounds/ is in .gitignore alongside .context/) and false-flags codex review logs that quote the token from past sessions. .context was already in SKIP_DIRS; .codex-rounds is the same pattern. Added. scripts/check_passport_reset_contract.py is a v3.6.3 lint, not in #108 frozen scope — the .codex-rounds skip is a real lint bug fix. Verified: 399 unittest baseline + 12 check_passport_reset_contract mutation tests + spec consistency lint all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
215 lines
7.0 KiB
Python
215 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Enforce the ARS v3.6.3 passport-reset-boundary co-location contract.
|
|
|
|
Contract: any text file that mentions the env-flag token `ARS_PASSPORT_RESET`
|
|
MUST also contain a reference to the protocol-doc stem `passport_as_reset_boundary`
|
|
so a reader encountering the flag can trace it back to the authoritative spec at
|
|
`academic-pipeline/references/passport_as_reset_boundary.md`.
|
|
|
|
Also enforces: `pending_decision.options[]` must have unique `value` fields within
|
|
each options array in YAML fenced blocks found in markdown files under the repo root.
|
|
|
|
Exemptions:
|
|
- The protocol doc itself — identified by filename + parent-dir match
|
|
(`references/passport_as_reset_boundary.md`). More robust than path-suffix
|
|
match against `--root`: works whether the lint is invoked from repo root
|
|
or from a sub-tree.
|
|
- Binary / non-UTF-8 files — skipped silently to avoid false positives from
|
|
embedded bytes that happen to spell the flag token.
|
|
|
|
Exit code: 0 clean, 1 on any violation (list of offending files printed to
|
|
stderr). CLI: `--root <path>` (default `.`).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
FLAG_TOKEN = "ARS_PASSPORT_RESET"
|
|
PROTOCOL_TOKEN = "passport_as_reset_boundary"
|
|
PROTOCOL_FILENAME = "passport_as_reset_boundary.md"
|
|
PROTOCOL_PARENT_DIRNAME = "references"
|
|
|
|
# Directories we never scan: VCS, caches, build output, local tooling scratch.
|
|
# `.context` and `.codex-rounds` are hand-off / codex-review log scratch
|
|
# (gitignored per repo .gitignore); files there routinely quote the
|
|
# ARS_PASSPORT_RESET token from prior work without owing the protocol-doc
|
|
# co-location.
|
|
SKIP_DIRS = {
|
|
".git",
|
|
"node_modules",
|
|
"__pycache__",
|
|
".venv",
|
|
"dist",
|
|
"build",
|
|
".gstack",
|
|
".context",
|
|
".codex-rounds",
|
|
}
|
|
|
|
|
|
def _is_under_skip_dir(path: Path, root: Path) -> bool:
|
|
"""True if any path component between root and file is a skip-listed dir."""
|
|
try:
|
|
rel = path.relative_to(root)
|
|
except ValueError:
|
|
return False
|
|
return any(part in SKIP_DIRS for part in rel.parts)
|
|
|
|
|
|
def _read_text_or_none(path: Path) -> str | None:
|
|
"""Return file contents as UTF-8 text, or None if unreadable / non-text."""
|
|
try:
|
|
return path.read_text(encoding="utf-8")
|
|
except (UnicodeDecodeError, OSError):
|
|
return None
|
|
|
|
|
|
def _is_protocol_doc(path: Path) -> bool:
|
|
"""True if `path` IS the authoritative protocol reference doc."""
|
|
return path.name == PROTOCOL_FILENAME and path.parent.name == PROTOCOL_PARENT_DIRNAME
|
|
|
|
|
|
def scan(root: Path) -> list[str]:
|
|
"""Walk `root` and return list of violation messages (empty if clean)."""
|
|
violations: list[str] = []
|
|
for path in sorted(root.rglob("*")):
|
|
if not path.is_file():
|
|
continue
|
|
if _is_under_skip_dir(path, root):
|
|
continue
|
|
|
|
content = _read_text_or_none(path)
|
|
if content is None or FLAG_TOKEN not in content:
|
|
continue
|
|
|
|
if _is_protocol_doc(path):
|
|
# Protocol doc is exempt — it IS the reference.
|
|
continue
|
|
|
|
if PROTOCOL_TOKEN not in content:
|
|
rel = path.relative_to(root).as_posix()
|
|
violations.append(
|
|
f"{rel}: mentions {FLAG_TOKEN} but does not reference "
|
|
f"{PROTOCOL_TOKEN} (co-location required by ARS v3.6.3 contract)"
|
|
)
|
|
|
|
return violations
|
|
|
|
|
|
# Regex to extract fenced YAML blocks from markdown files.
|
|
# Accepts `yaml`/`yml`, optional trailing whitespace after the fence,
|
|
# and CRLF line endings (Windows-authored docs).
|
|
_FENCED_YAML_RE = re.compile(
|
|
r"```ya?ml[ \t]*\r?\n(.*?)\r?\n```",
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
def scan_duplicate_option_values(root: Path) -> list[str]:
|
|
"""Walk `root` for markdown files; find YAML fenced blocks with pending_decision;
|
|
check each options array for duplicate `value` fields.
|
|
|
|
Returns a list of violation messages (empty if clean).
|
|
"""
|
|
violations: list[str] = []
|
|
for path in sorted(root.rglob("*.md")):
|
|
if not path.is_file():
|
|
continue
|
|
if _is_under_skip_dir(path, root):
|
|
continue
|
|
|
|
content = _read_text_or_none(path)
|
|
if content is None or "pending_decision" not in content:
|
|
continue
|
|
|
|
rel = path.relative_to(root).as_posix()
|
|
for match in _FENCED_YAML_RE.finditer(content):
|
|
block_text = match.group(1)
|
|
if "pending_decision" not in block_text:
|
|
continue
|
|
|
|
try:
|
|
data = yaml.safe_load(block_text)
|
|
except yaml.YAMLError:
|
|
# Malformed YAML block: skip silently. Other lints cover schema validity.
|
|
continue
|
|
|
|
if not isinstance(data, dict):
|
|
continue
|
|
|
|
entries = data.get("reset_boundary", [])
|
|
if not isinstance(entries, list):
|
|
continue
|
|
|
|
for entry in entries:
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
pd = entry.get("pending_decision")
|
|
if not isinstance(pd, dict):
|
|
continue
|
|
options = pd.get("options", [])
|
|
if not isinstance(options, list):
|
|
continue
|
|
|
|
seen: set[str] = set()
|
|
duplicates: set[str] = set()
|
|
for opt in options:
|
|
if not isinstance(opt, dict):
|
|
continue
|
|
val = opt.get("value")
|
|
if val is None:
|
|
continue
|
|
if val in seen:
|
|
duplicates.add(str(val))
|
|
seen.add(str(val))
|
|
|
|
entry_hash = entry.get("hash", "<unknown>")
|
|
for dup in sorted(duplicates):
|
|
violations.append(
|
|
f"{rel}: pending_decision.options[] in entry "
|
|
f"hash={entry_hash} contains duplicate value "
|
|
f"'{dup}'. Values must be unique within each options "
|
|
f"array (ARS v3.6.3 contract)."
|
|
)
|
|
|
|
return violations
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Enforce the ARS v3.6.3 passport-reset co-location contract: every "
|
|
f"file mentioning {FLAG_TOKEN} must also reference {PROTOCOL_TOKEN}."
|
|
)
|
|
)
|
|
parser.add_argument(
|
|
"--root",
|
|
default=".",
|
|
help="Repo root to scan (default: current directory).",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
root = Path(args.root).resolve()
|
|
if not root.is_dir():
|
|
print(f"error: --root {args.root} is not a directory", file=sys.stderr)
|
|
return 2
|
|
|
|
violations = scan(root)
|
|
violations += scan_duplicate_option_values(root)
|
|
if violations:
|
|
print("Passport reset contract lint FAILED:", file=sys.stderr)
|
|
for v in violations:
|
|
print(f" - {v}", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|