diff --git a/.changeset/dashboard-aria-member-retired-5830.md b/.changeset/dashboard-aria-member-retired-5830.md new file mode 100644 index 0000000000..a104aca90f --- /dev/null +++ b/.changeset/dashboard-aria-member-retired-5830.md @@ -0,0 +1,16 @@ +--- +'@object-ui/types': minor +--- + +**Published TS surface narrowed:** `DashboardComponentSchema` no longer declares +the `aria` member (`{ ariaLabel?, ariaDescribedBy?, role? }`). Its doc comment +claimed alignment with `@objectstack/spec AriaPropsSchema`, but the spec removed +`dashboard.aria` at the #3896 audit close-out — `DashboardSchema.shape.aria` is +a tombstone that refuses any value and tells authors to delete the key — and no +dashboard renderer ever read `schema.aria` (objectui#5830). + +What an author loses is the **type-level suggestion** only: the key was already +refused at parse (the Zod twin inherits the spec tombstone by reference), and +`BaseSchema`'s index signature means an existing `aria:` line still compiles. +There is **no runtime behaviour change** — the key never rendered, and stored +documents carrying it already failed validation before this release. diff --git a/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts b/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts new file mode 100644 index 0000000000..8c14cb7f43 --- /dev/null +++ b/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts @@ -0,0 +1,84 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Retirement pin — `DashboardComponentSchema.aria` (objectui#5830). + * + * The spec removed `dashboard.aria` at the #3896 audit close-out ("no + * dashboard renderer ever applied it"): `DashboardSchema.shape.aria` in + * `@objectstack/spec/ui` is a tombstone that refuses any value and tells + * authors to delete the key. objectui's Zod twin inherits that refusal by + * reference (`SpecDashboardFields`, `zod/complex.zod.ts` — `aria` is not in + * its exclusion list), and `packages/plugin-dashboard/src` has no + * `schema.aria` read site (pinned by that package's + * `dashboardAuthoredInputs.test.tsx`). The TS interface was the last surface + * still DECLARING the key — under a comment claiming alignment with + * `AriaPropsSchema`, the opposite of the contract: the member-level instance + * of #4631's "declared surfaces disagree". + * + * What the deletion changes at the type level, stated honestly: `BaseSchema` + * carries `[key: string]: any`, so an authored `aria:` on a dashboard literal + * still COMPILES after the removal — it falls to the index signature. A + * `@ts-expect-error` pin on an authored literal therefore cannot stick here + * (unlike `default-children-retired-contract-twins.test.ts`, whose interface + * has no index signature). The pinnable effect is that `aria` stops being a + * DECLARED member: the probe below extracts the interface's literal key set — + * the index signature is filtered out by `string extends K` — and asserts + * `aria` is out while its former neighbours stay in. Real enforcement because + * `packages/types/tsconfig.test.json` is chained from this package's + * `type-check` script (#3009). + */ + +import { describe, it, expect } from 'vitest'; +import type { DashboardComponentSchema } from '../complex'; +import { DashboardComponentSchema as DashboardComponentZodSchema } from '../zod/index.zod'; + +// Literal (declared) keys of T: `string extends K` is true only for the index +// signature's key, so mapping it to `never` leaves exactly the authored members. +type DeclaredKeys = { [K in keyof T as string extends K ? never : K]: T[K] }; +type Declared = keyof DeclaredKeys; + +describe('the TS interface no longer declares `aria` (objectui#5830)', () => { + it('`aria` is not a declared member; the neighbours it stood beside still are', () => { + // Type-level pin, erased at runtime: if the member came back, the first + // annotation would collapse to `false` and this file would fail + // `type-check`. (Reverse-verified at the PR: with the member restored, + // `tsc -p tsconfig.test.json` goes red on exactly this line.) + const ariaNotDeclared: 'aria' extends Declared ? false : true = true; + // Positive controls through the same extraction: a probe that saw no + // members at all would also report `aria` absent. + const widgetsDeclared: 'widgets' extends Declared ? true : false = true; + const dateRangeDeclared: 'dateRange' extends Declared ? true : false = true; + expect(ariaNotDeclared && widgetsDeclared && dateRangeDeclared).toBe(true); + }); +}); + +describe('the Zod twin refuses the key by name — the spec tombstone, inherited by reference', () => { + const legal = { type: 'dashboard' as const, widgets: [] }; + + it('a legal dashboard parses green — the control for the refusal below', () => { + expect(DashboardComponentZodSchema.safeParse(legal).success).toBe(true); + }); + + it('an arbitrary unknown key does not refuse — the red below is the tombstone, not strictness', () => { + // BaseSchema is not `.strict()`: an undeclared key rides through (or is + // stripped) but never refuses. So a red `aria` can only come from the + // DECLARED tombstone flowing in from the spec — the thing being pinned. + const r = DashboardComponentZodSchema.safeParse({ ...legal, objectui5830NotAKey: 'x' }); + expect(r.success).toBe(true); + }); + + it('`aria` is refused at its own path, with the removal message', () => { + const r = DashboardComponentZodSchema.safeParse({ ...legal, aria: { ariaLabel: 'Ops' } }); + expect(r.success).toBe(false); + if (r.success) return; + const issue = r.error.issues.find((i) => i.path.join('.') === 'aria'); + expect(issue, 'no issue at path `aria`').toBeTruthy(); + expect(issue!.message).toMatch(/removed/); + }); +}); diff --git a/packages/types/src/__tests__/p1-spec-alignment.test.ts b/packages/types/src/__tests__/p1-spec-alignment.test.ts index a7a0f5b72b..e06f1449fe 100644 --- a/packages/types/src/__tests__/p1-spec-alignment.test.ts +++ b/packages/types/src/__tests__/p1-spec-alignment.test.ts @@ -606,17 +606,16 @@ describe('P1.6 i18n & ARIA Protocol Alignment', () => { expect(schema.aria?.live).toBe('polite'); }); - it('should accept ARIA props on DashboardComponentSchema', () => { - const schema: DashboardComponentSchema = { - type: 'dashboard', - widgets: [], - aria: { - ariaLabel: 'Sales Dashboard', - role: 'region', - }, - }; - expect(schema.aria?.ariaLabel).toBe('Sales Dashboard'); - }); + // `should accept ARIA props on DashboardComponentSchema` REMOVED + // (objectui#5830): the spec removed `dashboard.aria` at the #3896 audit + // close-out — `DashboardSchema.shape.aria` is a tombstone that refuses any + // value — and `packages/plugin-dashboard/src` has no `schema.aria` read site + // (measured for #5742; pinned by that package's + // `dashboardAuthoredInputs.test.tsx`). The declared member is gone from the + // TS interface too; had this test stayed, it would have kept passing through + // `BaseSchema`'s index signature while asserting the opposite of the + // contract. The removal is pinned by + // `dashboard-aria-retired-contract-twins.test.ts`. it('should accept ARIA props on PageNodeSchema', () => { const schema: PageNodeSchema = { diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index 25e25ebae2..acfc513120 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -844,15 +844,17 @@ export interface DashboardComponentSchema extends BaseSchema { defaultRange?: SpecDateRangeDefaultRange; allowCustomRange?: boolean; }; - /** - * ARIA accessibility attributes. - * Aligned with @objectstack/spec AriaPropsSchema. - */ - aria?: { - ariaLabel?: string; - ariaDescribedBy?: string; - role?: string; - }; + // `aria` was DECLARED here until objectui#5830, under a comment claiming + // alignment with @objectstack/spec AriaPropsSchema — by then the opposite of + // the contract: the spec removed `dashboard.aria` at the #3896 audit + // close-out (no dashboard renderer ever applied it), so + // `DashboardSchema.shape.aria` is a tombstone that refuses any value, the + // Zod twin (`zod/complex.zod.ts`) inherits that refusal through + // `SpecDashboardFields`, and `plugin-dashboard` has no `schema.aria` read + // site. Note `BaseSchema`'s index signature still types an authored `aria` + // as `any` — this deletion removes the type-level suggestion and the false + // parity claim, not a key that ever rendered. Pinned by + // `__tests__/dashboard-aria-retired-contract-twins.test.ts`. } /**