Enhance Progression Graph Management with F15 Features and Evaluation Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m21s

- Updated `PROJECT_STATUS.md` to reflect the implementation of F15 features, including the unified slot review and handling of `findings_stale`.
- Enhanced `PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md` with detailed descriptions of new functionalities related to the match dialog and path quality assessments.
- Introduced new functions in `exercise_progression_graphs.py` to validate exercise visibility against progression graph settings, ensuring proper governance.
- Improved frontend components to support new governance parameters (visibility and club_id) in exercise creation workflows.
- Updated documentation in `HANDOVER.md` and `PLANNING_KI_ROADMAP.md` to outline the latest developments and validation results for the F15 features.
- Enhanced utility functions for exercise creation to incorporate governance settings, improving the overall user experience in the path builder and editor.
This commit is contained in:
Lars 2026-06-14 06:44:12 +02:00
parent b629f192ac
commit 4b9374765b
11 changed files with 291 additions and 50 deletions

View File

@ -15,7 +15,7 @@
**Plattform-Rechtstexte (P-01, 0.8.950.8.96):** Admin-Editor mit **Abschnitts- und Vollvorschau** (Markdown); fortlaufende Abschnittsnummerierung in der Anzeige/PDF (Darstellung, nicht DB-persistent).
**Parallel weiter relevant:** **Trainingsplan Phasen & Streams** (Migration **063**, Coach + Planung **0.8.1370.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036037), **Progressionsgraph** (032034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. **Planungs-KI Progressionsgraph** (Roadmap-first, Auto-Optimierung, Katalog-Kontext **0.8.233**): Ist-Doku **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**, Handover **`docs/HANDOVER.md`** §2.8.
**Parallel weiter relevant:** **Trainingsplan Phasen & Streams** (Migration **063**, Coach + Planung **0.8.1370.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036037), **Progressionsgraph** (032034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. **Planungs-KI Progressionsgraph** (Roadmap-first, Auto-Optimierung, Katalog-Kontext **0.8.233**, **F15** Match-Dialog + getrennte Pfad-QS lokal): Ist-Doku **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**, Handover **`docs/HANDOVER.md`** §2.8.
**Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) Abschnitt 12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **Abschnitt 11 Inline-Medien**, umgesetzt) · **Fachlicher Nutzerüberblick:** [`../../docs/FACHLICHE_NUTZERFUNKTIONEN.md`](../../docs/FACHLICHE_NUTZERFUNKTIONEN.md)

View File

