react-router correction pack

Paste into CLAUDE.md, .cursorrules, or any agent rules file on a project that uses react-router.

react-router correction pack · for projects on react-router@^7.12 || ^8

What we actually measured

5 Claude models were asked for idiomatic react-router code with no tools, purely from training knowledge. 7 reproduced failures across 18 runs, each verified against the release that broke the belief.

ModelStated cutoffreact-router version attribution stopsLag inside the window
Claude Haiku 4.52025-026.4.0 · 2022-09-13~29 months
Claude Sonnet 52026-017.3.0 · 2025-03-06~10 months
Claude Fable 5.12026-067.9.0 · 2025-09-12~9 months
Claude Opus 5.52026-067.9.0 · 2025-09-12~9 months
Claude Opus 52026-057.9.0 · 2025-09-12~8 months

A recent cutoff is not a defence. Every model here loses track of this library's release history well before the date it states as its own cutoff, and the stopping points cluster far tighter than the cutoffs do.

Read that column precisely. It is the newest react-router release whose contents the model can correctly attribute to that release — not the newest react-router feature it knows. Past that point a model will often write working code with a newer API while naming the wrong release for it, and that guess runs early — it names a release older than the one that shipped the feature. So the question this pack answers is not "does the model know this API" but "can it be trusted about which version the API arrived in" — which is the question that matters when you are pinned to a version.

How to read an entry

Every entry ends with a Reproduced against line. Where it names models, we have the generated code that got it wrong, dated, with the model's own words in the run write-up. Where it says no model yet, the correction is verified from the release notes but nothing has been probed for it — it is a fix, not a measurement, and the pack says so rather than blurring the two.

The section an entry sits in is the worst case if you act on the stale belief. The severity in brackets after a model's name is what that particular model's output actually did, which can be milder — a model can hold the wrong belief and still, on the day, write code that runs.

Two dates, and they answer different questions. The entries below were last checked against their primary sources on 2026-09-22 — that date is the floor, and it is the only verification claim this file makes. Separately, 1 of the 14 entries has had its own text change since that sweep, the newest on 2026-09-23. A text change is a change to the record — a re-scored severity, a corrected release date, a clarified note — and it is not a re-check against the source. Do not read the later date as the fresher of two checks; per-entry dates are in substance_changed_on in the JSON.

The corrections

Breaks the build, or throws at runtime

Act on the stale belief here and the code does not run. Fix these first.

dataStrategy (match.shouldCallHandler / match.shouldRevalidateArgs)

Renamed in react-router 7.10.0 (2025-12-02) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

In a custom dataStrategy, the per-match revalidation API lost its unstable_ prefix in react-router 7.10.0: match.unstable_shouldCallHandler() became match.shouldCallHandler() and match.unstable_shouldRevalidateArgs became match.shouldRevalidateArgs. The prefixed spellings exist on 7.6.0 through 7.9.5 and are gone from 7.10.0 onward — not aliased — so calling match.unstable_shouldCallHandler() on 7.10.0 or later throws TypeError: match.unstable_shouldCallHandler is not a function. match.shouldLoad was not removed: it still exists on every 7.x and 8.x release and is marked deprecated in favour of the two stabilised APIs, so code written against shouldLoad keeps working and code written against the unstable_ spellings does not.

The stale belief: That the per-match dataStrategy revalidation helpers are still the unstable_-prefixed ones — match.unstable_shouldCallHandler() and match.unstable_shouldRevalidateArgs — on current react-router, or that match.shouldLoad was removed when they were stabilised.

// Stale
import { createBrowserRouter } from 'react-router'

createBrowserRouter(routes, {
  async dataStrategy({ matches }) {
    // 7.10.0 and above: TypeError, the property is not there
    const toLoad = matches.filter((m) => m.unstable_shouldCallHandler())
    // ...
  },
})

// Current
import { createBrowserRouter } from 'react-router'

createBrowserRouter(routes, {
  async dataStrategy({ matches }) {
    // 7.10.0 and above: the stabilised spellings
    const toLoad = matches.filter((m) => m.shouldCallHandler())
    // m.shouldRevalidateArgs is the argument the route's shouldRevalidate would receive;
    // m.shouldCallHandler(defaultShouldRevalidate) overrides the internal default.
    // m.shouldLoad still exists and still works — it is deprecated, not removed.
  },
})

BISECTED 2026-09-22 over the 52-rung ladder, 6.4.0 to 8.4.0, one release per directory resolved alone (JOURNAL/189). Read from the shipped declarations and bundles with tools/lib/identifiers.mjs, whose identifier-wise matching is what makes this fact measurable at all: shouldCallHandler as a SUBSTRING occurs 48 times at 7.9.5, and every one of them is inside unstable_shouldCallHandler. A substring grep therefore reports the stabilised name as already present one release below its own boundary, which is the reading this session made first and corrected against the module (the ServerInstrumentation / unstable_ServerInstrumentation rule the module was written for, applied to a different pair). BOTH DIRECTIONS ARE PROBED and the old-name row is stated as a WINDOW rather than an absence, for LF3b's reason: unstable_shouldCallHandler is also absent at 7.5.0 and below, where it had not yet been introduced, and an absence true for two different reasons is not contiguous. Counts across the ladder: unstable_shouldCallHandler 0 at 7.5.0 and below, 32-48 across 7.6.0-7.9.5, 0 from 7.10.0; shouldCallHandler 0 through 7.9.5, 64-68 from 7.10.0 through 7.18.3, 36 across the 8.x line.

Reproduced against: Claude Opus 5.5 (S1) — react-router/v6-a, 2026-09-28.

Source: react-router CHANGELOG.md — 7.10.0, Minor Changes (read from the CHANGELOG.md shipped inside react-router@7.11.0) · 2025-12-02 · react-router CHANGELOG.md — 7.10.0, the deprecation of the older spelling · 2025-12-02 · react-router PR #14592, named by the 7.10.0 changelog entry · Executed against the published packages (tools/audit/bisect-facts.mjs, 2026-09-22) · 2026-09-22

middleware / createContext / RouterContextProvider / getContext

Renamed in react-router 7.9.0 (2025-09-12) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

React Router 7.9.0 (2025-09-12) stabilised the middleware and context APIs: the unstable_ prefix was removed from RouterContextProvider, createContext, the createBrowserRouter getContext option and the <HydratedRouter> getContext prop, and from the route config's middleware key and the MiddlewareFunction / RouterContext types. The prefixed spellings exist on 7.3.0 through 7.8.2 and are gone from 7.9.0 onward — not aliased — so import { unstable_createContext } from 'react-router' does not resolve on 7.9.0 or later, and a route module exporting unstable_middleware declares a key the router no longer reads, so the middleware never runs. Middleware itself was not removed or renamed away: it is the same API under the unprefixed names, and it remains present through 8.4.0.

The stale belief: That react-router's middleware and context APIs are still behind the unstable_ prefix — unstable_middleware, unstable_createContext, unstable_RouterContextProvider, unstable_getContext, unstable_MiddlewareFunction, unstable_RouterContext — on current react-router, or that middleware is still experimental and not yet stabilised. A model whose knowledge stops between 7.3.0 and 7.8.2 saw only the prefixed form, because that is the only form that ever existed before 7.9.0.

// Stale
// routes.ts / route module — the pre-7.9.0 spellings
import { unstable_createContext } from 'react-router'
import type { unstable_MiddlewareFunction } from 'react-router'

// 7.9.0 and above: neither name is exported. The named import fails to resolve;
// TypeScript reports TS2305 and a bundler reports a missing export.
export const userContext = unstable_createContext<User | null>(null)

export const unstable_middleware: unstable_MiddlewareFunction[] = [
  async ({ context }, next) => {
    context.set(userContext, await getUser())
    return next()
  },
]

// Current
// routes.ts / route module — 7.9.0 and above
import { createContext } from 'react-router'
import type { MiddlewareFunction } from 'react-router'

export const userContext = createContext<User | null>(null)

// the route config key lost its prefix too: `middleware`, not `unstable_middleware`.
export const middleware: MiddlewareFunction[] = [
  async ({ context }, next) => {
    context.set(userContext, await getUser())
    return next()
  },
]

// and the two entry points that take a starting context:
//   createBrowserRouter(routes, { getContext })   // was unstable_getContext
//   <HydratedRouter getContext={...} />           // was unstable_getContext

BISECTED 2026-09-23 over the same 52-rung ladder LF11 and LF12 were dated on, 6.4.0 to 8.4.0, one release per directory resolved alone. Read from the shipped code with tools/lib/identifiers.mjs.

