- 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 <[email protected]>
C1: writes lstat every path component and refuse symlinks (dangling included),
check the parent's real path after mkdir, and open with O_NOFOLLOW.
I1: only https://, ssh:// and git@host: remotes; protocol.file/ext/fd=never on
every fetch and push; global/system git config ignored.
I2: STC_UPLOAD_MAX_MB cap (default 100) by Content-Length and while streaming;
extension allowlist (415); stcourse.yaml refused; 409 unless overwrite=true;
sandboxing CSP on draft raw files.
I3: owner-only visibility/source.kind rule checked on the merged result at
publish and sync, and at resolve; publish also validates the merged tree.
I4: bare repos in <dataDir>/studio/repos/<id>.git; imported courses read-only.
I5: list status comes from the bare repo; drafts are created lazily.
Co-Authored-By: Claude Opus 5.5 <[email protected]>