--- gsd_state_version: 1.0 milestone: v1.7 milestone_name: Content Generation status: verifying stopped_at: Completed 45-01-PLAN.md — 9 content SKILL.md files, local-nexus-content source type, Creative group seeded, unified startup sequence, tsc clean last_updated: "2026-04-05T08:46:16.382Z" last_activity: 2026-04-05 progress: total_phases: 6 completed_phases: 6 total_plans: 21 completed_plans: 21 percent: 0 --- # Project State ## Project Reference See: .planning/PROJECT.md (updated 2026-04-04) **Core value:** A fresh onboard asks for ONE thing (root directory), auto-creates PM + Engineer agents, and drops you in the dashboard. **Current focus:** Phase 45 — content-as-skills ## Current Position Phase: 45 Plan: Not started Status: Phase complete — ready for verification Last activity: 2026-04-05 Progress: [░░░░░░░░░░] 0% ## Performance Metrics **Velocity:** - Total plans completed: 0 (v1.7) - Average duration: - - Total execution time: 0 hours ## Accumulated Context ### Decisions Decisions are logged in PROJECT.md Key Decisions table. Key constraints for v1.7: - content_jobs table + renderPipelineService stub must exist before any renderer is built — Phase 40 is the hard dependency for all other phases - Async job pattern is mandatory — all render requests return 202 + job ID immediately; never block HTTP on render - sourceTaskId is required on every generated asset from day one (prevents SSD orphan accumulation) - MAX_GENERATED_ASSET_BYTES constant bypasses the 10MB upload limit for generated/namespace — separate from upload route - Mermaid securityLevel must be "strict" — strip %%{init}%% and click directives before render, DOMPurify on SVG output - OKLCH via culori for all theme generation — HSL is forbidden as an intermediate (perceptually non-uniform) - Remotion bundle() called once at startup, not per-render — cached bundle path passed to renderMedia() per request - Remotion isolated in packages/content-renderer/ workspace package — webpack bundler must not enter Vite/tsc server context - Phase 42 and Phase 41 both depend on Phase 40 but are independent of each other (can parallelize if needed) - Phase 43 (PDF/Brand) depends on Phase 41 because PDF templates may reuse satori/SVG pipeline components - Phase 44 (Remotion) depends only on Phase 40 (job infra) — can start after Phase 40, independent of 41-43 - Phase 45 (Skills) is last — skill markdown files reference API contracts finalized in Phases 41-44 - AI-bridged conversion (CONV-05) is the fallback for all format pairs — never show a format pair as blocked - CONV-08: converter availability detected at startup via probe; unavailable direct paths fall to AI bridge - CONV-09: magic-byte MIME validation before processing — reject misnamed files with a clear error - [Phase 40]: content_jobs uses no FK for resultAssetId or sourceTaskId — avoids circular FK, tasks are string IDs not UUIDs - [Phase 40]: renderContent is a stub in Phase 40 — phases 41-45 add real renderers keyed by jobType - [Phase 40]: SSE uses EventEmitter subscription not polling for content_job.* events - [Phase 41-diagrams-icons-theme-engine]: Renderer stub files created to satisfy tsc module resolution — plans 02-04 replace with real implementations - [Phase 41-diagrams-icons-theme-engine]: puter-inference.ts created as shared non-streaming LLM helper for all server renderers - [Phase 41-diagrams-icons-theme-engine]: DOMPurify window cast uses any due to JSDOM/dompurify type incompatibility - [Phase 41-diagrams-icons-theme-engine]: Text role wcagAA always true — text is the reference color, not measured against itself; accent colors correctly report wcagAA: false at decorative L/C values - [Phase 41-diagrams-icons-theme-engine]: @types/culori and @types/wcag-contrast added as devDeps — culori v4 ships no TypeScript declarations; TS7016 errors resolved by DefinitelyTyped packages - [Phase 41-diagrams-icons-theme-engine]: DiagramSourcePanel dirty state set on onChange (not onBlur) — onBlur fires before state propagates in jsdom test environment - [Phase 41-diagrams-icons-theme-engine]: content-bundles.ts created in ui/src/types/ for shared DiagramBundle/IconSetBundle UI type contracts - [Phase 41]: @testing-library/react + jsdom added as UI devDeps — renderToStaticMarkup cannot test imperative DOM style.setProperty calls required by THEME-04 - [Phase 41]: ThemePreviewPanel scopes CSS vars to .nexus-theme-preview container ref; applyCustomTheme() sets on document.documentElement — two distinct patterns for preview vs global apply - [Phase 41]: THEME_META/ORDERED_THEMES re-added to ThemeContext as backward-compat exports for light/dark/custom — Phase 41-05 worktree commit dropped these, breaking Layout.tsx/MarkdownBody.tsx/InstanceGeneralSettings.tsx - [Phase 42-wallpapers-social-format-conversion-voice]: execFileNoThrow utility created as server/src/utils/ helper — plan referenced as 'project standard' but did not exist; created as missing critical functionality - [Phase 42-wallpapers-social-format-conversion-voice]: Stub renderer pattern: exports throw 'Not implemented' to satisfy tsc module resolution — Plans 02-04 replace with real implementations - [Phase 42-wallpapers-social-format-conversion-voice]: useSystemProviders uses plain useState+useEffect fetch (not React Query) for a lightweight one-time capability probe - [Phase 42-wallpapers-social-format-conversion-voice]: [Phase 42-wallpapers-social-format-conversion-voice]: Offline badge double guard: enableVoiceInput && providers?.whisperAvailable — shows only in voice mode with confirmed local Whisper binary - [Phase 42-wallpapers-social-format-conversion-voice]: PLATFORM_DIMENSIONS exported as named constant (12 platforms); sharp density:300 for SVG rasterization; PLATFORM_CHAR_LIMITS with 4 platforms; carousel returns slides[] in SocialPostBundle - [Phase 42-wallpapers-social-format-conversion-voice]: ffmpegPath cast as unknown as string for ffmpeg-static spawn — typings declare string|null but binary always present when package installed - [Phase 42-wallpapers-social-format-conversion-voice]: TEXT_BASED_EXTENSIONS allowlist prevents false 422 rejections for formats with no magic bytes (CSV/JSON/SVG) - [Phase 42-wallpapers-social-format-conversion-voice]: Direct fetch used in submitConvertJob to inspect 422 MIME error vs 202 success before throwing — api.request() throws on !res.ok but 422 is a valid business response - [Phase 42-wallpapers-social-format-conversion-voice]: Direct EventSource for pre-submitted convert jobs — useContentJob.submit() cannot track an existing jobId without re-submitting via contentJobs route - [Phase 42-wallpapers-social-format-conversion-voice]: FORMAT_GROUPS exported from ConvertPanel so ConvertPage can import for allowlist validation without duplicating the list - [Phase 42-wallpapers-social-format-conversion-voice]: WallpaperBundle/AppIconBundle/SocialPostBundle types defined locally in panel component files — no content-bundles.ts addition needed since these types are only consumed by their respective components - [Phase 43-documents-branding]: stripMarkdownFences duplicated locally in pdf-renderer — wallpaper-renderer does not export it - [Phase 43-documents-branding]: buildPdfSystemPrompt uses switch with 4 distinct LLM prompt variants per docType (report/invoice/api-docs/one-pager) - [Phase 43]: Social images in brand-renderer use SVG templates (colored rect + embedded logo) rather than LLM-generated — fast, deterministic, always on-brand - [Phase 43-documents-branding]: BrandKitBundle type defined in BrandKitResult.tsx and imported by BrandKitPanel — type co-located with display component, avoids duplication - [Phase 43-documents-branding]: iframe sandbox=allow-same-origin for email signature and letterhead previews — prevents script execution while allowing inline CSS - [Phase 44-video-presentations]: Remotion workspace package uses CommonJS module resolution — rspack bundler requires CJS, no type:module - [Phase 44-video-presentations]: getBundlePath caches bundle path at module level — bundle() called once at startup, not per-render - [Phase 44-video-presentations]: compositions/index.ts is UI-safe sub-export — no @remotion/bundler or @remotion/renderer imports - [Phase 44-video-presentations]: renderContent extended with optional companyId/jobId params for presentation SSE progress events - [Phase 44-video-presentations]: data.progress check uses typeof guard — prevents 0 (falsy) from triggering statusToProgress fallback in useContentJob - [Phase 44-video-presentations]: mp4Base64 blob URL created in useMemo and revoked in useEffect return — correct lifecycle for React renders - [Phase 44-video-presentations]: Ambient module declaration in server/src/types/content-renderer.d.ts provides type safety for dynamic import without pulling JSX into server tsc context - [Phase 44-video-presentations]: content-renderer NOT added as workspace dep in server/package.json — symlink causes tsc to walk JSX source; ambient declaration is sufficient for type safety and runtime works via monorepo node_modules - [Phase 45-content-as-skills]: SkillSourceConfig changed to discriminated union; local-nexus-content source uses SHA-1 content hash for idempotency; seedCreativeGroupMembers runs after fetchAll in unified startup block ### Pending Todos None yet. ### Blockers/Concerns - [v1.7 pre-start] Verify correct resvg package name: `@resvg/resvg-js` (v2.6.2) vs `resvg-js` (v0.1.97) — run `npm info @resvg/resvg-js` before pnpm add in Phase 41 - [v1.7 pre-start] Check whether playwright-chromium and @mermaid-js/mermaid-cli can share a Chromium binary via PUPPETEER_EXECUTABLE_PATH — could save ~300MB on Mac Mini SSD - [v1.7 pre-start] Run pnpm build after adding packages/content-renderer/ to verify no Vite/webpack conflicts before Phase 44 implementation - [v1.7 pre-start] Confirm pdf-lib scope: Playwright for design-rich PDFs, pdf-lib for data-driven invoices — decide at Phase 43 planning ## Session Continuity Last session: 2026-04-04T23:55:26.416Z Stopped at: Completed 45-01-PLAN.md — 9 content SKILL.md files, local-nexus-content source type, Creative group seeded, unified startup sequence, tsc clean Resume file: None