Files
STCourses/docs/contract-requests/studio.md
T

5.3 KiB

Contract requests from WP-C (studio)

Each item says what is missing, what the studio does locally for now, and what Phase 2 should change.

1. Features that need the ServerContext (apps/server)

startServer(config, features: Feature[]) builds the ServerContext internally, so a feature factory such as studioFeature(ctx) cannot be put into the feature list. The studio therefore does not touch apps/server/src/main.ts. Locally it runs through packages/feature-studio/dev/serve.ts, which mirrors startServer.

Proposal:

  • startServer(config, features: (Feature | ((ctx: ServerContext) => Feature))[]), and main.ts adds studioFeature.
  • An optional stop?(): void | Promise<void> on Feature, called from stop(). The studio uses it to stop its published-worktree watcher (an unref'd interval, so it never blocks exit).

2. Learners must read studio courses from the published worktree (config / main.ts)

Studio courses live at:

  • <dataDir>/courses/<id>: a bare repo with no stcourse.yaml, so discovery never loads it;
  • <dataDir>/worktrees/<id>/published: the published branch, which is what learners must see;
  • <dataDir>/worktrees/<id>/drafts/<author>: drafts. They sit at depth 3, so the depth-2 discovery never loads them.

createCourseRepository(config.courseDirs) does not scan <dataDir>/worktrees. The package exports studioCourseDirs(config), which appends it. Tests and the dev entry use it.

Proposal: main.ts calls createCourseRepository(studioCourseDirs(config)), or loadConfig always appends <dataDir>/worktrees.

Related, for Phase 2: the importer writes plain folders to <dataDir>/courses/<id>. These coexist with the studio's bare repos. A course id that exists either way cannot be created again in the studio. Adopting an imported course into the studio (git init, move it to the bare/worktree layout) is not part of v1.

3. Live preview: swap in the learner LessonView

The studio preview sits behind an adapter in @stc/feature-studio/web:

export interface LessonPreviewProps {
  courseId: string;
  markdown: string;
  lesson?: { id: string; title: string; type: string; anchor?: string } | null;
  resolveUrl: (rel: string) => string; // course-relative path -> URL of the draft file
}
export function setLessonPreview(c: React.ComponentType<LessonPreviewProps>): void;

v1 uses a small, safe built-in Markdown renderer. Proposal: @stc/feature-learner exports a LessonView that can render from a Markdown string plus a URL resolver, without fetching the published course. Phase 2 then calls setLessonPreview(adapter) in packages/feature-studio/src/web.ts, so drafts are previewed with the real renderer.

4. Web entry points (tsconfig paths)

Phase 1 packages have server code (Bun.$, node:fs) that must not enter the browser bundle, so the studio has a separate browser entry, src/web.ts. The @stc/* → packages/*/src path maps @stc/feature-studio/web to a directory that does not exist, and the workspace has no node_modules/@stc links. For now, apps/web/src/features.ts imports it with a relative path.

Proposal: add "@stc/*/web": ["packages/*/src/web.ts"] to the root tsconfig paths, and switch the line to import { studioWeb } from "@stc/feature-studio/web".

5. Role-aware navigation (contracts/web.ts)

WebRoute.nav cannot be limited by role, so the header "Studio" link shows for everyone. The page itself tells learners and anonymous users that the studio is for authors, and the API returns 401/403.

Proposal: add nav.roles?: Role[], and have the shell show the link only to matching users. This needs the learner package's session in the web shell.

6. Draft API for WP-D (AI)

Exported from @stc/feature-studio:

export interface DraftWriter {
  writeDraftFiles(courseId: string, authorId: string, files: { path: string; content: string }[], message: string): Promise<{ branch: string; commit: string }>;
}
export function createDraftWriter(ctx: { config: Pick<AppConfig, "dataDir"> }, opts?: {
  identity?: (authorId: string) => Promise<{ name: string; email: string } | null> | { name: string; email: string } | null;
}): DraftWriter;
  • It writes and commits on draft/<authorId> and never publishes.
  • Without identity, commits are authored as <authorId> <[email protected]>. WP-D should pass the signed-in user's name and email.
  • It throws for courses not managed by the studio, and for paths that are absolute, contain .. or touch .git.
  • studioFeature(ctx).draftWriter is the same API, already bound to ctx.

7. Authors and roles (decision to confirm)

Studio access goes to:

  • owner and author roles: all studio courses, and they can create courses;
  • users with a course_authors row: that course only.

Creating a course inserts the creator into course_authors. No new tables are added, so migrationsDirs is [].

8. External pushes straight into the bare repo (no action needed)

The platform reloads when the published worktree's HEAD changes: an external commit in that worktree, a studio publish, or a Sync. A direct git push into <dataDir>/courses/<id> is refused by git itself ("branch is currently checked out", checked with git 2.47), so the published files cannot silently fall behind main. Outside tools should commit in the published worktree, or push to a configured remote that the author then Syncs.