Enhance Gap Fill and Rematch Logic in Progression Path
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 39s
Test Suite / playwright-tests (push) Successful in 1m22s
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 39s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced `_step_neighbors_at_index` to safely retrieve neighboring steps without causing IndexErrors, improving robustness in gap fill specifications. - Updated `collect_gap_fill_specs` to utilize the new neighbor retrieval function, ensuring safe access to adjacent steps during gap fill processing. - Enhanced rematch logic in `_run_roadmap_rematch_loop` to incorporate `max_rematch_rounds`, allowing for controlled iterations during roadmap rematching. - Improved handling of unfilled roadmap slots in `collect_rematch_slot_indices`, ensuring accurate identification of gaps in the progression path. - Added tests to validate the new gap fill handling and rematch logic, ensuring reliability in path suggestion features.
This commit is contained in:
parent
de939481ba
commit
df93da9a03
|
|
@ -337,6 +337,18 @@ def _spec_dedupe_key(spec: Mapping[str, Any]) -> Tuple[Any, ...]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _step_neighbors_at_index(
|
||||||
|
steps: Sequence[Mapping[str, Any]],
|
||||||
|
idx: int,
|
||||||
|
) -> Tuple[Optional[Mapping[str, Any]], Optional[Mapping[str, Any]]]:
|
||||||
|
"""Vorheriger/nächster Pfadschritt ohne IndexError (Rand-Slots, leere Stufen)."""
|
||||||
|
if idx < 0 or idx >= len(steps):
|
||||||
|
return None, None
|
||||||
|
step_a = steps[idx - 1] if idx > 0 else None
|
||||||
|
step_b = steps[idx + 1] if idx + 1 < len(steps) else None
|
||||||
|
return step_a, step_b
|
||||||
|
|
||||||
|
|
||||||
def collect_gap_fill_specs(
|
def collect_gap_fill_specs(
|
||||||
*,
|
*,
|
||||||
steps: Sequence[Mapping[str, Any]],
|
steps: Sequence[Mapping[str, Any]],
|
||||||
|
|
@ -364,8 +376,10 @@ def collect_gap_fill_specs(
|
||||||
int(gap["from_exercise_id"]),
|
int(gap["from_exercise_id"]),
|
||||||
int(gap["to_exercise_id"]),
|
int(gap["to_exercise_id"]),
|
||||||
)
|
)
|
||||||
if idx is None:
|
if idx is None or idx + 1 >= len(steps):
|
||||||
continue
|
continue
|
||||||
|
step_a = steps[idx]
|
||||||
|
step_b = steps[idx + 1]
|
||||||
phase = gap.get("expected_phase") or "vertiefung"
|
phase = gap.get("expected_phase") or "vertiefung"
|
||||||
add(
|
add(
|
||||||
{
|
{
|
||||||
|
|
@ -377,12 +391,12 @@ def collect_gap_fill_specs(
|
||||||
"sketch": _default_sketch(
|
"sketch": _default_sketch(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
step_a=steps[idx],
|
step_a=step_a,
|
||||||
step_b=steps[idx + 1],
|
step_b=step_b,
|
||||||
phase=str(phase),
|
phase=str(phase),
|
||||||
rationale="Bibliothek enthält keine passende Brücke.",
|
rationale="Bibliothek enthält keine passende Brücke.",
|
||||||
),
|
),
|
||||||
"rationale": "Lücke zwischen benachbarten Schritten — keine passende Bibliotheks-Übung.",
|
"rationale": "Lücke zwischen benachbaren Schritten — keine passende Bibliotheks-Übung.",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -408,6 +422,7 @@ def collect_gap_fill_specs(
|
||||||
idx = int(ot.get("step_index") or 0)
|
idx = int(ot.get("step_index") or 0)
|
||||||
if idx < 0 or idx >= len(steps):
|
if idx < 0 or idx >= len(steps):
|
||||||
continue
|
continue
|
||||||
|
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)
|
||||||
add(
|
add(
|
||||||
|
|
@ -426,8 +441,8 @@ def collect_gap_fill_specs(
|
||||||
"sketch": _default_sketch(
|
"sketch": _default_sketch(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
step_a=steps[idx - 1],
|
step_a=step_a,
|
||||||
step_b=steps[idx + 1],
|
step_b=step_b,
|
||||||
phase=str(phase),
|
phase=str(phase),
|
||||||
rationale=f"Ersetzt themenfremden Schritt „{ot.get('title')}“.",
|
rationale=f"Ersetzt themenfremden Schritt „{ot.get('title')}“.",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ class ProgressionPathSuggestRequest(BaseModel):
|
||||||
include_llm_intent: bool = True
|
include_llm_intent: bool = True
|
||||||
include_path_qa: bool = True
|
include_path_qa: bool = True
|
||||||
auto_rematch_after_qa: bool = True
|
auto_rematch_after_qa: bool = True
|
||||||
|
max_rematch_rounds: int = Field(default=2, ge=0, le=3)
|
||||||
include_llm_path_qa: bool = True
|
include_llm_path_qa: bool = True
|
||||||
include_path_reorder: bool = True
|
include_path_reorder: bool = True
|
||||||
include_ai_gap_fill: bool = True
|
include_ai_gap_fill: bool = True
|
||||||
|
|
@ -438,7 +439,17 @@ def _load_supplemental_exercise_rows(
|
||||||
vis_params: Sequence[Any],
|
vis_params: Sequence[Any],
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Supplemental-Übungen mit Graph-Sichtbarkeit, Fallback Library-vis_sql."""
|
"""Supplemental-Übungen mit Graph-Sichtbarkeit, Fallback Library-vis_sql."""
|
||||||
ids = list(dict.fromkeys(int(x) for x in (exercise_ids or []) if int(x) > 0))
|
ids: List[int] = []
|
||||||
|
for raw in exercise_ids or []:
|
||||||
|
if raw is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
eid = int(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if eid > 0:
|
||||||
|
ids.append(eid)
|
||||||
|
ids = list(dict.fromkeys(ids))
|
||||||
if not ids:
|
if not ids:
|
||||||
return []
|
return []
|
||||||
if progression_graph_id and int(progression_graph_id) > 0:
|
if progression_graph_id and int(progression_graph_id) > 0:
|
||||||
|
|
@ -1222,7 +1233,19 @@ def _normalize_roadmap_steps_coverage(
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _maybe_rematch_roadmap_after_strip(
|
def _merge_rematch_unfilled(
|
||||||
|
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]],
|
||||||
|
rematch_new_unfilled: List[Tuple[int, StageSpecArtifact]],
|
||||||
|
) -> List[Tuple[int, StageSpecArtifact]]:
|
||||||
|
if not rematch_new_unfilled:
|
||||||
|
return roadmap_unfilled
|
||||||
|
remapped = {sp.major_step_index for _, sp in rematch_new_unfilled}
|
||||||
|
kept = [item for item in roadmap_unfilled if item[1].major_step_index not in remapped]
|
||||||
|
kept.extend(rematch_new_unfilled)
|
||||||
|
return kept
|
||||||
|
|
||||||
|
|
||||||
|
def _run_roadmap_rematch_loop(
|
||||||
cur,
|
cur,
|
||||||
*,
|
*,
|
||||||
tenant: TenantContext,
|
tenant: TenantContext,
|
||||||
|
|
@ -1237,6 +1260,7 @@ def _maybe_rematch_roadmap_after_strip(
|
||||||
stripped_off_topic: List[Dict[str, Any]],
|
stripped_off_topic: List[Dict[str, Any]],
|
||||||
off_topic_before_strip: List[Dict[str, Any]],
|
off_topic_before_strip: List[Dict[str, Any]],
|
||||||
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]],
|
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]],
|
||||||
|
gaps: List[Dict[str, Any]],
|
||||||
) -> Tuple[
|
) -> Tuple[
|
||||||
List[Dict[str, Any]],
|
List[Dict[str, Any]],
|
||||||
List[Dict[str, Any]],
|
List[Dict[str, Any]],
|
||||||
|
|
@ -1245,54 +1269,92 @@ def _maybe_rematch_roadmap_after_strip(
|
||||||
int,
|
int,
|
||||||
List[Tuple[int, StageSpecArtifact]],
|
List[Tuple[int, StageSpecArtifact]],
|
||||||
]:
|
]:
|
||||||
|
"""Phase A/B: Rematch-Schleife aus Strip, unfilled Slots und optimization_hints."""
|
||||||
rematch_log: List[Dict[str, Any]] = []
|
rematch_log: List[Dict[str, Any]] = []
|
||||||
rematch_rounds = 0
|
rematch_rounds = 0
|
||||||
if not body.auto_rematch_after_qa or not roadmap_ctx.stage_specs:
|
max_rounds = int(body.max_rematch_rounds or 0)
|
||||||
return steps, rematch_log, stripped_off_topic, [], rematch_rounds, roadmap_unfilled
|
if not body.auto_rematch_after_qa or max_rounds <= 0 or not roadmap_ctx.stage_specs:
|
||||||
|
off_topic_steps = detect_off_topic_steps(
|
||||||
|
cur,
|
||||||
|
steps,
|
||||||
|
brief=semantic_brief,
|
||||||
|
goal_query=goal_query,
|
||||||
|
)
|
||||||
|
return steps, rematch_log, stripped_off_topic, off_topic_steps, rematch_rounds, roadmap_unfilled
|
||||||
|
|
||||||
slot_indices, rematch_reasons = collect_rematch_slot_indices(
|
current_stripped = list(stripped_off_topic or [])
|
||||||
stripped_off_topic=stripped_off_topic,
|
use_initial_off_topic = not current_stripped
|
||||||
off_topic_steps=off_topic_before_strip if not stripped_off_topic else [],
|
off_topic_steps: List[Dict[str, Any]] = []
|
||||||
optimization_hints=[],
|
|
||||||
stage_specs=roadmap_ctx.stage_specs,
|
|
||||||
)
|
|
||||||
if not slot_indices:
|
|
||||||
return steps, rematch_log, stripped_off_topic, [], rematch_rounds, roadmap_unfilled
|
|
||||||
|
|
||||||
steps, rematch_log, rematch_new_unfilled = rematch_roadmap_slots(
|
for round_idx in range(max_rounds):
|
||||||
cur,
|
mini_qa = run_multistage_path_qa(
|
||||||
tenant=tenant,
|
off_topic_steps=off_topic_steps if round_idx > 0 else [],
|
||||||
body=body,
|
stripped_off_topic=current_stripped if round_idx == 0 else [],
|
||||||
goal_query=goal_query,
|
gaps=gaps if round_idx == 0 else [],
|
||||||
max_steps=max_steps,
|
llm_qa=None,
|
||||||
semantic_brief=semantic_brief,
|
llm_applied=False,
|
||||||
path_target_profile=path_target_profile,
|
roadmap_unfilled=roadmap_unfilled,
|
||||||
path_intent=path_intent,
|
)
|
||||||
roadmap_ctx=roadmap_ctx,
|
optimization_hints = list(mini_qa.get("optimization_hints") or [])
|
||||||
steps=steps,
|
|
||||||
slot_indices=slot_indices,
|
slot_indices, rematch_reasons = collect_rematch_slot_indices(
|
||||||
rematch_reasons=rematch_reasons,
|
stripped_off_topic=current_stripped if round_idx == 0 else [],
|
||||||
match_slot_fn=_match_roadmap_slot,
|
off_topic_steps=off_topic_before_strip if round_idx == 0 and use_initial_off_topic else [],
|
||||||
)
|
optimization_hints=optimization_hints,
|
||||||
rematch_rounds = 1
|
stage_specs=roadmap_ctx.stage_specs,
|
||||||
stripped_off_topic = prune_stripped_after_rematch(stripped_off_topic, rematch_log)
|
roadmap_unfilled=roadmap_unfilled,
|
||||||
if rematch_new_unfilled:
|
)
|
||||||
remapped = {sp.major_step_index for _, sp in rematch_new_unfilled}
|
if not slot_indices:
|
||||||
roadmap_unfilled = [
|
break
|
||||||
item for item in roadmap_unfilled if item[1].major_step_index not in remapped
|
|
||||||
]
|
steps, round_log, rematch_new_unfilled = rematch_roadmap_slots(
|
||||||
roadmap_unfilled.extend(rematch_new_unfilled)
|
cur,
|
||||||
|
tenant=tenant,
|
||||||
|
body=body,
|
||||||
|
goal_query=goal_query,
|
||||||
|
max_steps=max_steps,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
path_target_profile=path_target_profile,
|
||||||
|
path_intent=path_intent,
|
||||||
|
roadmap_ctx=roadmap_ctx,
|
||||||
|
steps=steps,
|
||||||
|
slot_indices=slot_indices,
|
||||||
|
rematch_reasons=rematch_reasons,
|
||||||
|
match_slot_fn=_match_roadmap_slot,
|
||||||
|
)
|
||||||
|
rematch_rounds += 1
|
||||||
|
for entry in round_log:
|
||||||
|
tagged = dict(entry)
|
||||||
|
tagged["round"] = rematch_rounds
|
||||||
|
rematch_log.append(tagged)
|
||||||
|
|
||||||
|
current_stripped = prune_stripped_after_rematch(current_stripped, round_log)
|
||||||
|
roadmap_unfilled = _merge_rematch_unfilled(roadmap_unfilled, rematch_new_unfilled)
|
||||||
|
use_initial_off_topic = False
|
||||||
|
|
||||||
|
off_topic_steps = detect_off_topic_steps(
|
||||||
|
cur,
|
||||||
|
steps,
|
||||||
|
brief=semantic_brief,
|
||||||
|
goal_query=goal_query,
|
||||||
|
)
|
||||||
|
if round_idx + 1 >= max_rounds:
|
||||||
|
break
|
||||||
|
if not off_topic_steps and not roadmap_unfilled:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not off_topic_steps:
|
||||||
|
off_topic_steps = detect_off_topic_steps(
|
||||||
|
cur,
|
||||||
|
steps,
|
||||||
|
brief=semantic_brief,
|
||||||
|
goal_query=goal_query,
|
||||||
|
)
|
||||||
|
|
||||||
off_topic_steps = detect_off_topic_steps(
|
|
||||||
cur,
|
|
||||||
steps,
|
|
||||||
brief=semantic_brief,
|
|
||||||
goal_query=goal_query,
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
steps,
|
steps,
|
||||||
rematch_log,
|
rematch_log,
|
||||||
stripped_off_topic,
|
current_stripped,
|
||||||
off_topic_steps,
|
off_topic_steps,
|
||||||
rematch_rounds,
|
rematch_rounds,
|
||||||
roadmap_unfilled,
|
roadmap_unfilled,
|
||||||
|
|
@ -2000,7 +2062,7 @@ def suggest_progression_path(
|
||||||
rematch_off_topic,
|
rematch_off_topic,
|
||||||
rematch_rounds,
|
rematch_rounds,
|
||||||
roadmap_unfilled,
|
roadmap_unfilled,
|
||||||
) = _maybe_rematch_roadmap_after_strip(
|
) = _run_roadmap_rematch_loop(
|
||||||
cur,
|
cur,
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
body=body,
|
body=body,
|
||||||
|
|
@ -2014,6 +2076,7 @@ def suggest_progression_path(
|
||||||
stripped_off_topic=stripped_off_topic,
|
stripped_off_topic=stripped_off_topic,
|
||||||
off_topic_before_strip=off_topic_before_strip,
|
off_topic_before_strip=off_topic_before_strip,
|
||||||
roadmap_unfilled=roadmap_unfilled,
|
roadmap_unfilled=roadmap_unfilled,
|
||||||
|
gaps=gaps,
|
||||||
)
|
)
|
||||||
if rematch_off_topic:
|
if rematch_off_topic:
|
||||||
off_topic_steps = rematch_off_topic
|
off_topic_steps = rematch_off_topic
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Auto-Rematch nach Pfad-QS — betroffene Roadmap-Slots erneut matchen (Phase A).
|
Auto-Rematch nach Pfad-QS — betroffene Roadmap-Slots erneut matchen (Phase A/B).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -14,6 +14,7 @@ def collect_rematch_slot_indices(
|
||||||
off_topic_steps: Sequence[Mapping[str, Any]],
|
off_topic_steps: Sequence[Mapping[str, Any]],
|
||||||
optimization_hints: Sequence[Mapping[str, Any]],
|
optimization_hints: Sequence[Mapping[str, Any]],
|
||||||
stage_specs: Sequence[StageSpecArtifact],
|
stage_specs: Sequence[StageSpecArtifact],
|
||||||
|
roadmap_unfilled: Optional[Sequence[Any]] = None,
|
||||||
) -> Tuple[Set[int], Dict[int, str]]:
|
) -> Tuple[Set[int], Dict[int, str]]:
|
||||||
"""Major-Step-Indizes für rematch_slot + Begründung pro Slot."""
|
"""Major-Step-Indizes für rematch_slot + Begründung pro Slot."""
|
||||||
spec_by_pos = list(stage_specs)
|
spec_by_pos = list(stage_specs)
|
||||||
|
|
@ -64,6 +65,18 @@ def collect_rematch_slot_indices(
|
||||||
if midx is not None:
|
if midx is not None:
|
||||||
_register(midx, str(hint.get("reason") or hint.get("issue") or "rematch_slot"))
|
_register(midx, str(hint.get("reason") or hint.get("issue") or "rematch_slot"))
|
||||||
|
|
||||||
|
for item in roadmap_unfilled or []:
|
||||||
|
if isinstance(item, (list, tuple)) and len(item) >= 2:
|
||||||
|
idx, spec = item[0], item[1]
|
||||||
|
midx = getattr(spec, "major_step_index", idx)
|
||||||
|
_register(int(midx), "Keine passende Übung für Roadmap-Stufe")
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
midx = _resolve_major(item)
|
||||||
|
if midx is not None:
|
||||||
|
issue = str(item.get("issue") or "roadmap_unfilled")
|
||||||
|
r = (item.get("reasons") or [issue])[0] if item.get("reasons") else issue
|
||||||
|
_register(midx, str(r))
|
||||||
|
|
||||||
return indices, reasons
|
return indices, reasons
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,3 +214,55 @@ def test_build_gap_fill_offer_exposes_context_preview():
|
||||||
)
|
)
|
||||||
assert offer["context_preview"]["start_situation"] == "Steppbewegung"
|
assert offer["context_preview"]["start_situation"] == "Steppbewegung"
|
||||||
assert "variable Rhythmen" in offer["goal_for_ai"]
|
assert "variable Rhythmen" in offer["goal_for_ai"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_gap_fill_specs_off_topic_last_step_no_crash():
|
||||||
|
"""Rand-Slot: off_topic am letzten Schritt darf keinen IndexError auslösen (500)."""
|
||||||
|
brief = build_semantic_brief("Mawashi Geri Kumite")
|
||||||
|
steps = [
|
||||||
|
{"exercise_id": 1, "title": "Stand", "roadmap_major_step_index": 0},
|
||||||
|
{"exercise_id": 2, "title": "Yoko Geri", "roadmap_major_step_index": 1},
|
||||||
|
]
|
||||||
|
specs = collect_gap_fill_specs(
|
||||||
|
steps=steps,
|
||||||
|
unfilled_gaps=[],
|
||||||
|
off_topic_steps=[
|
||||||
|
{
|
||||||
|
"step_index": 1,
|
||||||
|
"roadmap_major_step_index": 1,
|
||||||
|
"title": "Yoko Geri",
|
||||||
|
"expected_phase": "anwendung",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
llm_specs=[],
|
||||||
|
brief=brief,
|
||||||
|
goal_query="Mawashi Geri Kumite",
|
||||||
|
)
|
||||||
|
assert len(specs) == 1
|
||||||
|
assert specs[0]["source"] == "off_topic"
|
||||||
|
assert "Stand" in specs[0]["sketch"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_gap_fill_specs_off_topic_first_step_uses_safe_neighbors():
|
||||||
|
brief = build_semantic_brief("Mawashi Geri")
|
||||||
|
steps = [
|
||||||
|
{"exercise_id": 1, "title": "Yoko Geri", "roadmap_major_step_index": 0},
|
||||||
|
{"exercise_id": 2, "title": "Mawashi", "roadmap_major_step_index": 1},
|
||||||
|
]
|
||||||
|
specs = collect_gap_fill_specs(
|
||||||
|
steps=steps,
|
||||||
|
unfilled_gaps=[],
|
||||||
|
off_topic_steps=[
|
||||||
|
{
|
||||||
|
"step_index": 0,
|
||||||
|
"roadmap_major_step_index": 0,
|
||||||
|
"title": "Yoko Geri",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
llm_specs=[],
|
||||||
|
brief=brief,
|
||||||
|
goal_query="Mawashi Geri",
|
||||||
|
)
|
||||||
|
assert len(specs) == 1
|
||||||
|
assert "Mawashi" in specs[0]["sketch"]
|
||||||
|
assert "vorherigem Schritt" in specs[0]["sketch"]
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,19 @@ def test_collect_rematch_slot_indices_from_optimization_hints():
|
||||||
assert indices == {0}
|
assert indices == {0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_rematch_slot_indices_from_roadmap_unfilled():
|
||||||
|
specs = _stage_specs()
|
||||||
|
indices, reasons = collect_rematch_slot_indices(
|
||||||
|
stripped_off_topic=[],
|
||||||
|
off_topic_steps=[],
|
||||||
|
optimization_hints=[],
|
||||||
|
stage_specs=specs,
|
||||||
|
roadmap_unfilled=[(1, specs[1])],
|
||||||
|
)
|
||||||
|
assert indices == {1}
|
||||||
|
assert "Roadmap-Stufe" in reasons[1]
|
||||||
|
|
||||||
|
|
||||||
def test_rematch_roadmap_slots_replaces_only_target_slot():
|
def test_rematch_roadmap_slots_replaces_only_target_slot():
|
||||||
specs = _stage_specs()
|
specs = _stage_specs()
|
||||||
ctx = ProgressionRoadmapContext(
|
ctx = ProgressionRoadmapContext(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.225"
|
APP_VERSION = "0.8.226"
|
||||||
BUILD_DATE = "2026-06-07"
|
BUILD_DATE = "2026-05-22"
|
||||||
DB_SCHEMA_VERSION = "20260607090"
|
DB_SCHEMA_VERSION = "20260607090"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
|
|
@ -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.0", # planning_intent_context, finalize stage_specs, Prompt 089
|
"planning_exercise_suggest": "0.23.1", # Phase B: Rematch-Schleife mit optimization_hints + roadmap_unfilled
|
||||||
"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
|
||||||
|
|
@ -53,6 +53,14 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.226",
|
||||||
|
"date": "2026-05-22",
|
||||||
|
"changes": [
|
||||||
|
"Progressionsgraph Phase B: Rematch-Schleife (max_rematch_rounds) mit optimization_hints und roadmap_unfilled.",
|
||||||
|
"Fix: Graph-Bewertung/Match 500 bei off-topic am Rand-Slot (collect_gap_fill_specs IndexError).",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.225",
|
"version": "0.8.225",
|
||||||
"date": "2026-06-07",
|
"date": "2026-06-07",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user