409 lines
20 KiB
Python
409 lines
20 KiB
Python
"""Ebene 4: Guide. Eine Section pro Baustein, Stages seriell je Karte:
|
||
writer → pruefer → fix → done (persistiert in sections.stage, resume-fähig).
|
||
|
||
Der Writer bekommt die komplette Faktenbasis INLINE (Definitionen, Anker-Zitate,
|
||
verifizierte Beispiele — Lektion 48) und muss Marker als unsichtbare Invariante
|
||
setzen (Lektion 57). Der Prüfer ist EIN verschmolzener Call (Fakten+Coverage,
|
||
Lektion 52); deterministische Checks laufen davor im Code."""
|
||
|
||
import asyncio
|
||
import logging
|
||
import re
|
||
|
||
import db
|
||
import llm
|
||
import textkit
|
||
from config import SECTION_WOERTER_PRO_ATOM
|
||
|
||
log = logging.getLogger("creator2.guide")
|
||
|
||
EBENE = "guide"
|
||
_MARKER = re.compile(r"<!--\s*atom:\s*(\d+)\s*\|")
|
||
|
||
# Kritische Befund-Arten routen zurück auf den Prüfer, Stil-Arten nur in den Fix
|
||
# (vorwaerts ist Stil: ein Halbsatz-Fix, kein Fakten-Problem)
|
||
KRITISCH = ("marker_fehlend", "ziel_ohne_anker", "fachlich_falsch")
|
||
|
||
|
||
def _bausteine(topic: str) -> list[dict]:
|
||
return db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,))
|
||
|
||
|
||
def _atome_von(baustein_id: int) -> list[dict]:
|
||
return db.query("SELECT * FROM atome WHERE baustein_id=? AND status NOT IN"
|
||
" ('gemerged','verworfen') ORDER BY ord", (baustein_id,))
|
||
|
||
|
||
def _zitate(atom_id: int, max_n: int = 3) -> list[str]:
|
||
rows = db.query("SELECT zitat FROM anker WHERE atom_id=? AND start>=0 LIMIT ?",
|
||
(atom_id, max_n))
|
||
return [r["zitat"] for r in rows]
|
||
|
||
|
||
def _beispiel(atom_id: int) -> str:
|
||
row = db.one("SELECT inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'"
|
||
" AND status='verifiziert' LIMIT 1", (atom_id,))
|
||
return db.uj(row["inhalt"], {}).get("text", "") if row else ""
|
||
|
||
|
||
def _fragen_pool(atom_id: int, max_n: int = 2) -> list[str]:
|
||
rows = db.query("SELECT inhalt FROM artefakte WHERE atom_id=? AND typ='flashcard'"
|
||
" AND status='verifiziert' LIMIT ?", (atom_id, max_n))
|
||
return [f for r in rows if (f := db.uj(r["inhalt"], {}).get("frage", "").strip())]
|
||
|
||
|
||
def _atom_paket(a: dict) -> str:
|
||
zitate = "\n".join(f"> {z}" for z in _zitate(a["id"])) or "(kein Zitat)"
|
||
beispiel = _beispiel(a["id"])
|
||
teil = (f"MARKER (exakt so übernehmen): <!-- atom: {a['id']} | {a['titel']} | {a['level']} -->\n"
|
||
f"Definition: {a['definition']}\nBelege (VERBATIM zitierbar):\n{zitate}")
|
||
if beispiel:
|
||
teil += f"\nVerifiziertes Beispiel (Werte wörtlich übernehmen):\n{beispiel}"
|
||
fragen = _fragen_pool(a["id"])
|
||
if fragen:
|
||
teil += "\nFRAGEN-POOL:\n" + "\n".join(f"- {f}" for f in fragen)
|
||
return teil
|
||
|
||
|
||
def _laenge_band(n_atome: int) -> tuple[int, int]:
|
||
lo, hi = SECTION_WOERTER_PRO_ATOM
|
||
return lo * n_atome, hi * n_atome
|
||
|
||
|
||
# ── Stages ────────────────────────────────────────────────────────────────────
|
||
|
||
async def _stage_writer(ctx: llm.Kontext, b: dict) -> str:
|
||
atome = _atome_von(b["id"])
|
||
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],)) or {"text": b["titel"]}
|
||
lo, hi = _laenge_band(len(atome))
|
||
res = await llm.call(ctx, stage="writer", template="Guide-Writer",
|
||
werte={"titel": b["titel"], "ziel": ziel["text"],
|
||
"atome": "\n\n".join(_atom_paket(a) for a in atome),
|
||
"min_woerter": lo, "max_woerter": hi},
|
||
role="guide", n=len(atome), item=f"b{b['id']}", erwartet=dict)
|
||
if res is None:
|
||
return "writer" # Stage bleibt, nächster Lauf versucht erneut
|
||
kompakt, lang = str(res.get("kompakt", "")), str(res.get("lang", ""))
|
||
fehlend = _marker_fehlend(lang, atome)
|
||
if fehlend: # ein gezielter Zweitversuch mit explizitem Mangel
|
||
res2 = await llm.call(ctx, stage="writer2", template="Guide-Writer",
|
||
schritt="writer", role="guide", n=len(atome),
|
||
item=f"b{b['id']}-2",
|
||
werte={"titel": b["titel"], "ziel": ziel["text"],
|
||
"atome": "\n\n".join(_atom_paket(a) for a in atome),
|
||
"min_woerter": lo, "max_woerter": hi},
|
||
erwartet=dict)
|
||
if res2 and not _marker_fehlend(str(res2.get("lang", "")), atome):
|
||
kompakt, lang = str(res2.get("kompakt", "")), str(res2.get("lang", ""))
|
||
db.update("sections", "baustein_id", b["id"], text_kompakt=kompakt, text_lang=lang)
|
||
return "pruefer"
|
||
|
||
|
||
def _marker_fehlend(text: str, atome: list[dict]) -> list[int]:
|
||
da = {int(m) for m in _MARKER.findall(text)}
|
||
return [a["id"] for a in atome if a["id"] not in da]
|
||
|
||
|
||
def _det_auftraege(topic: str, b: dict, lang: str) -> list[str]:
|
||
"""Deterministische Checks vor dem Judge: Marker, Länge, Vorwärtsverweise."""
|
||
atome = _atome_von(b["id"])
|
||
auftraege = [f"KRITISCH: Marker für Atom {i} fehlt — exakt einfügen."
|
||
for i in _marker_fehlend(lang, atome)]
|
||
lo, hi = _laenge_band(len(atome))
|
||
woerter = len(lang.split())
|
||
if woerter > hi:
|
||
auftraege.append(f"Kürzen auf höchstens {hi} Wörter (aktuell {woerter}).")
|
||
elif woerter < lo:
|
||
auftraege.append(f"Ausbauen auf mindestens {lo} Wörter (aktuell {woerter}).")
|
||
auftraege += [f"Vorwärtsverweis „{t}“ (kommt erst in einem späteren Kapitel): beim"
|
||
f" ersten Auftreten mit einem Halbsatz einordnen („… dazu später mehr“)"
|
||
f" oder entfernen — nie unerklärt verwenden."
|
||
for t in _vorwaertsverweise(topic, b, lang)]
|
||
zitate = sum(1 for z in lang.splitlines() if z.lstrip().startswith(">"))
|
||
if zitate: # Lehren statt Abschreiben: Rohzitate gehören nicht in den Lehrtext
|
||
auftraege.append(f"{zitate} Blockquote-Zeile(n) („>“) im Text: Inhalt in eigenen"
|
||
f" Worten in den Fließtext einarbeiten, Zitat-Format entfernen.")
|
||
if "6=" in lang:
|
||
auftraege.append("PDF-Artefakt „6=“ im Text: gemeint ist Ungleichheit —"
|
||
" durch $\\neq$ ersetzen.")
|
||
for nr, absatz in enumerate(lang.split("\n\n"), 1):
|
||
if absatz.count("$") % 2: # ein einzelnes $ zieht Fließtext in die Formel
|
||
auftraege.append(f"Absatz {nr}: ungerade Anzahl $-Zeichen — jede Formel"
|
||
f" braucht öffnendes UND schließendes $ (Beispiel-Fehler:"
|
||
f" „$[.“ statt „$[$.“).")
|
||
ohne_mathe = re.sub(r"\$\$[\s\S]*?\$\$|\$[^$\n]*\$", "", lang)
|
||
nackt = sorted(set(re.findall(
|
||
r"\\(?:times|Sigma|Gamma|subseteq|neq|leq|geq|cup|cap|mid|forall|exists"
|
||
r"|mathbb|frac|text|dots|ldots|quad|qquad|bar|setminus)\b", ohne_mathe)))
|
||
if nackt: # KaTeX rendert nur innerhalb von $…$ — nackte Befehle bleiben Rohtext
|
||
auftraege.append(f"LaTeX ohne $-Delimiter im Text ({', '.join(nackt[:5])}…):"
|
||
f" jede Formel vollständig in $…$ bzw. $$…$$ einschließen.")
|
||
return auftraege
|
||
|
||
|
||
def _vorwaertsverweise(topic: str, b: dict, lang: str) -> list[str]:
|
||
"""Titel von Atomen SPÄTERER KAPITEL im Text (innerhalb eines Kapitels sind
|
||
Verweise normal — nur Kapitel-Sprünge stören den Lesefluss). Nur signifikante
|
||
Titel (≥2 Tokens oder ≥6 Zeichen — Lektion 35), Ganzwort, ohne Marker-Zeilen."""
|
||
kap_ord = {k["id"]: k["ord"] for k in
|
||
db.query("SELECT * FROM kapitel WHERE topic=?", (topic,))}
|
||
je_baustein = {bb["id"]: kap_ord.get(bb["kapitel_id"], 0) for bb in _bausteine(topic)}
|
||
mein_kapitel = je_baustein.get(b["id"], 0)
|
||
text = textkit.norm(_MARKER.sub("", lang))
|
||
treffer = []
|
||
for a in db.query(
|
||
"SELECT a.titel, bb.id AS bid FROM atome a JOIN bausteine bb ON a.baustein_id=bb.id"
|
||
" WHERE bb.topic=? AND a.status NOT IN ('gemerged','verworfen')", (topic,)):
|
||
if je_baustein.get(a["bid"], 0) <= mein_kapitel:
|
||
continue
|
||
t = textkit.norm(a["titel"])
|
||
if len(t) < 6 and len(t.split()) < 2:
|
||
continue
|
||
if re.search(rf"(?<![a-zäöüß0-9]){re.escape(t)}(?![a-zäöüß0-9])", text):
|
||
treffer.append(a["titel"])
|
||
return treffer
|
||
|
||
|
||
async def _stage_pruefer(ctx: llm.Kontext, b: dict, tag: str = "") -> str:
|
||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||
atome = _atome_von(b["id"])
|
||
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],)) or {"text": b["titel"]}
|
||
auftraege = _det_auftraege(ctx.topic, b, sec["text_lang"])
|
||
fakten = "\n\n".join(f"Atom {a['id']} ({a['titel']}): {a['definition']}\n"
|
||
+ "\n".join(f"> {z}" for z in _zitate(a["id"])) for a in atome)
|
||
res = await llm.call(ctx, stage=f"pruefer{tag}", template="Guide-Pruefer",
|
||
schritt="pruefer", role="judge", n=len(atome),
|
||
item=f"b{b['id']}{tag}",
|
||
werte={"ziel": ziel["text"], "fakten": fakten,
|
||
"kompakt": sec["text_kompakt"], "lang": sec["text_lang"]},
|
||
erwartet=dict)
|
||
for e in (res or {}).get("befunde", []):
|
||
art = str(e.get("art", "")).strip()
|
||
detail = str(e.get("detail", "")).strip()
|
||
if art in ("falsch", "luecke"):
|
||
auftraege.append(f"KRITISCH ({art}): {detail}")
|
||
elif art == "stil" and detail:
|
||
auftraege.append(detail)
|
||
db.update("sections", "baustein_id", b["id"], befunde=db.j(auftraege))
|
||
return "fix" if auftraege else "done"
|
||
|
||
|
||
async def _stage_fix(ctx: llm.Kontext, b: dict) -> str:
|
||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||
auftraege = db.uj(sec["befunde"])
|
||
if not auftraege:
|
||
return "done"
|
||
atome = _atome_von(b["id"])
|
||
fakten = "\n\n".join(_atom_paket(a) for a in atome)
|
||
res = await llm.call(ctx, stage="fix", template="Guide-Fix",
|
||
role="guide", n=len(auftraege), item=f"b{b['id']}",
|
||
werte={"kompakt": sec["text_kompakt"], "lang": sec["text_lang"],
|
||
"auftraege": "\n".join(f"- {a}" for a in auftraege),
|
||
"fakten": fakten},
|
||
erwartet=dict)
|
||
kritisch = any(a.startswith("KRITISCH") for a in auftraege)
|
||
if res:
|
||
lang = str(res.get("lang", ""))
|
||
if not _marker_fehlend(lang, atome): # Fix darf die Marker-Invariante nie brechen
|
||
db.update("sections", "baustein_id", b["id"],
|
||
text_kompakt=str(res.get("kompakt", "")), text_lang=lang, befunde=db.j([]))
|
||
if kritisch: # genau EIN Re-Check; Rest fängt die Ebenen-QA
|
||
await _stage_pruefer(ctx, b, tag="-re")
|
||
return "done"
|
||
db.update("sections", "baustein_id", b["id"], befunde=db.j([]))
|
||
return "done" # Fix fehlgeschlagen → Original behalten (im Zweifel behalten)
|
||
|
||
|
||
_STAGES = {"writer": _stage_writer, "pruefer": _stage_pruefer, "fix": _stage_fix}
|
||
|
||
|
||
async def _karte(ctx: llm.Kontext, b: dict) -> None:
|
||
db.execute("INSERT OR IGNORE INTO sections(baustein_id) VALUES(?)", (b["id"],))
|
||
for _ in range(6): # Stages seriell; Schutz gegen Stage-Schleifen
|
||
sec = db.one("SELECT stage FROM sections WHERE baustein_id=?", (b["id"],))
|
||
if sec["stage"] == "done":
|
||
db.update("bausteine", "id", b["id"], status="fertig")
|
||
return
|
||
naechste = await _STAGES[sec["stage"]](ctx, b)
|
||
if naechste == sec["stage"]:
|
||
return # kein Fortschritt (Writer erschöpft) → Karte offen lassen
|
||
db.update("sections", "baustein_id", b["id"], stage=naechste)
|
||
db.update("bausteine", "id", b["id"], status="fertig")
|
||
|
||
|
||
async def _kapitel_intros(ctx: llm.Kontext) -> None:
|
||
"""Advance Organizer je Kapitel: 2–4 Sätze Landkarte, KEINE Inhalte
|
||
(Redundancy-Effekt). Idempotent — gefüllte Intros bleiben."""
|
||
ziele = {z["id"]: z for z in db.query("SELECT * FROM lernziele WHERE topic=?", (ctx.topic,))}
|
||
alle = _bausteine(ctx.topic)
|
||
|
||
async def einer(kap: dict) -> None:
|
||
bausteine = [b for b in alle if b["kapitel_id"] == kap["id"]]
|
||
if not bausteine:
|
||
return
|
||
liste = "\n".join(f"- {b['titel']}: {ziele.get(b['ziel_id'], {}).get('text', '')}"
|
||
for b in bausteine)
|
||
res = await llm.call(ctx, stage="kapitel", template="Kapitel-Intro",
|
||
werte={"titel": kap["titel"], "bausteine": liste},
|
||
role="judge", n=len(bausteine), item=f"k{kap['id']}",
|
||
erwartet=dict)
|
||
intro = str((res or {}).get("intro", "")).strip()
|
||
if intro:
|
||
db.update("kapitel", "id", kap["id"], intro=intro)
|
||
|
||
offen = db.query("SELECT * FROM kapitel WHERE topic=? AND intro=''", (ctx.topic,))
|
||
await asyncio.gather(*(einer(k) for k in offen))
|
||
|
||
|
||
async def bauen(ctx: llm.Kontext) -> None:
|
||
ctx.ebene = EBENE
|
||
offen = [b for b in _bausteine(ctx.topic) if b["status"] != "fertig"
|
||
or (db.one("SELECT stage FROM sections WHERE baustein_id=?", (b["id"],)) or
|
||
{"stage": "writer"})["stage"] != "done"]
|
||
await asyncio.gather(*(_karte(ctx, b) for b in offen))
|
||
await _kapitel_intros(ctx)
|
||
|
||
|
||
def kapitel_struktur(topic: str) -> list[dict]:
|
||
"""Kapitel (Struktur-Ebene) entlang der Baustein-Ordnung; die Segmente sind
|
||
kontiguierlich, also wechselt H2 genau an den Kapitel-Grenzen."""
|
||
kaps = {k["id"]: k for k in
|
||
db.query("SELECT * FROM kapitel WHERE topic=? ORDER BY ord", (topic,))}
|
||
ziele = {z["id"]: z for z in db.query("SELECT * FROM lernziele WHERE topic=?", (topic,))}
|
||
kapitel: list[dict] = []
|
||
for b in _bausteine(topic):
|
||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||
k_id = b["kapitel_id"] if b["kapitel_id"] in kaps else None
|
||
if not kapitel or kapitel[-1]["kapitel_id"] != k_id:
|
||
k = kaps.get(k_id, {})
|
||
kapitel.append({"kapitel_id": k_id, "titel": k.get("titel", "Weitere Themen"),
|
||
"intro": k.get("intro", ""), "sections": []})
|
||
kapitel[-1]["sections"].append({
|
||
"baustein": b["id"], "titel": b["titel"],
|
||
"ziel": ziele.get(b["ziel_id"], {}).get("text", ""),
|
||
"kompakt": sec["text_kompakt"] if sec else "",
|
||
"lang": sec["text_lang"] if sec else ""})
|
||
return kapitel
|
||
|
||
|
||
def guide_markdown(topic: str) -> str:
|
||
t = db.one("SELECT titel FROM topics WHERE name=?", (topic,))
|
||
teile = [f"# {t['titel'] if t else topic}"]
|
||
for kap in kapitel_struktur(topic):
|
||
teile.append(f"## {kap['titel']}")
|
||
if kap["intro"]:
|
||
teile.append(kap["intro"])
|
||
for s in kap["sections"]:
|
||
if s["lang"]:
|
||
teile.append(f"### {s['titel']}\n\n{s['lang']}")
|
||
return "\n\n".join(teile)
|
||
|
||
|
||
# ── QA + Repair ───────────────────────────────────────────────────────────────
|
||
|
||
def _stopfrei(text: str) -> set[str]:
|
||
stop = {"der", "die", "das", "und", "oder", "kann", "eine", "einen", "für", "von",
|
||
"mit", "den", "dem", "sich", "auf", "aus", "sind", "wird", "werden", "lernende"}
|
||
return {t for t in textkit.tokens(text) if len(t) >= 4 and t not in stop}
|
||
|
||
|
||
def messen(ctx: llm.Kontext) -> list[dict]:
|
||
befunde = []
|
||
for b in _bausteine(ctx.topic):
|
||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||
if not sec or not sec["text_lang"]:
|
||
befunde.append({"art": "section_fehlt", "item": str(b["id"]), "detail": b["titel"]})
|
||
continue
|
||
lang = sec["text_lang"]
|
||
atome = _atome_von(b["id"])
|
||
for i in _marker_fehlend(lang, atome):
|
||
befunde.append({"art": "marker_fehlend", "item": str(b["id"]),
|
||
"detail": f"Atom {i}"})
|
||
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],))
|
||
if ziel:
|
||
noetig = _stopfrei(ziel["text"])
|
||
# Writer formuliert per Design frei — Schwelle 1/3 statt 1/2, und
|
||
# kompakt + Titel zählen mit (Ziel-Wörter erscheinen paraphrasiert).
|
||
da = textkit.tokens(f"{b['titel']} {sec['text_kompakt']} {lang}")
|
||
if noetig and len(noetig & da) / len(noetig) < 0.34:
|
||
befunde.append({"art": "ziel_ohne_anker", "item": str(b["id"]),
|
||
"detail": ziel["text"][:120]})
|
||
lo, hi = _laenge_band(len(atome))
|
||
w = len(lang.split())
|
||
if not lo <= w <= hi * 1.25: # QA-Band weiter als das Fix-Band (Lektion 75)
|
||
befunde.append({"art": "laenge", "item": str(b["id"]), "detail": f"{w} Wörter"})
|
||
for t in _vorwaertsverweise(ctx.topic, b, lang):
|
||
befunde.append({"art": "vorwaerts", "item": str(b["id"]), "detail": t})
|
||
return befunde
|
||
|
||
|
||
async def messen_llm(ctx: llm.Kontext) -> list[dict]:
|
||
"""Fachlich-falsch-Stichprobe: Verdacht + ZWEI unabhängige Bestätiger —
|
||
nur dreifach bestätigte Claims zählen (Lektion 15). Unveränderte Sections
|
||
werden übersprungen (qa_hash) — die Prüfung lief sonst jede Repair-Iteration
|
||
über ALLE Sections (gemessen: 806 QA-Calls für 50 Sections)."""
|
||
import hashlib
|
||
ctx.ebene = EBENE
|
||
befunde = []
|
||
offene_falsch = {b["item"] for b in db.query(
|
||
"SELECT item FROM befunde WHERE run_id=? AND ebene=? AND art='fachlich_falsch'"
|
||
" AND status='offen'", (ctx.run_id, EBENE))}
|
||
|
||
async def eine(b: dict) -> None:
|
||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||
if not sec or not sec["text_lang"]:
|
||
return
|
||
h = hashlib.sha256(sec["text_lang"].encode()).hexdigest()[:12]
|
||
if h == sec["qa_hash"] and str(b["id"]) not in offene_falsch:
|
||
return # unverändert und sauber — kein Re-Check
|
||
db.execute("UPDATE sections SET qa_hash=? WHERE baustein_id=?", (h, b["id"]))
|
||
atome = _atome_von(b["id"])
|
||
fakten = "\n".join(f"- {a['titel']}: " + " | ".join(_zitate(a["id"], 2)) for a in atome)
|
||
verdacht = await llm.call(ctx, stage="qa_falsch", template="QA-Guide-Falsch",
|
||
schritt="qa_judge", role="judge", item=f"b{b['id']}",
|
||
werte={"fakten": fakten, "text": sec["text_lang"]},
|
||
erwartet=list)
|
||
claims = [str(c.get("claim", "")).strip() for c in verdacht or []
|
||
if isinstance(c, dict) and c.get("claim")]
|
||
if not claims:
|
||
return
|
||
stimmen = await llm.panel(ctx, 2, stage="qa_falsch_check", template="QA-Guide-Falsch-Check",
|
||
schritt="qa_judge", role="judge", item=f"b{b['id']}",
|
||
werte={"fakten": fakten,
|
||
"claims": "\n".join(f"{i + 1}: {c}"
|
||
for i, c in enumerate(claims))},
|
||
erwartet=list)
|
||
for i, claim in enumerate(claims, 1):
|
||
ja = sum(1 for s in stimmen for e in s
|
||
if isinstance(e, dict) and e.get("claim") == i and e.get("falsch"))
|
||
if ja >= 2:
|
||
befunde.append({"art": "fachlich_falsch", "item": str(b["id"]),
|
||
"detail": claim[:200]})
|
||
|
||
await asyncio.gather(*(eine(b) for b in _bausteine(ctx.topic)))
|
||
return befunde
|
||
|
||
|
||
async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||
"""Kritische Befunde → zurück auf pruefer (voller Kontext + Re-Check);
|
||
Stil-Befunde → direkt in den Fix (genau ein Rewrite)."""
|
||
ctx.ebene = EBENE
|
||
betroffen: dict[int, list[str]] = {}
|
||
for b in befunde:
|
||
try:
|
||
b_id = int(b["item"])
|
||
except (ValueError, TypeError):
|
||
continue
|
||
betroffen.setdefault(b_id, []).append(b["art"])
|
||
for b_id, arten in betroffen.items():
|
||
if any(a in KRITISCH or a == "section_fehlt" for a in arten):
|
||
stage = "writer" if "section_fehlt" in arten else "pruefer"
|
||
db.execute("UPDATE sections SET stage=? WHERE baustein_id=?", (stage, b_id))
|
||
else:
|
||
auftraege = [f"Behebe: {a}" for a in arten]
|
||
db.update("sections", "baustein_id", b_id, stage="fix", befunde=db.j(auftraege))
|
||
db.update("bausteine", "id", b_id, status="repair")
|
||
if betroffen:
|
||
await bauen(ctx)
|
||
return bool(betroffen)
|