nexus/.planning/phases/43-documents-branding/43-01-PLAN.md

10 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
43-documents-branding 01 execute 1
server/src/services/renderers/types.ts
server/src/services/renderers/pdf-renderer.ts
server/src/services/content-job-runner.ts
server/src/__tests__/pdf-renderer.test.ts
server/package.json
true
DOC-01
DOC-02
DOC-03
truths artifacts key_links
renderPdfDocument produces a pdf-document-bundle with non-empty pdfBase64 for report docType
renderPdfDocument produces a pdf-document-bundle for invoice docType with line items
renderPdfDocument produces a pdf-document-bundle for api-docs docType
content-job-runner dispatches pdf-document jobType to pdf-renderer
PdfDocumentBundle and BrandKitBundle types exported from types.ts
path provides exports
server/src/services/renderers/pdf-renderer.ts PDF rendering via Playwright HTML-to-PDF
renderPdfDocument
path provides contains
server/src/services/renderers/types.ts PdfDocumentBundle + BrandKitBundle type definitions PdfDocumentBundle
path provides min_lines
server/src/__tests__/pdf-renderer.test.ts Unit tests for PDF renderer 40
from to via pattern
server/src/services/content-job-runner.ts server/src/services/renderers/pdf-renderer.ts dynamic import in renderContent switch case "pdf-document"
from to via pattern
server/src/services/renderers/pdf-renderer.ts server/src/services/renderers/diagram-renderer.js resolveBrowserPath import resolveBrowserPath
PDF renderer and shared types for Phase 43 document generation.

Purpose: Enable PDF report, invoice, one-pager, and API documentation generation via Playwright HTML-to-PDF. Also installs archiver and defines both bundle types needed by Plans 02-03. Output: Working pdf-renderer.ts, updated types.ts, job-runner wiring, archiver installed.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/43-documents-branding/43-RESEARCH.md

@server/src/services/renderers/types.ts @server/src/services/content-job-runner.ts @server/src/services/renderers/diagram-renderer.ts @server/src/services/puter-inference.ts

From server/src/services/renderers/types.ts:

export interface RenderResult {
  filename: string;
  contentType: string;
  buffer: Buffer;
}
export type ContentBundle = DiagramBundle | IconSetBundle | ThemePaletteBundle | WallpaperBundle | AppIconBundle | SocialPostBundle | ConvertBundle;

From server/src/services/renderers/diagram-renderer.ts:

export function resolveBrowserPath(): string; // resolves Playwright Chromium binary path

From server/src/services/puter-inference.ts:

export interface ChatMessage { role: "system" | "user" | "assistant"; content: string; }
export async function puterChatComplete(messages: ChatMessage[], model?: string): Promise<string>;

From server/src/services/content-job-runner.ts:

// renderContent() switch — add new case for "pdf-document"
export async function renderContent(jobType: string, input: Record<string, unknown>): Promise<RenderResult>;
Task 1: Install archiver, add bundle types, create pdf-renderer with tests server/package.json, server/src/services/renderers/types.ts, server/src/services/renderers/pdf-renderer.ts, server/src/__tests__/pdf-renderer.test.ts - server/src/services/renderers/types.ts (current bundle types and ContentBundle union) - server/src/services/renderers/diagram-renderer.ts (resolveBrowserPath, Playwright launch pattern, stripUnsafeDirectives) - server/src/services/renderers/wallpaper-renderer.ts (stripMarkdownFences helper — lines 42-50) - server/src/services/puter-inference.ts (puterChatComplete signature) - server/src/__tests__/diagram-renderer.test.ts (test mock pattern for playwright-core and puter-inference) - Test 1: renderPdfDocument with docType="report" returns { filename: "document-report.json", contentType: "application/json" } and buffer parses to PdfDocumentBundle with type "pdf-document-bundle" and non-empty pdfBase64 - Test 2: renderPdfDocument with docType="invoice" returns bundle with docType "invoice" - Test 3: renderPdfDocument with docType="api-docs" returns bundle with docType "api-docs" - Test 4: renderPdfDocument with docType="one-pager" returns bundle with docType "one-pager" - Test 5: LLM system prompt varies by docType (report vs invoice vs api-docs vs one-pager) 1. Install archiver: `pnpm --filter @paperclipai/server add archiver && pnpm --filter @paperclipai/server add -D @types/archiver`
2. Add to server/src/services/renderers/types.ts — append BEFORE the ContentBundle union:
```typescript
export interface PdfDocumentBundle {
  type: "pdf-document-bundle";
  docType: string;
  title: string;
  pdfBase64: string;
}

export interface BrandKitBundle {
  type: "brand-kit-bundle";
  spec: {
    name: string;
    tagline: string;
    primaryColor: string;
    secondaryColor: string;
    fontStyle: string;
    industry: string;
  };
  logoSvgBase64: string;
  avatarPngs: Record<string, string>;
  socialImages: Record<string, string>;
  signatureHtml: string;
  letterheadHtml: string;
  guidelinesPdfBase64: string;
  zipBase64: string;
}
```
Update ContentBundle union to include `| PdfDocumentBundle | BrandKitBundle`.

