diff --git a/docs/contract-requests/learner.md b/docs/contract-requests/learner.md index cf5c9cc..d6ce696 100644 --- a/docs/contract-requests/learner.md +++ b/docs/contract-requests/learner.md @@ -19,4 +19,12 @@ and let `startServer` take `Array`. ## 3. Package entry for the browser -`@stc/feature-learner`'s index exports both `learnerFeature` (server) and `learnerWeb`/`LessonView` (browser). Bun's browser bundler tree-shakes the server half (checked), so one entry works. The web shell imports `learnerWeb` from `@stc/feature-learner`; the studio can import `LessonView` the same way. +**Problem.** The package index exports both the server (`learnerFeature`, which pulls in Better Auth and `@better-auth/sso`) and the browser half (`learnerWeb`, `LessonView`). A browser bundle of the index fails: `@better-auth/sso` imports `X509Certificate` from `node:crypto`, which has no browser polyfill. The root `tsconfig.json` maps `@stc/*` to `packages/*/src`, so package.json `exports` subpaths are not used either. + +**Workaround in `feat/learner` (applied).** `LessonView` and `learnerWeb` are exported from the index (for server-side and type use) and from a browser-only entry, `@stc/feature-learner/web`. That subpath resolves through the existing mapping to `packages/feature-learner/web/src/index.ts`, a one-line re-export of `src/web`. `apps/web/src/features.ts` imports `learnerWeb` from it, and the studio should import `LessonView` from it too. + +**Proposal.** Add `"@stc/*/web": ["packages/*/src/web"]` ahead of the general mapping in `tsconfig.json` (Bun honours tsconfig paths), and let every feature package expose `src/web/index.ts`. The shim directory can then be deleted. This also settles the importer's relative import in `features.ts`. + +## 4. LessonView for the studio preview + +`import { LessonView } from "@stc/feature-learner/web"`. Props: `lesson: LessonPayload` (the JSON from `GET /api/courses/:id/lessons/:lessonId`), plus optional `look`, `track`, `onTrackChange`, `preview`. With `preview` set, it makes no notes, attempt or progress requests; it still loads code listings and decks through the normal media routes. It injects its own stylesheet (scoped under `.lr`) and works with or without a router. diff --git a/docs/superpowers/plans/2026-10-30-learner.md b/docs/superpowers/plans/2026-10-30-learner.md index f5c82be..e6c1e8b 100644 --- a/docs/superpowers/plans/2026-10-30-learner.md +++ b/docs/superpowers/plans/2026-10-30-learner.md @@ -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_`, 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(".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, `
` 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 `