Skip to content

Commit b77ea9a

Browse files
fix(spec-drift): close nine defects an adversarial review found in the freshness gate, two of them P1
#237 shipped green with a mutation-tested lock and still carried two P1s. An adversarial review of the merged commit found nine defects; all nine reproduce, all nine are fixed here, each with a regression lock that was mutation-tested by reverting the fix. The review happened because no AI reviewer runs on these repos — Gemini is sunset and Greptile is dark — so #237 had exactly one pair of eyes. That was the actual gap. THE TWO P1s ARE THE SAME BUG THIS GATE EXISTS TO END, RE-ENTERING BY A DOOR THE LOCK DID NOT WATCH F1 — the registry can name a checker in the WRONG MODE. `--surface` is a top-level arg, so `--check --surface X` is accepted, silently ignores the surface, and exits 0 with "OK". Swap `--check-fresh` for `--check` in any `semantic_coverage.checker` and the driver prints an authoritative all-green board — including `plugin-marketplaces`, which has real outstanding findings. Verified: check-surface-registry.py returned exit 0 on that registry, and so did the test suite, because both validated only that `checker[0]` is an existing FILE. That matters here specifically because the registry is the file humans are told to edit ("flipping a surface to `failing` is a one-line registry edit"). The next person who flips a surface, sees red, and "fixes" the flag gets a green board and no gate objects. Fixed in two layers: the registry gate now pins each checker's freshness-mode flag (and rejects any other mode flag, `--write` included — that would have the gate MUTATE the baseline it guards), and the extractors now REJECT `--surface` outside `--check-fresh` rather than ignoring it. Chose an explicit in-script allowlist over letting the registry self-declare its own mode, because a config that attests to its own correctness is the thing being defended against. F2 — `plugin-manifest` asserted a silently TRUNCATED closed enum. Its `_enum_from_description` had no closedness guard at all: "One of `command` (Claude Code v2.1.200+), `http`, or `mcp_tool`." -> ['command'] "One of `command`, `http`, or {/* min-version: … */}`mcp_tool`." -> ['command','http'] This is the same clause-truncation that broke permissionMode, but the three siblings behave OPPOSITELY under it: agent-definition and hook-config decline (residue guard), plugin-manifest asserted confident nonsense. A short enum is worse than an absence — the projection is the field-diff baseline, so the next report reads as a legitimate value REMOVAL and sends a human to reconcile a phantom. Not hypothetical: the captured plugins-reference page already carries MDX comments in table cells; it just has not landed on a `One of` row yet. Fixed, and the root cause fixed properly rather than papered over: a period followed by a digit is a VERSION NUMBER, not a sentence end. That one shared rule (`captured_source.clause_end`) turns all three cases correct — the closed set stays closed, and both truncating cases now recover all three values. THE OTHER SEVEN F3/F9 the driver exited "CLEAN (exit 0) over 0 field-level surface(s)" when the map was empty — flatly contradicting its own docstring. Now INOPERABLE, plus a declared `semantic_coverage_floor` so SHRINKING coverage is a reviewed edit rather than a quietly smaller green board. F4 bare next()/json.loads raise on a malformed capture -> python exits 1 -> the driver read DRIFT, so the watcher would open a RECONCILIATION issue for a PARSER breakage. Now INOPERABLE, preserving the 0/1/2 distinction. F5 a 30-entry enum gaining one member rendered two sides byte-identical for all 400 visible characters — the reader saw a differing byte COUNT. That is the single most likely real drift on `claude-hooks`, an ENFORCED surface, i.e. unreadable exactly when it mattered. Scalar lists now diff element-wise. F6 `True == 1` and `1 == 1.0`, so a bool becoming an int diffed to nothing. Type changes are now always findings. F7 the registry gate died with `TypeError: unhashable type` on a list-valued `enforcement` instead of naming the problem. F8 `re.DOTALL` on the MDX stripper is inert today and loaded tomorrow — an unbalanced `{/*` opening a 16-line block exists in the captured mcp-config doc. Now line-bounded, with the reason recorded. PLUS A GAP THE REVIEW SURFACED IN ITS OWN EVIDENCE Nothing verified that a vendor-meta.json describes the bytes on disk. `--check` proves the projection derives from the FILES and never reads sha256 at all — so a hand re-vendor recording a wrong hash passed every gate while the provenance quietly described a file that was not there. Three reference docs were re-vendored BY HAND during this work, each editing sha256 and bytes in a text editor, so that is the concrete shape of the risk. New scripts/check-vendor-meta-integrity.py verifies every declared file exists, its sha256 and byte count match, and no UNDECLARED file sits in a capture dir (an input with no provenance is one vendor-meta edit from being parsed as authority). Wired into ci.yml + the scripts-tests job. Demonstrated: corrupting a recorded sha fails the new gate while `--check` still prints "OK". VERIFICATION - All five extractors --check + --self-test OK, and NO projection changed — the parser fixes are provably behaviour-preserving on today's pages. - pytest scripts/tests/ -q: 226 passed (33 new). - Mutation-tested by reverting each fix in turn; every one turns the suite red: F1a 4 failed · F1b 2 · F2 2 · F3 1 · F5 1 · F6 4 · F8 2. - The mutation sweep also caught a REAL stale expectation in the #237 suite (test_each_diff_class_is_detected expected CHANGED_VALUE where element-wise diffing now correctly emits LIST_CHANGED). Updated the expectation, not the behaviour. - check-surface-registry OK; check-vendor-meta-integrity OK (5 captures, 23 files); projection-freshness exit 0; detector-health + sak-dashboard --check OK; audit-harness verify OK. Refs #237
1 parent 98dd72e commit b77ea9a

