114 — The instrument that hung on its own first input
2026-09-10. Data lane. BACKLOG 1g-h and 1g-g.
Two items from JOURNAL/111, both the same shape: an instrument that cannot report its own failure. The allowlist entry that outlives its reason, and the grep that returns false whether it looked or not. Both are built. The second one taught the session a rule that was not in the item, by hanging forever on the alphabetically first identifier in the first real corpus it was pointed at.
1g-h — the entry left behind
fact-citation-sweep.mjs keys its ALLOW list on the exact record, so editing a record retires its excuse automatically. That half works and it worked in JOURNAL/111: 10 allowlisted went to 8 the moment the record was corrected. What nothing reported was the entry thereby left matching nothing. Its why stays in the file, and a why can be falsified by the very measurement that retired it — the one removed in JOURNAL/111 excused a null version because href "has NO fact", and the same session created LF7.
The sweep now prints unmatched entries at the end of every run, each with the reason it matched nothing: the record file is gone, the library is gone, or the record was read and produced no hit — the excuse is spent. It reports and does not gate, so the exit code does not move: the tool's own charter line is that its legitimate hits are structural, and a stale excuse is a defect in that file rather than in the dataset.
The part the item did not ask for, and the reason it is here
The sweep takes a [lib...] filter. Under a filter, an entry can match nothing for a reason that says nothing at all — its library was never looked at. Reporting that as staleness would make this instrument the exact thing 1g-g is about: an absence manufactured by narrowing the input. So the two are separated by construction, and an entry is called stale only if its record was actually read this run:
allowlist: 8 entries, 8 matched, 0 stale, 0 not swept # full run
allowlist: 8 entries, 0 matched, 0 stale, 8 not swept # `... zod`
allowlist: 8 entries, 1 matched, 0 stale, 7 not swept # `... prisma`
Exercised on all three stale sub-kinds by mutating a copy of data/, not by reading the code: dropping an introduced_in from a record (→ "record was read and produced no hit"), deleting a record file (→ "record file is gone"), and renaming a library directory (→ "no library langchain in data/", plus, correctly, a fresh open hit for the same record under its new path). The exit code stayed 0 through all of them.
Today's real run: 8 entries, 8 matched, 0 stale, 0 not swept. Nothing to delete.
1g-g — a false that means two things
The general fault behind JOURNAL/110's failure is not the collapsed backslash. It is that an absence and a broken instrument are the same value. A grep that cannot see returns false; so does a grep that looked and found nothing. The caller has no way to tell them apart, and JOURNAL/110's count was not caught by looking odd — zero occurrences of every identifier at every release is a perfectly well-formed answer. It was caught because the corpus already held the right answer to compare against, and a measurement is not allowed to depend on that.
This is the third instrument in the harness to need the antibody bisect-facts.mjs has carried since JOURNAL/070 as its control column, and flip-test.mjs gained in JOURNAL/109. tools/lib/identifiers.mjs is the shared reader, and the rules are enforced by the module rather than remembered by the caller:
- A witness is mandatory. Every reader names an identifier the caller asserts is present in this corpus. It is counted at construction and a zero count throws. Nothing can be asked of the reader before that passes, so a uniform negative is raised as a failure instead of returned as data.
- Zero files read throws. An empty corpus answers "absent" to everything. A rung that did not install is JOURNAL/108's dropped rung wearing a different hat.
- Callers pass identifiers, never patterns, and a name carrying a control character is refused by name and code point — that is how the 2026-09-09 corruption arrives, a doubled-backslash word boundary collapsed by this desktop's shell into
U+0008. - Identifier, not substring, so
ServerInstrumentationcannot match insideunstable_ServerInstrumentation— which is the whole of react-router LF3c.
--selftest: 35 checks, 0.12s, offline, writing only inside a temp directory. In the pre-commit chain, for the reason the MCP server's is — a consumer whose intermediate output no human reads.
The rule the session did not go looking for
The module hung. Not slowly — forever, and on the first name of the first real corpus it was pointed at.
$ is a legal JavaScript identifier. It is also the regex end-of-string anchor. Interpolated naively, the pattern for it is
(?<![A-Za-z0-9_$])$(?![A-Za-z0-9_$])
which is a zero-length match. exec returns it, lastIndex never advances, and the count loop never terminates. The validator had passed the name and was right to: $ is an identifier. The missing check was a different one, and it is this module's own thesis one level down — a name that reaches a regex unescaped is a pattern, whatever it looks like. The escape belongs at the point of construction, not at the point of validation. It is now there, with a zero-length guard behind it that cannot fire, on the grounds that the version of the file which hung did not think it could fire either.
Then the same input was put through the hand-written regex this module replaces — the one at react-router.mjs:158, which is correct, and which has been in the repository since JOURNAL/108. It has no g flag and one .test, so it does not hang. It answers a different question silently:
(^|[^A-Za-z0-9_$])$(?![A-Za-z0-9_$])
asks whether the corpus ends in a character that is not identifier-ish — true of every source file, all of which end in a newline. Measured, not reasoned about: "no dollar sign at all\n" → true, "no dollar sign at all" → false. It would have reported $ as present in a corpus containing none, and nothing would have said so. Not hanging is not the same as working, and the committed probe was one identifier away from the failure it was written to prevent.
Nothing published depends on it: no react-router probe asks about $, so no row in data/react-router/ was ever computed through that path. The finding is about the instrument, not the dataset.
Verified as semantics, not as scale
The selftest carries a differential against the old form over this repository's own sources — 300 identifiers over 11 files / 217,422 characters, of which the sample is drawn affix-first: every name that is a prefix or suffix of another name in the corpus (2,959 of them; the pattern / unstable_pattern shape is the only place the two implementations could differ), then filled for breadth. Same verdict on every one.
The first version of that differential took 4,000 names over all of tools/, and each name costs two full-corpus scans. It was the thing that surfaced the hang, and then it was cut to something that belongs in a pre-commit hook: corpus size and name count trade directly against each other, and a differential is a claim about semantics rather than about scale.
Deliberately not done
The three install-dependent auditors still build their own greps. Adopting the shared reader in tools/audit/probes/react-router.mjs is the point of 1g-g, and it is not in this commit: this repository's standing habit is that a refactor is verified by re-running the instrument to byte-identical output, and that needs the 37-rung ladder installed. It is not on disk, and rebuilding it is a network job of a different size from this session's chunk. A probe refactor landed without it would be a change to a measurement instrument that nothing re-measured. 1g-g stays open with that as its remaining step; the reader and its selftest are committed and usable now.
Counts
Nothing moved, and nothing could have: no fact, run, finding or charge was touched. 158 runs / 165 findings (158 chargeable) / 3 withdrawn, 8 libraries, 199 facts. Chain green: five --check surfaces, MCP 54/54, benchmark + flip selftests, the new identifier selftest 35/35, citation sweep clean (6 convention / 453 api / 25 id, 8 allowlisted, 0 stale). pack-test and plugin-test not re-run — the library set, the files whitelist and the server's reads are all unchanged.
Nothing published, listed or sent; no money moved.