Skip to content

fix(console): open the record page scoped to the current app (PageSchema.apps) - #29631

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-77gu51
Nov 2, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread
Fixes #29592

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:控制台打开记录页时,先看当前应用:某张记录页在 apps 里列了这个应用,就在该应用里打开它;其它地方(含 Setup)照旧打开对象唯一的默认记录页。记录抽屉同样生效。page.apps 的活性登记从 planned 改为 live,描述去掉 PLANNED 标记。
  • 为什么改:An app cannot make its own sys_user record page the one a person opens to: on 18.x the console opens plugin-auth's sys_user_detail, and no declarable route reaches a second record page #28979 裁决 A 的 ui 半。规格半(PR feat(spec): a record page can name the apps whose routes open it (PageSchema.apps) #29437)已合入,但控制台没读 apps,所以 Duly 这类应用在自己的路由里打开用户记录,仍然看到平台的 sys_user_detail。
  • 风险与回滚:没有任何页面写 apps 时行为不变(测试有对照组,现有 291 个相关测试文件全绿)。路由段先解析成应用再比对应用名,不拿包 id 去比。回滚即 revert 本 PR。
  • 先看:ui/packages/react/src/hooks/usePageAssignment.ts(选页顺序:应用页、默认页、声明顺序)、ui/packages/app-shell/src/views/RecordDetailView.tsx(路由段解析为应用名)、ui/packages/app-shell/src/console/appObjectScope.ts(declaredRecordPage 与渲染选同一张页)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.appScopedPage-29592.test.tsx,应见 6 个用例全绿:Duly 应用里打开 duly_member,Setup 里仍是 sys_user_detail。
  • 需要你做的:无,席位落地。

What changes

Ruling A on #28979 lets a record page name the apps whose routes open it (PageSchema.apps); the spec half landed in #29437. This is the console half.

  • usePageAssignment (@object-ui/react) now reads its existing app option, the owning app's NAME. Order: the first record page of the object for which defaultRecordPageObject(page, app) answers the object, then the isDefault page, then declaration order. The option's type is unchanged; it was declared "reserved for future use" and read by nothing, and its only caller never passed it. No new parameter, so Clause-②: no (seat ruling).
  • RecordDetailView resolves the route's :appName segment with matchAppBySegment over the full metadata app list and passes app.name, never the raw segment (contract-review flag ③.2 on feat(spec): a record page can name the apps whose routes open it (PageSchema.apps) #29437: the segment is app._packageId ?? app.name). The record drawer, mounted under the host list route, reads the same segment, so a record opened in the drawer inside an app gets that app's page too. A package that ships two apps resolves its package-id segment to the first app, so the second app's scoped page opens under its name segment only, as the appAddressSegment docblock states.
  • appObjectScope.ts declaredRecordPage (the record route's object scope, objectui#13046) asks the same order with the app AppContent already resolved, so the route reads the objects of the page it renders. Only this picker was touched; the packaged-lookup and package-less arms are untouched.
  • Spec (the one edit the card owes, as amended): page.apps liveness row planned → live, citing the three console read points; the describe drops [PLANNED — not yet rendered] and the closing "Until the console reads it" sentence, and the TSDoc names the renderer. Forced by that: the describe pin in stack-app-record-page.test.ts now pins the marker's absence; liveness/state-counts/page.md and content/docs/references/ui/page.mdx are regenerated. The page.apps omit row in metadata-form-zod-reconciliation.test.ts stays, with a rewritten reason: live, but not offered in the Studio page form because no registered widget spells a list of app names (seat ruling; whether the form offer becomes a follow-up card is the seat's call at ACCEPT).

With no apps on any page, every branch falls through to the previous line.

Tests

  • ui/packages/react/src/hooks/__tests__/usePageAssignment.appScope-29592.test.tsx (10): the scoped page opens in its app whatever the served order; Setup and no-app open the default; a page listing several apps; declaration-order fallthrough; another object's scoped page ignored; scoped slotted page hands back slots; pageName still wins; CONTROL with no apps.
  • ui/packages/app-shell/src/views/RecordDetailView.appScopedPage-29592.test.tsx (6): the ruling's pin (Duly under com.duly and under duly opens duly_member; /apps/com.objectstack.setup/... opens sys_user_detail), the two-apps-one-package corner, the drawer, and the CONTROL over five segments.
  • ui/packages/app-shell/src/console/__tests__/appObjectScope.appScopedRecordPage-29592.test.tsx (3): the scope reads the scoped page's objects in Duly and the default's in Setup; the scope picker and usePageAssignment agree across apps and orders; CONTROL.
  • Existing assignment tests unchanged and green: usePageAssignment.isDefault-12195, .typeDiscriminator-9674, .declarationOrder-7298, .isRecordPageOf-12715.
