215 — The Boolean that was wrong in both directions

2026-09-23, data lane. BACKLOG 0e(d)-iv. Closed.

The item asked a narrow question. measured_range records which releases a fact's statement was actually executed against; build-hf.mjs publishes a row field bisected, derived as Boolean(fact.measured_range); and JOURNAL/213 had wanted to add a range to tailwindcss LF22 — a fact measured by tw-project-facts.mjs, which is not the bisector — and had taken it back out by hand rather than ship a row claiming the wrong instrument. The item's prescription was to grow the schema a way to say which instrument, compute bisected from that, and give the four second-instrument facts their coverage. Its last line was: Not urgent: nothing published is wrong today.

That line was false, in both directions, and the larger half was the one nobody was looking at.

What the survey found

126 of 231 facts carried a measured_range. Every one of them already names its instrument, in prose, in the free-text source field — so the question could be asked of the corpus rather than guessed at:

Neither is a lie anyone typed. Both are what a derived field does when it is derived from the presence of a value rather than from the value. In facts.json a reader can see the source string and tell the cases apart. In a row they cannot, and the row is the unit that travels (JOURNAL/198).

What was built

measured_range.instrument is now required, from a closed vocabulary — bisect-facts, tw-project-facts, benchmark-grading — and tools/lib/coverage.mjs holds it. bisected is computed from that value; measured_by ships beside it, because bisected: false on a row that does carry a range is otherwise unreadable — the reader cannot tell executed by something other than the bisector from nobody ever ran it, and those two are what the field exists to keep apart.

One of the three values is checkable and two are not, and the module says so rather than pretending otherwise. The bisector's coverage is already written down in a second place, so build-index.mjs joins a bisect-facts claim against tools/audit/probes/<lib>.mjs in both directions: a fact claiming it must have rows there, and a fact that has rows must claim it. That is JOURNAL/213's hand refusal made structural — one session's discipline turned into a build failure. The other two instruments record their coverage nowhere this module can join against; their values are declarations and are labelled as declarations, because a check that could not fail would publish a guarantee the module does not have.

Two consumers needed the rule and neither re-implements it (JOURNAL/211). The row-id → fact-id join is bisect-facts.mjs's own expression, moved here and imported back by it, with a differential over all 473 real probe rows asserting the two still agree. The static probe list is differentialled against the directory and throws on a mismatch: a library whose probe file was never added to the list would read as having no bisector coverage and pass the gate by being invisible to it, which is JOURNAL/183's flattering denominator one layer down.

The ranges are read off runs, not off the tools' source

Both instruments were re-run this session and every range was read off the output.

151 facts now publish as bisected (was 126, which included one that was not and excluded 26 that were), 4 as tw-project-facts, 1 as benchmark-grading, 75 with no range. No statement, date, severity or boundary moved: runs stay 178, findings 172, facts 231.

Found on the way, and it was live

Writing the module's selftest produced a clean exit 0 and no output. The cause is a fault class this repository has now paid for four times, arriving by a new route.

tools/lib/identifiers.mjs triggers its selftest on process.argv[2] === "--selftest" with no entry-point guard. JOURNAL/208, /210, /212 and /214 retrofitted guards onto tools that ran their main on any import; this shape is different and worse. Under a plain import the module is correctly inert — which is why import-safety.mjs, whose predicate imports each module with a plain argv, reported it clean. Under an import from a program invoked with --selftest, its selftest runs and calls process.exit(0), and the importer never gets control back. The programs that pass that flag are selftests.

It was live the moment build-hf.mjs reached identifiers.mjs through the new module's probe imports: node tools/build-hf.mjs --selftest, a pre-commit chain link, printed identifiers.mjs's selftest and exited 0 without running its own. A gate switched off that looks installed, two days after JOURNAL/212's and by a different mechanism.

The fix went to the class, not to the file that blocked me — JOURNAL/202's rule, which JOURNAL/214 recorded three sessions failing. A behavioural probe (one child per module, importing it from a program invoked with the flag) named 32 modules that never return control, against import-safety's 20. Three are library modules something actually imports — identifiers.mjs, page-content.mjs, indexnow.mjs — and five more were their cascades. All three are guarded in the established spelling, all three CLIs verified still speaking, and the re-probe reads 24: the remaining ones are entry-point CLIs nothing imports, which is JOURNAL/214's already-enumerated latent population.

The blind spot is import-safety.mjs's, not this session's, and it is not patched here. That tool belongs to the distribution lane and the widening — probe each module under the flags a selftest would pass, not only a bare argv — is filed for it rather than done to it. The reproduction is two lines and is in the item.

And the probe walked into the fault it was measuring

Running it had a side effect nobody asked for. Importing a module from a program invoked with --selftest runs that module's main, and two of the 132 are network tools: fetch-releases.mjs and fetch-candidates.mjs ran, reached the registries, and wrote data/<lib>/releases.json for all eight libraries and data/candidates.json. It was not visible in the probe's own output — both were killed by its 30-second timeout and reported exit null — and it was found afterwards, by git status, because the registry check at the end of the session disagreed with the one at the start.

This is JOURNAL/214's traction.mjs finding in a second place: one writes and it is the one that matters. That entry guarded the one writer it found; these two were invisible to it for the same reason identifiers.mjs was, and a behavioural probe that fires a module's main will fire a network tool's main.

Both refreshes were reverted and the tree rebuilt from HEAD's releases.json, for two reasons and the second is the stronger. A refresh moves data/index.json, the charge windows, the site and possibly a fact's measured_range; the Operations rule makes it a chunk and a decision, and an accident is neither. And candidates.json's refresh came back with github_rate_limited: true and a window shifted by a month — a measurement taken under the exact condition probeability.mjs's own rules say must never replace one. data/index.json is byte-identical to HEAD after the rebuild, which is the check that the revert was complete: it carries no measured_range, so nothing this session actually decided touches it, and anything left over from the accident would have shown there.

The registry reading is therefore the one the session started with, unchanged and unrefreshed.

Housekeeping

coverage.mjs --selftest (24 checks, every refusal driven until it fires, the gate asserted non-vacuous against the real 231-fact corpus) is the newest link in the pre-commit chain, for identifiers.mjs's reason: a consumer whose intermediate output no human reads. fact-citation-sweep.mjs run per the standing rule — 488 api joins, 8 allowlisted, 0 stale. Full chain green. Site 456 pages. verified_on not moved on any library: coverage metadata was added and no fact was re-checked against a primary source.

Registry check 20:02Z: 7 of 8 current, next.js behind on the patch pointer only (16.3.5 → 16.3.6, 148 → 150), fourteenth consecutive identical reading, not refreshed, noted in BACKLOG 0k. This session worked tailwindcss and zod coverage against releases releases.json already tracks and needed no refresh to do it — and it re-verified that reading at the end, after reverting the accidental one above, rather than quoting the number it opened with.

Gates unchanged: 18 open asks (counted by tools/asks.mjs, not by hand), none answerable by this lane. No account, no spend, no publish, no outbound, no new surface.