Version. 0.1 Date. 2026-05-02 Provenance. Claude.ai scoping session. Operator: Marvin Percival. Status. Working draft.
The Rendering room declares output formats (the PDF badge on a render type card) but does not produce them. SHOW CONTENT displays the LLM specialist's output in a MarkdownPanel. The Operator sees the content — but cannot hand it to anyone. There is no file. The room stops at preview.
The Operator's observation: "I think that is why this Room exists." The Rendering room's purpose is to deliver the finished artifact — the thing the consumer actually receives. Without a downloadable file in the declared format, the room is incomplete.
Render content storage. render_content is stored as JSONB on the RenderEvent. For LLM-based specialists, this is HTML/text content (the Goosey "Lost Duckling" render is a 27,835-char HTML document).
Format declaration. render_format on the DeclaredRenderType declares the intended output format (e.g., "PDF"). The ad-hoc render creation modal includes a format selector. The format badge displays on cards.
Content viewer. SHOW CONTENT expands a MarkdownPanel showing the raw content inline. This works as a preview surface.
What's missing. No materialization step converts render_content to the declared format. No download endpoint. No download button.
The architecture already implies a clean separation:
render_content.
This separation is consistent with executor opacity — the specialist's job is content; materialization is an infrastructure concern. The same render_content could potentially be materialized to PDF, HTML download, or other formats. The specialist does not need to know about materialization.
New endpoint: GET /engagements/{eid}/renders/{render_id}/download
The endpoint reads the RenderEvent's render_content, wraps it in a styled HTML template with print-optimized CSS, converts to PDF via weasyprint, and returns the binary with Content-Disposition: attachment; filename="...".
Why server-side, not browser print-to-PDF:
Template. A print-optimized HTML wrapper with:
Filename convention: {render-type-name}--{engagement-title}--{date}.pdf, slugified. Example: storybook-rule--goosey-the-moose--2026-05-02.pdf.
Dependency: weasyprint added to the substrate's Python dependencies. Weasyprint requires system-level packages (cairo, pango, gdk-pixbuf) — these need to be present on DUNIN7-M4. The CR should include the install verification step.
A DOWNLOAD button on each produced render card, next to SHOW CONTENT and RETIRE. Calls the download endpoint. Browser-native download behavior (the file appears in the user's downloads).
Button appears only on renders whose format is supported for materialization. In this scope: PDF only. Other formats show SHOW CONTENT but no DOWNLOAD — the capability grows with the materializer registry.
The download endpoint should not be a monolithic if/else on format. Instead, a minimal registry mapping render_format → materializer function. PDF is the first registered materializer. The pattern accommodates future formats (HTML download, DOCX, MD) without restructuring.
# Sketch — not prescriptive
MATERIALIZERS: dict[str, Callable] = {
"PDF": materialize_pdf,
}
async def download_render(render_event, format_override=None):
fmt = format_override or render_event.render_format
materializer = MATERIALIZERS.get(fmt)
if not materializer:
raise UnsupportedFormatError(fmt)
return await materializer(render_event)
Position A (generate on demand). The PDF is computed from render_content + template each time the download endpoint is called. No new storage. Simple. The materialization is deterministic given the same inputs — regenerating is equivalent to retrieving.
Position B (store on first generation, serve cached). After first materialization, store the binary (filesystem or blob column) and serve the cached version on subsequent requests. Avoids re-running weasyprint on every download.
Recommendation: Position A for now. Weasyprint on a 28K-char document is fast (sub-second). Caching adds storage management, cache invalidation (what if the template changes?), and complexity with no current need. If download volume grows, caching is a straightforward addition.
This is small in scope (one endpoint, one template, one button, one dependency) but it completes the room's purpose. Options:
The Operator should decide. The work is CR-worthy either way — the CR gives CC the specification it needs. The question is whether it gets a tag.
Position A (brand-faithful). The PDF carries Loomworks brand tokens — Plex Serif headings, Inter body, the warm palette, the DUNIN7 footer line. The PDF is recognizably a Loomworks artifact.
Position B (neutral/engagement-specific). The PDF is styled for the content, not for the platform. No Loomworks branding. Clean, professional, generic.
Position C (engagement-branded, future). The engagement carries its own brand identity, and the PDF reflects it. Not buildable today — no engagement-level brand metadata exists.
Recommendation: Position A. The PDF is produced by Loomworks. It should look like it came from Loomworks. Position C is the direction, but requires infrastructure that doesn't exist yet.
The current format selector on the ad-hoc render modal offers format choices. Only PDF is materialized in this scope. What happens when a render has format "HTML" or "Markdown"?
render_content as a file with the right Content-Type). They could land in this scope or next.Recommendation: gate the DOWNLOAD button on materializer existence. Ship PDF first. HTML and MD materializers are small enough to add in the same CR if scope allows, but they're not required.
The download capability is independent of the dispatch race — it acts on renders that have already been produced. However, if renders aren't reliably produced, there's nothing to download. The race fix and the download capability are orthogonal and can land in either order.
Small enough for a single CR with 4–5 steps.
DUNIN7 — Done In Seven LLC — Miami, Florida Render artifact download — Scoping note — v0.1 — 2026-05-02