Skip to content

fix(spec)!: defineStack's page cross-reference checks judge components inside page:tabs and page:accordion item children - #24417

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24349-page-walk-item-children
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24349-page-walk-item-children

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24349

Clause-②: yes (narrowing)

What changes

forEachPageComponent in packages/spec/src/stack.zod.ts is the one traversal behind two defineStack cross-reference checks: the inline-action target check and the inline grid column check. It went into an array under properties only when the array's entries were component-shaped, meaning an object with a string type. A page:tabs or page:accordion item 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 by page:tabs and page:accordion. The walk reads authorable entries only. It matches by shape on every component, which is how the exported walkAddressedPageComponents, the ADR-0087 conversion walker and lint's walkPageComponents read 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:

Inline action 'open_detail' on page 'crm_invoice_record' (regions.0.components.0.properties.items.1.children.0) references page 'no_such_modal_xyz' (via modal target) which is not defined in pages.

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, and pageComponentSlotPositions() derives the list from those marks. Its docblock says every page walk reads that list and none keeps its own. forEachPageComponent was 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-form details[] entry. The renderer draws nothing from such a record, and an entry with a type key 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 composes items[].children is judged too, as the other walks judge it. Controls pin what is not walked:

  • a panel record's own keys;
  • a child list holding no component;
  • a details[] entry with a type key and a children list;
  • a panel-shaped list under an undeclared key;
  • an undeclared key inside a declared panel.

Premises, verified on origin/main 31e7d0ba2

  • P1, holds. forEachPageComponent (stack.zod.ts:2406) visits regions[].components[] and slots.NAME. From each node it recurses only into properties arrays whose entries pass isComponentNode (:2384, a record with a string type). PageTabsProps.items[] is a closed object with no type key (ui/component.zod.ts:874), so the walk stopped there. The probe below measures it.
  • P2, holds. Read from the rows, not from the card's list: the componentSlot marks at base are children on the thin containers (:587) and on page:card (:1047), footer on page:card (:1069), retired body on page:card (:1057), and items[].children on page:tabs (:997) and page:accordion (:2726). The derived list prints those four positions; the only one reached through an item record is items[].children. As an independent read, the other z.array(z.unknown()) keys in the rows hold records (data / staticData), not components. The other items arrays (element:definition-list, the timeline items, the gantt bars) have no children. So the item-record positions are exactly tabs and accordion.
  • P3, holds. The callers are collectInlinePageActions (:2475, walk at :2480) and collectHydratedInlineColumnErrors (:3583, walk at :3645). At region level, both refuse with STACK_CROSS_REFERENCE_INVALID, status 422, and a located path.
  • P4: lint has a different walk, with no gap in the component-props gate. validateComponentProps walks through walkPageComponents (packages/lint/src/page-walk.ts). That walk reads pageComponentSlotPositions() (non-retired entries, items[].children included) and is pinned at validate-component-props.test.ts:152. It is not the same walk as forEachPageComponent, 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 over regions[].components[] only. It descends children and body and skips slots, footer and items[].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 defineStack with one record page and a declared child object. The button is an inline type: 'modal' action with a dangling target. The column is an identity-only scale column over a currency child field.

