Skip to content

Commit aa46322

Browse files
feat(spec)!: an object-grid page block types the seven members the grid reads, and resizableColumns retires for resizable (#21445) (#21463)
Fixes #21445 Clause-②: yes (narrowing) ## What this does `ComponentPropsMap['object-grid']` (`ObjectGridPropsSchema`, `packages/spec/src/ui/component.zod.ts`) now types the seven members objectui's `ObjectGrid` reads with a fixed shape, and retires `resizableColumns` to a `retiredKey()` tombstone naming `resizable`. Direction: triage 5958164933, accepted; claim 5958653004. Before this, `rowHeight: 42` passed every door and rendered as a compact grid, and every other off-shape value was substituted or dropped with no report. The public door now answers it. Measured through the component-props gate (`validateComponentProps`, the rule `os validate` / `os build` / `os lint` run) on this branch: ``` component-props-invalid properties.rowHeight rowHeight: Invalid option: expected one of "compact"|"short"|"medium"|"tall"|"extra_tall" (received 42) component-props-invalid properties.resizableColumns `object-grid` property `resizableColumns` was removed in @objectstack/spec 17.7.0 (ADR-0049) — ... component-props-unknown-key properties.operations.read Unrecognized key(s) on this `object-grid` operations block: `read`. ``` ### Item 1 — seven members, each measured at the `.objectui-sha` pin `89cad75d55` (`plugin-grid/src/ObjectGrid.tsx`) | member | was | now | read point at the pin | |:--|:--|:--|:--| | `rowHeight` | `z.unknown()` | `RowHeightSchema`, by reference | `resolveRowHeightMode` :1311 | | `rowColor` | `z.unknown()` | `RowColorConfigSchema`, by reference | `useRowColor` :2955 (`field`, `colors`) | | `navigation` | `z.unknown()` | `NavigationConfigSchema`, by reference (the carrier `object-kanban` / `object-calendar` / `object-timeline` already take) | `useNavigationOverlay` :2894 | | `conditionalFormatting` | `z.unknown()` | `ListViewSchema.shape.conditionalFormatting`, the list view's own member | `resolveConditionalFormatting` :2964 | | `bulkActionDefs` | `z.array(z.unknown())` | `z.array(BulkActionDefSchema)`, the list view's element | `resolveBulkActions` :4778 | | `aggregations` | `z.unknown()` | `[{ field, type }]`, `type` = `AggregationFunction` (the query AST vocabulary, by reference) | `useGroupedData` / `useServerGroupHeaders` :3095 / :3113 | | `operations` | `z.unknown()` | `{ create?, update?, delete?, export? }` booleans, strict; `read` / `import` refused with guidance | `create` :5468, `update` / `delete` :1898-1899, `export` :4088 / :5340 / :6141 | - `aggregations` and `operations` have no list-view counterpart, so they declare the measured shape in two module-private schemas (`GridAggregationSchema`, `GridOperationsSchema`). No export is added: `check:api-surface` is unchanged. - **`rowHeight`, mechanism assumption 4.** The renderer's set and the schema's set match. `resolveRowHeightMode` tests membership against `ROW_HEIGHT_TO_DENSITY_MODE`, which is typed as a record keyed by the spec's `RowHeight`, so it admits exactly the five values. Anything else answers `compact`. - **`aggregations`.** The grid's vocabulary (`AggregationType` in `useGroupedData.ts`) is exactly `AggregationFunction`'s six members. The pin holds that set, so a member added to the query AST enum reds here before it widens this door past the grid's reader. - **`operations`.** These are the four members any read point names. objectui's TypeScript twin also declares `read` and `import`, which nothing reads. Each is refused with that reason, not accepted as a toggle that toggles nothing. ### `bulkActionDefs` measured against `BulkActionDefSchema`: the gap, reported and not widened The renderer reads members the schema lacks. Every one of these gaps already stands on the list view's door, which takes the same schema: - **Def level.** `name`, `label`, `icon`, `variant`, `operation`, `execution`, `patch`, `params`, `confirmText`, `confirmLabel`, `visible`, `requiredPermissions`, `maxRecords` and `batchSize` are all declared. `actionDef` is read too (`useBulkExecutor`, `BulkActionDialog`), and the schema refuses it on purpose with `guidance`, because the renderer attaches it and an author does not write it. - **`params[]`.** `bulkParamToField` spreads every undeclared param key onto the widget's field bag, so `min` / `max` / `step` / `accept` / `lookupFilters` … reach a widget that reads them. The schema refuses them (`BULK_PARAM_WIDGET_CONFIG_KEYS`, the maintainer ruling recorded in `bulk-action.zod.ts`). - **`visible`.** objectui's `ExpressionWire` takes `{ dialect?, source }` with `dialect` optional. The spec's `EvaluatedExpressionInputSchema` envelope requires `dialect`. - **`params[].type`.** objectui types it `string`, while the spec types it `FieldType`. ### `conditionalFormatting`: one dialect, the list view's The shared evaluator (`@object-ui/core` `resolveConditionalFormatting`) also tolerates two objectui-native rule spellings, `{ field, operator, value, backgroundColor, … }` and `{ expression, … }`. The list view declares neither, so the grid door follows the list view and refuses both. The census found no bag that writes either. ### Item 2 — `resizableColumns` retired (objectui#6152 ruling A) - **Tombstone.** `resizableColumns` is a `retiredKey()` with the house prescription: "Use `resizable`. Rename the key; the value (a boolean) is unchanged." plus the `os migrate meta --from 17` sentence. `tsc` refuses it, and so does the parse. `resizable` still parses. - **D2 conversion `object-grid-resizable-columns-removed`.** It is protocol 18, `retiredFromLoadPath`, `retiredAfter` `17.6.0`, order 57, and it follows `resizable ?? resizableColumns` exactly: - when `resizable` is absent (or null), the value moves to `resizable` unchanged; - when `resizable` holds a value, the key strips as a lossless delete. It is scoped by component `type`. The fixture covers a region grid, a both-keys grid with different values, a kanban sibling and a grid without the key (both untouched), a nested grid and a slotted grid: 4 notices. - **Registration.** - `RETIRED_KEYS_BY_MAJOR[18]` carries `ui/ObjectGridProps:resizableColumns`, written by `gen:migration-registry`. - The D3 entry `object-grid-resizable-columns-retired` carries `conversionIds`, and it carries the judgment for a grid that authored both keys with different values. - Two `STEP18_RATIONALE` fragments are added at orders 62 / 63. - **The typed members' D3 entry** is `ui-object-grid-row-members-typed`. It has no conversion: a page component's `properties` is not parsed on the save or load path, and an off-shape value has no rewrite that keeps what the grid shows while honouring what the author wrote. - **Changeset.** `@objectstack/spec` `minor`, `!`, **BREAKING** banner, a FROM → TO table, and one ADR-0087 `registered` marker naming all three ids. ## Census of authored writers (Zone 2 assumption 3) - **objectstack** `origin/main` `53fd35e3e3`: the examples, `packages/platform-objects`, the spec tests, `content/docs` and `skills`. Zero `object-grid` blocks author any of the seven members or `resizableColumns`. Control: the same matcher finds `columns` on both showcase grids. - **objectui** at the pin `89cad75d55`, over 335 `object-grid` nodes, of which 76 carry a `properties` bag (23 of those in parsed JSON documents). The control: `columns` is found in 46 bags. - The one value a new shape refuses is objectui's root `README.md` "Data Grid" example, `"operations": { "create": true, "read": true, "update": true, "delete": true, "export": true }`. Its `read: true` is now refused. - objectui's own `content/docs/api/schema-reference.md` already dropped `operations.read` from its example as a key "which nothing reads" (objectui#11068). - **This producer is named in the dev report as a `needs_decision`.** The recommendation is to keep the refusal as implemented and correct the README in the objectui#6152 follow-up. The alternative is to declare `read` / `import` as two toggles that toggle nothing. - `resizableColumns`: zero writers in either repository. objectui's own mentions at the pin are its TypeScript twin and flat mirror (deprecated / tombstoned), the `data-table` component's own prop, and the `?? schema.resizableColumns` read itself. ## Clause-② — the measured arm | run | exit | reading | |:--|:--|:--| | `check-widening-tells --declaration no --diff` (final diff) | 4 | 8 tells, below | | `check-widening-tells --declaration yes --diff` | 0 | the gate never blocks a `yes` | - **T1, the bulk of the tells.** These fire on the new keys inside the two former `z.unknown()` bags (`aggregations[].field`, the four `operations` toggles). The gate's own text rules that shape: it is "a true refusal although it only narrows … declare `Clause-②: yes`". So the arm is `yes (narrowing)` although no export-listing row is added. - **T2, three tells.** These fire on the three new names in the multi-line `import { … } from './view.zod'` list, read as members of a closed set. They are a false positive of the matcher, noted below. ## Tests — at `52c4c42d72` - **`component-object-grid-typed-members.pin.test.ts`** (new, `local` project, 52 tests). - §1: each declared shape parses. It parses byte-identical, or to exactly what the shared schema answers for `navigation` (its defaults) and `conditionalFormatting` (the condition envelope). - §2: each off-shape value is refused, with the code AND the path. `rowHeight: 42` is `invalid_value` at `rowHeight`; `operations.read` is `unrecognized_keys` at `operations`, with its guidance text; a no-op `custom` bulk def is `custom` at `bulkActionDefs.0.execution`. - §3: identity of each by-reference def, plus the exact `AggregationFunction` set and the exact `operations` key set. - §4: the tombstone. `resizableColumns: true|false` is refused with the prescription, alone and beside `resizable`. `resizable: true|false` is byte-identical. A `@ts-expect-error` assignment of `resizableColumns` to `ObjectGridProps` is consumed, so the `tsc` channel is proven by `check:test-typecheck` staying green. - §5: the conversion's two arms and the untouched grid, and the step-18 / `RETIRED_KEYS_BY_MAJOR` wiring. - **`object-grid-resizable-columns-retirement.test.ts`** (new, `repo` project). This is the tree-scoped absence pin over the five roots `@objectstack/spec` already declares in `scripts/cross-package-test-inputs.mjs`, registered in `vitest.repo-tests.json`. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: **Test Files 602 passed (602), Tests 17769 passed | 1 todo**. - `pnpm --filter @objectstack/spec typecheck`: exit 0. `check:test-typecheck: OK — 52 file(s) / 246 error(s) / 135 pinned signature(s)`, unchanged. - **`repo` project.** 45 of its 50 files were run green, in four targeted runs. The five not run locally are `build-schemas-check-mode`, `dist-freshness`, `dist-freshness-adoption`, `publish-smoke-boot-failure` and `publish-smoke-port-collision`. They are script-machinery tests this diff does not touch; `build-schemas-check-mode` alone runs 7-18 s per case and the project overruns the ~10-minute foreground cap. **NOT MEASURED locally; declared to CI.** ### Ablations: one-time proofs, each restored by `scripts/ablation-replace.mjs` to blob == HEAD with `git diff HEAD` empty The pin imports `./component.zod` by relative path, so it reads `src/` and no build sits between the mutation and the run. | mutation | result | |:--|:--| | `rowHeight` back to `z.unknown()` | 3 failed / 49 passed: both `rowHeight` refusals and the identity pin | | tombstone key renamed away (`resizableColumns` becomes an undeclared key) | 3 failed: all three prescription pins (a bare `unrecognized_keys` is not the tombstone) | | `operations` back to `z.unknown()` | 6 failed: four refusals, the guidance-text pin and the key-set pin | | a `resizableColumns: false` planted in `examples/app-showcase/src/ui/pages/my-work.page.ts` | the absence pin fails, naming that file | The first attempt at the plant was refused by the tool, because the replacement contained its own anchor. It wrote nothing, and it was re-run with a non-overlapping anchor. ## Gates - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derives **114** families at `52c4c42d72`, identical to the seat's derivation. **All 114 exit 0** on that head; each exit code was captured before any pipe. - `--ran` reconciliation: "114 derived famil(ies) accounted for — 114 run, 0 NOT-MEASURED (a DERIVED zero — all 114 recorded an exit code and none of them is 3)". - `pnpm --filter @objectstack/spec check:generated` passes on the final head. Two artifacts were regenerated with `--fix` because the check proved them stale: - `content/docs/references/ui/component.mdx`; - `docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md` (+2 strict sites). - **Lint, a declared narrowing.** `node --stack-size=4000 node_modules/eslint/bin/eslint.js --no-inline-config --format json` ran on the changed `.ts` files: **10 files, 0 errors, 0 warnings**. - Population: `eslint.config.mjs`'s `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`. - Invariance: the config never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move a verdict on an untouched file. - The full `pnpm lint` is CI's. ## Acceptance notes (observations, not filed) - **`dropped-refinements.baseline.json` gains one site.** `ui/ObjectGridProps`'s `bulkActionDefs.element` is the list view's own `BulkActionDefSchema` refinement now reaching the grid door, and the gate printed the corrected entry. `droppedRefinementSites` moves 652 → 653. - **The regenerated reference page** now renders the shared `grouping.fields[]` nested shape with `order?` / `collapsed?` (their input face). This is generator output, not a hand edit. - **Same family, other rows, not touched here.** `object-kanban`'s `conditionalFormatting` is still `z.unknown()`, and three other rows declare `navigation: z.unknown()`. They are named for the family's closure card in the dev report. - **`check-widening-tells` T2 false positive.** A name added to a multi-line `import { … }` list is read as a new member of a closed set. It is named in the dev report. - **objectui follow-up (objectui#6152), once a release carrying this is installable:** - the TypeScript `resizableColumns` member becomes `never`, the flat mirror gets a tombstone, and the `?? schema.resizableColumns` read is dropped; - the README's `operations.read` comes out of its example; - the twin's `operations.read` / `.import` go; - the grid's two-dialect `conditionalFormatting` typing converges on the spec rule. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 49524f6 commit aa46322

14 files changed

Lines changed: 1167 additions & 28 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an `object-grid` page block's props type the seven members the grid reads with a fixed shape, and the legacy `resizableColumns` spelling is retired in favour of `resizable` (#21445)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered object-grid-resizable-columns-removed, object-grid-resizable-columns-retired, ui-object-grid-row-members-typed -->
10+
11+
**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path.
12+
13+
**`@objectstack/spec`**
14+
15+
- **Seven members of `ComponentPropsMap['object-grid']` are typed.** Each was `z.unknown()` (`bulkActionDefs` an array of it), although the console's `ObjectGrid` reads each with one shape. Any value passed, and the grid answered an off-shape one with a silent default: `rowHeight: 42` rendered as a compact grid, and an aggregation with an unknown function drew a zero nothing computed, or no number at all. Each member now takes the shape the grid reads:
16+
- `rowHeight` is the list view's `RowHeightSchema`: `compact`, `short`, `medium`, `tall` or `extra_tall`. These are exactly the five values the grid admits.
17+
- `rowColor` is the list view's `RowColorConfigSchema`, `{ field, colors }`.
18+
- `navigation` is the list view's `NavigationConfigSchema`, the same carrier `object-kanban`, `object-calendar` and `object-timeline` take.
19+
- `conditionalFormatting` is the list view's own member, `[{ condition, style }]`, with a CEL `condition` and a CSS `style` map.
20+
- `bulkActionDefs` is an array of the list view's `BulkActionDefSchema`.
21+
- `aggregations` is `[{ field, type }]`, with `type` drawn from the query AST's aggregation functions (`count`, `sum`, `avg`, `min`, `max`, `count_distinct`). No list-view schema declares this member, so the shape is the one the grid's grouping reads.
22+
- `operations` is `{ create?, update?, delete?, export? }`, the four booleans a grid read point names. `read` and `import` are refused with the reason: no grid read point reads either.
23+
- **`resizableColumns` is retired.** It was the legacy second spelling of `resizable`, read only when `resizable` was absent, so a grid authoring both silently ignored it. It is now a `retiredKey()` tombstone: writing it fails `tsc` (the input type is `never`) and fails the parse with a prescription naming `resizable`. Nothing in either repository wrote it.
24+
- **`ObjectGridProps`** (and `ObjectGridPropsParsed`) carry those types instead of `unknown`, and `resizableColumns` is `never`.
25+
26+
## FROM → TO
27+
28+
| you wrote on an `object-grid` | write instead |
29+
|:--|:--|
30+
| `resizableColumns: false` | `resizable: false` — the same boolean |
31+
| `resizableColumns: true` beside `resizable: false` | `resizable: false` — the grid has always followed `resizable` |
32+
| `rowHeight: 42`, `rowHeight: 'comfortable'` | `rowHeight: 'medium'`, or another of `compact` / `short` / `tall` / `extra_tall` |
33+
| `rowColor: 'red'` | `rowColor: { field: 'status', colors: { overdue: 'red' } }` |
34+
| `navigation: 'drawer'` | `navigation: { mode: 'drawer' }` |
35+
| `conditionalFormatting: [{ field: 'status', operator: 'equals', value: 'late', backgroundColor: '#fee2e2' }]` | `conditionalFormatting: [{ condition: "record.status == 'late'", style: { backgroundColor: '#fee2e2' } }]` |
36+
| `aggregations: [{ field: 'amount', type: 'median' }]` | a function the grid computes: `count`, `sum`, `avg`, `min`, `max` or `count_distinct` |
37+
| `operations: { create: true, read: true, import: false }` | `operations: { create: true }` — delete `read` and `import`; nothing reads them |
38+
39+
The one-line fix: rename `resizableColumns` to `resizable`, and write each of the seven members in the shape the list view declares for the same key (`aggregations` as `[{ field, type }]`, `operations` as four booleans). `os migrate meta --from 17` lists the mechanical `resizableColumns` edits for existing sources.
40+
41+
## The retirement kit
42+
43+
- **Tombstone.** `resizableColumns` is a `retiredKey()` on `ObjectGridPropsSchema`; its authorable-surface line carries `[RETIRED]`.
44+
- **Conversion.** `object-grid-resizable-columns-removed` (protocol 18, retired from the load path) follows the renderer's own precedence. It moves the value to `resizable` when `resizable` is absent, and deletes the key as a lossless strip when `resizable` holds a value. Its D3 record is the semantic entry `object-grid-resizable-columns-retired`, which carries the judgment for a grid that authored both keys with different values.
45+
- **Registration.** `RETIRED_KEYS_BY_MAJOR[18]` carries `ui/ObjectGridProps:resizableColumns`.
46+
- **The typed members** have the D3 entry `ui-object-grid-row-members-typed` and no conversion. Nothing on the load path refuses their shapes, and an off-shape value has no rewrite that keeps what the grid shows while honouring what the author wrote.
47+
48+
## Who is affected, measured
49+
50+
- **objectstack.** Measured on `origin/main` `53fd35e3e3`: zero `object-grid` blocks author any of the seven members or `resizableColumns` in the examples, `@objectstack/platform-objects`, the spec tests, the documentation and the published skills. The control: the same census finds the two showcase grids' `columns`.
51+
- **objectui.** Measured at the `.objectui-sha` pin, over 76 `object-grid` property bags in its sources, tests and documentation (23 of them in parsed JSON documents). One documentation example, the repository README's data grid, authors `operations.read: true`, which this row now refuses. No other bag authors a refused shape. The control: the same census finds `columns` in 46 bags.
52+
- **Deployed metadata** was not measured.

0 commit comments

Comments
 (0)