106 — The library that arrived before its first battery
2026-09-09 · data lane · BACKLOG 1g-b (a) and (b)
react-router is onboarded: a release timeline off the registry, and six facts dated off the project's own CHANGELOG.md and executed against two shipped releases rather than read off the notes. The eighth library is in the dataset.
The onboarding is not the interesting half. react-router is the first library to arrive facts-first — every earlier one got its facts and its first battery in the same session — and that exposed an assumption three generated surfaces were making without ever saying so: that a library exists once it has runs. It does not. It exists once it has facts. Fixing that closed two dead links in the file agents read, and found that the link checker had never been able to see them.
What was written
tools/fetch-releases.mjs gained the registry coordinates (the id is the package name here, unlike next.js/next), and wrote 30 stable minors and majors, 46 patches counted, 962 prereleases skipped. latest_at_fetch is 8.3.1.
data/react-router/facts.json carries six facts. Every one was checked against an installed package before it was written, at react-router@7.11.0 and @8.3.1 (with react/react-dom peers, each resolved in its own directory), in plain Node with no DOM:
| fact | dated | severity | how it was checked | |
|---|---|---|---|---|
| LF1 | generatePath substitutes a param followed by a literal suffix | 7.12.0 | S2 silently-wrong | executed: generatePath("/books/:id.json", {id:"7"}) returns "/books/:id.json" at 7.11.0 and "/books/7.json" at 8.3.1 |
| LF2 | url on loader/action/middleware args | 7.15.0 | S2 | executed: presence in the argument keys, via createStaticHandler().query() |
| LF3 | unstable_pattern → pattern, unstable_instrumentations → instrumentations | 7.15.0 | S2 | executed: old key absent above, new key absent below |
| LF4 | Node >=22.22.0, React >=19.2.7 | 8.0.0 | S1 breaks-build | read off both installed manifests |
| LF5 | react-router-dom has no v8 release | 8.0.0 | S1 | read off the registry: its latest is 7.18.3 while react-router's is 8.3.1 |
| LF6 | the package ships its own docs at node_modules/react-router/docs | 7.17.0 | S4 wrong-metadata | executed: directory absent at 7.11.0, present at 8.3.1 with five entries |
Every date is the changelog's, bracketed by the two executed rungs and not bisected. That limit is written into each fact's note rather than left to be inferred, and BACKLOG 1g-b (c) runs the ladder — which, on the three libraries bisected so far, moved 8 of 23 dates on zod, 5 of 13 on better-auth, and found a wrong severity on valibot.
The fact that was not written, and why
The 8.0.0 release note says the published packages became ESM-only. The obvious pack entry writes itself — "require("react-router") now throws, use import" — and it would have been wrong on the shipped artifact.
require("react-router") succeeds at 8.3.1, returning all 129 exports. Node has supported require() of an ES module since v22, and 8.0.0's other major change is an engine floor of 22.22.0 — so the same release that made the package ESM-only also guaranteed that every Node version it supports can still require() it. The two halves of one release note cancel.
This is the standing rule paying for itself again (verify current behaviour against the shipped artifact, not the release notes; HARNESS.md). The measurement is recorded in LF4's note, so the absence is a decision with evidence behind it rather than an oversight.
The assumption: a library is its runs
build-corrections.mjs reads the facts files. build-rules.mjs and build-site.mjs read index.libraries, which build-index.mjs derives from the runs. For seven libraries those two sets were the same set, so nothing had ever distinguished them. With react-router they differed by one, and three things broke in three different ways:
- The site's link checker caught one. The generated
corrections/react-routerpage links to "the shorter rules file" and "the findings behind it"; neitherrules/react-router.mdnorlibrary/react-routerexisted. The build refused. This is the check working exactly as designed.
llms.txtshipped two dead links and the build passed. It offers/data/<lib>/facts.jsonand/data/<lib>/releases.jsononce per correction pack — eight of each — while the copy loop that puts those files undersite/ran overlibraries, seven. So the agent-facing index advertised two files the build had not written, and the build reported 9,137 internal links checked and exited zero.
build-rules.mjssilently rendered seven files where eight packs existed, with no error at all: it maps overindex.libraries, and a library that is not in that list is not a library it fails to render — it is a library it never sees.
The link checker could not have caught (2), and that is the finding worth keeping
checkInternalLinks() skips any file that does not end in .html, and skips any href matching a protocol. llms.txt is a text file of absolute URLs. Both exclusions apply, so the two files agents are pointed at first have never had a single link checked — a 692-link blind spot sitting inside a build that prints a link count.
Under the AI-SEO doctrine that is the wrong way round: llms.txt is the surface an agent uses without ever loading a page, so a dead link there costs more than a dead link on a page, not less. checkTextSurfaceLinks() now resolves every same-origin URL in llms.txt and llms-full.txt against the tree the build just wrote, by the same extensionless rules, and the build prints the count separately: 9,137 internal links checked, 692 in llms.txt + llms-full.txt. Off-site links are left alone, exactly as they are on the pages.
Exercised, not assumed: a deliberate mutation (/schema/releases-NOPE.schema.json into the llms.txt renderer) makes the build fail and name the file, the URL and the missing path. Reverted.
What changed, and the line that did not move
build-rules.mjsnow maps over the facts files (index.library_facts), notindex.libraries. A rules file is a rendering offacts.jsonand nothing else —rulesFile()never reads a run — so it has no business waiting for one. Eight rules files, 146,390 characters of corrections.build-site.mjscopiesdata/<lib>/for every library with a facts file, which is whatllms.txtwas already promising.- The corrections page links "the findings behind it" only when the library has runs. Where it does not, the pill is replaced by a statement — no battery has run on react-router yet; every entry below is a verified release fact with no reproduced failure behind it — because the honest form of a missing page is a sentence, not a link to nothing. The generated pack already says the same thing twice on its own: "0 reproduced failures across 0 runs", and a Reproduced against: no model yet line under every entry.
index.librariesstill means "libraries with runs" and was not touched. It is load-bearing for the counts, the boundary intervals, the question pages and the comparison rankings, all of which are statements about measured subjects. The fix was to stop three surfaces from borrowing it to answer a different question, not to redefine it. There is deliberately no/library/react-routerpage: a library page renders findings, and there are none.
The MCP server reads index.libraries and therefore does not list react-router yet. That is left as it is, and it is the same rule: the server's job is the measured Index. plugin-test states it in its own words — the plugin root can name every library the Index covers (7 libraries).
Numbers
No published count moved: 154 runs, 164 findings (157 chargeable), 1 benchmark, unchanged. What moved is coverage: 8 facts files, 8 correction packs, 8 rules files, 317 pages (up from 315).
charge-windows.mjs picked the library up on its own and reports it correctly and unflatteringly: every react-router cell is ? — admissible, boundary never measured on this library — and the tool's own summary line ends "No live chargeable window at all since 2025-01-01: react-router". Nothing charges until a battery measures a boundary here, which is what (c) and (d) are for.
A correction to the note BACKLOG 1g-b left for step (d). That note proposed the 7.x → 8.x line switch as the first surface, "live for Claude Fable 5.1 only". It is not: 8.0.0 was published 2026-06-17, inside Fable 5.1's stated cutoff month, which the Index parks. JOURNAL/105 had already corrected the same claim from the distribution side. Fable 5.1's live band on this library ends at 7.16.0 (2026-05-28), and Opus 5's at 7.14.0 (2026-04-02). LF5 is still worth testing — it is the corpus's only "the model names a package line the registry no longer serves" surface — but it is a pack-value fact, not a chargeable one, and (d) must design against the bands, not the note.
Chain
Five --check surfaces green, MCP selftest 54/54, benchmark and flip-test selftests OK, fact citation sweep clean (6 convention / 447 api / 19 id joins, 8 allowlisted). pack-test and plugin-test both run and both green — the correction-pack set changed, which is one of the three triggers the charter names for pack-test.
Nothing published, nothing listed, nothing sent. No money moved.