Conversation
Host Test Results 1 files ± 0 1 suites ±0 28m 30s ⏱️ - 2h 7m 13s Results for commit d591caf. ± Comparison against earlier commit 3ab2260. Realm Server Test Results 1 files ±0 225 suites ±0 1h 20m 5s ⏱️ -56s Results for commit d591caf. ± Comparison against earlier commit 3ab2260. |
98d867b to
718793d
Compare
lukemelia
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] Lens: traced the pdf capture leg end to end — the engine branch and its post-render page/byte bounds, the singular-only and crop-mode refusals on both the shared parse and the engine's own guard, the contentType wiring from engine → render-runner → response, the optional-dims re-typing and its consumers, and the capture-only posture across the ledger identity, the GET DSL, and the worker hash guard. The base branch's type/media axes were re-checked only where this diff reads them.
Bottom line: one thing to settle before merge — the PDF renders under print media, not the screen media the render settled under, which both the code comments and the media-axis design say otherwise (inline thread on utils.ts). It's a decision, not a mechanical fix, and blocks only if you wanted screen. Everything else lands: the bounds fail by name rather than truncate, the refusals are enforced on both the parse and the engine guard, and the capture-only wiring keeps pdf out of the ledger consistently. Two minor non-blocking nits:
-
Stale comment in
runtime-common/index.ts. OnScreenshotPrerenderResponse, the line "The engine produces onlyimage/pngtoday" is falsified by this PR — the engine now emitsapplication/pdf. The change right above widenedScreenshotCaptureResultfor exactly that output, so update this comment in the same pass. Evergreen nit. -
pageCountis dropped from the POST response. Inhandle-screenshot-card.ts, a pdf capture hasspec === undefined→entryKey === undefined→ the capture-only normalization (result.captures.map(...)) runs and rebuilds each entry as name/url/width/height/deviceScaleFactor/base64. The engine computespageCountand render-runner carries it oncaptures[i], but it never reaches the response — so a pdf caller (the only surface pdf reaches today) gets the bytes without the page count already computed for the bounds check. If that's intended, ignore; if a pdf POST should expose it, add...(c.pageCount !== undefined ? { pageCount: c.pageCount } : {})to the map. Follow-up, not this PR.
A singular `type: 'pdf'` capture spec now paginates the settled render with page.pdf (printBackground, preferCSSPageSize — an author's @page rule wins, Chrome's default paper otherwise) instead of rasterizing it. The same settle sequence runs first; the result reports pageCount instead of pixel dimensions, and the response carries contentType application/pdf. Bounds are enforced post-render, the fullPage late-check pattern: 20 pages / 10 MB, each an error naming the cap rather than a truncation. pdf output is singular-only (one contentType per response) and refuses the raster crop modes (fullPage/clip/target) at the shared parse, with defensive engine guards for direct prerender-server callers. pdf stays capture-only: the GET DSL refuses type=pdf (a surface that persists everything it captures cannot yet serve paged documents), the POST handler derives no persist identity for pdf specs, and the worker task's guard hashes pdf renders to null so no producer can land one in the ledger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
718793d to
f3916db
Compare
page.pdf() renders under print media regardless of what the page settled under, so the media axis — the caller's control over media type — could not be honored: a spec asking for the default screen media still produced a print-media document. Emulate the requested media (screen today, the only value the parse admits) before page.pdf(), and clear the override in finally so a reused pooled page carries no media into the next capture. Cover it with a probe card that is 200px under screen media and 60000px under print: a screen-media pdf stays within the page cap, a print one would page well past it. Correct two comments the pdf leg falsified — the engine now produces application/pdf, and honors pdf as an output type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GsYGsuCqHJz9GAti4jheG
|
[Claude Code 🤖] Review-body nits, dispositions:
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
habdelra
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] This review went after what page.pdf() actually renders (checked against Chromium over CDP, driving Page.printToPDF with puppeteer 25.2.1's own pdf() defaults), the capture-only posture across every producer and consumer of the new encoding, and pooled-page hygiene. It did not run the realm-server or prerender suites.
One blocking decision: the paged document is not the viewport render. Page.printToPDF re-lays the document out at the paper's content width — 816 CSS px under puppeteer's letter/zero-margin defaults — whatever viewport the capture settled at, so viewport is inert for pdf output and the render-parity claim doesn't hold for anything width-dependent. Detail and measurements in the reply on the media thread in prerender/utils.ts. Everything else is small.
Recommendations:
- Decide paper-from-viewport vs. keeping Chrome's paper and correcting the parity claim — reply on the media thread in
prerender/utils.ts. - Make the
finallymedia reset best-effort so a dead page doesn't mask the real failure — inline on that block. - Reword the "a direct prerender-server caller could still send one" note; that route runs the same shared parse — inline on the crop-mode guard.
- Cover the two pdf caps, and decide whether a pre-render extent bound is worth it — inline on the cap block.
- The one shipped client of this surface writes pdf bytes into a
.pngfile:EXTENSION_BY_CONTENT_TYPEinpackages/boxel-cli/src/commands/screenshot.tsmaps only the three image types and falls back topng, while the CLI passes a--specfile'scaptureSpecthrough verbatim — so{"card": "…", "captureSpec": {"type": "pdf"}}lands a PDF at<card>.pngtoday. One line to add the mapping. Note that touchingpackages/boxel-cli/**puts this PR under the conventional-commit title rule, so a separate follow-up may be the cleaner home. Class: introduced here (the surface newly producesapplication/pdf); non-blocking either way.
Generated by Claude Code
page.pdf() lays the document out at the paper's content width (the card's @page size, or Chrome's default paper), not the capture viewport, so a viewport on a pdf spec would be an inert identity axis minting distinct ledger keys over byte-identical documents. Refuse it at the shared parse alongside fullPage/clip/target (a default-valued viewport still elides and parses) and in the engine's defensive guard, and correct the render-parity comments to say what the paged document actually is: the raster path's settle and media, laid out at paper width. Extract the post-render caps into checkPdfCaptureBounds so both error messages and boundary comparisons are unit-testable against synthesized buffers, and pin the page cap end-to-end with a 30000px-tall fixture. Make the finally media reset best-effort so a dying page surfaces the capture's real failure, and reword the defensive-guard comments: every wire surface runs the shared parse; the guards exist for in-process callers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
[Claude Code 🤖] All four review threads are addressed and resolved in d591caf, and the description is updated to the corrected render-parity claims plus the viewport refusal. The remaining review-body item — the CLI writing pdf bytes to a |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
[Claude Code 🤖] Buck asked me to land the merge before reviewing, so this is posted from their account. The branch was conflicting against its base; I resolved and pushed the merge as aa365a0d. All four conflicts were unions between this branch's pdf capture-only posture and the base branch's target capture-only posture: the doc comment on the POST handler, the persist-identity predicate there, the same predicate in the worker task, and two test files that each added a "refuses to persist" case. Both predicates now exclude captures, target, and type: 'pdf'; both test cases are kept. runtime-common and realm-server typecheck clean on the result.
Lens: convergence on the four open threads, re-verified against Chromium rather than against the replies. Bottom line: no blocking issues — this is ready to land. Two one-line follow-ups below, neither worth holding for.
Thread dispositions
- Media / viewport parity — resolved. With the media pinned, a declarative page's pdf is byte-identical across viewport widths (checked against Chromium over
Page.printToPDF, warm-up pages discarded — the first two pages of a launch are not byte-stable), so refusingviewportcosts nothing and the paper-width claim incapture-spec.tsholds as written. Worth noting the refusal's scope is right:deviceScaleFactor, which it deliberately leaves admitted, is not inert — dsf 1/2/3 produce three different documents on a page carrying amin-resolutionquery — so it stays a live axis rather than an inert one minting identities. - Best-effort media reset — resolved. The
finallynow wrapsemulateMediaType()in try/catch like the file's other pooled-page restores. - Guard comment wording — resolved. Both guards name the shared parse as covering every wire surface and scope themselves to in-process callers.
- Cap coverage — resolved. Both caps are unit-covered at the boundary and one past with exact wording, so a flipped comparison or swapped message can't ship green. I checked
countPdfPagesagainst real Chrome output rather than synthesized buffers: 2000px→2, 5000px→5, 21120px→20, 30000px→29 pages — so the count the cap is enforced on is accurate, and the 30000px fixture clears the 20-page cap by 9. Declining the pre-render extent bound is the right call; ascrollHeightestimate can't know the card's@pagesize.
Non-blocking follow-ups
- The CLI still writes pdf bytes into a
.pngfile.EXTENSION_BY_CONTENT_TYPEinpackages/boxel-cli/src/commands/screenshot.tsmaps only the three image types and falls back topng, and--specpasses a file entry'scaptureSpecthrough verbatim — so{"card": "…", "captureSpec": {"type": "pdf"}}lands a PDF at<card>.png. One line ('application/pdf': 'pdf') fixes it. Class: introduced here, since the surface newly producesapplication/pdf. Touching that package puts the PR under the conventional-commit title rule, so a separate follow-up is probably the cleaner home. pageCountnever reaches a POST caller. The engine computes it andrender-runnercarries it oncaptures[i], but inhandle-screenshot-card.tsa pdf spec leavesentryKeyundefined, so the capture-only normalization rebuilds each entry as name/url/width/height/deviceScaleFactor/base64. A pdf caller therefore getswidth: null,height: nulland no page count, on the one surface pdf reaches. Spreading...(c.pageCount !== undefined ? { pageCount: c.pageCount } : {})into that map closes it. Class: introduced here.
CI on the merge commit is still running, so this approval is on the code, not on a green run.
Generated by Claude Code
Summary
Second PR in the Cards to PDF chain (CS-12976); its base (#6118, the identity axes) is merged, so this diff reads clean against main.
A singular
type: 'pdf'capture spec now produces a paged document of the settled render:prerender/utils.ts): a pdf branch incaptureOneEntryruns the identical settle sequence, then emulates the media the spec asks for (screentoday) and callspage.pdf({ printBackground: true, preferCSSPageSize: true }).page.pdf()rendersprintmedia by default, so pinning the media keeps the paged document on the same styles the raster path captures; the override is cleared afterward — best-effort, so a dying page surfaces its real failure rather than the reset's — so a pooled page carries no media into the next capture. Layout width is the paper's, not a viewport's:Page.printToPDFlays the document out at the paper's content width, where an author's@page { size: … }rule wins and Chrome's default paper applies otherwise. The result reportspageCountinstead of pixel dimensions (ScreenshotCaptureResult.width/heightbecome optional; the declared-capture path is unaffected — its own result type keeps required dims), and the response'scontentTypeis derived from the capture (application/pdffor pdf) instead of hardcoded.fullPagelate-check pattern, since page count and byte size exist only after Chrome paginates): 20 pages / 10 MB viacheckPdfCaptureBounds, each an error naming the cap, never a truncation. Page counting scans Chromium's uncompressed/Type /Pagedictionaries with the page-tree/Countas a floor (countPdfPages).contentTypeper response, so no pdf in batch entries) and refuses the raster geometry —fullPage,clip,target, andviewport(pagination lays out at paper width, so a viewport would be an inert axis minting distinct ledger identities over byte-identical documents; a viewport spelling the engine default still elides and parses) — at the shared parse, with defensive engine guards for in-process callers that assemble capture entries directly._screenshot/DSL refusestype=pdfby name (that surface persists everything it captures and cannot yet serve paged documents), the POST handler derives no persist identity for pdf specs (url: null, bytes inbase64), and the worker task's guard hashes pdf renders to null so no producer can land one in the ledger.Testing
media-cache-dsl-test.ts: pdf parses on POST (singular-only, crop-mode and viewport refusals, with the default-valued viewport spelling still admitted), the GET surface refusestype=pdfwith its own wording,countPdfPagesunit coverage (page dictionaries,/Countfloor, zero-marker case),checkPdfCaptureBoundscoverage of both caps' boundary and error wording against synthesized buffers, and the task refuses to persist a pdf render under any claimed identity — all green locally.screenshot-card-test.ts: a POST pdf capture gets no persist identity and no served URL while the encoding rides the job args — green locally.prerendering-test.ts(real Chrome): a pdf capture of a tall card returns%PDF-bytes withcontentType: application/pdfand a page count, and the next raster capture on the same pooled page still renders at the default viewport. A probe card that is short under screen media and 60000px under print pins the media: the screen-media pdf stays within the page cap, where a print render would page well past it. A 30000px-tall card errors on the page cap by name, with no truncated document riding the error.runtime-commonandrealm-servertypecheck clean.🤖 Generated with Claude Code