Measured by the domain:ui execution seat while landing objectui#5684's parity guard (session 01EuPCi56cnGyykygi3z9w4m, branch claude/issue-5684-zod-mirror-drift-guard). Dedupe search run over the repo-scoped open-issue listing: no duplicate. #5853 is the same family but the OPPOSITE direction (mirror wider than the declaration) and is not covered here.
What was measured
#5684's guard applies one derived construction to 163 registered mirror/declaration pairs in packages/types/src/zod/: it reads each mirror's own .shape and asks, per key, whether the mirror would REFUSE the type the TS declaration declares. 146 pairs are clean. 17 carry live drift today, listed below with both sides printed from the TypeScript checker (not read off by eye).
Each one is declared != enforced on a published surface (@object-ui/types/zod): the validator refuses a spelling the published types invite.
Why this is a separate card
They are not one change. Each key is a two-way question and objectui#4605 answered it for BaseSchema only by measuring what the renderer actually reads:
- widen the mirror — correct when the TS side is authoritative and the renderer implements it;
- narrow the TS declaration — correct when the extra spelling is dead, which is an ADR-0049 enforce-or-remove removal, not a validator edit.
Widening every mirror to match its declaration without measuring the renderer would be consumer-side tolerance (AGENTS.md #0.1) and would publish spellings that render nothing. So this needs a per-key ruling, not a batch edit.
The 17, grouped by what the ruling turns on
A. Strict widenings — the TS side is a superset, the mirror is simply behind
| mirror |
key |
TS declares |
mirror accepts |
navigation.zod.ts#ButtonGroupSchema |
variant |
default | link | secondary | destructive | outline | ghost |
default | outline |
navigation.zod.ts#ButtonGroupSchema |
size |
default | icon | sm | lg |
default | sm | lg |
objectql.zod.ts#ObjectChartSchema |
chartType |
column | line | bar | horizontal-bar | area | pie | donut | scatter |
line | bar | area | pie | scatter |
form.zod.ts#FormSchema |
validationMode |
onChange | onSubmit | onBlur | onTouched | all |
onChange | onSubmit | onBlur |
form.zod.ts#SelectSchema |
defaultValue, value |
string | number | boolean |
string | number |
data-display.zod.ts#DataTableSchema |
selectable |
boolean | 'single' | 'multiple' |
boolean |
views.zod.ts#ViewSwitcherSchema |
defaultView, activeView, views |
ViewType (11 members, incl. chart) |
the same 10 spelled inline, chart missing |
B. DISJOINT vocabularies — one side is dead and the ruling picks which
| mirror |
key |
TS declares |
mirror accepts |
form.zod.ts#FormSchema |
mode |
disabled | read | edit |
create | edit | view |
navigation.zod.ts#HeaderBarSchema |
variant |
default | bordered | floating |
default | bordered | transparent |
complex.zod.ts#FilterFieldSchema |
operators |
FilterBuilderOperator with is_empty / is_not_empty |
the same list with is_null / is_not_null |
data-display.zod.ts#DataTableSchema |
rowActions |
boolean |
any[] |
form.zod.ts#CalendarSchema |
defaultValue, value |
Date | Date[] |
string | Date |
complex.zod.ts#ChatbotSchema |
body |
SchemaNode | SchemaNode[] (a rendered slot) |
Record<string, unknown> ("additional API body params") |
ChatbotSchema.body is the sharpest: the two sides do not merely differ in width, they mean different things under one key name.
C. Optionality — the mirror REQUIRES a key the declaration marks optional
| mirror |
key |
form.zod.ts#ComboboxSchema |
options |
form.zod.ts#CommandSchema |
groups |
Authoring { type: 'combobox' } with no options type-checks and is refused at parse time.
D. Structural / spec-derived — triage with #2231, not locally
complex.zod.ts#DashboardComponentSchema (aria, header, widgets, globalFilters — aria accepts only undefined on the mirror side), complex.zod.ts#DashboardWidgetSchema (options: the TS side declares unknown, so here the MIRROR is the stricter one), complex.zod.ts#FilterBuilderSchema (fields, inherited from FilterFieldSchema above), crud.zod.ts#CRUDSchema (operations: the TS index signature admits undefined in its value type, the mirror's z.record does not), form.zod.ts#FormSchema (fields, inherited element drift).
E. Deliberate divergence — must stay expressible, and does
layout.zod.ts#PageNodeSchema pageType. PageVisualizationAlias (packages/types/src/layout.ts) documents it outright: the five visualization names are retained on the TS side as a sanctioned local extension while the mirror takes the spec's vocabulary by reference, and the spec repudiates them. Widening the mirror here would re-add spellings the spec rejects. This entry should stay in the ledger with its reason rather than being "corrected". (slots on the same mirror is separate and unmeasured.)
Where they are recorded now
packages/types/src/__tests__/zod-mirror-parity.test.ts, KnownDrift. Each entry is pinned to its EXACT drifted key set, so new drift on a listed mirror turns red AND a stale entry turns red once its drift is corrected — the ledger cannot rot quietly, and removing an entry is the last step of correcting it.
Related: #5684 (the guard), #4605 and #5680 (the BaseSchema instance and its correction), #5186 (the FieldValidationRules instance — re-measured here and holding clean), #2231 (spec unification, which would dissolve group D), #5853 (the opposite direction).
Generated by Claude Code
Generated by Claude Code
Measured by the
domain:uiexecution seat while landing objectui#5684's parity guard (session01EuPCi56cnGyykygi3z9w4m, branchclaude/issue-5684-zod-mirror-drift-guard). Dedupe search run over the repo-scoped open-issue listing: no duplicate. #5853 is the same family but the OPPOSITE direction (mirror wider than the declaration) and is not covered here.What was measured
#5684's guard applies one derived construction to 163 registered mirror/declaration pairs in
packages/types/src/zod/: it reads each mirror's own.shapeand asks, per key, whether the mirror would REFUSE the type the TS declaration declares. 146 pairs are clean. 17 carry live drift today, listed below with both sides printed from the TypeScript checker (not read off by eye).Each one is
declared != enforcedon a published surface (@object-ui/types/zod): the validator refuses a spelling the published types invite.Why this is a separate card
They are not one change. Each key is a two-way question and objectui#4605 answered it for
BaseSchemaonly by measuring what the renderer actually reads:Widening every mirror to match its declaration without measuring the renderer would be consumer-side tolerance (AGENTS.md #0.1) and would publish spellings that render nothing. So this needs a per-key ruling, not a batch edit.
The 17, grouped by what the ruling turns on
A. Strict widenings — the TS side is a superset, the mirror is simply behind
navigation.zod.ts#ButtonGroupSchemavariantdefault | link | secondary | destructive | outline | ghostdefault | outlinenavigation.zod.ts#ButtonGroupSchemasizedefault | icon | sm | lgdefault | sm | lgobjectql.zod.ts#ObjectChartSchemachartTypecolumn | line | bar | horizontal-bar | area | pie | donut | scatterline | bar | area | pie | scatterform.zod.ts#FormSchemavalidationModeonChange | onSubmit | onBlur | onTouched | allonChange | onSubmit | onBlurform.zod.ts#SelectSchemadefaultValue,valuestring | number | booleanstring | numberdata-display.zod.ts#DataTableSchemaselectableboolean | 'single' | 'multiple'booleanviews.zod.ts#ViewSwitcherSchemadefaultView,activeView,viewsViewType(11 members, incl.chart)chartmissingB. DISJOINT vocabularies — one side is dead and the ruling picks which
form.zod.ts#FormSchemamodedisabled | read | editcreate | edit | viewnavigation.zod.ts#HeaderBarSchemavariantdefault | bordered | floatingdefault | bordered | transparentcomplex.zod.ts#FilterFieldSchemaoperatorsFilterBuilderOperatorwithis_empty/is_not_emptyis_null/is_not_nulldata-display.zod.ts#DataTableSchemarowActionsbooleanany[]form.zod.ts#CalendarSchemadefaultValue,valueDate | Date[]string | Datecomplex.zod.ts#ChatbotSchemabodySchemaNode | SchemaNode[](a rendered slot)Record<string, unknown>("additional API body params")ChatbotSchema.bodyis the sharpest: the two sides do not merely differ in width, they mean different things under one key name.C. Optionality — the mirror REQUIRES a key the declaration marks optional
form.zod.ts#ComboboxSchemaoptionsform.zod.ts#CommandSchemagroupsAuthoring
{ type: 'combobox' }with nooptionstype-checks and is refused at parse time.D. Structural / spec-derived — triage with #2231, not locally
complex.zod.ts#DashboardComponentSchema(aria,header,widgets,globalFilters—ariaaccepts onlyundefinedon the mirror side),complex.zod.ts#DashboardWidgetSchema(options: the TS side declaresunknown, so here the MIRROR is the stricter one),complex.zod.ts#FilterBuilderSchema(fields, inherited fromFilterFieldSchemaabove),crud.zod.ts#CRUDSchema(operations: the TS index signature admitsundefinedin its value type, the mirror'sz.recorddoes not),form.zod.ts#FormSchema(fields, inherited element drift).E. Deliberate divergence — must stay expressible, and does
layout.zod.ts#PageNodeSchemapageType.PageVisualizationAlias(packages/types/src/layout.ts) documents it outright: the five visualization names are retained on the TS side as a sanctioned local extension while the mirror takes the spec's vocabulary by reference, and the spec repudiates them. Widening the mirror here would re-add spellings the spec rejects. This entry should stay in the ledger with its reason rather than being "corrected". (slotson the same mirror is separate and unmeasured.)Where they are recorded now
packages/types/src/__tests__/zod-mirror-parity.test.ts,KnownDrift. Each entry is pinned to its EXACT drifted key set, so new drift on a listed mirror turns red AND a stale entry turns red once its drift is corrected — the ledger cannot rot quietly, and removing an entry is the last step of correcting it.Related: #5684 (the guard), #4605 and #5680 (the
BaseSchemainstance and its correction), #5186 (theFieldValidationRulesinstance — re-measured here and holding clean), #2231 (spec unification, which would dissolve group D), #5853 (the opposite direction).Generated by Claude Code
Generated by Claude Code