pub fn add_lesson(
dir: &Path,
language: Language,
id: &str,
) -> Result<PathBuf, AddLessonError>Expand description
Add a language lesson id to the course rooted at dir: write
lessons/<id>.yaml from lesson_template and its eval_-prefixed
sibling suite from eval_template, then register the lesson in the
manifest.
The effectful shell (§2.2) over the pure template selection. Three guards fire
before any write (§1.3.1): an unsafe slug is refused as
AddLessonError::InvalidId; a directory with no blendtutor.toml is refused
as AddLessonError::NotACourse — its manifest is opened up front, so a
non-course directory is never left with an orphan lesson; and an id whose
lesson file — or whose eval sibling — already exists is refused as
AddLessonError::AlreadyExists, the create-new write making that check
atomic so a duplicate never clobbers the existing lesson, a hand-authored
sibling suite, nor appends a second manifest entry. On success the lesson file
and its sibling are written, a [[lessons]] entry appended to
blendtutor.toml, and the course-relative lesson path returned. The sibling
is deliberately not registered in the manifest: it is a derived path
(eval_sibling_path), so it can never drift from the lesson it grades.
The manifest is opened first (the not-a-course guard) but its entry is appended
last, after the files are written. Two residual non-atomic windows remain, each
leaving a valid, recoverable state: a rare append failure after good writes
leaves an orphan lesson list (manifest-driven) simply omits; and a
pre-existing eval sibling refuses after the lesson file is written, leaving
an unregistered lesson a re-run then refuses on — recoverable by deleting the
scaffolded lesson or adopting it. Registering first would be worse: a list
row pointing at a file that was never written.