From 4b9374765be6a18cebdb7e6ebff75ddf7be99926 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 14 Jun 2026 06:44:12 +0200 Subject: [PATCH] Enhance Progression Graph Management with F15 Features and Evaluation Improvements - 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. --- .claude/docs/PROJECT_STATUS.md | 2 +- .../PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md | 50 +++++++--- .../routers/exercise_progression_graphs.py | 92 ++++++++++++++++++- ...t_exercise_progression_graph_visibility.py | 59 ++++++++++++ docs/HANDOVER.md | 35 ++++--- docs/architecture/PLANNING_KI_ROADMAP.md | 20 +++- .../PLANNING_PROGRESSION_GRAPH_KI.md | 38 ++++++-- .../ExerciseProgressionGraphPanel.jsx | 2 + .../ExerciseProgressionPathBuilder.jsx | 7 +- .../src/components/ProgressionGraphEditor.jsx | 5 +- frontend/src/utils/exerciseAiQuickCreate.js | 31 +++++-- 11 files changed, 291 insertions(+), 50 deletions(-) create mode 100644 backend/tests/test_exercise_progression_graph_visibility.py diff --git a/.claude/docs/PROJECT_STATUS.md b/.claude/docs/PROJECT_STATUS.md index 209c8fd..e1c2886 100644 --- a/.claude/docs/PROJECT_STATUS.md +++ b/.claude/docs/PROJECT_STATUS.md @@ -15,7 +15,7 @@ **Plattform-Rechtstexte (P-01, 0.8.95–0.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.137–0.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036–037), **Progressionsgraph** (032–034) — 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.137–0.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036–037), **Progressionsgraph** (032–034) — 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) diff --git a/.claude/docs/working/PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md b/.claude/docs/working/PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md index 3f5d0cf..571f5f8 100644 --- a/.claude/docs/working/PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md +++ b/.claude/docs/working/PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md @@ -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 diff --git a/backend/routers/exercise_progression_graphs.py b/backend/routers/exercise_progression_graphs.py index 9e4edaa..7b3c0bf 100644 --- a/backend/routers/exercise_progression_graphs.py +++ b/backend/routers/exercise_progression_graphs.py @@ -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): diff --git a/backend/tests/test_exercise_progression_graph_visibility.py b/backend/tests/test_exercise_progression_graph_visibility.py new file mode 100644 index 0000000..98cac37 --- /dev/null +++ b/backend/tests/test_exercise_progression_graph_visibility.py @@ -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", + ) diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index a9d5820..6159fe6 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -1,7 +1,7 @@ # Shinkan Jinkendo – Entwicklungsstand & Handover -**Stand:** 2026-05-22 -**App-Version / DB-Schema:** App **`0.8.233`** (Planungs-KI F11–F14, 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 F11–F14, 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.231–0.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 **~85–88 %** — gilt **Stufenlogik**, nicht leere Slots. **Gesamt-Pfad-QS** = Minimum aus **`roadmap_qa`** + **`assignment_qa`** (leere Slots → Besetzung ~8–15 %). 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 G0–G4:** Katalog in Einheits-Editor, Scopes `training_section`/`framework_slot`, Abschnitts-QS, Gruppenkontext-Pack — Details **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16, **`PLANNING_KI_ROADMAP.md`**. diff --git a/docs/architecture/PLANNING_KI_ROADMAP.md b/docs/architecture/PLANNING_KI_ROADMAP.md index 98b9cfb..56ec945 100644 --- a/docs/architecture/PLANNING_KI_ROADMAP.md +++ b/docs/architecture/PLANNING_KI_ROADMAP.md @@ -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 % | ~8–15 % | **~8–15 %** | 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. --- diff --git a/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md b/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md index 647b4e2..7015c99 100644 --- a/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md +++ b/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md @@ -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 0–4 (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** | ~85–88 % | ~8–15 % | **~8–15 %** | Roadmap ok, Besetzung fehlt | +| Nach Match + befüllte Slots | ~85–88 % | 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 (~8–15 % 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.231–0.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 diff --git a/frontend/src/components/ExerciseProgressionGraphPanel.jsx b/frontend/src/components/ExerciseProgressionGraphPanel.jsx index 2d02149..d12022a 100644 --- a/frontend/src/components/ExerciseProgressionGraphPanel.jsx +++ b/frontend/src/components/ExerciseProgressionGraphPanel.jsx @@ -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.') diff --git a/frontend/src/components/ExerciseProgressionPathBuilder.jsx b/frontend/src/components/ExerciseProgressionPathBuilder.jsx index 7b2efc1..46af07c 100644 --- a/frontend/src/components/ExerciseProgressionPathBuilder.jsx +++ b/frontend/src/components/ExerciseProgressionPathBuilder.jsx @@ -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) diff --git a/frontend/src/components/ProgressionGraphEditor.jsx b/frontend/src/components/ProgressionGraphEditor.jsx index d540725..5b87768 100644 --- a/frontend/src/components/ProgressionGraphEditor.jsx +++ b/frontend/src/components/ProgressionGraphEditor.jsx @@ -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) => ({ diff --git a/frontend/src/utils/exerciseAiQuickCreate.js b/frontend/src/utils/exerciseAiQuickCreate.js index 298a364..27c9e27 100644 --- a/frontend/src/utils/exerciseAiQuickCreate.js +++ b/frontend/src/utils/exerciseAiQuickCreate.js @@ -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, } }