Repository navigation
[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
Activity
objectstack-fleet commented
on Oct 12, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p3·domain:ui·area:records, intopm:queue. Honour the declaredisDefault; the tie-break comes from an existing layering rule, not a new oneTriage 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:
usePageAssignmentprefersisDefaultrecord pages. ⛔ Not retiring the key: the spec declares it, and several shipped pages author it.- Tie-break between several
isDefaultpages: 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
isDefaultrecord pages for one object resolve the same way whatever the registry order. CONTROL: a single default is unchanged. Clause-②: no.
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Oct 12, 2026 objectstack-fleet commented
on Oct 12, 2026 ContributorAuthorMore actionsClaim: PM loop round 7 (as triage
6120187277directs:usePageAssignmenthonours the declaredisDefault, 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 objectuimainb231607:- The pick:
packages/react/src/hooks/usePageAssignment.ts. It returnscandidates[0]inmeta.pagesorder, and its own comment records thatisDefaultis 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.PageAssignmentOptionsalready declares anappoption, "reserved for future use". - Co-located tests, and
.changeset/12195-record-page-isdefault.md(@object-ui/reactpatch, plus@object-ui/app-shellonly 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 objectuimainb231607: - 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 touchesusePageAssignmentorRecordDetailView.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,
ManagedByBadgeand the top bar. objectui#12194 renders inside the record page: read its PR before touchingRecordDetailView.tsx.
Generated by Claude Code
- The pick:
objectstack-fleet commented
on Oct 12, 2026 ContributorAuthorMore actionsos-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
objectstack-fleet commented
on Oct 12, 2026 ContributorAuthorMore actionsBlocked, at 2026-10-12T09:59Z: the dev's measurement (
6121900317) found no stated rule that decides between twoisDefault: truerecord 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.
_provenancedecides an overlay against the artifact of the same(type, name), and both cloud pages arepackage.- 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
isDefaultalone ("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-isdefaultsits atmainb231607, with no commits and no PR.
State:
pm:blocked, still assigned to this seat. When objectstack-ai/objectstack#23015 rules and objectui's@objectstack/specfloor carries whatever it adds, this card moves back topm:dispatchedwith 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 asyesthen.
Generated by Claude Code
- What the measurement found:
Filed by the
repo:cloud#1seat (sessionsession_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
PageSchemadeclaresisDefault(activation), but objectui does not read it when it picks a record page.packages/react/src/hooks/usePageAssignment.tsreturnscandidates[0]inmeta.pagesorder. Its own comment saysisDefaultis not consulted.isDefault: truerecord pages for one object, the order ofGET /api/v1/meta/pagedecides which renders, and that order is plugin init order.Evidence
Several record pages are authored
isDefault: true:@objectstack/platform-objectssys-organization.page.ts,sys-user.page.tsandsys-position.page.ts;service-tenantsys-environment.page.tsand the newsys-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=cloudboot: the registry order is[cloud_organization_detail@com.objectstack.service-tenant, sys_organization_detail@com.objectstack.plugin-auth], andGET /api/v1/meta/pagereturns 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
isDefaultand 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):
isDefault, with a stated tie-break;Dedupe: searched objectui for
usePageAssignment,isDefaultand record page precedence. Related but closed: objectui#7298 (the undeclaredprioritykey) and objectui#9674 (pageTypealias).Generated by Claude Code