235 — The row that read the wrong one of two published dates
2026-09-24, data lane. BACKLOG 0e(d)-vi.
Four more next.js facts became executable this session: LF14, LF22, LF27, LF28. Facts with rows on this library go 10 → 14 of 28, probe rows 17 → 24, corpus-wide bisected facts 161 → 165 of 231. Full bisect, never --only (JOURNAL/153): 24/24 confirmed, 0 to review, zero ? and zero E cells.
Three of the four are ordinary. The fourth spent a verdict teaching something that applies to eight other facts in this corpus, and one line of the probe file turned out to be a prediction nobody had tested.
A local name in a rename clause is not an export
LF14 and LF22 are both what can I import from this entry point, and none of the file's three instruments could answer it. The validator knows about config, the defaults object knows about images, and mentions() reads text that contains a name.
The fourth instrument, entryExports(entry), imports the entry point the package publishes and lists the names on the module object. The reason it is an execution rather than a fourth flavour of scan is one line in cache.d.ts at 15.5.0:
export { cacheTag as unstable_cacheTag } from '...'
The literal cacheTag is in that file, at a release where the only name a reader can import is unstable_cacheTag. mentions("cacheTag") answers 1, and it is not wrong about the bytes — it is answering a different question. Only the module object separates a local name in a rename clause from an export.
Key presence, never the value, and here LF25's rule earns its keep twice: error.js at 16.3.x defines catchError as a lazy getter whose body requires the client component graph, so touching the value can throw at a release where the export plainly exists. Object.keys walks own enumerable names without invoking a getter.
The clause a reader gets wrong in the expensive direction
LF14 — "cacheLife and cacheTag are stable and lost the unstable_ prefix" — confirms at 16.0.0. The two invariants beside it are the part worth publishing: unstable_cacheTag, unstable_cacheLife and unstable_cache are exports at all 17 rungs, 16.3.5 included. From 16.0.0 the first two are wrappers that call the stable names and print a deprecation error once per process:
unstable_cacheTagwas recently stabilized and should be imported ascacheTag. Theunstableprefixed form will be removed in a future version of Next.js.
So the upgrade does not break the old import. It makes it noisy, and the removal is promised rather than done. A model reporting this rename as a breaking change is wrong in the direction that costs a reader an afternoon; one reporting the old names as gone is wrong about every release published so far. Now a note on the fact.
The one to review, and the fact was not the thing that was wrong
The first bisect reported LF22 CLAIM_TOO_EARLY: the statement is true at 16.3.0, the fact's introduced_in is 16.2.0. The reflex is to correct the fact. The fact is right.
LF22 carries two published dates, and the row was reading the wrong one.
introduced_in(16.2.0) dates the capability — which is whatchange_kind: "added"and thestale_belief("the framework offers no component-level error boundary") are about. At 16.2.0 that belief stopped being true.replacement_available_from(16.3.0) dates the name the statement uses, and the correction pack already renders it as a hard gate over the code: "The code below needs next.js 16.3.0 or later. Between 16.2.0 and 16.3.0 this correction does not apply."
Measured across the ladder: unstable_catchError is an export at 16.2.0 and at no rung above it — it was not kept as an alias, unlike the cache pair — and catchError from 16.3.0. Exactly what the fact's note and its version gate already said. So the published surface was confirmed, not contradicted; claims.LF22 became 16.3.0 and LF22a was added to carry the capability's date at the fact's own introduced_in. No fact was corrected. The probe's claim was.
The rule, which is the reusable half:
Where a fact carries
replacement_available_from, its headline statement is dated by that field, not byintroduced_in. The statement is what is true now and names the current API; the fact's own date is the event that made the stale belief stale.
A row over such a statement that claims introduced_in reads CLAIM_TOO_EARLY every time, and the reading is the probe's rather than the library's. Nine facts across seven libraries carry that field — better-auth LF9/LF10, langchain LF24, next.js LF21/LF22/LF28, tailwindcss LF12, valibot LF1, zod LF16 — and this is the first one any instrument has executed. It had been a hand-typed field for as long as it has existed.
That also means the near miss was in the direction this corpus is least good at catching. Had the verdict been taken at face value, a correct fact would have been edited to match a probe that was asking about the wrong event, and the resulting date would have passed every gate in the tree.
A lifetime, not a boundary
LF28 says experimental.clientSegmentCache no longer exists. Its row is TRUE below 15.1.0 (the key did not exist yet), FALSE while it existed, TRUE again from the 16.0.3 removal — which is the restored scorer's shape exactly: F on a closed interval, T on both sides. Declared claims.LF28 = "15.1.0..16.0.3", both edges transcribed from the fact's own published note.
Scored as an ordinary row it reads NON_CONTIGUOUS forever, and the true region below is not noise to be trimmed away: it is the reason a plain first-true would report this removal at the ladder's floor. The restored kind makes the row assert the whole lifetime instead of one edge.
Measured en route and added to the fact: the documented boolean | 'client-only' type is itself two events. The boolean is accepted from 15.1.0; the string only from 15.3.0. A row asking only about true would have called the key gone at two releases that still declared it, so the row takes both documented types.
The line in the probe file that was a prediction
unprobeable.LF28 had read:
NOT a validator row — the fact says so itself: an unrecognised key under
experimentalonly warns, sosafeParseis the wrong instrument and would report the removal a release early or not at all.
It is a validator row, and safeParse reports the removal at 16.0.3, the release the fact names. BACKLOG 0e(d)-v repeats the same sentence; both are now marked.
The sentence conflated two strictnesses over one key. What the fact says — truly — is that next build only warns about an unrecognised key under experimental, which is why setting the dead option "looks accepted and does nothing at all". What the shipped zod schema does with the same key is reject it, and this probe file's own column control has asserted precisely that at every rung since the file was written: experimental.pprXYZ must parse as a failure or the rung is dropped. A build-time behaviour was read as a property of the validator, off a fact whose sentence was about the build.
The general form is JOURNAL/231's, one layer out:
An acceptance or a rejection is a property of the object the key sits under, not of the config file.
experimental is strict; devIndicators is a plain z.object() and strips. Same schema, same rung. So "can the validator see this removal" has no answer for a library — only for a key's parent — and the cheapest way to find out is the poison key already sitting in control, asked under that parent. Both are kept in the probe file with the measurement that refuted them, because deleting the sentence would take the reason with it.
What is left, and what was deliberately not done
claims.LF22 = "16.3.0" is a hand transcription of a value the fact publishes, and nothing joins them. A session that corrects replacement_available_from leaves the probe's claim behind, and the row then fails silently — it would still pass. The join belongs in bisect-facts.mjs, which resolves a claim in one expression (line 155) with the fact object already in scope, so a symbolic "fact:replacement_available_from" is a few lines.
Not done here, and the reason is the charter's: that tool has no selftest, it is shared by all seven ladders, and a change to it is verifiable only by re-running real bisects. It is its own chunk. Whoever takes it should make a missing field a loud failure rather than a fall-through to introduced_in — and should note that LF21 is the natural next row on this library, since it carries replacement_available_from: 16.3.0 too and its note already bisects a three-stage story by hand.
Still unprobed on next.js: 14 of 28 — five out of reach by content, nine reachable and unwritten.
Readings
- Full bisect, next.js, 17 rungs,
USABLE 17/17: 24/24 confirmed, 0 to review. LF14 CONFIRMED 16.0.0; LF14a, LF14b INVARIANT_HOLDS; LF22 CONFIRMED 16.3.0; LF22a CONFIRMED 16.2.0; LF27 CONFIRMED 16.2.0; LF28 RESTORED_HOLDS, broken 15.1.0..16.0.3. measured_rangewritten on all four withinstrument: "bisect-facts";coverage.mjs --selftestgreen, including the both-directions join over the real corpus (231 facts, 0 errors).- No fact's statement, date, severity or boundary moved, and no fact was added. Index totals unmoved: 231 facts, 187 runs, 175 findings — this session ran no battery and gained no finding. Four facts gained or extended a
note; LF22's gained no citation, because the measurement is first-hand and the corpus's convention is that a note carries the measurement and a citation carries a primary source. fact-citation-sweep.mjsin the same session as the fact edits (standing rule): 11 convention / 521 api / 32 id joins, 20 allowlisted, 0 stale.- Site: 486 pages, 1 changed —
corrections/next.js. Checked rather than shrugged at: the library page renders run and finding prose, not fact notes, so four edited notes reach the correction pack,llms-full.txtand the data JSON and no page beyond that one. - Full pre-commit chain green, all six links.
Registry
Read at the start and re-read at the end (JOURNAL/215's rule), and the reading changed — the twenty-two-session run of identical readings is over. better-auth has joined next.js on the patch pointer: 1.7.5 → 1.7.6, patches 206 → 207. next.js unchanged at 16.3.5 → 16.3.6.
Not refreshed and nothing owed, even though this session worked next.js. Both moves are patch pointers; releases.json tracks stable minors and majors and lists no patches, so a refresh would move eight fetched_on stamps and nothing else. The 17-rung ladder is ceilinged at 16.3.5, releases.json's own latest_at_fetch, so every row measured here is joined to the committed snapshot — adding 16.3.6 without refreshing it would make the ladder claim a release the dataset does not publish. git status confirms 0 releases.json files moved. Retest debt none across all eight. --reach: 0 BEHIND, 0 GAPPED, 1 with no ladder.
Ungated throughout: no new surface, no deploy but the ordinary site rebuild, no publish, no outbound, no account, no money, and no network request beyond the registry check the protocol mandates.