MEASURED COUNTS across the ladder. unstable_middleware 0 at 7.2.0 and below, 142-244 across 7.3.0-7.8.2, 0 from 7.9.0 through 8.4.0; unstable_createContext 0 / 28-94 / 0; unstable_RouterContextProvider 0 / 102-268 / 0; unstable_getContext 0 / 100-172 / 0; unstable_MiddlewareFunction 0 / 24-42 / 0. In the other direction RouterContextProvider is 0 through 7.8.2 and 162-282 from 7.9.0; getContext 0 then 106-172; MiddlewareFunction 0 then 24-42. Every old-name row is stated as a WINDOW (7.3.0..7.9.0) rather than as an absence, for LF3b's reason: the prefixed names are also absent at 7.2.0 and below, where they had not yet been introduced, and an absence true for two different reasons is not contiguous.

THREE IDENTIFIERS THE CHANGELOG NAMES CANNOT CARRY A ROW, and refusing them is the measurement worth keeping. (1) createContext is in the shipped bytes of EVERY rung — 40-45 occurrences at 6.4.0, eight releases before react-router had any such export — because it is React's own createContext, which this package calls. The declarations reader is identifier-wise, not substring-wise, and it is still polluted, because the collision is between two real identifiers with the same name. So createContext's arrival is dated off the package's EXPORT LIST instead (LF13c): 7.8.2's index.d.ts exports aq as unstable_createContext and 7.9.0's exports aq as createContext — same local binding, renamed at the boundary. (2) bare middleware is 50 occurrences at 7.3.0 and never zero afterwards, so the route config key's arrival is unreadable in the same way; its old spelling's window (LF13i) is what dates it here. (3) RouterContext is NON-CONTIGUOUS as a bare identifier — 4 at 7.3.0-7.6.0, 8 at 7.7.0, 0 at 7.8.0 through 7.8.2, then 76 from 7.9.0 — so a rising row on it would have been refused by the runner, correctly. This is LF12's onError trap met three more times in one fact: date the identifier that is unambiguous, and write down which ones were refused and why.

SEVERITY IS THE WORST OF TWO DIFFERENT FAILURE MODES, and the statement says both. The import half is breaks-build: a named import of a removed export is TS2305 and a bundler error. The route-config half is silently-wrong: a route module exporting unstable_middleware compiles, loads and runs, and the middleware simply never executes, which is the more dangerous of the two where the middleware was doing auth.

WHERE THIS FACT CAME FROM. Claimed unprompted by react-router/v3-d (Fable 5.1) during battery v3 on 2026-09-22 (JOURNAL/189) and deliberately NOT filed by that session, because it is outside v3's two pre-registered probe surfaces and filing a fact to capture a miss found mid-session inverts JOURNAL/046's order. This session ran no battery and gains no finding from it. NOTHING IS CHARGED AGAINST ANY SUBJECT HERE: react-router/v3-c (Opus 5) got this same event wrong in that battery, leaving the stabilisation in v8, but the correction is a fact and the charge would be a finding, and the run that produced it was not probing this surface. A battery that elicits middleware without naming it is BACKLOG 0i's remaining half.

Reproduced against: Claude Sonnet 5 (S1) — react-router/v4-a, 2026-09-23.

Source: react-router CHANGELOG.md — 7.9.0, Minor Changes (read from the CHANGELOG.md shipped inside react-router@7.11.0) · 2025-09-12 · react-router CHANGELOG.md — 7.9.0, the four APIs the entry lists by name · 2025-09-12 · react-router PR #14215, named by the 7.9.0 changelog entry · Executed against the published packages (tools/audit/bisect-facts.mjs, 2026-09-23) · 2026-09-23

react-router (package exports)

Added in react-router 7.0.0 (2024-11-22) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

React Router 7.0.0 (2024-11-22) collapsed @remix-run/router, react-router-dom, @remix-run/server-runtime and @remix-run/testing into the single react-router package. Import the DOM bindings — BrowserRouter, HashRouter, Link, NavLink, Form, useSubmit, useSearchParams, createBrowserRouter — from react-router itself, not from react-router-dom; the framework and server surface (Meta, Links, Scripts, ServerRouter, createStaticHandler, createRequestHandler, createCookieSessionStorage) is in the same package. On the 6.x line none of those names is exported by react-router, which published 59 names against 7.0.0's 111. react-router-dom still exists on the 7.x line as a re-export, but it has no 8.x release at all (LF5), so on react-router 8 an import from react-router-dom has no package to resolve to.

The stale belief: That React Router is on the 6.x line, that react-router is the routing core and react-router-dom the package an application installs, and that DOM components and hooks therefore have to be imported from react-router-dom.

// Stale
// The v6 arrangement: two packages, DOM bindings in the -dom one.
import { BrowserRouter, Link, useSearchParams } from 'react-router-dom'
import { matchPath } from 'react-router'

// On react-router 8 there is no `react-router-dom` release to install:
// npm error No matching version found for react-router-dom@^8

// Current
// 7.0.0 and above: one package.
import {
  BrowserRouter, Link, useSearchParams, matchPath,
  createBrowserRouter, Form, useSubmit,
} from 'react-router'

// The framework/server surface is in the same package:
import { createStaticHandler, createCookieSessionStorage } from 'react-router'

BISECTED 2026-09-10 over a 51-rung ladder, 6.4.0 to 8.3.1, one release per directory resolved alone, react and react-dom pinned to 19.2.8 on every rung (JOURNAL/116). Three clauses, all measured, all false on the whole 6.x line and true from 7.0.0 up: the eight DOM bindings are exported from react-router itself, the seven framework/server names are, and @remix-run/router is gone from the package's own dependency list — the last asserted in BOTH directions, because it is present at every 6.x rung and an absence stated of a name that was never there is true for reasons that have nothing to do with the change. 6.28.0 is the release immediately below 7.0.0 and the row is false there. THE LADDER'S 6.x RUNGS ARE NOT ALL OLDER THAN 7.0.0: the 6.x line continued in parallel — 7.0.0 is 2024-11-22, 6.29.0 is 2025-01-30 — so this row is a proposition about the LINE, not about time, and the two late 6.x rungs are on it deliberately. WHAT THIS LADDER CANNOT SEE is the other half of the changelog's sentence, that react-router-dom became a re-export: the runner installs one release of one package per rung, which is LF5's limit exactly. The react-router-dom half is verified against the registry instead — its latest dist-tag is 7.18.3 and it has no 8.x release. NOW SPLIT OFF AS LF9 (2026-09-10, JOURNAL/118): 7.0.0 also raised the package's own floors — engines.node from >=14 to >=20.0.0, the react peer from >=16.8 to >=18, and a react-dom peer declared for the first time and marked optional. That is a requirement change and a different statement, so it is a separate fact rather than a clause here (the split-at-every-and rule). This paragraph previously said it was measured and left queued; it is queued no longer. Both facts are dated 7.0.0 and they are not the same event: LF8 is what the package now CONTAINS, LF9 is what it now REQUIRES.

Reproduced against: Claude Haiku 4.5 (S1) — react-router/v2-a, 2026-09-10.

Source: react-router CHANGELOG.md — 7.0.0, Major Changes · 2024-11-22 · react-router PR #11505 · Bisected against the published packages (tools/audit/bisect-facts.mjs, 2026-09-10) · 2026-09-10

react-router@7 engine and peer requirements

New requirement in react-router 7.0.0 (2024-11-22) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

react-router 7 raised the package's own floors, and they have only gone up since. From 7.0.0 engines.node is >=20.0.0 (8.x: >=22.22.0) and the react peer range is >=18 (8.x: >=19.2.7); the whole 6.x line asked for Node 14 and React 16.8. 7.0.0 also declares a react-dom peer for the first time — 6.x declared none — but it is marked optional, so npm install react-router still installs no react-dom and a server-only or React Native project is unaffected. A project on Node 18 or React 17 cannot install react-router 7 or 8 without a peer/engine failure; there is no runtime deprecation warning first.

The stale belief: That react-router works on React 16.8+ and Node 14+, which was true for the whole 6.x line and stopped being true at 7.0.0.

// Stale
{
  "dependencies": {
    "react": "^17.0.2",
    "react-router": "^7.0.0"
  }
}
// npm: ERESOLVE — react-router@7 peer react@">=18" vs react@17.0.2

// Current
{
  "dependencies": {
    "react": "^18.3.1",
    "react-router": "^7.0.0"
  }
}
// Node >= 20 required. react-dom is an OPTIONAL peer: add it only
// if you use the DOM bindings, and npm will not install it for you.
// For React 17 / Node 14-18, stay on react-router 6.x.

