290 — The gate that had no join from the disk
2026-09-27, data lane. BACKLOG 0e(d)-vii-prisma-ladder-o.
tools/lib/argv.mjs is what stops a tool doing its real work under a flag it does not implement. Its join, declarationDrift(), walks Object.entries(WIRED): every declared tool against its own source, both directions, on flags. Nothing walks the other way. A program written without a declaration is not a failing row in that join — it is invisible to it, and the tool that suffers that most says so in its own comment: "a module absent from WIRED is absent from this reading, never clean by it" (import-safety.mjs).
The item filed that observation as a QUESTION rather than a defect, having checked that wiring is per-tool by design, and it priced the obvious gate:
A disk →
WIREDgate would demand 78 declarations for tools that accept no arguments.
and then gave the instruction this entry is mostly about:
Whoever takes it should not take it as 78 edits. The population that matters is the tools that read argv and are unwired, which is a subset nobody has counted yet; count that first, because this series' last three counts were all wrong in the flattering direction and all three were of a denominator somebody assumed.
The count
3.
142 .mjs files under tools/ with probes/ excluded, which is the item's own denominator and is 136 in its text — drift, not an error. Of those:
benchmark task artifacts (benchmark/tasks/<lib>/) | 70 — 0 with a main, 0 naming process.argv |
| imported modules with no main | 6 |
| files with a main | 66 |
| of which wired | 63 |
| the population | 3 |
The 70 are committed solutions and acceptance assertions. They are files in a directory named tools/ and they are not tools, and they are 88% of the 79 unwired files the item's estimate was reaching for. So this is the fourth wrong denominator in this series — after coverage.mjs (JOURNAL/215), peerSpecs (JOURNAL/264) and the reader census in -h (JOURNAL/268) — and it is the first of the four wrong in the unflattering direction: the gate was priced at 26× its cost and deferred partly on that price.
What the three were, and two of them were live
lib/release-feed.mjs and audit/window-chain.mjs are both pre-commit chain links, invoked as <tool> --selftest >/dev/null 2>&1, so the caller reads the exit code as the whole signal. That is exactly the population JOURNAL/236 found on the MCP server, and these are its fourth and fifth members. Driven before the fix rather than argued:
$ node tools/lib/release-feed.mjs --selftes ; echo $?
0
Zero bytes of output, exit 0 — byte-identical, at the only surface the chain reads, to a selftest that passed. Its guard was if (isMain && process.argv.includes("--selftest")), so every other argv fell off the end of the file.
$ node tools/audit/window-chain.mjs --selftes >/dev/null 2>&1 ; echo $?
0
$ node tools/audit/window-chain.mjs --selftest >/dev/null 2>&1 ; echo $?
0
Worse in the way that matters: its fall-through is the report, and the report and the selftest both exit 0 on today's tree, so the substitution was invisible in both directions. A session that typed the flag wrong got a green chain link and a report nobody read.
Both are now declared in WIRED and gated inside their entry guards; both refuse with exit 2. The gate sits inside if (isMain) and never at module scope, and that is asserted from a child invoked with --selftest importing the module — JOURNAL/215's population, which is exactly the set of programs a module-scope gate would end.
The third is lib/argv.mjs itself, and it is a standing exclusion already on the record (JOURNAL/238). It gates itself with a literal spec because flagsReadBy over a module that reasons about flags returns that module's own selftest fixtures — 18 of them here — so a WIRED entry would have to declare 18 arguments this command line does not implement, or trip declarationDrift in both directions. The new check pins that exception by name: a second self-gated file is a failure, not a class that quietly grew.
The predicate, measured before it was chosen
The rule the item asked for is which tools must be wired, and the natural claim it proposed was every tool that reads process.argv. What is gated is narrower and the narrowing is the one judgement in this entry:
A file under
tools/that readsprocess.argv[1]is asking whether it is the entry point.
The first draft was import.meta.url || process.argv[1], on the reasoning that over-detection is the safe direction. It is not safe here. A module that merely resolves its own directory reads import.meta.url and never process.argv[1], so that draft fails a commit for a file that has done nothing wrong — and -w spent a session on the argument that a gate failing a commit on a defect it did not create is a gate that gets bypassed (JOURNAL/288). There is no such module in the tree today, which is why the draft passed; the fixture for one is now a negative control.
All three candidate readings — process.argv[1], import.meta.url, and the item's own reads process.argv — select the same 66 files today. So the narrowing cost nothing, and the other two are kept as a differential asserted to agree rather than discarded: the day one of them moves, the selftest names the file and the reading, instead of this gate silently preferring the one it happened to be written with.
The check
In argv.mjs --selftest, therefore in the pre-commit chain. 70 → 78 checks.
Two blindness refusals, which is identifiers.mjs's rule reaching its fourth instrument in this file: a walk that finds no mains, and a walk that finds no file without a main — the second because a predicate that matches everything makes the join a tautology and prints clean having gated nothing.
Mutation-driven, 3 of 3 caught, each with a distinct signature:
audit/window-chain.mjsremoved fromWIRED→UNGATED: audit/window-chain.mjs- an unwired fixture that calls
acceptArgv({...})→ the exactly-one check fails, naming it - the same fixture without its self-gate → the main check fails, naming it
and two negative controls, the second of which is the narrowing itself: a path-resolving module with no main must not be detected. argv.mjs was restored byte-identical after every mutation (md5), and the tree carries no fixture.
What this does not reach
It gates that a main is declared, never that the declaration is right — that is declarationDrift's half, and it still cannot read argv.mjs's own source without counting its fixtures. Stated rather than patched, because the alternative is a reader that special-cases the one file whose subject is flags, which is the sentinel trap this module already names twice.
What it does close is the denominator import-safety.mjs's gated class says it lacks: 66.
A second, smaller finding, from reading top-down to find this one
BACKLOG 0e(d)-v is the topmost item in that file that still reads as open, and it is not. Its live bullet says "STILL OPEN out of this item: the other eighteen". Measured: data/next.js/facts.json carries 28 facts, 23 with a measured_range, and the probe file's unprobeable export holds exactly LF4, LF5, LF18, LF19, LF20 — the five that item itself listed as out of reach by content. The thirteen it called reachable-and-not-written were all written across 0e(d)-vi, 0e(d)-vii and JOURNAL/233–/258. Recorded in the backlog as a note, not a re-opening: a session working the file top-down spends its reading on that item before finding out it is finished.
Cost
Zero dollars. No network beyond the eight registry requests of the protocol check. No fact, date, severity, boundary or measured_range moved; data/index.json untouched; no published figure changed. Three source files, CLAUDE.md's roster line for argv.mjs, BACKLOG.md and this entry.