docs: WP-A learner package plan
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3ca05fcfbc
commit
0b117159cd
@@ -0,0 +1,136 @@
|
||||
# WP-A Learner App Implementation Plan
|
||||
|
||||
> **For agentic workers:** executed inline by the WP-A agent (no subagents), task by task, TDD, one commit per task. Steps use checkbox (`- [ ]`) syntax.
|
||||
|
||||
**Goal:** Build `packages/feature-learner`: sign-in (Better Auth), roles/groups admin API, the library, the lesson reader for every lesson type, notes, progress, gating, and the in-browser JS/TS exercise runner. It must render `sample`, `pure-snake` and `systemic-design`.
|
||||
|
||||
**Architecture:**
|
||||
- Server side (`src/server/*`): `learnerFeature(ctx): Feature & { auth: AuthService }` returns the JSON API routes and the `AuthService`. It runs its own migrations (`migrations/0001_learner_auth.sql`) lazily on first use until Phase 2 wires `migrationsDirs` into `migrate()`.
|
||||
- Auth: Better Auth with a **thin custom adapter over `Bun.SQL`** (`src/server/auth/adapter.ts`). One adapter serves both dialects because `@stc/db` already gives a `Bun.SQL` handle for SQLite and Postgres.
|
||||
- Web side (`src/web/*`): React routes `/`, `/c/:courseId`, `/c/:courseId/:lessonId`, `/c/:courseId/notes`, `/account`, `/account/admin`. Styles are plain CSS loaded as text and injected once, so the same module graph is safe to import on the server.
|
||||
- `LessonView` is a presentational lesson renderer that takes a `LessonPayload`. The studio uses it for preview.
|
||||
- The browser runner (`src/web/runner/*`) runs learner code and tests as ES modules from blob URLs inside a module Web Worker, with a hard timeout (the worker is terminated). TS is transpiled by the server's `Bun.Transpiler` (`POST /api/learner/transpile`).
|
||||
- Decks render client-side with pdfjs-dist. The worker script is served from `/api/learner/pdf.worker.mjs`.
|
||||
|
||||
**Tech Stack:** Bun 1.3, TypeScript, React 19, React Router 6, better-auth 1.7 (+ `@better-auth/sso` for SAML/OIDC SSO, `generic-oauth` for a configured OIDC provider), marked + DOMPurify, pdfjs-dist.
|
||||
|
||||
**Spec:** sections 4.1–4.4, 6, 7, 12, 13. Visual reference: `docs/design/mockups/platform-v1.html` minus its `[` / `]` / `/` / Ctrl+Enter shortcuts.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Inherited from `docs/superpowers/plans/2026-10-30-stcourses.md` (Bun only, plain CSS, no course content in the DB, both dialects, AGPL, tokens, keyboard rules, commit trailer). Package-specific:
|
||||
- Owned paths: `packages/feature-learner/**`, `docs/superpowers/plans/2026-10-30-learner.md`, `docs/contract-requests/learner.md`. Shared touches: one line in `apps/web/src/features.ts`, the feature wiring in `apps/server/src/main.ts`.
|
||||
- Frozen: `packages/contracts`, `packages/course-format`, `packages/db` (src + 0001), `packages/core`, `packages/ui`.
|
||||
- No new colours: course accent (manifest) and the tokens only. Literata only for lesson titles and reading text.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Auth adapter correctness on both dialects** (where operators, AND/OR grouping, dates and booleans as text/int). Test: Task 2 runs the adapter suite under `forEachDb`.
|
||||
2. **Owner bootstrap is race-safe**: exactly one owner even with concurrent sign-ups (unique key claim). Test: Task 3.
|
||||
3. **Visibility**: lesson, outline, code and zip routes all apply `canView` and return 404 for hidden courses; gating returns `locked` without content. Test: Task 5/7.
|
||||
4. **Zip download** handles spaces and non-ASCII names (UTF-8 flag) and never follows symlinks out of the root. Test: Task 7.
|
||||
5. **Runner isolation**: learner code runs only inside the worker; an infinite loop is killed by the timeout. Test: Task 8.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Package scaffold, migrations runner, test helper
|
||||
|
||||
**Files:** `packages/feature-learner/{package.json,src/index.ts,src/server/migrate.ts,migrations/0001_learner_auth.sql,test/helpers.ts,test/migrate.test.ts}`
|
||||
|
||||
**Interfaces (Produces):**
|
||||
- `migrationsDirs: string[]` (absolute path of `packages/feature-learner/migrations`).
|
||||
- `runMigrations(db: Db, dirs?: string[]): Promise<string[]>` — same bookkeeping table and semantics as `@stc/db` `migrate()`.
|
||||
- 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### Task 3: AuthService, owner bootstrap, claim groups, feature wiring
|
||||
|
||||
**Files:** `src/server/auth/index.ts`, `src/server/auth/groups.ts`, `src/server/feature.ts`, `test/auth.test.ts`, `apps/server/src/main.ts` (wiring), `docs/contract-requests/learner.md`
|
||||
|
||||
**Interfaces:**
|
||||
- `createAuth(ctx, opts?)` → `{ handler(req), service: AuthService, providers(): ProviderInfo[] }`.
|
||||
- Email/password always on; social providers (GitHub, Google, Microsoft, GitLab, Discord) from `STC_AUTH_<NAME>_ID/SECRET`; one generic OIDC provider from `STC_AUTH_OIDC_*`; SAML and per-domain OIDC through `@better-auth/sso` (owner-only registration).
|
||||
- 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### Task 5: Library, outline and lesson API
|
||||
|
||||
**Files:** `src/server/catalog.ts`, `src/server/lesson.ts`, `src/server/gating.ts`, `test/catalog.test.ts`, `test/lesson.test.ts`
|
||||
- `GET /api/library` → `{ featured, continue[], newest[], collections[], courses[] }` filtered by `canView`. Each course: id, title, pitch, authors, coverUrl, accent, formats (`text|video|slides|exercises`), lessonCount, minutes (video duration, text estimated at 220 wpm, exercises 15 min), collections, progress, status.
|
||||
- `GET /api/library/search?q=` → courses and lessons matching title/pitch/lesson titles.
|
||||
- `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.
|
||||
|
||||
### 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.
|
||||
|
||||
### Task 7: Code files and zip download
|
||||
|
||||
**Files:** `src/server/zip.ts`, `src/server/code.ts`, `test/zip.test.ts`, `test/code.test.ts`
|
||||
- `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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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`.
|
||||
Reference in New Issue
Block a user