Repository navigation
fix(deps): bump zod to the fixed line (4.6.1+) workspace-wide, with a regression pin - #19658
Conversation
zod 4.4.3's `treeifyError`, `formatError` (`error.format()`) and
`flattenError` (`error.flatten()`) walk an issue `path` by reading
`curr[el]` and testing it for truthiness before creating a node, so a path
element naming an `Object.prototype` member is answered by the prototype and
no node is ever created. Two failure modes follow:
- a TERMINAL element adopts the inherited member as the node and then does
`node._errors.push(...)` on it -- `TypeError: Cannot read properties of
undefined (reading 'push')`;
- a NON-TERMINAL element walks INTO `Object.prototype` and writes the next
segment onto it -- the refusal message is silently dropped from the
returned tree and the process gains a global prototype key.
This repo emits exactly the terminal shape: the landed `__proto__` refusals
guard open-key surfaces whose issue path is `['assignments','__proto__']`, so
a consumer formatting our own refusal crashed on it. 4.6.1 fixes it with a
`node()` helper that guards the read with `hasOwnProperty` and creates a
`__proto__` node through `defineProperty`.
Every declaration moves to the fixed line with its shape preserved (caret
stays caret, `apps/docs`'s exact pin stays exact), and the lockfile is
refreshed through pnpm rather than hand-edited. No `overrides` entry is
needed: every other zod range in the tree, workspace and vendor, already
admits 4.6.1, and the lockfile now holds a single zod copy.
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
…hat ship it Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…ype the pin Two mechanical consequences of moving zod to 4.6.1, both regenerated with the repo's own commands rather than hand-edited. `content/docs/references/**` (`gen:docs`): four pages now render `ViewFilterRule.operator` as REQUIRED where they rendered it optional. That is a CORRECTION, not a narrowing -- `operator` carries no `.optional()` and the runtime has always required it. The key is declared through `z.preprocess(normalizeFilterOperator, z.enum(...))`, and a `z.preprocess`'s inner transform hardcodes `_zod.optin = "optional"` regardless of what it wraps. 4.6.1 stops the requiredness computation from believing that flag: measured on one object with a preprocessed required key, `toJSONSchema(io: 'input').required` reads `["field"]` on 4.4.3 and `["operator","field"]` on 4.6.1. The flag itself is unchanged in both, so the existing `optin`/`optout` correction in `refuseProtoOwnKey` is unaffected and still load-bearing. The tracked JSON Schemas do not move -- `check:authorable-surface` is green. The pin's tree reads now go through `ownValue` for the container key too. A `ZodError` built from raw issues types its tree without `properties`, so the direct read did not compile; asking for an OWN `properties` is also the stricter assertion, and matches how every other lookup in the file is spelled. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 11 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 154 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 68752d9ce7f7ef636f5e87d8a9f567c0e823f08e && git checkout 68752d9ce7f7ef636f5e87d8a9f567c0e823f08e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8cbc3c0084a3c3c8ef5e59763ab3be96aa1a6f07 a6a34d0c05ecb35f359c74d1f7e5f3cead558cfa && git checkout -B drift-repro 8cbc3c0084a3c3c8ef5e59763ab3be96aa1a6f07 && git merge --no-ff a6a34d0c05ecb35f359c74d1f7e5f3cead558cfa
node scripts/docs-audit/affected-docs.mjs --json 8cbc3c0084a3c3c8ef5e59763ab3be96aa1a6f07
|
…eep their diagnosis
From zod 4.5.0 the `unrecognized_keys` issue carries `continue: true`. Two
things follow, and both were measured on this codebase with the same bodies
on 4.4.3 and 4.6.1:
1. a closed shape's own `.refine()` / `.superRefine()` now run AFTER the
unknown-key refusal, adding a second contradictory complaint;
2. `handleUnionResults` — byte-identical across 4.4.3 / 4.5.0 / 4.6.1, so
not itself the change — sees exactly one non-aborted member and returns
its issues UNWRAPPED, so the union never raises `invalid_union`.
On `ViewMetadataSchema` that made a `viewKind` + `config` ViewItem body answer
with the CONTAINER branch's "wrap it: defineView({ list: … })", and the
retirement prescription naming the retired member was never reached, because
`focusClaimedBranch` only fires on an `invalid_union`.
`closedObject` re-declares a closed shape through a constructor that marks its
unknown-key issue non-continuable before `runChecks` reads the payload.
`util.clone()` rebuilds through `_zod.constr`, so `.strict()`, `.extend()`,
`.refine()` and `.omit()` all carry it forward. It rewrites a flag on an issue
that was already raised and adds, removes or re-codes nothing, so the
acceptance face cannot move.
Applied at `strictObject` (every closed authoring shape), and at the three
closed shapes that do not come through it: the form-field base, the normalized
filter's group branch, and the two `lifecycle.onlyWhen` comparand arms. All
three keep their existing expression spelling so the strictness ledger and
`declaration-map` read them unchanged — `check:generated` regenerates nothing.
`apps/docs` moves from an exact `4.6.1` to `^4.6.1`, matching the twelve
sibling manifests; the lockfile is regenerated by pnpm and still resolves one
zod.
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
The floor move and the diagnosis repair land together and revert together, so the changeset that ships to consumers has to carry both halves: what an unknown-key refusal does to a union's envelope, the before/after an author reads at `PUT /api/v1/meta/view`, and the one spelling that does NOT get the repair (a bare `z.object(…).strict()` / zod's own `z.strictObject`). Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…door again The seven assertions that survived the zod floor move were passing over a shape they were never built to see. Both files declared their union fixture from raw zod primitives, and from zod 4.5.0 an `unrecognized_keys` issue carries `continue: true` — so a bare `z.strictObject` arm whose only complaint is an unknown key is the union's lone non-aborted member. `handleUnionResults` short-circuits on exactly that condition and returns the arm's issues unwrapped, so no `invalid_union` is raised and the expansion under test never runs. Both fixtures now feed `NormalizedFilterSchema`, a real product door whose `closedObject` seal makes its unknown-key refusal terminal. The union raises `invalid_union` again and the assertions measure the renderer. Not one assertion byte moved: 78 + 33 = 111 assertion statements extracted and diffed against the base, zero changed. Only the fixture declarations and the inputs they are fed changed. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…rimes its map Two regressions this PR introduced, one root cause each, both in `shared/strict-object.ts`. 1. `ZodClosedObject` was a module-level `const`. `strictObject` runs at module scope for schemas inside the `field -> strict-object -> suggestions -> field` cycle and now reaches that constructor on every call, so whenever the loader enters this module second the call lands in the constructor's temporal dead zone. Under `OS_EAGER_SCHEMAS=1` -- how `build-schemas.ts` runs -- importing the package root died with `ReferenceError: Cannot access 'ZodClosedObject' before initialization`, raised from `data/field-value.zod.ts` before a single schema was built. It is now built on first use behind a hoisted function, the shape `declarationStore()` already documents; `markUnknownKeyRefusalTerminal` becomes a hoisted declaration for the same reason. 2. zod 4.6's `safeParse` returns its failure with `error` as a lazy getter, so issue finalization -- and with it the unknown-key map's one build -- slides from the parse that refused the key to whenever a consumer first reads `.error`. Measured on both lines with the same bodies: the prescription an author reads is byte-identical, so no message is lost; what moves is WHEN this module reaches across its own import cycle. The closed-object parse wrapper now primes the map on the refusal path, restoring the property `strict-object.test.ts` pins, which is `main`'s and is unmodified here. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed and posted 2026-09-23T11:20Z by the at-tier review subagent the ① Derived judgments
② Semver level
③ Boundary flagsBlocking: none.
Implemented-by: VERDICT: PASS |
…n one side The merge of origin/main (afc3b64) routed four generated reference pages through the os-regen driver, which defers them instead of text-merging: ui/view, ui/page, data/object and api/protocol. Each was edited on both sides since the fork point (eff0a96), so the merge commit carries one side only. Regenerated from the merged tree with the chain the pre-commit hook names: `gen:schema` then `gen:docs`. No hand edit. Against main the four pages now differ by exactly the branch's own 32 changed lines, and against the pre-merge branch tip by exactly main's 46: both sides survive, nothing else moved. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…e left on one side The merge of origin/main (8cbc3c0) routed three generated reference pages through the os-regen driver, which defers them instead of text-merging: ui/view, data/object and api/protocol. Each was edited on both sides since the previous merge point (afc3b64), so the merge commit carries one side only. ui/page was edited on this branch only and keeps the branch bytes. Regenerated from the merged tree with the chain the pre-commit hook names: `gen:schema` then `gen:docs`. No hand edit. Against main the four pages differ by exactly the branch's own 32 changed lines, and against the pre-merge branch tip by exactly main's 54: both sides survive, nothing else moved. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Merge-forward note: the review record
|
| merge | main |
then | files the PR also edits, moved by the merge |
|---|---|---|---|
26b66df623 |
afc3b64928 |
9abe79f981: regenerate 4 reference pages |
filter.zod.ts, object.zod.ts, view.zod.ts |
cc2fc46311 |
8cbc3c0084 |
a6a34d0c05: regenerate 3 reference pages |
view.zod.ts (main's change: describe and docblock text from #19598) |
No Regen-provenance: line. Both merges carry main's edits into files this PR edits, so neither is a pure-regeneration hop, and the record does not carry forward by that rule.
What the seat checked at a6a34d0c05:
- The head differs from
main8cbc3c0084in exactly the 26 paths this PR edits (eff0a96223..dbd78c2d8a). - The first merge joined the same two commits the review's item 10 merged (
dbd78c2d8awithafc3b64928). The second merge'sview.zod.tsblob equalsgit merge-treeof8cbc3c0084with9abe79f981. closedObjectoccurrences are unchanged instrict-object.ts,filter.zod.ts,object.zod.tsandview.zod.ts(7 / 3 / 4 / 3).strict-object.ts, the pin anderror-map.test.tsare byte-identical todbd78c2d8a.- The dev ran at
a6a34d0c05: spec build,check:generated15/15, the pin 7/7,error-map.test.ts35/35. At9abe79f981it also ran the full spec suite (15420 passed) andformat-zod-union.test.ts(13/13).
The merge queue rebuilds the merge with main and runs the full suite.
Generated by Claude Code
Fixes #19581
Clause-②: no
Rewritten short by the
domain:spec#5seat (2026-09-23T11:24Z), which took this PR over from seat 2. The earlier long body is in the PR's edit history.Moves the
zodfloor to^4.6.1in the 13package.jsonfiles that declare it (ruling5770530634, letter B), and repairs the union diagnosis the bump would otherwise break (ruling5774631464, letter A).Why
On zod 4.4.3,
z.treeifyError(),error.format()anderror.flatten()read a path element such as__proto__ortoStringoffObject.prototype: they throw, or drop the message and pollute the prototype. zod 4.5.0 guards that read. The declared floor moves, not only the lockfile, so a downstream install of these packages cannot resolve 4.4.3.What changed
^4.4.3→^4.6.1;apps/docsmoves from exact4.4.3to^4.6.1. The lockfile holds one zod package,zod@4.6.1. No override was needed.continue: true. A union member whose only complaint is an unknown key then counts as not aborted, the union returns that member's issues unwrapped, and noinvalid_unionis raised. A retired value such astype: 'page'was then answered with "Wrap it:defineView({ list: … })" instead of the message that names it.closedObject(packages/spec/src/shared/strict-object.ts) makes that issue terminal again;view.zod.ts,filter.zod.tsandobject.zod.tsuse it for closed shapes not built throughstrictObject.strict-object.tsalso declares its constructor before first use and primes its map, soOS_EAGER_SCHEMAS=1imports load.packages/spec/src/shared/zod-error-formatter-proto-path.pin.test.ts: 7 pass on zod 4.6.1; on 4.4.3, 6 fail and the positive control passes.error-map.test.tsandcli/test/format-zod-union.test.tsbuilt their fixtures from raw zod primitives, which raise noinvalid_unionon 4.5+; their fixtures now go throughNormalizedFilterSchema, and their assertions are unchanged.ViewFilterRule.operatornow renders as required, matching what the schema already refuses. The tracked JSON Schemas do not move.patchon the 11 published packages that declare zod independencies.@objectstack/lint(devDependency only) andapps/docs(private) take no release.Review
At-tier contract review
5793905962: PASS atdbd78c2d8a. Its measurements back the claims above.Not in this PR
closedObject()rebuilds from_zod.def, so a.describe()/.meta()applied before wrapping is dropped (record ③). None of the four sites here does that. A bare.strict()closed shape silently loses its union'sinvalid_unionenvelope on zod 4.5.0+ — the refusal then names the wrong branch #19731 (the bare.strict()sweep) must carry the registry entry across.packages/spec/dropped-refinements.baseline.jsonstill records"measured": { "zod": "4.4.3" }. Nothing gates it, and its counts did not move.🤖 Generated with Claude Code
https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1