Commit
Document the full product flow
commit
938b181
13 changed files with +145 and −70
Jump to a changed file
- .env.example +3 −3
- README.md +61 −27
- client/src/App.css +11 −1
- client/src/App.tsx +3 −3
- client/src/components/FolderBrowser.tsx +30 −11
- server/providers/llmAnthropic.ts +2 −2
- server/routes/export.test.ts +1 −1
- shared/transcript.test.ts +1 −1
- shared/transcript.ts +1 −1
- spec/PLAN.md +1 −1
- spec/SPEC.md +2 −2
- spec/TASKS.md +1 −1
- spec/VERIFICATION.md +28 −16
modified .env.example +3 −3
| @@ -1,6 +1,6 @@ | ||
| 1 | -# Easiest: run "npm run setup" and it writes .env for you. | |
| 2 | -# Manual: copy this file to .env and replace the @insert-...@ placeholders with your real keys. | |
| 3 | -# With MOCK_PROVIDERS=1 no keys are required at all. | |
| 1 | +# Try the app without keys by running "npm run demo". It overrides this file for that run. | |
| 2 | +# For real providers, run "npm run setup" and it writes .env for you. | |
| 3 | +# To configure manually, copy this file to .env and replace the key placeholders. | |
| 4 | 4 | |
| 5 | 5 | # 1 = deterministic offline mocks for STT and LLM (used by tests and CI) |
| 6 | 6 | MOCK_PROVIDERS= |
modified README.md +61 −27
| @@ -1,49 +1,83 @@ | ||
| 1 | 1 | # VoiceTask |
| 2 | 2 | |
| 3 | -Talk through what you want to build. An AI interviewer asks one targeted question at a time (voice in, voice out), tracks coverage across nine spec categories, and writes a Claude-Code-ready spec pack (SPEC, PLAN, TASKS, VERIFICATION, HANDOFF) into your target project. Every requirement carries `[S<n>]` provenance markers pointing to the exact transcript segment you said it in. | |
| 3 | +VoiceTask turns a conversation about an idea into a build brief that a developer, agency, or coding assistant can use. | |
| 4 | 4 | |
| 5 | -## Quick start | |
| 5 | +You speak or type. VoiceTask asks one focused question at a time, tracks which parts of the idea are clear, and creates a zip with the requirements, plan, tasks, checks, and handoff notes. Each requirement points back to the exact part of the interview it came from. | |
| 6 | + | |
| 7 | +VoiceTask runs on your computer. It needs no account and saves each interview locally. | |
| 8 | + | |
| 9 | +## Try it without API keys | |
| 6 | 10 | |
| 7 | 11 | ``` |
| 8 | 12 | npm install |
| 9 | -npm run setup # interactive: offline demo (no keys) or real APIs, writes .env | |
| 13 | +npm run demo | |
| 14 | +``` | |
| 15 | + | |
| 16 | +Open the printed Vite URL in Chrome or Edge. | |
| 17 | + | |
| 18 | +Demo mode uses deterministic mock speech and interview providers. It does not need API keys and does not call a provider, even if an existing `.env` selects real providers. It is intended for trying the complete flow safely. | |
| 19 | + | |
| 20 | +## Use it with real speech and interview responses | |
| 21 | + | |
| 22 | +``` | |
| 23 | +npm run setup | |
| 10 | 24 | npm run dev |
| 11 | 25 | ``` |
| 12 | 26 | |
| 13 | -`npm run setup` asks whether you want the offline demo (deterministic mock providers, no API keys) or real APIs, and writes a local `.env` for you. It works the same on Windows, macOS, and Linux. | |
| 27 | +`npm run setup` asks you to choose real APIs or the offline demo. For real mode, it asks for an Anthropic key for the interview and an OpenAI key for speech-to-text, then writes a local `.env`. | |
| 28 | + | |
| 29 | +You can also copy `.env.example` to `.env` and fill in the values yourself. | |
| 14 | 30 | |
| 15 | -To configure by hand instead, copy `.env.example` to `.env` and replace the `@insert-anthropic-api-key@` and `@insert-openai-api-key@` placeholders with your real keys. The server refuses to start while a placeholder is still in place, so a half-finished `.env` fails loudly instead of failing on the first API call. | |
| 31 | +## How a session works | |
| 16 | 32 | |
| 17 | -Open the printed Vite URL in Chrome or Edge. Hold the mic button (or space) to talk, or type. Say "done" or click the done link to finish, then generate the spec pack. | |
| 33 | +1. Give the idea a short name and choose the project folder. | |
| 34 | +2. Click `Start talking`, speak, then click `Send recording`. You can type instead, or hold Space while you talk. | |
| 35 | +3. Answer one question at a time. The progress panel shows what is ready, in progress, and still to discuss. | |
| 36 | +4. Select `I'm done, wrap it up` when you have said enough. | |
| 37 | +5. Create the build brief and download the zip. | |
| 38 | +6. Copy the handoff message and send both to whoever will build the project. | |
| 18 | 39 | |
| 19 | -## APIs and models | |
| 40 | +## What the build brief contains | |
| 20 | 41 | |
| 21 | -| Purpose | Provider | Env vars | Default model | Alternatives | | |
| 22 | -|---|---|---|---|---| | |
| 23 | -| Interview + spec generation | Anthropic Messages API | `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` | `claude-opus-4-8` | `claude-sonnet-4-6` (cheaper, faster) | | |
| 24 | -| Speech-to-text | OpenAI audio transcriptions | `OPENAI_API_KEY`, `STT_MODEL` | `gpt-4o-mini-transcribe` | `gpt-4o-transcribe` (higher accuracy), `whisper-1` | | |
| 25 | -| Text-to-speech | Browser `speechSynthesis` | none | best installed voice | none (free, local) | | |
| 42 | +| File | Purpose | | |
| 43 | +|---|---| | |
| 44 | +| `SPEC.md` | Goals, users, requirements, limits, and source markers | | |
| 45 | +| `PLAN.md` | Technical approach, structure, and implementation decisions | | |
| 46 | +| `TASKS.md` | Ordered work items with verification commands | | |
| 47 | +| `VERIFICATION.md` | Checks that define when the project is finished | | |
| 48 | +| `HANDOFF.md` | The starting instructions for the person or coding assistant doing the work | | |
| 49 | +| `sources.json` | The interview text and timestamps referenced by source markers | | |
| 26 | 50 | |
| 27 | -Notes: | |
| 51 | +A source marker such as `[S7]` means the requirement came from interview segment S7. This makes it clear which decisions came from the user and which still need confirmation. | |
| 28 | 52 | |
| 29 | -- The interviewer uses structured outputs (`output_config.format` with a zod schema) with adaptive thinking, so every turn comes back as a validated `InterviewTurn`. A schema-mismatch is retried once, then surfaced as an error. | |
| 30 | -- Spec pack files are generated one Anthropic call per file, streamed, with adaptive thinking. | |
| 31 | -- `MOCK_PROVIDERS=1` swaps both providers for deterministic mocks; all tests run this way and need no network. | |
| 32 | -- Keys are read from the environment (a local `.env` is loaded if present) and never logged or written to disk. | |
| 53 | +## Local data and privacy | |
| 33 | 54 | |
| 34 | -## Environment variables | |
| 55 | +- VoiceTask has no accounts and no hosted sessions. | |
| 56 | +- Interview sessions are stored under `data/sessions/` on this computer. | |
| 57 | +- Generated files are written to the chosen project folder under `spec/`. | |
| 58 | +- The completed brief and transcript can be downloaded from the app. | |
| 59 | +- In real mode, audio is sent to OpenAI for transcription and interview text is sent to Anthropic for interview and file generation. | |
| 60 | +- API keys are read from environment variables or the local `.env`. They are not written to sessions, generated files, or logs. | |
| 35 | 61 | |
| 36 | -See `.env.example`. Summary: `MOCK_PROVIDERS`, `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`, `OPENAI_API_KEY`, `STT_MODEL`, `PORT` (default 3001). | |
| 62 | +## Providers and models | |
| 37 | 63 | |
| 38 | -## Commands | |
| 64 | +| Purpose | Provider | Environment variables | Default model | | |
| 65 | +|---|---|---|---| | |
| 66 | +| Interview and file generation | Anthropic Messages API | `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` | `claude-opus-4-8` | | |
| 67 | +| Speech-to-text | OpenAI audio transcriptions | `OPENAI_API_KEY`, `STT_MODEL` | `gpt-4o-mini-transcribe` | | |
| 68 | +| Spoken questions | Browser `speechSynthesis` | none | Installed browser voice | | |
| 69 | + | |
| 70 | +`claude-sonnet-4-6` is a lower-cost Anthropic alternative. `gpt-4o-transcribe` and `whisper-1` are supported speech-to-text alternatives. | |
| 71 | + | |
| 72 | +## Development commands | |
| 39 | 73 | |
| 40 | 74 | ``` |
| 41 | -npm run dev # server (3001) + client (Vite) with proxy | |
| 42 | -npm run typecheck # tsc --noEmit over shared, server, client | |
| 43 | -npm test # vitest, mock providers enforced | |
| 44 | -npm run build # production build to dist/ | |
| 75 | +npm run demo # start the full app with offline mock providers | |
| 76 | +npm run dev # start the app with the mode selected in .env | |
| 77 | +npm run typecheck # check TypeScript across shared, server, and client | |
| 78 | +npm test # run tests with mock providers and no network | |
| 79 | +npm run build # build the production client and server | |
| 80 | +npm run check # run typecheck, tests, and build | |
| 45 | 81 | ``` |
| 46 | 82 | |
| 47 | -## Project docs | |
| 48 | - | |
| 49 | -Product spec and implementation plan live in `spec/`. Guardrails for coding-agent sessions are in `CLAUDE.md`, blocker protocol in `BLOCKED.md`. | |
| 83 | +Product requirements and implementation decisions live in `spec/`. Coding-session rules are in `AGENTS.md` and `CLAUDE.md`. The blocker format is in `BLOCKED.md`. |
modified client/src/App.css +11 −1
| @@ -714,7 +714,8 @@ | ||
| 714 | 714 | border: none; |
| 715 | 715 | background: none; |
| 716 | 716 | color: var(--ink-soft); |
| 717 | - font-size: 19px; | |
| 717 | + font-size: 11px; | |
| 718 | + font-weight: 700; | |
| 718 | 719 | line-height: 1; |
| 719 | 720 | padding: 9px; |
| 720 | 721 | cursor: pointer; |
| @@ -1968,6 +1969,15 @@ | ||
| 1968 | 1969 | } |
| 1969 | 1970 | |
| 1970 | 1971 | @media (prefers-reduced-motion: reduce) { |
| 1972 | + *, | |
| 1973 | + *::before, | |
| 1974 | + *::after { | |
| 1975 | + scroll-behavior: auto !important; | |
| 1976 | + transition-duration: 0.01ms !important; | |
| 1977 | + animation-duration: 0.01ms !important; | |
| 1978 | + animation-iteration-count: 1 !important; | |
| 1979 | + } | |
| 1980 | + | |
| 1971 | 1981 | .orb, |
| 1972 | 1982 | .orb-eye, |
| 1973 | 1983 | .orb-ripple, |
modified client/src/App.tsx +3 −3
| @@ -242,7 +242,7 @@function SessionListScreen({ onOpen }: { onOpen: (id: string) => void }) { | ||
| 242 | 242 | aria-label={`Delete ${session.name}`} |
| 243 | 243 | onClick={() => void handleDelete(session.id)} |
| 244 | 244 | > |
| 245 | - <span aria-hidden="true">×</span> | |
| 245 | + Delete | |
| 246 | 246 | </button> |
| 247 | 247 | </li> |
| 248 | 248 | ))} |
| @@ -353,7 +353,7 @@function InterviewScreen({ sessionId, onBack }: { sessionId: string; onBack: () | ||
| 353 | 353 | <div className="screen interview-screen"> |
| 354 | 354 | <header className="topbar"> |
| 355 | 355 | <button type="button" className="back-link" onClick={onBack}> |
| 356 | - ← Sessions | |
| 356 | + All interviews | |
| 357 | 357 | </button> |
| 358 | 358 | </header> |
| 359 | 359 | {error ? <p className="error">{error}</p> : <p className="muted">Loading…</p>} |
| @@ -376,7 +376,7 @@function InterviewScreen({ sessionId, onBack }: { sessionId: string; onBack: () | ||
| 376 | 376 | <div className="screen interview-screen"> |
| 377 | 377 | <header className="topbar"> |
| 378 | 378 | <button type="button" className="back-link" onClick={onBack}> |
| 379 | - ← Sessions | |
| 379 | + All interviews | |
| 380 | 380 | </button> |
| 381 | 381 | <span className="topbar-title">{session.name}</span> |
| 382 | 382 | <div className="topbar-actions"> |
modified client/src/components/FolderBrowser.tsx +30 −11
| @@ -22,6 +22,14 @@export function FolderBrowser({ onSelect, onClose }: FolderBrowserProps) { | ||
| 22 | 22 | load(undefined) |
| 23 | 23 | }, []) |
| 24 | 24 | |
| 25 | + useEffect(() => { | |
| 26 | + function handleKeyDown(event: KeyboardEvent) { | |
| 27 | + if (event.key === 'Escape') onClose() | |
| 28 | + } | |
| 29 | + window.addEventListener('keydown', handleKeyDown) | |
| 30 | + return () => window.removeEventListener('keydown', handleKeyDown) | |
| 31 | + }, [onClose]) | |
| 32 | + | |
| 25 | 33 | async function handleCreateFolder() { |
| 26 | 34 | if (!listing || !newFolderName.trim()) return |
| 27 | 35 | setCreating(true) |
| @@ -39,24 +47,30 @@export function FolderBrowser({ onSelect, onClose }: FolderBrowserProps) { | ||
| 39 | 47 | |
| 40 | 48 | return ( |
| 41 | 49 | <div className="folder-browser-overlay" onClick={onClose}> |
| 42 | - <div className="folder-browser" onClick={(e) => e.stopPropagation()}> | |
| 43 | - <h2>Choose a folder</h2> | |
| 44 | - <p className="folder-browser-path">{listing?.path ?? 'Loading…'}</p> | |
| 50 | + <div | |
| 51 | + className="folder-browser" | |
| 52 | + role="dialog" | |
| 53 | + aria-modal="true" | |
| 54 | + aria-labelledby="folder-browser-title" | |
| 55 | + onClick={(event) => event.stopPropagation()} | |
| 56 | + > | |
| 57 | + <h2 id="folder-browser-title">Choose a folder</h2> | |
| 58 | + <p className="folder-browser-path">{listing?.path ?? 'Loading...'}</p> | |
| 45 | 59 | |
| 46 | 60 | <div className="folder-browser-list"> |
| 47 | 61 | {listing?.parent && ( |
| 48 | 62 | <button type="button" className="folder-browser-entry" onClick={() => load(listing.parent!)}> |
| 49 | - ⬆ Up one level | |
| 63 | + Up one level | |
| 50 | 64 | </button> |
| 51 | 65 | )} |
| 52 | - {listing?.directories.map((dir) => ( | |
| 66 | + {listing?.directories.map((directory) => ( | |
| 53 | 67 | <button |
| 54 | 68 | type="button" |
| 55 | - key={dir.path} | |
| 69 | + key={directory.path} | |
| 56 | 70 | className="folder-browser-entry" |
| 57 | - onClick={() => load(dir.path)} | |
| 71 | + onClick={() => load(directory.path)} | |
| 58 | 72 | > |
| 59 | - 📁 {dir.name} | |
| 73 | + {directory.name} | |
| 60 | 74 | </button> |
| 61 | 75 | ))} |
| 62 | 76 | {listing && listing.directories.length === 0 && !listing.parent && ( |
| @@ -70,20 +84,25 @@export function FolderBrowser({ onSelect, onClose }: FolderBrowserProps) { | ||
| 70 | 84 | <div className="folder-browser-new"> |
| 71 | 85 | <input |
| 72 | 86 | value={newFolderName} |
| 73 | - onChange={(e) => setNewFolderName(e.target.value)} | |
| 87 | + onChange={(event) => setNewFolderName(event.target.value)} | |
| 74 | 88 | placeholder="New folder name" |
| 89 | + aria-label="New folder name" | |
| 75 | 90 | disabled={creating} |
| 76 | 91 | /> |
| 77 | 92 | <button |
| 78 | 93 | type="button" |
| 79 | 94 | onClick={() => void handleCreateFolder()} |
| 80 | 95 | disabled={creating || !newFolderName.trim()} |
| 81 | 96 | > |
| 82 | - Create | |
| 97 | + {creating ? 'Creating...' : 'Create folder'} | |
| 83 | 98 | </button> |
| 84 | 99 | </div> |
| 85 | 100 | |
| 86 | - {error && <p className="error">{error}</p>} | |
| 101 | + {error && ( | |
| 102 | + <p className="error" role="alert"> | |
| 103 | + {error} | |
| 104 | + </p> | |
| 105 | + )} | |
| 87 | 106 | |
| 88 | 107 | <div className="folder-browser-actions"> |
| 89 | 108 | <button type="button" onClick={onClose}> |
modified server/providers/llmAnthropic.ts +2 −2
| @@ -12,13 +12,13 @@export function buildInterviewSystemPrompt(context: InterviewContext): string { | ||
| 12 | 12 | const weakest = weakestCategory(context.coverage) |
| 13 | 13 | return [ |
| 14 | 14 | 'You are a Socratic spec interviewer talking through a project idea with someone who may not be a developer or ever have written a spec before. Assume no technical background unless they use technical terms themselves.', |
| 15 | - 'Ask exactly one targeted question per turn, chosen to fill the biggest gap in the spec. Use plain, everyday language — short sentences, no jargon, no acronyms, no words like "requirements", "schema", "edge case", or "non-goal" in the question itself. Ask about the underlying idea instead (e.g. ask what should happen when something goes wrong, not "what are the edge cases").', | |
| 15 | + 'Ask exactly one targeted question per turn, chosen to fill the biggest gap in the spec. Use plain, everyday language, short sentences, no jargon, no acronyms, no words like "requirements", "schema", "edge case", or "non-goal" in the question itself. Ask about the underlying idea instead (e.g. ask what should happen when something goes wrong, not "what are the edge cases").', | |
| 16 | 16 | `Coverage categories, in tie-break priority order: ${CATEGORY_IDS.join(', ')}.`, |
| 17 | 17 | weakest |
| 18 | 18 | ? `The weakest category right now is "${weakest}". Your next question must target it, unless a contradiction takes priority.` |
| 19 | 19 | : 'Every category is already "clear".', |
| 20 | 20 | 'If the latest user answer conflicts with an earlier statement, do not ask a coverage question: set "contradiction" to describe both statements in plain language, referencing their segment ids, instead.', |
| 21 | - 'The interview ends only when the user answer is exactly "done" (case-insensitive, trimmed) — never for any other reason, even if "done" appears inside a longer answer.', | |
| 21 | + 'The interview ends only when the user answer is exactly "done" (case-insensitive, trimmed), never for any other reason, even if "done" appears inside a longer answer.', | |
| 22 | 22 | 'Reply with only the InterviewTurn structure described by the output schema.', |
| 23 | 23 | ].join('\n') |
| 24 | 24 | } |
modified server/routes/export.test.ts +1 −1
| @@ -83,7 +83,7 @@describe('export routes', () => { | ||
| 83 | 83 | expect(res.statusCode).toBe(200) |
| 84 | 84 | expect(res.headers['content-type']).toContain('text/markdown') |
| 85 | 85 | expect(res.headers['content-disposition']).toContain('family-recipes-transcript.md') |
| 86 | - expect(res.body).toContain('# Family Recipes — Interview Transcript') | |
| 86 | + expect(res.body).toContain('# Family Recipes: Interview Transcript') | |
| 87 | 87 | expect(res.body).toContain('**You:** a recipe app for my family') |
| 88 | 88 | }) |
| 89 | 89 |
modified shared/transcript.test.ts +1 −1
| @@ -13,7 +13,7 @@describe('formatTranscript', () => { | ||
| 13 | 13 | segment('S2', 'interviewer', 'who will use it?'), |
| 14 | 14 | ] |
| 15 | 15 | const text = formatTranscript(segments, 'Family Recipes') |
| 16 | - expect(text).toContain('# Family Recipes — Interview Transcript') | |
| 16 | + expect(text).toContain('# Family Recipes: Interview Transcript') | |
| 17 | 17 | expect(text).toContain('**You:** a recipe app for my family') |
| 18 | 18 | expect(text).toContain('**Interviewer:** who will use it?') |
| 19 | 19 | expect(text.indexOf('You:')).toBeLessThan(text.indexOf('Interviewer:')) |
modified shared/transcript.ts +1 −1
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import type { Segment } from './types' |
| 2 | 2 | |
| 3 | 3 | export function formatTranscript(segments: Segment[], projectName?: string): string { |
| 4 | - const lines: string[] = [projectName ? `# ${projectName} — Interview Transcript` : '# Interview Transcript', ''] | |
| 4 | + const lines: string[] = [projectName ? `# ${projectName}: Interview Transcript` : '# Interview Transcript', ''] | |
| 5 | 5 | |
| 6 | 6 | for (const segment of segments) { |
| 7 | 7 | const speakerLabel = segment.speaker === 'user' ? 'You' : 'Interviewer' |
modified spec/PLAN.md +1 −1
| @@ -16,7 +16,7 @@No other runtime dependencies without a BLOCKED.md entry. | ||
| 16 | 16 | |
| 17 | 17 | ``` |
| 18 | 18 | shared/ types + zod schemas (Session, Segment, Coverage, API payloads) |
| 19 | - transcript.ts: formatTranscript(segments) — plain-text rendering shared by | |
| 19 | + transcript.ts: formatTranscript(segments), renders plain text shared by | |
| 20 | 20 | the transcript download route and the client's copy-to-clipboard button |
| 21 | 21 | server/ |
| 22 | 22 | index.ts Fastify bootstrap, serves client build in prod |
modified spec/SPEC.md +2 −2
| @@ -10,7 +10,7 @@Differentiators over existing tools (verified 2026-07): ChatPRD interviews but h | ||
| 10 | 10 | |
| 11 | 11 | ## Users |
| 12 | 12 | |
| 13 | -Single user on their own machine. No auth, no multi-tenancy, no accounts. The primary user is not assumed to be a developer — they may be a non-technical founder, product person, or stakeholder describing an idea that someone else (a developer, an agency, a sandboxed coding agent) will build. Anything shown to the user — coverage category names, interviewer tone, empty states — is written in plain language, not developer jargon. A technical user who wants direct control (typing an exact path, editing files by hand afterward) is never blocked from doing so. | |
| 13 | +Single user on their own machine. No auth, no multi-tenancy, no accounts. The primary user is not assumed to be a developer. They may be a non-technical founder, product person, or stakeholder describing an idea that someone else (a developer, an agency, a sandboxed coding agent) will build. Anything shown to the user, including coverage category names, interviewer tone, and empty states, is written in plain language, not developer jargon. A technical user who wants direct control (typing an exact path, editing files by hand afterward) is never blocked from doing so. | |
| 14 | 14 | |
| 15 | 15 | ## User stories |
| 16 | 16 | |
| @@ -94,7 +94,7 @@Modes and safety: | ||
| 94 | 94 | |
| 95 | 95 | ## Out of scope (v1) |
| 96 | 96 | |
| 97 | -- Multi-user support, auth, or any cloud deployment. Local only. Sharing means the user downloads a file and sends it themselves (email, Slack, upload) — not a hosted link to a session or spec pack. | |
| 97 | +- Multi-user support, auth, or any cloud deployment. Local only. Sharing means the user downloads a file and sends it themselves (email, Slack, upload), not a hosted link to a session or spec pack. | |
| 98 | 98 | - Real-time streaming conversation (barge-in, VAD). Push-to-talk only. |
| 99 | 99 | - Paid TTS voices. Browser `speechSynthesis` only. |
| 100 | 100 | - Running or supervising Claude Code from inside the app. The app only writes files and prints the handoff command. |
modified spec/TASKS.md +1 −1
| @@ -97,7 +97,7 @@Work strictly in order unless a task's Depends line allows otherwise. One task a | ||
| 97 | 97 | - Depends: T18 |
| 98 | 98 | - Verify: `npm test`, `npm run typecheck`, and `npm run build` exit 0. Tests cover the readiness check plus deterministic handoff and recommendation copy. Manual: refresh a completed session with an existing pack and confirm the download remains visible. |
| 99 | 99 | |
| 100 | -- [ ] T20 Mainstream documentation and final product check | |
| 100 | +- [x] T20 Mainstream documentation and final product check | |
| 101 | 101 | - Rewrite README.md for a first-time evaluator: outcome, audience, what the pack contains, one-command offline demo, real-provider setup, privacy, and the send-to-someone flow. Update VERIFICATION.md to use cross-platform commands and run the complete product gate. |
| 102 | 102 | - Depends: T19 |
| 103 | 103 | - Verify: `npm run check` exits 0 and `git diff --check` reports no errors. |
modified spec/VERIFICATION.md +28 −16
| @@ -1,30 +1,42 @@ | ||
| 1 | 1 | # VERIFICATION: VoiceTask |
| 2 | 2 | |
| 3 | -## Commands | |
| 3 | +## Automated gate | |
| 4 | 4 | |
| 5 | -All of these must exit 0 for the project to be considered healthy: | |
| 5 | +From the repository root: | |
| 6 | 6 | |
| 7 | 7 | ``` |
| 8 | -npm run typecheck # tsc --noEmit over shared, server, client | |
| 9 | -npm test # vitest run, MOCK_PROVIDERS=1 enforced by vitest config | |
| 10 | -npm run build # vite build + server tsc build | |
| 8 | +npm run check | |
| 11 | 9 | ``` |
| 12 | 10 | |
| 13 | -Tests must not require network access or API keys. If a test needs a provider, it uses the mocks from `server/providers/`. | |
| 11 | +This must complete typechecking, all tests, and the production build with exit code 0. Tests use mock providers, require no API keys or network, and must work on Windows, macOS, and Linux without external archive tools. | |
| 14 | 12 | |
| 15 | -## Definition of done (whole project) | |
| 13 | +## Definition of done | |
| 16 | 14 | |
| 17 | -1. All checkboxes in `spec/TASKS.md` checked. | |
| 18 | -2. The three commands above exit 0 on a clean `npm install`. | |
| 19 | -3. The T12 smoke test passes: full session lifecycle through the HTTP API with mocks, generated pack valid per FR-011..FR-014. | |
| 20 | -4. `BLOCKED.md` contains either no entries or only entries genuinely requiring a product decision. | |
| 15 | +1. Every checkbox in `spec/TASKS.md` is checked. | |
| 16 | +2. `npm run check` exits 0 after a clean `npm install`. | |
| 17 | +3. The T12 smoke test covers a full session through generation. | |
| 18 | +4. Export tests prove that the zip contains all six required files and that HEAD reports readiness. | |
| 19 | +5. `BLOCKED.md` has no unanswered entry that prevents a required feature. | |
| 20 | +6. `git diff --check` reports no whitespace errors. | |
| 21 | 21 | |
| 22 | -## Manual smoke check (human, after the sandboxed run) | |
| 22 | +## Human smoke check | |
| 23 | 23 | |
| 24 | -1. `MOCK_PROVIDERS=1 npm run dev`, open the printed URL in Chrome. | |
| 25 | -2. Create a session with a temp directory as target. Type three answers, watch coverage advance, click Done, generate, confirm files appear in `<target>/spec/`. | |
| 26 | -3. With real keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY` set, `MOCK_PROVIDERS` unset): hold the record button, say a sentence, confirm it transcribes and a sensible question comes back. | |
| 24 | +1. Run `npm run demo` and open the printed URL in current Chrome or Edge. | |
| 25 | +2. Confirm the home screen explains the three-part flow, local storage, and the source-backed result before the form. | |
| 26 | +3. Create a session with a temporary target folder. | |
| 27 | +4. Click `Start talking`, grant microphone access, speak, then click `Send recording`. Confirm one answer is added. | |
| 28 | +5. Hold Space while speaking and release it. Confirm one answer is added and no second recording starts. | |
| 29 | +6. Type an answer. Confirm the topic counts and each topic status update in plain text. | |
| 30 | +7. Finish early, accept the open-topic confirmation, and create the build brief. | |
| 31 | +8. Confirm the primary zip download, handoff copy, file disclosure, and optional recommendation action appear. | |
| 32 | +9. Refresh the completed session and confirm the zip download is still available. | |
| 33 | +10. Open the zip and confirm `SPEC.md`, `PLAN.md`, `TASKS.md`, `VERIFICATION.md`, `HANDOFF.md`, and `sources.json` are present. | |
| 34 | +11. Check the home, interview, and completion screens at a narrow and wide browser width. Confirm focus is visible when using Tab and motion stops when reduced motion is enabled. | |
| 35 | + | |
| 36 | +## Optional real-provider check | |
| 37 | + | |
| 38 | +With real keys selected by `npm run setup`, run `npm run dev`. Record one sentence and confirm it is transcribed, then confirm the next question is relevant. This check can call paid provider APIs and is not part of automated verification. | |
| 27 | 39 | |
| 28 | 40 | ## Per-task verification |
| 29 | 41 | |
| 30 | -Each task in `spec/TASKS.md` carries its own Verify line; those are the gate for checking the task off. This file is the gate for finishing the session. | |
| 42 | +Each task in `spec/TASKS.md` has its own verification line. Run it before checking that task off. |