init
This commit is contained in:
460
backend/guide.py
Normal file
460
backend/guide.py
Normal file
@@ -0,0 +1,460 @@
|
||||
"""Stufe 4: Writer (DELIMITED, „bekannt"-Kontext, Verweis statt Wiederholung)
|
||||
→ deterministische Prüfung → 2er-Prüfer-Panel (qa_hash, fail-closed) → Fix
|
||||
(Cap 3, Klärung 3er einmalig). Gate re-prüft die harten Checks selbst."""
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
from . import belege, config, db, engine, llm, panels, textkit
|
||||
|
||||
_MARKER_RE = re.compile(r"<!--\s*atom:\s*(\d+)\s*-->")
|
||||
|
||||
|
||||
def _baustein(bid: int) -> dict:
|
||||
return db.one("SELECT * FROM bausteine WHERE id=?", bid)
|
||||
|
||||
|
||||
def _atome(bid: int) -> list[dict]:
|
||||
return db.query("SELECT * FROM atome WHERE baustein_id=? AND status='aktiv' "
|
||||
"ORDER BY ord", bid)
|
||||
|
||||
|
||||
def _band(n_atome: int) -> tuple[int, int]:
|
||||
lo, hi = config.SECTION_WOERTER_PRO_ATOM
|
||||
return lo * n_atome, hi * n_atome
|
||||
|
||||
|
||||
def _slug(titel: str) -> str:
|
||||
s = textkit.norm(titel).replace(" ", "-")
|
||||
return re.sub(r"[^a-z0-9äöüß-]", "", s)
|
||||
|
||||
|
||||
def _bekannt_kontext(topic: str, baustein: dict) -> str:
|
||||
"""Bereits behandelte Atome (frühere Kapitel/Bausteine) als Linkziele."""
|
||||
fruehere = db.query(
|
||||
"SELECT a.titel, b2.titel bt FROM atome a JOIN bausteine b2 ON "
|
||||
"b2.id=a.baustein_id JOIN kapitel k2 ON k2.id=b2.kapitel_id "
|
||||
"JOIN bausteine b ON b.id=? JOIN kapitel k ON k.id=b.kapitel_id "
|
||||
"WHERE a.topic=? AND a.status='aktiv' AND "
|
||||
"(k2.ord < k.ord OR (k2.ord=k.ord AND b2.ord < b.ord))",
|
||||
baustein["id"], topic)
|
||||
if not fruehere:
|
||||
return "-"
|
||||
return "\n".join(f"- {f['titel']} (im Abschnitt '{f['bt']}')"
|
||||
for f in fruehere[:60])
|
||||
|
||||
|
||||
@engine.worker("guide.writer")
|
||||
async def writer(task: dict) -> engine.Ergebnis:
|
||||
payload = db.uj(task["payload"], {})
|
||||
bid = payload["baustein_id"]
|
||||
b = _baustein(bid)
|
||||
atome = _atome(bid)
|
||||
if not atome:
|
||||
return engine.Ergebnis(daten={"skip": True})
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", bid)
|
||||
if sec and sec["text"] and sec["stage"] != "writer" and \
|
||||
not payload.get("neu"): # Resume-Skip — außer bei Neuschreiben
|
||||
return engine.Ergebnis(daten={"skip": True}, neue_tasks=[
|
||||
{"knoten": "det_pruefung", "item": task["item"].replace("baustein", "det")}])
|
||||
# Prosa-Form statt Feldnamen — „TITEL:/DEFINITION:" leakte 41× in Guides
|
||||
atome_text = "\n".join(
|
||||
f"<!-- atom: {a['id']} -->\n**{a['titel']}** — {a['definition']}"
|
||||
for a in atome)
|
||||
lo, hi = _band(len(atome))
|
||||
antwort = await llm.call(
|
||||
run_id=task["run_id"], stufe="guide", knoten="writer", item=task["item"],
|
||||
role="writer",
|
||||
skill_namen=["autor", "deutsch-praezise", "guide-lesbarkeit", "writer"],
|
||||
werte={"baustein": b["titel"], "atome": atome_text,
|
||||
"belege": belege.fenster_fuer_atome([a["id"] for a in atome])[:40000],
|
||||
"bekannt": _bekannt_kontext(task["topic"], b),
|
||||
"band": f"{lo}-{hi}"})
|
||||
if antwort is None:
|
||||
raise RuntimeError("writer ohne Ergebnis")
|
||||
text = textkit.block_text(antwort, "TEXT") or antwort
|
||||
text = re.sub(r"^=+\s*$", "", text, flags=re.MULTILINE).strip()
|
||||
# Führende Baustein-Titel-Überschrift entfernen — die setzt die Montage
|
||||
m = re.match(r"^#{1,3}\s+(.+)\n+", text)
|
||||
if m and textkit.jaccard(m.group(1), b["titel"], gestemmt=False) >= 0.6:
|
||||
text = text[m.end():]
|
||||
versuche = 0 if payload.get("neu") else \
|
||||
(sec or {}).get("fix_versuche") or 0 # Neuschreiben resettet den Fix-Cap
|
||||
db.execute("INSERT OR REPLACE INTO sections(baustein_id, stage, text, "
|
||||
"qa_hash, fix_versuche) VALUES(?,?,?,?,?)",
|
||||
bid, "geprueft", text, "", versuche)
|
||||
return engine.Ergebnis(daten={"woerter": len(text.split())}, neue_tasks=[
|
||||
{"knoten": "det_pruefung", "item": task["item"].replace("baustein", "det"),
|
||||
"payload": {"baustein_id": bid}}])
|
||||
|
||||
|
||||
def _det_befunde(topic: str, bid: int, text: str) -> list[dict]:
|
||||
atome = _atome(bid)
|
||||
eigene = {a["id"] for a in atome}
|
||||
marker = [int(m) for m in _MARKER_RE.findall(text)]
|
||||
befunde = []
|
||||
for a in atome:
|
||||
if marker.count(a["id"]) != 1:
|
||||
befunde.append({"art": "marker_fehlend" if a["id"] not in marker
|
||||
else "marker_doppelt",
|
||||
"item": f"baustein:{bid}",
|
||||
"detail": f"Atom {a['id']} ({a['titel']})"})
|
||||
for m in set(marker) - eigene:
|
||||
befunde.append({"art": "marker_fremd", "item": f"baustein:{bid}",
|
||||
"detail": f"Atom {m} gehört nicht zum Baustein"})
|
||||
lo, hi = _band(len(atome))
|
||||
woerter = len(re.sub(r"<!--.*?-->", "", text).split())
|
||||
if not lo * 0.75 <= woerter <= hi * 1.25:
|
||||
befunde.append({"art": "laenge", "item": f"baustein:{bid}",
|
||||
"detail": f"{woerter} Wörter, Band {lo}-{hi}"})
|
||||
leak = re.search(r"^(TITEL|TYP|DEFINITION|ZITAT|BELEG):", text, re.MULTILINE)
|
||||
if leak:
|
||||
befunde.append({"art": "format_leak", "item": f"baustein:{bid}",
|
||||
"detail": f"Rohformat im Text: {leak.group(0)}"})
|
||||
# Lesefluss-Checks: linear lesbar, keine Sprünge, keine Echo-Zeilen
|
||||
link = re.search(r"\]\(#[^)]*\)", text)
|
||||
if link:
|
||||
befunde.append({"art": "link_im_text", "item": f"baustein:{bid}",
|
||||
"detail": "Anker-Link im Fließtext (Rückbezug als Prosa)"})
|
||||
echo = re.search(r"^\*\*[^*\n]+\*\*\s+—", text, re.MULTILINE)
|
||||
if echo:
|
||||
befunde.append({"art": "atom_echo", "item": f"baustein:{bid}",
|
||||
"detail": f"Fettzeilen-Echo: {echo.group(0)[:60]}"})
|
||||
else:
|
||||
# Semantisches Echo: erster Satz nach dem Marker ≈ Atom-Definition
|
||||
# (fängt Fettzeilen OHNE Gedankenstrich, Gutachten-Rückfall Z.294/297)
|
||||
for a in atome:
|
||||
m = re.search(rf"<!--\s*atom:\s*{a['id']}\s*-->\s*\n+(.+)", text)
|
||||
if not m:
|
||||
continue
|
||||
erster = textkit.satz_split(m.group(1))
|
||||
if erster and textkit.jaccard(erster[0], a["definition"]) >= \
|
||||
config.ECHO_JACCARD:
|
||||
befunde.append({"art": "atom_echo", "item": f"baustein:{bid}",
|
||||
"detail": f"Definition-Echo bei Atom {a['id']}: "
|
||||
f"{erster[0][:60]}"})
|
||||
break
|
||||
h = re.search(r"^#{1,3}\s", text, re.MULTILINE)
|
||||
if h:
|
||||
befunde.append({"art": "h_ebene", "item": f"baustein:{bid}",
|
||||
"detail": "H1-H3 im Section-Text (nur #### erlaubt)"})
|
||||
# Vorwärtsverweise: Titel von Atomen SPÄTERER Kapitel fallen nicht früher
|
||||
b = _baustein(bid)
|
||||
spaetere = db.query(
|
||||
"SELECT a.titel FROM atome a JOIN bausteine b2 ON b2.id=a.baustein_id "
|
||||
"JOIN kapitel k2 ON k2.id=b2.kapitel_id JOIN bausteine b ON b.id=? "
|
||||
"JOIN kapitel k ON k.id=b.kapitel_id WHERE a.topic=? AND "
|
||||
"a.status='aktiv' AND k2.ord > k.ord", bid, topic)
|
||||
# Verlinkte Erwähnungen sind KEIN Vorwärtsverweis — „Verweis statt
|
||||
# Wiederholung" ist ja genau das gewollte Muster
|
||||
ohne_links = re.sub(r"\[[^\]]*\]\([^)]*\)", " ", text)
|
||||
text_norm = textkit.norm(ohne_links)
|
||||
for s in spaetere:
|
||||
titel_norm = textkit.norm(s["titel"])
|
||||
if len(titel_norm) >= 12 and titel_norm in text_norm:
|
||||
befunde.append({"art": "vorwaerts", "item": f"baustein:{bid}",
|
||||
"detail": f"'{s['titel']}' wird erst später gelehrt"})
|
||||
return befunde
|
||||
|
||||
|
||||
@engine.worker("guide.det_pruefung")
|
||||
async def det_pruefung(task: dict) -> engine.Ergebnis:
|
||||
payload = db.uj(task["payload"], {})
|
||||
bid = payload["baustein_id"]
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", bid)
|
||||
befunde = _det_befunde(task["topic"], bid, sec["text"]) if sec else []
|
||||
# Nur Befunde schließen, die es LIVE nicht mehr gibt — fortbestehende
|
||||
# Zeilen behalten ihre Historie (geklaert-Flag, Eskalations-Stand)
|
||||
frisch = {(b["art"], b["item"], b["detail"][:400]) for b in befunde}
|
||||
for row in db.query(
|
||||
"SELECT id, art, item, detail FROM befunde WHERE item=? AND "
|
||||
"status='offen' AND art IN ('marker_fehlend','marker_fremd',"
|
||||
"'marker_doppelt','vorwaerts','laenge','format_leak',"
|
||||
"'link_im_text','atom_echo','h_ebene')",
|
||||
f"baustein:{bid}"):
|
||||
if (row["art"], row["item"], row["detail"][:400]) not in frisch:
|
||||
db.update("befunde", "id=?", (row["id"],), status="veraltet")
|
||||
for b in befunde:
|
||||
befund_merken(task["run_id"], "det_pruefung", b["art"], b["item"],
|
||||
b["detail"], task["runde"])
|
||||
return engine.Ergebnis(daten={"befunde": len(befunde)}, neue_tasks=[
|
||||
{"knoten": "llm_pruefung", "item": task["item"].replace("det", "pruef"),
|
||||
"payload": {"baustein_id": bid}}])
|
||||
|
||||
|
||||
@engine.worker("guide.llm_pruefung", buendel=True)
|
||||
async def llm_pruefung(task: dict) -> engine.Ergebnis:
|
||||
mitglieder = [task] + task.get("_buendel", [])
|
||||
neue = [{"knoten": "redundanz", "item": f"r{task['runde']}:redundanz"}]
|
||||
teil, aktive = {}, {} # bid → (mitglied, section, hash)
|
||||
for m in mitglieder:
|
||||
bid = db.uj(m["payload"], {})["baustein_id"]
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", bid)
|
||||
if sec is None or not sec["text"]:
|
||||
teil[m["id"]] = "fertig"
|
||||
continue
|
||||
hash_ = hashlib.sha256(sec["text"].encode()).hexdigest()[:16]
|
||||
if sec["qa_hash"] == hash_: # unverändert seit letzter Prüfung
|
||||
teil[m["id"]] = "fertig"
|
||||
continue
|
||||
aktive[bid] = (m, sec, hash_)
|
||||
if not aktive:
|
||||
return engine.Ergebnis(daten={"skip": True}, neue_tasks=neue,
|
||||
teil_status=teil)
|
||||
sections_text = "\n\n".join(
|
||||
f"=== SECTION {bid} ===\n{sec['text']}\n\n"
|
||||
f"--- BELEGE für SECTION {bid} ---\n"
|
||||
+ belege.fenster_fuer_atome([a["id"] for a in _atome(bid)])[:30000]
|
||||
for bid, (_, sec, _) in aktive.items())
|
||||
|
||||
async def ruf(i):
|
||||
text = await llm.call(
|
||||
run_id=task["run_id"], stufe="guide", knoten="llm_pruefung",
|
||||
item=f"{task['item']}:p{i}", role="judge", n=len(aktive),
|
||||
skill_namen=["richter", "deutsch-praezise", "guide-pruefung"],
|
||||
werte={"sections": sections_text})
|
||||
if text is None:
|
||||
return None
|
||||
return {"befunde": [b for b in textkit.bloecke(text, "BEFUND")
|
||||
if b.get("art") in ("falsch", "luecke", "stil")
|
||||
and b.get("section", "").strip().isdigit()],
|
||||
"ok": {int(b["section"]) for b in textkit.bloecke(text, "OK")
|
||||
if b.get("section", "").strip().isdigit()}}
|
||||
|
||||
stimmen = await panels.panel(ruf, config.PRUEF_PANEL)
|
||||
gueltig = [s for s in stimmen if s is not None]
|
||||
if len(gueltig) < config.PRUEF_PANEL:
|
||||
# fail-closed: qa_hash NICHT setzen — nächste Runde prüft erneut
|
||||
for _, (m, _, _) in aktive.items():
|
||||
teil[m["id"]] = "fertig"
|
||||
return engine.Ergebnis(daten={"panel_ausfall": True}, neue_tasks=neue,
|
||||
teil_status=teil)
|
||||
gesamt = 0
|
||||
for bid, (m, sec, hash_) in aktive.items():
|
||||
je_stimme = [[b for b in s["befunde"] if int(b["section"]) == bid]
|
||||
for s in gueltig]
|
||||
if not all(bid in s["ok"] or je_stimme[k]
|
||||
for k, s in enumerate(gueltig)):
|
||||
teil[m["id"]] = "neu" # Section fehlt in einer Stimme → neuer Versuch
|
||||
continue
|
||||
bestaetigt = []
|
||||
for b in je_stimme[0]: # nur DOPPELT bestätigte Befunde zählen (Lektion 15)
|
||||
for anderes in je_stimme[1:]:
|
||||
if any(b["art"] == b2.get("art") and textkit.jaccard(
|
||||
b.get("stelle", ""), b2.get("stelle", ""),
|
||||
gestemmt=False) >= 0.3 for b2 in anderes):
|
||||
bestaetigt.append(b)
|
||||
break
|
||||
for b in bestaetigt:
|
||||
befund_merken(task["run_id"], "llm_pruefung", b["art"],
|
||||
f"baustein:{bid}",
|
||||
f"{b.get('stelle', '')[:150]} — {b.get('detail', '')[:200]}",
|
||||
task["runde"])
|
||||
db.update("sections", "baustein_id=?", (bid,), qa_hash=hash_)
|
||||
offen = db.query("SELECT id FROM befunde WHERE stufe='guide' AND item=? "
|
||||
"AND status='offen'", f"baustein:{bid}")
|
||||
if offen and (sec["fix_versuche"] or 0) < config.FIX_MAX_VERSUCHE:
|
||||
neue.append({"knoten": "fix", "item": f"r{task['runde']}:fix:{bid}",
|
||||
"payload": {"baustein_id": bid}})
|
||||
teil[m["id"]] = "fertig"
|
||||
gesamt += len(bestaetigt)
|
||||
return engine.Ergebnis(daten={"befunde": gesamt}, neue_tasks=neue,
|
||||
teil_status=teil)
|
||||
|
||||
|
||||
@engine.worker("guide.fix")
|
||||
async def fix(task: dict) -> engine.Ergebnis:
|
||||
payload = db.uj(task["payload"], {})
|
||||
bid = payload["baustein_id"]
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", bid)
|
||||
offen = db.query("SELECT * FROM befunde WHERE item=? AND status='offen'",
|
||||
f"baustein:{bid}")
|
||||
if sec is None or not offen:
|
||||
return engine.Ergebnis(daten={"skip": True})
|
||||
|
||||
if task["art"] == "klaerung": # 3er-Stichentscheid, je Befund genau einmal
|
||||
return await _klaerung(task, bid, sec, offen)
|
||||
|
||||
antwort = await llm.call(
|
||||
run_id=task["run_id"], stufe="guide", knoten="fix", item=task["item"],
|
||||
role="writer",
|
||||
skill_namen=["autor", "deutsch-praezise", "guide-lesbarkeit", "guide-fix"],
|
||||
werte={"section": sec["text"],
|
||||
"befunde": "\n".join(f"- [{b['art']}] {b['detail']}" for b in offen),
|
||||
"belege": belege.fenster_fuer_atome(
|
||||
[a["id"] for a in _atome(bid)])[:30000]})
|
||||
if antwort is None:
|
||||
raise RuntimeError("fix ohne Ergebnis")
|
||||
text = textkit.block_text(antwort, "TEXT")
|
||||
alte_marker = sorted(_MARKER_RE.findall(sec["text"]))
|
||||
if not text or sorted(_MARKER_RE.findall(text)) != alte_marker:
|
||||
# Fix, der die Marker-Invariante bricht, wird verworfen
|
||||
db.update("sections", "baustein_id=?", (bid,),
|
||||
fix_versuche=(sec["fix_versuche"] or 0) + 1)
|
||||
return engine.Ergebnis(daten={"verworfen": True})
|
||||
db.update("sections", "baustein_id=?", (bid,), text=text, qa_hash="",
|
||||
fix_versuche=(sec["fix_versuche"] or 0) + 1, stage="fix")
|
||||
for b in offen:
|
||||
db.update("befunde", "id=?", (b["id"],), status="behoben")
|
||||
return engine.Ergebnis(daten={"gefixt": len(offen)})
|
||||
|
||||
|
||||
async def _klaerung(task, bid, sec, offen) -> engine.Ergebnis:
|
||||
beleg_text = belege.fenster_fuer_atome([a["id"] for a in _atome(bid)])[:20000]
|
||||
for b in offen:
|
||||
if b["geklaert"]:
|
||||
continue
|
||||
|
||||
async def ruf(i, b=b):
|
||||
text = await llm.call(
|
||||
run_id=task["run_id"], stufe="guide", knoten="fix",
|
||||
item=f"{task['item']}:k{b['id']}:p{i}", role="judge",
|
||||
skill_namen=["richter", "deutsch-praezise", "klaerung"],
|
||||
werte={"befund": f"[{b['art']}] {b['detail']}", "belege": beleg_text})
|
||||
urteile = textkit.bloecke(text or "", "URTEIL")
|
||||
return urteile[0].get("urteil") if urteile else None
|
||||
|
||||
stimmen = await panels.panel(ruf, config.KLAERUNG_PANEL)
|
||||
ist_mangel = panels.mehrheit(
|
||||
stimmen, lambda s: True if s == "mangel"
|
||||
else (False if s == "kein_mangel" else None))
|
||||
db.update("befunde", "id=?", (b["id"],), geklaert=1,
|
||||
status="offen" if ist_mangel else "freispruch")
|
||||
return engine.Ergebnis(daten={"geklaert": len(offen)})
|
||||
|
||||
|
||||
@engine.worker("guide.kapitel_intro")
|
||||
async def kapitel_intro(task: dict) -> engine.Ergebnis:
|
||||
payload = db.uj(task["payload"], {})
|
||||
kap = db.one("SELECT * FROM kapitel WHERE id=?", payload["kapitel_id"])
|
||||
if kap is None:
|
||||
return engine.Ergebnis(daten={"skip": True})
|
||||
if kap["intro"]:
|
||||
return engine.Ergebnis(daten={"skip": True},
|
||||
neue_tasks=[{"knoten": "redundanz",
|
||||
"item": f"r{task['runde']}:redundanz"}])
|
||||
titel = db.query("SELECT titel FROM bausteine WHERE kapitel_id=? ORDER BY ord",
|
||||
kap["id"])
|
||||
vorherige = db.query("SELECT titel FROM kapitel WHERE topic=? AND ord<? "
|
||||
"ORDER BY ord", task["topic"], kap["ord"])
|
||||
antwort = await llm.call(
|
||||
run_id=task["run_id"], stufe="guide", knoten="kapitel_intro",
|
||||
item=task["item"], role="writer",
|
||||
skill_namen=["autor", "deutsch-praezise", "kapitel-intro"],
|
||||
werte={"kapitel": kap["titel"],
|
||||
"sections": "\n".join(f"- {t['titel']}" for t in titel),
|
||||
"vorherige": "\n".join(f"- {v['titel']}" for v in vorherige)
|
||||
or "- (keine)"})
|
||||
intro = textkit.block_text(antwort or "", "INTRO")
|
||||
if not intro: # Ausfall/Format kaputt: neuer Versuch statt leerem Intro
|
||||
raise RuntimeError("kapitel_intro ohne INTRO-Block")
|
||||
db.update("kapitel", "id=?", (kap["id"],), intro=intro[:800])
|
||||
return engine.Ergebnis(daten={"intro": True}, neue_tasks=[
|
||||
{"knoten": "redundanz", "item": f"r{task['runde']}:redundanz"}])
|
||||
|
||||
|
||||
def befund_merken(run_id: int, knoten: str, art: str, item: str, detail: str,
|
||||
runde: int) -> bool:
|
||||
"""Befund-Dedupe (creator2-Schlüssel art+item+detail): existiert derselbe
|
||||
Befund offen oder final (eskaliert/freispruch), KEINE neue Zeile — sonst
|
||||
startet die Eskalations-Kette (geklaert-Flag!) jede Runde von vorn."""
|
||||
kern = detail[:400]
|
||||
if db.one("SELECT id FROM befunde WHERE art=? AND item=? AND "
|
||||
"substr(detail,1,400)=? AND status IN ('offen','eskaliert',"
|
||||
"'freispruch')", art, item, kern):
|
||||
return False
|
||||
db.insert("befunde", run_id=run_id, stufe="guide", knoten=knoten, art=art,
|
||||
item=item, detail=detail[:500], runde=runde)
|
||||
return True
|
||||
|
||||
|
||||
def _rewrites(topic: str, bid) -> int:
|
||||
return db.one("SELECT COUNT(*) c FROM tasks WHERE topic=? AND knoten='writer' "
|
||||
"AND art='reparatur' AND item LIKE ?", topic,
|
||||
f"%:baustein:{bid}")["c"]
|
||||
|
||||
|
||||
@engine.worker("guide.gate")
|
||||
async def gate(task: dict) -> engine.Ergebnis:
|
||||
topic, runde = task["topic"], task["runde"]
|
||||
befunde, neue = [], []
|
||||
bausteine = db.query("SELECT b.id FROM bausteine b WHERE b.topic=?", topic)
|
||||
# Final entschiedene det-Befunde (Klärung/Eskalation) blocken nicht erneut
|
||||
final = {(r["art"], r["item"], r["detail"][:400]) for r in db.query(
|
||||
"SELECT art, item, detail FROM befunde WHERE status IN "
|
||||
"('freispruch','eskaliert') AND run_id IN (SELECT id FROM runs WHERE "
|
||||
"topic=?)", topic)}
|
||||
alle_marker: list[int] = []
|
||||
for b in bausteine:
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", b["id"])
|
||||
if sec is None or not sec["text"]:
|
||||
befunde.append({"art": "section_fehlt", "item": f"baustein:{b['id']}",
|
||||
"detail": ""})
|
||||
continue
|
||||
alle_marker += [int(m) for m in _MARKER_RE.findall(sec["text"])]
|
||||
for f in _det_befunde(topic, b["id"], sec["text"]):
|
||||
if f["art"] == "laenge": # Länge blockiert nicht hart (nur Befund)
|
||||
continue
|
||||
if (f["art"], f["item"], f["detail"][:400]) in final:
|
||||
continue
|
||||
befunde.append(f)
|
||||
for m in {m for m in alle_marker if alle_marker.count(m) > 1}:
|
||||
befunde.append({"art": "marker_global_doppelt", "item": f"atom:{m}",
|
||||
"detail": "Atom in mehreren Sections gelehrt"})
|
||||
# Kaputte Marker mit erschöpftem Fix → Section NEU schreiben (max 1×!
|
||||
# sonst resettet jedes Neuschreiben den Fix-Cap → Endlos-Karussell)
|
||||
for f in [f for f in befunde if f["art"].startswith("marker")]:
|
||||
bid = f["item"].split(":")[-1]
|
||||
sec = db.one("SELECT fix_versuche FROM sections WHERE baustein_id=?", bid)
|
||||
if sec and (sec["fix_versuche"] or 0) >= config.FIX_MAX_VERSUCHE \
|
||||
and _rewrites(topic, bid) < 1:
|
||||
neue.append({"knoten": "writer", "item": f"r{runde + 1}:baustein:{bid}",
|
||||
"art": "reparatur", "runde": runde + 1,
|
||||
"payload": {"baustein_id": int(bid), "neu": True}})
|
||||
# Inhaltliche Befunde (LLM/Redundanz): Fix, danach 3er-Klärung
|
||||
offene = db.query(
|
||||
"SELECT * FROM befunde b WHERE b.run_id IN (SELECT id FROM runs WHERE "
|
||||
"topic=?) AND b.stufe='guide' AND b.status='offen' AND b.art IN "
|
||||
"('falsch','luecke','redundanz','vorwaerts','format_leak')", topic)
|
||||
for b in offene:
|
||||
befunde.append({"art": b["art"], "item": b["item"],
|
||||
"detail": b["detail"][:120]})
|
||||
bid = b["item"].split(":")[-1]
|
||||
if not bid.isdigit():
|
||||
continue
|
||||
sec = db.one("SELECT fix_versuche FROM sections WHERE baustein_id=?", bid)
|
||||
versuche = (sec or {}).get("fix_versuche") or 0
|
||||
if versuche < config.FIX_MAX_VERSUCHE:
|
||||
neue.append({"knoten": "fix", "item": f"r{runde + 1}:fix:{bid}",
|
||||
"art": "reparatur", "runde": runde + 1,
|
||||
"payload": {"baustein_id": int(bid)}})
|
||||
elif not b["geklaert"]:
|
||||
neue.append({"knoten": "fix", "item": f"r{runde + 1}:klaerung:{bid}",
|
||||
"art": "klaerung", "runde": runde + 1,
|
||||
"payload": {"baustein_id": int(bid)}})
|
||||
elif _rewrites(topic, bid) < 1:
|
||||
neue.append({"knoten": "writer", "item": f"r{runde + 1}:baustein:{bid}",
|
||||
"art": "reparatur", "runde": runde + 1,
|
||||
"payload": {"baustein_id": int(bid), "neu": True}})
|
||||
else:
|
||||
# Exit nach Fix-Cap + 3er-Klärung + 1 Neuschreiben: eskalieren
|
||||
# statt ewig blocken — 3 Instanzen haben geprüft, Rest ist
|
||||
# dokumentierte Grenze (sichtbar in den Kennzahlen)
|
||||
db.update("befunde", "id=?", (b["id"],), status="eskaliert")
|
||||
befunde.pop() # zählt nicht mehr als gate-blockierend
|
||||
if befunde:
|
||||
# Re-Prüfung der gefixten Sections in der nächsten Runde
|
||||
for bid in {b["item"].split(":")[-1] for b in befunde
|
||||
if b["item"].startswith("baustein:")}:
|
||||
if bid.isdigit():
|
||||
neue.append({"knoten": "det_pruefung",
|
||||
"item": f"r{runde + 1}:det:{bid}", "art": "reparatur",
|
||||
"runde": runde + 1, "payload": {"baustein_id": int(bid)}})
|
||||
neue.append({"knoten": "redundanz", "item": f"r{runde + 1}:redundanz",
|
||||
"art": "reparatur", "runde": runde + 1})
|
||||
return engine.Ergebnis(gate_status="rot", befunde=befunde, neue_tasks=neue)
|
||||
|
||||
db.update("topics", "name=?", (topic,), status="guide_fertig")
|
||||
db.execute("UPDATE sections SET stage='fertig' WHERE baustein_id IN "
|
||||
"(SELECT id FROM bausteine WHERE topic=?)", topic)
|
||||
return engine.Ergebnis(gate_status="gruen", neue_tasks=[
|
||||
{"knoten": "montage", "item": "montage"}])
|
||||
Reference in New Issue
Block a user