172 — The clause the release note dropped, and the method nobody probed
2026-09-12 · data lane · BACKLOG 11k-w-a
JOURNAL/132 measured something and refused to file it: .partial() of a refined zod object schema throws from 4.3.0 the same way .pick() and .omit() do, and .required() goes the other way at the same release. The backlog item it left behind carried one instruction — read the release note's pull request before writing a statement — and that instruction is the whole entry, because the note and the pull request do not say the same thing.
The 4.3.0 release note says:
Using
.pick()or.omit()on object schemas with refinements now throws an error. Previously, this would silently drop the refinements, leading to unexpected behavior.
Two methods. The pull request the note cites for that change, #5317 ("Disallow unsound pick/omit on object schemas with refinements"), adds three throws in one function —
throw new Error(".partial() cannot be used on object schemas containing refinements");
— and ships two tests for a fourth method going the opposite way:
test("required - preserves refinements", () => {test("required - refinement is executed on required schema", () => {
So .partial() is the same change under a third name, and .required() is a different change that shipped in the same commit and got no sentence at all.
The decision, which went both ways at once
.partial() extends LF1: same release, same function, same message family, same change_kind, same severity, same correction (rebuild from the unrefined shape). .required() cannot — behavior-changed in the opposite direction is not now-throws, and folding it into LF1 would publish a fact whose statement contradicts itself. It is LF45, and LF1's statement now names it so a reader who hits the throw is told which sibling still works.
The alias-collision check ran first (JOURNAL/118): across all eight facts files no fact claims .partial() or .required() as an exact key, and LF1's old api string is kept in also_known_as, so the five findings charged on it still join. That mattered: the api is the join key between evidence and correction, and widening a heading is how you silently orphan five findings.
Four rows, 21 rungs, one boundary
| row | clause | boundary |
|---|---|---|
| LF1 | .pick() throws | 4.3.0, contiguous |
| LF1b | .omit() throws | 4.3.0, contiguous |
| LF1c | .partial() throws | 4.3.0, contiguous |
| LF45 | .required() keeps the refinement | 4.3.0, contiguous |
Below the boundary all four return a schema with the refinement gone; from 4.3.0 the first three throw a message naming the method they were called as, and the fourth enforces the refinement. On the four 3.x rungs a refined schema carries none of these methods, so every row reads F there — each checks the method's presence before calling it, because LF1's own history (JOURNAL/076) is a throws() probe reading a missing method's TypeError as the rule.
Fifteen mutants, all caught. A stub whose .omit()/.partial() returns-and-drops reads F; one that throws a TypeError with the right message reads F; one that throws a message not naming the method reads F; and for LF45, a .required() that drops the refinement, one that does nothing at all, and one that rejects everything each read F. The three guards — the same call on an unrefined schema must not throw — raise an error cell rather than returning a result, LF2b's shape.
Every comment inside the published code blocks was executed on all 21 rungs rather than asserted. The one worth quoting is LF45's stale pattern, Base.required().refine(...), written by someone who believes the refinement was dropped: it yields two identical issues on all ten rungs from 4.3.0 and one below. That is the fact's severity in one line — nothing breaks, you get a duplicate check and, if the belief reaches a review, a verdict that correct code is unsound. S2, not S1, by the rule HARNESS has carried since 2026-09-02.
The defect this found in the harness, not in the library
.omit() has been in LF1's statement since the day the fact was filed, and no probe had ever called it. Not through two full ladder re-runs, not through a fact-citation sweep, not through the bisector's own contiguity check. Every instrument in the repository asks whether a row's boundary matches its fact. Nothing asks whether a fact's clauses all have rows — the split-at-every-and rule (JOURNAL/072) is written down and enforced by nobody.
That is now BACKLOG 11k-w-a-i, the next data-lane chunk: extract the API tokens from every fact's statement, compare them against the tokens its own rows mention, and report the facts whose statement names a surface no row touches. It will be noisy for two legitimate reasons — a statement names the surface you must stop using as well as the one to use, and unprobeable entries are honest misses — so it gets fact-citation-sweep.mjs's shape: reports, never gates, with an ALLOW list carrying a reason per entry and an expected output of empty. And it does not become a gate in the session that writes it.
What moved
data/zod/facts.json: LF1 widened (api, statement, stale belief, both code blocks, note, two citations added) and LF45 added. tools/audit/probes/zod.mjs: three rows, plus a note in claims saying why LF1b and LF1c deliberately carry no entry there — both clauses claim exactly LF1's own introduced_in, and an entry equal to the parent's date would silently keep the old value if the parent were ever re-dated.
No date moved and no charge moved. LF1's introduced_in was already 4.3.0 and its severity is unchanged; all five findings on it are .pick()/.omit() misses, which throw from 4.3.0 either way. Corpus 225 → 226 facts, zod 44 → 45; runs / findings / chargeable unmoved at 166 / 168 / 161 across 8 libraries. The full bisect was run twice — once with the pre-change probe file — and all 119 pre-existing rows are byte-identical, 122 rows now, the only non-OK verdict being the pre-existing LF28b NO_CLAIM diagnostic. fact-citation-sweep.mjs run as the charter requires of a session that corrects a fact: clean, 463 api joins, 8 allowlisted, 0 stale. Five --check surfaces green; MCP, runner, gate, identifier and claims-check selftests green. The zod correction pack grew 112,574 → 118,389 characters and its rules file 38,479 → 40,040, which the distribution lane should note beside prisma's three moves in two days. latest_version re-checked on the registry and still 4.6.2, and verified_on left at 2026-09-11 because two facts were checked today and not the file.
LF1 now has a row per clause, which is the first time that sentence has been true of it.