diff --git a/backend/guide_board.py b/backend/guide_board.py index 284d989..221df1b 100644 --- a/backend/guide_board.py +++ b/backend/guide_board.py @@ -703,6 +703,39 @@ async def board_snapshot(topic: str, format_name: str, limit: int = 20) -> dict: return {"columns": columns, "qa_guide": note_guide} +async def repair_karten(topic: str, format_name: str) -> list[str]: + """QA-Befund-getriebenes Guide-Repair: Karten, die im jüngsten Guide-QA-Report + Befunde tragen, gehen zurück auf `pruefer` (md bleibt) — Prüfer+Fix beheben gezielt, + generate_guide resumt die offenen Karten und misst am Ende neu. Pendant zum + Blocks-Repair („Score unter 10 muss einen Fix-Pfad haben"). → betroffene Blocktitel.""" + import qa as qa_mod + tdir = qa_mod.QA_DIR / topic + reports = sorted(tdir.glob("guide-*.json"), key=lambda p: p.stat().st_mtime) if tdir.is_dir() else [] + rep = _json_file(reports[-1]) if reports else None + if not rep: + return [] + cards = {c["block_norm"]: c for c in await db.list_guide_cards(topic, format_name)} + norms: set[str] = set() + for e in rep.get("marker_fehlend", []): # "Block · sub" + norms.add(_norm_title(str(e).split(" · ")[0])) + for e in rep.get("ziel_ohne_anker", []): # "block_norm · (id) text" + norms.add(str(e).split(" · ")[0]) + for e in rep.get("laengen_ausreisser", []): # {"block": titel} + norms.add(_norm_title(e.get("block", "") if isinstance(e, dict) else str(e))) + for e in rep.get("lesbarkeit", []): # "Block: hinweis" + norms.add(_norm_title(str(e).split(":")[0])) + for t in rep.get("fachlich_falsch", []) or []: + norms.add(_norm_title(str(t))) + for e in rep.get("redundanz", []): # {"a": "Block: absatz", "b": …} + for seite in ("a", "b"): + norms.add(_norm_title(str(e.get(seite, "")).split(":")[0])) + betroffen = [] + for n in sorted(norms & set(cards)): + await db.set_guide_card(topic, format_name, n, stage="pruefer", status="open", gate_info="") + betroffen.append(cards[n]["block"]) + return betroffen + + async def reset_card(topic: str, format_name: str, block_norm: str, ab_stage: int) -> bool: """Reset ONE guide card to a stage (single-card variant of reset_from_stage): fields re-zeroed, md only wiped for writer(2) and earlier, lernziele only for 0.""" diff --git a/backend/routes.py b/backend/routes.py index 6855519..9b13ca1 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -9,9 +9,9 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import Response from agents import active_agents, provider_available -from config import PROJECTS_DIR, UNI_DIR, PROVIDERS +from config import DEFAULT_PROVIDER, PROJECTS_DIR, UNI_DIR, PROVIDERS from database import ( - create_guide, delete_guide, get_guide, list_guides, + create_guide, delete_guide, get_guide, list_guides, update_guide, create_topic, list_topics as db_list_topics, delete_topic, list_block_progress, get_block_progress, set_open_question, set_block_score_and_streak, @@ -681,6 +681,27 @@ async def get_guide_board(topic: str, format: str = "Guide"): return snap +@router.post("/guides/board/repair") +async def repair_guide_board(req: GuideBoardResetRequest): + """Befunde beheben fürs Guide-Board: Karten mit QA-Befunden zurück auf `pruefer`, + dann resumt generate_guide die offenen Karten (Prüfer+Fix) und misst neu.""" + import guide_board + topic = req.topic.strip() + guide = next((g for g in await list_guides() + if g["topic"] == topic and g["format"] == req.format), None) + if guide is None: + raise HTTPException(404, "Kein Guide für dieses Topic/Format") + if guide["status"] in ("queued", "generating"): + return {"ok": True, "status": "generating", "betroffen": []} + betroffen = await guide_board.repair_karten(topic, req.format) + if betroffen: + now = datetime.now(timezone.utc).isoformat() + await update_guide(guide["id"], status="queued", progress="Repair…", updated_at=now) + asyncio.create_task(generate_guide(guide["id"], topic, req.format, + guide.get("instructions") or "", DEFAULT_PROVIDER)) + return {"ok": True, "betroffen": betroffen} + + @router.post("/guides/board/reset") async def reset_guide_board(req: GuideBoardResetRequest): """Reset cards from a stage onward — without generation (pendant to blocks reset-stage).""" diff --git a/backend/tests/test_guide_board.py b/backend/tests/test_guide_board.py index b3b1235..41d49a7 100644 --- a/backend/tests/test_guide_board.py +++ b/backend/tests/test_guide_board.py @@ -417,3 +417,29 @@ async def test_laengen_trigger_startet_fix(testdb, tmp_path, monkeypatch): card.update(stage="fix", gate_info=(await db.list_guide_cards(TOPIC, FMT))[0]["gate_info"]) assert await gb._stage_fix(env, card) assert "Länge" in seen["auftraege"] and str(gb._writer_budget(1)) in seen["auftraege"] + + +async def test_repair_karten_setzt_befundkarten_auf_pruefer(testdb, tmp_path, monkeypatch): + """Guide-Repair: Karten mit QA-Befunden (alle Befundklassen-Formate) → pruefer, + md bleibt; Karten ohne Befund unangetastet. Kein Report → leere Liste.""" + import json as _json + import qa as qa_mod + db = testdb + monkeypatch.setattr(qa_mod, "QA_DIR", tmp_path / "qa") + for n in ("alpha", "beta", "gamma"): + await db.upsert_guide_card(TOPIC, FMT, n, n.title()) + await db.set_guide_card(TOPIC, FMT, n, stage="done", status="ok", md="\nText") + assert await gb.repair_karten(TOPIC, FMT) == [] # kein Report + tdir = tmp_path / "qa" / TOPIC + tdir.mkdir(parents=True) + (tdir / "guide-20260705-000000.json").write_text(_json.dumps({ + "marker_fehlend": ["Alpha · sub eins"], + "fachlich_falsch": ["Beta"], + "ziel_ohne_anker": [], "laengen_ausreisser": [], "redundanz": [], "lesbarkeit": [], + }), encoding="utf-8") + betroffen = await gb.repair_karten(TOPIC, FMT) + assert sorted(betroffen) == ["Alpha", "Beta"] + cards = {c["block_norm"]: c for c in await db.list_guide_cards(TOPIC, FMT)} + assert cards["alpha"]["stage"] == cards["beta"]["stage"] == "pruefer" + assert cards["alpha"]["md"] # Text bleibt — der Prüfer arbeitet auf dem Bestand + assert cards["gamma"]["stage"] == "done" diff --git a/frontend/src/api.js b/frontend/src/api.js index e0b8ce7..cd39cab 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -135,6 +135,16 @@ export async function resetGuideBoard(topic, format, abStage) { return jsonOrThrow(res) } +// Befunde beheben: Karten mit QA-Befunden zurück auf Prüfen + Resume-Lauf. +export async function repairGuideBoard(topic, format) { + const res = await fetch(`${BASE}/guides/board/repair`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ topic, format, ab_stage: 0 }), + }) + return jsonOrThrow(res) +} + export async function cancelBlocks(topic) { await fetch(`${BASE}/blocks/cancel?topic=${encodeURIComponent(topic)}`, { method: 'POST' }) } diff --git a/frontend/src/components/GuideBoardSection.vue b/frontend/src/components/GuideBoardSection.vue index c2f0d74..4dadf52 100644 --- a/frontend/src/components/GuideBoardSection.vue +++ b/frontend/src/components/GuideBoardSection.vue @@ -1,6 +1,6 @@