profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
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