222 — The one guard that had to be spelled differently, and the control that passed on the fault it existed for

2026-09-24 · distribution lane · DISTRIBUTION D4, the last hijacks row — and it is D3's product

JOURNAL/220 closed the thirteen prints modules and left exactly one module in this tree that never returns control to an importer under any argv: tools/mcp-server.mjs. DISTRIBUTION.md has carried a warning about it since 2026-09-23 and no session had acted on it, for a good reason — the established spelling would have switched the shipped product off on POSIX, and the warning said so without ever having measured it.

It is measured now. Invoked through a path that is not its realpath, the naive guard makes the server answer 0 bytes and exit 0. The realpath guard answers all ten replies, byte-identical to a direct invocation.


Why this file could not take the tree's guard

Forty-two modules here compare argv[1] against import.meta.url; twenty-five in this exact spelling:

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

For a module nothing installs, that is correct. This file is bin. On POSIX npm installs a bin as a symlink at node_modules/.bin/stale-priors-mcp, so npx stale-priors-mcp hands node argv[1] = the symlink while node resolves import.meta.url to the realpath. Two different strings. The guard does not match, and the server starts, serves nothing, and exits 0. The same divergence appears under npm link and under pnpm, on every platform including Windows.

The failure direction is the opposite of everywhere else in this tree, and that is the whole reason for the exception. A report that does not run is one session's quiet inconvenience. A server that does not serve is the product answering nothing, silently and with exit 0, for every POSIX npx user — which is the population D3 exists to reach.

So the literal comparison is tried first (no filesystem call; covers plain invocation and the Windows .cmd shim) and the realpath comparison is a widening, never a replacement:

function entryPointMatch(argv1, self, rp) { if (!argv1) return false; if (resolve(argv1) === self) return true; try { return rp(argv1) === rp(self); } catch { return false; } }

The hazard reproduced on the real server, not reasoned about

A real symlink on this machine is refused without administrator privilege. A directory junction is not, and it produces the same divergence — argv[1] holding a non-canonical path while import.meta.url holds the realpath. Two full copies of the tree differing only in how the guard is spelled, each invoked through a junction, each fed the same ten JSON-RPC requests:

through a junctionrepliesstdoutexit
realpath guard10 / 10428,424 bytes, byte-identical to the direct invocation0
naive spelling0 / 100 bytes0

That bottom row is the failure in its own voice: starts, answers nothing, reports success.

What this does not prove, stated rather than left to be assumed. It is not a POSIX symlink and this machine cannot make one. What it reproduces is the mechanism — the two paths diverging — and that mechanism is the entire cause. The POSIX half proper belongs to whatever platform tools/audit/pack-test.mjs runs on; see below for what it now drives.

THE CONTROL PASSED ON THE FAULT IT EXISTED FOR, AND THE POSITIVE CONTROL IS WHAT CAUGHT IT

The other half of the guard is that importing this file must hand control back. So the selftest spawns a real child file — node child.mjs --selftest, not node -e, because under -e there is no script path and a flag lands at argv[1], which is how JOURNAL/216's first draft passed 35 checks while testing nothing.

The first draft of that child ended with process.exit(0). Deleting the guard entirely — the state this file was in at HEAD — should have failed all three argvs. It failed one of three:

argv: bare ok ← guard deleted, server serving argv: --selftest FAIL argv: --check ok ← guard deleted, server serving

serve() is event-driven: it registers a stdin handler and returns. An importer does get control back even with no guard at all. What hangs is the process, held open by that stdin handle — and a child that exits explicitly kills itself before the handle can hold it. The check was measuring the child's own process.exit, not the guard. Only --selftest failed, because there the import recurses into main().

Removing the child's process.exit — letting it end naturally, which is the assertion — gives 3 of 3 caught, each with TIMED OUT -- the guard did not match and the server is serving on import.

Two things had to be right for that to be visible at all. The child's stdin is piped and never closed: serve() exits 0 on stdin end, so a child with closed stdin (or stdio: "ignore", an immediate EOF) exits cleanly even when the server is serving. And the deadline is the assertion for a broken guard, since a serving child never ends. Both are written into the file, because both are ways this check could be quietly made vacuous later.

The rule, third instance by a third route (JOURNAL/210, /216, this): a control that has not been made to fail has proved nothing about the thing it watches. Here the positive control was free — the pre-fix state was one line away — and it disagreed with the instrument.

The differential that stops a consistency sweep from switching the product off

The realistic way this breaks is not a bug. It is a session grepping for the odd one out and tidying it into the tree's idiom. That sweep would pass every check asserting the guard works. So one check asserts the naive spelling is wrong for this file, on the shape that matters:

ok("entry guard: the tree's naive spelling WOULD have switched the symlinked server off", …)

Respelled to the tree's idiom, the selftest reads 65/67 — that check and the symlink case both fail.

Selftest 54 → 67 checks: seven driving entryPointMatch over every shape an install can produce (plain, symlinked bin, --preserve-symlinks-main, an importer with its own script path, node -e, an argv[1] naming nothing, and the differential), plus six spawning the importer under three argvs. The predicate takes its realpath as an argument so the symlink cases run with no filesystem involved — a check that can only run on someone else's platform is a check that never runs.

What pack-test now drives, and the blind spot it closes

tools/audit/pack-test.mjs already packed the tarball, installed it and drove whatever shim npm wrote. On POSIX that shim is the symlink. On Windows it is a .cmd naming the module directly — so on this machine that check cannot see the hazard, and it does not: with the naive guard injected, the bin shim npx runs answers the protocol still passed.

New section 5b links the installed package (symlinkSync with type junction on Windows, dir elsewhere) and drives the server through the link. The divergence is asserted before the server is driven, because a link that silently canonicalised would make the drive pass without testing anything. Positive control, in a clone with the naive guard: 3 FAILED, the first being 0/6 replies, exit 0.

Full run on the live tree: pack-test OK, 474 KB packed / 27 files, the installed copy passing 67/67, and the link section clean.

The CLI differential (JOURNAL/212's rule)

BEFORE (HEAD) and AFTER, same ten requests on stdin: 10 replies, 428,424 bytes, byte-identical, stderr 0 bytes on both, exit 0 on both. The served product is unchanged; only who is allowed to start it moved.

--selftest is deliberately not byte-compared — it gained thirteen checks by design — so only its exit code is held.

State