From 44b5ec96c1cb7d93da7050ac45c28616263a223d Mon Sep 17 00:00:00 2001 From: Jonathan Borduas Date: Sat, 22 Aug 2026 02:38:08 +0100 Subject: [PATCH] check-tools-index: say why the two counts differ, instead of leaving a reader to subtract them Found by TEAMLEAD on the merged #569. The gate prints "declared population: 57" and "instruments on disk: 55" with nothing between them saying they are different nouns, so a reader subtracts and gets 2, four lines above rc=0. That is the SHAPE of a verdict contradicting its own report. It is not one. TEAMLEAD had to run comm to learn the 2 are estate-provenance.py and memory-index-check.py, both in tools/QUARANTINE.txt. The gate now computes the set difference itself and names each file with its reason: quarantined, not .py, or outside the index population. Computed, not asserted, so it stays true as the sets move. My own independent check of TEAMLEAD's gap disagreed with theirs -- I got 1 (merge-watch.sh) -- and I was wrong. I had compared the README's .py+.sh set against my own .py-only set with no quarantine filter, which is a THIRD reading of the noun and exactly the disease #345 records. Measuring all three sets by name instead of differencing counts settled it in one step, and that is the same lesson the reconciliation line now encodes for readers. read_ack returns (set, why-void), not a set. The first draft tested `n in ack` against the TUPLE, which answers False for every name and would have printed a confidently wrong reason for both files. Caught by inspecting the return shape before running it, not by the output looking wrong -- it would not have. Controls: real tree 0; the drift catch still fires (declared 55 vs command 57 exits 1); --selftest 0; check-orientation, check-goal-conformance and gate-selftests all 0. Filed by ARCHITECT, session c83ecf77. Found by TEAMLEAD. Refs #569, #345. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RTX4A8KkC9cRRrt17yzjWQ --- scripts/check-tools-index.py | 47 ++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/scripts/check-tools-index.py b/scripts/check-tools-index.py index 7a8c070..f95f1a6 100755 --- a/scripts/check-tools-index.py +++ b/scripts/check-tools-index.py @@ -400,20 +400,24 @@ def declared_population(root, text): # against the repository, not smuggled into a function meant to work on any tree. if not cmd_m and not row_m: return None, [" ---- tools/README.md declares no population — that leg NOT CHECKED", - " (a tree that never declared one is not drifting; see selftest)"] + " (a tree that never declared one is not drifting; see selftest)"], None if not cmd_m or not row_m: missing = "the published command" if not cmd_m else "the ⇐ THE DECLARED POPULATION row" - return 2, [f" VOID a population IS declared but {missing} is absent — established nothing"] + return 2, [f" VOID a population IS declared but {missing} is absent — established nothing"], None cmd, beside = cmd_m.group(1), int(cmd_m.group(2)) label, declared = row_m.group(1).strip(), int(row_m.group(2)) try: - r = subprocess.run(["sh", "-c", cmd + " | wc -l"], cwd=str(root), + r = subprocess.run(["sh", "-c", cmd], cwd=str(root), capture_output=True, text=True, timeout=20) - got = int(r.stdout.strip()) + # ⚠ NAMES, not just a count. TEAMLEAD read "declared 57" and "instruments on disk: 55" + # four lines apart and did the subtraction; resolving 57−55 cost them a set difference + # this function could have printed. A gate that makes a reader compute is half a gate. + declared_names = sorted({x.split("/")[-1] for x in r.stdout.split() if x.strip()}) + got = len(declared_names) except (subprocess.TimeoutExpired, ValueError) as e: return 2, [f" VOID the README's own command produced no count ({e!r}) — established", - f" nothing. cmd: {cmd}"] + f" nothing. cmd: {cmd}"], None lines = [f" ---- declared population: {declared} ({label})", f" ---- the README's own command returns: {got}", @@ -428,8 +432,8 @@ def declared_population(root, text): bad.append(f" DRIFT the number beside the command ({beside}) and the declared row" f" ({declared}) are not the same number") if bad: - return 1, lines + bad - return 0, lines + [" ok declaration, published command, and tree all agree"] + return 1, lines + bad, declared_names + return 0, lines + [" ok declaration, published command, and tree all agree"], declared_names def check(root): @@ -463,7 +467,7 @@ def check(root): # ⚠ Placed AFTER `unchecked` is declared, not beside the other README parsing 13 lines up, # where the first attempt put it and would have raised NameError on every run. Caught by # asserting the two line numbers against each other rather than by running it. - pop_rc, pop_lines = declared_population(root, text) + pop_rc, pop_lines, pop_names = declared_population(root, text) out.extend(pop_lines) if pop_rc is None: unchecked.append("declared population") @@ -547,6 +551,33 @@ def impound(label, directory, names, rel=""): return 2, out + [" VOID every top-level instrument is quarantined — the index leg" " established nothing"], True out.append(f" instruments on disk: {len(actual)} ({', '.join(actual)})") + # ⛔ TWO DIFFERENT NOUNS, FOUR LINES APART, WITH NOTHING BETWEEN THEM SAYING SO. + # "declared population: 57" and "instruments on disk: 55" print near each other and a reader + # subtracts them — TEAMLEAD did, on the merged version, and had to run `comm` to learn the 2 + # were quarantined. 57−55 sitting above rc=0 has the SHAPE of a verdict contradicting its own + # report. It is not one, and the fix is to say which two files and why, computed not asserted. + if pop_names is not None: + only_declared = [n for n in pop_names if n not in set(actual)] + only_disk = [n for n in actual if n not in set(pop_names)] + if only_declared or only_disk: + # ⚠ read_ack returns (set, why-void), NOT a set. Testing `n in ack` against the + # TUPLE answers False for every name and would have printed a confidently wrong + # reason for both files. Caught by inspecting the return shape, not by running it. + held, _ = read_ack(tools_dir) + held = {q.split("/")[-1] for q in (held or ())} + + def why(n): + if n in held: + return "quarantined (tools/QUARANTINE.txt)" + if not n.endswith(".py"): + return "not .py — the index leg globs tools/*.py only" + return "declared by the command but outside the index population" + out.append(f" ---- the two counts differ by {len(only_declared) + len(only_disk)} and" + f" they are DIFFERENT NOUNS, not a discrepancy:") + for n in only_declared: + out.append(f" {n}: declared, not indexed — {why(n)}") + for n in only_disk: + out.append(f" {n}: indexed, not declared — outside the published command") # Named, never merely counted — see NOT_AN_INSTRUMENT. out.append(" ---- excluded from the population as tests: " + (", ".join(excluded) if excluded else "none")