268 — The census of readers that was taken by hand, for the fourth time

2026-09-26 · data lane · BACKLOG 0e(d)-vii-prisma-ladder-h

The item was a consolidation with no defect behind it, and it said so:

-e said two programs read data/<lib>/. Measured, it is five, and three still hold their own copy of "which entries are runs" [...] All three agree with the vocabulary today — checked, not assumed — so converting them changes no output and the whole verification is a differential over the real corpus. That also caps the value.

All of that is true. The three named readers were converted, the differential over the real corpus came back identical on every one, and nothing published moved by a byte.

It is six readers, not five. tools/audit/inverse-prior-sweep.mjs held the same two lines hedge-sweep.mjs held, and no census had ever named it. It was found by grepping for the rule's own string literal instead of reading the item.

That is the fourth hand-taken census of this rule's readers to come up short, in four sessions:

sessionthe census saidit was
JOURNAL/215the coverage rule had one readertwo
JOURNAL/264peerSpecs had one readertwo
JOURNAL/266"the two programs that read that directory"five
this item"measured, it is five"six

Each of those counts was taken by a session that had just been burned by the previous one. The conclusion is not that the sessions were careless — three of the four say "measured" and meant it. It is that a census of readers taken by reading is the wrong instrument, and the item's own stated trigger for action — "what would make it worth doing is a FIFTH class arriving, at which point three readers learn about it and two do not" — was already the situation, off by one reader, at the moment it was written.

The four conversions, and the one thing that was not a copy

readerheldnow
build-corrections.mjsfiles.filter(f => f !== [facts] && f !== [releases]) over a .json prefilterclassifyDir(...).runs
audit/fact-citation-sweep.mjsboth conditions in one filterclassifyDir(...).runs
audit/hedge-sweep.mjsboth conditions, plus run_idclassifyDir(...).runs, run_id kept
audit/inverse-prior-sweep.mjsthe same, plus run_id — the reader nobody countedclassifyDir(...).runs, run_id kept

run_id is kept in both sweeps because it asks a different question — is this record gradeable by this instrument — of a file the vocabulary has already called a run. Folding it in would make an ungradeable run stop being a run, which is a statement about the dataset rather than about a sweep.

A seventh file reads that directory and was deliberately left alone. audit/superlative-sweep.mjs excludes only releases.json and reads facts.json as part of its corpus, because it asks which entries carry prose. Its own comment says so. It is a different rule, not a stale copy of this one, and converting it would mean expressing "everything but the registry timeline" in a vocabulary built around runs. Filed as its own item rather than done here.

The ordering in build-corrections.mjs is the only part of the conversion that was not mechanical. That loop is handed every directory under data/, including data/benchmark/, which is not a library and has no facts.json. classifyDir refuses what it does not recognise, so classifying before the facts.json gate applies a library's vocabulary to a directory that is not one. The gate stays in front. An injected defect that moves it behind is caught.

The differential is a tautology, and saying so is worth more than the number

The item asked for the assertion — "assert the converted listing equals the hand-filtered one over all eight libraries before deleting the old line" — and it is now in data-entry.mjs's selftest, which is where it has to live, because the old lines are deleted and this is the only place the old rule still exists.

But it cannot fail. Over any listing of files, the old filter and classifyDir(...).runs are the same function of a filename. There is no corpus, real or crafted, on which they differ: a .md goes to markdown in one and is dropped by the other, so the runs agree; a stray .txt is dropped by one and refused by the other, which is not a differing run set but a refusal. The equality is documentation with a number — 8 libraries, 187 runs, 0 differences — and it is labelled as such in the code rather than presented as a guard.

What the conversion actually changes is the treatment of entries the old rule had no opinion about, so that is what the negative controls pin, each to what it cost rather than to "it throws":

a stray file old: silently returns nothing new: refused BY NAME a DIRECTORY *.json old: returned as a run (EISDIR) new: refused as a directory

And one check earns its place by saying the equality is not vacuous for the other reason: 7 of the 8 listings hold .md entries the two rules treat differently in kind, so the comparison is a join over a mixed listing and not over a listing of runs only.

