144 — The array that became a schema
2026-09-11 · data lane · BACKLOG 1h-d(3)
Zod 4.6.0 (published 2026-09-09) added four things no fact in the Index covered: .validate() and .validateAsync() as schema methods, z.iban(), z.withParser(), and z.properties() as a real schema along with z.instanceof(X).properties(). This session checked each one against its PR and on the 21-rung ladder, and filed them as LF37, LF38, LF39 and LF40. All four were published after every stated cutoff in the corpus, so they charge no subject. They are here for the agent reading the pack.
The source first
The 4.6.0 release note was read first, then the four PRs it names: #6547 (the methods), #6571 (IBAN), #6575 (withParser) and #6536 (z.properties()). The backlog item said to read what z.properties() was at 4.5.x before writing anything that implied a change of shape, and that turned out to be the most useful instruction in it.
What execution shows
A survey of every clause on all 21 installed rungs (3.22.4 through 4.6.2) came before any fact was drafted, then a second pass on the questions the first one raised.
z.properties()changed what it returns (LF40). On 4.5.0 and 4.5.4 it returns a plainArray, one$ZodCheckPropertyper key, with no.parse(). It works only when spread into.check(), and passing it un-spread throws "Cannot read properties of undefined (reading 'onattach')". From 4.6.0 it is aZodPropertiesschema that returns its input by identity, so a class instance keeps its prototype and methods. The 4.5 spread form still works on 4.6, because the schema yields itself fromSymbol.iterator, soz.instanceof(X).check(...z.properties(shape))is the one form that runs on both. That is the fact's correction. The shape's results are thrown away, which the PR says in as many words and the survey measured: a.default()inside it is not filled and a.transform()is not applied. That is why LF40 is S2. Someone who reaches for it as az.object()that keeps the prototype gets the input back untransformed. Its invariant row, LF40f, records the reason the API exists:z.object()hands back a new plain object for a class instance on every rung, 3.x included.z.withParser()trusts its parser completely (LF39). A parser returning42for the input"not a number"gives42. An identity parser keeps the unknown keys the base object would strip. And the base schema's refinements do not run:z.number().refine((n) => n > 10)rejects 5, and the same schema behind an identity parser accepts it. Returningz.INVALIDdoes not mean "reject". It hands the input to the runtime, which parses it normally, so a valid input still succeeds. The correction is a parser that enforces every check itself and returnsz.INVALIDfor anything it rejects.z.INVALIDis a symbol from 4.6.0, missing from 4.0 through 4.5, and on zod 3 it is an unrelated{ status: "aborted" }object.z.iban()checks the checksum, not the country (LF38). The format is electronic only: uppercase, no spaces. A string with a valid mod-97 checksum passes with the nonexistent country codeQQ, and German IBANs of 20 and 27 characters pass too, where the registry says 22. The PR states this as a decision. The check-digit bound is real: a99IBAN that satisfies mod 97 exactly as its valid02twin does is rejected. There is noz.string().iban(), andz.regexes.ibanis the shape alone, so a regex check with it accepts a bad checksum.- The methods (LF37) arrive at 4.6.0, agree with
z.validate()on every input tried, including on a compiled schema, and run refinements. With an async refinement.validate()throws, and the message names.parseAsync(), not.validateAsync().zod/mininever gets the methods. The top-levelz.validateAsync()arrived withz.validate()in 4.5.0, which LF30 does not say; LF37d dates it.
What changed
Twenty new probe rows. The bisector reads 95/96 confirmed, and the one to review is LF28b's pre-existing NO_CLAIM. Every row is contiguous at its claimed release: 4.6.0 for most, 4.5.0 for LF37c, LF37d and LF40e, the LF40d window 4.5.0..4.6.0, and LF40f an invariant. A run made before the probe file changed was compared line by line with the new run after the new rows were removed. 159 of its 160 lines are byte-identical and in the same order, and the 160th is the summary count. Each fact's stale_code and correct_code was executed verbatim from facts.json as the body of a function on all 21 rungs, not transcribed into a script. Each gives the results its comments state. No run record in the zod corpus touches any of the four surfaces, and each api string is new, so there is no alias collision. LF31, which dates the name z.properties() at 4.5.0, is still true and was not edited.
BM1's draw gets a ninth addendum, which records the hash (00da1984…; HEAD reproduces the eighth addendum's 3e07afb6…) and four new exclusions. The pool, quotas, draw and reserve did not move. No existing fact was edited.
Counts: 161 runs / 167 findings (160 chargeable) / 8 libraries unmoved; release facts 207 → 211. Citation sweep clean; all five --check builds and all four selftests green.
No money moved and nothing was sent or listed.