Skip to content

feat(spec): page:header placedActions draws every placed header action, refused beside an actions list (#30018) - #30028

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-i7bk4t
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-i7bk4t

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #30018

维护者速读

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #29951 — this diff names the key in skills/objectstack-ui/rules/pages.md and in the docs, marked not drawn yet; no eval exercises it while no renderer reads it, so the eval and the qualifier drop ride with the reader.

What changes

  • PageHeaderProps.placedActions (packages/spec/src/ui/component.zod.ts): z.boolean().optional() beside actions, no default, so an unset key stays unset in the parsed output. true asks the header to draw every action of the page's object whose locations include record_header or record_more, read live from the object's metadata. Omitted or false, the actions list rules as today: an authored list is the whole set (PageHeaderProps.actions declares z.array(z.string()) but objectui's canonical page:header renderer consumes ActionDef objects — satisfying the schema deletes every header button #11592).

  • Refused together. The schema now ends in a .refine that refuses placedActions: true beside an authored actions list, an empty list included, at path placedActions. The message names both keys and tells the author to keep one: actions for a curated list that is the whole set, or placedActions: true for every placed action. The refine keeps the closed strictObject constructor, so unknown keys are still refused. @objectstack/lint validateComponentProps safe-parses ComponentPropsMap['page:header'], so the refusal reaches os validate / os build / os lint and the page save door as a component-props-invalid error. defineStack does not parse the properties bag, so nothing changes there.

  • Docblocks. The actions TSDoc and .describe() now name the opt-in and the exclusivity. The new key's describe opens with [PLANNED — not yet rendered].

  • Liveness, honest PLANNED. Component-level keys have no ledger row (the walk stops at the undrilled page/regions container), so the record is the describe marker plus a PLANNED RECORD (#30018 …) sentence in packages/spec/liveness/page.json _note, measured at main eee41490: PageHeaderRenderer in ui/packages/components/src/renderers/layout/containers.tsx reads no placedActions. The reader is console(record header): a url action placed at record_header on sys_user no longer renders in the 18.3 Console header menu, drawer or full page (18.2 rendered it) #29951's ui half (the renderer and the three platform record pages).

  • dropped-refinements.baseline.json gains ui/PageHeaderProps (1 site, root). The rule depends on the value true, which none of the projectable arms in shared/refinement-projection.ts states, so the published JSON Schema still accepts the pair and the parse refuses it. The row is the one the build printed.

  • Generated: packages/spec/authorable-surface/ui.json gains ui/PageHeaderProps:placedActions.

  • One line under ui/, as the seat ruled: UNPUBLISHED_EXEMPTIONS['page:header.placedActions'] in ui/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts, citing console(record header): a url action placed at record_header on sys_user no longer renders in the 18.3 Console header menu, drawer or full page (18.2 rendered it) #29951. The parity test's reverse direction reads the workspace spec dist and would otherwise refuse a spec key no page:header registration input publishes. console(record header): a url action placed at record_header on sys_user no longer renders in the 18.3 Console header menu, drawer or full page (18.2 rendered it) #29951's ui half must strike this entry when it declares the input (carries no stale unpublished-key exemption refuses it once the input is published).

  • Docs: content/docs/ui/pages.mdx (the actions row names the alternative; a new placedActions row) and content/docs/protocol/objectui/layout-dsl.mdx (one sentence), both saying the console renderer does not draw it yet.

  • Skills (Tier H, the only governed file): skills/objectstack-ui/rules/pages.md, the page:header catalogue row names placedActions: true (all placed; not drawn yet). The token ratchet has zero headroom on this file, so the row is paid for by deleting a redundant clause from the print-page paragraph ("and a full page with no regions"), which the condition just before it already states.

  • Changeset: @objectstack/spec minor, @objectstack/skills minor, an ordinary minor. Nothing ships in 18.3.x.

  • Generated reference: content/docs/references/ui/component.mdx regenerated by gen:docs (the new row and the rewritten actions describe), not hand-edited.

Qualifiers #29951 drops when the reader lands

The key is marked "not drawn yet" in six places, all true today and all false once the renderer reads it: the describe marker in component.zod.ts (the generated reference follows it), the page.json PLANNED sentence, the parity-test exemption, the pages.mdx row ("Declared ahead of the console's header renderer…"), the layout-dsl.mdx sentence, and pages.md ("not drawn yet").

Why

Ruling A on #29951 (record 6624172200): a record page's header should be able to say "every action placed on this object" without copying ids, so an action another package places at record_header / record_more shows up without a page edit. #11592 made an authored actions list the whole set, which stays the default; the opt-in is a separate, explicit key, and the two cannot be set together because they would be two different answers to the same question. This PR is the spec half only. Nothing ships in 18.3.x.

Tests

  • packages/spec/src/ui/component.test.ts, new describe "PageHeaderProps placedActions — every placed header action, or the authored list, never both", with the five cases the card asks for plus three:
    1. placedActions: true alone is accepted and kept.
    2. An actions list alone parses as before, and no placedActions key is materialized.
    3. placedActions: true beside ['convert_lead'] and beside [] is refused as exactly [{ code: 'custom', path: 'placedActions' }]; the message contains `placedActions: true`, `actions` and Keep one.
    4. placedActions: false beside an actions list is accepted.
    5. Absence leaves the key unset ({}, { title }, { title, actions }).
    6. A string 'true' is refused as invalid_type at placedActions.
    7. ComponentPropsMap['page:header'] is PageHeaderProps and refuses the same pair.
    8. The describe opens with the PLANNED marker.
  • packages/lint/src/validate-component-props.test.ts, new describe for spec(page:header): an explicit opt-in to draw every action placed at record_header / record_more, read live (#29951 ruling A, spec half) #30018: the pair gives one component-props-invalid error at pages[0].regions[0].components[0].properties.placedActions (both list forms), with where set to page "probe_page" · page:header and a message naming both keys; silent on true alone, a list alone, and false beside a list; through runAuthoringRules('validate', …) the finding is an error.

Acceptance notes

  • Spelling, no default, true/omitted/false meanings: as the card states. See What changes.
  • Refused together, message names both keys and says keep one; false beside actions accepted: tests 3 and 4, plus the lint door test.
  • actions docblock and describe rewritten to name the opt-in: yes.
  • Honest PLANNED liveness naming console(record header): a url action placed at record_header on sys_user no longer renders in the 18.3 Console header menu, drawer or full page (18.2 rendered it) #29951 as the reader: the describe marker and the page.json sentence.
  • One docs line and one skills line: pages.mdx and layout-dsl.mdx (docs), pages.md (skills).
  • Artifacts regenerated and committed: authorable-surface/ui.json, the dropped-refinements row; check:generated result in Verification.
  • Out of scope, untouched: the objectui reader and the three platform pages (sys_user_detail, sys_organization_detail, sys_approval_request_detail).
  • Other open PRs that edit PageHeaderProps: none. At head 371cb0fd, I fetched the head of each of the 19 open PRs and diffed it against its merge base with main. None touches any of this PR's 12 files, and none changes a PageHeaderProps or placedActions line in component.zod.ts.
  • Out-of-scope finding, not changed here: skills/objectui/guides/page-builder.md still lists the retired page:header key breadcrumb, so a skill teaches a key the closed shape refuses. I left it alone because that guide describes what renders and naming placedActions there would be untrue; the stale breadcrumb mention is worth its own card.

Verification

All runs went through scripts/pm/os-verify-lock.sh. The box was shared, so the timings are wall-clock on a shared machine.

  • Full build: pnpm build --concurrency=2, 119/119 tasks, exit 0.
  • Spec tests: pnpm --filter @objectstack/spec test, 811/811 files, 24378 passed plus 1 todo, exit 0.
    • The first run went red on one test: dropped-refinements.test.ts, "header totals match its body". The ledger's measured totals now count the new row (251 schemas, 1019 sites).
  • Spec typecheck: pnpm --filter @objectstack/spec typecheck, exit 0.
  • Repo pins: pnpm --filter @objectstack/spec test:repo, 90/90 files, 1539 tests, exit 0.
  • Generated artifacts: pnpm --filter @objectstack/spec check:generated, 14/14 artifacts, exit 0.
    • The first run flagged content/docs/references/** as stale, because the describe changed. gen:docs regenerated it, and that commit is in this PR.
  • Authorable surface: pnpm --filter @objectstack/spec check:authorable-surface, exit 0.
  • Lint door test: pnpm --filter @objectstack/lint exec vitest run src/validate-component-props.test.ts, 121 passed, exit 0.
  • ui parity test: registry-inputs-spec-parity, run through the console project against the built spec dist (which carries placedActions), 261 passed, exit 0.
  • Skills token ratchet: node scripts/check-skills-token-ratchet.mjs, exit 0. skills/objectstack-ui/rules/pages.md is 5669 tokens against a ceiling of 5669 (+0). The file has 427 lines before and after (22673 → 22674 bytes). The sum of all SKILL.md files is unchanged: 4536 lines and 226744 bytes. No SKILL.md was edited.
  • Changeset gate with this body: check-changeset-no-major --base origin/main --event exits 0. It reads Clause-②: yes (widening) and Metadata-protocol: added, finds no major, and grades no moved package patch.
    • Control: the same body with Metadata-protocol: none exits 1, because packages/spec/authorable-surface/ui.json moved.
  • dispatch-gates: node scripts/pm/dispatch-gates.mjs --commands printed 113 commands at head 371cb0fd. I ran each one byte for byte, and all 113 exited 0.
    • --ran reconciliation: "113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN".
  • Ablation, once: scripts/ablation-replace.mjs replaced the pair predicate in component.zod.ts with (header) => header !== null. The anchor went from 1 hit to 0, and the blob changed from 68041b88 to 00fa1529.
    • vitest run --project local src/ui/component.test.ts then went red: 2 failed and 359 passed. The two failures were the pair refusal and the ComponentPropsMap refusal. The other six tests in the describe stayed green.
    • Restored: the blob is back to 68041b88, matching HEAD, and git diff HEAD is empty.
Ablation readout
ablation-replace: anchor   x1 -> x0
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 68041b887e9b -> 00fa1529177a
     × refuses `placedActions: true` beside an authored `actions` list, an empty one included, at `placedActions`
     × is the row `ComponentPropsMap` judges `page:header` with, so the props door refuses the same pair
      Tests  2 failed | 359 passed (361)
ablation-replace: ok restored: blob == HEAD (68041b887e9b) and `git diff HEAD` is empty
Changeset gate readout (this body)
✓ This diff introduces no `major` bump.
✓ LEVEL AXIS: this PR declares clause-② `yes (widening)` and metadata-protocol `added`, and no package whose published source (`src/**`, a packed `bin` target, or a repo file its build copies in) it moves is graded `patch`.

Not run locally: the dogfood battery, because the title has no ! and there is no (narrowing) arm. The PR's check-runs carry everything else CI runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (The four changed TypeScript files pass eslint --no-inline-config with the root config, exit 0. CI's pnpm lint covers the full tree.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English, except the ## 维护者速读 block, which is written in Chinese for the maintainer.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzeWKWceaGdgGYf37di

…red actions list

placedActions: true draws every action of the bound object placed at
record_header / record_more, read live, in place of an authored actions
id list. The actions list keeps its meaning (an authored list is the
whole set). The pair is refused at placedActions; false beside a list is
accepted. Declared ahead of the header renderer: [PLANNED] describe
marker and a liveness/page.json _note record.

Claude-Session: https://claude.ai/code/session_01WGufzeWKWceaGdgGYf37di
Co-authored-by: Claude <noreply@anthropic.com>
…uthorable key

dropped-refinements.baseline.json gains ui/PageHeaderProps (root site):
the placedActions/actions rule is value-conditional, which no projected
refinement pattern publishes. authorable-surface/ui.json gains
ui/PageHeaderProps:placedActions (gen:schema).

Claude-Session: https://claude.ai/code/session_01WGufzeWKWceaGdgGYf37di
Co-authored-by: Claude <noreply@anthropic.com>
…ops row

The ledger's measured totals must equal its body: 251 schemas, 1019 sites.

Claude-Session: https://claude.ai/code/session_01WGufzeWKWceaGdgGYf37di
Co-authored-by: Claude <noreply@anthropic.com>
gen:docs output for the new PageHeaderProps key and the rewritten actions describe.

Claude-Session: https://claude.ai/code/session_01WGufzeWKWceaGdgGYf37di
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 4 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/dropped-refinements.baseline.json, packages/spec/liveness/page.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/objectui/layout-dsl.mdx (via placedActions (symbol, a field of const object PageHeaderProps, an authorable key of ui/PageHeaderProps), placedActions (literal, a string literal in PAGE_HEADER_PLACED_ACTIONS_BESIDE_LIST))
  • content/docs/ui/pages.mdx (via placedActions (symbol, a field of const object PageHeaderProps, an authorable key of ui/PageHeaderProps), placedActions (literal, a string literal in PAGE_HEADER_PLACED_ACTIONS_BESIDE_LIST))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-6.mdx (via PageHeaderProps (symbol, a top-level const object))

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.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/dropped-refinements.baseline.json, packages/spec/liveness/page.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 169 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 140d8ce805dc55245d9841a6ece6dfb956d9e071 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7f3e4eadbbd9f1cfa03a29d5de1868f2847e8144 — the merge of head 371cb0fd9d323a0cc3c3b536831c10fab7ec78b4 into base 140d8ce805dc55245d9841a6ece6dfb956d9e071, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7f3e4eadbbd9f1cfa03a29d5de1868f2847e8144 && git checkout 7f3e4eadbbd9f1cfa03a29d5de1868f2847e8144
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 140d8ce805dc55245d9841a6ece6dfb956d9e071 371cb0fd9d323a0cc3c3b536831c10fab7ec78b4 && git checkout -B drift-repro 140d8ce805dc55245d9841a6ece6dfb956d9e071 && git merge --no-ff 371cb0fd9d323a0cc3c3b536831c10fab7ec78b4

node scripts/docs-audit/affected-docs.mjs --json 140d8ce805dc55245d9841a6ece6dfb956d9e071

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 140d8ce805dc55245d9841a6ece6dfb956d9e071 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 371cb0fd9d323a0cc3c3b536831c10fab7ec78b4
Local-runs: none
Scope: packages/spec/src/ui/component.zod.ts, skills/objectstack-ui/rules/pages.md + packages/spec/src/shared/strict-object.ts, packages/spec/src/shared/refinement-projection.ts, packages/lint/src/validate-component-props.ts, packages/spec/scripts/liveness/check-liveness.mts, packages/spec/src/ui/page.zod.ts, ui/packages/components/src/renderers/layout/containers.tsx, skills/objectstack-ui/evals/views-apps-actions-pages.json, scripts/check-changeset-no-major.mjs

Inputs read: card #30018 (body, the claim 6624302286, triage 6624681862, os-dev-report 6626669313), the ruling 6624172200 on #29951, PR #30028 (body, 12-file list, net diff origin/main...371cb0fd, merge-base 63b86b38), and the check-runs on the head. The rest of the diff (changeset, docs, generated reference, authorable-surface/ui.json, dropped-refinements.baseline.json, liveness/page.json, the two test files, the one ui/ line) is the lane seat's ACCEPT at TIER_DEFAULT; it was read where the hits hunks make a claim about it, not reviewed.

① Derived judgments

  • Accept-set, ui/PageHeaderProps: widened by one key. placedActions: z.boolean().optional(), no default. Right: on main the closed shape (strictObject → closedObject(z.object(...).strict())) refuses the key as unknown, so nothing that parsed before is refused now. The pair placedActions: true + actions moves from an unrecognized_keys refusal (rule component-props-unknown-key) to a custom refusal at placedActions (rule component-props-invalid); still refused, a different door, not a narrowing. Clause-②: yes (widening) is the right reading.
  • placedActions: false beside a list is accepted. Right against the ruling: false asserts nothing the list does not, and refusing it would make the key a presence switch rather than a value switch.
  • No schema default. Right, and load-bearing: maxVisible in the same shape records the rule (an unset key must stay unset so a parsed header does not become an authored one); the renderer on main (PageHeaderRenderer, containers.tsx) reads no placedActions, so a materialised false would be noise in every stored page.
  • Public surface: authorable-surface/ui.json +1 row ui/PageHeaderProps:placedActions. Right; the generator enumerates .shape, which a zod 4 .refine() keeps (same-file precedent: ElementButtonPropsSchema is a strictObject(...).refine(...) chain, and the new describe-marker test reads .shape.placedActions).
  • Published JSON Schema ui/PageHeaderProps is wider than the runtime on the pair; ledger row ui/PageHeaderProps site "" added, totals 250→251 / 1018→1019. Right: the rule is value-conditional (true arms it). The closed list in shared/refinement-projection.ts has five arms (required-one-of, non-blank-string, dependent-required, banned-keys, banned-key-pattern); dependent-required is presence-only by its own docblock, and a presence-based "not both" would refuse false + list, which the runtime accepts, so no arm is exact. Dropped and annotated is the correct outcome, not a shortcut.
  • First consumer, @objectstack/lint validateComponentProps. It safe-parses ComponentPropsMap['page:header'] (the refined instance, pinned by the toBe(PageHeaderProps) test) and maps a non-unrecognized_keys, non-retired issue to component-props-invalid at ${base}.${issue.path}, so the finding lands at properties.placedActions with the schema message. The lint test pins exactly that. Right.
  • Liveness. No ledger row is possible (component-level keys sit under the undrilled page/regions container; _note on main says so for three earlier keys). The describe opens [PLANNED — not yet rendered], which check-liveness.mts MARKER_RE.planned (/\[planned|.../i) reads as planned; the _note PLANNED RECORD names the carrier (console(record header): a url action placed at record_header on sys_user no longer renders in the 18.3 Console header menu, drawer or full page (18.2 rendered it) #29951 ui half). The Spec property liveness check-run on the head is green. Right.
  • One ui/ line. UNPUBLISHED_EXEMPTIONS['page:header.placedActions'] in the console parity test books a spec key no registration input publishes; the key format matches the map's existing type.key entries. Within the ruling (spec half first; the ui half strikes it). Lane ACCEPT, noted here only because the liveness note cites it.

② Semver level

  • Changeset .changeset/30018-page-header-placed-actions.md: @objectstack/spec minor, @objectstack/skills minor, with Clause-②: yes (widening) and Metadata-protocol: added in both the PR body and the changeset. Matches what the diff publishes: a new authorable key on a published schema and a new authorable-surface row (added is required; the dev's none control exits 1 for that reason).
  • @objectstack/skills minor rather than the patch that skills-only changesets on main carry: right here. check-changeset-no-major leg 3 counts skills/** as a file @objectstack/skills's build copies into what it packs, and under a declared Clause-②: yes it refuses a moved package graded patch; the patch precedents are Clause-②: no PRs. Check Changeset is green on the head.
  • No 18.3.x note, per the maintainer amendment 「18.3.x 不发」; the card says the release note rides with the release that carries A. Right.
  • Clause-②: line: yes (widening) — the PR body, the changeset and the diff agree.

③ Boundary flags

  • open_questions: none. Nothing to escalate from the dev.
  • Dev flag: the one ui/ line. Answered above: it is a test-ledger booking, not a renderer read, and the ruling's sequencing ("spec half first, then the ui reader") is exactly what makes it necessary. Accepted.
  • Dev flag: skills/objectui/guides/page-builder.md line 101 lists breadcrumb on page:header. Verified in the tree at main: the guide names breadcrumb in the title row, and PageHeaderProps.breadcrumb is a retiredKey tombstone the closed shape refuses (spec 17, ADR-0087 D2). Real, out of this card's scope, not blocking; it belongs on its own card. Escalated to the seat to file; the dev left it untouched, which is the right call.
  • Dev flag: Skills-docs: follow-up #29951. Accepted: no renderer reads the key, so an eval that asks for placedActions would teach a key that draws nothing today. The existing eval views-apps-actions-pages.json still expects the declared action id in properties.actions, which the new catalogue row keeps as its first alternative.
  • Deviation: PR attribution links the card thread rather than the seat thread. Bookkeeping; no effect on the tree.
  • Qualifier ledger for console(record header): a url action placed at record_header on sys_user no longer renders in the 18.3 Console header menu, drawer or full page (18.2 rendered it) #29951. The PR names six "not drawn yet" sites (describe marker, _note sentence, parity exemption, pages.mdx, layout-dsl.mdx, pages.md). Verified each is in the diff; the ui half must drop all six. Recorded here so the next card's reviewer can count them.

④ Design judgment

packages/spec/src/ui/component.zod.ts — judged against ruling 6624172200 letter A and the first consumer (validateComponentProps).

  • Ruling A asks for one explicit opt-in on page:header that draws every action placed at record_header / record_more, read live, in place of an authored id list, with actions keeping its PageHeaderProps.actions declares z.array(z.string()) but objectui's canonical page:header renderer consumes ActionDef objects — satisfying the schema deletes every header button #11592 meaning, and spelling plus exclusivity as the seat's call, named in the PR. The tree delivers that: one boolean key placedActions; the describe states the live read and the two locations; the actions docblock and describe still say an authored list is the whole set and an omitted key draws none, now with the one addition "unless it sets placedActions: true"; the .refine refuses true beside a list (an empty list included: [] is "draw none" under PageHeaderProps.actions declares z.array(z.string()) but objectui's canonical page:header renderer consumes ActionDef objects — satisfying the schema deletes every header button #11592, which contradicts "draw every placed one"); the message names both keys and says keep one. The PR body's 维护者速读 names the spelling and the refusal, so the ruling's "named in the PR for the maintainer" condition is met.
  • Tried to break it: (a) placedActions: 'true' + list: the type issue lands at placedActions, the predicate reads === true so it adds nothing and cannot throw on a non-object payload (the refine runs on the parsed object). (b) placedActions: true + a retired key: the terminal unknown-key refusal of closedObject fires; the pair rule is moot. (c) A consumer calling .extend / .pick on PageHeaderProps would lose the check in zod 4; no consumer on main does (every use is parse / safeParse, plus .shape reads by the generators). (d) The refusal prose carries no tracker number (the docblock's ruling-record citation is a comment, not runtime text). (e) defineStack never parses properties, so the only doors are the lint rule and the page save door, both of which read ComponentPropsMap; the test that pins ComponentPropsMap['page:header'] === PageHeaderProps is what keeps the refine from being bypassed by a stale map entry.
  • Design holds. No must-fix. No should-fix.

skills/objectstack-ui/rules/pages.md — judged against the card's eval (the skill's views-apps-actions-pages.json on main; the card itself names no eval) and the spec it teaches on main.

  • The page:header catalogue row now reads actions (ids of the bound object's actions, never action nodes) or placedActions: true (all placed; not drawn yet). Against the spec on the head that is accurate on every clause: the two are exclusive ("or"), true is the spelling, and the qualifier matches the describe marker. The eval's expected output (the declared id in properties.actions, no sibling action node) is still what the row teaches first.
  • The clause paid for the row, "and a full page with no regions" in the print paragraph, names a refusal the spec does make (page.zod.ts checkPagePrintComposition, "print needs the document's blocks in regions"). After the edit the paragraph still states the condition ("Only a kind: 'full' page ... with its blocks in regions may declare it"), and no eval pins the deleted clause; the ratchet has zero headroom on this file, so the trade is a fair one and leaves nothing untrue. Observation only, not a finding.
  • Design holds. No must-fix. No should-fix.

Check-runs on the head at the time of this record: 24 success, 7 skipped, 0 failure, 31 in progress (Build Core, Build & E2E UI, Live E2E UI, Test Core 1-8, Test UI 1-16, Lint & Repo Gates, Lint UI, Type Check UI, Type Check · workspace). Completed and green include Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Dogfood Regression Gate, Check PR Size, Check Documentation Links, Flag docs affected by code changes, Build Docs, Build UI Deps and the scoped Lint / Type Check source-gate jobs. Nothing has concluded red. The verdict below is the contract verdict on the tree; the derived gates still running are the lane seat's merge condition.

Implemented-by: claude/project-thread-i7bk4t
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

席位核对 — ACCEPT · domain:spec#2 · head 371cb0fd9d323a0cc3c3b536831c10fab7ec78b4

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 11:27
@objectstack-fleet
objectstack-fleet Bot requested a review from hotlong as a code owner November 4, 2026 11:27
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 4, 2026 11:27
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit d59a1eb Nov 4, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-i7bk4t branch November 4, 2026 12:07
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 protocol:ui size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(page:header): an explicit opt-in to draw every action placed at record_header / record_more, read live (#29951 ruling A, spec half)

2 participants