From f192b4d72be4067762689db092c9ee6b36a62b6e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 13:17:29 +0000 Subject: [PATCH 1/2] fix(types): widen seven zod mirrors the renderer already implements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Group A of objectui#5927 — the strict-widening class, where the TS declaration is a superset and the hand-written zod mirror is simply behind. Each key was decided by MEASURING the renderer, per the #4605 precedent, not by matching the declaration. Refs #5927 --- .../src/__tests__/zod-mirror-parity.test.ts | 35 ++++++++++--------- packages/types/src/zod/data-display.zod.ts | 2 +- packages/types/src/zod/form.zod.ts | 6 ++-- packages/types/src/zod/navigation.zod.ts | 4 +-- packages/types/src/zod/objectql.zod.ts | 2 +- packages/types/src/zod/views.zod.ts | 2 +- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 9701fed226..5af90c03c0 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -47,13 +47,22 @@ * * ## KNOWN_DRIFT is a ratchet, not a waiver * - * 17 of the 163 pairs carry drift TODAY (measured, not assumed). Each is + * 13 of the 163 pairs carry drift TODAY (measured, not assumed). Each is * pinned to its EXACT drifted key set, so the entry fails when new drift appears on * that mirror AND when the recorded drift is fixed — a stale entry cannot rot - * quietly. Correcting them is not one change: the pairs below split into strict - * widenings, DISJOINT vocabularies where one side is dead, and at least one - * DELIBERATE divergence that must stay expressible (`PageNodeSchema.pageType`). - * Each carries its measurement and its reason inline. + * quietly. Correcting them is not one change: the pairs below split into DISJOINT + * vocabularies where one side is dead, required-vs-optional mismatches, structural + * pairs that ride @objectstack/spec unification (objectui#2231), and one DELIBERATE + * divergence that must stay expressible (`PageNodeSchema.pageType`). Each carries + * its measurement and its reason inline. + * + * It was 17 until objectui#5927 landed the seven STRICT WIDENINGS (group A of that + * card's grouping) — the class where the TS side is simply a superset and the + * renderer was measured to implement the missing spellings. Four entries left the + * ledger outright (`SelectSchema`, `ButtonGroupSchema`, `ObjectChartSchema`, + * `ViewSwitcherSchema`) and two shrank to the keys that are NOT widenings + * (`DataTableSchema` kept `rowActions`, `FormSchema` kept `fields`/`mode`). The + * remaining classes are rulings, not edits, and are deliberately still here. */ import { describe, it, expect } from 'vitest'; @@ -498,28 +507,20 @@ interface KnownDrift { 'complex.zod.ts#FilterFieldSchema': 'operators'; /** TS declares an index signature whose value type includes `undefined`; the mirror`s `z.record` value type does not. The mirror refuses `{ create: undefined }` only. */ 'crud.zod.ts#CRUDSchema': 'operations'; - /** `selectable`: TS `boolean | 'single' | 'multiple'` vs mirror `boolean` (strict widening). `rowActions`: TS `boolean` vs mirror `any[]` — DISJOINT, a ruling. */ - 'data-display.zod.ts#DataTableSchema': 'selectable' | 'rowActions'; + /** DISJOINT: TS declares `rowActions?: boolean` (show the column or not), the mirror declares `any[]` (the actions themselves). One of the two is dead; which is a ruling. (`selectable` was a second drifted key here until objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` — `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements `'single'` as a real mode.) */ + 'data-display.zod.ts#DataTableSchema': 'rowActions'; /** DISJOINT: TS `Date | Date[]`, mirror `string | Date`. The mirror refuses `Date[]`; the TS side refuses the ISO string the mirror accepts. */ 'form.zod.ts#CalendarSchema': 'defaultValue' | 'value'; /** OPTIONALITY: TS declares `options?`, the mirror REQUIRES it. Whether authoring a combobox without options is legal is a ruling. */ 'form.zod.ts#ComboboxSchema': 'options'; /** OPTIONALITY: TS declares `groups?`, the mirror REQUIRES it. */ 'form.zod.ts#CommandSchema': 'groups'; - /** DISJOINT on `mode`: TS `disabled|read|edit`, mirror `create|edit|view`. `validationMode` is a strict widening (`onTouched`, `all` missing from the mirror). `fields` is inherited element drift. */ - 'form.zod.ts#FormSchema': 'fields' | 'mode' | 'validationMode'; - /** strict widening: TS `string | number | boolean`, mirror `string | number` — the mirror refuses a boolean option value. */ - 'form.zod.ts#SelectSchema': 'defaultValue' | 'value'; + /** DISJOINT on `mode`: TS `disabled|read|edit`, mirror `create|edit|view`. `fields` is inherited element drift. (`validationMode` was a third drifted key until objectui#5927 widened it to react-hook-form's full `mode` vocabulary — `useForm({ mode })` in `renderers/form/form.tsx` forwards it verbatim and RHF implements `onTouched`/`all` as real branches.) */ + 'form.zod.ts#FormSchema': 'fields' | 'mode'; /** `pageType` is a DELIBERATE divergence, documented at `PageVisualizationAlias` (`../layout.ts`): the TS side retains five visualization names as a sanctioned local extension while the mirror takes the spec's vocabulary by reference, which repudiates them. Widening the mirror would re-add spellings the spec rejects. */ 'layout.zod.ts#PageNodeSchema': 'slots' | 'pageType'; - /** strict widening: the mirror is missing `link`/`secondary`/`destructive`/`ghost` on `variant` and `icon` on `size`. */ - 'navigation.zod.ts#ButtonGroupSchema': 'variant' | 'size'; /** DISJOINT: TS declares `floating`, the mirror declares `transparent`. One of the two renders nothing. */ 'navigation.zod.ts#HeaderBarSchema': 'variant'; - /** strict widening: the mirror is missing `column`, `horizontal-bar` and `donut`. */ - 'objectql.zod.ts#ObjectChartSchema': 'chartType'; - /** strict widening: `ViewType` (`../views.ts`) carries `chart`, which the mirror`s inline vocabulary omits. `views` is the same omission inside the element type. */ - 'views.zod.ts#ViewSwitcherSchema': 'defaultView' | 'views' | 'activeView'; } /* ── The invariant ──────────────────────────────────────────────────────────── */ diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index 5225a30567..96b4847182 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -177,7 +177,7 @@ export const DataTableSchema = BaseSchema.extend({ pageSize: z.number().optional().describe('Default page size'), pageSizeOptions: z.array(z.number()).optional().describe('Options for the rows-per-page selector (defaults to 5/10/20/50/100).'), searchable: z.boolean().optional().describe('Enable search'), - selectable: z.boolean().optional().describe('Enable row selection'), + selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'), sortable: z.boolean().optional().describe('Enable sorting'), exportable: z.boolean().optional().describe('Enable data export'), rowActions: z.array(z.any()).optional().describe('Row action buttons'), diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts index 1166198105..6637111b4a 100644 --- a/packages/types/src/zod/form.zod.ts +++ b/packages/types/src/zod/form.zod.ts @@ -244,8 +244,8 @@ export const SelectSchema = BaseSchema.extend({ name: z.string().optional().describe('Field name for form submission'), label: z.string().optional().describe('Select label'), placeholder: z.string().optional().describe('Placeholder text'), - defaultValue: z.union([z.string(), z.number()]).optional().describe('Default value'), - value: z.union([z.string(), z.number()]).optional().describe('Controlled value'), + defaultValue: z.union([z.string(), z.number(), z.boolean()]).optional().describe('Default value'), + value: z.union([z.string(), z.number(), z.boolean()]).optional().describe('Controlled value'), options: z.array(SelectOptionSchema).describe('Select options'), required: z.boolean().optional().describe('Whether field is required'), disabled: z.boolean().optional().describe('Whether field is disabled'), @@ -630,7 +630,7 @@ export const FormSchema = BaseSchema.extend({ showCancel: z.boolean().optional().describe('Show cancel button'), layout: z.enum(['vertical', 'horizontal', 'grid']).optional().describe('Form layout'), columns: z.number().optional().describe('Number of columns (for grid layout)'), - validationMode: z.enum(['onSubmit', 'onChange', 'onBlur']).optional().describe('Validation mode'), + validationMode: z.enum(['onSubmit', 'onChange', 'onBlur', 'onTouched', 'all']).optional().describe('Validation mode'), resetOnSubmit: z.boolean().optional().describe('Reset form on successful submit'), disabled: z.boolean().optional().describe('Disable entire form'), mode: z.enum(['create', 'edit', 'view']).optional().describe('Form mode'), diff --git a/packages/types/src/zod/navigation.zod.ts b/packages/types/src/zod/navigation.zod.ts index 72e953c901..04798cd01d 100644 --- a/packages/types/src/zod/navigation.zod.ts +++ b/packages/types/src/zod/navigation.zod.ts @@ -154,8 +154,8 @@ export const ButtonGroupButtonSchema = z.object({ export const ButtonGroupSchema = BaseSchema.extend({ type: z.literal('button-group'), buttons: z.array(ButtonGroupButtonSchema).optional().describe('Group buttons'), - variant: z.enum(['default', 'outline']).optional().describe('Button group variant'), - size: z.enum(['default', 'sm', 'lg']).optional().describe('Button group size'), + variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost', 'link']).optional().describe('Button group variant'), + size: z.enum(['default', 'sm', 'lg', 'icon']).optional().describe('Button group size'), }); /** diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 0e0002f71e..9daedd0715 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -668,7 +668,7 @@ export const ObjectChartSchema = BaseSchema.extend({ // Legacy inline path (objectName + aggregate). Optional now that a chart may // instead bind to a semantic-layer dataset (ADR-0021, #1890). objectName: z.string().optional().describe('ObjectQL object name (legacy inline path)'), - chartType: z.enum(['bar', 'line', 'pie', 'area', 'scatter']).describe('Chart type'), + chartType: z.enum(['bar', 'column', 'horizontal-bar', 'line', 'area', 'pie', 'donut', 'scatter']).describe('Chart type'), xAxisField: z.string().optional().describe('X axis field (legacy inline path)'), yAxisFields: z.array(z.string()).optional().describe('Y axis fields (legacy)'), aggregation: z.enum(['cardinality', 'sum', 'avg', 'min', 'max']).optional().describe('Aggregation (legacy)'), diff --git a/packages/types/src/zod/views.zod.ts b/packages/types/src/zod/views.zod.ts index 0a9f0c365a..febc03737b 100644 --- a/packages/types/src/zod/views.zod.ts +++ b/packages/types/src/zod/views.zod.ts @@ -22,7 +22,7 @@ import { BaseSchema, SchemaNodeSchema } from './base.zod.js'; /** * View Type Schema */ -export const ViewTypeSchema = z.enum(['list', 'detail', 'grid', 'kanban', 'calendar', 'timeline', 'map', 'gallery', 'gantt', 'tree']).describe('View type'); +export const ViewTypeSchema = z.enum(['list', 'detail', 'grid', 'kanban', 'calendar', 'timeline', 'map', 'gallery', 'gantt', 'chart', 'tree']).describe('View type'); /** * Detail View Field Schema From 956940b1b498724007f9c7f1b1fe3d926de75fc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 13:24:16 +0000 Subject: [PATCH 2/2] chore(changeset): record the seven zod-mirror widenings as a published-validator change Refs #5927 --- .../5927-zod-mirror-group-a-widenings.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .changeset/5927-zod-mirror-group-a-widenings.md diff --git a/.changeset/5927-zod-mirror-group-a-widenings.md b/.changeset/5927-zod-mirror-group-a-widenings.md new file mode 100644 index 0000000000..aae2128dd3 --- /dev/null +++ b/.changeset/5927-zod-mirror-group-a-widenings.md @@ -0,0 +1,47 @@ +--- +'@object-ui/types': minor +--- + +**`@object-ui/types/zod` now accepts seven spellings its own TypeScript declarations already declared** + +Seven keys across five hand-written zod mirrors refused values the published TS +types invite and the renderer implements — `declared !== enforced` on a published +validator. The mirrors are widened to their declarations. Nothing is narrowed and +nothing previously accepted is rejected, so this is additive for every author and +every host: schemas that parsed before still parse. + +The newly-accepted spellings, so a host can search for them: + +| schema (`@object-ui/types/zod`) | key | now also accepts | +|---|---|---| +| `ButtonGroupSchema` | `variant` | `secondary`, `destructive`, `ghost`, `link` | +| `ButtonGroupSchema` | `size` | `icon` | +| `ObjectChartSchema` | `chartType` | `column`, `horizontal-bar`, `donut` | +| `FormSchema` | `validationMode` | `onTouched`, `all` | +| `SelectSchema` | `defaultValue`, `value` | `boolean` | +| `DataTableSchema` | `selectable` | `'single'`, `'multiple'` (alongside `boolean`) | +| `ViewSwitcherSchema` / `ViewTypeSchema` | `defaultView`, `activeView`, `views[].type` | `chart` | + +**Each one was decided by measuring the renderer, not by matching the declaration.** +Widening a mirror to its declaration is only correct where the running code +implements the missing spelling; where a spelling is dead, the right fix is to +withdraw it from the declaration (ADR-0049 enforce-or-remove), not to teach the +validator to accept something that renders nothing. The read sites: +`buttonVariants`' `cva` map (`components/src/ui/button.tsx`) carries all six +variants and all four sizes; `AdvancedChartImpl` normalizes `column` to `bar`, +maps `horizontal-bar` to a real `BarChart` layout and gives `donut` its own inner +radius; `useForm({ mode })` hands `validationMode` straight to react-hook-form, +whose `isOnTouch` / `isOnAll` branches implement `onTouched` and `all`; +`toControlValue` / `matchOptionValue` (#3090) round-trip a boolean option value +with its type intact; `resolveSelectionMode` implements `'single'` as +replace-on-select with no select-all header, distinct from `'multiple'`; and +`chart` is a rendered view type with its own `case` in both `ListView` and +`ObjectView`. + +Consumer-visible type effect: `z.infer` of these schemas widens accordingly. +Widening an input contract cannot break a caller that was already passing a +narrower value, but code that exhaustively switches on the inferred union — e.g. +a `switch` over `chartType` with no `default` — will want the new arms. + +Refs objectui#5927 (group A of the 17 measured mirror drifts). The remaining +classes are rulings rather than edits and stay in the `KnownDrift` ledger.