docs(ai): mark plan tasks done

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 04:40:02 +01:00
co-authored by Claude Opus 5.5
parent 1f111b80db
commit 0ff8e6a5de
+22 -22
View File
@@ -44,8 +44,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- `createSecretBox(secret: string): { encrypt(plain: string): Promise<string>; decrypt(token: string): Promise<string> }`. HKDF-SHA-256 (salt `"stcourses/byok"`, info `"aes-gcm-256/v1"`) → AES-GCM-256; token `v1.<b64 iv>.<b64 ciphertext>`.
- `migrationsDirs: string[]` (empty: the package adds no tables).
- [ ] Tests: round trip; two encryptions differ (random IV); a box with another secret rejects; a tampered token rejects.
- [ ] Implement; commit `feat(ai): package scaffold and AES-GCM secret box`.
- [x] Tests: round trip; two encryptions differ (random IV); a box with another secret rejects; a tampered token rejects.
- [x] Implement; commit `feat(ai): package scaffold and AES-GCM secret box`.
### Task 2: Settings repository
@@ -59,8 +59,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- `getUser(userId): Promise<{ byok: (ProviderConfig & { hasApiKey }) | null; mentorOff: Record<courseId, boolean> }>`, `saveUserKey(userId, cfg & { apiKey })`, `removeUserKey(userId)`, `userApiKey(userId, kind)`, `setMentorOff(userId, courseId, off)`, `isMentorOff(userId, courseId)`.
- Storage: `ai_settings` row `('server','')` holds `{ provider, apiKeyCipher, mcpServers }`; row `('user', userId)` holds `{ byok, mentorOff }`. The per-course flag lives in `config_json` because the frozen primary key is `(scope, owner_id)`; the `mentor_off` column mirrors "off for every course" (always 0 for now). Contract request filed. Keys go in `user_api_keys` encrypted. Upserts are select-then-insert-or-update in a transaction.
- [ ] Tests (both DBs): empty server config is `null` provider; saving a key stores ciphertext only (raw row does not contain the key) and `serverApiKey()` returns it; omitting `apiKey` on save keeps the old key; BYOK save/remove; mentor toggle per course is independent.
- [ ] Implement; commit `feat(ai): settings repository for server config, BYOK keys and mentor toggles`.
- [x] Tests (both DBs): empty server config is `null` provider; saving a key stores ciphertext only (raw row does not contain the key) and `serverApiKey()` returns it; omitting `apiKey` on save keeps the old key; BYOK save/remove; mentor toggle per course is independent.
- [x] Implement; commit `feat(ai): settings repository for server config, BYOK keys and mentor toggles`.
### Task 3: SSE parsing and the Anthropic provider
@@ -74,8 +74,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- The provider remembers each response's full content blocks (thinking with signature, text, tool_use) by tool_use id and replays them unchanged for the matching assistant tool-call messages.
- Streams `{ text }` per `text_delta`, `{ toolCall }` per finished `tool_use` block; HTTP errors and `error` events throw with the API message; `stop_reason: "refusal"` yields a plain sentence.
- [ ] Tests with a fake `fetch` that returns canned SSE: text streams in order; a tool_use with split `input_json_delta` yields one parsed toolCall; request body has model, system, tools as `input_schema`, no `thinking`/`temperature`; a second call after a tool round replays the thinking block with its signature unchanged before `tool_result`; non-2xx throws with the error message.
- [ ] Implement; commit `feat(ai): Anthropic provider over fetch with streaming tool use`.
- [x] Tests with a fake `fetch` that returns canned SSE: text streams in order; a tool_use with split `input_json_delta` yields one parsed toolCall; request body has model, system, tools as `input_schema`, no `thinking`/`temperature`; a second call after a tool round replays the thinking block with its signature unchanged before `tool_result`; non-2xx throws with the error message.
- [x] Implement; commit `feat(ai): Anthropic provider over fetch with streaming tool use`.
### Task 4: OpenAI-compatible provider
@@ -85,8 +85,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- `createOpenAiProvider({ baseUrl, apiKey?, model, fetch? }): AiProvider`. POST `<baseUrl>/chat/completions`, `stream: true`, tools as `{ type: "function", function: { name, description, parameters } }`. Assistant tool-call messages map to `tool_calls`; `tool` messages to `{ role: "tool", tool_call_id }`. Accumulates `delta.tool_calls` by index and emits on finish or `[DONE]`.
- `createProvider(cfg: ProviderConfig, apiKey: string | null, fetchImpl?): AiProvider`.
- [ ] Tests: text deltas; fragmented tool-call arguments; no `Authorization` header without a key (Ollama); request mapping of a tool round.
- [ ] Implement; commit `feat(ai): OpenAI-compatible provider (covers Ollama)`.
- [x] Tests: text deltas; fragmented tool-call arguments; no `Authorization` header without a key (Ollama); request mapping of a tool round.
- [x] Implement; commit `feat(ai): OpenAI-compatible provider (covers Ollama)`.
### Task 5: Retrieval
@@ -100,8 +100,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- Sources: every `.md` under the course root (depth-limited, skipping `.git`, `node_modules`), manifest code dirs and common code extensions (size cap 200 KB per file, 2 000 files per course), `.md/.txt/.vtt/.srt/.json` text under `<dataDir>/cache/<courseId>/`. Markdown splits at headings; long chunks split at ~1 500 chars. Paths are mapped to lessons through `file`, `transcript` and `hints.file` (anchor heading when it matches).
- Never indexed: `browser.solution` files and any path segment `solutions`, `solution`, `reference` under `expansion/`, or `reference/` in Systemic-Design style modules (see `isSolutionPath`).
- [ ] Tests on the `sample` fixture: "sum of its arguments" hits `lessons/04-exercise.md` with lessonId `m2-text` or `m2-ex-add`; `exercises/add/solution.js` never appears; a deck text file in a temp `cache/sample/decks/d1/page-1.txt` is found with kind `deck`; note chunks passed as `extra` rank.
- [ ] Implement; commit `feat(ai): BM25 course retrieval over lessons, transcripts, code, deck text and notes`.
- [x] Tests on the `sample` fixture: "sum of its arguments" hits `lessons/04-exercise.md` with lessonId `m2-text` or `m2-ex-add`; `exercises/add/solution.js` never appears; a deck text file in a temp `cache/sample/decks/d1/page-1.txt` is found with kind `deck`; note chunks passed as `extra` rank.
- [x] Implement; commit `feat(ai): BM25 course retrieval over lessons, transcripts, code, deck text and notes`.
### Task 6: MCP client
@@ -112,8 +112,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- JSON-RPC 2.0: `initialize` (protocolVersion `2025-06-18`), `notifications/initialized`, `tools/list`, `tools/call`. stdio = newline-delimited JSON over `Bun.spawn`; streamable HTTP = POST with `Accept: application/json, text/event-stream`, JSON or SSE replies, `Mcp-Session-Id` echoed.
- `mcpServersFor(courseId, servers)` selects global servers plus ones listing the course.
- [ ] Tests: the fake server over stdio lists `echo` and `tools/call` returns its text; the same over HTTP (fake via `Bun.serve` on port 0, replying with SSE for `tools/call`) with the session id echoed; a server that exits gives a rejected promise, not a hang.
- [ ] Implement; commit `feat(ai): MCP client over stdio and streamable HTTP`.
- [x] Tests: the fake server over stdio lists `echo` and `tools/call` returns its text; the same over HTTP (fake via `Bun.serve` on port 0, replying with SSE for `tools/call`) with the session id echoed; a server that exits gives a rejected promise, not a hang.
- [x] Implement; commit `feat(ai): MCP client over stdio and streamable HTTP`.
### Task 7: Assistant core
@@ -125,8 +125,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- `runAssistant({ provider, messages, tools, signal, maxSteps = 6 }): AsyncIterable<AssistantEvent>` where `AssistantEvent = { type: "text", text } | { type: "tool", name } | { type: "error", message } | { type: "done", text }`. Tool calls become assistant messages with `toolCallId`/`name` and `tool` result messages; unknown tools and tool errors are fed back as error text.
- Citations: `citationsFor(chunks): { path, lessonId?, lessonTitle?, kind }[]` deduplicated.
- [ ] Tests: mentor on puts the sample `MENTOR.md` text in the system prompt; mentor off omits it and states full answers; a fake provider that calls `search_course` then answers gets the tool result and the loop ends with text; `read_lesson` on the exercise returns markdown; solution paths are refused.
- [ ] Implement; commit `feat(ai): assistant prompt, course tools and tool loop`.
- [x] Tests: mentor on puts the sample `MENTOR.md` text in the system prompt; mentor off omits it and states full answers; a fake provider that calls `search_course` then answers gets the tool result and the loop ends with text; `read_lesson` on the exercise returns markdown; solution paths are refused.
- [x] Implement; commit `feat(ai): assistant prompt, course tools and tool loop`.
### Task 8: Generator and draft sink
@@ -140,8 +140,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- `checkHouseStyle(req, files): string[]` warnings for missing beats/tiers.
- `generateDraft({ provider, req, course, ctx, sink, authorId }): Promise<{ location, paths, warnings }>`; every Markdown file starts with a draft marker comment.
- [ ] Tests: prompt for each style contains its beats; parse handles two files and rejects `../x`; fake provider output written by the fs sink lands under `<dataDir>/drafts/sample/…` and nothing changes in the course folder; missing tiers give warnings.
- [ ] Implement; commit `feat(ai): house-style generator writing drafts through a DraftSink`.
- [x] Tests: prompt for each style contains its beats; parse handles two files and rejects `../x`; fake provider output written by the fs sink lands under `<dataDir>/drafts/sample/…` and nothing changes in the course folder; missing tiers give warnings.
- [x] Implement; commit `feat(ai): house-style generator writing drafts through a DraftSink`.
### Task 9: Feature routes and jobs
@@ -157,8 +157,8 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- `GET /api/ai/studio/sources?courseId=`, `POST /api/ai/studio/generate` (enqueues job `ai.generate`), `GET /api/ai/studio/jobs/:id`.
- Job `ai.generate` runs `generateDraft` with the requesting user's provider.
- [ ] Tests through `createApp` with a fake `AuthService`: status disabled by default; admin config hides the key; non-owner gets 403; BYOK enables the assistant for that user only; chat with a fake provider streams text and `meta.citations` include `lessons/04-exercise.md`; system prompt holds mentor rules, and not after `PUT mentor {off:true}`; anonymous on a restricted course gets 404; generate as a learner gets 403; the job handler writes drafts under `<dataDir>/drafts/sample`.
- [ ] Implement; commit `feat(ai): HTTP routes, chat streaming and generation job`.
- [x] Tests through `createApp` with a fake `AuthService`: status disabled by default; admin config hides the key; non-owner gets 403; BYOK enables the assistant for that user only; chat with a fake provider streams text and `meta.citations` include `lessons/04-exercise.md`; system prompt holds mentor rules, and not after `PUT mentor {off:true}`; anonymous on a restricted course gets 404; generate as a learner gets 403; the job handler writes drafts under `<dataDir>/drafts/sample`.
- [x] Implement; commit `feat(ai): HTTP routes, chat streaming and generation job`.
### Task 10: Web UI
@@ -169,10 +169,10 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- Admin: provider kind, model, base URL, write-only key, MCP server rows. Account: BYOK form and remove. Generate: course, kind, style, sources checklist, topic, instructions, job progress, list of draft files with location.
- `readNdjson(stream)` client helper.
- [ ] Tests: `readNdjson` splits across chunk boundaries; `renderToString` of the banner component shows the exact sentence when off and nothing when on.
- [ ] Implement, check in headless Chrome via `packages/feature-ai/scripts/dev.ts` on port 3464; commit `feat(ai): assistant panel, AI settings and studio generator UI`.
- [x] Tests: `readNdjson` splits across chunk boundaries; `renderToString` of the banner component shows the exact sentence when off and nothing when on.
- [x] Implement, check in headless Chrome via `packages/feature-ai/scripts/dev.ts` on port 3464; commit `feat(ai): assistant panel, AI settings and studio generator UI`.
### Task 11: Contract requests and report
- [ ] `docs/contract-requests/ai.md`: feature factories in `startServer`; tool-call fields on `ChatMessage`; per-course `ai_settings` key; studio `createDraftWriter` wiring; cache layout for deck text and transcripts; learner lesson URL.
- [ ] Run `bun test`, `bun run typecheck`, the Postgres run; write `wp-ai-report.md`.
- [x] `docs/contract-requests/ai.md`: feature factories in `startServer`; tool-call fields on `ChatMessage`; per-course `ai_settings` key; studio `createDraftWriter` wiring; cache layout for deck text and transcripts; learner lesson URL.
- [x] Run `bun test`, `bun run typecheck`, the Postgres run; write `wp-ai-report.md`.