Skip to content

Commit 43a7a8d

Browse files
fix(spec): aria 墓碑不再指向同一大版本里已退休的落点 (#6756) (#6854)
`dashboard.widgets[].aria` 的 retiredKey() 提示声称共享的 `AriaProps` "stays live on `app.aria`",而 `App.aria` 自己就是同一个 17.0.0 里被移除的 retiredKey() 墓碑;两行之后推荐的 `os migrate meta --from 16` 恰恰会把 `aria` 从 app 源里剥掉。反向的一半同样坏:`App.aria` 让作者写到 "component/widget",widget 那一半正是 #5010 退休的 `dashboard.widgets[].aria`。 两个墓碑互指对方已退休的键。 按 liveness ledger 实测重新校准落点:page.aria / page.components[].aria / list view 的 aria 为 live;action.aria 为 live 但 PARTIAL;chart.aria 无 ledger 行,故不列入 —— 多列一个没有渲染器读的面就是在重犯本 issue 修的错。 migrate 动词由 "rewrite it" 改为 "remove it":该转换调用 stripKeys,做的是剥离。 接受集合逐字节不变:改动全部是 retiredKey() 的 guidance 参数、注释与 ledger 的 note 散文。gen:schema 独立复核 1308 个 authorable default 未变。 两处既有断言此前把缺陷钉死(dashboard.test.ts 要求 /app\.aria/、 app.test.ts 期待 'component/widget'),已按其原意重新瞄准而非删除。 Claude-Session: https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 41610f6 commit 43a7a8d

13 files changed

Lines changed: 231 additions & 19 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): `aria` 墓碑不再把作者指向同一个大版本里已经退休的落点(#6756
6+
7+
`dashboard.widgets[].aria``retiredKey()` 提示告诉升级中的作者,共享的
8+
`AriaProps` 形状「stays live on `app.aria`」。但 `App.aria` 自己就是一个
9+
`retiredKey()` 墓碑,和它在**同一个 17.0.0** 里被 2026-06 app liveness 审计移除了。
10+
于是这条提示把作者送向一扇不存在的门,然后在两行之后递给他们
11+
`os migrate meta --from 16` —— 而 `app-dead-authoring-keys-removed` 这个转换恰好
12+
会把 `aria` 从源里**剥掉**。按提示操作的代价是两轮返工外加一次静默的数据丢失。
13+
14+
反向的一半同样是坏的:`App.aria` 的处方说「declare `aria` on the
15+
**component/widget**」,而 widget 那一半指的正是 #5010 退休掉的
16+
`dashboard.widgets[].aria`。两个墓碑互相指向对方已经退休的键。
17+
18+
按实测(而非沿用原 issue 的猜测)重新校准了落点。`packages/spec`
19+
`aria: AriaPropsSchema` 共 25 处活声明,liveness ledger 的判定是:`page.aria`
20+
`page.components[].aria`、list view 的 `aria``live``action.aria` 为 live 但
21+
标注 PARTIAL;`app.aria` / `dashboard.aria` / `dashboard.widgets[].aria` /
22+
form view 的 `aria``dead``chart.aria` 没有 ledger 行。因此处方只列举了
23+
前三个无歧义的活载体 —— 多列一个没有渲染器读的面,就是在重犯本 issue 修的错。
24+
25+
同时把 `os migrate meta --from 16` 的动词从 "rewrite it" 改为 "remove it":该转换
26+
调用的是 `stripKeys(..., ['actionUrl','actionType','actionIcon','aria'])`,fixture 的
27+
`after``aria` 已被删除,它做的是剥离而不是搬迁。
28+
29+
**接受集合逐字节不变。** 改动的每一处都是 `retiredKey()` 的 guidance 参数、注释或
30+
ledger 的 `note` 散文;`retiredKey()` 无论字符串是什么都返回
31+
`z.never({ error: () => guidance }).optional()``gen:schema` 亦独立复核了
32+
1308 个 authorable default 未变。

content/docs/references/ui/app.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const result = ActionNavItemSchema.parse(data);
8484
| **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. |
8585
| **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. |
8686
| **defaultAgent** | `string` | optional | Platform agent bound to this app's ambient chat ('ask' is the implicit default; 'build' for authoring surfaces) — ADR-0063 §1 |
87-
| **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. |
87+
| **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. |
8888
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this app. |
8989
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
9090
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |

content/docs/references/ui/dashboard.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ Dashboard header action
111111
| **filterBindings** | `Record<string, string \| false>` | optional | Per-widget dashboard-filter bindings: filter name → this widget's field, or false to opt out |
112112
| **suppressWarnings** | `string[]` | optional | Build diagnostic rule ids suppressed on this widget |
113113
| **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. |
114-
| **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. |
114+
| **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. |
115115

116116
### Allowed Values: `DashboardWidget.type`
117117

docs/protocol-upgrade-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Last, it removes `dashboard.widgets[].responsive` (#4876) — the straggler of t
196196

197197
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.
198198

199-
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.
199+
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.
200200

201201
⚠️ 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.
202202

packages/spec/liveness/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@
250250
"aria": {
251251
"status": "dead",
252252
"verifiedAt": "2026-08-01",
253-
"note": "retiredKey tombstone (#4142) — app-level ARIA was never read; declare aria on the rendering component/widget."
253+
"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)."
254254
},
255255
"objects": {
256256
"status": "dead",

packages/spec/liveness/dashboard.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@
147147
"aria": {
148148
"status": "dead",
149149
"verifiedAt": "2026-08-04",
150-
"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."
150+
"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."
151151
}
152152
}
153153
},

packages/spec/src/migrations/registry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,9 @@ const step17: MigrationStep = {
893893
+ 'an author could FAIL A BUILD because a control that cannot render pointed at an action '
894894
+ 'that also did not. That widget branch is deleted with the keys. Lossless deletes in every '
895895
+ 'case — the keys contributed nothing to any rendered output — and the shared `AriaProps` '
896-
+ 'shape is untouched, staying live on `app.aria` and `page.components[].aria`. Move a '
896+
+ 'shape is untouched, staying live on `page.aria` / `page.components[].aria` and the list '
897+
+ 'view `aria` — not on `app.aria`, which this same major retires (see '
898+
+ '`app-dead-authoring-keys-removed`, which strips it). Move a '
897899
+ 'dashboard-wide affordance to `header.actions[]` (where `icon` is the header spelling of '
898900
+ '`actionIcon`); for per-row click-through use a dataset-bound `table`/`pivot`, whose rows '
899901
+ 'drill through the semantic layer already.\n\n'

packages/spec/src/ui/app.form.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ export const appForm = defineForm({
8787
// impression. The live surface is `FormView.sharing` (public data
8888
// collection), authored per form view.
8989
// `aria` removed: tombstoned in 17.0.0 — no renderer read app-level ARIA
90-
// attributes; declare `aria` on the component/widget that renders the
91-
// DOM node.
90+
// attributes; declare `aria` on the page component that renders the
91+
// DOM node (`page.components[].aria`). NOT on a dashboard widget —
92+
// `dashboard.widgets[].aria` was retired in the same major (#5010,
93+
// corrected in #6756).
9294
],
9395
},
9496
],

packages/spec/src/ui/app.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,11 @@ describe('AppSchema with areas', () => {
10431043
describe('retired dead keys carry prescriptions (#4001)', () => {
10441044
it.each([
10451045
['version', '1.0.0', 'manifest.version'],
1046-
['aria', { label: 'x' }, 'component/widget'],
1046+
// ⚠️ The expected fragment used to be `component/widget`. The widget half
1047+
// of that prescription named `dashboard.widgets[].aria`, retired by #5010
1048+
// in this same 17.0.0 — so the pin held `App.aria`'s prescription pointing
1049+
// at another tombstone (#6756). Re-aimed at the surviving carrier.
1050+
['aria', { label: 'x' }, 'page.components[].aria'],
10471051
['objects', [], 'defineStack'],
10481052
['apis', [], 'defineStack'],
10491053
] as const)('rejects `%s` with its upgrade prescription', (key, value, fragment) => {

packages/spec/src/ui/app.zod.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,12 +1399,16 @@ export const AppSchema = lazySchema(() => strictObject(
13991399

14001400
/**
14011401
* REMOVED — never read at the APP level (2026-06 liveness audit). ARIA
1402-
* attributes are live on the component/widget surfaces that render DOM.
1402+
* attributes are live on the page / page-component / list-view surfaces that
1403+
* render DOM. NOT on a dashboard widget: `dashboard.widgets[].aria` was
1404+
* retired in this same 17.0.0 (#5010), so "component/widget" pointed half of
1405+
* its readers at another tombstone (#6756).
14031406
*/
14041407
aria: retiredKey(
14051408
'`App.aria` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no ' +
1406-
'renderer read app-level ARIA attributes). Declare `aria` on the component/widget ' +
1407-
'that renders the DOM node instead. Delete the key.',
1409+
'renderer read app-level ARIA attributes). Declare `aria` on the page component ' +
1410+
'that renders the DOM node instead (`page.components[].aria`; `page.aria` and the ' +
1411+
'list view `aria` are live too). Delete the key.',
14081412
),
14091413

14101414
/**

0 commit comments

Comments
 (0)