Skip to content

feat(spec)!: record:line_items gets its ComponentPropsMap row, its columns are the inline grid column contract, and the showcase Tasks grid binds by name (#21142) - #21244

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-21142-line-items-columns-name
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-21142-line-items-columns-name

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 Add Changesets and GitHub Actions automation #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

claude added 7 commits October 1, 2026 18:34
…s by `name`

The `record:line_items` block on the project detail page wrote its columns
keyed `field`, while the line-items grid binds a column by `name`, so every
cell rendered empty.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <[email protected]>
…lumns are the inline grid column contract

The type leaves the string-arm registration ledger, which is now empty.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <[email protected]>
The os-regen driver kept the branch side of authorable-surface/ui.json and
the component reference page; regeneration restores main's
`ui/Action:outcomeMessages` beside this branch's record:line_items keys.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <[email protected]>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 11 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/lint/src/validate-component-props.ts, packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/field-types.mdx (via relationshipField (literal, a string literal in RecordLineItemsProps))
  • content/docs/data-modeling/fields.mdx (via relationshipField (literal, a string literal in RecordLineItemsProps))
  • content/docs/deployment/validating-metadata.mdx (via childObject (literal, a string literal in RecordLineItemsProps), relationshipField (literal, a string literal in RecordLineItemsProps))
  • content/docs/getting-started/common-patterns.mdx (via relationshipField (literal, a string literal in RecordLineItemsProps))
  • content/docs/protocol/objectql/types.mdx (via relationshipField (literal, a string literal in RecordLineItemsProps))
  • content/docs/protocol/objectui/layout-dsl.mdx (via ComponentPropsMap (symbol, a top-level const object), childObject (literal, a string literal in RecordLineItemsProps), relationshipField (literal, a string literal in RecordLineItemsProps))
  • content/docs/ui/pages.mdx (via relationshipField (literal, a string literal in RecordLineItemsProps))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-3.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-5.mdx (via ComponentPropsMap (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/lint/src/validate-component-props.ts, packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, …) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4727fcb22a7e6a28da1ad20ef74469fbabb8dd0e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 46c31cb17d8f7c4f6c25031b89c9f56e5ee7fa6e — the merge of head 75f98b26a43fa1d485285b05a0eda306bcc13fb9 into base 4727fcb22a7e6a28da1ad20ef74469fbabb8dd0e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 46c31cb17d8f7c4f6c25031b89c9f56e5ee7fa6e && git checkout 46c31cb17d8f7c4f6c25031b89c9f56e5ee7fa6e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4727fcb22a7e6a28da1ad20ef74469fbabb8dd0e 75f98b26a43fa1d485285b05a0eda306bcc13fb9 && git checkout -B drift-repro 4727fcb22a7e6a28da1ad20ef74469fbabb8dd0e && git merge --no-ff 75f98b26a43fa1d485285b05a0eda306bcc13fb9

node scripts/docs-audit/affected-docs.mjs --json 4727fcb22a7e6a28da1ad20ef74469fbabb8dd0e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4727fcb22a7e6a28da1ad20ef74469fbabb8dd0e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c2b9101398cce19e72815d0d0837eaa2db672340
Local-runs: none

Rendered 2026-10-01T21:37Z by an isolated reviewer. Inputs: card #21142 (body and its three comments: the triage grade 5929028089, the claim 5937827924, the dev report 5940709575), PR #21244 (body, 22-file list, net diff origin/main...refs/review/21142 with the fetched ref equal to the head above), the check-runs on the head, and objectui read-only at the .objectui-sha pin 31971ff1e28f (git show PIN:PATH). The dispatch order and the dispatching seat's conclusions were not read. Nothing was built, run or re-run.

① Derived judgments

Accept set, before and after. Before: record:line_items had no ComponentPropsMap row and was the one entry of STRING_ARM_REGISTERED_TYPES (component-type-vocabulary.ts:68); the props gate's if (!schema) continue; (validate-component-props.ts:190) skipped the whole bag, and the page schema types the bag as z.record(z.string(), z.unknown()) (page.zod.ts:324), so ANY key with ANY value parsed clean. After: a strict shape of exactly fifteen keys with relationshipField required, columns required as z.array(InlineGridColumnSchema).min(1), childObject optional, parentObject / parentId / recordId / amountField / totalField / title strings, readonly boolean, minRows / maxRows numbers, filter the ViewFilterRule array, sort the SortItem array, limit a positive integer; near-miss aliases (object / childObjectName / child, foreignKey / relationField / parentField, fields, label, sumField, rollupField, filters), a guidance reason for addLabel / sortField / formFields / inlineMode, and the component-node guidance sets every row shares. RIGHT.

The fifteen keys, re-measured at the pin. My own count of distinct schema.KEY reads in plugin-form/src/LineItemsPanel.tsx at 31971ff1e28f is exactly fifteen: amountField (2), childObject (19), columns (1), filter (1), limit (6), maxRows (1), minRows (1), parentId (1), parentObject (1), readonly (5), recordId (1), relationshipField (6), sort (2), title (1), totalField (3). The dev's line citations spot-checked (:221, :228, :319, :327, :341, :366, :368, :377, :515, :516, :667, :669, :702-:707, :722, :723, :778, :810) all hold, and the component's own LineItemsPanelSchema interface (:91) declares the same fifteen plus type. The wrapper adds no key: ElementDataSourceGate reads the node-level dataSource and composes the same filter / sort / limit; RECORD_LINE_ITEMS_DATA_SOURCE (plugin-form/src/index.tsx:556) writes the binding's object onto childObject and limit onto limit. requiredPermissions and aria have no read. RIGHT: fifteen declared, none invented, none missing.

Required keys. relationshipField: :515 queries { [schema.relationshipField]: parentId } and :674 hands it to the edit batch; the registration comment says in so many words that it "stays the author's: it is not part of the binding", and nothing on the panel or the gate derives it. RIGHT required. columns: :702 hands schema.columns through applyColumnPermissions (fieldWriteGate.ts:307, a filter-and-lock pass that adds no column) to the grid, where GridField.tsx:636 reads cfg.columns || []; there is no hydrateColumns on this path (hydration lives in deriveMasterDetail.ts for the master-detail carriers, and GridField.tsx names it only in comments). RIGHT required with at least one. childObject optional: RIGHT (index.tsx:557; :319 / :498 guard with a hint when absent).

The four refused detail-entry keys. Zero reads of addLabel / sortField / formFields / inlineMode, and zero add_label / sort_field / onRowExpand, anywhere in LineItemsPanel.tsx at the pin; the grid config it builds (:694-:710) carries exactly columns, total_field, min_rows, max_rows, allow_add, allow_delete. Refusing them with the master-detail prescription is RIGHT; the shared keys take the detail entry's types and alias table verbatim (compared against masterDetailDetailEntry() on main, component.zod.ts:4862). parentId / recordId as z.string() match the renderer's own interface (:91 declares both string); the master-detail BLOCK's string | number recordId is a different block's key. RIGHT.

columns by reference. shape.columns.element IS InlineGridColumnSchema (asserted by identity in the carrier test). At the pin fields/src/widgets/GridField.tsx declares GridColumn with exactly that schema's twenty keys (name, label, type, options, width, required, prefix, step, reference, displayField, idField, multiple, accept, defaultHidden, computed, expr, scale, autofill, readonlyWhen, requiredWhen), binds every cell, header and total on c.name (:742, :792, :871, :949, :975-:988), and folds nothing from field. RIGHT, and the one carrier difference (no hydration) is stated in the columns describe. Residual, noted in ③: the column schema's own per-key describes speak of hydration ("defaults to the child field's label via hydration", "derived from the child field's type when omitted", "which hydration copies onto an identity-only column"), and the generated Nested Shape table for this row reproduces them; on THIS carrier those defaults do not happen. The parent row's text contradicts them explicitly, by-reference sharing is the ruled design (triage: not a copy; Zone 2 #4), and neutralising the shared wording is a data/field.zod.ts follow-up, not this diff's defect.

ADR-0087 disposition, measured (its own paragraph). Four pre-PR shapes were judged against the pinned renderer. (a) A field-keyed column, the showcase shape: the grid keys cells on c.name and nothing folds field, so the shape NEVER worked; empty cells IS the card. A lossless rewrite exists (field to name) but there is no working old shape for a D2 window to keep loading; the rewrite is a repair, and the D3 entry and the changeset table carry it. (b) A block omitting relationshipField: :515 queries the child by a key spelled "undefined" and :674 writes the FK the same way; never worked, and no mechanical rewrite exists because nothing on this panel derives the FK. (c) A block omitting columns, or columns: []: cfg.columns || [] draws a grid with no cells; never useful, no rewrite exists. For (a) to (c) D3-only is RIGHT. (d) Two shapes that DO work at the pin and are refused after: a MongoDB-style record filter (LineItemsPanelSchema.filter?: any[] | Record, lowered by toFilterNodeSafely) and a sort entry without order (convertSortToQueryParams reads it as asc and calls that a runtime tolerance, not permission). Both have a lossless rewrite for their mappable part. What "refused" means here is measured, not assumed: the page schema does not parse properties (page.zod.ts:324), the kernel's authoring-key walk stops dead at that carrier (metadata-authoring-lint.ts:460-:466), the props gate is tier: 'advisory', surfaces: CLI_ONLY, commands: ALL (authoring-rules.ts:1049-:1056, so validate, build and lint), and applyConversionsToStoredItem replays without validating. A stored row therefore keeps loading and the renderer keeps honouring both forms; the only new output is an advisory finding to an author running the CLI. The existing D2 page-component-filter-record-to-rule-array is gated to RULE_ARRAY_FILTER_BLOCK_TYPES (conversions/registry.ts:12377: "The filter of any other component type is not this entry's surface"), so it does not reach this block, and the sort family's recorded disposition (18.object-block-sort-item-array) declines D2 on exactly this stored-row reasoning. Judged: D2 is owed where a working shape stops LOADING; nothing stops loading here, so D3-only with no D2 is RIGHT, the D3 entry's replacement names both target forms, and the entry's own sentence "nothing on the load path refuses the shape" is TRUE. Population: zero authored filter or sort on this block anywhere in the repo (census), deployed metadata NOT MEASURED, stated in the entry. Whether the 2026-09-12 option-B ruling should one day extend that D2's type list to this block is a ruling question raised in ③, not a defect of this diff.

Public surface, each move for its stated reason. api-surface/ui.json +3 (RecordLineItemsProps (const), (type), RecordLineItemsPropsParsed (type)); export-origins/ui.json +2 types, the same (type)-only pattern RecordHistoryProps has; declaration-map/ui.json +1; json-schema.manifest/ui.json +1; authorable-surface/ui.json +15, and the fifteen names are the fifteen keys; dropped-refinements.baseline.json adds ui/RecordLineItemsProps at columns.element (the column schema's currency-scale .superRefine) and filter.element (the same site ui/RecordRelatedListProps carries), counts 213 to 214 and 618 to 620; the strictness ledger counts 189 to 190 and component.zod.ts 57 to 58; docs index 166 to 167 and 1522 to 1523, component.mdx "44 more". All RIGHT.

Vocabulary. STRING_ARM_REGISTERED_TYPES becomes [] with the export kept; the type is known through its row (KNOWN_COMPONENT_TYPE_CANDIDATES folds map keys), is not a PageComponentType member, and the ledger-discipline loops now hold vacuously, which the new named pin covers. RIGHT.

defineStack control. stack.zod.ts:2808 dispatches collectHydratedInlineColumnErrors on object-master-detail-form only; with no hydration on this panel there is no resolved type to judge, so not extending it is RIGHT, and the control test asserts both directions (identity-only column with scale passes under this block, throws under the master-detail block).

Lint pins and header edits. validate-component-props.ts changes comments only; the new findings are the spec row reaching the existing dispatch. The pinned paths follow from the emission code: unrecognized_keys at ['columns', N] plus key gives ...properties.columns.N.field (:296-:320), the missing name gives component-props-invalid at ...columns.N.name. validate-component-types.test.ts moves record:line_items between two comment groups of one accept list; validate-page-field-bindings.test.ts changes a title (its COMPONENT_FIELD_SPECS does not carry the type). Behaviour-neutral: RIGHT. No lint changeset is RIGHT (no published lint code moved); no showcase changeset is RIGHT (private: true).

Producer. Five columns field to name, each naming a showcase_task field, pinned by the new showcase test. RIGHT.

Published prose, sentence by sentence. Changeset: every sentence TRUE, including the gate list (verified at authoring-rules.ts:1052), the save-and-load sentence (page.zod.ts:324), the fifteen-key list, the launch-window level (check-changeset-no-major.mjs header; ADR-0087 amendment of 2026-09-13), the census (my grep on origin/main: one example block, zero docs blocks, one prose tag-list mention in ui/react-pages.mdx). D3 entry strings (surface, replacement, reason, acceptanceCriteria): TRUE, including the eight-block control (my count: 1 record:alert, 3 record:details, 2 record:highlights, 1 record:path, 1 record:quick_actions). component.mdx rows: childObject hint and no fetch (:319, :778) TRUE; relationshipField query shape and no auto-detect TRUE; columns drawn as declared, identity-only is a text cell headed by its name (GridField.tsx:949; :207 and :217 read c.type ?? 'text') TRUE; parentObject default (:221) TRUE; parentId wins over recordId (:228) TRUE; amountField default amount when totalField is set (:669, :703) TRUE at the pin; totalField on save (:667) TRUE; title default "Line Items" (i18n/src/locales/en.ts:299) TRUE; readonly locks cells, hides Save, blocks add and delete (:706, :707, :723, :810) TRUE; filter AND-combined, never substituted (:515-:520, mergeFilterNodes) TRUE; sort storage-order default TRUE; limit default 500 and no pagination (DEFAULT_LINE_ITEMS_LIMIT = 500, :137) TRUE. Two FALSE items, both outside the shipped strings: (i) the comment sentence "the last registered record:* renderer without a ComponentPropsMap row" (D3 entry header comment, the map row comment, the lint test comment) is FALSE as written, because the pinned objectui also registers record:approvals and record:attachments (app-shell/src/views/record-approvals-renderer.tsx:80, record-attachments-renderer.tsx:69, console-internal, inputs: [className], absent from public-blocks.ts), neither of which has a row; it is TRUE read as "the last PUBLISHED record:* block without a row". (ii) The Nested Shape hydration describes discussed above. Neither moves the verdict.

② Semver level

@objectstack/spec: minor with the BREAKING banner, Clause-②: yes (narrowing) in the changeset body, adr-0087 marker registered ui-record-line-items-props-closed. The narrowing arm is the right one: the accept set shrinks from any bag to a strict fifteen-key shape; the new exports widen the TypeScript surface, but the closed pair takes one arm and the breaking arm governs. Pre-GA minor is what ADR-0087's 2026-09-13 amendment and check-changeset-no-major.mjs require for a metadata-facing narrowing. The registered id is new in the diff, lands in migrations/registry.ts step 18 with its rationale fragment at order: 57 (the only order: 57, no collision), and the regenerated registry entry is byte-identical to the entry file. The FROM-to-TO table and one-line fix are present and match the refusals. The PR body's Clause-②: yes omits the arm the changeset carries; the gate reads the arm from the changeset, so the declaration stands. Level and declaration MATCH the diff.

③ Boundary flags

Dev deviations, each answered. (1) File surface beyond the claim's list (three lint files, the carrier test, the new showcase test, the rationale fragment): each is a consequence of the row and none changes behaviour; accepted. (2) Guidance for four detail-entry keys: measured unread at the pin above; accepted. (3) filters: 'filter' spelled inline instead of spreading FILTERS_TO_FILTER: the const is declared at component.zod.ts:3569, below this schema, so the TDZ account under OS_EAGER_SCHEMAS=1 is plausible; no spec test enumerates doors by that constant (component.test.ts:3451 holds a fixed object-* list, and the map-wide pin at :2461 enumerates by shape and admits this door); accepted with the comment left in place. (4) Commit trailers: outside this review's remit. (5) The dropped-refinements baseline row the dispatch did not expect: verified correct and arithmetically consistent above; accepted.

open_questions: none declared; none found that this diff must answer.

out_of_scope_findings, each with its carrier. (1) The objectui mirror (public-blocks.zod.ts:167, registry inputs 5 of 15): carrier objectui#10872, named; accepted. (2) Footer total shown only with totalField at the pin, with amountField alone on objectui main: carrier the next .objectui-sha bump, named; the amountField describe is TRUE at the pin; accepted. (3) parentId / recordId as two read spellings of one concept: both read at :228, declared as measured on the initialValues / initialData precedent; carrier none; accepted as an enforce-or-remove note the seat may card separately. (4) field-no-consumers walks subforms / details only and does not credit a record:line_items column to the child object: ESCALATED. With a typed row landed, a child field whose only consumer is a line-items column is a reproducible lint false positive, which Prime Directive #10 names as a card, not a note; the dispatching seat should file it.

Flags raised by this review, none verdict-moving. (a) The shared column describes carry hydration wording this carrier contradicts; a data/field.zod.ts rewording to carrier-neutral text would make the Nested Shape rows true on all four carriers. (b) Three comments the diff leaves stale: component.zod.ts:153 ("unregistered types like record:line_items are authored in the wild"), validate-component-types.ts:44 ("the ledgered record:line_items"), validate-page-field-bindings.ts:31 ("unregistered types like record:line_items parse"). (c) The "last registered record:* renderer" comment wording, see ① (record:approvals, record:attachments). (d) ADR-0087: whether the 2026-09-12 option-B filter ruling should extend RULE_ARRAY_FILTER_BLOCK_TYPES to this block's filter door is a ruling for the maintainer, with zero authored population measured and deployed metadata unmeasured; this diff owes no D2 under the disposition read above.

Check-runs on the head at the final read (2026-10-01T21:36:59Z, REST commits/c2b9101…/check-runs): 35 runs, 33 success, 2 skipped (Console Pin Gate, which this diff does not trigger since the pin is untouched and nothing the pinned sibling imports is removed; Packed-tarball smoke (opt-in), not opted in), 0 failed, 0 still running. Lint & Repo Gates (which carries check:adr-0087-registration, check:changeset-no-major, check:spec-parsed-alias and check:migration-registry), Spec property liveness, Governed Surface Queue Guard, Build Core, the six Test Core shards, the three Dogfood shards, Temporal Conformance and the four type-check lanes are all success. No governed surface is in the file list, so no tier applies to the landing beyond this record.

Implemented-by: claude/issue-21142-line-items-columns-name
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

claude added 2 commits October 1, 2026 22:37
…ne-items-columns-name

The one textual conflict, dropped-refinements.baseline.json's two measured
counts, is resolved to 212 / 616: the reading build-schemas prints for the
merged source (taken in a throwaway worktree), which the merged ledger's own
212 entries and 616 sites corroborate. Both sides' entry hunks merged as is.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <[email protected]>
…e merged tree

The os-regen driver kept this branch's side of the five ui.json shards, the
strictness-ledger counts and the reference index in merge 21c74a7, dropping
main's FormFieldPublicPicker retirement. Regenerated from the merged source
after a container restart; build-schemas reads 616 sites across 212 schemas,
the counts the merge resolution recorded.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 75f98b26a43fa1d485285b05a0eda306bcc13fb9
Local-runs: none

Rendered 2026-10-01T23:48Z by an isolated reviewer, as a fresh record on the merged head: the pure-regeneration carry does not apply, because the hand-maintained packages/spec/dropped-refinements.baseline.json moved in the merge resolution. Inputs: card #21142 (body and all six comments: the triage grade 5929028089, the claim 5937827924, the first dev report 5940709575, the ACCEPT on the earlier head 5941249538, the dequeue note 5941361013, the base-merge round report 5942723870), PR #21244 (body, 22-file list, both comments including the earlier record 5941164801, and the net diff origin/main...refs/review/21142 with the fetched ref equal to the head above), and the check-runs on the head. The dispatch order and the dispatching seat's own conclusions were not read. Nothing was built, run or re-run; every git read below is git diff, git show, git log or git merge-tree --write-tree against fetched objects, with no checkout and no worktree.

① Derived judgments

The delta first. The head is the regeneration commit on top of merge 21c74a7, whose parents are the reviewed head c2b9101 and main at 4727fcb. The net diff (merge-base 4727fcb) is 22 files, +797 / -61: the same file set and the same per-file stat as the reviewed diff (62b90d74...c2b91013). Comparing the two diffs file by file with hunk headers and context stripped: 13 files carry byte-identical hunks, every hand-written one among them (component.zod.ts, component-type-vocabulary.ts and its test, inline-grid-column-carriers.test.ts, the D3 entry 18.ui-record-line-items-props-closed.ts, the four lint files, the showcase page and its new test, the changeset, and component.mdx). The other nine differ only as the merge accounts for:

  • api-surface/ui.json, authorable-surface/ui.json, declaration-map/ui.json, export-origins/ui.json, json-schema.manifest/ui.json, migrations/registry.ts: the added and removed lines are identical to the reviewed ones; only the surrounding context moved with main. The net additions over main are exactly RecordLineItemsProps (const) / (type) / RecordLineItemsPropsParsed (type) (+3), the fifteen ui/RecordLineItemsProps:KEY names (+15), one declaration-map row, two export-origin rows and one manifest row, and the registry's +70 lines are the same fragment and the same generated entry.
  • content/docs/references/index.mdx and the strictness-ledger ui.md: the +1 is the same (one UI schema, one ui/ strict shape); the base moved with main's publicPicker retirement (1522 to 1521, 189 to 188), so the lines now read 1521 to 1522 and 188 to 189.
  • dropped-refinements.baseline.json: only the two measured counts differ (211 / 614 to 212 / 616 instead of 213 / 618 to 214 / 620). The entry hunks are identical: ui/RecordLineItemsProps with columns.element and filter.element is the one entry added over main; main's two removals (ui/FormField at in.publicPicker.filter.element, ui/FormFieldPublicPicker at filter.element) stay removed; no other entry's sites changed; key order, the description and the other measured keys (zod 4.4.3, 369 projected, 0 without a JSON form) equal main's. Counted from the committed ledger itself: 212 entries, 616 sites, so the resolved counts are the ledger's own and not arithmetic on two receipts. packages/spec/scripts/dropped-refinements.test.ts:370-371 pins measured to exactly those two totals, and it is in the spec local vitest project that pnpm test runs, so Test Core on this head measured it.

Zero hand-written lines differ from the reviewed diff. The regenerated files reflect what main brought: every publicPicker occurrence is gone from the five ui.json shards and index.mdx at the head exactly as at main 4727fcb, whereas the merge commit 21c74a7 still carried the branch side (3 / 5 / 2 / 3 / 1 / 1 occurrences), which is the drop the os-regen driver makes and the regeneration commit discharged. check:generated runs in the required TypeScript Type Check job, which is success on this head. Neither main's incoming range (62b90d74..4727fcb2) nor its later commits (4727fcb2..30c530e5) mention record:line_items or touch component.zod.ts, component-type-vocabulary.ts, page.zod.ts, validate-component-props.ts or the carrier test.

migrations/registry.ts against what main now holds. The STEP18_RATIONALE fragment ui-record-line-items-props-closed keeps order: 57 and the generated semantic entry is present. main at 30c530e holds PR #21240's dataset-member-field-expression-refused at order: 58 and carries no order: 57 and no ui-record-line-items; the head carries no order: 58 and no dataset-member-field. joinRationale sorts fragments by order then id, and the file already holds two fragments at order: 56 on both sides, so even a tie would not collide. registry.ts is the only path main moved after 4727fcb that this PR also touches; it is not an os-regen path, so a textual merge is what the queue computes too, and git merge-tree --write-tree origin/main refs/review/21142 returns a tree with no conflict. GitHub's own mergeable_state on the PR read is clean. PR #21240's conversion 54 lives in conversions/registry.ts, which is not in this file list. RIGHT, no collision.

Carried judgments, on the evidence that their hunks are byte-identical. The accept set of ComponentPropsMap['record:line_items'] is unchanged: a strict shape of exactly fifteen keys, relationshipField required, columns required as z.array(InlineGridColumnSchema).min(1) by reference, childObject optional, filter / sort / limit as the sibling doors declare them, the near-miss alias table, and a guidance refusal for addLabel / sortField / formFields / inlineMode; re-read on this head in component.zod.ts and RIGHT. STRING_ARM_REGISTERED_TYPES is [] with the export kept, the type known through its row: RIGHT. The showcase's five columns keyed name, pinned by the new showcase test: RIGHT. The public-surface moves are the same +3 / +15 / +1 / +2 / +1 and the one baseline row, each for its stated reason: RIGHT. The defineStack control (identity-only check not extended to this carrier) and the lint pins (comments and a title only; the findings are the row reaching the existing dispatch): RIGHT, carried.

ADR-0087 disposition, D3-only. The D3 entry and the changeset are byte-identical to the reviewed ones. The two facts the disposition rests on were re-read at this head: page.zod.ts:324 still types a component's properties as z.record(z.string(), z.unknown()), so a stored page is never refused on save or load, and the component-props gate that reads the row is still tier: 'advisory', commands: ALL, surfaces: CLI_ONLY (authoring-rules.ts:1050-1054). Nothing stops loading, so no D2 is owed and the entry's own sentence "nothing on the load path refuses the shape" stays TRUE. Carried: RIGHT.

Prose. The changeset is byte-identical and every sentence re-read on this head stays TRUE (the fifteen-key list, the advisory gate list, the save-and-load sentence, the FROM-to-TO table, the census of one authored block, "Deployed metadata was not measured"). The PR body is unchanged; its "Tests (at HEAD c2b9101)" section is a statement about that head and remains true as one, and the head's own readings are in the base-merge report 5942723870. No sentence in the body became false. The two commit messages added this round carry the model-free trailer pair.

② Semver level

@objectstack/spec: minor with the BREAKING banner, Clause-②: yes (narrowing) in the changeset body, adr-0087 marker registered ui-record-line-items-props-closed; the changeset is byte-identical to the reviewed one, and no lint or showcase changeset is owed (no published lint code moved; the showcase is private). The narrowing arm is the right one: the accept set shrinks from any bag to a strict fifteen-key shape, the new exports widen only the TypeScript surface, and the closed pair takes one arm. Pre-GA minor for a metadata-facing narrowing is what the repo's launch-window convention and check:changeset-no-major require. The registered id is new in the diff and lands in migrations/registry.ts at order: 57 with no collision against main's order: 58. Check Changeset and Lint & Repo Gates (which carries check:adr-0087-registration, check:changeset-no-major and check:migration-registry) are success on this head. The PR body's Clause-②: yes line stands with the arm read from the changeset. Level and declaration MATCH the diff.

③ Boundary flags

Base-merge round report 5942723870: open_questions is empty and out_of_scope_findings is empty. Its three deviations, each answered. (1) The resolution counts were read with gen:schema in a throwaway detached worktree and then re-read by the full build on the committed merge: what matters is the committed state, and the committed ledger's own 212 entries and 616 sites equal the two measured counts, with the spec test above pinning that equality; accepted. (2) The regeneration commit's subject carries a "Recovery commit:" prefix: commit subjects are outside this record's remit, no published artefact carries one, and the trailers are the model-free pair; accepted. (3) PR #21240 landed mid-round and no second base merge was taken: judged above, no collision at order: 57 versus order: 58, a conflict-free merge-tree, and mergeable_state: clean; accepted. The merge group rebuilds on current main and re-runs Lint & Repo Gates, so check:migration-registry judges the merged registry there.

Flags of the earlier record 5941164801, none moved by this delta: the field-no-consumers escalation (the card's ACCEPT 5941249538 names #21091 as its carrier), the objectui mirror (objectui#10872), the footer-total difference carried by the next .objectui-sha bump, the parentId / recordId double spelling, the three stale code comments, the "last registered record:* renderer" wording, the shared column describes' hydration wording, and the option-B filter-ruling question. Each is a note or a ruling question with a named carrier or none owed; this round changed no source line, so none is re-opened here and none moves the verdict.

Flags raised by this review: none. One observation for the landing seat only: the PR body's test table names the earlier head, which is accurate as written; the head's readings live on the card.

Check-runs on the head at the final read (2026-10-01T23:44Z, REST commits/75f98b26...check-runs): 35 runs, 33 success, 2 skipped (Console Pin Gate, which this diff does not trigger since .objectui-sha is untouched and nothing the pinned sibling imports is removed; Packed-tarball smoke (opt-in), not opted in), 0 failed, 0 still running. All seven required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core (six shards), Dogfood Regression Gate (three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. No governed surface is in the file list, so no tier applies to the landing beyond this record.

Implemented-by: claude/issue-21142-line-items-columns-name
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

2 participants