Skip to content

fix(console): the record header reads placedActions, so a packaged sys_user header action draws again (#29951) - #30097

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

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-4y2o5l

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29951

维护者速读

Clause-②: no
Metadata-protocol: none

What changes

  • The header reads placedActions: true (ui/packages/components/src/renderers/layout/containers.tsx). PageHeaderRenderer hands the key to PageHeaderActions. true draws every action of the page's object whose locations include record_header or record_more, in declaration order, read from the object's metadata through the same useMetadataItem('object', …) lookup an id list uses, and through the same predicate, capability and switched-off chain after it. Omitted or false, an authored actions list is still 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). Only the boolean true opts in. A true beside an authored list, which the spec refuses at parse, draws none of the object's actions and is reported once with console.error, the same fail-closed answer a refused id array gets.
  • One placed-header reader. placedHeaderActions (ui/packages/types/src/ui-action.ts, exported from @object-ui/types) is the rule: every action actionRendersAt places at record_header or record_more, the first of a name. RecordDetailView's synthesized header (recordHeaderActions) now reads it instead of its inline filter, so the synthesized default page and a placedActions: true header draw the same set. No second reader.
  • Registration input. The page:header registration declares placedActions (boolean), and sdui.manifest.json is regenerated (node scripts/gen-sdui-manifest-node.mjs, in-tree; the only component change is that input), with scripts/sdui-manifest.record.json and the component reference pages it renders (content/docs/ui/components/page.mdx and index.mdx, node scripts/gen-sdui-reference.mjs).
  • The three platform pages opt in. sys_user_detail and sys_organization_detail (@objectstack/platform-objects) and sys_approval_request_detail (@objectstack/plugin-approvals) set { type: 'page:header', properties: { placedActions: true } } in place of the id lists feat(spec,ui)!: retire the record page's slots.tabs; a page that needs its own strip is kind: 'full' #27219 froze (11 ids on sys_user, 4 on sys_organization, none on the approval request). The tab strips feat(spec,ui)!: retire the record page's slots.tabs; a page that needs its own strip is kind: 'full' #27219 introduced are unchanged. Every action the old lists named was placed at record_header / record_more (the old pin held each list equal to exactly that set), so nothing the pages drew is dropped. sys_approval_request places no header action of its own (its decision actions are record_section), so its header now draws only what an installed package places there.
  • The "not drawn yet" qualifiers PR feat(spec): page:header placedActions draws every placed header action, refused beside an actions list (#30018) #30028 listed (five of six; the sixth is in the skills PR):
    • packages/spec/src/ui/component.zod.ts: the placedActions describe drops its [PLANNED — not yet rendered] marker and the docblock says the reader is live; content/docs/references/ui/component.mdx regenerated (gen:schema + gen:docs).
    • packages/spec/liveness/page.json: the _note's PLANNED RECORD sentence is rewritten as a LIVE RECORD that names PageHeaderActions, placedHeaderActions and the three producer pages.
    • ui/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts: UNPUBLISHED_EXEMPTIONS['page:header.placedActions'] struck (a strike note in its place, as the file does for parentDataSource).
    • content/docs/ui/pages.mdx and content/docs/protocol/objectui/layout-dsl.mdx: the qualifier is gone.
  • Changeset .changeset/29951-page-header-placed-actions-reader.md: patch for the six touched packages.

Why

Ruling A on #29951 (record 6624172200). #27219 rebuilt the platform record pages as kind: 'full' pages with their own tab strips and copied the platform's header action ids into page:header.actions. Under #11592 an authored list is the whole set, so a header action any other package places on sys_user (or sys_organization, sys_approval_request) stopped drawing: the 18.2 slotted page's synthesized header had read the object's placed actions live. #30018 (PR #30028) declared placedActions: true spec first; this is its reader and its producers. The tab strips #27219 was for stay. Nothing ships in 18.3.x.

Tests

  • Card pin ui/packages/app-shell/src/views/RecordDetailView.placedHeaderActions-29951.test.tsx, through the real RecordDetailView → page:header → PageHeaderActions, on the full page (route) and the drawer (embedded, as ObjectView mounts it). A sys_user def carries the platform's ban_user (record_header) and impersonate_user (record_more), two actions placed elsewhere, and two packaged type: 'url' actions (record_header and record_more, _packageId: 'com.qa.hdr').
  • packages/platform-objects/src/pages/record-page-header-actions.test.ts rewritten: per page, the object places header actions (non-vacuous), the header node is exactly { placedActions: true }, and ComponentPropsMap['page:header'] accepts it. sys-approval-request-page.test.ts pins the new node.
  • packages/spec/src/ui/component.test.ts: the marker test now asserts the describe carries no [PLANNED marker.
  • Ablation: the reader made to ignore the key (const placedOptIn = false && rawPlacedActions === true;) turns 4 of the 7 pin cases red (Unable to find role="button" and name "Ban User": shipped page, shipped drawer, CONTROL placed, refused pair) and leaves the 3 list cases green. File restored byte-identical (cmp).

Acceptance notes

Verification

  • Live, on a dev boot (pnpm build under os-verify-lock, 119/119; showcase backend pnpm dev -- --fresh -p 4317; Console from ui/ src via vite with DEV_PROXY_TARGET). A writable package com.qa.hdr stores hdr_user_profile (type: 'url', record_header, on sys_user) and the CONTROL hdr_account_profile on showcase_account. Served: GET /api/v1/meta/page/sys_user_detail, sys_organization_detail and sys_approval_request_detail each answer kind: 'full' with {"type":"page:header","properties":{"placedActions":true}}; /meta/object/sys_user serves hdr_user_profile. Playwright readings:
    • Setup → Users, full page: row Update Profile · Ban User · Set Password · Edit · ⋯; ⋯ = Refresh · Impersonate User · Set Manager · Change Password · Change Email · QA User Profile · Add to favorites · Share · Delete My Account.
    • Setup → Users, drawer: the same row; ⋯ = Impersonate User · Set Manager · Change Password · Change Email · QA User Profile · Add to favorites · Share · Open as full page · Delete My Account. The tab strip (Details · Positions · Permission Sets · Business Units · Sessions …) is unchanged.
    • Before the fix (round 1, main 6a4737ab), both readings were the same lists without QA User Profile. CONTROL showcase_account, full page and drawer: QA Account Profile · Edit · ⋯, as before.
  • ui suites from the ui/ root: the new pin 7/7; packages/components/src/__tests__/page-header*, packages/app-shell/src/views/RecordDetailView.*, packages/types/src/__tests__ and the console parity test: 518 files, 11620 tests green; registry-inputs-spec-parity.test.ts alone: 261 green with the exemption struck.
  • Package suites: @objectstack/platform-objects src/pages 46, @objectstack/plugin-approvals sys-approval-request-page.test.ts 20, @objectstack/spec src/ui/component.test.ts 361, all green.
  • Type-check: @object-ui/types, @object-ui/components, @object-ui/app-shell (type-check, from the ui root), @objectstack/platform-objects, @objectstack/plugin-approvals (typecheck), @objectstack/spec tsc --noEmit: clean.
  • Generated: gen:schema + gen:docs (reference), node scripts/gen-sdui-manifest-node.mjs (manifest + record), node scripts/gen-sdui-reference.mjs (content/docs/ui/components/); node scripts/check-sdui-manifest.mjs and pnpm check:sdui-reference green.
  • Gates: every line of node scripts/pm/dispatch-gates.mjs --commands on the final diff (119), then --ran: 119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN. 118 exit 0. pnpm check:platform-checklist exits 1 on an absent assertProtocolCompat anchor in packages/metadata-protocol/src/protocol.ts (areas/platform-core.json); it is red the same way on merge base d79eecfe5b and on origin/main f35b361997, outside this diff. Three first-pass reds were re-run: pnpm check:sdui-reference was this diff's own (component pages regenerated and committed, then green); pnpm --dir ui exec vitest run scripts/ (one 15 s timeout under load) and pnpm --filter @objectstack/spec run check:generated (check:test-typecheck's tsc ran out of heap because this container's NODE_OPTIONS carries no heap flag) were green re-run alone under the lock, the second with --max-old-space-size=4096 appended.

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 changed TypeScript files pass eslint with 0 errors: the seven under packages/ with the root config and --no-inline-config, the six under ui/ from the ui root. 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 with Claude Code

https://claude.ai/code/session_01WGufzh8VRbTbBnqwnrXsSL

…s_user header action draws again (#29951)

Ruling A on #29951 (record 6624172200), ui half of #30018's spec key.

- PageHeaderActions (@object-ui/components) reads page:header's
  placedActions: true and draws every action of the page's object placed
  at record_header / record_more, read live, through placedHeaderActions
  (@object-ui/types), the one placed-header reader RecordDetailView's
  synthesized header now reads too. Omitted or false, an authored actions
  list is still the whole set (#11592). A true beside a list draws none
  of the object's actions and is reported once. The page:header
  registration publishes the input.
- sys_user_detail, sys_organization_detail and
  sys_approval_request_detail set placedActions: true in place of the id
  lists #27219 froze. Tab strips unchanged.
- Drop the "not drawn yet" qualifiers: the describe marker (reference
  regenerated), the page liveness PLANNED record (re-graded live), the
  registry-inputs parity exemption, pages.mdx and layout-dsl.mdx. The
  skills/objectstack-ui/rules/pages.md qualifier goes in a separate PR
  (Tier H).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzh8VRbTbBnqwnrXsSL
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:ui tests tooling labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/plugin-approvals, @objectstack/spec, touching 17 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (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.

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

  • content/docs/permissions/authentication.mdx (via set_user_password (literal, a string literal in regions))
  • content/docs/protocol/objectui/actions.mdx (via ban_user (literal, a string literal in regions))
  • content/docs/protocol/objectui/layout-dsl.mdx (via placedActions (symbol, a field of const object PageHeaderProps, an authorable key of ui/PageHeaderProps))
  • content/docs/protocol/objectui/record-alert.mdx (via resend_verification_email (literal, a string literal in regions))
  • content/docs/ui/pages.mdx (via placedActions (symbol, a field of const object PageHeaderProps, an authorable key of ui/PageHeaderProps))

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

  • content/docs/releases/v17/17-5.mdx (via set_user_manager (literal, a string literal in regions))
  • 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
  • 1 changed file(s) yielded no anchor (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 — 170 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 f68b47cacede768ec0b4b54aae8656f8a7f88bb2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cd9f7e2ba7b51d8d3d87c0ba137c8359244c3809 — the merge of head 6d81dac28c962ac76b435e04d3879154023df545 into base f68b47cacede768ec0b4b54aae8656f8a7f88bb2, 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 cd9f7e2ba7b51d8d3d87c0ba137c8359244c3809 && git checkout cd9f7e2ba7b51d8d3d87c0ba137c8359244c3809
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f68b47cacede768ec0b4b54aae8656f8a7f88bb2 6d81dac28c962ac76b435e04d3879154023df545 && git checkout -B drift-repro f68b47cacede768ec0b4b54aae8656f8a7f88bb2 && git merge --no-ff 6d81dac28c962ac76b435e04d3879154023df545

node scripts/docs-audit/affected-docs.mjs --json f68b47cacede768ec0b4b54aae8656f8a7f88bb2

⚠️ 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 f68b47cacede768ec0b4b54aae8656f8a7f88bb2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ions

The unconsumed #30018 changeset rides the same release as this reader, so
its "declared ahead of its reader" bullet now says the console's
page:header renderer reads the key and the PLANNED marker is gone. The
@objectstack/skills bullet stays until the skills follow-up.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzh8VRbTbBnqwnrXsSL
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd

维护者速读(席位终稿)

  • 改了什么:记录页头部现在能读 page:header 的 placedActions: true,实时画出对象上所有放在 record_header / record_more 的动作。用户、组织、审批请求三个平台记录页改用这个开关,其他包加在 sys_user 上的动作(比如 QA User Profile)在抽屉和整页里都重新出现。
  • 为什么改:feat(spec,ui)!: retire the record page's slots.tabs; a page that needs its own strip is kind: 'full' #27219 把这三个页面的头部动作写成了固定 id 列表,按 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 规则,固定列表就是全部,其他包加的动作因此消失。这是 18.2 → 18.3 的回退,按裁决 A 修复(裁决记录 6624172200)。
  • 风险与回滚:低。三个页面原来列出的动作都放在 record_header / record_more 上,一个都没少;标签页结构不变。未设置该键的页头照旧只画列出的动作。回滚即 revert 本 PR。
  • 先看:ui-action.ts 新增的 placedHeaderActions(记录页合成头部和 page:header 共用这一个读取器),以及 containers.tsx 中 PageHeaderActions 的开关处理。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.placedHeaderActions-29951.test.tsx。
  • 需要你做的:本 PR 无。skills/objectstack-ui/rules/pages.md 的限定语属于 Tier H,会在本 PR 合并后单独开 Refs #29951 的小 PR,届时需要你合并。
  • 席位意见:ACCEPT。

席位核对 · ACCEPT · head 3d6eebd408 · 2026-11-04T13:42Z

  1. Shape:
    • Fixes #29951, 23 files, about 594 lines.
    • One new changeset: @object-ui/{components,types,app-shell}, @objectstack/{spec,platform-objects,plugin-approvals} patch, with Clause-②: no / Metadata-protocol: none.
    • check-governed-merges --pr 30097: NOT governed.
    • The Tier H skills file is out, as the order split it.
  2. Fix, read by the seat:
    • One reader. placedHeaderActions (@object-ui/types) uses actionRendersAt for record_header / record_more and keeps the first entry of each name. It is the same predicate the synthesized RecordDetailView header used inline, and that header now calls it, so there is no second dialect.
    • The header honours the opt-in.
    • Pages. sys_user_detail, sys_organization_detail and sys_approval_request_detail switch to placedActions: true. Every id in the old frozen lists was placed at the header, so nothing is dropped, and the tab strips stay.
    • Qualifiers. The describe marker, the liveness _note (re-graded live), pages.mdx and layout-dsl.mdx are dropped. The generated reference and manifest are regenerated.
    • Second commit 3d6eebd408, at the seat's request: the unconsumed .changeset/30018-page-header-placed-actions.md "Declared ahead of its reader" bullet now says the console reads the key. Both changesets ride the same release, so the CHANGELOG no longer contradicts itself. The PR body's line "Not edited here" is superseded by this commit.
  3. Evidence:
    • The card's pin is 7/7. The ablation (reader ignores the key) turns 4 red.
    • 119/119 gates ran, --ran 0 NOT-MEASURED. pnpm check:platform-checklist is red on main itself (protocol.ts#assertProtocolCompat anchor, seen on d79eecfe5b and f35b361997), so that red is not this PR's.
    • Live on a dev boot: a packaged url action on sys_user shows in Setup's member drawer and in the full page's overflow menu.
  4. Landing: ordinary queue. When every check on this head is green or expected-skip: relay pr_ready + automerge_enable, follow it to MERGED, and record the landing on 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. PR 2 (pages.md, Tier H) then opens from the restarted branch for the maintainer.

Check Changeset refuses an edit to a changeset already on the merge base
(the foreign-changeset rule, #17712), so the reworded bullet goes back.
This PR's own changeset says the console reads placedActions; the stale
sentence in 30018's entry is left to the release-time notes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzh8VRbTbBnqwnrXsSL
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd

席位核对 · ACCEPT carried to head 6d81dac28c · 2026-11-04T13:47Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 14:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit a52761f Nov 4, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4y2o5l branch November 4, 2026 14:46
huangyiirene pushed a commit that referenced this pull request Nov 4, 2026
PR #30097 made the header renderer read placedActions, so the objectui
page-builder guide names it beside actions and the objectstack-ui pages
catalogue drops "not drawn yet".

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAjSbVpQUeFNuTTm3me
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/l tests tooling

Projects

None yet

1 participant