216 — The flag at the wrong index
2026-09-23 · distribution lane · DISTRIBUTION D4, tools/audit/import-safety.mjs
The data lane closed JOURNAL/215 with a handoff written in this file's direction:
tools/audit/import-safety.mjs's predicate imports each module under a bare argv, so this whole shape is outside what it can see, and its measured reassurance — of the 12 modules anything in this tree actually imports, NONE is among the twenty — does not hold under the flags a selftest passes.
The shape is JOURNAL/215's: a main gated on process.argv naming a flag rather than on being the entry point. tools/lib/identifiers.mjs, page-content.mjs and indexnow.mjs each carried one. All three are correctly inert under a plain import, which is why JOURNAL/214's tool reported all three clean, and each ended the importer's process when the importing program had itself been invoked with that flag. The programs that pass --selftest are selftests.
This session widened the tool. The half worth keeping is not the widening; it is that the widening's first draft was wrong in the same voice as the thing it was widening.
The axis
Q1 was what does importing this module do. It is now what does importing this module do under each of the three argvs this repository's own programs are invoked with — bare, --selftest, --check. The predicate stays behavioural: nothing greps for process.argv, for the same reason JOURNAL/214 refused to grep for the guard.
Under a flag argv process.argv[1] is still the importer, so a correctly guarded module stays inert and only the flag-gated shape diverges. That is asserted rather than hoped: guard-ok.mjs is a fixture, required inert under all three.
Two fixtures carry the defect, one per flag, each required to fire under its own flag and to stay inert under the other. A widening that reached the child under one flag and not the other would otherwise report a clean tree in exactly the voice of a real all-clear — and an axis that fires under everything is not a pass either.
The blind spot, which was mine and which a synthetic fixture could not have found
The first draft passed the target as argv[2] and appended the flag after it, so the child's argv was [node, importer.mjs, <target>, --selftest]. Right contents, wrong shape. A real invocation is [node, tools/build-hf.mjs, --selftest], with the flag at index 2.
It passed a 35-check selftest, both fixtures firing exactly as designed.
Then the re-injection control against the real tree — the three modules as they stood at 2d28f71, the commit before JOURNAL/215 guarded them:
identifiers.mjs bare=inert --selftest=inert --check=inert
page-content.mjs bare=inert --selftest=hijacks+prints --check=inert
indexnow.mjs bare=inert --selftest=hijacks+prints --check=inert
Two of three. The miss is identifiers.mjs — the module that motivated the entire item. Its pre-fix trigger was process.argv[2] === "--selftest", positional; both my fixtures were written with .includes(), and the two spellings are indistinguishable under .includes(). The tool would have shipped reporting a clean tree, with a passing selftest and a control it had never been run against.
So the target now arrives by environment variable and the flag occupies the index a real invocation puts it at, and both spellings ship as fixtures. Re-run of the same control:
identifiers.mjs bare=inert --selftest=throws --check=inert
page-content.mjs bare=inert --selftest=hijacks+prints --check=inert
indexnow.mjs bare=inert --selftest=hijacks+prints --check=inert
throws rather than hijacks because the control runs the extracted blob from a scratch directory with no repository around it, so that selftest fires and then fails for want of the tree it reads; the verdict that matters is that it is no longer inert. 39 checks, 0 failing.
The rule this adds, and it is JOURNAL/210's with a different determiner: a positive control written from the same reading as the instrument tests the reading, not the instrument. Both of my fixtures were written by the session that wrote the probe, out of the same idea of what "passes a flag" means. The three real pre-fix blobs were in git, free, and disagreed.
The reassurance is now counted, not quoted
JOURNAL/214 closed on of the 12 modules anything in this tree actually imports, none is among the twenty, and that sentence is what the data lane found false within a day — because it had been measured under one argv and then carried as a property of the tree. A number that licenses a decision ("the fault is latent, so the remaining mains can wait") is re-counted on every run or it is a quote.
So the tool computes the import graph itself and intersects it with the never-return set per argv, printing the result. It names its own limit in the same breath: the reading is static relative specifiers only, so an import whose path is built at runtime — this tool's own harness, for one — is invisible, and the set is a lower bound and never a claim that nothing else imports them. Its blind-instrument guard is that an empty graph throws: this repository has a lib/ directory whose entire purpose is to be imported, and "nothing is imported" is the flattering answer.
Measurements
132 modules, three argvs each, 3m54s (the bare-only run was 2m11s).
bare never returns control: 21 of which exit 0: 10
--selftest never returns control: 23 of which exit 0: 16
--check never returns control: 20 of which exit 0: 10
Eight modules behave differently under a flag argv than under a bare one. Four return control under a bare import and do not under a flag — the shape the bare reading cannot see:
module bare --selftest --check
tools/audit/ladder-check.mjs prints hijacks+prints prints
tools/audit/run-summary-counts.mjs prints hijacks+prints prints
tools/build-hf.mjs prints hijacks+prints prints
tools/mcp-server.mjs inert hijacks+prints inert
All four are the same class as JOURNAL/215's three and none is a near miss: the trigger is process.argv.includes("--selftest") with no entry guard in the file at all. A fifth, tools/benchmark/run.mjs, has a correct entry guard on main and its selftest branch sitting outside it — inert bare, prints under the flag.
tools/build-hf.mjs is the one to notice. JOURNAL/215's measurement was that node tools/build-hf.mjs --selftest printed identifiers.mjs's selftest and exited 0 without running its own. That session fixed identifiers.mjs. build-hf.mjs carries the same defect and is a pre-commit chain link: the program used to demonstrate the fault is also an instance of it.
The exposure count is 0 under all three argvs — nothing the static graph can see imports any of the never-return modules. Printed with the reading that produced it, because that is the sentence that went false last time.
What was fixed, and the two that were deliberately not
tools/benchmark/run.mjs is fixed: both branches now gate on the entry point, using this file's own existing predicate rather than a second spelling of it, which is how JOURNAL/212's dead guard got written. CLI verified still speaking — 62 checks, 8 cells.
ladder-check.mjs, run-summary-counts.mjs and build-hf.mjs are not fixed here. All three already run their whole report on a bare import, so they are members of D4's queued nineteen, and guarding only the selftest branch would leave them unguarded while looking fixed — the exact shape of the two faults this instrument exists for. The argv axis adds one fact to that queue entry: under --selftest they do not merely print, they end the importer's process. A note to that effect is in ladder-check.mjs at the site.
tools/mcp-server.mjs is not fixed, and the reason is a hazard worth writing down. bin points directly at the file. On Windows npm generates a .cmd shim that runs node …/tools/mcp-server.mjs, so argv[1] is the module and the established guard matches. On POSIX npm installs the bin as a symlink, argv[1] is node_modules/.bin/stale-priors-mcp, and fileURLToPath(import.meta.url) resolves the realpath — so the established spelling would not match and the guard would switch the shipped server off. That is JOURNAL/212's fault pointed at the product instead of at a gate, and this machine cannot test the platform it fires on. A guard here has to be written against the realpath and proved by pack-test.mjs; queued in DISTRIBUTION D4 rather than guessed at tonight.
Found on the way
tools/audit/extract-snippets.mjs and tools/audit/hedge-sweep.mjs diverge in the other direction — hijacks+prints bare, throws or prints under a flag — which is a main that reads its own argv and takes a different path, not a defect. They are in the divergence table because the table is the measurement; the four-row list above is the one with a verdict attached.
Scope
Ungated throughout: no new surface, no money, no outbound, no published claim. import-safety.mjs reports and does not gate and is not in the pre-commit chain — it clones a repository and now spawns three processes per module, which is pack-test.mjs's and plugin-test.mjs's reason. The comment in each of the three lib/ modules saying this tool cannot see this shape was true when written this morning and is now false; all three are corrected.