component placed at 31e7d0ba2 (base) this head
button, region refused, 422, (regions.0.components.0) refused, same
button, page:tabs items[1].children accepted refused, 422, (regions.0.components.0.properties.items.1.children.0)
button, page:accordion items[0].children accepted refused, 422, (…properties.items.0.children.0)
record:line_items column, region refused, 422 refused, same
record:line_items column, page:tabs items[1].children accepted refused, 422, region-level message at (…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:
    • The forEachPageComponent docblock ("reaches every container spelling") now states three traversal facts, panels among them.
    • The inline-action docblock ("every root, every nesting") was false on base and is true now; it now names the panels.
    • The grid-column docblock's line-items bullet ("a block under a page:tabs item's children is not yet visited") is rewritten.
  • 18.ui-record-line-items-props-closed.ts: its reason's "reach: a block under a page:tabs item's children is not yet judged" now names the panel positions in the reach.
  • registry.ts.template: the STEP18_RATIONALE fragment ui-record-line-items-props-closed carried 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: at 31e7d0ba2 the RecordLineItemsProps TSDoc names no tabs exclusion (:2440–:2538), so that file is untouched.
  • The master-detail siblings (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 defineStack on this head, with this worktree's packages/spec/dist resolved 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_items block declares a type on 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 no page:tabs or page:accordion (zero hits, with the control term page:card hitting). No producer edit was needed.

ADR-0087 disposition (G5)

The changeset .changeset/24349-page-walk-panel-children.md contains:

  • the ! title;
  • Clause-②: yes (narrowing);
  • one ADR-0087 marker, of the registered kind, naming the new step-18 semantic entry ui-page-panel-children-cross-references-judged. Its STEP18_RATIONALE fragment is at order 103. No existing entry records the inline-action target refusal, so already-registered could not cover that half;
  • a FROM → TO table.

check:adr-0087-registration passes 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 at regions.0.components.0 gives the reference finding, and the panel case must equal it with only the location changed. Each case asserts code, status and the whole issues list. 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.zod source, so there is no dist leg). scripts/ablation-replace.mjs disabled the panel loop: the anchor went from 1 hit to 0 and the blob from a35d4fb7 to 7d7d6e30. 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 and git diff HEAD is 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 3d635d4cc changed no file under packages/spec or packages/lint: the merges b0f8b9e3c, f5e6ad17d and 3d635d4cc, and patch round 1's changeset-only commit e9303fbbc. These readings hold for the final tree.

Gates

  • Local, on the final head 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 base 71fa166e9. All 91 ran, each with its exit code captured before any pipe, and 90 exit 0. node scripts/check-empty-changeset.mjs --base origin/main exits 1 by design, because this PR changes a changeset it did not add (see the section above). --ran reconciliation: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN. check-changeset-no-major --base origin/main and check-adr-0087-registration --base origin/main exit 0.
  • Before patch round 1, at b0f8b9e3c. The same 91 commands against merge base ee882b845: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.
  • First run, at bb9d44234. The same 91-command list (merge base 31e7d0ba2) had 3 exit-3 prerequisite refusals: check:doc-formula-expressions, check:dual-build-cjs-loads and check:lean-entry-closure, each because a sibling package was not built. On the final head all three ran and passed.
  • Local-only family. pnpm check:dev-prereqs: exit 0, at b0f8b9e3c and again at 3d635d4cc.
  • Not run locally. The whole farm, and the CI jobs the derivation names as CI's own shell (the Test Core shards, 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 on main and 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 (REWORK 6257450280). This change made two of its sentences false. Both are now true at release:

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 a record:line_items block in a tab or accordion panel, a position the block's identity-only check did not reach before this change."

Check Changeset stays red on this PR by design. check-empty-changeset refuses 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 the skip-changeset label and not restoring the old text. Check Changeset is not one of the five required contexts.

Acceptance notes

  • @objectstack/lint's validateResponsiveStyles keeps its own regions-only walk (children / body; no slots, footer or panels), so a responsiveStyles node inside a tab is not checked by its five style rules. No in-repo producer puts responsiveStyles in a panel, footer or slot, and no public door was measured, so this is noted and not filed. Carrier: none.
  • The pageComponentSlotPositions() docblock (ui/component.zod.ts:8648) names three readers; forEachPageComponent is now a fourth. The list is illustrative and is not edited (outside the claim's surface).
  • The direct-array rule in forEachPageComponent still reaches undeclared keys and the retired page: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

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/migrations/registry.ts.template), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/migrations/registry.ts.template) — pages documenting those are invisible to this run
  • 3 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 — 143 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 8baeda52096bf2caf7007ceb8dd88735fe455ac8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from beb8e229e41a77298b7e172f537e22045852f298 — the merge of head 3d635d4cc837880fba1c37944321d541be264557 into base 8baeda52096bf2caf7007ceb8dd88735fe455ac8, 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 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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24417 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T01:03Z

