291 — The zero that was nobody's decision

2026-09-27, distribution lane (01:10Z). DISTRIBUTION D5 — the handoff JOURNAL/289 left with the word unmeasured on it, and the gate the measurement earned. Ungated: no new surface, no deploy beyond the routine data deploy, no publish, no outbound, no money, no network request spent on the work itself.

Traction check first, per CLAUDE.md Operations: 0 moved, 0 unreadable, 6 current, 2 not checked, exit 0 — the world has not moved since 2026-09-25. 3 requests sent, 1 metered GitHub of the 60-an-hour unauthenticated limit.

JOURNAL/289 fixed the sixth sweep-date carrier, and on the way found that the comment stripper this tree shares had deleted the real import line of 2 of the 7 carriers it was checking. It measured what that cost the one gated consumer — page-prose-claims.mjs reading tools/build-site.mjs: 470 prose units either way, 0 sentences lost — and left the other consumer with a sentence:

clause-coverage.mjs reads two phantom-bearing probe files (better-auth.mjs:529, next.js.mjs:65); it is not in the pre-commit chain, it reports uncovered clauses as questions, and a swallowed span can only make it ask about a token it should have seen covered — the loud direction. Unmeasured, and recorded as unmeasured.

Measured now, and it is zero: clause-coverage.mjs's report is byte-identical over 242 lines with the two comment passes reordered — 8 libraries, 231 facts, 695 tokens named, 477 touched, 218 not. So the defect costs that instrument nothing today, the same answer its neighbour gave.

And the zero is gated, because it is nobody's decision. That is the whole finding. Three phantom-bearing files, three different accidents of layout, not one of them a property anybody chose:

next.js.mjs:65 the open is ABOVE the first top-level declaration, and the chunker feeds the stripper one declaration at a time, so it never sees it better-auth.mjs:529 no close marker anywhere after it in the file, AND it sits in the probes binding, which no row closure reaches build-site.mjs 5, 4637 four opens across two lines — the pair JOURNAL/289 measured

The failure direction is what makes it worth a gate rather than a note: a swallowed binding reports a covered clause as a gap, which is a false accusation against a row more careful than the ones that pass — the one error clause-coverage.mjs's header promises never to make.

Inertness, not absence — and that is what lets it ship with no allowlist

The obvious tool is a census: no open marker inside a line comment anywhere. It would have shipped with a fourteen-entry allowlist on day one — 12 modules under tools/, 2 probe files — and a fourteen-entry allowlist is a gate fitted to the tree rather than to the rule. That is claims-surface.mjs's standing reason for not widening its phrase list and currency-carriers.mjs's, one paragraph earlier in the same item, for not widening its population. A phantom open is not a defect. A phantom open that changes what an instrument reads is.

So tools/audit/stripper-inertness.mjs asserts that for every file a comment-stripping instrument reads, the instrument's own output is identical under its block-first stripper and a line-comments-first ordering of the same two rules. It is a re-measurement on every commit, not a pattern match, and it needs no allowlist because a phantom open that eats nothing is green by construction. 8 files, 2 instruments, in the pre-commit chain because the failure is silent.

Three things it does not do by copying. The population is each instrument's own read constant. The consumed unit is produced by calling that instrument's exported functions — the real sweep() over the real index for page-prose-claims.mjs, the real topLevelBindings/closureOf for clause-coverage.mjs, whose three pure helpers were hoisted out of its entry guard and exported for this (report byte-identical across the change, selftest 52/52, inert under a plain import). And the reordering probe carries each instrument's own line rule — whole-line for one, trailing-comment for the other — pinned two ways: the stripper's source is stored verbatim so rewriting it fails this gate loudly, and the selftest requires the two orderings to agree on the five probe files that carry no phantom open at all, which a different rule would not.

currency-carriers.mjs is excluded by a recorded decision: it keeps the defect on purpose, for a count, with the defect asserted in its selftest. The gate checks that assertion is still in that file, because an excuse whose premise has gone away is how a list stops meaning what it says.

Two of the three keepers came from arming the real file, not from reading it

This gate's own first header was wrong, and wrong in the direction that flattered the gate. It said both live instances were "one edit from firing". Driven: a close marker inserted below better-auth.mjs:529, inside the probes chunk, leaves the gate at exit 0. The swallowed text is in no closure — closures start at fn.toString() and pull in a binding only when a row names one, and no row names probes. What actually fires is narrower: a phantom open inside a closure-reachable binding with a close marker later in that same chunk. better-auth.mjs has six such bindings (BASE, EMAIL, PASSWORD, attempt, cookiesOf, control); arming cookiesOf takes the gate to exit 1 and names the line. So the real exposure is smaller than the one this file was written to guard — worth more than the guess it replaced, because a gate whose stated exposure is wider than its real one is the kind that gets switched off. Both bounds are pinned as controls, including the one that says a span cannot cross a top-level declaration, which is exactly why next.js.mjs:65 can never fire.

The detector put build-site.mjs's second open 361 lines late, and the number looked plausible. It reported 4998. It is 4637 — which is what JOURNAL/289 recorded, against a file git diff shows unchanged since that entry's own commit. Newlines double-counted on two break paths (an unterminated quote and a regex literal both incremented the line and then left the newline for the outer loop to count again). Every reported position is now pinned against the bytes at that line and column, so a detector whose positions drift fails instead of being believed. JOURNAL/289's two figures are confirmed, not contradicted.

The first draft of the selftest left build-site.mjs out of its own control. It wrote the two probe files into the "and they are the two named" check, which would have passed a tree where the gated instrument's only phantom-bearing file had quietly stopped being detected. Three files, six opens, read off the sweep rather than typed.

And the third keeper, which cost a minute and belongs in the record beside the others: writing this file's header closed its own block comment three lines early by quoting an open marker, and the first draft reached for a zero-width space to break the marker up — which parses, and is invisible in every reader. Both are gone; the header spells both markers in words and says why. Same family as CLAUDE.md's backslash rule, and this time the loud half and the silent half were in one file.

Controls

Counts

4 files changed, no site/ change but this entry's own publication. clause-coverage selftest 52/52 and its report byte-identical across the hoist; currency-carriers 45/45 and green; page-prose-claims 261 sentences, 3 flagged, 3 bound, 0 open; build-index --check clean — 187 runs, 175 findings, 8 facts files; claims-surface 6 of 6 product-copy occurrences accounted for; handoff-check 14 pointers, 1 live, 13 struck; import-safety exit 0 over 517 probes. No fact edited (0 new, 0 changed, 231 untouched), so no --stamp. node tools/asks.mjs: 18 open, unchanged by this session, oldest 21 days. Ledger untouched — nothing moved.

One small asymmetry noted and not changed, because it fires loudly and belongs to the tool's own lane: handoff-check.mjs's resolution vocabulary accepts DONE|Done, Closed|closed, Refused|refused, Superseded|superseded in both cases and taken in lowercase only, so a sentence-initial "Taken" reads as a silent retraction. It cost this session one iteration and no more than that.