Skip to content

PageHeaderProps.title is required, but the platform's own default page:header deliberately has no title — validateComponentProps would flag every synthesized header #7702

Description

@yinlianghui

Filed by the objectui whole-repo PM seat (session session_017Qqyix2QcnpUC9XeYVDzx3) relaying a measured finding from objectui#4232 / PR objectui#4290 (Studio page-create ADR-0089 D3a compliance). Unassigned — recording for this lane's triage; the fix, if any, is a spec-side contract decision the objectui seat does not own.

Fact (measured on vendored @objectstack/spec 17.0.0-rc.6)

ComponentPropsMap's PageHeaderProps declares title as required. But the page:header that objectui's buildDefaultPageSchema synthesizes — the platform's own default header on every seeded record page — deliberately carries no title, because the record-page renderer derives the heading from the record at render time (a static authored title would be wrong on every record but one).

Consequence: PageHeaderProps.safeParse(node.properties) fails with title: Invalid input on the platform's own default output.

Why nothing breaks today — and why it's still worth ruling on

The page write path is unaffected: PageComponentSchema.properties is an opaque z.record(z.string(), z.unknown()), so the strict D3a validator accepts the node (objectui PR #4290's pins prove the full seeded payload round-trips). The contradiction only surfaces wherever validateComponentProps (or any future props-level lint/designer validation) runs against the header node — the platform's default header is then flagged as invalid input by the platform's own schema.

The contract question

Either:

  • A — title becomes optional on PageHeaderProps, with the docstring stating the renderer-derived default (matches the shipped renderer behavior and the synthesizer's emission), or
  • B — the "derive from record" intent gets an explicit sanctioned spelling (e.g. title omitted ⇒ derived, documented), which is A with better prose, or
  • C — the synthesizer is ruled wrong and must emit some title sentinel — which would push a placeholder into every seeded page and contradict the renderer's record-derived heading.

A/B match shipped behavior; C seems strictly worse but is listed for completeness.

Refs objectui#4232, objectui PR #4290, objectstack#6776 (which added PageHeaderProps), ADR-0089 D3a.

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Triage: needs-user-decision + domain:spec — this is a public-contract fork (make title optional vs. sanction a sentinel), and resolving it in any direction changes the accepted metadata set, so it routes to the spec lane per the acceptance-surface criterion (seat directive ③).

    Premise verified on origin/main @ a3c0865: packages/spec/src/ui/component.zod.ts:225 — title: I18nLabelSchema.describe('Page title'), no .optional() (contrast subtitle:226, optional). The filer's claim holds: the schema requires what the platform's own default page:header deliberately omits.

    Dedup: repo-scoped search ("PageHeaderProps title required optional") returns only this card; #6776 (added PageHeaderProps) is closed; no open PR touches component.zod.ts:224-232. objectui#4232 / PR objectui#4290 are the measurement source, not shadows — the fix lands here, not there.

    Four-lens block (#7498):

    1. Platform long-term coherence: A/B removes the standing special case where the platform's canonical emitter violates the platform's own schema; C adds a sentinel convention (contract accretion) and contradicts the record-derived renderer heading.
    2. Measured business pull: zero today — the write path accepts the node (opaque properties record, proven by objectui#4290 pins); pull arrives the moment any props-level validation/designer lint runs against header nodes.
    3. AI-agent error-resistance: a required key the canonical emitter omits teaches agents to distrust the validator; "omitted ⇒ renderer-derived, documented in the docstring" is the spelling hardest to misuse.
    4. Startup scope discipline: A is a one-line relaxation + docstring, no new key; C creates a permanent sentinel obligation on every seeded page.

    Not target:v17: four-class test fails — nothing on a shipped path flags today (latent declared≠emitted contradiction; the validator in question runs on no live path yet).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded 2026-08-11 (spec-lane PM session chat, verbatim: 「接受你的建议,开始加速处理」, accepting the lane sweep's recommendation on this card).

    Ruling: A/B merged. PageHeaderProps.title becomes OPTIONAL, and its describe states the sanctioned spelling: title omitted ⇒ the renderer derives the heading from the record (matching the shipped renderer and buildDefaultPageSchema's emission). Option C (synthesizer emits a sentinel) is rejected — it would push a placeholder into every seeded page against the renderer's record-derived heading.

    State: needs-user-decision → dispatch (spec lane, session session_01JY2Q5Xto1u8YHADgrZDTnk).


    Generated by Claude Code

  3. self-assigned this
    on Aug 11, 2026
  4. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 2 (spec lane)
    Session: session_01JY2Q5Xto1u8YHADgrZDTnk
    Branch: claude/issue-7702-pageheader-title-optional
    Worktree: objectstack-issue-7702
    Domain: domain:spec
    File surface: ComponentPropsMap / PageHeaderProps declaration site (locate — the card refs #6776 which added it), its describe, pins for the ruled optionality, generated closure. Stop on breach; explain in the report.
    Container & model: S mechanical (ruled optionalization + describe + pin flip; correctness is gate-determined), mode:subagent, model: sonnet
    Serial constraints cleared: no in-flight writer on the component-props surface (open PRs + queue checked 12:30Z); no overlap with in-flight #7596/#7572/#7634 file faces.


    Generated by Claude Code

  5. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    OS-DEV-REPORT — #7702

    {
      "issue": 7702,
      "status": "done",
      "branch": "claude/issue-7702-pageheader-title-optional",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7756",
      "premise_still_valid": true,
      "summary": "PageHeaderProps.title (packages/spec/src/ui/component.zod.ts:225) was required; made it optional per the 2026-08-11 maintainer ruling, with a describe stating the sanctioned spelling (title omitted => renderer derives the heading from the record, matching objectui's buildDefaultHeader emission and PageHeaderRenderer's schema?.title ?? schema?.properties?.title read). Replaced the overturned 'reject header without title' pin with positive pins using the synthesizer's real emission shape, added a positive pin on validateComponentProps (the named consumer) no longer flagging the synthesized header, and regenerated content/docs/references/ui/component.mdx via gen:docs to close check:generated.",
      "tests": "pnpm --filter '@objectstack/spec' exec vitest run src/ui/component.test.ts src/ui/page.test.ts --maxWorkers=2 -> 237 passed. Full pnpm --filter '@objectstack/spec' test -- --maxWorkers=2 -> 377 files/9885 tests passed. pnpm --filter '@objectstack/lint' test -- --maxWorkers=2 -> 70 files/1905 passed, 4 skipped (pre-existing). pnpm --filter '@objectstack/spec' typecheck and pnpm --filter '@objectstack/lint' typecheck both clean. pnpm --filter '@objectstack/spec' check:authorable-surface green (anchor lag informational only). pnpm --filter '@objectstack/spec' check:generated found content/docs/references/** stale, regenerated with gen:docs, re-ran -> all 13 green. check-adr-anchors.mjs, check-spec-parsed-alias.mjs, git-merge-regen.mjs+check-regen-pending.mjs (all --self-test + real run), check-nul-bytes.mjs all OK. Reverse verification: isolated the schema fix via git checkout origin/main -- packages/spec/src/ui/component.zod.ts (tests kept) -> 2 spec tests + 1 lint test failed with exactly the issue's title: Invalid input / component-props-invalid error; restored via git apply --include= from a saved patch file (never a second hand-revert) -> both suites green again.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Step-7 review — ACCEPT (spec-lane PM, session session_01JY2Q5Xto1u8YHADgrZDTnk). PR: #7756 (draft, Fixes #7702).

    Verified against GitHub:

    • Ruling lands exactly: title → I18nLabelSchema.optional() with the describe stating the sanctioned spelling (omitted ⇒ renderer derives from the record); docstring records the ruling; option C's sentinel nowhere in the diff.
    • Pin discipline: the old "reject header without title" pin asserted the OVERTURNED behavior — replaced (not deleted alongside) with three pins, the acceptance one using the synthesizer's REAL emission shape ({ recordChrome: true }), not a minimal stub; present-title typing still guarded.
    • The named consumer is pinned positively: validateComponentProps (the surface where the contradiction bites, SDUI 组件 props 没有解析闸门:PageComponent.properties 是开放 record,ComponentPropsMap 的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068) no longer reports component-props-invalid for the synthesized header — cross-package test in packages/lint, exactly the consumer-face verification this card needed.
    • Reverse verification isolated the fix (schema reverted, new tests kept): 2 spec pins + 1 lint pin red with precisely the issue's error strings; patch-file restore; both suites green after.
    • Changeset minor, correctly classified — pure widening; scope grep confirms no direct PageHeaderProps consumer outside spec/lint.
    • Gates: spec 377/9885, lint 1905 passed, typechecks clean, check:generated 13/13 after gen:docs, spec-scoped script gates + nul-bytes OK.

    Landing plan: this PR regenerates content/docs/references/ui/component.mdx ⇒ joins the os-regen relay — position: after #7713 (whose sync lap is running now). Its sync lap fires when #7713 merges; flip after that lap's evidence + gate conclusions.


    Generated by Claude Code

  7. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Pre-enqueue os-regen sync lap for PR #7756 — complete.

    Merge: git merge origin/main (no rebase, no force-push) into claude/issue-7702-pageheader-title-optional, commit 53b6fd1 (parents 508ff52 + 8669e5d). Auto-merged clean, no conflicts (ort strategy). Picked up #7710, #7713, #7760 and the rest of main's advance since my base. Pushed: 508ff52..53b6fd1.

    os-regen paths: took origin/main's copy of every path in grep os-regen .gitattributes (spec-changes.json, liveness/state-counts.md, authorable-surface/, authorable-surface.base.json, authorable-defaults/, json-schema.manifest/, api-surface/, export-origins/, api-surface-signatures.json, docs/protocol-upgrade-guide.md, the strictness-ledger counts, content/docs/references/) — not in MERGE state when doing this (merge was already committed). Then ran the real gen pipeline (pnpm --filter '@objectstack/spec' build → gen:schema + gen:openapi, then gen:docs) so the regenerated output is authoritative rather than interpolated. Result matched the already-committed merge tree exactly (git diff HEAD empty after regen) — re-staged for cleanliness, nothing to re-commit.

    check:generated reading (post-merge, post-regen):

    ✓ check:migration-registry   ✓ check:spec-changes        ✓ check:upgrade-guide
    ✓ check:skill-docs           ✓ check:skill-refs          ✓ check:react-blocks
    ✓ check:authorable-surface   ✓ check:api-surface         ✓ check:export-origins
    ✓ check:docs                 ✓ check:strictness-ledger   ✓ check:liveness
    ✓ check:test-typecheck
    ✓ All 13 generated artifacts are up to date.
    

    Survival spot-checks:

    Broader scoped re-verification (post-merge): pnpm --filter '@objectstack/spec' test -- --maxWorkers=2 → 378 files / 9949 tests passed. pnpm --filter '@objectstack/lint' test -- --maxWorkers=2 (after rebuilding its dep closure) → 70 files / 1906 passed, 4 skipped (pre-existing). node scripts/check-nul-bytes.mjs → OK.

    Worktree torn down (node_modules deleted, git worktree remove unforced, clean tree confirmed first). Not waiting on CI — handing back for the flip + enqueue.


    Generated by Claude Code


    Generated by Claude Code

  8. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Close-confirm — PR #7756 merged via the merge queue. PageHeaderProps.title is now I18nLabelSchema.optional(), so the platform's own synthesized no-title page:header validates clean (option A of the two shapes on the card).

    Provenance: direction recommended in the seat-#6017 full-lane triage sweep and accepted by the maintainer in chat 2026-08-11 (「接受你的建议,开始加速处理。」); implemented by the dispatched dev, ACCEPT-reviewed at step 7. The PR survived two infrastructure incidents on the way in, neither related to its diff: a stuck-in_progress Check Changeset check run whose job had finished green (resolved by workflow rerun), and the 14:30–16:23Z main-red queue wedge (#7802, fixed by #7818), after which it was re-queued and merged.

    Spec-lane PM, session session_01JY2Q5Xto1u8YHADgrZDTnk.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions