269 lines
16 KiB
Markdown
269 lines
16 KiB
Markdown
---
|
|
phase: 41-diagrams-icons-theme-engine
|
|
plan: "02"
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["41-01"]
|
|
files_modified:
|
|
- server/src/services/renderers/diagram-renderer.ts
|
|
- server/src/__tests__/diagram-renderer.test.ts
|
|
- server/src/services/renderers/icon-renderer.ts
|
|
- server/src/__tests__/icon-renderer.test.ts
|
|
autonomous: true
|
|
requirements: [DIAG-01, DIAG-02, DIAG-04, DIAG-05, ICON-01, ICON-02, ICON-03]
|
|
must_haves:
|
|
truths:
|
|
- "renderDiagram calls the LLM to synthesize Mermaid syntax from a natural language prompt before rendering"
|
|
- "Mermaid source with %%{init}%% or click directives is stripped before rendering"
|
|
- "renderDiagram returns a JSON bundle with svgBase64 and pngBase64"
|
|
- "renderIconSet returns a JSON bundle with N icons, each having svgSource and PNG variants at 16/32/64"
|
|
- "SVG output is sanitized via DOMPurify before storage"
|
|
- "Diagram supports architecture, flowchart, ERD, sequence, and mind map types"
|
|
artifacts:
|
|
- path: "server/src/services/renderers/diagram-renderer.ts"
|
|
provides: "LLM prompt synthesis + server-side Mermaid rendering via Playwright + DOMPurify + resvg"
|
|
exports: ["renderDiagram", "stripUnsafeDirectives", "buildDiagramPrompt"]
|
|
- path: "server/src/services/renderers/icon-renderer.ts"
|
|
provides: "LLM-driven SVG icon generation with SVGO cleanup + PNG rasterization"
|
|
exports: ["renderIconSet"]
|
|
- path: "server/src/__tests__/diagram-renderer.test.ts"
|
|
provides: "Tests for LLM synthesis, security stripping and bundle structure"
|
|
- path: "server/src/__tests__/icon-renderer.test.ts"
|
|
provides: "Tests for icon bundle structure and SVG validation"
|
|
key_links:
|
|
- from: "server/src/services/renderers/diagram-renderer.ts"
|
|
to: "LLM inference (chat pattern)"
|
|
via: "buildDiagramPrompt() + LLM call before Playwright render"
|
|
pattern: "buildDiagramPrompt"
|
|
- from: "server/src/services/renderers/diagram-renderer.ts"
|
|
to: "playwright-core chromium"
|
|
via: "chromium.launch({ executablePath })"
|
|
pattern: "chromium\\.launch"
|
|
- from: "server/src/services/renderers/icon-renderer.ts"
|
|
to: "svgo"
|
|
via: "optimize(svgString)"
|
|
pattern: "optimize.*preset-default"
|
|
---
|
|
|
|
<objective>
|
|
Implement the diagram renderer (LLM prompt-to-Mermaid synthesis + Playwright headless render + DOMPurify + resvg PNG) and the icon renderer (LLM SVG generation + SVGO + sharp PNG variants). Both produce JSON bundle assets following the types from Plan 01.
|
|
|
|
Purpose: Server-side rendering engines for diagrams and icons, consumed by the job runner.
|
|
Output: Two renderer files with tests covering LLM synthesis, security, bundle structure, and rasterization.
|
|
</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/41-diagrams-icons-theme-engine/41-RESEARCH.md
|
|
@.planning/phases/41-diagrams-icons-theme-engine/41-01-SUMMARY.md
|
|
|
|
<interfaces>
|
|
<!-- From server/src/services/renderers/types.ts (created in Plan 01) -->
|
|
```typescript
|
|
export interface RenderResult {
|
|
filename: string;
|
|
contentType: string;
|
|
buffer: Buffer;
|
|
}
|
|
|
|
export interface DiagramBundle {
|
|
type: "diagram-bundle";
|
|
svgBase64: string;
|
|
pngBase64: string;
|
|
mermaidSource: string;
|
|
stripped: boolean;
|
|
}
|
|
|
|
export interface IconSetBundle {
|
|
type: "icon-set-bundle";
|
|
style: "outline" | "filled" | "rounded";
|
|
icons: Array<{
|
|
name: string;
|
|
svgSource: string;
|
|
pngs: Record<string, string>;
|
|
}>;
|
|
}
|
|
```
|
|
|
|
<!-- Existing pattern from server/src/routes/org-chart-svg.ts -->
|
|
```typescript
|
|
// sharp(svgBuffer, { density: 144 }).resize(width).png().toBuffer()
|
|
```
|
|
|
|
<!-- DOMPurify in Node pattern (existing server deps) -->
|
|
```typescript
|
|
import { JSDOM } from "jsdom";
|
|
import DOMPurify from "dompurify";
|
|
const { window } = new JSDOM("");
|
|
const purify = DOMPurify(window as unknown as Window);
|
|
const cleanSvg = purify.sanitize(rawSvg, { USE_PROFILES: { svg: true } });
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Diagram renderer with LLM synthesis + Playwright headless Mermaid + security stripping + tests</name>
|
|
<files>server/src/services/renderers/diagram-renderer.ts, server/src/__tests__/diagram-renderer.test.ts</files>
|
|
<read_first>server/src/services/renderers/types.ts, server/src/routes/org-chart-svg.ts, ui/src/components/MarkdownBody.tsx, server/src/services/content-job-runner.ts, server/src/services/chat.ts</read_first>
|
|
<behavior>
|
|
- stripUnsafeDirectives("graph TD\n A-->B\n click A \"https://evil.com\"") returns { cleaned: "graph TD\n A-->B", stripped: true }
|
|
- stripUnsafeDirectives("%%{init: {\"theme\": \"dark\"}}%%\ngraph TD\n A-->B") returns cleaned without %%{init}%% block, stripped: true
|
|
- stripUnsafeDirectives("graph TD\n A-->B") returns { cleaned: "graph TD\n A-->B", stripped: false }
|
|
- buildDiagramPrompt("A login flow with validation", "flowchart") returns a system+user prompt instructing the LLM to output valid Mermaid flowchart syntax
|
|
- buildDiagramPrompt with diagramType "architecture" includes architecture-specific preamble hints
|
|
- renderDiagram({ prompt: "A login flow", diagramType: "flowchart", darkMode: false }) calls LLM to get Mermaid source, then renders via Playwright, returns RenderResult with DiagramBundle JSON
|
|
- renderDiagram with LLM-generated source containing click + init directives sets stripped=true in bundle
|
|
</behavior>
|
|
<action>
|
|
1. Create `server/src/__tests__/diagram-renderer.test.ts`:
|
|
- Unit tests for `stripUnsafeDirectives` (pure function, no mocks needed):
|
|
- Test strips `%%{init}%%` blocks
|
|
- Test strips `click NodeId "url"` lines
|
|
- Test strips `click NodeId call fn()` lines
|
|
- Test leaves clean source unchanged, stripped=false
|
|
- Test strips both init and click simultaneously
|
|
- Unit tests for `buildDiagramPrompt`:
|
|
- Test returns string containing "flowchart" when diagramType is "flowchart"
|
|
- Test returns string containing "architecture" when diagramType is "architecture"
|
|
- Test includes the user's natural language description in the prompt
|
|
- Test instructs LLM to output ONLY valid Mermaid syntax (no markdown fences, no explanation)
|
|
- Integration test for `renderDiagram` (mock BOTH the LLM inference call AND playwright-core's chromium.launch):
|
|
- Mock the LLM/chat inference function to return a Mermaid source string like `"graph TD\n A[Login]-->B[Validate]\n B-->C[Dashboard]"`
|
|
- Mock `chromium.launch` to return `{ newPage: () => page, close: async () => {} }`
|
|
- Mock `page.setContent`, `page.waitForSelector`, `page.$eval` to return a simple SVG string `<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="50"/></svg>`
|
|
- Assert result is JSON with `type: "diagram-bundle"`, has `svgBase64`, `pngBase64`, `mermaidSource`, `stripped`
|
|
- Assert the mermaidSource in the bundle is the LLM-generated Mermaid (not the original natural language prompt)
|
|
- Assert `browser.close()` is called (cleanup verification)
|
|
|
|
2. Create `server/src/services/renderers/diagram-renderer.ts`:
|
|
- `stripUnsafeDirectives(source: string): { cleaned: string; stripped: boolean }`:
|
|
- `INIT_BLOCK_RE = /%%\{[\s\S]*?\}%%/g`
|
|
- `CLICK_LINE_RE = /^\s*click\s+.*/gim`
|
|
- Apply both regexes, trim, compare to determine stripped
|
|
- `buildDiagramPrompt(description: string, diagramType: string): { system: string; user: string }`:
|
|
- System prompt: "You are a Mermaid diagram generator. Output ONLY valid Mermaid syntax. No markdown fences, no explanation, no comments. The output must be directly parseable by mermaid.render()."
|
|
- Include diagram-type-specific preamble hints:
|
|
- "flowchart": "Use graph TD or graph LR syntax"
|
|
- "sequence": "Use sequenceDiagram syntax with participant declarations"
|
|
- "erd": "Use erDiagram syntax with entity relationships"
|
|
- "architecture": "Use architecture-beta syntax with groups and services"
|
|
- "mindmap": "Use mindmap syntax with indented hierarchy"
|
|
- User prompt: the natural language description from the user
|
|
- `resolveBrowserPath(): string`:
|
|
- Check env `PLAYWRIGHT_BROWSERS_PATH` first
|
|
- Fall back to glob `~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome`
|
|
- Throw clear error if not found: "Playwright Chromium not found. Run npx playwright install chromium"
|
|
- `buildMermaidHtml(source: string, darkMode: boolean): string`:
|
|
- Returns HTML page that loads mermaid from CDN (`https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js`)
|
|
- Calls `mermaid.initialize({ startOnLoad: false, securityLevel: "strict", theme: darkMode ? "dark" : "default" })`
|
|
- Calls `mermaid.render("render", source)` and sets innerHTML of `#render` div
|
|
- `renderDiagram(input: Record<string, unknown>): Promise<RenderResult>`:
|
|
- Extract `prompt` (string, natural language description), `diagramType` (string, default "flowchart"), `darkMode` (boolean, default false) from input
|
|
- **LLM SYNTHESIS STEP (DIAG-01):** Call `buildDiagramPrompt(prompt, diagramType)` to construct the LLM prompt, then call the LLM via the existing chat inference pattern (model after how renderIconSet calls the LLM — look at chat.ts or similar). The LLM returns Mermaid syntax from the natural language description.
|
|
- If input contains a `source` field (string) instead of `prompt`, skip the LLM step and use the provided Mermaid source directly (this is the re-render path from DiagramSourcePanel where the user edits Mermaid source and re-submits)
|
|
- Strip unsafe directives from the Mermaid source (whether LLM-generated or user-provided)
|
|
- Launch Playwright chromium (headless, executablePath from resolveBrowserPath)
|
|
- Set page content with buildMermaidHtml, wait for `#render svg` (15s timeout)
|
|
- Extract SVG innerHTML via page.$eval
|
|
- Sanitize SVG with DOMPurify (USE_PROFILES: { svg: true })
|
|
- Rasterize to PNG via `new Resvg(svgClean, { dpi: 144 }).render().asPng()`
|
|
- Build DiagramBundle JSON: svgBase64, pngBase64, mermaidSource (the cleaned Mermaid syntax, NOT the original prompt), stripped
|
|
- Return `{ filename: "diagram-bundle.json", contentType: "application/json", buffer: Buffer.from(JSON.stringify(bundle)) }`
|
|
- ALWAYS close browser in finally block
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && pnpm --filter server exec vitest run src/__tests__/diagram-renderer.test.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -c "buildDiagramPrompt" server/src/services/renderers/diagram-renderer.ts` returns at least 2 (definition + usage)
|
|
- `grep -c "stripUnsafeDirectives" server/src/services/renderers/diagram-renderer.ts` returns at least 1
|
|
- `grep -c "DOMPurify" server/src/services/renderers/diagram-renderer.ts` returns at least 1
|
|
- `grep -c "Resvg" server/src/services/renderers/diagram-renderer.ts` returns at least 1
|
|
- `grep "securityLevel.*strict" server/src/services/renderers/diagram-renderer.ts` matches
|
|
- `grep "finally" server/src/services/renderers/diagram-renderer.ts` matches (browser cleanup)
|
|
- `grep "diagram-bundle" server/src/services/renderers/diagram-renderer.ts` matches
|
|
- The LLM inference call exists in renderDiagram (grep for the chat/inference function name)
|
|
- All tests in diagram-renderer.test.ts pass
|
|
</acceptance_criteria>
|
|
<done>Diagram renderer synthesizes Mermaid syntax from natural language via LLM (DIAG-01), strips unsafe directives (DIAG-05), renders to SVG+PNG via Playwright (DIAG-02), supports all 5 diagram types via prompt hints (DIAG-04); browser always cleaned up; all tests green</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Icon renderer with LLM SVG generation + SVGO + PNG variants + tests</name>
|
|
<files>server/src/services/renderers/icon-renderer.ts, server/src/__tests__/icon-renderer.test.ts</files>
|
|
<read_first>server/src/services/renderers/types.ts, server/src/services/renderers/diagram-renderer.ts, server/src/services/chat.ts</read_first>
|
|
<action>
|
|
1. Create `server/src/__tests__/icon-renderer.test.ts`:
|
|
- Test `validateAndCleanSvg` (pure function):
|
|
- Valid SVG with path returns cleaned output with viewBox="0 0 24 24" and xmlns present
|
|
- SVG missing viewBox gets normalized to "0 0 24 24"
|
|
- SVG with no path/circle/rect elements returns error
|
|
- Test `renderIconSet` with mocked LLM call:
|
|
- Mock the chat/inference function to return SVG strings
|
|
- Assert result is JSON with `type: "icon-set-bundle"`, contains correct number of icons
|
|
- Assert each icon has svgSource (string) and pngs object with "16", "32", "64" keys (base64 strings)
|
|
|
|
2. Create `server/src/services/renderers/icon-renderer.ts`:
|
|
- `validateAndCleanSvg(raw: string): { svg: string; valid: boolean; error?: string }`:
|
|
- Run SVGO optimize with preset-default
|
|
- Check for viewBox, add `viewBox="0 0 24 24"` if missing
|
|
- Check for xmlns, add `xmlns="http://www.w3.org/2000/svg"` if missing
|
|
- Validate at least one `<path>`, `<circle>`, or `<rect>` exists
|
|
- Return cleaned SVG or error
|
|
- `buildIconPrompt(description: string, style: string, count: number): string`:
|
|
- System prompt enforcing: 24x24 viewBox, stroke-width=1.5 for outline, currentColor fill, no text elements, no embedded images
|
|
- Include style constraint (outline: stroke only; filled: fill only; rounded: stroke with stroke-linecap="round" stroke-linejoin="round")
|
|
- Request exactly `count` icons as a JSON array of `{ name: string, svg: string }` objects
|
|
- `renderIconSet(input: Record<string, unknown>): Promise<RenderResult>`:
|
|
- Extract description (string), style ("outline"|"filled"|"rounded", default "outline"), count (1|4|8|16, default 4)
|
|
- Call LLM via the existing chat inference pattern (look at how chat.ts or similar calls the active provider)
|
|
- Parse LLM JSON response (try/catch — if parse fails, retry once with explicit "respond with JSON only" instruction)
|
|
- For each icon SVG: validateAndCleanSvg, then generate PNG variants at 16, 32, 64 via sharp:
|
|
`sharp(Buffer.from(svgString), { density: 96 }).resize(size).png().toBuffer()`
|
|
- Build IconSetBundle JSON with all icons, base64-encode PNG buffers
|
|
- Return `{ filename: "icon-set-bundle.json", contentType: "application/json", buffer: Buffer.from(JSON.stringify(bundle)) }`
|
|
- Handle partial failures: if some icons fail validation, include only valid ones and note count in bundle metadata
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && pnpm --filter server exec vitest run src/__tests__/icon-renderer.test.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep "icon-set-bundle" server/src/services/renderers/icon-renderer.ts` matches
|
|
- `grep "optimize" server/src/services/renderers/icon-renderer.ts` matches (SVGO)
|
|
- `grep "viewBox" server/src/services/renderers/icon-renderer.ts` matches
|
|
- `grep "sharp" server/src/services/renderers/icon-renderer.ts` matches (PNG rasterization)
|
|
- All tests in icon-renderer.test.ts pass
|
|
</acceptance_criteria>
|
|
<done>Icon renderer generates SVG icons via LLM, cleans with SVGO, validates, rasterizes to PNG at 3 sizes; all tests green</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `pnpm --filter server exec vitest run src/__tests__/diagram-renderer.test.ts src/__tests__/icon-renderer.test.ts` — all tests pass
|
|
- `pnpm tsc --noEmit --project server/tsconfig.json` — no type errors
|
|
- Both renderers return `RenderResult` matching the type contract from Plan 01
|
|
- Diagram renderer has an LLM synthesis step (buildDiagramPrompt + inference call) before Playwright render
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Diagram renderer synthesizes Mermaid from natural language via LLM (DIAG-01), strips unsafe directives (DIAG-05), renders to SVG+PNG via Playwright (DIAG-02)
|
|
- Diagram renderer supports all 5 diagram types via LLM prompt hints (DIAG-04)
|
|
- Diagram renderer supports direct Mermaid source input for the re-render path (DiagramSourcePanel edits)
|
|
- Icon renderer produces cohesive SVG icon sets from LLM (ICON-01, ICON-02) with multi-size PNG export (ICON-03)
|
|
- Both renderers produce JSON bundles stored as single assets (content_jobs.resultAssetId pattern)
|
|
- All tests pass
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/41-diagrams-icons-theme-engine/41-02-SUMMARY.md`
|
|
</output>
|