You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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) #29592
Filing gate: ③ follow-up the ruling assigns. The director's ruling on #28979 (batch #453 item 1, letter A, maintainer 「同意」) binds a page-level app scope. The spec half landed in PR #29437 (b16ee8ba06, 2026-11-02 20:11Z), which closes #28979. The key is declared [PLANNED — not yet rendered] until the console reads it. ⛔ Not a claim. No grade: triage sets it.
Reader: domain:ui, the console's record-page resolution (usePageAssignment in ui/packages/app-shell).
What landed in spec
PageSchema.apps?: string[] on a type: 'record' page bound to an object. The entries are app names (AppSchema.name), at least 1, unique.
defaultRecordPageObject(page, app?) in packages/spec/src/ui/default-record-page.ts is the one reading of a slot. With app passed, it answers which object this page is the record page for inside that app. Without app, it reads the deployment default (isDefault).
There is one page per object per app. The stack door, the registry door and the save door refuse a second one (DEFAULT_RECORD_PAGE_CONFLICT, carrying app).
What the console must do
On a record route, resolve the page in this order:
Resolve the route's app segment to the app with matchAppBySegment (package id first, then name, already in the tree). Take the first page for which defaultRecordPageObject(p, app.name) === objectName.
Otherwise, use today's default read (isDefault).
Otherwise, use today's declaration order.
With no scoped page, every branch falls through to today's line.
⚠️Contract review flag ③.2 on #29437 (must get right). The route segment is NOT the app name. /apps/SEGMENT is appRouteSegment(app) = app._packageId ?? app.name (ui/packages/app-shell/src/utils/appRoute.ts). So resolve the segment to the app, then pass app.name. Never compare the raw segment to apps entries.
A corner to pin: a package that ships two apps shares one package-id segment, and matchAppBySegment resolves that segment to the first app. The second app's scoped page is reachable only under its name segment, as the appAddressSegment docblock states.
Done when
Pin 1 (the ruling's own). The duly composition opens its scoped sys_user page (duly_member, apps: ['duly']) from People inside the Duly app. Setup → Users, under /apps/com.objectstack.setup/..., still opens plugin-auth's sys_user_detail.
With no apps anywhere, resolution is byte-identical to today. The existing assignment tests stay green unchanged.
The two-apps-one-package corner above is pinned.
The spec liveness row for page.apps flips from planned to live, citing the console reader. The describe's [PLANNED — not yet rendered] marker and its closing "until the console reads it" sentence are removed in the same PR. That is the one spec edit this card owes.
Downstream: objectstack-ai/duly#113 tracks duly's URL-action stopgap. It can be deleted once this card lands.
Claim: PM loop round 7 · 2026-11-02T21:08Z
Session: session_01QsR2jTtndxBXWfjH1SiQ6w (card thread, os-dev); dispatched by seat session session_01QsR2jRFuJz9QBZqPVHYVhJ
Account: zhuangjianguo
Branch: claude/project-thread-77gu51 (platform-assigned to the card thread; it can push only this branch)
Worktree: objectstack-issue-29592
Domain: domain:ui
Seat: domain:ui#2 (conceded by seat 3, which drafted the brief)
File surface: ui/packages/react/src/hooks/usePageAssignment.ts (the record-page resolver) and its call site in ui/packages/app-shell/src/views/RecordDetailView.tsx (~L1049); the record-page picker in ui/packages/app-shell/src/console/appObjectScope.ts (declaredRecordPage, ~L348) only, disjoint from the packaged-lookup and package-less root arms that #29551 / #29552 edit (merge main if either lands first); packages/spec/liveness/page.json row apps (planned → live, citing the console read point) and the page.zod.ts describe (drop [PLANNED — not yet rendered] and the "until the console reads it" sentence); new tests; one changeset (patches). Stop on breach; explain in the report
Container & model: M, mode:thread, model: default tier unless dispatch-gates --tier derives a mandate on the final file list (one spec edit), effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: page.apps is enforced on write (one app-scoped record page per object per app) but no console reader honours it, so every app opens the object's isDefault page | inside an app that declares its own record page, the record view opens the wrong page (ruling A on #28979: Duly People should open duly_member, Setup Users still sys_user_detail) | app builders shipping an app-scoped record page
Thread-read: none
Premise: read from code at origin/main2da42e74: page.jsonapps is planned with the measured note "the console's page assignment (ui usePageAssignment / appObjectScope.declaredRecordPage) reads no app"; RecordDetailView.tsx:1049 calls usePageAssignment(objectName, { pageName }) with no app. The os-dev confirms live before code.
Serial constraints cleared: appObjectScope.ts is shared with open #29551 and #29552 (seat 3, other arms): record-page picker only, merge main on landing. If usePageAssignment (published @object-ui/react) must gain a parameter or option, report first: that makes Clause-②: yes (widening) and a contract review, and amends this claim
Claim amendment (to 6588510119) · 2026-11-02T21:10Z
File surface (added): the spec's forced followers of the liveness flip: packages/spec/src/stack-app-record-page.test.ts (the describe-marker pin flips), packages/spec/liveness/state-counts/page.md (generated), content/docs/references/ui/page.mdx (generated), and the page.apps omit row in metadata-form-zod-reconciliation.test.ts, kept with a rewritten truthful reason (live in the console; not offered in the Studio form because no widget picks an app-name list). No page.form.ts growth and no metadata-forms translation regen here. Also the record drawer's host that embeds RecordDetailView inside an app list, so it passes the route's app the same way (ruling A: the object's record view inside that app opens the scoped page, as a route or in the drawer); the os-dev names the file in its report.
Clause-②: no (unchanged). The resolver reads the existing PageAssignmentOptions.app?: string, published as "reserved for future use" and never read; the type does not change. Census at origin/main2da42e74: the one caller (RecordDetailView.tsx:1049) never passes app, and hotcrm has no caller. The option's doc and the changeset say it now names the owning app.
Metadata-protocol: none (unchanged)
{
"issue": 29592,
"status": "done",
"branch": "claude/project-thread-77gu51",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29631",
"session": "session_01QsR2jTtndxBXWfjH1SiQ6w",
"premise_still_valid": true,
"head": "5b257756",
"summary": "usePageAssignment (@object-ui/react) now reads its existing, previously unread `app` option as the owning app's NAME: the record page for which defaultRecordPageObject(page, app) answers the object opens before the isDefault page, then declaration order. RecordDetailView resolves the route's :appName segment with matchAppBySegment and passes app.name (never the raw segment), on the record route and in the record drawer; appObjectScope.ts declaredRecordPage asks the same order with the app AppContent resolved. Spec: page.apps liveness planned -> live citing the three console read points, describe marker and closing sentence dropped, describe pin flipped, state-counts and page.mdx regenerated, the form-ledger omit row kept with a rewritten reason (seat ruling). With no apps anywhere resolution is unchanged.",
"files_changed": [
".changeset/29592-app-scoped-record-page.md",
"content/docs/references/ui/page.mdx",
"packages/spec/liveness/page.json",
"packages/spec/liveness/state-counts/page.md",
"packages/spec/src/stack-app-record-page.test.ts",
"packages/spec/src/system/metadata-form-zod-reconciliation.test.ts",
"packages/spec/src/ui/page.zod.ts",
"ui/packages/app-shell/src/console/__tests__/appObjectScope.appScopedRecordPage-29592.test.tsx",
"ui/packages/app-shell/src/console/appObjectScope.ts",
"ui/packages/app-shell/src/views/RecordDetailView.appScopedPage-29592.test.tsx",
"ui/packages/app-shell/src/views/RecordDetailView.tsx",
"ui/packages/react/src/hooks/__tests__/usePageAssignment.appScope-29592.test.tsx",
"ui/packages/react/src/hooks/usePageAssignment.ts"
],
"line_budget": "13 files, 551 changed lines (+517/-34) vs merge base at 8fead845 (dispatch-gates count, generated files included); under the 3000 human-merge threshold",
"tests": "New: usePageAssignment.appScope-29592 10/10, RecordDetailView.appScopedPage-29592 6/6, appObjectScope.appScopedRecordPage-29592 3/3; existing usePageAssignment isDefault-12195, typeDiscriminator-9674, declarationOrder-7298, isRecordPageOf-12715 green unchanged. Sweep: 291 ui test files naming RecordDetailView/appObjectScope/usePageAssignment/readAppObjectScope, 3 locked batches at 2 workers: 98/98 788 tests, 98/98 654, 95/95 847. Spec: stack-app-record-page + metadata-form-zod-reconciliation 103/103. Post-merge at 5b257756: 18 files 113 tests, spec 103/103. Ablations via scripts/ablation-replace.mjs (anchor 1->0, blob changed, restored blob == HEAD, git diff HEAD empty; ui vitest aliases @object-ui/* to src so no dist leg): A raw segment -> record-view 3 failed/3 passed; B hook ignores app -> 8 failed/11 passed; C picker ignores app -> 2 failed/1 passed. Live: app-crm --fresh + Vite console on branch source; sys_user page crm_member with apps ['crm_app'] saved by PUT /api/v1/meta/page (200): /apps/com.example.crm/sys_user/record/ID opens crm_member, /apps/crm_app/... canonicalises and opens it, /apps/com.objectstack.setup/... opens sys_user_detail; before leg (three ui files from origin/main) CRM opened sys_user_detail. Screenshots /mnt/project-files/issue-29592/.",
"gates": "dispatch-gates --commands derived 107 at 0275f773: 107 run, all exit 0; --ran: 107 run, 0 NOT-MEASURED, 0 UNRUN (vitest run scripts/ 170 files 4972 tests and check:type-check-debt under the lock). --tier: no path-derived mandate (page.zod.ts Clause-2 suspect hint; describe text only). After merge 5b257756: check:generated 0, check:liveness 0, check:type-check-debt 0, check-changeset-no-major 0, check-empty-changeset 0, check:nul-bytes 0. Type-check react/app-shell/spec 0; eslint touched ui files 0 errors. PR body gates: changeset-no-major 0, adr-0087-registration 0, closing-keyword-parity 0, partof-closing-keyword 0. CI check-runs: in_progress at report time.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"deviations": [
"Spec surface beyond page.json + page.zod.ts, accepted by the seat before code: stack-app-record-page.test.ts pin flip, state-counts/page.md and references/ui/page.mdx regenerated, metadata-form-zod-reconciliation.test.ts omit row reason rewritten (row kept, no page.form.ts growth).",
"usePageAssignment gained no parameter: the existing reserved app option is now read (seat: Clause-2 no); option doc updated, changeset says it names the owning app.",
"Record drawer included by seat ruling; host file ui/packages/app-shell/src/views/RecordDetailView.tsx (embedded under /apps/:appName/:objectName).",
"Live measurement used a runtime-saved page on app-crm, not the duly package (not in this repo); the ruling's duly pin is a test composition."
],
"mcp_calls": "3 - mcp__github__issue_read (get x1, get_comments x2); no write tools",
"api_writes": "2 - relay pr_create (objectstack#29631), post-stamped os-dev-report comment on #29592",
"open_questions": [],
"security_findings": "withheld",
"out_of_scope_findings": [
"carrier: seat at ACCEPT - Studio page form does not offer page.apps (no app-name widget); whether to file a follow-up card for the form offer. noted, not filed"
]
}
Landed · 2026-11-02T23:33Z · PR #29631 merged through the queue as 3482f3d8; the merge commit carries all 13 files of the accepted head 5b257756 (usePageAssignment reading its app option, RecordDetailView resolving the route's app by matchAppBySegment on the route and in the drawer, appObjectScopedeclaredRecordPage asking the same order, the page.apps liveness flip to live with the describe and its generated followers, the reconciliation omit row's new reason, three test files, the changeset). Delivered in full: ruling A on #28979 is now live in the console; pm:dispatched removed. Noted, not filed: the Studio page form does not offer apps (no app-name list widget). Next on this resolver: #29552 leg 1 can now build on main.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
pm:queue· 2026-11-02T20:59Z⛔ Not a claim.
sys_userrecord page the one a person opens to: on 18.x the console opens plugin-auth'ssys_user_detail, and no declarable route reaches a second record page #28979 ruling A: the console opens a record page scoped to the current app (PageSchema.apps). The spec half landed in PR feat(spec): a record page can name the apps whose routes open it (PageSchema.apps) #29437. p2.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ
Claim: PM loop round 7 · 2026-11-02T21:08Z
Session:
session_01QsR2jTtndxBXWfjH1SiQ6w(card thread, os-dev); dispatched by seat sessionsession_01QsR2jRFuJz9QBZqPVHYVhJAccount:
zhuangjianguoBranch:
claude/project-thread-77gu51(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-29592Domain:
domain:uiSeat:
domain:ui#2(conceded by seat 3, which drafted the brief)File surface:
ui/packages/react/src/hooks/usePageAssignment.ts(the record-page resolver) and its call site inui/packages/app-shell/src/views/RecordDetailView.tsx(~L1049); the record-page picker inui/packages/app-shell/src/console/appObjectScope.ts(declaredRecordPage, ~L348) only, disjoint from the packaged-lookup and package-less root arms that #29551 / #29552 edit (mergemainif either lands first);packages/spec/liveness/page.jsonrowapps(planned→live, citing the console read point) and thepage.zod.tsdescribe (drop[PLANNED — not yet rendered]and the "until the console reads it" sentence); new tests; one changeset (patches). Stop on breach; explain in the reportContainer & model: M,
mode:thread,model: default tier unless dispatch-gates --tier derives a mandate on the final file list (one spec edit),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
page.apps is enforced on write (one app-scoped record page per object per app) but no console reader honours it, so every app opens the object's isDefault page | inside an app that declares its own record page, the record view opens the wrong page (ruling A on #28979: Duly People should open duly_member, Setup Users still sys_user_detail) | app builders shipping an app-scoped record pageThread-read: none
Premise: read from code at
origin/main2da42e74:page.jsonappsisplannedwith the measured note "the console's page assignment (ui usePageAssignment / appObjectScope.declaredRecordPage) reads no app";RecordDetailView.tsx:1049callsusePageAssignment(objectName, { pageName })with no app. The os-dev confirms live before code.Serial constraints cleared:
appObjectScope.tsis shared with open #29551 and #29552 (seat 3, other arms): record-page picker only, merge main on landing. IfusePageAssignment(published@object-ui/react) must gain a parameter or option, report first: that makesClause-②: yes (widening)and a contract review, and amends this claimobjectstack-fleet commentedon Nov 2, 2026
domain:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ
Claim amendment (to 6588510119) · 2026-11-02T21:10Z
File surface (added): the spec's forced followers of the liveness flip:
packages/spec/src/stack-app-record-page.test.ts(the describe-marker pin flips),packages/spec/liveness/state-counts/page.md(generated),content/docs/references/ui/page.mdx(generated), and thepage.appsomit row inmetadata-form-zod-reconciliation.test.ts, kept with a rewritten truthful reason (live in the console; not offered in the Studio form because no widget picks an app-name list). Nopage.form.tsgrowth and no metadata-forms translation regen here. Also the record drawer's host that embedsRecordDetailViewinside an app list, so it passes the route's app the same way (ruling A: the object's record view inside that app opens the scoped page, as a route or in the drawer); the os-dev names the file in its report.Clause-②: no (unchanged). The resolver reads the existing
PageAssignmentOptions.app?: string, published as "reserved for future use" and never read; the type does not change. Census atorigin/main2da42e74: the one caller (RecordDetailView.tsx:1049) never passesapp, and hotcrm has no caller. The option's doc and the changeset say it now names the owning app.Metadata-protocol: none (unchanged)
objectstack-fleet commentedon Nov 2, 2026
domain:ui#2 · zhuangjianguo · session_01QsR2jTtndxBXWfjH1SiQ6w
os-dev-report
{ "issue": 29592, "status": "done", "branch": "claude/project-thread-77gu51", "pr": "https://github.com/objectstack-ai/objectstack/pull/29631", "session": "session_01QsR2jTtndxBXWfjH1SiQ6w", "premise_still_valid": true, "head": "5b257756", "summary": "usePageAssignment (@object-ui/react) now reads its existing, previously unread `app` option as the owning app's NAME: the record page for which defaultRecordPageObject(page, app) answers the object opens before the isDefault page, then declaration order. RecordDetailView resolves the route's :appName segment with matchAppBySegment and passes app.name (never the raw segment), on the record route and in the record drawer; appObjectScope.ts declaredRecordPage asks the same order with the app AppContent resolved. Spec: page.apps liveness planned -> live citing the three console read points, describe marker and closing sentence dropped, describe pin flipped, state-counts and page.mdx regenerated, the form-ledger omit row kept with a rewritten reason (seat ruling). With no apps anywhere resolution is unchanged.", "files_changed": [ ".changeset/29592-app-scoped-record-page.md", "content/docs/references/ui/page.mdx", "packages/spec/liveness/page.json", "packages/spec/liveness/state-counts/page.md", "packages/spec/src/stack-app-record-page.test.ts", "packages/spec/src/system/metadata-form-zod-reconciliation.test.ts", "packages/spec/src/ui/page.zod.ts", "ui/packages/app-shell/src/console/__tests__/appObjectScope.appScopedRecordPage-29592.test.tsx", "ui/packages/app-shell/src/console/appObjectScope.ts", "ui/packages/app-shell/src/views/RecordDetailView.appScopedPage-29592.test.tsx", "ui/packages/app-shell/src/views/RecordDetailView.tsx", "ui/packages/react/src/hooks/__tests__/usePageAssignment.appScope-29592.test.tsx", "ui/packages/react/src/hooks/usePageAssignment.ts" ], "line_budget": "13 files, 551 changed lines (+517/-34) vs merge base at 8fead845 (dispatch-gates count, generated files included); under the 3000 human-merge threshold", "tests": "New: usePageAssignment.appScope-29592 10/10, RecordDetailView.appScopedPage-29592 6/6, appObjectScope.appScopedRecordPage-29592 3/3; existing usePageAssignment isDefault-12195, typeDiscriminator-9674, declarationOrder-7298, isRecordPageOf-12715 green unchanged. Sweep: 291 ui test files naming RecordDetailView/appObjectScope/usePageAssignment/readAppObjectScope, 3 locked batches at 2 workers: 98/98 788 tests, 98/98 654, 95/95 847. Spec: stack-app-record-page + metadata-form-zod-reconciliation 103/103. Post-merge at 5b257756: 18 files 113 tests, spec 103/103. Ablations via scripts/ablation-replace.mjs (anchor 1->0, blob changed, restored blob == HEAD, git diff HEAD empty; ui vitest aliases @object-ui/* to src so no dist leg): A raw segment -> record-view 3 failed/3 passed; B hook ignores app -> 8 failed/11 passed; C picker ignores app -> 2 failed/1 passed. Live: app-crm --fresh + Vite console on branch source; sys_user page crm_member with apps ['crm_app'] saved by PUT /api/v1/meta/page (200): /apps/com.example.crm/sys_user/record/ID opens crm_member, /apps/crm_app/... canonicalises and opens it, /apps/com.objectstack.setup/... opens sys_user_detail; before leg (three ui files from origin/main) CRM opened sys_user_detail. Screenshots /mnt/project-files/issue-29592/.", "gates": "dispatch-gates --commands derived 107 at 0275f773: 107 run, all exit 0; --ran: 107 run, 0 NOT-MEASURED, 0 UNRUN (vitest run scripts/ 170 files 4972 tests and check:type-check-debt under the lock). --tier: no path-derived mandate (page.zod.ts Clause-2 suspect hint; describe text only). After merge 5b257756: check:generated 0, check:liveness 0, check:type-check-debt 0, check-changeset-no-major 0, check-empty-changeset 0, check:nul-bytes 0. Type-check react/app-shell/spec 0; eslint touched ui files 0 errors. PR body gates: changeset-no-major 0, adr-0087-registration 0, closing-keyword-parity 0, partof-closing-keyword 0. CI check-runs: in_progress at report time.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "deviations": [ "Spec surface beyond page.json + page.zod.ts, accepted by the seat before code: stack-app-record-page.test.ts pin flip, state-counts/page.md and references/ui/page.mdx regenerated, metadata-form-zod-reconciliation.test.ts omit row reason rewritten (row kept, no page.form.ts growth).", "usePageAssignment gained no parameter: the existing reserved app option is now read (seat: Clause-2 no); option doc updated, changeset says it names the owning app.", "Record drawer included by seat ruling; host file ui/packages/app-shell/src/views/RecordDetailView.tsx (embedded under /apps/:appName/:objectName).", "Live measurement used a runtime-saved page on app-crm, not the duly package (not in this repo); the ruling's duly pin is a test composition." ], "mcp_calls": "3 - mcp__github__issue_read (get x1, get_comments x2); no write tools", "api_writes": "2 - relay pr_create (objectstack#29631), post-stamped os-dev-report comment on #29592", "open_questions": [], "security_findings": "withheld", "out_of_scope_findings": [ "carrier: seat at ACCEPT - Studio page form does not offer page.apps (no app-name widget); whether to file a follow-up card for the form offer. noted, not filed" ] }objectstack-fleet commentedon Nov 2, 2026
domain:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ
Landed · 2026-11-02T23:33Z · PR #29631 merged through the queue as
3482f3d8; the merge commit carries all 13 files of the accepted head5b257756(usePageAssignmentreading itsappoption,RecordDetailViewresolving the route's app bymatchAppBySegmenton the route and in the drawer,appObjectScopedeclaredRecordPageasking the same order, thepage.appsliveness flip tolivewith the describe and its generated followers, the reconciliation omit row's new reason, three test files, the changeset). Delivered in full: ruling A on #28979 is now live in the console;pm:dispatchedremoved. Noted, not filed: the Studio page form does not offerapps(no app-name list widget). Next on this resolver: #29552 leg 1 can now build onmain.