From f63b09fc9c3bcb90f1a2a2e83bc5f62d7c5dc425 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 11 Jun 2026 10:48:36 +0200 Subject: [PATCH] Refine Technique Path Scope Logic and Enhance Test Coverage - Updated the `exercise_passes_technique_path_scope` function to clarify the requirements for technique inclusion, ensuring that the primary technique must appear in the exercise text. - Enhanced the logic to allow for relaxed matching based on parts of the primary topic, improving flexibility in exercise validation. - Added new tests to validate the rejection of off-topic exercises, specifically addressing cases where only stage goals mention the primary technique. - Improved the selection logic in `pick_best_path_hit` to ensure proper handling of roadmap stage matches. --- backend/planning_exercise_semantics.py | 23 +++++--- .../test_planning_roadmap_stage_match.py | 56 +++++++++++++++++++ 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/backend/planning_exercise_semantics.py b/backend/planning_exercise_semantics.py index 38798e2..c7b0d77 100644 --- a/backend/planning_exercise_semantics.py +++ b/backend/planning_exercise_semantics.py @@ -207,7 +207,10 @@ def exercise_passes_technique_path_scope( relaxed: bool = False, ) -> bool: """ - Technik-Pfad: keine Geschwister-Technik; Haupttechnik im Übungstext oder Stufen-Lernziel. + Technik-Pfad: keine Geschwister-Technik; Haupttechnik muss im Übungstext vorkommen. + + Das Stufen-Lernziel allein reicht nicht — sonst würden themenfremde Übungen (z. B. Kumite) + nur wegen „Mawashi Geri“ im Lernziel durch das Gate rutschen. """ primary = _normalize_phrase(primary_topic) if not primary: @@ -218,11 +221,14 @@ def exercise_passes_technique_path_scope( if excludes and _blob_matches_stage_excludes(blob, excludes): return False - in_exercise = _phrase_in_blob(primary, blob) - in_stage = _phrase_in_blob(primary, _blob_from_fields("", "", learning_goal, [])) - if in_exercise or in_stage: + if _phrase_in_blob(primary, blob): return True - return relaxed + + if relaxed: + parts = [p for p in primary.split() if len(p) >= 4] + if parts and any(_phrase_in_blob(part, blob) for part in parts): + return True + return False def _detect_development_arc(q_lower: str) -> List[str]: @@ -1245,15 +1251,16 @@ def pick_best_path_hit( return best chosen = _scan(strict=True) - if chosen: - return chosen - chosen = _scan(strict=False) if chosen: return chosen if roadmap_stage_match: return None + chosen = _scan(strict=False) + if chosen: + return chosen + # Notfall (nur retrieval-first / Brücken): bester verbleibender Treffer fallback: Optional[Dict[str, Any]] = None fallback_key: Tuple[float, float] = (-1.0, -1.0) diff --git a/backend/tests/test_planning_roadmap_stage_match.py b/backend/tests/test_planning_roadmap_stage_match.py index 0a9f97d..56d768b 100644 --- a/backend/tests/test_planning_roadmap_stage_match.py +++ b/backend/tests/test_planning_roadmap_stage_match.py @@ -249,6 +249,62 @@ def test_pick_best_skips_kumite_for_mawashi_athletic_path(): assert int(chosen["id"]) == 2 +def test_technique_scope_rejects_kumite_when_only_stage_goal_mentions_mawashi(): + siblings = technique_sibling_excludes("mawashi geri") + assert not exercise_passes_technique_path_scope( + primary_topic="mawashi geri", + title="Kumite Grundstellungen", + summary="Partner-Distanz und freier Kampf", + goal="Kumite-Technik", + learning_goal="Sprungkraft und Koordination für Mawashi Geri", + sibling_excludes=siblings, + relaxed=True, + ) + + +def test_pick_best_roadmap_rejects_kumite_with_path_primary_topic(): + q = "gesprungener Mawashi Geri Sprungphase" + brief = enrich_brief_with_path_constraints(build_semantic_brief(q), q) + primary = (brief.primary_topic or "mawashi geri").strip() + stage_goal = "Sprungvorbereitung für Mawashi Geri" + stage_brief = build_stage_match_brief( + learning_goal=stage_goal, + path_primary_topic=primary, + path_technique_excludes=technique_sibling_excludes(primary), + ) + hits = [ + { + "id": 1, + "title": "Kumite Distanztraining", + "summary": "Partnerarbeit", + "goal": "Kampfvorbereitung", + "score": 0.95, + "semantic_score": 0.4, + "stage_semantic_score": 0.35, + }, + { + "id": 2, + "title": "Sprungkraft Plyometrie", + "summary": "Absprung für Tritttechnik", + "goal": "Sprungkraft Mawashi Geri Vorbereitung", + "score": 0.7, + "semantic_score": 0.45, + "stage_semantic_score": 0.42, + }, + ] + chosen = pick_best_path_hit( + hits, + set(), + stage_learning_goal=stage_goal, + roadmap_stage_match=True, + stage_match_brief=stage_brief, + path_primary_topic=primary, + path_technique_excludes=technique_sibling_excludes(primary), + ) + assert chosen is not None + assert int(chosen["id"]) == 2 + + def test_technique_scope_rejects_sibling_geri_for_mawashi_path(): siblings = technique_sibling_excludes("mawashi geri") assert any("mae" in s for s in siblings)