230 lines
10 KiB
Markdown
230 lines
10 KiB
Markdown
---
|
|
phase: 43-documents-branding
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- 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
|
|
autonomous: true
|
|
requirements: [DOC-01, DOC-02, DOC-03]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "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"
|
|
artifacts:
|
|
- path: "server/src/services/renderers/pdf-renderer.ts"
|
|
provides: "PDF rendering via Playwright HTML-to-PDF"
|
|
exports: ["renderPdfDocument"]
|
|
- path: "server/src/services/renderers/types.ts"
|
|
provides: "PdfDocumentBundle + BrandKitBundle type definitions"
|
|
contains: "PdfDocumentBundle"
|
|
- path: "server/src/__tests__/pdf-renderer.test.ts"
|
|
provides: "Unit tests for PDF renderer"
|
|
min_lines: 40
|
|
key_links:
|
|
- from: "server/src/services/content-job-runner.ts"
|
|
to: "server/src/services/renderers/pdf-renderer.ts"
|
|
via: "dynamic import in renderContent switch"
|
|
pattern: 'case "pdf-document"'
|
|
- from: "server/src/services/renderers/pdf-renderer.ts"
|
|
to: "server/src/services/renderers/diagram-renderer.js"
|
|
via: "resolveBrowserPath import"
|
|
pattern: "resolveBrowserPath"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs. -->
|
|
|
|
From server/src/services/renderers/types.ts:
|
|
```typescript
|
|
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:
|
|
```typescript
|
|
export function resolveBrowserPath(): string; // resolves Playwright Chromium binary path
|
|
```
|
|
|
|
From server/src/services/puter-inference.ts:
|
|
```typescript
|
|
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:
|
|
```typescript
|
|
// renderContent() switch — add new case for "pdf-document"
|
|
export async function renderContent(jobType: string, input: Record<string, unknown>): Promise<RenderResult>;
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Install archiver, add bundle types, create pdf-renderer with tests</name>
|
|
<files>server/package.json, server/src/services/renderers/types.ts, server/src/services/renderers/pdf-renderer.ts, server/src/__tests__/pdf-renderer.test.ts</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<behavior>
|
|
- 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)
|
|
</behavior>
|
|
<action>
|
|
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
|
|
</action>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>pnpm --filter @paperclipai/server test --run src/__tests__/pdf-renderer.test.ts</automated>
|
|
</verify>
|
|
<done>PDF renderer produces PdfDocumentBundle for all 4 doc types (report, invoice, one-pager, api-docs). archiver installed. Both PdfDocumentBundle and BrandKitBundle types defined.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Wire pdf-document jobType into content-job-runner</name>
|
|
<files>server/src/services/content-job-runner.ts</files>
|
|
<read_first>
|
|
- server/src/services/content-job-runner.ts (current renderContent switch)
|
|
</read_first>
|
|
<action>
|
|
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).
|
|
</action>
|
|
<acceptance_criteria>
|
|
- grep -q '"pdf-document"' server/src/services/content-job-runner.ts
|
|
- grep -q 'renderPdfDocument' server/src/services/content-job-runner.ts
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>pnpm --filter @paperclipai/server exec tsc --noEmit</automated>
|
|
</verify>
|
|
<done>content-job-runner dispatches "pdf-document" jobType to renderPdfDocument. TypeScript compiles cleanly.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/43-documents-branding/43-01-SUMMARY.md`
|
|
</output>
|