This commit is contained in:
Team3
2026-07-05 00:38:03 +02:00
parent d25824229e
commit 219993ffd5
4 changed files with 115 additions and 23 deletions

View File

@@ -294,6 +294,24 @@ async def _llm_verdicts(template: str, topic: str, key: str, items: list[str]) -
# ── Report ──────────────────────────────────────────────────────────────────────────
def freispruch_pfad(topic: str) -> Path:
return QA_DIR / topic / "freispruch.json"
def _paar_key(a: str, b: str) -> str:
return "||".join(sorted((_norm_title(a), _norm_title(b))))
def lade_freispruch(topic: str) -> dict[str, list[str]]:
"""Persistierte 2:1-Freisprüche des Repair-Stichentscheids (repair._mit_stichentscheid):
mehrheitlich als „behalten" geurteilte Befunde zählen nicht mehr in die Note — sonst
pendelte sie dauerhaft unter 10 ohne Fix-Pfad (gemessen: kanban-smoke 9.4, aak 9.2).
Die Detektoren bleiben unverändert; ein Freispruch ist ein persistiertes Urteil,
kein Detektor-Tuning. Freigesprochene bleiben im Report sichtbar."""
d = _json_file(freispruch_pfad(topic))
return d if isinstance(d, dict) else {}
async def qa_report(topic: str, llm: bool = False) -> dict | None:
cards = await db.kanban_cards(topic, board="inventory", stage="done_block")
if not cards:
@@ -317,6 +335,11 @@ async def qa_report(topic: str, llm: bool = False) -> dict | None:
hy = hygiene(blocks)
n_sections = sum(len(_sections(t)) for t in corpus.values()) or 1
frei = lade_freispruch(topic)
frei_fremd = set(frei.get("fremd") or [])
fremd_frei = [t for t in fr if _norm_title(t) in frei_fremd]
fr = [t for t in fr if _norm_title(t) not in frei_fremd]
if llm and d:
v = await _llm_verdicts("QA-Dubletten", topic, "dubletten",
[f"A: {p['a']}\nB: {p['b']}" for p in d[:LLM_SAMPLE]])
@@ -334,6 +357,10 @@ async def qa_report(topic: str, llm: bool = False) -> dict | None:
[f"A: {p['a']}\nB: {p['b']}" for p in chunk])
for k, p in enumerate(chunk, 1):
p["llm"] = v.get(k, "?")
frei_sub = set(frei.get("sub_dubletten") or [])
for p in sd:
if p.get("llm") == "ja" and _paar_key(p["a"], p["b"]) in frei_sub:
p["freispruch"] = True # 2:1-Urteil „behalten" — sichtbar, aber notenfrei
unecht: list[str] | None = None
if llm and blocks:
verdacht = []
@@ -350,6 +377,8 @@ async def qa_report(topic: str, llm: bool = False) -> dict | None:
v2 = await _llm_verdicts("QA-Bausteine", topic, "bausteine-b2",
[f"{b['title']}{b['description'] or '(ohne Beschreibung)'}" for b in verdacht])
unecht = [b["title"] for k, b in enumerate(verdacht, 1) if v2.get(k) == "nein"]
frei_unecht = set(frei.get("unecht") or [])
unecht = [t for t in unecht if _norm_title(t) not in frei_unecht]
art_rows = [dict(r) for r in await db.get_sub_artefakte(topic)]
fragen = [dict(r) for r in await db.list_question_pattern(topic)]
@@ -362,8 +391,10 @@ async def qa_report(topic: str, llm: bool = False) -> dict | None:
n_cons = sum(1 for r in sub_rows if r["status"] == "consensus")
if n_cons:
quoten_art["sub_dubletten_verdacht"] = round(len(sd) / n_cons, 3)
if llm: # confirmed pairs only — the bare candidate list is suspicion, not damage
quoten_art["sub_dubletten"] = round(sum(1 for p in sd if p.get("llm") == "ja") / n_cons, 3)
if llm: # confirmed pairs only — the bare candidate list is suspicion, not damage;
# freigesprochene (2:1 „behalten") zählen nicht mehr
quoten_art["sub_dubletten"] = round(
sum(1 for p in sd if p.get("llm") == "ja" and not p.get("freispruch")) / n_cons, 3)
summary = _json_file(arbeit_dir(topic) / "lauf-summary.json") or {}
report = {
"topic": topic, "erstellt": datetime.now(timezone.utc).isoformat(),
@@ -377,6 +408,7 @@ async def qa_report(topic: str, llm: bool = False) -> dict | None:
},
"quoten_artefakte": quoten_art,
**({"unecht": unecht} if unecht is not None else {}),
**({"fremd_freigesprochen": fremd_frei} if fremd_frei else {}),
"dubletten": d, "sub_dubletten": sd, "luecken": lk, "fremd": fr, "beleg": bl, "hygiene": hy,
"artefakte": art,
"lauf": summary,