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

4.7 KiB

Task 6 — get_lesson response (first uncompleted lesson default)

Index: README. Spec: design.

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
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:

pub mod tutor_tools;
  • Step 3: Run tests
cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED"

Expected: test result: ok. 4 passed.

  • Step 4: Commit
git add src/tools/tutor_tools.rs src/tools/mod.rs
git commit -m "feat: get_lesson response (first uncompleted lesson default)"