feat(learner): search results show lesson matches cleanly; contract notes for the browser entry and LessonView; plan marked done

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 04:54:53 +01:00
co-authored by Claude Opus 5.5
parent 89c51f013d
commit 061ae7a2d9
3 changed files with 46 additions and 24 deletions
+30 -19
View File
@@ -43,16 +43,16 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- Tables: `auth_user`, `auth_session`, `auth_account`, `auth_verification`, `auth_sso_provider` (Better Auth, snake_case columns, ISO text dates, integer booleans), `learner_meta (key PK, value)`, `learner_group_claims (user_id, group_id)`.
- test helper `forEachDb(name, fn)` = `@stc/db`'s helper plus the learner migrations.
- [ ] Step 1: failing test: after `runMigrations` the tables exist on both dialects and a second run applies nothing.
- [ ] Step 2: implement; Step 3: green on SQLite and Postgres; commit.
- [x] Step 1: failing test: after `runMigrations` the tables exist on both dialects and a second run applies nothing.
- [x] Step 2: implement; Step 3: green on SQLite and Postgres; commit.
### Task 2: Better Auth adapter over Bun.SQL
**Files:** `src/server/auth/adapter.ts`, `test/adapter.test.ts`
**Interfaces:** `bunSqlAdapter(db: Db)` → Better Auth `DBAdapterInstance`. Model `x` → table `auth_<snake(x)>`, field `camelCase` → `snake_case`. `supportsDates/Booleans/JSON: false`, no transactions (SQLite has one connection; a nested query in a transaction would deadlock).
- [ ] Failing tests (both dialects): create/findOne/findMany (eq, ne, in, not_in, contains, starts_with, ends_with, lt/gt, insensitive mode, OR grouping, sort, limit, offset), update, updateMany, delete, deleteMany, count, consumeOne, incrementOne.
- [ ] Implement, green, commit.
- [x] Failing tests (both dialects): create/findOne/findMany (eq, ne, in, not_in, contains, starts_with, ends_with, lt/gt, insensitive mode, OR grouping, sort, limit, offset), update, updateMany, delete, deleteMany, count, consumeOne, incrementOne.
- [x] Implement, green, commit.
### Task 3: AuthService, owner bootstrap, claim groups, feature wiring
@@ -64,15 +64,15 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- First user to sign up becomes `owner` (claimed via `INSERT` into `learner_meta` key `owner`, PK conflict loses); others get `learner`.
- `syncClaimGroups(db, userId, names)` creates groups by name and replaces the user's claim-sourced memberships; manual memberships are untouched.
- `learnerFeature(ctx)` returns `Feature & { auth }`; `startServer` accepts feature factories `(ctx) => Feature` and installs a feature's `auth` (contract request filed; two-line change in `main.ts`).
- [ ] Failing tests: sign-up → session cookie → `userFromRequest` returns roles `[owner]`; second user `[learner]`; concurrent first sign-ups give one owner; sign-out clears; claim sync keeps manual memberships.
- [ ] Implement, green, commit.
- [x] Failing tests: sign-up → session cookie → `userFromRequest` returns roles `[owner]`; second user `[learner]`; concurrent first sign-ups give one owner; sign-out clears; claim sync keeps manual memberships.
- [x] Implement, green, commit.
### Task 4: Admin API (roles, groups, course ACL, library curation)
**Files:** `src/server/admin.ts`, `test/admin.test.ts`
- Routes (owner only, 403 otherwise): `GET /api/admin/users`, `PUT /api/admin/users/:id/roles`, `GET|POST /api/admin/groups`, `DELETE /api/admin/groups/:id`, `PUT /api/admin/groups/:id/members`, `GET|PUT /api/admin/courses/:id/acl`, `GET|PUT /api/admin/library` (featured + collections).
- The last owner cannot drop their own owner role.
- [ ] Tests first; implement; commit.
- [x] Tests first; implement; commit.
### Task 5: Library, outline and lesson API
@@ -82,13 +82,13 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- `GET /api/courses/:id` → outline, tracks, gating, `locked` per module, progress, diagnostics (errors only for owner/authors).
- `GET /api/courses/:id/lessons/:lessonId` → `LessonPayload` (markdown, hints, decks, video, transcript, code, attachments, browser exercise, local command, prev/next) or `{ locked: true }`.
- Gating: module *i* locked when any lesson of an earlier module is not done; owners and course authors bypass.
- [ ] Tests against the sample fixture plus temp courses; implement; commit.
- [x] Tests against the sample fixture plus temp courses; implement; commit.
### Task 6: Progress, notes, exercise attempts API
**Files:** `src/server/userdata.ts`, `test/userdata.test.ts`
- `PUT /api/courses/:id/progress/:lessonId` `{state, positionSeconds?}`; notes CRUD (`GET/POST /api/courses/:id/notes`, `PATCH/DELETE /api/notes/:id`); attempts `GET/PUT /api/courses/:id/lessons/:lessonId/attempt?track=`. A passing attempt marks the lesson done. Anonymous → 401. Hidden course → 404.
- [ ] Tests on both dialects (acceptance: notes persist on SQLite and Postgres); implement; commit.
- [x] Tests on both dialects (acceptance: notes persist on SQLite and Postgres); implement; commit.
### Task 7: Code files and zip download
@@ -96,41 +96,52 @@ Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain
- `GET /api/courses/:id/code/:moduleId` → `{ dir, command, files: {path,size}[] }` (skips `.git`, `node_modules`, symlinks escaping root; caps 2000 files).
- `GET /api/courses/:id/code/:moduleId/zip` → `application/zip`, `Content-Disposition` via `contentDisposition("<dir name>.zip")`.
- `zip(entries)`: store/deflate, CRC32, UTF-8 name flag.
- [ ] Tests: zip round-trip via `unzip -l`, non-ASCII names, symlink escape skipped; commit.
- [x] Tests: zip round-trip via `unzip -l`, non-ASCII names, symlink escape skipped; commit.
### Task 8: Browser exercise runner
**Files:** `src/web/runner/{assert.ts,worker-source.ts,runner.ts}`, `src/server/transpile.ts`, `test/runner.test.ts`
- `createBrowserRunner(language, { transpile })`: `ExerciseRunner`. Worker disables `fetch`, `XMLHttpRequest`, `WebSocket`, `importScripts` before importing code. Results carry expected/actual for failed asserts.
- [ ] Tests (Bun supports module workers and blob URLs): starter fails, solution passes, a syntax error is reported, `while(true)` times out, TS is transpiled.
- [ ] commit.
- [x] Tests (Bun supports module workers and blob URLs): starter fails, solution passes, a syntax error is reported, `while(true)` times out, TS is transpiled.
- [x] commit.
### Task 9: Markdown pipeline
**Files:** `src/web/markdown.ts`, `test/markdown.test.ts`
- `renderLessonHtml(md, { courseId, baseFile, tracks, lessonFiles })` → sanitized HTML: heading ids, code blocks tagged with their track (`data-t`), bold track labels tagged, `<details>` turned into folds; consecutive hint folds become a tier group (`data-tier`, `data-tiers`), relative images/links rewritten to `/api/media/...` or lesson routes.
- [ ] Tests with Pure Snake-style input; commit.
- [x] Tests with Pure Snake-style input; commit.
### Task 10: Web shell and library
**Files:** `src/web/{routes.tsx,api.ts,styles.css,shell/*,library/*}`
- Header extras (course switcher, search, Reading/Work, account) portalled into the Phase 0 header; course outline sidebar (mobile overlay, Escape closes); library: featured hero, continue row, New row, collection rows of covers, format/length/in-progress filters, search.
- [ ] Component smoke tests with `react-dom/server` render; screenshot check later; commit.
- [x] Component smoke tests with `react-dom/server` render; screenshot check later; commit.
### Task 11: Lesson views and `LessonView`
**Files:** `src/web/lesson/*`
- Text (track switcher, Reading/Work, folds, hint tiers), video (range-served `<video>`, resume, done near end, timestamp notes), deck viewer (pdfjs, thumbs, arrows, fullscreen, per-slide note), code files (tree, tabs, copy, zip), exercise page (editor, run, reset, per-test results, hints, local command; local-only: command + "Mark done" + optional pasted evidence), notes list, floating action bar.
- [ ] Render tests for `LessonView` with each lesson type; commit.
- [x] Render tests for `LessonView` with each lesson type; commit.
### Task 12: Account and admin pages
**Files:** `src/web/account/*`
- Sign in / create account (email+password), configured provider buttons, SSO by email, sign out; owner admin page for users/roles, groups, featured course and collections.
- [ ] Render tests; commit.
- [x] Render tests; commit.
### Task 13: Wiring, verification, report
- [ ] Append `learnerWeb` to `apps/web/src/features.ts`; `learnerFeature` in `main.ts`.
- [ ] `bun test`, `bun run typecheck`, Postgres run; dev server on 3461 with `STC_COURSE_DIRS=./fixtures/courses:./overlays`; headless Chrome screenshots of library, text, video, deck, code, exercise, account for all three courses; fix what looks wrong.
- [ ] Write `/home/user/STCourses/.superpowers/sdd/2026-10-30-stcourses/wp-learner-report.md`.
- [x] Append `learnerWeb` to `apps/web/src/features.ts`; `learnerFeature` in `main.ts`.
- [x] `bun test`, `bun run typecheck`, Postgres run; dev server on 3461 with `STC_COURSE_DIRS=./fixtures/courses:./overlays`; headless Chrome screenshots of library, text, video, deck, code, exercise, account for all three courses; fix what looks wrong.
- [x] Write `/home/user/STCourses/.superpowers/sdd/2026-10-30-stcourses/wp-learner-report.md`.
---
## Execution notes (added when the plan was finished)
- Tasks 1–9 were done by the first implementer (0b11715..99c0e49). Tasks 10–13 were done after a hand-over (89c51f0 onwards).
- Tasks 10–12 landed in one commit, because the route modules import each other (the routes list loads the lesson, library and account pages).
- Styles are a TS string (`src/web/styles.ts`) injected once, not a `.css` text import: Bun's dev server loses text imports on hot rebuilds.
- The browser entry is `@stc/feature-learner/web` (see `docs/contract-requests/learner.md` §3); the index cannot be bundled for the browser.
- Decks render client-side with pdf.js; the importer's `<dataDir>/cache/decks` is not used.
- The exercise editor is a textarea with a line gutter, as in the mockup (CodeMirror was optional).