Skip to content

Commit 7d674df

Browse files
feat(spec)!: eight list members of object-grid, object-kanban and object-calendar take the shape each block reads; object-grid columns held (#21464, stage 2) (#21559)
Part of #21464 Clause-②: yes (narrowing) ## Fix round (contract review `5966757164`, item 1) The review found that the grid reads `options` off an AUTHORED `object-grid` column. The group-header formatter (`plugin-grid/src/ObjectGrid.tsx:2997-3001` at the pin) takes the column whose `field` is the grouping field, reads `colOverride?.options || objectDefField?.options` with the column winning, and draws the group-header labels from it. objectui pins that as behaviour in `gridGroupingMembers-8071.test.tsx:260-301`. `ListColumn` declares no `options`, so the by-reference `columns` narrowing refused a value the grid draws, a working writer by the seat's test (`5966636964`). This round: - returns `object-grid` `columns` to `z.unknown()` and re-adds it to the enumeration pin's ledger as `held-for-decision`, with the reader `ObjectGrid.tsx:2158` and `:2997-3001` and the carrier objectstack-ai/objectui#11544, in the shape the pin uses for kanban `conditionalFormatting` (objectstack-ai/objectui#11522); - removes its cases from the companion pin (§1 three, §2 five, §3 one); - corrects the four texts that said the grid never reads `options`: the member docblock, the changeset's FROM → TO table, the changeset's "Who is affected" paragraph, and this body's A3 list. `editable` stays described as unread, which the review confirmed; - narrows the D3 entry, its generated registry region and the rationale fragment to the eight members, and regenerates the reference page. The other eight members stand as reviewed. ## What this does Stage 2 (S-list) of the `ComponentPropsMap` `z.unknown()` close-out, per triage `5961300594`, the seat answer `5963787404` (staging A) and the claim `5965611825`. Eight members the list blocks read with a fixed shape were `z.unknown()` (an array of it for the lists). Any value passed the component-props gate, and the renderer dropped or substituted an off-shape one with no report. Each member now takes the shape its block reads, measured at the `.objectui-sha` pin `89cad75d55`. The family's ninth, `object-grid` `columns`, is held (above). | row · member | was | now | read point at the pin | |:--|:--|:--|:--| | `object-grid` · `columns` | `z.array(z.unknown())` | **held**, unchanged: `z.array(z.unknown())` | `plugin-grid/src/ObjectGrid.tsx:2158` `normalizeColumns`; the group-header formatter reads an authored column's `options` (`:2997-3001`), which `ListColumn` does not declare (objectstack-ai/objectui#11544) | | `object-grid` · `fields` | `z.array(z.unknown())` | `z.array(z.string())`, the measured shape (no list-view counterpart) | `plugin-grid/src/ObjectGrid.tsx:1946`; the draw path looks each entry up as `objectSchema.fields[fieldName]` (`:3969`, `:4012`) | | `object-grid` · `selection` | `z.unknown()` | `ListViewSchema.shape.selection` (`SelectionConfigSchema`), by reference | `.type`, `:4799-4812` | | `object-grid` · `selectable` | `z.unknown()` | `boolean`, `'single'` or `'multiple'`, the measured shape | `:4813-4815`, handed to the table at `:5333`; `components/src/renderers/complex/data-table.tsx:644` `resolveSelectionMode` | | `object-grid` · `rowActions` | `z.array(z.unknown())` | `ListViewSchema.shape.rowActions`, by reference | `:1834-1835`, `string[]` | | `object-grid` · `bulkActions` | `z.array(z.unknown())` | `ListViewSchema.shape.bulkActions`, by reference | `:4763` `batchActions ?? bulkActions`, then `resolveBulkActions` by name | | `object-grid` · `batchActions` | `z.array(z.unknown())` | the same def as `bulkActions` | the same read, which takes `batchActions` first | | `object-kanban` · `columns` | `z.array(z.unknown())` | all bare value strings, or all lanes `{ id, title, cards?, limit?, className?, collapsed? }` (module-private `ObjectKanbanLaneSchema`) | `plugin-kanban/src/ObjectKanban.tsx:1177-1188` dispatches on the first entry; `index.tsx:129-158` buckets by `id` and keeps static `cards`; `KanbanImpl.tsx:540`, `:568`, `:742` read `limit`, `className`, `collapsed` | | `object-calendar` · `calendar` | `z.unknown()` | `ListViewSchema.shape.calendar` (`CalendarConfigSchema`), by reference | `plugin-calendar/src/ObjectCalendar.tsx:294-297` returns the block as the config; `:857`, `:1056`, `:1141` read its five bindings | A static kanban card is a record row. Its `id` and `title` are typed, and the rest of the card is the row's own values. That one new `z.unknown()` member, `object-kanban columns[].cards[].*`, carries a `records` line in the enumeration pin. **`bulkActions` / `batchActions` (A4).** The grid reads `schema.batchActions ?? schema.bulkActions` (`ObjectGrid.tsx:4763`), so `batchActions` is the second spelling of one capability, read first. objectui's own type calls it the legacy alias. `ListViewSchema` declares only `bulkActions`. Both members now hold `bulkActions`'s def, and neither is retired here. **`selection` default.** `SelectionConfigSchema` defaults `type` to `none`. The grid reads an object with no `type` as ON (objectui#9837, ruling A-prime: presence enables). That default reaches only a parsed document, never the bag the grid reads. It is the list view's declaration either way, and objectui#9837 holds the question. The member's docblock records it. ## The census (A1), whole A writer is a page-component node: an object literal naming the type, a literal annotated with the block's type, a `schema={{…}}` on the block's React component, a call into a local helper that builds the node, or a direct parse through the row. Each member's value is resolved through same-file constants. The control is `objectName` on the same nodes. The instrument is a TypeScript-AST walk over every `.ts`, `.tsx`, `.js`, `.json`, `.md`, `.mdx` and `.yaml` file, with fenced code in the documents parsed too. | corpus | `object-grid` nodes | `object-kanban` nodes | `object-calendar` nodes | control `objectName` | |:--|--:|--:|--:|:--| | objectstack `49161683fb` (`examples/`, `packages/` incl. `packages/apps/`, `content/`, `skills/`, `apps/`) | 57 | 30 | 5 | 47 / 27 / 4 | | objectui `89cad75d55` (whole tree) | 689 | 240 | 160 | 293 / 108 / 98 | | row · member | objectstack values (parse) | objectui static values (distinct) · parse · refused · not static | |:--|:--|:--| | grid `columns` (held, not narrowed) | 5 | 310 (143) · not parsed by this PR · 20 not static | | grid `fields` | 0 | 16 (10) · 13 · 3 · 2 | | grid `selection` | 0 | 17 (4) · 17 · 0 · 1 | | grid `selectable` | 0 | 2 (1) · 2 · 0 · 1 | | grid `rowActions` | 0 | 10 (5) · 10 · 0 · 1 | | grid `bulkActions` | 0 | 23 (9) · 21 · 2 · 1 | | grid `batchActions` | 0 | 5 (3) · 5 · 0 · 0 | | kanban `columns` | 1 (1) | 108 (39) · 107 · 1 · 12 | | calendar `calendar` | 0 | 60 (26) · 58 · 2 · 6 | The objectui values were parsed through the built rows on this branch. Across the eight typed members, objectstack holds one value (the protocol docs' lane example, which parses), and objectui holds 241 static values: 233 parse and 8 are refused. The grid `columns` row is listed for completeness. Its 5 objectstack values (the showcase's two grids among them) and 310 objectui values meet no new shape here. ## Writer parse results (A3) No refused value is one the renderer draws. Each of the 8 refused objectui values is a test fixture whose value the renderer drops, skips or refuses: - **2 `{ name }` entries in `bulkActions`** (`bulkActionMembers-8071`, `bulkActionDefsUnusableMember-8730`). The fold skips them, and both tests assert the skip. - **3 object entries in `fields`** (`serverGroupedSelectIdentity-11105`). They copy the node the list view hands the grid at run time, as that test's own header says, so they are not an authored page. - **A lane `color`** (`objectKanbanColumnMembers-8071`). The console retired it, and the test marks it an undeclared member. The lane now refuses it with a prescription naming `className`. - **The calendar's retired `dateField` / `endField` aliases** (`calendar-date-alias-refusal-8355`). The test asserts their refusal. The 24 values that are not static are helper parameters, `.map` results and the run-time hand-offs (`plugin-view/src/ObjectView.tsx:2462`, `plugin-designer`). None is an authored page. **The held member.** Under the first head, `object-grid` `columns` was narrowed by reference, and this list carried 40 refused grid-column values. Among them, "16 column keys the grid never reads" counted 14 `editable` and 2 `options`, and said no authored-column read names either key. That was right for `editable` and wrong for `options`: the 2 `options` values (`gridGroupingMembers-8071`) are drawn in the group headers. So `columns` is held, and its 40 values are no longer refused by this PR. Under the triage caveat ("a narrowing that would refuse a measured writer is reported, not shipped silently"), this list is the report. A3 is applied with the seat's test (`5966636964`): a writer is a value the renderer draws. A refused fixture that probes the renderer's drop is not one. On that test the eight members stand, and `columns` is held. ## A2, per member Typed by reference (4): grid `selection`, `rowActions`, `bulkActions`; calendar `calendar`. Typed to the same def (1): grid `batchActions`. Typed to the renderer's read (3): grid `fields`, `selectable`; kanban `columns`. Held for a ruling (1): grid `columns` (objectstack-ai/objectui#11544). None is runner-forwarded. ## The pin (A5) - Eight `staged` lines leave the enumeration pin's ledger, and its `list-family` stage goes with them. `object-grid` `columns[]` stays as `held-for-decision`. One `records` line is added for `object-kanban columns[].cards[].*`. - **`no-reader` is removed.** It had no user since PR #21531, the pin's own checks never require a kind to be in use, and no other file pins its vocabulary (`git grep no-reader` finds only an unrelated prose use in `scripts/pm/check-half-states.mjs`). - The typed members are pinned in their own file, `component-list-family-typed-members.pin.test.ts`, as #21445's were. §1 checks that each declared shape parses, byte-identical, or to what the list view's schema answers where a default materializes. §2 checks each refusal by code and path, and for the kanban's two-array union by the arm's own issue. §3 checks identity with the list view's defs and the measured vocabularies. §4 checks the D3 registration. - **Ablation, fix round**, at `8b276755c2`: `object-grid` `rowActions` was reverted to `z.array(z.unknown()).optional()`, with no ledger line. It landed: anchor x1 to x0, replacement x0 to x1, blob `285edfb205` to `2b6b2f8643`. Red: **Tests 4 failed | 95 passed (99)**. The enumeration pin's §1 received exactly `[ 'object-grid rowActions[]' ]`, its census-equals-ledger control failed, and the companion pin failed its two `rowActions` cases. The restore was proven: blob after restore `285edfb205` == blob at HEAD, and `git diff HEAD` was empty. Green rerun: **Tests 99 passed (99)**. - **Ablation, first head**, at `870e7327ec`, via `node scripts/ablation-replace.mjs` in wrap mode. The mutation reverted `object-grid` `selection` to `z.unknown().optional()`, with no ledger line. It landed: anchor x1 to x0, replacement x0 to x1, blob `e60c46e776` to `2111ab1538`. Both pins went red: **Tests 7 failed | 101 passed (108)**. The enumeration pin's §1 received exactly `[ 'object-grid selection' ]`, and its census-equals-ledger control failed. The typed-member pin failed its five `selection` cases. The restore was proven: blob after restore `e60c46e776` == blob at HEAD, and `git diff HEAD` was empty. The green rerun showed **Tests 108 passed (108)**. The pins import `./component.zod` from source, so no build sits between mutation and run. ## The rest of the kit (A6, A7) - `component-type-vocabulary.ts`: the `KNOWN_COMPONENT_TYPES` docblock now lists `ai:chat_window` among the kept retired rows (contract review `5965171663` item 10). Comment only. - ADR-0087 D3 entry `ui-object-grid-kanban-calendar-list-members-typed`, and its step-18 rationale fragment at order 66. #21459's PR #21547 landed (`72af58c621`) while this was in review, with its own fragment `page-requires-non-compiled-kind-refused` also at 66, in a different gap. The second merge kept both fragments and both D3 entries (each id found twice in `registry.ts`), and `check:migration-registry` reads the generated regions current with no regeneration owed. The registry header allows equal orders, which render in `id` order. - Regenerated by `check:generated --fix` (only what it proved stale): `content/docs/references/ui/component.mdx`, and the strictness-ledger counts for `ui/`. The `ui/` count moves from 189 to 191 sites: +1 strict (the lane) and +1 passthrough (the card). In the fix round only the reference page was stale, and its `columns` row is back to `any[]`. - Changeset: `@objectstack/spec` `minor`, a **BREAKING** banner, `Clause-②: yes (narrowing)`, a FROM → TO table, the measured census, and the ADR-0087 marker `registered`. - The `ObjectGridPropsParsed` docblock had said the parsed state differs "on exactly one key — `data`". That was already untrue after #21445, and this change adds the `selection.type` default. It now names the defaults. ## Gates, at `8b276755c2` (after merging `origin/main` `ce532184d1` through `os-regen-merge.sh`) - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 114 commands (9 paths, 688 changed lines, merge base `ce532184d1`). Every exit code was written to disk before any pipe. `--ran` reconciled: **114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN**. - On this head, after a full `turbo run build` (72 tasks), all 114 ran and every one exited 0. On the first head, six gates answered `PREREQUISITE NOT MET` (exit 3) before the build; those lines were kept apart, not counted. - `pnpm --filter @objectstack/spec build`: exit 0. `check:generated`: exit 0, "All 15 generated artifacts are up to date". `check:liveness`, `check:migration-registry`, `check:strictness-ledger`, `check:authorable-surface` and `check:api-surface`: exit 0. - `pnpm --filter @objectstack/spec test`: **Test Files 606 passed (606), Tests 17952 passed, 1 todo**. `typecheck`: exit 0, `check:test-typecheck: OK`. - `pnpm --filter @objectstack/lint test` (the one import side of `ComponentPropsMap`): **119 files, 5620 tests passed**. - `pnpm check:doc-authoring` and `pnpm check:nul-bytes`: exit 0. - `check-widening-tells`: `--declaration no` gives exit 4 with 9 T1 tells, all at the new lane schema's keys inside the former `z.unknown()` bag. That is the shape the gate's own text rules a true refusal, so declare `yes`. `--declaration yes` gives exit 0. - The changeset gates were run with this body as the `--event` payload; their verdict lines are in the dev report. - NOT MEASURED: the Console Pin Gate, Dogfood and the full `pnpm lint`. Reason: they are CI-owned. objectui's source indexes `SpecObjectCalendarProps['data']`, `SpecObjectFormProps['layout']` and `SpecObjectKanbanProps['swimlaneField']`, none of them a narrowed member. ## Acceptance notes - The held `columns`: every `ListColumnSchema` member is read by the grid at the pin (the draw path, `useColumnSummary` at `:3170-3177` for `summary`), and the grid reads one key `ListColumn` does not declare, `options`, in the group headers. Typing `columns` waits on objectstack-ai/objectui#11544. - objectui's own tests that probe the dropped shapes (above) will see the spec refuse those values when objectui bumps `@objectstack/spec`. The objectui block mirror takes the grid row by reference (`ObjectGridBlockSchema.properties`). No objectui edit was made here. #21464 remains open for S-form, S-metric and S-objectui-held. object-kanban `conditionalFormatting` stays held on objectstack-ai/objectui#11522, and object-grid `columns` on objectstack-ai/objectui#11544. --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bd70706 commit 7d674df

9 files changed

Lines changed: 646 additions & 42 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: eight list members of an `object-grid`, `object-kanban` or `object-calendar` page block take the shape the block reads instead of any value — the grid's `fields`, `selection`, `selectable`, `rowActions`, `bulkActions` and `batchActions`, the kanban's `columns` and the calendar's `calendar` (#21464)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered ui-object-grid-kanban-calendar-list-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 rows: 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+
- **Eight members are typed.** `ComponentPropsMap['object-grid']`, `['object-kanban']` and `['object-calendar']` declared these members as `z.unknown()` (an array of it for the lists), although each renderer reads them with one shape. Any value passed, and an off-shape one was dropped or substituted with no report: an object entry in `fields` named no field; a `{ name }` entry in `bulkActions` was skipped; a kanban lane list mixing objects and strings drew a blank lane; a calendar block with no `startDateField` placed no event.
16+
- **The list view's own members, by reference**, where a list view declares one: the grid's `selection` (`{ type }`, with `none`, `single` or `multiple`), `rowActions` and `bulkActions` (action-name strings), and the calendar's `calendar` (`{ startDateField, endDateField?, titleField?, colorField?, allDayField? }`). `batchActions`, the second spelling of `bulkActions` that the grid reads first, takes `bulkActions`'s def. Neither spelling is retired here.
17+
- **The measured shape**, where no list view declares the member: the grid's `fields` (field-name strings), the grid's `selectable` (`true`, `false`, `'single'` or `'multiple'`), and the kanban's `columns` (all lanes `{ id, title, cards?, limit?, className?, collapsed? }`, or all bare value strings). A lane `id` and `title` are strings, a static card carries a string `id` and `title` beside its row's own values, and `limit` is a positive integer.
18+
- **`ObjectGridProps`, `ObjectKanbanProps` and `ObjectCalendarProps`** (and their `…Parsed` twins) carry these types on the eight members instead of `unknown`.
19+
- **The grid's `columns` is not narrowed** and still accepts any value. The list view's column entry is its by-reference shape, and the grid's draw path reads exactly that, but the grid's group headers also draw the labels from an authored column's `options` (the column whose `field` is the grouping field, ahead of the field's own options). The list view's column entry declares no `options`, so typing `columns` now would refuse a value the grid draws. It is held until that read is ruled.
20+
- **The enumeration pin** loses eight lines and keeps the grid's `columns` as held for that ruling. One `z.unknown()` member is added and recorded: the rest of a static kanban card (its row's own values, beside the typed `id` and `title`).
21+
22+
## FROM → TO
23+
24+
| you wrote | write instead |
25+
|:--|:--|
26+
| `object-grid` `fields: [{ field: 'name', width: 240 }]` | `fields: ['name']`, or the entry on `columns` |
27+
| `object-grid` `selection: 'multiple'` | `selection: { type: 'multiple' }` |
28+
| `object-grid` `selectable: 'none'` | `selectable: false`, or `selection: { type: 'none' }` |
29+
| `object-grid` `bulkActions: [{ name: 'approve' }]` (also `batchActions`, `rowActions`) | `bulkActions: ['approve']`, or the full def on `bulkActionDefs` |
30+
| `object-kanban` `columns: [{ id: 'done', title: 'Done' }, 'todo']` | one spelling per list: `columns: [{ id: 'done', title: 'Done' }, { id: 'todo', title: 'To Do' }]` |
31+
| `object-kanban` a lane `color: 'red'` | `className: 'border-t-2 border-red-500'` |
32+
| `object-kanban` a lane `{ id: 1, title: 'One' }` | `{ id: '1', title: 'One' }` |
33+
| `object-calendar` `calendar: { dateField: 'kickoff', endField: 'wrapup' }` | `calendar: { startDateField: 'kickoff', endDateField: 'wrapup' }` |
34+
35+
The one-line fix: write each member as the list view declares it, or as the table above shows. No conversion is registered, because an off-shape value has no rewrite that both keeps what the block shows today and honours what the author wrote; the D3 entry `ui-object-grid-kanban-calendar-list-members-typed` carries that judgment.
36+
37+
## Who is affected, measured
38+
39+
A writer is a page-component node: an object literal naming the type, a literal annotated with the block's type, a `schema={{…}}` on the block's React component, a call into a local helper that builds the node, or a direct parse through the row. Each member's value is read through same-file constants, and the control is `objectName` on the same nodes.
40+
41+
- **objectstack** at `49161683fb`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/` and `apps/`: 57 `object-grid`, 30 `object-kanban` and 5 `object-calendar` nodes (the control on 47 / 27 / 4 of them). The one authored value among the eight members is a kanban `columns` (lanes, in the protocol docs), and it parses. No node authors another of the eight.
42+
- **objectui** at the `.objectui-sha` pin `89cad75d55`: 689 `object-grid`, 240 `object-kanban` and 160 `object-calendar` nodes (the control on 293 / 108 / 98). Across the eight members, 241 values are static, and 233 of them parse. Each of the 8 that do not is a test fixture whose value the renderer drops, skips or refuses: 2 object entries in `bulkActions` (the renderer skips them, and the tests assert the skip), 3 object entries in `fields` that copy the hand-off the list view makes to the grid at run time (not an authored page), a lane `color` (retired in the console; the test marks it an undeclared member), and 2 uses of the calendar's retired `dateField` / `endField` aliases (the test asserts their refusal). No refused value is one the renderer draws. 24 values are not static (helper parameters, `.map` results and the run-time hand-offs); none of them is an authored page. The grid's `columns` (310 static values) is held because 2 of them author a column `options` the grid draws in its group headers, a fixture written to pin that behaviour.
43+
- **Deployed metadata** was not measured.

0 commit comments

Comments
 (0)