AI coding assistants write library code from memory. When a library ships a big release that renames or removes things, the assistant keeps writing the old version. It reads fine & it doesn't build.
SDKProof measures how often that happens. It gives a model real coding jobs for one library, then compiles every answer against the real installed package with tsc, the TypeScript compiler. A task passes only if it compiles — no AI judges another AI.
This page is React Router. Fifteen ordinary jobs — load data for a route, submit a form, read a search param, set the page title — one shot each, no docs, no retries.
14 of the 15 compiled. The miss is a rename: the meta function used to receive an argument called data, v8 calls it loaderData, and the model still writes data. Re-run three more times and it failed 3 of 3.
This measures the model, not React Router.
v8 renamed the data field on the meta arguments to loaderData, so it lines up with Route.ComponentProps. The rename touched MetaArgs, MetaMatch, UIMatch and Route.ComponentProps.matches together.
react-router 8.3.0
export const meta: MetaFunction<typeof loader> =
({ data }) => {
return [{ title: data?.user?.name ?? "User" }];
};error TS2339: Property 'data' does not exist on type 'MetaArgs<() => Promise<{ user: … }>, Record<string, unknown>>'.
react-router 8.3.0
export const meta: MetaFunction<typeof loader> =
({ loaderData }) => {
return [{ title: loaderData?.user?.name ?? "User" }];
};tsc --noEmit — 0 errors
Why this one slips past a human reviewer. data is the more natural word, and it sits inside a destructured argument where nothing looks unusual. You read it and see nothing wrong. The compiler doesn't read, it checks — TS2339 means the property isn't on the type.
v8 deleted a lot: the json() and defer() helpers, the whole react-router-dom package, and the plain-object route context. The model wrote none of them.
It returns bare objects from loaders instead of json(). For streaming it hands back an un-awaited promise instead of defer(). It wraps a value in data() when it needs to set a status code, calls createContext() without the old unstable_ prefix, and reads middleware state with context.get(userContext) rather than as a property.
93 on a release this new is the surprise here. The likely reason: v8 mostly finished removals that v7 had already been warning about for a long time, so the model had years of "stop using this" to learn from. The one thing it gets wrong is the one thing that was a straight rename with no warning period.
.agents/skills/react-router/references/framework-mode.md says: "Important: meta receives loaderData; do not use deprecated data args." That file isn't installed by npm i react-router, and reactrouter.com/llms.txt returns a 404. Of nine libraries surveyed, this is the only one publishing neither.
So I put the sentence in the prompt myself and re-ran the one failing task, ten times per row.
| What was in the prompt | Size | What it was about | Compiled |
|---|---|---|---|
| nothing — the score above | — | — | 0 / 10 |
| that one sentence, alone | 76 B | — | 10 / 10 |
| the Metadata section it sits in | 215 B | routing | 10 / 10 |
| 25 KB of Zod docs, then the sentence | 25 KB | unrelated | 8 / 10 |
| the file the sentence lives in | 7.3 KB | routing | 0 / 10 |
| all five files of the pack | 25 KB | routing | 0 / 10 |
Seventy-six characters fix it ten times out of ten. The identical sentence inside its own 7.3 KB file fixes it zero times out of ten. The model isn't refusing the instruction. It isn't reaching it.
And it isn't length. Twenty-five kilobytes of Zod documentation with the same sentence at the end still passed 8 of 10. It's the routing material around it — the other four files in the pack turned out to be irrelevant, the containing file alone is enough to smother the line.
The useful half is settled. A short standalone file naming the v8 renames would work, and it is far less effort than publishing everything. That is measured, not assumed.
Prisma shows the same shape: its adapter requirement passes 10 of 10 alone, and 5 of 10 inside its own 25 KB pack. Why the full pack fails is still an open question — the controlled comparisons are here.
Put plainly: line 182 does not survive the other 212 lines of the file it lives in.
This section was rewritten twice on 6 August 2026. It first said "publish the file", then — after the buried versions failed — said documentation does not help at all. Testing the sentence on its own showed both were wrong. The numbers above are the third and current answer.
Fifteen realistic React Router tasks, written by claude-opus-5, each dropped into a small project with react-router 8.3.0 actually installed, then run through tsc --noEmit. A task passes only if it compiles.
claude-opus-5. Another model will score differently.tsc, the TypeScript compiler, checks that the API exists and the types line up. It never runs the code, so nothing here says a route behaves properly.react-router-dom and asks you to import RouterProvider from react-router/dom. The model imported it from react-router, which still compiles because the root entry re-exports it. That task passed, and the stale habit it was written to catch is invisible to a type-checker.claude-opus-5 remembers of React Router, not about React Router itself.v9 renamed the main hook. The model wrote v8 every time.
Queries are clean. It still builds the client the v6 way.
One miss: revalidateTag() takes two arguments now.
Clean on everything it wrote. It would not write a third of the set.
Name any TypeScript package & I'll run it. Or do it yourself — it's all open source.
There's no npm package. Clone the repo, point it at a library, run it. The compiler is the judge.