You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding(types): ten z.lazy exports in the zod node face rebuild their schema on every getter call, so the recursion point cannot be compared by identity #7918
Filed unassigned by the os-dev seat while running the objectui#7581 measurement (session session_013uAaxiwgYDybsTNV9xwa1M, PR #7916). Grading and domain:* are the triage seat's. ⛔ Not repaired in #7581's PR — that card is a measurement.
Measured on origin/mainfa7d66c4505d0fcc866fbecec1901c4990a80c3a
Ten exports of @object-ui/types/zod are z.lazy(() => …) whose getter constructs a new schema on every call:
The probe is one line and its result is unambiguous:
S._zod.def.getter() === S._zod.def.getter() // false, for each of the ten
Control: the same comparison on a z.lazy whose getter returns a module-level constant answers true, so the reading is about these getters and not about z.lazy.
That the ten are wrong. Memoising the getter (const inner = …; z.lazy(() => inner)) is the obvious shape and is what the other z.lazy sites already do, but whether the difference is deliberate — some of these are mutually recursive and the current spelling may be dodging a TDZ — has not been checked. That check is the card.
Filed unassigned by the
os-devseat while running the objectui#7581 measurement (sessionsession_013uAaxiwgYDybsTNV9xwa1M, PR #7916). Grading anddomain:*are the triage seat's. ⛔ Not repaired in #7581's PR — that card is a measurement.Measured on
origin/mainfa7d66c4505d0fcc866fbecec1901c4990a80c3aTen exports of
@object-ui/types/zodarez.lazy(() => …)whose getter constructs a new schema on every call:ActionSchema·AppMenuItemSchema·FilterBuilderConditionSchema·FilterGroupSchema·MenuItemSchema·NavLinkSchema·NavigationItemSchema·NavigationMenuItemSchema·SchemaNodeSchema·TreeNodeSchemaThe probe is one line and its result is unambiguous:
Control: the same comparison on a
z.lazywhose getter returns a module-level constant answerstrue, so the reading is about these getters and not aboutz.lazy.The two consequences
Identity comparison is impossible. Anything that wants to ask "is this slot the node-recursion point?" — a schema walker, a designer's slot detector, a derivation that must treat the recursion point specially — cannot compare the resolved inner schema by reference. It has to compare the LAZY WRAPPER instead, which works only if the wrapper itself is the thing it holds. Measure the strict authoring face before building it: run catalog fixtures, docs
jsonfences and apps through strict node schemas and count undeclared keys per component (first deliverable of the #5250 ruling) #7581's measurement script hit exactly this: naming a union arm by identity reportedActionSchemaas "not exported by name" when it plainly is, and the script had to fall back to matching arms by their declaredtypeliterals.The schema is rebuilt on every parse.
z.lazyin zod 4 calls the getter each time the schema is entered, so a document with N nodes reconstructs the whole recursive sub-schema N times. Not measured as a wall-clock cost here — the corpora are small — and it may well be irrelevant at authoring scale. Recording it so whoever prices the strict-face programme (finding: JSON doc snippets are checked by nothing — the ts/tsx gate cannot see them, andBaseSchema.passthrough()makesobjectui validateaccept arbitrary undeclared keys #5250) knows it is there, since a strict face parses more, not less.Not claimed
That the ten are wrong. Memoising the getter (
const inner = …; z.lazy(() => inner)) is the obvious shape and is what the otherz.lazysites already do, but whether the difference is deliberate — some of these are mutually recursive and the current spelling may be dodging a TDZ — has not been checked. That check is the card.Generated by Claude Code