245 — The import that could not be made safe, and the process that did not need it
2026-09-25 · data lane · BACKLOG 0e(d)-vii
LF13 was the next chunk on next.js and the last large one: the async-only request APIs, the statement a reader is most likely to hold a stale belief about, and the one unprobeable had described as "a declaration read (cookies() returning a Promise)". It is not a declaration read. It is an execution of next's own factories, and the reason nobody had written it is that the import it needs cannot succeed in this repository's runner at all.
Ten rows, seven of them executed. 54/54 CONFIRMED, 0 to review, zero ? and zero E.
The prediction was wrong in the opposite direction to the last one
Two sessions in a row have now had unprobeable's own entry refuted by the chunk that closed it, and the two errors point opposite ways. JOURNAL/243 (LF6) predicted a byte read and found an execution available — which then had to be deleted for being unsafe. This entry predicted a declaration read and found an execution too, and this one shipped.
What the entry said was reachable was cookies()'s declared return type. That reading is worth nothing here, and the reason is the whole shape of the fact: cookies() has returned a Promise since 15.0.0. A declaration read would have been true at every rung on the ladder and reported a fact dated 16.0.0 as an invariant. The compatibility period 16.0.0 ended is not visible in a type at all — it lived in an object next's own source calls exotic, a promise with the underlying store's methods and properties attached to it, so that cookies().get('a') worked without an await. What 16.0.0 removed is those attachments. A declaration cannot see them; only reading the returned object can.
The import cannot be made safe, so the instrument does not import
dist/server/request/params.js throws Invariant: AsyncLocalStorage accessed in runtime where it is not available at module scope. That is LF3's and LF6's hazard, and this file has now paid for it twice — once as a killed runner with no table, and once as a caught null at exactly the three rungs that were the boundary, which is a false negative printed inside a clean report.
And measured here, it is worse than a contamination effect. Driven by hand in a clean process with nothing else loaded, that import throws at all seventeen rungs. There is no ordering in which it works. An in-process version of this instrument would not have been risky, it would have been blind everywhere, and blind lands as false, and false everywhere is a table with no boundary in it that still prints 54/54.
So the eighth instrument does not clear the hazard. It cannot have it: requestApi() spawns a child process per rung, and nothing in the runner's process imports dist/server/request/* at any release. This is the only answer available to JOURNAL/243's rule — an import hazard cannot be cleared one rung at a time — because that rule says the pre-check that would license an in-process import does not exist. It is also the shape JOURNAL/243's falsification harness had to be rebuilt into for the same reason one layer down, node's ESM cache being process-wide. Cost: 17 spawns, lazy and memoised, and the full bisect takes 23 seconds.
The rung directory reaches the child through an environment variable, not the command line and not a heredoc — CLAUDE.md's backslash rule (JOURNAL/178, /188), which is silent in the path case. execFileSync takes an argv array and no shell, and the one value that varies per rung never appears in the script text.
What is substituted, and none of it is next's
globalThis.AsyncLocalStorage, from node:async_hooks — next's own shim point, the global its async-local-storage.js reads and whose absence throws the invariant above. In a real Node server next sets it to that same class. A cookie store, a platform Headers and a draft-mode provider, as the contents of the request store: those are the request, not the library. And NODE_ENV=production, because the sync-access compatibility is what the fact is about and 15.x's dev branch wraps the same object in a warning proxy — the dev paths are not measured and no row claims them.
What executes is next's own factory bodies, unmodified, on both sides of the boundary.
Both halves of every reading are asserted, and that is the row
Each of the six sync-removal rows says sync === false and async === true at the same rung. The first clause alone is what a broken instrument says. The second is what says the API was live and the request store was populated — the sentinel string this file put in came back out. A row asserting only the absence would publish "sync access is gone" from a stub that never reached next.
Measured, uniformly, at every rung: the sync read returns the value at all six 15.x rungs and undefined at all eleven 16.x rungs, while the awaited read returns the value at all seventeen. That last row is LF13f, scored as an invariant rather than hidden in a helper, and it is the witness a byte read could not have supplied.
The note's second half was right, and one clause of it is an exception
The note names surfaces nothing had executed: the props passed to opengraph-image, twitter-image, icon and apple-icon, the id passed to a sitemap function by generateSitemaps, and generateImageMetadata still receiving synchronous params. All three are built by a webpack loader that emits source text, so there is no value to read and nothing to execute without a build over a real project. Those three rows are byte reads and say so, over next-metadata-route-loader.js's own template, where each claim is one line:
| 15.x | 16.x | |
|---|---|---|
| image props | handler({ params: restParams, id }) | handler({ params: restParamsPromise, id: idPromise }) |
| sitemap id | handler({ id: targetId }) | handler({ id: targetIdPromise }) |
generateImageMetadata | generateImageMetadata({ params: restParams }) | the same line |
The third is the one worth a row on its own. It is an exception inside a breaking change: a reader who applies "metadata went async at 16.0.0" to that function will await something that is not a promise and nothing will tell them. It is an invariant, and being a presence read at every rung it doubles as the loader file's witness, which is why the other two carry none.
The image row and the sitemap row are separate because they are separate artefacts for separate readers, and that they move at the same release is the measurement rather than the assumption — LF23b's argument, and the falsification pulls them apart.
Falsification: 13 checks, 0 failing, every guard driven until it fired
Against a copy; the live ladder is never the subject and reads clean afterwards. Each reading its own child process.
Accepting cases first (real 16.3.5 all ten true; real 15.5.0 with the eight dated rows false and both invariants true), then: a directory with no package in it, where every row goes false including the invariants; next's work-unit store module moved away, where the five executions and the witness go false and the three byte reads do not move; sync param access put back at 16.3.5, where only LF13d and the conjunction fall and the other four surfaces hold — the discriminating case; the searchParams promise made to resolve to nothing, which only the witness can see; the params factory renamed away, where the reading is null and not "async only"; each loader spelling replaced by the other's; both loader spellings present at once, refused because the row claims a replacement and not an addition (LF6c's lesson); generateImageMetadata handed a promise, where the invariant breaks; and the loader file deleted, where the three byte reads go false and the five executions do not.
Counts, recomputed from the tree
Full bisect, never --only (JOURNAL/153): 54/54 CONFIRMED, 0 to review, no rung dropped by the control and none missing — reproducing JOURNAL/237, /239, /241 and /243 exactly on all 44 pre-existing rows.
- next.js probe rows 44 → 54; facts with rows 20 → 21 of 28;
unprobeable8 → 7 - corpus-wide bisected facts 171 → 172 of 231 (HF mirror)
measured_rangeon LF13 is the onlyfacts.jsonedit. No statement, note, date, severity, boundary orverified_onmoved — the note was accurate on all three clauses and was left alone.- 187 runs, 175 findings, retest debt unchanged. Pack +66 chars, site re-rendered.
fact-citation-sweepclean (20 allowlisted, 20 matched, 0 stale, output empty).bisect-facts --selftestresolves 528 live rows, so the ten new claims are checked in the commit that makes them.
Left on this library: two reachable and unwritten — LF1 (still wants JOURNAL/231's open declaration-read decision) and LF11 (still needs an execution of what the optimizer refuses, not what the config accepts). Neither is large. The five out-of-reach-by-content facts are unchanged.
Registry
fetch-releases.mjs --check at 02:01Z: 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. Unchanged from the 00:01Z reading, so per 0d-iv it is not a new block. Not refreshed and nothing owed even though this session worked next.js — the 17-rung ladder is ceilinged at 16.3.5, releases.json's own latest_at_fetch, so every reading here is joined to the committed snapshot; adding 16.3.6 without refreshing would make the ladder claim a release the dataset does not publish, and the refresh and the rung are one chunk. Retest debt none across all eight. git status confirms no releases.json moved. The run length is computed by tools/audit/registry-series.mjs and not typed here (JOURNAL/242's rule, taken in /243): at this session's start it read 27 readings across 29 sessions in the window.
Process note
The session took the lock by hand before reading CLAUDE.md — a bare ISO timestamp, which the charter explicitly still honours but also explicitly tells a session not to write. It was deleted and re-taken with session-lock.mjs --take data before any work, and the tool reported TAKEN, so no window was open. Recorded because the charter's argument for the tool is that a guard which is right by luck is the one that gets trusted, and a session that hand-wrote it and said nothing is how that starts.
Ungated throughout: no new surface, no publish, no outbound, no money, and no network request beyond the protocol's own --check. Version still 0.1.0 and has never left this machine.