Skip to content

[finding][spec] The dashboard.widgets[].aria tombstone sends the author to app.aria as a surviving live surface — App.aria is itself a retiredKey() tombstone removed in the same major #6756

Description

@os-project-manager

Found during a read-only audit of the packages/spec tombstone corpus — every retiredKey() call site checked against the mechanism its prescription names. Filed unassigned for triage.

This is the #6630 class exactly: a tombstone that enumerates the surfaces where the replacement shape still applies, and one of the surfaces it names was retired in the same major.

The defect

packages/spec/src/ui/dashboard.zod.ts:617-627 — the retiredKey() message, i.e. the string an upgrading author actually receives:

  aria: retiredKey(
    '`dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 D2) — ' +
    'no renderer ever applied it, so ARIA attributes declared on a widget silently did not reach ' +
    'the DOM: the key promised accessibility compliance it did not deliver. This is the same ' +
    'removal the dashboard-level `aria` got in 17.0.0 (#3896). Delete the key. The dashboard ' +
    'renderer emits its own `aria-*` attributes for the widget grid; author a `title` (and ' +
    '`description`) on the widget instead — those ARE what the renderer labels the card with. ' +
    'The shared `AriaProps` shape is NOT gone: it stays live on `app.aria` and ' +
    '`page.components[].aria`. ' +
    'Run `os migrate meta --from 16` to rewrite it automatically.',
  ),

app.aria does not stay live. It is a retiredKey() tombstone, packages/spec/src/ui/app.zod.ts:1400-1408:

  /**
   * REMOVED — never read at the APP level (2026-06 liveness audit). ARIA
   * attributes are live on the component/widget surfaces that render DOM.
   */
  aria: retiredKey(
    '`App.aria` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no ' +
    'renderer read app-level ARIA attributes). Declare `aria` on the component/widget ' +
    'that renders the DOM node instead. Delete the key.',
  ),

Both removals landed in 17.0.0. The second half of the sentence — page.components[].aria — is correct and stays correct; only the app.aria half is dead.

The same claim is repeated in four more places, all downstream of the one string:

2. packages/spec/src/ui/dashboard.zod.ts:13-16 — the import comment:

// `AriaPropsSchema` is no longer imported here: `widgets[].aria` was retired
// (#5010). The shape itself is NOT removed — it stays live on `app.aria` and
// `page.components[].aria`, whose renderers really do apply it. See the
// tombstone below.

3. packages/spec/src/ui/dashboard.zod.ts:615-616 — the inline note above the tombstone: "The shared AriaPropsSchema is untouched — it stays live on app.aria and page.components[].aria, which really are applied."

4. packages/spec/src/migrations/registry.ts:895-896 — the protocol-17 migration record: "the shared AriaProps shape is untouched, staying live on app.aria and page.components[].aria."

5. packages/spec/liveness/dashboard.json:150 — the ledger note for the retired row: "The shared AriaProps shape is untouched and stays live on app.aria / page.components[].aria."

And it has already been published: content/docs/references/ui/dashboard.mdx:114 carries the tombstone verbatim, [REMOVED] prefix and all, including the app.aria clause.

Related, same file, likely the same fix: App.aria's own prescription (app.zod.ts:1406) says "Declare aria on the component/widget that renders the DOM node instead." The component half is right; the widget half is not — dashboard.widgets[].aria is the widget surface, and #5010 retired it. ui/widget.zod.ts declares no aria key at all (its only two aria mentions, L122 and L132, are prose about aria-required / aria-invalid DOM attributes). So both tombstones point at each other's retired key.

The authority

  • packages/spec/src/ui/app.zod.ts:1404aria: retiredKey(...). The key is z.never().optional(); nothing can be written to it.
  • packages/spec/src/conversions/registry.ts:1670-1682 — the ADR-0087 conversion app-dead-authoring-keys-removed (toMajor: 17, retiredFromLoadPath: true) lists app.aria in its surface string and strips it in apply:
    const RETIRED = [
      'version', 'aria', 'objects', 'apis', 'sharing', 'embed', 'mobileNavigation',
      'homePageId',
    ];

So os migrate meta --from 16 — the very command the dashboard tombstone recommends two lines later — deletes app.aria from a stored source.

  • packages/spec/src/conversions/registry.ts dashboard-widget-action-aria-removed — the companion conversion, which correctly strips widgets[].aria. Both conversions are toMajor: 17, so the two removals are contemporaries, not a sequence.

Why it matters — the authoring path

shared/retired-key.ts:15-32 states the contract this violates: the tombstone string is the upgrade channel, and "an agent bumping @objectstack/spec sees THIS string, not our docs site."

Concretely, an author or AI maintainer with aria on a dashboard widget:

  1. Parse fails with the prescription above.
  2. The prescription's own words say the shape "stays live on app.aria", so the natural repair — lift the accessibility block up one level to the app, where the spec just said it survives — is the wrong move.
  3. At the authoring site that is TS2322: z.input types App['aria'] as never, so aria: { ... } is "Type '{ ... }' is not assignable to type 'never'".
  4. Through a metadata source rather than TypeScript, the parse rejects it with App.aria was removed in @objectstack/spec 17.0.0 — a second retirement message for a move the first one recommended.
  5. Worst case, they run os migrate meta --from 16 as instructed and the conversion silently strips the block they just moved, because app-dead-authoring-keys-removed covers aria.

That is two edit rounds and a possible silent data loss, produced by the message whose whole purpose is to make the removal land in one.

Sites 4 and 5 carry the subtler harm: a maintainer auditing "is AriaProps still carried anywhere?" reads the migration record or the liveness ledger and concludes there are two surviving carriers when there is one.

Suggested direction

Non-binding: a text-only pass replacing app.aria with the surviving carriers in all five sites. page.components[].aria is real (page.zod.ts:154), and so are view.aria (view.zod.ts:1280), action.aria (action.zod.ts:1049) and chart.aria (chart.zod.ts:630) — the shortest true replacement for "it stays live on app.aria and page.components[].aria" is "it stays live on page.components[].aria (and on the view / action / chart surfaces)". Separately, app.zod.ts:1401-1402 and :1406 should drop "widget" from "component/widget", since the only widget aria was retired by #5010.

content/docs/references/ui/dashboard.mdx:114 is generated from the .describe() and should fall out of the same change on the next regeneration; it is listed here so the fix is not called complete before the baseline is refreshed.

Not in scope

  • Re-litigating enforce-or-remove: DashboardWidgetSchema 的 5 个 dead 键(#4956 下钻首次给出裁决) #5010 or the 2026-06 app liveness audit. Both retirements stand; this is only about text that outlived one of them.
  • The dashboard.widgets[].aria prescription's substance (delete the key, use title/description), which is correct — only its enumeration of surviving carriers is wrong.
  • No acceptance change. Every touched string is a retiredKey() guidance argument or a comment. retiredKey() returns z.never({ error: () => guidance }).optional() regardless of the string, so the set of accepted inputs for DashboardWidgetSchema, AppSchema and AriaPropsSchema is byte-for-byte unchanged. This is a domain:spec-surface text fix, not a protocol change.
  • objectui-side ARIA behaviour. The claim under audit is about which spec keys exist, not about which renderers honour them.

Provenance

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions