111 — The invariant whose ladder began at its own boundary

2026-09-09. Data lane. BACKLOG 1g-e.

href was queued as a candidate invariant fact: a thing react-router has always done, published as the answer a developer on either side of LF1's boundary actually wants. It is not an invariant. It has two boundaries of its own, both below the floor of the ladder that was about to certify it as having none — and one of them is exactly that floor.

What the item asked for

JOURNAL/110 executed href("/books/:id.json", { id: "7" }) at 7.11.0, 7.14.2 and 8.3.1, got "/books/7.json" at all three, and recorded that href never had generatePath's suffixed-param problem. One arm of react-router/v1 had recommended href unaided; another had explicitly doubted it. BACKLOG 1g-e queued the measurement with the right warning attached: do not write it from the three rungs already measured — an invariant claimed over a window narrower than the ladder is a bound wearing an invariant's clothes.

So the row went on the ladder as kind: "invariant", with a guard row beside it (the ordinary whole-segment call) so a flat result could not be produced by an href that had stopped substituting anything at all.

The first run said INVARIANT_HOLDS, and it was wrong

Both rows came back true at all 21 rungs of the committed ladder, 7.9.0–8.3.1. The runner said INVARIANT_HOLDS. That is the verdict the fact would have been published on.

The ladder's floor is 7.9.0. href does not handle a suffixed param until 7.9.0. The row was flat because the ladder began at the release that made it flat.

Extended down the whole 7.x line — nine more minors, then the 7.1.x patches and 7.8.1/7.8.2 for adjacency, 37 rungs in all — both halves are false at the bottom and the run reports INVARIANT_BROKEN, false at 7.0.0.

The two edges, both bisected

rowfirst truefalse atwhat it dates
LF7 (suffixed param)7.9.07.8.2the behaviour change
LF7b (whole segment)7.2.07.1.5href itself arriving

Both edges are adjacent, so both are bisects and not bounds. The companion row is what stops 7.9.0 being misread: on its own, "false below 7.9.0" is equally consistent with href having arrived at 7.9.0, which would be a different fact entirely. It arrived at 7.2.0, seven minors lower, so 7.9.0 is a behaviour change to an API that already existed — which is what the fact now says, measured rather than assumed.

The changelog line makes two claims and both were split out and probed, per valibot's rule: "href() now correctly processes routes that have an extension after the parameter or are a single optional parameter". LF7c is the second half. Both go true at exactly 7.9.0.

The below-boundary behaviour is not generatePath's

This is the part a three-rung spot-check could not have reached, and it changes the correction rather than merely dating it. Below 7.9.0 href does not return the pattern unchanged the way generatePath does. It silently deletes the suffix:

href("/books/:id.json", { id: "7" })   ->  "/books/7"      at 7.2.0 .. 7.8.2
                                       ->  "/books/7.json" from 7.9.0

href("/:lang?", {})                    ->  ""              at 7.2.0 .. 7.8.2
                                       ->  "/"             from 7.9.0

An empty string in an href attribute resolves to the current URL, not to the site root. Two different silently-wrong answers, neither erroring, neither warning.

So the honest correction is a band, not a recommendation:

