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
32 changes: 32 additions & 0 deletions .changeset/aria-tombstone-dead-carrier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@objectstack/spec": patch
---

fix(spec): `aria` 墓碑不再把作者指向同一个大版本里已经退休的落点(#6756)

`dashboard.widgets[].aria` 的 `retiredKey()` 提示告诉升级中的作者,共享的
`AriaProps` 形状「stays live on `app.aria`」。但 `App.aria` 自己就是一个
`retiredKey()` 墓碑,和它在**同一个 17.0.0** 里被 2026-06 app liveness 审计移除了。
于是这条提示把作者送向一扇不存在的门,然后在两行之后递给他们
`os migrate meta --from 16` —— 而 `app-dead-authoring-keys-removed` 这个转换恰好
会把 `aria` 从源里**剥掉**。按提示操作的代价是两轮返工外加一次静默的数据丢失。

反向的一半同样是坏的:`App.aria` 的处方说「declare `aria` on the
**component/widget**」,而 widget 那一半指的正是 #5010 退休掉的
`dashboard.widgets[].aria`。两个墓碑互相指向对方已经退休的键。

按实测(而非沿用原 issue 的猜测)重新校准了落点。`packages/spec` 里
`aria: AriaPropsSchema` 共 25 处活声明,liveness ledger 的判定是:`page.aria`、
`page.components[].aria`、list view 的 `aria` 为 `live`;`action.aria` 为 live 但
标注 PARTIAL;`app.aria` / `dashboard.aria` / `dashboard.widgets[].aria` /
form view 的 `aria` 为 `dead`;`chart.aria` 没有 ledger 行。因此处方只列举了
前三个无歧义的活载体 —— 多列一个没有渲染器读的面,就是在重犯本 issue 修的错。

同时把 `os migrate meta --from 16` 的动词从 "rewrite it" 改为 "remove it":该转换
调用的是 `stripKeys(..., ['actionUrl','actionType','actionIcon','aria'])`,fixture 的
`after` 里 `aria` 已被删除,它做的是剥离而不是搬迁。

**接受集合逐字节不变。** 改动的每一处都是 `retiredKey()` 的 guidance 参数、注释或
ledger 的 `note` 散文;`retiredKey()` 无论字符串是什么都返回
`z.never({ error: () => guidance }).optional()`。`gen:schema` 亦独立复核了
1308 个 authorable default 未变。
2 changes: 1 addition & 1 deletion content/docs/references/ui/app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const result = ActionNavItemSchema.parse(data);
| **embed** | `never` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. |
| **mobileNavigation** | `never` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. |
| **defaultAgent** | `string` | optional | Platform agent bound to this app's ambient chat ('ask' is the implicit default; 'build' for authoring surfaces) — ADR-0063 §1 |
| **aria** | `never` | optional | [REMOVED] `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. |
| **aria** | `never` | optional | [REMOVED] `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 page component that renders the DOM node instead (`page.components[].aria`; `page.aria` and the list view `aria` are live too). Delete the key. |
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this app. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/ui/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Dashboard header action
| **filterBindings** | `Record<string, string \| false>` | optional | Per-widget dashboard-filter bindings: filter name → this widget's field, or false to opt out |
| **suppressWarnings** | `string[]` | optional | Build diagnostic rule ids suppressed on this widget |
| **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, which objectui `useResponsiveConfig` really does read; move the layout there if you need breakpoint behaviour today. Run `os migrate meta --from 16` to rewrite it automatically. |
| **aria** | `never` | optional | [REMOVED] `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. |
| **aria** | `never` | optional | [REMOVED] `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 `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 16` to remove it automatically. |

### Allowed Values: `DashboardWidget.type`

Expand Down
2 changes: 1 addition & 1 deletion docs/protocol-upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Last, it removes `dashboard.widgets[].responsive` (#4876) — the straggler of t

Finally it CONVERGES `dashboard.widgets[].compareTo` (#5011) — the one entry in this step that is not a removal but a vocabulary merge, and the one whose defect was worst-shaped. The widget declared three arms with confident TSDoc; the analytics executor implements one contract, `DatasetSelection.compareTo` = `{ kind, dimension? }`, which has no `offset` in it. On the ADR-0021 dataset path the two string arms were DROPPED by the renderer (a comparison silently absent from a widget whose author asked for one) and `{ offset }` was forwarded into that contract with no dimension, so the executor threw `compareTo requires a timeDimension "undefined"` and errored the whole widget. All three arms worked on the legacy inline chart path. Same key, two fates — and the failing one was the path the spec itself calls canonical, which is why this ranks above an ordinary declared-but-unread key: the documentation was actively teaching a shape that crashes. The widget now declares the executor's own words, so `declared = enforced` holds by construction with no second vocabulary left to drift. `dimension` is optional and resolved by the EXECUTOR (one dated time dimension → that one; zero or several → a loud error naming the candidates), which is a producer-side resolution rule, not the consumer-side tolerance PD #12 forbids. The bare strings and `{ offset: '1y' }` replay mechanically; every other `{ offset }` duration is a semantic TODO below, because `previousPeriod` shifts by the resolved window's own length and rewriting `7d` into it would change which rows the comparison counts. The converged slot is also union-free, which is not cosmetic: zod collapses a failed union into one bare `Invalid input` and #5014 showed that curated guidance inside a union arm never reaches the author at all.

The same widget drill retires four more keys (#5010): the action trio `actionUrl`/`actionType`/`actionIcon`, and `aria`. The trio described a per-widget action BUTTON that no renderer in either repo has ever drawn — all 14 `actionUrl` reads in DashboardRenderer are scoped to `header.actions[]`, a different schema — and `actionIcon` had zero references anywhere outside its own declaration. `aria` is the dashboard-level `aria` removed by the #3896 sweep, one level down: declared ARIA attributes that never reached the DOM, i.e. an accessibility guarantee an author could state and nothing honoured. It survived that sweep for the same reason `responsive` did — `widgets` had no ledger drill until #4956 — not on evidence. This removal also settles a second-order cost the trio was carrying: `packages/lint`'s dashboard action-ref rule enforced ERROR-severity reference integrity on `widgets[].actionUrl`, its docblock calling the key "the per-widget button" and claiming to mirror a runtime dispatch that does not exist, so an author could FAIL A BUILD because a control that cannot render pointed at an action that also did not. That widget branch is deleted with the keys. Lossless deletes in every case — the keys contributed nothing to any rendered output — and the shared `AriaProps` shape is untouched, staying live on `app.aria` and `page.components[].aria`. Move a dashboard-wide affordance to `header.actions[]` (where `icon` is the header spelling of `actionIcon`); for per-row click-through use a dataset-bound `table`/`pivot`, whose rows drill through the semantic layer already.
The same widget drill retires four more keys (#5010): the action trio `actionUrl`/`actionType`/`actionIcon`, and `aria`. The trio described a per-widget action BUTTON that no renderer in either repo has ever drawn — all 14 `actionUrl` reads in DashboardRenderer are scoped to `header.actions[]`, a different schema — and `actionIcon` had zero references anywhere outside its own declaration. `aria` is the dashboard-level `aria` removed by the #3896 sweep, one level down: declared ARIA attributes that never reached the DOM, i.e. an accessibility guarantee an author could state and nothing honoured. It survived that sweep for the same reason `responsive` did — `widgets` had no ledger drill until #4956 — not on evidence. This removal also settles a second-order cost the trio was carrying: `packages/lint`'s dashboard action-ref rule enforced ERROR-severity reference integrity on `widgets[].actionUrl`, its docblock calling the key "the per-widget button" and claiming to mirror a runtime dispatch that does not exist, so an author could FAIL A BUILD because a control that cannot render pointed at an action that also did not. That widget branch is deleted with the keys. Lossless deletes in every case — the keys contributed nothing to any rendered output — and the shared `AriaProps` shape is untouched, staying live on `page.aria` / `page.components[].aria` and the list view `aria` — not on `app.aria`, which this same major retires (see `app-dead-authoring-keys-removed`, which strips it). Move a dashboard-wide affordance to `header.actions[]` (where `icon` is the header spelling of `actionIcon`); for per-row click-through use a dataset-bound `table`/`pivot`, whose rows drill through the semantic layer already.

⚠️ One protocol-17 change turns metadata ON rather than off, and it is the one to read first: declarative `apis:` endpoints EXECUTE from 17 (#5040). The surface used to be inert end to end — no route mounted, no matcher, every key including `authRequired` parsed and enforced nothing — which is why #4936 refused a non-empty `apis:` outright. 17 ships the executor and narrows that refusal to a per-endpoint publish gate, so an endpoint that passes the gate is MOUNTED and serves traffic the moment it is published. Any historical `apis:` block therefore changes meaning without changing a byte. Review every entry before upgrading, and pay particular attention to an explicit `authRequired: false`: the schema default is `true`, so an omission is safe, and only that explicit `false` opens anonymous access — which ADR-0121 D6 now pairs with a mandatory armed `rateLimit` (`enabled: true`; the key defaults to `false`, so a budget written without it meters nothing). Paths also move under the namespace carve-out `/api/v1/apps/<manifest.namespace>/<subpath>` (ADR-0121 D1/D2). The full checklist is the `declarative-apis-endpoints-live` semantic entry below; it is a security review, not a rename, so nothing about it is applied for you.

Expand Down
2 changes: 1 addition & 1 deletion packages/spec/liveness/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@
"aria": {
"status": "dead",
"verifiedAt": "2026-08-01",
"note": "retiredKey tombstone (#4142) — app-level ARIA was never read; declare aria on the rendering component/widget."
"note": "retiredKey tombstone (#4142) — app-level ARIA was never read; declare aria on the rendering page component (`page.components[].aria`, live in page.json). NOT on a dashboard widget: `dashboard.widgets[].aria` is `dead` in dashboard.json, retired by #5010 in this same major — corrected 2026-08-08 (#6756)."
},
"objects": {
"status": "dead",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/liveness/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
"aria": {
"status": "dead",
"verifiedAt": "2026-08-04",
"note": "CALL GRAPH CLOSED BY HAND 2026-08-03, re-measured 2026-08-04 across both repos: no consumer of `widget.aria` anywhere. The `aria-*` attributes in DashboardRenderer / DatasetWidget are the renderer's own DOM attributes, and objectui's one `.aria` read (plugin-view/src/ObjectView.tsx:989) is the VIEW's. Same false-compliance shape as the dashboard-level `aria` removed in the #3896 sweep — an accessibility guarantee an author can declare and nothing honours. The shared AriaProps shape is untouched and stays live on `app.aria` / `page.components[].aria`. Retired 2026-08-04 via #5010 / ADR-0049 D2 (retiredKey tombstone + the protocol-17 `dashboard-widget-action-aria-removed` conversion). The row stays because the tombstone keeps the key in the walked shape — the rls.priority precedent — and no authorWarn is needed: authoring it is now a tsc error and a parse error carrying the prescription."
"note": "CALL GRAPH CLOSED BY HAND 2026-08-03, re-measured 2026-08-04 across both repos: no consumer of `widget.aria` anywhere. The `aria-*` attributes in DashboardRenderer / DatasetWidget are the renderer's own DOM attributes, and objectui's one `.aria` read (plugin-view/src/ObjectView.tsx:989) is the VIEW's. Same false-compliance shape as the dashboard-level `aria` removed in the #3896 sweep — an accessibility guarantee an author can declare and nothing honours. The shared AriaProps shape is untouched and stays live on `page.aria` / `page.components[].aria` and the list view `aria` (this ledger's own `page.props.aria` and `view.props.list.children.aria` rows) — corrected 2026-08-08 (#6756): it does NOT stay live on `app.aria`, whose `app.json` row is `dead` and which the protocol-17 `app-dead-authoring-keys-removed` conversion strips. Retired 2026-08-04 via #5010 / ADR-0049 D2 (retiredKey tombstone + the protocol-17 `dashboard-widget-action-aria-removed` conversion). The row stays because the tombstone keeps the key in the walked shape — the rls.priority precedent — and no authorWarn is needed: authoring it is now a tsc error and a parse error carrying the prescription."
}
}
},
Expand Down
4 changes: 3 additions & 1 deletion packages/spec/src/migrations/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,9 @@ const step17: MigrationStep = {
+ 'an author could FAIL A BUILD because a control that cannot render pointed at an action '
+ 'that also did not. That widget branch is deleted with the keys. Lossless deletes in every '
+ 'case — the keys contributed nothing to any rendered output — and the shared `AriaProps` '
+ 'shape is untouched, staying live on `app.aria` and `page.components[].aria`. Move a '
+ 'shape is untouched, staying live on `page.aria` / `page.components[].aria` and the list '
+ 'view `aria` — not on `app.aria`, which this same major retires (see '
+ '`app-dead-authoring-keys-removed`, which strips it). Move a '
+ 'dashboard-wide affordance to `header.actions[]` (where `icon` is the header spelling of '
+ '`actionIcon`); for per-row click-through use a dataset-bound `table`/`pivot`, whose rows '
+ 'drill through the semantic layer already.\n\n'
Expand Down
6 changes: 4 additions & 2 deletions packages/spec/src/ui/app.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ export const appForm = defineForm({
// impression. The live surface is `FormView.sharing` (public data
// collection), authored per form view.
// `aria` removed: tombstoned in 17.0.0 — no renderer read app-level ARIA
// attributes; declare `aria` on the component/widget that renders the
// DOM node.
// attributes; declare `aria` on the page component that renders the
// DOM node (`page.components[].aria`). NOT on a dashboard widget —
// `dashboard.widgets[].aria` was retired in the same major (#5010,
// corrected in #6756).
],
},
],
Expand Down
6 changes: 5 additions & 1 deletion packages/spec/src/ui/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,11 @@ describe('AppSchema with areas', () => {
describe('retired dead keys carry prescriptions (#4001)', () => {
it.each([
['version', '1.0.0', 'manifest.version'],
['aria', { label: 'x' }, 'component/widget'],
// ⚠️ The expected fragment used to be `component/widget`. The widget half
// of that prescription named `dashboard.widgets[].aria`, retired by #5010
// in this same 17.0.0 — so the pin held `App.aria`'s prescription pointing
// at another tombstone (#6756). Re-aimed at the surviving carrier.
['aria', { label: 'x' }, 'page.components[].aria'],
['objects', [], 'defineStack'],
['apis', [], 'defineStack'],
] as const)('rejects `%s` with its upgrade prescription', (key, value, fragment) => {
Expand Down
10 changes: 7 additions & 3 deletions packages/spec/src/ui/app.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1399,12 +1399,16 @@ export const AppSchema = lazySchema(() => strictObject(

/**
* REMOVED — never read at the APP level (2026-06 liveness audit). ARIA
* attributes are live on the component/widget surfaces that render DOM.
* attributes are live on the page / page-component / list-view surfaces that
* render DOM. NOT on a dashboard widget: `dashboard.widgets[].aria` was
* retired in this same 17.0.0 (#5010), so "component/widget" pointed half of
* its readers at another tombstone (#6756).
*/
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.',
'renderer read app-level ARIA attributes). Declare `aria` on the page component ' +
'that renders the DOM node instead (`page.components[].aria`; `page.aria` and the ' +
'list view `aria` are live too). Delete the key.',
),

/**
Expand Down
Loading
Loading