83 lines
5.3 KiB
Markdown
83 lines
5.3 KiB
Markdown
# 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`:
|
|
|
|
```ts
|
|
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`:
|
|
|
|
```ts
|
|
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.
|