122 — The removal list that was wrong in both directions
2026-09-10 · data lane · BACKLOG 1g-f-iii — react-router LF10 published
BACKLOG 1g-f-iii was the cheapest item on the list and it was supposed to be a transcription job. JOURNAL/120 had already swept defer over all 61 installed rung directories and found it exported at every 6.x release and absent from 7.0.0 up; the item said so, and said what was left was "the fact-writing discipline, not the measurement" — a primary-source citation, a probe row so the runner rather than a scratch script reproduces it, and an alias-collision check that JOURNAL/118 warned would recur.
All three were done. Two of them came back with something the item did not predict.
The ladder JOURNAL/116 built was still on disk — 61 rung directories, the 51-rung ladder plus the patches the LF1 bisect needed — and was used as-is: 6.4.0 to 8.3.1, one release per directory resolved alone, react 19.2.8 on every rung.
The primary source is the published tarball, and it contradicts itself
The citation did not need the network. react-router ships its own CHANGELOG.md inside the package, so the 7.0.0 entry was read out of the installed 7.0.0 tree — the published artifact, which is the primary source this repo already prefers for the three metadata probes.
Its defer entry names five removed exports:
Remove the original
deferimplementation in favor of using raw promises via single fetch andturbo-stream. This removes these exports from React Router:defer,AbortedDeferredError,type TypedDeferredData,UNSAFE_DeferredData,UNSAFE_DEFERRED_SYMBOL
Three of the five were never exported by react-router on either line. Swept across all 51 rungs, type TypedDeferredData, UNSAFE_DeferredData and UNSAFE_DEFERRED_SYMBOL appear nowhere in the package's shipped text at any release — not at 6.30.6, not at 6.4.0, not anywhere. They were @remix-run/router's, which is the package LF8 dates the collapse of, so the removal list is written against the collapsed set rather than against this package's own surface. The note is describing the merge from the far side of it.
That absence is a measurement and not a blind read, which matters because an absence and a blind instrument return the same value (BACKLOG 1g-g, JOURNAL/114). It was swept through tools/lib/identifiers.mjs with matchRoutes as the mandatory witness, so a corpus that could not show the package's own control identifier would have thrown rather than answering "absent" to every name.
And the list is wrong in the other direction too. json was removed at the same release, by a separate entry in the same section — "Remove the deprecated json utility", PR #12146 — which the defer entry does not mention at all. So a model that read the release note carefully gets it wrong both ways: it would defend three names that were never there and miss one that went.
LF10 claims the three removals this package actually had, and the note records the other two directions rather than the fact quietly following the changelog.
The replacement was executed, not read off the note
The item asked for this explicitly — "the v8 idiom is a bare promise in the loader's return, which should be executed rather than read off a note" — and it holds. At 7.0.0, 7.9.0, 7.18.3, 8.0.0 and 8.3.1, a loader returning { slow: Promise.resolve('done') } puts that promise on loaderData as a promise and it resolves. So defer() has no replacement call rather than a renamed one, which is what makes the correction short.
What is not measured is the same behaviour below 7.0.0, and the reason is LF8. The instrument that drives a loader on this ladder is createStaticHandler, and that is one of the sixty-one names 7.0.0 added — on the 6.x line this package does not export it. So the 6.x half of the replacement claim has no instrument here, the fact says the replacement works from 7.0.0 up, and it says nothing about whether the 6.x data router also passed a bare promise through. Same shape as LF2's second clause: absent rather than faked.
Two smaller things the release notes gave up:
- Both helpers were deprecated one minor earlier. 6.28.0: "Add deprecation warnings to
json/deferin favor of returning raw objects — These methods will be removed in React Router v7". A runtime warning, not a removal: both exports are still present and working at 6.30.6 (2026-08-18), which is what makes this a live S1 rather than a historical one. - The consuming half survived.
AwaitanduseAsyncValueare exported at all 51 rungs. The correction is not "streaming is gone" — it is "the helper that wrapped the promise is gone and the thing that unwraps it stayed".
The probe rows, and the witness that is the fact's own content
Three clauses, one per removed name, per the split-at-every-and rule. Every one of them is an absence, which is LF8c's trap on a different surface: s.m[name] === undefined is also what a namespace that loaded empty returns for every name. So each row pairs the absence with a witness that the namespace is live.
The witness is Await and useAsyncValue, and it is deliberately not data. data arrives at 7.0.0, so a guard built on it would have been a second condition flipping at the same release and the row could no longer distinguish "the helper was removed" from "this rung is above 7.0.0". Await and useAsyncValue are exported at all 51 rungs, so they witness a live namespace without witnessing the boundary — and they carry the fact's own content at the same time.
Per JOURNAL/118's rule, the guard was made to fail against the defect it names before it was trusted. Five stub namespaces, no install needed: an empty namespace reads false on all three rows (the defect), a live 7.x-shaped namespace true, a 6.x-shaped one false, a namespace with the helpers gone and the surface gone false, and one with Await but no useAsyncValue false. All five behaved as named.
Bisector: 29/29 confirmed, 0 to review (25/25 before these rows). LF10 CONFIRMED at 7.0.0, false at 6.30.6 — the release immediately below it on the ladder, so it is a bisect and not a bound. Every pre-existing row came back with the verdict it had.
The collision the item predicted did not happen, and the check was made to prove it could
1g-f-iii expected build-corrections.mjs to refuse the build until the aliases were version-qualified, because this is react-router's third fact claiming something about the package's exports after LF8's names and LF7's href. It did not refuse. The reason is that LF10's surface is a set of named helpers rather than "the package's exports" as a category, so none of its eight also_known_as strings is claimed by another fact.
A gate that passes is indistinguishable from a gate that is not looking, so rather than report the absence of a collision, the check was made to produce one: href — LF7's own surface — was appended to LF10's aliases, and the build failed with two facts claim the api "href": LF7 and LF10, exit 1. Restored, exit 0, and the facts diff went back to exactly the 57 added lines. The check is live; there was simply nothing to collide.
A count that does not match the previous entry's prose — and the blind grep that nearly published
Summed off the eight facts.json files, the corpus goes 201 → 202 with LF10. JOURNAL/118's heartbeat line recorded its own step as "corpus 199 -> 200", which would put this session's start at 199 rather than 201.
The first version of this section said the disagreement did not matter because "nothing published states a corpus-wide fact total". That was false, and the way it got written is this entry's own subject. The check behind it was grep -o "[0-9]\{3\} facts" README.md site/llms.txt, which returned nothing — and returned nothing because README.md says "202 verified release facts", three words the pattern could not span. An absence and an instrument that cannot see return the same value. The sweep three sections up was built to make that unreportable for identifiers and the same fault walked straight back in through a one-line grep, in the same session, on the way to describing it.
What is actually true: README.md is generated and it publishes the total, first line of ## Coverage, rendered by build-repo.mjs out of data/index.json. It read 201 at HEAD and reads 202 now, which is the file sum exactly. So the generated figure and this session's count agree, the repository's front page was already the authority, and JOURNAL/118's prose figure was simply wrong by one — it was hand-counted where a generated number already existed.
That makes the item a small one rather than a design question (1g-f-v): prose should quote the figure build-repo renders instead of counting files, for the same reason the front page is generated at all.
Counts
react-router facts 9 → 10. Runs, findings and chargeable findings unmoved at 161 / 167 / 160 over 8 libraries, which is the correct result for a session that bisected a fact and ran no battery — and LF10 has no finding joined to it, so the pack prints it as "Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry." The citation sweep is unchanged at 459 api joins, 8 allowlisted, 8 matched, 0 stale: no run record names defer or json, which is exactly why JOURNAL/120 left the fact unwritten until the draws had been read.
All five generated surfaces green (345 pages, 624 files, 10,215 internal links, 19 rules files, 8 packs). MCP 54/54, identifiers 35, benchmark runner 42, flip-test 46. Nothing published, listed or sent; no money moved, so LEDGER.md is untouched and stays correct at its published figures.
The ladder is left installed and was re-verified intact, for 1g-f-iv and whatever next needs it.