Skip to content

17 zod mirrors refuse spellings their TypeScript declarations declare — each needs a widen-the-mirror / narrow-the-declaration ruling #5927

Description

@claude

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, globalFiltersaria 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queue

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions