# Task 2 — `CurriculumLoader` (`OnceLock`-cached, no `expect()`) > **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). ## Goal Provide a lazy, cached accessor over the embedded curriculum that returns `Result<&Curriculum, BridgeError>` instead of panicking. Required by every tutor tool. CLAUDE.md §10 forbids `expect()` in production. ## Prerequisites - Task 1 merged. ## Files - Modify: `src/tutor.rs` - Modify: `src/error.rs` ## Steps - [ ] **Step 1: Add a `Curriculum` variant to `BridgeError` in `src/error.rs`** ```rust #[error("curriculum data is malformed: {0}")] Curriculum(String), ``` - [ ] **Step 2: Append failing tests to `src/tutor.rs` (inside the existing `#[cfg(test)]` block)** ```rust #[test] fn loader_returns_curriculum_on_first_call() { let loader = CurriculumLoader::default(); let c = loader.curriculum().expect("embedded curriculum must parse"); assert_eq!(c.version, "1.0"); } #[test] fn find_exercise_by_id_returns_correct_exercise() { let loader = CurriculumLoader::default(); let exercise = loader.find_exercise("1-1-a").unwrap(); assert_eq!(exercise.id, "1-1-a"); } #[test] fn find_exercise_with_unknown_id_returns_none() { let loader = CurriculumLoader::default(); assert!(loader.find_exercise("99-99-z").is_none()); } #[test] fn get_lesson_unknown_module_returns_none() { let loader = CurriculumLoader::default(); assert!(loader.get_lesson(99, 1).is_none()); } #[test] fn loader_reports_curriculum_error_for_malformed_json() { let result = CurriculumLoader::from_str("{ not valid json"); assert!(result.is_err()); } ``` - [ ] **Step 3: Implement `CurriculumLoader` (above the `#[cfg(test)]` block)** ```rust use std::sync::OnceLock; use crate::error::BridgeError; const CURRICULUM_JSON: &str = include_str!("../curriculum/curriculum.json"); #[derive(Default)] pub struct CurriculumLoader { cell: OnceLock>, } impl CurriculumLoader { pub fn curriculum(&self) -> Result<&Curriculum, BridgeError> { let result = self.cell.get_or_init(|| { serde_json::from_str::(CURRICULUM_JSON).map_err(|e| e.to_string()) }); result .as_ref() .map_err(|msg| BridgeError::Curriculum(msg.clone())) } pub fn from_str(json: &str) -> Result { let curriculum: Curriculum = serde_json::from_str(json).map_err(|e| BridgeError::Curriculum(e.to_string()))?; let loader = Self::default(); let _ = loader.cell.set(Ok(curriculum)); Ok(loader) } pub fn get_module(&self, module_id: u32) -> Option<&Module> { self.curriculum().ok()?.modules.iter().find(|m| m.id == module_id) } pub fn get_lesson(&self, module_id: u32, lesson_id: u32) -> Option<&Lesson> { self.get_module(module_id)? .lessons .iter() .find(|l| l.id == lesson_id) } pub fn find_exercise(&self, exercise_id: &str) -> Option<&Exercise> { for module in &self.curriculum().ok()?.modules { for lesson in &module.lessons { if let Some(ex) = lesson.exercises.iter().find(|e| e.id == exercise_id) { return Some(ex); } } } None } pub fn all_exercises(&self) -> Vec<&Exercise> { self.curriculum() .ok() .map(|c| { c.modules .iter() .flat_map(|m| m.lessons.iter()) .flat_map(|l| l.exercises.iter()) .collect() }) .unwrap_or_default() } } ``` - [ ] **Step 4: Run tests (Green)** ```bash cargo test tutor::tests 2>&1 | grep -E "test result|FAILED" ``` Expected: `test result: ok. 6 passed`. - [ ] **Step 5: Commit** ```bash git add src/tutor.rs src/error.rs git commit -m "feat: implement CurriculumLoader with OnceLock cache and structured error" ```