You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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
Found during a read-only audit of the packages/spectombstone 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:1404 — aria: 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:
So os migrate meta --from 16 — the very command the dashboard tombstone recommends two lines later — deletesapp.aria from a stored source.
packages/spec/src/conversions/registry.tsdashboard-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:
Parse fails with the prescription above.
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.
At the authoring site that is TS2322: z.input types App['aria'] as never, so aria: { ... } is "Type '{ ... }' is not assignable to type 'never'".
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.
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.
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
Audited at origin/main = 252f71bd69df95650547f6e5a02ae018aa5948aa, read exclusively via git show origin/main:<path> (never the shared working tree).
Census. 105 retiredKey() call sites across 28 non-test files under packages/spec/src (git grep -n "retiredKey(" origin/main -- packages/spec/src, excluding *.test.ts, the helper shared/retired-key.ts, and prose mentions in conversions/registry.ts / migrations/registry.ts / six files that only discuss tombstones). This finding is one of them.
Measurement.git grep -n "app.aria\|App.aria" origin/main -- packages/spec/src returns 6 hits: 1 authority (app.zod.ts:1405, the tombstone), 1 conversion surface string (conversions/registry.ts:1675), and the 4 false-claim sites listed above. Extended to the whole repo, git grep -ln "stays live on \app.aria`"addspackages/spec/liveness/dashboard.jsonandcontent/docs/references/ui/dashboard.mdx`.
Control against a false absence. The instrument that finds live carriers is git grep -n "aria: AriaPropsSchema" origin/main -- packages/spec/src, which returns 25 live declarations across 5 files — component.zod.ts (20), page.zod.ts (2), action.zod.ts (1), chart.zod.ts (1), view.zod.ts (1). app.zod.ts is absent from that list and present in the retiredKey( list, so the two queries separate live from tombstoned rather than merely failing to find anything. The other half of the same sentence is the positive control: page.zod.ts:151,154 really do declare responsive: ResponsiveConfigSchema and aria: AriaPropsSchema, so the sentence's neighbour claim about page.components[].responsive (dashboard.zod.ts:18-21) checks out and is not part of this finding.
Found during a read-only audit of the
packages/spectombstone corpus — everyretiredKey()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— theretiredKey()message, i.e. the string an upgrading author actually receives:app.ariadoes not stay live. It is aretiredKey()tombstone,packages/spec/src/ui/app.zod.ts:1400-1408:Both removals landed in 17.0.0. The second half of the sentence —
page.components[].aria— is correct and stays correct; only theapp.ariahalf 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:3.
packages/spec/src/ui/dashboard.zod.ts:615-616— the inline note above the tombstone: "The sharedAriaPropsSchemais untouched — it stays live onapp.ariaandpage.components[].aria, which really are applied."4.
packages/spec/src/migrations/registry.ts:895-896— the protocol-17 migration record: "the sharedAriaPropsshape is untouched, staying live onapp.ariaandpage.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 onapp.aria/page.components[].aria."And it has already been published:
content/docs/references/ui/dashboard.mdx:114carries the tombstone verbatim,[REMOVED]prefix and all, including theapp.ariaclause.Related, same file, likely the same fix:
App.aria's own prescription (app.zod.ts:1406) says "Declareariaon the component/widget that renders the DOM node instead." The component half is right; the widget half is not —dashboard.widgets[].ariais the widget surface, and #5010 retired it.ui/widget.zod.tsdeclares noariakey at all (its only twoariamentions, L122 and L132, are prose aboutaria-required/aria-invalidDOM attributes). So both tombstones point at each other's retired key.The authority
packages/spec/src/ui/app.zod.ts:1404—aria: retiredKey(...). The key isz.never().optional(); nothing can be written to it.packages/spec/src/conversions/registry.ts:1670-1682— the ADR-0087 conversionapp-dead-authoring-keys-removed(toMajor: 17,retiredFromLoadPath: true) listsapp.ariain itssurfacestring and strips it inapply:So
os migrate meta --from 16— the very command the dashboard tombstone recommends two lines later — deletesapp.ariafrom a stored source.packages/spec/src/conversions/registry.tsdashboard-widget-action-aria-removed— the companion conversion, which correctly stripswidgets[].aria. Both conversions aretoMajor: 17, so the two removals are contemporaries, not a sequence.Why it matters — the authoring path
shared/retired-key.ts:15-32states the contract this violates: the tombstone string is the upgrade channel, and "an agent bumping@objectstack/specsees THIS string, not our docs site."Concretely, an author or AI maintainer with
ariaon a dashboard widget: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.z.inputtypesApp['aria']asnever, soaria: { ... }is "Type '{ ... }' is not assignable to type 'never'".App.aria was removed in @objectstack/spec 17.0.0— a second retirement message for a move the first one recommended.os migrate meta --from 16as instructed and the conversion silently strips the block they just moved, becauseapp-dead-authoring-keys-removedcoversaria.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
AriaPropsstill 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.ariawith the surviving carriers in all five sites.page.components[].ariais real (page.zod.ts:154), and so areview.aria(view.zod.ts:1280),action.aria(action.zod.ts:1049) andchart.aria(chart.zod.ts:630) — the shortest true replacement for "it stays live onapp.ariaandpage.components[].aria" is "it stays live onpage.components[].aria(and on the view / action / chart surfaces)". Separately,app.zod.ts:1401-1402and:1406should drop "widget" from "component/widget", since the only widgetariawas retired by #5010.content/docs/references/ui/dashboard.mdx:114is 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
dashboard.widgets[].ariaprescription's substance (delete the key, usetitle/description), which is correct — only its enumeration of surviving carriers is wrong.retiredKey()guidance argument or a comment.retiredKey()returnsz.never({ error: () => guidance }).optional()regardless of the string, so the set of accepted inputs forDashboardWidgetSchema,AppSchemaandAriaPropsSchemais byte-for-byte unchanged. This is adomain:spec-surfacetext fix, not a protocol change.Provenance
origin/main=252f71bd69df95650547f6e5a02ae018aa5948aa, read exclusively viagit show origin/main:<path>(never the shared working tree).retiredKey()call sites across 28 non-test files underpackages/spec/src(git grep -n "retiredKey(" origin/main -- packages/spec/src, excluding*.test.ts, the helpershared/retired-key.ts, and prose mentions inconversions/registry.ts/migrations/registry.ts/ six files that only discuss tombstones). This finding is one of them.git grep -n "app.aria\|App.aria" origin/main -- packages/spec/srcreturns 6 hits: 1 authority (app.zod.ts:1405, the tombstone), 1 conversion surface string (conversions/registry.ts:1675), and the 4 false-claim sites listed above. Extended to the whole repo,git grep -ln "stays live on \app.aria`"addspackages/spec/liveness/dashboard.jsonandcontent/docs/references/ui/dashboard.mdx`.git grep -n "aria: AriaPropsSchema" origin/main -- packages/spec/src, which returns 25 live declarations across 5 files —component.zod.ts(20),page.zod.ts(2),action.zod.ts(1),chart.zod.ts(1),view.zod.ts(1).app.zod.tsis absent from that list and present in theretiredKey(list, so the two queries separate live from tombstoned rather than merely failing to find anything. The other half of the same sentence is the positive control:page.zod.ts:151,154really do declareresponsive: ResponsiveConfigSchemaandaria: AriaPropsSchema, so the sentence's neighbour claim aboutpage.components[].responsive(dashboard.zod.ts:18-21) checks out and is not part of this finding.repo:objectstack-ai/objectstack app.aria(0 hits),AriaProps(3 hits — AriaProps 与 WAI-ARIA 1.2 的两处缺口:aria.live只活在 objectui 的扩展里,aria-labelledby全无对应 #5058 open, but it is about WAI-ARIA 1.2 coverage gaps in the shape, not about which surfaces carry it; widget-contract.mdx 整页 + quick-reference.mdx 一行仍在教 #5055 退役的 WidgetManifest 家族 —— 手写散文面,门禁不覆盖 #6413 and ADR-0049 enforce-or-remove: ui/widget.zod.ts 全文件 + ui/i18n.zod.ts 五个形状实测无门(#4001 批 16 改判) #5055 closed, both about the retiredWidgetManifestfamily),is:open aria tombstone(2 hits — [P3] objectuiActionDef's[key: string]: anyabsorbs every retired and misspelled action key — removing a spec key produces no compile error #4075 objectuiActionDefindex signature, [PM seat] triage (objectstack-wide) — 🟢 Routine #6015 the PM triage routine),is:open retiredKey prescription(3 hits — objectql/protocol-batch-atomic.test.ts mock driver advertises the retiredsupports.transactionsbit — invisible because the mock is: any#6546, [finding][spec] #6414 retired the L2 ETL layer, butretry-policy.zod.tsstill teachesETLPipeline.retryas a live surface in six places — including theretryDelayMstombstone an upgrading author actually reads #6630, [PM seat] triage (objectstack-wide) — 🟢 Routine #6015; none covering this text).retry-policy.zod.tsstill teachesETLPipeline.retryas a live surface in six places — including theretryDelayMstombstone an upgrading author actually reads #6630, which is the same shape on a different surface (retry-policy.zod.ts/ ETL) and has already been fixed at this SHA — theretryDelayMstombstone now enumerates three surfaces, not four.