@ -1,6 +1,6 @@
# Progressionsgraph — Slot-Editor (Phase B)
# Progressionsgraph — Slot-Editor (Phase B + F15)
**Stand:** 2026-06-10 · **Status:** In Umsetzung
**Stand:** 2026-05-22 · **Status:** Umgesetzt (F14 + F15 lokal nach 0.8.233)
## Ziel
@ -35,35 +35,52 @@ Leere Slots in der Roadmap sind erlaubt; Kanten nur zwischen aufeinanderfolgende
slots: Slot[], // index = major_step_index
pathSkillExpectations?,
lastFindings?, // path_qa-Snapshot
findingsStale?: boolean, // Bewertung veraltet (↔ Artefakt findings_stale)
dirty: boolean,
}
```
**Hydration:** `planning_roadmap` + Kanten → Slots; `slot_contents[]` für Entwürfe; Primärkette aus `next_exercise`.
**Speichern:** Batch-Delete bestehender Pfad-/Schwester-Kanten → `edges/sequence` (Primärkette) → einzelne `sibling`-Kanten → `PUT`/`sequence` mit Artefakt inkl. `slot_contents`, optional `last_findings`.
**Speichern:** Batch-Delete bestehender Pfad-/Schwester-Kanten → `edges/sequence` (Primärkette) → einzelne `sibling`-Kanten → `PUT`/`sequence` mit Artefakt inkl. `slot_contents`, `last_findings`, **`findings_stale`**.
## Findings-Panel
Nutzt `path_qa` (`overall_ok`, `quality_score`, `issues`, `recommendations`, `gap_fill_offers`, …).
Nutzt `path_qa`:
| Feld | Bedeutung |
|------|-----------|
| `quality_score` | Gesamt = **min(`roadmap_qa`, `assignment_qa`)** |
| `roadmap_qa` | Stufen/Roadmap (LLM `topic_coverage`, …) |
| `assignment_qa` | Slot-Befüllung (`empty_slot_count`, …) |
| `overall_ok`, `issues`, `recommendations`, `gap_fill_offers`, … | wie bisher |
**API:** `POST /api/planning/progression-path-suggest` mit `evaluate_only: true` und `evaluate_steps[]` — QA ohne Re-Match.
Persistenz: `planning_roadmap.last_findings`.
**Bewertung veraltet:** Jede Graph-Änderung setzt `findingsStale: true` → Banner im Panel. Nach „Graph bewerten“ → `false`. Persistenz: `planning_roadmap.findings_stale`.
## Match-Flow („Übungen matchen“)
1. **Schritt 1:** `evaluate_only` + volle Pfad-QS (wie „Graph bewerten“)
2. **Schritt 2:** `unified_slot_review: true`**`ProgressionOptimizeCompareModal`**
3. Pro Slot: aktuell vs. beste Bibliothek vs. optional KI-Vorschlag
4. **Vorauswahl:** Bibliothek nur wenn Stufen-Fit ≥ 50 % und besser als Baseline; sonst KI (bei leerem/schwachem Slot)
5. **Übernahme:** nur gewählte Slots speichern — **keine** automatische Nach-Bewertung
## Artefakt-Erweiterung (`GraphPlanningRoadmapArtifact`)
Zusätzlich optional:
Optional:
- `slot_contents[]``{ major_step_index, primary, siblings[] }`
- `last_findings` — letzter `path_qa`-Snapshot
- **`findings_stale`** — bool, Bewertung bezieht sich nicht mehr auf aktuellen Graph-Stand
## UI (konsolidiert)
- **Eine Oberfläche:** `ExerciseProgressionGraphPanel` embeddet `ProgressionGraphEditor` (Slots + Findings)
- Kein separater Slot-Editor, kein 4-Schritt-KI-Wizard, kein `ProgressionChainEditor` im Panel
- Route `/progression-graphs/:id` → Redirect nach `/exercises` (Deep-Link wählt Graph)
- **Phase C:** Übersicht mit Kacheln (Name, Start, Ziel)
- **Slot-Keys:** stabil `slot-{index}` (nicht Lernziel-Text) — sonst Fokusverlust beim Tippen
## Ersetzt (Legacy, nicht mehr im Panel)
@ -71,11 +88,14 @@ Zusätzlich optional:
## Implementierungsreihenfolge
| ID | Inhalt |
|----|--------|
| B.0 | Draft + Laden/Speichern Slots ↔ Kanten |
| B.1 | Slot-Karten, Bibliothek + Entwurf |
| B.2 | Findings-Panel + `evaluate_only` |
| B.3 | Entwürfe im Artefakt + „Übung anlegen“ |
| B.4 | Route + Panel vereinfachen |
| B.5 | `last_findings` + Phase-C-Vorbereitung |
| ID | Inhalt | Status |
|----|--------|--------|
| B.0 | Draft + Laden/Speichern Slots ↔ Kanten | ✅ |
| B.1 | Slot-Karten, Bibliothek + Entwurf | ✅ |
| B.2 | Findings-Panel + `evaluate_only` | ✅ |
| B.3 | Entwürfe im Artefakt + „Übung anlegen“ | ✅ |
| B.4 | Route + Panel vereinfachen | ✅ |
| B.5 | `last_findings` + Phase-C-Vorbereitung | ✅ |
| F15 | Unified Slot-Review, getrennte QS, `findings_stale` | ✅ |
**Ist-Doku:** `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md` §8.1 · `docs/HANDOVER.md` §2.8 F15

View File

@ -4,7 +4,7 @@ Optional Übungsvarianten als Knoten-Endpunkte; Sequenz-Bulk-Anlage.
AuthZ analog training_plan_templates: Bearbeiten/Löschen wie Übungen; Governance-Übergänge zentral.
"""
import json
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Mapping, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field, model_validator
@ -19,6 +19,7 @@ from club_tenancy import (
assert_library_content_editable,
assert_library_content_governance_transition,
assert_valid_governance_visibility,
is_platform_admin,
library_content_visible_to_profile,
)
@ -176,6 +177,87 @@ def _assert_variant_for_exercise(cur, exercise_id: int, variant_id: Optional[int
raise HTTPException(status_code=400, detail="Variante gehört nicht zur gewählten Übung")
def _exercise_allowed_in_progression_graph(
exercise_row: Mapping[str, Any],
*,
graph_visibility: str,
graph_club_id: Optional[int],
profile_id: int,
role: str,
) -> bool:
"""Prüft, ob eine Übung zur Sichtbarkeit des Progressionsgraphen passt."""
ex_vis = (exercise_row.get("visibility") or "private").strip().lower()
gvis = (graph_visibility or "private").strip().lower()
if gvis == "private":
if ex_vis == "official":
return True
if ex_vis == "club":
return True
if ex_vis == "private":
if is_platform_admin(role):
return True
try:
return int(exercise_row.get("created_by") or 0) == int(profile_id)
except (TypeError, ValueError):
return False
return False
if gvis == "club":
if ex_vis == "official":
return True
if ex_vis != "club":
return False
ex_club = exercise_row.get("club_id")
if ex_club is None:
return False
if graph_club_id is None:
return True
return int(ex_club) == int(graph_club_id)
return ex_vis == "official"
def _assert_exercises_allowed_in_graph(
cur,
graph_id: int,
profile_id: int,
role: str,
*exercise_ids: int,
) -> None:
"""400 wenn eine Übung nicht zur Graph-Sichtbarkeit passt."""
row = _graph_row(cur, graph_id)
gvis = (row.get("visibility") or "private").strip().lower()
gclub_raw = row.get("club_id")
gclub = int(gclub_raw) if gclub_raw is not None else None
unique = list(dict.fromkeys(exercise_ids))
if not unique:
return
ph = ",".join(["%s"] * len(unique))
cur.execute(
f"SELECT id, title, visibility, club_id, created_by FROM exercises WHERE id IN ({ph})",
tuple(unique),
)
by_id = {int(r2d(r)["id"]): r2d(r) for r in cur.fetchall()}
for eid in unique:
ex = by_id.get(int(eid))
if not ex:
continue
if _exercise_allowed_in_progression_graph(
ex,
graph_visibility=gvis,
graph_club_id=gclub,
profile_id=profile_id,
role=role,
):
continue
title = (ex.get("title") or "").strip() or f"#{eid}"
raise HTTPException(
status_code=400,
detail=(
f"Übung „{title}“ (Sichtbarkeit: {ex.get('visibility') or 'private'}) "
f"passt nicht zum Progressionsgraphen ({gvis})."
),
)
def _insert_edge_row(
cur,
graph_id: int,
@ -359,8 +441,10 @@ def list_visibility_promotion_candidates(
if not library_content_visible_to_profile(
cur,
profile_id,
(exd.get("visibility") or "private").strip().lower(),
exd.get("club_id"),
exd.get("created_by"),
role,
exd,
):
continue
exercises.append(
@ -565,6 +649,9 @@ def create_progression_edge(
cur = get_cursor(conn)
_require_graph_write(cur, graph_id, profile_id, role)
_assert_exercises_exist(cur, body.from_exercise_id, body.to_exercise_id)
_assert_exercises_allowed_in_graph(
cur, graph_id, profile_id, role, body.from_exercise_id, body.to_exercise_id
)
fv = body.from_exercise_variant_id
tv = body.to_exercise_variant_id
_assert_variant_for_exercise(cur, body.from_exercise_id, fv)
@ -613,6 +700,7 @@ def create_progression_sequence(
ex_ids = [s.exercise_id for s in steps]
_assert_exercises_exist(cur, *ex_ids)
_assert_exercises_allowed_in_graph(cur, graph_id, profile_id, role, *ex_ids)
try:
for i in range(n_seg):

View File

@ -0,0 +1,59 @@
"""Sichtbarkeit: Progressionsgraph ↔ Übungen (Promotion, Kanten, Match)."""
from routers.exercise_progression_graphs import _exercise_allowed_in_progression_graph
def test_club_graph_rejects_private_exercise():
assert not _exercise_allowed_in_progression_graph(
{"visibility": "private", "club_id": None, "created_by": 1},
graph_visibility="club",
graph_club_id=5,
profile_id=1,
role="trainer",
)
def test_club_graph_accepts_matching_club_exercise():
assert _exercise_allowed_in_progression_graph(
{"visibility": "club", "club_id": 5, "created_by": 2},
graph_visibility="club",
graph_club_id=5,
profile_id=1,
role="trainer",
)
def test_club_graph_accepts_official_exercise():
assert _exercise_allowed_in_progression_graph(
{"visibility": "official", "club_id": None, "created_by": 99},
graph_visibility="club",
graph_club_id=5,
profile_id=1,
role="trainer",
)
def test_private_graph_accepts_own_private_exercise():
assert _exercise_allowed_in_progression_graph(
{"visibility": "private", "club_id": None, "created_by": 7},
graph_visibility="private",
graph_club_id=None,
profile_id=7,
role="trainer",
)
def test_official_graph_requires_official_exercise():
assert not _exercise_allowed_in_progression_graph(
{"visibility": "club", "club_id": 5, "created_by": 2},
graph_visibility="official",
graph_club_id=None,
profile_id=1,
role="trainer",
)
assert _exercise_allowed_in_progression_graph(
{"visibility": "official", "club_id": None, "created_by": 2},
graph_visibility="official",
graph_club_id=None,
profile_id=1,
role="trainer",
)

View File

@ -1,7 +1,7 @@
# Shinkan Jinkendo Entwicklungsstand & Handover
**Stand:** 2026-05-22
**App-Version / DB-Schema:** App **`0.8.233`** (Planungs-KI F11F14, Katalog-Kontext); DB siehe **`backend/version.py`** (`DB_SCHEMA_VERSION`, Migration **088**).
**Stand:** 2026-05-22 (F15 Graph-Match & getrennte Pfad-QS, lokal nach **0.8.233**)
**App-Version / DB-Schema:** App **`0.8.233`** (Planungs-KI F11F14, Katalog-Kontext); **F15** siehe §2.8 — DB unverändert (`DB_SCHEMA_VERSION`, Migration **088**).
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
@ -114,11 +114,25 @@ 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.2310.8.232** |
| **F13** | **`planning_catalog_context`** (Fokus/Stil/TT/ZG) im Match + Graph-Artefakt | ✅ **0.8.233** |
| **F14** | **`ProgressionGraphEditor`** — Slot-UI + Planungskontext-Dropdowns | ✅ **0.8.233** |
| **F15** | Unified Slot-Review (Match-Dialog), getrennte Pfad-QS, `findings_stale` | ✅ lokal (nach 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; **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):** Roadmap-QS nach Trainer-Roadmap oft **~8588 %** — gilt **Stufenlogik**, nicht leere Slots. **Gesamt-Pfad-QS** = Minimum aus **`roadmap_qa`** + **`assignment_qa`** (leere Slots → Besetzung ~815 %). Workbench universell; Mae Geri Referenzfall.
#### F15 — Match-Dialog, Bewertung, Pfad-QS (Stand 2026-05-22)
| Thema | Ist |
|--------|-----|
| **„Übungen matchen“** | Schritt 1: `evaluate_only` (wie „Graph bewerten“) · Schritt 2: `unified_slot_review: true` → Dialog **pro Slot** (Bewertung, Bibliotheks-Alternative, optional KI) |
| **Vorauswahl Dialog** | Bibliothek nur bei Stufen-Fit **≥ 50 %** und besser als aktuell; bei leerem Slot + schwacher Bibliothek → **KI-Vorschlag** vorausgewählt |
| **Übernahme** | Nur gewählte Slots speichern — **keine** automatische teure Nach-Bewertung |
| **Bewertung veraltet** | Nach Graph-Änderungen Hinweis im Findings-Panel; persistiert als **`findings_stale`** im `planning_roadmap`-Artefakt (mit Speichern) |
| **Getrennte QS** | `path_qa.roadmap_qa` (Stufen/Roadmap/LLM) + `path_qa.assignment_qa` (Slot-Befüllung); **`quality_score`** = Minimum beider |
| **UX-Fix** | Slot-Karten: stabiler React-Key (`slot-{index}`) — Lernziel editierbar ohne Fokusverlust |
**Code:** `ProgressionOptimizeCompareModal.jsx`, `planning_exercise_path_builder.py` (`_build_unified_slot_review_entry`, `_slot_auto_select_*`), `planning_exercise_path_qa.py` (`build_*_qa_snapshot`), `progression_graph_planning_artifact.py` (`findings_stale`), `progressionGraphDraft.js`
**Backend-Kern:** `planning_progression_roadmap.py`, `planning_exercise_path_builder.py`, `planning_catalog_context.py`, `planning_path_rematch.py`, `planning_path_refine_stage.py`, `planning_path_qa_pipeline.py`, `planning_skill_expectations.py`, `planning_exercise_form_context.py`, `planning_exercise_path_ai_fill.py`, `progression_graph_planning_artifact.py`
@ -129,12 +143,12 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
**Offen (priorisiert):**
1. Dev-Regression: Gewaltschutz / Breitensport / Kinder (nicht nur Mae Geri)
2. **PathBuilder-Parität** — gleiche Katalog-Dropdowns wie GraphEditor
3. QS-UI — positive LLM-Hinweise als Highlights
4. UI-Wizard (4 Schritte: Ziel → Roadmap → Match → Lücken)
5. Graph-Erweiterungsmodus (Start ab Knoten)
6. Phase D — Auto KI-Gap-Fill bei persistent leeren Slots
7. **Trainingsplanung Phase G** — Gruppenkontext-Pack, Scopes `training_section` / `framework_slot` (Ist-Doku §16)
8. Technik-Katalog konfigurierbar (Backlog)
3. UI-Wizard (4 Schritte: Ziel → Roadmap → Match → Lücken)
4. Graph-Erweiterungsmodus (Start ab Knoten)
5. Phase D — Auto KI-Gap-Fill bei persistent leeren Slots
6. **Trainingsplanung Phase G** — Gruppenkontext-Pack, Scopes `training_section` / `framework_slot` (Ist-Doku §16)
7. Technik-Katalog konfigurierbar (Backlog)
8. **H1** — Katalog-Prompt-Snippets (modulare LLM-Anweisungen)
#### Übungs-KI Formular / Schnellanlage (Stand **0.8.171**)
@ -271,8 +285,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
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`.
3. **QS-UI:** positive LLM-Empfehlungen als Highlights statt nur Optimierungspotenziale.
3. **PathBuilder-Parität:** `planning_catalog_context`-Dropdowns auch in `ExerciseProgressionPathBuilder`.
4. **UI-Wizard:** 4 Schritte (Ziel & Katalog → Roadmap → Match → Lücken); Backend-Pipeline unverändert.
5. **Phase D:** automatisches KI-Gap-Fill bei persistent `roadmap_unfilled`.
6. **Trainingsplanung G0G4:** Katalog in Einheits-Editor, Scopes `training_section`/`framework_slot`, Abschnitts-QS, Gruppenkontext-Pack — Details **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16, **`PLANNING_KI_ROADMAP.md`**.

