Refine Technique Path Scope Logic and Enhance Test Coverage
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m24s

- 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.
This commit is contained in:
Lars 2026-06-11 10:48:36 +02:00
parent 713a344d17
commit f63b09fc9c
2 changed files with 71 additions and 8 deletions

View File

@ -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)

View File

@ -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)