From 87f258be38df8dd42f7c5220a93773b1c2cf1c93 Mon Sep 17 00:00:00 2001
From: Lars
Date: Tue, 9 Jun 2026 10:17:30 +0200
Subject: [PATCH] Enhance Path QA with Roadmap-First Features and Gap Detection
Improvements
- Introduced `roadmap_qa_mode` to manage QA behavior based on roadmap-first logic, improving gap detection between major steps.
- Updated `detect_path_gaps` to skip gaps for roadmap-planned neighbor pairs, enhancing the accuracy of path assessments.
- Added new helper function `is_roadmap_planned_neighbor_pair` to facilitate roadmap neighbor checks.
- Updated relevant tests to validate new functionality and ensure robustness.
- Incremented application version to 0.8.209 to reflect these changes.
---
backend/planning_exercise_path_builder.py | 31 ++++++++++--
backend/planning_exercise_path_qa.py | 31 +++++++++++-
.../tests/test_planning_exercise_path_qa.py | 48 ++++++++++++++++++-
backend/version.py | 12 ++++-
docs/HANDOVER.md | 2 +-
docs/architecture/PLANNING_KI_ROADMAP.md | 2 +-
.../ExerciseProgressionPathBuilder.jsx | 5 ++
7 files changed, 119 insertions(+), 12 deletions(-)
diff --git a/backend/planning_exercise_path_builder.py b/backend/planning_exercise_path_builder.py
index 2aaad49..d2ca3f8 100644
--- a/backend/planning_exercise_path_builder.py
+++ b/backend/planning_exercise_path_builder.py
@@ -652,10 +652,18 @@ def suggest_progression_path(
reorder_applied = False
reorder_notes: List[str] = []
+ roadmap_qa_mode: Optional[str] = None
if body.include_path_qa:
- gaps = detect_path_gaps(cur, steps, brief=semantic_brief)
+ if roadmap_first:
+ roadmap_qa_mode = "roadmap_first_lite"
+ gaps = detect_path_gaps(
+ cur,
+ steps,
+ brief=semantic_brief,
+ roadmap_first=roadmap_first,
+ )
unfilled_gaps: List[Dict[str, Any]] = []
- if gaps:
+ if gaps and not roadmap_first:
bridge_fn = _make_bridge_search_fn(
cur,
tenant=tenant,
@@ -676,6 +684,8 @@ def suggest_progression_path(
brief=semantic_brief,
bridge_search_fn=bridge_fn,
)
+ elif gaps and roadmap_first:
+ unfilled_gaps = list(gaps)
if body.include_llm_path_qa:
llm_qa, llm_qa_applied = try_llm_qa_progression_path(
@@ -687,7 +697,12 @@ def suggest_progression_path(
bridge_inserts=bridge_inserts,
)
- if body.include_path_reorder and llm_qa_applied and llm_qa:
+ if (
+ body.include_path_reorder
+ and not roadmap_first
+ and llm_qa_applied
+ and llm_qa
+ ):
q_score = llm_qa.get("quality_score")
try:
q_val = float(q_score) if q_score is not None else None
@@ -700,7 +715,12 @@ def suggest_progression_path(
steps, stripped_off_topic = strip_off_topic_steps_from_path(steps, off_topic_steps)
if stripped_off_topic:
off_topic_steps = []
- gaps = detect_path_gaps(cur, steps, brief=semantic_brief)
+ gaps = detect_path_gaps(
+ cur,
+ steps,
+ brief=semantic_brief,
+ roadmap_first=roadmap_first,
+ )
llm_gap_specs = parse_llm_suggested_new_exercises(
llm_qa,
@@ -746,12 +766,15 @@ def suggest_progression_path(
llm_applied=llm_qa_applied,
reorder_applied=reorder_applied,
reorder_notes=reorder_notes,
+ roadmap_qa_mode=roadmap_qa_mode,
)
target_profile_summary = path_target_profile.to_summary_dict(cur)
retrieval_parts = ["profile_v1", "full_library", "path_builder", "semantics"]
if roadmap_first:
retrieval_parts.append("roadmap_first")
+ if roadmap_qa_mode:
+ retrieval_parts.append(roadmap_qa_mode)
if body.include_path_qa:
retrieval_parts.append("path_qa")
if llm_qa_applied:
diff --git a/backend/planning_exercise_path_qa.py b/backend/planning_exercise_path_qa.py
index aad489e..2f41247 100644
--- a/backend/planning_exercise_path_qa.py
+++ b/backend/planning_exercise_path_qa.py
@@ -141,21 +141,45 @@ def measure_step_transition_gap(
}
+def is_roadmap_planned_neighbor_pair(
+ step_a: Mapping[str, Any],
+ step_b: Mapping[str, Any],
+) -> bool:
+ """Aufeinanderfolgende Major Steps aus roadmap_first — kein Skill-Übergangs-Lücke."""
+ if step_a.get("roadmap_match_source") != "stage_spec":
+ return False
+ if step_b.get("roadmap_match_source") != "stage_spec":
+ return False
+ idx_a = step_a.get("roadmap_major_step_index")
+ idx_b = step_b.get("roadmap_major_step_index")
+ if idx_a is None or idx_b is None:
+ return False
+ try:
+ return int(idx_b) == int(idx_a) + 1
+ except (TypeError, ValueError):
+ return False
+
+
def detect_path_gaps(
cur,
steps: Sequence[Mapping[str, Any]],
*,
brief: PlanningSemanticBrief,
+ roadmap_first: bool = False,
) -> List[Dict[str, Any]]:
if len(steps) < 2:
return []
gaps: List[Dict[str, Any]] = []
total_segments = len(steps) - 1
for i in range(total_segments):
+ step_a = steps[i]
+ step_b = steps[i + 1]
+ if roadmap_first and is_roadmap_planned_neighbor_pair(step_a, step_b):
+ continue
gap = measure_step_transition_gap(
cur,
- steps[i],
- steps[i + 1],
+ step_a,
+ step_b,
brief=brief,
segment_index=i,
total_segments=total_segments,
@@ -516,6 +540,7 @@ def build_path_qa_summary(
llm_applied: bool,
reorder_applied: bool = False,
reorder_notes: Optional[Sequence[str]] = None,
+ roadmap_qa_mode: Optional[str] = None,
) -> Dict[str, Any]:
offers = list(gap_fill_offers or [])
off_topic = list(off_topic_steps or [])
@@ -534,6 +559,7 @@ def build_path_qa_summary(
"llm_qa_applied": llm_applied,
"reorder_applied": reorder_applied,
"reorder_notes": list(reorder_notes or []),
+ "roadmap_qa_mode": roadmap_qa_mode,
}
if llm_qa:
summary["overall_ok"] = bool(llm_qa.get("overall_ok", True))
@@ -562,6 +588,7 @@ __all__ = [
"build_path_qa_summary",
"detect_off_topic_steps",
"detect_path_gaps",
+ "is_roadmap_planned_neighbor_pair",
"strip_off_topic_steps_from_path",
"find_step_pair_index",
"insert_bridge_exercises",
diff --git a/backend/tests/test_planning_exercise_path_qa.py b/backend/tests/test_planning_exercise_path_qa.py
index f781cbf..929e187 100644
--- a/backend/tests/test_planning_exercise_path_qa.py
+++ b/backend/tests/test_planning_exercise_path_qa.py
@@ -1,7 +1,11 @@
-"""Tests Planungs-KI Phase E — Pfad-QA."""
+"""Tests Planungs-KI Phase E/F — Pfad-QA."""
from planning_exercise_path_builder import _pick_best_path_hit
from planning_exercise_semantics import build_semantic_brief
-from planning_exercise_path_qa import apply_llm_path_reorder
+from planning_exercise_path_qa import (
+ apply_llm_path_reorder,
+ detect_path_gaps,
+ is_roadmap_planned_neighbor_pair,
+)
def test_pick_best_path_hit_prefers_semantic_score():
@@ -62,6 +66,46 @@ def test_apply_llm_path_reorder_permutation():
assert notes
+def test_is_roadmap_planned_neighbor_pair():
+ a = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 1}
+ b = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 2}
+ c = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 4}
+ assert is_roadmap_planned_neighbor_pair(a, b) is True
+ assert is_roadmap_planned_neighbor_pair(a, c) is False
+ assert is_roadmap_planned_neighbor_pair({"exercise_id": 1}, b) is False
+
+
+def test_detect_path_gaps_skips_roadmap_neighbors():
+ brief = build_semantic_brief("Mae Geri")
+ steps = [
+ {
+ "exercise_id": 1,
+ "title": "A",
+ "roadmap_match_source": "stage_spec",
+ "roadmap_major_step_index": 0,
+ },
+ {
+ "exercise_id": 2,
+ "title": "B",
+ "roadmap_match_source": "stage_spec",
+ "roadmap_major_step_index": 1,
+ },
+ ]
+
+ class _FakeCur:
+ def execute(self, *args, **kwargs):
+ return None
+
+ def fetchall(self):
+ return []
+
+ def fetchone(self):
+ return {"title": "X", "summary": "", "goal": ""}
+
+ gaps = detect_path_gaps(_FakeCur(), steps, brief=brief, roadmap_first=True)
+ assert gaps == []
+
+
def test_apply_llm_path_reorder_invalid_ignored():
steps = [{"exercise_id": 1}, {"exercise_id": 2}]
reordered, applied, _ = apply_llm_path_reorder(steps, {"ordered_step_indices": [0, 0]})
diff --git a/backend/version.py b/backend/version.py
index 919529e..f0d45b9 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
-APP_VERSION = "0.8.208"
+APP_VERSION = "0.8.209"
BUILD_DATE = "2026-06-07"
DB_SCHEMA_VERSION = "20260606086"
@@ -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.20.0", # Phase D: planning_context an suggestExerciseAi
+ "planning_exercise_suggest": "0.20.1", # F3-Polish: roadmap_first QA lite (keine Brücken/Reorder)
"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.209",
+ "date": "2026-06-07",
+ "changes": [
+ "F3-Polish: roadmap_first — keine Brücken zwischen Major Steps, kein LLM-Reorder.",
+ "Pfad-QS: Lücken nur noch bei Nicht-Roadmap-Übergängen; roadmap_qa_mode in Response.",
+ ],
+ },
{
"version": "0.8.208",
"date": "2026-06-07",
diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md
index 35c6322..869ba42 100644
--- a/docs/HANDOVER.md
+++ b/docs/HANDOVER.md
@@ -108,7 +108,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
| **E2** | Pfad-Neuordnung (LLM) + KI-Neuanlage bei unüberbrückbaren Lücken | ✅ **0.8.187** |
| **E3** | `gap_fill_offers`, Off-Topic-Strip, voller KI-Call bei Lücken | ✅ **0.8.203** |
| **F0–F2** | Roadmap-Pipeline + LLM-Prompts (078/079) | ✅ **0.8.205** |
-| **F3** | `roadmap_first` — Retrieval pro `stage_spec` | ✅ **0.8.206** |
+| **F3** | `roadmap_first` — Retrieval + QA lite (keine Brücken/Reorder) | ✅ **0.8.209** |
| **F4** | Roadmap-Review UI + `roadmap_override` | ✅ **0.8.207** |
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | ✅ **0.8.208** |
diff --git a/docs/architecture/PLANNING_KI_ROADMAP.md b/docs/architecture/PLANNING_KI_ROADMAP.md
index dd19042..8f0cda3 100644
--- a/docs/architecture/PLANNING_KI_ROADMAP.md
+++ b/docs/architecture/PLANNING_KI_ROADMAP.md
@@ -62,7 +62,7 @@ Diese Roadmap ergänzt die **Architektur-Refaktor-Roadmap** (`UMSETZUNGSPLAN_ROA
- [x] Retrieval pro `major_step` + `stage_spec` statt iterativem Pfad-Bau
- [x] Gap-Angebote für unbesetzte Roadmap-Stufen (`roadmap_unfilled`)
-- [ ] QA/Lücken vollständig an Roadmap koppeln (Brücken optional reduzieren)
+- [x] QA/Lücken an Roadmap gekoppelt (`roadmap_first_lite`: keine Brücken/Reorder zwischen Major Steps)
### F4 — UI (0.8.207)
diff --git a/frontend/src/components/ExerciseProgressionPathBuilder.jsx b/frontend/src/components/ExerciseProgressionPathBuilder.jsx
index 4082596..57c5bae 100644
--- a/frontend/src/components/ExerciseProgressionPathBuilder.jsx
+++ b/frontend/src/components/ExerciseProgressionPathBuilder.jsx
@@ -907,6 +907,11 @@ export default function ExerciseProgressionPathBuilder({
: ''}
) : null}
+ {pathQa.roadmap_qa_mode === 'roadmap_first_lite' ? (
+
+ QS an Roadmap gekoppelt: keine Brücken/Reihenfolge zwischen Major Steps (didaktisch bereits geplant).
+
+ ) : null}
) : null}