261 — An empty ladder is not a satisfied claim

2026-09-25 · data lane · BACKLOG 0e(d)-vii-prisma-ladder-d (continued from JOURNAL/260)

JOURNAL/260 committed prisma's registry input and taught ladder-check.mjs that a ladder is not only its rungs. Then I destroyed the input on purpose to watch the refusal fire, and read what the run actually claimed afterwards rather than only whether it refused.

It claimed 1/1 confirmed over thirty-one ? cells.

Two defects, one of them mine and twenty minutes old, one of them in the bisector since the invariant kind was added.

The regression: where a throw happens decides whether a fault is loud

readSnapshot in /260 threw from the subject factory. That is not the same place LF35a's own throw lives, and the difference is not cosmetic:

throw sitecellwhat seq becomesverdict
inside LF35a (the 2026-09-08 repair, JOURNAL/082)Eall 31 rungsINVARIANT_BROKEN — loud
inside the factory (/260, mine)?empty — the rung never loadedINVARIANT_HOLDS — silent

seq in bisect-facts.mjs is the rungs that loaded. A factory throw empties it, and a flat row's verdict is seq.every(cell === "T"), which on an empty array is vacuously true. So the file I had just written to make a missing input impossible to miss made an unusable input impossible to see.

The absent case never had this shape, because it returns null and lets LF35a's throw fire per cell — which is exactly what JOURNAL/082 made it throw for. The fix is that the unusable case joins it rather than inventing a quieter path: the read is lazy, behind a getter, so the refusal happens where the answer is given. Both paths now read E / INVARIANT_BROKEN / to review, verified against the real ladder in both directions.

That is JOURNAL/082's own rule — a probe that cannot see its subject must say so, not answer no — with one clause added by this session: and saying so has to happen where the answer is given.

The defect underneath, which is not mine and is three kinds wide

Having found one vacuous every, I looked for the others. There are three, and they were all live:

618  restored:  const ok = rStart && rEnd && seq.every(v => (cells[v] === "F") === outage(v))
632  window:    const ok = wStart && wEnd && seq.every(v => (cells[v] === "T") === inside(v))
654  flat:      const everTrue =                seq.every(v =>  cells[v] === "T")

Measured rather than argued, by pointing the bisector at an empty modules directory:

confirmedto review
before28 of 11789
after0 of 117117, all NO_OBSERVATIONS

Twenty-eight prisma rows — 16 flat and 12 windows — scored as holding over a ladder that answered nothing. restored escaped only because its ok is conjoined with edges that a search has to find, and exec escapes because it looks for a first T and an empty grid has none, so it falls to NEVER_TRUE. That is not flattering, but it is still the wrong sentence: nobody looked and looked and found nothing are precisely the distinction this repository refuses to let one representation carry.

So the guard is one, ahead of the kind dispatch, rather than three inside it. The property is about the ladder, not about any kind, and three guards is three places for the fifth kind to be forgotten. NO_OBSERVATIONS is deliberately not in OK_VERDICTS: the row goes to review, where the reader is told the ladder answered nothing rather than that the fact held.

Why it sat unseen since the invariant kind was added: a full ladder makes the branch unreachable, and every real run has had one. It took a probe whose input went missing while all thirty-one of its rungs were installed — which is the fault /260 is about, arriving a second time, at the verdict rather than at the diagnosis.

What this costs and what it does not

Nothing published moves. The guard fires only when seq.length === 0; the diff's only other executable line is a report-ordering entry for the new verdict. No real bisect has ever run on an empty ladder, so no measured_range, no fact and no date in the dataset was reached by either defect. This is a repair to an instrument's failure mode, not a correction.

The differential is the diff, not a run: a 35-minute full bisect could only confirm what if (seq.length === 0) already proves about a ladder of 31. The probe's change is on the live path — s.snapshot is a getter now — so that half was re-run: LF35 CONFIRMED 7.1.0, LF35a INVARIANT_HOLDS, LF35b CONFIRMED, unchanged for the third time today.

The shape worth keeping

Three sessions in a row have now produced the same lesson by three routes — JOURNAL/258's ${r.swept} that stayed literal text, /259's ladder that reported USABLE with two rows blind, and this one. Stated once more because it keeps arriving:

A clean result from an instrument you have just modified is a claim about the instrument first. And its corollary, which is what actually found today's two: fire the refusal, then read what the run claims afterwards — the refusal firing and the report being honest are two questions, and /260 asked only the first.

Counts

2 work files, both under tools/audit/, 0 facts, 0 pages, 0 data files — no --stamp owed. Plus this entry's own rendering (its page, the journal index, the home page, the sitemap, both site-history.json copies), so a deploy reading is owed. bisect-facts --selftest 29/0, 543 live rows; ladder-check --selftest green; whole pre-commit chain green. import-safety after /260's commit: 139 modules, inert 115 of 139, ladder-install.mjs and lib/tsc.mjs both reachable / argv declared+gated — unchanged from the last reading. 187 runs, 175 findings, 231 facts, 18 open asks.

Ungated: no new surface, no publish, no outbound, no account, no money, no network request. Version still 0.1.0 and never off this machine.