READ OFF THE INSTALLED MANIFESTS AND BISECTED 2026-09-10 over the 51-rung ladder 6.4.0..8.3.1 (JOURNAL/118), one release per directory resolved alone. Exactly two floor changes exist in that range and both are majors: 7.0.0 (node 14 -> 20, react 16.8 -> 18, react-dom peer added optional) and 8.0.0 (LF4). Nothing moves anywhere on the 6.x line and nothing moves inside the 7.x line. THREE THINGS THE ARTIFACT SAYS THAT THE RELEASE NOTE DOES NOT. (1) The 7.0.0 changelog announces the Node floor TWICE and the shipped value is the second: update minimum node version to 18 (#11690) and then Drop support for Node 18, update minimum Node vestion to 20 (#12171) are both in the same section, and the published package.json says >=20.0.0. A pack written off the first line would have told readers Node 18 was enough. (2) The react-dom peer's ARRIVAL is not in the changelog at all; it is only in the manifest, and so is the peerDependenciesMeta flag that makes it optional — which is why JOURNAL/108's ladder installed no react-dom and had to add it explicitly. (3) The 6.x floor is spelled TWO ways for one value: >=14 on 6.4.0..6.15.x and >=14.0.0 from 6.16.0. The probe therefore compares parsed minimum versions rather than range STRINGS; asserting the literal >=20.0.0 would have been true only on the 7.x line and read as a window closing at 8.0.0 rather than as a floor that rose twice (HARNESS: write the predicate the fact claims, not the narrowest one that seems safe).

Reproduced against: Claude Haiku 4.5 (S1) — react-router/v2-a, 2026-09-10.

Source: react-router CHANGELOG.md — 7.0.0 major changes · 2024-11-22 · react-router CHANGELOG.md — 7.0.0 major changes · 2024-11-22 · npm registry metadata for react-router

future.v8_middleware / UNSAFE_MiddlewareEnabled / getLoadContext (react-router@8 middleware defaults)

Default changed in react-router 8.0.0 (2026-06-17) · boundary measured on 39 releases, react-router 7.0.0 through 8.4.0

React Router 8.0.0 (2026-06-17) made middleware and the RouterContextProvider context UNCONDITIONAL and removed the opt-in. There is no future.v8_middleware flag on 8.x: v8_middleware is not a key of FutureConfig on any 8.x release, so a future object carrying it names a property the type does not have. The context argument handed to loader, action and middleware is always a RouterContextProvider instance, the exported type UNSAFE_MiddlewareEnabled is gone, and the Future module augmentation (interface Future { v8_middleware: true }) that used to type context in Data Mode is no longer needed and no longer reads anything. A custom server's getLoadContext must now RETURN a RouterContextProvider; returning a plain object is no longer supported. On 7.9.0 through 7.18.x the flag existed and all of this was conditional on it. Do not add future: { v8_middleware: true } to a react-router@8 config, and do not tell a reader on 8.x to enable middleware — it is on. FutureConfig itself is NOT empty on 8.x and this fact does not say it is: 8.0.0-8.3.1 declare it empty and 8.4.0 declares unstable_routePatternMatching. react-router keeps using future flags; v8_middleware is simply no longer one of them.

The stale belief: That react-router's stabilised middleware and typed context still have to be opted into — via future: { v8_middleware: true } in react-router.config.ts, or via the Future module augmentation, or by declaring UNSAFE_MiddlewareEnabled — and that without the flag context is undefined (client) or the adapter's plain AppLoadContext object (server). That was exactly true on 7.9.0 through 7.18.x, which is why a model whose knowledge stops inside that ten-minor window states it confidently; it is the whole of what react-router@8 removed.

// Stale
// react-router.config.ts — the 7.9.0-7.18.x opt-in, on react-router@8
import type { Config } from '@react-router/dev/config'

export default {
  future: {
    // not a key of `FutureConfig` on any 8.x release: this flag is gone.
    v8_middleware: true,
  },
} satisfies Config

// ...and the type-level half of the same opt-in, also dead on 8.x:
declare module 'react-router' {
  interface Future {
    v8_middleware: true
  }
}

// ...and a custom server still returning a plain object:
export const getLoadContext = () => ({ db })   // no longer supported on 8.x

// Current
// react-router.config.ts on react-router@8 — nothing to enable.
import type { Config } from '@react-router/dev/config'

export default {} satisfies Config

// No `Future` augmentation: `context` is a RouterContextProvider unconditionally.
import { createContext } from 'react-router'
import type { MiddlewareFunction } from 'react-router'

export const userContext = createContext<User | null>(null)

export const middleware: MiddlewareFunction[] = [
  async ({ context }, next) => {
    context.set(userContext, await getUser())
    return next()
  },
]

// A custom server's getLoadContext RETURNS a provider on 8.x:
import { RouterContextProvider } from 'react-router'

export const getLoadContext = () => {
  const context = new RouterContextProvider()
  context.set(dbContext, db)
  return context
}

WHY THIS IS A SEPARATE FACT FROM LF13, checked rather than assumed. LF13 dates the RENAME of the exports at 7.9.0 (the unstable_ prefix coming off) and its claims are about what the package EXPORTS. This fact is about what the package REQUIRES OF A CONFIG, and its boundary is 8.0.0, ten minors higher. The two are independent and the tempting version merges them into one weaker statement: at 7.9.0 the exports lost the prefix unconditionally, with no flag involved (7.9.0's index.d.ts carries createContext, RouterContextProvider and MiddlewareFunction and none of the prefixed spellings), while the flag governed what context IS. Two true statements about different things.

MEASURED over the 52-rung ladder, one release per directory resolved alone, react/react-dom pinned 19.2.8. Three contiguous windows, all closing at 8.0.0 and nowhere else: v8_middleware in the shipped code is 0 on every rung 6.4.0-7.8.2, 76-84 across 7.9.0-7.18.3, and 0 on 8.0.0, 8.1.0, 8.2.0, 8.3.1 and 8.4.0; MiddlewareEnabled is 0 / 32-56 / 0 and UNSAFE_MiddlewareEnabled 0 / 4 / 0, both opening at 7.3.0. Every row is stated as a WINDOW rather than as an absence, for LF3b's reason: these names are also absent below their arrival, and an absence true for two different reasons is not contiguous.

THE TWO TYPE ROWS OPEN SIX MINORS BELOW THE FLAG AND THAT IS REAL, not an instrument artefact: the conditional MiddlewareEnabled shipped from 7.3.0, the unstable_ middleware era, while future.v8_middleware arrives only with the 7.9.0 stabilisation. So the three rows share an upper edge and have two different lower edges, which is why the fact is dated off the upper one.

WHAT WAS EXECUTED AND WHAT RESTS ON THE CHANGELOG, because the difference decides the severity and this fact carries S1 on a clause the ladder cannot reach. EXECUTED: the three windows above; the v8_middleware KEY absent from FutureConfig on all five 8.x rungs and present across 7.9.0-7.18.3; the UNSAFE_MiddlewareEnabled export present in the root declarations' export list on 7.3.0-7.18.3 and absent 8.0.0-8.4.0, which is the breaks-build clause — a named type import of a removed export is TS2305. NOT EXECUTED, and stated from the 8.0.0 changelog only: the getLoadContext return-type requirement, and the TS2353 an excess v8_middleware key would raise in a typed config. BOTH live in @react-router/dev, a SECOND package, and this runner installs one release of one package per rung — the same limit that makes LF5 unprobeable. FutureConfig is declared inside react-router but is NOT on its public export list (TS2459 when imported by name), so even the excess-property check cannot be written against this package; it was attempted and the control arm refused, which is how the limit was found rather than assumed.

THE FLAG IS A RUNTIME NO-OP IN DATA MODE AT EVERY RELEASE, AND MEASURING THAT IS WHAT KEPT THIS FACT HONEST. Driven through createStaticHandler().query() with generateMiddlewareResponse on 7.8.2, 7.9.0, 7.13.2, 7.18.3, 8.0.0 and 8.4.0, with and without future: { v8_middleware: true }: from 7.9.0 up the middleware runs and context is a RouterContextProvider in BOTH arms, and on 8.x the flag is accepted and silently ignored — no throw, no warning. So the harm from the stale belief is NOT a data-mode runtime failure, and an earlier draft of this fact that assumed one would have been wrong. It also settles a question this fact was opened to ask: LF13's correct_code does NOT need the flag, and is correct as published.

THE OTHER DIRECTION IS DELIBERATELY NOT FILED HERE. A reader pinned to 7.9.0-7.18.x in framework mode who is told by an 8.x-shaped model that middleware needs no flag omits it, and there context really is undefined or the adapter's plain object. Nothing charges for that under the rules as they stand — it is correct at current truth and wrong about every release below it, which is BACKLOG 0g's inverse-prior shape and its open sub-question (ii) about whether the dataset has a slot for it. It is named here so the omission is on the record rather than looking like an oversight, and it is not smuggled into this fact's severity.

ONE BOUNDARY INSIDE THE WINDOW IS CHANGELOG-DATED AND LADDER-BOUNDED, NOT DATED HERE. The Future augmentation key is spelled middleware at 7.9.0 (type MiddlewareEnabled = Future extends { middleware: infer T extends boolean }) and v8_middleware afterwards; react-router's own 7.9.1 entry calls it "Fix internal Future interface naming from middleware -> v8_middleware". This ladder carries 7.9.0 and then 7.9.4, so it bounds that edge at (7.9.0, 7.9.4] and cannot place it. No row claims it and the statement does not mention the 7.9.0 spelling — a fact should not date an event off a release note, which is the rule three bisects were written for.

WHERE THIS FACT CAME FROM. react-router/v4-d (Opus 5) gated the stabilised middleware API behind future: { v8_middleware: true } during battery v4 on 2026-09-23 and flagged the spelling as one of two things it was least sure of (JOURNAL/197). It was right about the flag and right to be unsure. NOTHING IS CHARGED HERE: that run was not probing this surface, the correction is a fact and the charge would be a finding, and the session that files the fact is not the session that gains a finding from it (JOURNAL/046). 8.0.0 is below every drawable subject's stated cutoff, so charge-windows.mjs prints it o or . and no battery on it can charge anyone — which is a reason this fact will stay uncharged rather than a reason to go looking.

AND ONE CORRECTION TO THE BACKLOG ITEM THAT COMMISSIONED IT. BACKLOG 0m published "v8_middleware occurs 0 times at 7.8.2, 62 at 7.9.0, 0 at 8.4.0". The direction is right and the middle number is wrong: it is 76 at 7.9.0. Checked against four readings of the same tarball, none of which produces 62 — identifier-wise 76, substring 76, lines containing it 72, dist/production alone 38. A related trap the recount did find: the whole TARBALL holds 8 occurrences at 8.0.0, in CHANGELOG.md and docs/, which react-router ships from 7.17.0 (LF6). The claim here is about the shipped CODE and the reader is told so, because a count taken over the tarball would make this fact's upper edge disappear.

THE OBVIOUS PARENT ROW WAS MEASURED AND REFUSED, AND IT CAUGHT A FALSE SENTENCE IN THIS FACT'S OWN FIRST DRAFT. interface FutureConfig {} — declared and empty — looks like the clean way to say "no future flags on 8.x", and it is NOT CONTIGUOUS: empty at 7.0.0-7.2.0, non-empty 7.3.0-7.18.3, empty 8.0.0-8.3.1, and NON-EMPTY AGAIN at 8.4.0, which declares unstable_routePatternMatching. A rising row on it would have been refused by the runner, correctly — LF13's RouterContext trap and LF12's onError trap, a third time. The draft statement said FutureConfig "is declared as the empty interface {}", which is a claim about CURRENT truth that is false at the newest release the Index serves, and it was written from a reading taken at 8.0.0 alone and generalised across the major. The ladder is what caught it: measure the proposition on every rung, not on the boundary rung. A SECOND candidate parent was then refused for a different reason, and it is the one worth carrying. Reading the v8_middleware KEY out of the interface FutureConfig body came back NON_CONTIGUOUS — true at 7.9.0-7.9.4, false to 7.13.1, true from 7.13.2 — which reads like a library event and is an artefact of the parse: react-router ships TWELVE declarations of interface FutureConfig in one release's declaration files and they disagree with each other, so "the first one" is an arbitrary choice among twelve and lands on a different one at different releases (7.9.0's first is EMPTY; 7.13.2's holds only unstable_passThroughRequests). The per-file draft of the same parse looked correct and was right BY LUCK — it broke out of its loop on the first NON-EMPTY match, skipping the empty declaration the concatenated reader lands on, and its reading agreed with the changelog so nothing looked wrong. The row that finally stands is the conjunction of two rows that ARE sound: RouterContextProvider present in the shipped code AND v8_middleware absent from it — one count over the whole corpus, with no "which declaration" to get wrong, false on every rung below 8.0.0 for two different reasons (the export is absent up to 7.8.2, the flag's name is present from 7.9.0) and true on all five 8.x rungs. This is also why the statement says v8_middleware is not a KEY of FutureConfig on 8.x on the strength of a zero COUNT: the key cannot be in a declaration if the name is in none of the shipped bytes, and that inference needs no parse. 4/4 rows confirmed, 0 to review.

RE-RUN 2026-09-23 OVER 55 RUNGS (JOURNAL/217), AND THIS FACT IS THE ONE WHOSE measured_range GOT NARROWER RATHER THAN WIDER. The ladder gained 6.25.0, 6.30.0 and 8.3.0 — three tracked stable minors it had been skipping — and all four rows confirm again, with the three counting rows reading every one of the 55 rungs and the 8.x windows now closing over SIX 8.x rungs rather than five. But LF14c, the export-list row, is E on all sixteen 6.x rungs: that line ships no root declarations export list for the instrument to read, which is the instrument being BLIND and is not the type being unexported. So the range over which this fact's whole statement was executed is 7.0.0..8.4.0, 39 rungs, not the harness's 6.4.0..8.4.0, 55. The range is a property of what ran, not of the ladder — tailwindcss LF28's rule (JOURNAL/215), here for the first time on a fact whose rows disagree with each other about how far down they can see.

Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.

Source: react-router CHANGELOG.md — v8.0.0, Major Changes (read from the CHANGELOG.md shipped inside react-router@8.4.0) · 2026-06-17 · react-router CHANGELOG.md — v8.0.0, the five consequences the entry lists under that heading · 2026-06-17 · react-router CHANGELOG.md — v8.0.0, the getLoadContext clause (NOT executed here: it is a second package) · 2026-06-17 · react-router PR #15078, named by the v8.0.0 changelog entry · Executed against the published packages (tools/audit/bisect-facts.mjs, 2026-09-23) · 2026-09-23

react-router-dom

Removed in react-router 8.0.0 (2026-06-17)

The react-router-dom package is not published on the v8 line — 8.0.0 removed it. Its npm latest tag still points at 7.18.3, while react-router's latest is on the 8.x line, so npm install react-router-dom in a v8 project installs a v7 router beside your v8 one and leaves two major lines of React Router in the same tree. On v8 there is no DOM package to install: import RouterProvider and HydratedRouter from react-router/dom, and everything else from react-router. In v7 this was already the real layout — react-router-dom was kept only as a re-export so v6 imports would keep resolving.

The stale belief: That a browser React Router app installs and imports from react-router-dom, which is where BrowserRouter, RouterProvider, Link and the hooks live.

// Stale
// v8: react-router-dom has no 8.x release, so this resolves to 7.18.3
import { RouterProvider, Link, useNavigate } from 'react-router-dom'

// Current
// v8
import { RouterProvider } from 'react-router/dom'
import { Link, useNavigate } from 'react-router'

MEASURED AGAINST THE REGISTRY 2026-09-09, which is what makes the claim checkable rather than a reading of the release note: react-router-dom's dist-tags are latest: 7.18.3 (published 2026-08-28) with no 8.x version present, while react-router's latest is 8.3.1. So the package was not unpublished and is not dead - it is still being patched on the 7.x line - it simply does not follow react-router to v8, which is the shape a model is most likely to get wrong: the import does not fail to resolve, it resolves to the wrong major. NOT BISECTABLE by tools/audit/bisect-facts.mjs, which installs one release of ONE package per rung; this fact is about a second package's dist-tags, so it carries no measured_range.

Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.

Source: react-router CHANGELOG.md — 8.0.0 major changes · 2026-06-17 · react-router CHANGELOG.md — 8.0.0 major changes · 2026-06-17 · npm registry metadata for react-router-dom

react-router@8 engine and peer requirements

New requirement in react-router 8.0.0 (2026-06-17) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

react-router 8 raised both floors: engines.node is >=22.22.0 and the react / react-dom peer range is >=19.2.7. On the 7.x line they are >=20.0.0 and >=18. A project on Node 20 or React 18 that installs react-router@latest gets a peer-dependency failure from npm (or a silently unmet peer under --legacy-peer-deps), not a runtime deprecation warning. Pin react-router@^7 if either floor is a problem; the 7.x line is still published and patched.

The stale belief: That react-router runs on Node 20 and React 18, which was true for the whole 7.x line.

// Stale
{
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router": "latest"
  }
}

// Current
{
  "dependencies": {
    "react": "^19.2.7",
    "react-dom": "^19.2.7",
    "react-router": "^8.0.0"
  }
}
// or stay on the maintained 7.x line:
// "react-router": "^7.18.0"   // Node >=20, React >=18

READ OFF THE INSTALLED MANIFESTS 2026-09-09, not off the release note: react-router@7.11.0 declares engines {"node":">=20.0.0"} and peers react >=18 / react-dom >=18; react-router@8.3.1 declares engines {"node":">=22.22.0"} and peers react >=19.2.7 / react-dom >=19.2.7. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). Both floors move at 8.0.0 and neither moves before it: 7.18.3, the last release on the 7.x line and the rung immediately below the major, still declares >=20.0.0 and >=18. The two halves were probed separately so that a release moving one floor without the other would report rather than confirm. The same 8.0.0 entry also announces the published packages becoming ESM-only. THAT HALF IS DELIBERATELY NOT WRITTEN AS A FACT, BECAUSE IT WAS MEASURED AND DOES NOT PRODUCE THE FAILURE IT SOUNDS LIKE: require("react-router") succeeds at 8.3.1 on Node 24.13.0, returning all 129 exports, because Node >= 22 supports require() of an ES module - and the new engine floor of 22.22.0 guarantees every supported Node can do it. A pack entry saying "require() now throws" would have been wrong on the shipped artifact.

Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.

Source: react-router CHANGELOG.md — 8.0.0 major changes · 2026-06-17 · react-router CHANGELOG.md — 8.0.0 major changes · 2026-06-17 · npm registry metadata for react-router

defer / json (loader and action data helpers)

Removed in react-router 7.0.0 (2024-11-22) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

React Router 7.0.0 (2024-11-22) removed the defer and json helpers, and AbortedDeferredError, from react-router's exports. A loader now returns a plain object and a slow value is a bare promise on it — the promise is passed through to loaderData untouched, so defer() has no replacement call rather than a renamed one; json() is replaced by returning the object directly, or Response.json() where a real Response is wanted, or data() where a status or headers are needed. The half of the API that CONSUMES a deferred value is unchanged: Await and useAsyncValue are exported at every release from 6.4.0 to 8.3.1. On the 6.x line all three removed names are exported at every release up to and including 6.30.6 (2026-08-18), so this is a live break for any code written against that line.

The stale belief: That a loader streams a slow value by wrapping it — return defer({ slow: promise }) — and returns JSON by wrapping it in json(...), which is how the v6 data router worked for its whole life and is what almost every v6-era tutorial, answer and migration guide shows.

// Stale
// The v6 idiom. On react-router 7 and 8 neither name is exported,
// so this fails at import time, not at request time:
//   SyntaxError: The requested module 'react-router' does not provide
//   an export named 'defer'
import { defer, json } from 'react-router'

export async function loader() {
  return defer({ slow: fetchSlowThing() })
}

export async function otherLoader() {
  return json({ ok: true })
}

// Current
// 7.0.0 and above: no wrapper. A bare promise on a plain object is
// passed through to `loaderData` as a promise.
import { data } from 'react-router'

export async function loader() {
  return { slow: fetchSlowThing() }   // no `defer`
}

export async function otherLoader() {
  return { ok: true }                 // no `json`
}

// Only where a status or headers are needed:
export async function thirdLoader() {
  return data({ ok: true }, { status: 201 })
}

// UNCHANGED — the consuming half survived the removal:
import { Await, useAsyncValue } from 'react-router'

BISECTED 2026-09-10 over the 51-rung ladder JOURNAL/116 built, 6.4.0 to 8.3.1, one release per directory resolved alone (JOURNAL/122). Three clauses, one per removed name, each asserted against a LIVE namespace rather than as a bare absence: s.m[name] === undefined is also what a namespace that loaded empty returns for every name, so every row requires Await and useAsyncValue to be exported at the same rung. That witness is deliberately not data, which arrives AT 7.0.0 and would have been a second condition flipping at the same release. THE RELEASE NOTE'S OWN REMOVAL LIST IS WRONG IN BOTH DIRECTIONS AND THIS FACT FOLLOWS THE MEASUREMENT, NOT THE NOTE. The 7.0.0 defer entry names five removed exports; type TypedDeferredData, UNSAFE_DeferredData and UNSAFE_DEFERRED_SYMBOL were never exported by react-router on either line and appear nowhere in its shipped text at any of the 51 rungs (swept with the witness identifier present, so the absence is measured and not a blind read). They were @remix-run/router's, the package LF8 dates the collapse of, so that list is written against the collapsed set rather than against this package. In the other direction json is a real removal at the same release — a separate changelog entry, PR #12146 — that the defer entry does not mention at all. THE REPLACEMENT WAS EXECUTED RATHER THAN READ OFF THE NOTE: 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. WHAT IS NOT MEASURED is the same behaviour BELOW 7.0.0, and the reason is LF8: the instrument is createStaticHandler, which is one of the names 7.0.0 added, so on the 6.x line there is nothing on this ladder to drive a loader with. The fact therefore claims the replacement works from 7.0.0 up and says nothing about whether the 6.x data router also passed a bare promise through. BOTH helpers were deprecated one minor earlier: 6.28.0's note is "Add deprecation warnings to json/defer in favor of returning raw objects — These methods will be removed in React Router v7", which is a warning at runtime and not a removal; the exports are still present and working at 6.30.6. This is the third fact dated 7.0.0 and the three are distinct events: LF8 is what the package now CONTAINS, LF9 what it REQUIRES, LF10 what it no longer contains.

Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.

Source: react-router CHANGELOG.md — 7.0.0, Major Changes (read from the published 7.0.0 tarball) · 2024-11-22 · react-router PR #12146 — cited in the 7.0.0 section as a separate entry from the defer removal · 2024-11-22 · react-router CHANGELOG.md — 6.28.0, Minor Changes (the deprecation, one minor before the removal) · 2024-11-06 · Bisected against the published packages (tools/audit/bisect-facts.mjs, 2026-09-10) · 2026-09-10

Runs, but is silently wrong

Nothing errors. The behaviour is simply not what a model trained earlier will tell you.

onError (HydratedRouter / RouterProvider prop)

Renamed in react-router 7.11.0 (2025-12-17) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

The client error-reporting prop on <HydratedRouter> and <RouterProvider> lost its unstable_ prefix in react-router 7.11.0: unstable_onError became onError, and the type unstable_ClientOnErrorFunction became ClientOnErrorFunction. The prefixed spellings exist on 7.8.2 through 7.10.0 and are gone from 7.11.0 onward. Because an unrecognised prop on a React component is ignored rather than rejected, <RouterProvider unstable_onError={report}> on 7.11.0 or later renders without any error, warning or type complaint at runtime — the handler is simply never called, so every error it was installed to report is silently dropped. This is not the same prop as the long-standing onError?: (error: unknown) => void, which exists on other react-router surfaces across the whole range and is unaffected.

The stale belief: That the client-side error handler on <HydratedRouter> / <RouterProvider> is still spelled unstable_onError on current react-router, and that passing that prop installs a working error handler.

// Stale
import { RouterProvider } from 'react-router/dom'

// 7.11.0 and above: the prop is not recognised, no error is raised,
// and `report` is never called for any routing error.
<RouterProvider router={router} unstable_onError={report} />

// Current
import { RouterProvider } from 'react-router/dom'

// 7.11.0 and above
<RouterProvider router={router} onError={report} />

// and in Framework Mode
// <HydratedRouter onError={report} />

BISECTED 2026-09-22 over the 52-rung ladder, 6.4.0 to 8.4.0 (JOURNAL/189), read from the shipped declarations and bundles with tools/lib/identifiers.mjs. THE TYPE NAME IS THE LOAD-BEARING ROW, not the prop name: onError is an identifier this package also uses for an unrelated and much older prop — onError?: (error: unknown) => void is declared on both sides of the boundary — so a row asserting the presence of onError would be true at every rung for a reason that has nothing to do with this rename. ClientOnErrorFunction is unambiguous and moves exactly once: 0 at 7.10.0 and below, 28 from 7.11.0 through 7.18.3, 18 across the 8.x line. The old-name rows are stated as WINDOWS for LF3b's reason (an absence below 7.8.2 is true because the unstable spelling had not been introduced): unstable_onError 128-144 across 7.8.2-7.10.0 and 0 elsewhere, unstable_ClientOnErrorFunction 28 across 7.8.2-7.10.0 and 0 elsewhere. THE SEVERITY IS SCORED ON REACT'S OWN BEHAVIOUR rather than on the library's: a removed function throws when called (LF11, one fact over, scored S1 for exactly that), but a removed PROP is swallowed by JSX, so the failure is an error reporter that reports nothing and says so nowhere. A TypeScript user typing the props explicitly does see it; a JavaScript user does not.

Reproduced against: Claude Fable 5.1 (S2), Claude Opus 5.5 (S2) — react-router/v3-d, 2026-09-22; react-router/v6-a, 2026-09-28.

Source: react-router CHANGELOG.md — 7.11.0, Minor Changes (read from the CHANGELOG.md shipped inside react-router@7.11.0) · 2025-12-17 · react-router PR #14546, named by the 7.11.0 changelog entry · Executed against the published packages (tools/audit/bisect-facts.mjs, 2026-09-22) · 2026-09-22

generatePath

Behaviour changed in react-router 7.12.0 (2026-01-07) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

generatePath substitutes a dynamic segment that is followed by a literal suffix since react-router 7.12.0. generatePath("/books/:id.json", { id: "7" }) returns "/books/7.json". On 7.11.0 and every release down to 6.9.0 the same call returns the pattern unchanged — "/books/:id.json" — with no error, no warning and no missing-parameter message, so the URL is silently wrong and a fetch against it 404s at runtime rather than failing at the call site. 7.12.0 RESTORED this behaviour rather than introducing it: react-router substituted suffixed params correctly from 6.4.0 through 6.8.2, and 6.9.0 broke it, so the outage ran two years and ten months. On 7.12.0 and above, call generatePath directly and delete any hand-rolled String.replace substitution written to work around this.

The stale belief: That generatePath only substitutes a parameter that occupies a whole path segment, so a pattern like /books/:id.json or /files/:name.csv has to be built by string replacement or a template literal. Held from either direction: it is what the 6.9.0-to-7.11.0 releases actually did, and it is also wrong about the 6.4.0-to-6.8.2 releases, where the plain call already worked.

// Stale
import { generatePath } from 'react-router'

// The workaround, because generatePath returned the pattern unchanged
// for a param followed by a literal suffix:
const url = '/books/:id.json'.replace(':id', String(book.id))

// Current
import { generatePath } from 'react-router'

// 7.12.0 and above: the suffix is preserved and the param is substituted
const url = generatePath('/books/:id.json', { id: String(book.id) })
// -> '/books/7.json'

EXECUTED 2026-09-09 against the shipped packages, not read off the note: react-router@7.11.0 returns "/books/:id.json" and react-router@8.3.1 returns "/books/7.json" for the same call, in plain Node with no DOM. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). The changelog's 7.12.0 is CONFIRMED as the boundary: the call returns the pattern unchanged at 7.11.0, the release immediately below it, and the substituted path at 7.12.0 and every rung above. RE-CONFIRMED 2026-09-09 on the ladder extended to 37 rungs (7.0.0-8.3.1), the boundary unmoved. THIS FACT IS A RESTORATION AND THAT ONLY BECAME VISIBLE WHEN THE LADDER CROSSED THE MAJOR (2026-09-10, JOURNAL/116, 51 rungs from 6.4.0). The 7.12.0 boundary and the false cell at 7.11.0 are unchanged and were never in doubt; what moved is the clause "and below". The call is CORRECT from 6.4.0 through 6.8.2, breaks at 6.9.0 (2023-03-10) and is not correct again until 7.12.0 (2026-01-07) — an outage of two years and ten months, bisected at patch granularity between 6.4.0 and 6.10.0 because at minor granularity the true region is a single cell at the ladder's floor and unreportable. THE RELEASE THAT BROKE IT DESCRIBES ITSELF AS A FIX: 6.9.0's changelog entry is "Fix generatePath incorrectly applying parameters in some cases" (PR #10078), and on this input it is the release where the substitution stopped happening. A reader whose knowledge of this API predates 6.9.0 holds a belief that is correct about 6.4.0-6.8.2, wrong about the whole 6.9.0-7.11.0 range, and correct again about 7.12.0 and above. THE OTHER PATH BUILDER HAS ITS OWN BOUNDARY AND IT IS LOWER: href handles the same suffixed pattern from 7.9.0 (LF7), three minors before this fix. On 7.9.0 through 7.11.0 the two disagree on identical input — href("/books/:id.json", { id: "7" }) returns "/books/7.json" while generatePath("/books/:id.json", { id: "7" }) returns the pattern unchanged — so a reader pinned in that band has a correct builder available and does not need the hand-rolled replace. Below 7.9.0 neither is safe: href silently DELETES the suffix there rather than leaving it unsubstituted, which is a different wrong answer, not the same one.

