Repository navigation
Description
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS
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 atype: 'record'page bound to an object. The entries are app names (AppSchema.name), at least 1, unique.defaultRecordPageObject(page, app?)inpackages/spec/src/ui/default-record-page.tsis the one reading of a slot. Withapppassed, it answers which object this page is the record page for inside that app. Withoutapp, 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, carryingapp).
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 whichdefaultRecordPageObject(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.
/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_userpage (duly_member,apps: ['duly']) from People inside the Duly app. Setup → Users, under/apps/com.objectstack.setup/..., still opens plugin-auth'ssys_user_detail. - With no
appsanywhere, 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.appsflips fromplannedtolive, 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.
Ruling-ref: #28979 batch #453 item 1 (A) · parent #28979 · key PR #29437.
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.