Commit 24c554d
Fixes #21142
Clause-②: yes
Seam: renderer `@object-ui/plugin-form` `LineItemsPanel` →
`@object-ui/fields` `GridField` (binds `column.name`) ← producer:
objectstack showcase `record:line_items` `columns[].field`
The showcase project page's Tasks grid keyed all five of its columns
`field`. The line-items grid binds a column by `name`, so every cell
rendered empty. Nothing refused it: `record:line_items` had no
`ComponentPropsMap` row, so the component-props gate skipped its props
bag as unregistered. This PR fixes the producer and adds the row (the
claim's call, per triage `5929028089`). From here on a `field`-keyed
column is refused at authoring, with the rename to `name`.
## What changed
- **`examples/app-showcase/src/ui/pages/project-detail.page.ts`**: the
five columns are keyed `name` (`title`, `status`, `priority`,
`estimate_hours`, `due_date`). Nothing else on the page moved.
- **`packages/spec/src/ui/component.zod.ts`**: new row
`ComponentPropsMap['record:line_items']` = `RecordLineItemsProps`, a
strict shape of the fifteen keys objectui's `LineItemsPanel` reads
(measured below):
- `relationshipField` is required, and so is `columns` (at least one).
Nothing on this panel derives either one.
- `childObject` is optional, because the component-level `dataSource`
binding can supply it.
- `filter`, `sort` and `limit` take the declarations every sibling door
takes: the ViewFilterRule array, the SortItem array and a positive
integer.
- The keys it shares with an `object-master-detail-form` detail entry
take that entry's types and alias table.
- The four entry keys this block does not read (`addLabel`, `sortField`,
`formFields`, `inlineMode`) are refused with a `guidance` reason.
- New types `RecordLineItemsProps` and `RecordLineItemsPropsParsed`.
- **`columns` IS `InlineGridColumnSchema`**, by reference and not a copy
(Zone 2 #4: same shape, see below). The retired `field` spelling is
refused by name with the prescription naming `name`. One carrier
difference is stated in the `describe()`: this panel does not hydrate a
column from the child field. For the same reason, `defineStack`'s
identity-only check (`collectHydratedInlineColumnErrors`) is
deliberately NOT extended to this block, and a control pin holds that.
- **`packages/spec/src/ui/component-type-vocabulary.ts`**:
`record:line_items` leaves `STRING_ARM_REGISTERED_TYPES`, which is now
empty. The export stays, and its docblock records why it is empty. The
type stays KNOWN through its row.
- **ADR-0087**: new D3 semantic entry
`ui-record-line-items-props-closed`, plus a step-18 rationale fragment
(order 57). `gen:migration-registry` regenerated the registry. No D2
conversion: page-component `properties` is not parsed on the save or
load path, and the census found one producer, respelled here.
- **Pins flipped / added**:
- `validate-component-props.test.ts`: `record:line_items` leaves the
unregistered-skip `it.each`. A new suite asserts the `field`-keyed
columns fire `component-props-unknown-key` at
`...properties.columns.N.field`, with `component-props-invalid` at
`...columns.N.name`. The `name`-keyed control is silent.
- `component-type-vocabulary.test.ts`: known through the row, not on the
ledger, not an enum member.
- `inline-grid-column-carriers.test.ts`: a fourth-carrier section
covering identity of the column element, the `field` refusal (code
`unrecognized_keys` at path `['columns', 0]`), the currency `scale`
refusal, the bogus key, `relationshipField` and `columns` required,
`.min(1)`, the alias and guidance refusals, and full-read-set and
showcase controls. A last control shows that `defineStack` does not
judge an identity-only line-items column, while the same column under
`object-master-detail-form` is judged.
- New showcase test
`examples/app-showcase/test/project-detail-line-items.test.ts`: the five
columns are keyed `name`, each names a `showcase_task` field, the block
parses against `RecordLineItemsProps` with its keys intact, and no
`field`-keyed line-items column exists anywhere in the showcase.
- **Prose made false by the change**: `validate-component-props.ts`
header (the skip list and its "earlier editions" history),
`validate-component-types.test.ts` comment, and the
`validate-page-field-bindings.test.ts` test title ("skips a component
type its descriptor table does not carry"). Those are comments and a
title only; no lint behaviour changed.
- **Generated**:
- `dropped-refinements.baseline.json`: new site
`ui/RecordLineItemsProps` at `columns.element` and `filter.element`,
plus its two counts.
- Also regenerated: `api-surface/ui.json`, `export-origins/ui.json`,
`declaration-map/ui.json`, `authorable-surface/ui.json`,
`json-schema.manifest/ui.json`,
`content/docs/references/ui/component.mdx` and `index.mdx`, and
`docs/audits/...strictness-ledger.counts/ui.md`.
- **Changeset** `.changeset/21142-line-items-columns-name.md`:
`@objectstack/spec` minor, BREAKING, `Clause-②: yes (narrowing)`,
ADR-0087 `registered ui-record-line-items-props-closed`. What reads it
is the component-props gate (advisory findings on `objectstack validate`
/ `build` / `lint`). The stored-page save and load path does not parse
`properties`.
## Measurements
**Premise**: holds. At `origin/main` `1ecb871beb`,
`project-detail.page.ts:76` authors `amountField: 'estimate_hours'` and
`:79`–`:104` author five `columns` keyed `field:`. `record:line_items`
was the only entry of `STRING_ARM_REGISTERED_TYPES`
(`component-type-vocabulary.ts:68`), and its row-lessness was pinned in
`validate-component-props.test.ts:495`.
**Read set at the `.objectui-sha` pin `31971ff1e28f`** (objectui
`packages/plugin-form/src/LineItemsPanel.tsx`; `SchemaRenderer` hoists
`properties` onto `schema`). A count of `schema.KEY` reads gives exactly
fifteen keys:
- `childObject` `:319`, `:327`, `:498`, `:516`, `:638`, `:673`, `:702`,
`:778`
- `relationshipField` `:515`, `:674`
- `columns` `:702`
- `parentObject` `:221`
- `parentId` and `recordId` `:228`
- `amountField` `:669`, `:703`
- `totalField` `:667`, `:669`, `:703`
- `title` `:722`
- `readonly` `:706`, `:707`, `:723`, `:810`
- `minRows` `:704`
- `maxRows` `:705`
- `filter` `:366`
- `sort` `:368`, `:377`
- `limit` `:341`, `:437`
The wrapper adds no key. `ElementDataSourceGate.tsx` reads the
node-level `dataSource` plus the same `filter` / `sort` / `limit`
(`:421`, `:434`, `:445`). The mapping `RECORD_LINE_ITEMS_DATA_SOURCE`
(`plugin-form/src/index.tsx:556`) writes the binding's `object` onto
`childObject`. `requiredPermissions` and `aria`, which other record rows
declare, have no read here, so they are not declared.
**objectui `main` (`d59f11c0d3dc`, pin is an ancestor: `merge-base
--is-ancestor` exit 0)**:
- Same fifteen keys; the per-key read counts are identical.
- One semantic difference. At the pin the grid's footer total appears
only when `totalField` is set (`total_field: schema.totalField ?
schema.amountField || 'amount' : undefined`). On `main` (objectui
`55a12a8e1`, round 8) it appears whenever `amountField` is named. So the
showcase's `amountField` with no `totalField` draws a footer only once
the console pin moves past that commit.
- `GridField` on `main` declares `GridColumn = InlineGridColumn`, which
is the spec's type by reference (objectui `75dcc81c3`).
- `0a3e5409f` (grid `sort_field`) changes `GridField` and
`MasterDetailForm`, not anything this block reads or hands the grid.
- No key is read at the pin but retired on `main`.
**Column shape (Zone 2 #4)**: at the pin, `GridField.tsx`'s `GridColumn`
interface declares exactly the twenty keys `InlineGridColumnSchema`
declares (`name`, `label`, `type`, `options`, `width`, `required`,
`prefix`, `step`, `reference`, `displayField`, `idField`, `multiple`,
`accept`, `defaultHidden`, `computed`, `expr`, `scale`, `autofill`,
`readonlyWhen`, `requiredWhen`). Same shape, so the row references the
schema by identity. The one difference belongs to the carrier:
`LineItemsPanel` hands `columns` straight to `applyColumnPermissions`
and then the grid (`:702`), with no `hydrateColumns` step. An
identity-only `{ name }` column therefore draws as a text cell headed by
its name, and the `describe()` says so.
**Census (Zone 2 #5)** at `1ecb871beb`, matcher `type:
'record:line_items'`:
- `examples/`: 1 producer, the showcase page, 5 `field`-keyed columns,
respelled here.
- `content/docs/`: 0 blocks. One prose tag-list mention in
`ui/react-pages.mdx:38`.
- `packages/` non-test: 0.
- Lit control: the same matcher shape finds 8 other `record:*` blocks in
`examples/` (3 `record:details`, 2 `record:highlights`, 1 each
`record:path`, `record:quick_actions`, `record:alert`).
- Test fixtures naming the type: the three lint test files above. Each
was re-judged: one flipped, one comment moved, and one title made
honest. The field-bindings fixture is not a props-gate input.
**Served showcase page end to end: NOT MEASURED.** Neither checkout has
a console build (`packages/console/dist` is absent in both). `pnpm dev`
runs `check:console-sha` first, and producing that build needs a full
objectui build at the pin, which this dispatch holds read-only. What is
measured instead:
- The page's block parses against the row with its five `name` keys
intact (showcase test).
- The objectstack conversion registry carries no `record:line_items`
rewrite (`git grep` in `packages/spec/src/conversions`: 0 hits), so no
load-time conversion on this side intervenes.
## Tests (at HEAD `c2b91013`)
All through `scripts/pm/os-verify-lock.sh`, exit codes read from its
`VERDICT` line. The tree is the merged one: `origin/main` `62b90d74`
merged through `os-regen-merge.sh`, plus the regeneration commit.
| command | result |
|:--|:--|
| `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2` | 596 files, 17466 passed, 1 todo |
| `pnpm --filter @objectstack/spec run typecheck` (tsc + scripts + test
layer) | exit 0 |
| `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2` | 119
files, 5503 passed |
| `pnpm --filter @objectstack/lint run typecheck` | exit 0 |
| `pnpm --filter @objectstack/sdui-parser exec vitest run
--maxWorkers=2` | 13 files, 217 passed |
| `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2` | 200 files passed, 3 skipped; 2973 passed, 19 skipped |
| `pnpm --filter @objectstack/example-showcase exec vitest run
--maxWorkers=2` | 30 files, 391 passed |
| `pnpm --filter @objectstack/example-showcase run typecheck` | exit 0 |
| `pnpm --filter @objectstack/spec check:generated` | all 15 artifacts
up to date |
- Dependency closures were rebuilt first (`pnpm
--workspace-concurrency=2 --filter` with the `PKG^...` closure of lint,
metadata-protocol, sdui-parser and the showcase, plus `@objectstack/spec
build`).
- `protocol.meta-types-degenerate-derivation.test.ts` (the served-schema
count pins) is green unchanged. The new row moves no count: `page` still
serves 24 top-level properties, because `properties` is an open record.
- **Pre-merge** (`2479fb67` / `742c6970`): the same suites were green.
Six transient failures — module-not-found on `@objectstack/spec/*` and
`@objectstack/platform-objects/*` — came from a concurrent dist rebuild
by the gate run. They were re-run on stable dists: green (4 files / 30
tests, and 30 files / 391 tests).
- **eslint, narrowed (measured)**: `eslint --no-inline-config --format
json` over the 12 changed `.ts` files reports 12 results, 0 errors, 0
warnings, and none ignored, so all 12 are in the config's population.
`eslint.config.mjs` never enables type-aware linting (its `:327-329`),
so this diff cannot move the verdict on any file it did not touch. The
repo-wide `pnpm lint` is CI's.
## Reverse verification (ablation)
The map row `'record:line_items': RecordLineItemsProps,` was deleted
through `scripts/ablation-replace.mjs` (anchor 1 → 0, blob
`35459aca2181` → `d052475a42e4`), committed state first. Then `pnpm
--filter @objectstack/spec build`. `ablation-dist-preflight.mjs
--absent` reported the marker absent from all 230 built files, and the
tree carried only the source mutation.
- Spec (src): `component-type-vocabulary.test.ts` +
`inline-grid-column-carriers.test.ts` → **8 failed** / 55 passed. These
are the vocabulary pin and seven of the eight new carrier tests. The
eighth, the `defineStack` control, reads no row and stays green as
intended.
- Lint (spec `dist`): `validate-component-props.test.ts` → **1 failed**
/ 49 passed. "reports a `field`-keyed column" saw zero findings, which
is the pre-fix silence. Its `name`-keyed control stays green (vacuously
under the ablation).
- Showcase: 4/4 green, as expected, because that test parses with
`RecordLineItemsProps` directly rather than through the map.
- Restore: `git checkout HEAD`, blob back to `35459aca2181` == HEAD,
`git diff HEAD` empty, whole-tree `git status --porcelain` clean.
Rebuilt spec, preflight "marker present in 14 built files" and "working
tree clean against HEAD". Lint suite back to 50/50.
## Gates
- Derived with `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (no paths) at `c2b91013`: **113
commands**, the same list as the pre-merge derivation.
- All 113 ran at `c2b91013`, sequentially, each exit code captured
before any pipe: **113 × exit 0**. This includes
`check:adr-0087-registration` (`registered
ui-record-line-items-props-closed (new here)`),
`check:changeset-no-major`, `check:nul-bytes`, `check:issue-citations`,
`check:doc-authoring`, `check:spec-parsed-alias`,
`check:migration-registry`, `check:dual-build-cjs-loads`, and
`check:type-check-debt` (re-measure, 336s).
- `dispatch-gates --ran` reconciliation: 113 derived, 113 run, 0
NOT-MEASURED, 0 UNRUN.
- The pre-merge run (`ffcd210a`), taken alongside a closure build, did
not measure seven gates. Five answered PREREQUISITE NOT MET (exit 3).
`check:dts-closure` named a package whose declarations were mid-rebuild.
`check:type-check-debt` hit a 420s cap. All seven are in the 113 × exit
0 above.
- **NOT MEASURED here (CI's)**: the CI jobs that dispatch-gates names as
outside the list (Test Core shards, Dogfood, Temporal Conformance, Build
Core, the four type-check lanes), and the repo-wide `pnpm lint`.
## Acceptance notes
- **`amountField` footer differs between the pin and objectui `main`**
(above). This is not a defect here: the console pin bump carries it in.
- **Two spellings of one concept, both read**: `parentId` wins over
`recordId` (`LineItemsPanel.tsx:228`). Both are declared as measured, on
the `object-master-detail-form` `initialValues` / `initialData`
precedent, and the `describe()` names the precedence. Retiring one is a
separate enforce-or-remove question; not filed (no reach measured).
- **The objectui mirror is objectui's**: objectui#10872 waits on this
row. Two items there are now stale: objectui
`packages/types/src/zod/public-blocks.zod.ts:167`, which says "the spec
carries no row", and the registry `inputs` for this block, which declare
5 of the 15 keys.
- **`field-no-consumers`** (`validate-field-consumers.ts`) walks child
collections by the keys `subforms` / `details`, so it does not credit a
`record:line_items` block's column names to the child object. Reach was
not measured: the showcase task fields are consumed elsewhere. Noted
only.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ee42f00 commit 24c554d
22 files changed
Lines changed: 797 additions & 61 deletions
File tree
- .changeset
- content/docs/references
- ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- examples/app-showcase
- src/ui/pages
- test
- packages
- lint/src
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
364 | 364 | | |
365 | 365 | | |
366 | 366 | | |
367 | | - | |
| 367 | + | |
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
| |||
375 | 375 | | |
376 | 376 | | |
377 | 377 | | |
378 | | - | |
| 378 | + | |
379 | 379 | | |
380 | 380 | | |
381 | 381 | | |
| |||
0 commit comments