Ablations (each mutated through scripts/ablation-replace.mjs, restored to HEAD and verified)
  • A, compare the raw segment (matchAppBySegment(metadataApps, appName)?.name → the segment): record-view suite 3 failed / 3 passed (package-id segment, two-apps corner, drawer red; the name segment stays green, as expected).
  • B, the hook ignores the app (if (opts.app && false)): 8 failed / 11 passed across the three new files.
  • C, the scope picker ignores the app: 2 failed / 1 passed in the agreement file.
Sweep

Every ui test file naming RecordDetailView, appObjectScope, usePageAssignment or readAppObjectScope (291 files, git grep -l at 8fead84), three locked batches at 2 workers: 98/98 files 788 tests, 98/98 files 654 tests, 95/95 files 847 tests. Spec: src/stack-app-record-page.test.ts + src/system/metadata-form-zod-reconciliation.test.ts 103/103. check:liveness and check:generated green after regeneration.

Acceptance notes

  • Live measurement (app-crm --fresh backend, Vite console on this branch's source, 1440×900): a record page crm_member for sys_user with apps: ['crm_app'] was saved through PUT /api/v1/meta/page/crm_member (200). Under /apps/com.example.crm/sys_user/record/ID it opens crm_member; /apps/crm_app/... canonicalises to the package-id segment and opens it too; /apps/com.objectstack.setup/sys_user/record/ID opens sys_user_detail. Before leg (the three ui files restored from origin/main in the same running console): the CRM route opened sys_user_detail. Screenshots are in the project files under issue-29592/ (before-crm-app-user-record.png, after-crm-app-user-record.png, after-setup-users-record.png); the seat links them.
  • The record drawer is included by seat ruling; its host is RecordDetailView mounted with embedded under the list route (/apps/:appName/:objectName).
  • The Studio page form does not offer apps (no app-name widget); see the reconciliation ledger row.

Verification

Local, dispatch-gates --commands --repo objectstack-ai/objectstack derived 107 families on this diff: all 107 run at 0275f773, every one exit 0, --ran reconciliation 107 run, 0 NOT-MEASURED, 0 UNRUN (the ui vitest run scripts/ suite and check:type-check-debt under the verify lock: 170 files / 4972 tests green; debt exit 0). dispatch-gates --tier: no path-derived mandate (page.zod.ts is a Clause-② suspect hint only; the change there is describe text).

After merging main (5b257756, which brought #29552's appObjectScope.ts arm in cleanly): spec rebuilt, check:generated and check:liveness exit 0; the 16 ui test files naming appObjectScope / readAppObjectScope / usePageAssignment plus the record-view file and #29552's new test, 18 files / 113 tests green; the two spec files 103/103; ratchets rerun on the new head: check:type-check-debt, check-changeset-no-major, check-empty-changeset, check:nul-bytes exit 0. Type-check (@object-ui/react, @object-ui/app-shell, @objectstack/spec) exit 0 and eslint 0 errors on the touched ui files, both at 0275f773. CI check-runs: see this PR.


Generated by Claude Code

usePageAssignment now reads its app option (the app's name) and opens the
record page whose PageSchema.apps lists that app before the object's default
record page. RecordDetailView resolves the route's app segment to its app with
matchAppBySegment and passes the app's name, never the raw segment; the record
drawer reads the same host-route segment. The appObjectScope record-page
picker asks the same order, so a record route reads the objects of the page
it renders. With no apps anywhere, resolution is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTtndxBXWfjH1SiQ6w
…page

The console now opens the record page an app scopes, so the apps describe
drops its planned marker and its liveness row reads live, citing the console
reader. The form ledger keeps its omit row with the reason it is not offered
in the page form yet. Tests pin the hook, the record view (route and drawer,
package-id and name segments, two apps in one package) and the record route's
object scope agreeing with the hook.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTtndxBXWfjH1SiQ6w
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

1 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 PageSchema (symbol, a top-level const))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/liveness/page.json, packages/spec/liveness/state-counts/page.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 168 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 4d8095ba90d8855f5b1e012d378b9e48b3a5668e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 4d8095ba90d8855f5b1e012d378b9e48b3a5668e

⚠️ 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 4d8095ba90d8855f5b1e012d378b9e48b3a5668e → 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:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ

席位核对 — ACCEPT · domain:ui#2 · head 5b257756 · 2026-11-02T23:07Z

  • 形态: draft PR on main, Fixes #29592 on its own line and no other card number beside a closing keyword. 13 files, +517/−34, Tier S (check-governed-merges --pr 29631: 0 of 13 paths governed; under 3000 lines). All inside claim 6588510119 as amended by 6588557190 (the spec's forced followers, the omit row kept with a rewritten reason, the record drawer). The drawer host is RecordDetailView.tsx itself (mounted embedded under /apps/:appName/:objectName), so no extra file. appObjectScope.ts touches only declaredRecordPage and its one call; Console: a console-created (package-less) app's record route reads the whole /meta/page list, and its root the whole object registry #29552's arm came in through the main merge cleanly.
  • 前提实测: at origin/main the apps liveness row read planned with the note "the console's page assignment reads no app", and RecordDetailView.tsx:1049 passed no app. Live on app-crm --fresh with a sys_user record page crm_member scoped apps: ['crm_app']: before, the CRM route opened sys_user_detail; after, /apps/com.example.crm/sys_user/record/ID opens crm_member (the name segment canonicalises to the same), and /apps/com.objectstack.setup/sys_user/record/ID still opens sys_user_detail. The seat looked at all three screenshots in the project files (issue-29592/before-crm-app-user-record.png, after-crm-app-user-record.png, after-setup-users-record.png).
  • 门禁 check-runs (pinned to head 5b25775652498d9ce56104e498a290e824b32908): 58 success, 7 skipped, 0 failed, 0 pending (65 runs), including Check Changeset, Type Check UI, Lint UI, Test UI (16 shards), Build & E2E UI and Spec property liveness. Dev local gates: 107 derived, 107 run, all exit 0, --ran 0 NOT-MEASURED; --tier no path-derived mandate; ratchets and check:generated / check:liveness re-run green on this head after merging main. New tests 10 + 6 + 3 green; the four existing assignment suites unchanged and green; sweep 291 ui files green. Ablations: raw segment reds 3, hook ignoring the app reds 8, picker ignoring the app reds 2; all restored to HEAD.
  • Code read: the scoped page is asked through the spec's own defaultRecordPageObject(page, app), the same reading the write doors use to refuse a second page per app, so the first match is the only one. The route compares matchAppBySegment(...)?.name, never the segment (flag ③.2 on feat(spec): a record page can name the apps whose routes open it (PageSchema.apps) #29437). With no app named, or no page naming it, every branch falls through to the previous line; the hook's memo keys on opts.app, not the options object, so passing { app: undefined } re-renders nothing new.
  • 接受的偏差: the screenshots are not embedded in the PR body (the relay cannot upload images to GitHub); the PR's Acceptance notes describe each one and the seat checked them. The Studio page form still does not offer apps: no widget picks a list of app names, and the reconciliation ledger row now says so truthfully.
  • 范围外去向: the Studio form offer of apps is not filed: authoring works through code and the metadata API (measured: PUT /api/v1/meta/page/crm_member 200), so there is no measured wrong result, only a missing editor. Recorded here. No Skills-docs: line is owed.
  • 落地条款: Clause-②: no (the resolver reads the existing reserved app option; type unchanged; the one caller never passed it, hotcrm has none), Metadata-protocol: none (the spec change is describe text and a liveness verdict), patch changesets for @object-ui/react, @object-ui/app-shell and @objectstack/spec; no contract review is owed. Land by pr_ready + automerge_enable.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ui: the console opens a record page scoped to the current app (PageSchema.apps), so an app can reach its own sys_user page (#28979 ruling A)

1 participant