REWORK (patch round 1, same claim 6256664531, same dev) at head b0f8b9e3cdc6c6499275880400e67a7c8ad4f28b, before the contract review. One item: the dev's own out-of-scope finding. Everything else in the seat's read holds:

  • The walk descends the panel positions pageComponentSlotPositions declares. That is the list the other page walks read, so the walk follows the declared shapes and does not name tabs.

  • The direct-array rule is unchanged.

  • The 16 pins pair each refusal with its region-level twin, and the ablation turns 14 of them red.

  • The new step-18 entry, plus its STEP18_RATIONALE fragment in registry.ts.template, is accepted as a deviation: it is the narrowing's own disposition.

  • The item: .changeset/24190-line-items-hydrated-check.md is pending on main (unreleased), and it ships in the same release as this PR's changeset. Two of its sentences become false here:

    • :15, "A block under a page:tabs item's children is not yet judged.";
    • :28, the clause "so not yet judged" about the showcase block.

    The seat extends the file surface to that one file. Make both sentences true at release:

    Then re-read this PR's own changeset sentence that cites it ("which the entry that brought the block's identity-only check lists as not yet judged") and keep it true.

  • The PR body: the changeset gate asks a PR that edits someone else's pending release note to say so on the PR. Name the note and what changed under it in the PR body, and return the body edits as pr_body_edits pairs.

  • Push once after check-changeset-no-major and check-adr-0087-registration exit 0 locally, plus the changed-file gates. CI is read on the new head.

  • After the push: the seat checks the new head and runs the contract review at CONTRACT_REVIEW_TIER (schema face, a narrowing). needs:contract-review goes on the PR then.


Generated by Claude Code

… 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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24417 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T01:47Z

