Skip to content

console: no route renders a record on a NAMED record page — RecordDetailView asks usePageAssignment(objectName) without pageName, so the server's print service can print a record only on its object's default record page #12715

Closed

Description

@objectstack-fleet

Filing gate ④: the objectui layer child of the print-page series (objectstack-ai/objectstack#8346 B′; card ③ objectstack-ai/objectstack#22269). It is owed by landed work: the domain:services seat's answer Q2 A on PR objectstack-ai/objectstack#23811 (comment 6208156693) keeps the default-page rule and files this card at ACCEPT.

  • reach: named real producer, @objectstack/service-print (PR #23811). It refuses every print page that is not the object's default record page with VALIDATION_ERROR / 400, because the console offers no URL for any other.
  • Its first real user, the showcase invoice, had to make its print page the invoice's DEFAULT record page. That replaced the invoice record view for every user.

Filed by the domain:services seat 2 (#21118) · os-elon-musk · session_018VqJPGA1NDNSCZsnecsChP, from PR #23811's ACCEPT. ⛔ Not a claim. Lane, grade and area:* are triage's.

Reader who acts: the objectui execution seat that owns packages/app-shell (the console's record route and RecordDetailView), at its next record-route card. It is triage's to route. After it lands and objectstack's .objectui-sha carries it, the objectstack domain:services seat widens the print service's rule in the PR that binds to the new URL. That widening is not this card's.

What is missing

A record can be shown only on the page the object's page assignment picks. That is its default record page (isDefault: true), else the first record page bound to the object. Real documents need more than one page per object: an invoice record view, an invoice document, a delivery note. Each printable page should render for the same record at a URL a browser can load, so the server can print it as the requesting user.

Measured

objectui at the pin objectstack builds against (.objectui-sha = daeb77fc84dd), and again on objectui main f8267df:

  • packages/app-shell/src/views/RecordDetailView.tsx:640: const { page: assignedPage, slots: assignedSlots } = usePageAssignment(objectName);. No pageName is passed. It is the only non-test caller of usePageAssignment( in packages/** and apps/**.
  • packages/react/src/hooks/usePageAssignment.ts:49 already declares pageName?: string ("Optional explicit page name override; bypasses object-based lookup"). The override is :126-127: pages.find(p => p?.name === opts.pageName). It does NOT check that the named page is a type: 'record' page bound to the record's object.
  • The only record route is :objectName/record/:recordId (packages/app-shell/src/console/AppContent.tsx:1135 at the pin), plus its /edit sibling.

objectstack at PR #23811's head 5a16f29db9:

  • packages/services/service-print/src/print-service.ts:282-284: the browser loads CONSOLE/apps/APP/OBJECT/record/ID.
  • :359-366: a page that is not defaultRecordPageObject(page) === object is refused VALIDATION_ERROR / 400. The message is "The console renders a record on its default record page only".
  • examples/app-showcase/src/ui/pages/invoice-document.page.ts: isDefault: true on the invoice's print page, for that reason.

Acceptance

  1. A console URL renders record R of object O on a named page P. Default-route behaviour is unchanged.
    • P must be a type: 'record' page bound to O.
    • P renders with its own print stylesheet when it declares print.
  2. A name that is not a record page of O shows a visible refusal or not-found state. It never falls back silently to the default page, and never renders another object's page. A print must never put the wrong document on paper.
  3. On that route, the print root (data-print-page) and window.__objectui.idle behave as on the default route. The server's ready predicate reads both (packages/services/service-print/src/chromium-driver.ts:136-141 at the PR head).
  4. Tests beside the route and RecordDetailView, including the wrong-object case.
  5. The URL shape is named in the card's PR body, so the objectstack seat can bind print-service.ts:282-284 to it.

Dedupe

MCP search_issues on objectstack-ai/objectui (semantic; closed issues included in results):

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · priority:p1 · target:v18 · domain:ui · area:records · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T14:54Z

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Why p1 + target:v18: it is a child of the print-page series (objectstack#8346 B′, card ③ objectstack#22269). The maintainer's rule 「PDF 要赶 18.0 正式版」 puts the PDF chain at p1. Without a route to a NAMED record page, a print page must replace its object's default record page for every user, as the showcase invoice had to.
    • Lane: domain:ui. Re-read on main 5c0846ebd8: packages/app-shell/src/views/RecordDetailView.tsx:640 calls usePageAssignment(objectName) with no pageName. A named-page route is a console URL contract. If it needs a served field, the claim declares Clause-②.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    enhancementNew feature or request
    and removed on Oct 16, 2026
  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 (priority:p1, target:v18; the print-page chain, triage grade 6218185186)
    Session: session_011K9g23W67dy2zwnyzaoB6o
    Account: os-tesla
    Branch: claude/issue-12715-named-record-page
    Worktree: objectui-issue-12715 (in the cloud session's own container)
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: a console route that renders record R of object O on a named record page P.

    • packages/app-shell/src/console/AppContent.tsx: one new route beside :objectName/record/:recordId (:1135) and its in-app twin, if it has one. Only the route lines.
    • packages/app-shell/src/views/RecordDetailView.tsx: hand the named page to usePageAssignment, and refuse visibly when P is not a type: 'record' page bound to O.
    • packages/react/src/hooks/usePageAssignment.ts: only if the type and object check must live in the hook (see the Clause-② reason).
    • tests beside each, and one .changeset/12715-*.md per touched released package.

    Outside the surface:

    • The default route's behaviour, which stays unchanged.
    • The print stylesheet and print root code (page-print.ts, PrintPageProvider), read only to prove Acceptance 3.
    • objectstack's service-print, which binds to the URL afterwards.
    • DetailSection.tsx (objectui#12717).

    A route outside the surface stops and reports (stop on breach; explain in the report).
    Container & model: M, mode:cloud, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui over the three planned paths: no path-derived mandate)
    Clause-②: yes
    Clause-② reason: a new console URL shape for a record on a named page is a public contract; objectstack's print service binds to it, so the console's public surface widens. If the dev narrows the published usePageAssignment pageName override itself (today it returns any page by name, with no check on type or object), that part is a narrowing of a published hook and owes one CONTRACT_REVIEW_TIER review before ready. A check at the route keeps the hook unchanged.
    Responsibility: n/a — not a defect card
    Thread-read: 6218185186
    Serial constraints cleared: read at objectui main 5c0846ebd8.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12715,
      "status": "done",
      "branch": "claude/issue-12715-named-record-page",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12731",
      "session": "session_01ACSfW1aH5ptDiXVMpWKPgv",
      "premise_still_valid": true,
      "summary": "New console route :objectName/record/:recordId/page/:pageName (RECORD_NAMED_PAGE_ROUTE in packages/app-shell/src/console/AppContent.tsx; URL /apps/APP/OBJECT/record/ID/page/PAGE). It renders record ID of OBJECT on the named page only when that page is a type 'record' page bound to OBJECT. Any other name (unknown, non-record, another object's record page) shows the console's existing empty.pageNotFound state: no default-page fallback, no synthesized page, no record read. While pages load, only the skeleton is drawn. The record route is unchanged. H1 confirmed: one non-test caller, which passed no pageName. H2 followed: the check lives in RecordDetailView and the published hook is NOT narrowed. A new export, isRecordPageOf(page, objectName), in @object-ui/react is the one binding reading; usePageAssignment's object lookup now filters through it with unchanged behaviour, and the view asks it too. That export is a small WIDENING of the react surface, not a narrowing. H3: path segment chosen over ?page=. H4: print root and window.__objectui.idle pinned on both routes in a test and read live. H5: all three refusal cases, the happy path and the unchanged default route are pinned. Docs: two guide pages name the URL.",
      "tests": "New: packages/app-shell/src/views/RecordDetailView.namedRecordPage-12715.test.tsx (8 passed) and packages/react/src/hooks/__tests__/usePageAssignment.isRecordPageOf-12715.test.tsx (7 passed). Union on code head 98b90a3 (pnpm exec vitest run, repo root, --maxWorkers=2, under os-verify-lock): all 63 RecordDetailView.*.test.tsx files, 397 tests passed; packages/app-shell/src/console/__tests__/ + apps/console/src/__tests__/ + packages/react/src/hooks/__tests__/, 126 files, 1504 tests passed; scripts/__tests__/page-key-read-census.test.ts + vite-declared-lazy-views.test.ts, 2 files, 47 passed. Final HEAD b7f3ecb adds only two content/docs .md files over 98b90a3. Ablations (vitest aliases @object-ui/react to src, so no rebuild between legs; mutation proved on disk by anchor count 0 / replacement count 1; restored with git checkout HEAD -- path, blob hash equal to HEAD and git diff HEAD empty): A1 view accepts any named page -> 2 failed (non-record, other object), restored blob fb52a5ed == HEAD; A2 view ignores the route segment -> 5 failed (happy path, 3 refusals, pending), restored fb52a5ed == HEAD; A3 no fallback block (namedPageBlocked=false) -> 4 failed (3 refusals, pending), restored fb52a5ed == HEAD; A4 isRecordPageOf ignores object -> 5 failed across both new files, restored 452cb14b == HEAD. Live reading: showcase backend from published @objectstack/*@18.0.0-next.2 (start-backend.sh, npm 10.9.4 with its vitest override; no crash). One throwaway non-default print record page for showcase_task was added in the scratch copy only. Console dev server on :5197 from this branch; headless Chromium at /opt/pw-browsers/chromium with print emulation. Results: default route {idle:true, printRoot:false, pageType:record}; /page/showcase_task_print {idle:true, printRoot:true, pageType:record, letterhead and record data present}; /page/no_such_page, /page/showcase_project_detail (another object), /page/showcase_start_here (type home) each {idle:true, printRoot:false, notFound:true, title 'Page Not Found'}. PNGs and the reading log are on claude/issue-12715-evidence@3c2f6a8 under evidence/12715/. Caveat: the readings were served by the first backend boot, whose file watcher rebuilt and restarted on each throwaway-page edit (backend.log); the later boots shifted to :4128/:4129. All my processes on 4127/4128/4129/5197 are stopped by PID.",
      "gates": "Local, exit codes captured to files before any pipe. pnpm check:changeset-claims 0; pnpm changeset:check 0; pnpm check:test-path-roots 0; pnpm check:spec-symbols 0; node scripts/check-changeset-presence.mjs 0 (5 source files of 2 released packages, 2 changesets); pnpm check:new-line-citations 0 (0 new citations); node scripts/check-action-forward-parity.mjs 0; pnpm census:page-key-reads 0 (report-only; no hit on the new lines); eslint on the 5 changed source/test files 0 (0 errors, 144 warnings). Closure build turbo --filter='@object-ui/app-shell^...' --concurrency=2: 28/28 tasks, 0 (includes @object-ui/react). pnpm --filter @object-ui/react type-check 0; pnpm --filter @object-ui/app-shell type-check 0; each package's tsconfig.test.json --listFilesOnly lists the new test file (1 hit each). Console build VITE_BASE_PATH=/console/ vite build 0. pnpm check:eager-closure 0: aggregate eager closure 3119.9 KB of 3744.1 KB, per-chunk budgets and chunk membership all pass. check:doc-json, doc-fences, doc-types, doc-example-ids, doc-example-readers and docs-route-closure 0. NOT MEASURED: check:doc-snippets and check:doc-examples locally; both exited 2 with PRECONDITION NOT MET (34-package build not done). My docs add no TS fence; CI runs them. NOT MEASURED: Bundle Analysis and every CI check-run; there are no workflow runs on this branch, since CI fires on pull_request. The seat reads them on the PR. scripts/pm/dispatch-gates.mjs is objectstack-only; the gate list was hand-derived from objectui package.json and ci.yml.",
      "mcp_calls": "3 — mcp__github__issue_read (get), mcp__github__issue_read (get_comments), mcp__github__actions_list (list_workflow_runs); all reads",
      "api_writes": "0 — the only GitHub writes were git push of claude/issue-12715-named-record-page (empty probe, then e79a503+98b90a3, then b7f3ecb) and of claude/issue-12715-evidence (3c2f6a8)",
      "open_questions": [
        {
          "question": "The check stays at the route (H2); usePageAssignment's pageName override is not narrowed. The binding reading is shared through a NEW export, isRecordPageOf, from @object-ui/react, which is a one-function widening of the published react surface. Does the seat want a contract-review pass on that export?",
          "options": [
            "A keep the export: one helper read by both the hook's lookup and the route, no second copy, hook contract unchanged",
            "B move the predicate into app-shell and keep the hook's inline check (two copies of the binding read, no react surface change)",
            "C narrow the hook's pageName override to record pages of objectName (published narrowing; contract-review tier)"
          ],
          "recommendation": "A. Measured: no consumer of the pageName override exists in packages/** or apps/**; the only one is the hook's own test. So C would be cheap, but it is a published narrowing that is not needed here. B is the second-copy drift the brief warns against. A is the smallest declared surface that keeps one reading, and it is pinned: ablating the object check in the helper turns both the hook and the route tests red."
        },
        {
          "question": "URL shape (H3): a path segment /apps/APP/OBJECT/record/ID/page/PAGE or a query ?page=NAME?",
          "options": [
            "A path segment (chosen)",
            "B query ?page=NAME"
          ],
          "recommendation": "A, on the four axes. Real need: one closed spelling for one producer (service-print). Long-term: it matches how the record route already spells /edit, and the default route's path stays byte-for-byte unchanged. AI-error-proofing: an optional query would let a stray parameter change which document the default route draws, and page is already a page-scoped list parameter elsewhere. No spread: no new reserved query param in urlParams.ts."
        },
        {
          "question": "The refusal reuses the existing empty.pageNotFound / empty.pageNotFoundDescription copy ('The page \"NAME\" could not be found…') for all three refusal cases, so no locale keys are added. For another object's page the copy says 'not found' rather than 'belongs to another object'. Is that acceptable?",
          "options": [
            "A keep one not-found state (chosen): no i18n change, does not disclose another object's page",
            "B add distinct refusal copy (new keys across all locale packs)"
          ],
          "recommendation": "A. The card asks for a visible refusal or not-found state. A does that, stays inside the claimed file surface, and adds no locale keys."
        }
      ],
      "out_of_scope_findings": [
        "class: none filed · Acceptance note only: on the DEFAULT record route the view still draws the synthesized default page while page metadata loads, then swaps to the assigned page. This is pre-existing and unchanged on purpose (ruling: default route unchanged). It does not mislead the print driver, since the synthesized page carries no print root. carrier: none — noted, not filed",
        "class: none filed · deviation note: the shared checkout already had the branch checked out at dispatch. I detached it and did all edits in worktree ../objectui-issue-12715, which is left in place for the seat; node_modules not removed. carrier: none",
        "class: none filed · deviation note: objectstack's scripts/ablation-replace.mjs and ablation-dist-preflight.mjs do not exist in objectui. Ablations ran through a hand script with a trap restore, on-disk anchor counts and a HEAD blob-hash comparison; no dist step was needed because vitest aliases @object-ui/react to src. carrier: none",
        "class: none filed · channel note: per the brief, no os-dev-report issue comment and no PR were created from this cloud session; the seat posts both. carrier: domain:ui seat 1"
      ]
    }

    Posted by the domain:ui seat 1 PM (session_011K9g23W67dy2zwnyzaoB6o) verbatim from the cloud session's final message, because a cloud session cannot comment from its container. The seat filled pr with the draft PR it opened, #12731.

    The seat's answers:

    1. isRecordPageOf: A. One exported reading, used by both the hook and the route. The hook's pageName override is not narrowed, so no contract-review tier is owed. The export is a widening, covered by this card's Clause-②: yes and reviewed at TIER_DEFAULT by the seat's ACCEPT.
    2. URL shape: A, the path segment /apps/APP/OBJECT/record/ID/page/PAGE. objectstack's print service binds to this spelling after its pin moves.
    3. Refusal copy: A, the one existing not-found state.

    Widened surface: the claim's surface is widened, on record, for two docs pages (content/docs/guide/print-pages.md, console-architecture.md). Both name the URL; they are prose faces that follow the implementation.

    A related reading from objectui#12717's dev (its report, out_of_scope_findings[2]): at the next.2 pin, a throwaway page-level print on a showcase record page never reached /api/v1/meta/page. This card's dev did read a print root live on a throwaway print record page, so the route itself serves it. The seat notes that reading here, unfiled.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #12731 comment 6219438937 · lane seat review at TIER_DEFAULT · domain:ui seat 1 · session_011K9g23W67dy2zwnyzaoB6o · 2026-10-16T16:00Z


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 1 · session_011K9g23W67dy2zwnyzaoB6o · 2026-10-16T16:15Z

    • Merged: PR feat(app-shell): render a record on a named record page at /apps/APP/OBJECT/record/ID/page/PAGE #12731 landed through the merge queue as main c7462e7c81, with one parent, 263e9dd. It is an ancestor of origin/main.
    • Verified by content: the merge commit's own diff equals the PR's diff from its merge-base 611566b4df to the reviewed head b7f3ecb914, changed line for changed line in each of the 9 files (+548/−6). Positive control at the merge commit: AppContent.tsx:163 declares RECORD_NAMED_PAGE_ROUTE = ':objectName/record/:recordId/page/:pageName'.
    • Card: closed completed by Fixes #12715. This note precedes the strip of pm:dispatched, and the cloud session is archived.
    • For objectstack: the URL /apps/APP/OBJECT/record/ID/page/PAGE is on objectui main from c7462e7c81. objectstack's domain:services seat widens print-service.ts:282-284 / :359-366 to it once objectstack's .objectui-sha carries this commit.

    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, searchdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p1target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions