"""Beleg-Fenster um Anker (−300/+1200): Faktenbasis für Writer und Prüfer. Überlappende Fenster derselben Quelle werden gemerged (creator2-Muster). Außerdem: Facetten-Abdeckung — jede Soll-BELEG-Stelle braucht ein Atom in der Nähe (das feine 100-%-Maß, Gutachten-Lücke „Namensherkunft").""" from . import config, db, laden, textkit def beleg_stellen(topic: str) -> list[dict]: """Alle auffindbaren Beleg-Stellen bestätigter Soll-Punkte.""" stellen = [] for s in db.query("SELECT id, belege FROM soll WHERE topic=? AND " "status='bestaetigt' AND freispruch=''", topic): for beleg in db.uj(s["belege"], []): quelle = db.one("SELECT * FROM quellen WHERE id=? AND " "status='geladen'", beleg["quelle"]) if not quelle: continue try: text = laden.snapshot_lesen(quelle) except OSError: continue pos = textkit.finde_zitat(text, beleg["zitat"]) if pos: stellen.append({"soll_id": s["id"], "quelle_id": quelle["id"], "start": pos[0], "ende": pos[1], "zitat": beleg["zitat"][:120]}) return stellen def unbedeckte_belege(topic: str) -> list[dict]: """Beleg-Stellen OHNE aktives Atom im ±EXTRAKT_FENSTER derselben Quelle.""" anker = {} for a in db.query( "SELECT an.quelle_id q, an.start s FROM anker an JOIN atome a ON " "a.id=an.atom_id WHERE a.topic=? AND a.status='aktiv' AND " "an.start>=0", topic): anker.setdefault(a["q"], []).append(a["s"]) return [st for st in beleg_stellen(topic) if not any(abs(s - st["start"]) <= config.EXTRAKT_FENSTER for s in anker.get(st["quelle_id"], []))] def abdeckung_prozent(topic: str) -> float | None: stellen = beleg_stellen(topic) if not stellen: return None frei = len(unbedeckte_belege(topic)) return round((len(stellen) - frei) / len(stellen) * 100, 1) def fenster_fuer_atome(atom_ids: list[int]) -> str: spans: dict[int, list[list[int]]] = {} for aid in atom_ids: for a in db.query("SELECT * FROM anker WHERE atom_id=? AND start>=0", aid): von = max(0, a["start"] - config.BELEG_FENSTER_VOR) bis = a["ende"] + config.BELEG_FENSTER_NACH spans.setdefault(a["quelle_id"], []).append([von, bis]) teile = [] for qid, fenster in spans.items(): quelle = db.one("SELECT * FROM quellen WHERE id=?", qid) if not quelle or quelle["status"] != "geladen": continue text = laden.snapshot_lesen(quelle) fenster.sort() gemerged = [fenster[0]] for von, bis in fenster[1:]: if von <= gemerged[-1][1]: gemerged[-1][1] = max(gemerged[-1][1], bis) else: gemerged.append([von, bis]) for von, bis in gemerged: teile.append(f"[Quelle: {quelle['titel'][:60]}]\n{text[von:bis]}") return "\n\n---\n\n".join(teile)