Files
quantum-bridge-mcp/docs/superpowers/plans/2026-04-29-quantum-tutor/02-curriculum-loader.md
T
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

145 lines
4.1 KiB
Markdown

# 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<Result<Curriculum, String>>,
}
impl CurriculumLoader {
pub fn curriculum(&self) -> Result<&Curriculum, BridgeError> {
let result = self.cell.get_or_init(|| {
serde_json::from_str::<Curriculum>(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<Self, BridgeError> {
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"
```