Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .changeset/5927-zod-mirror-group-a-widenings.md
Original file line numberDiff line numberDiff line change
@@ -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.
35 changes: 18 additions & 17 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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 ──────────────────────────────────────────────────────────── */
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/data-display.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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'),
Expand Down
6 changes: 3 additions & 3 deletions packages/types/src/zod/form.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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'),
Expand DownExpand Up@@ -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'),
Expand Down
4 changes: 2 additions & 2 deletions packages/types/src/zod/navigation.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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'),
});

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/objectql.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)'),
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/zod/views.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Loading