Enhance Planning Exercise Path Builder and Retrieval Logic
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
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 1m23s

- Updated the path selection logic to incorporate semantic gating, ensuring only relevant exercises are considered based on semantic scores.
- Introduced new functions for building path target profiles and resolving semantic skill weights, enhancing the contextual understanding of exercise suggestions.
- Improved the retrieval process by applying dynamic retrieval weights based on semantic strength, refining the accuracy of exercise recommendations.
- Incremented version to 0.8.188 and updated changelog to document these enhancements in planning AI functionality.
This commit is contained in:
Lars 2026-05-23 12:38:38 +02:00
parent c2c736dafc
commit 5b73d1a1f5
6 changed files with 333 additions and 73 deletions

View File

@ -23,9 +23,12 @@ from planning_exercise_path_ai_fill import insert_ai_proposals_for_gaps
from planning_exercise_retrieval import run_multistage_planning_retrieval
from planning_exercise_semantics import (
PlanningSemanticBrief,
apply_dynamic_retrieval_weights,
apply_path_retrieval_weights,
brief_to_summary_dict,
build_semantic_brief,
enrich_target_with_semantic_expectations,
exercise_passes_path_semantic_gate,
resolve_semantic_skill_weights,
step_phase_for_index,
step_retrieval_query,
try_enrich_semantic_brief_with_llm,
@ -33,9 +36,7 @@ from planning_exercise_semantics import (
from planning_exercise_target_pipeline import build_planning_target_with_query_pipeline
from planning_exercise_progression import apply_progression_context_to_pack
from planning_exercise_suggest import (
INTENT_SUGGEST_NEXT,
_enrich_planning_hits_with_variant_meta,
_intent_weights,
_load_skill_ids_for_exercise,
_normalize_query,
resolve_planning_exercise_intent,
@ -58,6 +59,8 @@ class ProgressionPathSuggestRequest(BaseModel):
def _pick_best_path_hit(
hits: List[Dict[str, Any]],
used_exercise_ids: Set[int],
*,
semantic_brief: Optional[PlanningSemanticBrief] = None,
) -> Optional[Dict[str, Any]]:
best: Optional[Dict[str, Any]] = None
best_key: Tuple[float, float] = (-1.0, -1.0)
@ -66,6 +69,12 @@ def _pick_best_path_hit(
if eid in used_exercise_ids:
continue
sem = float(hit.get("semantic_score") or 0.0)
if semantic_brief and not exercise_passes_path_semantic_gate(
semantic_score=sem,
title=str(hit.get("title") or ""),
brief=semantic_brief,
):
continue
score = float(hit.get("score") or 0.0)
key = (sem, score)
if key > best_key:
@ -74,6 +83,52 @@ def _pick_best_path_hit(
return best
def _build_path_target_profile(
cur,
*,
goal_query: str,
semantic_brief: PlanningSemanticBrief,
include_llm_intent: bool,
) -> Tuple[PlanningTargetProfile, Dict[str, Any], str]:
"""Einmaliges Erwartungsprofil für den gesamten Pfad (Query + Semantik + Skills)."""
empty_unit = {
"id": None,
"framework_slot_id": None,
"origin_framework_slot_id": None,
}
pipeline_context = {
"unit_title": None,
"group_name": None,
"section_title": None,
"section_guidance_notes": goal_query,
"section_exercise_count": 0,
"planned_count": 0,
"anchor_title": None,
"anchor_exercise_id": None,
"last_section_exercise_title": None,
"progression_graph_id": None,
"unit_skill_profile": None,
"section_skill_profile": None,
"has_planning_reference": False,
"expectation_mode": "query_only",
}
target, intent, _scenario, query_intent_summary = build_planning_target_with_query_pipeline(
cur,
unit=empty_unit,
planned_exercise_ids=[],
section_planned_exercise_ids=[],
anchor_exercise_id=None,
query=goal_query,
heuristic_intent=resolve_planning_exercise_intent(goal_query, "free_search"),
include_llm_intent=include_llm_intent,
context_summary=pipeline_context,
has_planning_reference=False,
)
skill_weights = resolve_semantic_skill_weights(cur, semantic_brief)
target = enrich_target_with_semantic_expectations(target, skill_weights=skill_weights)
return target, query_intent_summary, intent
def _hit_to_path_step(hit: Dict[str, Any], *, is_bridge: bool = False) -> Dict[str, Any]:
raw_vid = hit.get("suggested_variant_id")
variant_id: Optional[int] = None
@ -118,10 +173,16 @@ def _run_path_step_retrieval(
bridge_mode: bool = False,
step_a: Optional[Dict[str, Any]] = None,
step_b: Optional[Dict[str, Any]] = None,
path_target_profile: Optional[PlanningTargetProfile] = None,
path_intent: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], PlanningTargetProfile, Dict[str, Any], str]:
step_query = step_retrieval_query(semantic_brief, goal_query, step_index, max_steps)
if bridge_mode and step_a and step_b:
step_query = f"{semantic_brief.retrieval_query or goal_query} brücke zwischen schritten"
phase = step_phase_for_index(semantic_brief, step_index, max_steps)
parts = [semantic_brief.primary_topic or semantic_brief.retrieval_query or goal_query]
if phase:
parts.append(phase)
step_query = _normalize_query(" ".join(p for p in parts if p) + " brücke")
pack: Dict[str, Any] = {
"unit_id": None,
@ -158,7 +219,7 @@ def _run_path_step_retrieval(
if step_index == 0 and not bridge_mode:
heuristic_intent = resolve_planning_exercise_intent(goal_query, "free_search")
else:
heuristic_intent = INTENT_SUGGEST_NEXT
heuristic_intent = path_intent or resolve_planning_exercise_intent(goal_query, "free_search")
has_plan_ref = bool(pack.get("has_planning_reference"))
pipeline_context = {
@ -178,25 +239,25 @@ def _run_path_step_retrieval(
"expectation_mode": "query_only" if step_index == 0 and not planned_ids else "planning_hybrid",
}
target_profile, intent, _scenario, query_intent_summary = build_planning_target_with_query_pipeline(
cur,
unit=pack["unit"],
planned_exercise_ids=pack["planned_exercise_ids"],
section_planned_exercise_ids=[],
anchor_exercise_id=pack.get("anchor_exercise_id"),
query=goal_query if step_index == 0 and not bridge_mode else step_query,
heuristic_intent=heuristic_intent,
include_llm_intent=include_llm_intent and step_index == 0 and not bridge_mode,
context_summary=pipeline_context,
has_planning_reference=has_plan_ref,
)
if path_target_profile is not None:
target_profile = path_target_profile
intent = path_intent or resolve_planning_exercise_intent(goal_query, "free_search")
query_intent_summary = {}
else:
target_profile, intent, _scenario, query_intent_summary = build_planning_target_with_query_pipeline(
cur,
unit=pack["unit"],
planned_exercise_ids=pack["planned_exercise_ids"],
section_planned_exercise_ids=[],
anchor_exercise_id=pack.get("anchor_exercise_id"),
query=goal_query if step_index == 0 and not bridge_mode else step_query,
heuristic_intent=heuristic_intent,
include_llm_intent=include_llm_intent and step_index == 0 and not bridge_mode,
context_summary=pipeline_context,
has_planning_reference=has_plan_ref,
)
weights = apply_dynamic_retrieval_weights(
_intent_weights(intent),
semantic_brief,
scenario="free_search" if step_index == 0 and not bridge_mode else "progression",
has_planning_reference=has_plan_ref,
)
weights = apply_path_retrieval_weights(semantic_brief)
profile_id = tenant.profile_id
role = tenant.global_role
@ -233,6 +294,8 @@ def _make_bridge_search_fn(
exercise_kind_any: Optional[List[str]],
semantic_brief: PlanningSemanticBrief,
planned_ids: List[int],
path_target_profile: PlanningTargetProfile,
path_intent: str,
) -> Callable[..., List[Dict[str, Any]]]:
def _bridge_search(
step_a: Dict[str, Any],
@ -255,8 +318,19 @@ def _make_bridge_search_fn(
bridge_mode=True,
step_a=step_a,
step_b=step_b,
path_target_profile=path_target_profile,
path_intent=path_intent,
)
return hits
gated = [
h
for h in hits
if exercise_passes_path_semantic_gate(
semantic_score=float(h.get("semantic_score") or 0.0),
title=str(h.get("title") or ""),
brief=semantic_brief,
)
]
return gated or hits[:8]
return _bridge_search
@ -283,16 +357,21 @@ def suggest_progression_path(
cur, goal_query, semantic_brief
)
path_target_profile, first_intent_summary, path_intent = _build_path_target_profile(
cur,
goal_query=goal_query,
semantic_brief=semantic_brief,
include_llm_intent=body.include_llm_intent,
)
used: Set[int] = set()
steps: List[Dict[str, Any]] = []
planned_ids: List[int] = []
anchor_id: Optional[int] = None
anchor_variant_id: Optional[int] = None
target_profile: Optional[PlanningTargetProfile] = None
first_intent_summary: Dict[str, Any] = {}
for step_index in range(max_steps):
hits, target_profile, query_intent_summary, _intent = _run_path_step_retrieval(
hits, _tp, _qis, _intent = _run_path_step_retrieval(
cur,
tenant=tenant,
goal_query=goal_query,
@ -305,11 +384,11 @@ def suggest_progression_path(
include_llm_intent=body.include_llm_intent,
exercise_kind_any=body.exercise_kind_any,
semantic_brief=semantic_brief,
path_target_profile=path_target_profile,
path_intent=path_intent,
)
if step_index == 0:
first_intent_summary = query_intent_summary
hit = _pick_best_path_hit(hits, used)
hit = _pick_best_path_hit(hits, used, semantic_brief=semantic_brief)
if not hit:
break
@ -349,6 +428,8 @@ def suggest_progression_path(
exercise_kind_any=body.exercise_kind_any,
semantic_brief=semantic_brief,
planned_ids=planned_ids,
path_target_profile=path_target_profile,
path_intent=path_intent,
)
steps, bridge_inserts, unfilled_gaps = insert_bridge_exercises(
cur,
@ -378,7 +459,13 @@ def suggest_progression_path(
)
if body.include_path_reorder and llm_qa_applied and llm_qa:
steps, reorder_applied, reorder_notes = apply_llm_path_reorder(steps, llm_qa)
q_score = llm_qa.get("quality_score")
try:
q_val = float(q_score) if q_score is not None else None
except (TypeError, ValueError):
q_val = None
if llm_qa.get("overall_ok") or (q_val is not None and q_val >= 0.45):
steps, reorder_applied, reorder_notes = apply_llm_path_reorder(steps, llm_qa)
path_qa = build_path_qa_summary(
gaps=gaps,
@ -390,7 +477,7 @@ def suggest_progression_path(
reorder_notes=reorder_notes,
)
target_profile_summary = target_profile.to_summary_dict(cur) if target_profile else None
target_profile_summary = path_target_profile.to_summary_dict(cur)
retrieval_parts = ["profile_v1", "full_library", "path_builder", "semantics"]
if body.include_path_qa:
retrieval_parts.append("path_qa")

View File

@ -14,7 +14,11 @@ from planning_exercise_profiles import (
load_exercise_match_profiles_bulk,
score_exercise_against_target,
)
from planning_exercise_semantics import PlanningSemanticBrief, score_exercise_semantic_relevance
from planning_exercise_semantics import (
PlanningSemanticBrief,
exercise_passes_path_semantic_gate,
score_exercise_semantic_relevance,
)
_MAX_LIBRARY_ROWS = 8000
_PROFILE_LOAD_BATCH = 400
@ -195,6 +199,7 @@ def rank_visible_library_hits(
if isinstance(semantic_brief_raw, PlanningSemanticBrief):
semantic_brief = semantic_brief_raw
step_phase = pack.get("path_step_phase")
path_mode = pack.get("context_mode") == "progression_path"
last_planned_skills: Set[int] = set()
planned_ids = pack.get("planned_exercise_ids") or []
@ -274,6 +279,18 @@ def rank_visible_library_hits(
step_phase=step_phase,
)
if (
path_mode
and semantic_brief
and semantic_brief.semantic_strength >= 0.55
and not exercise_passes_path_semantic_gate(
semantic_score=semantic_score,
title=str(row.get("title") or ""),
brief=semantic_brief,
)
):
continue
score = (
weights.get("semantic", 0.0) * semantic_score
+ weights["fulltext"] * ft_norm

View File

@ -39,6 +39,17 @@ _OTHER_TECHNIQUE_PATTERNS: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
("gedan barai", ("age uke", "soto uke")),
)
_TECHNIQUE_EXPECTED_SKILLS: Dict[str, Tuple[str, ...]] = {
"mae geri": ("Geri Waza", "Koordination", "Gleichgewicht", "Kime"),
"mawashi geri": ("Geri Waza", "Koordination", "Gleichgewicht"),
"yoko geri": ("Geri Waza", "Koordination", "Gleichgewicht"),
"ushiro geri": ("Geri Waza", "Koordination", "Gleichgewicht"),
"sakuto geri": ("Geri Waza", "Koordination", "Gleichgewicht"),
"mikazuki geri": ("Geri Waza", "Koordination", "Gleichgewicht"),
}
_DEFAULT_TECHNIQUE_SKILLS: Tuple[str, ...] = ("Geri Waza", "Koordination", "Gleichgewicht")
_ARC_PHASES: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
("einstieg", ("einstieg", "erlernen", "lernen", "anfänger", "anfaenger", "beginn", "grund")),
("grundlage", ("grundlage", "fundament", "basis", "basic")),
@ -223,19 +234,17 @@ def build_semantic_brief(query: Optional[str]) -> PlanningSemanticBrief:
if arc:
strength = max(strength, 0.55 if technique else 0.45)
extra_phrases = _keyword_phrases_from_query(q)
for ph in extra_phrases:
if ph not in must and not any(ph in m or m in ph for m in must):
if len(ph) >= 5:
must.append(ph)
# Keine generischen Stichwörter in must_phrases — sonst verwässert das Scoring.
retrieval_parts = list(must)
if primary:
retrieval_parts.append(primary)
if arc:
retrieval_parts.extend(arc[:2])
retrieval = " ".join(dict.fromkeys(retrieval_parts))[:500] if retrieval_parts else q
if len(q) >= 24 and not technique:
strength = max(strength, 0.4)
retrieval = " ".join(must[:4]) if must else q
if arc and primary:
retrieval = f"{primary} {' '.join(arc[:2])}"
return PlanningSemanticBrief(
primary_topic=primary,
topic_type=topic_type,
@ -279,7 +288,8 @@ def merge_semantic_brief_llm(
pass
if data.get("must_phrases"):
data["retrieval_query"] = " ".join(data["must_phrases"][:4])[:500]
core = semantic_core_phrases(PlanningSemanticBrief.model_validate(data))
data["retrieval_query"] = " ".join(core[:4])[:500] if core else data.get("retrieval_query", "")
out = PlanningSemanticBrief.model_validate(data)
if out.primary_topic and out.topic_type == "general":
out = out.model_copy(update={"topic_type": "technique"})
@ -351,16 +361,17 @@ def step_retrieval_query(
) -> str:
phase = step_phase_for_index(brief, step_index, max_steps)
parts: List[str] = []
if brief.retrieval_query:
parts.append(brief.retrieval_query)
elif goal_query:
parts.append(goal_query)
if brief.primary_topic and brief.primary_topic not in " ".join(parts).lower():
if brief.primary_topic:
parts.append(brief.primary_topic)
elif brief.retrieval_query:
parts.append(brief.retrieval_query.split()[0] if brief.retrieval_query else "")
if phase:
hint = _PHASE_QUERY_HINTS.get(phase, phase)
parts.append(hint)
return _normalize_query(" ".join(parts)) or _normalize_query(goal_query)
parts.append(phase)
if not parts and brief.retrieval_query:
parts.append(brief.retrieval_query)
elif not parts and goal_query:
parts.append(goal_query)
return _normalize_query(" ".join(p for p in parts if p)) or _normalize_query(goal_query)
def apply_dynamic_retrieval_weights(
@ -440,30 +451,37 @@ def score_exercise_semantic_relevance(
reasons: List[str] = []
must = list(brief.must_phrases or [])
exclude = list(brief.exclude_phrases or [])
core = semantic_core_phrases(brief)
core_hits = sum(1 for ph in core if _phrase_in_blob(ph, blob))
must_hits = sum(1 for ph in must if _phrase_in_blob(ph, blob))
exclude_hits = sum(1 for ph in exclude if _phrase_in_blob(ph, blob))
score = 0.0
if must:
must_ratio = must_hits / len(must)
score += 0.55 * must_ratio
if must_hits == len(must):
reasons.append("Alle Kernbegriffe der Anfrage im Übungstext")
elif must_hits > 0:
reasons.append("Teilweise passende Kernbegriffe")
elif brief.primary_topic and _phrase_in_blob(brief.primary_topic, blob):
score += 0.45
reasons.append(f"Thema „{brief.primary_topic}“ im Übungstext")
if core:
core_ratio = core_hits / len(core)
score += 0.62 * core_ratio
if core_hits == len(core):
reasons.append("Kern-Thema der Anfrage im Übungstext")
elif core_hits > 0:
reasons.append("Teilweise passend zum Kern-Thema")
elif brief.primary_topic and _phrase_in_blob(brief.primary_topic, blob):
score += 0.5
score += 0.55
reasons.append(f"Thema „{brief.primary_topic}“ im Übungstext")
if exclude_hits > 0:
penalty = min(0.55, 0.18 * exclude_hits)
if must_hits == 0 or exclude_hits >= must_hits:
score -= penalty
reasons.append("Enthält ausgeschlossene Nebenthemen")
if must and core != must:
extra_ratio = must_hits / len(must)
score += 0.12 * extra_ratio
primary_ok = bool(core_hits) or (
brief.primary_topic and _phrase_in_blob(brief.primary_topic, blob)
)
if exclude_hits > 0 and not primary_ok:
penalty = min(0.65, 0.22 * exclude_hits)
score -= penalty
reasons.append("Enthält ausgeschlossene Nebenthemen")
elif exclude_hits > 0 and primary_ok:
score -= min(0.12, 0.06 * exclude_hits)
if step_phase and step_phase in _PHASE_QUERY_HINTS:
phase_markers = next((markers for phase, markers in _ARC_PHASES if phase == step_phase), ())
@ -479,13 +497,134 @@ def score_exercise_semantic_relevance(
return max(0.0, min(1.0, round(score, 4))), reasons[:4]
def semantic_core_phrases(brief: PlanningSemanticBrief) -> List[str]:
"""Harte Kernphrasen fürs Matching."""
if brief.primary_topic:
return [_normalize_phrase(brief.primary_topic)]
core = [_normalize_phrase(p) for p in (brief.must_phrases or [])[:2] if p]
return [p for p in core if p]
def resolve_semantic_skill_weights(cur, brief: PlanningSemanticBrief) -> Dict[int, float]:
"""Deterministisches Fähigkeitserwartungsprofil aus Technik-Thema."""
topic = _normalize_phrase(brief.primary_topic or "")
if topic in _TECHNIQUE_EXPECTED_SKILLS:
names = list(_TECHNIQUE_EXPECTED_SKILLS[topic])
elif brief.topic_type == "technique" or "geri" in topic:
names = list(_DEFAULT_TECHNIQUE_SKILLS)
else:
return {}
weights: Dict[int, float] = {}
for name in names[:6]:
cur.execute(
"""
SELECT id, name FROM skills
WHERE (status IS NULL OR status = 'active')
AND LOWER(name) LIKE %s
ORDER BY CASE WHEN LOWER(name) = %s THEN 0 WHEN LOWER(name) LIKE %s THEN 1 ELSE 2 END,
LENGTH(name) ASC
LIMIT 1
""",
(f"%{name.lower()}%", name.lower(), f"{name.lower()}%"),
)
row = cur.fetchone()
if row:
sid = int(row["id"])
weights[sid] = max(weights.get(sid, 0.0), 1.0)
return weights
def enrich_target_with_semantic_expectations(
target,
*,
skill_weights: Dict[int, float],
):
from planning_exercise_profiles import PlanningTargetProfile, _merge_weight_maps, _normalize_weight_map
if not skill_weights:
return target
merged = _normalize_weight_map(_merge_weight_maps(dict(target.skill_weights), skill_weights, scale=1.0))
sources = list(target.sources)
if "semantic_expectation" not in sources:
sources.append("semantic_expectation")
return PlanningTargetProfile(
focus_area_ids=dict(target.focus_area_ids),
style_direction_ids=dict(target.style_direction_ids),
training_type_ids=dict(target.training_type_ids),
target_group_ids=dict(target.target_group_ids),
skill_weights=merged,
skill_gap_weights=dict(target.skill_gap_weights),
skill_plan_weights=dict(target.skill_plan_weights),
sources=sources,
)
def apply_path_retrieval_weights(brief: PlanningSemanticBrief) -> Dict[str, float]:
"""Pfad-Builder: Semantik + Profil dominieren."""
sem = float(brief.semantic_strength or 0.0)
if sem >= 0.65:
return {
"semantic": 0.50,
"fulltext": 0.16,
"profile": 0.26,
"progression": 0.04,
"skill": 0.04,
"plan": 0.0,
"repeat_unit": -0.40,
"repeat_group": -0.15,
}
if sem >= 0.35:
return {
"semantic": 0.38,
"fulltext": 0.18,
"profile": 0.28,
"progression": 0.06,
"skill": 0.06,
"plan": 0.04,
"repeat_unit": -0.35,
"repeat_group": -0.15,
}
return {
"semantic": 0.22,
"fulltext": 0.22,
"profile": 0.28,
"progression": 0.10,
"skill": 0.10,
"plan": 0.08,
"repeat_unit": -0.30,
"repeat_group": -0.15,
}
def exercise_passes_path_semantic_gate(
*,
semantic_score: float,
title: str,
brief: PlanningSemanticBrief,
) -> bool:
if brief.semantic_strength < 0.55:
return True
if semantic_score >= 0.20:
return True
topic = brief.primary_topic or ""
if topic and _phrase_in_blob(topic, (title or "").lower()):
return True
return False
__all__ = [
"PlanningSemanticBrief",
"apply_dynamic_retrieval_weights",
"apply_path_retrieval_weights",
"brief_to_summary_dict",
"build_semantic_brief",
"enrich_target_with_semantic_expectations",
"exercise_passes_path_semantic_gate",
"merge_semantic_brief_llm",
"resolve_semantic_skill_weights",
"score_exercise_semantic_relevance",
"semantic_core_phrases",
"step_phase_for_index",
"step_retrieval_query",
"try_enrich_semantic_brief_with_llm",

View File

@ -1,17 +1,25 @@
"""Tests Planungs-KI Phase E — Pfad-QA."""
from planning_exercise_path_builder import _pick_best_path_hit
from planning_exercise_semantics import build_semantic_brief
from planning_exercise_path_qa import apply_llm_path_reorder
def test_pick_best_path_hit_prefers_semantic_score():
brief = build_semantic_brief("Mae Geri Perfektion")
hits = [
{"id": 1, "title": "Mawashi", "score": 0.9, "semantic_score": 0.1},
{"id": 2, "title": "Mae Geri", "score": 0.75, "semantic_score": 0.85},
]
chosen = _pick_best_path_hit(hits, set())
chosen = _pick_best_path_hit(hits, set(), semantic_brief=brief)
assert chosen["id"] == 2
def test_pick_best_path_hit_skips_off_topic_when_gate():
brief = build_semantic_brief("Mae Geri")
hits = [{"id": 1, "title": "Kumite Grundstellung", "score": 0.9, "semantic_score": 0.05}]
assert _pick_best_path_hit(hits, set(), semantic_brief=brief) is None
def test_pick_best_path_hit_skips_used():
hits = [{"id": 1, "title": "A", "score": 0.5, "semantic_score": 0.5}]
assert _pick_best_path_hit(hits, {1}) is None

View File

@ -12,10 +12,10 @@ def test_build_semantic_brief_mae_geri():
"Von Erlernen bis zur Perfektion, des Fußtritts Mae Geri"
)
assert brief.primary_topic == "mae geri"
assert "mae geri" in brief.must_phrases
assert brief.must_phrases == ["mae geri"]
assert "mawashi geri" in brief.exclude_phrases
assert "perfektion" not in brief.must_phrases
assert brief.semantic_strength >= 0.8
assert "einstieg" in brief.development_arc or "perfektion" in brief.development_arc
def test_semantic_score_prefers_mae_over_mawashi():
@ -64,4 +64,5 @@ def test_step_retrieval_query_carries_topic_and_phase():
q0 = step_retrieval_query(brief, brief.retrieval_query, 0, 5)
q4 = step_retrieval_query(brief, brief.retrieval_query, 4, 5)
assert "mae geri" in q0.lower()
assert q0 != q4
assert "mae geri" in q4.lower()
assert "einstieg grundübung" not in q0.lower()

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.187"
APP_VERSION = "0.8.188"
BUILD_DATE = "2026-05-23"
DB_SCHEMA_VERSION = "20260531074"
@ -29,7 +29,7 @@ MODULE_VERSIONS = {
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
"methods": "0.1.0",
"exercises": "2.37.0", # Planungs-KI P1: Szenario-Pipeline + Query-Intent-Overlay
"planning_exercise_suggest": "0.15.0", # Phase E2: Pfad-Neuordnung + KI-Lückenfüller
"planning_exercise_suggest": "0.15.1", # Pfad: fixes Semantik-Gate + Skill-Erwartungsprofil
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
"training_programs": "0.1.0",
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
@ -44,6 +44,14 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
{
"version": "0.8.188",
"date": "2026-05-23",
"changes": [
"Fix Pfad-Builder: Mae-Geri-Thema — kein Skill-Profil ab Schritt 2, verwässerte must_phrases.",
"Pfad-Retrieval: hartes Semantik-Gate, Geri-Waza/Koordination/Gleichgewicht-Erwartungsprofil, QS-Reorder nur bei ok.",
],
},
{
"version": "0.8.187",
"date": "2026-05-23",