Priced by the domain:spec @ objectui seat on #7935 (the pricing card the #5250 ruling names as its step 2). ⛔ Filed unassigned, not claiming. Appetite is in this body and is binding: the maintainer approved the direction, not a blank cheque on scope (#5250, director comment 5534418546).
This is the FIRST card of the strict-authoring-face programme, and it is not the strict face. It is the structural precondition #7581's measurement identified: until the node recursion point resolves per-type, every document-level number the programme quotes is measuring the recursion point instead of the components.
What is true today
packages/types/src/zod/base.zod.ts:53:
export const SchemaNodeSchema = z.lazy(() =>
z.union([BaseSchemaCore, z.string(), z.number(), z.boolean(), z.null(), z.undefined()])
);
(The export additionally carries an explicit z.ZodType annotation taking an any type argument — the recursion-breaking annotation objectui#7760 is in flight to replace. Elided above only because a type-argument bracket does not survive GitHub's body sanitizer; the union arms are the part this card is about.)
BaseSchemaCore is the ~21 base keys and nothing type-specific. Every child slot (body, children, and every per-component redeclaration of them) is z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]). ⇒ per-type enforcement is root-only, at every depth, for every component type. That is the finding already recorded as #7869, measured independently there (an off-spec size on a nested icon node is accepted; the same node alone is refused).
The measurement this card is priced from
Measured on origin/main 5505aec1a07c26976d160a39f152a48b088cfbb4, zod 4.4.3, face read from packages/types/dist/zod/index.zod.js, over the catalog + docs corpora — 553 node documents (431 catalog JSON files, 204 ```json fence openings). Four twins, one corpus, one commit:
| reading |
recursion point |
strict? |
documents refused |
| R1 baseline — the face as shipped |
BaseSchemaCore |
no |
45 / 553 |
R2 naive strict — what a .strict() flip really does today |
BaseSchemaCore |
yes |
294 / 553 |
| R3 this card — redirect only |
AnyComponentSchema |
no |
54 / 553 |
| R4 redirect + strict — where the programme is going |
AnyComponentSchema |
yes |
176 / 553 |
Controls: R1 and R2 reproduce scripts/measure-strict-authoring-face.mjs at the same commit (46 / 555 and 296 / 555) — the 2-document and 1-refusal difference is corpus 3, the 2 authored documents this reading omits.
⭐ The two results that decide the ordering
- The redirect costs 9 documents. R3 minus R1 = 54 minus 45. Every one is enumerated below.
- The redirect removes 118 documents of phantom redness. R2 minus R4 = 294 minus 176. Those 118 are refused today only because the recursion point unrecognises every child's own declared props — no per-component declaration budget can ever close them. ⇒ pricing the strict face on 294 rather than 176 prices the wrong programme.
The 9 documents, and why each refuses — both classes are pre-existing debt
Class A — a child node whose type resolves in no arm of AnyComponentSchema (4 documents, 7 spellings):
examples/schema-catalog/src/schemas/components-form-calendar/form-integration.json — child ui:calendar
examples/schema-catalog/src/schemas/components-layout-semantic/complete-layout.json — children header, nav, main, footer
content/docs/utilities/runner.mdx#L269 — child my-component
content/docs/utilities/runner.mdx#L323 — child h1
Class B — a child node already red under its OWN schema today, shielded until now by the recursion point (5 documents):
examples/schema-catalog/src/schemas/components-complex-filter-builder/search-interface.json — filter-builder: field invalid_type, operator / logic invalid_value
examples/schema-catalog/src/schemas/components-feedback-toaster/with-toast-trigger.json — button: onClick custom
content/docs/api/schema-reference.md#L173 — badge: variant invalid_value
content/docs/api/schema-reference.md#L1267 — dashboard: type invalid_value + unrecognized_keys
content/docs/guide/schema-playground.md#L98 — stack: direction invalid_value, gap invalid_type
⇒ The redirect creates no new defect class. It surfaces two existing ones, at a size that fits in one card.
The cost driver that is NOT in the document count — the module cycle
SchemaNodeSchema is declared in base.zod.ts, which today imports only tombstone.zod.js and expression.zod.js. AnyComponentSchema is built in index.zod.ts from all 13 category modules, and 14 modules import base.zod.js. ⇒ pointing the recursion point at the union makes base.zod.ts depend on index.zod.ts, which depends transitively back on base.zod.ts. z.lazy defers the evaluation, not the module graph, so this needs a deliberate break — a late-binding holder that index.zod.ts fills at load, or relocating the union. ⛔ Do not discover this at implementation time; it is the reason this card is not a one-line diff.
And one hard constraint, measured, that a naive implementation will get wrong
The recursion point must be recognised by identity on the exported wrapper, ⛔ never through .unwrap(). Measured at the same commit:
exported wrapper identity stable : true (two module reads return the same object)
CardSchema.body union contains S : true (identity survives through a declared slot)
S.unwrap() === S.unwrap() : false
getter() === getter() : false
_zod.innerType stable : true (zod 4.4.3 caches the resolved inner)
This is #7918's consequence ①, relocated by os-sam on #7935 (comment 5563938784): the public handle is unstable for eight of the ten lazy exports, and SchemaNodeSchema is one of them. Identity on the export works today and is what scripts/measure-strict-authoring-face.mjs already relies on.
Appetite — one dispatched card, one PR
In scope: the redirect, the module-cycle break, the 9 documents above, and a pin that a nested off-spec node is now refused (the #7869 reproduction, both directions).
⛔ Falls off the back rather than growing this card:
- Any of the 7 Class-A spellings that turns out to need a new component arm — that is a public-surface widening, and it splits into its own card rather than riding this one.
- Everything strict. This card does not add
.strict() anywhere; R3, not R4, is its acceptance number.
- The declaration-repair worklist (the 184-node / 2099 per-component reading). Not this card.
If the 9 do not fit, the card is wrong and comes back for re-pricing — ⛔ do not widen it in flight.
Grading notes
Refs: #5250 (the ruling) · #7935 (this pricing) · #7581 / PR #7916 (the measurement) · #7869 (the finding) · #7760 (in flight, same files) · #7918 (the unstable public handle) · #6318
Generated by Claude Code
Priced by the
domain:spec @ objectuiseat on #7935 (the pricing card the #5250 ruling names as its step 2). ⛔ Filed unassigned, not claiming. Appetite is in this body and is binding: the maintainer approved the direction, not a blank cheque on scope (#5250, director comment5534418546).This is the FIRST card of the strict-authoring-face programme, and it is not the strict face. It is the structural precondition #7581's measurement identified: until the node recursion point resolves per-type, every document-level number the programme quotes is measuring the recursion point instead of the components.
What is true today
packages/types/src/zod/base.zod.ts:53:(The export additionally carries an explicit
z.ZodTypeannotation taking ananytype argument — the recursion-breaking annotation objectui#7760 is in flight to replace. Elided above only because a type-argument bracket does not survive GitHub's body sanitizer; the union arms are the part this card is about.)BaseSchemaCoreis the ~21 base keys and nothing type-specific. Every child slot (body,children, and every per-component redeclaration of them) isz.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]). ⇒ per-type enforcement is root-only, at every depth, for every component type. That is the finding already recorded as #7869, measured independently there (an off-specsizeon a nestediconnode is accepted; the same node alone is refused).The measurement this card is priced from
Measured on
origin/main5505aec1a07c26976d160a39f152a48b088cfbb4, zod4.4.3, face read frompackages/types/dist/zod/index.zod.js, over the catalog + docs corpora — 553 node documents (431 catalog JSON files, 204```jsonfence openings). Four twins, one corpus, one commit:BaseSchemaCore.strict()flip really does todayBaseSchemaCoreAnyComponentSchemaAnyComponentSchemaControls: R1 and R2 reproduce
scripts/measure-strict-authoring-face.mjsat the same commit (46 / 555 and 296 / 555) — the 2-document and 1-refusal difference is corpus 3, the 2 authored documents this reading omits.⭐ The two results that decide the ordering
The 9 documents, and why each refuses — both classes are pre-existing debt
Class A — a child node whose
typeresolves in no arm ofAnyComponentSchema(4 documents, 7 spellings):examples/schema-catalog/src/schemas/components-form-calendar/form-integration.json— childui:calendarexamples/schema-catalog/src/schemas/components-layout-semantic/complete-layout.json— childrenheader,nav,main,footercontent/docs/utilities/runner.mdx#L269— childmy-componentcontent/docs/utilities/runner.mdx#L323— childh1Class B — a child node already red under its OWN schema today, shielded until now by the recursion point (5 documents):
examples/schema-catalog/src/schemas/components-complex-filter-builder/search-interface.json—filter-builder:fieldinvalid_type,operator/logicinvalid_valueexamples/schema-catalog/src/schemas/components-feedback-toaster/with-toast-trigger.json—button:onClickcustomcontent/docs/api/schema-reference.md#L173—badge:variantinvalid_valuecontent/docs/api/schema-reference.md#L1267—dashboard:typeinvalid_value + unrecognized_keyscontent/docs/guide/schema-playground.md#L98—stack:directioninvalid_value,gapinvalid_type⇒ The redirect creates no new defect class. It surfaces two existing ones, at a size that fits in one card.
The cost driver that is NOT in the document count — the module cycle
SchemaNodeSchemais declared inbase.zod.ts, which today imports onlytombstone.zod.jsandexpression.zod.js.AnyComponentSchemais built inindex.zod.tsfrom all 13 category modules, and 14 modules importbase.zod.js. ⇒ pointing the recursion point at the union makesbase.zod.tsdepend onindex.zod.ts, which depends transitively back onbase.zod.ts.z.lazydefers the evaluation, not the module graph, so this needs a deliberate break — a late-binding holder thatindex.zod.tsfills at load, or relocating the union. ⛔ Do not discover this at implementation time; it is the reason this card is not a one-line diff.And one hard constraint, measured, that a naive implementation will get wrong
The recursion point must be recognised by identity on the exported wrapper, ⛔ never through
.unwrap(). Measured at the same commit:This is #7918's consequence ①, relocated by
os-samon #7935 (comment5563938784): the public handle is unstable for eight of the ten lazy exports, andSchemaNodeSchemais one of them. Identity on the export works today and is whatscripts/measure-strict-authoring-face.mjsalready relies on.Appetite — one dispatched card, one PR
In scope: the redirect, the module-cycle break, the 9 documents above, and a pin that a nested off-spec node is now refused (the #7869 reproduction, both directions).
⛔ Falls off the back rather than growing this card:
.strict()anywhere; R3, not R4, is its acceptance number.If the 9 do not fit, the card is wrong and comes back for re-pricing — ⛔ do not widen it in flight.
Grading notes
yes. This changes contract accept/reject behaviour at every depth below the root. finding(types):safeValidateSchemaenforces per-type component schemas on the ROOT node only — every nested child is matched against the passthroughBaseSchema, so a DECLARED key with a wrong-typed value passes at any depth #7869's triage (comment5555946183) already reached that verdict and asked for a census before the tightening could be priced: "先立一张 report-only 普查卡,量出「若逐类型 union 下沉到每一层,今天会新增多少个拒绝、分布在哪些 fixture 与文档语料里」。有了那个数字,收紧才是一次可定价的改动而不是一次赌博。" ⇒ That census is the table above; this card is the priced change it asked for.unknown— the single-or-list producer objectui#7069 named systematic is unauditable at the type level because of it #7760 (dispatched, in flight) touches the same ten recursive mirrors to give them an explicit INPUT type argument. Sequence after it or coordinate; ⛔ do not dispatch both ontobase.zod.tsat once.safeValidateSchemaenforces per-type component schemas on the ROOT node only — every nested child is matched against the passthroughBaseSchema, so a DECLARED key with a wrong-typed value passes at any depth #7869 (recording-only; no mechanism proposed there).Refs: #5250 (the ruling) · #7935 (this pricing) · #7581 / PR #7916 (the measurement) · #7869 (the finding) · #7760 (in flight, same files) · #7918 (the unstable public handle) · #6318
Generated by Claude Code