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

158 lines
4.7 KiB
Markdown

# Task 6 — `get_lesson` response (first uncompleted lesson default)
> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md).
## Goal
Pure response function for `get_lesson`. When `lesson_id` is `None`, returns the first lesson in the module that has at least one unsolved exercise (spec §2.1). Each call surfaces the next pending exercise rather than always the first one. The function is pure (no I/O) and parameterized by the caller's `UserProgress`.
## Prerequisites
- Task 2 merged (CurriculumLoader).
- Task 3 merged (UserProgress type).
## Files
- Create: `src/tools/tutor_tools.rs`
- Modify: `src/tools/mod.rs` (declare module)
## Steps
- [ ] **Step 1: Create `src/tools/tutor_tools.rs` with implementation + tests**
```rust
use serde_json::{json, Value};
use crate::progress::UserProgress;
use crate::tutor::CurriculumLoader;
pub fn get_lesson_response(
loader: &CurriculumLoader,
progress: &UserProgress,
module_id: u32,
lesson_id: Option<u32>,
) -> Value {
let module = match loader.get_module(module_id) {
None => return json!({ "error": format!("module {} not found", module_id) }),
Some(m) => m,
};
let resolved = lesson_id
.or_else(|| first_unsolved_lesson(module, progress))
.or_else(|| module.lessons.first().map(|l| l.id));
let resolved = match resolved {
Some(id) => id,
None => return json!({ "error": format!("module {} has no lessons", module_id) }),
};
let lesson = match loader.get_lesson(module_id, resolved) {
None => return json!({ "error": format!("module {} lesson {} not found", module_id, resolved) }),
Some(l) => l,
};
let pending_exercise = lesson
.exercises
.iter()
.find(|e| !progress.has_solved(&e.id));
if pending_exercise.is_none() && lesson.exercises.iter().all(|e| progress.has_solved(&e.id)) {
return json!({
"module_id": module_id,
"lesson_id": resolved,
"title": lesson.title,
"concept": lesson.concept,
"module_completed": all_lessons_completed(module, progress),
"message": "Toutes les exercices de cette leçon sont résolus.",
});
}
json!({
"module_id": module_id,
"lesson_id": resolved,
"title": lesson.title,
"concept": lesson.concept,
"example_circuit": lesson.example_circuit,
"what_to_observe": lesson.what_to_observe,
"exercise": pending_exercise.map(|e| json!({
"id": e.id,
"prompt": e.prompt,
"hint": e.hint,
})),
})
}
fn first_unsolved_lesson(module: &crate::tutor::Module, progress: &UserProgress) -> Option<u32> {
module
.lessons
.iter()
.find(|l| l.exercises.iter().any(|e| !progress.has_solved(&e.id)))
.map(|l| l.id)
}
fn all_lessons_completed(module: &crate::tutor::Module, progress: &UserProgress) -> bool {
module
.lessons
.iter()
.flat_map(|l| l.exercises.iter())
.all(|e| progress.has_solved(&e.id))
}
#[cfg(test)]
mod tests {
use super::*;
fn loader() -> CurriculumLoader { CurriculumLoader::default() }
#[test]
fn get_lesson_returns_first_pending_exercise_for_known_module() {
let resp = get_lesson_response(&loader(), &UserProgress::default(), 1, Some(1));
assert_eq!(resp["module_id"].as_u64().unwrap(), 1);
assert_eq!(resp["lesson_id"].as_u64().unwrap(), 1);
assert_eq!(resp["exercise"]["id"].as_str().unwrap(), "1-1-a");
}
#[test]
fn get_lesson_skips_solved_exercises_within_a_lesson() {
let mut progress = UserProgress::default();
progress.mark_solved("1-1-a");
let resp = get_lesson_response(&loader(), &progress, 1, Some(1));
assert_eq!(resp["exercise"]["id"].as_str().unwrap(), "1-1-b");
}
#[test]
fn get_lesson_default_resolves_to_first_lesson_with_pending_exercises() {
let resp = get_lesson_response(&loader(), &UserProgress::default(), 1, None);
assert_eq!(resp["lesson_id"].as_u64().unwrap(), 1);
}
#[test]
fn get_lesson_unknown_module_returns_error_payload() {
let resp = get_lesson_response(&loader(), &UserProgress::default(), 99, None);
assert!(resp.get("error").is_some());
}
}
```
- [ ] **Step 2: Declare `tutor_tools` in `src/tools/mod.rs`**
Add at the top:
```rust
pub mod tutor_tools;
```
- [ ] **Step 3: Run tests**
```bash
cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED"
```
Expected: `test result: ok. 4 passed`.
- [ ] **Step 4: Commit**
```bash
git add src/tools/tutor_tools.rs src/tools/mod.rs
git commit -m "feat: get_lesson response (first uncompleted lesson default)"
```