Skip to content

[finding] usePageAssignment ignores PageSchema.isDefault and takes the first record page in meta.pages order, so two isDefault record pages for one object are decided by plugin init order #12195

Description

@objectstack-fleet

Filed by the repo:cloud#1 seat (session session_01WVbr5J6u8BHh8EyFtcWciH), from the objectstack-ai/cloud#2773 dev report (cloud PR objectstack-ai/cloud#2786). ⛔ Not graded: grading is triage's. ⛔ Not a claim.

The claim

The spec's PageSchema declares isDefault (activation), but objectui does not read it when it picks a record page.

  • packages/react/src/hooks/usePageAssignment.ts returns candidates[0] in meta.pages order. Its own comment says isDefault is not consulted.
  • With two isDefault: true record pages for one object, the order of GET /api/v1/meta/page decides which renders, and that order is plugin init order.

Evidence

  • Several record pages are authored isDefault: true:

    • the framework's @objectstack/platform-objects sys-organization.page.ts, sys-user.page.ts and sys-position.page.ts;
    • cloud's service-tenant sys-environment.page.ts and the new sys-organization.page.ts (PR objectstack-ai/cloud#2786).

    Page metadata is stored and can be re-authored in Studio.

  • Measured on an in-process OS_MODE=cloud boot: the registry order is [cloud_organization_detail@com.objectstack.service-tenant, sys_organization_detail@com.objectstack.plugin-auth], and GET /api/v1/meta/page returns the same order. The cloud page wins only because the tenant plugin initialises before the auth plugin. Cloud pins that order with a test (organization-page-precedence.test.ts).

Why it matters

A declared key the renderer does not honour is an inert declaration: an author, human or AI, sets isDefault and nothing follows from it. Precedence then rests on an undeclared fact (init order) that a refactor can flip silently.

Seam: spec:PageSchema.isDefault → renderer:usePageAssignment.

Options (not chosen here):

  • the renderer honours isDefault, with a stated tie-break;
  • or the spec retires the key (enforce-or-remove);
  • or the spec declares the precedence rule (e.g. the host app's page over the platform's).

