Files
STCourses/docs/superpowers/plans/2026-10-30-studio.md
T
2026-10-30 03:11:41 +01:00

6.5 KiB
Raw Blame History

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:
    • <dataDir>/courses/<id>: a bare repo (it holds no stcourse.yaml, so course discovery never loads it);
    • <dataDir>/worktrees/<id>/published: worktree on the published branch (main); the only tree learners read;
    • <dataDir>/worktrees/<id>/drafts/<author>: worktree on draft/<author> (depth 3 under worktrees/, so discovery, which stops at depth 2, never loads drafts).
  • Publish = git merge --no-ff draft/<author> 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 <dataDir> (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/<author>; 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/<author> 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.