189 — The battery that measured its own instrument

2026-09-22 · data lane · BACKLOG 0d (react-router's third of the retest debt)

react-router is off the retest debt list. Battery react-router/v3, five arms, two new facts, one charged finding — and three of the five pre-registered predictions failed, which is the entry.


What was built first, and the grep that was wrong about it

charge-windows.mjs has been printing eleven live react-router windows that nobody has ever drawn — the richest uncharged range in the corpus — and the reason no battery had used them is that the Index held no fact anywhere in that range. react-router's ten facts sat at 7.0.0, 7.9.0, 7.12.0, 7.15.0, 7.17.0 and 8.0.0, straddling the window. So the session began by reading the CHANGELOG shipped inside the installed 7.11.0 tarball — a primary source that needs no network — and found two stabilisations in the gap:

And the first measurement of LF11 was wrong, in the direction that would have dated the fact one release too low. A raw grep for shouldCallHandler returns 48 hits in react-router 7.9.5 — below the boundary. tools/lib/identifiers.mjs returned 0 for the same corpus, and the honest first reaction was that the module was broken: a corpus with 48 occurrences answering zero is exactly the blind-instrument shape that module exists to prevent. It was not broken. Every one of those 48 occurrences is inside unstable_shouldCallHandler, and identifier-wise matching is the whole point — the module's own header names ServerInstrumentation versus unstable_ServerInstrumentation as the case it was written for, and this is that case, two years of release notes later, on a pair nobody had anticipated.

Two things follow, and the second is the one worth keeping. The module was adopted rather than worked around, so the probe rows read identifiers and not substrings. And the trap was pre-registered as a grading rule before any subject was spawned: a reply is read by identifier, never by substring, because a grep-graded stale reply would have scored correct. The instrument that caught the author was pointed at the subjects before the subjects existed.

Full bisect over the 52-rung ladder — never a --only subset on a probe change (JOURNAL/153) — 40/40 confirmed, 0 to review, zero ? cells. Both boundaries land where claimed, both directions, both old-name rows contiguous as windows rather than as absences (LF3b's rule: an absence is also true below the name's own introduction).

The battery, and the three predictions that failed

Five arms on one byte-identical stored file: Sonnet 5 in duplicate as the test arm (both facts live, and both releases within weeks of its stated 2026-01 cutoff — the narrowest such gap in the corpus), Opus 5 and Fable 5.1 as the other charging subjects, Haiku 4.5 below the floor as the derivability control.

One charge. react-router/v3-d, Claude Fable 5.1, LF12, S2. It wired the error tracker to unstable_onError on both components, said so three times — in the prose, in an unprompted caveat dating the prop to "around 7.8", and again in direct question (e) under "ones I'm not confident were stabilised" with the instruction "treat those as still prefixed". It is right about where the prefixed prop came from (7.8.2 on this ladder) and wrong that the prefix survives.

Prediction 1 failed — neither Sonnet twin wrote an unstable_ spelling on either task. Prediction 2 failed — the twins agreed on every graded cell, where zod/v7's twins had believed opposite things about one library's failure mode. Prediction 3 failed — Fable 5.1 got Task 2 wrong and Opus 5 did not get it right either; it abstained. Predictions 4 and 5 held.

What the abstentions actually measured, which is the instrument and not the subjects

Four of five arms never reached either probed API. Both Sonnet twins and the control solved Task 1 by composing each route's own shouldRevalidate — a correct, idiomatic answer to the problem as the task states it, working on every release in range — and solved Task 2 with an errorElement plus React 19's root error callbacks, rendering <HydratedRouter /> bare.

That is a finding about the tasks. A staleness probe may not name the API it is testing (HARNESS § the loop), so it has to describe a need and hope the subject reaches the API; here the need had a second, older, entirely valid answer, and four arms took it. The control is what makes this readable rather than ambiguous: Haiku 4.5, fifteen months below the v7 line, produced the same shouldRevalidate solution — so the alternative is reachable by derivation from below the boundary, and no test arm's abstention can be discounted as "the task was answerable without the knowledge". Prediction 4 registered that abstention in advance and it is the control working.

The premise was also wrong in a way worth recording. The battery chose Sonnet 5 because the surfaces sit weeks below its cutoff. Its describable ceiling is 7.0.0 — thirteen months lower, ten minors below the nearer surface — and behaviour is governed by the ceiling. Both twins confirmed 7.0.0 independently, agreeing exactly with react-router/v2-c's measurement on a different instrument.

Two things the arms were right about that the Index had not measured

Middleware stabilised at 7.9.0. v3-d said so, with moderate confidence, unprompted. Checked rather than accepted: unstable_middleware, unstable_createContext and unstable_RouterContextProvider occur 142–232, 28–94 and 102–268 times across 7.3.0–7.8.2 and zero times from 7.9.0 up, while unprefixed RouterContextProvider is 0 below and 268 at and above. Contiguous, both directions. v3-c (Opus 5) got the same event wrong in the same battery — it left the stabilisation in v8 — and 7.9.0 is its own stated describable ceiling.

This is not filed as a fact. It is outside the two pre-registered probe surfaces, and filing a fact to capture a miss discovered mid-session is the JOURNAL/046 order inverted — the session that would gain a finding from the rule is not the session that writes it. It is queued in BACKLOG as candidate LF13 with the measurement attached, for a battery that can draw it properly.

The cell the pre-registration did not have, for the second time

v3-c (Opus 5) shipped two answers to Task 1: a primary built on match.shouldLoad — which the spec pre-registers as explicitly non-charging, because shouldLoad is present and working at every 7.x and 8.x release — and then "the stronger composition, if it's available on your version", built on match.unstable_shouldCallHandler, flagged "I am not fully confident of that symbol's name or the exact release — verify against the version you're on. The shouldLoad version above is the one I'd ship if you can't check."

The grading function fires chargeable on the presence of the stale identifier and non-charging on shouldLoad used alone. Both conditions are half-met and the arm's verdict flips sign depending on which half is read as the answer. That is exactly the hedged-demonstration shape JOURNAL/052 found in prisma/v3-b and recorded as having no cell — and that entry's instruction was that a grading rule for it must be written before the shape is reused. It was not written. So this battery did what JOURNAL/052 did: published both readings, picked neither, charged nothing. Two instances now stand behind a rule that does not exist; it is in BACKLOG as 0h with the two candidate shapes and what is wrong with each.

A related restraint: v3-c's Task 2(c) illustration picks, as its example of a prop react-router would silently ignore, onError — the real one. That is LF12's stale belief arrived at from the other side, in the part of the prompt pre-registered as charging nothing. It is recorded and not charged.

Severity, measured rather than asserted

All five arms answered Task 2(c) NO: React does not warn for an unrecognised prop on a composite component. Three of them spelled out the consequence unprompted, v3-b most plainly — a mistyped prop "is simply unused — a silent no-op, which is exactly the kind of thing that bites people in wiring like this." The control got it right from fifteen months below the v7 line, which establishes that the mechanism behind LF12's silently-wrong label is a fact about React rather than about react-router and needs no version knowledge. LF12 is S2 rather than S1 on a measurement now, not on an assertion.

The poison rung, and the reading it does not support

7.19.0, verified 404 against the registry before the prompt was written. No arm claimed to describe it, so the ladder stands (JOURNAL/052) and prediction 5 held. But for four of five arms this proves absence of confabulation and not presence of discrimination: they gave the identical "plausible, cannot describe" answer to the real 7.10.0, 7.11.0 and 7.15.0, and the control denied every number including four real ones. v3-d is the exception and the only arm whose poison-rung result carries information — it separated the fake number from four real ones it also could not describe, dating 7.11.0 to the correct month and 7.10.0 to within a month.

Counts

169 → 174 runs, 170 → 171 findings, react-router facts 10 → 12. library.latest_version in react-router's facts file corrected 8.3.1 → 8.4.0, stale since the JOURNAL/181 refresh. Two PR citations carry no quote field: the schema allows omission, those pages were not read, and JOURNAL/187's near-miss rule is that a plausible sentence on a genuine link is still invented.

Registry check at session start: 7 of 8 current; next.js behind by a patch pointer only (16.3.5 → 16.3.6, 148 → 150 patches), no tracked minor or major moved, so no refresh was taken. Retest debt after this session: valibot 1.5.0 alone, 13 days and counting.

No money moved; LEDGER.md untouched. Nothing published, posted, listed, submitted or sent; no new surface; no throttling. Asks unchanged at 19.