Reproduced against: Claude Opus 5 (S2) — react-router/v1-c, 2026-09-09.

Source: react-router CHANGELOG.md — 7.12.0 · 2026-01-07 · react-router PR #14269 · react-router CHANGELOG.md — 6.9.0, Patch Changes (the release that broke it) · 2023-03-10 · Bisected across the major (tools/audit/bisect-facts.mjs, 2026-09-10) · 2026-09-10

pattern (loader / action argument)

Renamed in react-router 7.15.0 (2026-05-05) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

The instrumentation APIs lost their unstable_ prefix in react-router 7.15.0. The route pattern handed to a loader/action is args.pattern, not args.unstable_pattern, and the router option is instrumentations, not unstable_instrumentations; the unstable_ServerInstrumentation, unstable_ClientInstrumentation, unstable_InstrumentRequestHandlerFunction, unstable_InstrumentRouterFunction, unstable_InstrumentRouteFunction and unstable_InstrumentationHandlerResult types dropped the prefix too. The old names are gone, not aliased: reading args.unstable_pattern on 7.15.0 and above yields undefined in JavaScript rather than a type error, so a telemetry span or log line built from it is labelled undefined.

The stale belief: That the route pattern inside a loader is args.unstable_pattern and the router option is unstable_instrumentations, because the instrumentation API is still unstable.

