diff --git a/backend/bausteine.py b/backend/bausteine.py index 6732ad4..58ede56 100644 --- a/backend/bausteine.py +++ b/backend/bausteine.py @@ -56,8 +56,12 @@ SUBBAUSTEIN_KAPPE = 900 # Subbaustein-Finde-Loop je Chunk (15 min) KONSOLIDIERUNG_CHUNK = 600 # bis hierher EIN globaler Judge (dedupt alles); darüber chunked + Merge-Pass — nur Fallback-Pfad DEDUP_MAX_RUNDEN = 3 # finaler Dedup-Pass: max. Iterationen (die kleinere Liste blockt je Runde neu) DEDUP_MIN_DELTA = 3 # Abbruch, wenn eine Runde weniger als dieses % der Liste entfernt (konvergiert) +DEDUP_PAAR_FLOOR = 0.6 # Mindest-Cosine fürs Kandidaten-Paar (Complete-Link aggregiert → kein Chaining) +DEDUP_PAARE_CHUNK = 40 # Paare je Judge-Paket (Pairwise-Verifikation statt Block-Mischer) +FILTER_CHUNK = 35 # zu beurteilende Bausteine je Judge im Degradier-Pass (volle Liste als Kontext) # Frage-Muster-Chunks per LPT nach Sub-Last balancieren (Makespan), statt nach Baustein-Anzahl. FRAGE_CHUNK_SUBS = 50 # Ziel-Summe relevanter Subs je Chunk +FRAGE_MAX_RUNDEN = 3 # Nachhol-Runden für Subs ohne Muster (LLM lässt pro Chunk ~18 % aus) FAKTEN_CHUNK_SUBS = 25 # Fakten-Extraktion: kleinere Chunks (Fakten sind umfangreicher als Muster) FAKTEN_CHECK_PANEL = 3 # Judges je Chunk im Fakten-Check (Mehrheit beanstandet) KONSOLIDIERUNG_PANEL = 3 # Mapping-Judges je Chunk (Panel → Reconcile statt Einzel-Judge) @@ -71,7 +75,7 @@ _bausteine_cancelled: set[str] = set() _bausteine_step: dict[str, int] = {} BAUSTEINE_STEPS = ( - "Recherche", "Konsolidierung", "Klärung", + "Recherche", "Konsolidierung", "Klärung", "Dedup", "Bausteine-Filter", "Subbausteine finden", "Subbausteine wählen", "Subbausteine klären", "Fakten finden", "Fakten prüfen", "Fakten fix", "Stufen finden", "Stufen wählen", "Stufen klären", @@ -207,7 +211,7 @@ def _bausteine_steps(topic: str) -> tuple: laufen alle Pakete parallel; der Schritt bleibt, bis das letzte Paket fertig ist. """ q = lade_quelle(topic) - base = ("Recherche", "Konsolidierung", "Klärung", "Dedup") + base = ("Recherche", "Konsolidierung", "Klärung", "Dedup", "Bausteine-Filter") rest = ( "Subbausteine finden", "Subbausteine wählen", "Subbausteine klären", "Fakten finden", "Fakten prüfen", "Fakten fix", @@ -237,7 +241,7 @@ def _melde_p(set_p, topic: str, schritt: str): # Sonderschritte (Quelle laden, Ergänzung) gehören zur Phase „Inventar". PHASEN = ( ("Quelle", ("Quelle aufbereiten",)), - ("Inventar", ("Recherche", "Konsolidierung", "Klärung", "Dedup", "Ergänzung")), + ("Inventar", ("Recherche", "Konsolidierung", "Klärung", "Dedup", "Bausteine-Filter", "Ergänzung")), ("Subbausteine", ("Subbausteine finden", "Subbausteine wählen", "Subbausteine klären")), ("Fakten", ("Fakten finden", "Fakten prüfen", "Fakten fix")), ("Stufen", ("Stufen finden", "Stufen wählen", "Stufen klären")), @@ -298,7 +302,8 @@ def _alle_slot_dateien(files: dict) -> list[Path]: dyn = (list(arbeit.glob("subbaustein-*")) + list(arbeit.glob("fakten-*")) + list(arbeit.glob("stufe-*")) + list(arbeit.glob("relevanz-*")) + list(arbeit.glob("frage-muster-*")) + list(arbeit.glob("gliederung-*")) + list(arbeit.glob("artefakt-*")) + list(arbeit.glob("recherche-*")) + list(arbeit.glob("konsolidierung-*")) - + list(arbeit.glob("klaerung*")) + list(arbeit.glob("dedup-*"))) if arbeit.is_dir() else [] + + list(arbeit.glob("klaerung*")) + list(arbeit.glob("dedup-*")) + + list(arbeit.glob("inventar-filter*"))) if arbeit.is_dir() else [] return [ *files["recherche"], files["recherche_mapping"], *(p for slots in files["auswahl"].values() for p in slots), @@ -1577,31 +1582,79 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict final_by_titel = _final_by_titel(range(len(chunks))) ergebnis = {t: (final_by_titel.get(t) or roh_by_titel.get(t) or []) for t, _ in bausteine} - # Phase „Fragen prüfen": Chunks mit ≥1 leeren Baustein eine Runde nachholen. + # Phase „Fragen prüfen": pro-Sub-Vollständigkeit. Generatoren stürzen zufällig ab (~15 %), + # je Chunk 1 Agent ohne Retry → Subs (ganze Bausteine) fallen still durch. Darum mehrere + # Runden, die NUR die fehlenden Subs gezielt nachfordern (kurze Pakete, Frage-Muster-Recherche). set_p("Fragen prüfen…", step=_step_idx(topic, "Fragen prüfen")) - leer_titel = {t for t, _ in bausteine if not ergebnis.get(t)} - if leer_titel: - nach = [ci for ci, idxs in enumerate(chunks) if any(bausteine[i][0] in leer_titel for i in idxs)] - _log(topic, f"Frage-Muster: {len(leer_titel)} Baustein(e) ohne Muster — Nachrunde über {len(nach)} Chunk(s)") - for ci in nach: - roh_path(ci).unlink(missing_ok=True) - final_path(ci).unlink(missing_ok=True) - await finde_alle(nach) + + def _fehlende_subs() -> list[tuple[str, list[str]]]: + out = [] + for t, subs in bausteine: + hab = {_norm_titel(e["subbaustein"]) for e in ergebnis.get(t) or []} + miss = [s for s in subs if _norm_titel(s) not in hab] + if miss: + out.append((t, miss)) + return out + + def _nach_block(items): # items: [(baustein_titel, [fehlende sub_titel])] + bloecke = [] + for titel, subs in items: + zeilen = [] + for s in subs: + z = f"- {s}" + fk = fakten_by.get((titel, _norm_titel(s))) + if fk and (ft := _fakten_zeilen(fk)): + z += "\n" + "\n".join(" " + l for l in ft.split("\n")) + zeilen.append(z) + bloecke.append(f"BAUSTEIN: {titel}\nSUBBAUSTEINE:\n" + "\n".join(zeilen)) + return "\n\n".join(bloecke) + + async def _nachfordere(runde, pi, items): + fp = arbeit / f"frage-muster-nach{runde}-c{pi}.json" + if _frage_muster_chunk_schema(_json_datei(fp)): + return # Resume + subs_total = sum(len(s) for _, s in items) + await run_single_slot( + ctx, f"Frage-Muster Nachholung R{runde}/{pi}", + key=f"bausteine-{topic}-frage-muster-nach{runde}-c{pi}", + prompt=_prompt("Frage-Muster-Recherche", topic=topic, bausteine=_nach_block(items), + out_path=fp, extra=_extra(instructions)), + role="fast", capabilities="files", + payload=lambda result, p=fp: _frage_muster_chunk_schema(_json_datei(p)), + timeout=_timeout("frage_muster", subs_total), + ) + + for runde in range(1, FRAGE_MAX_RUNDEN + 1): + fehlend = _fehlende_subs() + if not fehlend: + break + n_subs = sum(len(s) for _, s in fehlend) + _log(topic, f"Frage-Muster Runde {runde}: {n_subs} Sub(s) in {len(fehlend)} Baustein(en) ohne Muster — Nachforderung") + pakete = _lpt_chunks([len(s) for _, s in fehlend], FRAGE_CHUNK_SUBS) + paket_items = [[fehlend[i] for i in idxs] for idxs in pakete] + await _gather_fortschritt( + [_nachfordere(runde, pi, items) for pi, items in enumerate(paket_items)], + len(paket_items), _melde_p(set_p, topic, "Fragen prüfen")) if is_cancelled(): return None - for titel, eintraege in _waehle_all(nach).items(): - roh_by_titel[titel] = eintraege - await klaere_alle(nach) - if is_cancelled(): - return None - for titel, eintraege in _final_by_titel(nach).items(): - final_by_titel[titel] = eintraege - for t, _ in bausteine: - if not ergebnis.get(t): - ergebnis[t] = final_by_titel.get(t) or roh_by_titel.get(t) or [] - rest = [t for t, _ in bausteine if not ergebnis.get(t)] - if rest: - _log(topic, f"Frage-Muster: {len(rest)} Baustein(e) bleiben leer (Fallback Live): {rest[:5]}") + # Output je Paket parsen + neu gewonnene Subs mergen (Vorhandene nicht überschreiben). + for pi, items in enumerate(paket_items): + titel_subs = {t: subs for t, subs in items} + ctitel = list(titel_subs.keys()) + for e in _frage_muster_chunk_schema(_json_datei(arbeit / f"frage-muster-nach{runde}-c{pi}.json")) or []: + titel = _match_sub(e["baustein"], ctitel) + if titel not in titel_subs: + continue + sub = _match_sub(e["subbaustein"], titel_subs[titel]) + hab = {_norm_titel(x["subbaustein"]) for x in ergebnis.get(titel) or []} + if _norm_titel(sub) in hab: + continue + ergebnis.setdefault(titel, []).append({"subbaustein": sub, "frage": e["frage"]}) + + rest = _fehlende_subs() + if rest: + n = sum(len(s) for _, s in rest) + _log(topic, f"Frage-Muster: {n} Sub(s) in {len(rest)} Baustein(en) bleiben nach {FRAGE_MAX_RUNDEN} Runden leer: {[t for t, _ in rest][:5]}") return ergebnis @@ -1972,6 +2025,21 @@ def _aspekt_marker(titel: str) -> int: return sum(1 for m in _ASPEKT_MARKER if m in t) +_VERWEIS_RE = re.compile(r'^(Satz|Lemma|Korollar|Bemerkung|Definition)\s*[\d.]+\s*(\([a-z]\)|[a-z])?\s*$', re.I) + + +def _ist_verweis(titel: str) -> bool: + """True für reine Verweis-/Platzhalter-Titel OHNE sprechenden Inhalt: „Satz 7.18", „Lemma 6.2", + „Korollar 6.18" (Nummer ohne Namen) sowie markierte Stellen „Bedingung (**)". NICHT „Satz 6.24: + Cook/Levin" (hat Namen) und NICHT kurze Fachsymbole wie „P⊆NP"/„Σ*" (echte Konzepte).""" + t = titel.strip() + if _VERWEIS_RE.match(t): + return True + if re.search(r'\(\*+\)', t): # markierte Stelle „(**)" / „(*)" + return True + return False + + def _canonical(kandidaten: list[dict], idxs: list[int], gesehen_norm: set[str]) -> dict: """Repräsentant eines Clusters = das Hauptkonzept (wenigste Eigenschafts-Marker — das Problem selbst, nicht „… ∈ NP"); Tie → häufigster norm-Titel → meiste Reader. Titel global eindeutig @@ -1983,7 +2051,9 @@ def _canonical(kandidaten: list[dict], idxs: list[int], gesehen_norm: set[str]) def gewicht(nb: str): ms = by_norm[nb] reader = set().union(*[set(kandidaten[m]["reader"]) for m in ms]) if ms else set() - return (-_aspekt_marker(nb), len(ms), len(reader)) # aspekt-arm zuerst = Hauptkonzept + # Verweis-/Platzhalter-Titel ("Satz 7.18") zuletzt — sprechendes Mitglied bevorzugen. + echt = not _ist_verweis(kandidaten[ms[0]]["titel"]) + return (echt, -_aspekt_marker(nb), len(ms), len(reader)) best = max(by_norm, key=gewicht) k = max(by_norm[best], key=lambda m: len(kandidaten[m]["beschreibung"])) @@ -2231,10 +2301,17 @@ async def _klaere_inventar(ctx: GenContext, set_p, files: dict) -> bool: return True set_p("Klärung läuft…", step=_step_idx(topic, "Klärung")) rest_rows = await db.list_bausteine(topic, status="rest") - if rest_rows: + # Durchgängiges Gate (EDC „Define"): auch Konsens-Bausteine mit Verweis-/Platzhalter-Titel + # prüfen („Satz 7.18", „Korollar 6.18", „Bedingung (**)") — sie umgehen sonst jede Prüfung. + verdaechtig = [b for b in await db.list_bausteine(topic, status="konsens") if _ist_verweis(b["titel"])] + pruef_rows = rest_rows + verdaechtig + if pruef_rows: arbeit = files["arbeit"] paths = [arbeit / f"klaerung-j{j}.json" for j in range(1, KONSOLIDIERUNG_PANEL + 1)] - offen = [(j, p) for j, p in enumerate(paths, 1) if _runde_schema(_json_datei(p), final=True) is None] + # final=False: ein Judge mit versehentlich nicht-leerem `rest` darf nicht komplett ausfallen + # (sonst kollabiert das Panel auf 1 Judge). Sein `aufnehmen` zählt; rest-Einträge gelten als + # nicht-aufgenommen. Die „rest leer"-Vorgabe steht weiter im Prompt. + offen = [(j, p) for j, p in enumerate(paths, 1) if _runde_schema(_json_datei(p)) is None] for _, p in offen: p.unlink(missing_ok=True) if offen: @@ -2242,19 +2319,20 @@ async def _klaere_inventar(ctx: GenContext, set_p, files: dict) -> bool: "key": f"bausteine-{topic}-klaerung-j{j}", "prompt": _prompt( "Bausteine-Klaerung", topic=topic, - rest="\n".join(f"- {b['titel']}" for b in rest_rows), + rest="\n".join(f"- {b['titel']} — {b['beschreibung']}" if b['beschreibung'] else f"- {b['titel']}" + for b in pruef_rows), final="\n- Entscheide JEDEN Eintrag. `rest` MUSS leer sein.", out_path=p, ), "role": "judge", "capabilities": "files", - "payload": (lambda result, p=p: _runde_schema(_json_datei(p), final=True)), + "payload": (lambda result, p=p: _runde_schema(_json_datei(p))), } for j, p in offen] vorhanden = KONSOLIDIERUNG_PANEL - len(offen) await _race(topic, "Klärung", slots, max(1, 2 - vorhanden), - _timeout("auswahl_mapping", len(rest_rows)), provider, cancelled=is_cancelled, grace=KONSENS_GRACE) + _timeout("auswahl_mapping", len(pruef_rows)), provider, cancelled=is_cancelled, grace=KONSENS_GRACE) if is_cancelled(): return False - outs = [r for p in paths if (r := _runde_schema(_json_datei(p), final=True))] + outs = [r for p in paths if (r := _runde_schema(_json_datei(p)))] if not outs: _bausteine_errors[topic] = "Klärung fehlgeschlagen" return False @@ -2263,18 +2341,85 @@ async def _klaere_inventar(ctx: GenContext, set_p, files: dict) -> bool: for aufnehmen, _ in outs: for nt in {_norm_titel(_titel(t)) for t in aufnehmen}: votes[nt] = votes.get(nt, 0) + 1 - for b in rest_rows: + # Umbenenn-Vorschläge (additiv aus dem Roh-JSON — _runde_schema kennt das Feld nicht): + # behaltene Verweis-/Platzhalter-Titel → sprechender Name aus dem Inhalt. Alt-Titel-Norm + # bleibt stabil (bricht den votes-Match nicht); je Alt-Titel der häufigste Vorschlag. + umbenenn: dict[str, dict[str, int]] = {} + for p in paths: + d = _json_datei(p) + umb = d.get("umbenennen") if isinstance(d, dict) else None + if isinstance(umb, dict): + for alt, neu in umb.items(): + neu = str(neu).strip() + if neu: + umbenenn.setdefault(_norm_titel(str(alt)), {}).setdefault(neu, 0) + umbenenn[_norm_titel(str(alt))][neu] += 1 + gesehen_norm = {b["titel_norm"] for b in await db.list_bausteine(topic, status="konsens")} + for b in pruef_rows: auf = votes.get(b["titel_norm"], 0) * 2 >= len(outs) - await db.set_baustein_status(topic, b["titel_norm"], "konsens" if auf else "verworfen") + if not auf: + await db.set_baustein_status(topic, b["titel_norm"], "verworfen") + continue + neu_titel = None + if _ist_verweis(b["titel"]) and (vors := umbenenn.get(b["titel_norm"])): + kand = max(vors, key=lambda k: (vors[k], len(k))) + if not _ist_verweis(kand): + neu_titel = kand + if neu_titel: + nn, t, n = _norm_titel(neu_titel), neu_titel, 2 + while nn in gesehen_norm: + t, nn, n = f"{neu_titel} ({n})", _norm_titel(f"{neu_titel} ({n})"), n + 1 + gesehen_norm.add(nn) + await db.set_baustein_status(topic, b["titel_norm"], "konsens", titel=t, neu_norm=nn) + else: + await db.set_baustein_status(topic, b["titel_norm"], "konsens") await db.set_step_status(topic, "Klärung", "fertig") return True +def _paare_schema(data) -> dict[int, bool] | None: + """{"paare": {"1": "ja", "2": "nein", …}} → {paar_nr: True/False} · sonst None.""" + if not isinstance(data, dict) or not isinstance(data.get("paare"), dict): + return None + out: dict[int, bool] = {} + for k, v in data["paare"].items(): + try: + nr = int(k) + except (ValueError, TypeError): + continue + out[nr] = str(v).strip().casefold() in ("ja", "yes", "true", "1") + return out or None + + +def _cliquen(n: int, kanten: list[tuple[int, int]]) -> list[list[int]]: + """Complete-Link: greedy maximale Cliquen über die bestätigten Dubletten-Kanten. Eine Gruppe + entsteht nur, wenn ALLE ihre Knoten paarweise verbunden sind → kein Chaining (A=B + B=C bildet + KEINE Gruppe {A,B,C}, solange A=C fehlt). Nur Cliquen ≥2 werden zurückgegeben.""" + adj: dict[int, set[int]] = {i: set() for i in range(n)} + for a, b in kanten: + adj[a].add(b) + adj[b].add(a) + benutzt: set[int] = set() + gruppen: list[list[int]] = [] + for v in sorted(range(n), key=lambda x: -len(adj[x])): + if v in benutzt or not adj[v]: + continue + clique = {v} + for u in sorted(adj[v], key=lambda x: -len(adj[x])): + if u not in benutzt and clique <= adj[u] | {u}: # u mit ALLEN bisherigen verbunden + clique.add(u) + if len(clique) >= 2: + gruppen.append(sorted(clique)) + benutzt |= clique + return gruppen + + async def _dedup_inventar(ctx: GenContext, set_p, files: dict) -> bool: - """Finaler iterativer Dedup-Pass über die fertige Konsens-Liste: Embedding-Block + LLM- - Gruppierung. Fängt Dubletten, die Konsolidierung (Block-Grenzen, Cap) und Klärung (lange - Liste, Lost-in-the-Middle) übersehen. Pro Gruppe bleibt EIN Repräsentant, der Rest wird - verworfen. Iteriert, weil die kleinere Liste je Runde neu blockt (Cross-Block-Reste).""" + """Finaler Dedup-Pass über die fertige Konsens-Liste: Pairwise-Verifikation (Entity + Resolution). Embedding liefert Kandidaten-PAARE (Cosine ≥ DEDUP_PAAR_FLOOR), ein Judge + bestätigt JEDES Paar einzeln (ja = dieselbe Dublette). NUR bestätigte Paare werden zu + Merge-Kanten (Union-Find) — kein Chaining, kein Aspekt-Über-Mergen wie beim Block-Mischer. + Pro Gruppe bleibt EIN Repräsentant (Hauptkonzept), der Rest wird verworfen.""" topic, is_cancelled = ctx.topic, ctx.is_cancelled if await db.get_step_status(topic, "Dedup") == "fertig": return True @@ -2283,39 +2428,178 @@ async def _dedup_inventar(ctx: GenContext, set_p, files: dict) -> bool: return True set_p("Dedup…", step=_step_idx(topic, "Dedup")) arbeit = files["arbeit"] - for runde in range(1, DEDUP_MAX_RUNDEN + 1): - konsens = await db.list_bausteine(topic, status="konsens") - if len(konsens) < 2: - break + konsens = await db.list_bausteine(topic, status="konsens") + if len(konsens) >= 2: + import numpy as np texts = [f"{b['titel']} — {b['beschreibung']}" if b["beschreibung"] else b["titel"] for b in konsens] sims = await asyncio.to_thread(embedding.embed_sims, texts) - if sims is None: - break - blocks = await asyncio.to_thread(embedding.capped_blocks, sims, None, None) - gruppen = await _block_gruppieren(ctx, set_p, arbeit, konsens, blocks, - praefix=f"dedup-r{runde}", schritt="Dedup") - if is_cancelled(): - return False - weg = 0 - for idxs in gruppen: - if len(idxs) < 2: - continue - # Repräsentant = Hauptkonzept (wenigste Eigenschafts-Marker), dann kürzester Titel; Rest verwerfen. - rep = min(idxs, key=lambda k: (_aspekt_marker(konsens[k]["titel"]), len(konsens[k]["titel"]), k)) - for k in idxs: - if k != rep: - await db.set_baustein_status(topic, konsens[k]["titel_norm"], "verworfen") - weg += 1 - atomic_write_json(arbeit / f"dedup-runde-{runde}.json", - {"vorher": len(konsens), "entfernt": weg, - "gruppen": [[konsens[k]["titel"] for k in g] for g in gruppen if len(g) > 1]}, indent=1) - _log(topic, f"Dedup Runde {runde}: {len(konsens)} → {len(konsens) - weg} (−{weg})") - if weg <= max(1, len(konsens) * DEDUP_MIN_DELTA // 100): # konvergiert → Schluss - break + if sims is not None: + n = len(konsens) + iu = np.triu_indices(n, k=1) + kand = [(int(iu[0][m]), int(iu[1][m])) for m in np.where(sims[iu] >= DEDUP_PAAR_FLOOR)[0]] + _log(topic, f"Dedup: {len(kand)} Kandidaten-Paare (Cosine ≥ {DEDUP_PAAR_FLOOR}) → Pairwise-Filter") + pakete = [kand[i:i + DEDUP_PAARE_CHUNK] for i in range(0, len(kand), DEDUP_PAARE_CHUNK)] + + def paar_path(pi): return arbeit / f"dedup-paar-c{pi}.json" + + async def _filt(pi, paare): + fp = paar_path(pi) + if _paare_schema(_json_datei(fp)): + return # Resume + zeilen = "\n\n".join( + f"{j + 1}.\nA: {konsens[a]['titel']} — {konsens[a]['beschreibung']}" + f"\nB: {konsens[b]['titel']} — {konsens[b]['beschreibung']}" + for j, (a, b) in enumerate(paare)) + await run_single_slot( + ctx, f"Dedup-Paare {pi}", + key=f"bausteine-{topic}-dedup-paar-c{pi}", + prompt=_prompt("Bausteine-Paar-Filter", topic=topic, paare=zeilen, out_path=fp), + role="judge", capabilities="files", + payload=lambda result, p=fp: _paare_schema(_json_datei(p)), + timeout=_timeout("auswahl_mapping", len(paare)), + ) + + await _gather_fortschritt([_filt(pi, p) for pi, p in enumerate(pakete)], + len(pakete), _melde_p(set_p, topic, "Dedup")) + if is_cancelled(): + return False + # Bestätigte "ja"-Kanten sammeln, dann COMPLETE-LINK (greedy Cliquen) statt Single-Link + # Union-Find — verhindert Chaining (A=B + B=C mergt NICHT A,C ohne direktes A=C). + kanten, ja = [], 0 + for pi, paare in enumerate(pakete): + urteil = _paare_schema(_json_datei(paar_path(pi))) or {} + for j, (a, b) in enumerate(paare): + if urteil.get(j + 1): + kanten.append((a, b)) + ja += 1 + gruppen = _cliquen(n, kanten) + weg = 0 + for idxs in gruppen: + # Repräsentant = Hauptkonzept (wenigste Eigenschafts-Marker), dann kürzester Titel. + rep = min(idxs, key=lambda k: (_aspekt_marker(konsens[k]["titel"]), len(konsens[k]["titel"]), k)) + for k in idxs: + if k != rep: + await db.set_baustein_status(topic, konsens[k]["titel_norm"], "verworfen") + weg += 1 + from collections import Counter + atomic_write_json(arbeit / "dedup-runde-1.json", + {"vorher": n, "entfernt": weg, "paare_geprueft": len(kand), "paare_ja": ja, + "clique_groessen": dict(sorted(Counter(len(g) for g in gruppen).items())), + "gruppen": [[konsens[k]["titel"] for k in g] for g in gruppen]}, indent=1) + _log(topic, f"Dedup (pairwise): {n} → {n - weg} (−{weg}); {ja}/{len(kand)} Paare bestätigt") await db.set_step_status(topic, "Dedup", "fertig") return True +def _filter_schema(data) -> dict[int, int] | None: + """{"fragmente": {"3": 7, "12": 8}} → {baustein_nr: eltern_nr} · None bei ungültiger Struktur. + Leeres dict = gültig (nichts zu degradieren). Eltern ≠ sich selbst.""" + if not isinstance(data, dict) or not isinstance(data.get("fragmente"), dict): + return None + out: dict[int, int] = {} + for k, v in data["fragmente"].items(): + try: + nr, eltern = int(k), int(v) + except (ValueError, TypeError): + continue + if nr != eltern: + out[nr] = eltern + return out + + +# Reine Notation/Symbole ohne eigenständiges Konzept — eng gehalten (FP~0 an aak geprüft; +# „KNF"/„MST"/„NP" treffen NICHT). Diese werden autonom verworfen (brauchen keinen Eltern). +_FILTER_NOTATION = re.compile(r'^\s*\|.{1,6}\|\s*$|^Güte\s+\d+\s*$') +# Eigenschaft-/Laufzeit-Verdacht — markiert Zeilen fürs Judge-Urteil (KEIN Auto-Drop, FP zu hoch: +# „NP-Schwere", Reduktionen mit „∈NP" sind echte Bausteine). Ergänzt _aspekt_marker. +_FILTER_PRAEDIKAT = re.compile( + r'ist NP-(vollständig|schwer)|NP-(Vollständigkeit|Schwere) von|ETH (Konsequenz|Lower Bound)' + r'|Approximationsschema nach|Laufzeit O\(|∈ ?NP', re.I) + + +def _filter_verdacht(b: dict) -> bool: + """Heuristik-Flag: könnte eine Eigenschaft/ein Detail eines anderen Bausteins sein.""" + return _aspekt_marker(b["titel"]) > 0 or bool(_FILTER_PRAEDIKAT.search(f"{b['titel']} {b['beschreibung'] or ''}")) + + +async def _filter_inventar(ctx: GenContext, set_p, files: dict) -> bool: + """Degradier-Pass (Granularität): trennt echte Bausteine von Fragmenten (Eigenschaften, + Beweis-Gadgets, Notation, Laufzeit-Details). Jeder Judge sieht die VOLLE Baustein-Liste + (Self-Containment ist relational) und markiert Fragmente MIT Eltern-Baustein aus der Liste. + Fragment + Eltern-in-Liste → verworfen (Inhalt kommt als Subbaustein des Eltern zurück). + Ohne Eltern oder im Zweifel → behalten (kein Konzept-Verlust).""" + topic, is_cancelled = ctx.topic, ctx.is_cancelled + if await db.get_step_status(topic, "Bausteine-Filter") == "fertig": + return True + set_p("Bausteine-Filter…", step=_step_idx(topic, "Bausteine-Filter")) + arbeit = files["arbeit"] + konsens_all = await db.list_bausteine(topic, status="konsens") + # Sicherheitsnetz: reine Notation autonom verwerfen (FP~0, kein Eltern nötig). Der Judge + # übersieht solche Symbole zuverlässig (Recall-Problem), darum deterministisch vorab. + konsens, notation_weg = [], [] + for b in konsens_all: + if _FILTER_NOTATION.search(b["titel"]): + await db.set_baustein_status(topic, b["titel_norm"], "verworfen") + notation_weg.append(b["titel"]) + else: + konsens.append(b) + if notation_weg: + _log(topic, f"Bausteine-Filter: {len(notation_weg)} reine Notation verworfen: {notation_weg[:6]}") + if len(konsens) < 2: + await db.set_step_status(topic, "Bausteine-Filter", "fertig") + return True + n = len(konsens) + # ⚠ markiert Verdachts-Zeilen (Eigenschaft/Laufzeit) — der Judge MUSS sie pro Eintrag prüfen. + def _zeile(i, b): + mark = "⚠ " if _filter_verdacht(b) else "" + return f"{i}. {mark}{b['titel']} — {b['beschreibung']}" if b["beschreibung"] else f"{i}. {mark}{b['titel']}" + voll = "\n".join(_zeile(i, b) for i, b in enumerate(konsens, 1)) + chunks = [list(range(i, min(i + FILTER_CHUNK, n + 1))) for i in range(1, n + 1, FILTER_CHUNK)] + + def filt_path(ci): return arbeit / f"inventar-filter-c{ci}.json" + + async def _beurteile(ci, nummern): + fp = filt_path(ci) + if _filter_schema(_json_datei(fp)) is not None: + return # Resume + await run_single_slot( + ctx, f"Bausteine-Filter {ci}", + key=f"bausteine-{topic}-inventar-filter-c{ci}", + prompt=_prompt("Bausteine-Filter", topic=topic, liste=voll, + von=nummern[0], bis=nummern[-1], out_path=fp), + role="judge", capabilities="files", + payload=lambda result, p=fp: _filter_schema(_json_datei(p)), + timeout=_timeout("auswahl_mapping", len(nummern)), + ) + + await _gather_fortschritt([_beurteile(ci, nm) for ci, nm in enumerate(chunks)], + len(chunks), _melde_p(set_p, topic, "Bausteine-Filter")) + if is_cancelled(): + return False + frag: dict[int, int] = {} + for ci, nummern in enumerate(chunks): + urteil = _filter_schema(_json_datei(filt_path(ci))) or {} + nset = set(nummern) + for nr, eltern in urteil.items(): + if 1 <= eltern <= n and nr in nset: + frag[nr] = eltern + # Ketten-Schutz: ein Baustein, der selbst Eltern eines Fragments ist, bleibt (sein Kind braucht den Anker). + eltern_set = set(frag.values()) + weg, debug = 0, [] + for nr, eltern in frag.items(): + if nr in eltern_set: + continue + b = konsens[nr - 1] + await db.set_baustein_status(topic, b["titel_norm"], "verworfen") + weg += 1 + debug.append({"fragment": b["titel"], "eltern": konsens[eltern - 1]["titel"]}) + atomic_write_json(arbeit / "inventar-filter.json", + {"vorher": n, "degradiert": weg, "fragmente": debug}, indent=1) + _log(topic, f"Bausteine-Filter: {n} → {n - weg} (−{weg} Fragmente → Subbausteine)") + await db.set_step_status(topic, "Bausteine-Filter", "fertig") + return True + + # --- Gliederung (Bausteine-Artefakt: Kapitel-Struktur, vom Guide nur gelesen) --- def _gliederung_komplett(files: dict) -> bool: @@ -2709,7 +2993,7 @@ async def _reset_db_ab_phase(topic: str, label: str) -> None: await db.delete_subbausteine(topic) if idx <= 1: # Inventar: Inventar + Recherche-Schritte — Sichtung bleibt await db.delete_bausteine(topic) - await db.delete_pipeline_state(topic, ["Recherche", "Konsolidierung", "Klärung", "Dedup"]) + await db.delete_pipeline_state(topic, ["Recherche", "Konsolidierung", "Klärung", "Dedup", "Bausteine-Filter"]) if idx <= 0: # Quelle: Sichtung neu (Coverage/inhalt + Schritt) await db.delete_coverage(topic) await db.delete_pipeline_state(topic, ["Quelle aufbereiten"]) @@ -2788,6 +3072,10 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str = if is_cancelled(): abgebrochen() return + if not await _filter_inventar(ctx, set_p, files): + if is_cancelled(): + abgebrochen() + return konsens_rows = await db.list_bausteine(topic, status="konsens") entries = { i: (f"{b['titel']} — {b['beschreibung']}" if b["beschreibung"] else b["titel"]) diff --git a/backend/database.py b/backend/database.py index bf35482..1a180bc 100644 --- a/backend/database.py +++ b/backend/database.py @@ -764,14 +764,18 @@ async def list_bausteine(topic: str, status: str | None = None) -> list[dict]: return out -async def set_baustein_status(topic: str, titel_norm: str, status: str, titel: str | None = None, beschreibung: str | None = None) -> None: - """Status setzen; optional Titel/Beschreibung aktualisieren (z.B. nach semantischem Merge).""" +async def set_baustein_status(topic: str, titel_norm: str, status: str, titel: str | None = None, beschreibung: str | None = None, neu_norm: str | None = None) -> None: + """Status setzen; optional Titel/Beschreibung aktualisieren (z.B. nach semantischem Merge). + `neu_norm` benennt den Norm-Schlüssel um (Klärung: Verweis-Titel → sprechender Name). Nur + sicher, solange noch keine Subbausteine/Fakten am alten `titel_norm` hängen.""" db = await get_db() fields = {"status": status, "updated_at": _now()} if titel is not None: fields["titel"] = titel if beschreibung is not None: fields["beschreibung"] = beschreibung + if neu_norm is not None and neu_norm != titel_norm: + fields["titel_norm"] = neu_norm sets = ", ".join(f"{k} = :{k}" for k in fields) await db.execute( f"UPDATE bausteine SET {sets} WHERE topic = :topic AND titel_norm = :titel_norm", diff --git a/backend/fragen-vorrat-aak.md b/backend/fragen-vorrat-aak.md new file mode 100644 index 0000000..c765a18 --- /dev/null +++ b/backend/fragen-vorrat-aak.md @@ -0,0 +1,177 @@ +# Frage-Muster für Lern-Prüfung: aak + +--- + +## BAUSTEIN: CliqueAndIndependentSet-Problem + +### Subbaustein: Independent Set: Knotenmenge ohne Kanten zwischen je zwei Knoten +**Muster:** Was ist die formale Definition eines Independent Set in einem Graphen G = (V, E)? + +### Subbaustein: Komplementgraph (V, E) bildet IS und Clique aufeinander ab +**Muster:** Wie hängt die Existenz einer Clique in G mit der Existenz eines Independent Set in G' (dem Komplementgraphen) zusammen? + +### Subbaustein: Existenz von Independent Set der Größe k ist NP-vollständig +**Muster:** Durch welche Polynomialzeitreduktion lässt sich zeigen, dass Independent Set NP-schwer ist? + +### Subbaustein: Existenz von Clique der Größe k ist NP-vollständig +**Muster:** Wie wird in Satz 6.26 die NP-Schwere von k-Clique bewiesen? + +### Subbaustein: Reduktion k-CLIQUE ⪯ k-INDEPENDENT-SET mittels Komplementgraph +**Muster:** Wie transformiert man eine Instanz (G, k) von CLIQUE in eine Instanz von INDEPENDENT-SET? + +### Subbaustein: Clique: Knotenmenge, in der je zwei Knoten durch eine Kante verbunden sind +**Muster:** Wann ist eine Knotenmenge C ⊆ V eine Clique in einem Graphen G = (V, E)? + +### Subbaustein: Complementärgraph: Independent Set in G ist Clique in G̅ +**Muster:** Welche Beziehung besteht zwischen einem Independent Set in G und einer Clique in G̅? + +### Subbaustein: Beide Probleme sind NP-vollständig +**Muster:** Welche fundamentale Konsequenz ergibt sich aus Satz 6.16, wenn ein NP-vollständiges Problem in P liegt? + +### Subbaustein: Frage: Existiert Independent Set bzw. Clique der Größe k in G? +**Muster:** Was ist die Eingabe und was die Ausgabe bei den Entscheidungsproblemen k-Clique und k-Independent-Set? + +### Subbaustein: Komplementarität: Independent Set in G = Clique im Komplementgraphen +**Muster:** Warum sind Clique und Independent Set gegenseitig in Polynomialzeit aufeinander reduzierbar? + +### Subbaustein: CLIQUE: Knotenmenge in der je zwei Knoten durch Kante verbunden sind +**Muster:** Welche Bedingung müssen alle Knotenpaare einer Clique erfüllen? + +### Subbaustein: Komplementär: IS in G = CLIQUE in G̅ +**Muster:** Was bleibt bei der Bildung des Komplementgraphen G̅ gleich und was ändert sich? + +### Subbaustein: Existenz von IS bzw. CLIQUE der Größe k ist NP-vollständig +**Muster:** Welche untere Schranke für die Laufzeit von Algorithmen für Independent Set folgt aus der ETH? + +### Subbaustein: Formale Sprachen: CLIQUE = {(G,k) | G enthält Clique ≥ k} +**Muster:** Wie ist die formale Sprache CLIQUE über dem Alphabet Σ = {0, 1} kodiert? + +### Subbaustein: Formale Sprachen: INDEPENDENT-SET = {(G,k) | G enthält IS ≥ k} +**Muster:** Welche Struktur hat die formale Sprache INDEPENDENT-SET? + +### Subbaustein: Komplementgraph G̅: Kantenmenge E̅ = {(u,v) | u≠v und {u,v} ∉ E} +**Muster:** Wie unterscheiden sich die Kantenmengen von G und seinem Komplementgraphen G̅? + +### Subbaustein: K-Clique in G entspricht K-Unabhängige-Menge in G̅ +**Muster:** Bleibt die Größe k bei der Reduktion von k-Clique auf k-Independent-Set erhalten? + +### Subbaustein: Beide Probleme sind in NP (Verifizierer existiert) +**Muster:** Welche Eigenschaft müssen Zertifikat und Verifizierer für ein Problem in NP erfüllen? + +### Subbaustein: NP-vollständig via gegenseitige Reduktion über Komplementgraph +**Muster:** Wie folgt aus Korollar 6.18 die NP-Vollständigkeit von Independent Set? + +--- + +## BAUSTEIN: Reduktion CLIQUE → CLIQUE-NOMEMBER + +### Subbaustein: Füge isolierten Knoten v zu G hinzu: G' = G ∪ {v} +**Muster:** Wie wird bei der Reduktion von CLIQUE auf CLIQUE-NOMEMBER der neue Graph G' konstruiert? + +### Subbaustein: G hat k-Clique ⟺ G' hat (k+1)-Clique mit v +**Muster:** Warum kann der hinzugefügte Knoten v in keiner gültigen k-Clique von G' enthalten sein? + +### Subbaustein: Reduktion beweist CLIQUE-NOMEMBER ∈ NP-vollständig +**Muster:** Welche drei Bedingungen müssen erfüllt sein, damit CLIQUE-NOMEMBER als NP-vollständig gilt? + +### Subbaustein: CLIQUE: Eingabe Graph G, Frage: existiert K-clique? +**Muster:** Was ist die Eingabe und was ist die Frage beim Entscheidungsproblem CLIQUE? + +### Subbaustein: CLIQUE-NOMEMBER: existiert Knoten der nicht in jeder maximalen Clique liegt? +**Muster:** Wie ist das Problem CLIQUE-NOMEMBER gemäß Skript 6.50 formal definiert? + +### Subbaustein: Reduktion: isolierten Knoten v zu G hinzufügen → G' +**Muster:** Welche Elemente werden bei der Reduktion von CLIQUE auf CLIQUE-NOMEMBER gegenüber der ursprünglichen Instanz verändert? + +### Subbaustein: G' hat Clique der Größe k genau dann wenn G eine hat +**Muster:** Warum bleibt die Cliquengröße k bei der Reduktion unverändert? + +### Subbaustein: v ist in G' in keiner k-Clique (isoliert) +**Muster:** Welche Eigenschaft hat der Knoten v in der konstruierten Instanz (G', v, k)? + +### Subbaustein: G hat Clique der Größe k ⟺ G' hat Clique der Größe k ohne v +**Muster:** Wie hängt eine k-Clique in G mit einer k-Clique in G' zusammen? + +### Subbaustein: Polynomielle Transformation +**Muster:** Warum ist die beschriebene Reduktion in polynomieller Zeit berechenbar? + +--- + +## BAUSTEIN: Independent Set + +### Subbaustein: Für alle u, v ∈ S gilt: {u, v} ∉ E +**Muster:** Welche Bedingung muss für je zwei Knoten eines Independent Set gelten? + +### Subbaustein: Komplementär zur Clique +**Muster:** Wie hängt ein Independent Set in G mit einer Clique im Komplementgraphen G' zusammen? + +### Subbaustein: NP-vollständiges Problem +**Muster:** Welche Komplexitätsklasse enthält Independent Set und wie wurde dies bewiesen? + +### Subbaustein: Knotenmenge ohne Kanten zwischen je zwei Knoten der Menge +**Muster:** Was bedeutet es, dass die Knoten eines Independent Set paarweise nicht adjazent sind? + +### Subbaustein: Komplementär zum Clique-Problem +**Muster:** In welchem Graphen entspricht ein Independent Set einer Clique? + +### Subbaustein: Independent Set S⊆V: keine Kante zwischen je zwei Knoten in S +**Muster:** Wie unterscheidet sich ein Independent Set von einer beliebigen Teilmenge von V? + +### Subbaustein: INDEPENDENT-SET = {(G,k) | G enthält unabhängige Menge der Größe ≥k} +**Muster:** Welche Sprache formalisiert das Entscheidungsproblem Independent Set? + +--- + +## BAUSTEIN: Tiefensuche (DFS) für Zykluserkennung + +### Subbaustein: Weiß: unbesuchter Knoten, Grau: aktuell in Bearbeitung, Schwarz: fertig +**Muster:** Welche Farbe hat ein Knoten während er von der DFS bearbeitet wird und welche nach Abschluss? + +### Subbaustein: DFS-Zykluserkennung in O(V+E) bei adjacency List +**Muster:** Warum beträgt die Laufzeit der DFS-Zykluserkennung bei Adjazenzliste Θ(|V|+|E|)? + +### Subbaustein: Schwarz: vollständig bearbeiteter Knoten +**Muster:** Wann wird ein Knoten in der DFS schwarz gefärbt? + +### Subbaustein: Rückkante (grau → weiß): signalisiert Zyklus +**Muster:** Zu einem Knoten welcher Farbe muss eine Kante führen, um einen Zyklus anzuzeigen? + +### Subbaustein: Tree Edge (weiß): Kante zu unbesuchtem Knoten +**Muster:** Welche Kante wird als Tree Edge bezeichnet? + +### Subbaustein: Grau: aktuell in Bearbeitung (in DFS-Weite) +**Muster:** Was bedeutet es, wenn ein Knoten während der DFS grau gefärbt ist? + +--- + +## BAUSTEIN: Turingmaschine für 0^n (Zweierpotenz) + +### Subbaustein: Eingabe: n Nullen in unärer Codierung +**Muster:** In welcher Codierung wird die Eingabezahl n der TM für 0^n dargestellt? + +### Subbaustein: Akzeptiert nur wenn n = 2^k für ein k ≥ 0 +**Muster:** Nach welchem Kriterium entscheidet die TM, ob eine Eingabe akzeptiert wird? + +### Subbaustein: Phase 1: Markiere jede zweite 0 mit x (alternierend) +**Muster:** Wie markiert die TM die Nullen im ersten Schritt? + +--- + +## BAUSTEIN: 3-SAT zu 3-Färbung Reduktion + +### Subbaustein: Knotenzahl linear in Variablen und Klauseln +**Muster:** Aus welchen Komponenten setzt sich die Knotenmenge V der konstruierten Instanz zusammen? + +### Subbaustein: Dreieck erzwingt drei verschiedene Farben für die drei Knoten +**Muster:** Warum benötigen die drei Knoten xi, x̄i und vi eines jeden Dreiecks drei verschiedene Farben? + +--- + +## BAUSTEIN: MC-Knapsack + +### Subbaustein: Ziel: Maximierung des Gesamtwerts +**Muster:** Was ist die Zielfunktion beim Maximum-Cut Knapsack Problem? + +--- + +**Gesamt: 48 Frage-Muster** diff --git a/templates/Prompt/Bausteine-Filter.md b/templates/Prompt/Bausteine-Filter.md new file mode 100644 index 0000000..961d721 --- /dev/null +++ b/templates/Prompt/Bausteine-Filter.md @@ -0,0 +1,39 @@ +Unten ist die vollständige Baustein-Liste eines Lern-Guides zum Thema "{topic}", durchnummeriert. Deine Aufgabe: Trenne **echte Bausteine** von **Fragmenten**, die in Wahrheit zu einem anderen Baustein der Liste gehören. + +VOLLSTÄNDIGE LISTE (als Kontext — du brauchst sie, um Eltern zu finden): +{liste} + +BEURTEILE die Nummern **{von} bis {bis}** — gehe sie **EINZELN** durch, einen Eintrag nach dem anderen. Die übrigen Einträge sind nur Kontext (mögliche Eltern). + +## Vorgehen pro Eintrag (zwingend für JEDEN einzeln) +Für jeden Eintrag {von}–{bis}: +1. Was ist das **Subjekt**? (Worüber wird geredet?) +2. Ist dieses Subjekt selbst ein anderer Eintrag der Liste — und sagt der Eintrag darüber nur eine EIGENSCHAFT, einen BEWEIS-TEIL, eine NOTATION oder ein LAUFZEIT-DETAIL aus? + - **Ja → Fragment**, Eltern = die Nummer dieses Subjekts. + - Nein, es steht für sich → Baustein (behalten). +Mit **⚠** markierte Zeilen sind Verdachtsfälle (Eigenschaft/Laufzeit/Notation) — prüfe sie besonders sorgfältig. Entscheide am Inhalt, nicht an der Markierung. + +## Was ist ein BAUSTEIN (eigenständige Lerneinheit — behalten)? +Ein Baustein ist self-contained: Man kann ihn erklären, OHNE einen anderen Baustein als Subjekt vorauszusetzen. +- Ein **Problem**: „3-SAT", „Clique", „Knapsack", „Dominating Set". +- Ein **Verfahren/Algorithmus**: „LPT Scheduling", „Christofides", „FPTAS". +- Eine **Definition/ein Konzept**: „NP", „Reduktion", „Verifizierer", „KNF". +- Ein **benannter Satz MIT eigener Aussage**: „Cook-Levin: SAT ist NP-vollständig". + +## Was ist ein FRAGMENT (gehört zu einem anderen Baustein → degradieren)? +Self-Containment-Test: Setzt der Eintrag ein ANDERES Konzept der Liste als Subjekt voraus? Dann ist er dessen Eigenschaft/Teil, kein eigener Baustein. +- **Eigenschaft/Status** eines Problems X (das selbst in der Liste steht): „X ist NP-vollständig", „X ∈ NP", „NP-Schwere von X", „Approximationsgüte von X". → Eltern = X. +- **Beweis-/Reduktions-Gadget**: „αEnde", „A-Komponente", „Dummy Items", „Schedule D*", „Knoten z", Hilfsvariablen. → Eltern = der Satz/die Reduktion, in deren Beweis es vorkommt. +- **Reine Notation/Symbol**: „|x|", „Σ∗", „Güte 2". → Eltern = die definierende Definition. +- **Laufzeit-/Größen-Detail**: „O(|V|⁴) Verifizierer-Laufzeit", „|V'| = |V| bei Reduktion", „Reduktion in O(|E|)". → Eltern = der Algorithmus/die Reduktion. + +## Regeln +- Ein Fragment wird NUR degradiert, wenn sein **Eltern-Baustein in der Liste steht** (gib dessen Nummer an). Findest du keinen Eltern → behalte es (kein „Eltern"). +- Zweifel betrifft die EIGENSTÄNDIGKEIT: Ist unklar, ob ein Eintrag für sich steht → behalten. Aber eine klare Eigenschaft/Notation/ein Beweis-Teil MIT Eltern in der Liste IST ein Fragment — nicht aus Vorsicht behalten. +- Eine eigenständige **Reduktion zwischen zwei Problemen** ist ein Baustein, KEIN Fragment („3-SAT ≤ Clique"). +- Urteile am INHALT (nach dem „—"), nicht am Titel. + +Schreibe NUR die JSON-Datei nach: {out_path} + +Format (nur die Fragment-Nummern aus {von}–{bis}, je mit Eltern-Nummer; `fragmente` darf leer sein): +{{"fragmente": {{"12": 5, "13": 5, "27": 19}}}} diff --git a/templates/Prompt/Bausteine-Klaerung.md b/templates/Prompt/Bausteine-Klaerung.md index 4b4c8e5..a8a7b9a 100644 --- a/templates/Prompt/Bausteine-Klaerung.md +++ b/templates/Prompt/Bausteine-Klaerung.md @@ -1,20 +1,26 @@ -Zum Thema "{topic}" sind unten Bausteine, die in der Recherche nur EINMAL genannt wurden. Die Quelle wurde vollständig durchsucht. Entscheide für JEDEN Eintrag: ist er ein echter, eigenständiger Baustein → `aufnehmen`, oder nicht → verwerfen (in keine Liste). +Zum Thema "{topic}" stehen unten Baustein-Kandidaten, jeder als "Titel — Beschreibung". **Beurteile am INHALT (nach dem —), nicht am Titel.** Entscheide für JEDEN Eintrag: echter, eigenständiger Baustein → `aufnehmen`, sonst verwerfen (in keine Liste). -EINZELN GENANNTE EINTRÄGE (jeweils entscheiden): +KANDIDATEN (jeweils entscheiden): {rest} Was ein Baustein IST: eine eigenständige LERNEINHEIT — ein Konzept, ein Verfahren, ein Problem, eine Definition, ein benannter Satz MIT inhaltlicher Aussage. -Was KEIN Baustein ist (→ verwerfen): -- Bloße Verweise/Nummern ohne Inhalt: "Satz 7.18", "Bemerkung 7.22", "Lemma 6.2", "D*". +**Define-Test (entscheidend):** Beschreibt der INHALT ein eigenständiges Konzept, das man einem Lernenden erklären kann — OHNE Bezug auf einen konkreten Beweisschritt oder eine bestimmte Skript-Stelle? Ja → aufnehmen. Nein → verwerfen. + +Was KEIN Baustein ist (→ verwerfen, AUCH wenn eine Beschreibung dabei steht): +- Hilfskonstrukte, die nur INNERHALB eines Beweises/einer Reduktion existieren: "Knoten z", "Hilfsvariable", "Bedingung (**)" (ein Hilfsknoten / eine markierte Bedingung — kein eigenständiges Konzept). - Beweis-Fragmente/Zwischenschritte: "Beweis (b) Güte", "Beweis ⊂ Richtung", "Eigenschaft (a)". +- Generische Platzhalter ohne eigenes Konzept: "Optimale Lösung", "Lösung" (nur im Kontext eines konkreten Problems sinnvoll). +- Reine Verweise OHNE inhaltliche Beschreibung: "Satz 7.18" (nichts Sprechendes nach dem —). - Zu Vages oder erkennbar Erfundenes. +**Verweis-Titel MIT echtem Inhalt → aufnehmen UND umbenennen:** Hat ein Eintrag einen nichtssagenden Titel ("Satz 7.18", "Korollar 6.18", "Lemma 6.2"), beschreibt die Beschreibung aber ein echtes Konzept → nimm ihn auf und gib im Feld `umbenennen` einen sprechenden Namen aus dem Inhalt. Im `aufnehmen`-Eintrag bleibt der **Original-Titel** stehen (für die Zuordnung); der neue Name steht NUR in `umbenennen`. + Beispiele: - "Clique-Member — k-Clique, die einen festen Knoten v enthält" → **aufnehmen** (eigenständiges Problem). -- "Satz 7.18" → **verwerfen** (bloßer Verweis, keine Aussage). -- "Beweis (b) Güte" → **verwerfen** (Fragment). -- "Satz 7.8: ΔTSP1 hat Güte 2" → **aufnehmen** (benannter Satz MIT Aussage). +- "Satz 7.18 — MGA hat Worst-Case-Güte 2" → **aufnehmen** + `umbenennen`: "Satz 7.18" → "MGA-Algorithmus (Güte 2)". +- "Bedingung (**) — Ungleichungen pi ≤ pk im Scheduling-Beweis" → **verwerfen** (Hilfskonstrukt im Beweis). +- "Satz 7.18" (keine Beschreibung) → **verwerfen** (bloßer Verweis). Regeln: - Im Zweifel an der Eigenständigkeit → eher aufnehmen. Dubletten werden später separat entfernt; hier zählt nur: echter Baustein oder Müll. @@ -22,5 +28,5 @@ Regeln: Schreibe NUR die JSON-Datei nach: {out_path} -Format (kein weiterer Text in der Datei): -{{"aufnehmen": ["Titel — Kurzbeschreibung"], "rest": []}} +Format (kein weiterer Text in der Datei; `umbenennen` darf leer sein): +{{"aufnehmen": ["Titel — Kurzbeschreibung"], "umbenennen": {{"alter Titel": "sprechender Name"}}, "rest": []}} diff --git a/templates/Prompt/Bausteine-Recherche.md b/templates/Prompt/Bausteine-Recherche.md index fd076d6..c21fbd0 100644 --- a/templates/Prompt/Bausteine-Recherche.md +++ b/templates/Prompt/Bausteine-Recherche.md @@ -6,6 +6,11 @@ Regeln: - Ein Baustein löst GENAU EIN PROBLEM. Varianten derselben Lösung gehören in den einen Baustein, nicht als eigene Einträge (richtig: `
` ist ein Baustein, `` mit allen Typen ist ein Baustein, "Ich-Botschaften" ist ein Baustein, "Present Perfect" mit allen Signalwörtern ist ein Baustein; falsch: 21 Einträge für jeden input-Typ oder je ein Eintrag pro unregelmäßigem Verb, aber auch Sammeleinträge, die mehrere Probleme mischen). - Ein Baustein ist ATOMAR: genau eine Idee, vollständig in sich. Test: Man kann nichts entfernen, ohne ihn unvollständig zu machen — und es fehlt nichts, um ihn zu verstehen. - Granularität: ein Baustein ist eine LERNEINHEIT, kein Lexikon-Eintrag. Familien, die zusammen gelernt werden (z. B. die font-*-Eigenschaften, die Modalverben), sind EIN Baustein. +- Ebene (wichtig): ein Baustein ist self-contained — erklärbar, OHNE einen ANDEREN Baustein als Subjekt vorauszusetzen. Das sind Probleme, Verfahren/Algorithmen, Definitionen, benannte Sätze mit eigener Aussage. Ein Detail, das ein anderes Konzept voraussetzt, gehört IN dessen Baustein (als Teil), nicht als eigener Eintrag: + - Eine EIGENSCHAFT eines Konzepts gehört zum Konzept: „Clique" ist ein Baustein; „Clique ist NP-vollständig", „Clique ∈ NP", „Laufzeit von Clique" sind Teile davon — kein eigener Eintrag. + - Ein BEWEIS-/Reduktions-Bauteil gehört zum Satz/zur Reduktion: „A-Komponente", „Dummy-Item", „αEnde", „Hilfsvariable" — kein eigener Eintrag. + - Reine NOTATION/Symbole gehören zu ihrer Definition: „|x|", „Σ∗" — kein eigener Eintrag. + - Eine eigenständige REDUKTION zwischen zwei Problemen ist dagegen ein eigener Baustein („3-SAT ≤ Clique"). - KEINE Kategorien, KEINE Bewertung, KEINE Reihenfolge nach Wichtigkeit — nur eine flache, durchnummerierte Liste. - Es gibt KEINE Ziel-Anzahl. Höre erst auf, wenn die Recherche nichts Neues mehr hergibt. - Erfinde nichts: nimm nur Bausteine auf, die du in der Recherche belegt hast. Notiere pro Baustein die Quelle (URL bzw. Dateipfad). Gibt es keine Einzel-Quelle, reicht die Sammel-Quelle (Handbuch-Kapitel, Lehrbuch, Übersichtsseite, Verzeichnis).