Files
imbad0202__academic-researc…/scripts/sync_adapter_docs.py
T

119 lines
4.1 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""sync_adapter_docs: keep academic-pipeline/references/adapters/overview.md
field tables in lockstep with literature_corpus_entry.schema.json.
Markers in overview.md:
<!-- GENERATED:LITERATURE_CORPUS_REQUIRED:START --> ... auto ... <!-- GENERATED:LITERATURE_CORPUS_REQUIRED:END -->
<!-- GENERATED:LITERATURE_CORPUS_OPTIONAL:START --> ... auto ... <!-- GENERATED:LITERATURE_CORPUS_OPTIONAL:END -->
Modes:
sync_adapter_docs.py # rewrite the marked regions in place
sync_adapter_docs.py --check # fail if rewrite would change anything
Exit codes:
0 — no drift (or rewrote successfully without --check)
1 — drift detected under --check
2 — invocation error (schema or target missing)
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
SCHEMA_PATH = REPO_ROOT / "shared/contracts/passport/literature_corpus_entry.schema.json"
DEFAULT_TARGET = REPO_ROOT / "academic-pipeline/references/adapters/overview.md"
MARKERS = {
"LITERATURE_CORPUS_REQUIRED": "required",
"LITERATURE_CORPUS_OPTIONAL": "optional",
}
def _short_type(prop_def: dict) -> str:
t = prop_def.get("type")
if isinstance(t, list):
v3.7.1 Step 0+1: SHA byte-equivalence gate + D1 trust-chain frontmatter schema (#76) * feat(v3.7.1 Step 0): inversion manifest + SHA byte-equivalence lint Spec: docs/design/2026-04-30-ars-v3.6.8-trust-provenance-and-drift-transparency-spec.md § Step 0 (round-1 codex F-004 amend) Establishes v3.7.1's own inversion manifest separate from the v3.6.7 frozen scope, plus a byte-equivalence SHA gate that asserts the v3.6.7 PATTERN PROTECTION blocks in synthesis_agent / research_architect_agent / report_compiler_agent stay byte-identical to the v3.6.7 base commit (derived via `git log -1 -- scripts/v3_6_7_inversion_manifest.json`). Why: Step 3a will add a "Two-Layer Citation Emission" prompt block to those three agents OUTSIDE their v3.6.7-tagged blocks. The SHA gate keeps that boundary enforceable from PR-1 onward, so any accidental edit inside the v3.6.7 block is caught at CI time. How: - scripts/v3_6_8_inversion_manifest.json — empty entries for now; Step 3a will populate with two-layer-citation invariants. - scripts/check_v3_6_8_pattern_protection.py — reuses v3.6.7 lint's heading-based extractor for byte-equivalent block isolation; resolves default branch via `git symbolic-ref refs/remotes/origin/HEAD` → $GITHUB_DEFAULT_BRANCH → hard-fail; handles shallow CI clones via `git fetch --unshallow`; computes SHA-256 with raw-bytes-minus-BOM. - .github/workflows/spec-consistency.yml — adds `fetch-depth: 0` to checkout (so `git log -1` and `git show <commit>:<path>` see history) + two new lint steps right after the v3.6.7 mutation tests. - scripts/test_check_v3_6_8_pattern_protection.py — 12 tests pinning the contract: happy path + intra-block mutation (FAIL) + extra-block H2 append directly after EOF newline (PASS) + same with extra blank line (FAIL — pins the Step 3a contributor rule) + manifest shape validation + boundary error cases. Verification (local): - baseline: spec-consistency / v3.6.7 protection (12 inv) / v3.6.7 mutation (58/58) / audit-artifact-consistency — all green. - new: SHA gate happy-path PASS; 12/12 mutation tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(v3.7.1 Step 1): D1 frontmatter schema split + trust-chain lint Spec: docs/design/2026-04-30-ars-v3.6.8-trust-provenance-and-drift-transparency-spec.md § 3.1 D1 — Frontmatter trust-strength conflation § Step 1 — Frontmatter schema split Splits the previously-single `verified` field into seven orthogonal trust-chain fields on `literature_corpus_entry.schema.json` so downstream consumers can distinguish source acquisition, source verification against the original artifact, and description provenance. The user-set human-read signal stays user-owned in the §3.6 peer file (NOT entry-stored) per round-1 codex F-005 amend; the entry schema remains `additionalProperties: false`. Schema (additive — backward-compatible with v3.6.4 fixtures): - 7 new optional properties: source_acquired, source_acquisition_date, source_acquisition_path, source_verified_against_original, source_verification_method, description_source, description_last_audit. - 2 new `allOf` branches enforcing spec firm rules #1 and #2: #1: source_verified_against_original=true ⇒ source_acquired=true AND source_verification_method ∈ {codex_audit, manual_grep, vision_check} (round-2 R2-007 amend: 'none' enumerated but FORBIDDEN here) #2: source_acquired=false ⇒ description_last_audit ∈ {null, "none"} - Firm rule #3 (no literal human_read_source / human_read_at on entry) is automatically caught by the existing `additionalProperties: false`. Agent prompts: - deep-research/agents/bibliography_agent.md — adds "Trust-Chain Frontmatter Discipline (v3.7.1+)" H2 with field semantics, three firm rules, and a refusal-on-uncertain rule (do not infer verification from derivative-bibliography agreement; emit verified=false when in doubt). - academic-paper/agents/literature_strategist_agent.md — same H2 framed for the downstream consumer perspective: read-only, surface inconsistencies rather than paper over them. Lint + tests: - scripts/check_v3_6_8_frontmatter_trust_schema.py — defense-in-depth over the schema; produces friendly spec-cited error messages when scanning passports / single entries / bare lists. - scripts/test_check_v3_6_8_frontmatter_trust_schema.py — 20 tests: schema self-consistency + 7-field presence + additionalProperties preserved + positive/negative for each firm rule + payload-shape coverage + v3.6.4 fixture regression. - .github/workflows/spec-consistency.yml — wires lint + pytest into CI. Verification (local): - All 4 baselines green: spec-consistency, v3.6.7 protection (12 inv), v3.6.7 mutation (58/58), audit-artifact-consistency. - v3.6.4 backward compat: existing literature_corpus schema lint green; corpus-consumer-protocol lint green; all 3 adapter fixtures pass new trust-schema lint (no entry currently carries trust fields). - New: 20/20 trust-schema mutation tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 1 cascade): regenerate adapters/overview.md + escape union type pipe CI's `sync_adapter_docs.py --check` step caught two issues from the Step 1 schema additions: 1. The 7 new trust-chain properties on literature_corpus_entry.schema.json needed to cascade into the auto-generated property table at academic-pipeline/references/adapters/overview.md. 2. `description_last_audit: ["string", "null"]` was the first union-typed property in the schema. `_short_type` rendered the union as `null | string`, but Markdown table cells are pipe-delimited, so the row split into extra columns. The fix escapes the separator to `\|` so the union renders as a literal pipe inside the cell. The `_short_type` patch is generic — any future union-typed property will render correctly without re-escaping at the call site. Verification: `python3 scripts/sync_adapter_docs.py --check` PASSES; all PR-1 lints + tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 1 R1): close 2 codex P2 findings on D1 schema Round-1 codex review (`/codex review --base main`) returned 0 P1 + 2 P2. Both findings closed in this commit. P2-1 — Schema rule #2 hole on missing description_last_audit: The `if/then` allOf branch only constrained `description_last_audit` when the property was present, so an entry with `source_acquired: false` that omitted the field passed both schema and lint. Spec § 3.1 firm rule #2 says REQUIRES, which is strict (the field must be present, value must be null or "none"). Schema gains `required: ["description_last_audit"]` under the rule-2 `then` branch; lint mirrors with a friendly "is missing" diagnostic instead of defaulting `entry.get(...)` to None. New test pins the missing-field FAIL case. P2-2 — description_source enum hard-coded v1..v3: Spec § 3.1 yaml uses `bibliography_v<n>` as a template (any non-negative integer n). The schema enum hard-coded v1..v3, so a corpus that follows the documented format with v4+ would fail validation. Switched to a regex `^(original_pdf|bibliography_v[0-9]+|secondary_summary)$` and added three tests: arbitrary revision numbers (v0 / v4 / v17 / v999) PASS, canonical values (original_pdf / secondary_summary / v1) still PASS, unrelated strings (bib_v1 / bibliography_vX / typos) still FAIL. Verification (local): - 24/24 trust-schema tests pass (was 20 before R1 fixes); - 12/12 SHA gate tests + 58/58 v3.6.7 mutation tests still green; - All 4 baseline lints + sync_adapter_docs --check + corpus consumer protocol all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 0 R2): close codex P2 — anti-self-baseline guard on SHA gate Round-2 codex review (`/codex review --base main`) returned 0 P1 + 1 P2 that's effectively P1-grade in scope: the SHA gate could self-baseline. The hole: a PR that mutates `scripts/v3_6_7_inversion_manifest.json` AND a v3.6.7-tagged PATTERN PROTECTION block in the same commit causes `git log -1 -- v3_6_7_inversion_manifest.json` to resolve to that very commit. The base block (`git show <commit>:<file>`) and the HEAD block (read from worktree) then point at the same modified content, the SHA hashes match trivially, and the boundary rule the gate exists to enforce goes silently un-checked. Codex's framing: "the gate hashes modified content as its own baseline." Fix (per spec round-4 R4-002 single-source-of-truth + new round-2 P2 closure): add an anti-self-baseline guard at the front of `check_byte_equivalence`. The guard reads `v3_6_7_inversion_manifest.json` bytes at HEAD and at `merge-base $GITHUB_BASE_REF / origin/<default-branch> HEAD`, and refuses to run the SHA gate if the bytes differ — instructing the contributor to land manifest amendments in a separate PR (under a v3.7+ amendment process) so the next gate run sees the new manifest as its baseline. Edge cases handled: - Manifest absent at PR base → "manifest creation is not a v3.7.1-PR action" - Manifest absent at HEAD but present at base → "deletion is not a v3.7.1-PR action" - Manifest unchanged → guard passes, fall through to `git log -1` derivation - No PR base detectable (local detached state) → guard treated as advisory; CI on the canonical repo catches the attack Verification (local): - New `test_anti_self_baseline_guard_rejects_manifest_mutation_in_pr`: mutate manifest in worktree → lint refuses with the round-2 message. - Pre-existing 12 SHA gate tests stay green; one (manifest-deletion test) refined to accept either the new guard message or the legacy loader error, since the guard now catches deletion earlier with a more precise message. - All 4 baseline lints + sync_adapter_docs + corpus consumer protocol + literature corpus schema + 95 tests across the three v3.6.x suites green. Spec round-9 amendment will document the anti-self-baseline invariant in §388 acceptance criteria; that's a separate spec PR. This commit lands the runtime enforcement first (per `feedback_codex_iterative_spec_review_to_zero.md` discipline — close findings before round-3, regardless of where the spec text catches up). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 0 R3): close codex P2 — include heading prefix in SHA range Round-3 codex review caught a v3.6.7-extractor inheritance bug. Spec § 388 canonical range says "start at the LINE containing `## PATTERN PROTECTION (v3.6.7)`", so the `## ` heading prefix is part of the hashed bytes. But the v3.6.7 lint's `_extract_block` finds the marker via case-insensitive substring search — it returns a slice starting at `PATTERN...`, dropping the heading prefix. That's harmless for v3.6.7's own invariant greps (they search keywords inside the block; prefix doesn't matter), but the v3.7.1 SHA gate inherited the same start position and would have accepted a `## PATTERN PROTECTION (v3.6.7)` → `### PATTERN PROTECTION (v3.6.7)` mutation as byte-equivalent. Spec wording would say the gate had silently approved a heading-level demotion. Fix: `_extract_block_bytes` now wraps the v3.6.7 extractor and walks the start position backward to the start of the marker's line, so the heading prefix bytes are inside the hashed range. End position and termination logic untouched — byte-equivalence to the v3.6.7 extractor is preserved everywhere except the prefix. Two new tests: - `test_heading_prefix_mutation_is_caught`: mutate `##` → `###` on synthesis_agent.md → SHA gate FAILS (was previously a silent pass). - `test_extractor_includes_heading_prefix_bytes`: unit-level check that the extractor returns different bytes for H2 vs H3 input and that the prefix bytes are literally in the returned slice. This closes round-3's only finding (0 P1, 1 P2). The base-commit hash in CI updates because the hash range now includes the previously-elided prefix bytes — `[v3.7.1 SHA gate]` PASS lines show new sha256 prefixes on all 3 protected files, but the comparison itself stays trivially equal at the new commit. Verification (local): - 15/15 SHA gate tests + 24/24 trust schema tests + 58/58 v3.6.7 mutation tests = 97 total green. - All 4 baselines + sync_adapter_docs + corpus consumer protocol + literature corpus schema all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 0 R4): close codex P2 — history-scan in anti-self-baseline guard Round-4 codex review found a touch-and-revert bypass of the round-2 guard. The byte-only check at HEAD vs base lets the following pattern through: commit A: modify v3.6.7 manifest + modify v3.6.7 protected block commit B: revert manifest to original bytes (block edit stays) → HEAD-vs-base manifest BYTES are equal (round-2 guard sees nothing wrong) → `git log -1 -- manifest` still resolves to commit B as the SHA gate's baseline → `git show B:<protected>` returns the modified block content → HEAD reads the same modified block → SHA(B's modified) == SHA(HEAD's modified) → gate trivially passes Codex's framing: "the guard needs to reject any manifest touch in merge-base..HEAD, not just final content changes." Fix: insert a `git log --format=%H merge-base..HEAD -- manifest` history scan as the FIRST check in `_v3_6_7_manifest_unchanged_in_pr`. Any commit that touches the manifest in the PR range — whether the touch is ultimately retained at HEAD or reverted later — produces a non-empty log output and the guard rejects with a list of the offending SHAs. The round-2 byte-comparison stays as a defense-in-depth backstop in case the path-filtered `git log` ever under-reports (corrupted history, filter bug). The "missing at HEAD / missing at base" branches are kept since they catch a class the history scan can't (manifest re-created fresh in this PR). New test: - `test_anti_self_baseline_guard_history_scan_called` patches `_run_git` to inject a synthetic non-empty `git log` result, asserts the guard rejects with the round-2+round-4 closure message. Pre-existing tests: - `test_anti_self_baseline_guard_rejects_manifest_mutation_in_pr` refined to accept either the byte-mismatch message OR the touched-by message, since worktree mutations now fall through to the byte backstop (history is unchanged, only worktree differs). Verification (local): - 16/16 SHA gate tests + 24/24 trust schema tests + 58/58 v3.6.7 mutation tests = 98 total green. - All 4 baselines + sync_adapter_docs + corpus consumer protocol all green. Spec round-9 amendment will document the history-scan invariant alongside the round-2 byte-equality check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 0 R4 followup): pin GITHUB_BASE_REF in guard tests CI on `push` event runs revealed that the round-2 + round-4 anti-self- baseline guard tests assumed PR context. Without `GITHUB_BASE_REF` set (present on `pull_request` events, absent on `push`), the guard's `_detect_pr_base_ref` returns None and falls through to advisory mode ("local detached state"), so the tests' "guard MUST reject" assertions were not exercised on push runs. Fix: both guard tests now `monkeypatch.setenv("GITHUB_BASE_REF", "main")` to force the guard onto its real reject path regardless of trigger event. The `pull_request` event run was already green; this fix unblocks the matching `push` event run on the same commit. No production-code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 1 R6): close codex P2 — strict 'none' literal in firm rule #2 Round-6 codex review caught a spec-vs-impl wording widening I introduced without authority. Spec § 3.1 firm rule #2 (line 120) reads: `source_acquired: false` REQUIRES `description_last_audit: none` That is the literal sentinel string "none". Spec § 3.1 yaml at line 111 lists the value vocabulary as `<round_id> | none` — there is no null alternative. R1 closure had widened this to "null OR 'none'" by mistake. The schema `then` branch carried `oneOf: [null, "none"]`; the lint allowed both. That meant an entry with `source_acquired: false` and `description_last_audit: null` passed CI even though the spec demands the sentinel. Fix: - Schema rule-#2 then-branch: `{type: "string", const: "none"}` only. - Lint rule-#2 check: only literal "none" passes; null is rejected with a "literal sentinel string" diagnostic citing spec § 3.1 line 111. - Top-level `description_last_audit` field type stays `[string, null]` — null is still legal when source_acquired=true and the entry hasn't been audited yet. The tightening is scoped to the rule-#2 case only. Tests: - `test_rule2_acquired_false_with_audit_null_passes` flipped to `..._fails` with positive assertion that schema rejects + lint surfaces the literal-only message. Old name removed (it asserted the now-incorrect lenient behaviour). - All other pre-existing tests still green; the `verified_false_does_not_constrain_method` test fixture already used `description_last_audit: "none"` (string), so it stays compatible. Verification (local): - 24/24 trust schema + 16/16 SHA gate + 58/58 v3.6.7 mutation = 98 green. - All 4 baselines + sync_adapter_docs + corpus consumer + corpus schema green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 1 R7): close codex P2 — agent prompts cascade for R6 strict-'none' Round-7 codex review caught a cascade gap from R6. R6 tightened the schema and lint to require literal "none" (rejecting null) when source_acquired=false, but two agent prompts (bibliography_agent + literature_strategist_agent) still told agents "MUST be null or 'none'". An agent following its own prompt for an unacquired source could emit description_last_audit: null and produce a passport that fails the new CI lint — the documented agent contract diverged from the enforced contract. Per `feedback_cross_model_review_cascade_inconsistency.md`: large revisions need a second-round codex pass to catch downstream cascade, exactly the kind R7 surfaced. Fix: - bibliography_agent.md (Trust-Chain Discipline H2): - Inline yaml comment on description_last_audit clarifies "null only when source_acquired=true; rule-#2 case requires literal 'none'". - Firm rule #2 prose rewritten to require literal "none", cite spec line 120 + line 111 yaml vocabulary, and explain CI lint enforcement. - literature_strategist_agent.md (Trust-Chain Discipline H2): - Same yaml-comment + rewritten firm rule #2. - Adds "such entries fail the trust-chain CI lint" guidance so the consumer agent surfaces the inconsistency rather than silently treating null as acceptable. - literature_corpus_entry.schema.json field-level description: - Clarifies that the field-level `[string, null]` permits both broadly, but the rule-#2 then-branch tightens to literal "none" only when source_acquired=false. Prevents future readers from assuming the field-level wording is the full contract. No production-code logic change in this commit (it's all prompts + schema description text); the rule enforcement landed in R6. Verification (local): - 98 tests green across all three v3.6.x suites; - All 4 baselines + sync_adapter_docs (no cascade — descriptions kept their first-sentence summary intact) + corpus consumer + corpus schema all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 0 R8): close codex P2 — file-level vs block-level BOM stripping Round-8 codex review caught a normalization scope error introduced by R3. R3 wrapped the v3.6.7 extractor with line-start backtracking + a helper called `_normalize_bytes` that stripped a leading UTF-8 BOM from the EXTRACTED block. Spec § Step 0 says "the FILE's BOM (if any) is excluded" — the exclusion is FILE-level (byte 0), not block-level. Because all three current v3.6.7-protected blocks start MID-FILE, inserting U+FEFF immediately before `## PATTERN PROTECTION (v3.6.7)` created a block whose extracted slice started with the BOM bytes, which `_normalize_bytes` then silently stripped, making the HEAD block hash identical to the base hash even though the canonical byte range had changed. Real content mutation, gate said PASS. Codex's framing: "this should only strip a BOM when the block begins at byte 0 (or normalize the full file before extraction)." Fix: - Renamed `_normalize_bytes` → `_strip_file_bom`. Old name kept as a module-level alias only for backward compatibility with anything that imports it; tests use the new name. - `_extract_block_bytes` now takes raw `file_bytes: bytes` (was: a decoded `str`) and applies `_strip_file_bom` BEFORE extraction. This restricts BOM stripping to byte 0 of the file; any BOM that appears later (e.g. inserted before a heading as a hidden mutation) stays in the hashed range and trips the gate. - Two call sites in `check_byte_equivalence` updated to pass raw bytes; intermediate `.decode("utf-8", errors="replace")` calls removed (the function now does the decode internally on BOM-stripped bytes). New tests: - `test_extractor_strips_only_file_level_bom_not_block_level`: unit- level check that file-level BOM (byte 0) IS stripped, but BOM before heading (mid-file) is NOT. - `test_bom_before_heading_attack_caught_by_lint`: end-to-end mutation test that injects BOM right before the v3.6.7 marker on disk and asserts the lint FAILS with BYTE-EQUIVALENCE FAIL. Renamed test (signature change, semantics unchanged): - `test_normalize_strips_bom_only_when_present` → `test_strip_file_bom_only_at_byte_zero`, with an added assertion that mid-input BOM bytes are NOT stripped (pinning the round-8 invariant at the helper level too). Verification (local): - 18/18 SHA gate + 24/24 trust schema + 58/58 v3.6.7 mutation = 100 green. - All 4 baselines + sync_adapter_docs + corpus consumer + corpus schema green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 0 R9): close codex P3 — anchor marker search to heading line Round-9 codex review caught a false-fail risk introduced by R3's substring-based marker search. A v3.7.1 PR may legitimately add prose BEFORE the protected block that mentions `PATTERN PROTECTION (v3.6.7)` — for instance, a "Two-Layer Citation Emission" section's introductory paragraph that explains how new invariants relate to the existing v3.6.7 PATTERN PROTECTION block. The pre-round-9 substring search would have matched the prose mention first, hashed the wrong byte range, and false-failed CI on a valid edit. Codex's framing: "anchor the lookup to the Markdown heading line." Note: this is a P3 finding (not P2/P1). Per `feedback_codex_review_vs_resume_audit_scope.md`, P3-only is ship-OK, but the false-fail risk is real correctness-grade — fixing now keeps the lint usable for legitimate v3.7.1 work. Fix: - Add `import re`. - Replace the substring `text.lower().find(marker.lower())` with an anchored heading regex: `(?im)^[ \t]*#{1,3}[ \t]+<marker>`. The `(?m)` MULTILINE flag makes `^` match line starts; `(?i)` matches the case-insensitive marker per the v3.6.7 lint convention. NO `\b` after the marker (it ends with `)`, a non-word char, so `\b` would not match there). Tests: - New `test_prose_mention_of_marker_does_not_misanchor_extractor`: text containing a prose mention of the marker before the actual heading; extractor must return the heading-anchored block, NOT the prose mention. Asserts the returned bytes start with `## PATTERN PROTECTION (v3.6.7)\\n` and that the prose paragraph's content is not swallowed. - `test_bom_before_heading_attack_caught_by_lint` updated to accept either "BYTE-EQUIVALENCE FAIL" or "marker missing at PR HEAD" as the rejection diagnostic. After R9, BOM bytes injected before the heading break the `^[ \\t]*#{1,3}` shape so the regex misses, and the lint falls into the missing-marker branch — still a hard fail, just a different message. Both are correct. Verification (local): - 19/19 SHA gate (added 1 prose-mention test) + 24/24 trust schema + 58/58 v3.6.7 mutation = 101 total green. - All 4 baselines + sync_adapter_docs + corpus consumer + corpus schema all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v3.7.1 Step 0 R10): close codex P2 — independent block-end search Round-10 codex review caught an incomplete fix from R9. R9 anchored the block START to a Markdown heading line, but `_extract_block_bytes` was still calling the v3.6.7 legacy extractor and using `len(block)` from that result as the slice length. The v3.6.7 extractor uses substring search — when prose before the protected block mentions `PATTERN PROTECTION (v3.6.7)`, it returns the slice starting at the PROSE position. So `len(block)` equalled "prose-to-next-heading", and adding that length to the heading-anchored line_start produced a slice covering the WRONG byte range (truncated or extended depending on relative offsets). CI would falsely fail valid v3.7.1 PRs. Codex's framing: "the slice length comes from the wrong prose-to- heading fragment, so the SHA range is truncated/extended." Fix: stop calling the v3.6.7 legacy extractor entirely. Anchor BOTH endpoints to heading lines: - START: `(?im)^[ \t]*#{1,3}[ \t]+<marker>` (round-9, kept). - END: independent search for the next H1/H2/H3 heading AFTER the marker line, or EOF — `(?m)^[ \t]*#{1,3}[ \t]+`. Mirrors the v3.6.7 lint's heading-to-next-heading-or-EOF termination semantics but starts the END search from the line AFTER the marker, not from the `pos = len(marker)` cursor the substring extractor used. Removed unused import: `_extract_block as _v3_6_7_extract_block` from `check_v3_6_7_pattern_protection`. The v3.6.8 lint no longer delegates any extraction work to the v3.6.7 extractor — it computes the canonical range from scratch using heading-anchored regexes. (The v3.6.7 PROTECTION_BLOCK constant is still imported as the marker text source of truth.) New test: - `test_prose_mention_does_not_truncate_block_range`: prose paragraph mentions marker before the real heading; extractor must return the block from the heading through the next heading (full body), excluding the prose paragraph and the v3.7.1 section's heading. Verification (local): - 20/20 SHA gate (added 1 R10 closure test) + 24/24 trust schema + 58/58 v3.6.7 mutation = 102 total green. - All 4 baselines + sync_adapter_docs + corpus consumer + corpus schema 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>
2026-05-08 06:19:04 +08:00
# Markdown-table cells are pipe-delimited, so a JSON Schema
# union type rendered as `null | string` would split into extra
# columns. Escape the separator with `\|` so GitHub renders it
# as a literal pipe inside the cell.
return r" \| ".join(sorted(x for x in t if x))
if t:
return t
if "oneOf" in prop_def:
return "oneOf"
if "$ref" in prop_def:
return prop_def["$ref"].split("/")[-1]
return "any"
def _short_desc(prop_def: dict) -> str:
d = (prop_def.get("description") or "").strip()
m = re.match(r"(.+?[.!?])(?:\s|$)", d)
return (m.group(1) if m else d) or ""
def build_table(schema: dict, which: str) -> str:
required = set(schema.get("required", []))
props = schema.get("properties", {})
rows = []
for name, prop_def in sorted(props.items()):
is_req = name in required
if which == "required" and not is_req:
continue
if which == "optional" and is_req:
continue
rows.append(f"| `{name}` | {_short_type(prop_def)} | {_short_desc(prop_def)} |")
header = "| Field | Type | Description (first sentence) |\n|---|---|---|"
return header + "\n" + "\n".join(rows) + "\n"
def regenerate_file(content: str, schema: dict) -> str:
for marker_name, which in MARKERS.items():
start = f"<!-- GENERATED:{marker_name}:START -->"
end = f"<!-- GENERATED:{marker_name}:END -->"
if start not in content or end not in content:
continue
table = build_table(schema, which)
pattern = re.compile(re.escape(start) + r".*?" + re.escape(end), re.DOTALL)
content = pattern.sub(f"{start}\n{table}{end}", content)
return content
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--check", action="store_true", help="Exit 1 on drift, do not modify files.")
ap.add_argument("--target", type=Path, default=DEFAULT_TARGET, help="Overview file to update.")
args = ap.parse_args()
if not SCHEMA_PATH.exists():
print(f"ERROR: schema missing at {SCHEMA_PATH}", file=sys.stderr)
return 2
if not args.target.exists():
print(f"ERROR: target missing at {args.target}", file=sys.stderr)
return 2
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
old = args.target.read_text(encoding="utf-8")
new = regenerate_file(old, schema)
if old == new:
return 0
if args.check:
print(
f"DRIFT: {args.target} is out-of-date relative to the schema.",
file=sys.stderr,
)
print("Run `python scripts/sync_adapter_docs.py` to regenerate.", file=sys.stderr)
return 1
args.target.write_text(new, encoding="utf-8")
return 0
if __name__ == "__main__":
sys.exit(main())