// Stale
export async function loader(args: Route.LoaderArgs) {
  // undefined on 7.15.0 and above - the span name silently becomes 'undefined'
  span.setAttribute('route', args.unstable_pattern)
}

// Current
export async function loader({ pattern }: Route.LoaderArgs) {
  // 7.15.0 and above
  span.setAttribute('route', pattern)
}

EXECUTED 2026-09-09 against the shipped packages: the loader argument keys are [context, params, unstable_pattern, request] at react-router@7.11.0 and [context, params, pattern, request, url] at 8.3.1 — so the rename is measured in both directions, the old key absent above and the new key absent below. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). The changelog's 7.15.0 is CONFIRMED and every clause of the statement was probed separately rather than the headline: the new argument key arrives at 7.15.0 (absent at 7.14.2); the old key is GONE and not aliased there; all six renamed TYPES are present unprefixed in the shipped dist/ at 7.15.0 and every unstable_ spelling of them absent, both directions checked, so the type clause is read off the published declarations rather than left as a citation; and the router OPTION unstable_instrumentations, which is a different identifier from the argument and would otherwise have ridden on it, is likewise gone at 7.15.0. FOUND BY THE LADDER AND NOT CLAIMED BY THIS FACT: unstable_pattern itself arrived at 7.9.5, a PATCH release, so it exists on exactly 7.9.5 through 7.14.2. That matters for how the row is scored rather than for what the fact says - probed as the old key is absent it is also true below 7.9.5, where there was no such key to remove, and a bisect reads that as a boundary at the floor. THE action CLAUSE IS NOW EXECUTED TOO (2026-09-22, JOURNAL/179). The statement says the pattern handed to a loader/action is args.pattern, and the rows read only the loader until the clause-coverage sweep named it (JOURNAL/176). The added row checks BOTH directions on the action's own argument object - pattern present and unstable_pattern absent - because the load-bearing half of this fact is that the old name is gone rather than aliased, and an action that kept an alias the loader dropped is exactly the asymmetry a single-handler probe averages away. It agrees with the loader on all 51 rungs: false at 7.14.2, true from 7.15.0. No middleware row was added to this fact, deliberately: LF2's statement names middleware and this one does not, and a probe answers its own statement rather than the neighbouring fact's.

Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.

