PLAN.md
9,933 bytes
| 1 | # PLAN: VoiceTask |
|---|---|
| 2 | |
| 3 | ## Stack |
| 4 | |
| 5 | - Node.js >= 20, TypeScript strict, npm. |
| 6 | - Server: Fastify (`fastify`, `@fastify/multipart`, `@fastify/static`, `@fastify/cors`). |
| 7 | - Client: Vite + React + TypeScript. No UI framework, plain CSS in one stylesheet. |
| 8 | - LLM: `@anthropic-ai/sdk`. STT: OpenAI REST API via `fetch` inside the provider only (no OpenAI SDK dependency). |
| 9 | - Validation/schemas: `zod` (shared between API validation and LLM structured output). |
| 10 | - Tests: `vitest`. Typecheck: `tsc --noEmit`. |
| 11 | - Local demo: `npm run demo` passes an explicit demo flag to the server. The flag is applied after `.env` is read, so it always forces deterministic mock providers, and it locks that process on mocks even if keys are saved from the setup screen while it runs. |
| 12 | - Provider setup: keys can be entered in the app. Providers are resolved per request instead of once at boot, so a saved key takes effect without a restart, and a server with no key still starts and serves the setup screen. |
| 13 | |
| 14 | No other runtime dependencies without a BLOCKED.md entry. |
| 15 | |
| 16 | ## Layout |
| 17 | |
| 18 | ``` |
| 19 | shared/ types + zod schemas (Session, Segment, Coverage, API payloads) |
| 20 | transcript.ts: formatTranscript(segments), renders plain text shared by |
| 21 | the transcript download route and the client's copy-to-clipboard button |
| 22 | server/ |
| 23 | index.ts Fastify bootstrap, serves client build in prod |
| 24 | routes/ sessions.ts, audio.ts, generate.ts, blockers.ts, fs.ts, export.ts, config.ts |
| 25 | config/ |
| 26 | runtimeConfig.ts provider mode status, .env merge/write, live switch of process env |
| 27 | store/ sessionStore.ts (JSON files under data/sessions/<id>/session.json) |
| 28 | providers/ |
| 29 | types.ts SttProvider, InterviewLlm interfaces |
| 30 | factory.ts env-based selection, MOCK_PROVIDERS=1 forces mocks, lazy per-request wrappers |
| 31 | sttOpenai.ts sttMock.ts |
| 32 | llmAnthropic.ts llmMock.ts |
| 33 | engine/ |
| 34 | coverage.ts categories, state transitions |
| 35 | interview.ts one turn: transcript + coverage in, InterviewTurn out |
| 36 | summary.ts running summary of segments older than the last 40 |
| 37 | generator/ |
| 38 | prompts.ts per-file generation prompts |
| 39 | generate.ts orchestrates the 5 files + sources.json, backup logic |
| 40 | provenance.ts marker validation (FR-013) |
| 41 | blockers/ |
| 42 | parse.ts BLOCKED.md parser |
| 43 | exporter/ |
| 44 | zip.ts dependency-free ZIP (store method) writer used by the export route |
| 45 | client/ |
| 46 | src/App.tsx session list/create, interview view |
| 47 | src/api.ts typed fetch wrappers over shared types |
| 48 | src/audio.ts MediaRecorder push-to-talk |
| 49 | src/share.ts deterministic handoff and recommendation copy |
| 50 | src/tts.ts speechSynthesis wrapper with browser-locale voice selection |
| 51 | src/labels.ts plain-language copy for coverage category ids |
| 52 | src/components/ Transcript, CoveragePanel, QuestionCard, GeneratePanel, FolderBrowser, |
| 53 | TranscriptExport, OutcomePreview, ProviderSetup |
| 54 | ``` |
| 55 | |
| 56 | ## Product experience direction |
| 57 | |
| 58 | - Audience: a non-technical founder, product person, or stakeholder who has an idea but not a written build brief. |
| 59 | - Home screen job: explain the outcome, remove setup anxiety, and start one interview. |
| 60 | - Visual system: cool cloud canvas, white surfaces, dark navy type, signal indigo actions, coral voice accents, and green completion states. Display type is Sora, body type is DM Sans, and transcript source markers use a system monospace face. |
| 61 | - Layout: a clear two-column first screen pairs the product promise with the create form. A voice-to-requirement preview is the signature element and shows a spoken thought becoming a source-backed requirement. |
| 62 | - Interaction: familiar cards, labeled actions, visible focus, restrained motion, and reduced-motion support. Progress and recording state never rely on color alone. |
| 63 | - Accessibility: question updates use a polite live region. Custom controls expose focus, and modal folder browsing contains and restores keyboard focus. |
| 64 | |
| 65 | ## Coverage categories (fixed, order matters for tie-breaking) |
| 66 | |
| 67 | 1. `goal` (what and why), 2. `users`, 3. `core-flow`, 4. `data`, 5. `integrations`, |
| 68 | 6. `edge-cases`, 7. `constraints` (stack, platform, performance), 8. `non-goals`, 9. `verification` (what "done" looks like). |
| 69 | |
| 70 | ## Key interfaces (shared/) |
| 71 | |
| 72 | ```ts |
| 73 | interface Segment { id: string; ts: string; speaker: "user" | "interviewer"; text: string } |
| 74 | type CoverageLevel = "missing" | "partial" | "clear" |
| 75 | type Coverage = Record<CategoryId, CoverageLevel> |
| 76 | interface Session { |
| 77 | id: string; name: string; targetDir: string; createdAt: string; |
| 78 | segments: Segment[]; coverage: Coverage; summary: string; |
| 79 | status: "interviewing" | "done"; openBlockers: string[] |
| 80 | } |
| 81 | interface InterviewTurn { |
| 82 | coverage: Coverage; nextQuestion: string; |
| 83 | contradiction: { segmentIds: string[]; description: string } | null; |
| 84 | done: boolean; summaryUpdate: string | null |
| 85 | } |
| 86 | ``` |
| 87 | |
| 88 | `InterviewTurn` is also the zod schema used as the LLM structured output format. |
| 89 | |
| 90 | ## Anthropic API usage (llmAnthropic.ts) |
| 91 | |
| 92 | - Model from `ANTHROPIC_MODEL`, default `claude-opus-4-8`. Key from `ANTHROPIC_API_KEY`. |
| 93 | - Interview turn: `client.messages.parse()` with `output_config: { format: zodOutputFormat(InterviewTurnSchema) }`, `thinking: { type: "adaptive" }`, `max_tokens: 4096`. Input: system prompt (interviewer persona + category definitions + rules from SPEC FR-005..FR-008), then summary + last 40 segments + current coverage as the user message. Do not set `temperature` (removed on this model family). |
| 94 | - Spec pack generation: one call per output file (5 calls), plain text output, `client.messages.stream()` with `finalMessage()`, `max_tokens: 32000`. Each prompt receives the full transcript with segment ids and instructs: English output, provenance markers `[S<n>]` on every FR (SPEC.md call only), verification commands per task (TASKS.md call only). |
| 95 | - Schema-mismatch handling per SPEC edge cases: one retry, then error. Use the SDK's typed error classes. |
| 96 | |
| 97 | ## STT provider (sttOpenai.ts) |
| 98 | |
| 99 | - `POST https://api.openai.com/v1/audio/transcriptions`, model from `STT_MODEL` (default `gpt-4o-mini-transcribe`), key from `OPENAI_API_KEY`, multipart upload of the webm blob. Returns plain text. |
| 100 | - Reject transcripts that are empty after trimming (maps to the empty-audio edge case). |
| 101 | |
| 102 | ## Mocks (sttMock.ts, llmMock.ts) |
| 103 | |
| 104 | - `sttMock`: returns `"mock transcript <n>"` with an incrementing counter, or, if the uploaded "audio" buffer is valid UTF-8 text, echoes it back (lets tests inject specific answers through the audio path). |
| 105 | - `llmMock` interview: deterministic script keyed on turn count; marks one category `clear` per turn in the fixed category order, asks a canned question about the next category, returns `done: true` when all are `clear` or when the last user segment is exactly "done". |
| 106 | - `llmMock` generation: emits minimal valid files; SPEC.md contains two FRs, the first with a valid `[S1]` marker and the second with a bogus `[S999]` marker so the provenance validator (FR-013) is exercised end to end. |
| 107 | |
| 108 | ## API routes |
| 109 | |
| 110 | ``` |
| 111 | POST /api/sessions {name, targetDir} -> Session |
| 112 | GET /api/sessions -> Session[] (id, name, status only) |
| 113 | GET /api/sessions/:id -> Session |
| 114 | POST /api/sessions/:id/audio multipart audio -> {segment, turn: InterviewTurn} |
| 115 | POST /api/sessions/:id/answer {text} -> {segment, turn: InterviewTurn} |
| 116 | POST /api/sessions/:id/generate {overwrite?: boolean} -> {files: string[], warnings: string[]} |
| 117 | POST /api/sessions/:id/blockers {} -> {questions: string[]} (reads <targetDir>/BLOCKED.md) |
| 118 | GET /api/fs/browse ?path=<abs path, default home dir> -> {path, parent, directories: [{name, path}]} |
| 119 | POST /api/fs/mkdir {path, name} -> {path} (creates one subdirectory) |
| 120 | GET /api/sessions/:id/export/spec-pack.zip -> application/zip download of the generated spec pack |
| 121 | HEAD /api/sessions/:id/export/spec-pack.zip -> 200 when a generated pack exists, otherwise the same 404 as GET |
| 122 | GET /api/sessions/:id/export/transcript.md -> text/markdown download of formatTranscript(session.segments) |
| 123 | ``` |
| 124 | |
| 125 | All request/response bodies validated with the shared zod schemas. Errors: `{error: string}` with appropriate status codes. |
| 126 | |
| 127 | ## Decisions already made (do not revisit) |
| 128 | |
| 129 | - Fastify over Express: built-in schema validation hooks and multipart support. |
| 130 | - JSON file storage over SQLite: single user, small data, trivially inspectable. |
| 131 | - One LLM call per generated file instead of one giant call: keeps each output small enough to be reliable and lets provenance validation run per file. |
| 132 | - Browser TTS over API TTS: zero cost and zero latency-sensitive infra for v1. |
| 133 | - The generated spec pack intentionally mirrors the structure of this very spec pack (SPEC/PLAN/TASKS/VERIFICATION/HANDOFF); templates in `generator/prompts.ts` should be derived from these files. |
| 134 | - Zip export is hand-rolled (`exporter/zip.ts`, STORE method only, no compression) instead of adding a zip dependency: the format is small and well-specified, and it keeps the "no dependency without a BLOCKED.md entry" rule intact for a feature this contained. |
| 135 | - `/api/fs/browse` and `/api/fs/mkdir` expose the local filesystem over HTTP with no auth check beyond what the rest of the app already assumes (single local user, no auth by design). This is acceptable only because the app is local-only per the Out of scope section; it must never ship if that decision changes. |
| 136 | - Coverage category labels shown in the UI are looked up from `client/src/labels.ts`, kept separate from the `CategoryId` values in `shared/types.ts` so the wire format/category ids never change, only the display text. |
| 137 | - Product sharing remains user-initiated. The handoff message contains only project-specific guidance. The separate recommendation action contains the public VoiceTask link and never modifies generated files. |
| 138 | - Pack readiness is derived from the existing zip export route with a HEAD request, rather than adding duplicate persisted state to the session. |
| 139 | |