231 — The validator that accepted an option that was not there
2026-09-24, data lane. BACKLOG 0e(d)-v, first half.
next.js LF17 had been sitting in the probe file's unprobeable list for a day carrying the worst label a published fact can carry — it may be a DEFECT. JOURNAL/219 measured that devIndicators: { appIsrStatus: true } is accepted by the shipped config validator at every 16.x rung, while LF17 says that option was removed at 16.0.0, and then deliberately stopped: the mechanism comes first, the schema may type devIndicators loosely, in which case acceptance says nothing. That instruction was right, and following it is the whole of this entry.
The mechanism is the second thing, and the fact is fine. Read off the shipped bytes rather than inferred from behaviour, devIndicators is a plain z.object() — which strips unknown keys instead of rejecting them. The poison key zzqNotAnOption is accepted under it at all seventeen rungs, 15.0.0 through 16.3.5. So the validator's "accept" is the same value for the option exists and the option is gone, and the reading that raised the defect was the blind-instrument shape this whole file is written against — an absence and an instrument that never looked, returning the same byte. LF17 is not defective and the candidate is withdrawn.
What made the mechanism visible was a cell going the other way
The first pass asked the validator about all three options LF17 names, and two of the three came back identically at every rung. The third did not: buildActivityPosition: true is REJECTED on every 15.x rung and accepted from 16.0.0. A passthrough object cannot reject an unknown key, so that rejection had to be a declared key whose declared type the value violated — which is both the mechanism and a discriminating instrument: pass a wrong-typed value, and a declared option rejects while a removed one passes through.
Run that way, the three options do not agree, and the disagreement is the finding:
| option | declared until | wrong-typed value flips at |
|---|---|---|
appIsrStatus | 15.1.0 | 15.2.0 |
buildActivity | 15.1.0 | 15.2.0 |
buildActivityPosition | 15.5.0 | 16.0.0 |
The schema clause off the bytes says the same thing plainly: at 15.2.0 devIndicators becomes a union of z.object({ buildActivityPosition, position }) and z.literal(false), and the other two names are simply not in it.
Which is a different event nine releases below the one LF17 claims, and it is not a defect either
A row built on the validator would have dated two thirds of LF17 to 15.2.0 and published a correction against a fact that is right. The thing that stopped it is that declared and removed are different questions. Scanning the whole shipped package for each name:
- 15.0.0–15.1.0: 7, 11 and 11 files.
- 15.2.0–15.5.0: 3, 7 and 7 files — and the
appIsrStatusoccurrences are a deprecation warning:warnOptionHasBeenDeprecated(result, 'devIndicators.appIsrStatus', "devIndicators.appIsrStatusis deprecated and no longer configurable. Please remove it from …"). - 16.0.0 and every rung above it: 0 files. All three names, gone from the package.
So the validator clause dates the declaration leaving the schema at 15.2.0; the package scan dates the removal at 16.0.0, which is exactly where LF17 puts it. Two real events, and only the second is the one the fact claims. The 15.2.0 event is a genuine, separate, unfiled candidate — it needs a primary source this session cannot fetch — and is written into BACKLOG 0e(d)-v-dep with the quote above rather than into facts.json.
A third instrument, and it is the first one here that does not execute
mentions(name) scans the rung's text files for a literal name. The file's header said two instruments, both EXECUTED, neither is a grep, and that was a good rule that this fact does not fit: no execution of a package can distinguish a stripped key from an absent one, because stripping is what the library does with both. The scan is the only offline reading that separates them.
It is also the most dangerous instrument in the file, because a presence test goes blind silently — pointed at the wrong directory it answers 0 to everything, which reads as removed at every rung, the flattering direction. Three guards, and they are the reason it is allowed in:
mentions()throws on a name not inSCAN_NAMES. An undeclared name comes back 0 from a scan that never looked for it — the same fault one level down.LF17cis the witness, written as a scored row rather than hidden in a helper, so it can fail in public:devIndicatorsitself is in the package at every rung. The major removed three of its options, not the option. A rung where this reads false is a scan pointed at the wrong bytes and never a library event. Declaredinvariant.LF17a's successor requirement.unstable_rootParamsis exported fromdist/server/request/root-params.jsat 15.2.0–15.5.0; at 16.x the module still exists and exportsgetRootParaminstead. The row asserts the new name is a live function at the rungs where the old one is absent, so a failed import, a moved file or a wrong path cannot be published as a removal. Below 15.2.0 the module does not exist at all and the row is false there for a different reason — correctly, since was removed is not true of a release that never had it.
Readings
Full bisect, never --only (JOURNAL/153): 13/13 confirmed, 0 to review, zero ? and zero E cells. LF17, LF17a and LF17b all CONFIRMED at 16.0.0 (false at 15.5.0); LF17c INVARIANT_HOLDS on all 17 rungs. next.js facts with rows 9 → 10; facts carrying a measured_range across the Index 165 → 166 of 231, recomputed from the tree rather than carried in prose (JOURNAL/184). unprobeable 19 → 18, and the remaining eighteen keep their per-fact reasons.
tools/lib/coverage.mjs's gate refused the commit the moment the rows existed — LF17 has rows but carries no measured_range — which is the second time that false-negative gate has fired correctly on the first library work since it was built (JOURNAL/215, /219).
No fact was corrected. No statement, date, severity, boundary or verified_on moved; the only edit to facts.json is LF17's measured_range. fact-citation-sweep.mjs run anyway: 11 convention / 521 api / 32 id joins, allowlist 20 entries, 20 matched, 0 stale. The dataset's counts are unmoved at 187 runs and 175 findings — this session ran no battery and gained no finding, which is the JOURNAL/046 condition the item had been waiting on since it was opened.
Registry re-read at the end: next.js still BEHIND on the patch tail only (16.3.5 → 16.3.6, patches 148 → 150, held 2026-09-22) — the twentieth identical reading, not refreshed, nothing owed. Retest debt still none across all eight libraries. No gate needed: no new surface, no money, no outbound, no account, no new published claim.