97 lines
4.2 KiB
Python
97 lines
4.2 KiB
Python
"""Invarianten nach einem (Fake-)E2E-Lauf: was IMMER gelten muss, egal welches Szenario.
|
|
|
|
Nutzt bewusst eigene, schlichte Prüfungen statt Pipeline-Heuristiken (Muster qa.py) —
|
|
geteilte blinde Flecken machen den Check wertlos. Rückgabe: Liste von Verstößen,
|
|
leer = alles konsistent.
|
|
"""
|
|
|
|
import json
|
|
|
|
import database as db
|
|
from textkit import _norm_title
|
|
|
|
_LEVELS_OK = {"beginner", "advanced", "expert"}
|
|
_RELEVANZ_OK = {"relevant", "peripheral"}
|
|
|
|
|
|
def _json(path):
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
|
|
async def pruefe_invarianten(topic: str, files: dict | None = None,
|
|
mit_artefakten: bool = True) -> list[str]:
|
|
fehler: list[str] = []
|
|
subs = [dict(r) for r in await db.list_subblocks(topic)]
|
|
cons = [r for r in subs if r["status"] == "consensus"]
|
|
|
|
for r in cons:
|
|
wo = f"{r['block']}/{r['sub_title']}"
|
|
fk = None
|
|
try:
|
|
fk = json.loads(r["facts"]) if r["facts"] else None
|
|
except ValueError:
|
|
fehler.append(f"facts unparsebar: {wo}")
|
|
if not (isinstance(fk, dict) and (fk.get("key_points") or fk.get("cited_facts"))):
|
|
fehler.append(f"consensus-Sub ohne facts: {wo}")
|
|
if r["level"] not in _LEVELS_OK:
|
|
fehler.append(f"consensus-Sub ohne gültiges level: {wo}")
|
|
if r["relevance"] not in _RELEVANZ_OK:
|
|
fehler.append(f"consensus-Sub ohne relevance: {wo}")
|
|
|
|
if mit_artefakten:
|
|
art = [dict(r) for r in await db.get_sub_artefakte(topic)]
|
|
fragen = [dict(r) for r in await db.list_question_pattern(topic)]
|
|
versorgt = {(r["block_norm"], r["sub_norm"]) for r in art}
|
|
versorgt |= {(r["block_norm"], r["sub_norm"]) for r in fragen}
|
|
lebend = {(r["block_norm"], r["sub_norm"]) for r in subs if r["status"] in ("consensus", "variant")}
|
|
for r in cons:
|
|
if r["relevance"] == "relevant" and (r["block_norm"], r["sub_norm"]) not in versorgt:
|
|
fehler.append(f"relevanter Sub ohne Frage/Artefakt: {r['block']}/{r['sub_title']}")
|
|
for bn, sn in sorted({(r["block_norm"], r["sub_norm"]) for r in art} |
|
|
{(r["block_norm"], r["sub_norm"]) for r in fragen}):
|
|
if (bn, sn) not in lebend:
|
|
fehler.append(f"Waise (Ziel-Sub existiert nicht): {bn}/{sn}")
|
|
|
|
# keine hängengebliebenen Karten
|
|
for c in await db.kanban_cards(topic):
|
|
if c["stage"] == "dead":
|
|
fehler.append(f"dead-Karte: {c['board']}/{c['card_id']}")
|
|
|
|
if files is not None:
|
|
if not files["final"].exists():
|
|
fehler.append("blocks.md fehlt")
|
|
sc = _json(files["sidecar"])
|
|
if not isinstance(sc, dict):
|
|
fehler.append("sidecar-Datei fehlt/unparsebar")
|
|
else: # Sidecar und DB-consensus müssen dieselbe Sub-Menge tragen
|
|
db_menge = {(r["block_norm"], r["sub_norm"]) for r in cons}
|
|
sc_menge = {(_norm_title(bt), _norm_title(str(s.get("title", ""))))
|
|
for bt, ss in sc.items() for s in ss if isinstance(s, dict)}
|
|
for extra in sorted(sc_menge - db_menge):
|
|
fehler.append(f"Sidecar-Sub fehlt in DB: {extra}")
|
|
for extra in sorted(db_menge - sc_menge):
|
|
fehler.append(f"DB-consensus fehlt im Sidecar: {extra}")
|
|
return fehler
|
|
|
|
|
|
async def pruefe_guide_invarianten(topic: str, format_name: str = "Guide") -> list[str]:
|
|
"""Jeder relevante consensus-Sub trägt einen Sub-Marker im Guide (Muster
|
|
guide_qa.marker_fehlend, ohne LLM)."""
|
|
import guide_qa
|
|
fehler: list[str] = []
|
|
cards = [dict(r) for r in await db.list_guide_cards(topic, format_name)]
|
|
if not cards:
|
|
return ["keine Guide-Karten"]
|
|
for c in cards:
|
|
if c["status"] != "ok" or not (c.get("md") or "").strip():
|
|
fehler.append(f"Guide-Karte nicht ok: {c['block']} ({c['status']})")
|
|
subs_rel: dict[str, set] = {}
|
|
for r in await db.list_subblocks(topic):
|
|
if r["status"] == "consensus" and r["relevance"] != "peripheral":
|
|
subs_rel.setdefault(r["block_norm"], set()).add(r["sub_norm"])
|
|
fehler += [f"Sub-Marker fehlt: {m}" for m in guide_qa.marker_fehlend(cards, subs_rel)]
|
|
return fehler
|