3. Create server/src/services/renderers/pdf-renderer.ts:
- Import `chromium` from playwright-core, `resolveBrowserPath` from diagram-renderer, `puterChatComplete` from puter-inference, `RenderResult` and `PdfDocumentBundle` from types
- Create a local `stripMarkdownFences(raw: string): string` helper that removes ```html and ``` fences (same pattern as wallpaper-renderer line 42-50 — do NOT import from wallpaper-renderer as it is not exported)
- Create `buildPdfSystemPrompt(docType: string): string` with type-specific instructions for "report", "invoice", "one-pager", "api-docs" — each variant has different structural instructions. CRITICAL: system prompt must say "Use only inline CSS in a <style> block. No external URLs, no <link> tags, no <script> tags. Use web-safe system fonts: Arial, Georgia, monospace."
- Export `renderPdfDocument(input: Record<string, unknown>): Promise<RenderResult>`:
  - Extract `docType` (default "report"), `prompt`, `title` (default "Document") from input
  - Call puterChatComplete with system prompt + user prompt
  - Strip markdown fences from LLM output
  - Launch Chromium via resolveBrowserPath(), set HTML content with waitUntil "networkidle", call page.pdf({ format: "A4", printBackground: true, margin: { top: "20mm", bottom: "20mm", left: "20mm", right: "20mm" } })
  - Wrap Uint8Array in Buffer.from()
  - Build PdfDocumentBundle with pdfBase64 = pdfBuffer.toString("base64")
  - Return { filename: `document-${docType}.json`, contentType: "application/json", buffer: Buffer.from(JSON.stringify(bundle)) }
  - Use try/finally to always close browser

4. Create server/src/__tests__/pdf-renderer.test.ts:
- vi.mock("playwright-core") — mock chromium.launch to return { newPage: () => page, close: vi.fn() } where page has setContent and pdf (returns Buffer.from("fake-pdf"))
- vi.mock("../services/puter-inference.js") — mock puterChatComplete to return "<!DOCTYPE html><html><body>test</body></html>"
- vi.mock("../services/renderers/diagram-renderer.js") — mock resolveBrowserPath to return "/fake/chromium"
- Write tests per behavior section above
- grep -q "PdfDocumentBundle" server/src/services/renderers/types.ts - grep -q "BrandKitBundle" server/src/services/renderers/types.ts - grep -q "renderPdfDocument" server/src/services/renderers/pdf-renderer.ts - grep -q "resolveBrowserPath" server/src/services/renderers/pdf-renderer.ts - grep -q "archiver" server/package.json pnpm --filter @paperclipai/server test --run src/__tests__/pdf-renderer.test.ts PDF renderer produces PdfDocumentBundle for all 4 doc types (report, invoice, one-pager, api-docs). archiver installed. Both PdfDocumentBundle and BrandKitBundle types defined. Task 2: Wire pdf-document jobType into content-job-runner server/src/services/content-job-runner.ts - server/src/services/content-job-runner.ts (current renderContent switch) Add a new case to the `renderContent()` switch in content-job-runner.ts, before the `default` case:
```typescript
case "pdf-document": {
  const { renderPdfDocument } = await import("./renderers/pdf-renderer.js");
  return renderPdfDocument(input);
}
```

Follow exact pattern of existing cases (dynamic import with .js extension, destructured named export).
- grep -q '"pdf-document"' server/src/services/content-job-runner.ts - grep -q 'renderPdfDocument' server/src/services/content-job-runner.ts pnpm --filter @paperclipai/server exec tsc --noEmit content-job-runner dispatches "pdf-document" jobType to renderPdfDocument. TypeScript compiles cleanly. - `pnpm --filter @paperclipai/server test --run src/__tests__/pdf-renderer.test.ts` passes - `pnpm --filter @paperclipai/server exec tsc --noEmit` compiles without errors - `grep -q "pdf-document" server/src/services/content-job-runner.ts` succeeds

<success_criteria>

  • PDF renderer handles all 4 document types (report, invoice, one-pager, api-docs)
  • Both PdfDocumentBundle and BrandKitBundle types exported from types.ts
  • archiver package installed
  • Job runner dispatches pdf-document jobs correctly
  • All tests pass, TypeScript clean </success_criteria>
After completion, create `.planning/phases/43-documents-branding/43-01-SUMMARY.md`