15 files changed

Lines changed: 875 additions & 25 deletions

.github/workflows/ci.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,26 @@ jobs:
9191
fi
9292
9393
# Offline + deterministic: the surface registry must equal the watcher's
94-
# SOURCES array, every extractor must exist, and every monitored surface
95-
# must carry a valid capture config (kind, expect_regex, min_bytes, ext).
94+
# SOURCES array, every extractor must exist, every monitored surface must
95+
# carry a valid capture config (kind, expect_regex, min_bytes, ext), and
96+
# every surface must state its semantic-coverage level — including, for
97+
# field-level surfaces, that the declared checker runs in its FRESHNESS
98+
# mode. Pinning the mode is the point: a checker named with `--check`
99+
# instead of `--check-fresh` compares frozen against frozen and prints an
100+
# authoritative green, which is precisely the failure this gate exists for.
96101
- name: Upstream-surface registry consistency
97102
run: python3 scripts/check-surface-registry.py
98103

104+
# Offline + deterministic: every vendor-meta.json must describe the bytes
105+
# actually on disk. Nothing else checks this — `--check` proves the
106+
# projection derives from the FILES and never reads sha256/bytes at all, so
107+
# a hand re-vendor that recorded a wrong hash passed every gate while the
108+
# provenance quietly described a file that was not there. Three reference
109+
# docs were re-vendored by hand during the freshness work, so this is the
110+
# concrete shape of the risk, not a theoretical one.
111+
- name: Vendored capture provenance integrity
112+
run: python3 scripts/check-vendor-meta-integrity.py
113+
99114
# Offline + deterministic: fixtures exercise all 6 fetch-taxonomy statuses
100115
# (FETCH_OK|UNREACHABLE|MOVED|RATELIMITED|SHAPE_CHANGED|TRUNCATED) plus the
101116
# tier-1 append/dedup, tier-2 FETCH_OK-only gating (052-AT-SPEC), and the

.github/workflows/python-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,5 @@ jobs:
147147
- run: pip install --no-cache-dir 'pytest>=8' pyyaml jsonschema
148148
- name: Run scripts/ unit tests
149149
run: pytest scripts/tests/ -v --tb=short
150+
- name: Vendored capture provenance integrity
151+
run: python3 scripts/check-vendor-meta-integrity.py

