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

139 lines
4.7 KiB
Markdown

# Task 9 — `get_progress` response (current_lesson + unlock rule)
> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md).
## Goal
Build a structured progress payload: per-module status (`locked | unlocked | in_progress | completed`), current module + current lesson, total exercises solved, percent complete. Spec §4 unlock rule: module `n+1` becomes `unlocked` once at least ⌈2/3⌉ of module `n`'s exercises are solved.
## Prerequisites
- Task 8 merged (`tutor_tools.rs` populated).
## Files
- Modify: `src/tools/tutor_tools.rs`
## Steps
- [ ] **Step 1: Append failing tests**
```rust
#[test]
fn get_progress_with_no_progress_returns_module_1_unlocked_others_locked() {
let resp = get_progress_response(&loader(), &UserProgress::default());
assert_eq!(resp["current_module"].as_u64().unwrap(), 1);
let modules = resp["modules"].as_array().unwrap();
assert_eq!(modules[0]["status"].as_str().unwrap(), "unlocked");
if modules.len() > 1 {
assert_eq!(modules[1]["status"].as_str().unwrap(), "locked");
}
assert_eq!(resp["total_exercises_solved"].as_u64().unwrap(), 0);
}
#[test]
fn get_progress_completed_module_marked_completed() {
let mut progress = UserProgress::default();
progress.mark_solved("1-1-a");
progress.mark_solved("1-1-b");
let resp = get_progress_response(&loader(), &progress);
assert_eq!(resp["modules"].as_array().unwrap()[0]["status"].as_str().unwrap(), "completed");
}
#[test]
fn get_progress_returns_current_lesson_for_active_module() {
let resp = get_progress_response(&loader(), &UserProgress::default());
assert_eq!(resp["current_lesson"].as_u64().unwrap(), 1);
}
```
- [ ] **Step 2: Implementation**
```rust
pub fn get_progress_response(loader: &CurriculumLoader, progress: &UserProgress) -> Value {
let curriculum = match loader.curriculum() {
Ok(c) => c,
Err(e) => return json!({ "error": e.to_string() }),
};
let total_exercises: usize = loader.all_exercises().len();
let solved_count = progress.solved_exercises.len().min(total_exercises);
let mut prev_unlocks_next = true;
let modules: Vec<Value> = curriculum
.modules
.iter()
.map(|m| {
let total_in_module: usize = m.lessons.iter().map(|l| l.exercises.len()).sum();
let solved_in_module = m
.lessons
.iter()
.flat_map(|l| l.exercises.iter())
.filter(|e| progress.has_solved(&e.id))
.count();
let status = if !prev_unlocks_next {
"locked"
} else if total_in_module > 0 && solved_in_module == total_in_module {
"completed"
} else if solved_in_module > 0 {
"in_progress"
} else {
"unlocked"
};
// Spec §4: next module unlocks when ⌈2/3⌉ of current is solved.
let unlock_threshold = (total_in_module * 2 + 2) / 3;
prev_unlocks_next = solved_in_module >= unlock_threshold && total_in_module > 0;
json!({
"id": m.id,
"title": m.title,
"status": status,
"exercises_solved": solved_in_module,
"exercises_total": total_in_module,
})
})
.collect();
let (current_module, current_lesson) = current_position(curriculum, progress);
let percent = if total_exercises == 0 { 0 } else { (solved_count * 100) / total_exercises };
json!({
"current_module": current_module,
"current_lesson": current_lesson,
"modules": modules,
"total_exercises_solved": solved_count,
"total_exercises": total_exercises,
"percent_complete": percent,
})
}
fn current_position(curriculum: &crate::tutor::Curriculum, progress: &UserProgress) -> (u32, u32) {
for module in &curriculum.modules {
for lesson in &module.lessons {
if lesson.exercises.iter().any(|e| !progress.has_solved(&e.id)) {
return (module.id, lesson.id);
}
}
}
let last = curriculum.modules.last();
(
last.map(|m| m.id).unwrap_or(1),
last.and_then(|m| m.lessons.last()).map(|l| l.id).unwrap_or(1),
)
}
```
- [ ] **Step 3: Run tests**
```bash
cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED"
```
Expected: `test result: ok. 14 passed`.
- [ ] **Step 4: Commit**
```bash
git add src/tools/tutor_tools.rs
git commit -m "feat: get_progress with current_lesson and module unlock rule (spec §4)"
```