220 — The thirteen, and the probe that found nothing because it imported nothing
2026-09-24 · distribution lane · DISTRIBUTION D4, the entry-guard batch's second half
JOURNAL/218 closed the twenty-one modules that never returned control to an importer and left thirteen behind — the prints rows, which return control after doing their whole job. Six of them rebuild a public surface on being imported. All thirteen are guarded now.
That half is mechanical. The half worth writing is that the instrument built to verify this batch was blind on its first draft, and its blind reading was a clean tree — the same shape as JOURNAL/216's wrong-index fixture and JOURNAL/210's control-written-from-the-instrument's-reading, arriving for a third time by a third route.
What was done
Thirteen modules now run their command line only when they are the command line, in the established spelling:
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
Never a hand-built file:// URL, which on Windows never matches and switches the guard off while it looks installed (JOURNAL/212).
Seven have no exports, so the whole body below the import block moved inside the guard, in tools/traction.mjs's shape. Six of them export — flip-exposure, freshness, ladder-check, run-summary-counts, tw-postcss-severity, build-hf — and for those only the command line moved. The exports and everything they close over stay at module scope, because the point of the guard is that a legitimate importer still gets the exports and gets nothing else.
ladder-check.mjs carried a comment saying it was deliberately unguarded, written by JOURNAL/216 when guarding its selftest branch alone would have left it unguarded while looking fixed. That reasoning was right and is now spent; the comment is corrected in the same commit, because a stale "deliberately not done" reads to the next session as a standing decision.
tools/mcp-server.mjs is untouched, on JOURNAL/216's symlink hazard, and it is now the whole of the class: the only module in this tree that does not return control under any argv.
The instrument against the committed tree: Q1 prints 13 → 0, Q2 unguarded 11 → 0, reachable 25 → 36, never-returns bare 0, --check 0, --selftest 4 → 1.
The probe that found nothing because it imported nothing
The import side needed a reading import-safety.mjs cannot give — JOURNAL/218's own finding is that its writes column is git status before and after, and a generated surface rebuilt from unchanged data is rewritten to identical bytes. So a throwaway probe: one child process per module, importing it against a scratch copy, watching six committed sentinel files' mtimes.
Its first draft passed SUBJECT + "/" + MOD to import(). Node reads a bare path as a package name, so every child threw Cannot find package 'tools' — and the probe's own summary line read:
=> never returned 0/13, spoke 0, moved a sentinel 0
A clean tree. Thirteen modules that print thousands of bytes and six that rewrite the site, all reported silent, by an instrument that had imported none of them. The threw field was in the output and said so; the summary line did not read it.
What caught it was that the BEFORE run is a positive control. It is supposed to find thirteen talkative modules and six moved surfaces; a run against the unguarded tree that comes back clean is the instrument confessing. Had the probe been written after the guards, its blind reading and its true reading would have been the same three zeroes, and the batch would have been "verified".
The second draft was wrong at a different index, and it is JOURNAL/216's fault exactly. The child ran under node --input-type=module -e, where there is no script path, so an appended --selftest lands at argv[1] — and the positional process.argv[2] spelling, which is the shape that motivated this entire class, is asked the wrong question. The child is now a real file invoked as node child.mjs --selftest, with the target arriving by environment variable. The rule JOURNAL/216 wrote about the subject applies to the instrument: a real invocation is the only fixture that reproduces a real invocation.
What the readings say
CLI side — 19 invocations, each module under its declared flags, before and after. The noise floor was measured first: two BEFORE runs, 19 of 19 byte-identical. That is the only reason the attribution below is a reading rather than an excuse.
After: 18 byte-identical on stdout, stderr and exit code. The nineteenth, tw-postcss-severity bare, differs in which rung appears in a tapable stack trace — and two consecutive AFTER runs differ the same way, so it is that tool's own nondeterminism, which the two BEFORE runs had agreed on by luck. Its stdout, the report, is byte-identical.
--check passing is not proof the write path lives, and six of these thirteen exist to write. Every --check in the pre-commit chain would pass over a guard that had silenced a build. So each builder was run without --check in a scratch clone: all six still wrote their surface.
Import side — one child per module, three argvs, write side read off mtime.
| argv | spoke | never returned | moved a public surface |
|---|---|---|---|
| bare | 13 → 0 | 0 → 0 | 6 → 0 |
--selftest | 13 → 0 | 3 → 0 | 5 → 0 |
--check | 13 → 0 | 0 → 0 | 0 → 0 |
The six that rebuild on a bare import, each moving its own sentinel: build-corrections (corrections/zod.md), build-hf (hf/README.md), build-index (data/index.json), build-repo (README.md), build-rules (rules/zod.md), build-site (site/index.html). JOURNAL/218 inferred that population from one measurement of build-site; this counts it.
And the control that separates guarded from broken: the six exporters still hand an importer their exports — 11, 5, 3, 2, 1 and 13 names respectively.
Pre-commit chain green. It drives eight of these thirteen files itself.
What this does not do
The widening is still not done. import-safety.mjs still reads writes off git status, so it still labels a 765-file rebuild prints. Everything above was measured by a throwaway probe in a scratch directory. Folding mtime — or a content hash — into the tool across 132 modules, with its cost measured rather than assumed, stays queued for either lane. Writing this entry as though the instrument had been fixed would be the JOURNAL/214 shape: a property of one session's scratch work carried as a property of the tree.
tools/mcp-server.mjs is not guarded, and the reason is in D4 unchanged. Its failure mode is not a report that runs twice; it is the product silently answering nothing for every POSIX npx user, on the one platform this machine cannot test. The realpath spelling plus pack-test.mjs is a chunk of its own, and a session taking it should say which half it proved and which it could not.
The 4 entry-dead rows are an older and different question and are untouched.
Gates
None. No new surface, no money, no outbound, no published claim, no change to any generated surface's contents. Asks unchanged.