.harness-hash

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ add245f35fb0dbd63d5ab475020bb367b53be4c611eb509d614c8a1f5ca8ea32 .audit-harness
55
0ffea346a8c310677f2176e9f367370c8b308f82633bf62ccd00a95985e09469 .audit-harness/scripts/escape-scan.sh
66
d29a2e9b94f7fd94f4f8953314323f96ca424cbaa6a985f6783b48ee2feae0f8 .audit-harness/scripts/gherkin-lint.sh
77
9c588a980e89dbc9ea2bcf0992daff51ec72b926ecb1644e21838b9fc5c2883d .audit-harness/scripts/harness-hash.sh
8-
a56b9ef535b85725c5740f1d5287ad12f96d7c5fd0fd62aa64ff1938892e950c .github/workflows/ci.yml
8+
5b912f59eeef3c9b5f7de98d8ae27a0d8b2eeb03411f34d9fa0e205d061b7f7f .github/workflows/ci.yml
99
7342d70d8e4a3531262c71127b6c285406807b677cc8f404c580fed9ed5349f7 .github/workflows/codeql.yml
1010
a47735f905b1463c148d0b71f5db852beec873782be18b6d1e7a55ec54a1baf4 .github/workflows/doc-quality.yml
1111
592a86e79d00a0bbbaed2e20d2fba91aba6744baa4cfb8222ea41fe422543fce .github/workflows/e2e-integration.yml
1212
43194d2f4ce1c37f70b4db099262149a4cd28f541eaa363f73adf05801b89935 .github/workflows/harness-hash-verify.yml
1313
b0302fc487734c9d925accc83f58ac86587eb991ec35d675c43282aebf295ecf .github/workflows/leading-indicator-watch.yml
1414
87e8277670edd32734871ea373f37395329a19a572e9ccc39a9af3789087ecff .github/workflows/lint.yml
1515
64d1e82fe68d531d30f5f4815af7cf852c564c5c406d3db47329cacbed8e421b .github/workflows/partner-name-guard.yml
16-
f2c7225b9755299a3864584e8037f35cc74d4a1de890801f2f35605e0572e242 .github/workflows/python-tests.yml
16+
60ffea7476e7cb9f3fb8ac47e6ca2ebf109eb708960b802e83ce821089671d63 .github/workflows/python-tests.yml
1717
45fbc5b54171b6e10052a69894a1e0a6955013e7d88fa16706a0ed55eae830f4 .github/workflows/release.yml
1818
db162cbe380e2d61bbcc820cdd5a89807ca1a7cf221b8f5e28ef3c0ce9ed1767 .github/workflows/schema-drift.yml
1919
a71c2bf4169af59d3708009fc95d7d097fdcb68f3d4a70337e8578f93c085279 .github/workflows/sign-dogfood-bundle.yml

scripts/check-surface-registry.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,35 @@
4242
_COVERAGE_STATUSES = {"field-level", "byte-hash-only"}
4343
_ENFORCEMENTS = {"failing", "report-only"}
4444

45+
# Which flag makes each checker compare against the CAPTURED tree, i.e. which flag
46+
# actually performs a FRESHNESS check rather than a self-consistency one.
47+
#
48+
# Validating only that `checker[0]` is an existing file is not enough, and the gap
49+
# is not theoretical: swapping `--check-fresh` for `--check` on any extractor
50+
# re-arms the exact frozen-vs-frozen bug this machinery exists to end, and the
51+
# driver then prints an authoritative all-green board — including for surfaces
52+
# with real outstanding findings. `--surface` is a top-level arg, so the wrong
53+
# mode accepts it and silently ignores it; nothing else in the chain objects.
54+
#
55+
# The registry is precisely the file humans are told to edit ("flipping a surface
56+
# to `failing` is a one-line registry edit"), so the next person who sees red and
57+
# "fixes" the flag would get a green board. Hence: pin the SEMANTICS of the argv,
58+
# not just its head. A checker absent from this table is an error — adding one is
59+
# a deliberate, reviewed act.
60+
_FRESH_MODE_FLAG = {
61+
# spec-projection-diff's --check reads specs/_vendor/<surface>/snapshot<ext>
62+
# (repointed in #234); it has no separate --check-fresh.
63+
"scripts/spec-projection-diff.py": "--check",
64+
"scripts/extract-agent-definition-projection.py": "--check-fresh",
65+
"scripts/extract-hook-config-projection.py": "--check-fresh",
66+
"scripts/extract-marketplace-catalog-projection.py": "--check-fresh",
67+
"scripts/extract-plugin-manifest-projection.py": "--check-fresh",
68+
}
69+
70+
# Mode flags a checker must never carry instead of (or in addition to) its fresh
71+
# mode. `--write` would have the gate MUTATE the baseline it is meant to guard.
72+
_MODE_FLAGS = {"--check", "--check-fresh", "--extract", "--write", "--self-test", "--diff", "--list", "--strict"}
73+
4574

