---
phase: 42-wallpapers-social-format-conversion-voice
plan: 03
type: execute
wave: 2
depends_on: [42-01]
files_modified:
  - server/src/services/renderers/convert-renderer.ts
  - server/src/routes/convert.ts
  - server/src/app.ts
autonomous: true
requirements: [CONV-01, CONV-02, CONV-03, CONV-04, CONV-05, CONV-06, CONV-07, CONV-09]
must_haves:
  truths:
    - "Image format conversion (PNG, JPG, SVG, WebP, GIF) works via sharp"
    - "Audio/video format conversion works via ffmpeg-static"
    - "Data format conversion (CSV, JSON, XLSX) works via xlsx + csv-parse"
    - "Unsupported format pairs fall through to AI-bridge via puterChatComplete"
    - "Uploaded files are validated via magic-byte detection before processing"
    - "POST /api/companies/:companyId/convert accepts multipart upload and returns 202"
    - "GET /api/system/converters returns capability map"
  artifacts:
    - path: "server/src/services/renderers/convert-renderer.ts"
      provides: "Format conversion router: sharp, ffmpeg, xlsx/csv, AI-bridge"
      exports: ["renderConvert"]
    - path: "server/src/routes/convert.ts"
      provides: "Multipart upload route + capability endpoint"
      contains: "fileTypeFromBuffer"
    - path: "server/src/app.ts"
      provides: "Mount point for convert routes"
      contains: "convertRoutes"
  key_links:
    - from: "server/src/routes/convert.ts"
      to: "server/src/services/renderers/convert-renderer.ts"
      via: "content job dispatch with convert type"
      pattern: "jobType.*convert"
    - from: "server/src/routes/convert.ts"
      to: "file-type"
      via: "magic-byte MIME validation"
      pattern: "fileTypeFromBuffer"
    - from: "server/src/services/renderers/convert-renderer.ts"
      to: "sharp"
      via: "image format conversion"
      pattern: "sharp"
    - from: "server/src/services/renderers/convert-renderer.ts"
      to: "ffmpeg-static"
      via: "audio/video conversion"
      pattern: "ffmpegPath"
---

<objective>
Implement the format conversion renderer (routing to sharp/ffmpeg/xlsx/AI-bridge by format pair) and the multipart upload route with magic-byte MIME validation.

Purpose: This plan fulfills all CONV requirements on the server side. The conversion UI in Plan 06 depends on these endpoints.
Output: Working convert-renderer.ts, convert route with MIME validation, capability endpoint.
</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/phases/42-wallpapers-social-format-conversion-voice/42-RESEARCH.md
@.planning/phases/42-wallpapers-social-format-conversion-voice/42-01-SUMMARY.md

@server/src/services/renderers/types.ts
@server/src/services/converter-capabilities.ts
@server/src/routes/chat-files.ts
@server/src/routes/content-jobs.ts
@server/src/app.ts
@server/src/services/puter-inference.ts
</context>

<interfaces>
From server/src/services/renderers/types.ts (after Plan 01):
```typescript
export interface ConvertBundle {
  type: "convert-bundle";
  outputFilename: string;
  outputMime: string;
  outputBase64: string;
  method: "direct" | "ai-bridge";
}
export interface RenderResult {
  filename: string;
  contentType: string;
  buffer: Buffer;
}
```

From server/src/services/converter-capabilities.ts (Plan 01):
```typescript
export interface ConverterCapabilities {
  imageConverter: boolean;
  audioVideoConverter: boolean;
  docConverter: boolean;
  dataConverter: boolean;
}
export function converterCapabilitiesService(): { get(): Promise<ConverterCapabilities> };
```

From server/src/routes/chat-files.ts (multer pattern):
```typescript
const fileUpload = multer({ storage: multer.memoryStorage() });
```
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Implement convert renderer with format routing</name>
  <files>server/src/services/renderers/convert-renderer.ts</files>
  <read_first>server/src/services/renderers/icon-renderer.ts, server/src/services/renderers/types.ts, server/src/services/puter-inference.ts</read_first>
  <action>
Replace the stub convert-renderer.ts with a full implementation:

1. Export `async function renderConvert(input: Record<string, unknown>): Promise<RenderResult>`:
   - Extract fileBase64 (string), sourceMime (string), targetFormat (string), originalFilename (string) from input
   - Decode fileBase64 to Buffer
   - Route by format category using helper functions:

2. Image conversion (isImagePair): use sharp
   - `sharp(fileBuffer).toFormat(targetFormat as keyof sharp.FormatEnum).toBuffer()`
   - Supported: png, jpg/jpeg, webp, gif, svg (SVG input handled by sharp with density: 300)
   - Return ConvertBundle with method: "direct"

3. Audio/video conversion (isAVPair): use ffmpeg-static
   - Copy the ffmpeg spawn pattern from research (Pitfall 3: use `ffmpegPath as unknown as string`)
   - Spawn ffmpeg with `-f {sourceFormat} -i pipe:0 -f {targetFormat} pipe:1`
   - stdin.write(fileBuffer), stdin.end(), collect stdout chunks
   - Return ConvertBundle with method: "direct"

4. Data conversion (isDataPair): use xlsx + csv-parse
   - CSV to JSON: parse CSV with csv-parse, return JSON array
   - JSON to CSV: read JSON array, generate CSV with headers from first object keys
   - XLSX to JSON: xlsx.read(buffer), first sheet to json
   - JSON to XLSX: build worksheet from JSON array, write xlsx buffer
   - CSV to XLSX: parse CSV first, then build xlsx
   - XLSX to CSV: read xlsx to JSON, then serialize CSV
   - Return ConvertBundle with method: "direct"

