From 8bbedfbc348ca422192724c1782986e80d6cbba3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 23:30:12 +0000 Subject: [PATCH 1/4] refactor(spec)!: retire pass-through-only list-view keys striped/bordered/virtualScroll (ADR-0049, #7176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer ruling 2026-08-10: every measured reader copies the key forward and ObjectGrid, where the chains end, never applies any of the three — copy-without-apply is dead in effect. retiredKey() tombstones on ListViewSchema (radiating into ObjectListViewSchema), ADR-0087 D2 conversion view-list-passthrough-keys-removed + D3 chain step + six RETIRED_KEYS_BY_MAJOR[17] entries, ledger rows flipped to dead with the retirement note, form inputs pruned, docs updated, changeset with FROM->TO. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r --- .../view-list-passthrough-keys-retired.md | 56 +++++++++++++++ .../protocol/objectui/widget-contract.mdx | 15 ++-- content/docs/references/ui/view.mdx | 12 ++-- docs/protocol-upgrade-guide.md | 3 + .../lint/src/lint-liveness-properties.test.ts | 17 +++++ .../en.metadata-forms.generated.ts | 6 -- .../es-ES.metadata-forms.generated.ts | 6 -- .../ja-JP.metadata-forms.generated.ts | 6 -- .../zh-CN.metadata-forms.generated.ts | 6 -- packages/spec/authorable-surface/ui.json | 12 ++-- packages/spec/liveness/state-counts.md | 4 +- packages/spec/liveness/view.json | 15 ++-- packages/spec/spec-changes.json | 12 ++++ packages/spec/src/conversions/registry.ts | 63 ++++++++++++++++ .../retired-keys/17.ui__ListView__bordered.ts | 9 +++ .../retired-keys/17.ui__ListView__striped.ts | 8 +++ .../17.ui__ListView__virtualScroll.ts | 9 +++ .../17.ui__ObjectListView__bordered.ts | 8 +++ .../17.ui__ObjectListView__striped.ts | 8 +++ .../17.ui__ObjectListView__virtualScroll.ts | 8 +++ packages/spec/src/migrations/registry.ts | 50 ++++++++++++- packages/spec/src/ui/view.form.ts | 3 +- packages/spec/src/ui/view.test.ts | 72 +++++++++++++++++-- packages/spec/src/ui/view.zod.ts | 33 +++++++-- 24 files changed, 377 insertions(+), 64 deletions(-) create mode 100644 .changeset/view-list-passthrough-keys-retired.md create mode 100644 packages/spec/src/migrations/entries/retired-keys/17.ui__ListView__bordered.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/17.ui__ListView__striped.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/17.ui__ListView__virtualScroll.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/17.ui__ObjectListView__bordered.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/17.ui__ObjectListView__striped.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/17.ui__ObjectListView__virtualScroll.ts diff --git a/.changeset/view-list-passthrough-keys-retired.md b/.changeset/view-list-passthrough-keys-retired.md new file mode 100644 index 0000000000..229d869b4a --- /dev/null +++ b/.changeset/view-list-passthrough-keys-retired.md @@ -0,0 +1,56 @@ +--- +"@objectstack/spec": major +--- + +refactor(spec)!: retire the pass-through-only list-view keys `striped` / `bordered` / `virtualScroll` (#7176, ADR-0049) + +`ListViewSchema` (and its `ObjectListViewSchema` copy) declared three grid +display keys the liveness ledger graded `live` — and the citations turned out to +be **forwarding copies, not appliers**. The measured chains (objectui +`origin/main@11c1e71`): the react spec-bridge copies each key onto its node, +`plugin-list` copies it onto the grid node, `plugin-view`/`app-shell` copy it +again — and the chain ends at `ObjectGrid.tsx` with **zero** occurrences of any +of the three. `DataTable` reads neither `striped` nor `virtualScroll`; the +table frame is the renderer's own `borderless` constant. So an author who wrote +`striped: true` got a parse-clean no-op — the exact silent-no-op shape ADR-0049 +enforce-or-remove exists to end. Maintainer ruling (2026-08-10): copy-without- +apply is dead in effect; retire, and if objectui wants any of the three as real +behavior, that is an implementation card filed first, with the key pending it. + +FROM → TO: + +| Was | Now | +|:--|:--| +| `list: { striped: true }` | delete the key — there is no authorable striped-rows switch | +| `list: { bordered: true }` | delete the key — the grid frame is the renderer's own constant, not authorable | +| `list: { virtualScroll: true }` | delete the key — large datasets page via the view's `pagination` block | + +The retirement kit: + +- **Tombstones, not deletions** (`retiredKey()`): authoring any of the three is + now a `tsc` error (input type `never`) and a parse error carrying the + prescription itself — why the key never did anything and the one-line fix — + on both `ListViewSchema` and `ObjectListViewSchema`, whose walked shape + copies the tombstones. All six `${defKey}:${name}` spellings are registered + in `RETIRED_KEYS_BY_MAJOR[17]`. +- **ADR-0087 D2 conversion + D3 chain step** + (`view-list-passthrough-keys-removed`): `os migrate meta --from 16` strips + the keys from `list` and named `listViews` entries, one notice per stripped + key. `retiredFromLoadPath` — the tombstone owns the refusal; no alias window. +- **Liveness ledger**: the three `view.json` rows flip from `live` (the + pass-through citation) to `dead` with the retirement note — the rows stay + because `retiredKey` keeps the keys in the walked shape. +- The view metadata form loses its `striped`/`bordered` inputs (a form input + for an unenforced key is the UI half of false compliance); `virtualScroll` + never had one. +- `content/docs/protocol/objectui/widget-contract.mdx`'s Performance section + stops pointing at `view.virtualScroll` — the pointer was installed this same + unreleased major when `widget.performance`'s tombstone was retired, and it + aimed at a switch nothing read. + +**Behaviour that changes:** none at runtime. A view declaring any of the three +rendered identically without it before this change; what changes is that the +declaration is now refused at parse, with the reason, instead of being accepted +and silently ignored. + + diff --git a/content/docs/protocol/objectui/widget-contract.mdx b/content/docs/protocol/objectui/widget-contract.mdx index 33c9530e29..c083846ffd 100644 --- a/content/docs/protocol/objectui/widget-contract.mdx +++ b/content/docs/protocol/objectui/widget-contract.mdx @@ -250,16 +250,21 @@ What a widget contributes is the state on the control it renders ## Performance -There is **no** performance block anywhere in this contract. Virtualization for -large datasets is configured on the **view**: set the boolean `virtualScroll` on a -list-shaped view (`ListViewSchema` in `packages/spec/src/ui/view.zod.ts`). That is -the only virtual-scrolling switch objectui reads. +There is **no** performance block anywhere in this contract — and since 17.0.0 +(#7176) there is no authorable virtual-scrolling switch anywhere else either. +The view-level boolean `virtualScroll` this section used to point at was retired +under ADR-0049 enforce-or-remove: every measured reader only copied the key +forward and the grid renderer never applied it, so authoring it was a +parse-clean no-op. Large datasets page via the view's `pagination` block. `widget.performance` was removed at the #3896 audit close-out and its tombstone was subsumed by #5055 when the manifest that carried it was itself removed — so there is no longer a key to reject, because there is no longer a shape to author - it into. Use the view's `virtualScroll`. + it into. The view-level `virtualScroll` it used to defer to was itself retired + at #7176 (pass-through-only — copied by every bridge, applied by nothing); real + list virtualization is an implementation card first, and the key stays retired + pending it. ## Theme diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 7680fe43f7..5dbf3df754 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -475,8 +475,6 @@ List chart view configuration | **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | | **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields | | **resizable** | `boolean` | optional | Enable column resizing | -| **striped** | `boolean` | optional | Striped row styling | -| **bordered** | `boolean` | optional | Show borders | | **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | @@ -498,7 +496,6 @@ List chart view configuration | **rowActions** | `string[]` | optional | Actions available for individual row items | | **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | | **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | -| **virtualScroll** | `boolean` | optional | Enable virtual scrolling for large datasets | | **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | | **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | | **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'pdf' \| 'json'>[]` | optional | Available export format options | @@ -512,6 +509,9 @@ List chart view configuration | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | | **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. Run `os migrate meta --from 16` to rewrite existing sources automatically. | --- @@ -563,8 +563,6 @@ List chart view configuration | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | | **resizable** | `boolean` | optional | Enable column resizing | -| **striped** | `boolean` | optional | Striped row styling | -| **bordered** | `boolean` | optional | Show borders | | **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | @@ -586,7 +584,6 @@ List chart view configuration | **rowActions** | `string[]` | optional | Actions available for individual row items | | **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | | **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | -| **virtualScroll** | `boolean` | optional | Enable virtual scrolling for large datasets | | **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | | **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | | **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'pdf' \| 'json'>[]` | optional | Available export format options | @@ -600,6 +597,9 @@ List chart view configuration | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | | **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index a44a0b5e7f..a1d8e2f64b 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -238,6 +238,8 @@ The plugin manifest loses its whole `loading` block in this step (#4914, ADR-004 The action LOCATION vocabulary loses `global_nav` in this step (#6888, ADR-0049, maintainer ruling 2026-08-09). It was declared from the day `ACTION_LOCATIONS` was written and no product surface ever served it: the console command palette composes its groups from nav items, objects, dashboards, pages, reports, recent items and record search, and reads no action metadata at all — so an action declaring this location never reached a user. What lifts it above ordinary inert-declaration cleanup is that the authoring tool PROMISED the surface: the Studio designer previewed a mock `⌘K · Command palette` frame for exactly this value, so an author (very often an AI, ADR-0033) declared it, watched it "render", shipped it, and got nothing — the ADR-0078 shape arriving through a location vocabulary rather than through a missing key. It was retired rather than implemented because the demand evidence is empty: no user has asked for command-palette actions and the only two declarers were our own showcase corpus, so wiring the palette would have been capability expansion with no pull. This is an enum VALUE, not a key, so — as with `crypto.hash` and the two aggregate functions above — there is no `retiredKey()` tombstone: the enum error map carries the prescription, keyed on the received value so only the spelling that used to be legal is told it "was removed". The conversion strips the value from `action.locations` and KEEPS the key even when the array empties, because on this surface `locations: []` and an absent `locations` are different declarations: the empty array is the documented headless shape (callable over REST/MCP/AI, capability gate and audit trail intact), while an absent key means nobody placed the action — which is what `packages/lint`'s `action-no-placement` warns about. An object-less action, whose only reason for declaring `global_nav` was that it has no row and no record header to render on, is therefore migrated to the declaration it always meant. +It also removes the three pass-through-only list-view display keys `striped` / `bordered` / `virtualScroll` (#7176, ADR-0049 enforce-or-remove, maintainer ruling 2026-08-10). All three were graded live on reads that turned out to be forwarding copies: the react spec-bridge, plugin-list and plugin-view/app-shell each copy the key onto the next node, and the chain ends at ObjectGrid, which never spells any of the three — so an author who wrote `striped: true` got a parse-clean no-op, the exact silent-no-op shape enforce-or-remove exists to end. Copy-without-apply is dead in effect; per the ruling, if objectui wants one of these as real behavior, that is an implementation card filed first, and the key stays retired pending it. + ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | @@ -261,6 +263,7 @@ The action LOCATION vocabulary loses `global_nav` in this step (#6888, ADR-0049, | `action-inert-keys-removed` | `action.shortcut / action.bulkEnabled` | action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | retired — `migrate meta` only | | `flow-inert-keys-removed` | `flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId` | flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle) | retired — `migrate meta` only | | `view-inert-keys-removed` | `view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria` | view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live) | retired — `migrate meta` only | +| `view-list-passthrough-keys-removed` | `view.list.striped / view.list.bordered / view.list.virtualScroll` | view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove) | retired — `migrate meta` only | | `dashboard-inert-keys-removed` | `dashboard.aria / dashboard.performance / dashboard.widgets[].performance` | dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them) | retired — `migrate meta` only | | `dashboard-widget-responsive-removed` | `dashboard.widgets[].responsive` | dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected) | retired — `migrate meta` only | | `dashboard-widget-action-aria-removed` | `dashboard.widgets[].actionUrl / dashboard.widgets[].actionType / dashboard.widgets[].actionIcon / dashboard.widgets[].aria` | dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (#5010 — no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description) | retired — `migrate meta` only | diff --git a/packages/lint/src/lint-liveness-properties.test.ts b/packages/lint/src/lint-liveness-properties.test.ts index e4ae4e4d74..fb0ae35749 100644 --- a/packages/lint/src/lint-liveness-properties.test.ts +++ b/packages/lint/src/lint-liveness-properties.test.ts @@ -160,6 +160,23 @@ describe('lintLivenessProperties', () => { expect(msgs.some((m) => m.includes('form.data'))).toBe(false); }); + // list.striped / list.bordered / list.virtualScroll left the surface with + // the #7176 retirement (pass-through-only; keys REMOVED, the strict parse + // owns them now). Their ledger rows are dead WITHOUT authorWarn, so the + // advisory lint must stay silent — the tombstone's rejection is the channel. + it('the #7176 pass-through-only list keys do not warn (the strict parse owns them now)', () => { + const findings = lintLivenessProperties({ + views: [{ + object: 'task', + list: { type: 'grid', striped: true, bordered: true, virtualScroll: true }, + }], + }); + const msgs = paths(findings); + expect(msgs.some((m) => m.includes('list.striped'))).toBe(false); + expect(msgs.some((m) => m.includes('list.bordered'))).toBe(false); + expect(msgs.some((m) => m.includes('list.virtualScroll'))).toBe(false); + }); + it('stays silent on a clean grid view', () => { const findings = lintLivenessProperties({ views: [{ object: 'task', list: { type: 'grid', columns: ['title'] } }], diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index 5fae90e350..3ff558d194 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -641,12 +641,6 @@ export const enMetadataForms: NonNullable = { resizable: { label: "Resizable" }, - striped: { - label: "Striped" - }, - bordered: { - label: "Bordered" - }, compactToolbar: { label: "Compact Toolbar" }, diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index fc5739d8bc..dada915da5 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -641,12 +641,6 @@ export const esESMetadataForms: NonNullable = resizable: { label: "Redimensionable" }, - striped: { - label: "Con franjas" - }, - bordered: { - label: "Con borde" - }, compactToolbar: { label: "Barra compacta" }, diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index d63656b100..ffeab033a8 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -641,12 +641,6 @@ export const jaJPMetadataForms: NonNullable = resizable: { label: "サイズ変更可" }, - striped: { - label: "縞表示" - }, - bordered: { - label: "枠線" - }, compactToolbar: { label: "コンパクトツールバー" }, diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index 744bd04c00..fc94393eff 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -641,12 +641,6 @@ export const zhCNMetadataForms: NonNullable = resizable: { label: "可调整大小" }, - striped: { - label: "斑马纹" - }, - bordered: { - label: "边框" - }, compactToolbar: { label: "紧凑工具栏" }, diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 8d274e953c..b5ade541c5 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -631,7 +631,7 @@ "ui/ListView:allowPrinting", "ui/ListView:appearance", "ui/ListView:aria", - "ui/ListView:bordered", + "ui/ListView:bordered [RETIRED]", "ui/ListView:bulkActionDefs", "ui/ListView:bulkActions", "ui/ListView:calendar", @@ -667,14 +667,14 @@ "ui/ListView:sharing", "ui/ListView:showRecordCount", "ui/ListView:sort", - "ui/ListView:striped", + "ui/ListView:striped [RETIRED]", "ui/ListView:tabs", "ui/ListView:timeline", "ui/ListView:tree", "ui/ListView:type", "ui/ListView:userActions", "ui/ListView:userFilters", - "ui/ListView:virtualScroll", + "ui/ListView:virtualScroll [RETIRED]", "ui/NavigationArea:description", "ui/NavigationArea:icon", "ui/NavigationArea:id", @@ -694,7 +694,7 @@ "ui/ObjectListView:allowPrinting", "ui/ObjectListView:appearance", "ui/ObjectListView:aria", - "ui/ObjectListView:bordered", + "ui/ObjectListView:bordered [RETIRED]", "ui/ObjectListView:bulkActionDefs", "ui/ObjectListView:bulkActions", "ui/ObjectListView:calendar", @@ -730,14 +730,14 @@ "ui/ObjectListView:sharing", "ui/ObjectListView:showRecordCount", "ui/ObjectListView:sort", - "ui/ObjectListView:striped", + "ui/ObjectListView:striped [RETIRED]", "ui/ObjectListView:tabs", "ui/ObjectListView:timeline", "ui/ObjectListView:tree", "ui/ObjectListView:type", "ui/ObjectListView:userActions", "ui/ObjectListView:userFilters", - "ui/ObjectListView:virtualScroll", + "ui/ObjectListView:virtualScroll [RETIRED]", "ui/ObjectNavItem:badge", "ui/ObjectNavItem:badgeVariant", "ui/ObjectNavItem:filters", diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 2648a9b2ee..427de6ccb7 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -39,7 +39,7 @@ for both corollaries. | `skill` | 16 | 0 | 1 | 0 | 17 | | `dataset` | 27 | 0 | 0 | 0 | 27 | | `page` | 23 | 0 | 0 | 1 | 24 | -| `view` | 80 | 0 | 6 | 0 | 86 | +| `view` | 77 | 0 | 9 | 0 | 86 | | `report` | 21 | 0 | 0 | 0 | 21 | | `dashboard` | 34 | 0 | 7 | 0 | 41 | | `webhook` | 19 | 0 | 0 | 0 | 19 | @@ -57,4 +57,4 @@ for both corollaries. | `api` | 25 | 0 | 0 | 2 | 27 | | `capability` | 12 | 0 | 0 | 0 | 12 | | `qa` | 4 | 0 | 5 | 0 | 9 | -| **total** | **777** | **6** | **52** | **8** | **843** | +| **total** | **774** | **6** | **55** | **8** | **843** | diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index 339832fe26..1a3a6da4ee 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -64,12 +64,14 @@ "note": "objectui: ObjectGrid.tsx (audit L15)." }, "striped": { - "status": "live", - "note": "objectui: ObjectGrid.tsx (audit L15)." + "status": "dead", + "verifiedAt": "2026-08-12", + "note": "REMOVED 2026-08-12 (#7176, ADR-0049 enforce-or-remove) — the L15 citation was pass-through-only: every measured reader (spec-bridge, plugin-list, plugin-view/app-shell) copied the key forward and ObjectGrid, where the chain ends, never spells it. Tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 view-list-passthrough-keys-removed conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); there is no authorable striped-rows switch — delete the key." }, "bordered": { - "status": "live", - "note": "objectui: ObjectGrid.tsx (audit L15)." + "status": "dead", + "verifiedAt": "2026-08-12", + "note": "REMOVED 2026-08-12 (#7176, ADR-0049 enforce-or-remove) — the L15 citation was pass-through-only: every measured reader copied the key forward and ObjectGrid never spells it (the grid frame is the renderer's own borderless constant; DataTable's only 'bordered' hit is a comment). Tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 view-list-passthrough-keys-removed conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); delete the key." }, "compactToolbar": { "status": "live", @@ -166,8 +168,9 @@ "note": "objectui: ListView.tsx:1343 forwards rich defs to ObjectGrid (BulkActionDialog). Dispatch: useBulkExecutor.ts run() — per-record fan-out by default; execution:'aggregate' defs go through the ONE-call bulkCall branch injecting params._selectedIds (ObjectGrid.runBulkActionAggregate, objectui#3139). Verified objectui@4bf612c. #4457 gave the def a SHAPE (it was z.record(z.any())): the entry schema is BulkActionDefSchema, strict, and it refuses the combinations that parse but the executor never reads — so this row's liveness now covers the keys inside a def, not just the array." }, "virtualScroll": { - "status": "live", - "note": "objectui: ObjectGrid.tsx (audit L15)." + "status": "dead", + "verifiedAt": "2026-08-12", + "note": "REMOVED 2026-08-12 (#7176, ADR-0049 enforce-or-remove) — the L15 citation was pass-through-only: every measured reader copied the key forward and ObjectGrid never spells it; VirtualGrid genuinely virtualizes but is only exported, never instantiated, and its props have no virtualScroll member. Tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 view-list-passthrough-keys-removed conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); large datasets page via pagination — delete the key." }, "conditionalFormatting": { "status": "live", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 166d8dbf9d..a8f7c4e8ff 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -182,6 +182,12 @@ "conversionId": "view-inert-keys-removed", "toMajor": 17 }, + { + "surface": "view.list.striped / view.list.bordered / view.list.virtualScroll", + "to": "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", + "conversionId": "view-list-passthrough-keys-removed", + "toMajor": 17 + }, { "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", @@ -1248,6 +1254,12 @@ "conversionId": "view-inert-keys-removed", "toMajor": 17 }, + { + "surface": "view.list.striped / view.list.bordered / view.list.virtualScroll", + "to": "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", + "conversionId": "view-list-passthrough-keys-removed", + "toMajor": 17 + }, { "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 014a92ff45..0cfa735b49 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -2266,6 +2266,68 @@ const viewInertKeysRemoved: MetadataConversion = { }, }; +/** + * View container: list-shaped entries lose `striped`/`bordered`/`virtualScroll` + * (#7176, ADR-0049 enforce-or-remove, maintainer ruling 2026-08-10). Every + * measured reader was a forwarding copy — react spec-bridge → plugin-list → + * plugin-view/app-shell — and the chains end at ObjectGrid, which never spells + * any of the three: copy-without-apply is dead in effect. List-shaped slots + * only (`list` + named `listViews`); form-shaped entries never declared them. + */ +const viewListPassthroughKeysRemoved: MetadataConversion = { + id: 'view-list-passthrough-keys-removed', + toMajor: 17, + retiredFromLoadPath: true, + surface: 'view.list.striped / view.list.bordered / view.list.virtualScroll', + summary: "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", + apply(stack, emit) { + const LIST_KEYS = ['striped', 'bordered', 'virtualScroll'] as const; + return mapCollection(stack, 'views', (view, path) => { + let touched = false; + const next: Record = { ...view }; + const list = next.list; + if (list && typeof list === 'object' && !Array.isArray(list)) { + const cleaned = stripKeys(list as Record, LIST_KEYS, emit, `${path}.list`); + if (cleaned !== list) { next.list = cleaned; touched = true; } + } + const named = next.listViews; + if (named && typeof named === 'object' && !Array.isArray(named)) { + const rebuilt: Record = { ...(named as Record) }; + let subTouched = false; + for (const [name, lv] of Object.entries(rebuilt)) { + if (lv && typeof lv === 'object' && !Array.isArray(lv)) { + const cleaned = stripKeys(lv as Record, LIST_KEYS, emit, `${path}.listViews.${name}`); + if (cleaned !== lv) { rebuilt[name] = cleaned; subTouched = true; } + } + } + if (subTouched) { next.listViews = rebuilt; touched = true; } + } + return touched ? next : view; + }); + }, + fixture: { + before: { + views: [{ + object: 'crm_lead', + list: { type: 'grid', columns: ['name'], resizable: true, striped: true, bordered: true, virtualScroll: true }, // resizable survives + listViews: { + all: { type: 'grid', columns: ['name'], striped: false }, + }, + }], + }, + after: { + views: [{ + object: 'crm_lead', + list: { type: 'grid', columns: ['name'], resizable: true }, + listViews: { + all: { type: 'grid', columns: ['name'] }, + }, + }], + }, + expectedNotices: 4, + }, +}; + /** dashboard.aria / dashboard.performance / widgets[].performance. */ const dashboardInertKeysRemoved: MetadataConversion = { id: 'dashboard-inert-keys-removed', @@ -6346,6 +6408,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly> 'ui/ElementRecordPickerProps:displayField', 'ui/ElementRecordPickerProps:multiple', 'ui/ElementRecordPickerProps:searchFields', + // #7176 — pass-through-only (ADR-0049 enforce-or-remove, maintainer ruling + // 2026-08-10): every measured reader copied the key forward and the chain ends + // at ObjectGrid, which never spells it; the grid frame is the renderer's own + // constant (`borderless`), never this key. Distinct from the LIVE + // `ui/PageCardProps:bordered`, a different surface on the page-card container. + // Conversion `view-list-passthrough-keys-removed` strips it from sources. + 'ui/ListView:bordered', + // #7176 — pass-through-only (ADR-0049 enforce-or-remove, maintainer ruling + // 2026-08-10): every measured reader (react spec-bridge, plugin-list, + // plugin-view/app-shell) copied the key forward and the chain ends at + // ObjectGrid, which never spells it — copy-without-apply is dead in effect. + // Conversion `view-list-passthrough-keys-removed` strips it from sources. + 'ui/ListView:striped', + // #7176 — pass-through-only (ADR-0049 enforce-or-remove, maintainer ruling + // 2026-08-10): every measured reader copied the key forward and the chain ends + // at ObjectGrid, which never spells it. objectui's VirtualGrid genuinely + // virtualizes but is only exported, never instantiated by ObjectGrid, and its + // props carry no `virtualScroll` member — so no grid ever virtualized off this + // key. Conversion `view-list-passthrough-keys-removed` strips it from sources. + 'ui/ListView:virtualScroll', + // #7176 — the `ObjectListViewSchema` copy of `ui/ListView:bordered`: the def is + // `ListViewSchema.omit({userFilters}).extend(…)`, so the tombstone lands in + // this walked shape too and `authorable-surface/` marks it `[RETIRED]` + // separately. Registered per key, as gate (b) reads them — nothing radiates + // from the base (the `shared/FieldMapping:transform` precedent). + 'ui/ObjectListView:bordered', + // #7176 — the `ObjectListViewSchema` copy of `ui/ListView:striped`: the def is + // `ListViewSchema.omit({userFilters}).extend(…)`, so the tombstone lands in + // this walked shape too and `authorable-surface/` marks it `[RETIRED]` + // separately. Registered per key, as gate (b) reads them — nothing radiates + // from the base (the `shared/FieldMapping:transform` precedent). + 'ui/ObjectListView:striped', + // #7176 — the `ObjectListViewSchema` copy of `ui/ListView:virtualScroll`: the + // def is `ListViewSchema.omit({userFilters}).extend(…)`, so the tombstone lands + // in this walked shape too and `authorable-surface/` marks it `[RETIRED]` + // separately. Registered per key, as gate (b) reads them — nothing radiates + // from the base (the `shared/FieldMapping:transform` precedent). + 'ui/ObjectListView:virtualScroll', // #6946 — three SDUI page-component props, retired by maintainer ruling // 2026-08-09 (decision-inbox round, 「全部接受」): objectui#3829 route (c) // for the first two, objectui#3818 for the third. Registered per key, as diff --git a/packages/spec/src/ui/view.form.ts b/packages/spec/src/ui/view.form.ts index df6e426847..1d34786747 100644 --- a/packages/spec/src/ui/view.form.ts +++ b/packages/spec/src/ui/view.form.ts @@ -53,8 +53,7 @@ export const viewForm = defineForm({ columns: 2, fields: [ { field: 'resizable', colSpan: 1 }, - { field: 'striped', colSpan: 1 }, - { field: 'bordered', colSpan: 1 }, + // `striped` / `bordered` inputs removed with the keys (#7176 — pass-through-only, retired). { field: 'compactToolbar', colSpan: 1 }, { field: 'rowHeight', colSpan: 1 }, { field: 'selection', type: 'composite', colSpan: 2 }, diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 5479430442..e9d2312690 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { ViewSchema, ListViewSchema, + ObjectListViewSchema, FormViewSchema, FormSectionSchema, KanbanConfigSchema, @@ -1154,11 +1155,12 @@ describe('Enhanced ListViewSchema', () => { }); it('should accept grid features', () => { + // `striped` / `bordered` left this fixture with their #7176 retirement — + // the surviving grid-feature keys are what this asserts now. const listView: ListView = { columns: ['name', 'status'], resizable: true, - striped: true, - bordered: true, + compactToolbar: true, }; expect(() => ListViewSchema.parse(listView)).not.toThrow(); @@ -1222,8 +1224,6 @@ describe('Enhanced ListViewSchema', () => { sort: [{ field: 'annual_revenue', order: 'desc' }], searchableFields: ['account_name', 'industry'], resizable: true, - striped: true, - bordered: false, selection: { type: 'multiple', }, @@ -1480,7 +1480,6 @@ describe('Real-World Enhanced View Examples', () => { { field: 'status', width: 100 }, ], resizable: true, - striped: true, selection: { type: 'multiple', }, @@ -1537,8 +1536,6 @@ describe('Real-World Enhanced View Examples', () => { { field: 'completion', type: 'percent', align: 'right', width: 100 }, ], resizable: true, - striped: true, - bordered: true, selection: { type: 'single', }, @@ -2807,6 +2804,67 @@ describe('ListViewSchema — retired responsive/performance (#3896 close-out)', }); }); +// ============================================================================ +// #7176: ListView striped / bordered / virtualScroll — pass-through-only, +// retired under ADR-0049 enforce-or-remove (maintainer ruling 2026-08-10). +// Every measured reader copied the key forward; ObjectGrid, where the chains +// end, never spells any of the three. +// ============================================================================ +describe('ListViewSchema — retired striped/bordered/virtualScroll (#7176 pass-through-only)', () => { + it('REJECTS the retired `striped` with the prescription', () => { + expect(() => ListViewSchema.parse({ + type: 'grid', columns: ['name'], striped: true, + })).toThrow(/`view\.striped`.*removed.*no renderer ever applied it.*Delete the key/s); + }); + it('REJECTS the retired `bordered` with the prescription', () => { + expect(() => ListViewSchema.parse({ + type: 'grid', columns: ['name'], bordered: true, + })).toThrow(/`view\.bordered`.*removed.*Delete the key/s); + }); + it('REJECTS the retired `virtualScroll` with the prescription, naming the live mechanism', () => { + expect(() => ListViewSchema.parse({ + type: 'grid', columns: ['name'], virtualScroll: true, + })).toThrow(/`view\.virtualScroll`.*removed.*large datasets page via `pagination`/s); + }); + it('the prescriptions carry the pinned migrate sentence (#7176 rides the protocol-17 conversion)', () => { + for (const key of ['striped', 'bordered', 'virtualScroll'] as const) { + let message = ''; + try { + ListViewSchema.parse({ type: 'grid', columns: ['name'], [key]: true }); + } catch (e) { message = String((e as Error).message); } + expect(message).toMatch(/#7176/); + expect(message).toMatch(/Run `os migrate meta --from 16` to rewrite existing sources automatically\./); + } + }); + it('accepts the live grid siblings byte-identically (rowHeight/selection/pagination/resizable)', () => { + const siblings = { + resizable: true, + rowHeight: 'compact', + selection: { type: 'multiple' }, + pagination: { pageSize: 50, pageSizeOptions: [25, 50, 100] }, + }; + const parsed = ListViewSchema.parse({ type: 'grid', columns: ['name'], ...siblings }); + expect(parsed.resizable).toStrictEqual(siblings.resizable); + expect(parsed.rowHeight).toStrictEqual(siblings.rowHeight); + expect(parsed.selection).toStrictEqual(siblings.selection); + expect(parsed.pagination).toStrictEqual(siblings.pagination); + }); + it('a clean parse output does not resurrect the keys', () => { + const parsed = ListViewSchema.parse({ type: 'grid', columns: ['name'] }); + expect(parsed).not.toHaveProperty('striped'); + expect(parsed).not.toHaveProperty('bordered'); + expect(parsed).not.toHaveProperty('virtualScroll'); + }); + it('ObjectListViewSchema (the .extend copy) rejects the keys with the same prescription', () => { + expect(() => ObjectListViewSchema.parse({ + type: 'grid', columns: ['name'], striped: true, + })).toThrow(/`view\.striped`.*removed/s); + expect(() => ObjectListViewSchema.parse({ + type: 'grid', columns: ['name'], virtualScroll: true, + })).toThrow(/`view\.virtualScroll`.*removed/s); + }); +}); + describe('HttpMethodSubsetSchema/HttpRequestSchema backward compat', () => { it('should still be importable from view.zod', () => { expect(HttpMethodSubsetSchema).toBeDefined(); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 1bc31cdfe8..026aa35813 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1352,8 +1352,8 @@ export const ListViewSchema = lazySchema(() => strictObject({ /** Grid Features */ resizable: z.boolean().optional().describe('Enable column resizing'), - striped: z.boolean().optional().describe('Striped row styling'), - bordered: z.boolean().optional().describe('Show borders'), + // `striped` / `bordered` — tombstoned at the bottom of this shape with + // `virtualScroll` (#7176, pass-through-only; see the retired block below). compactToolbar: z.boolean().optional().describe('Collapse Group/Color/Density/Hide-fields into a single View settings popover'), /** Selection */ @@ -1410,9 +1410,6 @@ export const ListViewSchema = lazySchema(() => strictObject({ + '`${ctx.selection.ids}` / `${ctx.selection.count}`.', ), - /** Performance */ - virtualScroll: z.boolean().optional().describe('Enable virtual scrolling for large datasets'), - /** Conditional Formatting */ conditionalFormatting: z.array(strictObject({ surface: 'this conditional formatting rule', @@ -1492,6 +1489,32 @@ export const ListViewSchema = lazySchema(() => strictObject({ 'Delete the key. ' + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', ), + + // `striped` / `bordered` / `virtualScroll` REMOVED (#7176, ADR-0049 + // enforce-or-remove; maintainer ruling 2026-08-10). Every measured reader was + // a forwarding copy — react spec-bridge → plugin-list → plugin-view/app-shell + // — and the chain ends at ObjectGrid, which never spells any of the three: + // copy-without-apply is dead in effect. Per the ruling, if objectui wants one + // of these as real behavior, that is an implementation card filed first, and + // the key stays retired pending it. + striped: retiredKey( + '`view.striped` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — ' + + 'every measured reader only copied it forward and no renderer ever applied it, so authoring ' + + 'it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. ' + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', + ), + bordered: retiredKey( + '`view.bordered` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — ' + + 'every measured reader only copied it forward and no renderer ever applied it (the grid frame ' + + "is the renderer's own constant, not authorable). Delete the key. " + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', + ), + virtualScroll: retiredKey( + '`view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — ' + + 'every measured reader only copied it forward and no grid ever virtualized off it; authoring ' + + 'it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. ' + + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', + ), })); /** From 780ac177b666f2150a0a54ea80e29338574ad45d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:16:53 +0000 Subject: [PATCH 2/4] test(spec): match the striped prescription's actual casing in the pin test Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r --- packages/spec/src/ui/view.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index e9d2312690..a64cb46d3d 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -2814,7 +2814,7 @@ describe('ListViewSchema — retired striped/bordered/virtualScroll (#7176 pass- it('REJECTS the retired `striped` with the prescription', () => { expect(() => ListViewSchema.parse({ type: 'grid', columns: ['name'], striped: true, - })).toThrow(/`view\.striped`.*removed.*no renderer ever applied it.*Delete the key/s); + })).toThrow(/`view\.striped`.*removed.*no renderer ever applied it.*delete the key/s); }); it('REJECTS the retired `bordered` with the prescription', () => { expect(() => ListViewSchema.parse({ From 106cee14422916223c4430b6c7a40be66bedd516 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 03:00:15 +0000 Subject: [PATCH 3/4] merge origin/main (os-regen artifacts taken from main; regeneration follows) --- content/docs/references/ui/view.mdx | 12 ++++++------ docs/protocol-upgrade-guide.md | 3 --- packages/spec/authorable-surface/ui.json | 12 ++++++------ packages/spec/liveness/state-counts.md | 4 ++-- packages/spec/spec-changes.json | 12 ------------ 5 files changed, 14 insertions(+), 29 deletions(-) diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 5dbf3df754..7680fe43f7 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -475,6 +475,8 @@ List chart view configuration | **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | | **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields | | **resizable** | `boolean` | optional | Enable column resizing | +| **striped** | `boolean` | optional | Striped row styling | +| **bordered** | `boolean` | optional | Show borders | | **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | @@ -496,6 +498,7 @@ List chart view configuration | **rowActions** | `string[]` | optional | Actions available for individual row items | | **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | | **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **virtualScroll** | `boolean` | optional | Enable virtual scrolling for large datasets | | **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | | **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | | **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'pdf' \| 'json'>[]` | optional | Available export format options | @@ -509,9 +512,6 @@ List chart view configuration | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | | **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | -| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | -| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | -| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. Run `os migrate meta --from 16` to rewrite existing sources automatically. | --- @@ -563,6 +563,8 @@ List chart view configuration | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | | **resizable** | `boolean` | optional | Enable column resizing | +| **striped** | `boolean` | optional | Striped row styling | +| **bordered** | `boolean` | optional | Show borders | | **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | @@ -584,6 +586,7 @@ List chart view configuration | **rowActions** | `string[]` | optional | Actions available for individual row items | | **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | | **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | +| **virtualScroll** | `boolean` | optional | Enable virtual scrolling for large datasets | | **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | | **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | | **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'pdf' \| 'json'>[]` | optional | Available export format options | @@ -597,9 +600,6 @@ List chart view configuration | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | | **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | -| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | -| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | -| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index a1d8e2f64b..a44a0b5e7f 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -238,8 +238,6 @@ The plugin manifest loses its whole `loading` block in this step (#4914, ADR-004 The action LOCATION vocabulary loses `global_nav` in this step (#6888, ADR-0049, maintainer ruling 2026-08-09). It was declared from the day `ACTION_LOCATIONS` was written and no product surface ever served it: the console command palette composes its groups from nav items, objects, dashboards, pages, reports, recent items and record search, and reads no action metadata at all — so an action declaring this location never reached a user. What lifts it above ordinary inert-declaration cleanup is that the authoring tool PROMISED the surface: the Studio designer previewed a mock `⌘K · Command palette` frame for exactly this value, so an author (very often an AI, ADR-0033) declared it, watched it "render", shipped it, and got nothing — the ADR-0078 shape arriving through a location vocabulary rather than through a missing key. It was retired rather than implemented because the demand evidence is empty: no user has asked for command-palette actions and the only two declarers were our own showcase corpus, so wiring the palette would have been capability expansion with no pull. This is an enum VALUE, not a key, so — as with `crypto.hash` and the two aggregate functions above — there is no `retiredKey()` tombstone: the enum error map carries the prescription, keyed on the received value so only the spelling that used to be legal is told it "was removed". The conversion strips the value from `action.locations` and KEEPS the key even when the array empties, because on this surface `locations: []` and an absent `locations` are different declarations: the empty array is the documented headless shape (callable over REST/MCP/AI, capability gate and audit trail intact), while an absent key means nobody placed the action — which is what `packages/lint`'s `action-no-placement` warns about. An object-less action, whose only reason for declaring `global_nav` was that it has no row and no record header to render on, is therefore migrated to the declaration it always meant. -It also removes the three pass-through-only list-view display keys `striped` / `bordered` / `virtualScroll` (#7176, ADR-0049 enforce-or-remove, maintainer ruling 2026-08-10). All three were graded live on reads that turned out to be forwarding copies: the react spec-bridge, plugin-list and plugin-view/app-shell each copy the key onto the next node, and the chain ends at ObjectGrid, which never spells any of the three — so an author who wrote `striped: true` got a parse-clean no-op, the exact silent-no-op shape enforce-or-remove exists to end. Copy-without-apply is dead in effect; per the ruling, if objectui wants one of these as real behavior, that is an implementation card filed first, and the key stays retired pending it. - ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | @@ -263,7 +261,6 @@ It also removes the three pass-through-only list-view display keys `striped` / ` | `action-inert-keys-removed` | `action.shortcut / action.bulkEnabled` | action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | retired — `migrate meta` only | | `flow-inert-keys-removed` | `flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId` | flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle) | retired — `migrate meta` only | | `view-inert-keys-removed` | `view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria` | view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live) | retired — `migrate meta` only | -| `view-list-passthrough-keys-removed` | `view.list.striped / view.list.bordered / view.list.virtualScroll` | view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove) | retired — `migrate meta` only | | `dashboard-inert-keys-removed` | `dashboard.aria / dashboard.performance / dashboard.widgets[].performance` | dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them) | retired — `migrate meta` only | | `dashboard-widget-responsive-removed` | `dashboard.widgets[].responsive` | dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected) | retired — `migrate meta` only | | `dashboard-widget-action-aria-removed` | `dashboard.widgets[].actionUrl / dashboard.widgets[].actionType / dashboard.widgets[].actionIcon / dashboard.widgets[].aria` | dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (#5010 — no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description) | retired — `migrate meta` only | diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index b5ade541c5..8d274e953c 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -631,7 +631,7 @@ "ui/ListView:allowPrinting", "ui/ListView:appearance", "ui/ListView:aria", - "ui/ListView:bordered [RETIRED]", + "ui/ListView:bordered", "ui/ListView:bulkActionDefs", "ui/ListView:bulkActions", "ui/ListView:calendar", @@ -667,14 +667,14 @@ "ui/ListView:sharing", "ui/ListView:showRecordCount", "ui/ListView:sort", - "ui/ListView:striped [RETIRED]", + "ui/ListView:striped", "ui/ListView:tabs", "ui/ListView:timeline", "ui/ListView:tree", "ui/ListView:type", "ui/ListView:userActions", "ui/ListView:userFilters", - "ui/ListView:virtualScroll [RETIRED]", + "ui/ListView:virtualScroll", "ui/NavigationArea:description", "ui/NavigationArea:icon", "ui/NavigationArea:id", @@ -694,7 +694,7 @@ "ui/ObjectListView:allowPrinting", "ui/ObjectListView:appearance", "ui/ObjectListView:aria", - "ui/ObjectListView:bordered [RETIRED]", + "ui/ObjectListView:bordered", "ui/ObjectListView:bulkActionDefs", "ui/ObjectListView:bulkActions", "ui/ObjectListView:calendar", @@ -730,14 +730,14 @@ "ui/ObjectListView:sharing", "ui/ObjectListView:showRecordCount", "ui/ObjectListView:sort", - "ui/ObjectListView:striped [RETIRED]", + "ui/ObjectListView:striped", "ui/ObjectListView:tabs", "ui/ObjectListView:timeline", "ui/ObjectListView:tree", "ui/ObjectListView:type", "ui/ObjectListView:userActions", "ui/ObjectListView:userFilters", - "ui/ObjectListView:virtualScroll [RETIRED]", + "ui/ObjectListView:virtualScroll", "ui/ObjectNavItem:badge", "ui/ObjectNavItem:badgeVariant", "ui/ObjectNavItem:filters", diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 427de6ccb7..2648a9b2ee 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -39,7 +39,7 @@ for both corollaries. | `skill` | 16 | 0 | 1 | 0 | 17 | | `dataset` | 27 | 0 | 0 | 0 | 27 | | `page` | 23 | 0 | 0 | 1 | 24 | -| `view` | 77 | 0 | 9 | 0 | 86 | +| `view` | 80 | 0 | 6 | 0 | 86 | | `report` | 21 | 0 | 0 | 0 | 21 | | `dashboard` | 34 | 0 | 7 | 0 | 41 | | `webhook` | 19 | 0 | 0 | 0 | 19 | @@ -57,4 +57,4 @@ for both corollaries. | `api` | 25 | 0 | 0 | 2 | 27 | | `capability` | 12 | 0 | 0 | 0 | 12 | | `qa` | 4 | 0 | 5 | 0 | 9 | -| **total** | **774** | **6** | **55** | **8** | **843** | +| **total** | **777** | **6** | **52** | **8** | **843** | diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index a8f7c4e8ff..166d8dbf9d 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -182,12 +182,6 @@ "conversionId": "view-inert-keys-removed", "toMajor": 17 }, - { - "surface": "view.list.striped / view.list.bordered / view.list.virtualScroll", - "to": "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", - "conversionId": "view-list-passthrough-keys-removed", - "toMajor": 17 - }, { "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", @@ -1254,12 +1248,6 @@ "conversionId": "view-inert-keys-removed", "toMajor": 17 }, - { - "surface": "view.list.striped / view.list.bordered / view.list.virtualScroll", - "to": "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", - "conversionId": "view-list-passthrough-keys-removed", - "toMajor": 17 - }, { "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", From 972ad4e0f5f725558aeb6329937f0afce7432495 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 03:07:21 +0000 Subject: [PATCH 4/4] chore(spec): regenerate artifacts after merging origin/main (4dc1c7da3 relay: #8230/#8232/#8234/#8236/#8239) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r --- content/docs/references/ui/view.mdx | 12 ++++++------ docs/protocol-upgrade-guide.md | 3 +++ packages/spec/authorable-surface/ui.json | 12 ++++++------ packages/spec/liveness/state-counts.md | 4 ++-- packages/spec/spec-changes.json | 12 ++++++++++++ 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 7680fe43f7..5dbf3df754 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -475,8 +475,6 @@ List chart view configuration | **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | | **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields | | **resizable** | `boolean` | optional | Enable column resizing | -| **striped** | `boolean` | optional | Striped row styling | -| **bordered** | `boolean` | optional | Show borders | | **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | @@ -498,7 +496,6 @@ List chart view configuration | **rowActions** | `string[]` | optional | Actions available for individual row items | | **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | | **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | -| **virtualScroll** | `boolean` | optional | Enable virtual scrolling for large datasets | | **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | | **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | | **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'pdf' \| 'json'>[]` | optional | Available export format options | @@ -512,6 +509,9 @@ List chart view configuration | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | | **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. Run `os migrate meta --from 16` to rewrite existing sources automatically. | --- @@ -563,8 +563,6 @@ List chart view configuration | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | | **resizable** | `boolean` | optional | Enable column resizing | -| **striped** | `boolean` | optional | Striped row styling | -| **bordered** | `boolean` | optional | Show borders | | **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover | | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | @@ -586,7 +584,6 @@ List chart view configuration | **rowActions** | `string[]` | optional | Actions available for individual row items | | **bulkActions** | `string[]` | optional | Actions available when multiple rows are selected | | **bulkActionDefs** | `{ name: string; label?: string; icon?: string; variant?: Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'outline'>; … }[]` | optional | Rich bulk action definitions (schema-driven, executed via BulkActionDialog). Use a def for a mass data-plane mutation ('update' with a `patch` / 'delete') that no action expresses, or for an `operation: 'custom'` + `execution: 'aggregate'` entry (objectui#3139) that dispatches the action it NAMES once for the whole selection — the renderer injects `params._selectedIds: string[]` (read that on the server, not `recordId`) so a single call can produce one aggregate artifact (zip of QR codes, merged PDF, batch print). Aggregate results are all-or-nothing: a handler that cannot cover the whole selection must reject, and per-row retry is replaced by re-running the action. `batchSize` does not apply (the call is never chunked); set `maxRecords` on defs whose server work is expensive. For the PER-RECORD dispatch use `bulkActions: ['']` instead — the bare-string form, promoted with the action's own label, params and `visible`; a 'custom' def without `execution: 'aggregate'` has no dispatcher and is refused at parse time (#4457). Toolbar url/api actions can also interpolate the current selection via `${ctx.selection.ids}` / `${ctx.selection.count}`. | -| **virtualScroll** | `boolean` | optional | Enable virtual scrolling for large datasets | | **conditionalFormatting** | `{ condition: string \| object; style: Record }[]` | optional | Conditional formatting rules for list rows | | **inlineEdit** | `boolean` | optional | Allow inline editing of records directly in the list view | | **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'pdf' \| 'json'>[]` | optional | Available export format options | @@ -600,6 +597,9 @@ List chart view configuration | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | | **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **striped** | `never` | optional | [REMOVED] `view.striped` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it, so authoring it was a parse-clean no-op. There is no authorable striped-rows switch; delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **bordered** | `never` | optional | [REMOVED] `view.bordered` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no renderer ever applied it (the grid frame is the renderer's own constant, not authorable). Delete the key. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **virtualScroll** | `never` | optional | [REMOVED] `view.virtualScroll` was removed in @objectstack/spec 17.0.0 (#7176, ADR-0049 enforce-or-remove) — every measured reader only copied it forward and no grid ever virtualized off it; authoring it was a parse-clean no-op. Delete the key; large datasets page via `pagination`. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: object[] }` | optional | | diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index a44a0b5e7f..a1d8e2f64b 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -238,6 +238,8 @@ The plugin manifest loses its whole `loading` block in this step (#4914, ADR-004 The action LOCATION vocabulary loses `global_nav` in this step (#6888, ADR-0049, maintainer ruling 2026-08-09). It was declared from the day `ACTION_LOCATIONS` was written and no product surface ever served it: the console command palette composes its groups from nav items, objects, dashboards, pages, reports, recent items and record search, and reads no action metadata at all — so an action declaring this location never reached a user. What lifts it above ordinary inert-declaration cleanup is that the authoring tool PROMISED the surface: the Studio designer previewed a mock `⌘K · Command palette` frame for exactly this value, so an author (very often an AI, ADR-0033) declared it, watched it "render", shipped it, and got nothing — the ADR-0078 shape arriving through a location vocabulary rather than through a missing key. It was retired rather than implemented because the demand evidence is empty: no user has asked for command-palette actions and the only two declarers were our own showcase corpus, so wiring the palette would have been capability expansion with no pull. This is an enum VALUE, not a key, so — as with `crypto.hash` and the two aggregate functions above — there is no `retiredKey()` tombstone: the enum error map carries the prescription, keyed on the received value so only the spelling that used to be legal is told it "was removed". The conversion strips the value from `action.locations` and KEEPS the key even when the array empties, because on this surface `locations: []` and an absent `locations` are different declarations: the empty array is the documented headless shape (callable over REST/MCP/AI, capability gate and audit trail intact), while an absent key means nobody placed the action — which is what `packages/lint`'s `action-no-placement` warns about. An object-less action, whose only reason for declaring `global_nav` was that it has no row and no record header to render on, is therefore migrated to the declaration it always meant. +It also removes the three pass-through-only list-view display keys `striped` / `bordered` / `virtualScroll` (#7176, ADR-0049 enforce-or-remove, maintainer ruling 2026-08-10). All three were graded live on reads that turned out to be forwarding copies: the react spec-bridge, plugin-list and plugin-view/app-shell each copy the key onto the next node, and the chain ends at ObjectGrid, which never spells any of the three — so an author who wrote `striped: true` got a parse-clean no-op, the exact silent-no-op shape enforce-or-remove exists to end. Copy-without-apply is dead in effect; per the ruling, if objectui wants one of these as real behavior, that is an implementation card filed first, and the key stays retired pending it. + ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | @@ -261,6 +263,7 @@ The action LOCATION vocabulary loses `global_nav` in this step (#6888, ADR-0049, | `action-inert-keys-removed` | `action.shortcut / action.bulkEnabled` | action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | retired — `migrate meta` only | | `flow-inert-keys-removed` | `flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId` | flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle) | retired — `migrate meta` only | | `view-inert-keys-removed` | `view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria` | view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live) | retired — `migrate meta` only | +| `view-list-passthrough-keys-removed` | `view.list.striped / view.list.bordered / view.list.virtualScroll` | view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove) | retired — `migrate meta` only | | `dashboard-inert-keys-removed` | `dashboard.aria / dashboard.performance / dashboard.widgets[].performance` | dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them) | retired — `migrate meta` only | | `dashboard-widget-responsive-removed` | `dashboard.widgets[].responsive` | dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected) | retired — `migrate meta` only | | `dashboard-widget-action-aria-removed` | `dashboard.widgets[].actionUrl / dashboard.widgets[].actionType / dashboard.widgets[].actionIcon / dashboard.widgets[].aria` | dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (#5010 — no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description) | retired — `migrate meta` only | diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 8d274e953c..b5ade541c5 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -631,7 +631,7 @@ "ui/ListView:allowPrinting", "ui/ListView:appearance", "ui/ListView:aria", - "ui/ListView:bordered", + "ui/ListView:bordered [RETIRED]", "ui/ListView:bulkActionDefs", "ui/ListView:bulkActions", "ui/ListView:calendar", @@ -667,14 +667,14 @@ "ui/ListView:sharing", "ui/ListView:showRecordCount", "ui/ListView:sort", - "ui/ListView:striped", + "ui/ListView:striped [RETIRED]", "ui/ListView:tabs", "ui/ListView:timeline", "ui/ListView:tree", "ui/ListView:type", "ui/ListView:userActions", "ui/ListView:userFilters", - "ui/ListView:virtualScroll", + "ui/ListView:virtualScroll [RETIRED]", "ui/NavigationArea:description", "ui/NavigationArea:icon", "ui/NavigationArea:id", @@ -694,7 +694,7 @@ "ui/ObjectListView:allowPrinting", "ui/ObjectListView:appearance", "ui/ObjectListView:aria", - "ui/ObjectListView:bordered", + "ui/ObjectListView:bordered [RETIRED]", "ui/ObjectListView:bulkActionDefs", "ui/ObjectListView:bulkActions", "ui/ObjectListView:calendar", @@ -730,14 +730,14 @@ "ui/ObjectListView:sharing", "ui/ObjectListView:showRecordCount", "ui/ObjectListView:sort", - "ui/ObjectListView:striped", + "ui/ObjectListView:striped [RETIRED]", "ui/ObjectListView:tabs", "ui/ObjectListView:timeline", "ui/ObjectListView:tree", "ui/ObjectListView:type", "ui/ObjectListView:userActions", "ui/ObjectListView:userFilters", - "ui/ObjectListView:virtualScroll", + "ui/ObjectListView:virtualScroll [RETIRED]", "ui/ObjectNavItem:badge", "ui/ObjectNavItem:badgeVariant", "ui/ObjectNavItem:filters", diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 2648a9b2ee..427de6ccb7 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -39,7 +39,7 @@ for both corollaries. | `skill` | 16 | 0 | 1 | 0 | 17 | | `dataset` | 27 | 0 | 0 | 0 | 27 | | `page` | 23 | 0 | 0 | 1 | 24 | -| `view` | 80 | 0 | 6 | 0 | 86 | +| `view` | 77 | 0 | 9 | 0 | 86 | | `report` | 21 | 0 | 0 | 0 | 21 | | `dashboard` | 34 | 0 | 7 | 0 | 41 | | `webhook` | 19 | 0 | 0 | 0 | 19 | @@ -57,4 +57,4 @@ for both corollaries. | `api` | 25 | 0 | 0 | 2 | 27 | | `capability` | 12 | 0 | 0 | 0 | 12 | | `qa` | 4 | 0 | 5 | 0 | 9 | -| **total** | **777** | **6** | **52** | **8** | **843** | +| **total** | **774** | **6** | **55** | **8** | **843** | diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 166d8dbf9d..a8f7c4e8ff 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -182,6 +182,12 @@ "conversionId": "view-inert-keys-removed", "toMajor": 17 }, + { + "surface": "view.list.striped / view.list.bordered / view.list.virtualScroll", + "to": "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", + "conversionId": "view-list-passthrough-keys-removed", + "toMajor": 17 + }, { "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", @@ -1248,6 +1254,12 @@ "conversionId": "view-inert-keys-removed", "toMajor": 17 }, + { + "surface": "view.list.striped / view.list.bordered / view.list.virtualScroll", + "to": "view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove)", + "conversionId": "view-list-passthrough-keys-removed", + "toMajor": 17 + }, { "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)",