"""Anker-Repair: unverankerte Zitate (start=-1) werden erst deterministisch neu gematcht — ohne LLM-Call.""" import db import inventar import llm from conftest import run_anlegen, topic_anlegen def test_merge_kollision_und_kette(): topic = topic_anlegen("mergekante") a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="lang genug", status="neu", braucht=db.j([])) b = db.insert("atome", topic=topic, titel="B", typ="begriff", definition="kurz", status="neu", braucht=db.j([])) c = db.insert("atome", topic=topic, titel="C", typ="begriff", definition="x", status="neu", braucht=db.j([])) # beide haben dieselbe Kante zu C → blindes Umhängen würde UNIQUE verletzen db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art) VALUES(?,?,?,'braucht')", (topic, a, c)) db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art) VALUES(?,?,?,'braucht')", (topic, b, c)) # und eine Kante zwischen den Merge-Partnern → würde Selbstkante db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art) VALUES(?,?,?,'braucht')", (topic, b, a)) inventar._merge(topic, a, b) kanten = db.query("SELECT * FROM kanten WHERE topic=?", (topic,)) assert len(kanten) == 1 and kanten[0]["von_atom"] == a and kanten[0]["zu_atom"] == c # Merge-Kette: (B, C) — B ist schon gemerged, Wurzel A übernimmt inventar._merge(topic, b, c) assert db.one("SELECT merged_into FROM atome WHERE id=?", (c,))["merged_into"] == a async def test_anker_rematch_ohne_llm(tmp_path): topic = topic_anlegen("rematch") run = run_anlegen(topic) snap = tmp_path / "q.md" snap.write_text("Vorher. Der Satz steht hier drin. Nachher.", encoding="utf-8") q = db.insert("quellen", topic=topic, art="datei", titel="q", snapshot=str(snap), hash="h", status="atome") a = db.insert("atome", topic=topic, titel="T", typ="begriff", definition="d", status="ohne_anker", braucht=db.j([])) # Extraktion fand das Zitat nicht (Whitespace-Differenz), hat es aber gespeichert db.insert("anker", atom_id=a, quelle_id=q, start=-1, ende=-1, zitat="Der Satz steht hier drin.") ctx = llm.Kontext(run, topic, "minimax") ctx.ebene = "inventar" assert await inventar._anker_fixen_batch(ctx, [a]) is True row = db.one("SELECT * FROM anker WHERE atom_id=? AND start>=0", (a,)) assert row is not None assert db.one("SELECT status FROM atome WHERE id=?", (a,))["status"] == "neu" assert db.query("SELECT id FROM events WHERE run_id=?", (run,)) == [] # kein LLM-Call assert inventar.messen(ctx) == [] or all( b["art"] != "atom_ohne_anker" for b in inventar.messen(ctx)) def test_titel_kern_faltet_schreibvarianten(): import textkit assert textkit.titel_kern("ΔTSP2 Tour‑Länge") == textkit.titel_kern("ΔTSP2 Tour-Länge") assert textkit.titel_kern("FeedbackVertexSet ∈ NP") == textkit.titel_kern("Feedback Vertex Set ∈ NP") assert textkit.titel_kern("!!!") == "" def test_fallback_nutzt_jaccard_schwelle(monkeypatch): import embedding a = "Polynomialzeitreduktionen sind transitiv wenn L1 auf L2 und L2 auf L3 reduzierbar sind" b = ("Polynomialzeitreduktionen sind transitiv wenn eine Sprache L1 auf L2" " und L2 auf L3 in Polynomialzeit reduzierbar ist") # Kosinus-Schwelle allein: Paraphrase rutscht durch (aak Lauf 8) assert embedding.kandidaten_paare([a, b], 0.75) == [] paare = embedding.kandidaten_paare([a, b], 0.75, 0.3) assert [(i, j) for i, j, _ in paare] == [(0, 1)] async def test_titel_dublette_wird_gemerged_trotz_ferner_definition(): topic = topic_anlegen("titeldup") run = run_anlegen(topic) # identischer Titel, Definitionen lexikalisch fern (Jaccard < 0.3): # nur der Titel-Kern-Generator bringt das Paar ans Panel a = db.insert("atome", topic=topic, titel="VERTEX COVER Problem", typ="begriff", definition="Entscheidungsproblem, ob ein Graph ein Vertex Cover" " der Größe höchstens k enthält.", status="neu", braucht=db.j([])) b = db.insert("atome", topic=topic, titel="VERTEX COVER Problem", typ="begriff", definition="Gefragt wird nach einer Knotenmenge, die jede Kante" " abdeckt und maximal k Elemente hat.", status="neu", braucht=db.j([])) ctx = llm.Kontext(run, topic, "minimax") ctx.ebene = "inventar" await inventar._judge_dedup(ctx) stati = {r["id"]: r["status"] for r in db.query( "SELECT id, status FROM atome WHERE topic=?", (topic,))} assert sorted(stati.values()) == ["gemerged", "neu"]