What Claude Opus 5 gets wrong about react-router — battery v1-c, tested 2026-09-09

Run react-router--claude-opus-5--v1-c--2026-09-09

Summary

The only arm of four licensed to charge, and it charges once. Claude Opus 5 affirmed its May 2026 cutoff while qualifying recall density — the qualifying side of the line both Fable 5.1 arms fell off — and then stated, in the present tense and with a mechanism, that generatePath silently returns /books/:id.json unsubstituted. react-router 7.12.0 fixed that on 2026-01-07, four months inside its cutoff; the shipped 8.3.1 package returns /books/7.json and 7.11.0 returns the pattern, both executed this session. Charged S2: the code works, but the advice built on it is to redesign the application's URLs and to write a regression test against an upgrade that already happened. Boundary 7.9.0 / 7.10.0 — the Index's first measured boundary on react-router — and the battery's prediction is confirmed here by twenty days, the weak confirmation its spec named in advance.

SubjectClaude Opus 5 claude-opus-5, Anthropic
Invoked asAgent tool, model alias "opus"; prompt sent verbatim from prompts/sent/react-router-v1.txt. Pre-registered as the second charging subject, run single (LF1 is its only live fact; LF2 and LF3 are parked in its cutoff month). This is the only arm of four that is licensed to charge anything.
Cutoff the model states2026-05
Newest react-router release it could place7.9.0 · 2025-09-12 (~8 month lag)
Oldest react-router release it could not place7.10.0 · 2025-12-02 (so this run brackets the subject’s boundary to 2025-09-12 – 2025-12-02)
In its own words"The most recent version I can describe with reasonable confidence is React Router 7.9.x, around September 2025 — the release I associate with middleware being stabilized behind a future.v8_middleware flag. That is also the highest number I'd assert as real from memory. ... So: no clean gap between 'latest I know of' and 'latest I can describe' here — my knowledge just stops, which likely means I'm behind." BRACKET PROVENANCE — Both ends come from direct question (c), asked last. It described 7.9.0's contents and dated it to September 2025 (actual 2025-09-12), then named 7.10.0 as "the first release I know only as a number, not contents". The self-placement is unusually explicit — "my knowledge just stops, which likely means I'm behind" — and it declined to assert any release above 7.9.0 as real.
Library at test timereact-router 8.3.1 (npm), verified 2026-09-09
Batteryreact-router/v1-c · 4 tasks, 4 direct questions · probe window 7.12.0 to 7.15.0
Tool uses during test0 (a run with any tool use is void — we measure training knowledge, not retrieval)
Tested2026-09-09
Findings1, of which 1 chargeable

Findings

F1 · States as current behaviour that the path helper silently returns the pattern unsubstituted, and ships a workaround for a footgun the library removed eight months ago

S2silently-wrong · generatePath · behavior-changed · changed in react-router 7.12.0 (2026-01-07) · chargeable

react-router 7.12.0 published 2026-01-07, four months inside this subject's stated May 2026 cutoff, which it affirmed in direct question (b) while qualifying only the density of its recall. This is the Index's first charged finding on react-router and the first measured boundary on the library.

What the model believes

Asked for the concrete URL and then, separately, whether there is anything about the pattern shape worth knowing, it answered in the present tense: "generatePath(\"/books/:id.json\", { id: \"7\" });" produces "/books/:id.json" — "Not /books/7.json. It silently returns the pattern back, unsubstituted — no throw, no warning." It then explained the mechanism it believes is current: "generatePath interpolates whole path segments only. Its implementation splits the pattern on / and tests each segment against an anchored regex along the lines of ^:([\w-]+)\??$. :id.json is a param plus a literal suffix, so the segment fails that test and is passed through verbatim." It called the resulting asymmetry between matching and generating "a real footgun" and shipped a workaround as the code it would ship.

What it wrote
// The code the draw says it would actually ship, and the reason it gives for it.
// app/lib/paths.ts
import { generatePath } from "react-router";

export const BOOK_JSON = "/books/:id.json";           // still the single source of truth
export const bookJsonPath = (id: string) =>
  `${generatePath("/books/:id", { id })}.json`;        // -> "/books/7.json"

// Its first recommendation is to change the URL rather than call the helper:
//   "Restructure the route to /books/:id/export.json (or .../data.json) so the param
//    is a whole segment. Then generatePath — and, in framework mode, the typed href()
//    — just work."
What works on react-router 8.3.1
import { generatePath } from "react-router";

// Executed this session against react-router@8.3.1, installed under its real name
// beside react@19.2.8:
//   generatePath("/books/:id.json", { id: "7" })  ->  "/books/7.json"
// and against react-router@7.11.0, the release below the boundary:
//   generatePath("/books/:id.json", { id: "7" })  ->  "/books/:id.json"
// The draw describes 7.11.0's behaviour and states it as current.
export const BOOK_JSON = "/books/:id.json";
export const bookJsonPath = (id: string) => generatePath(BOOK_JSON, { id });

// href() was ALSO executed at all three rungs and returns "/books/7.json" at
// 7.11.0, 7.14.2 and 8.3.1 — it never had this problem in the tested window.
Impact

