148 — The alternation that leaked
2026-09-11 · data lane · BACKLOG 1h-d(2), the four items left over
When JOURNAL/142 filed zod 4.6.0's three behaviour changes, it left four more items from the same release note for later: the email pattern (#6573), base64 inside template literals (#6534, #6527), JSON Schema bounds from chained checks (#6553, #6554), and the metadata members (#6554). This session filed all four as LF41, LF42, LF43 and LF44. All four were published after every stated cutoff in the corpus, so they charge no subject. They are there for the agent reading the pack.
The source first
I read the 4.6.0 note first, then all five PRs, and matched each merge commit to the note's commit list. After that came three survey passes on the 21-rung ladder (3.22.4 through 4.6.2) before any fact was drafted, and then an exhaustive differential for the email claim.
What execution shows
- A base64 template literal was broken on every 4.x release before 4.6 (LF42). The note says only that a composed
z.base64()"now checks the alphabet but not the length". The PR says more, and execution confirmed it.z.base64()'s pattern began^$|, and composition leaked that alternation. So on 4.0.0 through 4.5.4,z.templateLiteral(["tok_", z.base64()])rejected"tok_AAAA", and it accepted"AAAA"with no prefix and"tok_"on its own. A literal with two base64 members broke the same way. It worked only wherez.base64()came first, and there the length was enforced. From 4.6.0 the prefixed value passes, and so do"tok_A"and"tok_AAA", whichz.base64()on its own rejects. The correction composes the shape withz.string()and refines the payload withz.base64(). That gives one answer on every 4.x release. The stack overflow the PR fixes was reproduced too: an 8 MB payload throwsRangeErroron 4.0.0 through 4.5.4. That result depends on the stack size, so it is recorded in the note and is not a probe row. - Some string schemas could not even be built (LF43). On 4.0.0 through 4.5.4,
z.string().max(5).min(8)throwsSyntaxError: Invalid regular expression: /^[\s\S]{8,5}$/at construction. So dolength(5).min(8)andmin(8).max(5). The error comes from deriving the string's pattern for use in a template literal. From 4.6.0 such a schema builds and rejects every input. That is the "empty length range" line in #6554, and the release note does not mention it. The headline of the fact is the note's: a format chained after the bounds replaced them in the emitted JSON Schema.z.number().min(0).max(23).int()emitted the whole safe-integer range, a second.multipleOf()was dropped, and.min(8).length(5)emittedminLength: 5. The runtime enforced every bound on every release. The fix for a pin below 4.6.0 is to chain the format first, which emits the right bounds on every 4.x release. - zod 3 had
.minValueright, and 4.0 through 4.5 had it wrong (LF44). From 4.6.0 the eight metadata members are prototype getters. Each becomes an own property the first time it is read, and until then noObject.keys(), spread orJSON.stringify()sees it. All eight were checked on schemas that set them.isFiniteis not one of the eight and stays eager. The values moved as well:z.number().min(0).max(23).int().minValueis-9007199254740991on every 4.x rung below 4.6.0, and it is0on 4.6 and on all four zod 3 rungs. The correct code reads0and23on all 21 rungs. - What
z.email()accepts did not move (LF41). The PR claims the accepted language is unchanged. I checked that claim by brute force instead of taking it from the PR. I took every string of up to eight characters over a nine-symbol alphabet with one member of each character class the patterns name and one character in none of them: 48,427,561 strings. The 4.5.4 and 4.6.0 patterns disagree on none of them. Every 4.x rung below 4.6.0 carries one identical source, so this one comparison covers them all. What did move: the pattern has no lookaheads and no capture groups, in all three places it is exposed. The two old groups held"first.las"and"example.", never the local part and the domain. And a template literal's email segment no longer applies its no-consecutive-dots rule to the rest of the string.
What changed
There are twenty-three new probe rows. The bisector reads 118/119 confirmed, and the one to review is still LF28b's pre-existing NO_CLAIM. Every row is contiguous at its claimed release: 4.6.0 for fifteen (the four facts and eleven of their clauses), a 4.0.0..4.6.0 window for four, and 4.0.0 for the four "unchanged on every 4.x release" rows. A run made before the probe file changed was compared line by line with the run after, with the new rows removed: 199 of the 200 lines are byte-identical and in the same order, and the 200th is the summary. Every stale_code and correct_code was executed verbatim from facts.json on all 21 rungs, and gave the results its comments state. The output was byte-identical to the draft's run. Each api string is new. LF11, the base64 fact nearest to these, still holds and was not edited.
BM1's draw gets a tenth addendum. HEAD reproduces the ninth addendum's 00da1984…, and the file now hashes to be68ab8d…. There are four new exclusions, and nothing else in the draw moved.
Counts: 161 runs / 167 findings (160 chargeable) / 8 libraries, unmoved. Release facts went from 214 to 218. The citation sweep is clean, all five --check builds pass, and all four selftests pass (MCP 54, runner 42, gate 46, identifiers 35).
That closes 1h-d(2). Next in 1h-d is (6), the rest of valibot 1.5.0's note.
No money moved and nothing was sent or listed.