The two path builders were fixed four months apart on identical input: href at 7.9.0 (2025-09-12, PR #13797), generatePath at 7.12.0 (2026-01-07, PR #14269). LF1's note now carries the band.

The rule this pays for

AT_FLOOR exists because a dated row that is already true at the lowest rung has found a bound, not a boundary (JOURNAL/067, /076). A flat row has the same disease and no antibody. absent and invariant are scored on "true at every installed rung", which is a proposition about the ladder, and the runner has no way to say that the ladder is too short — every invariant that is really a bound reports as holding.

The precedent was already in the corpus and it was the expensive one: zod LF13 was published as introduced_in: 4.0.0 off a ladder that bottomed out at 4.0.0, and only an extension below the major showed the proposition was true down there too. That extension confirmed a flatness. This one refuted one, which is the direction that ships a wrong fact.

Standing rule (HARNESS): a flat row is a claim about the ladder until the ladder has been extended past where the claim could fail. Before publishing invariant or absent, add rungs below the floor — to the bottom of the major at least — and re-run. If the row stays flat, the range is evidence; if it breaks, the flatness was the floor. There is no verdict that can tell you this, so it is a step and not a check.

Two instruments reported their own failure, and one of them was mine

The first scout written this session guessed entry points (dist/development/index.mjs, then index.js) and failed to load both 8.x rungs — the exact guessing that tools/lib/rung.mjs's entryPoints() exists to replace, and which pinned zod's ladder to the 4.x line in JOURNAL/076. It was rewritten through the shared resolver. Separately, a sed invocation meant to patch a script's line endings silently matched nothing, and the edit was redone with the file-write tool. Both are BACKLOG 1g-g's class: an ad-hoc instrument that manufactures a negative. Neither reached the corpus, because neither produced a uniform negative — the failures were visible as failures. That is luck, not method, and 1g-g is still open.

The citation sweep earned its charter line in the same session

The charter says to run fact-citation-sweep.mjs in the session that corrects a fact. Creating one turns out to be the same trigger. react-router/fable-5-1-v1-a.json non_findings[3] — the record praising the draw that recommended href — carried introduced_in: null, and an allowlist entry excusing it on the grounds that href "has NO fact" and that the measurement behind it was "the opposite of a dated event." Both premises died this session.

The record now carries 7.9.0 and a correction annotation saying that its three named rungs all sit at or above the boundary, so "correct at every release tested" must not be read as "correct at every release". The verdict is unchanged — the draw's recommendation was right, and is right for any reader on 7.9.0 or above. The two dead allowlist entries are removed with the reason.

Note the shape: the allowlist is keyed on the record, so editing the record retired the excuse automatically (10 allowlisted → 8). What the tool does not do is report an allowlist entry that no longer matches anything — a stale excuse with a false reason sits in the file until someone reads it. Queued as 1g-h.

What did not move

No run, no finding, no charge. 158 runs / 165 findings / 158 chargeable / 3 withdrawn, S1 32 S2 85 S3 25 S4 23 — all unchanged, because bisecting and publishing a fact charges nothing. Facts 198 → 199; react-router 6 → 7, the first library fact added to it since onboarding.

LF7 charges nobody today and the sweep says so. 7.9.0 is Claude Opus 5's exact measured boundary and below Claude Fable 5.1's (7.10.0), so it is known for both; it is above Claude Haiku 4.5's boundary but above its stated cutoff too, and Claude Sonnet 5 has no measured boundary on this library. charge-windows.mjs prints the row as . ? o o. The fact is a correction rather than a charge, and its value is to a reader pinned below the boundary — which is exactly the half of the dataset that has no charging arm to find it.

All five bisected facts were re-run over the widened 37-rung ladder and every verdict is unchanged; measured_range widened from 7.9.0..8.3.1 / 21 rungs to 7.0.0..8.3.1 / 37 on all of them. 16/16 confirmed, nothing to review, no rung dropped and no control failure. react and react-dom resolved to 19.2.8 on all 37 rungs, so no row is the peer's boundary showing through a semver range.

Chain green: five --check surfaces, MCP 54/54, benchmark and flip-test selftests, citation sweep clean. pack-test and plugin-test not re-run — the library set, the files whitelist and the server's reads are all unchanged.

Not published, not listed, not sent; no money moved.

Operational: two sessions were live in this working tree at once

.session.lock did not exist at 02:02:14Z, so this session took the lock and worked. At 02:15 the staging step surfaced files this session never touched — tools/benchmark/draw.mjs, tools/build-index.mjs, tools/build-site.mjs, prompts/benchmark*.md and JOURNAL/112 — with mtimes of 02:10, 02:12 and 02:15Z, i.e. written while this session was running. The distribution lane was live in the same tree and had not written the lock, so the guard could not see it. git add -A had already staged the other lane's in-progress work; it was unstaged before anything was committed.

The two file sets turned out to be separable, and were separated by inspection rather than by luck:

This session committed only its own files, named explicitly, and left every file the other lane touched in the working tree for it to commit. Nothing of the other lane's work was committed, reverted or rebuilt away.

The guard is not sufficient and this is the ask for Sam. It is written to be taken at session start and released at session end, and it protects nothing against a session that does not take it — the failure is silent, and the first symptom is git add -A sweeping another lane's half-finished pre-registration into an unrelated commit. Two candidate fixes, neither taken unilaterally because both change the session protocol: have the lock be checked again immediately before staging (cheap, catches exactly this, does not prevent the overlap), or have each lane stage a named file list rather than -A (also cheap, and would have made this a non-event). Queued as BACKLOG 1g-j. No data was lost and nothing incorrect was published.