6.2 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 addsstudioFeature.- An optional
stop?(): void | Promise<void>onFeature, called fromstop(). 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)
Layout after the review (controller ruling on I4):
<dataDir>/studio/repos/<id>.git: studio bare repos. They contain nostcourse.yaml, so discovery never loads them.<dataDir>/worktrees/<id>/published: the published branch, which is what learners read.<dataDir>/worktrees/<id>/drafts/<author>: drafts. They sit at depth 3, so the depth-2 discovery never loads them.<dataDir>/courses/<id>: the importer's plain folders (WP-B), untouched by the studio.
createCourseRepository(config.courseDirs) scans neither <dataDir>/worktrees nor <dataDir>/courses. The package exports studioCourseDirs(config), which returns the configured dirs plus <dataDir>/courses and <dataDir>/worktrees. Tests and the dev entry use it.
Proposal: main.ts calls createCourseRepository(studioCourseDirs(config)), or loadConfig appends both dirs.
Imported courses (source.kind other than authored, with their root under <dataDir>/courses) are read-only in the studio for v1, with the message "Imported course: read-only in the studio for now". There was a TODO(studio) in feature.ts (readOnlyMessage) for converting one into a studio-managed repo; v1.1 S resolved it with the owner-only "Make editable in the studio" action (see docs/operations.md, "Imported courses in the studio"). The studio refuses to create a course whose id already exists as <dataDir>/courses/<id>. WP-B should likewise refuse an import id that <dataDir>/studio/repos/<id>.git already uses.
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).draftWriteris the same API, already bound toctx.
7. Authors and roles (decision to confirm)
Studio access goes to:
ownerandauthorroles: all studio courses, and they can create courses;- users with a
course_authorsrow: 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.
9. Core media route serves SVG/HTML inline (core, Phase 2)
The studio's draft raw route now sends Content-Security-Policy: …; sandbox and X-Content-Type-Options: nosniff. The learner-facing /api/media/:courseId/* in packages/core serves author-supplied SVG inline from the app's origin and sets neither header.
Proposal: add the same two headers in serveMedia.