Source: react-router CHANGELOG.md — 7.15.0 · 2026-05-05

url (loader / action / middleware argument)

Added in react-router 7.15.0 (2026-05-05) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

loader, action and middleware receive a url argument since react-router 7.15.0, where it was stabilised from unstable_url. It is the normalised URL: React Router's own transport details — the .data suffix on client data requests and the index / _routes search params — are already stripped from it. Read args.url rather than new URL(args.request.url) when you want the URL the user is on. This matters most on the v8 line, where the future.v8_passThroughRequests flag was removed and the raw incoming request is now always what your loader gets, so request.url can carry those suffixes.

The stale belief: That the only URL available inside a loader or action is request.url, and that parsing it with new URL(...) yields the route's own pathname.

// Stale
export async function loader({ request }: Route.LoaderArgs) {
  // On a client data request this pathname can be '/books/7.data',
  // and the search params can carry React Router's own `index` key.
  const { pathname, searchParams } = new URL(request.url)
  return load(pathname, searchParams)
}

// Current
export async function loader({ url }: Route.LoaderArgs) {
  // 7.15.0 and above: already normalised - no .data suffix, no `index` param
  return load(url.pathname, url.searchParams)
}

TWO HALVES, ONE EXECUTED. The presence of url was executed 2026-09-09 against the shipped packages: a createStaticHandler loader driven through query() sees argument keys [context, params, unstable_pattern, request] at react-router@7.11.0 and [context, params, pattern, request, url] at 8.3.1, in plain Node with no DOM. The second half — that request.url carries the .data suffix on the v8 line — is taken from the 8.0.0 changelog entry removing future.v8_passThroughRequests and has NOT been executed here; it needs a framework-mode server request, which createStaticHandler does not produce. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). The changelog's 7.15.0 is CONFIRMED for the presence half: no url argument at 7.14.2, the release immediately below it, and a real URL whose pathname is the route's at 7.15.0 and above. THE stabilised from unstable_url CLAUSE IS NOW MEASURED RATHER THAN QUOTED, and it moves nothing in this fact while adding a date the changelog does not give: unstable_url exists on exactly 7.13.2 through 7.14.2 - absent at 7.13.1, absent from 7.15.0 - so the unstable spelling arrived in a PATCH release and lasted three releases. The second half of the statement, that request.url carries the .data suffix on the v8 line, is still unexecuted for the reason above: it needs a framework-mode server request that createStaticHandler does not produce. ALL THREE HANDLERS ARE NOW EXECUTED (2026-09-22, JOURNAL/179). The statement's first sentence names loader, action AND middleware, and until this date only the loader had ever been run: the clause-coverage sweep (JOURNAL/176) reported action and middleware as named surfaces no row touches, which is zod LF1's shape exactly. Two rows were added over the rebuilt 51-rung ladder and both land where the loader's row lands - url present and a real URL whose pathname is the route's, false at 7.14.2, true at 7.15.0 and every release above it - so the three handlers stabilised together and no correction is owed. The action is reached by a POST through the same createStaticHandler; THE MIDDLEWARE NEEDED AN INSTRUMENT RATHER THAN AN ARGUMENT, and this is the part worth carrying forward: a middleware does not run under a plain query() at all, so six attempts at the row read a well-formed null at every rung - the honest-looking answer middleware never receives url, produced by an instrument that never ran a middleware. It is enabled by query's generateMiddlewareResponse option, which the shipped bundle still calls respond in its own error string. Both possible confounds were measured across the ladder before the row was believed: the route key is middleware (not unstable_middleware) at every rung from 7.11.0 up, four minors BELOW this boundary, and generateMiddlewareResponse is in the shipped bundle over the same range - so neither the handler's own stabilisation nor the enabling option moves inside the window this fact dates. The second half of the statement, about request.url on the v8 line, is STILL unexecuted and still declared so.

Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.

