Repository navigation
fix(spec)!: defineStack judges a record:line_items column that declares no type as the type it renders (#24190) - #24347
Conversation
…s against the child field's type collectHydratedInlineColumnErrors walks the fourth carrier of the inline grid column, a record:line_items page block's columns, resolving the child object the way the renderer does (an object dataSource binding's object wins over childObject). The CONTROL test flips into a parity test against object-master-detail-form. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ch; changeset Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ne-items-hydrated-check
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 143 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 9d8e4375a742f41bafad8ded4d01802b4e168c3b && git checkout 9d8e4375a742f41bafad8ded4d01802b4e168c3b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a21fd6c531e50a53deb26a7d04886fbb073164e8 ced64a4aff570ff45217b1b865f11001054d6a81 && git checkout -B drift-repro a21fd6c531e50a53deb26a7d04886fbb073164e8 && git merge --no-ff ced64a4aff570ff45217b1b865f11001054d6a81
node scripts/docs-audit/affected-docs.mjs --json a21fd6c531e50a53deb26a7d04886fbb073164e8 |
席位核对 (seat check) · PR #24347 ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, assignee zhuangjianguo. The body's line 1 is Fixes #24190, line 3 is Clause-②: yes (narrowing); no other closing keyword. 5 files, +275/-65. The changeset is minor with the ! title, Clause-②: yes (narrowing), one ADR-0087 marker (not-required (already-registered ui-record-line-items-props-closed)), and a FROM → TO table. |
| The card's three bullets and both riders | (1) collectHydratedInlineColumnErrors walks record:line_items columns with the same judge as the other carriers (stack.zod.ts, the page walk). (2) The CONTROL test is now a PARITY test against object-master-detail-form for the same column. (3) Rider (i): the docblock lists four carriers. Rider (ii): the "pinned on purpose" note is gone. |
The child object, read against the renderer at the pin 0707b4ae3712 |
objectui packages/plugin-form/src/index.tsx:666 maps the binding's object onto childObject (RECORD_LINE_ITEMS_DATA_SOURCE), and LineItemsPanel renders inside ElementDataSourceGate. So a binding that names an object wins over an authored childObject, and an API binding names none. lineItemsChildObject follows that order. Right. |
| Accepted deviation: two text edits outside the listed surface | The RecordLineItemsProps TSDoc (component.zod.ts) and the step-18 entry ui-record-line-items-props-closed both said the check does not reach this block. This PR makes that false, so the os-dev rule (fix a published sentence the change falsifies) applies. Both are text only: no id, conversion or matcher changes. The contract review judges them with the rest. |
Who is affected (dev's census on ec749fbb44) |
2 example blocks and 1 skill block, none carrying scale. No shipped example changes verdict. |
Local runs (dev, on af540b850) |
the carriers file 52/52; spec 694 files and 21050 tests passed; typecheck exit 0; check:generated 14/14. Ablation: removing the line-items branch turned 5 tests red, and removing the binding read turned 1 red; both restores were proven by blob hash. Gates: 92 derived, 91 run with exit 0, 1 NOT-MEASURED (check:dual-build-cjs-loads, exit 3). |
| Out of scope | forEachPageComponent does not visit a page:tabs item's children. Its docblock says it "reaches every container spelling", so inline-action targets and this PR's column check are not judged under tabs. The showcase project detail page's own line-items block sits there. The seat re-read the walk on main and confirms it. The seat files a domain:spec card in this round and links it here. |
| Landing | After the contract review PASS and a fully green head: pr_ready then automerge_enable. The merge closes #24190. |
Generated by Claude Code
Contract reviewServed-tier: Rendered: 2026-10-18T15:22Z Inputs, and nothing else: card #24190 (body and all seven comments: ① Derived judgments
② Semver level
③ Boundary flags
Two FAIL items, both prose on shipped surfaces, both one edit each: ① item 8 (the step-18 rationale fragment in Implemented-by: VERDICT: FAIL Generated by Claude Code |
席位核对 (seat check) · PR #24347 ·
|
… reach, and every shipped reach sentence names the positions the page walk visits The rationale fragment for ui-record-line-items-props-closed said the panel draws columns with no hydration and that defineStack's identity-only check does not reach it. The changeset, the entry's reason and the stack.zod.ts docblock said the check reaches the block wherever a page carries it; the walk visits a region's components, a container's component children and a slot, and not a page:tabs item's children. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
席位核对 (seat check) · PR #24347 · patch round 1 ·
|
Contract reviewServed-tier: Rendered: 2026-10-18T16:10Z. Round 2: the previous record on this PR, Inputs, and nothing else: card #24190 (body and all nine comments: What this head adds over the one reviewed before. One commit, ① Derived judgments
② Semver level
③ Boundary flags
Both round-1 FAIL items are fixed on this head: ① 8 (the fragment states the reach, Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Contract review adopted ·
Generated by Claude Code |
Fixes #24190
Clause-②: yes (narrowing)
defineStack's resolved-type check,collectHydratedInlineColumnErrorsinpackages/spec/src/stack.zod.ts, now walks the fourth carrier of the inline grid column: arecord:line_itemspage block'scolumns. A column that declares notypeis judged as the type it renders, against the field of the child object the panel lists, with the column schema's own refusal (STACK_CROSS_REFERENCE_INVALID, 422). Today that refusesscaleon an identity-only column over acurrencychild field. The same column now gets the same answer underrecord:line_itemsand underobject-master-detail-form.What changed
packages/spec/src/stack.zod.ts. The page walk (the sharedforEachPageComponenttraversal) judges arecord:line_itemsnode'scolumnswith the same per-columnjudgethe other three carriers use. The location readsPage 'NAME' (PATH) record:line_items columns[K].KEY. A new helper,lineItemsChildObject, resolves the child object the way the renderer does (below). The function's docblock lists four carriers instead of "All three carriers" (rider i from6236510990). Its line-items bullet names the positions the walk visits (a region's components, a container's component children, a slot) and says a block under apage:tabsitem'schildrenis not yet visited (patch round 1, contract review6251625691item ① 9). TheforEachPageComponentdocblock and the call-site comment name the new user.packages/spec/src/inline-grid-column-carriers.test.ts. The line-items section's "pinned on purpose" note is gone (rider ii). The CONTROL test "defineStack does not judge an identity-only line-items column" is flipped into a PARITY test: for the same columns, the line-items block and the master-detail block both refuse withSTACK_CROSS_REFERENCE_INVALID/ 422, one finding each, and the finding text past its location is byte-identical. New cases: refusal on a region, inside a container, under a slot; the objectdataSourcebinding wins overchildObject(both directions); an API binding falls back tochildObject; a column invalid on its own is left to the props half; controls; an unresolved child object (undeclared, or none) is not judged; the remedy builds. The refusal cases assert the envelope (code,status), the located subject, the column name, the child field's type (field 'crm_invoice_line.amount' is \currency``) and the ruled first sentence.packages/spec/src/ui/component.zod.ts. TSDoc only, onRecordLineItemsProps. Its paragraph said the check "walks the three master-detail carriers and not this block", which this change makes false. It also said the panel draws untyped columns as text "until the.objectui-shapin carries" objectui#12854. The pin carries it now (see G2). No schema, key or.describe()changes. Measured: this TSDoc block is not emitted intopackages/spec/dist/*.d.ts(zero hits for the old or the new text after the build), so it ships no bytes.packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts. Text only, no id, conversion or matcher change. Itsreasonsaid "defineStack's identity-only column check does not reach this block", which this change makes false. Itsreplacementsaid "this block draws a column exactly as declared and hydrates nothing from the child object's field", which contradicts the refusal this change adds. The entry now states the identity-only reach in its surface, replacement, reason and acceptance criteria, in the words theobject-master-detail-formentry uses for the same reach, plus this change's population census. Patch round 1 (item ① 9): itsreasonnames the three positions the walk visits and says a block under apage:tabsitem'schildrenis not yet judged, instead of "wherever a page carries it" (no tracker number in that author-facing text). The census notes that the showcase project detail block sits under apage:tabsitem.packages/spec/src/migrations/registry.ts.template(patch round 1, contract review6251625691item ① 8). Text only.idandorderare unchanged, and so is the list's shape (step18-rationale-merge.test.ts). TheSTEP18_RATIONALEfragmentui-record-line-items-props-closed(order 57) said the panel draws its columns "with no hydration from the child object's field, sodefineStack's identity-only check does not reach it". The upgrade guide prints that fragment just above this entry'sreason, andobjectstack migrate metaprints it too. It now states the reach the way the master-detail fragment (order 56) states its own: the panel hydrates an untyped column from the child field, so the identity-only check reaches the block, against the child object the panel lists, in a page region's components, a container's component children and a slot. It also says a block under apage:tabsitem'schildrenis not yet judged.gen:migration-registryregenerates the gitignoredregistry.ts; nothing else to commit..changeset/24190-line-items-hydrated-check.md.@objectstack/specminor, the!title shape,Clause-②: yes (narrowing), a FROM → TO table and the one-line fix. ItsdefineStackbullet names the three positions the walk reaches and thepage:tabsexclusion (patch round 1, item ① 9). Its ADR-0087 disposition isnot-required (already-registered ui-record-line-items-props-closed): the entry pre-dates the merge base, and this diff makes its text cover this change.Measured before editing (
origin/mainec749fbb44)collectHydratedInlineColumnErrorswas atstack.zod.ts:3576and the "All three carriers" sentence at:3560. The "pinned on purpose" note was atinline-grid-column-carriers.test.ts:615–:620and the CONTROL test at:740. All matched..objectui-shais0707b4ae37120d45d6401170666a0b76cb7a3546. In the objectui clone,git merge-base --is-ancestor 54e79f989 0707b4ae3712exits 0. Exit 0 proves ancestry in any checkout, so no control leg is owed (control leg run anyway: the pin's 60th ancestor,e43a16207, also exits 0). At the pin,packages/plugin-form/src/LineItemsPanel.tsxhandshydrateColumns(schema.columns, childSchema, …)to the grid. That is the samehydrateColumnsfromderiveMasterDetail.tsthe master-detail carriers call.childSchemaisdataSource.getObjectSchema(schema.childObject).schema.childObject, and its registration (packages/plugin-form/src/index.tsx,RECORD_LINE_ITEMS_DATA_SOURCE = { object: 'childObject', … }) wraps it inElementDataSourceGate. When the node carries an objectdataSourcebinding (a plain object with a non-empty stringobject,isElementDataSourceConfig), the gate writes the binding'sobjectontochildObjectover the authored one (next[objectKey] = composed.object, "binding wins" inelement-data-source.ts). An API binding (provider: 'api') is not such a config, so the panel readsproperties.childObjectas authored.lineItemsChildObjectfollows the same order. This differs from the master-detail block, which has onlydetails[].childObject. The spec declares both channels:RecordLineItemsProps.childObjectis optional "because the component-leveldataSourcebinding can supply the object instead", and its docblock says the block's mapping writes the binding'sobjectontochildObject. So this is the declared precedence, not a consumer-side fallback.examples/app-showcase:project-detail.page.ts(five typed columns) andinvoice-document.page.ts(four{ name, label }columns, two of them overcurrencyfields). One inskills/objectstack-ui/rules/pages.md(four{ name }columns). None carriesscale, andscaleis the only type-conditional column key today. So no shipped example, skill or fixture is refused by the new check.Shapes left unjudged
Each of these follows the function's own rule, "an unresolved column is not a wrong one":
childObjectnor an object binding (the panel shows its configuration hint and draws nothing);page:tabsitem'schildren.forEachPageComponentrecurses only into arrays underpropertieswhose entries are components. Apage:tabsitem{ value, label, children }is not a component, so the walk never reaches the components under it. Probe throughdefineStackon this branch: the same refused column is ACCEPTED under apage:tabsitem for bothrecord:line_itemsandobject-master-detail-form. An inline action with a dangling modal target is ACCEPTED there too, while all three are REFUSED at region level. The showcase project detail page'srecord:line_itemssits exactly there. This blind spot is in the shared traversal, so it reaches every check that uses it, not only this card's walk. It is the subject of spec(stack):forEachPageComponentnever visits apage:tabsitem'schildren, sodefineStack's page cross-reference checks (inline action targets, inline grid columns) do not judge anything placed inside tabs #24349 and is not changed here, because widening the traversal also narrows the inline-action check. This PR's shipped sentences name only the positions the walk visits. The sibling master-detail text onmain(its step-18 rationale fragment and entry) carries the same "wherever a page carries it" claim and is also left to spec(stack):forEachPageComponentnever visits apage:tabsitem'schildren, sodefineStack's page cross-reference checks (inline action targets, inline grid columns) do not judge anything placed inside tabs #24349.Tests
All runs are on the final head
ced64a4af(patch round 1 on top ofaf540b850, which is this branch withorigin/main68a090fb9merged), throughscripts/pm/os-verify-lock.sh. Patch round 1 also ranpnpm --filter @objectstack/spec gen:migration-registry("wrote src/migrations/registry.ts (475 semantic, 269 retired-key, 224 retired-def)") andpnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 --project repo scripts/step18-rationale-merge.test.ts(Tests 9 passed (9)). It ranpnpm check:doc-authoring(exit 0, "19013 customer-facing string(s) across 1429 spec sources clean"),pnpm --filter @objectstack/spec check:upgrade-guide(exit 0) andpnpm --filter @objectstack/spec check:spec-changes(exit 0). The builtpackages/spec/dist/migrations/index.jscarries the new fragment, and the old "does not reach it" sentence is gone. Each reading in the list below was re-taken onced64a4af. The ablation after the list is the round-1 reading onc07bb2cc8; this round changed no code line it mutated (only a docblock instack.zod.ts):pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/inline-grid-column-carriers.test.ts:Tests 52 passed (52), VERDICT command-exit 0.pnpm --filter @objectstack/spec typecheck: exit 0 (tsc --noEmit,check:scripts-typecheck, andcheck:test-typecheck: OK).pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2(the wholelocalproject):Test Files 694 passed (694),Tests 21050 passed | 1 todo (21051), VERDICT command-exit 0.pnpm --filter @objectstack/spec check:generated: "All 14 generated artifacts are up to date" afterpnpm --filter @objectstack/spec build. Nothing was regenerated and the tree is clean.test:repo(the vitestrepoproject) was not run locally. It belongs to CI'sTest Core.Ablation, one behaviour at a time, with
scripts/ablation-replace.mjs(anchor must hit, blob hash moves, restore proven by blob == HEAD and an emptygit diff HEAD). The test imports./stack.zodrelatively, so it reads source and nodist/leg applies.if (node.type === 'record:line_items') {was replaced with a type no node carries. Expected and observed direction: RED.Tests 5 failed | 47 passed (52). The PARITY test, the region, container and slot refusals, the binding test and the API-binding test failed, and every control stayed green. Restored: blob9f842c19fe4c== HEAD.const binding = node.dataSource;was replaced withundefined. RED,1 failed | 51 passed: only the binding-precedence test failed. Restored: blob == HEAD.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) was run atced64a4afand derives 92 commands, the same 92 as ataf540b850. All 92 were run locally, each exit code captured before any pipe.--ranreconciliation: "92 derived famil(ies) accounted for — 91 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".pnpm check:dual-build-cjs-loads. Reason: PREREQUISITE NOT MET, because it reads every package'sdist/(92 packages unbuilt in this worktree). That is a full-monorepo build, which belongs to CI.pnpm --filter @objectstack/lint run check:doc-formula-expressionsandpnpm check:lean-entry-closureread builtformula,lintandobjectql. Those were built before the run (turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/objectql --concurrency=2), and both gates exited 0 inside it.node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0, ".changeset/24190-line-items-hydrated-check.md [BREAKING+bang+clause-②-narrowing] not-required (already-registered)".node scripts/check-changeset-no-major.mjs --base origin/main: exit 0. Its level axis was NOT APPLICABLE locally ("nopull_requestpayload"), so CI's run on this PR reads theClause-②line above.pnpm --filter @objectstack/spec run check:api-surface: "public API surface + factory signatures unchanged".pnpm check:nul-bytes: OK.Acceptance notes
defineStack/objectstack validatepublish, so it owes one review atCONTRACT_REVIEW_TIER, per triage's grade6238078348.examples/app-showcase/src/ui/pages/invoice-document.page.ts(the comment above itsrecord:line_itemscolumns) says the block "does not fill a column from the child field, so an identity-only{ name }column prints its raw field name".examples/app-showcase/test/invoice-document-line-items.test.ts(its header) says the block "draws a column exactly as declared". Both stopped being true when the console pin took objectui#12854, not because of this change. Comment-only, owned by nobody: no card.page:tabstraversal blind spot (above) is spec(stack):forEachPageComponentnever visits apage:tabsitem'schildren, sodefineStack's page cross-reference checks (inline action targets, inline grid columns) do not judge anything placed inside tabs #24349.RecordLineItemsPropsTSDoc, the step-18 migration entry, and, in patch round 1, itsSTEP18_RATIONALEfragment inregistry.ts.template) are fixes of sentences this change makes false. None changes a schema, a key, an id, an order or a conversion.Generated by Claude Code