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
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:
parent
b629f192ac
commit
4b9374765b
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
59
backend/tests/test_exercise_progression_graph_visibility.py
Normal file
59
backend/tests/test_exercise_progression_graph_visibility.py
Normal 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",
|
||||
)
|
||||
|
|
@ -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`**.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user