5. AI-bridge fallback (all other pairs): use puterChatComplete
   - System prompt: "Convert the following {sourceMime} content to {targetFormat} format. Return ONLY the converted content, no explanation."
   - For text-based formats: pass file content as string
   - For binary formats: describe the conversion needed and return best-effort result
   - Return ConvertBundle with method: "ai-bridge"

6. Helper functions (not exported):
   - `isImageFormat(mime: string): boolean` — checks for image/png, image/jpeg, image/webp, image/gif, image/svg+xml
   - `isAVFormat(mime: string): boolean` — checks for audio/* and video/*
   - `isDataFormat(mime: string): boolean` — checks for text/csv, application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
   - `getTargetMime(targetFormat: string): string` — maps format string to MIME type
  </action>
  <verify>
    <automated>cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20</automated>
  </verify>
  <acceptance_criteria>
    - grep "renderConvert" server/src/services/renderers/convert-renderer.ts
    - grep "sharp" server/src/services/renderers/convert-renderer.ts
    - grep "ffmpegPath" server/src/services/renderers/convert-renderer.ts
    - grep "xlsx" server/src/services/renderers/convert-renderer.ts
    - grep "csv-parse" server/src/services/renderers/convert-renderer.ts
    - grep "puterChatComplete" server/src/services/renderers/convert-renderer.ts
    - grep "ai-bridge" server/src/services/renderers/convert-renderer.ts
    - grep "isImageFormat" server/src/services/renderers/convert-renderer.ts
  </acceptance_criteria>
  <done>Convert renderer routes to sharp (images), ffmpeg (audio/video), xlsx/csv-parse (data), and AI-bridge (fallback) based on format pair.</done>
</task>

<task type="auto">
  <name>Task 2: Create multipart convert route with MIME validation and wire to app.ts</name>
  <files>server/src/routes/convert.ts, server/src/app.ts</files>
  <read_first>server/src/routes/chat-files.ts, server/src/routes/content-jobs.ts, server/src/app.ts</read_first>
  <action>
1. Create server/src/routes/convert.ts following the chat-files.ts multer pattern:

   ```typescript
   import { Router } from "express";
   import multer from "multer";
   import { fileTypeFromBuffer } from "file-type";
   ```

   POST /api/companies/:companyId/convert:
   - multer.memoryStorage() with single("file") upload
   - Extract targetFormat from req.body.targetFormat
   - Run fileTypeFromBuffer(file.buffer) for magic-byte detection
   - Build a map of extension-to-expected-MIME for validation:
     png→image/png, jpg/jpeg→image/jpeg, gif→image/gif, webp→image/webp, svg→image/svg+xml, mp3→audio/mpeg, mp4→video/mp4, wav→audio/wav, ogg→audio/ogg, csv→text/csv, json→application/json, xlsx→application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
   - If fileTypeFromBuffer returns a result AND the detected MIME differs from the extension-implied MIME, return 422 with { error, actualMime, claimedMime } (CONV-09)
   - Note: text-based files (CSV, JSON, SVG, MD, HTML) may return null from fileTypeFromBuffer — allow these through (they have no magic bytes)
   - Create content job via contentJobStore (follow content-jobs.ts pattern): jobType "convert", input { fileBase64: buffer.toString('base64'), sourceMime, targetFormat, originalFilename }
   - Dispatch via contentJobRunner
   - Return 202 with { jobId, status: "queued" }

   GET /api/system/converters:
   - Import converterCapabilitiesService from converter-capabilities.ts
   - Return JSON of capabilities

2. Wire in server/src/app.ts:
   - Import convertRoutes from "./routes/convert.js"
   - Mount on the api router alongside existing content-jobs routes
   - Place AFTER the body-parser middleware (multer needs raw body access)

CRITICAL: Use file-type ESM named import `{ fileTypeFromBuffer }` — NOT default import (Pitfall 2 from research).
CRITICAL: Do NOT validate text-based files that return null from fileTypeFromBuffer — they have no magic bytes.
  </action>
  <verify>
    <automated>cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20</automated>
  </verify>
  <acceptance_criteria>
    - grep "fileTypeFromBuffer" server/src/routes/convert.ts
    - grep "multer" server/src/routes/convert.ts
    - grep "targetFormat" server/src/routes/convert.ts
    - grep "422" server/src/routes/convert.ts
    - grep "actualMime" server/src/routes/convert.ts
    - grep "system/converters" server/src/routes/convert.ts
    - grep "convertRoutes" server/src/app.ts
    - grep "202" server/src/routes/convert.ts
  </acceptance_criteria>
  <done>Multipart convert route validates MIME via magic bytes, dispatches async job, returns 202. Capability endpoint exposes converter availability. Route mounted in app.ts.</done>
</task>

</tasks>

<verification>
- `cd /opt/nexus/server && npx tsc --noEmit` passes with zero errors
- Convert route handles multipart upload with MIME validation
- GET /api/system/converters endpoint exists
- All format categories (image, AV, data, AI-bridge) have conversion paths
</verification>

<success_criteria>
- Convert renderer routes all format pairs to appropriate converter or AI fallback
- Multipart upload route validates MIME via magic bytes (CONV-09)
- Route returns 202 with job ID (async pattern)
- Capability endpoint returns converter availability map (CONV-08)
- tsc compiles cleanly
</success_criteria>

<output>
After completion, create `.planning/phases/42-wallpapers-social-format-conversion-voice/42-03-SUMMARY.md`
</output>
