Skip to content

fix(spec): the page-component conversion walk reaches nested containers, not only regions and slots (#6775) - #7034

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6775-conversion-walker-slots
Aug 9, 2026
Merged

fix(spec): the page-component conversion walk reaches nested containers, not only regions and slots (#6775)#7034
os-zhuang merged 1 commit into
mainfrom
claude/issue-6775-conversion-walker-slots

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #6775

Premise re-verified against origin/main @ 0f539bd — half of it had already landed

The card measures two gaps. Measured before touching anything, with the built spec of this worktree (applyConversions(..., { includeRetired: true }) + PageSchema.safeParse):

shape spec-valid converted on origin/main
regions[].components[] (baseline) yes yes
slots.header on a regions: [] slotted page yes yes — landed in #6776
page:cardproperties.children[] yes no
page:tabsproperties.items[].children[] yes no
page:cardproperties.body[] / footer[] yes no
slots.header → nested children[] yes no

So premise_still_valid: true, but narrowed: the slots.* half was fixed by #6776 (the walker and its doc already carry it). What remained live is container nesting, and that half reproduced exactly as the card describes — spec-valid, unconverted, silent.

What changed

mapPageComponents now descends into the containers a component nests its sub-tree in, matching walkPageComponents (packages/lint/src/page-walk.ts) key for key and path spelling for path spelling:

  • properties.children[] — generic layout nesting
  • properties.items[].children[]page:tabs / page:accordion panels
  • properties.body[] / properties.footer[]page:card

Recognised by shape (an array), not by component type — the same rule lint applies, because properties is an open bag that nothing validates per-type on the load path. A record:alert's body: 'Confirm the work.' string is not an array and is never mistaken for a slot.

The mapper runs on the container first and the descent reads the mapped component. That is what keeps the walk single-visit under page-card-body-to-children, which moves a container key (properties.bodyproperties.children): the sub-tree is walked once under the canonical key, not once per spelling. Pinned by a test asserting the inner notice path is …properties.children[0].properties.children, never …body[0]….

Copy-on-write is unchanged and pinned: an untouched nested branch keeps its reference, and a stack where nothing converts is still returned by identity.

Why this was load-bearing rather than tidy

The standard answer for a site a conversion cannot reach is the tombstone — the key is typed never, so tsc refuses it at the authoring site and the parse refuses it at load, wherever it sits. That answer does not hold for a key that stays live elsewhere on the surface. page-header-subtitle-alias retires description on page-header components while description remains a declared prop on others (element:text_input helper text), so it cannot be tombstoned: properties.description parses green at any position. A header inside a card, or on the slotted record page objectui's own guide prescribes, got no rewrite and no diagnostic from any of the three layers — conversion silent, PageSchema satisfied, props gate advisory/CLI-only and running on already-converted metadata.

Reachable-set difference vs. the lint walker (enumerated, as asked — not silently widened)

Two differences remain after this change, both deliberate, both pinned by the parity test:

  1. Source-authored pages (kind: 'html' | 'react' | 'jsx') — lint skips them, the conversion still visits them. Lint's skip prevents findings about a derived region cache the author never wrote; a conversion still has to normalize that cache or a stored page rehydrates in a shape the runtime no longer serves. Matching lint here would remove reach conversions have had since feat(spec): 登记 ADR-0087 D2 conversion page-header-subtitle-alias(descriptionsubtitle) #5509, so it was not done.
  2. Depth ceiling of 32 containers — no lint counterpart, because lint walks parsed JSON only. This walker also runs on hand-built defineStack objects, where a self-referencing children is reachable; mirrors MAX_REGION_DEPTH for flow regions.

Nothing else differs: the parity test walks a page carrying the probe at all eight positions lint yields and asserts the conversion's notice paths are the same set.

Fixture coverage — reverse verification, direction predicted first

Predicted before measuring: with the new fixtures/tests in place and the walker reverted to origin/main, every fixture pair for a walker-backed conversion and every nested walker test goes RED, while region-level and slot-level assertions stay GREEN.

Measured (git checkout HEAD -- walk.ts, run src/conversions): 16 failures, 181 passing — exactly 7 fixture pairs + 9 nested walker tests; region- and slot-level assertions green throughout. Restoring the walker: 197/197 green.

The 7 conversions whose fixtures now carry a slots.* node and a container-nested node:

conversion notices before → after
page-header-subtitle-alias 2 → 7
record-picker-display-field-to-label-field 2 → 4
record-picker-inert-keys-removed 2 → 4
page-card-body-to-children 1 → 4
inline-action-api-params-to-body-extra 1 → 3
page-tabs-type-to-tab-style 3 → 5
page-component-visibility-to-visibleWhen (v15, same walker) 1 → 3

A second, independent RED→GREEN data point: the new cross-walker parity test in packages/lint resolves @objectstack/spec from dist, so it first ran against the old built walker and failed on precisely the four nested positions; after rebuilding spec it passes.

New tests:

  • packages/spec/src/conversions/page-component-walk.test.ts — parity per container shape, three-deep recursion, nesting inside a named slot, single-visit under the moved container key, copy-on-write + reference sharing, shape gating (prose body, tab records, non-dict children), cycle termination, and the depth ceiling (converts at 32, stops at 33, no throw).
  • packages/lint/src/page-walk-conversion-parity.test.ts — the cross-walker guard described above.

Verification

Dependency closure built first (#6371): turbo run build --filter='./packages/*' --filter='./examples/*^...' — 66/66. Consumer sweep run in the '…@objectstack/spec' direction (#6218), i.e. from the spec walker outward to its consumers: metadata/database-loader, metadata-protocol (protocol, stored-migration, runtime-authoring-gate), objectql (plugin, rule-validator), service-datasource — all exercise the conversion entry points, none pins region-only reach; their suites are green below.

Every check:* gate in .github/workflows/lint.yml, enumerated one by one — 68 steps, all PASS, including pnpm lint, pnpm --filter @objectstack/spec exec tsc --noEmit, check:generated --reconcile-only, check:spec-changes, check:upgrade-guide, check:authorable-surface, check:api-surface, check:docs, both turbo build legs, turbo typecheck (121/121), examples + downstream-contract typecheck. Plus node scripts/check-adr-0087-registration.mjs --base origin/main → ✓ (no declared-breaking changeset).

Test suites in full: @objectstack/spec 9127/9127 · @objectstack/lint 1771/1771 · @objectstack/metadata-protocol 820/820 · @objectstack/cli 1095/1095 · @objectstack/metadata 588/588.

Generated trees produced by generators only (gen:schema, gen:docs, gen:spec-changes, gen:upgrade-guide, gen:skill-docs, gen:skill-refs, gen:api-surface) — no drift, nothing under content/docs/references changed, so there was nothing to git add.

Scope

Walker + fixtures + tests + changeset. No conversion registry entry added or removed (CONVERSIONS_BY_MAJOR is byte-identical) — sibling territory per #6815 / #5488. Doc comments on the entries that claimed "region level is the reach, deliberately" were corrected, since the change makes them false.

Out of scope, noted not acted on: packages/spec/src/system/i18n-resolver.ts:897 documents its own region-level-only component visit for page:header. That is a separate walker with its own contract; widening it is not this card's business.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cd32yJ2omZsgUXxiAjeBcE


Generated by Claude Code

…rs, not only regions and slots (#6775)

`mapPageComponents` visited `pages[].regions[].components[]` and
`pages[].slots.<slot>` and stopped. A component nested inside another
component's `properties` — a card's `children` / `body` / `footer`, a
`page:tabs` / `page:accordion` panel's `items[].children` — was visited by
nobody, so no page-component conversion rewrote it, while
`walkPageComponents` in `packages/lint` has descended into those containers
from the start. Every conversion therefore reached strictly less than the
lint rule that judges its result.

The walk now descends into the same containers lint does, to any depth, with
the same path spelling, so a conversion notice and a lint finding name one
site with one string. The mapper still runs on the container first and the
descent reads the MAPPED component, so `page-card-body-to-children` — which
moves `properties.body` to `properties.children` — walks its sub-tree exactly
once, under the canonical key. Copy-on-write is unchanged: an untouched
sub-tree keeps its reference, and a stack where nothing converts is returned
by identity.

The load-path cost this fixes belongs to `page-header-subtitle-alias`. Every
other entry leans on a tombstone for the sites a conversion cannot reach;
this one has none, because `description` stays a live declared prop on other
components, so `properties.description` parses green at any position. A
header authored in a card or on a `kind: 'slotted'` record page got no
rewrite and no diagnostic from any layer.

Fixtures for all seven walker-backed conversions now pin the slotted and
nested positions beside the region-level one, and a cross-walker parity test
in `packages/lint` — the only place that can see both walkers — asserts the
two reachable sets agree, pinning the two deliberate differences: lint skips
source-authored pages (the conversion must still normalize their derived
cache) and the conversion walk keeps a 32-container depth ceiling for
hand-built `defineStack` objects.

No conversion registry entry added or removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cd32yJ2omZsgUXxiAjeBcE
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 10:40am

Request Review

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

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

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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/xl tests tooling

Projects

None yet

2 participants