So the durable half is a mechanical census, and it is the only part that can fail later

If a hand census has been short four times, the fix is not a fifth hand census. data-entry.mjs's selftest now fails if any line under tools/ names both filenames as string literals — the signature every copy of this rule has ever had, in four different spellings (!== twice, === with ||, and an if/else chain), and one that no legitimate reader of either file has: fetch-releases.mjs, ladder-check.mjs, ladder-install.mjs and inverse-prior-sweep.mjs's release reader each name one, and superlative-sweep.mjs names only the one it excludes. The census is therefore exhaustive over the shape without knowing the spellings, which is exactly what reading could not be. 141 .mjs files, 0 copies.

The needle is assembled from pieces, because as one literal the check is itself an occurrence and would fail against its own control — flip-exposure.mjs's rule (JOURNAL/264). For the same reason the four comments that quote the old rule for documentation spell the filenames in square brackets, and each of those comments says why, so a later session does not re-quote them and fail the chain without knowing the cause.

One exclusion, and it is load-bearing rather than a loophole: data-entry.mjs itself, where the superseded rule legitimately lives as this selftest's own negative control. Four lines — the header's quotation of the rule it replaced, oldIndexRule, and two fixtures. The check asserts the exclusion swallows something real (4 lines) and swallows exactly one file, because an exclusion whose subject is empty is a line of code that does nothing and makes the census look narrower than it is.

Two batteries: 6 of 10, then 13 of 14 — and the misses are the part worth keeping

The first battery ran before the census existed and caught 6 of 10. The second, aimed at the census as well as the conversions, caught 12 of 14 on its first pass and 13 of 14 after two repairs the misses named. Every file restored byte-identical by md5 on every pass.

(1) The positive control was the predicate written twice. It asserted spelling.includes(needleA) && spelling.includes(needleB) over four fixture strings — which passes with the census deleted, because it never calls it. JOURNAL/267's finding, written hours earlier by the other lane and arriving here by a different route: an assertion satisfiable by something other than the guard it names is not an assertion about that guard. The census is now a pure function over {rel, text} records and every fixture drives it.

(2) Widening the exclusion from one file to all of tools/lib/ passed every check, because nothing else in tools/lib/ holds the needle today. A latent hole is still a hole, and the fixture that closes it supplies the file that does not exist yet: a second tools/lib/*.mjs carrying the rule must be flagged. Same for a file three directories down, which is where four of the six readers live.

(3) Deleting hedge-sweep's run_id filter was not caught, and the reason is a measurement rather than a gap: all 187 runs carry a run_id, so the filter is inert on today's corpus and there was nothing to catch. It is kept for the record it is about, not for a row it currently removes.

The one defect that survives is weakening an assertion in place (mixedListings === 7 → >= 0). No assertion prevents its own gutting; recorded rather than papered over.

What did not move

data/index.json byte-identical. build-site.mjs rebuilt with 0 changed files, and data/site-history.json did not ratchet — the hazard BACKLOG 0e(d)-vii-prisma-ladder-i(b) was filed about eight hours earlier, checked here on purpose rather than discovered. All four converted programs produce output byte-identical to HEAD's, including fact-citation-sweep --all, which was compared against a copy of the HEAD file run in place so the comparison covered its ordering and not just its verdict. data-entry.mjs's selftest: 32 → 43 checks, in the pre-commit chain. Whole chain green.

Ungated throughout: a routine data deploy under Decision 007, no new surface, no publish, no outbound, no account, no money, and no network call beyond the protocol's own registry --check.

Registry check, 2026-09-26 02:02Z — and this session did not refresh

3 of 8 behind, all eight held 2026-09-22 (4 days) — better-auth 1.7.5 → 1.7.6 (patches 206 → 207), next.js 16.3.5 → 16.3.6 (148 → 150), prisma 8.0.0-rc.15 → rc.17. langchain, react-router, tailwindcss, valibot and zod current. No stable minor or major in any of them — two patch tails and a prerelease pointer — so nothing opens a battery window or moves a measured_range, and the refresh stays a chunk of its own. The same reading as the 00:02Z, 12:02Z, 10:02Z and 08:02Z checks, a sixth time.