# 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 = 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)" ```