View File

@ -89,14 +89,24 @@ Details und Module: **`PLANNING_PROGRESSION_GRAPH_KI.md`**.
- [x] Vier Planungskontext-Dropdowns im Editor
- [x] `progressionGraphDraft.js` — Artefakt + API-Payload
### F15 — Match-Dialog & getrennte Pfad-QS (2026-05-22, lokal)
- [x] **`unified_slot_review`** — Dialog pro Slot (Bibliothek + KI, Stufen-Fit-Vergleich)
- [x] Vorauswahl: Bibliothek nur bei Stufen-Fit ≥ 50 %; sonst KI bei leerem/schwachem Slot
- [x] Übernahme ohne teure Auto-Nach-Bewertung; manuell „Graph bewerten“
- [x] **`path_qa.roadmap_qa`** + **`path_qa.assignment_qa`**; Gesamt = Minimum
- [x] **`findings_stale`** im Graph-Artefakt — Hinweis „Bewertung veraltet“ (persistiert)
- [x] Slot-Key-Fix — Lernziel editierbar ohne Fokusverlust
### Validierung (Referenz Mae Geri, 2026-05)
| Phase | Pfad-QS | Ergebnis |
|-------|---------|----------|
| Vor Roadmap/KI | ~65 % | Lücken, falsche Reihenfolge, Off-Topic |
| Nach Trainer-Roadmap + KI-Gap-Fill | **~88 % OK** | Vollständige Abdeckung; positive LLM-Hinweise |
| Phase | Roadmap-QS | Besetzung | Gesamt | Ergebnis |
|-------|------------|-----------|--------|----------|
| Vor Roadmap/KI | — | — | ~65 % | Lücken, Off-Topic |
| Roadmap ok, Slots leer | ~88 % | ~815 % | **~815 %** | Besetzung fehlt |
| Nach Match + Fill | ~88 % | hoch | **~85 %+** | Vollständige Abdeckung |
**Fazit:** Workbench + Katalog + Roadmap sind universell; Technik-Hardcoding allein reicht für Didaktik nicht.
**Fazit:** Roadmap-QS und Besetzungs-QS getrennt betrachten; Workbench + Katalog + Roadmap universell.
---

View File

@ -157,6 +157,10 @@ flowchart TB
| `auto_refine_stage_spec` | bool | Stufen-Spec bei `stage_mismatch` schärfen (Default **true**) |
| `max_rematch_rounds` | int | Rematch-Runden 04 (Default **3**) |
| `include_path_qa`, `include_llm_path_qa`, `include_ai_gap_fill` | bool | QS, LLM-Ganzpfad, Lücken-Angebote |
| `evaluate_only` | bool | Nur QS auf `evaluate_steps[]` — kein Match |
| `unified_slot_review` | bool | Pro-Slot-Review (Bibliothek + optional KI) für Match-Dialog; erfordert `baseline_evaluate_steps` + Roadmap |
| `baseline_evaluate_steps` | array? | Slot-Stand für Schritt 1 / Review-Baseline |
| `baseline_path_qa_snapshot` | object? | `path_qa` aus evaluate_only (Schritt 1 des Match-Flows) |
### 4.2 Wichtige Response-Felder
@ -166,7 +170,9 @@ flowchart TB
| `steps[]` | Gematchte Übungen; pro Schritt u. a. `roadmap_*`, `skill_expectations` |
| `path_skill_expectations` | Pfadweite Skill-Erwartungen |
| `gap_fill_offers[]` | Lücken mit `context_preview`, `goal_for_ai` |
| `path_qa` | QS inkl. `qa_tiers`, `optimization_hints`, `rematch_log`, `refine_log` |
| `path_qa` | QS inkl. `qa_tiers`, `optimization_hints`, `rematch_log`, `refine_log`; **F15:** auch `roadmap_qa`, `assignment_qa` (siehe §8.1) |
| `slot_reviews[]` | Bei `unified_slot_review`: je Slot `library_alternative`, `ai_alternative`, `auto_select`-Flags |
| `findings_stale` | Im Graph-Artefakt (nicht API-Response): Bewertung veraltet seit letztem „Graph bewerten“ |
| `target_profile_summary` | Erwartungsprofil inkl. Katalog-Dimensionen (nach Match) |
| `match_summary` | `library_matches`, `gap_fill_offer_count`, `roadmap_unfilled_count` |
@ -221,12 +227,13 @@ Tests: `test_planning_roadmap_stage_match.py`, `test_planning_path_rematch.py`,
### Referenz-Validierung (Mae Geri, 2026-05)
| Phase | Pfad-QS | Ergebnis |
|-------|---------|----------|
| Vor Roadmap/KI-Anpassung | ~65 % | Strukturelle Lücken (Grundlagen, Reihenfolge, Zielgenauigkeit) |
| Nach Trainer-Roadmap + KI-Angebote in leeren Slots | **~88 % OK** | Vollständige Curriculum-Abdeckung; positive LLM-Empfehlungen |
| Phase | Roadmap-QS | Besetzung | Gesamt (min) | Ergebnis |
|-------|------------|-----------|--------------|----------|
| Vor Roadmap/KI-Anpassung | — | — | ~65 % | Strukturelle Lücken |
| Nach Trainer-Roadmap, **Slots leer** | ~8588 % | ~815 % | **~815 %** | Roadmap ok, Besetzung fehlt |
| Nach Match + befüllte Slots | ~8588 % | hoch | **~85 %+** | Vollständige Curriculum-Abdeckung |
**Lesson:** Workbench + Katalog-Kontext + Roadmap sind der Hebel; Technik-Hardcoding allein reicht nicht für Didaktik.
**Lesson:** **`roadmap_qa`** und **`assignment_qa`** getrennt interpretieren; Gesamt-QS allein bei leerer Roadmap irreführend (historisch nur LLM-Roadmap-Score).
---
@ -330,6 +337,21 @@ API: `path_qa.qa_tiers`, `path_qa.optimization_hints` — **kein** anfrage-spezi
Phase G: gleiche Tiers für Abschnitts-QS nach Einheits-Match.
### 8.1 Getrennte Pfad-QS — Roadmap vs. Übungsbesetzung (F15)
`build_path_qa_summary()` in `planning_exercise_path_qa.py` liefert drei Ebenen:
| Feld | Inhalt | Score-Logik |
|------|--------|-------------|
| **`roadmap_qa`** | Stufenlogik, LLM `topic_coverage`, Roadmap-Hinweise | LLM-`quality_score` oder heuristisch (Lücken, Hints) |
| **`assignment_qa`** | Leere Slots, Off-Topic auf belegten Slots, Fill-Statistik | Stark abwertend bei leeren Slots (~815 % bei 100 % leer) |
| **`quality_score`** (gesamt) | Anzeige „Pfad-QS gesamt“ | **`min(roadmap_qa, assignment_qa)`** |
| **`overall_ok`** | Gesamt-OK | Beide Dimensionen müssen OK sein |
UI: **`ProgressionFindingsPanel`** — zwei Unterblöcke; Match-Dialog zeigt Roadmap- vs. Besetzungs-Prozent. Nach Graph-Änderung: **`findings_stale: true`** im Artefakt → Hinweis „Bewertung veraltet“ (bis erneut „Graph bewerten“ + Speichern).
Tests: `test_planning_path_qa_split.py`, `test_planning_deterministic_quality_score.py`
## 9. Fähigkeiten-Scoring-Anbindung
Modul: `planning_skill_expectations.py`
@ -379,6 +401,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js`
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ | 0.8.2310.8.232 |
| **F13** | **Katalog-Kontext** (`planning_catalog_context`) im Match + Graph-Artefakt | ✅ | **0.8.233** |
| **F14** | `ProgressionGraphEditor` Slot-UI + Planungskontext-Dropdowns | ✅ | 0.8.233 |
| **F15** | Unified Slot-Review, getrennte Pfad-QS, `findings_stale`, Match-Vorauswahl | ✅ | lokal (2026-05-22) |
| **H1** | **Katalog-Prompt-Snippets** — modulare LLM-Anweisungen pro Dimension | 🔲 | Spec **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** |
| **G** | Trainingsplanung: eigene Pipeline + Wiederverwendung Bausteine (§16) | 🔲 | — |
| **UX** | Wizard/Stepper; PathBuilder-Parität Katalog | 🔲 | — |
@ -391,8 +414,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js`
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`
3. **QS-UI** — positive LLM-Hinweise als „Highlights“, nicht als „Optimierungspotenziale“
3. **PathBuilder-Parität** — gleiche `planning_catalog_context`-Dropdowns in `ExerciseProgressionPathBuilder`
4. **UI-Wizard** — 4 Schritte (Ziel → Roadmap → Match → Lücken); Backend unverändert
5. **Graph-Erweiterungsmodus** — Start ab gewähltem Knoten / letzter Sequenz
6. **Phase D** — automatisches KI-Gap-Fill bei persistent leeren Slots

View File

@ -285,10 +285,12 @@ function ExerciseProgressionGraphPanel(
}
}
const promoteClubId = nextVis === 'club' ? resolvePromoteClubId() : null
await api.updateExerciseProgressionGraph(selectedGraphId, {
name,
description: metaDescription.trim() || null,
visibility: metaVisibility,
...(promoteClubId != null ? { club_id: promoteClubId } : {}),
})
await refreshGraphs()
alert('Graph-Metadaten gespeichert.')

View File

@ -589,6 +589,7 @@ export default function ExerciseProgressionPathBuilder({
const [gapPrepFocusAreaId, setGapPrepFocusAreaId] = useState('')
const [gapPrepError, setGapPrepError] = useState('')
const [loadedPlanningHint, setLoadedPlanningHint] = useState(false)
const [graphGovernance, setGraphGovernance] = useState({ visibility: 'private', clubId: null })
const [wizardStep, setWizardStep] = useState(1)
const [pathInsertNotice, setPathInsertNotice] = useState('')
@ -670,6 +671,10 @@ export default function ExerciseProgressionPathBuilder({
.getExerciseProgressionGraph(Number(graphId))
.then((g) => {
if (cancelled) return
setGraphGovernance({
visibility: g?.visibility || 'private',
clubId: g?.club_id ?? null,
})
const art = g?.planning_roadmap
if (!art) return
if (art.goal_query) setGoalQuery(String(art.goal_query))
@ -1056,7 +1061,7 @@ export default function ExerciseProgressionPathBuilder({
setQuickSaving(true)
setQuickAiError('')
try {
const payload = buildQuickCreateExercisePayloadFromDraft(quickCreateDraft)
const payload = buildQuickCreateExercisePayloadFromDraft(quickCreateDraft, graphGovernance)
const created = await api.createExercise(payload)
if (!created?.id) throw new Error('Anlegen fehlgeschlagen')
insertExerciseFromOffer(created, activeOffer)

View File

@ -880,7 +880,10 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
setSlotQuickSaving(true)
setSlotQuickError('')
try {
const payload = buildQuickCreateExercisePayloadFromDraft(slotQuickCreateDraft)
const payload = buildQuickCreateExercisePayloadFromDraft(slotQuickCreateDraft, {
visibility: graphMeta?.visibility || 'private',
clubId: graphMeta?.club_id ?? null,
})
const created = await api.createExercise(payload)
if (!created?.id) throw new Error('Anlegen fehlgeschlagen')
setDraft((prev) => ({

View File

@ -208,11 +208,27 @@ export function aiPreviewToQuickCreateDraft(preview, { title, focusAreaId, sketc
}
}
/**
* Sichtbarkeit/club_id für Schnellanlage (z. B. aus Progressionsgraph).
* @param {{ visibility?: string, clubId?: number|null }} [governance]
*/
function resolveQuickCreateGovernance(governance) {
const rawVis = (governance?.visibility || 'private').trim().toLowerCase()
const vis = rawVis === 'club' || rawVis === 'official' ? rawVis : 'private'
let clubId = null
if (vis === 'club' && governance?.clubId != null && governance.clubId !== '') {
const n = Number(governance.clubId)
if (Number.isFinite(n) && n > 0) clubId = n
}
return { visibility: vis, club_id: clubId }
}
/**
* createExercise-Payload aus bearbeitetem Entwurf.
* @param {{ visibility?: string, clubId?: number|null }} [governance]
* @throws {Error}
*/
export function buildQuickCreateExercisePayloadFromDraft(draft) {
export function buildQuickCreateExercisePayloadFromDraft(draft, governance) {
const title = (draft?.title || '').trim()
if (title.length < 3) {
throw new Error('Titel: mindestens 3 Zeichen.')
@ -239,6 +255,7 @@ export function buildQuickCreateExercisePayloadFromDraft(draft) {
if (summary && !stripHtmlToText(summary).trim()) summary = null
const skills = (draft?.skillChoices || []).filter((c) => c.include).map((c) => c.after)
const { visibility, club_id: clubId } = resolveQuickCreateGovernance(governance)
return {
title,
@ -247,7 +264,7 @@ export function buildQuickCreateExercisePayloadFromDraft(draft) {
execution,
preparation: prep,
trainer_notes: trainerNotes,
visibility: 'private',
visibility,
status: 'draft',
equipment: [],
focus_areas_multi: [{ focus_area_id: fid, is_primary: true }],
@ -256,15 +273,16 @@ export function buildQuickCreateExercisePayloadFromDraft(draft) {
target_groups_multi: [],
age_groups: [],
skills,
club_id: null,
club_id: clubId,
}
}
/**
* createExercise-Payload aus bestätigter Vorschau (Checkbox-Modus).
* @param {{ visibility?: string, clubId?: number|null }} [governance]
* @throws {Error}
*/
export function buildQuickCreateExercisePayloadFromPreview(preview, { title, focusAreaId, sketchPlain }) {
export function buildQuickCreateExercisePayloadFromPreview(preview, { title, focusAreaId, sketchPlain, ...governance } = {}) {
const sketchHtml = aiPlainTextToMinimalHtml(sketchPlain)
const fieldMap = {}
for (const c of preview?.instructionChoices || []) {
@ -288,6 +306,7 @@ export function buildQuickCreateExercisePayloadFromPreview(preview, { title, foc
}
const skills = (preview?.skillChoices || []).filter((c) => c.include).map((c) => c.after)
const { visibility, club_id: clubId } = resolveQuickCreateGovernance(governance)
const fid = Number(focusAreaId)
if (!Number.isFinite(fid) || fid < 1) {
@ -301,7 +320,7 @@ export function buildQuickCreateExercisePayloadFromPreview(preview, { title, foc
execution,
preparation: prep,
trainer_notes: trainerNotes,
visibility: 'private',
visibility,
status: 'draft',
equipment: [],
focus_areas_multi: [{ focus_area_id: fid, is_primary: true }],
@ -310,7 +329,7 @@ export function buildQuickCreateExercisePayloadFromPreview(preview, { title, foc
target_groups_multi: [],
age_groups: [],
skills,
club_id: null,
club_id: clubId,
}
}