Repository navigation
fix(spec)!: defineStack's page cross-reference checks judge components inside page:tabs and page:accordion item children - #24417
Conversation
…s, so a page:tabs or page:accordion item's children are judged Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ion panels against their region-level twins Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…hildren-cross-references-judged, and re-true the line-items reach Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ge-walk-item-children
📓 Docs Drift Check2 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 beb8e229e41a77298b7e172f537e22045852f298 && git checkout beb8e229e41a77298b7e172f537e22045852f298
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8baeda52096bf2caf7007ceb8dd88735fe455ac8 3d635d4cc837880fba1c37944321d541be264557 && git checkout -B drift-repro 8baeda52096bf2caf7007ceb8dd88735fe455ac8 && git merge --no-ff 3d635d4cc837880fba1c37944321d541be264557
node scripts/docs-audit/affected-docs.mjs --json 8baeda52096bf2caf7007ceb8dd88735fe455ac8 |
席位核对 (seat check) · PR #24417 ·
|
… in a tab or accordion panel too, and this change's note stops citing it as unjudged Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ge-walk-item-children
…ge-walk-item-children
席位核对 (seat check) · PR #24417 ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, assignee zhuangjianguo. Line 1 is Fixes #24349, and Clause-②: yes (narrowing) starts a line. 7 files, net of the merges. Not governed. The changeset is minor on @objectstack/spec, with the ! title, one ADR-0087 marker (registered ui-page-panel-children-cross-references-judged), and a FROM → TO table. The seat applied the dev's five body pairs; the read-back is identical. |
| The walk | forEachPageComponent descends the panel positions pageComponentSlotPositions() declares: today items[].children on page:tabs and page:accordion, authorable entries only. That is the list the other page walks read, so the walk names no container. The direct-array rule is unchanged. |
| Pins and ablation (dev) | 16 tests, each refusal against its region-level twin. Controls: a panel record's own keys, a non-component list, a details[] entry with a type key, an undeclared list key, an undeclared panel key. Ablating the panel loop turns 14 of the 16 red; the restore is proven by blob hash. Spec: 697 files, 21267 tests passed; typecheck exit 0; check:generated 14/14. Public door: base accepted, head refused all three probes, with the region-level message located through the panel. |
| Accepted deviations | packages/spec/src/migrations/registry.ts.template: the new entry's STEP18_RATIONALE fragment and the line-items fragment's re-true, both part of the narrowing's own disposition. .changeset/24190-line-items-hydrated-check.md, by the seat's surface extension: lines 15 and 28 only, now true at the release they share. |
Check Changeset red by design |
check-empty-changeset refuses a PR that changes a changeset it did not add. This PR edits the pending .changeset/24190-line-items-hydrated-check.md, which is the DELIBERATE CORRECTION class. The workflow's own text (.github/workflows/pr-automation.yml, route 0) says to write the confirmation on the PR and leave the check red; the label is refused for this class (ruling D on #18375). The written confirmation: the note is .changeset/24190-line-items-hydrated-check.md, and what changed under it is in the PR body's "Pending release note corrected (patch round 1)" section. The seat confirms that correction here: both sentences this PR made false now state the release truthfully, and nothing else in that note moved. The job runs on pull_request only (on: pull_request, no merge_group), and Check Changeset is not a required context. So the seat's three conditions for enqueuing over a designed red all hold: the source says it is red by design on a pushed branch; it does not run on merge_group; and this comment names the gate and the cause. |
| Out of scope | Lint's validateResponsiveStyles keeps its own regions-only walk: no slots, footer or panels. It has no producer and no public-door reading, so it is not filed: Acceptance notes, dropped. |
| CI | At this record, on 3d635d4cc: in progress, apart from the designed red above. The landing waits for every other check to be green or an expected skip. |
| Landing | After a contract-review PASS and a green head (apart from the designed red): pr_ready then automerge_enable. The merge closes #24349 and frees stack.zod.ts for #24400. |
Generated by Claude Code
Contract reviewServed-tier: ① Derived judgmentsThe one code change is the panel loop in
Prime Directive #10, sentence by sentence: the re-trued walk docblock names the three readers of the list by name; the inline-action docblock's "every root, every nesting, a ② Semver level
Disposition: a semantic entry is the right form, not a bare marker. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Contract review adopted ·
Generated by Claude Code |
Fixes #24349
Clause-②: yes (narrowing)
What changes
forEachPageComponentinpackages/spec/src/stack.zod.tsis the one traversal behind twodefineStackcross-reference checks: the inline-action target check and the inline grid column check. It went into an array underpropertiesonly when the array's entries were component-shaped, meaning an object with a stringtype. Apage:tabsorpage:accordionitem is a panel record ({ label, value?, children }), not a component, so the walk stopped at the item. Nothing inside a tab or an accordion panel was judged.The walk now also descends the panel positions that
pageComponentSlotPositions()(ui/component.zod.ts) derives from the component rows. Today there is one such position,items[].children, declared bypage:tabsandpage:accordion. The walk reads authorable entries only. It matches by shape on every component, which is how the exportedwalkAddressedPageComponents, the ADR-0087 conversion walker and lint'swalkPageComponentsread the same list. The existing direct-array rule is unchanged, so no shape other than a panel's child list changes verdict.A component in a panel now gets the region-level finding, with only its location changed:
Why the declared list and not a generic record descent (G1)
The prop schemas already say where components hang. A row marks each composition key with
componentSlot, andpageComponentSlotPositions()derives the list from those marks. Its docblock says every page walk reads that list and none keeps its own.forEachPageComponentwas the one walk that did not read it.A generic rule ("descend any record in a props array into any array of component-shaped entries") would also descend records that no row declares as a panel. One example is an
object-master-detail-formdetails[]entry. The renderer draws nothing from such a record, and an entry with atypekey looks like a component. That is the false-positive edge. The declared list follows the schemas and names no component type: a custom type that composesitems[].childrenis judged too, as the other walks judge it. Controls pin what is not walked:details[]entry with atypekey and achildrenlist;Premises, verified on
origin/main31e7d0ba2forEachPageComponent(stack.zod.ts:2406) visitsregions[].components[]andslots.NAME. From each node it recurses only intopropertiesarrays whose entries passisComponentNode(:2384, a record with a stringtype).PageTabsProps.items[]is a closed object with notypekey (ui/component.zod.ts:874), so the walk stopped there. The probe below measures it.componentSlotmarks at base arechildrenon the thin containers (:587) and onpage:card(:1047),footeronpage:card(:1069), retiredbodyonpage:card(:1057), anditems[].childrenonpage:tabs(:997) andpage:accordion(:2726). The derived list prints those four positions; the only one reached through an item record isitems[].children. As an independent read, the otherz.array(z.unknown())keys in the rows hold records (data/staticData), not components. The otheritemsarrays (element:definition-list, the timeline items, the gantt bars) have nochildren. So the item-record positions are exactly tabs and accordion.collectInlinePageActions(:2475, walk at:2480) andcollectHydratedInlineColumnErrors(:3583, walk at:3645). At region level, both refuse withSTACK_CROSS_REFERENCE_INVALID, status 422, and a located path.validateComponentPropswalks throughwalkPageComponents(packages/lint/src/page-walk.ts). That walk readspageComponentSlotPositions()(non-retired entries,items[].childrenincluded) and is pinned atvalidate-component-props.test.ts:152. It is not the same walk asforEachPageComponent, so it is not changed here. Lint's other page rules use the same shared walk, with one exception:validateResponsiveStyles(validate-responsive-styles.ts:100,:187). That rule keeps its own walk overregions[].components[]only. It descendschildrenandbodyand skips slots,footeranditems[].children, so it has the same kind of gap through a different walk. It is not changed here (see Acceptance notes).Public-door probe (G6)
The probe calls
defineStackwith one record page and a declared child object. The button is an inlinetype: 'modal'action with a danglingtarget. The column is an identity-onlyscalecolumn over acurrencychild field.31e7d0ba2(base)(regions.0.components.0)page:tabsitems[1].children(regions.0.components.0.properties.items.1.children.0)page:accordionitems[0].children(…properties.items.0.children.0)record:line_itemscolumn, regionrecord:line_itemscolumn,page:tabsitems[1].children(…properties.items.1.children.0)A page whose tabs item contains the tabs node itself (a cycle) is accepted and terminates on both base and head.
Sentences re-trued (G3)
stack.zod.ts:forEachPageComponentdocblock ("reaches every container spelling") now states three traversal facts, panels among them.page:tabsitem'schildrenis not yet visited") is rewritten.18.ui-record-line-items-props-closed.ts: its reason's "reach: a block under apage:tabsitem'schildrenis not yet judged" now names the panel positions in the reach.registry.ts.template: theSTEP18_RATIONALEfragmentui-record-line-items-props-closedcarried the same sentence and is re-trued the same way. The template is also where the new entry's fragment goes.ui/component.zod.ts: at31e7d0ba2theRecordLineItemsPropsTSDoc names no tabs exclusion (:2440–:2538), so that file is untouched.18.ui-object-master-detail-form-details-closed.ts:46, template:3092, "reaches the block wherever a page carries it") were false on base and are true after the walk change, so they are unchanged.In-repo producers (G4)
Four authored pages nest components under a panel item. Each was built through
defineStackon this head, with this worktree'spackages/spec/distresolved and a positive control (a dangling button under a tab) refused in the same run:examples/app-showcase/src/ui/pages/project-detail.page.ts, with the 22 showcase objects;packages/platform-objects/src/pages/sys-user.page.ts;packages/platform-objects/src/pages/sys-organization.page.ts;packages/plugins/plugin-approvals/src/sys-approval-request.page.ts.All four build. None holds an inline action target or an identity-only typed-key column under a panel; the showcase
record:line_itemsblock declares atypeon all five columns. One documentation example nests record blocks under tabs (content/docs/protocol/objectui/layout-dsl.mdx), with nothing the checks judge. The published skills carry nopage:tabsorpage:accordion(zero hits, with the control termpage:cardhitting). No producer edit was needed.ADR-0087 disposition (G5)
The changeset
.changeset/24349-page-walk-panel-children.mdcontains:!title;Clause-②: yes (narrowing);registeredkind, naming the new step-18 semantic entryui-page-panel-children-cross-references-judged. ItsSTEP18_RATIONALEfragment is at order 103. No existing entry records the inline-action target refusal, soalready-registeredcould not cover that half;check:adr-0087-registrationpasses it:registered ui-page-panel-children-cross-references-judged (new here: …).Tests
New
packages/spec/src/stack-page-walk-panels.test.ts, 16 tests. Each refusal is pinned against its region-level twin: the same component built atregions.0.components.0gives the reference finding, and the panel case must equal it with only the location changed. Each case assertscode,statusand the wholeissueslist. It covers both callers (the inline action,record:line_items,object-master-detail-form) under tabs, under accordion, and in tabs nested in an accordion panel. It also covers a panel inside a container, a panel under a slot, two panels reporting in document order, every declared panel position on a custom type, the remedies, and the controls listed above.Ablation (the new tests import
./stack.zodsource, so there is nodistleg).scripts/ablation-replace.mjsdisabled the panel loop: the anchor went from 1 hit to 0 and the blob froma35d4fb7to7d7d6e30. The run went 14 failed, 2 passed: the panel-record control and the remedy stay green. The undeclared-key control goes red only through its own positive-twin assertion. The restore was proven by the tool: blob equals HEAD andgit diff HEADis empty.At
bb9d44234:pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 697 files, 21267 passed, 1 todo,VERDICT command-exit 0;pnpm --filter @objectstack/spec typecheck:VERDICT command-exit 0;pnpm --filter @objectstack/spec check:generated: 14 of 14 artifacts up to date.The later commits up to the final head
3d635d4ccchanged no file underpackages/specorpackages/lint: the mergesb0f8b9e3c,f5e6ad17dand3d635d4cc, and patch round 1's changeset-only commite9303fbbc. These readings hold for the final tree.Gates
3d635d4cc(patch round 1).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived the same 91 commands from 7 changed paths against merge base71fa166e9. All 91 ran, each with its exit code captured before any pipe, and 90 exit 0.node scripts/check-empty-changeset.mjs --base origin/mainexits 1 by design, because this PR changes a changeset it did not add (see the section above).--ranreconciliation:91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN.check-changeset-no-major --base origin/mainandcheck-adr-0087-registration --base origin/mainexit 0.b0f8b9e3c. The same 91 commands against merge baseee882b845:91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.bb9d44234. The same 91-command list (merge base31e7d0ba2) had 3 exit-3 prerequisite refusals:check:doc-formula-expressions,check:dual-build-cjs-loadsandcheck:lean-entry-closure, each because a sibling package was not built. On the final head all three ran and passed.pnpm check:dev-prereqs: exit 0, atb0f8b9e3cand again at3d635d4cc.Test Coreshards,Build Core, the type-check program steps), are CI's. This PR's check-runs are their record.Pending release note corrected (patch round 1)
This PR also edits
.changeset/24190-line-items-hydrated-check.md, another PR's release note. It is still pending onmainand unreleased, so it ships in the same release as this PR's changeset. The seat extended this claim's file surface to that one file (REWORK6257450280). This change made two of its sentences false. Both are now true at release::15said "A block under apage:tabsitem'schildrenis not yet judged." That sentence is gone. The sentence before it now says the check reaches the block "in a slot and, since 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, in apage:tabsorpage:accordionitem'schildren".:28said the showcase project detail block sits "under apage:tabsitem, so not yet judged". It keeps its dated reading onec749fbb44and drops "so not yet judged".Nothing else in that note changed (lines 15 and 28 only): its frontmatter,
Clause-②line, ADR-0087 marker, FROM → TO table and figures are as they were. This PR's own changeset cited the old sentence ("which the entry that brought the block's identity-only check lists as not yet judged"). It now says "That includes arecord:line_itemsblock in a tab or accordion panel, a position the block's identity-only check did not reach before this change."Check Changesetstays red on this PR by design.check-empty-changesetrefuses a PR that changes a changeset it did not add, and names this the DELIBERATE CORRECTION class. Its remedy is this written confirmation on the PR, not theskip-changesetlabel and not restoring the old text.Check Changesetis not one of the five required contexts.Acceptance notes
@objectstack/lint'svalidateResponsiveStyleskeeps its own regions-only walk (children/body; no slots,footeror panels), so aresponsiveStylesnode inside a tab is not checked by its five style rules. No in-repo producer putsresponsiveStylesin a panel, footer or slot, and no public door was measured, so this is noted and not filed. Carrier: none.pageComponentSlotPositions()docblock (ui/component.zod.ts:8648) names three readers;forEachPageComponentis now a fourth. The list is illustrative and is not edited (outside the claim's surface).forEachPageComponentstill reaches undeclared keys and the retiredpage:card.body, as it did on base. Narrowing it to the declared direct slots would change the verdict of shapes other than a panel's children, so it is left as it is.Generated by Claude Code