Repository navigation
service-print: print a record on a named print page, so an app need not make its everyday record page the printed document (hotcrm REQ-0008 quote print) #26583
Description
Ruled: 6439306670 · letter A · 2026-10-27T10:21Z
Filing gate ② a decision only the maintainer can make (a new capability: which page a record prints on). Filed by the repo:hotcrm seat (huangyiirene, session session_01J8VVAnPkgK6CWrJa6t7Fk5) at the maintainer's request in the HotCRM project thread: she chose 「等平台支持打印指定页」 over shipping the quote print on the default page, then asked 「那是否应该给平台提功能建议决策卡」 (2026-10-27).
维护者速读
One-line problem. To print a quote today, HotCRM must turn the quote's everyday record page into the printed document. Reps then lose the status path, the highlights, the Details/Related tabs and the activity feed on every quote, every day, for the sake of one button.
Recommendation: A, letting service-print print a record on a named print page of its object through the console's named record-page route. That route already shipped in 18.2.0 and refuses a wrong name with a visible not-found state. Only the server half is missing.
Background
service-print(feat(service-print): print a record's print page to a PDF in headless Chromium as the requesting user, and archive it on the record #23811,b15f8a199) rendersCONSOLE/apps/APP/OBJECT/record/IDand refuses any page that is not the object's default record page (packages/services/service-print/src/print-service.ts,refuseUnlessPrintPage). The reason given at the time was that "no URL renders a named, non-default page for a record". The PR body names the missing surface as "(objectui): a route that renders a named print page for a record".- That surface has since landed. objectui#12715 added
RECORD_NAMED_PAGE_ROUTE=:objectName/record/:recordId/page/:pageName(ui/packages/app-shell/src/console/AppContent.tsx:280). It is honoured only for a record page of that object, with no fallback, and its comment says "a print of this URL can never put a different document on paper" (RecordDetailView.tsxaround line 769). It arrived with theui/importfd4083141, which is an ancestor of the 18.2.0 version commit27f38ada4. - A print page may hold only the printable block subset (ruling 「8346 B′」,
packages/spec/src/ui/page.zod.ts"The print page" section). So a default page that prints cannot also be the working view.
Governing text: content/docs/ui/print-pages.mdx currently says "The server prints a record on the page the console renders it on, which is the object's default record page … A print page that is not the default is refused." Option A rewrites that sentence. No ADR found (grep -rln "8346" docs/adr finds only an unrelated mention).
Protocol: no spec key changes. print and isDefault keep their meaning. The request body already carries page.
Premise (re-check)
git show origin/main:packages/services/service-print/src/print-service.ts | grep -n "is not the default record page"→ one hit (the refusal is still on main).git grep -n RECORD_NAMED_PAGE_ROUTE origin/main -- ui/→ the route exists.- Measured in hotcrm by the feat(identity): implement ADR-0068 unified user-context contract (D1–D4) #2312 thread on 18.1.0: converting
crm_quote's default record page to a print page removed the title, the highlights strip, the status path, the Details/Related/Attachments tabs and the Discussion/Activity feed from QTE-0004. Edit, the header actions and the attachments panel remained. The print itself works (201 in about 3.9 s, a 2-page PDF archived).
Options
| What changes | What the customer sees | Work | |
|---|---|---|---|
| A | service-print accepts any record page of the object that declares print, and renders …/record/ID/page/PAGE (the record route stays for the default page). The refusal narrows to "not a record page of this object" and "declares no print". Docs and tests follow. |
A quote, invoice or contract keeps its working page and gains a separate "document" page. An app can ship several documents per object. | domain:server, 1 PR, small: one check, one URL and its tests. The console half has landed. No measured precedent of this exact size; #23811 built the whole service (+4074/−68). |
| B | Keep the default-page rule. | Every app that prints an object gives up that object's working page, so HotCRM's quote print stays parked. | none |
| C | Let the default page print while hiding its non-printable blocks on paper. | One page serves both uses, but the document layout (terms, letterhead) shows on screen and the working blocks shape the paper. | Larger: it reopens ruling 8346 B′'s printable subset. |
In business terms: A is "the quote has a print template", like Salesforce Quote Templates, Odoo QWeb reports or Dynamics Word templates, all of which are separate from the record layout. B is "printing costs you the record screen". C is "the screen is the paper".
os-decision-facets
- ① 长远合理性:A 缩小特例。打印页和工作页各司其职,与主流 CRM 的「打印模板独立于记录布局」一致,复用已发布的具名记录页路由,不新增契约。
- ② 业务拉动:今天就撞上。hotcrm feat(identity): implement ADR-0068 unified user-context contract (D1–D4) #2312(REQ-0008 报价打印第一期)已做完并实测,被这条规则挡住,维护者选择等平台;合同打印(第二期)会再撞一次。
- ③ 防 AI 犯错:A 仍是响亮拒绝。页名不属于该对象、或页面没声明
print都回 400;控制台的具名路由对错误页名显示「找不到页面」,不会退回默认页,不会印错文档。 - ④ 不扩散:A 不新增任何键,只放宽一条服务端检查并改一个 URL;B 让每个应用各自牺牲工作页,C 会重开 8346 B′ 的可打印子集。
Prior rulings read: 8346, print page, default record page, named record page → #22158, #22269, #22270, #23985 (all closed, all the #8346 chain), objectui#12715 landed; ADR none; thread: the HotCRM project thread above.
Recommendation: A. Judged on ① alone, A is where printing should end up in two years: documents are pages of their own, as in every mainstream CRM. ②③④ do not flip it. The confidence gap is that A's size is estimated, not measured. Self-check: "on ① alone, A; do ②③④ flip it: no".
After the ruling: for A, triage routes it to domain:server; one PR changes refuseUnlessPrintPage, builds the URL from req.page when it is not the default, updates print-pages.mdx (en and translations) and adds a dogfood print of a non-default page. hotcrm #2312 unblocks on the npm release that carries it. For B or C, hotcrm #2312 is re-decided by the maintainer in the HotCRM thread.
The reader who acts: the objectstack domain:server seat (service-print), then the repo:hotcrm seat to land #2312.
Dedupe: semantic searches on objectstack issues ("print a record on a named print page that is not the default record page"; "service-print print page separate from record page quote document template"; "service-print render named record page route") gave 3, 4 and 7 hits. All are closed, and none asks for this.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 27, 2026
Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #411 · 2026-10-27T10:10Z
Director seat, summon #51 continuation (ledger objectstack#12708). Presented to the maintainer in batch #411 as item 1 of 3 (two new cards and one re-ruling). Seat increment (re-verified on
origin/main500bc9d5; the card is not restated):packages/services/service-print/src/print-service.ts:339–363still refuses any page that is not the object's default record page;ui/packages/app-shell/src/console/AppContent.tsx:280exportsRECORD_NAMED_PAGE_ROUTE = ':objectName/record/:recordId/page/:pageName'and mounts it at :1449 — the console half the feat(service-print): print a record's print page to a PDF in headless Chromium as the requesting user, and archive it on the record #23811 PR body named as missing has shipped (in 18.2.0). Precedent for the workload column: PR feat(service-print): print a record's print page to a PDF in headless Chromium as the requesting user, and archive it on the record #23811 built the whole service, +4074/−68 over 44 files; A is one check, one URL and tests.needs-user-decisionalone, no pm-state, nodomain:*; the ruling's stroke queues it indomain:server.print); ④ B pushes the cost onto every printing app, C reopens 8346 B′.Prior rulings read: 8346 B′ (printable subset), spec(print page) ① of #8346: a
pagegains an optionalprintdeclaration, a linted printable-block subset, the list-export retirement sentence made true, and the three zero-reader document schemas retired #22158 / print page ③ of #8346: a render service with one headless-Chromium driver renders a print page under the requesting user's principal and archives the PDF as a sys_file (M2) #22269 / print page ④ of #8346: a record-page "generate PDF" action that calls the render service and attaches the archived PDF to the record #22270 / showcase: the invoice print page's line-items table prints the raw field namesdescription/quantity/unit_price/amountas its headings — itsrecord:line_itemscolumns declare nolabel, and that block draws a column exactly as declared #23985, objectui#12715; ADR none; thread: the HotCRM project thread.objectstack-fleet commentedon Oct 27, 2026
Ruling: batch #411 item 1 · letter A · maintainer 「26583 A 25870 A」 2026-10-27T10:19Z
Recorded by the director seat (summon #51 continuation,
session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708). The maintainer answered batch #411 with 「26583 A 25870 A」. Brief 6439129875. Letter A:service-printprints a record on any record page of its object that declaresprint, rendering the console's named record-page route (…/record/ID/page/PAGE,RECORD_NAMED_PAGE_ROUTE) whenreq.pageis not the default; the record route stays for the default page.refuseUnlessPrintPagenarrows to two loud refusals — the page is not a record page of this object, or it declares noprint.content/docs/ui/print-pages.mdx(en and translations) loses the "a print page that is not the default is refused" sentence; a dogfood print of a non-default page pins the new door. Not B (every printing app gives up its working page) and not C (reopens ruling 8346 B′'s printable subset). No spec key changes:Clause-②: no,Metadata-protocol: none.Workload (card column):
domain:server· 1 PR · S (one check, one URL, tests, docs); precedent PR #23811 built the whole service (+4074/−68). hotcrm #2312 unblocks on the npm release that carries it; therepo:hotcrmseat lands #2312 then.Execution:
needs-user-decision→pm:queuein this act,domain:serveradded (the card names the reader); priority is triage's on first grade.Ruling-ref:names this comment.objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
enhancement·priority:p1·domain:server·area:records·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T10:56Z⛔ Not a claim, ⛔ not a dispatch.
Skills-docs:line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B). If the PR measures that no key is added, it is p2, and the PR says so.service-printisdomain:server. The schema key, if any, falls under the server fence (a spec seat signs off, or it splits).Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 9 · card thread · printing a record on a named print page
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01FEkQ5kGuMFreHR7gCGkpxLAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-qp0ohr(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: ruling
6439306670letter A (maintainer 「26583 A 25870 A」), triage6440154784(p1; p2 if no key is added):service-printprints on any record page of its object that declaresprint, rendering the console's named record-page route for a non-default page;refuseUnlessPrintPagekeeps only the not-a-record-page and no-printrefusals. Intake split withdomain:server#2for the 18.2.0 wave-2 cards (#25807).File surface:
packages/services/service-print/**(print-service.ts, its tests),content/docs/ui/print-pages.mdxand its translations, a dogfood print of a non-default page underpackages/qa/dogfood/test/, a changeset. ⛔ Nopackages/spec/**(lane red line).Container & model:
S,mode:thread, default model and effort (set at thread open)Clause-②: yes (widening) — expected: a non-default print page that was refused 400 now prints; the dev measures it on the diff
Metadata-protocol: none
Responsibility: an app that prints an object must give up that object's working record page (hotcrm #2312 quote print is blocked)
Thread-read: 6440154784
Premise: holds (read 2026-10-27T10:59Z at
origin/mainc739de755c: card open,pm:queue, no live claim)Serial constraints cleared: none known on these paths; the dev re-measures open PRs on the files it touches before
pr_create.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26583,
"status": "done",
"branch": "claude/project-thread-qp0ohr",
"pr": "#26662",
"session": "session_01FEkQ5kGuMFreHR7gCGkpxL",
"premise_still_valid": true,
"summary": "service-print now prints a record on any record page of its object that declares print: the default record page keeps the console record route, any other print page renders on the named record-page route .../record/ID/page/PAGE (RECORD_NAMED_PAGE_ROUTE). refuseUnlessPrintPage keeps two 400 refusals (not a record page of the object; no print) and the 404 for an unknown name; the not-the-default arm is gone. Predicate both sides use: the console isRecordPageOf, p.type === record AND p.object === OBJECT (usePageAssignment.ts:94-98, asked by RecordDetailView.tsx:787-792); the server now reads exactly that (local isRecordPageOf in print-service.ts). No authorable key is added (no packages/spec edit), so the card grades p2 per triage 6440154784; the PR body says so. Docs: content/docs/ui/print-pages.mdx rewritten (the repo has no translation of that page).",
"tests": "Red first on main c739de7: service-print src/print-service.test.ts with tests only changed = 3 failed / 47 passed (named-route accept, encoded named URL, typeless default page); at head 50/50. Dogfood (print-service.ts restored from c739de7, then restored to HEAD, git diff HEAD empty, blob hash equal): print-service-named-page + print-service-archive + print-service-render (real console bundle, real Chromium, both placements, OS_PAGE_REACT_ENABLED=on) = 3 failed / 9 passed, each failure the old refusal: The page showcase_project_brief is not the default record page of showcase_project (isDefault: true); at head 13/13 (stand-in files) and 8/8 (render). pdftotext of the real render: the brief PDF reads Project brief / Brief / PROJECT NAME STATUS / Compliance Audit On Hold / START DATE END DATE, so the console drew the named page for that project. pnpm --filter @objectstack/service-print test 8 files / 114 tests passed; service-print typecheck exit 0 (include src, tests compiled); dogfood typecheck exit 0; eslint --no-inline-config on touched files exit 0. Ablation: none beyond the main-restore legs (a behaviour change, pinned red/green on the base blob).",
"gates": "Head 25f758a. dispatch-gates --commands --repo objectstack-ai/objectstack: 89 commands, exit codes captured before any pipe; --ran: 89 derived famil(ies) accounted for, 88 run, 1 NOT-MEASURED. 88 exit 0 (check-plugin-teardown-shape --self-test and spec check:skill-examples first exited 3 on prerequisites, re-run green after fetching positive control 621a487 and building client-react). NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: needs every package dist, only the dogfood closure was built; CI runs it. CI check-runs on PR 26662: in_progress at report time (not awaited, per os-dev).",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"mcp_calls": "3: mcp__github__issue_read x3 (card get, comments x2), all reads",
"api_writes": "2 relay writes: pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /issues/26662/assignees), os-dev-report comment (POST /issues/26583/comments). Plus 2 git push (not REST). Write budget spent: push, pr_create, report = the three allowed lines.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door printRecordToFile / POST /api/v1/print/records against the console served by createConsoleStaticPlugin with OS_PAGE_REACT_ENABLED unset (its default): measured in print-service-render.dogfood.test.ts at head AND on the untouched invoice leg, all 6 legs fail with The print engine failed at the local executable ...: page.waitForFunction: EvalError: Evaluating a string as JavaScript violates the following Content Security Policy directive (script-src self nonce wasm-unsafe-eval, packages/cli/src/utils/console.ts:851-852). chromium-driver.ts:303 passes PRINT_READY_PREDICATE as a string, which needs unsafe-eval; with OS_PAGE_REACT_ENABLED=on all 8 pass. CI skips the render file (no console bundle), so main never saw it. Every server print fails on a default deployment, which also blocks objectstack-ai/hotcrm#2312. · dedupe words: service-print CSP unsafe-eval waitForFunction; print engine failed EvalError Content Security Policy; PRINT_READY_PREDICATE string console CSP",
"carrier: server seat 1 · noted, not filed: examples/app-showcase/src/ui/pages/invoice-document.page.ts:12-14 says the invoice print page is the default record page because that is the only page a record prints on; no longer the only option after this PR (private package comment, outside the claim file surface)",
"carrier: server seat 1 · noted, not filed: Clause-2 boundary: a registry page body with NO type that is the default record page and declares print was accepted on main (defaultRecordPageObject reads absent type as record) and is refused 400 at head (the console isRecordPageOf reads type exactly). On main that print drew the synthesized default page, not the print page. No authoring door produces such a body (defineStack parse, save door and stored-row hydration fill the declared default); only a plugin raw registry.registerItem. Declared yes (widening); the seat may re-arm."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landed
PR #26662 merged at 2026-10-27T12:46Z by the merge queue as
5dcbdb429bonmain(Fixes #26583; ACCEPT6441767573).git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26662-*'origin/mainby contentgit cat-file -e origin/main:packages/services/service-print/src/print-service.test.tsgit merge-base --is-ancestor 5dcbdb429b origin/mainState: this card closed by
Fixes(completed);pm:dispatchedremoved.Generated by Claude Code