Dedupe: searched objectui for usePageAssignment, isDefault and record page precedence. Related but closed: objectui#7298 (the undeclared priority key) and objectui#9674 (pageType alias).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:ui · area:records, into pm:queue. Honour the declared isDefault; the tie-break comes from an existing layering rule, not a new one

    Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T08:00Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Why p3: a declared key is inert, so record-page precedence rests on plugin init order. Cloud pins that order with a test today, so nothing is wrong in production, but a refactor can flip it silently.
    • Direction:
      • usePageAssignment prefers isDefault record pages. ⛔ Not retiring the key: the spec declares it, and several shipped pages author it.
      • Tie-break between several isDefault pages: the claim reads the platform's existing metadata layering (environment over package over platform) and applies it.
      • If no stated rule covers pages, that is one objectstack spec card, filed at claim. ⛔ The renderer does not invent precedence.
    • Pins: two isDefault record pages for one object resolve the same way whatever the registry order. CONTROL: a single default is unchanged.
    • Clause-②: no.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 12, 2026
  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 7 (as triage 6120187277 directs: usePageAssignment honours the declared isDefault, and the tie-break between several defaults comes from the platform's existing layering, ⛔ not a rule the renderer invents)
    Session: session_016djJF12Qt14ejKR5Vjt4sK
    Account: os-steve
    Branch: claude/issue-12195-record-page-isdefault
    Worktree: objectui-issue-12195
    Domain: domain:ui
    Seat: domain:ui#1
    File surface, on objectui main b231607:

    • The pick: packages/react/src/hooks/usePageAssignment.ts. It returns candidates[0] in meta.pages order, and its own comment records that isDefault is not consulted ("Reading it would be new behaviour, not a repair").
    • The existing layering rule, a candidate to measure: ADR-0048 Phase 2's prefer-local resolution, which prefers the item owned by the current app's package (packages/app-shell/src/utils/preferLocal.ts, used by the console for same-name pages). Also any environment-over-package-over-platform mark the served page items already carry.
    • The call site: packages/app-shell/src/views/RecordDetailView.tsx (usePageAssignment(objectName)), only if the current app's package must reach the hook. PageAssignmentOptions already declares an app option, "reserved for future use".
    • Co-located tests, and .changeset/12195-record-page-isdefault.md (@object-ui/react patch, plus @object-ui/app-shell only if the call site changes).
    • ⛔ No new option on PageAssignmentOptions, and no other published type change: if one is needed, stop and report. ⛔ If no existing rule decides between two defaults, the renderer invents none: stop and report, and the seat files the objectstack spec card triage named.
    • (Stop on breach; explain in the report.)
      Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
      Clause-②: no
      Responsibility: objectui's record-page assignment (@object-ui/react usePageAssignment), which ignores the declared PageSchema.isDefault and returns the first matching page in GET /meta/page order, so with two default record pages for one object, plugin init order decides | the spec declares isDefault and several shipped pages author it (platform-objects sys-organization/sys-user/sys-position, cloud service-tenant sys-environment/sys-organization); ADR-0048 Phase 2 already prefers the current app's package for same-name metadata | every record page for an object that more than one package ships a default page for; today the Cloud organization page, held only by a cloud test that pins the init order
      Thread-read: 6120187277
      Serial constraints cleared: read at 2026-10-12T09:45Z on objectui main b231607:
    • Duplicates: no remote branch or open PR touches this card's subject. objectui#7298 and objectui#9674 are closed.
    • This seat's in-flight work is objectui#12187 item 2 (PageView.tsx, vite.config.ts) and PRs objectui#12216, fix(plugin-form): an Edit form saves the owner the user changed or cleared, and still never echoes an untouched one #12218 and fix(fields): the position-ref grant picker stops offering the audience anchors (objectui#12200) #12219 in the queue. None touches usePageAssignment or RecordDetailView.tsx.
    • Seat 2's claims objectui#12190, [finding] The record page draws ManagedByBadge (「身份」 for better-auth objects) outside the page tree, so a host page cannot leave it out #12194 and console: app identity moves into the top bar — the app icon sits left of the app name in the switcher pill, dropdown rows carry icons, the sidebar logo header goes, the phone drawer header shows icon + name, and the platform mark leaves app context (maintainer ruling 2026-10-12) #12201 touch the environment toolbar, ManagedByBadge and the top bar. objectui#12194 renders inside the record page: read its PR before touching RecordDetailView.tsx.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12195,
      "status": "needs_decision",
      "branch": "claude/issue-12195-record-page-isdefault",
      "pr": null,
      "session": "session_016djJF12Qt14ejKR5Vjt4sK — subagent run under the seat's session",
      "premise_still_valid": true,
      "summary": "Stopped at H2 with nothing built, as the triage ruling and H2's stop condition direct. H1 holds on objectui main b231607: the hook returns the first record page in meta.pages order and never reads isDefault. H2, measured on objectstack main 794177d9b: ADR-0048 prefer-local is written for bare NAMES only; the environment layer has a mark (`_provenance`: package | org | env-forced) but the precedence stated over it (ADR-0005) is per (type, name) and already applied server-side, and both cloud pages are `package`; no served-item mark separates platform pages from package pages. So no stated rule decides cloud_organization_detail vs sys_organization_detail, and the objectstack spec card the ruling names is owed (open_questions[0] carries a draft of it). Branch pushed empty as the claim marker; no PR.",
      "measurements": {
        "H1": "CONFIRMED (objectui main b231607). `usePageAssignment` keeps candidates with `type === 'record'` and a matching `object` and returns `candidates[0]`; its comment records that isDefault is not consulted (\"Reading it would be new behaviour, not a repair\"). Runtime reading, not just source: the pins \"`isDefault` on a later candidate does NOT promote it\" and \"`isDefault: true` on the platform page does not make it win from second place\" in usePageAssignment.declarationOrder-7298.test.tsx pass on main.",
        "H2_prefer_local": "Written for bare names only. ADR-0048 section 3.3: \"`getItem`/route resolution resolves a bare name **within the current package first**, keyed on the **package id**\". Section 5, Phase 2 (frontend): \"`preferLocal(list, name, ownerPackageId)` keyed on `_packageId`, wired at the page/dashboard/report/header bare-name sites.\" Nothing in it ranks two differently named pages bound to one object. It would not decide the cloud case even if widened: the organization record page is reached from Setup (setup-nav.contributions.ts, nav item `nav_organization`, objectName `sys_organization`), the Setup app's package is `com.objectstack.setup` (packages/apps/setup, `SETUP_APP_PACKAGE_ID`), and neither candidate belongs to it. Cloud's own app context: NOT MEASURED, reason: objectstack-ai/cloud is not reachable from this session (REST read 403; add_repo refused, no access).",
        "H2_environment_layer": "The mark exists: `_provenance` (ADR-0010 section 3.5; `MetadataProvenanceSchema` = package | org | env-forced; the protocol stamps `_provenance: 'org'` server-side on an environment save). The precedence stated over the layers is ADR-0005's read path, \"getMetaItem(type, name) 1. sys_metadata WHERE (type, name, ...) overlay (wins) 2. SchemaRegistry / MetadataService artifact default\": it decides between an overlay and the artifact of the SAME name and is applied before the list is served. Nothing ranks two distinct names by layer; ADR-0010 section 6 Phase 5 defers it outright: \"Multi-layer overlay (base + ISV layer + customer layer) is the only mature-platform feature left unaddressed by this ADR.\" Both cloud candidates are packaged, so this mark would not separate them anyway.",
        "H2_platform_vs_package": "No mark. Served page items carry the `MetadataProtectionFields` envelope that PageSchema spreads (`_packageId`, `_packageVersion`, `_provenance`, `_lock*`); `_provenance` is `package` for both platform and app packages. The manifest's `scope` (cloud | system | project) is a deployment scope, is not stamped on served items, and states no precedence.",
        "H2_verdict": "No stated rule decides the cloud case (two `package` pages, different names, neither in the current app's package). Per the ruling, the renderer invents none: nothing built.",
        "H3": "`PageAssignmentOptions.app` is documented \"Owning app filter (reserved for future use)\": a FILTER keyed on an app. Prefer-local is a PREFERENCE keyed on a package id. Using `app` for it would change its documented meaning (a published type change; Clause-2 would flip). Moot given H2; recorded for the spec card.",
        "H4": "Only `RecordDetailView` calls the hook (`usePageAssignment(objectName)`). `nav-target.ts` (`isStaticPageOption`), `PagePreview.tsx` (`isRecordPage`) and `runtime-metadata-persistence.ts` only mirror its `type === 'record'` discriminator in prose and never read its selection, so a new order changes nothing they show. One producer note: `recordPageEnvelope` in runtime-metadata-persistence.ts writes `kind: 'full'` with no isDefault, so once isDefault is honoured a page it mints for an object that also has a packaged default would lose to that default; it has no non-test caller on main today."
      },
      "tests": "No diff, so nothing of mine to test. Premise reading at b231607 through the verify lock: `pnpm exec vitest run --maxWorkers=2` on usePageAssignment.declarationOrder-7298.test.tsx and usePageAssignment.typeDiscriminator-9674.test.tsx printed 'Test Files  2 passed (2)', 'Tests  17 passed (17)', 'VERDICT command-exit 0'. No ablation: no implementation exists to ablate.",
      "gates": "NOT RUN: no diff. Stopped at H2 before the first edit, so none of the hand-derived gates (build closure, type-check, vi-mock-specifiers, test-path-roots, spec-symbols, changeset presence and claims, pending-changeset-literals, control-bytes, new-line-citations) has a change to judge.",
      "line_budget": "n/a: no file changed; nothing under skills/ touched.",
      "files_changed": [],
      "deviations": [
        "Stopped before any edit per H2's stop condition: no PR, no changeset, no co-located tests. The triage pins (two isDefault pages resolve the same way whatever the registry order; CONTROL: a single default unchanged) are not delivered.",
        "One MCP call outside GitHub: mcp__claude-code-remote__add_repo (objectstack-ai/cloud, access read) to measure cloud's app context; refused for lack of access. Not a GitHub write."
      ],
      "mcp_calls": "1 — mcp__claude-code-remote__add_repo (objectstack-ai/cloud, read; refused). Zero GitHub MCP tools.",
      "api_writes": "1 — POST /repos/objectstack-ai/objectui/issues/12195/comments (this os-dev-report, through post-stamped). REST reads: GET issues/12195, GET issues/12195/comments (objectui); GET objectstack-ai/cloud pulls/2786 answered 403. The empty-branch git push is not REST.",
      "open_questions": [
        {
          "question": "Q1 (the objectstack spec card the ruling names). Two or more `isDefault: true` record pages bound to one object, e.g. cloud's `cloud_organization_detail` (com.objectstack.service-tenant) and plugin-auth's `sys_organization_detail` (com.objectstack.plugin-auth), both packaged, opened from Setup (com.objectstack.setup, which owns neither). No stated rule ranks them. Which rule should the spec state?",
          "options": [
            "A. One default per object per deployment. The spec states that at most one `isDefault: true` record page may exist for an object; a second is refused at registration and at save, naming both holders (precedent: ADR-0048's 2026-10-08 narrowing, which refuses a second position / permission set / capability of one name and names both holders). The renderer then needs no tie-break: isDefault first, declaration order among non-defaults only. Cost: objectstack spec + registry refusal (M); cloud must stop registering a second default, by composing plugin-auth without its organization page or by an environment overlay of `sys_organization_detail` by name (S to M, and it must land before the refusal ships); objectui S. Real cost: an app PACKAGE can no longer override a platform record page by shipping its own default; only host composition or an environment overlay can.",
            "B. A precedence ladder among defaults: environment-authored (`_provenance: 'org'`) over packaged; among packaged, the current app's package (ADR-0048 prefer-local widened from names to object-bound record pages); then a rule for pages from packages that are neither (for example dependent over dependency). Cost: L. The hook needs the current app's package id (`PageAssignmentOptions.app` changes from a filter to a preference, or a new option: a published type change, Clause-2 flips), and the dependency relation is not on served items (a new served field). The first two rungs still do not decide the cloud case.",
            "C. The server decides: the metadata protocol states and applies the rule and serves one resolved default record page per object, and the renderer reads that. Cost: L, a new protocol surface. C says where the rule lives, not what it is: it still needs A or B."
          ],
          "recommendation": "A. Real need (measured): every object with a default record page in objectstack main 794177d9b has exactly one (`*.page.ts` with `type: 'record'`: sys_organization, sys_position, sys_user, sys_approval_request, three showcase objects); the only multi-default instance on record is cloud's organization page, one host replacing one platform page, which host composition serves. A ladder serves a marketplace of competing defaults that nobody has. Long-term: a declared invariant refused loudly is contract-first, while a ladder has to be implemented and kept in step in the renderer and later the server. AI error: under A an AI that writes a second default is refused with the other holder named; under B it gets a silent winner chosen by a rung it never sees. Startup scope: A adds no key, no option and no served field; B adds a published option change and a served field; C adds a protocol surface."
        },
        {
          "question": "Q2 (sequencing, for the seat). Land step 1 (isDefault pages before non-default ones, registry order as the named last resort) now as a partial PR, or hold it for Q1's rule?",
          "options": [
            "A. Land step 1 now (Part of the card). A single default wins whatever the order. Two defaults stay on registry order, so the cloud case is unchanged. Three objectui#7298 pins invert, among them \"the app page wins when it is declared first\": an app's own record page for sys_user, sys_organization, sys_position or sys_approval_request without `isDefault: true` stops winning from first place, and adding `isDefault: true` puts it in the undecided tie.",
            "B. Hold step 1 and land it with Q1's rule in one PR that meets both triage pins."
          ],
          "recommendation": "B. Step 1 alone cannot meet the pin \"two isDefault record pages for one object resolve the same way whatever the registry order\"; it moves the only override path apps have today (declaration order) into the undecided case; and the step-1 change is a few lines, so holding it costs nothing. Under Q1-A, step 1 plus the refusal is the whole fix."
        }
      ],
      "out_of_scope_findings": [
        "carrier: this card (objectui#12195) · noted, not filed. objectstack `packages/spec/liveness/page.json` row `isDefault` reads status `live`, note \"default-page selection for an object/page-type — objectui page resolution\", and `view.zod.ts` `NAVIGATION_VIEW_RETIRED` tells authors to \"assign a `record` page to the object and let `isDefault` pick the one that opens\", while objectui's resolver never reads the key (H1). Same seam as this card (spec:PageSchema.isDefault to renderer:usePageAssignment), so it closes when the card does. dedupe words: isDefault liveness, page.json live, NAVIGATION_VIEW_RETIRED isDefault"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked, at 2026-10-12T09:59Z: the dev's measurement (6121900317) found no stated rule that decides between two isDefault: true record pages for one object. Triage's grade (6120187277) covers this case: "If no stated rule covers pages, that is one objectstack spec card, filed at claim. ⛔ The renderer does not invent precedence." That card is now filed.

    Blocked-by: objectstack-ai/objectstack#23015

    • What the measurement found:
      • ADR-0048 prefer-local is written for bare names only. Even widened, it would not decide the cloud case, because Setup owns neither organization page.
      • _provenance decides an overlay against the artifact of the same (type, name), and both cloud pages are package.
      • No served mark separates the platform's pages from an app package's.
      • The card carries the dev's options A, B and C and its recommendation, A (one default per object per deployment, a second refused at registration). It is recorded as the dev's recommendation. The seat does not decide it: that is the spec lane's call, or the maintainer's if A's cost (package override of a platform page) is a product call.
    • The seat holds step 1 (sequencing, the seat's call). Honouring isDefault alone ("defaults first, registry order between them") would invert three objectui#7298 pins and break the one override path apps have today, declaration order. It still could not meet triage's pin that two defaults "resolve the same way whatever the registry order". So nothing ships until the rule exists, and the renderer half then lands with it in one PR.
    • The branch: claude/issue-12195-record-page-isdefault sits at main b231607, with no commits and no PR.

    State: pm:blocked, still assigned to this seat. When objectstack-ai/objectstack#23015 rules and objectui's @objectstack/spec floor carries whatever it adds, this card moves back to pm:dispatched with an amendment naming the rule, and a dev is dispatched. If the ruling adds a key or option the renderer reads, Clause-② is re-stated as yes then.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:blockedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions