Enhance Slot Difference Annotation and Rematch Suggestion Logic
All checks were successful
Deploy Development / deploy (push) Successful in 43s
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 34s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 43s
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 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `_annotate_slot_diffs` to mark trivial ID swaps in slot differences, improving clarity in comparison results. - Added `_actionable_slot_diffs` to filter out non-actionable differences, streamlining the evaluation process. - Implemented `_build_rematch_suggestion_diffs` to generate suggestions based on rematch logs, enhancing the path optimization workflow. - Updated `_build_progression_compare_response` to incorporate actionable slot differences and rematch suggestions, improving the response structure. - Enhanced frontend components to display rematch suggestions and handle trivial differences more effectively. - Bumped version to reflect the new features and improvements.
This commit is contained in:
parent
e828a5da32
commit
dccb065181
|
|
@ -465,7 +465,7 @@ skill_level_definitions (
|
||||||
|
|
||||||
**Fachliche Grenze aktuell:** Mehrere gleichwertige „Pakete“ paralleler Alternativen sind **modellierbar** (mehrere ausgehende Kanten), aber noch **nicht** über eine dedizierte „Alternativgruppe“ in der UI trivial pflegbar; siehe `technical/TRAINING_FRAMEWORK_SPEC.md` §4.
|
**Fachliche Grenze aktuell:** Mehrere gleichwertige „Pakete“ paralleler Alternativen sind **modellierbar** (mehrere ausgehende Kanten), aber noch **nicht** über eine dedizierte „Alternativgruppe“ in der UI trivial pflegbar; siehe `technical/TRAINING_FRAMEWORK_SPEC.md` §4.
|
||||||
|
|
||||||
**KI-Planung (Workbench, Stand 0.8.233):** Am Graph können Trainer neben Kanten ein **`planning_roadmap`**-Artefakt (Curriculum-Stufen) und **`planning_catalog_context`** (Primärfokus, Stilrichtung, Trainingsstil, Zielgruppe aus den Katalog-Dimensionen §1) pflegen. Die Roadmap-first-Pipeline matcht Übungen pro Stufe; Didaktik und Reihenfolge kommen aus Roadmap + QS, nicht aus Technik-Hardcoding. Technische Details: **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**. Für **Trainingsplanung** (Einheit, Abschnitt, Rahmen-Slot) gelten dieselben Katalog- und Retrieval-Bausteine mit anderen Scopes — Phase G, siehe Roadmap **`PLANNING_KI_ROADMAP.md`**.
|
**KI-Planung (Workbench, Stand 0.8.233):** Am Graph können Trainer neben Kanten ein **`planning_roadmap`**-Artefakt (Curriculum-Stufen) und **`planning_catalog_context`** (Primärfokus, Stilrichtung, Trainingsstil, Zielgruppe aus den Katalog-Dimensionen §1) pflegen. Die Roadmap-first-Pipeline matcht Übungen pro Stufe; Didaktik und Reihenfolge kommen aus Roadmap + QS, nicht aus Technik-Hardcoding. **Geplant (H1):** Katalog-Dimensionen zusätzlich als **Prompt-Snippets** in LLM-Aufrufen (Priorität Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung) — **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`**. Technische Details: **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**. Für **Trainingsplanung** (Einheit, Abschnitt, Rahmen-Slot) gelten dieselben Katalog- und Retrieval-Bausteine mit anderen Scopes — Phase G, siehe Roadmap **`PLANNING_KI_ROADMAP.md`**.
|
||||||
|
|
||||||
### Trainingsrahmen‑Vorlage (Rahmenprogramm, CURR‑002 Stufe 2 / CURR‑009)
|
### Trainingsrahmen‑Vorlage (Rahmenprogramm, CURR‑002 Stufe 2 / CURR‑009)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2208,20 +2208,120 @@ def _normalize_slot_title(title: Optional[str]) -> str:
|
||||||
return (title or "").strip().casefold()
|
return (title or "").strip().casefold()
|
||||||
|
|
||||||
|
|
||||||
def _filter_trivial_slot_diffs(diffs: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]]:
|
def _annotate_slot_diffs(
|
||||||
"""Gleicher sichtbarer Titel = kein inhaltlicher Wechsel (nur ID-Doppel in der Bibliothek)."""
|
diffs: Sequence[Mapping[str, Any]],
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Kennzeichnet reine ID-Tausche (gleicher Titel) — bleiben sichtbar, zählen aber nicht als inhaltlich."""
|
||||||
out: List[Dict[str, Any]] = []
|
out: List[Dict[str, Any]] = []
|
||||||
for raw in diffs or []:
|
for raw in diffs or []:
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
continue
|
continue
|
||||||
bt = _normalize_slot_title(raw.get("baseline_title"))
|
entry = dict(raw)
|
||||||
pt = _normalize_slot_title(raw.get("proposed_title"))
|
bt = _normalize_slot_title(entry.get("baseline_title"))
|
||||||
if bt and pt and bt == pt:
|
pt = _normalize_slot_title(entry.get("proposed_title"))
|
||||||
continue
|
entry["trivial_id_swap"] = bool(bt and pt and bt == pt)
|
||||||
out.append(dict(raw))
|
out.append(entry)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _actionable_slot_diffs(diffs: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
return [d for d in diffs if not d.get("trivial_id_swap")]
|
||||||
|
|
||||||
|
|
||||||
|
def _last_rematch_replacements_by_slot(
|
||||||
|
rematch_log: Sequence[Mapping[str, Any]],
|
||||||
|
) -> Dict[int, Mapping[str, Any]]:
|
||||||
|
"""Letzter erfolgreicher Replace je Slot (Multi-Runden-Rematch)."""
|
||||||
|
out: Dict[int, Mapping[str, Any]] = {}
|
||||||
|
for entry in rematch_log or []:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
if str(entry.get("action") or "") != "replaced":
|
||||||
|
continue
|
||||||
|
if entry.get("new_exercise_id") is None:
|
||||||
|
continue
|
||||||
|
midx = entry.get("roadmap_major_step_index")
|
||||||
|
if midx is None:
|
||||||
|
continue
|
||||||
|
out[int(midx)] = entry
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _build_rematch_suggestion_diffs(
|
||||||
|
baseline_steps: Sequence[Mapping[str, Any]],
|
||||||
|
rematch_log: Sequence[Mapping[str, Any]],
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Vorschläge aus Rematch-Protokoll, wenn End-Pfad vs. Baseline identisch wirkt."""
|
||||||
|
base_by = _steps_by_major_index(baseline_steps)
|
||||||
|
replacements = _last_rematch_replacements_by_slot(rematch_log)
|
||||||
|
diffs: List[Dict[str, Any]] = []
|
||||||
|
for midx, entry in sorted(replacements.items()):
|
||||||
|
base = base_by.get(midx, {})
|
||||||
|
base_id = base.get("exercise_id")
|
||||||
|
new_id = entry.get("new_exercise_id")
|
||||||
|
base_title = (base.get("title") or "").strip() or None
|
||||||
|
new_title = (entry.get("new_title") or "").strip() or None
|
||||||
|
same_id = False
|
||||||
|
if base_id is not None and new_id is not None:
|
||||||
|
try:
|
||||||
|
same_id = int(base_id) == int(new_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
same_id = False
|
||||||
|
if same_id:
|
||||||
|
bt = _normalize_slot_title(base_title)
|
||||||
|
pt = _normalize_slot_title(new_title)
|
||||||
|
if bt and pt and bt == pt:
|
||||||
|
continue
|
||||||
|
diffs.append(
|
||||||
|
{
|
||||||
|
"roadmap_major_step_index": midx,
|
||||||
|
"baseline_exercise_id": int(base_id) if base_id is not None else None,
|
||||||
|
"baseline_title": base_title,
|
||||||
|
"proposed_exercise_id": int(new_id) if new_id is not None else None,
|
||||||
|
"proposed_title": new_title,
|
||||||
|
"baseline_slot_status": base.get("slot_status"),
|
||||||
|
"proposed_slot_status": "matched",
|
||||||
|
"changed": True,
|
||||||
|
"from_rematch_log": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return diffs
|
||||||
|
|
||||||
|
|
||||||
|
def _overlay_rematch_suggestions_on_steps(
|
||||||
|
proposed_steps: Sequence[Mapping[str, Any]],
|
||||||
|
suggestion_diffs: Sequence[Mapping[str, Any]],
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Ergänzt proposed_steps um Rematch-Kandidaten (für selektive Übernahme)."""
|
||||||
|
if not suggestion_diffs:
|
||||||
|
return list(proposed_steps or [])
|
||||||
|
prop_by = _steps_by_major_index(proposed_steps)
|
||||||
|
for diff in suggestion_diffs:
|
||||||
|
if not isinstance(diff, dict) or not diff.get("from_rematch_log"):
|
||||||
|
continue
|
||||||
|
midx = diff.get("roadmap_major_step_index")
|
||||||
|
new_id = diff.get("proposed_exercise_id")
|
||||||
|
if midx is None or new_id is None:
|
||||||
|
continue
|
||||||
|
existing = dict(prop_by.get(int(midx), {}))
|
||||||
|
existing.update(
|
||||||
|
{
|
||||||
|
"exercise_id": int(new_id),
|
||||||
|
"title": diff.get("proposed_title") or existing.get("title"),
|
||||||
|
"variant_id": existing.get("variant_id"),
|
||||||
|
"roadmap_major_step_index": int(midx),
|
||||||
|
"is_ai_proposal": False,
|
||||||
|
"slot_status": "matched",
|
||||||
|
"roadmap_match_source": "rematch_suggestion",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
prop_by[int(midx)] = existing
|
||||||
|
ordered: List[Dict[str, Any]] = []
|
||||||
|
for midx in sorted(prop_by.keys()):
|
||||||
|
ordered.append(prop_by[midx])
|
||||||
|
return ordered
|
||||||
|
|
||||||
|
|
||||||
def _build_progression_slot_diffs(
|
def _build_progression_slot_diffs(
|
||||||
baseline_steps: Sequence[Mapping[str, Any]],
|
baseline_steps: Sequence[Mapping[str, Any]],
|
||||||
proposed_steps: Sequence[Mapping[str, Any]],
|
proposed_steps: Sequence[Mapping[str, Any]],
|
||||||
|
|
@ -2269,24 +2369,46 @@ def _build_progression_compare_response(
|
||||||
if isinstance(proposed_eval, dict) and isinstance(proposed_eval.get("path_qa"), dict)
|
if isinstance(proposed_eval, dict) and isinstance(proposed_eval.get("path_qa"), dict)
|
||||||
else pipeline_qa
|
else pipeline_qa
|
||||||
)
|
)
|
||||||
slot_diffs = _filter_trivial_slot_diffs(
|
slot_diffs = _annotate_slot_diffs(
|
||||||
_build_progression_slot_diffs(baseline_steps, proposed_steps),
|
_build_progression_slot_diffs(baseline_steps, proposed_steps),
|
||||||
)
|
)
|
||||||
|
actionable_diffs = _actionable_slot_diffs(slot_diffs)
|
||||||
|
slot_diffs_source = "steps"
|
||||||
|
rematch_log = (
|
||||||
|
pipeline_qa.get("rematch_log")
|
||||||
|
if isinstance(pipeline_qa.get("rematch_log"), list)
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
apply_steps = list(proposed_steps)
|
||||||
|
if not actionable_diffs and rematch_log:
|
||||||
|
rematch_raw = _build_rematch_suggestion_diffs(baseline_steps, rematch_log)
|
||||||
|
rematch_diffs = _annotate_slot_diffs(rematch_raw)
|
||||||
|
rematch_actionable = _actionable_slot_diffs(rematch_diffs)
|
||||||
|
if rematch_actionable:
|
||||||
|
actionable_diffs = rematch_actionable
|
||||||
|
slot_diffs = rematch_diffs
|
||||||
|
slot_diffs_source = "rematch_log"
|
||||||
|
apply_steps = _overlay_rematch_suggestions_on_steps(proposed_steps, rematch_actionable)
|
||||||
return {
|
return {
|
||||||
**dict(proposed),
|
**dict(proposed),
|
||||||
"comparison_mode": True,
|
"comparison_mode": True,
|
||||||
"baseline_steps": baseline_steps,
|
"baseline_steps": baseline_steps,
|
||||||
"baseline_path_qa": baseline_qa,
|
"baseline_path_qa": baseline_qa,
|
||||||
"proposed_steps": proposed_steps,
|
"proposed_steps": apply_steps,
|
||||||
|
"proposed_steps_pipeline": proposed_steps,
|
||||||
"proposed_path_qa": fair_qa,
|
"proposed_path_qa": fair_qa,
|
||||||
"proposed_path_qa_pipeline": pipeline_qa,
|
"proposed_path_qa_pipeline": pipeline_qa,
|
||||||
"slot_diffs": slot_diffs,
|
"slot_diffs": slot_diffs,
|
||||||
"slot_diff_count": len(slot_diffs),
|
"slot_diffs_actionable": actionable_diffs,
|
||||||
|
"slot_diff_count": len(actionable_diffs),
|
||||||
|
"slot_diff_count_including_trivial": len(slot_diffs),
|
||||||
|
"slot_diffs_source": slot_diffs_source,
|
||||||
|
"optimization_actionable": len(actionable_diffs) > 0,
|
||||||
"baseline_quality_score": _path_qa_quality_score(baseline_qa),
|
"baseline_quality_score": _path_qa_quality_score(baseline_qa),
|
||||||
"proposed_quality_score": _path_qa_quality_score(fair_qa),
|
"proposed_quality_score": _path_qa_quality_score(fair_qa),
|
||||||
"proposed_pipeline_quality_score": _path_qa_quality_score(pipeline_qa),
|
"proposed_pipeline_quality_score": _path_qa_quality_score(pipeline_qa),
|
||||||
"path_qa": fair_qa,
|
"path_qa": fair_qa,
|
||||||
"steps": proposed_steps,
|
"steps": apply_steps,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
"""Tests Vergleichs-Diffs (triviale ID-Tausche ausfiltern)."""
|
"""Tests Vergleichs-Diffs (triviale ID-Tausche markieren, Rematch-Vorschläge)."""
|
||||||
from planning_exercise_path_builder import (
|
from planning_exercise_path_builder import (
|
||||||
|
_actionable_slot_diffs,
|
||||||
|
_annotate_slot_diffs,
|
||||||
|
_build_progression_compare_response,
|
||||||
_build_progression_slot_diffs,
|
_build_progression_slot_diffs,
|
||||||
_filter_trivial_slot_diffs,
|
_build_rematch_suggestion_diffs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_filter_trivial_slot_diffs_same_title_different_id():
|
def test_annotate_trivial_id_swap():
|
||||||
diffs = [
|
diffs = [
|
||||||
{
|
{
|
||||||
"roadmap_major_step_index": 1,
|
"roadmap_major_step_index": 1,
|
||||||
|
|
@ -15,10 +18,13 @@ def test_filter_trivial_slot_diffs_same_title_different_id():
|
||||||
"proposed_title": "Rhythmuswechsel in der Kumite-Beinarbeit",
|
"proposed_title": "Rhythmuswechsel in der Kumite-Beinarbeit",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
assert _filter_trivial_slot_diffs(diffs) == []
|
annotated = _annotate_slot_diffs(diffs)
|
||||||
|
assert len(annotated) == 1
|
||||||
|
assert annotated[0]["trivial_id_swap"] is True
|
||||||
|
assert _actionable_slot_diffs(annotated) == []
|
||||||
|
|
||||||
|
|
||||||
def test_filter_trivial_slot_diffs_keeps_real_title_change():
|
def test_annotate_keeps_real_title_change():
|
||||||
diffs = [
|
diffs = [
|
||||||
{
|
{
|
||||||
"roadmap_major_step_index": 1,
|
"roadmap_major_step_index": 1,
|
||||||
|
|
@ -28,12 +34,12 @@ def test_filter_trivial_slot_diffs_keeps_real_title_change():
|
||||||
"proposed_title": "Neu",
|
"proposed_title": "Neu",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
filtered = _filter_trivial_slot_diffs(diffs)
|
annotated = _annotate_slot_diffs(diffs)
|
||||||
assert len(filtered) == 1
|
assert annotated[0]["trivial_id_swap"] is False
|
||||||
assert filtered[0]["proposed_title"] == "Neu"
|
assert len(_actionable_slot_diffs(annotated)) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_build_slot_diffs_then_filter():
|
def test_build_slot_diffs_then_annotate():
|
||||||
baseline = [
|
baseline = [
|
||||||
{"roadmap_major_step_index": 0, "exercise_id": 1, "title": "A"},
|
{"roadmap_major_step_index": 0, "exercise_id": 1, "title": "A"},
|
||||||
{"roadmap_major_step_index": 1, "exercise_id": 10, "title": "Gleich"},
|
{"roadmap_major_step_index": 1, "exercise_id": 10, "title": "Gleich"},
|
||||||
|
|
@ -43,5 +49,54 @@ def test_build_slot_diffs_then_filter():
|
||||||
{"roadmap_major_step_index": 1, "exercise_id": 77, "title": "Gleich"},
|
{"roadmap_major_step_index": 1, "exercise_id": 77, "title": "Gleich"},
|
||||||
]
|
]
|
||||||
raw = _build_progression_slot_diffs(baseline, proposed)
|
raw = _build_progression_slot_diffs(baseline, proposed)
|
||||||
assert len(raw) == 1
|
annotated = _annotate_slot_diffs(raw)
|
||||||
assert _filter_trivial_slot_diffs(raw) == []
|
assert len(annotated) == 1
|
||||||
|
assert annotated[0]["trivial_id_swap"] is True
|
||||||
|
assert _actionable_slot_diffs(annotated) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_rematch_suggestion_diffs_when_end_path_matches_baseline():
|
||||||
|
baseline = [
|
||||||
|
{"roadmap_major_step_index": 1, "exercise_id": None, "title": "Lernziel Slot 2"},
|
||||||
|
{"roadmap_major_step_index": 4, "exercise_id": 50, "title": "Bestehend"},
|
||||||
|
]
|
||||||
|
proposed = list(baseline)
|
||||||
|
rematch_log = [
|
||||||
|
{
|
||||||
|
"roadmap_major_step_index": 1,
|
||||||
|
"action": "replaced",
|
||||||
|
"round": 1,
|
||||||
|
"new_exercise_id": 101,
|
||||||
|
"new_title": "Rhythmuswechsel in der Kumite-Beinarbeit",
|
||||||
|
"replaced_exercise_id": None,
|
||||||
|
"replaced_title": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roadmap_major_step_index": 1,
|
||||||
|
"action": "replaced",
|
||||||
|
"round": 3,
|
||||||
|
"new_exercise_id": 102,
|
||||||
|
"new_title": "Kumite Beinarbeit — vertiefung",
|
||||||
|
"replaced_exercise_id": 101,
|
||||||
|
"replaced_title": "Rhythmuswechsel in der Kumite-Beinarbeit",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
diffs = _build_rematch_suggestion_diffs(baseline, rematch_log)
|
||||||
|
assert len(diffs) == 1
|
||||||
|
assert diffs[0]["proposed_exercise_id"] == 102
|
||||||
|
assert diffs[0]["from_rematch_log"] is True
|
||||||
|
|
||||||
|
compare = _build_progression_compare_response(
|
||||||
|
{"steps": baseline, "path_qa": {"overall_ok": True, "quality_score": 0.88}},
|
||||||
|
{
|
||||||
|
"steps": proposed,
|
||||||
|
"path_qa": {
|
||||||
|
"overall_ok": False,
|
||||||
|
"quality_score": 0.65,
|
||||||
|
"rematch_log": rematch_log,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert compare["slot_diffs_source"] == "rematch_log"
|
||||||
|
assert compare["slot_diff_count"] == 1
|
||||||
|
assert compare["proposed_steps"][0]["exercise_id"] == 102
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
||||||
| Überblick DB | `.claude/docs/technical/DATABASE_SCHEMA.md` |
|
| Überblick DB | `.claude/docs/technical/DATABASE_SCHEMA.md` |
|
||||||
| Domäne | `.claude/docs/functional/DOMAIN_MODEL.md` |
|
| Domäne | `.claude/docs/functional/DOMAIN_MODEL.md` |
|
||||||
| **Gewichtetes Fähigkeiten-Scoring (Phase 3)** | `.claude/docs/technical/SKILL_SCORING_SPEC.md` |
|
| **Gewichtetes Fähigkeiten-Scoring (Phase 3)** | `.claude/docs/technical/SKILL_SCORING_SPEC.md` |
|
||||||
|
| **Planungs-KI — Katalog-Prompt-Snippets (H1)** | **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** |
|
||||||
| **Lieferliste inkl. Medien & Formular-UX** | `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §6, §16 |
|
| **Lieferliste inkl. Medien & Formular-UX** | `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §6, §16 |
|
||||||
| **Fachlicher Nutzerüberblick (Design/Product)** | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
|
| **Fachlicher Nutzerüberblick (Design/Product)** | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
|
||||||
|
|
||||||
|
|
@ -113,8 +114,9 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
||||||
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ **0.8.231–0.8.232** |
|
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ **0.8.231–0.8.232** |
|
||||||
| **F13** | **`planning_catalog_context`** (Fokus/Stil/TT/ZG) im Match + Graph-Artefakt | ✅ **0.8.233** |
|
| **F13** | **`planning_catalog_context`** (Fokus/Stil/TT/ZG) im Match + Graph-Artefakt | ✅ **0.8.233** |
|
||||||
| **F14** | **`ProgressionGraphEditor`** — Slot-UI + Planungskontext-Dropdowns | ✅ **0.8.233** |
|
| **F14** | **`ProgressionGraphEditor`** — Slot-UI + Planungskontext-Dropdowns | ✅ **0.8.233** |
|
||||||
|
| **H1** | Katalog-Prompt-Snippets (modulare LLM-Anweisungen) | 🔲 Spec **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** |
|
||||||
|
|
||||||
**Architektur (verbindlich):** Drei Schichten — (1) **Katalog-Dimensionen** (DB, jetzt im Match verdrahtet), (2) **Technik-Disambiguierung** (Code, nur bei `topic_type=technique`), (3) **Didaktik** (Roadmap + LLM-QS, nicht im Vokabular). Progressionsgraph = **Roadmap-first**, **keine Gruppenanalyse**. Bestehender Graph = **leichter Nachfolger-Bias** ab Schritt 2. Trainingsplanung = **eigene Pipeline** (Phase G) — Wiederverwendung der Bausteine, siehe Ist-Doku §16.
|
**Architektur (verbindlich):** Drei Schichten — (1) **Katalog-Dimensionen** (DB, jetzt im Match verdrahtet; **H1:** zusätzlich Prompt-Snippets), (2) **Technik-Disambiguierung** (Code, nur bei `topic_type=technique`), (3) **Didaktik** (Roadmap + LLM-QS, nicht im Vokabular). Progressionsgraph = **Roadmap-first**, **keine Gruppenanalyse**. Bestehender Graph = **leichter Nachfolger-Bias** ab Schritt 2. Trainingsplanung = **eigene Pipeline** (Phase G) — Wiederverwendung der Bausteine, siehe Ist-Doku §16.
|
||||||
|
|
||||||
**Validierung (Mae Geri, Härtetest):** Pfad-QS vor Optimierung ~65 % → nach Trainer-Roadmap + KI-Gap-Fill **~88 % OK**. Workbench ist **universell** gedacht; Mae Geri war Referenzfall, kein Sonder-Patch.
|
**Validierung (Mae Geri, Härtetest):** Pfad-QS vor Optimierung ~65 % → nach Trainer-Roadmap + KI-Gap-Fill **~88 % OK**. Workbench ist **universell** gedacht; Mae Geri war Referenzfall, kein Sonder-Patch.
|
||||||
|
|
||||||
|
|
@ -267,7 +269,8 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
||||||
|
|
||||||
### Planungs-KI (priorisiert)
|
### Planungs-KI (priorisiert)
|
||||||
|
|
||||||
1. **Dev-Regression:** Katalog-Match für Gewaltschutz, Breitensport, Kinder — nicht nur Mae-Geri-Härtetest.
|
1. **H1 Katalog-Prompt-Snippets:** modulare LLM-Anweisungen — **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** (Priorität: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung).
|
||||||
|
2. **Dev-Regression:** Katalog-Match für Gewaltschutz, Breitensport, Kinder — nicht nur Mae-Geri-Härtetest.
|
||||||
2. **PathBuilder-Parität:** `planning_catalog_context`-Dropdowns auch in `ExerciseProgressionPathBuilder`.
|
2. **PathBuilder-Parität:** `planning_catalog_context`-Dropdowns auch in `ExerciseProgressionPathBuilder`.
|
||||||
3. **QS-UI:** positive LLM-Empfehlungen als Highlights statt nur Optimierungspotenziale.
|
3. **QS-UI:** positive LLM-Empfehlungen als Highlights statt nur Optimierungspotenziale.
|
||||||
4. **UI-Wizard:** 4 Schritte (Ziel & Katalog → Roadmap → Match → Lücken); Backend-Pipeline unverändert.
|
4. **UI-Wizard:** 4 Schritte (Ziel & Katalog → Roadmap → Match → Lücken); Backend-Pipeline unverändert.
|
||||||
|
|
|
||||||
229
docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md
Normal file
229
docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
# Planungs-KI — Katalog-Snippets für modulare Prompts
|
||||||
|
|
||||||
|
**Stand:** 2026-05-22
|
||||||
|
**Status:** Spezifikation (Phase **H1** — Umsetzung offen)
|
||||||
|
**Bezüge:** `PLANNING_PROGRESSION_GRAPH_KI.md` §4.4 · `AI_PROMPT_TARGET_ARCHITECTURE.md` §2.4 · `planning_catalog_context.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problem
|
||||||
|
|
||||||
|
Seit **F13 (0.8.233)** fließen Primärfokus, Trainingsstil, Zielgruppe und Stilrichtung als `planning_catalog_context` in **Retrieval und Scoring** (`PlanningTargetProfile`).
|
||||||
|
|
||||||
|
Die **LLM-Prompts** (Roadmap, Stufen-Spec, Pfad-QS, Gap-Fill) erhalten diese Dimensionen **nicht** als differenzierte Bewertungs- und Planungslogik — höchstens indirekt über Freitext oder JSON-Kataloglisten in Intent-Prompts.
|
||||||
|
|
||||||
|
**Folge:** Ein Breitensport-Pfad kann mit Leistungsgruppen-Kriterien bewertet werden; Gewaltschutz wird wie Technik-Curriculum behandelt; QS-Hinweise und Rematch-Vorschläge passen fachlich nicht zum gewählten Kontext.
|
||||||
|
|
||||||
|
**Ziel:** Gleiche **Prompt-Basis**, aber **kaskadierte Snippet-Blöcke** pro Katalog-Ausprägung — keine Matrix aus Voll-Prompt-Kopien.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Priorität der Dimensionen (absteigend)
|
||||||
|
|
||||||
|
Verbindliche Reihenfolge bei Konflikten und beim Rollout:
|
||||||
|
|
||||||
|
| Rang | Dimension | DB-Tabelle | Snippet-Rolle |
|
||||||
|
|------|-----------|------------|----------------|
|
||||||
|
| **1** | **Primärfokus** | `focus_areas` | Definiert *worüber* geplant und bewertet wird (Technik-Curriculum vs. Gewaltschutz vs. Fitness …). **Dominant.** |
|
||||||
|
| **2** | **Trainingsstil** | `training_types` | Definiert *wie* trainiert wird (Methodik, Belastungsaufbau, Wettkampf vs. Breitenansatz im Training). |
|
||||||
|
| **3** | **Zielgruppe** | `target_groups` | Definiert *für wen* (Kinder, Breitensport, Leistungsgruppe) — Tempo, Komplexität, Sicherheit. |
|
||||||
|
| **4** | **Stilrichtung** | `style_directions` | Vereins-/Stil-Linie (Shotokan, WKF …) — **Nuancen**, kein neuer Planungstyp. |
|
||||||
|
|
||||||
|
**Kaskaden-Regel:** Bei widersprüchlichen Snippet-Aussagen gilt: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architektur — drei Schichten (Erinnerung)
|
||||||
|
|
||||||
|
| Schicht | Heute | Mit H1 |
|
||||||
|
|---------|-------|--------|
|
||||||
|
| **Retrieval** | Katalog-Gewichte in `merge_catalog_context_into_target` | unverändert |
|
||||||
|
| **Technik-Gates** | `planning_exercise_semantics.py`, nur `topic_type=technique` | unverändert |
|
||||||
|
| **LLM-Prompts** | kaum Katalog-spezifisch | **`catalog_guidance_block`** pro Aufruf |
|
||||||
|
|
||||||
|
Snippets **ersetzen** keine Technik-Disambiguierung und **duplizieren** keine Retrieval-Gewichte — sie steuern **Didaktik, Bewertungsmaßstäbe und Formulierung** für das Modell.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Snippet-Modell
|
||||||
|
|
||||||
|
### 4.1 Lookup-Schlüssel
|
||||||
|
|
||||||
|
Pro Katalog-Eintrag ein stabiler **`snippet_key`** (nicht nur numerische ID — IDs können sich in Dev/Import unterscheiden):
|
||||||
|
|
||||||
|
```
|
||||||
|
focus:{slug} z. B. focus:gewaltschutz
|
||||||
|
training_type:{slug} z. B. training_type:kumite
|
||||||
|
target_group:{slug} z. B. target_group:breitensport
|
||||||
|
style:{slug} z. B. style:shotokan
|
||||||
|
```
|
||||||
|
|
||||||
|
**Primär** aus `slug` der DB-Zeile; Fallback normalisierter `name` (ASCII, lower, `_`).
|
||||||
|
|
||||||
|
Mehrfachauswahl im UI: pro Dimension **höchstens ein Snippet** — die Zeile mit `is_primary: true`, sonst erste Zeile, sonst höchste `weight`.
|
||||||
|
|
||||||
|
### 4.2 Snippet-Inhalt (Struktur)
|
||||||
|
|
||||||
|
Jedes Snippet liefert strukturierte Textbausteine (Deutsch, für LLM):
|
||||||
|
|
||||||
|
| Feld | Pflicht | Inhalt |
|
||||||
|
|------|---------|--------|
|
||||||
|
| `planning_lens` | ja | 2–4 Sätze: Was ist das Planungsziel in dieser Dimension? |
|
||||||
|
| `qa_criteria` | ja | Bullet-artige Kriterien für Pfad-QS (was ist „gut“, was ist kein Mangel) |
|
||||||
|
| `roadmap_hints` | empfohlen | Stufenlogik, typische Phasen, was vermeiden |
|
||||||
|
| `anti_patterns` | optional | Explizite Fehlbewertungen vermeiden (z. B. „keine Wettkampf-Tiefe verlangen“) |
|
||||||
|
| `rematch_guard` | optional | Wann **kein** Auto-Rematch sinnvoll (Breitensport: keine Perfektions-Slots erzwingen) |
|
||||||
|
|
||||||
|
Phase **H1:** flache Markdown-Strings im Code-Modul.
|
||||||
|
Phase **H2 (optional):** Tabelle `planning_catalog_prompt_snippets` oder JSONB an Katalog-Zeilen, Admin-editierbar.
|
||||||
|
|
||||||
|
### 4.3 Platzhalter in `ai_prompts`
|
||||||
|
|
||||||
|
Neue **gemeinsame** Platzhalter (Mustache), in alle betroffenen Prompts einfügen:
|
||||||
|
|
||||||
|
| Platzhalter | Bedeutung |
|
||||||
|
|-------------|-----------|
|
||||||
|
| `{{catalog_guidance_block}}` | Gerenderter Gesamttext (alle aktiven Snippets, kaskadiert) |
|
||||||
|
| `{{catalog_context_json}}` | Kompakte JSON-Zusammenfassung der gewählten IDs/Namen (Audit) |
|
||||||
|
| `{{#has_catalog_guidance}}` … `{{/has_catalog_guidance}}` | Block nur wenn mindestens ein Snippet aktiv |
|
||||||
|
|
||||||
|
**Optional fein (später):** `{{catalog_focus_snippet}}`, `{{catalog_training_type_snippet}}`, … — Phase H1 nur `_block` + JSON.
|
||||||
|
|
||||||
|
### 4.4 Betroffene Prompt-Slugs (Reihenfolge Einbindung)
|
||||||
|
|
||||||
|
| Priorität | Slug | Migration | Wirkung |
|
||||||
|
|-----------|------|-----------|---------|
|
||||||
|
| 1 | `planning_exercise_path_qa` | bestehend | Pfad-QS, `quality_score`, Empfehlungen |
|
||||||
|
| 2 | `planning_progression_roadmap` | 078 | Major Steps, Didaktik |
|
||||||
|
| 3 | `planning_progression_stage_spec` | 079 | Stufen-Gates, Erfolgskriterien |
|
||||||
|
| 4 | `planning_progression_start_target` | 087 | Start/Ziel-Extraktion |
|
||||||
|
| 5 | `planning_progression_goal_analysis` | 078 | Zielanalyse |
|
||||||
|
| 6 | Gap-Fill / Übungs-KI | 085+ | `planning_context` ergänzen |
|
||||||
|
|
||||||
|
Intent-Prompts (`planning_exercise_search_intent`, …) **optional** Phase H1.5 — dort bereits Katalog-JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Builder (Backend)
|
||||||
|
|
||||||
|
**Neues Modul:** `backend/planning_catalog_prompt_snippets.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def build_catalog_guidance_for_prompt(
|
||||||
|
cur,
|
||||||
|
catalog: Optional[ProgressionPlanningCatalogContext],
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
catalog_guidance_block: str
|
||||||
|
catalog_context_json: str
|
||||||
|
has_catalog_guidance: bool
|
||||||
|
snippet_keys: list[str] # Metadaten für Logs/Tests
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
|
||||||
|
1. `catalog` aus Request oder `planning_roadmap.planning_catalog_context` (wie F13).
|
||||||
|
2. Pro Dimension aktives Item auflösen → `snippet_key` → Text aus Registry.
|
||||||
|
3. Snippets in **Prioritätsreihenfolge** §2 zu `_block` zusammenfügen (Überschriften: „Primärfokus“, „Trainingsstil“, …).
|
||||||
|
4. Fehlende Snippets: Dimension **weglassen** (kein Default-Text) — besser kein Snippet als falscher.
|
||||||
|
|
||||||
|
**Einbindung:** Orchestratoren rufen Builder auf und mergen in `variables` vor `load_and_render_ai_prompt`:
|
||||||
|
|
||||||
|
- `planning_exercise_path_qa.py` → `try_llm_qa_progression_path`
|
||||||
|
- `planning_progression_roadmap.py` (Roadmap-/Stage-Pipeline)
|
||||||
|
- `planning_exercise_path_builder.py` (catalog an QA/Match durchreichen)
|
||||||
|
|
||||||
|
`ProgressionPathSuggestRequest` trägt `planning_catalog_context` bereits — kein neues API-Feld nötig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Beispiel-Snippets (Review-Entwurf)
|
||||||
|
|
||||||
|
### 6.1 Primärfokus — Gewaltschutz (`focus:gewaltschutz`)
|
||||||
|
|
||||||
|
**planning_lens:** Planung zielt auf Prävention, Deeskalation, Grenzen und sichere Übungsformen — nicht auf Wettkampf-Perfektion oder Technik-Show.
|
||||||
|
|
||||||
|
**qa_criteria:** Gute Pfade bauen Sicherheit, Kommunikation und Alternativen auf; „Lücken“ sind fehlende Deeskalations- oder Rollenspiel-Stufen, nicht fehlende Kick-Varianten.
|
||||||
|
|
||||||
|
**anti_patterns:** Nicht nach Kumite-Tiefe, Explosivität oder Wettkampf-Belastung bewerten.
|
||||||
|
|
||||||
|
### 6.2 Primärfokus — Technik / Kumite-Beinarbeit (`focus:kumite` o. ä.)
|
||||||
|
|
||||||
|
**planning_lens:** Curriculum für eine Technik oder Kumite-Teilaspekt; aufeinander aufbauende Belastung und Anwendungsnähe sind erwünscht.
|
||||||
|
|
||||||
|
**qa_criteria:** Kohärente Progression Grundlagen → Anwendung → Vertiefung; Übergänge ohne Sprünge; themenfremde Kraft-/Ausdauer-Inseln abwerten.
|
||||||
|
|
||||||
|
### 6.3 Trainingsstil — Breitensport (`training_type:breitensport` o. Name-Match)
|
||||||
|
|
||||||
|
**planning_lens:** Partizipation, Verständlichkeit, Freude am Bewegen; weniger maximale Spezialisierung.
|
||||||
|
|
||||||
|
**qa_criteria:** Hohe OK-Rate bei moderatem Schwierigkeitsanstieg; „Perfektion“-Stufen nur optional, nicht als Pflicht-Lücke.
|
||||||
|
|
||||||
|
**rematch_guard:** Keine leeren Slots erzwingen, nur um eine Leistungs-Perfektionsstufe zu füllen.
|
||||||
|
|
||||||
|
### 6.4 Zielgruppe — Leistungsgruppe (`target_group:leistungsgruppe`)
|
||||||
|
|
||||||
|
**qa_criteria:** Höhere Anspruchskurven, Belastungs- und Kombinationsprogressionen sind relevant; Lücken in Spezialisierung können echte Hinweise sein.
|
||||||
|
|
||||||
|
*(Weitere Snippets iterativ ergänzen — nicht alle Katalog-Zeilen sofort.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Rollout-Phasen
|
||||||
|
|
||||||
|
### H1 — Minimal viable (Progressionsgraph)
|
||||||
|
|
||||||
|
- [ ] Modul `planning_catalog_prompt_snippets.py` + Registry (5–8 Snippets: 2–3 Foki, 2 Trainingsstile, 2 Zielgruppen)
|
||||||
|
- [ ] Einbindung in **`planning_exercise_path_qa`** + **`planning_progression_roadmap`** + **`planning_progression_stage_spec`**
|
||||||
|
- [ ] Migration Prompt-Templates: Abschnitt `{{#has_catalog_guidance}}…{{/has_catalog_guidance}}`
|
||||||
|
- [ ] Tests: gleicher Pfad + unterschiedlicher Katalog → unterschiedlicher `catalog_guidance_block`; Snapshot QA-Variablen
|
||||||
|
- [ ] Dev-Regression: Gewaltschutz, Breitensport, Kinder — **Hinweistexte** müssen zum Kontext passen (nicht Mae-Geri-Kriterien)
|
||||||
|
|
||||||
|
### H1.5
|
||||||
|
|
||||||
|
- [ ] Rematch/Refine: `rematch_guard` aus Snippets respektieren (weniger Ping-Pong bei Breitensport)
|
||||||
|
- [ ] Intent-Prompts + Gap-Fill-Kontext
|
||||||
|
|
||||||
|
### H2 — Betrieb
|
||||||
|
|
||||||
|
- [ ] Snippets in DB, Admin-UI oder Markdown-Import
|
||||||
|
- [ ] Versionierung / Audit wie `ai_prompts`
|
||||||
|
|
||||||
|
### H3 — Phase G (Trainingsplanung)
|
||||||
|
|
||||||
|
- [ ] Gleicher Builder, anderer Orchestrator (Abschnitts-QS, Slot-Suggest)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Tests & Akzeptanz
|
||||||
|
|
||||||
|
| Test | Erwartung |
|
||||||
|
|------|-----------|
|
||||||
|
| `test_catalog_prompt_snippets_priority` | Bei Konflikt gewinnt Fokus-Snippet-Text in `_block`-Reihenfolge |
|
||||||
|
| `test_path_qa_variables_include_guidance` | Mit Gewaltschutz-Kontext enthält gerendeter Prompt „Deeskalation“ o. ä., nicht „Kumite-Perfektion“ |
|
||||||
|
| `test_path_qa_no_snippet_without_catalog` | Ohne Katalog: `has_catalog_guidance=false`, Prompt unverändert wie heute |
|
||||||
|
| Manuell | Mae-Geri-Pfad + Breitensport-Kontext: QS-Highlights ohne Leistungs-Belastungs-Forderungen |
|
||||||
|
|
||||||
|
**Nicht Ziel von H1:** Retrieval-Gewichte neu kalibrieren; Technik-Tuples externalisieren (separates Backlog **H** in Roadmap).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Abgrenzung zu anderen Fixes
|
||||||
|
|
||||||
|
| Thema | Dokument / Fix |
|
||||||
|
|-------|----------------|
|
||||||
|
| 88 % vs. 65 % falsche Pipeline | Evaluate-only für Pfad-QS; fairer Compare — Code-Stand 2026-05-22 |
|
||||||
|
| Triviale ID-Tausche im Dialog | `_filter_trivial_slot_diffs` |
|
||||||
|
| Katalog nur im Retrieval | F13 — bleibt, Snippets ergänzen LLM-Schicht |
|
||||||
|
|
||||||
|
Snippets lösen **fachliche Fehlbewertung** — nicht Pipeline-Inkonsistenz allein.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Changelog
|
||||||
|
|
||||||
|
| Datum | Änderung |
|
||||||
|
|-------|----------|
|
||||||
|
| 2026-05-22 | Erstfassung — Priorität Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung; H1–H3 Rollout |
|
||||||
|
|
@ -156,6 +156,20 @@ Details: **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16 · Domäne **`DOMAIN_MODEL.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase H1 — Katalog-Prompt-Snippets (Spez geplant)
|
||||||
|
|
||||||
|
**Spec:** **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`**
|
||||||
|
|
||||||
|
Modulare Textbausteine pro Katalog-Ausprägung in LLM-Prompts (Roadmap, Pfad-QS, Stufen-Spec) — **nicht** neue Retrieval-Welt.
|
||||||
|
|
||||||
|
**Priorität (absteigend):** Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung.
|
||||||
|
|
||||||
|
- [ ] `planning_catalog_prompt_snippets.py` + Registry (5–8 Snippets)
|
||||||
|
- [ ] Platzhalter `{{catalog_guidance_block}}` in Pfad-QS + Roadmap-Prompts
|
||||||
|
- [ ] Dev-Regression: Gewaltschutz / Breitensport / Kinder — QS-Hinweise passend zum Kontext
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase H — Plattform (Backlog)
|
## Phase H — Plattform (Backlog)
|
||||||
|
|
||||||
- [ ] Technik-Disambiguierung konfigurierbar (DB statt `_GERI_TECHNIQUES` in Code)
|
- [ ] Technik-Disambiguierung konfigurierbar (DB statt `_GERI_TECHNIQUES` in Code)
|
||||||
|
|
@ -167,7 +181,7 @@ Details: **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16 · Domäne **`DOMAIN_MODEL.
|
||||||
|
|
||||||
| Von | Nach | Hinweis |
|
| Von | Nach | Hinweis |
|
||||||
|-----|------|---------|
|
|-----|------|---------|
|
||||||
| F13 | G0 | Katalog-Kontext in Einheitsplanung |
|
| F13 | G0, **H1** | Katalog-Kontext in Einheitsplanung; Snippets in LLM-Prompts |
|
||||||
| F7, F11 | G1, G2 | Skill-Expectations + QS-Muster |
|
| F7, F11 | G1, G2 | Skill-Expectations + QS-Muster |
|
||||||
| F4, F9 | G3 | Graph-Roadmap kann Rahmen referenzieren |
|
| F4, F9 | G3 | Graph-Roadmap kann Rahmen referenzieren |
|
||||||
| G | H | Workflow-Engine lohnt bei verzweigten Planungsflows |
|
| G | H | Workflow-Engine lohnt bei verzweigten Planungsflows |
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
`.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md` (Zielarchitektur) ·
|
`.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md` (Zielarchitektur) ·
|
||||||
`.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md` (Retrieval/Scoring) ·
|
`.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md` (Retrieval/Scoring) ·
|
||||||
`.claude/docs/technical/SKILL_SCORING_SPEC.md` (Fähigkeiten-Scoring) ·
|
`.claude/docs/technical/SKILL_SCORING_SPEC.md` (Fähigkeiten-Scoring) ·
|
||||||
|
**`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** (H1 — modulare Katalog-Prompts) ·
|
||||||
`docs/architecture/PLANNING_KI_ROADMAP.md` (Produkt-Roadmap Phase G+)
|
`docs/architecture/PLANNING_KI_ROADMAP.md` (Produkt-Roadmap Phase G+)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -183,6 +184,8 @@ Shinkan unterscheidet **drei Schichten** (kein monolithisches „Vokabular“):
|
||||||
|
|
||||||
**Seit 0.8.233:** `planning_catalog_context` im Request und im Graph-Artefakt (`planning_catalog_context` JSON). Fließt in `PlanningTargetProfile` → Hybrid-Retrieval (`score_exercise_against_target`: „Fokusbereich passend“, …). Zusätzlich additive Text-Signale aus Anfrage + Start/Ziel + Notizen (`planning_exercise_text_signals`).
|
**Seit 0.8.233:** `planning_catalog_context` im Request und im Graph-Artefakt (`planning_catalog_context` JSON). Fließt in `PlanningTargetProfile` → Hybrid-Retrieval (`score_exercise_against_target`: „Fokusbereich passend“, …). Zusätzlich additive Text-Signale aus Anfrage + Start/Ziel + Notizen (`planning_exercise_text_signals`).
|
||||||
|
|
||||||
|
**Geplant (H1):** dieselben Dimensionen als **kaskadierte Prompt-Snippets** in Roadmap-, Stufen-Spec- und Pfad-QS-Prompts — Priorität Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung — siehe **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`**.
|
||||||
|
|
||||||
**Technik-Gates** (`technique_scope`, Geschwister-Ausschluss) nur bei `topic_type == "technique"` — Fokus-Pfade (Gewaltschutz, Fitness, …) werden nicht wie Mae-Geri-Pfade behandelt.
|
**Technik-Gates** (`technique_scope`, Geschwister-Ausschluss) nur bei `topic_type == "technique"` — Fokus-Pfade (Gewaltschutz, Fitness, …) werden nicht wie Mae-Geri-Pfade behandelt.
|
||||||
|
|
||||||
Fallback: fehlt `planning_catalog_context` im Request, wird aus gespeichertem `planning_roadmap` am Graph geladen.
|
Fallback: fehlt `planning_catalog_context` im Request, wird aus gespeichertem `planning_roadmap` am Graph geladen.
|
||||||
|
|
@ -376,6 +379,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js`
|
||||||
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ | 0.8.231–0.8.232 |
|
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ | 0.8.231–0.8.232 |
|
||||||
| **F13** | **Katalog-Kontext** (`planning_catalog_context`) im Match + Graph-Artefakt | ✅ | **0.8.233** |
|
| **F13** | **Katalog-Kontext** (`planning_catalog_context`) im Match + Graph-Artefakt | ✅ | **0.8.233** |
|
||||||
| **F14** | `ProgressionGraphEditor` Slot-UI + Planungskontext-Dropdowns | ✅ | 0.8.233 |
|
| **F14** | `ProgressionGraphEditor` Slot-UI + Planungskontext-Dropdowns | ✅ | 0.8.233 |
|
||||||
|
| **H1** | **Katalog-Prompt-Snippets** — modulare LLM-Anweisungen pro Dimension | 🔲 | Spec **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** |
|
||||||
| **G** | Trainingsplanung: eigene Pipeline + Wiederverwendung Bausteine (§16) | 🔲 | — |
|
| **G** | Trainingsplanung: eigene Pipeline + Wiederverwendung Bausteine (§16) | 🔲 | — |
|
||||||
| **UX** | Wizard/Stepper; PathBuilder-Parität Katalog | 🔲 | — |
|
| **UX** | Wizard/Stepper; PathBuilder-Parität Katalog | 🔲 | — |
|
||||||
| **H** | Technik-Disambiguierung konfigurierbar (DB statt Code-Tuples) | 🔲 | Backlog |
|
| **H** | Technik-Disambiguierung konfigurierbar (DB statt Code-Tuples) | 🔲 | Backlog |
|
||||||
|
|
@ -385,7 +389,8 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js`
|
||||||
|
|
||||||
## 12. Offenes Backlog (priorisiert)
|
## 12. Offenes Backlog (priorisiert)
|
||||||
|
|
||||||
1. **Dev-Regression:** Gewaltschutz + Breitensport + Kinder (ohne Mae Geri) — Katalog-Match verifizieren
|
1. **H1 Katalog-Prompt-Snippets** — modulare LLM-Anweisungen (Priorität: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung) — **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`**
|
||||||
|
2. **Dev-Regression:** Gewaltschutz + Breitensport + Kinder (ohne Mae Geri) — Katalog-Match verifizieren
|
||||||
2. **PathBuilder-Parität** — gleiche `planning_catalog_context`-Dropdowns in `ExerciseProgressionPathBuilder`
|
2. **PathBuilder-Parität** — gleiche `planning_catalog_context`-Dropdowns in `ExerciseProgressionPathBuilder`
|
||||||
3. **QS-UI** — positive LLM-Hinweise als „Highlights“, nicht als „Optimierungspotenziale“
|
3. **QS-UI** — positive LLM-Hinweise als „Highlights“, nicht als „Optimierungspotenziale“
|
||||||
4. **UI-Wizard** — 4 Schritte (Ziel → Roadmap → Match → Lücken); Backend unverändert
|
4. **UI-Wizard** — 4 Schritte (Ziel → Roadmap → Match → Lücken); Backend unverändert
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ function severityStyle(pathQa) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function PathQaPipelineDetails({ pathQa, draft, title, compact = false }) {
|
function PathQaPipelineDetails({ pathQa, fairQa = null, draft, title, compact = false }) {
|
||||||
const { fixHints: optimizationHints } = useMemo(
|
const { fixHints: optimizationHints } = useMemo(
|
||||||
() => splitPathQaHints(pathQa),
|
() => splitPathQaHints(pathQa),
|
||||||
[pathQa],
|
[pathQa],
|
||||||
|
|
@ -34,7 +34,7 @@ function PathQaPipelineDetails({ pathQa, draft, title, compact = false }) {
|
||||||
const rematchLog = Array.isArray(pathQa?.rematch_log) ? pathQa.rematch_log : []
|
const rematchLog = Array.isArray(pathQa?.rematch_log) ? pathQa.rematch_log : []
|
||||||
const refineLog = Array.isArray(pathQa?.refine_log) ? pathQa.refine_log : []
|
const refineLog = Array.isArray(pathQa?.refine_log) ? pathQa.refine_log : []
|
||||||
const qaTiers = Array.isArray(pathQa?.qa_tiers) ? pathQa.qa_tiers : []
|
const qaTiers = Array.isArray(pathQa?.qa_tiers) ? pathQa.qa_tiers : []
|
||||||
const qualityPct = pathQaQualityPercent(pathQa)
|
const qualityPct = pathQaQualityPercent(fairQa || pathQa)
|
||||||
const hasContent =
|
const hasContent =
|
||||||
qaTiers.length > 0
|
qaTiers.length > 0
|
||||||
|| (pathQa?.rematch_applied && rematchLog.length > 0)
|
|| (pathQa?.rematch_applied && rematchLog.length > 0)
|
||||||
|
|
@ -53,10 +53,17 @@ function PathQaPipelineDetails({ pathQa, draft, title, compact = false }) {
|
||||||
background: 'color-mix(in srgb, var(--accent) 6%, var(--surface2))',
|
background: 'color-mix(in srgb, var(--accent) 6%, var(--surface2))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong style={{ fontSize: compact ? '11px' : '12px' }}>
|
<strong style={{ fontSize: compact ? '11px' : '12px' }}>
|
||||||
{title}
|
{title}
|
||||||
{qualityPct != null ? ` · ${pathQa.overall_ok ? 'OK' : 'Hinweise'} (${qualityPct} %)` : ''}
|
{qualityPct != null
|
||||||
|
? ` · ${(fairQa || pathQa)?.overall_ok ? 'OK' : 'Hinweise'} (${qualityPct} % fair bewertet)`
|
||||||
|
: ''}
|
||||||
</strong>
|
</strong>
|
||||||
|
{fairQa && pathQa && pathQaQualityPercent(pathQa) !== qualityPct ? (
|
||||||
|
<p style={{ margin: '4px 0 0', fontSize: '10px', color: 'var(--text3)' }}>
|
||||||
|
Rematch-Protokoll (Pipeline-Score {pathQaQualityPercent(pathQa) ?? '—'} %) — nur Prozessinfo, nicht Pfad-QS.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
{qaTiers.length > 0 ? (
|
{qaTiers.length > 0 ? (
|
||||||
<ul style={{ margin: '6px 0 0', paddingLeft: '16px', color: 'var(--text2)', fontSize: '11px' }}>
|
<ul style={{ margin: '6px 0 0', paddingLeft: '16px', color: 'var(--text2)', fontSize: '11px' }}>
|
||||||
{qaTiers.map((tier) => (
|
{qaTiers.map((tier) => (
|
||||||
|
|
@ -282,6 +289,7 @@ export default function ProgressionFindingsPanel({
|
||||||
onRematchSlots = null,
|
onRematchSlots = null,
|
||||||
onOptimizeCompare = null,
|
onOptimizeCompare = null,
|
||||||
optimizationPreviewQa = null,
|
optimizationPreviewQa = null,
|
||||||
|
optimizationPreviewFairQa = null,
|
||||||
canOptimizeCompare = false,
|
canOptimizeCompare = false,
|
||||||
optimizeCompareBusy = false,
|
optimizeCompareBusy = false,
|
||||||
rematchBusy = false,
|
rematchBusy = false,
|
||||||
|
|
@ -509,8 +517,9 @@ export default function ProgressionFindingsPanel({
|
||||||
{optimizationPreviewQa ? (
|
{optimizationPreviewQa ? (
|
||||||
<PathQaPipelineDetails
|
<PathQaPipelineDetails
|
||||||
pathQa={optimizationPreviewQa}
|
pathQa={optimizationPreviewQa}
|
||||||
|
fairQa={optimizationPreviewFairQa}
|
||||||
draft={draft}
|
draft={draft}
|
||||||
title="3-Stufen-Optimierung (Vorschlag nach Match)"
|
title="3-Stufen-Optimierung (Vorschlag — nur im Vergleichsdialog)"
|
||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@ import {
|
||||||
applyGapOfferToDraft,
|
applyGapOfferToDraft,
|
||||||
applyMatchResponseToDraft,
|
applyMatchResponseToDraft,
|
||||||
applySelectedCompareSteps,
|
applySelectedCompareSteps,
|
||||||
compareResponseHasCuratedSlotChanges,
|
compareResponseHasActionableSlotChanges,
|
||||||
compareResponseHasSlotChanges,
|
compareResponseHadRematchWithoutActionableDiffs,
|
||||||
curatedSlotDiffs,
|
compareSlotDiffs,
|
||||||
pathQaQualityPercent,
|
pathQaQualityPercent,
|
||||||
applyResolvedStructuredToDraft,
|
applyResolvedStructuredToDraft,
|
||||||
buildPlanningArtifactFromDraft,
|
buildPlanningArtifactFromDraft,
|
||||||
|
|
@ -505,36 +505,38 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
setTargetSummary(res?.target_profile_summary || null)
|
setTargetSummary(res?.target_profile_summary || null)
|
||||||
const baselineQa = res?.baseline_path_qa || null
|
const baselineQa = res?.baseline_path_qa || null
|
||||||
const proposedQa = res?.proposed_path_qa || res?.path_qa || null
|
const proposedQa = res?.proposed_path_qa || res?.path_qa || null
|
||||||
const pipelineQa = res?.proposed_path_qa_pipeline || null
|
const actionableCount =
|
||||||
setPathQa(baselineQa)
|
res?.slot_diff_count ?? compareSlotDiffs(res, { actionableOnly: true }).length
|
||||||
setProposedPathQa(pipelineQa)
|
|
||||||
|
|
||||||
const openCompareDialog = (diffCount, noticePrefix) => {
|
const openCompareDialog = (diffCount, noticePrefix) => {
|
||||||
setComparePayload(res)
|
setComparePayload(res)
|
||||||
|
setProposedPathQa(res?.proposed_path_qa_pipeline || null)
|
||||||
setCompareOpen(true)
|
setCompareOpen(true)
|
||||||
const bPct = pathQaQualityPercent(baselineQa)
|
const bPct = pathQaQualityPercent(baselineQa)
|
||||||
const pPct = pathQaQualityPercent(proposedQa)
|
const pPct = pathQaQualityPercent(proposedQa)
|
||||||
let notice = `${noticePrefix}${diffCount} Slot-Anpassung(en) — Vergleichsdialog geöffnet.`
|
let notice = diffCount > 0
|
||||||
|
? `${noticePrefix}${diffCount} Slot-Anpassung(en) — Vergleichsdialog geöffnet.`
|
||||||
|
: 'Vergleichsdialog geöffnet — keine übernehmbaren Slot-Änderungen im End-Pfad.'
|
||||||
if (bPct != null && pPct != null && pPct !== bPct) {
|
if (bPct != null && pPct != null && pPct !== bPct) {
|
||||||
notice += ` QS ${bPct} % → ${pPct} %.`
|
notice += ` Vorschlag fair bewertet: ${bPct} % → ${pPct} %.`
|
||||||
}
|
}
|
||||||
setMatchNotice(notice)
|
setMatchNotice(notice)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source === 'match') {
|
if (source === 'match') {
|
||||||
if (compareResponseHasCuratedSlotChanges(res)) {
|
if (compareResponseHasActionableSlotChanges(res)) {
|
||||||
openCompareDialog(curatedSlotDiffs(res).length, 'KI schlägt ')
|
openCompareDialog(actionableCount, 'KI schlägt ')
|
||||||
return { opened: true, res }
|
return { opened: true, res }
|
||||||
}
|
}
|
||||||
|
setProposedPathQa(null)
|
||||||
|
setComparePayload(null)
|
||||||
return { opened: false, res }
|
return { opened: false, res }
|
||||||
}
|
}
|
||||||
|
|
||||||
setComparePayload(res)
|
openCompareDialog(
|
||||||
setCompareOpen(true)
|
actionableCount,
|
||||||
if (compareResponseHasSlotChanges(res)) {
|
compareResponseHasActionableSlotChanges(res) ? 'KI schlägt ' : '',
|
||||||
const diffCount = res.slot_diff_count ?? res.slot_diffs?.length ?? 0
|
)
|
||||||
openCompareDialog(diffCount, 'KI schlägt ')
|
|
||||||
}
|
|
||||||
return { opened: true, res }
|
return { opened: true, res }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -565,31 +567,25 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { draft: matched, remainingOffers } = applyMatchResponseToDraft(
|
const evalRes = await fetchPathEvaluate(synced)
|
||||||
{
|
const { draft: evaluated, remainingOffers } = applyEvaluateResult(synced, evalRes)
|
||||||
...synced,
|
|
||||||
progressionRoadmap: compareRes?.progression_roadmap || synced.progressionRoadmap,
|
|
||||||
pathSkillExpectations:
|
|
||||||
compareRes?.path_skill_expectations || synced.pathSkillExpectations,
|
|
||||||
},
|
|
||||||
compareRes,
|
|
||||||
)
|
|
||||||
const syncedMatched = syncProgressionRoadmapFromSlots(matched)
|
|
||||||
const evalRes = await fetchPathEvaluate(syncedMatched)
|
|
||||||
const { draft: evaluated, remainingOffers: evalOffers } = applyEvaluateResult(
|
|
||||||
syncedMatched,
|
|
||||||
evalRes,
|
|
||||||
)
|
|
||||||
setDraft(evaluated)
|
setDraft(evaluated)
|
||||||
setGapFillOffers(evalOffers.length ? evalOffers : remainingOffers)
|
setGapFillOffers(remainingOffers)
|
||||||
|
setProposedPathQa(null)
|
||||||
|
setComparePayload(null)
|
||||||
const evalPct = pathQaQualityPercent(evalRes?.path_qa)
|
const evalPct = pathQaQualityPercent(evalRes?.path_qa)
|
||||||
const ms = compareRes?.match_summary
|
if (compareResponseHadRematchWithoutActionableDiffs(compareRes)) {
|
||||||
if (ms) {
|
setMatchNotice(
|
||||||
let notice = `Match: ${ms.library_matches ?? 0}/${ms.slot_count ?? '?'} Slots aus Bibliothek, ${ms.gap_fill_offer_count ?? 0} KI-Angebote.`
|
`Match: Auto-Rematch ohne übernehmbare End-Änderung — dein Pfad bleibt unverändert${
|
||||||
if (evalPct != null) {
|
evalPct != null ? ` (Pfad-QS ${evalPct} %).` : '.'
|
||||||
notice += ` Pfad-QS (Bewertung): ${evalPct} %.`
|
}`,
|
||||||
}
|
)
|
||||||
setMatchNotice(notice)
|
} else {
|
||||||
|
setMatchNotice(
|
||||||
|
`Match: Kein inhaltlicher Optimierungsvorschlag — Pfad unverändert${
|
||||||
|
evalPct != null ? ` (${evalPct} %).` : '.'
|
||||||
|
}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await saveProgressionGraphDraft(api, graphId, {
|
await saveProgressionGraphDraft(api, graphId, {
|
||||||
|
|
@ -1221,7 +1217,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
onGenerateGapAi={openGapFillPrep}
|
onGenerateGapAi={openGapFillPrep}
|
||||||
onRematchSlots={runMatch}
|
onRematchSlots={runMatch}
|
||||||
onOptimizeCompare={runOptimizeCompare}
|
onOptimizeCompare={runOptimizeCompare}
|
||||||
optimizationPreviewQa={proposedPathQa}
|
optimizationPreviewQa={compareOpen ? proposedPathQa : null}
|
||||||
|
optimizationPreviewFairQa={compareOpen ? comparePayload?.proposed_path_qa : null}
|
||||||
canOptimizeCompare={draftHasLibrarySlotAssignments(draft)}
|
canOptimizeCompare={draftHasLibrarySlotAssignments(draft)}
|
||||||
optimizeCompareBusy={comparing}
|
optimizeCompareBusy={comparing}
|
||||||
rematchBusy={matching}
|
rematchBusy={matching}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* Gegenüberstellung: bestehender Pfad vs. optimierter Match-Vorschlag.
|
* Gegenüberstellung: bestehender Pfad vs. optimierter Match-Vorschlag.
|
||||||
*/
|
*/
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { pathQaQualityPercent } from '../utils/progressionGraphDraft'
|
import { compareSlotDiffs, pathQaQualityPercent } from '../utils/progressionGraphDraft'
|
||||||
|
|
||||||
function qaLabel(pathQa) {
|
function qaLabel(pathQa) {
|
||||||
const pct = pathQaQualityPercent(pathQa)
|
const pct = pathQaQualityPercent(pathQa)
|
||||||
|
|
@ -18,7 +18,8 @@ export default function ProgressionOptimizeCompareModal({
|
||||||
onApplySelected,
|
onApplySelected,
|
||||||
applying = false,
|
applying = false,
|
||||||
}) {
|
}) {
|
||||||
const slotDiffs = Array.isArray(comparison?.slot_diffs) ? comparison.slot_diffs : []
|
const slotDiffs = compareSlotDiffs(comparison, { actionableOnly: true })
|
||||||
|
const trivialDiffs = (comparison?.slot_diffs || []).filter((d) => d?.trivial_id_swap)
|
||||||
const [selected, setSelected] = useState(() => new Set())
|
const [selected, setSelected] = useState(() => new Set())
|
||||||
|
|
||||||
const allKeys = useMemo(
|
const allKeys = useMemo(
|
||||||
|
|
@ -176,6 +177,19 @@ export default function ProgressionOptimizeCompareModal({
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{comparison?.slot_diffs_source === 'rematch_log' ? (
|
||||||
|
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '0 0 14px', lineHeight: 1.45 }}>
|
||||||
|
Vorschläge stammen aus dem Auto-Rematch-Protokoll (letzte Runde je Slot), weil der sichtbare
|
||||||
|
End-Pfad deinem aktuellen Stand entspricht.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{trivialDiffs.length > 0 ? (
|
||||||
|
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '0 0 14px', lineHeight: 1.45 }}>
|
||||||
|
{trivialDiffs.length} reine ID-Tausche (gleicher Titel) — nicht übernehmbar, daher nicht gelistet.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{slotDiffs.length === 0 ? (
|
{slotDiffs.length === 0 ? (
|
||||||
<p style={{ fontSize: '12px', color: 'var(--text2)' }}>
|
<p style={{ fontSize: '12px', color: 'var(--text2)' }}>
|
||||||
Keine inhaltlichen Abweichungen — der End-Stand entspricht deinem Pfad.
|
Keine inhaltlichen Abweichungen — der End-Stand entspricht deinem Pfad.
|
||||||
|
|
|
||||||
|
|
@ -911,16 +911,37 @@ export function draftHasLibrarySlotAssignments(draft) {
|
||||||
return slotsToSlotAssignments(draft).length >= 1
|
return slotsToSlotAssignments(draft).length >= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Diff-Einträge nur für Slots, die vorher schon eine Bibliotheks-Übung hatten. */
|
/** Alle Slot-Diffs inkl. reiner ID-Tausche (gleicher Titel). */
|
||||||
export function curatedSlotDiffs(comparison) {
|
export function compareSlotDiffs(comparison, { actionableOnly = false } = {}) {
|
||||||
const diffs = comparison?.slot_diffs
|
if (actionableOnly && Array.isArray(comparison?.slot_diffs_actionable)) {
|
||||||
if (!Array.isArray(diffs)) return []
|
return comparison.slot_diffs_actionable
|
||||||
return diffs.filter((d) => d?.baseline_exercise_id != null)
|
}
|
||||||
|
return Array.isArray(comparison?.slot_diffs) ? comparison.slot_diffs : []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vergleich würde eine bestehende Zuordnung ändern (Dialog bei Match). */
|
/** Inhaltliche Abweichungen (nicht nur gleicher Titel, andere ID). */
|
||||||
|
export function compareResponseHasActionableSlotChanges(res) {
|
||||||
|
const count = res?.slot_diff_count
|
||||||
|
if (count != null) return Number(count) > 0
|
||||||
|
return compareSlotDiffs(res, { actionableOnly: true }).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Diff-Einträge nur für Slots, die vorher schon eine Bibliotheks-Übung hatten. */
|
||||||
|
export function curatedSlotDiffs(comparison, { actionableOnly = true } = {}) {
|
||||||
|
return compareSlotDiffs(comparison, { actionableOnly }).filter(
|
||||||
|
(d) => d?.baseline_exercise_id != null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vergleich würde eine bestehende Zuordnung inhaltlich ändern (Dialog bei Match). */
|
||||||
export function compareResponseHasCuratedSlotChanges(res) {
|
export function compareResponseHasCuratedSlotChanges(res) {
|
||||||
return curatedSlotDiffs(res).length > 0
|
return curatedSlotDiffs(res, { actionableOnly: true }).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareResponseHadRematchWithoutActionableDiffs(res) {
|
||||||
|
if (compareResponseHasActionableSlotChanges(res)) return false
|
||||||
|
const rematch = res?.proposed_path_qa_pipeline?.rematch_log
|
||||||
|
return Array.isArray(rematch) && rematch.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alle Graph-Übungs-IDs für Retriever-Boost (Slots + Geschwister + gespeichertes Artefakt). */
|
/** Alle Graph-Übungs-IDs für Retriever-Boost (Slots + Geschwister + gespeichertes Artefakt). */
|
||||||
|
|
@ -1063,10 +1084,9 @@ export function applyMatchStepsToSlots(draft, apiSteps) {
|
||||||
return syncProgressionRoadmapFromSlots({ ...draft, slots: nextSlots, dirty: true })
|
return syncProgressionRoadmapFromSlots({ ...draft, slots: nextSlots, dirty: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vergleichs-Antwort: mindestens ein Slot mit anderer Übung als im Ist-Stand. */
|
/** Vergleichs-Antwort: mindestens ein inhaltlicher Slot-Unterschied. */
|
||||||
export function compareResponseHasSlotChanges(res) {
|
export function compareResponseHasSlotChanges(res) {
|
||||||
const count = res?.slot_diff_count ?? res?.slot_diffs?.length ?? 0
|
return compareResponseHasActionableSlotChanges(res)
|
||||||
return Number(count) > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nur ausgewählte Slots aus Optimierungs-Vorschlag übernehmen. */
|
/** Nur ausgewählte Slots aus Optimierungs-Vorschlag übernehmen. */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user