Filed by the domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T13:33Z.
⛔ Unlabelled and unrouted — an execution seat files, triage grades and routes. Suggested lane: domain:spec.
The defect
A closed shape declared with a bare z.object(...).strict() — or with zod's own z.strictObject(...) — rather than this package's strictObject helper silently loses its union's invalid_union envelope on zod 4.5.0+. A refusal behind such a union then reports the wrong branch's prescription: the author is told to fix something they did not write.
Why, measured
zod 4.5.0 adds continue: true to the unrecognized_keys issue. ⇒ util.aborted(result) is false for a member whose only complaint is an unknown key, so handleUnionResults' single-non-aborted short-circuit fires and returns that member's issues unwrapped. The union never raises invalid_union, and focusClaimedBranch — which only fires on invalid_union — never runs.
⭐ The discriminating control: handleUnionResults is byte-identical on 4.4.3 / 4.5.0 / 4.6.1, so ⛔ it is not what moved.
⚠️ Provenance and its limit: this mechanism is the reading of the round that delivered PR #19658, ⛔ not re-derived by the filing seat — the shared checkout has no node_modules and this seat will not install into one. It is being verified by that PR's at-tier contract review. If it is falsified, this card's premise goes with it.
The population, measured first-hand at origin/main by the filing seat
| reading |
value |
bare .strict() occurrences under packages/spec/src, non-test |
235 |
| files carrying them |
94 |
lit control — this package's strictObject( on the same instrument |
395 |
⇒ the instrument sees both spellings, so the 235 is a reading and ⛔ not a dead scan.
⚠️ One discrepancy stated rather than smoothed over: the delivering round reported 95 files; this seat measures 94 with its own pathspec. ⛔ Neither number is adopted as settled — whoever takes this re-derives the population and says which pathspec it used.
⛔ Only the shapes reachable inside a union are affected, so 235 is an upper bound on the candidate set, ⛔ not the defect count. The sweep's job is to narrow it.
Also present in non-test source: z.strictObject( at packages/spec/src/data/driver/turso.zod.ts, packages/spec/src/migrations/registry.ts, and one semantic migration entry.
The remedy already exists
closedObject (packages/spec/src/shared/strict-object.ts, landing with PR #19658) is a one-call fix: it re-declares the shape through a z.core.$constructor that marks the unknown-key issue non-continuable inside _zod.parse, before runChecks reads the payload — and because util.clone() rebuilds through _zod.constr, .strict(), .extend(), .refine() and .omit() all carry it forward.
⚠️ Blocked-by PR #19658: closedObject does not exist on main until it lands. Five sites were already converted there (the three $and/$or/$not cases in filter.zod.ts and the two lifecycle.onlyWhen arms in object.zod.ts) — ⛔ this card is the rest.
⚠️ A trap the taker must not walk into
The strictness ledger's AST reader and declaration-map's unwinder are sensitive to the expression spelling of a closed shape, not just its behaviour. The delivering round's first spelling took two sites off the ledger and dropped a schema from declaration-map/ui.json — both instruments caught it — and it re-spelled so the seal sits outside an intact z.object(...).strict() chain, after which check:generated regenerates nothing.
⇒ ⛔ do not convert in bulk without re-running check:generated per batch. ⭐ If conversion becomes common, the durable fix is to teach those two readers to follow closedObject(...) — the 「extend the detector, add a --self-test case, ⛔ never route around it」 rule. ⛔ No handler is claimed for that today.
Duplicate-search words
bare strict, unrecognized_keys continue, invalid_union envelope, closedObject, handleUnionResults short-circuit
Generated by Claude Code
Filed by the
domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T13:33Z.⛔ Unlabelled and unrouted — an execution seat files, triage grades and routes. Suggested lane:
domain:spec.The defect
A closed shape declared with a bare
z.object(...).strict()— or with zod's ownz.strictObject(...)— rather than this package'sstrictObjecthelper silently loses its union'sinvalid_unionenvelope on zod 4.5.0+. A refusal behind such a union then reports the wrong branch's prescription: the author is told to fix something they did not write.Why, measured
zod 4.5.0 adds
continue: trueto theunrecognized_keysissue. ⇒util.aborted(result)is false for a member whose only complaint is an unknown key, sohandleUnionResults' single-non-aborted short-circuit fires and returns that member's issues unwrapped. The union never raisesinvalid_union, andfocusClaimedBranch— which only fires oninvalid_union— never runs.⭐ The discriminating control:
handleUnionResultsis byte-identical on 4.4.3 / 4.5.0 / 4.6.1, so ⛔ it is not what moved.node_modulesand this seat will not install into one. It is being verified by that PR's at-tier contract review. If it is falsified, this card's premise goes with it.The population, measured first-hand at
origin/mainby the filing seat.strict()occurrences underpackages/spec/src, non-teststrictObject(on the same instrument⇒ the instrument sees both spellings, so the 235 is a reading and ⛔ not a dead scan.
⛔ Only the shapes reachable inside a union are affected, so 235 is an upper bound on the candidate set, ⛔ not the defect count. The sweep's job is to narrow it.
Also present in non-test source:
z.strictObject(atpackages/spec/src/data/driver/turso.zod.ts,packages/spec/src/migrations/registry.ts, and one semantic migration entry.The remedy already exists
closedObject(packages/spec/src/shared/strict-object.ts, landing with PR #19658) is a one-call fix: it re-declares the shape through az.core.$constructorthat marks the unknown-key issue non-continuable inside_zod.parse, beforerunChecksreads the payload — and becauseutil.clone()rebuilds through_zod.constr,.strict(),.extend(),.refine()and.omit()all carry it forward.closedObjectdoes not exist onmainuntil it lands. Five sites were already converted there (the three$and/$or/$notcases infilter.zod.tsand the twolifecycle.onlyWhenarms inobject.zod.ts) — ⛔ this card is the rest.The strictness ledger's AST reader and
declaration-map's unwinder are sensitive to the expression spelling of a closed shape, not just its behaviour. The delivering round's first spelling took two sites off the ledger and dropped a schema fromdeclaration-map/ui.json— both instruments caught it — and it re-spelled so the seal sits outside an intactz.object(...).strict()chain, after whichcheck:generatedregenerates nothing.⇒ ⛔ do not convert in bulk without re-running
check:generatedper batch. ⭐ If conversion becomes common, the durable fix is to teach those two readers to followclosedObject(...)— the 「extend the detector, add a--self-testcase, ⛔ never route around it」 rule. ⛔ No handler is claimed for that today.Duplicate-search words
bare strict,unrecognized_keys continue,invalid_union envelope,closedObject,handleUnionResults short-circuitGenerated by Claude Code