Enhance Planning Intent Context and Stage Specification Finalization
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
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 1m13s

- Introduced `intent_context` and `semantic_brief` parameters in `try_llm_stage_specs` to improve context handling for stage specifications.
- Updated `build_goal_analysis` to extract explicit exclusions from goal queries, enhancing constraint management.
- Enhanced `roadmap_context_from_override` to enrich semantic briefs with path constraints and finalize stage specifications with intent context.
- Incremented application version to reflect these updates.
This commit is contained in:
Lars 2026-06-11 08:47:26 +02:00
parent 3c12363b8f
commit 4ef3f00e6b
6 changed files with 464 additions and 13 deletions

View File

@ -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: 24 prüfbare Kriterien an Kurzbeschreibung + Übungsziel (nicht nur Technikname im Titel)
- anti_patterns: 25 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';

View File

@ -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",
]

View File

@ -298,6 +298,8 @@ def try_llm_stage_specs(
goal_query: str, goal_query: str,
goal_analysis: GoalAnalysisArtifact, goal_analysis: GoalAnalysisArtifact,
major_steps: Sequence[MajorStep], major_steps: Sequence[MajorStep],
intent_context: Optional[Mapping[str, Any]] = None,
semantic_brief: Optional[PlanningSemanticBrief] = None,
) -> Tuple[Optional[List[StageSpecArtifact]], bool]: ) -> Tuple[Optional[List[StageSpecArtifact]], bool]:
obj = _run_prompt_json( obj = _run_prompt_json(
cur, cur,
@ -306,6 +308,11 @@ def try_llm_stage_specs(
"goal_query": goal_query or "", "goal_query": goal_query or "",
"goal_analysis_json": json.dumps(goal_analysis.model_dump(), ensure_ascii=False), "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), "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: if not obj:
@ -522,9 +529,14 @@ def build_goal_analysis(
if notes.strip(): if notes.strip():
criteria.append(f"Berücksichtigung: {notes.strip()[:200]}") 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} constraints: Dict[str, Any] = {"partner_required": False, "group_analysis": False}
if notes.strip(): if notes.strip():
constraints["trainer_notes"] = notes.strip()[:500] constraints["trainer_notes"] = notes.strip()[:500]
excluded = extract_explicit_exclusions(goal_query, notes or None)
if excluded:
constraints["excluded_themes"] = excluded
return GoalAnalysisArtifact( return GoalAnalysisArtifact(
primary_topic=topic, primary_topic=topic,
@ -955,6 +967,44 @@ def roadmap_context_from_override(
semantic_brief=semantic_brief, 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( return ProgressionRoadmapContext(
goal_query=goal_query.strip(), goal_query=goal_query.strip(),
max_steps=effective_max, max_steps=effective_max,
@ -1122,24 +1172,59 @@ def run_progression_roadmap_pipeline(
) )
ctx.roadmap = roadmap 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, roadmap.major_steps,
goal_analysis=goal_analysis, goal_analysis=goal_analysis,
goal_query=goal_query, goal_query=goal_query,
semantic_brief=brief, semantic_brief=brief,
) )
stage_specs = list(heuristic_specs)
if include_llm_roadmap and cur is not None: if include_llm_roadmap and cur is not None:
llm_specs, spec_ok = try_llm_stage_specs( llm_specs, spec_ok = try_llm_stage_specs(
cur, cur,
goal_query=llm_goal_query, goal_query=llm_goal_query,
goal_analysis=goal_analysis, goal_analysis=goal_analysis,
major_steps=roadmap.major_steps, major_steps=roadmap.major_steps,
intent_context=intent.to_api_dict(),
semantic_brief=brief,
) )
if spec_ok and llm_specs: if spec_ok and llm_specs:
stage_specs = llm_specs stage_specs = list(llm_specs)
ctx.llm_stage_spec_applied = True ctx.llm_stage_spec_applied = True
ctx.prompt_slugs.append(PROMPT_SLUG_STAGE_SPEC) 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: if ctx.llm_goal_analysis_applied or ctx.llm_roadmap_applied or ctx.llm_stage_spec_applied:
ctx.pipeline_phase = "roadmap_v1_llm" ctx.pipeline_phase = "roadmap_v1_llm"

View File

@ -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 []))

View File

@ -1,8 +1,8 @@
# Shinkan Jinkendo Version Information # Shinkan Jinkendo Version Information
APP_VERSION = "0.8.222" APP_VERSION = "0.8.223"
BUILD_DATE = "2026-06-07" BUILD_DATE = "2026-06-07"
DB_SCHEMA_VERSION = "20260607088" DB_SCHEMA_VERSION = "20260607089"
MODULE_VERSIONS = { MODULE_VERSIONS = {
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste) "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 "skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
"methods": "0.1.0", "methods": "0.1.0",
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume "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_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
"training_programs": "0.1.0", "training_programs": "0.1.0",
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung "planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
@ -53,6 +53,14 @@ MODULE_VERSIONS = {
} }
CHANGELOG = [ 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", "version": "0.8.222",
"date": "2026-06-07", "date": "2026-06-07",

View File

@ -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` Modul: `planning_skill_expectations.py`
@ -245,7 +261,7 @@ Integration:
--- ---
## 9. KI-Lücken (Gap-Fill) ## 10. KI-Lücken (Gap-Fill)
Flow: Flow:
1. `roadmap_unfilled` / QA-Lücken → `gap_fill_offers` 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 | | 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) 1. **UI-Überarbeitung** — Wizard mit 4 Schritten, progressive disclosure (Briefing unten)
2. **Graph-Erweiterungsmodus** — Start ab gewähltem Knoten / letzter Sequenz 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 | | Datei | Abdeckung |
|-------|-----------| |-------|-----------|
| `test_planning_progression_roadmap.py` | Roadmap-Pipeline, Override, Start/Ziel | | `test_planning_progression_roadmap.py` | Roadmap-Pipeline, Override, Start/Ziel |
| `test_planning_exercise_path_builder.py` | Pfad-Annotierung, Skill-Expectations auf Steps | | `test_planning_exercise_path_builder.py` | Pfad-Annotierung, Skill-Expectations auf Steps |
| `test_planning_skill_expectations.py` | Skill-Erwartungen, Scopes | | `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_path_ai_fill.py` | Gap-Fill, `expected_skills` in goal text |
| `test_planning_exercise_form_context.py` | `planning_context`, Gap-Snapshot | | `test_planning_exercise_form_context.py` | `planning_context`, Gap-Snapshot |
| `test_progression_graph_planning_artifact.py` | JSONB-Artefakt-Validierung | | `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 | | 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 | | Datum | Änderung |
|-------|----------| |-------|----------|