diff --git a/.changeset/dashboard-config-aria-retired-5852.md b/.changeset/dashboard-config-aria-retired-5852.md new file mode 100644 index 0000000000..9ce2a42a1a --- /dev/null +++ b/.changeset/dashboard-config-aria-retired-5852.md @@ -0,0 +1,42 @@ +--- +'@object-ui/types': minor +--- + +**Retired the designer-surface dashboard `aria` pair — `DashboardConfig.aria` and `DashboardConfigSchema.aria`** (objectui#5852). + +Both spellings are named verbatim above so a host can grep its own sources: the +retired member is `aria`, on the TypeScript interface `DashboardConfig` +(`@object-ui/types`, `designer.ts`) and on its Zod mirror `DashboardConfigSchema` +(`@object-ui/types/zod`). It declared `{ label?: string; description?: string }`. + +**Why.** The spellings `label`/`description` match neither `@objectstack/spec`'s +`AriaProps` vocabulary (`ariaLabel` / `ariaDescribedBy` / `role`) nor anything a +renderer maps, so no read point could have consumed them even in principle. +Re-measured on `main` at the retirement: zero `.aria` reads in +`packages/plugin-designer/src`, `packages/plugin-dashboard/src` and +`apps/console/src`; zero occurrences of either name anywhere in the `objectstack` +repo; and `DashboardConfigPanel.tsx` — the panel the interface's own doc comment +says it serves — imports neither name. + +**The two directions differ, and neither is a no-op:** + +- **TypeScript (a narrowed suggestion, not a compile break).** `DashboardConfig` + carries a `[key: string]: any` catch-all, so an existing `aria:` line still + compiles; what is gone is the editor suggestion and the false implication that + the key was part of the contract. +- **Zod (a behaviour change — read this one).** `aria` is now an ADR-0049 + retirement tombstone (`z.never().optional()`), following this package's + existing convention. Previously an authored `aria` was **accepted and + preserved** in `safeParse` output; it is now **refused by name**, with `aria` + in the issue path and a message telling the author to delete the key. A plain + deletion was deliberately not taken: `DashboardConfigSchema` is a bare + `z.object` with no `.strict()`, so deleting the key would have made an + authored `aria` **silently disappear** from the parsed output instead — a + quiet data loss in place of a loud refusal. + +**External caveat.** In-repo consumer count is zero, but that is not the npm +count: `@object-ui/types` is published, and stored dashboard configuration is +not reachable from this repo. A host that authored `aria` on a `DashboardConfig` +document will now see a validation error naming the key where it previously saw +a silently carried value. The remedy is to delete the key — it never reached a +renderer. diff --git a/packages/types/src/__tests__/dashboard-config.test.ts b/packages/types/src/__tests__/dashboard-config.test.ts index d4b5c81d5a..48ab5f4abd 100644 --- a/packages/types/src/__tests__/dashboard-config.test.ts +++ b/packages/types/src/__tests__/dashboard-config.test.ts @@ -62,7 +62,11 @@ describe('DashboardConfig TypeScript Types', () => { headerActions: [ { label: 'Export', action: 'export', icon: 'Download', variant: 'outline' }, ], - aria: { label: 'Sales dashboard', description: 'Interactive sales overview' }, + // `aria: { label, description }` REMOVED (objectui#5852): the member is + // gone from the `DashboardConfig` declaration. Kept here it would have + // gone on compiling through the interface's `[key: string]: any` + // catch-all while asserting the opposite of the contract — the + // green-wash objectui#5830 called out on the sibling member. }; expect(config.widgets).toHaveLength(1); expect(config.widgets![0].type).toBe('metric'); @@ -199,10 +203,44 @@ describe('DashboardConfig Zod Validation', () => { expect(result.success).toBe(true); }); - it('should validate aria accessibility attributes', () => { + // `should validate aria accessibility attributes` was FLIPPED, not deleted + // (objectui#5852). It asserted `success === true` for an authored `aria`. + // After the retirement a PLAIN DELETION would have kept it green — this + // schema is a bare `z.object` with no `.strict()`, so an undeclared key is + // accepted and silently stripped (measured; the same behaviour objectui#6068 + // recorded). That green would have meant "nothing looked", which is why the + // mirror carries a `z.never()` tombstone instead and this pin now asserts the + // refusal by name. + it('refuses the retired `aria` key by name, with the removal message', () => { const result = DashboardConfigSchema.safeParse({ aria: { label: 'Sales dashboard', description: 'Interactive overview' }, }); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find((i) => i.path.join('.') === 'aria'); + expect(issue, 'no issue at path `aria`').toBeTruthy(); + // The message is asserted, not just its existence: a `z.never()` with no + // `error` would refuse with zod's generic "expected never, received + // object", which names the key only via the path and tells the author + // nothing about what to do. The tombstone carries a real message. + expect(issue!.message).toMatch(/RETIRED \(objectui#5852\)/); + expect(issue!.message).toMatch(/delete the key/); + }); + + it('CONTROL: an arbitrary undeclared key is NOT refused — the red above is the tombstone, not strictness', () => { + // Without this control, the refusal above is equally consistent with the + // schema having become `.strict()`, which would refuse every unknown key. + const result = DashboardConfigSchema.safeParse({ objectui5852NotAKey: 'x' }); + expect(result.success).toBe(true); + // ...and it is dropped from the output, which is exactly what a plain + // deletion of `aria` would have silently done to an authored value. + expect(result.success && 'objectui5852NotAKey' in result.data).toBe(false); + }); + + it('CONTROL: a legal config still parses green — the tombstone narrowed nothing else', () => { + const result = DashboardConfigSchema.safeParse({ + title: 'Sales', columns: 12, showHeader: true, + }); expect(result.success).toBe(true); }); }); diff --git a/packages/types/src/designer.ts b/packages/types/src/designer.ts index 9d21a20156..1d7ae5b6da 100644 --- a/packages/types/src/designer.ts +++ b/packages/types/src/designer.ts @@ -620,12 +620,29 @@ export interface DashboardConfig { }>; // -- Accessibility --------------------------------------------------------- - - /** ARIA properties */ - aria?: { - label?: string; - description?: string; - }; + // + // `aria?: { label?, description? }` was DECLARED here until objectui#5852. + // It was never a contract: the spellings (`label`/`description`) match + // neither `@objectstack/spec`'s `AriaProps` (`ariaLabel` / `ariaDescribedBy` + // / `role`) nor anything a renderer maps, so no read point could have + // consumed it even in principle. Measured on `origin/main` at the retirement: + // zero `.aria` reads in `packages/plugin-designer/src`, + // `packages/plugin-dashboard/src` and `apps/console/src` (the same grep + // family finds the live `schema.aria` reads in `plugin-detail`'s + // `record-quick-actions.tsx` and `plugin-list`'s `ListView.tsx`), and zero + // occurrences of either name in the `objectstack` repo. + // + // `DashboardConfigPanel.tsx` — the panel this interface's own doc comment + // says it serves — imports `ConfigPanelSchema` from `@object-ui/components` + // and neither `DashboardConfig` nor `DashboardConfigSchema`, so the key + // documented an integration that does not exist. + // + // Note the `[key: string]: any` catch-all below still types an authored + // `aria` as `any`: this deletion removes the type-level SUGGESTION, not a + // key that ever rendered (same shape as objectui#5830 on + // `DashboardComponentSchema.aria`). The Zod twin does carry teeth — see the + // `z.never()` tombstone in `zod/complex.zod.ts`. Pinned by + // `__tests__/dashboard-config.test.ts`. /** Catch-all for additional properties */ [key: string]: any; diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index 6435a5958f..cf2accb6f4 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -563,6 +563,18 @@ export const DashboardWidgetConfigSchema = z.object({ * Dashboard Config Schema — Zod validator for DashboardConfigPanel data model. * * Validates the unified dashboard configuration used by create/edit workflows. + * + * The `aria` member is an ADR-0049 retirement tombstone (objectui#5852), + * following this package's convention (`data-display.zod.ts` + * `StaticTableColumnSchema`, the set `crud.zod.ts` `confirm` established): + * `z.never().optional()` REFUSES an authored value at parse time with the key + * named in the error path, rather than letting it be silently stripped the way + * an undeclared key would be on this non-`.strict()` object. Loud refusal is + * the ruled outcome — `aria` was accepted-and-preserved for as long as it was + * declared, so a plain deletion would have converted a preserved key into a + * silent drop. The TS twin (`../designer.ts` `DashboardConfig`) no longer + * declares it at all; both halves are pinned by + * `__tests__/dashboard-config.test.ts`. */ export const DashboardConfigSchema = z.object({ id: z.string().optional().describe('Dashboard identifier'), @@ -592,10 +604,7 @@ export const DashboardConfigSchema = z.object({ icon: z.string().optional(), variant: z.string().optional(), })).optional().describe('Header action buttons'), - aria: z.object({ - label: z.string().optional(), - description: z.string().optional(), - }).optional().describe('ARIA accessibility attributes'), + aria: z.never({ error: 'RETIRED (objectui#5852) — `aria` is no longer part of DashboardConfig; delete the key. The `{ label, description }` spellings matched no renderer vocabulary and nothing ever read them.' }).optional().describe('RETIRED (objectui#5852) — the `{ label, description }` spellings matched no renderer vocabulary and no read point ever consumed them; delete the key. For real ARIA use the spec vocabulary (`ariaLabel` / `ariaDescribedBy` / `role`) on a surface that reads it.'), }); /**