nexus/.planning/research/ARCHITECTURE.md
2026-04-04 04:25:21 +00:00

535 lines
27 KiB
Markdown

# Architecture Research
**Domain:** Content generation integration — Nexus v1.7
**Researched:** 2026-04-04
**Confidence:** HIGH (based on direct codebase inspection of /opt/nexus)
## Standard Architecture
### System Overview
```
+---------------------------------------------------------------------------------+
| UI Layer (React/Vite) |
| +------------------+ +------------------+ +--------------+ +----------------+ |
| | ChatPanel | | ContentJobViewer | | ThemePreview | | DiagramRenderer| |
| | (existing, | | (new) | | (new) | | (new, wraps | |
| | minor extension)| | progress+result | | CSS vars | | mermaid dep) | |
| +--------+---------+ +--------+---------+ +------+-------+ +-------+--------+ |
| | | | | |
+-----------|--------------------|--------------------|-----------------|------------+
| HTTP/SSE | HTTP/SSE | HTTP | (client-side)
+-----------|--------------------|--------------------|-------------------------+
| | API Layer (Express) | |
| +--------v----------------------------------------------v------------------+ |
| | /api/companies/:id/content-jobs (new) | |
| | /api/content-jobs/:id (new) | |
| | /api/companies/:id/themes/generate (new) | |
| +-------------------------------------------------------------------------+ |
+---------------------------------------------------------------------------------+
| Service Layer (Node.js) |
| +-------------------+ +------------------+ +---------------------------------+|
| | contentJobService | | themeEngineService| | renderPipelineService ||
| | (new) | | (new) | | (new) ||
| | enqueue, status, | | palette gen, | | routes jobs to renderer adapters ||
| | list | | WCAG check,export | | ||
| +--------+----------+ +------------------+ +---------------+-----------------+|
| | | |
| +--------v----------------------------------------------------v--------------+ |
| | Renderer Adapters (new, behind interface) | |
| | +-------------+ +------------+ +----------+ +-----------+ +----------+ | |
| | | Mermaid | | SVG | | Remotion | | PDF | | Image | | |
| | | (isomorphic)| | (generator)| | (CLI) | | (Puppeteer| | (Sharp) | | |
| | +-------------+ +------------+ +----------+ +-----------+ +----------+ | |
| +-------------------------------------------------------------------------+ |
+---------------------------------------------------------------------------------+
| Storage + Events Layer (existing, minimally extended) |
| +------------------+ +--------------------+ +-----------------------------+ |
| | StorageService | | publishLiveEvent | | assets table (existing) | |
| | (existing) | | (existing +3 types)| | content_jobs table (new) | |
| +------------------+ +--------------------+ +-----------------------------+ |
+---------------------------------------------------------------------------------+
```
### Component Responsibilities
| Component | Responsibility | Status | Notes |
|-----------|----------------|--------|-------|
| `contentJobService` | Queue and track async render jobs; emit live events on status change | New | Factory function, matches `chatService` pattern |
| `renderPipelineService` | Route render requests to the correct renderer adapter | New | Strategy pattern over adapters |
| `themeEngineService` | Palette generation, WCAG AA validation, CSS/JSON/Tailwind exports | New | Pure computation, no DB, deterministic |
| `mermaidRendererAdapter` | Mermaid DSL string to SVG buffer, server-side | New | Uses `@mermaid-js/mermaid-isomorphic`; no Chromium needed |
| `remotionRendererAdapter` | Invoke Remotion CLI subprocess, return MP4/WebM path | New | Subprocess; outputs go to storage namespace `generated/videos` |
| `svgGeneratorAdapter` | Template-based SVG generation (icons, banners, placeholders) | New | No binary deps; pure string construction + existing sanitizer |
| `pdfRendererAdapter` | HTML to PDF via Puppeteer (arm64 Chromium on M4) | New | Subprocess; Puppeteer arm64 works on Apple Silicon |
| `imageProcessorAdapter` | Composite and resize via Sharp | Modified | Sharp already in `server/package.json`; extend for content use |
| `placeholderService` | Manifest tracking for draft assets | Existing | Already implemented; optionally extend PlaceholderEntry with `contentJobId` |
| `assetService` | CRUD for the `assets` table | Existing | Already handles `createdByAgentId`; use as-is |
| `StorageService` | Provider-agnostic blob storage | Existing | Use `generated/` namespace prefix for all new content |
| `publishLiveEvent` | SSE fan-out to UI subscribers | Existing | Extend `LIVE_EVENT_TYPES` with 3 new content job event types |
| `ContentJobViewer` (UI) | Poll/stream job status; show progress, render result inline | New | Subscribes to SSE live events |
| `DiagramRenderer` (UI) | Client-side Mermaid render using existing `mermaid` dep | New | `mermaid ^11.12.0` already in `ui/package.json` |
| `ThemePreview` (UI) | Live palette preview via CSS custom properties | New | No server round-trip for preview |
| `ContentGallery` (UI) | Workspace page showing all generated assets | New | Pagination via `assetService.list` |
## Recommended Project Structure
New files follow existing monorepo conventions: factory functions, co-located types, no class syntax.
```
server/src/
├── services/
│ ├── content-job.ts # contentJobService factory
│ ├── render-pipeline.ts # renderPipelineService — adapter dispatch
│ ├── theme-engine.ts # themeEngineService — pure palette computation
│ └── renderers/
│ ├── index.ts # RendererAdapter interface + barrel
│ ├── mermaid-renderer.ts # Mermaid DSL -> SVG (server-side isomorphic)
│ ├── remotion-renderer.ts # Remotion CLI subprocess wrapper
│ ├── svg-generator.ts # Template SVG (icons, placeholders, banners)
│ └── pdf-renderer.ts # HTML -> PDF via Puppeteer
├── routes/
│ ├── content-jobs.ts # GET/POST /companies/:id/content-jobs
│ └── themes.ts # POST /companies/:id/themes/generate
└── types/
└── content.ts # Server-internal ContentJobType, ContentJobStatus
packages/db/src/
├── schema/
│ └── content_jobs.ts # NEW table (upstream-safe, no upstream equivalent)
└── migrations/
└── NNNN_add_content_jobs.sql
packages/shared/src/
├── types/
│ └── content.ts # ContentJob, ContentJobStatus shared types
└── constants.ts # LIVE_EVENT_TYPES extended (+3 content.job.* types)
packages/
└── remotion-compositions/ # NEW workspace package
├── package.json
└── src/
└── index.ts # Remotion composition definitions
ui/src/
├── components/
│ ├── ContentJobViewer.tsx # Job progress + result display
│ ├── ContentJobCard.tsx # Compact job status card
│ ├── DiagramRenderer.tsx # Mermaid client-side wrapper
│ ├── ThemePreview.tsx # Live palette preview
│ └── GeneratedAssetCard.tsx # Thumbnail + download + metadata
└── pages/
└── ContentGallery.tsx # Gallery of generated assets per workspace
```
### Structure Rationale
- **`server/src/services/renderers/`**: Isolates binary-dependent adapters behind a shared `RendererAdapter` interface. New renderers plug in without touching the core job service.
- **`content_jobs` table**: Separate from `assets`. A job tracks render lifecycle (queued to running to done/failed); on success it writes an `assets` row and records the `assetId`. This mirrors how `heartbeat_runs` tracks execution separately from its outputs.
- **`packages/remotion-compositions/`**: Remotion compositions must be bundled ahead of time. Keeping them in a dedicated workspace package lets the bundle step run once at startup, not on every render request.
- **Content as skills**: Skills (`company_skills`) are Markdown instruction files. Content type skills tell agents which `/api/companies/:id/content-jobs` endpoint to call and with what parameters. No new schema needed.
## Architectural Patterns
### Pattern 1: Async Job with SSE Progress
**What:** Long-running renders (Remotion, Puppeteer PDF) run asynchronously. The service creates a `content_jobs` row with `status: "queued"`, immediately returns the job record, then spawns the renderer. Live events push progress to the UI over the existing SSE stream.
**When to use:** Any render taking more than ~200ms: Remotion, PDF, large Mermaid diagrams. Fast operations (SVG generation, theme palette) can be synchronous HTTP.
**Trade-offs:** One DB row per render. Adds durable history of what was generated. Acceptable at solo-user scale.
**Example:**
```typescript
// server/src/services/content-job.ts
export function contentJobService(db: Db, storage: StorageService) {
return {
async enqueue(companyId: string, input: ContentJobInput): Promise<ContentJob> {
const [row] = await db
.insert(contentJobs)
.values({ companyId, type: input.type, params: input.params, status: "queued" })
.returning();
publishLiveEvent({ companyId, type: "content.job.started", payload: { jobId: row.id } });
// Non-blocking — kick off render
renderPipelineService(storage).render(row)
.then(async (result) => {
await db.update(contentJobs)
.set({ status: "done", assetId: result.assetId, completedAt: new Date() })
.where(eq(contentJobs.id, row.id));
publishLiveEvent({ companyId, type: "content.job.done", payload: { jobId: row.id, assetId: result.assetId } });
})
.catch(async (err) => {
await db.update(contentJobs)
.set({ status: "failed", errorMessage: String(err) })
.where(eq(contentJobs.id, row.id));
publishLiveEvent({ companyId, type: "content.job.failed", payload: { jobId: row.id } });
});
return toContentJob(row);
},
};
}
```
### Pattern 2: RendererAdapter Interface
**What:** Each renderer implements a shared interface. `renderPipelineService` selects the adapter based on `ContentJobType`. Adding a new renderer requires only: (a) implement the interface, (b) register in the dispatch table.
**When to use:** Every new content type.
**Trade-offs:** Thin abstraction, no framework needed. Appropriate for the codebase size and single-user scale.
**Example:**
```typescript
// server/src/services/renderers/index.ts
export interface RendererAdapter {
type: ContentJobType;
render(
params: Record<string, unknown>,
storage: StorageService,
companyId: string
): Promise<{ objectKey: string; contentType: string; byteSize: number }>;
}
```
### Pattern 3: Content Types as Skill Files
**What:** A Mermaid-generation "skill" is a Markdown file in `company_skills` that instructs agents: "When asked for a diagram, call `POST /api/companies/:id/content-jobs` with `{type: 'mermaid', params: {dsl: '...'}}` and wait for `content.job.done` event." No new schema required.
**When to use:** All content types — this is how they become installable skills.
**Trade-offs:** Agent must know the API contract, included in the skill markdown. Works with all adapters (Claude Code, Hermes, Ollama) since skills are plain text.
### Pattern 4: Theme Engine as Pure Function, Preview Client-Side
**What:** Theme generation is a pure computation: seed hex color in, palette object out. Preview injects CSS custom properties directly into the DOM — no server round-trip. Saving a theme stores the palette JSON via `StorageService`.
**When to use:** Theme generation and live preview.
**Trade-offs:** No server latency for preview feedback. The JSON is reusable for all export formats (CSS, Tailwind config, design tokens).
## Data Flow
### Async Content Job Request Flow
```
Agent / UI
|
v
POST /api/companies/:id/content-jobs
{ type: "mermaid", params: { dsl: "graph TD..." } }
|
v
contentJobService.enqueue()
INSERT content_jobs WHERE status = "queued"
publishLiveEvent("content.job.started")
(non-blocking) -> renderPipelineService.render()
|
v (async)
renderPipelineService
selects MermaidRendererAdapter
|
v
MermaidRendererAdapter.render()
Mermaid DSL -> SVG Buffer (via @mermaid-js/mermaid-isomorphic)
|
v
StorageService.putFile()
objectKey: "{companyId}/generated/diagrams/2026/04/04/{uuid}-diagram.svg"
|
v
assetService.create()
INSERT assets (objectKey, contentType, byteSize, createdByAgentId)
|
v
contentJobService callback
UPDATE content_jobs SET status = "done", asset_id = ...
publishLiveEvent("content.job.done", { jobId, assetId })
|
v
UI (SSE subscriber)
ContentJobViewer receives event -> fetches asset URL -> renders preview inline
```
### Theme Generation Flow (Synchronous)
```
User picks seed color
|
v
ThemePreview component
(no server round-trip — CSS custom properties injected directly into DOM)
|
v (on "Save Theme")
POST /api/companies/:id/themes/generate
{ seedColor: "#4a90d9" }
|
v
themeEngineService.generate()
Compute palette (tints, shades, semantic tokens, WCAG AA checks)
Returns palette JSON
|
v
StorageService.putFile()
objectKey: "{companyId}/themes/{uuid}-theme.json"
contentType: "application/json"
|
v
assetService.create() -> 201 { assetId, downloadUrl }
```
### Mermaid Client-Side Fast Path
```
Agent sends message with ```mermaid code block
|
v
ChatMarkdownMessage (existing component, minor extension)
Detects ```mermaid fence
|
v
DiagramRenderer (new component, wraps existing mermaid dep)
Calls mermaid.render() client-side (mermaid ^11.12 already in ui/package.json)
Displays SVG inline
"Save as asset" button -> POST /api/companies/:id/content-jobs (server path)
```
### State Transitions: content_jobs
```
queued -> running (renderPipelineService picks up job)
running -> done (renderer returns, asset created)
running -> failed (renderer throws, error recorded)
```
## New vs Modified: Explicit Breakdown
### New (does not exist)
| Artifact | Type | Purpose |
|----------|------|---------|
| `content_jobs` table | DB schema + migration | Track async render job lifecycle |
| `contentJobService` | Server service | Enqueue, status, list jobs |
| `renderPipelineService` | Server service | Route jobs to renderer adapters |
| `themeEngineService` | Server service | Palette generation + WCAG validation |
| `mermaidRendererAdapter` | Server renderer | Server-side Mermaid to SVG |
| `remotionRendererAdapter` | Server renderer | Remotion CLI to MP4/WebM |
| `svgGeneratorAdapter` | Server renderer | Template SVG generation |
| `pdfRendererAdapter` | Server renderer | HTML to PDF via Puppeteer |
| `content-jobs.ts` route | API route | Create and list content jobs |
| `themes.ts` route | API route | Synchronous theme generation |
| `packages/remotion-compositions/` | Workspace package | Remotion composition definitions |
| `packages/shared/src/types/content.ts` | Shared type | `ContentJob`, `ContentJobStatus` |
| `ContentJobViewer` | UI component | Job progress + result display |
| `DiagramRenderer` | UI component | Client-side Mermaid wrapper |
| `ThemePreview` | UI component | Live palette preview |
| `GeneratedAssetCard` | UI component | Asset thumbnail + actions |
| `ContentGallery` | UI page | Workspace content library |
### Modified (exists, needs extension)
| Artifact | Change | Risk |
|----------|--------|------|
| `packages/shared/src/constants.ts` | Add 3 new `LIVE_EVENT_TYPES`: `content.job.started`, `content.job.done`, `content.job.failed` | LOW — additive only |
| `server/src/app.ts` | Mount `contentJobRoutes` and `themeRoutes` | LOW — two lines |
| `ChatMarkdownMessage` | Detect triple-backtick mermaid fence; render via `DiagramRenderer` | MEDIUM — existing component, test carefully |
| `assetService` | Add `list(companyId, opts)` method for gallery pagination | LOW — new method, no schema change |
| `packages/db/src/schema/index.ts` | Export `contentJobs` table | LOW |
| `packages/db/src/index.ts` | Export `contentJobs` from db package | LOW |
## Data Model
### New Table: `content_jobs`
No changes to existing tables. Standalone new table; upstream-safe because Paperclip has no content generation system.
```sql
CREATE TABLE content_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company_id UUID NOT NULL REFERENCES companies(id),
type TEXT NOT NULL, -- 'mermaid' | 'remotion' | 'pdf' | 'svg' | 'theme' | 'image'
status TEXT NOT NULL DEFAULT 'queued', -- 'queued' | 'running' | 'done' | 'failed'
params JSONB NOT NULL DEFAULT '{}', -- renderer-specific input params
asset_id UUID REFERENCES assets(id), -- set on success
error_message TEXT, -- set on failure
created_by_agent_id UUID REFERENCES agents(id),
created_by_user_id TEXT,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX content_jobs_company_status_idx ON content_jobs(company_id, status);
CREATE INDEX content_jobs_company_created_idx ON content_jobs(company_id, created_at DESC);
```
### Storage Namespaces (extends existing StorageService path conventions)
```
{companyId}/generated/diagrams/YYYY/MM/DD/{uuid}-{name}.svg
{companyId}/generated/videos/YYYY/MM/DD/{uuid}-{name}.mp4
{companyId}/generated/pdfs/YYYY/MM/DD/{uuid}-{name}.pdf
{companyId}/generated/images/YYYY/MM/DD/{uuid}-{name}.png
{companyId}/generated/icons/YYYY/MM/DD/{uuid}-{name}.svg
{companyId}/themes/{uuid}-theme.json
{companyId}/placeholders/{uuid}-placeholder.svg
```
The `StorageService.putFile()` method already handles path construction from `namespace` + `originalFilename` + timestamp. Pass `namespace: "generated/diagrams"` etc.
## Integration Points
### Existing System Touch Points
| Integration Point | How Content Gen Connects | Notes |
|-------------------|--------------------------|-------|
| `assetService` + `assets` table | Every rendered output creates an asset row | `createdByAgentId` already supported; agents get credit |
| `StorageService` | All rendered blobs stored via existing `putFile()` | Use `generated/` namespace prefix; no service changes |
| `publishLiveEvent` | Job lifecycle events push to SSE stream | Extend `LIVE_EVENT_TYPES` in `packages/shared/src/constants.ts` |
| `ChatMarkdownMessage` | Inline diagram rendering; "save as asset" button | Mermaid already a UI dep; add `DiagramRenderer` wrapper |
| `companySkills` + `skill-registry` | Content types as installable skill markdown files | No schema change; skills are text files agents read as context |
| `placeholderService` | Placeholder assets tracked in PLACEHOLDERS.md manifest | Optionally extend `PlaceholderEntry` with `contentJobId` |
| `hardwareService` | Detect if Remotion/Puppeteer can run | M4 Mac Mini: arm64 Chromium available, 24GB unified memory sufficient |
| `companyId` scoping | All content jobs scoped to `companyId` | Consistent with every other resource in the system |
| Agent task sessions | Agents invoke content APIs during task execution | Use `createdByAgentId`; same pattern as `documents`, `work-products` |
### External Dependencies (new server deps)
| Dependency | Purpose | Platform Notes |
|------------|---------|---------------|
| `@mermaid-js/mermaid-isomorphic` | Server-side Mermaid to SVG | No Chromium needed; fast; preferred over Puppeteer for Mermaid |
| `puppeteer` | HTML to PDF | ~300MB install; bundled arm64 Chromium works on M4; only add if PDF is a phase priority |
| `remotion` (CLI) | Video/presentation render | Add as devDep in `remotion-compositions` package; CLI called via subprocess |
Mermaid client-side and Sharp are already present. No changes needed for those paths.
## Scaling Considerations
This is a Mac Mini M4 single-user deployment. Analysis focuses on resource contention, not user count.
| Concern | Approach |
|---------|----------|
| Concurrent render jobs | Node.js event loop is safe for I/O. CPU-bound renders (Remotion, Puppeteer) spawn subprocesses, keeping the event loop responsive. |
| Remotion render duration | Renders can take minutes. Never synchronous HTTP. Async job pattern + SSE progress is mandatory. |
| Chromium memory (PDF/Puppeteer) | Puppeteer can use 500MB+ per render. Serialize PDF renders via an in-memory queue (one at a time). |
| Storage growth | Generated content accumulates. Add `retention_days` field to `content_jobs`; implement a cleanup cron using the existing `cron.ts` service. |
| Remotion bundle step | Bundle compositions once at server startup (or on demand). Never bundle on each render request — it takes 30-60s. |
## Build Order (Phase Dependencies)
Dependencies flow from infrastructure upward to content types upward to UI.
```
Phase A: Core Infrastructure (unblocks everything)
- Add content_jobs schema + migration (db package)
- Extend LIVE_EVENT_TYPES with content.job.* (shared package)
- Implement contentJobService (server)
- Implement renderPipelineService stub (server)
- Add API routes + app.ts mounts (server)
Phase B: Fast Content Types (no heavy binary deps; validates pipeline end-to-end)
- svgGeneratorAdapter (pure TypeScript; icons, placeholders)
- mermaidRendererAdapter (@mermaid-js/mermaid-isomorphic; no Chromium)
- themeEngineService (pure computation)
- UI: DiagramRenderer, ThemePreview, ContentJobViewer
Phase C: Client-Side Mermaid + Content Gallery
- ChatMarkdownMessage extension (detect mermaid fence)
- DiagramRenderer client-side component
- ContentGallery page + assetService.list()
- GeneratedAssetCard
Phase D: Document Generation (introduces Puppeteer)
- Add puppeteer to server deps
- pdfRendererAdapter
- PDF download flow in UI
Phase E: Video / Presentations (introduces Remotion)
- packages/remotion-compositions/ workspace package
- remotionRendererAdapter (CLI subprocess)
- Video playback in UI
Phase F: Image Generation
- imageProcessorAdapter using Sharp (banners, OG images, social cards)
- imageGenerationAdapter interface (Stable Diffusion / cloud APIs — future)
- Social media content generation
Phase G: Content as Skills (no code, pure skill markdown)
- Skill markdown files for each content type in company_skills
- Agent-callable via existing skill system
```
## Anti-Patterns
### Anti-Pattern 1: Rendering inside chatService
**What people do:** Add Mermaid rendering to `chatService` or `documentService` because content requests arrive from chat.
**Why it's wrong:** Couples unrelated concerns. Future content types (video, PDF) would bloat chatService and block upstream rebases.
**Do this instead:** `chatService` calls `contentJobService.enqueue()`. Rendering is entirely separate. Chat is a trigger, not an owner.
### Anti-Pattern 2: Synchronous HTTP response for long renders
**What people do:** `POST /render/remotion` holds the connection open for 2+ minutes while rendering.
**Why it's wrong:** HTTP timeout (30s default on most proxies). No progress feedback. Retry hell.
**Do this instead:** Return a `contentJobId` immediately with `202 Accepted`. Client subscribes to SSE `content.job.done` event.
### Anti-Pattern 3: One DB table per content type
**What people do:** Add separate `diagrams`, `presentations`, `themes` tables.
**Why it's wrong:** The existing `assets` table already handles typed binary blobs. The `content_jobs` table handles any render job regardless of output type. Fragmented schema multiplies migration surface.
**Do this instead:** Use `content_jobs.type` to discriminate job types. Use `assets.content_type` to discriminate output format. One jobs table, one assets table.
### Anti-Pattern 4: Bypassing StorageService for renderer output
**What people do:** Remotion adapter writes to `/tmp` and returns a filesystem path.
**Why it's wrong:** Bypasses the provider abstraction (local disk vs S3), deduplication (sha256), the `assets` table, and download URL generation.
**Do this instead:** Renderer writes output to a `Buffer`, passes to `StorageService.putFile()`, returns `objectKey`. Asset serving goes through the existing `/api` asset download route.
### Anti-Pattern 5: Modifying upstream DB tables
**What people do:** Add a `generated_content_type` column to the existing `assets` table.
**Why it's wrong:** Modifies upstream schema — migration conflict on next `git rebase upstream/master`. Violates the display-only fork constraint.
**Do this instead:** Use the `content_jobs.asset_id` FK as the signal that an asset is generated. Query `content_jobs JOIN assets` to distinguish generated from uploaded. Keep `assets` table untouched.
### Anti-Pattern 6: Remotion bundle on every render request
**What people do:** Call `bundle()` inside the render adapter on each job.
**Why it's wrong:** Bundling takes 30-60s. Renders that should take 5s take 90s.
**Do this instead:** Bundle once at server startup (or lazily on first render, cached). The `remotionRendererAdapter` calls `renderMedia()` against the pre-built bundle path.
## Sources
- Direct inspection of `/opt/nexus` codebase (2026-04-04):
- `server/src/services/` — factory function service patterns
- `server/src/storage/``StorageService` / `StorageProvider` interfaces
- `server/src/storage/service.ts``buildObjectKey()` namespace + path conventions
- `server/src/services/live-events.ts` — SSE event bus (`publishLiveEvent`, `subscribeCompanyLiveEvents`)
- `server/src/services/voice-pipeline.ts` — async subprocess service pattern
- `server/src/services/placeholder-service.ts` — existing `PlaceholderEntry` manifest service
- `server/src/services/assets.ts``assetService` factory (minimal; extend for listing)
- `server/src/services/work-products.ts` — job/output separation pattern
- `packages/db/src/schema/assets.ts` — existing `assets` table
- `packages/db/src/schema/documents.ts` — document + revision pattern
- `packages/shared/src/constants.ts``LIVE_EVENT_TYPES` (currently 9 types)
- `server/src/app.ts` — route mounting conventions
- `server/src/routes/voice.ts` — SSE streaming response pattern
- `ui/package.json` — confirms `mermaid ^11.12.0` already installed
- `server/package.json` — confirms `sharp`, `ffmpeg-static` already installed
- Mermaid v11 isomorphic: https://mermaid.js.org/config/usage.html
- Remotion CLI rendering: https://www.remotion.dev/docs/cli/render
- `@mermaid-js/mermaid-isomorphic` for server-side rendering without a browser
---
*Architecture research for: Nexus v1.7 Content Generation*
*Researched: 2026-04-04*