- The convert job reloads the course on success and on failure, so learners
get the intact import (rolled back) or the published studio course, never a
course whose folder moved away.
- POST /convert on a course with an incomplete conversion (.converting-<id>,
staging or retired folders) queues a job that recovers and finishes it
instead of answering 409; the studio page shows "Conversion incomplete" with
Retry for the owner.
- The 409 for pending import jobs names their kinds and ids; the dead withLock
in git.ts is gone; the dot-folder discovery rule is documented in
docs/operations.md and spec section 5.
- Route tests (SQLite and Postgres) inject a failed cross-filesystem copy, a
checksum mismatch and a pre-commit failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
POST /api/studio/courses/:id/convert (owner only, CSRF-guarded by core) queues
studio.convert-import; GET /api/studio/jobs/:id reports progress. Read-only
responses say whether the caller may convert. Resolves TODO(studio) (ruling R9).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>