diff --git a/scripts/check-tools-index.py b/scripts/check-tools-index.py index f8b4614..7a8c070 100755 --- a/scripts/check-tools-index.py +++ b/scripts/check-tools-index.py @@ -363,6 +363,75 @@ def parse_count(tok): return WORDS.get(tok.lower()) +# ⛔ THE DECLARED POPULATION DECAYS, AND THE SENTENCE GUARDING IT IS PRINCIPLE-SHAPED. +# tools/README.md declares a number AND publishes the command that produces it, under the sentence +# "Run it; do not trust the number below." Nothing ran it. On 2026-08-22 the declaration read 55 +# and its own command returned 57 — and all seven rows of that table had drifted in one day. +# Principle-shaped rules bind 0 of 5 times in this repository; event-attached ones bind 3 of 3. +# This is the event. +# +# POPULATION the declared instrument count in tools/README.md +# PREDICATE equals what the command published beside it returns +# CHANNEL the working tree this gate was pointed at, via sh -c, cwd=root +# +# ⚠ The command is EXTRACTED from the README, never re-implemented here. Re-implementing it would +# make this gate a SEVENTH reading of a noun that already had six (#345), and the two would drift +# apart silently. Extracting it also catches the case a hand-written predicate would miss: editing +# the command without editing the number. +DECLARED_CMD = re.compile(r"^\s*(ls tools/[^\n]*?)\s+⇒\s*(\d+)\.", re.M) +DECLARED_ROW = re.compile(r"^(.*?)\s+(\d+)\s+⇐ THE DECLARED POPULATION\s*$", re.M) + + +def declared_population(root, text): + """(rc, lines). rc None = this tree declares no population, so the check does not apply.""" + cmd_m = DECLARED_CMD.search(text) + row_m = DECLARED_ROW.search(text) + + # ⛔ "NEVER DECLARED" AND "DECLARATION DELETED" ARE DIFFERENT PROPOSITIONS (class A, a collapsed + # pair). The first draft returned VOID for both and broke two existing self-tests, which run + # check() over minimal FIXTURE trees whose README never carried a declaration. A fixture that + # never declared a population is not drifting — the check does not apply to it. + # + # BOTH absent ⇒ N/A, joins the unchecked legs, reports PARTIAL, never 'clean'. + # ONE absent ⇒ VOID. A half-present declaration IS mutilated, and no fixture has that shape. + # + # ⚠ That leaves "someone deletes the declaration from the REAL README" uncatchable HERE, by + # construction. selftest() carries that one instead — a repository-specific fact asserted + # 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)"] + 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"] + + 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), + capture_output=True, text=True, timeout=20) + got = int(r.stdout.strip()) + 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}"] + + lines = [f" ---- declared population: {declared} ({label})", + f" ---- the README's own command returns: {got}", + f" {cmd}"] + bad = [] + if got != declared: + bad.append(f" DRIFT the ⇐ THE DECLARED POPULATION row says {declared}, its own command" + f" says {got}") + if beside != declared: + # ⚠ Two numbers inside one README disagreeing is a DIFFERENT failure from either + # disagreeing with the tree. Naming them apart is what lets a reader fix the right one. + 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"] + + def check(root): """Return (exit_code, lines). Pure enough to test against a fixture tree.""" out = [] @@ -391,6 +460,14 @@ def check(root): failed = False unchecked = [] # legs that established NOTHING, so the summary cannot claim them + # ⚠ 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) + out.extend(pop_lines) + if pop_rc is None: + unchecked.append("declared population") + held = set() # names impounded at the TOP level (for the `extra` leg only) held_paths = set() # every impounded path, tools/-relative — the ack file's population @@ -651,7 +728,13 @@ def impound(label, directory, names, rel=""): if unchecked and not failed: out.append(f" PARTIAL rows and prose verified; {len(unchecked)} leg(s) established" f" NOTHING: {', '.join(unchecked)} — not 'clean'") - return (1 if failed else 0), out, bool(unchecked) + # ⛔ pop_rc MUST REACH THE EXIT CODE. The first draft printed "DRIFT ... says 55, its own + # command says 57" and then exited 0, because the lines went into `out` and the code was never + # read. That is class D — the verdict contradicts its own report — committed inside the gate + # whose job is catching drift. A caller reading only the status saw a clean tree. + if pop_rc == 2: + return 2, out, bool(unchecked) + return (1 if (failed or pop_rc) else 0), out, bool(unchecked) def selftest(): @@ -1121,6 +1204,25 @@ def held_count(lines_): rc, _, _ = check(root) ok &= (rc == 2) print(f" {'ok ' if rc == 2 else 'FAIL'} void: an unparseable index exits 2, not 0 (got {rc})") + # ⛔ THE ONE CASE declared_population() CANNOT CATCH, ASSERTED WHERE IT BELONGS. + # It treats "no declaration at all" as N/A so it still works on fixture trees; the cost is that + # deleting the declaration from the REAL tools/README.md would go quiet. This is that catcher. + real = Path(__file__).resolve().parent.parent / "tools" / "README.md" + if not real.is_file(): + print(" FAIL tools/README.md unreadable from the checkout — cannot assert") + ok = False + else: + rt = real.read_text(encoding="utf-8") + hit_cmd, hit_row = bool(DECLARED_CMD.search(rt)), bool(DECLARED_ROW.search(rt)) + if hit_cmd and hit_row: + print(" ok the real tools/README.md still declares a population AND publishes" + " its command") + else: + print(" FAIL the real tools/README.md lost %s — the declaration is what this gate" + % ("its published command" if not hit_cmd else "its ⇐ THE DECLARED POPULATION row")) + print(" is aimed at, and removing it must not read as passing") + ok = False + return 0 if ok else 3 diff --git a/tools/README.md b/tools/README.md index ffb20a4..86c8737 100644 --- a/tools/README.md +++ b/tools/README.md @@ -10,7 +10,7 @@ below is taken over it.** ``` INSTRUMENT ≡ a non-test executable directly under tools/, EXCLUDING quarantined subdirectories - ls tools/*.py tools/*.sh | grep -v '/test_' ⇒ 55. Run it; do not trust the number below. + ls tools/*.py tools/*.sh | grep -v '/test_' ⇒ 57. Run it; do not trust the number below. ``` ⚠ **The first draft of this section declared 54 and published a command that returns 55** — off by the @@ -18,19 +18,27 @@ single `.sh`. ⛔ **In the section whose entire purpose is removing that ambigui RUNNING the command rather than trusting it.** ★ **`merge-watch.sh` is an instrument; a definition that excludes it because of its extension is drawing the population around a file suffix.** -**Measured 2026-08-21 at `c7b5a46`, one second, all six readings, so a reader meeting an older figure +**Measured 2026-08-22 at `e66aeb4`, one second, all six readings, so a reader meeting an older figure can place it:** ``` -top-level non-test executables (.py + .sh) 55 ⇐ THE DECLARED POPULATION - of which .py 54 ⚠ every count published on 2026-08-21 used THIS subset -top-level tools/*.py, including test_ 109 -ALL .py under tools/ recursively 132 -ALL .py under tools/ excluding test_ 74 -rows in the index table below 57 +top-level non-test executables (.py + .sh) 57 ⇐ THE DECLARED POPULATION + of which .py 56 ⚠ every count published on 2026-08-21 used THIS subset +top-level tools/*.py, including test_ 112 +ALL .py under tools/ recursively 135 +ALL .py under tools/ excluding test_ 76 +rows in the index table below 58 files under tools/teamlead/ 23 ⛔ QUARANTINED — belonging is an OPEN QUESTION ``` +⛔ **ALL SEVEN of these drifted in ONE DAY** — 55→57, 54→56, 109→112, 132→135, 74→76, 57→58 — **and +the sentence *"Run it; do not trust the number below"* did not catch any of them, because nothing ran +it.** ★ **That sentence is PRINCIPLE-shaped, and principle-shaped rules bind 0 of 5 times here; +event-attached ones bind 3 of 3.** ⇒ **`scripts/check-tools-index.py` now EXTRACTS the command from +the fence above and runs it, and exits 1 when the declaration and its own command disagree.** ⚠ **It +is a gate, not a promise: this table's other six rows are still unguarded, and only the declared row +has a catcher.** + ⚠ **SUBDIRECTORIES ARE OUT OF SCOPE AND THAT IS NOT AN OVERSIGHT.** `scripts/check-tools-index.py` holds them to a **weaker contract by design** — *named in their own README, not row + prose + count* — and `tools/teamlead/` is under quarantine, where **belonging is deliberately unresolved.** ⇒ ★ **The