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
Conversation
…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 lint pins, regenerate artifacts Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <[email protected]>
…ath form Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <[email protected]>
…ne-items-columns-name
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]>
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: Rendered 2026-10-01T21:37Z by an isolated reviewer. Inputs: card #21142 (body and its three comments: the triage grade ① Derived judgmentsAccept set, before and after. Before: The fifteen keys, re-measured at the pin. My own count of distinct Required keys. The four refused detail-entry keys. Zero reads of
ADR-0087 disposition, measured (its own paragraph). Four pre-PR shapes were judged against the pinned renderer. (a) A Public surface, each move for its stated reason. Vocabulary.
Lint pins and header edits. Producer. Five columns Published prose, sentence by sentence. Changeset: every sentence TRUE, including the gate list (verified at ② Semver level
③ Boundary flagsDev 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)
Flags raised by this review, none verdict-moving. (a) The shared column describes carry hydration wording this carrier contradicts; a Check-runs on the head at the final read (2026-10-01T21:36:59Z, REST Implemented-by: VERDICT: PASS |
…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]>
Contract reviewServed-tier: 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 ① Derived judgmentsThe delta first. The head is the regeneration commit on top of merge 21c74a7, whose parents are the reviewed head c2b9101 and
Zero hand-written lines differ from the reviewed diff. The regenerated files reflect what
Carried judgments, on the evidence that their hunks are byte-identical. The accept set of 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: 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 ② Semver level
③ Boundary flagsBase-merge round report Flags of the earlier record 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 Implemented-by: VERDICT: PASS |
Fixes #21142
Clause-②: yes
Seam: renderer
@object-ui/plugin-formLineItemsPanel→@object-ui/fieldsGridField(bindscolumn.name) ← producer: objectstack showcaserecord:line_itemscolumns[].fieldThe showcase project page's Tasks grid keyed all five of its columns
field. The line-items grid binds a column byname, so every cell rendered empty. Nothing refused it:record:line_itemshad noComponentPropsMaprow, 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 triage5929028089). From here on afield-keyed column is refused at authoring, with the rename toname.What changed
examples/app-showcase/src/ui/pages/project-detail.page.ts: the five columns are keyedname(title,status,priority,estimate_hours,due_date). Nothing else on the page moved.packages/spec/src/ui/component.zod.ts: new rowComponentPropsMap['record:line_items']=RecordLineItemsProps, a strict shape of the fifteen keys objectui'sLineItemsPanelreads (measured below):relationshipFieldis required, and so iscolumns(at least one). Nothing on this panel derives either one.childObjectis optional, because the component-leveldataSourcebinding can supply it.filter,sortandlimittake the declarations every sibling door takes: the ViewFilterRule array, the SortItem array and a positive integer.object-master-detail-formdetail entry take that entry's types and alias table.addLabel,sortField,formFields,inlineMode) are refused with aguidancereason.RecordLineItemsPropsandRecordLineItemsPropsParsed.columnsISInlineGridColumnSchema, by reference and not a copy (Zone 2 Add Changesets and GitHub Actions automation #4: same shape, see below). The retiredfieldspelling is refused by name with the prescription namingname. One carrier difference is stated in thedescribe(): 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_itemsleavesSTRING_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.ui-record-line-items-props-closed, plus a step-18 rationale fragment (order 57).gen:migration-registryregenerated the registry. No D2 conversion: page-componentpropertiesis not parsed on the save or load path, and the census found one producer, respelled here.validate-component-props.test.ts:record:line_itemsleaves the unregistered-skipit.each. A new suite asserts thefield-keyed columns firecomponent-props-unknown-keyat...properties.columns.N.field, withcomponent-props-invalidat...columns.N.name. Thename-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, thefieldrefusal (codeunrecognized_keysat path['columns', 0]), the currencyscalerefusal, the bogus key,relationshipFieldandcolumnsrequired,.min(1), the alias and guidance refusals, and full-read-set and showcase controls. A last control shows thatdefineStackdoes not judge an identity-only line-items column, while the same column underobject-master-detail-formis judged.examples/app-showcase/test/project-detail-line-items.test.ts: the five columns are keyedname, each names ashowcase_taskfield, the block parses againstRecordLineItemsPropswith its keys intact, and nofield-keyed line-items column exists anywhere in the showcase.validate-component-props.tsheader (the skip list and its "earlier editions" history),validate-component-types.test.tscomment, and thevalidate-page-field-bindings.test.tstest title ("skips a component type its descriptor table does not carry"). Those are comments and a title only; no lint behaviour changed.dropped-refinements.baseline.json: new siteui/RecordLineItemsPropsatcolumns.elementandfilter.element, plus its two counts.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.mdxandindex.mdx, anddocs/audits/...strictness-ledger.counts/ui.md..changeset/21142-line-items-columns-name.md:@objectstack/specminor, BREAKING,Clause-②: yes (narrowing), ADR-0087registered ui-record-line-items-props-closed. What reads it is the component-props gate (advisory findings onobjectstack validate/build/lint). The stored-page save and load path does not parseproperties.Measurements
Premise: holds. At
origin/main1ecb871beb,project-detail.page.ts:76authorsamountField: 'estimate_hours'and:79–:104author fivecolumnskeyedfield:.record:line_itemswas the only entry ofSTRING_ARM_REGISTERED_TYPES(component-type-vocabulary.ts:68), and its row-lessness was pinned invalidate-component-props.test.ts:495.Read set at the
.objectui-shapin31971ff1e28f(objectuipackages/plugin-form/src/LineItemsPanel.tsx;SchemaRendererhoistspropertiesontoschema). A count ofschema.KEYreads gives exactly fifteen keys:childObject:319,:327,:498,:516,:638,:673,:702,:778relationshipField:515,:674columns:702parentObject:221parentIdandrecordId:228amountField:669,:703totalField:667,:669,:703title:722readonly:706,:707,:723,:810minRows:704maxRows:705filter:366sort:368,:377limit:341,:437The wrapper adds no key.
ElementDataSourceGate.tsxreads the node-leveldataSourceplus the samefilter/sort/limit(:421,:434,:445). The mappingRECORD_LINE_ITEMS_DATA_SOURCE(plugin-form/src/index.tsx:556) writes the binding'sobjectontochildObject.requiredPermissionsandaria, which other record rows declare, have no read here, so they are not declared.objectui
main(d59f11c0d3dc, pin is an ancestor:merge-base --is-ancestorexit 0):totalFieldis set (total_field: schema.totalField ? schema.amountField || 'amount' : undefined). Onmain(objectui55a12a8e1, round 8) it appears wheneveramountFieldis named. So the showcase'samountFieldwith nototalFielddraws a footer only once the console pin moves past that commit.GridFieldonmaindeclaresGridColumn = InlineGridColumn, which is the spec's type by reference (objectui75dcc81c3).0a3e5409f(gridsort_field) changesGridFieldandMasterDetailForm, not anything this block reads or hands the grid.main.Column shape (Zone 2 #4): at the pin,
GridField.tsx'sGridColumninterface declares exactly the twenty keysInlineGridColumnSchemadeclares (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:LineItemsPanelhandscolumnsstraight toapplyColumnPermissionsand then the grid (:702), with nohydrateColumnsstep. An identity-only{ name }column therefore draws as a text cell headed by its name, and thedescribe()says so.Census (Zone 2 #5) at
1ecb871beb, matchertype: 'record:line_items':examples/: 1 producer, the showcase page, 5field-keyed columns, respelled here.content/docs/: 0 blocks. One prose tag-list mention inui/react-pages.mdx:38.packages/non-test: 0.record:*blocks inexamples/(3record:details, 2record:highlights, 1 eachrecord:path,record:quick_actions,record:alert).Served showcase page end to end: NOT MEASURED. Neither checkout has a console build (
packages/console/distis absent in both).pnpm devrunscheck:console-shafirst, and producing that build needs a full objectui build at the pin, which this dispatch holds read-only. What is measured instead:namekeys intact (showcase test).record:line_itemsrewrite (git grepinpackages/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 itsVERDICTline. The tree is the merged one:origin/main62b90d74merged throughos-regen-merge.sh, plus the regeneration commit.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2pnpm --filter @objectstack/spec run typecheck(tsc + scripts + test layer)pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2pnpm --filter @objectstack/lint run typecheckpnpm --filter @objectstack/sdui-parser exec vitest run --maxWorkers=2pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2pnpm --filter @objectstack/example-showcase run typecheckpnpm --filter @objectstack/spec check:generatedpnpm --workspace-concurrency=2 --filterwith thePKG^...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:pagestill serves 24 top-level properties, becausepropertiesis an open record.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 --no-inline-config --format jsonover the 12 changed.tsfiles reports 12 results, 0 errors, 0 warnings, and none ignored, so all 12 are in the config's population.eslint.config.mjsnever enables type-aware linting (its:327-329), so this diff cannot move the verdict on any file it did not touch. The repo-widepnpm lintis CI's.Reverse verification (ablation)
The map row
'record:line_items': RecordLineItemsProps,was deleted throughscripts/ablation-replace.mjs(anchor 1 → 0, blob35459aca2181→d052475a42e4), committed state first. Thenpnpm --filter @objectstack/spec build.ablation-dist-preflight.mjs --absentreported the marker absent from all 230 built files, and the tree carried only the source mutation.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, thedefineStackcontrol, reads no row and stays green as intended.dist):validate-component-props.test.ts→ 1 failed / 49 passed. "reports afield-keyed column" saw zero findings, which is the pre-fix silence. Itsname-keyed control stays green (vacuously under the ablation).RecordLineItemsPropsdirectly rather than through the map.git checkout HEAD, blob back to35459aca2181== HEAD,git diff HEADempty, whole-treegit status --porcelainclean. Rebuilt spec, preflight "marker present in 14 built files" and "working tree clean against HEAD". Lint suite back to 50/50.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) atc2b91013: 113 commands, the same list as the pre-merge derivation.c2b91013, sequentially, each exit code captured before any pipe: 113 × exit 0. This includescheck: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, andcheck:type-check-debt(re-measure, 336s).dispatch-gates --ranreconciliation: 113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN.ffcd210a), taken alongside a closure build, did not measure seven gates. Five answered PREREQUISITE NOT MET (exit 3).check:dts-closurenamed a package whose declarations were mid-rebuild.check:type-check-debthit a 420s cap. All seven are in the 113 × exit 0 above.pnpm lint.Acceptance notes
amountFieldfooter differs between the pin and objectuimain(above). This is not a defect here: the console pin bump carries it in.parentIdwins overrecordId(LineItemsPanel.tsx:228). Both are declared as measured, on theobject-master-detail-forminitialValues/initialDataprecedent, and thedescribe()names the precedence. Retiring one is a separate enforce-or-remove question; not filed (no reach measured).packages/types/src/zod/public-blocks.zod.ts:167, which says "the spec carries no row", and the registryinputsfor this block, which declare 5 of the 15 keys.field-no-consumers(validate-field-consumers.ts) walks child collections by the keyssubforms/details, so it does not credit arecord:line_itemsblock's column names to the child object. Reach was not measured: the showcase task fields are consumed elsewhere. Noted only.Generated by Claude Code