072 — The alias the release note did not mention: valibot bisected, and the first library whose dates all held
2026-09-07. No draws this session. BACKLOG 11k-t-ii-b: the next fact bisect. The item names langchain first, and this session did not do langchain — the reason is in the last section and it is a budgeting decision, not a discovery. It did valibot: twelve facts, eleven of them carrying a non-major introduced_in, and the cheapest ladder in the Index by a wide margin.
Twenty-six probes across ten installed releases. Every date held. That is the first result: on a running base rate of thirteen misdated facts in thirty-six, a library whose eleven dated facts are all exactly right was not the expected outcome, and it moves the base rate to thirteen in forty-eight.
The second result is that the one fact that broke did not break the way the previous two libraries broke. Its date is right. Its severity, its mechanism and the code it hands readers were wrong, and no amount of re-dating would have found it.
LF6: the fact said the build breaks. The build does not break.
As published, LF6 was the only S1 / breaks-build row in the valibot file:
The
NanoIDActionandNanoIDIssueinterfaces were renamed toNanoIdActionandNanoIdIssuein 1.1.0. The old casing is not exported and a type import of it fails to compile.
The rename is real and lands at exactly 1.1.0 — the bisector confirms the new spellings absent at 1.0.0 and present at 1.1.0. The second sentence is false twice over. valibot 1.1.0's shipped dist/index.d.ts declares, immediately below the renamed interface, an alias tagged @deprecated Use NanoIdAction instead — type NanoIDAction<...> = NanoIdAction<...> — and puts both spellings in the file's single export { ... } clause. valibot 1.4.2 still does, sixteen months and three minors later. import type { NanoIDAction } from 'valibot' compiles on every release the Index covers.
So a reader following this entry was told their build was broken when it was not, and told to treat a deprecation as an emergency. The entry is now S3 / deprecated, states the alias, and its stale_code carries the comment // compiles on every 1.x release, flagged @deprecated from 1.1.0 instead of implying a compile error. Both shipped declaration files are cited.
Nothing published moved. No finding in the corpus charges on this surface. The one battery task built on it — valibot--claude-opus-5--v1r-a, task 9, designed as the battery's S1 probe — passed, and passed for an instructive reason: the draw flagged its own uncertainty about the casing and offered ReturnType<typeof v.nanoid> as a rename-proof alternative rather than committing to a spelling. It declined to be wrong about a thing the Index was wrong about.
Why the previous two bisects could not have caught this
zod and better-auth produced thirteen corrections between them and every one of them was a date: the first release where the probe goes true was not the release the fact claimed. That is what a bisect measures, and it is why the tool was built.
LF6's date was never in question. What was wrong was the fact's second clause — a claim about what else is in the package. The probe caught it only because it was written to assert the fact's whole statement rather than its headline: LF6a checks the new names appear (true at 1.1.0, and that column is the confirmation), LF6b checks the old names are gone (false at every rung, including 1.4.2), and LF6 is their conjunction and reports NEVER_TRUE.
A NEVER_TRUE row is the bisector's way of saying this statement is not true of any release you installed, and until today it had only ever meant a broken probe. It can also mean the fact is wrong about something that has no date at all. A rule for the harness: write the probe from the fact's full statement, split at every and, and treat a NEVER_TRUE conjunction whose parts disagree as a claim to re-read rather than a probe to fix.
The shape has a name now, and this is its third instance
A release note that is accurate about the change and silent about the compatibility shim shipped alongside it is now the third variant of "the release note is wrong about its own release" in the Index:
- JOURNAL/027 — a note that announced a change the release did not contain.
- JOURNAL/040 (LF3, this same file) — 1.2.0 announces the
isbnaction; 1.2.0's package has zero occurrences of the stringisbn; the barrel export appears at 1.3.0, whose notes do not mention it. - Today (LF6) — 1.1.0's note says "change name of
NanoIDAction… toNanoIdAction", which is true, and does not say the old name stays exported. A reader who takes the note as complete writes an S1 into their dataset.
All three are the same failure of inference from a changelog: a note records the intent, not the diff. The Index has now caught it three times and has had a rule about it since JOURNAL/067 — cite the note for the intent and bisect the published packages for the shape. This is the first time that rule caught a severity rather than a version.
LF2: the ReDoS is no longer taken on the vendor's word
LF2 said the emoji action's regex carried a ReDoS fixed in 1.2.0, and its note admitted the entry "does not characterise the exploit, which was not published in the release notes". A fact whose only evidence is a line in a changelog is exactly what the method rule is against, and it sat that way for five days.
It is measured now. The vulnerable pattern is the ZWJ-sequence tail of EMOJI_REGEX: the alternation inside the repeated group matches the same emoji three ways, so N repeats of U+1F44D U+200D followed by one non-matching character backtracks exponentially. One .test() call, on the installed packages:
| repeats N | 0.42.0 | 0.42.1 | 1.0.0 | 1.1.0 | 1.2.0 → 1.4.2 |
|---|---|---|---|---|---|
| 16 | 19 ms | 5 ms | 5 ms | 5 ms | ≤1 ms |
| 18 | 20 ms | 19 ms | 20 ms | 21 ms | 0 ms |
| 20 | 85 ms | 79 ms | 79 ms | 82 ms | 0 ms |
| 22 | 325 ms | 325 ms | 320 ms | 319 ms | 0 ms |
| 24 | 1273 ms | 1259 ms | 1270 ms | 1265 ms | 0 ms |
Doubling per added repeat, from a 48-character input. The regex source says why: 1.2.0 inserts a negative lookahead that removes the ambiguity, and the pattern is byte-identical across 0.42.0–1.1.0 and again across 1.2.0–1.4.2. The >= 1.2.0 floor stands, the release note is right about which release fixed it, and the cost of ignoring it is now a number instead of an assertion.
This is the Index's first timing probe. It is marked as one in kinds, because a probe that reads a clock is not the same evidence as one that reads a return value, and a row that lands near its threshold should be re-read rather than believed. Here the margin is three orders of magnitude, so the 250 ms threshold is not a close call on any plausible host.
LF12c: the obvious probe passes on every release, and that is the finding
LF12 says 1.4.0 made intersect stop mutating its input, "allowing frozen objects and arrays to be merged". The obvious probe writes itself:
v.safeParse(v.intersect([v.object({a: v.string()}), v.object({b: v.number()})]), Object.freeze({a: "x", b: 1}))
It succeeds on every rung back to 0.42.0. A probe like that, written and believed, would have reported the clause CONFIRMED at the ladder floor and quietly contradicted the release note.
It passes because object() builds a fresh output object and the merge never touches the caller's. The write only reaches the input when a branch passes it through by reference — unknown() is the smallest such branch — and then the merge assigns onto the frozen original:
v.safeParse(v.intersect([v.unknown(), v.object({a: v.string()})]), Object.freeze({a: "x"}))
// ≤1.3.1: TypeError: Cannot assign to read only property 'a'
// ≥1.4.0: { success: true, output: { a: "x" } }
Contiguous, flipping at exactly 1.4.0. Generalisation for the harness: when a fact says a library stopped doing something, a probe that does not reproduce the old behaviour is not evidence of the new one. Find the input that fails below the boundary before trusting the input that passes above it. Nested objects, arrays and looseObject all fail to reproduce this one; the reproducing shape is in the probe file with the negatives named.
Three things this ladder could do that the previous two could not
1. It reaches below the major, so a 1.0.0 fact is a real bisect. 0.42.0 and 0.42.1 are the last releases before 1.0.0, and the modular API the probes drive arrived at 0.31.0, so every probe runs unchanged down there. LF10 (exactOptional, introduced_in: "1.0.0") is therefore observed false at 0.42.1 and true at 1.0.0 — a named release below the boundary, which is what makes a row a bisect rather than a spot-check. zod's eight 4.0.0 facts still have nothing below them (BACKLOG 11k-t-iii); this is the first major-boundary fact in the Index that does.
Worth recording as a scoping observation and not acted on: the facts file says not_for: "valibot@0.x, whose API differs substantially from the v1 line", and twenty-six probes just executed unchanged on 0.42.x. The substantial difference is at 0.31.0, not at 1.0.0. Queued as 11k-t-ii-c rather than edited, because not_for is advice about who the correction pack is for, and narrowing it is a decision about scope rather than a dating error.
2. Its answer cannot be npm's. valibot declares no dependencies at any release on the ladder. The failure that dominated JOURNAL/070 — a shared node_modules deduplicating one copy of a dependency across every rung and manufacturing five corrections — has nothing to work with here. The ladder is still installed one release per directory, because the rule does not bend for a library that happens not to need it, but this is the first bisect whose result is unambiguously the library's.
3. It cost about a tenth of the previous one. Ten rungs, no peer dependencies, no --install failures, no re-running the ladder three ways to find out which tree to believe. better-auth took a whole session for thirteen facts and most of it went on the install layout.
Why valibot and not langchain
11k-t-ii-b names the order langchain (14), prisma (9), next.js (8), valibot (11), ranked roughly by how many dated facts each carries, and flags that langchain needs a Python subject hook — a venv per rung — before its ladder can exist at all.
Taking langchain first would have spent this session building that hook and installing fourteen-plus langchain venvs, and would likely have ended with the instrument half-built and no facts checked. valibot has nearly the same number of dated facts, needs no new infrastructure, and installs in under a minute. The order is now: valibot (done), prisma, next.js, langchain last with its Python hook, tailwindcss after that with the CSS auditor shape. Ranking by fact count alone ignored that one of the four costs an instrument.
What is now true of the corpus
| facts | checked by the bisector | corrected | still unchecked | |
|---|---|---|---|---|
| zod | 31 | 23 (22 probed + LF4 settled by tsc off-runner) | 8 | 8, all at the 4.0.0 major (11k-t-iii) |
| better-auth | 13 | 13 | 5 | 0 |
| valibot | 12 | 12 | 1 | 0 |
| total | 56 | 48 | 14 | 8 |
Fourteen in forty-eight — but the fourteenth is not a date, so the misdating rate is thirteen in forty-eight and holds. Three libraries bisected, four to go, counted by non-major dates: prisma (12), langchain (11), next.js (8), tailwindcss (7, CSS).
Files
tools/audit/probes/valibot.mjs— new; 26 probes over a 10-rung ladder, with the negatives that do not reproduce LF12c named in the probe, and the three non-runtime probe kinds declared.data/valibot/facts.json— LF6 rewritten (S1→S3, statement, stale_code, two shipped-declaration citations); LF2's note replaced with the measured characterisation;verified_on→ 2026-09-07.corrections/valibot.md,data/index.json,site/— regenerated.HARNESS.md— three rules from this session.