diff --git a/backend/migrations/089_ai_prompt_planning_intent_enrichment.sql b/backend/migrations/089_ai_prompt_planning_intent_enrichment.sql new file mode 100644 index 0000000..7ebcff7 --- /dev/null +++ b/backend/migrations/089_ai_prompt_planning_intent_enrichment.sql @@ -0,0 +1,67 @@ +-- Migration 089: Planungs-Intent — Zielanalyse + Stufenspecs (anti_patterns, success_criteria) + +UPDATE ai_prompts SET + description = 'Phase A: Ist-/Soll, Erfolgskriterien und explizite Ausschlüsse (ohne Gruppenkontext).', + template = $t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen Progressionsgraphen. + +Anfrage: {{goal_query}} +Semantic Brief: {{semantic_brief_json}} + +Wichtig: +- Keine Gruppenanalyse — nur didaktischer Pfad für die Technik/das Thema. +- Explizite Negationen aus der Anfrage (ohne/kein/nicht …) in constraints.excluded_themes übernehmen — nicht raten. +- success_criteria: messbar, für späteres Übungs-Matching (Titel + Kurzbeschreibung + Übungsziel). + +Antworte NUR mit JSON: +{ + "primary_topic": "Hauptthema", + "start_assumption": "Voraussetzungen für den Einstieg", + "target_state": "Konkreter Zielzustand der Progression", + "success_criteria": ["messbare Kriterien entlang des Pfads"], + "constraints": { + "partner_required": false, + "excluded_themes": ["wörtliche Negationen, z. B. keine Kumite-Anwendung"], + "trainer_notes": "optional: Fokus aus Ergänzungen" + } +}$t$, + default_template = template +WHERE slug = 'planning_progression_goal_analysis'; + +UPDATE ai_prompts SET + description = 'Phase C: Belastung, Übungstyp, Erfolgskriterien und anti_patterns je Major Step — für Retrieval-Matching.', + template = $t$Du bist Assistent für Kampfsport-Trainer und spezifizierst didaktische Stufen eines Progressionsgraphen. + +Anfrage: {{goal_query}} +Zielanalyse: {{goal_analysis_json}} +Major Steps: {{major_steps_json}} +Planungs-Intent (Pfadweite Regeln): {{intent_context_json}} +Semantic Brief: {{semantic_brief_json}} + +Aufgabe je Major Step — Felder für automatisches Übungs-Matching (nicht nur Titel): +- learning_goal: messbares Stufen-Lernziel (was die Übung bringen soll) +- load_profile: z. B. koordination, präzision, kraft, athletik +- exercise_type: kihon_einzel | partner_drill | kombination | kraft_auxiliary +- success_criteria: 2–4 prüfbare Kriterien an Kurzbeschreibung + Übungsziel (nicht nur Technikname im Titel) +- anti_patterns: 2–5 Dinge, die für diese Stufe unpassend sind + +Regeln: +1. Jede explicit_exclusions / excluded_themes aus intent_context und Zielanalyse MUSS in anti_patterns jeder Stufe vorkommen (umformuliert ok). +2. Keine neuen Ausschlüsse erfinden, die nicht in Anfrage/Intent/Zielanalyse stehen. +3. success_criteria Pfad-weit + stufenspezifisch kombinieren. +4. partner_drill nur wenn Partner/Kumite nicht ausgeschlossen ist. + +Antworte NUR mit JSON: +{ + "stage_specs": [ + { + "major_step_index": 0, + "learning_goal": "…", + "load_profile": ["koordination"], + "exercise_type": "kihon_einzel", + "success_criteria": ["…"], + "anti_patterns": ["…"] + } + ] +}$t$, + default_template = template +WHERE slug = 'planning_progression_stage_spec'; diff --git a/backend/planning_intent_context.py b/backend/planning_intent_context.py new file mode 100644 index 0000000..89dd5e2 --- /dev/null +++ b/backend/planning_intent_context.py @@ -0,0 +1,216 @@ +""" +Gemeinsame Intent-Anreicherung für Planungs-Retrieval. + +Progressionsgraph (Roadmap stage_specs) und später Trainingsplanung +(Abschnitt/Slot) nutzen dieselben Bausteine: + Intent-Kontext bauen → Specs finalisieren → Matching-Gates. +""" +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Any, Dict, List, Mapping, Optional, Sequence + +from planning_exercise_semantics import ( + PlanningSemanticBrief, + resolve_path_anti_patterns, +) + +_NEGATION_CLAUSE_RE = re.compile( + r"\b(?:ohne|kein(?:e|en|er|em)?|nicht)\s+[^,.;\n]+", + flags=re.IGNORECASE, +) + + +def extract_explicit_exclusions(*texts: Optional[str]) -> List[str]: + """Lesbare Negationsklauseln aus Freitext (ohne Themen-Raten).""" + out: List[str] = [] + for raw in texts: + s = (raw or "").strip() + if not s: + continue + for m in _NEGATION_CLAUSE_RE.finditer(s): + clause = m.group(0).strip().rstrip(".,;") + if clause and clause.lower() not in {x.lower() for x in out}: + out.append(clause[:220]) + return out[:12] + + +@dataclass +class PlanningIntentContext: + """Pfad-/Abschnittsweiter Planungs-Intent — domänenneutral.""" + + source_query: str = "" + primary_topic: str = "" + path_anti_patterns: List[str] = field(default_factory=list) + path_success_criteria: List[str] = field(default_factory=list) + explicit_exclusions: List[str] = field(default_factory=list) + context_notes: str = "" + + def to_api_dict(self) -> Dict[str, Any]: + return { + "source_query": self.source_query, + "primary_topic": self.primary_topic, + "path_anti_patterns": self.path_anti_patterns[:16], + "path_success_criteria": self.path_success_criteria[:10], + "explicit_exclusions": self.explicit_exclusions[:10], + "context_notes": self.context_notes[:1200] or None, + } + + +def build_planning_intent_context( + goal_query: str, + *, + semantic_brief: Optional[PlanningSemanticBrief] = None, + goal_analysis: Optional[Mapping[str, Any]] = None, + extra_context: Optional[str] = None, + primary_topic: Optional[str] = None, +) -> PlanningIntentContext: + """Intent aus Anfrage, Zielanalyse und optionalem Kontext — ohne Sonderregeln pro Thema.""" + ga = dict(goal_analysis or {}) + notes_parts = [extra_context or ""] + constraints = ga.get("constraints") if isinstance(ga.get("constraints"), dict) else {} + if isinstance(constraints, dict): + trainer_notes = str(constraints.get("trainer_notes") or "").strip() + if trainer_notes: + notes_parts.append(trainer_notes) + + combined_notes = " ".join(p.strip() for p in notes_parts if p and p.strip()) + explicit = extract_explicit_exclusions(goal_query, combined_notes or None) + ga_excluded = constraints.get("excluded_themes") if isinstance(constraints, dict) else None + if isinstance(ga_excluded, list): + for item in ga_excluded: + s = str(item or "").strip() + if s and s.lower() not in {x.lower() for x in explicit}: + explicit.append(s[:220]) + + path_anti = resolve_path_anti_patterns( + goal_query, + semantic_brief=semantic_brief, + extra_context=combined_notes or None, + ) + path_success: List[str] = [] + for item in ga.get("success_criteria") or []: + s = str(item or "").strip() + if s and s not in path_success: + path_success.append(s[:240]) + target = str(ga.get("target_state") or "").strip() + if target and len(target) >= 8: + line = f"Zielzustand erreichbar: {target[:200]}" + if line not in path_success: + path_success.append(line) + + topic = (primary_topic or ga.get("primary_topic") or "").strip() + if semantic_brief and not topic: + topic = (semantic_brief.primary_topic or "").strip() + + return PlanningIntentContext( + source_query=(goal_query or "").strip(), + primary_topic=topic, + path_anti_patterns=path_anti, + path_success_criteria=path_success, + explicit_exclusions=explicit, + context_notes=combined_notes[:1200], + ) + + +def _dedupe_preserve(items: Sequence[str], *, limit: int = 14) -> List[str]: + out: List[str] = [] + seen: set[str] = set() + for raw in items: + s = str(raw or "").strip() + if not s: + continue + key = s.lower() + if key in seen: + continue + seen.add(key) + out.append(s[:240]) + if len(out) >= limit: + break + return out + + +def finalize_stage_spec_artifact( + spec: "StageSpecArtifact", + *, + major_step: Optional["MajorStep"] = None, + intent: PlanningIntentContext, +) -> "StageSpecArtifact": + """Pfad-Intent in eine Stufenspezifikation mergen (LLM oder heuristisch).""" + from planning_progression_roadmap import MajorStep, StageSpecArtifact + + learning_goal = (spec.learning_goal or (major_step.learning_goal if major_step else "")).strip() + phase = (major_step.phase if major_step else "").strip().lower() + + anti = _dedupe_preserve( + [ + *(spec.anti_patterns or []), + *intent.explicit_exclusions, + *intent.path_anti_patterns, + ], + limit=14, + ) + success = _dedupe_preserve( + [ + *(spec.success_criteria or []), + *intent.path_success_criteria, + ( + f"Übung liefert messbar: {learning_goal[:160]}" + if learning_goal + else "" + ), + ( + f"Kurzbeschreibung und Übungsziel passen zur Phase {phase}" + if phase + else "Kurzbeschreibung und Übungsziel passen zum Stufen-Lernziel" + ), + ], + limit=8, + ) + + idx = spec.major_step_index + if major_step is not None: + idx = major_step.index + + return StageSpecArtifact( + major_step_index=idx, + learning_goal=learning_goal, + load_profile=list(spec.load_profile or []), + exercise_type=(spec.exercise_type or "").strip(), + success_criteria=success, + anti_patterns=anti, + ) + + +def finalize_stage_specs_with_intent( + specs: Sequence["StageSpecArtifact"], + major_steps: Sequence["MajorStep"], + *, + intent: PlanningIntentContext, + fallback_specs: Optional[Sequence["StageSpecArtifact"]] = None, +) -> List["StageSpecArtifact"]: + """Alle Stufen mit gleichem Pfad-Intent anreichern; fehlende Indizes aus Fallback.""" + from planning_progression_roadmap import MajorStep, StageSpecArtifact + + by_idx = {int(s.major_step_index): s for s in specs} + fallback_by_idx = {int(s.major_step_index): s for s in (fallback_specs or [])} + out: List[StageSpecArtifact] = [] + for major in major_steps: + raw = by_idx.get(major.index) or fallback_by_idx.get(major.index) + if raw is None: + raw = StageSpecArtifact( + major_step_index=major.index, + learning_goal=major.learning_goal, + ) + out.append(finalize_stage_spec_artifact(raw, major_step=major, intent=intent)) + return out + + +__all__ = [ + "PlanningIntentContext", + "build_planning_intent_context", + "extract_explicit_exclusions", + "finalize_stage_spec_artifact", + "finalize_stage_specs_with_intent", +] diff --git a/backend/planning_progression_roadmap.py b/backend/planning_progression_roadmap.py index 725296a..7881b55 100644 --- a/backend/planning_progression_roadmap.py +++ b/backend/planning_progression_roadmap.py @@ -298,6 +298,8 @@ def try_llm_stage_specs( goal_query: str, goal_analysis: GoalAnalysisArtifact, major_steps: Sequence[MajorStep], + intent_context: Optional[Mapping[str, Any]] = None, + semantic_brief: Optional[PlanningSemanticBrief] = None, ) -> Tuple[Optional[List[StageSpecArtifact]], bool]: obj = _run_prompt_json( cur, @@ -306,6 +308,11 @@ def try_llm_stage_specs( "goal_query": goal_query or "", "goal_analysis_json": json.dumps(goal_analysis.model_dump(), ensure_ascii=False), "major_steps_json": json.dumps([m.model_dump() for m in major_steps], ensure_ascii=False), + "intent_context_json": json.dumps(dict(intent_context or {}), ensure_ascii=False), + "semantic_brief_json": json.dumps( + brief_to_summary_dict(semantic_brief) if semantic_brief else {}, + ensure_ascii=False, + ), }, ) if not obj: @@ -522,9 +529,14 @@ def build_goal_analysis( if notes.strip(): criteria.append(f"Berücksichtigung: {notes.strip()[:200]}") + from planning_intent_context import extract_explicit_exclusions + constraints: Dict[str, Any] = {"partner_required": False, "group_analysis": False} if notes.strip(): constraints["trainer_notes"] = notes.strip()[:500] + excluded = extract_explicit_exclusions(goal_query, notes or None) + if excluded: + constraints["excluded_themes"] = excluded return GoalAnalysisArtifact( primary_topic=topic, @@ -955,6 +967,44 @@ def roadmap_context_from_override( semantic_brief=semantic_brief, ) + from planning_exercise_semantics import enrich_brief_with_path_constraints + from planning_intent_context import ( + build_planning_intent_context, + finalize_stage_specs_with_intent, + ) + + enriched_brief = enrich_brief_with_path_constraints( + semantic_brief, + goal_query.strip(), + extra_context=_merge_roadmap_notes( + structured.roadmap_notes if structured else None, + structured.start_situation if structured else None, + structured.target_state if structured else None, + ), + ) + intent = build_planning_intent_context( + goal_query.strip(), + semantic_brief=enriched_brief, + goal_analysis=goal_analysis.model_dump(), + extra_context=_merge_roadmap_notes( + structured.roadmap_notes if structured else None, + structured.start_situation if structured else None, + structured.target_state if structured else None, + ), + primary_topic=goal_analysis.primary_topic, + ) + stage_specs = finalize_stage_specs_with_intent( + stage_specs, + majors, + intent=intent, + fallback_specs=build_stage_specs( + majors, + goal_analysis=goal_analysis, + goal_query=goal_query.strip(), + semantic_brief=enriched_brief, + ), + ) + return ProgressionRoadmapContext( goal_query=goal_query.strip(), max_steps=effective_max, @@ -1122,24 +1172,59 @@ def run_progression_roadmap_pipeline( ) ctx.roadmap = roadmap - stage_specs = build_stage_specs( + from planning_exercise_semantics import enrich_brief_with_path_constraints + from planning_intent_context import ( + build_planning_intent_context, + finalize_stage_specs_with_intent, + ) + + brief = enrich_brief_with_path_constraints( + brief, + goal_query, + extra_context=_merge_roadmap_notes( + resolved.roadmap_notes, + resolved.start_situation, + resolved.target_state, + ), + ) + intent = build_planning_intent_context( + goal_query, + semantic_brief=brief, + goal_analysis=goal_analysis.model_dump(), + extra_context=_merge_roadmap_notes( + resolved.roadmap_notes, + resolved.start_situation, + resolved.target_state, + ), + primary_topic=goal_analysis.primary_topic, + ) + + heuristic_specs = build_stage_specs( roadmap.major_steps, goal_analysis=goal_analysis, goal_query=goal_query, semantic_brief=brief, ) + stage_specs = list(heuristic_specs) if include_llm_roadmap and cur is not None: llm_specs, spec_ok = try_llm_stage_specs( cur, goal_query=llm_goal_query, goal_analysis=goal_analysis, major_steps=roadmap.major_steps, + intent_context=intent.to_api_dict(), + semantic_brief=brief, ) if spec_ok and llm_specs: - stage_specs = llm_specs + stage_specs = list(llm_specs) ctx.llm_stage_spec_applied = True ctx.prompt_slugs.append(PROMPT_SLUG_STAGE_SPEC) - ctx.stage_specs = stage_specs + ctx.stage_specs = finalize_stage_specs_with_intent( + stage_specs, + roadmap.major_steps, + intent=intent, + fallback_specs=heuristic_specs, + ) if ctx.llm_goal_analysis_applied or ctx.llm_roadmap_applied or ctx.llm_stage_spec_applied: ctx.pipeline_phase = "roadmap_v1_llm" diff --git a/backend/tests/test_planning_intent_context.py b/backend/tests/test_planning_intent_context.py new file mode 100644 index 0000000..e85d5a8 --- /dev/null +++ b/backend/tests/test_planning_intent_context.py @@ -0,0 +1,58 @@ +"""Tests gemeinsames Planungs-Intent-Modul (Progressionsgraph → Trainingsplanung).""" +from planning_intent_context import ( + build_planning_intent_context, + extract_explicit_exclusions, + finalize_stage_specs_with_intent, +) +from planning_progression_roadmap import ( + MajorStep, + StageSpecArtifact, + build_goal_analysis, + build_stage_specs, + run_progression_roadmap_pipeline, +) +from planning_exercise_semantics import build_semantic_brief, enrich_brief_with_path_constraints + + +def test_extract_explicit_exclusions_parses_negations(): + q = "gesprungener Mawashi Geri, keine Kumite-Anwendung gewünscht" + out = extract_explicit_exclusions(q) + assert any("kumite" in x.lower() for x in out) + + +def test_build_planning_intent_context_includes_path_anti(): + q = "Mawashi Geri Sprungkraft, keine Kumite-Anwendung" + brief = enrich_brief_with_path_constraints(build_semantic_brief(q), q) + ga = build_goal_analysis(q, brief) + intent = build_planning_intent_context(q, semantic_brief=brief, goal_analysis=ga.model_dump()) + assert intent.explicit_exclusions + assert any("kumite" in a for a in intent.path_anti_patterns) + + +def test_finalize_stage_specs_merges_intent_into_each_stage(): + q = "gesprungener Mawashi Geri, keine Kumite-Anwendung" + brief = enrich_brief_with_path_constraints(build_semantic_brief(q), q) + ga = build_goal_analysis(q, brief) + intent = build_planning_intent_context(q, semantic_brief=brief, goal_analysis=ga.model_dump()) + majors = [ + MajorStep(index=0, phase="grundlage", learning_goal="Grundtechnik Mawashi", consolidates=["m1"]), + MajorStep(index=1, phase="vertiefung", learning_goal="Sprungkoordination", consolidates=["m2"]), + ] + raw_specs = [ + StageSpecArtifact(major_step_index=0, learning_goal=majors[0].learning_goal), + StageSpecArtifact(major_step_index=1, learning_goal=majors[1].learning_goal), + ] + finalized = finalize_stage_specs_with_intent(raw_specs, majors, intent=intent) + assert len(finalized) == 2 + for spec in finalized: + assert spec.success_criteria + assert any("kumite" in a.lower() for a in spec.anti_patterns) + assert any("messbar" in c.lower() or "übungsziel" in c.lower() for c in spec.success_criteria) + + +def test_pipeline_stage_specs_carry_exclusions_without_llm(): + q = "gesprungener Mawashi Geri Sprungphase, keine Kumite-Anwendung" + ctx = run_progression_roadmap_pipeline(q, max_steps=3, include_llm_roadmap=False) + assert len(ctx.stage_specs) == 3 + for spec in ctx.stage_specs: + assert any("kumite" in a.lower() for a in (spec.anti_patterns or [])) diff --git a/backend/version.py b/backend/version.py index 8fc2b84..7d5fe5e 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,8 +1,8 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.222" +APP_VERSION = "0.8.223" BUILD_DATE = "2026-06-07" -DB_SCHEMA_VERSION = "20260607088" +DB_SCHEMA_VERSION = "20260607089" MODULE_VERSIONS = { "legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste) @@ -38,7 +38,7 @@ MODULE_VERSIONS = { "skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions "methods": "0.1.0", "exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume - "planning_exercise_suggest": "0.22.0", # skill_expectations, planning_roadmap persist (088), stage_specs override + "planning_exercise_suggest": "0.23.0", # planning_intent_context, finalize stage_specs, Prompt 089 "training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint "training_programs": "0.1.0", "planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung @@ -53,6 +53,14 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.223", + "date": "2026-06-07", + "changes": [ + "planning_intent_context: gemeinsamer Intent für anti_patterns/success_criteria (Phase G-ready).", + "Migration 089: LLM-Prompts Zielanalyse + stage_spec mit Intent-Kontext; finalize nach Pipeline.", + ], + }, { "version": "0.8.222", "date": "2026-06-07", diff --git a/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md b/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md index bacadd1..f456456 100644 --- a/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md +++ b/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md @@ -225,7 +225,23 @@ Validierung: `progression_graph_planning_artifact.py` · Tests: `test_progressio --- -## 8. Fähigkeiten-Scoring-Anbindung +## 8. Planungs-Intent (gemeinsam mit Trainingsplanung) + +Modul: **`planning_intent_context.py`** — domänenneutral, wiederverwendbar in Phase G. + +| Baustein | Progressionsgraph heute | Trainingsplanung (Phase G) | +|----------|-------------------------|----------------------------| +| `build_planning_intent_context` | Aus `goal_query`, Zielanalyse, Notizen | Aus `section_guidance`, Slot, Einheit | +| `explicit_exclusions` | Negationen (`ohne/kein/nicht …`) | gleich | +| `path_anti_patterns` | → jede `stage_spec.anti_patterns` | → Abschnitts-/Slot-Brief | +| `path_success_criteria` | → `success_criteria` + Matching | → Slot-Erwartung | +| `finalize_stage_specs_with_intent` | Nach heuristischer/LLM-Roadmap | Analog für Sektionen | + +LLM: Migration **089** — Prompts `planning_progression_goal_analysis` + `planning_progression_stage_spec` erhalten `intent_context_json`; Ausschlüsse nicht erfinden, nur aus Anfrage übernehmen. + +Matching: `anti_patterns` + `success_criteria` → `build_stage_match_brief` → Retrieval-Gate (Titel + Summary + Goal). + +## 9. Fähigkeiten-Scoring-Anbindung Modul: `planning_skill_expectations.py` @@ -245,7 +261,7 @@ Integration: --- -## 9. KI-Lücken (Gap-Fill) +## 10. KI-Lücken (Gap-Fill) Flow: 1. `roadmap_unfilled` / QA-Lücken → `gap_fill_offers` @@ -257,7 +273,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js` --- -## 10. Implementierungsstände (Phasen) +## 11. Implementierungsstände (Phasen) | Phase | Inhalt | Status | Version | |-------|--------|--------|---------| @@ -275,7 +291,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js` --- -## 11. Offenes Backlog (priorisiert) +## 12. Offenes Backlog (priorisiert) 1. **UI-Überarbeitung** — Wizard mit 4 Schritten, progressive disclosure (Briefing unten) 2. **Graph-Erweiterungsmodus** — Start ab gewähltem Knoten / letzter Sequenz @@ -292,13 +308,14 @@ Kern: Wizard ① Ziel & Start/Ziel → ② Roadmap → ③ Match → ④ Lücken --- -## 12. Tests +## 13. Tests | Datei | Abdeckung | |-------|-----------| | `test_planning_progression_roadmap.py` | Roadmap-Pipeline, Override, Start/Ziel | | `test_planning_exercise_path_builder.py` | Pfad-Annotierung, Skill-Expectations auf Steps | | `test_planning_skill_expectations.py` | Skill-Erwartungen, Scopes | +| `test_planning_intent_context.py` | Intent-Kontext, finalize stage_specs | | `test_planning_exercise_path_ai_fill.py` | Gap-Fill, `expected_skills` in goal text | | `test_planning_exercise_form_context.py` | `planning_context`, Gap-Snapshot | | `test_progression_graph_planning_artifact.py` | JSONB-Artefakt-Validierung | @@ -306,7 +323,7 @@ Kern: Wizard ① Ziel & Start/Ziel → ② Roadmap → ③ Match → ④ Lücken --- -## 13. Dokumenten-Index (Drift vermeiden) +## 14. Dokumenten-Index (Drift vermeiden) | Frage | Primäre Quelle | |-------|----------------| @@ -322,7 +339,7 @@ Kern: Wizard ① Ziel & Start/Ziel → ② Roadmap → ③ Match → ④ Lücken --- -## 14. Changelog (Dokument) +## 15. Changelog (Dokument) | Datum | Änderung | |-------|----------|