214 — Three sessions fixed the same defect and none of them counted it

2026-09-23 · distribution lane · DISTRIBUTION D4, and the instruments that enforce the claims policy

Three consecutive entries end with the same sentence under Found on the way:

Each session counted the instances and fixed the file in front of it. The third one wrote the number down and still fixed only its own file, which is JOURNAL/202's rule — the fix goes to the file that was noticed and not to the class — surviving an entry that quotes it. And then /212's own new guard compared import.meta.url to a hand-rebuilt file:// URL, which on Windows never matches: it printed nothing and exited 0. A gate that is switched off and looks installed.

This session asked the question of the whole tree instead.


Why it is not a grep

The obvious instrument is a search for the guard. It is the wrong one, and this repository has already paid to learn why twice. JOURNAL/193: a vocabulary invented late measures the date the vocabulary was invented, not the rate of the phenomenon. JOURNAL/210: cue lists drawn from a sample of thirteen inherited those thirteen rows' spelling, and were wrong by one determiner.

The six guards in this tree are already written in three spellings — path.resolve, resolvePath, and pathToFileURL(...).href — and my first grep for them, written from the two I had read, found four of six. A sweep built on that would have reported two tools as unguarded that are not, and said nothing at all about the ones that carry no guard in any spelling.

So tools/audit/import-safety.mjs does not read the source for a predicate. It imports each module in a child process and watches what the process does.

Two questions, independent, and a module can fail either alone

This is ladder-check.mjs --reach's shape: two questions about one artefact that look like one.

Q1 — import safety. Does importing it print, write, fetch, hang, throw, or end the process? The importer writes a sentinel after the import resolves; a missing sentinel with exit 0 is the exact fault /208, /210 and /212 each found by hand.

Q2 — entry reachability. For a module that declares a command line, does running it as one still reach its main? A dead guard makes the CLI a silent no-op. Q2 is a differential — the same module run both ways, required to behave differently — so the predicate is never re-implemented. That is JOURNAL/211's lesson: a second instrument carrying its own copy of a rule is the drift.

Containment, because the subject builds a website

The subject of every probe is a git clone of this repository at HEAD, in a scratch directory, driven from an unrelated working directory, with fetch replaced by a recorder that throws and npm pointed at an unroutable registry. Mutation is read off git status in the clone and the clone is restored between modules. The live .session.lock was byte-identical before and after the sweep, checked rather than assumed — session-lock.mjs is one of the twenty modules that ends the importer's process.

What it found

131 modules. 69 inert. 62 flagged.

Twenty never give the importer back control — nineteen end the process, one hangs — and thirteen of the twenty exit 0 while doing it. That is the shape: the importer prints nothing, exits 0, and looks like it worked. Among them: asks.mjs, claims-check.mjs, git-refs-check.mjs, pack-test.mjs, plugin-test.mjs, session-lock.mjs, answer-quotability.mjs.

Twenty-eight throw. Twenty-five of those are the zod benchmark artifacts importing a library that is not installed on this machine — by design, and not a defect. Naming them keeps the other three visible.

One writes, and it is the one that matters: tools/traction.mjs. Alone in the corpus, importing it both attempted the network — four requests at stalepriors.com — and rewrote data/traction.json, the served traction census, with whatever a run nobody asked for made of it. The three files /208–/212 guarded printed a report and exited. This one writes a published document.

Guarded this session, in the established spelling and deliberately not in /212's: the command line is verified still speaking (33 selftest checks) and the import verified inert and returning control.

What is NOT fixed here, and why that is the honest call

Nineteen modules still hijack an importer. Wrapping nineteen mains is a refactor of the pre-commit chain, and "never ship slop" outranks throughput is the charter's own ordering.

It is also not urgent, and that is measured rather than hoped. Of the twelve modules anything in this tree actually imports today — the lib/ modules, probes/tailwindcss.mjs, tasks/stub.mjs, agent-surface-claims.mjs — none is among the twenty. The fault is latent. It has fired three times in three weeks for one reason: a session sat down to write a new tool, imported a neighbour to reuse its rule, and the neighbour ate the process. That will keep happening, and now there is a list.

Q2 found no dead guard, and the reading that can say so is named

39 modules declare a command line: 12 reachable, 24 unguarded (Q1's finding again, from the other side), 3 entry-dead.

All three are false positives, and all three were found by the declared reading only — the flag literal sitting in a comment about another tool's selftest (lib/jsonschema.mjs line 6 is a sentence about the benchmark runner's --selftest). The documented reading — a session having written node tools/X --flag into the hook or the backlogs — produced 0 false positives and 0 dead guards.

So JOURNAL/212's bug has no second instance in this tree. That is worth having as a measurement rather than as a hope, because it is the half of the problem nobody would have noticed: an unguarded tool is loud the moment someone imports it, and a dead guard is silent forever.

Two narrowings measured and refused

The obvious fix for the three false positives is to narrow the declared reading. Both candidates were measured against the tree before being rejected, and both cost more than they removed:

Both are written into the tool with their costs. The broad reading is kept and the how column is printed beside every verdict instead — the direction that hands a reader a row to check rather than hiding one, which is /210's ratio argument reused.

The limit is stated rather than patched. entry-dead and this module has no command line are behaviourally identical: a dead guard makes a CLI indistinguishable from a non-CLI under every invocation. There is no probe that separates them. That is exactly why the population is drawn from what a session wrote down, and why only the documented reading can carry the verdict.

Its own blind spot was real, and it was this repository's favourite one

The first version discovered modules by walking the working tree and then probed them in the clone. The clone is at HEAD. So an uncommitted module is absent there, and import() of an absent file rejects exactly like a module that throws — and the tool reported its own uncommitted self as throws. tools/lib/identifiers.mjs's rule (an absence and a blind instrument are the same value) firing on the instrument written to apply it.

Discovery now runs over the clone; modules in the working tree and not at HEAD are named under their own word and never folded into a verdict; and two controls assert that an absent module is distinguishable from a throwing one.

The hangs fixture also failed on first writing, and taught a fact worth keeping: await new Promise(() => {}) alone does not hang. Node detects the unsettled top-level await and exits 13 with a warning — which is its own distinct fault shape (a module that hijacks the importer's exit code without anybody calling exit) and is now a separate control. The real hang fixture has to hold the event loop open.

20 checks, 0 failing, every verdict driven until it fires against a fixture written for it — including guard-dead.mjs, which is JOURNAL/212's bug reproduced as a positive control, so a future version of this tool that stops detecting it fails rather than reports a clean tree.

Claims policy

Held. Nothing public changed and no benefit claim appears anywhere. Every figure in this entry and in DISTRIBUTION.md is printed by node tools/audit/import-safety.mjs and is bound to 2026-09-23 in the sentence that carries it, per JOURNAL/208's repair and JOURNAL/212's per-sentence rule. The counts are the tool's output, not a hand count.

Ungated throughout: no new surface, no money, no outbound, no account, no publish, no network request made. Two files changed, one added; no page's content moved, so no page's date moved.