4675
def _check_semantic_coverage(name: str, surface: dict, problems: list[str]) -> None:
4776
"""Validate one surface's semantic_coverage block (projection-freshness.py contract)."""
@@ -74,11 +103,30 @@ def _check_semantic_coverage(name: str, surface: dict, problems: list[str]) -> N
74103
problems.append(f"{name}: field-level coverage needs `checker` as a non-empty list of argv strings")
75104
elif not os.path.isfile(os.path.join(REPO_ROOT, checker[0])):
76105
problems.append(f"{name}: semantic_coverage.checker script not found: {checker[0]}")
77-
if cov.get("enforcement") not in _ENFORCEMENTS:
106+
elif checker[0] not in _FRESH_MODE_FLAG:
107+
problems.append(
108+
f"{name}: '{checker[0]}' is not a registered freshness checker. Add it to _FRESH_MODE_FLAG in "
109+
"this script, naming the flag that makes it read the CAPTURED tree — a checker whose mode is "
110+
"unpinned can silently compare frozen against frozen."
111+
)
112+
else:
113+
required = _FRESH_MODE_FLAG[checker[0]]
114+
supplied = [a for a in checker[1:] if a in _MODE_FLAGS]
115+
if supplied != [required]:
116+
problems.append(
117+
f"{name}: checker must run '{checker[0]}' in its freshness mode '{required}', got mode flag(s) "
118+
f"{supplied or 'none'}. A checker in the wrong mode compares frozen against frozen and reports "
119+
"an authoritative green — the exact failure this gate exists to detect."
120+
)
121+
122+
enforcement = cov.get("enforcement")
123+
# str() first: a list/dict here is unhashable and `in` would raise TypeError,
124+
# killing the gate with a traceback instead of naming the problem.
125+
if not isinstance(enforcement, str) or enforcement not in _ENFORCEMENTS:
78126
problems.append(
79-
f"{name}: semantic_coverage.enforcement '{cov.get('enforcement')}' not one of {sorted(_ENFORCEMENTS)}"
127+
f"{name}: semantic_coverage.enforcement {enforcement!r} not one of {sorted(_ENFORCEMENTS)}"
80128
)
81-
if cov.get("enforcement") == "report-only" and not isinstance(cov.get("note"), str):
129+
if enforcement == "report-only" and not isinstance(cov.get("note"), str):
82130
problems.append(f"{name}: report-only coverage needs a `note` saying what is pending and when it flips")
83131

84132

