Skip to content

docs(layout): the PageHeader page teaches the canonical page:header node (objectui#3906) - #11162

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-3906-page-header-phase-2
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-3906-page-header-phase-2

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #3906
Clause-②: no

Phase 2 of the maintainer ruling on objectui#3906 (comment 5230005161, direction (b)): the PageHeader docs page's author face becomes the canonical page:header node. Phase 1 (the truth banner) landed as PR #5922; this is the last phase.

What changed

  • content/docs/layout/page-header.mdx, rewritten. Every example, the property table, both layouts, the styling numbers, accessibility and responsive behaviour are measured against PageHeaderRenderer in @object-ui/components and the spec's PageHeaderProps (the ComponentPropsMap['page:header'] row). No number was carried over from the old page or from @object-ui/layout's PageHeader. The section "Gating a header action on a relation field" was already canonical-true and is kept as it was. The Phase-1 banner gives way to the rewritten page, and the page-header / layout:page-header alias is named once, as an alias, in its own short section.
  • content/docs/guide/layout.md, the PageHeader section only, moved to the same key. The hunk near the page node's button label that draft PR feat(cli): objectui validate and objectui check judge through the strict authoring face (objectui#5250, slice A) #11069 edits is untouched.
  • The catalog demo is re-keyed to page:header, with its props in properties, and renamed layout-page-header/pageheader-with-actions to layout-page-header/basic-page-header (index regenerated with scripts/regenerate-catalog-index.py, which moved only this entry).
  • The demo's test re-derives its pins for the canonical node: shape, a real SchemaRenderer render, the contract, and the registration's declared inputs. The objectui#3787 hand-rolled-copy guard stays, now checked against the canonical renderer's classes.
  • An empty-frontmatter changeset, .changeset/3906-page-header-canonical-docs.md, declares that nothing is released.

What the demo became, and why (the dispatch asked me to say which)

The old demo was page-header with icon and two button children. I measured the canonical node, and neither shape carries over:

  • It draws no children. A real render leaves the buttons out. objectui's mirror refuses the key on this node (children: invalid_type, objectui#9256), and the manifest reports not-a-container.
  • icon is refused by name. It is an ADR-0087 D2 tombstone on PageHeaderProps.
  • actions can't stand in for the buttons. It holds action IDS, and those resolve against the object bound to the page. A docs demo has no bound object, so ids render nothing and the renderer warns once per id. Inline action objects are refused by the contract (actions.0: expected string).

So the demo shows only title and subtitle, which is the shape the canonical node accepts and can render here. That is why it no longer carries "with actions" in its id. The page explains why the live demo has no actions and links to Slotted pages for the header action row.

Why properties, measured

  • The spec's page component shape refuses title/subtitle written beside type (unrecognized_keys).
  • objectui's safeValidateSchema judges page:header props only inside properties. Written beside type, even subTitle or icon goes through unchecked.
  • SchemaRenderer moves properties.* up onto the node before the renderer reads it, so both spellings render. Only the properties spelling is validated, so it is the one the page teaches.

Beyond the claim's file surface (declared, not silent)

The claim lists four paths. Three more files had to move, because my change turned two tests outside that list red:

  • packages/types/src/__tests__/page-actions-refusal-7926.test.ts: its LIT CONTROL scans guide/layout.md for a page-header JSON fence with actions. Its own comment says it is really looking for the action-id channel of page:header. It now looks for page:header with properties.actions. This is the same defect class as this card (the alias taught as the header's key), in a test that reads the guide.
  • examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx: NODE_CENSUS.button goes from 118 to 116, because the demo's two buttons left the catalog. This is the catalog-authored case the table's header permits, recorded in the table's own format.
  • .changeset/3906-page-header-canonical-docs.md: check-changeset-presence counts any file under a released package's src/, and the types test is one.

Tests

Final union at c79ae7497, run after the last commit:

  • pnpm exec vitest run over examples/schema-catalog/, every test the markdown-test-inputs ledger lists as reading content/docs/guide/layout.md or content/docs/**, the catalog-corpus walkers, and the three packages/layout page-header tests: Test Files 66 passed (66), Tests 3136 passed (3136).
  • Type-check, after building the closure: pnpm --filter @object-ui/example-schema-catalog type-check and pnpm --filter @object-ui/types type-check both exit 0. --listFilesOnly confirms all three changed tests are in those programs.
  • check-doc-snippet-types: exit 0, "Every covered documentation snippet compiles against the built types." check-doc-example-types: exit 0.
  • check-doc-component-types: exit 0, "Every documented component type is registered."
  • check-doc-fence-languages, check-doc-example-ids ("414 real reference(s) all resolve"), check-doc-example-shared-reader and check-doc-links ("Links are valid across 17 scan roots."): all exit 0.
  • check-control-bytes and check-new-cross-file-line-citations ("0 new citation(s)"): exit 0.
  • check-changeset-presence (empty frontmatter, "a complete answer to this gate"), check-changeset-no-major, check-changeset-claims, check-pending-changeset-literals, check-changeset-fixed: all exit 0.
  • regenerate-catalog-index.py --check: exit 0.
  • eslint --no-inline-config on the four changed code files: 0 errors, 0 warnings. The .md, .mdx, .json and changeset files match no eslint configuration, so they are not linted. The config has no type-aware linting (no projectService or parserOptions), so this diff cannot change the verdict on any untouched file.
  • Reverse check (one-off, nothing left in the tree), run with ablation-replace.mjs:
    • The mutation re-keyed the demo's "type": "page:header" back to "type": "page-header". The anchor went from 1 hit to 0 and the blob changed.
    • pageheader-with-actions.test.tsx turned red: 7 failed, 16 passed. The failures were the shape, render, styling and mirror pins, plus three controls built on the demo.
    • Restore: blob equals HEAD and git diff HEAD is empty.
    • Two demo pins do not look at type and stayed green: the spec row's full parse and the declared-inputs check.

Acceptance notes (observations, not filed)

  • Stale comments on the alias's icon input. Two comments give "the docs page publishes the prop, and the docs page's only live demo writes icon: users" as reasons to keep that input: one in packages/layout/src/index.ts, one in packages/layout/src/__tests__/page-header-authorable-keys.test.tsx. After this PR both clauses are false. The objectui#3829 ruling itself still holds, because the alias renderer really draws the icon. The same test file also says the manifest pins run on "the real demo JSON"; they now run on an inline fixture that is that former demo JSON word for word. Nothing reads these comments. Carrier: whoever next touches the alias registration.
  • Stale reason text in the version ledger. The doc-version-claims row for guide/layout.md quotes the icon tombstone as "(feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set #6946, ADR-0087 D2)". The guide now quotes the installed spec's message exactly, "(ADR-0087 D2)". The ledger's claim key, @objectstack/spec 17.0.0, still matches, so the test is green; only the row's reason text is out of date.
  • sdui-parser's validateTree has no concept of the properties bag, so a page:header node spelled the spec's way gets unknown-prop "properties" there. The JSX tier writes attributes, so no author hits this today. The demo test validates the node in its post-move shape and says why.
  • objectui's safeValidateSchema refuses page-header and layout:page-header at type (invalid_union), exactly as it refuses an unknown type, so the old demo never validated there. Retiring the alias is not this card.

Out-of-scope findings (for the seat to file; not filed here)

  • (c) content/docs/guide/slotted-pages.md, "Example: customize only the header", writes eyebrow: 'ACCOUNT' and icon: 'building-2' under page:header properties. objectui's safeValidateSchema rejects that exact node: properties.icon: invalid_type and properties: unrecognized_keys [eyebrow]. The same node without those two keys validates.
  • (b) PageHeaderProps.breadcrumb is described as "Show breadcrumb" and defaults to true. PageHeaderRenderer renders only an empty element marked data-page-breadcrumb-slot and draws no trail. Measured through a real SchemaRenderer render, in both the bare and record layouts.

Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec


Generated by Claude Code

…ode (objectui#3906)

Phase 2 of the maintainer ruling on objectui#3906, direction (b): the page's
author face becomes the canonical `page:header` node, rendered by
`PageHeaderRenderer` in `@object-ui/components` and contracted by the spec's
`PageHeaderProps`.

- content/docs/layout/page-header.mdx: rewritten against the canonical
  renderer and PageHeaderProps; props, layouts, styling, accessibility and
  responsive behaviour re-measured. The `page-header` alias is named once, as
  an alias.
- content/docs/guide/layout.md: the PageHeader section moves to the same key.
- The catalog demo is re-keyed to `page:header` with its props in
  `properties`. It carries no actions or children: the canonical node draws
  no children, and its action ids resolve against a bound object that a docs
  demo does not have. Renamed to layout-page-header/basic-page-header
  accordingly; index regenerated.
- The demo's test re-derives shape, render, contract and declaration pins
  for the canonical node; the alias-registration pins stay, on an inline
  fixture holding the former demo JSON.
- page-actions-refusal-7926 LIT CONTROL now looks for the action-id channel
  where the guide authors it (page:header, properties.actions).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
The only file under a released package the change touches is a test in
@object-ui/types; an empty-frontmatter changeset declares it.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…er demo (objectui#3906)

The PageHeader docs demo no longer authors two `button` children: the
canonical `page:header` node draws none and the contract refuses the key on
it. The dom-leak census counts catalog button nodes, so it moves 118 -> 116,
the catalog-authored case its header sanctions.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…(objectui#3906)

A `page` node defaults to `pageType: record`, whose header always owns the
heading, so the example could not show the delegation it describes. With
`pageType: app` the titled `page:header` is what drops the page's own `<h1>`
(measured: one `<h1>` with the header, the page's own without it).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: types examples tests labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 3 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6872-app-shell-branding-title-jsdoc.md

  • names content/docs/guide/layout.md → content/docs/guide/layout.md — edited by this change

    Correct the AppShellBranding.title doc comment. It read "Page title suffix (sets document.title)" while useAppShellBranding assigns document.title = title wholesale — nothing is appended; the caller composes the whole string (the console passes "App label — Product name"). That comment ships in dist/index.d.ts and is the only description a consumer sees on editor hover, so a reader who followed it passed a suffix-only fragment and got a truncated title with no error. The comment now carries the same wording as content/docs/layout/app-shell.mdx, and agrees with the AppShellProps tables in the package README and content/docs/guide/layout.md. No runtime behaviour changes; the wholesale assignment and the four-surface agreement are now pinned by tests.

.changeset/7926-page-node-refuses-actions.md

  • names content/docs/guide/layout.md → content/docs/guide/layout.md — edited by this change

    Scope. One key, by name; the node is NOT strict. A census over this tree read 91 authored page-tagged objects with a blind-spot reading of 8 unreadable sites, and found only actions (3 sites, all in content/docs/guide/layout.md) and breadcrumbs (1 site, its own question, untouched) surviving passthrough on a real page node — every other undeclared key belongs to a different declaration that merely spells type: 'page'. PageNodeSchema still passes unknown renderer props through.

.changeset/8871-page-node-refuses-breadcrumbs.md

  • names content/docs/guide/layout.md → content/docs/guide/layout.md — edited by this change

    Three author sites, all teaching passages in content/docs/guide/layout.md, and that count corrects the actions refusal's "1 site": its census reads every git-tracked JSON file, every json fence in .md/.mdx, and every TS/TSX object literal via the TypeScript AST (PR fix(types,docs): refuse actions by name on the page node, teach the shape that draws (objectui#7926) #8870), and it undercounted for two different reasons. The Schema API block declared breadcrumbs?: ArrayANGLE-BRACKETS({ label, href }) outright and its literal does carry type: 'page', but that literal sits inside a markdown typescript fence — a fence language the census's json-fence reader never visits, so it was never read at all. Best Practices §2 authored it on a fragment inside a json fence the census does read, but that fragment never writes type, so a page-tagged filter correctly excluded it. No example app, catalog fixture, template or customer document writes the key, so the refusal strands no authored document in this tree.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with a8c550938 (merge-base with origin/main): 8 file(s) changed outside .changeset/, read against 1782 pending declaration(s) that publish a body (2388 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3114.4 KB 3149.4 KB
Main entry chunk (gzip) 149.5 KB 350 KB
Entry file index-B7ueCIFa.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 559.68KB 134.21KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.99KB 63.22KB
fields (index.js) 261.81KB 66.54KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.02KB 44.01KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 229.83KB 63.15KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.30KB 6.99KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c79ae7497bb575a6cf12c32d4d815ff51b82d232
Local-runs: none

Inputs: card #3906 (body and all 9 comments, including ruling 5230005161, claim 5901738818 and dev report 5902224540), PR #11162 (body, 9-file list, net diff), and the 43 check-runs on the head (filter=latest): 40 success, 3 skipped (dependabot and the two coverage placeholders); nothing pending, nothing red. Sources read at the head for the claims: packages/components/src/renderers/layout/containers.tsx, packages/components/src/custom/RecordTitleChip.tsx, packages/components/src/renderers/layout/page.tsx, packages/react/src/SchemaRenderer.tsx, packages/react/src/utils/inlineAria.ts, packages/types/src/zod/public-blocks.zod.ts and index.zod.ts, packages/sdui-parser/src/validate.ts, packages/layout/src/index.ts and PageHeader.tsx, the two packages/layout pin tests, the catalog corpus tests, the gate scripts, and the installed spec @objectstack/spec@17.4.0 (the lockfile's resolution) — src/ui/component.zod.ts (PageHeaderProps), src/ui/page.zod.ts (PageComponentSchema), src/ui/i18n.zod.ts (I18nLabelSchema, AriaPropsSchema).

① Derived judgments

Accept set and public surface: unchanged — right. No schema, prop, type, export or registration moves. The page-header / layout:page-header registration in packages/layout/src/index.ts is untouched and still declares title, subtitle, icon, actions, the children slot and isContainer: true. The only file under a released package's src/ is a test (@object-ui/types), whose change is a scan control, not an assertion on behaviour. The catalog is an example app, not a published surface.

The rewritten page and the guide's section, claim by claim against the head. Every number and claim is borne out:

  • Renderer and contract naming: ComponentRegistry.register('header', PageHeaderRenderer, { namespace: 'page', skipFallback: true }) at containers.tsx:2395; ComponentPropsMap['page:header'] is PageHeaderProps (spec component.zod.ts:2933), a strictObject with surface: 'this page:header'.
  • Prop table: title and subtitle are I18nLabelSchema.optional() (a string or an inline locale map), title's describe says omit it to derive from the record; actions is z.array(z.string()).optional(); breadcrumb, recordChrome, showStar, showCopyId are z.boolean().default(true) (the page's spec defaults); maxVisible / mobileMaxVisible are z.number().int().positive().optional() with no schema default (the page's renderer defaults 3 and 1 match readMax(...) ?? 3 / ?? 1 at containers.tsx:1955-1957, and readMax falls back for a contract-rejected value); aria is AriaPropsSchema with exactly ariaLabel, ariaDescribedBy, role. description is a PageHeaderProps alias pointing at subtitle (refused, named). icon is retiredKey(...) and the guide's quotation matches the installed message verbatim: "page:header property icon was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — no renderer ever read it … Delete the key."
  • Styling, bare layout: root header element cn('flex flex-col gap-2 pb-4 border-b', className) (:2375), title row flex items-center justify-between gap-4 (:2382), h1 text-2xl font-semibold tracking-tight (:2385), subtitle p text-sm text-muted-foreground (:2387), no breakpoint variant. Record chip: root flex flex-col sm:flex-row sm:flex-wrap items-start sm:items-center justify-between gap-3 sm:gap-4 pb-4 border-b (:2337), title column flex flex-col min-w-0 sm:min-w-48 flex-1 (:2342), h1 text-xl sm:text-2xl font-bold truncate (RecordTitleChip.tsx:108), subtitle text-sm text-muted-foreground mt-1 (:2362). The layout choice is hasRecord = !!(ctx?.data && ctx.objectSchema) and not recordChrome: false (:2177-2178), as the page's Layout section states.
  • Breadcrumb: both layouts render only an empty div[data-page-breadcrumb-slot] (:2344-2347, :2380); nothing fills it — the page says so. Without a record and with no title, the bare layout draws no h1 ({explicitTitle && ...} at :2384).
  • Actions: ids resolved through resolveDeclaredActionIds and useMetadataItem('object', ...); with no bound object needsActionLookup is false and each id goes to warnUnresolvedHeaderActionId(id, undefined, 'no-object', ...), which is a warn-once ledger keyed on object, id and reason (:1363-1372) — "renders none of them and says so once per id" is exact. Toolbar role="toolbar" labelled detail.pageHeaderActions / "Page header actions" (:2033-2034); overflow trigger labelled "More actions" (:2044). Overflow splits at maxVisible (:1970-1975).
  • Children: no read of schema.children, body or renderChildren anywhere in PageHeaderRenderer (:1423-2392); the mirror's PageHeaderBlockSchema tombstones both body and children (public-blocks.zod.ts:195-202), so "refuses children on this node by name" is right.
  • The properties teaching (the dev's claim): true at the head. PageComponentSchema (page.zod.ts:203) is a strictObject whose node-level keys are type, id, label, properties, events, style, className, responsiveStyles, visibleWhen, visibility, dataSource and the visibility options — no title / subtitle, so those beside type are unrecognized_keys, and properties is an open record there. objectui's mirror judges properties by the spec row (propsBag('page:header', stripImportedDefaults(SpecPageHeaderProps))) on a BaseSchema.extend (passthrough) node, so a top-level title passes through unchecked. SchemaRenderer hoists properties.* onto the node (skipping type / id) before the renderer reads schema?.title ?? schema?.properties?.title. className is declared on the node by PageComponentSchema and refused inside the strict PageHeaderProps — the page's "one exception" holds. The near-miss suggestion the page attributes to subTitle is the installed spec's own strictObject behaviour (its unrecognized-key path carries "Did you mean" suggestions).
  • The page-level h1 claim: page.tsx:530 defaults pageType to record; headerOwnsTitle = pageType === 'record' || pageHeaderOwnsTitle(schema) (:660-662), and pageHeaderOwnsTitle walks regions[].components and children (recursing into both, depth-bounded) for a page:header whose title has literal text (:104-190). The page states exactly that.
  • Accessibility: resolveInlineAriaProps writes aria-label, aria-describedby and role only when authored and adds no default role (inlineAria.ts:60-71) — "the header adds no role of its own" holds; this paragraph was already canonical in Phase 1 (finding(components,plugin-list,plugin-detail): the spec's nested aria bag has one shared reader now; the page root still drops it, and two blocks keep their own copies #11083).
  • One wording imprecision, not a false claim: the guide's "className is the one key that belongs on the node itself, beside type" is true of the header's own keys (the sentence's scope) but PageComponentSchema also admits id, label, style, events, visibleWhen, dataSource on the node. Nothing an author copies from the block is refused. Carrier: whoever next edits that section; not a blocking finding.

Demo shape and rename. basic-page-header.json is { type: 'page:header', properties: { title, subtitle } } — only keys the canonical node accepts, in the one position they are judged. The old pageheader-with-actions example id is gone from the catalog (index.ts import and registry entry both moved, index regenerated and regenerate-catalog-index --check green per the report). Every reference: Doc Example Id Check is green on the head ("414 real reference(s) all resolve" per the report), all eight test shards are green (so no test reads the old id), and the eight catalog corpus tests I read name neither id. The only strings still spelling pageheader-with-actions at the head are the TEST FILE's path — kept on purpose and cited from page-header.mdx:164 and page-header-authorable-keys.test.tsx:287 — and a comment in packages/layout/src/index.ts:88-89 that names the deleted example id as "the docs page's only live demo" (stale prose, nothing reads it; declared by the dev, see ③).

Re-derived pins. SHAPE (root page:header, keys exactly properties and type, the objectui#3787 guard re-derived against border-b pb-4 font-semibold tracking-tight text-2xl text-muted-foreground), RENDER (real SchemaRenderer, HEADER root, h1 text, subtitle, zero buttons), STYLING (bare-layout classes as the page states, no responsive variant), CONTRACT (PageComponentSchema full parse, the spec row full parse, safeValidateSchema, each with a control asserting code and path: icon: invalid_type, properties: unrecognized_keys, : unrecognized_keys for a top-level title, children: invalid_type), DECLARATION (manifest on the hoisted shape, with unknown-prop and not-a-container controls). The alias pins (#3900 / #3972 / #3987 / #11044) moved onto ALIAS_FIXTURE, which is the former demo JSON verbatim, so they measure what they measured. Dropped: the alias RENDER pin and the #3786 alias-styling pin. Still-live alias behaviour: children render on the alias is pinned in packages/layout/src/__tests__/containment-declared-slot-9910.test.tsx:112-113; the alias's title/subtitle render is pinned in page-header-authorable-keys.test.tsx:390-403. What is now unpinned anywhere is the alias renderer's spacing numbers (gap-3 pb-4 border-b, PageHeader.tsx:217). That pin existed to hold the docs page's Container bullets to the code (#3786); the page no longer states those numbers, so nothing documented is left unguarded. Acceptable; named so the seat sees it.

Nothing copied. The old page's numbers (gap-3, gap-x-4 gap-y-2, font-bold, text-3xl on desktop, the icon chip, showBack, the action → children → actions precedence, the page-header-subtitle-alias conversion narrative) are all gone; every retained number traces to containers.tsx or RecordTitleChip.tsx above. The kept "Gating a header action on a relation field" section is canonical (its quoted warning is the page:header action "…" visible label containers.tsx passes). The alias is named once on the page (its own section) and once in the guide (one blockquote), as an alias, registration intact.

② Semver level

Clause-②: no — right. The diff publishes nothing: two docs pages, a catalog example and its pins, a catalog census number, and one released-package TEST file's scan control. check-changeset-presence counts every file under a released package's src/ and states "No carve-out for test files under src/ … answered by the empty-frontmatter exemption" (scripts/check-changeset-presence.mjs:162-163), so .changeset/3906-page-header-canonical-docs.md with empty frontmatter is the gate's own prescribed answer, not a workaround. Changeset Declaration, Bump Policy, Fixed Group, Claim Re-read and Overwrite Report are all green on the head. No released behaviour changes: page-header stays registered and unchanged, page:header's renderer, registration and mirror arm are untouched.

③ Boundary flags

open_questions[0] — file-surface breach of three paths. Recommend the seat RATIFY (option A), with C at its discretion. Each is forced by the docs change, minimal, and not a weakened assertion:

  • packages/types/src/__tests__/page-actions-refusal-7926.test.ts: the LIT CONTROL scanned guide/layout.md for a page-header fence with a top-level actions array, which the guide no longer contains. It now looks for page:header with properties.actions — the same reachability control on the same channel its own comment names ("page:header's actions is the READ action-id channel"), still requiring a header fence with an array. Same defect class as the card (the alias read as the header's key), no tolerance added; the census assertions on page nodes are untouched. The ledger scripts/markdown-test-inputs.mjs:516 already lists this test as a reader of guide/layout.md, so it was owed.
  • examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx: NODE_CENSUS.button 118 → 116 because the demo's two type: 'button' children left the catalog. The table's header sanctions exactly this ("These move when the CATALOG is authored, not when a renderer changes"); the entry is written in the table's own comment format beside the finding(types,examples): every toast demo hangs an action object off onClick, which is declared as a function and read by no dispatcher #6250 and finding(examples,docs): the four alert-dialog schema-catalog fixtures author an actions array no surface carries — the docs page's own examples render an empty footer #7693 entries; no renderer touched.
  • The empty changeset: forced by the presence gate as in ②.
    One thing the dev did not measure: whether another open claim holds either test file (condition 3 of the in-place-fix exemption). The seat can settle that with one board read; nothing in the card's thread names a competing claim.

Deviations, each:

  • Demo renamed pageheader-with-actions → basic-page-header: appropriate. The canonical node draws no children, refuses icon, and its actions are ids that resolve only against a bound object (measured above), so a docs demo "with actions" would render none; the seat's order asked for a demo carrying only keys the canonical node accepts, and this is that demo. The test file keeps its path because packages/layout cites it by path — correct.
  • "No validator diagnostic" scoped: honest and declared. SDUI_BASE_PROPS (validate.ts:137-160) has no properties entry, so validateTree reports unknown-prop "properties" on the spec-spelled authored node; the pin validates the hoisted shape (the one SchemaRenderer hands the renderer) and says so in its describe header. The three validators that know the bag all pass the authored JSON with controls.
  • Alias pins kept on ALIAS_FIXTURE: right, and required — retiring the alias is not this card.

Out-of-scope notes, each checked at the head:

  • (c) content/docs/guide/slotted-pages.md:104-111 — real, pre-existing (untouched by this PR). The "customize only the header" example writes eyebrow: 'ACCOUNT' and icon: 'building-2' under page:header properties; PageHeaderProps declares no eyebrow and tombstones icon, so the spec row refuses that node. The fence types properties as an open record, so no snippet gate catches it. For the seat to file.
  • (b) PageHeaderProps.breadcrumb — real, pre-existing. Described "Show breadcrumb", default true; both layouts render only an empty slot div and no code outside containers.tsx fills it. A spec-to-renderer seam; for the seat to file.
  • Stale alias-icon comments — real, made stale BY this PR, comment-only: packages/layout/src/index.ts:87-89 ("the docs page publishes the prop, and the docs page's only live demo (layout-page-header/pageheader-with-actions) writes icon: users") and page-header-authorable-keys.test.tsx:131, :144, :284-288 (the "real demo JSON" pointer now means ALIAS_FIXTURE). The objectui#3829 ruling stands because PageHeader.tsx:233 really draws the icon. Both files are released-package source outside the claim; leaving them was right. Carrier: the next alias-registration touch.
  • doc-version-claims why-text — real, made stale BY this PR, cosmetic. The row's key content/docs/guide/layout.md :: @objectstack/spec 17.0.0 still matches the guide (the quote keeps "removed in @objectstack/spec 17.0.0"), so the ratchet is green; only the row's why quotes the old "(feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set #6946, ADR-0087 D2)" wording. The guide now matches the installed 17.4.0 message exactly, which is an improvement. Carrier: the next ledger touch.
  • sdui-parser and the properties bag — real, pre-existing (SDUI_BASE_PROPS above). The JSX tier writes attributes, so no author hits it today.
  • safeValidateSchema refusing page-header at type — real, pre-existing: no mirror arm declares the literal, and AnyComponentSchema is a discriminated union, so the alias is invalid_union like any unknown type. Belongs to the alias-retirement follow-up, not here.

Also for the seat, not a finding: the PR is still draft: true and mergeable_state: behind; the guide hunk draft PR #11069 edits (near :179) is outside this diff (which starts at :254), so whichever lands second merges main.

Implemented-by: claude/issue-3906-page-header-phase-2
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 01:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit ce26f40 Sep 30, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-3906-page-header-phase-2 branch September 30, 2026 01:59
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 examples package: types tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(layout): page-header 文档页把 legacy alias 当作者面来教 —— canonical key 是 page:header,页面定位需裁定

2 participants