Enhance Roadmap Step Handling and Off-Topic Logic
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 52s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 41s
Test Suite / playwright-tests (push) Successful in 1m51s
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 52s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 41s
Test Suite / playwright-tests (push) Successful in 1m51s
- Improved off-topic step handling by incorporating roadmap major step indices for better indexing and detection. - Refactored `collect_gap_fill_specs` to streamline the insertion logic for off-topic steps, ensuring correct placement based on major step indices. - Introduced `_normalize_roadmap_steps_coverage` function to standardize roadmap steps coverage, enhancing the handling of missing slots. - Added `prune_stripped_after_rematch` function to clean up stripped off-topic steps after rematching, improving the overall rematching process. - Updated tests to validate new rematching and off-topic handling features, ensuring robustness against edge cases. - Incremented application version to reflect these updates.
This commit is contained in:
parent
1d94c2ebf1
commit
713a344d17
|
|
@ -363,15 +363,35 @@ def collect_gap_fill_specs(
|
||||||
)
|
)
|
||||||
|
|
||||||
for ot in off_topic_steps:
|
for ot in off_topic_steps:
|
||||||
|
major_idx = ot.get("roadmap_major_step_index")
|
||||||
|
idx: Optional[int] = None
|
||||||
|
if major_idx is not None:
|
||||||
|
try:
|
||||||
|
mi = int(major_idx)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
mi = None
|
||||||
|
if mi is not None:
|
||||||
|
idx = next(
|
||||||
|
(
|
||||||
|
i
|
||||||
|
for i, s in enumerate(steps)
|
||||||
|
if s.get("roadmap_major_step_index") is not None
|
||||||
|
and int(s["roadmap_major_step_index"]) == mi
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if idx is None:
|
||||||
idx = int(ot.get("step_index") or 0)
|
idx = int(ot.get("step_index") or 0)
|
||||||
if idx <= 0 or idx >= len(steps) - 1:
|
if idx < 0 or idx >= len(steps):
|
||||||
continue
|
continue
|
||||||
phase = ot.get("expected_phase") or "vertiefung"
|
phase = ot.get("expected_phase") or "vertiefung"
|
||||||
|
insert_after = max(idx - 1, -1)
|
||||||
add(
|
add(
|
||||||
{
|
{
|
||||||
"source": "off_topic",
|
"source": "off_topic",
|
||||||
"insert_after_index": idx - 1,
|
"insert_after_index": insert_after,
|
||||||
"replace_step_index": idx,
|
"replace_step_index": idx,
|
||||||
|
"roadmap_major_step_index": major_idx,
|
||||||
"gap": {
|
"gap": {
|
||||||
"expected_phase": phase,
|
"expected_phase": phase,
|
||||||
"off_topic_title": ot.get("title"),
|
"off_topic_title": ot.get("title"),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,11 @@ from pydantic import BaseModel, Field
|
||||||
from tenant_context import TenantContext, library_content_visibility_sql
|
from tenant_context import TenantContext, library_content_visibility_sql
|
||||||
from planning_exercise_profiles import PlanningTargetProfile
|
from planning_exercise_profiles import PlanningTargetProfile
|
||||||
from planning_path_qa_pipeline import run_multistage_path_qa
|
from planning_path_qa_pipeline import run_multistage_path_qa
|
||||||
from planning_path_rematch import collect_rematch_slot_indices, rematch_roadmap_slots
|
from planning_path_rematch import (
|
||||||
|
collect_rematch_slot_indices,
|
||||||
|
prune_stripped_after_rematch,
|
||||||
|
rematch_roadmap_slots,
|
||||||
|
)
|
||||||
from planning_stage_context import build_contextualized_stage_goal, resolve_path_start_target
|
from planning_stage_context import build_contextualized_stage_goal, resolve_path_start_target
|
||||||
from planning_exercise_path_qa import (
|
from planning_exercise_path_qa import (
|
||||||
apply_llm_path_reorder,
|
apply_llm_path_reorder,
|
||||||
|
|
@ -704,6 +708,129 @@ def _match_roadmap_slot(
|
||||||
return step, None
|
return step, None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_roadmap_steps_coverage(
|
||||||
|
steps: List[Dict[str, Any]],
|
||||||
|
*,
|
||||||
|
roadmap_ctx: ProgressionRoadmapContext,
|
||||||
|
max_steps: int,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Ein Eintrag pro Roadmap-Major-Step — fehlende Slots als leere Platzhalter."""
|
||||||
|
stage_specs = list(roadmap_ctx.stage_specs or [])[:max_steps]
|
||||||
|
if not stage_specs:
|
||||||
|
return steps
|
||||||
|
|
||||||
|
major_by_index: Dict[int, MajorStep] = {}
|
||||||
|
if roadmap_ctx.roadmap:
|
||||||
|
major_by_index = {m.index: m for m in roadmap_ctx.roadmap.major_steps}
|
||||||
|
|
||||||
|
by_major: Dict[int, Dict[str, Any]] = {}
|
||||||
|
for raw in steps:
|
||||||
|
step = dict(raw)
|
||||||
|
midx = step.get("roadmap_major_step_index")
|
||||||
|
if midx is not None:
|
||||||
|
by_major[int(midx)] = step
|
||||||
|
|
||||||
|
out: List[Dict[str, Any]] = []
|
||||||
|
for spec in sorted(stage_specs, key=lambda s: s.major_step_index):
|
||||||
|
midx = int(spec.major_step_index)
|
||||||
|
if midx in by_major:
|
||||||
|
out.append(by_major[midx])
|
||||||
|
continue
|
||||||
|
major = major_by_index.get(midx)
|
||||||
|
goal = (spec.learning_goal or "").strip()
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"exercise_id": None,
|
||||||
|
"variant_id": None,
|
||||||
|
"title": goal or f"Slot {midx + 1}",
|
||||||
|
"is_ai_proposal": False,
|
||||||
|
"roadmap_major_step_index": midx,
|
||||||
|
"roadmap_phase": major.phase if major else None,
|
||||||
|
"roadmap_learning_goal": goal or None,
|
||||||
|
"roadmap_match_source": "stage_spec",
|
||||||
|
"reasons": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_rematch_roadmap_after_strip(
|
||||||
|
cur,
|
||||||
|
*,
|
||||||
|
tenant: TenantContext,
|
||||||
|
body: ProgressionPathSuggestRequest,
|
||||||
|
goal_query: str,
|
||||||
|
max_steps: int,
|
||||||
|
semantic_brief: PlanningSemanticBrief,
|
||||||
|
path_target_profile: PlanningTargetProfile,
|
||||||
|
path_intent: str,
|
||||||
|
roadmap_ctx: ProgressionRoadmapContext,
|
||||||
|
steps: List[Dict[str, Any]],
|
||||||
|
stripped_off_topic: List[Dict[str, Any]],
|
||||||
|
off_topic_before_strip: List[Dict[str, Any]],
|
||||||
|
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]],
|
||||||
|
) -> Tuple[
|
||||||
|
List[Dict[str, Any]],
|
||||||
|
List[Dict[str, Any]],
|
||||||
|
List[Dict[str, Any]],
|
||||||
|
List[Dict[str, Any]],
|
||||||
|
int,
|
||||||
|
List[Tuple[int, StageSpecArtifact]],
|
||||||
|
]:
|
||||||
|
rematch_log: List[Dict[str, Any]] = []
|
||||||
|
rematch_rounds = 0
|
||||||
|
if not body.auto_rematch_after_qa or not roadmap_ctx.stage_specs:
|
||||||
|
return steps, rematch_log, stripped_off_topic, [], rematch_rounds, roadmap_unfilled
|
||||||
|
|
||||||
|
slot_indices, rematch_reasons = collect_rematch_slot_indices(
|
||||||
|
stripped_off_topic=stripped_off_topic,
|
||||||
|
off_topic_steps=off_topic_before_strip if not stripped_off_topic else [],
|
||||||
|
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(
|
||||||
|
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
|
||||||
|
stripped_off_topic = prune_stripped_after_rematch(stripped_off_topic, rematch_log)
|
||||||
|
if rematch_new_unfilled:
|
||||||
|
remapped = {sp.major_step_index for _, sp in rematch_new_unfilled}
|
||||||
|
roadmap_unfilled = [
|
||||||
|
item for item in roadmap_unfilled if item[1].major_step_index not in remapped
|
||||||
|
]
|
||||||
|
roadmap_unfilled.extend(rematch_new_unfilled)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_steps_roadmap_first(
|
def _build_steps_roadmap_first(
|
||||||
cur,
|
cur,
|
||||||
*,
|
*,
|
||||||
|
|
@ -906,7 +1033,6 @@ def _run_evaluate_only_path_qa(
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
)
|
)
|
||||||
steps, stripped_off_topic = strip_off_topic_steps_from_path(steps, off_topic_steps)
|
|
||||||
llm_gap_specs = parse_llm_suggested_new_exercises(
|
llm_gap_specs = parse_llm_suggested_new_exercises(
|
||||||
llm_qa,
|
llm_qa,
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
|
|
@ -918,7 +1044,7 @@ def _run_evaluate_only_path_qa(
|
||||||
gap_specs = collect_gap_fill_specs(
|
gap_specs = collect_gap_fill_specs(
|
||||||
steps=steps,
|
steps=steps,
|
||||||
unfilled_gaps=fresh_large_gaps or unfilled_gaps,
|
unfilled_gaps=fresh_large_gaps or unfilled_gaps,
|
||||||
off_topic_steps=off_topic_steps if not stripped_off_topic else [],
|
off_topic_steps=off_topic_steps,
|
||||||
llm_specs=llm_gap_specs,
|
llm_specs=llm_gap_specs,
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
|
|
@ -1374,20 +1500,15 @@ def suggest_progression_path(
|
||||||
roadmap_first=roadmap_first,
|
roadmap_first=roadmap_first,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if roadmap_first and roadmap_ctx is not None:
|
||||||
roadmap_first
|
(
|
||||||
and body.auto_rematch_after_qa
|
steps,
|
||||||
and roadmap_ctx is not None
|
rematch_log,
|
||||||
and roadmap_ctx.stage_specs
|
stripped_off_topic,
|
||||||
):
|
rematch_off_topic,
|
||||||
slot_indices, rematch_reasons = collect_rematch_slot_indices(
|
rematch_rounds,
|
||||||
stripped_off_topic=stripped_off_topic,
|
roadmap_unfilled,
|
||||||
off_topic_steps=off_topic_before_strip if not stripped_off_topic else [],
|
) = _maybe_rematch_roadmap_after_strip(
|
||||||
optimization_hints=[],
|
|
||||||
stage_specs=roadmap_ctx.stage_specs,
|
|
||||||
)
|
|
||||||
if slot_indices:
|
|
||||||
steps, rematch_log, rematch_new_unfilled = rematch_roadmap_slots(
|
|
||||||
cur,
|
cur,
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
body=body,
|
body=body,
|
||||||
|
|
@ -1398,25 +1519,12 @@ def suggest_progression_path(
|
||||||
path_intent=path_intent,
|
path_intent=path_intent,
|
||||||
roadmap_ctx=roadmap_ctx,
|
roadmap_ctx=roadmap_ctx,
|
||||||
steps=steps,
|
steps=steps,
|
||||||
slot_indices=slot_indices,
|
stripped_off_topic=stripped_off_topic,
|
||||||
rematch_reasons=rematch_reasons,
|
off_topic_before_strip=off_topic_before_strip,
|
||||||
match_slot_fn=_match_roadmap_slot,
|
roadmap_unfilled=roadmap_unfilled,
|
||||||
)
|
|
||||||
rematch_rounds = 1
|
|
||||||
if rematch_new_unfilled:
|
|
||||||
remapped = {sp.major_step_index for _, sp in rematch_new_unfilled}
|
|
||||||
roadmap_unfilled = [
|
|
||||||
item
|
|
||||||
for item in roadmap_unfilled
|
|
||||||
if item[1].major_step_index not in remapped
|
|
||||||
]
|
|
||||||
roadmap_unfilled.extend(rematch_new_unfilled)
|
|
||||||
off_topic_steps = detect_off_topic_steps(
|
|
||||||
cur,
|
|
||||||
steps,
|
|
||||||
brief=semantic_brief,
|
|
||||||
goal_query=goal_query,
|
|
||||||
)
|
)
|
||||||
|
if rematch_off_topic:
|
||||||
|
off_topic_steps = rematch_off_topic
|
||||||
gaps = detect_path_gaps(
|
gaps = detect_path_gaps(
|
||||||
cur,
|
cur,
|
||||||
steps,
|
steps,
|
||||||
|
|
@ -1500,6 +1608,13 @@ def suggest_progression_path(
|
||||||
path_qa["rematch_log"] = rematch_log
|
path_qa["rematch_log"] = rematch_log
|
||||||
path_qa["rematch_rounds"] = rematch_rounds
|
path_qa["rematch_rounds"] = rematch_rounds
|
||||||
|
|
||||||
|
if roadmap_first and roadmap_ctx is not None:
|
||||||
|
steps = _normalize_roadmap_steps_coverage(
|
||||||
|
steps,
|
||||||
|
roadmap_ctx=roadmap_ctx,
|
||||||
|
max_steps=max_steps,
|
||||||
|
)
|
||||||
|
|
||||||
target_profile_summary = path_target_profile.to_summary_dict(cur)
|
target_profile_summary = path_target_profile.to_summary_dict(cur)
|
||||||
retrieval_parts = ["profile_v1", "full_library", "path_builder", "semantics"]
|
retrieval_parts = ["profile_v1", "full_library", "path_builder", "semantics"]
|
||||||
if roadmap_first:
|
if roadmap_first:
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,14 @@ def detect_off_topic_steps(
|
||||||
goal_query: Optional[str] = None,
|
goal_query: Optional[str] = None,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Schritte ohne Bezug zum Pfad-Thema (z. B. reine Kraftübungen bei Mae Geri)."""
|
"""Schritte ohne Bezug zum Pfad-Thema (z. B. reine Kraftübungen bei Mae Geri)."""
|
||||||
if brief.semantic_strength < 0.55 or len(steps) < 2:
|
if len(steps) < 2:
|
||||||
|
return []
|
||||||
|
roadmap_stage_steps = any(
|
||||||
|
(step.get("roadmap_match_source") == "stage_spec")
|
||||||
|
or (step.get("roadmap_learning_goal") or "").strip()
|
||||||
|
for step in steps
|
||||||
|
)
|
||||||
|
if brief.semantic_strength < 0.55 and not roadmap_stage_steps:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
path_anti = resolve_path_anti_patterns(goal_query or "", semantic_brief=brief)
|
path_anti = resolve_path_anti_patterns(goal_query or "", semantic_brief=brief)
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,8 @@ def rematch_roadmap_slots(
|
||||||
for m, s in steps_by_major.items()
|
for m, s in steps_by_major.items()
|
||||||
if s.get("exercise_id") is not None
|
if s.get("exercise_id") is not None
|
||||||
}
|
}
|
||||||
|
if old and old.get("exercise_id") is not None:
|
||||||
|
used.add(int(old["exercise_id"]))
|
||||||
planned_ids, anchor_id, anchor_variant_id = _context_before_major(
|
planned_ids, anchor_id, anchor_variant_id = _context_before_major(
|
||||||
steps_by_major, int(major_idx)
|
steps_by_major, int(major_idx)
|
||||||
)
|
)
|
||||||
|
|
@ -196,7 +198,35 @@ def rematch_roadmap_slots(
|
||||||
return ordered, rematch_log, new_unfilled
|
return ordered, rematch_log, new_unfilled
|
||||||
|
|
||||||
|
|
||||||
|
def prune_stripped_after_rematch(
|
||||||
|
stripped_off_topic: Sequence[Mapping[str, Any]],
|
||||||
|
rematch_log: Sequence[Mapping[str, Any]],
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Entfernt aus stripped_off_topic Slots, die per Rematch ersetzt wurden."""
|
||||||
|
replaced: Set[int] = set()
|
||||||
|
for entry in rematch_log or []:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
if str(entry.get("action") or "") != "replaced":
|
||||||
|
continue
|
||||||
|
midx = entry.get("roadmap_major_step_index")
|
||||||
|
if midx is not None:
|
||||||
|
replaced.add(int(midx))
|
||||||
|
if not replaced:
|
||||||
|
return list(stripped_off_topic or [])
|
||||||
|
out: List[Dict[str, Any]] = []
|
||||||
|
for item in stripped_off_topic or []:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
midx = item.get("roadmap_major_step_index")
|
||||||
|
if midx is not None and int(midx) in replaced:
|
||||||
|
continue
|
||||||
|
out.append(dict(item))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"collect_rematch_slot_indices",
|
"collect_rematch_slot_indices",
|
||||||
|
"prune_stripped_after_rematch",
|
||||||
"rematch_roadmap_slots",
|
"rematch_roadmap_slots",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -95,8 +95,9 @@ def test_rematch_roadmap_slots_replaces_only_target_slot():
|
||||||
|
|
||||||
def _fake_match(cur, *, stage_spec, used, **kwargs):
|
def _fake_match(cur, *, stage_spec, used, **kwargs):
|
||||||
assert stage_spec.major_step_index == 1
|
assert stage_spec.major_step_index == 1
|
||||||
assert 20 not in used
|
assert 20 in used
|
||||||
assert 10 in used
|
assert 10 in used
|
||||||
|
assert 30 in used
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
"exercise_id": 21,
|
"exercise_id": 21,
|
||||||
|
|
@ -131,3 +132,41 @@ def test_rematch_roadmap_slots_replaces_only_target_slot():
|
||||||
assert log[0]["replaced_exercise_id"] == 20
|
assert log[0]["replaced_exercise_id"] == 20
|
||||||
assert log[0]["new_exercise_id"] == 21
|
assert log[0]["new_exercise_id"] == 21
|
||||||
assert not unfilled
|
assert not unfilled
|
||||||
|
|
||||||
|
|
||||||
|
def test_rematch_excludes_replaced_exercise_from_used():
|
||||||
|
specs = _stage_specs()
|
||||||
|
ctx = ProgressionRoadmapContext(
|
||||||
|
goal_query="Mawashi Geri",
|
||||||
|
max_steps=3,
|
||||||
|
stage_specs=specs,
|
||||||
|
)
|
||||||
|
steps = [
|
||||||
|
{"exercise_id": 10, "title": "OK", "roadmap_major_step_index": 0},
|
||||||
|
{"exercise_id": 99, "title": "Mae Geri", "roadmap_major_step_index": 1},
|
||||||
|
]
|
||||||
|
seen_used = []
|
||||||
|
|
||||||
|
def _fake_match(cur, *, used, stage_spec, **kwargs):
|
||||||
|
seen_used.append(set(used))
|
||||||
|
return (
|
||||||
|
{"exercise_id": 42, "title": "Neu", "roadmap_major_step_index": stage_spec.major_step_index},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
rematch_roadmap_slots(
|
||||||
|
None,
|
||||||
|
tenant=None,
|
||||||
|
body=None,
|
||||||
|
goal_query="Mawashi",
|
||||||
|
max_steps=3,
|
||||||
|
semantic_brief=None,
|
||||||
|
path_target_profile=None,
|
||||||
|
path_intent="",
|
||||||
|
roadmap_ctx=ctx,
|
||||||
|
steps=steps,
|
||||||
|
slot_indices={1},
|
||||||
|
rematch_reasons={1: "technique_scope"},
|
||||||
|
match_slot_fn=_fake_match,
|
||||||
|
)
|
||||||
|
assert 99 in seen_used[0]
|
||||||
|
|
|
||||||
|
|
@ -1022,10 +1022,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
const applyPathMatchResponse = (res, q) => {
|
const applyPathMatchResponse = (res, q) => {
|
||||||
const qa = res?.path_qa || null
|
const qa = res?.path_qa || null
|
||||||
const rawRows = (Array.isArray(res?.steps) ? res.steps : []).map(mapApiStepToRow)
|
const rawRows = (Array.isArray(res?.steps) ? res.steps : []).map(mapApiStepToRow)
|
||||||
const rows =
|
const rows = applyOffTopicFlags(rawRows, qa)
|
||||||
Array.isArray(qa?.stripped_off_topic_steps) && qa.stripped_off_topic_steps.length > 0
|
|
||||||
? rawRows
|
|
||||||
: applyOffTopicFlags(rawRows, qa)
|
|
||||||
if (rows.length < 2) {
|
if (rows.length < 2) {
|
||||||
throw new Error('Zu wenig Schritte im Vorschlag.')
|
throw new Error('Zu wenig Schritte im Vorschlag.')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -750,12 +750,14 @@ export function applyMatchStepsToSlots(draft, apiSteps) {
|
||||||
siblings: [...(slot.siblings || [])],
|
siblings: [...(slot.siblings || [])],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const touchedMajors = new Set()
|
||||||
for (const step of steps) {
|
for (const step of steps) {
|
||||||
if (step.roadmap_major_step_index == null || !Number.isFinite(Number(step.roadmap_major_step_index))) {
|
if (step.roadmap_major_step_index == null || !Number.isFinite(Number(step.roadmap_major_step_index))) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const idx = Number(step.roadmap_major_step_index)
|
const idx = Number(step.roadmap_major_step_index)
|
||||||
if (idx < 0 || idx >= nextSlots.length) continue
|
if (idx < 0 || idx >= nextSlots.length) continue
|
||||||
|
touchedMajors.add(idx)
|
||||||
|
|
||||||
const isProposal = Boolean(step.is_ai_proposal) || step.exercise_id == null
|
const isProposal = Boolean(step.is_ai_proposal) || step.exercise_id == null
|
||||||
if (isProposal) {
|
if (isProposal) {
|
||||||
|
|
@ -773,6 +775,12 @@ export function applyMatchStepsToSlots(draft, apiSteps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < nextSlots.length; i += 1) {
|
||||||
|
if (!touchedMajors.has(i)) {
|
||||||
|
nextSlots[i].primary = emptySlotExercise()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return syncProgressionRoadmapFromSlots({ ...draft, slots: nextSlots, dirty: true })
|
return syncProgressionRoadmapFromSlots({ ...draft, slots: nextSlots, dirty: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user