"""Deterministische Bausteine: textkit, jsonx, auto_loop, QA-Note.""" import auto_loop import jsonx import qa import textkit def test_finde_zitat_whitespace_tolerant(): text = "Der Automat\nbesteht aus Zuständen." span = textkit.finde_zitat(text, "Der Automat besteht aus Zuständen.") assert span is not None start, ende = span assert text[start:ende].split() == ["Der", "Automat", "besteht", "aus", "Zuständen."] def test_finde_zitat_case_fallback_und_fehlschlag(): assert textkit.finde_zitat("DER AUTOMAT LÄUFT", "der Automat läuft") is not None assert textkit.finde_zitat("völlig anderer Text", "der Automat läuft") is None def test_finde_zitat_locker_interpunktion(): # PDF-Extrakte variieren Interpunktion/Dashes/Quotes — Wortlaut gleich → Treffer text = "Es gilt: „Ein Automat – mit Zuständen – akzeptiert reguläre Sprachen." zitat = 'Ein Automat, mit Zuständen, akzeptiert reguläre Sprachen' span = textkit.finde_zitat(text, zitat) assert span is not None assert "Automat" in text[span[0]:span[1]] # Paraphrase (andere Wörter) bleibt draußen assert textkit.finde_zitat(text, "Ein DFA mit Zuständen akzeptiert alle Sprachen") is None # zu kurze Zitate matchen locker nicht (Eindeutigkeit) assert textkit.finde_zitat("abc def", "ab, c") is None def test_finde_zitat_dekomponierte_umlaute(): """Lektion 82: pdftotext liefert dekomponierte Umlaute (a+U+0308), das LLM-Zitat präkomponierte (ä) — die Locker-Stufe faltet beide auf den Basisbuchstaben. Vorher scheiterte fast jedes deutsche Zitat (aak: 59/96 Atome ohne Anker).""" import unicodedata text = unicodedata.normalize("NFD", "Satz 6.25. 3-SAT ist NP-vollständig. Beweis folgt später.") span = textkit.finde_zitat(text, "Satz 6.25. 3-SAT ist NP-vollständig.") assert span is not None assert text[span[0]:span[1]].startswith("Satz 6.25") # umgekehrt: präkomponierter Text, dekomponiertes Zitat assert textkit.finde_zitat( "Die Erfüllbarkeit boolescher Ausdrücke ist zentral.", unicodedata.normalize("NFD", "Erfüllbarkeit boolescher Ausdrücke ist zentral")) is not None def test_snapshot_schreiben_normalisiert_nfc(): """Korpus-Snapshots werden am Import NFC-normalisiert — die exakte Zitat-Stufe darf nicht an unsichtbar anderen Bytes scheitern.""" import unicodedata import korpus roh = unicodedata.normalize("NFD", "NP-vollständig und erfüllbar") assert not unicodedata.is_normalized("NFC", roh) pfad, _h = korpus._snapshot_schreiben("nfc-test", roh) inhalt = open(pfad, encoding="utf-8").read() assert unicodedata.is_normalized("NFC", inhalt) assert inhalt == unicodedata.normalize("NFC", roh) def test_snapshot_schreiben_ersetzt_kontrollzeichen(): """PDF-Schriften mappen Sonderglyphen (ε) auf Steuerbytes — der Reader echot sie als Müll und das Zitat wird unmatchbar. Import ersetzt sie durch Leerzeichen; \\n und \\t bleiben (Layout).""" import korpus pfad, _h = korpus._snapshot_schreiben("ctrl-test", "Algorithmus (A\x0f )\nZeile\tzwei\x07!") inhalt = open(pfad, encoding="utf-8").read() assert inhalt == "Algorithmus (A )\nZeile\tzwei !" def test_finde_zitat_fuzzy_listing_zeilennummern(): """Stufe 4 (Lektion 83): pdftotext streut Listing-Zeilennummern in den Text, das Reader-Zitat lässt sie weg — fuzzy mit harter Distanzschranke matcht trotzdem.""" text = ("Algorithmus ListScheduling(L=(J1 , . . . , Jn ),m)\n 1 for i=1 to m do\n" " 2 Ei = 0; Bi = ∅;\n 3 od\n 4 for j=1 to n do\n" " 5 wähle i mit Ei minimal;\n 6 od\n") zitat = ("Algorithmus ListScheduling(L=(J1 , . . . , Jn ),m) for i=1 to m do Ei = 0; " "Bi = ∅; od for j=1 to n do wähle i mit Ei minimal; od") span = textkit.finde_zitat(text, zitat) assert span is not None assert text[span[0]:span[1]].startswith("Algorithmus ListScheduling") def test_finde_zitat_fuzzy_rekonstruierte_glyphe(): """PDF verlor die ε-Glyphe („(A )"), der Reader zitiert „(Aε)" — 1 Zeichen Differenz auf langem Zitat → Treffer.""" text = ("Man nennt eine solche Familie von Algorithmen (A ) ein vollständiges " "polynomielles Approximationsschema (FPTAS).") zitat = ("Man nennt eine solche Familie von Algorithmen (Aε) ein vollständiges " "polynomielles Approximationsschema (FPTAS).") assert textkit.finde_zitat(text, zitat) is not None def test_finde_zitat_fuzzy_mehrdeutig_bleibt_draussen(): """Eindeutigkeits-Guard: zwei fast identische Definitionen (SubSetSum/Partition- Muster) → kein Anker statt Falsch-Anker.""" a = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl K. Entscheide: Gibt es eine Teilmenge S?" b = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl X. Entscheide: Gibt es eine Teilmenge T?" zitat = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl Q. Entscheide: Gibt es eine Teilmenge U?" assert textkit.finde_zitat(a + "\n\nDazwischen steht anderer Text.\n\n" + b, zitat) is None def test_finde_zitat_fuzzy_grenzen(): """Paraphrase über der Distanzschranke bleibt draußen; Kurz-Zitate nie fuzzy.""" text = "Der List-Scheduling-Algorithmus verteilt Jobs der Reihe nach auf die am wenigsten belastete Maschine." assert textkit.finde_zitat(text, "List Scheduling weist jeden Job der aktuell " "günstigsten Maschine in Reihenfolge zu und stoppt") is None assert textkit.finde_zitat("kurzer Text über ähm Dinge", "kurzer Test über ähm Dinge") is None def test_snapshot_schreiben_ftfy_mojibake(): """ftfy am Import: Mojibake und Ligaturen werden repariert (Lektion 83).""" import importlib.util import pytest if importlib.util.find_spec("ftfy") is None: pytest.skip("ftfy nicht installiert — NFC-Fallback deckt test_snapshot_schreiben_normalisiert_nfc") import korpus pfad, _h = korpus._snapshot_schreiben("ftfy-test", "effiziente NP-vollständige Suffixe") inhalt = open(pfad, encoding="utf-8").read() assert "vollständige" in inhalt # Mojibake ä → ä assert "Suffixe" in inhalt # Ligatur ffi → ffi def test_abschnitte_und_ueberlappung(): text = ("Absatz eins.\n\n" * 50) + ("Absatz zwei.\n\n" * 50) teile = textkit.abschnitte(text, max_chars=300) assert all(len(t) <= 300 for _, t in teile) assert "".join(t for _, t in teile) == text assert textkit.ueberlappung((0, 10), (5, 15)) == 0.5 assert textkit.ueberlappung((0, 10), (20, 30)) == 0.0 def test_negations_guard(): assert textkit.negations_menge("L ist nicht regulär") != textkit.negations_menge("L ist regulär") def test_jsonx_fences_und_prosa(): assert jsonx.parse('```json\n{"a": 1}\n```') == {"a": 1} assert jsonx.parse('Hier das Ergebnis: [{"b": "x — y"}] Danke!') == [{"b": "x — y"}] assert jsonx.parse("kein json") is None assert jsonx.parse('{"s": "mit \\"quote\\" und }"}') == {"s": 'mit "quote" und }'} async def test_auto_loop_bedingungen(): note, grund = await auto_loop.auto_repair_loop("t", 10.0, None) assert grund == "fertig" async def stillstand(): return 8.0, False note, grund = await auto_loop.auto_repair_loop("t", 8.0, stillstand) assert grund == "stillstand" zaehler = {"n": 0} async def livelock(): zaehler["n"] += 1 return 8.0, True # bewegt, aber nie besser → hartes Limit greift note, grund = await auto_loop.auto_repair_loop("t", 7.0, livelock, max_iter=4) assert grund == "limit" and zaehler["n"] == 4 def test_qa_note_formel(): assert qa.note([], 10) == 10.0 n = qa.note([{"art": "marker_fehlend", "item": "1", "detail": ""}], 10) assert n < 10.0 viele = [{"art": "atom_ohne_anker", "item": str(i), "detail": ""} for i in range(50)] assert 0.0 <= qa.note(viele, 10) <= 9.9 def test_det_auftraege_blockquote_und_artefakt(): import db import guide from conftest import topic_anlegen topic = topic_anlegen("detcheck") ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv") b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu") a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d", status="neu", baustein_id=b_id, braucht=db.j([])) lang = (f"\n" + "Fließtext. " * 45 + "\n> wörtliches Rohzitat aus der Quelle\nEs gilt u 6= v." + "\n\nDas Blank-Symbol $[. ist speziell und liegt in $[ \\in \\Gamma$.") auftraege = guide._det_auftraege(topic, {"id": b_id, "ord": 0}, lang) text = " ".join(auftraege) assert "Blockquote" in text and "6=" in text assert "ungerade Anzahl $-Zeichen" in text