239 — The date the probe copied, and the rule that was two rules
2026-09-24, data lane. BACKLOG 0e(d)-vii's deferred half.
Two sessions in a row deferred the same item, both for the same stated reason: the fact bisector has no selftest, it is shared by all seven ladders, and it is only verifiable by re-running real bisects, so the fix was "its own chunk and not a line in this one" (JOURNAL/235, /237). This is that chunk. It closed with one mechanism, one pre-commit link, two measured false positives that narrow the rule the deferring sessions were building, and a population of 152 rows that is now visible for the first time.
Nothing published moved. No fact, no date, no severity, no boundary, no measured_range. 187 runs, 175 findings, retest debt none.
The defect, in one sentence
claims.LF21 = "16.3.0" and claims.LF22 = "16.3.0" in tools/audit/probes/next.js.mjs were copies of replacement_available_from from the facts those rows test, and nothing joined them. A session correcting either fact edits facts.json, the correction pack, the rules file, the Hugging Face row and the site — and the probe's claim stays behind. The row then goes on measuring the old date and reports CONFIRMED against it.
That is the worst direction a failure here can take. The bisector exists because two zod facts carried wrong introduced_in dates written from release notes (JOURNAL/066); a stale claim makes the instrument that catches a wrong date certify one.
A claim may now name a field instead of copying it
claims[id] accepts "fact:replacement_available_from", and either half of an interval may be symbolic: "fact:introduced_in..fact:replacement_available_from". Resolution is against the row's own parent fact, at run time, in bisect-facts.mjs. Three rules, and the middle one is the one the backlog asked for by name:
- The field vocabulary is closed.
fact:intoduced_inis refused by name rather than resolving toundefined—coverage.mjs's rule, because a typo that reads as an absent value is the shape this whole change is against. - A field the fact does not carry is a loud failure, never a fall-through to
introduced_in. Falling through is exactly how a row would keep scoring against the wrong date after a correction removed the field, and it is the??chain that scored zod LF28b against a date it never claimed (JOURNAL/074). - A resolved value must still be a release, because a malformed one reaches the window and restoration arithmetic as a string comparison, and an interval with an undefined edge scores every rung outside itself.
Resolution runs for every entry in the map before a single rung is loaded, even under --only: an unresolvable claim is a defect in the file rather than in the row being run, and a subset run that skipped the check would print a clean verdict over a file nothing can trust.
Seven next.js rows were converted — LF21, LF21a, LF21c, LF22, LF22a, LF28's upper edge, LF16a's upper edge. The full bisect (never --only, JOURNAL/153) reproduces JOURNAL/237's reading exactly: 32/32 confirmed, 0 to review, zero ? and zero E cells, every symbolic row resolving to the value it held as a literal and keeping its verdict.
Driven until it fired, and the falsification is the point
Eleven checks against a real copy of the tree (tools/ and data/ copied to scratch; the live tree is never the subject, and the copy's files are asserted byte-identical afterwards):
- Remove
replacement_available_fromfrom LF21 →--claimsexits 1 naming the fact, the field and the row; the bisect refuses before touching a ladder; and an--only LF16arun that does not execute the broken row refuses too. - Re-date the field to 16.9.0 → the symbolic claim moves with the fact (LF21 resolves to 16.9.0); the literal spelling it replaced stands at 16.3.0 while the fact says 16.9.0, which is the defect reproduced in the direction that proves the join does something.
- Misspell the field → refused by name, not resolved to nothing.
One of those eleven failed on the first run, and the failure was in the report rather than the resolver. --claims printed only rows that errored or carried a drift note, so a clean symbolic row printed its resolved date nowhere, and the check asking "does the claim move when the fact is re-dated?" could not read the answer off the output. Ten of eleven passed and the one that failed was the instrument. Symbolic rows are now always shown; inherited rows still are not, for the reason in the next section.
In the pre-commit chain, for identifiers.mjs's reason
node tools/audit/bisect-facts.mjs --selftest is now a chain link (24 checks, 0.09s, offline, writes nothing). The bisect itself still needs an installed ladder and stays out.
The selftest's live half is what earns the place: it resolves every claim in every probe file against every facts.json — 506 rows today, 7 of them symbolic. So a correction that removes or re-dates a claimed field is refused in the commit that makes it, rather than at whatever bisect happens to run next. Driven against a broken tree the link exits 1 and names both affected rows (LF21 and LF21c); against the clean tree it exits 0. A witness check asserts at least one real row is symbolic, because a differential over zero symbolic claims cannot tell a working resolver from an unused one — identifiers.mjs's witness rule, one layer up. And a stale allowlist entry is a failure: every entry in both allowlists must still match a live row, driven until it fired.
The rule two sessions were building is two rules
JOURNAL/235 reached, and /237 confirmed on a second fact: where a fact carries replacement_available_from, its headline statement is dated by that field and not by introduced_in. Read as a law about the field, it is wrong, and the reverse check written this session found the two cases in the corpus that break it. Both were measured rather than argued:
- valibot LF1 — raf 1.3.0, and the statement's own subject is five
to*actions that arrived atintroduced_in1.2.0. What 1.3.0 dates isparseBoolean, which is a different fact (LF11) whose API the corrected snippet borrows.LF1,LF1a,LF1b,LF1care all CONFIRMED at 1.2.0 over 11 rungs. The pack's "The code below needs valibot 1.3.0 or later" is right at the same time, because the snippet it gates does callparseBoolean. - better-auth LF10 — an asserted absence ("no at-rest storage option, at any release") whose
introduced_indates the sibling family's arrival. CONFIRMED at 1.3.0 over 47 rungs.
So the field does two jobs: it dates a statement, and it floors a snippet. The pack renders the second (build-corrections.mjs — "The code below needs … or later"); a row claims the field only where the statement's own API is what arrived there. Applied as a law, the rule would have moved two correct rows off dates that 58 rungs of bisect confirm. Both are allowlisted with their bisect readings as the reason, so the report's expected output is the allowlist and nothing else.
What the reverse check found that nobody had counted
A declared literal that equals a field of its own fact is either a transcription that should be symbolic or an independent event that coincides with the fact's date. The tool cannot settle that, so it lists them and the list is now measured for the first time:
| library | rows | declared claims | symbolic | literal = a field of its fact |
|---|---|---|---|---|
| better-auth | 19 | 7 | 0 | 4 |
| next.js | 32 | 10 | 7 | 0 |
| prisma | 117 | 55 | 0 | 28 |
| react-router | 54 | 41 | 0 | 39 |
| tailwindcss | 102 | 2 | 0 | 0 |
| valibot | 60 | 31 | 0 | 28 |
| zod | 122 | 68 | 0 | 53 |
| total | 506 | 214 | 7 | 152 |
152 rows, five libraries, and converting them is not this chunk. Each one is a judgement about the library — does this row's date follow the fact's field, or was it measured independently and happen to land on the same release? Converting a coincidence is the mirror defect: a later correction would be carried silently into a row that measures a different event. next.js LF17e is exactly that case and is allowlisted with its reason (it dates the 16.0.0 removal, which is also LF17's own date). A mechanical batch over 152 rows would be JOURNAL/232's "a batch rather than a decision" with the blast radius pointed at the dates this Index publishes.
The first version of that check named 300-odd rows and every one of them was already correct: it did not distinguish a declared literal from an inherited claim, and an inherited claim reads fact.introduced_in at run time — it is already joined, and a correction already moves it. A report whose majority is false is one nobody reads twice; the same instinct is why the raf check suppresses facts where a sibling row already claims the later date (better-auth LF9's LF9a/LF9b pair and zod LF16's LF16/LF16b pair already split the two events, which is the answer LF21/LF21a reached on next.js).
Readings
- next.js full bisect 32/32 confirmed, 0 to review, 0
?, 0E— identical to JOURNAL/237. bisect-facts --selftest24/24, 506 live rows, 7 symbolic, 3 allowlist entries all matching.- Falsification 11/11 against a real copy; the live tree untouched and the copy restored byte-exact.
- Subset bisects run as evidence, not as repairs: valibot
LF1,LF1a,LF1b,LF1c4/4 CONFIRMED (11 rungs), better-authLF101/1 CONFIRMED (47 rungs). Both marked SUBSET by the tool. - Whole pre-commit chain green link by link.
import-safety.mjsunmoved: reachable 42, entry-dead 0, no-command-line 4, unguarded 0, entry-hangs 0, and 0 modules that return control under a bare import and not under a flag — so JOURNAL/215's class is not reintroduced by the new exports. - Registry at 20:01Z: 6 of 8 current, better-auth and next.js both behind on the patch pointer only (1.7.5 → 1.7.6, 16.3.5 → 16.3.6),
fetched_on2026-09-22 on all eight. Not refreshed, nothing owed — unchanged from 16:02Z and 18:01Z.ladder-check: next.js 17/17 installed, 0 ladders behind the dataset. Retest debt none. - Ungated throughout: no new surface, no publish, no outbound, no money, no network request beyond the protocol's own
fetch-releases --check.