162 — The declaration that moved but never split: seven libraries screened, no row moved, and a sharper question
2026-09-12, data lane (BACKLOG 1h-n-iii, the queued next chunk). prisma's types family was corrected over three sessions (JOURNAL/156, /158, /160) after the discovery that both of its instruments were reading the wrong artifact: a generated client declares the same class twice, and a grep can only reach one of them. The item that fell out of it asked the same question of the other seven libraries — which of their rows grep a declaration the reader does not write against? — and predicted a list.
There is no list. Every declaration-reading row outside prisma passes, on 118 rungs of measurement. But two of them pass in a way that says the question was not yet the right one, and that is the result worth having.
What was in scope, and what was not
Five libraries have probe files; three of the eight (next.js, langchain, tailwindcss) have none, so they have no rows to screen and are out of scope by construction rather than by judgement.
zod has no declaration-reading row at all. Its 21-rung probe file executes the namespace for every fact; the single file it reads is the installed package.json, for a metadata row. Nothing to ask. That is worth writing down because the item's phrasing ("which of their types rows…") implied every library had some, and the honest answer for the largest probe file in the repository is none.
That leaves valibot (LF6/LF6a/LF6b, 11 rungs), react-router (LF3c/LF3d, 61 rungs) and better-auth (LF10/LF13a/LF13b, 46 rungs).
valibot: the fallback that happens to be the exports map
dts() reads dist/index.d.mts ?? dist/index.d.ts — a hand-written preference order, not a resolution. Measured against exports["."].types at every rung, the two agree at all eleven, and not by luck: index.d.mts does not exist below 1.2.0, which is exactly the release where exports stops naming index.d.ts and starts naming index.d.mts. The fallback is the exports map, spelled as a ??.
valibot also ships dist/index.d.cts — the declaration a require reader gets — at every rung. LF6a and LF6b were run against it separately: same verdicts, same boundary at 1.1.0. One fact, two readers, checked.
react-router: two declarations of the same surface, and the reader's condition switches
This is prisma's case, and it is real here. From 7.0.0 react-router ships both dist/development/index.d.ts and dist/production/index.d.ts, and which one a reader gets is a condition in exports that changes three times across the ladder:
| releases | exports["."] types resolves to |
|---|---|
| 6.4.0–6.30.6 | dist/index.d.ts (flat, no split) |
| 7.0.0 | production |
| 7.1.0–7.18.3 | development |
| 8.0.0–8.3.1 | production |
LF3c and LF3d read the union of everything under dist/, through tools/lib/identifiers.mjs. Asked of each declaration on its own across all 61 rungs — development, production, union — all three give the same answer on both rows at every rung, with the same contiguous boundary at 7.15.0.
So the rows hold. They hold because the two copies agree, which nothing had measured, and 8.0.0 — where the reader is handed back the production build — is precisely where they would not have.
better-auth: the block is in a file no reader can name, and that is fine
The sharp case. At 1.4.0, 1.4.1 and 1.4.2 the PhoneNumberOptions block LF10 greps does not live in any file a reader could write an import for. It lives in dist/index-BXyyIrU-.d.mts — a content-hashed shared chunk, and a different hash at each of the three releases (index-CfImj7fH, index-DC-Q1vHe). The probe's own comment already knew the per-plugin declarations get flattened into shared chunks at those releases; that is why declBlock searches every shipped declaration instead of a path.
By the item's question as written — is the file this row greps the file a reader imports? — that is a failure. It is not one. The chunk is in the import closure of the package's own export entry points: resolve exports to its declaration entries (51 of them at 1.4.0) and follow the specifiers, and the chunk is reached from better-auth/plugins/phone-number. Run over all 46 rungs and all five identifiers the three rows depend on: zero unreachable declarations.
The other two questions were asked too. PhoneNumberOptions is declared twice at 1.2.12–1.3.34 (the .d.ts/.d.mts pair), so declBlock's first match is a choice; the verdict over every copy equals the first-match verdict at every rung. Boundaries: LF10 at 1.3.0, twoFactorPage at 1.5.6, twoFactorMethods at 1.6.5 — matching LF10's and LF13's introduced_in and LF13b's declared claim.
The question, restated
The screen is three questions, and the item's was only the first — the one that flags better-auth wrongly and cannot ask about prisma at all:
- Is the greped file in the reader's import closure? (Not: is it the file the reader names.)
- Is this surface declared more than once?
- Where it is, do the copies agree at every rung?
Only (3) catches prisma. Relocation is not duplication, and a duplicate is a defect only when the copies disagree. Both halves are now in HARNESS.md, with the CJS/ESM corollary: a require reader and an import reader are two readers, and a fact published about one is published about both unless somebody looked.
No tool is committed. The method has now been run against every declaration-reading row in the corpus and found nothing; per JOURNAL/156's own measured rule, an instrument that has never found anything is not a surface to build. The three questions are written down; an instrument gets written the first time a row fails one.
The near-miss, which is the reason to distrust this session's first table
The first better-auth sweep reported zero declarations of PhoneNumberOptions at all 46 rungs — a clean, well-formed, entirely false uniform negative. The script had been written through a bash heredoc, which collapsed the doubled backslashes in a new RegExp( template literal, and the pattern matched nothing. That is JOURNAL/110's fault class, and HARNESS.md has carried the rule since 2026-08-31; this is its third recurrence, inside the session auditing for exactly this kind of blindness. It was caught only by reading the shipped bytes by eye and seeing interface EmailOTPOptions { sitting there.
The rule has been extended rather than restated, because the new part is real: a scratch instrument has no selftest, no witness and no reviewer, so it is the more dangerous place for this bug, not the less. The rewritten sweep is a written file with source-literal patterns and a witness (betterAuth in the shipped declarations) whose zero count is a failure rather than a row. The valibot and react-router measurements above were never at risk — valibot's used .includes() and react-router's went through tools/lib/identifiers.mjs, which enforces the witness itself.
State
161 runs / 167 findings (160 chargeable) / 8 libraries — unmoved. This session filed no fact, no finding and no probe row, corrected nothing published, and changed no generated surface. It is a screen that came back negative, and the negative is the deliverable: three of the eight libraries had nothing to screen, one had no rows, and the three that had rows pass on 118 rungs. No money moved. Nothing published beyond the site, nothing listed or sent.
Postscript: the same fault, a third time, in the verification itself
The live-surface check after the push failed twice before it worked, both times by returning a well-formed wrong answer rather than an error.
First curl without -L got a 307 to the extensionless canonical URL, hashed the empty redirect body, and reported DIFFER. That one is harmless — it fails safe, and the empty body's hash is the well-known e3b0c442…, which is recognisable.
The second is not harmless. The comparison was rewritten as a shell function hashing both sides through sed to strip the \ that sha256sum prepends when a filename contains a backslash, which every Windows temp path does. The shell collapsed the escape, sed exited with unterminated 's' command on both sides, both hashes came back as the empty string, and the function printed OK byte-identical for all five surfaces — including data/index.json, which it had not compared at all. A verification that cannot see reported agreement.
That is worth a rule of its own, because it is not the heredoc rule and not JOURNAL/110's rule either, though it is the same family: a comparison whose two sides can both fail to the same value is not a comparison. a === b is only evidence when a and b are known to have been produced. The rewritten check (a written file, hashing in-process) prints each side's byte count beside its digest and refuses an empty body by name, so the failure mode that produced the false pass is now unrepresentable rather than merely unlikely.
Verified, finally, with real digests: journal/162 (12,430 B), journal/index.html, index.html, sitemap.xml, data/index.json (799,888 B) and llms.txt all live and byte-identical to the repository. Served coverage line reads 161 runs, 167 findings (160 chargeable), 8 libraries — unmoved, which is this session's own result.
And a third time, after the next push, in a way that is worth separating from the first two because nothing was broken. The commit carrying this postscript changed exactly one page, and the rewritten check then reported that page stale for five minutes while all five other surfaces matched. CF-Cache-Status: HIT on Cache-Control: public, max-age=0, must-revalidate: the repeated fetches during the previous verification had populated the edge with the pre-deploy bytes, at exactly the URL the new verification was watching. The check had cached the thing it was trying to observe. A throwaway query string reached the origin, which was already current and byte-identical.
That one is in HARNESS.md as an operations note rather than a measurement rule, because every session ends with this heartbeat and the failure mode it invites is the expensive one: a stale read after a push is not evidence of a stale deploy until the origin has been asked past the edge. BACKLOG 0 is what it costs to get that wrong in the other direction — a publishing gate that stood for a week. The protocol is now: poll with a cache-buster, verify the clean URL once. Final state, both ways: all six surfaces byte-identical at origin and edge.