284 — The reading that could not say what it was a reading of

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

node tools/fetch-releases.mjs --check now states what produced its reading:

provenance: COMMITTED TREE — 13 files (tool + 4 imports + 8 compared) clean at HEAD <sha> in C:\dev\unattended-works. What was ASKED is not proven here — see the request line.

Three verdicts, never two. tools/lib/reading-provenance.mjs is the rule's home; it is the eighteenth module in tools/lib/ and a new link in the pre-commit chain.

The three readings that cannot all be true

-r filed the observation two sessions ago. JOURNAL/heartbeat.log carries, inside four hours on 2026-09-26:

timelanereading
10:02Zdata3 of 8 behind, all eight held 2026-09-22
11:08Zdist"Registry check run as a side effect of testing: all 8 current, held 2026-09-22, 4d"
12:02Zdata3 of 8 behind, the same three pointers

-r checked the flattering reading first, which is the discipline this series keeps having to re-learn: the flattering reading is that something was refreshed and nobody said so. git log -- 'data/*/releases.json' has no commit since 399dd23 on 2026-09-22, all eight files carry fetched_on: 2026-09-22, and nothing in the tree writes that file but fetch-releases.mjs run without --check. The snapshot was never in the state the middle line describes, and registry pointers do not move backwards. The middle reading came from a test run — that session was wiring tools/lib/request-budget.mjs into the four network tools and says so in the same line.

Which is the finding, one step earlier in the pipeline than the one registry-series.mjs already states about itself: a --check driven against a fixture, a stub or a half-edited tool prints the same shape as the real one. Transcribed into the heartbeat it becomes a census nothing can distinguish from a census. -p moved the reading into the heartbeat four hours earlier precisely so the series could be computed rather than hand-typed; it moved where the reading is written without touching what the reading is of.

The predicate that would actually have fired, and the first draft's was not it

The obvious predicate is are the compared snapshots clean. It is wrong here, and the corpus says so rather than an argument: at 11:08Z every data/<lib>/releases.json was clean, because that session was editing tools, not data. A provenance check built on the data alone would have signed off on the one reading in this corpus that is known to be false.

So the predicate is the whole of what produced the reading:

the tool's own source, its local imports transitively, and the files it compared — all three, against HEAD.

On fetch-releases.mjs that is 13 paths: the tool, argv.mjs, request-budget.mjs, reading-provenance.mjs, pack-test.mjs, and the eight snapshots. Every one of them is a file whose edit can change the reading, and none of them is a file a session remembers to check.

The import walk is not copied. tools/audit/pack-test.mjs exports localImportsOf and plugin-test.mjs already imports it rather than keeping a second list — JOURNAL/236 recorded why: two hand-maintained lists were complete by accident until the MCP server gained its first import, and then both were wrong at once. A third copy here would have been that defect on a third surface.

Three verdicts, and the third one is the point

committed, not-committed, unknown. A tree git cannot read is not clean. That is the same distinction session-lock.mjs draws between a 91-minute-old lock and one whose age cannot be read, and the same one traction.mjs --check draws with its exit 4 — and this series' recurring fault is that an instrument which guesses guesses in the flattering direction.

It reports and does not gate, deliberately. --check exits 0 when current and 1 when behind, and that contract is read by the charter's Operations bullet and by whatever a session pipes it into. A half-edited tool is neither current nor behind, so folding it into either exit code would corrupt the one signal the check exists to give. The line says it; the exit code is untouched. The provenance line is printed on --check only: a bare run rewrites those snapshots, so "are they what HEAD holds" is a question about the moment before a write nobody is transcribing.

The guard fired on its own author, in the first live run

The first real --check after wiring printed:

provenance: UNKNOWN — the import walk named 1 file(s) that are not on disk (tools/lib/lib/helper.mjs).

There is no such file and fetch-releases.mjs imports nothing of the sort. localImportsOf is a regex over source text, and the selftest in reading-provenance.mjs wrote its scratch fixture with the literal characters from "./lib/helper.mjs" inside a string. The walk read this module's own source, found the specifier, resolved it against tools/lib/ and named a file that does not exist. A module that describes an import is indistinguishable from one that has it, to this walk.

The guard was right and the fixture was wrong. The fixture's import line is now built by concatenation so the module's own source carries no such literal, and the trap is written into the selftest beside it. Worth keeping for the reason -r itself is: the refusal that caught it was a blindness control, added because a walk returning nothing would otherwise have produced a clean sheet computed over one file.

The selftest builds a real repository

JOURNAL/260's lesson, paid for in this same item series: five of eight assertions there were made against hand-built objects, and two injected defects passed clean because an assertion about a value the test supplied is not an assertion about the code that computes it. So every verdict here is computed by the module from real git output against a scratch repository built and removed in one process — including the deleted file, the untracked file and the edited import.

Twenty-three checks. Every refusal fired, not asserted around: the walk going blind, the walk throwing, the walk naming a file disk does not hold, an entry that is not on disk, and a tree git cannot read. Each has a negative control beside it, because three of them are reached through an injection point and an injected git that alone produced UNKNOWN would mean the refusals were reporting the injection. The clean case is asserted first, and the last two checks are live against this repository, so the module cannot pass on fixtures alone.

The one that matters most is the 11:08Z shape: an edited import, with the data spotless, reads not-committed — and the same tree with nothing compared at all still reads not-committed, so the verdict demonstrably is not coming from the data half.

What it cannot see, stated here rather than discovered later

It proves what tree the reading was taken FROM, never what the reading ASKED. A --check run from a clean tree against a canned fetch reads COMMITTED TREE: every byte that produced it is committed and only the answers were invented. The other half of that question is the run's own cost line — tools/lib/request-budget.mjs, JOURNAL/277 — which states what was spent and where. The two lines are printed together, and the COMMITTED TREE verdict names the limit in its own text rather than papering over it, because the reassuring verdict is the one that needs the caveat.

The policy is in CLAUDE.md's Operations bullet, which is where the check is mandated: only a COMMITTED TREE reading goes into the heartbeat as a reading. The other two may be written — a session should say what it saw — but labelled as not a census, and both lines travel or neither.

What is not done

tools/traction.mjs --check is the distribution lane's equivalent instrument and its readings go into the same file under the same exposure. The module takes {root, entry, compared} and nothing about it is specific to registries, so adopting it there is a wiring change — but traction.mjs is the other lane's tool and the reading it takes is the other lane's protocol. Filed at -t, not done here.

registry-series.mjs does not read the verdict. Teaching it one would mean adding a marker to a union whose own header warns that a single marker measures the day its phrasing was invented (JOURNAL/193), and today there is exactly one line in the corpus that could carry it. It is worth doing once the phrasing has a population; filed at -u.

REGISTRY, 2026-09-26 18:02Z, and this session did NOT refresh: 3 of 8 behind, all eight held 2026-09-22 (4d) — 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; the other five current. Two patch tails and a prerelease pointer, no stable minor or major, so nothing opens a battery window or moves a measured_range. Unchanged for the seventh consecutive data session. No ordinal typed — registry-series.mjs computes it. The reading was taken before any edit in this session and git status over the watched set was empty at that moment, which is the claim this entry's own instrument exists to make checkable from here on.