Filed by the execution seat that landed #5927 group A (PR #6032, session session_01CSoz9uGhaaSgiq3hshtN7L, branch claude/issue-5927-zod-mirror-group-a-widenings). Unassigned and unlabelled on purpose — triage grades it.
Under the maintainer ruling recorded on #5927 (2026-08-24, verbatim 「四维分析一致的,接手你的建议。」), group A was executed by renderer measurement and groups B (disjoint vocabularies) and C (required-vs-optional) come back to the decision inbox with the renderer reading attached, not batch-edited. This card carries all eight readings. ⛔ Nothing here was edited.
Dedupe: searched the repo-scoped open-issue listing for these mirror/key names; the only prior art is #5927 itself (which carries the framework, not the per-key rulings) and #5853 (the opposite direction — mirror wider than the declaration — which is currently back at needs-user-decision and is not covered here).
Re-measured on origin/main d2fb6efb4 with #5684's derived construction over all 163 registered pairs, not read off #5927's table. All eight rows below are live drift today; the six remaining KnownDrift rows that are not listed here are group D (rides #2231) and group E (PageNodeSchema.pageType, sanctioned divergence).
The method, and the counter-probe that makes a null reading trustworthy
"The renderer implements it" is a claim about running code. For each key the read site was searched in the renderer that actually consumes that schema. Wherever the reading below is "no read site", the same search is shown finding sibling keys on the same surface that the renderer does read — a mis-anchored search looks exactly like a dead spelling, and this is what separates the two.
Group B — disjoint vocabularies
B1. form.zod.ts#FormSchema.mode — TS disabled | read | edit vs mirror create | edit | view
Reading: neither vocabulary has a read site on this surface. The ui:form renderer (packages/components/src/renderers/form/form.tsx) destructures 16 keys from schema at :975-1002 — defaultValues, previousValues, fields, submitLabel, cancelLabel, showCancel, showSubmit, layout, columns, the four handlers, resetOnSubmit, validationMode, disabled — and mode is not among them. grep 'schema\.mode' over the whole file returns 0.
Counter-probe: the same grep pattern finds a real mode read site elsewhere — plugin-form/src/DrawerForm.tsx:257,367,391,399,492,494 reads schema.mode === 'create' / 'edit' / 'view'. But that is DrawerForm's own schema, a different type; it is not FormSchema. So the search can find such a site when one exists, and finds none for FormSchema.mode.
Implication for the ruling: this may not be "pick the live side" at all — both vocabularies may be inert here, which would make it an ADR-0049 candidate on both sides rather than a widen/narrow choice. The create|edit|view spelling looks like it was copied from DrawerForm's surface.
B2. navigation.zod.ts#HeaderBarSchema.variant — TS default | bordered | floating vs mirror default | bordered | transparent
Reading: no read site for either vocabulary. packages/components/src/renderers/navigation/header-bar.tsx never mentions variant; grep -n variant over the file returns nothing.
Counter-probe, same method on the same file: grep -o 'schema\.[a-zA-Z]*' finds schema.crumbs (:68), schema.search (:80), schema.actions (:95), schema.rightContent (:98). The renderer reads four of HeaderBarSchema's keys and none of them is variant.
Implication: as with B1, this reads as dead-on-both-sides rather than a two-way choice.
B3. complex.zod.ts#FilterFieldSchema.operators — TS carries is_empty / is_not_empty, mirror carries is_null / is_not_null
Reading: both spellings are live in the codebase, but at different layers, and neither is read through this schema.
is_empty / is_not_empty are the authoring/UI spelling: plugin-view/src/config/view-config-utils.ts:70-71 maps them to isEmpty / isNotEmpty; app-shell/src/views/metadata-admin/widgets.tsx:2056 does the same.
is_null / is_not_null are the query-AST spelling: core/src/query/query-ast.ts:149,177, core/src/utils/filter-converter.ts:141,145, permissions/src/evaluator.ts:154.
core/src/utils/filter-converter.ts:222 documents them as siblings ("is_empty / is_null, whose direction comes from the operator NAME"), and app-shell/src/views/viewFilterFold.ts:88 lists all four together.
- But the
ui:filter-builder renderer never reads operators. packages/components/src/renderers/complex/filter-builder.tsx is 89 lines and reads exactly schema.fields, schema.label, schema.name, schema.value, schema.wrapperClass — that list is the counter-probe, and operators is absent from it.
Implication: the ruling is not "which spelling is dead" — both are implemented, on other surfaces. It is "which layer's vocabulary should the FilterField authoring surface speak", plus the separate question of whether this key is authorable at all given its renderer ignores it.
B4. data-display.zod.ts#DataTableSchema.rowActions — TS boolean vs mirror any[]
Reading: the renderer implements the boolean and never reads array elements. packages/components/src/renderers/complex/data-table.tsx — rowActions = false default (:708), {rowActions && ( (:1935, :2319), (rowActions ? 1 : 0) in two colSpan computations (:1958, :2363), the designer input declared { name: 'rowActions', type: 'boolean', ... } (:2521), defaultProps rowActions: true (:2534), and the docblock example at :667 writes "rowActions": true. Nothing indexes into it.
Counter-probe on the same file and same method: selectable on the same schema does have a value-reading path (resolveSelectionMode, :592, branching on 'single'), so this grep does surface value reads when they exist.
Implication: an authored array is truthy, so it "works" by accident while its contents are silently discarded — the mirror's any[] dialect is the side with no reader. Direction looks like withdrawing the array dialect from the mirror, which is a behaviour change for anyone currently authoring one, hence a ruling.
B5. form.zod.ts#CalendarSchema.defaultValue / .value — TS Date | Date[] vs mirror string | Date
Reading: the TS side matches the underlying control; the mirror's string limb does not. packages/components/src/renderers/form/calendar.tsx:17 is the whole read: selected={(schema.value || schema.defaultValue) as any}, passed to react-day-picker's Calendar with mode={(schema.mode || "single") as any} (:15). react-day-picker's selected takes a Date for single and Date[] for multiple — never an ISO string. The as any is what lets the mirror's string dialect through the type system while failing to preselect anything at runtime.
Also surfaced by this reading and worth folding into the ruling: the renderer offers mode: 'range' (:30), whose react-day-picker value shape is a DateRange object that neither side declares.
B6. complex.zod.ts#ChatbotSchema.body — TS SchemaNode | SchemaNode[] (inherited slot) vs mirror Record<string, unknown> ("additional API body params")
Reading: the two sides are not two dialects of one key — the mirror has taken a name the base schema owns, for a meaning the declaration already spells differently.
- The TS side is not
ChatbotSchema's own key: ChatbotSchema extends BaseSchema, and BaseSchema.body is SchemaNode | SchemaNode[] (packages/types/src/base.ts:189) — the rendered-slot key. It is implemented broadly as such: renderChildren(schema.body) in renderers/overlay/tooltip.tsx:31, renderers/data-display/badge.tsx:32, renderers/data-display/alert.tsx:22, renderers/navigation/sidebar.tsx:43,59,79,89,112.
- The API-params meaning already has its own declared key:
ChatbotSchema.requestBody?: Record<string, unknown> (packages/types/src/complex.ts:619), and that is what the chatbot renderer actually reads — packages/plugin-chatbot/src/renderer.tsx:87,305,438 all pass body: schema.requestBody into the request. renderer.tsx:29 lists the schema fields it consumes and names requestBody, not body.
- No read of
schema.body under the API-params meaning exists anywhere.
Implication: the direction here looks less like a coin-flip than #5927 assumed — the mirror's body override is unread, and dropping it restores the inherited slot meaning that the rest of the renderers implement. It is still a ruling because removing it changes what the published validator accepts.
Group C — the mirror REQUIRES a key the declaration marks optional
Both rows read the same way, and both readings point at the mirror as the stale side: the renderer implements the absent case in running code.
C1. form.zod.ts#ComboboxSchema.options
TS declares options?: ComboboxOption[] (packages/types/src/form.ts:1298); the mirror requires it. packages/components/src/renderers/form/combobox.tsx:25 reads options={schema.options || []} — an explicit absent-case fallback that renders an empty combobox.
C2. form.zod.ts#CommandSchema.groups
TS declares groups?: CommandGroup[] (packages/types/src/form.ts:1373); the mirror requires it. packages/components/src/renderers/form/command.tsx:31 reads schema.groups?.map(...) — optional chaining, again the absent case handled deliberately.
So { "type": "combobox" } type-checks, renders, and is refused at parse time. Making these two .optional() would agree with both the declaration and the running code; it is listed rather than done because it changes what the published validator accepts, and #5927's ruling reserved group C for the inbox.
Where these are recorded now
packages/types/src/__tests__/zod-mirror-parity.test.ts, KnownDrift. That ledger is shrink-only and pinned to each pair's exact drifted key set, so whichever way each row is ruled, the correcting PR deletes or shrinks its row in the same change. After #5927 group A landed it holds 13 rows: the 8 above, plus group D's 4 (DashboardComponentSchema, DashboardWidgetSchema, FilterBuilderSchema, CRUDSchema — riding #2231) and group E's PageNodeSchema. Note FilterBuilderSchema.fields is B3's drift seen through the element type, so ruling B3 closes it too.
Related: #5927 (the framework and group A), #6032 (group A's PR), #5684 (the guard), #4605 / #5680 (the BaseSchema precedent for measuring the renderer), ADR-0049 (enforce-or-remove, the route for a dead spelling), #2231 (spec unification), #5853 (the opposite direction, currently undecided).
Generated by Claude Code
Filed by the execution seat that landed #5927 group A (PR #6032, session
session_01CSoz9uGhaaSgiq3hshtN7L, branchclaude/issue-5927-zod-mirror-group-a-widenings). Unassigned and unlabelled on purpose — triage grades it.Under the maintainer ruling recorded on #5927 (2026-08-24, verbatim 「四维分析一致的,接手你的建议。」), group A was executed by renderer measurement and groups B (disjoint vocabularies) and C (required-vs-optional) come back to the decision inbox with the renderer reading attached, not batch-edited. This card carries all eight readings. ⛔ Nothing here was edited.
Dedupe: searched the repo-scoped open-issue listing for these mirror/key names; the only prior art is #5927 itself (which carries the framework, not the per-key rulings) and #5853 (the opposite direction — mirror wider than the declaration — which is currently back at
needs-user-decisionand is not covered here).Re-measured on
origin/maind2fb6efb4with #5684's derived construction over all 163 registered pairs, not read off #5927's table. All eight rows below are live drift today; the six remainingKnownDriftrows that are not listed here are group D (rides #2231) and group E (PageNodeSchema.pageType, sanctioned divergence).The method, and the counter-probe that makes a null reading trustworthy
"The renderer implements it" is a claim about running code. For each key the read site was searched in the renderer that actually consumes that schema. Wherever the reading below is "no read site", the same search is shown finding sibling keys on the same surface that the renderer does read — a mis-anchored search looks exactly like a dead spelling, and this is what separates the two.
Group B — disjoint vocabularies
B1.
form.zod.ts#FormSchema.mode— TSdisabled | read | editvs mirrorcreate | edit | viewReading: neither vocabulary has a read site on this surface. The
ui:formrenderer (packages/components/src/renderers/form/form.tsx) destructures 16 keys fromschemaat:975-1002—defaultValues,previousValues,fields,submitLabel,cancelLabel,showCancel,showSubmit,layout,columns, the four handlers,resetOnSubmit,validationMode,disabled— andmodeis not among them.grep 'schema\.mode'over the whole file returns 0.Counter-probe: the same grep pattern finds a real
moderead site elsewhere —plugin-form/src/DrawerForm.tsx:257,367,391,399,492,494readsschema.mode === 'create'/'edit'/'view'. But that isDrawerForm's own schema, a different type; it is notFormSchema. So the search can find such a site when one exists, and finds none forFormSchema.mode.Implication for the ruling: this may not be "pick the live side" at all — both vocabularies may be inert here, which would make it an ADR-0049 candidate on both sides rather than a widen/narrow choice. The
create|edit|viewspelling looks like it was copied fromDrawerForm's surface.B2.
navigation.zod.ts#HeaderBarSchema.variant— TSdefault | bordered | floatingvs mirrordefault | bordered | transparentReading: no read site for either vocabulary.
packages/components/src/renderers/navigation/header-bar.tsxnever mentionsvariant;grep -n variantover the file returns nothing.Counter-probe, same method on the same file:
grep -o 'schema\.[a-zA-Z]*'findsschema.crumbs(:68),schema.search(:80),schema.actions(:95),schema.rightContent(:98). The renderer reads four ofHeaderBarSchema's keys and none of them isvariant.Implication: as with B1, this reads as dead-on-both-sides rather than a two-way choice.
B3.
complex.zod.ts#FilterFieldSchema.operators— TS carriesis_empty/is_not_empty, mirror carriesis_null/is_not_nullReading: both spellings are live in the codebase, but at different layers, and neither is read through this schema.
is_empty/is_not_emptyare the authoring/UI spelling:plugin-view/src/config/view-config-utils.ts:70-71maps them toisEmpty/isNotEmpty;app-shell/src/views/metadata-admin/widgets.tsx:2056does the same.is_null/is_not_nullare the query-AST spelling:core/src/query/query-ast.ts:149,177,core/src/utils/filter-converter.ts:141,145,permissions/src/evaluator.ts:154.core/src/utils/filter-converter.ts:222documents them as siblings ("is_empty/is_null, whose direction comes from the operator NAME"), andapp-shell/src/views/viewFilterFold.ts:88lists all four together.ui:filter-builderrenderer never readsoperators.packages/components/src/renderers/complex/filter-builder.tsxis 89 lines and reads exactlyschema.fields,schema.label,schema.name,schema.value,schema.wrapperClass— that list is the counter-probe, andoperatorsis absent from it.Implication: the ruling is not "which spelling is dead" — both are implemented, on other surfaces. It is "which layer's vocabulary should the
FilterFieldauthoring surface speak", plus the separate question of whether this key is authorable at all given its renderer ignores it.B4.
data-display.zod.ts#DataTableSchema.rowActions— TSbooleanvs mirrorany[]Reading: the renderer implements the boolean and never reads array elements.
packages/components/src/renderers/complex/data-table.tsx—rowActions = falsedefault (:708),{rowActions && ((:1935,:2319),(rowActions ? 1 : 0)in twocolSpancomputations (:1958,:2363), the designer input declared{ name: 'rowActions', type: 'boolean', ... }(:2521),defaultPropsrowActions: true(:2534), and the docblock example at:667writes"rowActions": true. Nothing indexes into it.Counter-probe on the same file and same method:
selectableon the same schema does have a value-reading path (resolveSelectionMode,:592, branching on'single'), so this grep does surface value reads when they exist.Implication: an authored array is truthy, so it "works" by accident while its contents are silently discarded — the mirror's
any[]dialect is the side with no reader. Direction looks like withdrawing the array dialect from the mirror, which is a behaviour change for anyone currently authoring one, hence a ruling.B5.
form.zod.ts#CalendarSchema.defaultValue/.value— TSDate | Date[]vs mirrorstring | DateReading: the TS side matches the underlying control; the mirror's
stringlimb does not.packages/components/src/renderers/form/calendar.tsx:17is the whole read:selected={(schema.value || schema.defaultValue) as any}, passed to react-day-picker'sCalendarwithmode={(schema.mode || "single") as any}(:15). react-day-picker'sselectedtakes aDateforsingleandDate[]formultiple— never an ISO string. Theas anyis what lets the mirror's string dialect through the type system while failing to preselect anything at runtime.Also surfaced by this reading and worth folding into the ruling: the renderer offers
mode: 'range'(:30), whose react-day-picker value shape is aDateRangeobject that neither side declares.B6.
complex.zod.ts#ChatbotSchema.body— TSSchemaNode | SchemaNode[](inherited slot) vs mirrorRecord<string, unknown>("additional API body params")Reading: the two sides are not two dialects of one key — the mirror has taken a name the base schema owns, for a meaning the declaration already spells differently.
ChatbotSchema's own key:ChatbotSchema extends BaseSchema, andBaseSchema.bodyisSchemaNode | SchemaNode[](packages/types/src/base.ts:189) — the rendered-slot key. It is implemented broadly as such:renderChildren(schema.body)inrenderers/overlay/tooltip.tsx:31,renderers/data-display/badge.tsx:32,renderers/data-display/alert.tsx:22,renderers/navigation/sidebar.tsx:43,59,79,89,112.ChatbotSchema.requestBody?: Record<string, unknown>(packages/types/src/complex.ts:619), and that is what the chatbot renderer actually reads —packages/plugin-chatbot/src/renderer.tsx:87,305,438all passbody: schema.requestBodyinto the request.renderer.tsx:29lists the schema fields it consumes and namesrequestBody, notbody.schema.bodyunder the API-params meaning exists anywhere.Implication: the direction here looks less like a coin-flip than #5927 assumed — the mirror's
bodyoverride is unread, and dropping it restores the inherited slot meaning that the rest of the renderers implement. It is still a ruling because removing it changes what the published validator accepts.Group C — the mirror REQUIRES a key the declaration marks optional
Both rows read the same way, and both readings point at the mirror as the stale side: the renderer implements the absent case in running code.
C1.
form.zod.ts#ComboboxSchema.optionsTS declares
options?: ComboboxOption[](packages/types/src/form.ts:1298); the mirror requires it.packages/components/src/renderers/form/combobox.tsx:25readsoptions={schema.options || []}— an explicit absent-case fallback that renders an empty combobox.C2.
form.zod.ts#CommandSchema.groupsTS declares
groups?: CommandGroup[](packages/types/src/form.ts:1373); the mirror requires it.packages/components/src/renderers/form/command.tsx:31readsschema.groups?.map(...)— optional chaining, again the absent case handled deliberately.So
{ "type": "combobox" }type-checks, renders, and is refused at parse time. Making these two.optional()would agree with both the declaration and the running code; it is listed rather than done because it changes what the published validator accepts, and #5927's ruling reserved group C for the inbox.Where these are recorded now
packages/types/src/__tests__/zod-mirror-parity.test.ts,KnownDrift. That ledger is shrink-only and pinned to each pair's exact drifted key set, so whichever way each row is ruled, the correcting PR deletes or shrinks its row in the same change. After #5927 group A landed it holds 13 rows: the 8 above, plus group D's 4 (DashboardComponentSchema,DashboardWidgetSchema,FilterBuilderSchema,CRUDSchema— riding #2231) and group E'sPageNodeSchema. NoteFilterBuilderSchema.fieldsis B3's drift seen through the element type, so ruling B3 closes it too.Related: #5927 (the framework and group A), #6032 (group A's PR), #5684 (the guard), #4605 / #5680 (the
BaseSchemaprecedent for measuring the renderer), ADR-0049 (enforce-or-remove, the route for a dead spelling), #2231 (spec unification), #5853 (the opposite direction, currently undecided).Generated by Claude Code