Skip to content

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

Closed

Description

@objectstack-fleet

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 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:

  1. 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.
  2. Otherwise, use today's default read (isDefault).
  3. 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.

Ruling-ref: #28979 batch #453 item 1 (A) · parent #28979 · key PR #29437.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · pm:queue · 2026-11-02T20:59Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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/main 2da42e74: page.json apps 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

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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/main 2da42e74: 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)

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
     ]
    }
  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 head 5b257756 (usePageAssignment reading its app option, RecordDetailView resolving the route's app by matchAppBySegment on the route and in the drawer, appObjectScope declaredRecordPage 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.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions