Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .changeset/view-list-passthrough-keys-retired.md
Original file line numberDiff line numberDiff line change
@@ -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.

<!-- adr-0087: registered view-list-passthrough-keys-removed -->
15 changes: 10 additions & 5 deletions content/docs/protocol/objectui/widget-contract.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

<Callout type="info">
`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.
</Callout>

## Theme
Expand Down
12 changes: 6 additions & 6 deletions content/docs/references/ui/view.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.) |
Expand All@@ -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: ['<name>']` 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<string, string> }[]` | 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 |
Expand All@@ -512,6 +509,9 @@ List chart view configuration
| **aria** | `{ ariaLabel?: string \| Record<string, string>; 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. |


---
Expand DownExpand Up@@ -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.) |
Expand All@@ -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: ['<name>']` 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<string, string> }[]` | 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 |
Expand All@@ -600,6 +597,9 @@ List chart view configuration
| **aria** | `{ ariaLabel?: string \| Record<string, string>; 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 | |


Expand Down
3 changes: 3 additions & 0 deletions docs/protocol-upgrade-guide.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand All@@ -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 |
Expand Down
Loading
Loading