Source: react-router CHANGELOG.md — 7.15.0 · 2026-05-05 · react-router CHANGELOG.md — 8.0.0 · 2026-06-17

href

Behaviour changed in react-router 7.9.0 (2025-09-12) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

href builds a path correctly for a dynamic segment followed by a literal suffix, and for an omitted single optional parameter, since react-router 7.9.0. href("/books/:id.json", { id: "7" }) returns "/books/7.json", and href("/:lang?", {}) returns "/". On 7.8.2 and below the same two calls return "/books/7" and "" — the literal suffix is silently deleted rather than left unsubstituted, and the omitted-optional case yields an empty string, which as an href attribute resolves to the current URL rather than to the site root. Neither call errors or warns on either side. href is therefore not a safe substitute for generatePath below 7.9.0: between 7.9.0 and 7.11.0 it is the only one of the two that is correct on a suffixed pattern, and below 7.9.0 both are wrong in different ways.

The stale belief: That href has always handled suffixed and optional path parameters correctly, so it can be recommended as the substitute for generatePath on any react-router 7 release — or, the mirror of it, that href shares generatePath's suffixed-param bug on every release and should be avoided.

// Stale
import { href } from 'react-router'

// On 7.8.2 and below the '.json' is silently dropped:
const url = href('/books/:id.json', { id: '7' })
// -> '/books/7'   (not '/books/7.json')

// and an omitted single optional param yields the empty string:
const home = href('/:lang?', {})
// -> ''            (not '/')

// Current
import { href } from 'react-router'

// 7.9.0 and above: the literal suffix survives
const url = href('/books/:id.json', { id: '7' })
// -> '/books/7.json'

const home = href('/:lang?', {})
// -> '/'

BISECTED 2026-09-09 over a 37-rung ladder, 7.0.0 to 8.3.1, one release per directory resolved alone, react and react-dom pinned to 19.2.8 on every rung (JOURNAL/111). Both clauses of the changelog line go true at exactly 7.9.0 and are false at 7.8.2, the release immediately below it. THIS FACT WAS ALMOST PUBLISHED AS AN INVARIANT AND IT IS NOT ONE. JOURNAL/110 executed href("/books/:id.json", { id: "7" }) at 7.11.0, 7.14.2 and 8.3.1, got "/books/7.json" at all three, and recorded that href never had generatePath's problem; a first bisect over the committed 21-rung ladder agreed, reporting the row true at all 21 rungs. Every one of those rungs is at or above 7.9.0 — the ladder's floor was this fact's own boundary. Extended down the 7.x line the row is false at nine rungs. A COMPANION ROW DATES THE API ITSELF so this date cannot be misread as href arriving: the ordinary whole-segment call href("/books/:id", { id: "7" }) is false at 7.1.5 and true from 7.2.0, where the utility was introduced, so 7.9.0 is a behaviour change to an API that had existed for seven minors. The two path builders were fixed four months apart on the same input — href at 7.9.0 (2025-09-12, PR #13797), generatePath at 7.12.0 (2026-01-07, PR #14269, this library's LF1) — and 7.9.0 is also the last release before the 81-day publishing gap that JOURNAL/110 measured as setting the model boundary on this library.

Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.

Source: react-router CHANGELOG.md — 7.9.0, Patch Changes · 2025-09-12 · react-router PR #13797 · react-router CHANGELOG.md — 7.2.0, Minor Changes (the API's own arrival) · 2025-02-18 · Bisected against the published packages (tools/audit/bisect-facts.mjs, 2026-09-09) · 2026-09-09

Wrong facts about the library

Not code — versions, minimums and metadata that models state confidently and get wrong.

node_modules/react-router/docs

Added in react-router 7.17.0 (2026-06-04) · boundary measured on 55 releases, react-router 6.4.0 through 8.4.0

Since react-router 7.17.0 the installed package carries a subset of the official documentation as Markdown at node_modules/react-router/docs, so a coding agent working in a project can read that project's own version of the docs off disk instead of fetching a doc site that may describe a different release. The shipped subset is index.md plus start/, how-to/, explanation/ and upgrading/; the auto-generated API reference (api/), community/ and the tutorials are excluded.

The stale belief: That the react-router npm package ships only dist/, a README and a changelog, and that documentation has to come from the web.

# 7.17.0 and above - the docs are in the install
ls node_modules/react-router/docs
# explanation  how-to  index.md  start  upgrading

EXECUTED 2026-09-09: the directory is absent from an installed react-router@7.11.0 and present at 8.3.1 with exactly the five entries listed. S4 rather than something higher because nothing breaks if a model does not know this - it is a capability the model would not think to use, which is the failure mode the wrong-metadata level covers. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). The changelog's 7.17.0 is CONFIRMED: no docs directory at 7.16.0, the release immediately below it, and exactly the five entries at 7.17.0 and every rung above. The probe asserts the EXACT set rather than the directory's existence, so a release that also shipped the generated api/ reference would falsify the statement instead of passing a presence check. Recorded partly because it bears on this Index's own subject: a library shipping a correction channel for coding agents inside the package.

Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.

