Repository navigation
docs(layout): name page:header as the canonical author key on the page-header docs (Phase 1) - #5922
Merged
Merged
Conversation
…e-header docs
The layout `page-header` docs page taught the legacy alias as the author face:
its only live demo writes `"type": "page-header"`, and nothing on the page said
that the contract's key is `page:header`. The layout guide's PageHeader section
did the same in two snippets.
Phase 1 of the 2026-08-09 maintainer ruling (direction (b), phased): a banner
that tells the truth about the key's status now, so an AI author copying from
the page copies the canonical spelling, ahead of the full-page rewrite that is
serialized behind objectui#3789.
Measured against the installed @objectstack/spec, with controls:
- ComponentPropsMap has `page:header` (row -> PageHeaderProps), and neither
`page-header` nor `layout:page-header`; control `page:tabs` = present.
- PageComponentType includes `page:header`, not `page-header`; control
`page:footer` = present.
- PageHeaderProps.safeParse rejects `icon` by name (retiredKey) and
`description` as an unrecognized key of "this `page:header`", with a
nonsense key as the negative control.
- The alias is STILL ACCEPTED, not tombstoned and not dropped:
registerLayout() registers `page-header` (namespace `layout`, no
skipFallback, so the bare key too), and it is what the page's demo renders.
No schema, registry or renderer change; the demo and the page's measured
Styling/Props stay on the alias's renderer, which is Phase 2's job.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
os-zhuang
marked this pull request as ready for review
August 24, 2026 01:57
This was referenced Aug 24, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Oct 7, 2026
…ode (objectui#3906) (objectstack-ai#11162) Fixes objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 "(objectstack-ai#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](https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #3906 — Phase 1 of the 2026-08-09 maintainer ruling (direction (b), phased). The reference is deliberately non-closing: the ruling keeps this card open for Phase 2 (the full-page rewrite to the canonical renderer, serialized behind #3789) and says the card is re-marked
pm:blocked/Blocked-by: #3789once Phase 1 lands. A closing keyword here would silently retire the card Phase 2 needs. My dispatch word asked for the closing spelling instead; the divergence is flagged to the PM seat in the report.What was wrong
The layout
page-headerdocs page taught the legacy alias as the author face. Its only live demo —examples/schema-catalog/src/schemas/layout-page-header/pageheader-with-actions.json— opens with"type": "page-header", and no line on the page said what the contract's key is.content/docs/guide/layout.mddid the same in its PageHeader section (ajsonUsage snippet and atypescriptSchema API block, both keyedpage-header).Premise re-verified on today's
origin/main(0fce2ef81) before writing: still true, nothing had corrected it.Measurement 1 — the canonical key, read from the SCHEMA
Read from the installed
@objectstack/spec(not from another doc page), each line with a control:Third, independent statement:
PageHeaderPropsis declaredstrictObject({ surface: 'thispage:header', ... })(src/ui/component.zod.ts:393), so the shape names itself — and its rejection messages quote that name back, as shown below.ComponentPropsMap['page:header'] = PageHeaderPropsis atsrc/ui/component.zod.ts:2428;'page:header'sits inPageComponentTypeatsrc/ui/page.zod.ts:56.Measurement 2 — what the alias actually does today
State: still accepted. Not tombstoned, not silently dropped.
packages/layout/src/index.ts:121—ComponentRegistry.register('page-header', PageHeader, { namespace: 'layout', ... }), with noskipFallback, andRegistry.ts:219registers the bare key alongsidelayout:page-headerin exactly that case. So apage-headernode renders today, and it is what this page's demo renders.page:headercomes fromComponentRegistry.register('header', PageHeaderRenderer, { namespace: 'page', skipFallback: true })inpackages/components/src/renderers/layout/containers.tsx:1744— the record-aware one.typethroughComponentPropsMap;page-headerhas no row, andPageComponentSchema.propertiesis an openz.record(z.string(), z.unknown())bag (src/ui/page.zod.ts). So a misspelled or retired key written under the alias is neither rejected nor reported.Note on method:
BaseSchemabeing.passthrough()makes "the alias survivessafeParse" a phantom assertion — true of any key — so the readings above are shape membership plus the parser's own named verdicts:The nonsense key is the negative control that makes the first three readings mean something.
Docs-surface sweep, with a control
Scope searched: the whole repo minus
node_modules/.git/dist, for the alias written as an author key —Control (same command shape, a string I had read with my own eyes in
pageheader-with-actions.json):"type": "button"→ 181 hits repo-wide, including the two in that file. The search is not broken.Hits, and the disposition of each:
content/docs/layout/page-header.mdx(the page itself; alias taught via the demo)content/docs/guide/layout.md:235(jsonUsage) and:251(typescriptSchema API)### Usageexamples/schema-catalog/src/schemas/layout-page-header/pageheader-with-actions.json:2examples/schema-catalog/test/pageheader-with-actions.test.tsx(3 sites)packages/layout/CHANGELOG.md:108content/docs/guide/slotted-pages.mdalready teachespage:headercorrectly and is left alone.Before / after for every instance changed
1.
content/docs/layout/page-header.mdx— before: the page opened straight intoThe PageHeader component provides a consistent header…, with the first mention ofpage:header39 lines down and framed as a prop discussion. After: a banner immediately under the frontmatter that names the canonical key, states the alias's measured state in the three-way vocabulary (accepted / tombstoned / dropped → accepted), says what the alias costs an author (no props dispatch), and points at Slotted pages for the canonical node. No existing alias mention was deleted — the alias still renders, and authors with stored documents need to know where they stand.2.
content/docs/guide/layout.md— before:## PageHeader Component→ intro →### Usagewith"type": "page-header", no statement of the canonical key anywhere in the section. After: the same truth in one blockquote between the intro and### Usage, so it sits above the snippet a reader copies. The snippets themselves are unchanged — they document the layout component, whose props (icon,showBack,children) are not the canonical node's.Neither edit touches the schema, the registry or a renderer: the page was what was wrong.
Verification
Gate union re-run after the final commit, at
1086a1215. Exit codes captured by redirect-then-capture (never across a pipe); verdict lines quoted as each gate printed them.check-doc-links.mjscheck-doc-component-types.mjscheck-changeset-presence.mjscheck-changeset-no-major.mjscheck-control-bytes.mjscheck-skills-paths.mjsNo ablation. This is a docs-only diff — there is no guard to delete and re-measure. Saying so plainly rather than staging a theatrical one. The substitute evidence is Measurements 1 and 2 above, each with its control.
check-doc-snippet-types.mjs— NOT RUN, and that is a precondition, not a verdict. It exited 1 with:naming 15 unbuilt packages (
packages/types,packages/core,packages/components, …). That state is identical with and without this diff — a markdown edit cannot build or unbuild a package. Reported as not-run rather than claimed green. What makes it a safe not-run for this diff specifically:content/docs/layout/page-header.mdxis covered by the gate (not inUNGATED_DOCS), but it contains 0ts/tsxfences — control:content/docs/guide/slotted-pages.mdhas 4 andcontent/docs/guide/architecture.mdhas 9, counted by the same command. The gate compiles nothing from it either way.content/docs/guide/layout.mdis in the gate'sUNGATED_DOCSdebt list, so its green would have said nothing about that page regardless.Lint narrowing, declared with its three pieces of evidence. The repo-wide
pnpm lint(turbo run lint) was not run; the narrowing is a measurement, not a skip:npx eslint --no-inline-config --format jsonon both changed files reports, for each,File ignored because no matching configuration was supplied.--format json: 2 results,errorCount: 0, and the only warning on each is that ignore notice — i.e. 0 linted files in this diff..md/.mdxfiles (git diff --name-only), no source, no config, no tsconfig. eslint applies no configuration to those extensions, so no rule's verdict on any untouched file can move.Control bytes, self-scanned beyond the gate: a control-character grep over both changed files returns no match (exit 1). Positive control: the same command over a file holding one
\x01byte matches (exit 0).Not touched, deliberately
⛔ No schema, registry or renderer change. ⛔ Nothing under
packages/. ⛔ The demo JSON stays on the alias — re-keying it is Phase 2 work and would invalidate the page's measured Styling section, which was taken against this renderer. ⛔ Not marked ready, no auto-merge.Generated by Claude Code