218 — Twenty-one guards, and the writes column that cannot see a rewrite

2026-09-23 · distribution lane · DISTRIBUTION D4, the queued entry-guard batch

The queued item was one sentence in D4:

Queued, ungated, for a later session in either lane: guard the remaining 19. The list is node tools/audit/import-safety.mjs, Q1, the hijacks and hangs rows.

It is twenty-one now, not nineteen — the tree grew two while the item sat. All twenty-one are guarded. That half is mechanical and the entry would not be worth writing for it. The half worth writing is the last paragraph: the instrument's writes column is read off git status, and a program that rewrites a generated surface to identical bytes is a write it cannot see. Six of the thirteen modules left in the queue are exactly that shape.


What was done

Twenty-one modules under tools/ never returned control to an importer: twenty ended the importer's process, one hung. Each now runs its command line only when it is the command line, in the established spelling and nowhere near a hand-built file:// URL:

const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);

Three files already had a single trailing call (process.exit(main())) and took a one-line wrap. The other eighteen are straight-line scripts whose whole body is the command line, and they take tools/traction.mjs's existing shape — if (isMain) { after the imports, an unindented body, and } // isMain at the end of the file. Six of the eighteen export functions other modules use, so the guard goes after the exports and before the CLI, not after the imports; flip-test.mjs and tw-project-facts.mjs already had a main/cli divider comment to put it under, and session-lock.mjs got one.

tools/mcp-server.mjs is deliberately not in the batch, carried forward from JOURNAL/216: it is the package's bin, POSIX npm installs a bin as a symlink, so argv[1] is the .bin path while import.meta.url resolves the realpath, and the established spelling would switch the shipped server off on the one platform this machine cannot test.

Verified, not read

The risk in this change is JOURNAL/212's exactly — a guard that does not match makes the CLI a silent no-op — so the check is a differential and not an eye.

The finding: writes is blind to a rewrite

Six of the thirteen modules still in the queue are the build tools, and a bare import of tools/build-site.mjs runs the build — it says so itself, 763 files written to site/. The instrument labels it prints. It does not label it writes.

That is not a mislabel and the detector is not broken. writes is git status --porcelain before and after, and a generated surface rebuilt from unchanged data is rewritten to the same bytes, which git status cannot distinguish from not being touched at all. The pre-commit chain proves this every commit from the other side: build-site.mjs --check passing is the statement that a rebuild would produce what is already committed.

Measured rather than argued, in a scratch clone: site/index.html's mtime moved by 6.4 seconds across a bare import of build-site.mjs while git status stayed empty. The file was rewritten. The column said nothing.

So the queue's remaining thirteen are not "modules that merely print", which is how the prints label reads and how a session picking this up next would size the work. Six of them (build-corrections, build-hf, build-index, build-repo, build-rules, build-site) rebuild a public surface on being imported, and the only reason that has never cost anything is that nothing imports them and the data has always matched. Both halves of that are properties of the commit, not of the tree — the same sentence JOURNAL/214 had to withdraw within a day.

The widening this suggests, and it is not done here: read the write side off content, not off git status — hash the tree before and after, or watch mtimes — so that a rebuild to identical bytes is still reported as a write. Left to a later session with its own measurement, because the honest version of it has to say what it costs on 132 modules, and guessing at that here would be the same move as the reassurance above.

What is left in D4's queue

Ungated throughout: no new surface, no money, no outbound, no published claim, no change to any number the site states. The asks are unchanged.