@@ -173,6 +221,24 @@ def main() -> int:
173221
field_level = sum(
174222
1 for s in reg_surfaces.values() if (s.get("semantic_coverage") or {}).get("status") == "field-level"
175223
)
224+
# A floor, so shrinking semantic coverage is a visible edit rather than a
225+
# quietly smaller green board. Without it, "the map got smaller" and "the map
226+
# is clean" look identical downstream.
227+
floor = (reg.get("semantic_coverage_floor") or {}).get("field_level")
228+
if not isinstance(floor, int) or isinstance(floor, bool) or floor < 1:
229+
problems.append("registry: semantic_coverage_floor.field_level must be a positive integer")
230+
elif field_level < floor:
231+
problems.append(
232+
f"registry: {field_level} field-level surfaces is below the declared floor of {floor}. "
233+
"Semantic coverage SHRANK. If that is intended, lower the floor in the same change so the "
234+
"reduction is reviewed."
235+
)
236+
if problems:
237+
print(f"surface-registry consistency: {len(problems)} PROBLEM(S):")
238+
for problem in problems:
239+
print(f" - {problem}")
240+
print("\nFix: edit BOTH spec-drift-check.sh SOURCES and the registry in the same change.")
241+
return 1
176242
print(
177243
f"surface-registry consistency: OK — {len(reg_surfaces)} surfaces, registry == watcher "
178244
f"SOURCES, all extractors defined, all capture configs valid, all semantic-coverage levels "
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
"""Verify every vendor-meta.json records the bytes actually on disk.
3+
4+
WHY THIS EXISTS
5+
---------------
6+
`vendor-meta.json` is the provenance record for a deep capture: per file, the
7+
source URL, the upstream commit where applicable, and the sha256 + byte count of
8+
the exact bytes vendored. Every downstream argument about what upstream said, and
9+
every re-vendor decision, rests on that record being true.
10+
11+
Nothing checked it. Each extractor's `--check` proves the projection is a faithful
12+
derivation of the FILES, and `--self-test` proves the anchors parse — neither reads
13+
`sha256` or `bytes` at all. So a hand re-vendor that copied a new page in but
14+
recorded the wrong hash (or forgot to update it) passed every gate in the repo
15+
while the provenance quietly described a file that no longer existed.
16+
17+
That is not a hypothetical shape: three reference docs were re-vendored BY HAND
18+
during the freshness work, each one editing sha256 and bytes in a text editor.
19+
A silent provenance lie is worse than a stale capture, because the stale capture
20+
is at least honestly labelled.
21+
22+
WHAT IT CHECKS, for every specs/_vendor/upstream/<contract>/vendor-meta.json:
23+
24+
1. every declared file exists;
25+
2. its sha256 matches the bytes on disk;
26+
3. its `bytes` matches the real size;
27+
4. every file in the directory is DECLARED (an undeclared file is an
28+
unprovenanced input the extractor could silently start parsing);
29+
5. the required top-level keys are present.
30+
31+
Stdlib only. Offline. Read-only.
32+
33+
Exit 0 = every capture's provenance is true; 1 = a mismatch; 2 = usage/parse error.
34+
35+
Usage:
36+
check-vendor-meta-integrity.py [--vendor-root DIR]
37+
"""
38+
39+
from __future__ import annotations
40+
41+
import argparse
42+
import hashlib
43+
import json
44+
import os
45+
import sys
46+
47+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
48+
DEFAULT_VENDOR_ROOT = os.path.join(REPO_ROOT, "specs", "_vendor", "upstream")
49+
50+
# Files that live beside a capture but are documentation ABOUT it, not inputs to it.
51+
NOT_CAPTURE_INPUTS = {"vendor-meta.json", "projection.json", "projection.v1.json", "PROVENANCE.md"}
52+
53+
REQUIRED_META_KEYS = ("contract", "spec_version", "files")
54+
55+
56+
def _sha256(path: str) -> tuple[str, int]:
57+
digest = hashlib.sha256()
58+
size = 0
59+
with open(path, "rb") as fh:
60+
while chunk := fh.read(1 << 20):
61+
digest.update(chunk)
62+
size += len(chunk)
63+
return digest.hexdigest(), size
64+
65+
66+
def check_capture(capture_dir: str, problems: list[str]) -> int:
67+
"""Verify one capture directory. Returns the number of files checked."""
68+
name = os.path.basename(capture_dir)
69+
meta_path = os.path.join(capture_dir, "vendor-meta.json")
70+
try:
71+
with open(meta_path, encoding="utf-8") as fh:
72+
meta = json.load(fh)
73+
except (OSError, json.JSONDecodeError) as exc:
74+
problems.append(f"{name}: cannot read vendor-meta.json: {exc}")
75+
return 0
76+
77+
for key in REQUIRED_META_KEYS:
78+
if key not in meta:
79+
problems.append(f"{name}: vendor-meta.json is missing required key '{key}'")
80+
81+
declared: set[str] = set()
82+
checked = 0
83+
for entry in meta.get("files", []):
84+
filename = entry.get("file")
85+
if not filename:
86+
problems.append(f"{name}: a files[] entry has no 'file' key")
87+
continue
88+
declared.add(filename)
89+
path = os.path.join(capture_dir, filename)
90+
if not os.path.isfile(path):
91+
problems.append(f"{name}/{filename}: declared in vendor-meta.json but MISSING on disk")
92+
continue
93+
94+
actual_sha, actual_bytes = _sha256(path)
95+
checked += 1
96+
recorded_sha = entry.get("sha256")
97+
if recorded_sha and recorded_sha != actual_sha:
98+
problems.append(
99+
f"{name}/{filename}: sha256 MISMATCH — vendor-meta records {recorded_sha[:16]}…, "
100+
f"file is {actual_sha[:16]}…. The provenance record describes bytes that are not there."
101+
)
102+
elif not recorded_sha:
103+
problems.append(f"{name}/{filename}: no sha256 recorded — the capture has no verifiable provenance")
104+
recorded_bytes = entry.get("bytes")
105+
if isinstance(recorded_bytes, int) and recorded_bytes != actual_bytes:
106+
problems.append(
107+
f"{name}/{filename}: bytes MISMATCH — vendor-meta records {recorded_bytes}, file is {actual_bytes}"
108+
)
109+
110+
# An UNDECLARED file in a capture dir is an input with no provenance at all.
111+
# The extractors select inputs by vendor-meta `role`, so such a file is inert
112+
# today — and one vendor-meta edit away from being parsed as authority.
113+
on_disk = {
114+
f
115+
for f in os.listdir(capture_dir)
116+
if os.path.isfile(os.path.join(capture_dir, f)) and f not in NOT_CAPTURE_INPUTS
117+
}
118+
for orphan in sorted(on_disk - declared):
119+
problems.append(
120+
f"{name}/{orphan}: present in the capture directory but NOT declared in vendor-meta.json — "
121+
"an input with no provenance record"
122+
)
123+
return checked
124+
125+
126+
def main() -> int:
127+
parser = argparse.ArgumentParser(description="Verify vendored capture provenance against the bytes on disk.")
128+
parser.add_argument("--vendor-root", default=DEFAULT_VENDOR_ROOT, help="root holding <contract>/ capture dirs")
129+
args = parser.parse_args()
130+
131+
if not os.path.isdir(args.vendor_root):
132+
print(f"ERROR: vendor root not found: {args.vendor_root}", file=sys.stderr)
133+
return 2
134+
135+
captures = sorted(
136+
os.path.join(args.vendor_root, d)
137+
for d in os.listdir(args.vendor_root)
138+
if os.path.isfile(os.path.join(args.vendor_root, d, "vendor-meta.json"))
139+
)
140+
if not captures:
141+
# An empty sweep reporting success is the same failure class this repo
142+
# keeps finding: a gate that checked nothing must not look like a pass.
143+
print(f"ERROR: no captures found under {args.vendor_root} — this gate verified NOTHING.", file=sys.stderr)
144+
return 2
145+
146+
problems: list[str] = []
147+
total = sum(check_capture(c, problems) for c in captures)
148+
149+
if problems:
150+
print(f"vendor-meta integrity: {len(problems)} PROBLEM(S):")
151+
for problem in problems:
152+
print(f" - {problem}")
153+
print(
154+
"\nFix: re-derive the record from the bytes, never the other way round. "
155+
"After replacing a vendored file, recompute sha256 + bytes and re-run the extractor's --write."
156+
)
157+
return 1
158+
159+
print(
160+
f"vendor-meta integrity: OK — {len(captures)} capture(s), {total} vendored file(s); "
161+
"every sha256 and byte count matches the bytes on disk, and every file is declared."
162+
)
163+
return 0
164+
165+
166+
if __name__ == "__main__":
167+
sys.exit(main())

scripts/extract-agent-definition-projection.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@
104104
# never a value, and their embedded version number contains periods — which
105105
# silently truncated the permissionMode value clause mid-list and dropped the
106106
# very value this projection needs to record. Stripped before clause analysis.
107-
_MDX_COMMENT = re.compile(r"\{/\*.*?\*/\}", re.DOTALL)
107+
# NOT re.DOTALL, deliberately. Applied only to a single table-cell description,
108+
# where every observed marker is a one-line min-version/max-version note. An
109+
# unbalanced `{/*` opening a 16-line commented-out block exists in the wild
110+
# (specs/_vendor/upstream/mcp-config/claude-code-mcp.md), so if this is ever
111+
# lifted to document scope DOTALL + non-greedy would silently swallow real
112+
# content. Keeping it line-bounded fails visibly instead.
113+
_MDX_COMMENT = re.compile(r"\{/\*[^\n]*?\*/\}")
108114

109115

110116
# ── Extraction: the reference doc (the spec — there is no machine schema) ───
@@ -180,7 +186,8 @@ def _enum_from_description(desc: str) -> tuple[list[str] | None, list[str] | Non
180186
if idx == -1:
181187
continue
182188
clause = desc[idx + len(marker) :]
183-
stop = min((j for j in (clause.find(s) for s in stops) if j != -1), default=-1)
189+
candidates = [captured_source.clause_end(clause) if s == "." else clause.find(s) for s in stops]
190+
stop = min((j for j in candidates if j != -1), default=-1)
184191
if stop != -1:
185192
clause = clause[:stop]
186193
# Order-preserving dedupe: a value list is a SET, and an alias clause
@@ -475,7 +482,23 @@ def cmd_check_fresh(vendor_dir: str, surface: str | None = None) -> int:
475482
print(f"{label}: INOPERABLE — cannot read {committed_path}: {exc}", file=sys.stderr)
476483
return 2
477484

478-
fresh = build_projection(vendor_dir, reference_doc_path=snapshot)
485+
try:
486+
fresh = build_projection(vendor_dir, reference_doc_path=snapshot)
487+
except SystemExit as exc:
488+
# A deliberate anchor failure already exits 2; keep that code.
489+
return exc.code if isinstance(exc.code, int) else 2
490+
except Exception as exc: # noqa: BLE001 - any parse breakage is INOPERABLE, never drift
491+
# Bare next()/json.loads() inside the extractors raise StopIteration /
492+
# JSONDecodeError on a malformed capture. Uncaught, python exits 1, which
493+
# the driver reads as DRIFT — so the watcher would open a RECONCILIATION
494+
# issue for a PARSER breakage. Reconciling a phantom drift is worse than
495+
# no signal, which is the distinction the 0/1/2 split exists to protect.
496+
print(
497+
f"{label}: INOPERABLE — the extractor could not parse the captured page "
498+
f"({os.path.relpath(snapshot, REPO_ROOT)}): {type(exc).__name__}: {exc}",
499+
file=sys.stderr,
500+
)
501+
return 2
479502
context = (
480503
f"committed projection vs the captured '{surface}' page "
481504
f"({os.path.relpath(snapshot, REPO_ROOT)}, standing in for {doc_file})"
@@ -807,6 +830,12 @@ def main() -> int:
807830
)
808831
args = parser.parse_args()
809832

833+
# --surface only means anything in --check-fresh. Accepting and IGNORING it
834+
# in another mode is what let a registry entry name `--check --surface X` and
835+
# still exit 0 with an authoritative "OK", comparing frozen against frozen.
836+
if args.surface is not None and not args.check_fresh:
837+
parser.error("--surface applies only to --check-fresh; in any other mode it would be silently ignored")
838+
810839
if args.self_test:
811840
return cmd_self_test(args.vendor_dir)
812841
if args.check:

0 commit comments

Comments
 (0)