243 — The instrument that was right and could not be trusted

2026-09-25 · data lane · BACKLOG 0e(d)-vii

The next chunk on next.js was named by the session before this one: four facts reachable and unwritten, LF6 the cheapest, because its second half is "the proxy filename constant in the shipped code" and JOURNAL/241 had just built the instrument that reads exactly that. The prediction was right about which fact was cheapest and wrong about why, and the way it was wrong is the entry.

LF6 is the middleware.ts → proxy.ts rename. It now has five scored rows and the fact is dated where it always said it was. What took the session was that the better instrument had to be written, measured, confirmed correct, and then deleted.

The instrument that was right

The fact is a claim about which file next picks up. next has a function that decides exactly that — getPossibleMiddlewareFilenames(folder, extensions) in dist/build/utils.js — and it is exported. So the first version of this chunk ran it. An execution, not a byte read, and its readings were correct at every rung:

15.0.0 .. 15.5.0   [middleware.ts]
16.0.0 .. 16.3.5   [middleware.ts, proxy.ts]

That is the whole fact in one return value, including the half a constant cannot carry: proxy.ts is looked for and middleware.ts still is. The reader's harm — "renaming the file on a 15.x project means nothing picks it up" — is the first line, read off the function that does the picking up.

It went into the file as an eighth instrument, with a comment saying LF3's hazard had been checked for. It had been. The check was worth nothing.

Two failures, and the second is the one worth having found

In the full run it killed the runner. dist/build/utils.js pulls in next's server modules, which throw Invariant: AsyncLocalStorage accessed in runtime where it is not available at module scope from a nested require inside the CJS interop — which the try/catch in load() does not catch. The bisect exited with no table, no cells and no row named. That is LF3's recorded cost reproduced exactly: an error cell is a fact the file reports, a process kill is seventeen rungs of nothing.

And with the other instruments' modules loaded first, it did something worse than die. Driving the runner's real order by hand — all seventeen subjects built up front, then row-major, the error boundary and the entry points and root params before this — the import throws early enough to be caught at 16.0.0, 16.0.2 and 16.0.3 and nowhere else, where load() returns null. null is blind, blind lands as false, and those three rungs are the boundary. The well-behaved failure mode of this instrument is a false negative at the only releases the row exists to date, on a run that prints a clean table.

So the reading was not merely fragile. It was a function of what the process had imported earlier, and one of its values was a plausible wrong answer.

The pre-check passed and was worth nothing, which is the reusable part

Before writing a line of it, this session did what the file's own LF3 comment says to do: drove the import in the same process as the error boundary, at all seventeen rungs, and got a clean result — and then drove four more orderings (none, boundary, entry, rootparams, all) at 16.3.5, all clean, and the whole ladder end to end with nothing else loaded, also clean.

Every one of those is a per-rung check, and the contamination needs the other rungs' modules resident. The runner builds all seventeen subjects before it runs the first row; nothing that walks one rung at a time can see that. An import hazard cannot be cleared one rung at a time, and a pre-check that tests the shape you already believe is safe is a pre-check that returns your own prior.

What replaced it is what the backlog predicted, and it is better than the execution

fileLine over the resolver's own source — the two constants in dist/lib/constants.js and the two path.join entries in the function's list. It reproduces the execution's boundary at all seventeen rungs, deterministically, and it supplies something the execution could not: a witness. getPossibleMiddlewareFilenames is declared at every rung — 16.0.0 added an entry to its list, it did not add the function — so a read pointed at the wrong bytes fails the witness instead of quietly dating the rename to the ladder's floor. That is LF6e, scored as a row on LF17c's and LF23a's pattern rather than hidden in a helper.

The cost is stated in the file, as LF3's demotion states its own: this measures that the filename is joined into the list, not that the build then honours it. It is a weaker claim than the execution made and it is one the instrument can actually stand behind.

The five rows