ACCEPT at head 3d635d4cc837880fba1c37944321d541be264557 (claim 6256664531, patch round 1: REWORK 6257450280; reports on #24349). A contract review at CONTRACT_REVIEW_TIER is owed: the schema face, a narrowing (Clause-②: yes (narrowing)). It runs on this head, isolated from the seat. needs:contract-review is on the PR until its record is adopted.

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3d635d4cc837880fba1c37944321d541be264557
Local-runs: none
Scope: packages/spec/src/stack.zod.ts, packages/spec/src/migrations/entries/semantic/18.ui-page-panel-children-cross-references-judged.ts, .changeset/24349-page-walk-panel-children.md + .changeset/24190-line-items-hydrated-check.md, packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/stack-page-walk-panels.test.ts, packages/spec/src/ui/component.zod.ts (the componentSlot marks and pageComponentSlotPositions), packages/spec/src/system/i18n-resolver.ts and packages/spec/src/conversions/walk.ts (the two sibling spec walks), packages/lint/src/page-walk.ts and packages/lint/src/validate-responsive-styles.ts, packages/spec/src/migrations/entries/semantic/18.ui-object-master-detail-form-details-closed.ts and 18.inline-grid-column-identity-only-currency-scale-refused.ts (sibling shape), packages/spec/src/migrations/types.ts, scripts/check-empty-changeset.mjs, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, .github/workflows/pr-automation.yml (route 0), the four in-repo producers the PR names, and objectui at the console pin 0707b4ae3: packages/components/src/renderers/layout/containers.tsx (PageTabsRenderer, PageAccordionRenderer) and packages/types/src/node-slots.ts. Face: schema, a narrowing of what defineStack accepts. Net diff read against merge base 71fa166e9: 7 files, +415/-20, no generated artefact among them.

① Derived judgments

The one code change is the panel loop in forEachPageComponent (stack.zod.ts:2424-2451): after the unchanged direct-array rule, it descends every position of pageComponentSlotPositions() that carries a panelKey and is not retired, reading props[key] as a list of panel records and visiting only each record's panelKey array. Today that list is exactly one position, items[].children, derived from the componentSlot marks on PageTabsProps.items[].children (component.zod.ts:997) and PageAccordionProps.items[].children (:2726); the direct marks (children :587 :1047, footer :1069, retired body :1057) carry no panelKey and are untouched by the loop. Each accept-set change this implies, judged against what the renderer at the pin draws:

  • An inline action with a type: 'modal' target naming no declared page, or a type: 'flow' target naming no declared flow, inside a page:tabs or page:accordion item's children — accepted on base, refused now with STACK_CROSS_REFERENCE_INVALID 422 and the region-level sentence located through the panel (regions.0.components.0.properties.items.1.children.0). RIGHT. PageTabsRenderer draws renderChildren(item.children) (containers.tsx:989) and PageAccordionRenderer draws the same inside AccordionContent (:1189); objectui's own slot table says items[].children for both (node-slots.ts:157-158). The consumer at stack.zod.ts:4126-4143 judges both halves (flow against flowNames, modal against pageNames), so the entry's and changeset's two-target claim is the code's. The guards that judge a target only when the stack declares at least one flow, or at least one page, and the modal-names-a-page-only rule are the region-level ones, unchanged.
  • An identity-only column carrying scale over a currency child field, on a record:line_items block's columns or an object-master-detail-form block's details[].columns, inside a panel — accepted on base, refused now with the column schema's own message at the panel path. RIGHT: collectHydratedInlineColumnErrors (:3673-3691) reads both carriers through the same walk; the child-object resolution (lineItemsChildObject) and the must-be-declared-in-this-stack rule are unchanged.
  • Depth and roots. A panel inside a container, under a slot, or tabs inside an accordion panel — all reached, because the loop recurses through visitComponent from every root the walk already had. RIGHT; the test pins each against its region-level twin with only the path changed.
  • A custom component type composing items[].children — now judged, matched by shape. RIGHT and required for consistency: the exported walkAddressedPageComponents (i18n-resolver.ts:2339-2367), the conversion walker (conversions/walk.ts:304-329) and lint's walkPageComponents (page-walk.ts:132-178) all match a position by shape on every component, and the pageComponentSlotPositions docblock says every walk reads that one list and keeps none of its own.
  • Retired positions excluded (!retired). RIGHT: that is the authoring-walk rule the exported walk and lint's walk apply; the conversion walker alone reads retired positions, for stored documents. Today no panel position is retired, so the filter is inert and correct by construction.
  • What does not change verdict, each pinned by a control in the test: a panel record's own keys (an action on the item is not a component); a children list holding no component; an object-master-detail-form details[] entry carrying a type key and a children list (the direct-array rule treats it as a component node and stops at it for want of properties, on base and head alike; the renderer draws nothing from it); a panel-shaped list under an undeclared key; an undeclared key inside a declared panel. RIGHT: PageTabsRenderer and PageAccordionRenderer read nothing else on an item as components (the :587 candidate helper only counts badges).
  • The direct-array rule is unchanged, including its reach into any component-shaped array under properties and into the retired page:card.body. Narrowing it would have moved verdicts outside the card; leaving it is right, and the acceptance note says so accurately.
  • Doors. No HTTP handler or metadata save path names STACK_CROSS_REFERENCE_INVALID, collectInlinePageActions or collectHydratedInlineColumnErrors outside stack.zod.ts (grep over every package src, non-test: zero hits), and the error class docblock (:3131-3134) records the same. The region-level refusal is reached only through defineStack — os validate (validate.ts:240 loadConfig evaluates the config's default export, and a throw there is the command's refusal), os build, the serve/migrate host configs and DevPlugin. The panel refusal is reached at exactly those doors and no other; the save door runs neither on base nor on head, so there is no door the region-level one reaches that the panel one misses.
  • Order and paths. Direct arrays are visited before panel lists on one node, matching the walks' arbitrated order; the dot-separated path form matches the direct rule's; a node is never visited twice (a panel record visited as a component by the direct rule has its children read only by the panel rule, since the direct rule descends only under properties).
  • Producers. The four in-repo pages that nest components under a panel (showcase project detail; sys-user; sys-organization; sys-approval-request) carry no modal or flow target and no scale key; the showcase record:line_items block declares a type on every column, so the identity-only rule has nothing to judge there. The published skills carry zero page:tabs or page:accordion hits against one page:card control file. No producer edit was owed. RIGHT.

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 page:tabs or page:accordion item's children included" and the grid-column docblock's line-items bullet now describe the code; the line-items entry reason ("wherever a page carries it — in a page region's components, in a container's component children, in a page:tabs or page:accordion item's children and in a slot") is true, its dated population sentence was already free of "not yet judged"; the master-detail sibling's "reaches the block wherever a page carries it" (entry :46, template :3092) was false on base and is true now, correctly left unchanged; the new entry's surface, reason and acceptanceCriteria claim the reach the code has, name defineStack and objectstack validate as the doors, and say no conversion is registered, which is true. No sentence claims the save door or the load path.

② Semver level

@objectstack/spec: minor, a ! title, a BREAKING banner, Clause-②: yes (narrowing), one adr-0087 marker in the registered form naming ui-page-panel-children-cross-references-judged, and a FROM → TO table with the one-line fix. That is the repo's launch-window convention for an accept-set narrowing: check-changeset-no-major on this head prints "direction arm: narrowing — a BREAKING change; during the launch window it ships minor" and "no major bump", and route 1 of the Check Changeset workflow says breaking-ness is carried by the banner plus the disposition, not the level. The changeset matches what the diff publishes: the only published change is the narrowed accept set of defineStack and the new semantic entry; no key, export or stored shape is added, removed or renamed.

Disposition: a semantic entry is the right form, not a bare marker. not-required (already-registered …) could not cover the inline-action half — no step-18 entry records the dangling modal/flow target refusal (the eight entries naming page actions are about action rows and url schemes; dashboard-header-modal-target-page-only is a dashboard surface) — and not-required (no-migration-prescription) is refused on a body that frames a rewrite, which this FROM → TO table does. The entry is a refusal-only D3 record, which is the shape of its siblings (ui-object-master-detail-form-details-closed, inline-grid-column-identity-only-currency-scale-refused, ui-record-line-items-props-closed: id, surface, replacement, reason, acceptanceCriteria, no conversionIds, no relevantWhen; 380 of 402 step-18 entries omit relevantWhen). Its STEP18_RATIONALE fragment sits in id order between ui-page-action-row-navigation-executor-refused and ui-record-json-retired, as the list's sort-by-id rule requires; order values are not unique by design. check-adr-0087-registration on this head: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered ui-page-panel-children-cross-references-judged (new here: …)". Agreed.

③ Boundary flags

  • Check Changeset red (three runs on this head, all failure). Read in the job log of each: the only red step is check-empty-changeset's foreign-changeset rule on .changeset/24190-line-items-hydrated-check.md ("present on the merge base and CHANGED by this PR"), the DELIBERATE CORRECTION class; in the same job check-adr-0087-registration and check-changeset-no-major pass. That is the only reason it is red. Route 0 asks for the confirmation on the PR naming the note and what changed under it, no skip-changeset label, and the check left red; the PR body's "Pending release note corrected" section does that, the label list carries no skip-changeset, and this record is the written confirmation: lines 15 and 28 of that note are now true at the release both notes share (line 15 names the panel reach "since spec(stack): forEachPageComponent never visits a page:tabs item's children, so defineStack's page cross-reference checks (inline action targets, inline grid columns) do not judge anything placed inside tabs #24349"; line 28 keeps its dated reading on ec749fbb44 and drops "so not yet judged"), and nothing else in it changed — the net diff carries exactly those two hunks; frontmatter, Clause-②, marker, table and figures are byte-identical. Confirmed.
  • Deviation (1), registry.ts.template outside the claim's file list. Answered: the claim's disposition item owes the entry its STEP18_RATIONALE fragment, and the template is that fragment's one home; the line-items fragment's re-true is the G3 instruction applied to the same sentence in its second carrier.
  • Deviation (2), merges of origin/main through os-regen-merge.sh with nothing to regenerate. Answered: the net diff against the merge base holds no generated artefact and no file outside the seven the PR names.
  • Deviation (3), a model-identifier trailer amended before the first push. Answered from the remote: the seven pushed commits carry the model-free trailer pair (8 trailer lines, 0 model identifiers).
  • Round-1 deviations (container restart, check:type-check-debt first, worktree recreated). Procedural, no bearing on the diff; the final-head gate readings are the PR's record and CI's are read below.
  • Acceptance note: validateResponsiveStyles keeps its own walk. Verified: validate-responsive-styles.ts:98-103,177,193-197 descends children / properties.children / body / properties.body from regions[].components[] only — no slots, no footer, no panels. The note is accurate and this PR is right not to touch packages/lint. ESCALATED to the seat as a finding outside this claim: that rule keeps a list of its own, which the pageComponentSlotPositions docblock forbids, and a responsiveStyles node in a tab, a card footer or a slot escapes the five style rules at os lint. It is reproducible from the code and is the same class as this card; it owes a domain:spec card with its lint rider rather than a note. Not a FAIL item here — pre-existing, outside the claim, not introduced by this diff.
  • Acceptance note: the pageComponentSlotPositions() docblock names three readers. Answered: a one-line doc follow-up for whoever next edits component.zod.ts; outside this claim's sentence surface. Related wording nit for the same follow-up: the template fragment and the test header say "the list every other page walk reads", which the responsive-styles rule contradicts; the walk's own docblock lists the three by name and is exact. Not an accept-set claim.
  • PR body: a page whose tabs item contains the tabs node itself terminates on both base and head. Not re-measured here (read-only); unreachable from JSON or YAML metadata, and the direct-array rule carries the same exposure on base for a container that contains itself, so the class is unchanged by this diff and does not bear on the accept set.
  • Lint's component-props gate (P4). Verified: page-walk.ts:132 filters !position.retired and :161-178 descends items[].children; a different walk with no gap, correctly left alone.
  • Check-runs on this head, read at 2026-10-19T02:17Z: the five required contexts are all success — Lint & Repo Gates, TypeScript Type Check (all four lanes), Test Core (the aggregate and all six shards), Build Core, Governed Surface Queue Guard — as are Dogfood Regression Gate, Spec property liveness and every claim, size and docs check; nothing is queued or in progress; Check Changeset is failure on all three runs, by design as above, and is not a required context. No governed surface is in the file list (Governed Surface Queue Guard green). Landing stays the owning seat's act under Multi-agent discipline §7.

Implemented-by: claude/issue-24349-page-walk-item-children
Reviewed-by: session_019oFY6Hi35J7atvgVFnmoaK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review adopted · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T02:19Z

  • Adopted: contract review 6258251325 (Served-tier: CONTRACT_REVIEW_TIER, schema face; Head-sha 3d635d4cc837880fba1c37944321d541be264557, the current head; PASS, no FAIL item). The seat read it against the head. needs:contract-review is removed in this act.
  • Check Changeset: red on all three runs for one reason only, read in each job log: the foreign-changeset rule on .changeset/24190-line-items-hydrated-check.md, the DELIBERATE CORRECTION class. check-adr-0087-registration and check-changeset-no-major pass in the same job. The written confirmation is the PR body's section and seat check 6257915803. The three conditions for enqueuing over a designed red hold, as that seat check records.
  • The review's escalation, carried, not filed: packages/lint/src/validate-responsive-styles.ts keeps its own regions-only walk (children / body, with no slots, footer or panels). The pageComponentSlotPositions docblock says no page walk keeps its own list. It is pre-existing and outside this claim. The filing gate asks for a measured wrong result at a public door or a named producer before a card. The review read the code but did not run os lint, so the seat carries it on its seat post as noted, not filed. It is filed when a dispatch measures it: one responsiveStyles node in a tab, run through os lint. The two docblock nits (the reader count in pageComponentSlotPositions, and "the list every other page walk reads" in the template fragment and the test header) ride that same card.
  • Pre-landing on this head: 52 check-runs: 38 success, 11 skipped, 3 failure, all three the designed Check Changeset red. The five required contexts are green. check-expected-skips: OK. check-governed-merges: not governed, +415/−20.
  • Landing: pr_ready then automerge_enable, in this act. The merge closes spec(stack): forEachPageComponent never visits a page:tabs item's children, so defineStack's page cross-reference checks (inline action targets, inline grid columns) do not judge anything placed inside tabs #24349 and frees stack.zod.ts for spec(object-extension): the extension refusal says defineStack() attaches a top-level action with objectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 02:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 19, 2026 02:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit db1f1fd Oct 19, 2026
52 of 56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24349-page-walk-item-children branch October 19, 2026 02:49
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 size/m tests tooling

Projects

None yet

2 participants