Enhance Stage Mismatch Handling and Roadmap Slot Purging
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced `_purge_stage_mismatch_roadmap_slots` to clear slots with persistent stage mismatches, improving the relevance of exercise suggestions. - Updated `collect_gap_fill_specs` to handle stage mismatch issues more effectively, providing clearer rationale and title hints for off-topic exercises. - Modified `_filter_learning_goal_candidate_ids` to enforce stricter filtering criteria, ensuring only relevant candidates are considered. - Enhanced `rematch_roadmap_slots` to incorporate slot assignment history, preventing conflicts with previously assigned exercises. - Bumped version to 0.8.230 to reflect the new features and improvements.
This commit is contained in:
parent
8a4be795f4
commit
d448c3191f
|
|
@ -425,9 +425,22 @@ def collect_gap_fill_specs(
|
||||||
step_a, step_b = _step_neighbors_at_index(steps, idx)
|
step_a, step_b = _step_neighbors_at_index(steps, idx)
|
||||||
phase = ot.get("expected_phase") or "vertiefung"
|
phase = ot.get("expected_phase") or "vertiefung"
|
||||||
insert_after = max(idx - 1, -1)
|
insert_after = max(idx - 1, -1)
|
||||||
|
stage_goal = str(ot.get("roadmap_learning_goal") or "").strip()
|
||||||
|
if str(ot.get("issue") or "") == "stage_mismatch" and stage_goal:
|
||||||
|
title_hint = stage_goal[:120]
|
||||||
|
rationale = (
|
||||||
|
f"Keine passende Bibliotheks-Übung für Stufen-Lernziel „{stage_goal[:100]}“."
|
||||||
|
)
|
||||||
|
sketch_rationale = (
|
||||||
|
f"Slot braucht Übung passend zu: {stage_goal[:200]}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
title_hint = f"{topic} — {phase} (Ersatz für themenfremden Schritt)"
|
||||||
|
rationale = f"Schritt „{ot.get('title')}“ passt nicht zum Pfad-Thema."
|
||||||
|
sketch_rationale = f"Ersetzt themenfremden Schritt „{ot.get('title')}“."
|
||||||
add(
|
add(
|
||||||
{
|
{
|
||||||
"source": "off_topic",
|
"source": "off_topic" if ot.get("issue") != "stage_mismatch" else "stage_mismatch",
|
||||||
"insert_after_index": insert_after,
|
"insert_after_index": insert_after,
|
||||||
"replace_step_index": idx,
|
"replace_step_index": idx,
|
||||||
"roadmap_major_step_index": major_idx,
|
"roadmap_major_step_index": major_idx,
|
||||||
|
|
@ -435,18 +448,19 @@ def collect_gap_fill_specs(
|
||||||
"expected_phase": phase,
|
"expected_phase": phase,
|
||||||
"off_topic_title": ot.get("title"),
|
"off_topic_title": ot.get("title"),
|
||||||
"off_topic_exercise_id": ot.get("exercise_id"),
|
"off_topic_exercise_id": ot.get("exercise_id"),
|
||||||
|
"roadmap_learning_goal": stage_goal or None,
|
||||||
},
|
},
|
||||||
"phase": phase,
|
"phase": phase,
|
||||||
"title_hint": f"{topic} — {phase} (Ersatz für themenfremden Schritt)",
|
"title_hint": title_hint,
|
||||||
"sketch": _default_sketch(
|
"sketch": _default_sketch(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
step_a=step_a,
|
step_a=step_a,
|
||||||
step_b=step_b,
|
step_b=step_b,
|
||||||
phase=str(phase),
|
phase=str(phase),
|
||||||
rationale=f"Ersetzt themenfremden Schritt „{ot.get('title')}“.",
|
rationale=sketch_rationale,
|
||||||
),
|
),
|
||||||
"rationale": f"Schritt „{ot.get('title')}“ passt nicht zum Pfad-Thema.",
|
"rationale": rationale,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ def _filter_learning_goal_candidate_ids(
|
||||||
anti_patterns=stage_anti,
|
anti_patterns=stage_anti,
|
||||||
path_primary_topic=path_primary or None,
|
path_primary_topic=path_primary or None,
|
||||||
path_technique_excludes=path_tech_excludes,
|
path_technique_excludes=path_tech_excludes,
|
||||||
relaxed=True,
|
relaxed=False,
|
||||||
):
|
):
|
||||||
out.append(eid)
|
out.append(eid)
|
||||||
return out
|
return out
|
||||||
|
|
@ -1322,6 +1322,78 @@ def _normalize_roadmap_steps_coverage(
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _purge_stage_mismatch_roadmap_slots(
|
||||||
|
cur,
|
||||||
|
*,
|
||||||
|
steps: List[Dict[str, Any]],
|
||||||
|
roadmap_ctx: ProgressionRoadmapContext,
|
||||||
|
goal_query: str,
|
||||||
|
semantic_brief: PlanningSemanticBrief,
|
||||||
|
) -> Tuple[List[Dict[str, Any]], List[Tuple[int, StageSpecArtifact]]]:
|
||||||
|
"""Leert Slots mit persistentem stage_mismatch — KI-Gap statt schlechter Bibliotheks-Übung."""
|
||||||
|
issues = detect_off_topic_steps(
|
||||||
|
cur,
|
||||||
|
steps,
|
||||||
|
brief=semantic_brief,
|
||||||
|
goal_query=goal_query,
|
||||||
|
)
|
||||||
|
purge_majors: Set[int] = set()
|
||||||
|
for item in issues:
|
||||||
|
if str(item.get("issue") or "") != "stage_mismatch":
|
||||||
|
continue
|
||||||
|
midx = item.get("roadmap_major_step_index")
|
||||||
|
if midx is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
purge_majors.add(int(midx))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if not purge_majors:
|
||||||
|
return steps, []
|
||||||
|
|
||||||
|
stage_specs = list(roadmap_ctx.stage_specs or [])
|
||||||
|
spec_by_major = {int(s.major_step_index): s for s in stage_specs}
|
||||||
|
major_by_index: Dict[int, MajorStep] = {}
|
||||||
|
if roadmap_ctx.roadmap:
|
||||||
|
major_by_index = {m.index: m for m in roadmap_ctx.roadmap.major_steps}
|
||||||
|
|
||||||
|
new_unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
||||||
|
out: List[Dict[str, Any]] = []
|
||||||
|
for raw in steps:
|
||||||
|
step = dict(raw)
|
||||||
|
midx = step.get("roadmap_major_step_index")
|
||||||
|
if midx is None or int(midx) not in purge_majors:
|
||||||
|
out.append(step)
|
||||||
|
continue
|
||||||
|
major_idx = int(midx)
|
||||||
|
spec = spec_by_major.get(major_idx)
|
||||||
|
if spec is None:
|
||||||
|
out.append(step)
|
||||||
|
continue
|
||||||
|
step_index = next(
|
||||||
|
(i for i, sp in enumerate(stage_specs) if int(sp.major_step_index) == major_idx),
|
||||||
|
major_idx,
|
||||||
|
)
|
||||||
|
major = major_by_index.get(major_idx)
|
||||||
|
goal = (spec.learning_goal or step.get("roadmap_learning_goal") or "").strip()
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"exercise_id": None,
|
||||||
|
"variant_id": None,
|
||||||
|
"title": goal or f"Slot {major_idx + 1}",
|
||||||
|
"is_ai_proposal": False,
|
||||||
|
"roadmap_major_step_index": major_idx,
|
||||||
|
"roadmap_phase": major.phase if major else step.get("roadmap_phase"),
|
||||||
|
"roadmap_learning_goal": goal or None,
|
||||||
|
"roadmap_match_source": "unfilled",
|
||||||
|
"slot_status": "unfilled",
|
||||||
|
"reasons": ["Keine passende Bibliotheks-Übung für Stufen-Lernziel"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
new_unfilled.append((step_index, spec))
|
||||||
|
return out, new_unfilled
|
||||||
|
|
||||||
|
|
||||||
def _merge_rematch_unfilled(
|
def _merge_rematch_unfilled(
|
||||||
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]],
|
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]],
|
||||||
rematch_new_unfilled: List[Tuple[int, StageSpecArtifact]],
|
rematch_new_unfilled: List[Tuple[int, StageSpecArtifact]],
|
||||||
|
|
@ -1401,6 +1473,16 @@ def _run_roadmap_rematch_loop(
|
||||||
|
|
||||||
_track_rejected(off_topic_before_strip)
|
_track_rejected(off_topic_before_strip)
|
||||||
_track_rejected(current_stripped)
|
_track_rejected(current_stripped)
|
||||||
|
slot_assignment_history: Dict[int, Set[int]] = {}
|
||||||
|
for raw in steps:
|
||||||
|
midx = raw.get("roadmap_major_step_index")
|
||||||
|
eid = raw.get("exercise_id")
|
||||||
|
if midx is None or eid is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
slot_assignment_history.setdefault(int(midx), set()).add(int(eid))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
for round_idx in range(max_rounds):
|
for round_idx in range(max_rounds):
|
||||||
mini_qa = run_multistage_path_qa(
|
mini_qa = run_multistage_path_qa(
|
||||||
|
|
@ -1462,6 +1544,7 @@ def _run_roadmap_rematch_loop(
|
||||||
rematch_reasons=rematch_reasons,
|
rematch_reasons=rematch_reasons,
|
||||||
match_slot_fn=_match_roadmap_slot,
|
match_slot_fn=_match_roadmap_slot,
|
||||||
rejected_by_major=rejected_by_major,
|
rejected_by_major=rejected_by_major,
|
||||||
|
slot_assignment_history=slot_assignment_history,
|
||||||
)
|
)
|
||||||
rematch_rounds += 1
|
rematch_rounds += 1
|
||||||
for entry in round_log:
|
for entry in round_log:
|
||||||
|
|
@ -1475,6 +1558,16 @@ def _run_roadmap_rematch_loop(
|
||||||
rejected_by_major.setdefault(int(midx), set()).add(int(rid))
|
rejected_by_major.setdefault(int(midx), set()).add(int(rid))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
new_eid = entry.get("new_exercise_id")
|
||||||
|
if (
|
||||||
|
str(entry.get("action") or "") == "replaced"
|
||||||
|
and new_eid is not None
|
||||||
|
and midx is not None
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
slot_assignment_history.setdefault(int(midx), set()).add(int(new_eid))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
current_stripped = prune_stripped_after_rematch(current_stripped, round_log)
|
current_stripped = prune_stripped_after_rematch(current_stripped, round_log)
|
||||||
roadmap_unfilled = _merge_rematch_unfilled(roadmap_unfilled, rematch_new_unfilled)
|
roadmap_unfilled = _merge_rematch_unfilled(roadmap_unfilled, rematch_new_unfilled)
|
||||||
|
|
@ -1500,6 +1593,22 @@ def _run_roadmap_rematch_loop(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
steps, purged_unfilled = _purge_stage_mismatch_roadmap_slots(
|
||||||
|
cur,
|
||||||
|
steps=steps,
|
||||||
|
roadmap_ctx=roadmap_ctx,
|
||||||
|
goal_query=goal_query,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
)
|
||||||
|
if purged_unfilled:
|
||||||
|
roadmap_unfilled = _merge_rematch_unfilled(roadmap_unfilled, purged_unfilled)
|
||||||
|
off_topic_steps = detect_off_topic_steps(
|
||||||
|
cur,
|
||||||
|
steps,
|
||||||
|
brief=semantic_brief,
|
||||||
|
goal_query=goal_query,
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
steps,
|
steps,
|
||||||
rematch_log,
|
rematch_log,
|
||||||
|
|
|
||||||
|
|
@ -865,6 +865,11 @@ def stage_focus_phrases_from_learning_goal(learning_goal: str) -> List[str]:
|
||||||
tokens = _significant_stage_tokens(lg, strip_negated=True)
|
tokens = _significant_stage_tokens(lg, strip_negated=True)
|
||||||
phrases: List[str] = []
|
phrases: List[str] = []
|
||||||
norm_lg = _normalize_phrase(lg)
|
norm_lg = _normalize_phrase(lg)
|
||||||
|
tech_hit = _find_technique_in_text(norm_lg)
|
||||||
|
if tech_hit:
|
||||||
|
primary = tech_hit[0]
|
||||||
|
if primary not in phrases:
|
||||||
|
phrases.append(primary)
|
||||||
if len(norm_lg) >= 8:
|
if len(norm_lg) >= 8:
|
||||||
phrases.append(norm_lg[:120])
|
phrases.append(norm_lg[:120])
|
||||||
for i in range(len(tokens) - 1):
|
for i in range(len(tokens) - 1):
|
||||||
|
|
@ -879,14 +884,22 @@ def stage_focus_phrases_from_learning_goal(learning_goal: str) -> List[str]:
|
||||||
|
|
||||||
def stage_refinement_criteria_from_learning_goal(learning_goal: str) -> List[str]:
|
def stage_refinement_criteria_from_learning_goal(learning_goal: str) -> List[str]:
|
||||||
"""Erfolgskriterien für Phase C — nur aussagekräftige Mehrwort-Phrasen."""
|
"""Erfolgskriterien für Phase C — nur aussagekräftige Mehrwort-Phrasen."""
|
||||||
|
lg = (learning_goal or "").strip()
|
||||||
|
if len(lg) < 3:
|
||||||
|
return []
|
||||||
|
norm_lg = _normalize_phrase(lg)
|
||||||
out: List[str] = []
|
out: List[str] = []
|
||||||
for phrase in stage_focus_phrases_from_learning_goal(learning_goal):
|
if len(norm_lg) >= 15:
|
||||||
p = str(phrase or "").strip()
|
out.append(norm_lg[:120])
|
||||||
if not p:
|
tokens = _significant_stage_tokens(lg, strip_negated=True)
|
||||||
|
for i in range(len(tokens) - 1):
|
||||||
|
a, b = tokens[i], tokens[i + 1]
|
||||||
|
if len(a) < 5 or len(b) < 5:
|
||||||
continue
|
continue
|
||||||
if " " in p or len(p) >= 12:
|
pair = f"{a} {b}"
|
||||||
out.append(p[:120])
|
if len(pair) >= 12 and pair not in out:
|
||||||
return out[:4]
|
out.append(pair)
|
||||||
|
return out[:3]
|
||||||
|
|
||||||
|
|
||||||
def exercise_title_matches_peer_stage_goal(
|
def exercise_title_matches_peer_stage_goal(
|
||||||
|
|
@ -1095,6 +1108,9 @@ def build_stage_match_brief(
|
||||||
constraints = parse_stage_goal_constraints(lg)
|
constraints = parse_stage_goal_constraints(lg)
|
||||||
must: List[str] = []
|
must: List[str] = []
|
||||||
norm_lg = _normalize_phrase(lg)
|
norm_lg = _normalize_phrase(lg)
|
||||||
|
tech_hit = _find_technique_in_text(norm_lg)
|
||||||
|
if tech_hit and tech_hit[0] not in must:
|
||||||
|
must.insert(0, tech_hit[0])
|
||||||
if primary_path and primary_path not in must:
|
if primary_path and primary_path not in must:
|
||||||
must.insert(0, primary_path[:120])
|
must.insert(0, primary_path[:120])
|
||||||
for token in constraints.positive_tokens:
|
for token in constraints.positive_tokens:
|
||||||
|
|
@ -1165,12 +1181,15 @@ def score_exercise_stage_fit(
|
||||||
if part.lower().startswith("lernziel:"):
|
if part.lower().startswith("lernziel:"):
|
||||||
lg_hint = part.split(":", 1)[-1].strip()
|
lg_hint = part.split(":", 1)[-1].strip()
|
||||||
break
|
break
|
||||||
|
if not lg_hint:
|
||||||
|
lg_hint = (stage_brief.retrieval_query or "").split("|")[0].strip()
|
||||||
if not lg_hint:
|
if not lg_hint:
|
||||||
for mp in stage_brief.must_phrases or []:
|
for mp in stage_brief.must_phrases or []:
|
||||||
if mp and len(_normalize_phrase(mp)) >= 8:
|
if mp and len(_normalize_phrase(mp)) >= 8:
|
||||||
lg_hint = mp
|
lg_hint = mp
|
||||||
break
|
break
|
||||||
focus_phrases = stage_focus_phrases_from_learning_goal(lg_hint) if lg_hint else []
|
focus_phrases = stage_focus_phrases_from_learning_goal(lg_hint) if lg_hint else []
|
||||||
|
tech_hit = _find_technique_in_text(_normalize_phrase(lg_hint)) if lg_hint else None
|
||||||
if not focus_phrases:
|
if not focus_phrases:
|
||||||
focus_phrases = [
|
focus_phrases = [
|
||||||
t
|
t
|
||||||
|
|
@ -1185,6 +1204,16 @@ def score_exercise_stage_fit(
|
||||||
score = min(1.0, score + bonus)
|
score = min(1.0, score + bonus)
|
||||||
if hits >= max(1, len(focus_phrases) // 2):
|
if hits >= max(1, len(focus_phrases) // 2):
|
||||||
reasons = ["Stufen-Schwerpunkte im Übungstext", *reasons]
|
reasons = ["Stufen-Schwerpunkte im Übungstext", *reasons]
|
||||||
|
non_tech = [
|
||||||
|
p
|
||||||
|
for p in focus_phrases
|
||||||
|
if not tech_hit or _normalize_phrase(p) != tech_hit[0]
|
||||||
|
]
|
||||||
|
specific_hits = sum(1 for p in non_tech if _phrase_in_blob(p, blob))
|
||||||
|
if tech_hit and _phrase_in_blob(tech_hit[0], blob) and specific_hits == 0:
|
||||||
|
score = min(score, 0.16)
|
||||||
|
if "Nur Technik-Bezug" not in reasons:
|
||||||
|
reasons = ["Nur Technik-Bezug, Stufen-Schwerpunkte fehlen", *reasons]
|
||||||
learning_goal_for_equiv = lg_hint or (stage_brief.must_phrases[0] if stage_brief.must_phrases else "")
|
learning_goal_for_equiv = lg_hint or (stage_brief.must_phrases[0] if stage_brief.must_phrases else "")
|
||||||
if learning_goal_for_equiv and exercise_title_equivalent_to_stage_goal(title, learning_goal_for_equiv):
|
if learning_goal_for_equiv and exercise_title_equivalent_to_stage_goal(title, learning_goal_for_equiv):
|
||||||
score = max(score, 0.42)
|
score = max(score, 0.42)
|
||||||
|
|
@ -1246,8 +1275,6 @@ def exercise_passes_stage_fit(
|
||||||
learning_goal=lg,
|
learning_goal=lg,
|
||||||
anti_patterns=anti_patterns,
|
anti_patterns=anti_patterns,
|
||||||
)
|
)
|
||||||
stage_sem = stage_semantic_score
|
|
||||||
if stage_sem is None:
|
|
||||||
stage_sem, _ = score_exercise_stage_fit(
|
stage_sem, _ = score_exercise_stage_fit(
|
||||||
title=title,
|
title=title,
|
||||||
summary=summary,
|
summary=summary,
|
||||||
|
|
@ -1262,7 +1289,19 @@ def exercise_passes_stage_fit(
|
||||||
threshold = _MIN_TITLE_EQUIV_SEMANTIC
|
threshold = _MIN_TITLE_EQUIV_SEMANTIC
|
||||||
else:
|
else:
|
||||||
threshold = min_stage_semantic
|
threshold = min_stage_semantic
|
||||||
return float(stage_sem or 0.0) >= threshold
|
|
||||||
|
if float(stage_sem or 0.0) >= threshold:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if relaxed and not title_equiv:
|
||||||
|
focus = stage_focus_phrases_from_learning_goal(lg)
|
||||||
|
tech = _find_technique_in_text(_normalize_phrase(lg))
|
||||||
|
non_tech = [p for p in focus if not tech or _normalize_phrase(p) != tech[0]]
|
||||||
|
specific_hits = sum(1 for p in non_tech if _phrase_in_blob(p, blob))
|
||||||
|
if specific_hits >= 2 and float(stage_sem or 0.0) >= 0.14:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def apply_stage_match_retrieval_weights(brief: PlanningSemanticBrief) -> Dict[str, float]:
|
def apply_stage_match_retrieval_weights(brief: PlanningSemanticBrief) -> Dict[str, float]:
|
||||||
|
|
@ -1539,16 +1578,7 @@ def pick_best_path_hit(
|
||||||
chosen = _scan(strict=False)
|
chosen = _scan(strict=False)
|
||||||
if chosen:
|
if chosen:
|
||||||
return chosen
|
return chosen
|
||||||
return _pick_roadmap_rank_fallback(
|
return None
|
||||||
hits,
|
|
||||||
used_exercise_ids,
|
|
||||||
stage_learning_goal=stage_goal,
|
|
||||||
stage_anti_patterns=stage_anti_patterns,
|
|
||||||
path_primary_topic=path_primary_topic,
|
|
||||||
path_technique_excludes=path_technique_excludes,
|
|
||||||
stage_match_brief=stage_brief,
|
|
||||||
peer_learning_goals=peer_learning_goals,
|
|
||||||
)
|
|
||||||
|
|
||||||
chosen = _scan(strict=False)
|
chosen = _scan(strict=False)
|
||||||
if chosen:
|
if chosen:
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ def rematch_roadmap_slots(
|
||||||
rematch_reasons: Mapping[int, str],
|
rematch_reasons: Mapping[int, str],
|
||||||
match_slot_fn,
|
match_slot_fn,
|
||||||
rejected_by_major: Optional[Mapping[int, Set[int]]] = None,
|
rejected_by_major: Optional[Mapping[int, Set[int]]] = None,
|
||||||
|
slot_assignment_history: Optional[Mapping[int, Set[int]]] = None,
|
||||||
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Tuple[int, StageSpecArtifact]]]:
|
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Tuple[int, StageSpecArtifact]]]:
|
||||||
"""
|
"""
|
||||||
Ersetzt nur betroffene Slots; andere Schritte und used-Set bleiben konsistent.
|
Ersetzt nur betroffene Slots; andere Schritte und used-Set bleiben konsistent.
|
||||||
|
|
@ -180,6 +181,18 @@ def rematch_roadmap_slots(
|
||||||
)
|
)
|
||||||
|
|
||||||
reason = str(rematch_reasons.get(int(major_idx)) or "rematch_slot")
|
reason = str(rematch_reasons.get(int(major_idx)) or "rematch_slot")
|
||||||
|
if new_step:
|
||||||
|
try:
|
||||||
|
new_eid = int(new_step.get("exercise_id") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
new_eid = 0
|
||||||
|
hist = (
|
||||||
|
slot_assignment_history.get(int(major_idx), set())
|
||||||
|
if slot_assignment_history
|
||||||
|
else set()
|
||||||
|
)
|
||||||
|
if new_eid > 0 and new_eid in hist:
|
||||||
|
new_step = None
|
||||||
if new_step:
|
if new_step:
|
||||||
steps_by_major[int(major_idx)] = new_step
|
steps_by_major[int(major_idx)] = new_step
|
||||||
rematch_log.append(
|
rematch_log.append(
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,8 @@ def test_pick_roadmap_relaxed_for_non_technique_stage():
|
||||||
{
|
{
|
||||||
"id": 11,
|
"id": 11,
|
||||||
"title": "Adduktoren Dehnung am Boden",
|
"title": "Adduktoren Dehnung am Boden",
|
||||||
"summary": "Flexibilität Hüfte",
|
"summary": "Flexibilität Hüfte, Adduktoren dehnen",
|
||||||
"goal": "Mobilität",
|
"goal": "Mobilität — Adduktoren dehnen",
|
||||||
"score": 0.68,
|
"score": 0.68,
|
||||||
"semantic_score": 0.22,
|
"semantic_score": 0.22,
|
||||||
"stage_semantic_score": 0.22,
|
"stage_semantic_score": 0.22,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.229"
|
APP_VERSION = "0.8.230"
|
||||||
BUILD_DATE = "2026-05-22"
|
BUILD_DATE = "2026-05-22"
|
||||||
DB_SCHEMA_VERSION = "20260607090"
|
DB_SCHEMA_VERSION = "20260607090"
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
||||||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
||||||
"planning_exercise_suggest": "0.23.4", # Stufen-Match: Fallback mit Gate, Peer-Slot-Schutz, LG-Kandidaten-Filter
|
"planning_exercise_suggest": "0.23.5", # Roadmap-Match strikt; stage_mismatch → unfilled + KI-Gap
|
||||||
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user