Repository navigation
Commit 7d674df
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
- .changeset
- content/docs/references/ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages/spec/src
- migrations
- entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
0 commit comments