Source: react-router CHANGELOG.md — 7.17.0 · 2026-06-04 · react-router CHANGELOG.md — 7.17.0 · 2026-06-04 · react-router PR #15121

Not corrections — recorded for honesty

Claims seen in a run but not yet verified against a primary source. Never treated as findings:

  • On the v8 line, does a client data request's request.url reach a loader carrying the .data suffix and the index/_routes search params, or does the router still strip them? (Why it matters: It is LF2's second clause, the Index has never executed it, and this arm asserted the stripping confidently while the fact's note leans the other way for v8 (future.v8_passThroughRequests was removed). Neither direction can be charged until a framework-mode server producing a real client data request is driven; createStaticHandler does not produce one.) (open since 2026-09-09)
  • This arm's only charged failure and its uncharged one are the same shape — an unstable_ prefix it believes is still there — and they sit two minors apart (7.10.0, 7.11.0) in a stretch where it also correctly identified a THIRD stabilisation (middleware, 7.9.0). Its describable ceiling is 7.9.0. So the subject appears to know stabilisations up to exactly its boundary and to assume the prefix persists immediately above it, rather than being uniformly ignorant above the boundary. Is unstable_ prefix persistence a systematically predictable stale prior — i.e. does a subject's last-known prefix state carry forward across its boundary more reliably than other kinds of change? That would be a class of fact worth mining deliberately rather than stumbling into, and this library alone has more of them. (open since 2026-09-22)
  • Does defer's removal at 7.0.0 belong in the Index as a fact, and does this battery's silence about it mean anything? — A sweep run this session over all 61 installed rungs found defer exported at every 6.x rung including 6.30.6 (published 2026-08-18) and absent at every rung from 7.0.0 up — contiguous, witness never absent, zero unreadable rungs, which is bisect-quality evidence of a removal at exactly the release this battery probes. The Index holds no fact for it. It was measured BEFORE these draws were read and recorded in the spec as queued rather than written, so that a draw using defer could not be charged against a fact retrofitted to it. Neither Haiku twin used defer, so nothing turns on it here — but a v6-era subject writing a deferred loader is a live S1 that this corpus currently cannot charge, which is the same gap v1-d opened and LF8 closed. (open since 2026-09-10)
  • Fable 5.1 (v3-d) and now Opus 5.5 both keep a surface's unstable_ prefix across their boundary, on the same two surfaces, while each correctly names the 7.9.0 stabilisation right at it. v3-d's open question asked whether prefix persistence is a predictable stale prior; this is a second subject with the same shape on the same bytes. Still n=2 subjects on one library. (open since 2026-09-28)
  • Does href() substitute a param followed by a literal suffix at every release, or does it have a boundary of its own below 7.9.0? (Why it matters: It is the correct answer this battery hands developers on both sides of LF1's boundary, and it is currently measured at three rungs (7.11.0, 7.14.2, 8.3.1) rather than across the bisector's ladder. If it is an invariant it is worth a fact of its own with change_kind: invariant; three rungs is not enough to publish one.) (open since 2026-09-09)
  • SECOND INSTANCE OF HEDGED DEMONSTRATION, AND THE RULE JOURNAL/052 ASKED FOR IS NOW OVERDUE. prisma/v3-b answered "cannot describe" and then wrote four correct attributed claims; this arm shipped a correct primary answer and a stale alternative flagged as unverified. Both flip sign depending on which half is read, and both were resolved by publishing both readings and charging nothing. The question JOURNAL/052 left open is unchanged and now has two instances behind it: what is the grading rule for a reply that demonstrates a belief it simultaneously disclaims? Two candidate shapes, neither adopted here: score the answer the subject SAYS to ship (which makes a hedge a complete defence, and a model could hedge everything), or score every form offered (which charges a subject for correctly flagging its own uncertainty, penalising the behaviour the Index most wants). A third possibility is that hedged demonstration is its own published category beside CORRECT/WRONG/ABSTAIN, which is what these two runs have de facto created without naming. (open since 2026-09-22)
  • This arm was drawn to test a direction of HARNESS.md § A ladder measures a claim, a code battery measures a demonstration that the published corollary does not cover. The corollary runs one way — a window marked LIVE off a ladder-only boundary may already be known, so charge-windows.mjs's + can be too generous. The inverse is that a window marked KNOWN off a stated boundary may not be held at all, which would make o too conservative and would mean the sweep is silently parking drawable windows. One instance now exists and it came out the reassuring way: the known cell was genuinely known, and the demonstration reproduced the stated boundary exactly at both ends. n=1, on the subject whose stated boundary was already the most thoroughly corroborated in this library. The question stays open: does a known cell ever fail to be held? Answering it needs o cells drawn on subjects whose boundary came from a single stated reading, which is the opposite selection from the one made here. (open since 2026-09-23)
  • Sonnet 5's react-router lag is 13.4 months against its own 9.5-month median, and its per-library lags are bimodal — {2.5, 5.7, 9.0, 9.5} against {13.1, 13.3, 13.4, 14.4}. Is the split a property of the libraries or of this subject? — Four of the eight sit within a month of each other at ~13-14 months, which is close enough to the subject's own release date to look like a floor rather than a library effect, and the other four are spread across seven months. If the high cluster is a floor, the quantity the Index calls a boundary is measuring two different things depending on which side a library falls, and the loud-library mechanism cannot be tested on a library in the high cluster at all. The test is cheap and needs no new battery: the same split can be computed for every subject out of data/index.json.knowledge_boundary, which now has eight libraries for four subjects. (open since 2026-09-10)
  • THE TWINS AGREED ON EVERY GRADED CELL, WHICH IS THE OPPOSITE OF WHAT THIS BATTERY PREDICTED AND OF WHAT zod/v7 FOUND. Prediction 2 registered that they would disagree on at least one graded cell, on the reasoning that the gap between surface and cutoff was narrower here than in zod/v7, where byte-identical twins believed opposite things about one library's failure mode. Here they produced the same verdict on both facts, by the same route, with the same unprompted stability claim — and differed only on ungraded choices (React root callback: onCaughtError/onUncaughtError versus onRecoverableError) and on their gap-end self-report (v7.1.0 versus v7.2-7.3). Is duplication informative mainly where the subject REACHES the probed surface, and near-redundant where both twins abstain? If so the duplication rule could be spent where it pays, but the corpus has two data points and that is not enough to change a standing rule. (open since 2026-09-22)
  • This arm's hedge on Task 1(b) names the exact possibility it got wrong — "I'm not fully confident whether a later 7.x release dropped the prefix and stabilized this" — and the code does not act on it. The same shape appears in the twin -b. HARNESS.md § The charge follows the designated form resolves the grading (one designated form, charged), but it leaves a measurement question open: when a subject correctly identifies the axis on which it is stale, does that predict anything about whether it is stale on that axis? Here it flagged the prefix and was prefixed-wrong; react-router/v3-d flagged unstable_onError the same way and was also wrong; react-router/v4-d (Opus 5, this battery) flagged the exact minor as its uncertainty and was right about the substance. Three instances, no rule. A sweep over the corpus for self-flagged axes and their outcomes would say whether the flag is informative or is uniform politeness. (open since 2026-09-23)

Findings, code and citations: data/react-router/ — one JSON file and one write-up per model, each finding carrying the release that broke the belief, its publication date and a verbatim quote from the primary source. Corrections: data/react-router/facts.json. This file is generated by tools/build-corrections.mjs; if the prose and the data ever disagree, that is a bug in the generator, not a stale pack.


The Stale Priors Index, Unattended Works (https://stalepriors.com/corrections/react-router.md), CC BY 4.0. Retrieved 2026-09-22. Licence: <https://creativecommons.org/licenses/by/4.0/> (SPDX-License-Identifier: CC-BY-4.0). Quoted changelog text belongs to its authors; nothing here grants any right in the libraries described. Terms in full: <https://stalepriors.com/data/LICENSE>.

Honest note on maintenance: this pack is generated, not written by hand. tools/build-corrections.mjs renders it from the verified release facts in data/react-router/facts.json joined to the findings in the runs it cites, and the build fails if a finding names an API no verified fact corrects. Hand-written packs drifted from the data twice before this was automated; the dataset remains the authority.