From fc37da9789e1ef459b45a933f1905eca5b5ad6ac Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 30 Oct 2026 03:11:41 +0100 Subject: [PATCH] docs(studio): WP-C implementation plan Co-Authored-By: Claude Opus 5.5 --- docs/superpowers/plans/2026-10-30-studio.md | 77 +++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-30-studio.md diff --git a/docs/superpowers/plans/2026-10-30-studio.md b/docs/superpowers/plans/2026-10-30-studio.md new file mode 100644 index 0000000..97362bd --- /dev/null +++ b/docs/superpowers/plans/2026-10-30-studio.md @@ -0,0 +1,77 @@ +# STCourses WP-C Studio Implementation Plan + +> **For agentic workers:** executed inline by the WP-C agent (superpowers:executing-plans). Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Browser studio for studio-managed courses: create a course, edit the manifest, lessons (Markdown with live preview), deck links, attachments and exercises in a per-author draft worktree; every save is a commit by the author; publish merges the draft into the published branch that learners read; Sync with a remote only on request; conflicts are shown and resolved by the author, never automatically. + +**Architecture:** +- `packages/feature-studio/src/server/git.ts`: a thin `git` CLI wrapper over `Bun.$` (explicit identity, no hooks, no signing) plus a per-course async mutex. +- `packages/feature-studio/src/server/store.ts`: `StudioStore`, the git model of spec section 12: + - `/courses/`: a **bare** repo (it holds no `stcourse.yaml`, so course discovery never loads it); + - `/worktrees//published`: worktree on the published branch (`main`); the only tree learners read; + - `/worktrees//drafts/`: worktree on `draft/` (depth 3 under `worktrees/`, so discovery, which stops at depth 2, never loads drafts). +- Publish = `git merge --no-ff draft/` inside the published worktree. A conflicting merge is aborted at once, so the published tree is never left half-merged, and the conflicting paths are returned. The author then runs "Update draft" (merge of `main`, or `origin/main` for Sync, into the draft worktree), resolves each file in the studio, and publishes again. +- Save uses optimistic concurrency: each file carries the hash of the content the editor loaded; a mismatch with the file on disk (an external edit) gives 409 with both versions. Nothing is overwritten. +- `watch.ts`: polls the published HEAD of each managed course (default every 3 s, unref'd timer) and calls `ctx.courses.reload(id)` when it changed (external commit, pull, publish). +- `feature.ts`: `studioFeature(ctx)` with `/api/studio/*` routes. Access: owner, `author` role, or a `course_authors` row for that course. Courses not managed by the studio (overlays, fixture/imported folders) are listed read-only: "This course is managed outside STCourses". +- `draft-writer.ts`: `createDraftWriter(ctx)` for WP-D. +- Web (`src/web/*`): `/studio` (course list, New course) and `/studio/:courseId` (outline sidebar, editors, status bar with Publish / Update draft / Sync and the conflict view). Work look from the mockup. The lesson preview sits behind a `LessonPreview` adapter; v1 is a small safe Markdown renderer; Phase 2 swaps in the learner `LessonView` (contract request). + +**Tech Stack:** Bun 1.3, TypeScript strict, git CLI 2.4x via `Bun.$`, React 19 + React Router 6, `@stc/course-format` for validation, `@stc/ui` primitives. + +**Spec:** sections 5, 9.1, 12 (git model), 13. Brief: `.superpowers/sdd/2026-10-30-stcourses/phase-1-brief.md` (WP-C). + +## Global Constraints + +Inherits every item of the Global Constraints in `docs/superpowers/plans/2026-10-30-stcourses.md`. Additionally: +- Never write into a course whose root is outside `` (overlays) — the studio refuses writes for non-managed courses. +- Paths written through the studio must be relative, stay inside the worktree, and never touch `.git`. +- No new DB tables (authors use the existing `course_authors`); `migrationsDirs` is exported empty. + +## Review Focus + +1. A conflicting external commit to `main` is detected at publish and shown; the published tree keeps the external content (test: acceptance). +2. Drafts never reach learners: before publish the `CourseRepository` still serves the published content (test: acceptance). +3. An external edit to a draft file between load and save gives 409, not an overwrite (test: store). +4. Paths with spaces and non-ASCII characters save, commit and publish (test: store). +5. Path traversal (`..`, absolute, `.git/…`) is rejected (test: store). + +## Tasks + +### Task 1: git wrapper and StudioStore (create, drafts, save) + +**Files:** Create `packages/feature-studio/src/server/{git.ts,store.ts,paths.ts}`; Test `packages/feature-studio/test/store.test.ts`. + +**Interfaces (Produces):** `createStudioStore(dataDir): StudioStore` with `createCourse`, `isManaged`, `listManaged`, `ensureDraft`, `readFile`, `listFiles`, `saveFiles`, `status`. + +- [ ] Step 1: failing tests: create gives bare repo + published worktree with a valid manifest; draft worktree on `draft/`; save commits with the author's name/email; stale base hash gives a conflict; traversal rejected; non-ASCII path works. +- [ ] Step 2: run, FAIL. Step 3: implement. Step 4: PASS. Step 5: commit. + +### Task 2: publish, update draft, conflict resolution, sync + +**Files:** extend `store.ts`; Test `packages/feature-studio/test/publish.test.ts`. + +- [ ] Step 1: failing tests: publish merges; external conflicting commit gives `{ok:false, conflicts}` and leaves `main` untouched; dirty published tree refuses publish; update draft shows ours/theirs; resolve + publish succeeds; abort; Sync with a local bare remote (pull fast-forward, push, conflict reported); no remote gives an error. +- [ ] Steps 2–5 as above. + +### Task 3: watcher and draft writer + +**Files:** `src/server/{watch.ts,draft-writer.ts}`; Test `test/watch.test.ts`, `test/draft-writer.test.ts`. + +- [ ] Failing tests: external commit triggers `courses.reload`; `writeDraftFiles` commits on `draft/` and the published tree is unchanged. Implement, pass, commit. + +### Task 4: HTTP feature and acceptance + +**Files:** `src/server/feature.ts`, `src/index.ts`; Test `test/feature.test.ts`, `test/acceptance.test.ts`. + +- [ ] Failing tests: 401/403 for anonymous and learners; overlay course read-only; create → edit lesson → publish → learner sees it via `CourseRepository` and `/api/media`; conflicting external commit shown, not overwritten; manifest validation errors returned. Implement, pass, commit. + +### Task 5: Studio web UI + +**Files:** `src/web/{api.ts,markdown.ts,preview.tsx,StudioHome.tsx,CourseStudio.tsx,editors/*.tsx,studio.css}`, `src/web.ts`; Test `test/markdown.test.ts`. + +- [ ] Failing test for the Markdown renderer (escaping, headings, fences, lists, links without `javascript:`). Implement UI. Commit. + +### Task 6: wiring, dev server, contract requests, verification + +- [ ] Append to `apps/web/src/features.ts`; dev entry `packages/feature-studio/dev/serve.ts`; `docs/contract-requests/studio.md`; run `bun test`, typecheck, Postgres run; headless Chrome screenshots of `/studio` and `/studio/:id`; report.