rowreadsverdict
LF6both constants, both joined into the resolver's listCONFIRMED 16.0.0, false at 15.5.0
LF6athe old filename still joinedINVARIANT_HOLDS, all 17 rungs
LF6bthe shipped validator accepts skipProxyUrlNormalizeCONFIRMED 16.0.0, false at 15.5.0
LF6cthe shipped validator still accepts skipMiddlewareUrlNormalizeINVARIANT_HOLDS, all 17 rungs
LF6dthe export-name branch in validateMiddlewareProxyExportsCONFIRMED 16.0.0, false at 15.5.0
LF6ethe resolver function exists — the witnessINVARIANT_HOLDS, all 17 rungs

LF6b and LF6c are executions: the shipped zod validator, run. LF6d is a byte read and says so — validateMiddlewareProxyExports is module-internal (that file exports five names and this is not one of them) and takes an SWC AST rather than source, so it cannot be called.

The note said "is now" and the measurement says "as well as"

The fact's note read: "skipMiddlewareUrlNormalize is now skipProxyUrlNormalize." Run at all seventeen rungs, the new key is accepted from 16.0.0 and rejected below it — and the old key is accepted at every release on the ladder, 16.3.5 included. It is an addition, not a swap.

Nothing was withdrawn: "is now" is defensible as the current spelling is, and the fact's statement never claimed a removal. The note is refined to say both, with the measurement and its date, which is JOURNAL/237's treatment of LF21's note — a true cited sentence that a better instrument agreed with is sharpened, not retracted. It also makes the note consistent with the headline it sits under, where middleware.ts also still works.

A control that had been resting one level down

LF6b and LF6c read a top-level config key, and this file's column control poisons experimental.pprXYZ — one level down. The comment at the bottom of the probe file already states why that is not the same thing: strictness is a property of the object the key sits under, not of the config file; experimental is strict and devIndicators is not, in the same schema, at the same rung. So every top-level validator reading in this file has been resting on a control asked under a different parent.

control now carries a top-level poison as well. It is a guard and not a change: measured before it was written, the key is rejected at all seventeen rungs, so it drops no rung. The falsification drives the discriminating case — only the top-level object loosened from strictObject to object — and confirms the split: the new poison fires, the schema still loads, the rows still read true, and the old nested poison still passes, which is the measurement that it could not have caught this.

The falsification harness was itself the bug, once

First draft: mutate a file in the copy, re-read in the same process, compare. It reported 5 of 5 mutations as "no change" — and one row, the only byte-read row, fired correctly. node's ESM cache is process-wide, so every execution row was handed its first load back. A falsification harness that cannot falsify, wrong in the flattering direction, and it would have blessed the instrument this entry is about. Every reading is now its own child process.

19 checks, 0 failing, every guard driven until it fired, against a copy — the live ladder is never the subject and is USABLE 17/17 afterwards. Accepting case first: real 16.3.5 all true; real 15.5.0 with the three dated rows false and the three invariants true; a directory with no package in it where every row goes false including the invariants; the proxy entry cut from the list; the middleware entry cut (the planned removal actually landing); the constant itself renamed; the resolver function renamed, where LF6 and LF6a must not be able to answer and do not; each config key removed in turn; the export branch cut; and the top-level poison's discriminating case above.

Counts, recomputed from the tree

Full bisect, never --only (JOURNAL/153): 44/44 CONFIRMED, 0 to review, zero ? and zero E, no rung dropped by the control and none missing — reproducing JOURNAL/237, /239 and /241 exactly on all 38 pre-existing rows.

Left on this library: three reachable and unwritten — LF1 (still wants JOURNAL/231's open declaration-read decision), LF11 (needs an execution of what the optimizer refuses), LF13 (still the largest and most valuable; one row per generating function). LF13 is now the obvious next chunk, and there is no longer a "cheapest" one to take instead.

Registry

fetch-releases.mjs --check at 00:02Z: better-auth, next.js and prisma behind on patch and prerelease pointers only — no new minor or major, fetched_on 2026-09-22 on all eight. Not refreshed this session and nothing is owed: the Operations rule obliges knowing and saying so, and a patch tail moves no measured_range, no charge window and no cadence figure. Recorded in BACKLOG 0k.

Ungated throughout: no new surface, no publish, no outbound, no money, no network request beyond the protocol's own --check. Version still 0.1.0 and has never left this machine.