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

4.7 KiB

Task 9 — get_progress response (current_lesson + unlock rule)

Index: README. Spec: design.

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
    #[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
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
cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED"

Expected: test result: ok. 14 passed.

  • Step 4: Commit
git add src/tools/tutor_tools.rs
git commit -m "feat: get_progress with current_lesson and module unlock rule (spec §4)"