A developer is told, in the present tense and with a mechanism, that the router's own path helper cannot build this URL. The concrete costs are all in the draw's own answer: its first recommendation is to change the application's public URLs so the param becomes a whole segment; its second is a hand-written concatenation helper plus a unit test written to catch "a router upgrade that changes this behavior" — an upgrade that happened eight months before the question was asked; and its third is to abandon the path suffix for content negotiation. A URL scheme redesigned around a fixed bug is not recoverable by a later npm update. The generated code does produce the right string, which is what caps this at S2 rather than S1.

Scope note

Charged on the present-tense claim about generatePath only. Two subsidiary claims are separated out. (1) Its account of the OLD mechanism is accurate — 7.11.0 does return the pattern verbatim with no throw and no warning, verified this session — so the finding is that the belief is stale, not that it was ever wrong. (2) Its claim about the typed helper, "Note href() ... is built on the same segment-wise interpolation, so I would not expect it to rescue a partial segment either", is FALSE at every rung tested: href("/books/:id.json", { id: "7" }) returns "/books/7.json" at 7.11.0, 7.14.2 and 8.3.1. It is not charged separately because the draw explicitly hedged it and told the developer to check — "worth a 30-second check against your version rather than trusting me" — which is an imprecision under the code-vs-claim rule, not a finding. Its claim that matching handles the pattern is also correct and is not charged.

Verified against

What it got right, and near misses

Recorded so the run cannot be read as a hit list. A model that is right for an obsolete reason is recorded here, not as a finding.

KindAPINote
imprecisioninstrumentations Task 2's one-word answer is "no" — the router provides no dedicated instrumentation hook — and the instrumentation API has existed in unstable_ form since a 7.9.x patch, well inside this subject's cutoff. It is not charged, for two independent reasons and both are structural rather than generous. First, the only published fact covering this API is LF3, dated to the 7.15.0 stabilisation (2026-05-05), which is inside this subject's stated cutoff MONTH and therefore parked. Second, the draw hedged onto the true option and named it: "I have a faint, low-confidence sense that an unstable_instrumentation-style API was discussed for tracing loaders/actions with route ids; if your version is newer than mid-2025, check the changelog before building the manual version." Under the code-vs-claim rule that is an imprecision, and under HARNESS.md § The hedge lands on the true option it is the pattern already recorded twice. (LF3's release is parked in this subject's stated cutoff month, and the draw hedged onto the correct API by name.) [chargeable miss — the arm licensed to charge states a cutoff below the release under test; absent from the finding count]
imprecisionurl Task 3 reads the URL with new URL(request.url) and never mentions the normalised url argument the loader has received since 7.15.0 (LF2). Working code, and the additive-API rule makes it an imprecision rather than a finding: it did not claim no such argument exists. LF2 is parked for this subject in any case. Its list of ways the loader's URL can differ from the address bar is unusually good and mostly correct — prefetch and fetcher loads running for URLs the user is not on, the hash never reaching the server, proxies rewriting the origin, clientLoader synthesising the Request.
correct THE ATTRIBUTION ANCHOR, and it is exact. Task 4 produced a working framework-mode scaffold — @react-router/dev/vite plugin, react-router.config.ts typed with Config, app/routes.ts using index/route/layout/prefix, a route module with Route.LoaderArgs/Route.ComponentProps from ./+types/, and the rootDirs typegen wiring — and in direct question (c) dated 7.0.0 to "Nov 22, 2024", which is the exact publication date. An arm that places the anchor to the day is readable, and its boundary answer counts.
context The poison rung produced no false positive and no discrimination, which is the honest reading. 7.19.0 has never been published and this arm declined it — but it declined every number above 7.9.0, real ones included, and said so explicitly: "I can only positively vouch for 7.5.0 and 7.9.0. For every 8.x number and every 7.x above 7.9, 'I don't know' is the accurate answer, not 'that doesn't exist.'" Per HARNESS.md § An abstention is not a denial, that is a subject declining to guess above its own boundary rather than a subject discriminating a fake from a real release. What the rung does establish is the thing it exists for: nothing in this arm's version answers is confabulated, so its (c) answer can be read as a boundary.
context THE PREDICTION IS CONFIRMED ON THIS ARM AND THE SPEC CALLED THE WEAKNESS IN ADVANCE. Predicted: a boundary later than this subject's 2025-08-23 median across the seven measured libraries. Measured: 7.9.0, 2025-09-12 — later, by twenty days. prompts/react-router.md pre-registered exactly this outcome as a weak confirmation: "react-router published only three minors in the whole of 2025 H2 and nothing at all between 2025-09-12 and 2025-12-02 — an 81-day gap sitting directly on Opus 5's median. A boundary that lands at 7.9.0 is only 20 days above the median and should be reported as a weak confirmation, not a strong one." It landed on 7.9.0. Read against the two Fable 5.1 arms, which the same prediction FALSIFIED at the same release, the twenty days are better explained by the release gap than by the loud-library mechanism.

Open questions from this run

Sources

Battery specification: prompts/react-router.md in the studio repo. Every finding above also carries its own citation.