- Media exclusions move from the editable root .gitignore to the repo's
info/exclude, and dirtyFiles never counts files listed in media.json, so an
author's or remote's .gitignore cannot block publish or Sync. Publish and
Sync refuse commits that track a file over referenced media.
- Importer jobs (code-repo, pdf-deck, transcribe) and the cover route run
under the course lock now shared with the studio (core withCourseLock) and
re-check the course inside it; after conversion they fail with "this course
is now edited in the studio". Convert answers 409 while an import job for
the course is queued or running, and the job re-checks.
- Crash-safe conversion: the import folder is hidden (.converting-<id>) before
the published worktree exists; cross-filesystem moves copy into a staging
dir, fsync, verify size and sha256, then rename; the source is retired as
.converted-<id>.<n>. Recovery runs at server start and before every convert;
a re-run of a finished conversion reports success. The loader skips
dot-folders.
- Minor: no raw NUL bytes in source, link fallback only on EXDEV/EPERM/EMLINK,
referenced media are not read whole by the editor, missing media are logged,
mediaDir JSDoc, docs on per-author copies across filesystems.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Contracts and core:
- Feature gains auth and stop; FeatureFactory; core composeServer builds the
context, runs the factories, then the job queue from the union of their jobs
(R8/R14). startServer uses it; main.ts composes learner, import, studio, AI.
- AppConfig/loadConfig own the layout and settings: courseDirs include
<dataDir>/courses and <dataDir>/worktrees, studioReposDir, cacheDir,
importRoots, uploadMaxMb, aiAllowedBaseUrls, transcribeUrl, python.
resolveConfig derives the same for tests; cachePaths shares the importer's
cache layout.
- canView: the global author role views every course (R14); the learner,
studio and AI packages all call it.
- migrate(db, packages): core first, then packages in name order.
- Media: nosniff on every response, a sandbox CSP on svg/html/js; a Range on
an empty file is 416.
- courseUrl/lessonUrl and the lesson route patterns live in @stc/contracts.
Packages:
- import, studio and AI read AppConfig instead of process.env; the importer
refuses ids used by studio/repos.
- learner migrations run through core migrate; library covers fall back to
the importer's cached candidate behind a canView-checked route.
- studio preview renders through the learner LessonView (anchors honoured,
draft URLs); AI drafts go to the studio draft branch for studio courses (as
the requesting user) and to <dataDir>/drafts otherwise, and the generator
page says which.
Web: tsconfig paths for @stc/<feature>/web browser entries; features.ts
imports them by package name; nav.roles hides links by role.
Tests: compose, config, canView, media headers, migrations order, covers,
preview, nav, SPA bundle without server code, and an end-to-end flow on
SQLite and Postgres.
Co-Authored-By: Claude Opus 5.5 <[email protected]>