diff --git a/documentation/tests/wire-contract-gate-2026-08-08/BEFORE.md b/documentation/tests/wire-contract-gate-2026-08-08/BEFORE.md index 72571e0..9096486 100644 --- a/documentation/tests/wire-contract-gate-2026-08-08/BEFORE.md +++ b/documentation/tests/wire-contract-gate-2026-08-08/BEFORE.md @@ -75,3 +75,33 @@ exit=1 SELFTEST OK — the gate convicts a planted unreachable tag and the plant is the only difference. exit=0 ``` + +--- + +## ⚠ The search implementation changed AFTER this capture, and the capture was re-verified + +The run above used `grep -rnE --include=…` to test whether a tag occurs in the receiving repo. That +**works on a workstation and returns nothing on the CI runner**, whose image carries python3 and git +and deliberately little else — its `grep` does not support `--include`. Empty stdout was then read as +"the tag is absent", so the gate convicted **all 174** checked tags and CI went red while the +pre-push hook was green (runs 260–262). + +That is the gate silently reading a tool failure as a finding, which is worse than no gate, and it is +the error-swallowing this repo's rules forbid. The search is now **pure Python**: one token index per +receiving repo, no subprocess, no external dependency. + +**This capture was NOT re-generated — it is re-verified**, which is the stronger claim. Every one of +the 40 fields recorded above was re-tested against the new implementation: + +``` +fields recorded in BEFORE.md: 40 +agree=40 disagree=0 +``` + +— i.e. the new implementation finds exactly the four this session fixed (`operator_key_configured`, +`wg_handshake_age_s`, `healed_at`, `escrow_stale`) present, and the other 36 still absent. **The +number 40 stands under both implementations**; only the mechanism and its portability changed. + +The whole-token property also survives by construction: a token index treats `healed_at` and +`privsep_healed_at` as distinct tokens, so the substring false negative that the control caught +cannot come back. diff --git a/scripts/wire_contract_gate.py b/scripts/wire_contract_gate.py index 216070e..1ebf096 100644 --- a/scripts/wire_contract_gate.py +++ b/scripts/wire_contract_gate.py @@ -59,7 +59,6 @@ gate must publish its holes. import os import re import shutil -import subprocess import sys import tempfile @@ -338,23 +337,50 @@ def walk(by_dir, by_name, start_dir, start_type, seen=None, prefix=""): return out -def receiver_has(tag, repo_root): - """Whole-token match, NOT substring. +TOKEN_RE = re.compile(r"[A-Za-z0-9_]+") - A plain `grep -F healed_at` also matches `privsep_healed_at`, so a genuinely dropped field is - reported as received. That false NEGATIVE was found by running this gate's control (R-260's - list) and noticing one known field missing from the output — which is the whole argument for - making a gate re-find the instances it was written for before trusting it. + +def receiver_tokens(repo_root): + """Every `[A-Za-z0-9_]+` token in the receiver's PRODUCTION Go and templates, as a set. + + WHOLE-TOKEN, NOT SUBSTRING. A substring test reports a genuinely dropped field as received: + `healed_at` occurs inside `privsep_healed_at`. That false negative was caught by this gate's own + control — R-260 named `healed_at`, so its absence from the first run's output was the tell. + + PURE PYTHON, NO `grep`. The first version shelled out to `grep -rnE --include=...`, which works + on a workstation and returns NOTHING on the CI runner, whose image carries python3 and git and + deliberately little else — its grep does not support `--include`. Empty stdout was then read as + "the tag is absent", so the gate convicted ALL 174 checked tags and CI went red while the + pre-push hook was green. **A gate that silently reads a tool failure as a finding is worse than + no gate**, and swallowing that error is the exact thing this repo's rules forbid. Reading the + files here removes the dependency, is portable, and is one pass instead of ~350 subprocesses. + + Test files and `testdata/` are excluded deliberately: a tag present only in a fixture is not + decodable by production — and that is R-262 exactly, where the cross-repo golden carries two + fields no hub struct reads. """ - pattern = r"(^|[^A-Za-z0-9_])" + re.escape(tag) + r"([^A-Za-z0-9_]|$)" - r = subprocess.run( - ["grep", "-rnE", "--include=*.go", "--include=*.html", pattern, repo_root], - capture_output=True, text=True) - for line in r.stdout.splitlines(): - if "_test.go" in line or "/testdata/" in line: - continue - return True - return False + toks = set() + n = 0 + for dp, dn, fn in os.walk(repo_root): + dn[:] = [d for d in dn if d not in (".git", "vendor", "node_modules", "testdata")] + for f in fn: + if f.endswith("_test.go"): + continue + if not (f.endswith(".go") or f.endswith(".html")): + continue + p = os.path.join(dp, f) + try: + with open(p, encoding="utf-8", errors="replace") as fh: + toks.update(TOKEN_RE.findall(fh.read())) + except OSError as e: + # never swallowed: an unreadable source file makes the answer unknown, not "absent" + die("wire-contract gate INCONCLUSIVE: cannot read %s: %s" % (p, e)) + n += 1 + if n == 0: + die("wire-contract gate INCONCLUSIVE: no production .go/.html found under %s — a receiver " + "with no source cannot be searched, and an empty search is not evidence of absence." + % repo_root) + return toks def run(root_override=None, quiet=False): @@ -368,6 +394,8 @@ def run(root_override=None, quiet=False): " This gate compares two repositories; it cannot pass without both." % (label, path)) indexes = {k: build_index(v) for k, v in repos.items()} + # one pass per receiving repo, not one subprocess per tag + rtokens = {k: receiver_tokens(v) for k, v in repos.items()} convictions = [] checked = skipped = 0 @@ -396,7 +424,7 @@ def run(root_override=None, quiet=False): skipped += 1 continue checked += 1 - if not receiver_has(tag, repos[receiver]): + if tag not in rtokens[receiver]: missing.append((tag, dotted)) if missing: convictions.append((label, receiver, missing))