diff --git a/AUFTRAG-inventar-fix.md b/AUFTRAG-inventar-fix.md new file mode 100644 index 0000000..cabbe9f --- /dev/null +++ b/AUFTRAG-inventar-fix.md @@ -0,0 +1,98 @@ +# Auftrag: Inventar-Pipeline reparieren — zu viele, fehlende und schlecht benannte Bausteine + +## Kontext + +Das Thema `aak` (Uni-Quelle `uni/aak`: Skript + Serien + Klausuren + Hausaufgaben) erzeugte **108 Bausteine**. Soll-Abgleich mit dem Skript ergibt: sinnvoll wären **~60–70** (davon ~40–45 Skript-Kern). Der letzte QA-Lauf (`storage/qa/aak/20260705-201016.json`) zeigt 36 % Dubletten-Verdacht bei Note 10.0 und Lücken-Quote 0.0 — obwohl Kernkonzepte fehlen. + +Zahlenfluss des Laufs (aus `storage/topics/aak/arbeit/inventar-*.json`): +~1150 Roh-Nennungen (24 Chunks × 2 Research-Pässe) → 203 Kandidaten → Filter degradiert 69, Recheck holt 17 zurück → 134 → Dedup prüft 183 Paare, merged nur 17 (9 %) → 117 → Gruppierung: 5 Umbrellas, 17 Umbrella-Merges per Type-Gate verworfen → Supplement +4 → **108**. + +Die Pipeline-Stages stehen in `inventory_stages` (`backend/board_inventory.py:1614`). Alle Zeilennummern unten beziehen sich auf den Branch `refactor` inkl. der offenen (uncommitteten) Diffs — siehe „Leitplanken". + +## Befund: sechs Root Causes + +### R1 — Extraktion erlaubt Beweisschritt-Granularität (Hauptursache „zu viele") +- `templates/Prompt/Blocks-Research.md` erklärt „named theorems with their own statement" und jede „standalone REDUCTION" zum eigenen Baustein. `templates/Prompt/Blocks-Source-Uni.md` nennt „formulas and theorems" als Zielkategorie, ohne Granularitätsgrenze. +- Folge im aak-Lauf: 14 Beweisschritt-/Beweis-Objekt-Bausteine (z. B. `Type-I`, `Schedule D*`, `Gewichtsfunktion d2`, `Zick-Zack-Tour`, `Optimaler Rundweg OPT((Kn,d2)) = n`, `3-SAT ≤ K-COLOR: G=(V,E) Konstruktion`) und 8 Bausteine für EIN Konzept („untere Schranken via ETH": `Lower Bound via k/n/|E|/|T|` + 4 weitere). +- Verstärker: `_is_named_statement` (`backend/blocks.py:1355`) schützt alles mit Reduktions-Syntax vor Demotion — auch Konstruktionsdetails wie `…: G=(V,E) Konstruktion`. + +### R2 — Quellen-Nummern als Titel, Reparaturstufe greift nicht (Hauptursache „Benennung") +- Research-Pässe übernehmen Skript-Nummerierungen als Titel („Satz 7.13", „Bemerkung 7.22", „Definition 6.19"). 6 finale Bausteine tragen reine Satz-/Bemerkungs-Nummern. +- `_REFERENCE_RE` (`backend/blocks.py:1022`) matcht nur nackte Referenzen („Bemerkung 7.22"), NICHT „Satz 7.13 (Christofides)" oder „Satz 7.6: Kriterium für Eulerschen Kreis" — die laufen als „gute" Titel durch. +- Rename ist Best-Effort: In `clarify` wird nur umbenannt, wenn ein Judge freiwillig `rename` liefert (`backend/board_inventory.py:663`). Beispiel Cluster `08c52a84`: Judge j1 schlug für „Bemerkung 7.22" den korrekten Konzeptnamen vor („Untere Schranke für FPTAS …"), die Aggregation verwarf ihn. +- Singletons überspringen Naming komplett: `_choose_title` läuft nur bei `len(rows) > 1` (`backend/board_inventory.py:725` und `:802`). + +### R3 — Titel-Split zerschneidet Namen (abgeschnittene Titel) +- `_title = entry.split(" — ")[0]` (`backend/textkit.py:35`) + aggressives `_norm_dash` (`backend/textkit.py:77`): ein en-Dash MIT einseitigem Space **innerhalb** eines Titels wird zum Separator → Split mitten im Titel. +- Es gibt keine Klammer-Balance-Prüfung und keinen Repair. Ergebnis: `Aε-Algorithmus (Güte 1+ε, Laufzeit O(n3` und `N P via nicht-deterministische Turingmaschine (Definition 6.` liefen wortwörtlich bis `done`. +- (Die 60-Zeichen-Kürzung in `backend/board_artefacts.py:81` betrifft nur Ordnernamen, nicht Titel — kein Fix nötig.) + +### R4 — Dedup-Dämme brechen (36 % Verdacht überleben) +- Merge braucht Einstimmigkeit zweier Judges (`if ja1 and ja2`, `backend/board_inventory.py:1200`); „uneinig" → kein Merge, kein Tiebreak. +- Parameter-Familien werden als Paare je einzeln mit „nein" beschieden (`Lower Bound via k`↔`n`↔`|E|`↔`|T|`, alle QA-Jaccard 1.0). +- Kandidaten-Generierung verpasst Komposita: `_canonical_key` (`backend/blocks.py:1118`) trennt „Cliquenproblem" nicht in „Clique"+Stopwort → „Clique" vs. „Cliquenproblem (k-Clique)" wird ggf. nie Judge-Kandidat. +- Type-Gate der Gruppierung verwarf 17 Umbrella-Merges (`grund: "type-gate"` in `inventar-gruppierung.json`) — exakt die späteren QA-Dubletten (Approximations-Grundbegriffe, Type-I/II+Schedule-Objekte, NP-Definitionen). +- Filter-Recheck holte 17 korrekte Degradierungen zurück (`ueberstimmt`/`floor_veto` in `inventar-filter-535eb84c.json`) — darunter die komplette Lower-Bound-Familie. + +### R5 — Echte Lücken trotz Quote 0.0 (Ursache „fehlende") +- Fehlend im aak-Inventar: **SAT/Satz von Cook-Levin** (zentralster Satz), **MGA (Güte 2)**, **Sahni-PTAS Ak**, **Satz von Lawler**, **Satz 6.16 (P=NP-Kriterium)**, Lastfunktion (Def 7.26, per Supplement teilweise nachgeholt). +- QA-Lücken sind token-basiert (`backend/qa.py:163`): 108 übergranulare Bausteine decken jede Sektion irgendwie ab → Konzeptlücken unsichtbar, Supplement (`_proc_gap_check`) ergänzt zu wenig. +- Verlust-Vektor upstream: `clarify` verlangt für Einzel-Reader-Funde Einstimmigkeit aller Judges (`accept = v >= len(outs)`, `backend/board_inventory.py:657`) — ein einziges „nein" verwirft einen real existierenden Fund als `failed-quorum`. + +### R6 — QA-Note ist blind für das Problem +- `NOTE_GEWICHTE` (`backend/qa.py:40`) enthält keine Dubletten. 36 % Verdacht + Verschlechterung zum Vorlauf (+0.06) ergeben trotzdem Note 10.0. + +## Maßnahmen (priorisiert; M1–M3 = Ursachen, M4–M5 = Dämme, M6 = Messung) + +### M1 — Titel-Hygiene, deterministisch +1. `_norm_dash`/Titel-Split klammer-bewusst machen: Dash innerhalb offener Klammer ist KEIN Separator. Nach dem Split: unbalancierte Klammern → Titel reparieren (bevorzugt: Split rückgängig / nächsten Separator nehmen). +2. `_REFERENCE_RE` erweitern: auch `Satz 7.13 (Christofides)`, `Satz 7.6: Kriterium …`, `… (Definition 6.19)` erkennen. Deterministischer Strip: Referenz-Präfix/Suffix entfernen, Konzeptrest behalten („Satz 7.6: Kriterium für Eulerschen Kreis" → „Kriterium für Eulerschen Kreis"). Reine Nummern ohne Konzeptrest („Bemerkung 7.22") → Rename-PFLICHT (Judge muss Konzeptnamen liefern, sonst bleibt die Karte vor `done` hängen bzw. wird saniert), statt Best-Effort. +3. Naming/Sanierung auch für Singletons erzwingen, wenn Titel `_is_reference`, klammer-unbalanciert oder >80 Zeichen ist (`board_inventory.py:725`, `:802`, `_sanierung_noetig` `:492`). + +### M2 — Granularitätsgrenze in den Prompts +1. `Blocks-Research.md` + `Blocks-Source-Uni.md` (und Geschwister `Blocks-Source-*.md`): Negativ-Liste ergänzen — KEINE eigenen Bausteine sind: einzelne Beweisschritte, Hilfskonstruktionen/Beweis-Objekte (Hilfs-Schedules, Gewichtsfunktionen, konstruierte Touren), berechnete Einzel-Instanzen, Parameter-Instanzen desselben Arguments (dieselbe Technik „via k / via n / via |E|" = EIN Baustein). Solche Inhalte gehören als Aspekt in den Eltern-Baustein. +2. Titel-Regel in beide Naming-Prompts (`Blocks-Naming.md`, `Blocks-Klaerung.md`, `Blocks-Sanierung.md`): Titel ist der KONZEPTNAME; Quellen-Nummerierungen („Satz 7.13") sind als Titel verboten, dürfen aber in der Beschreibung stehen. +3. Sätze bleiben erlaubt als Baustein, wenn sie ein eigenständig prüfbares Ergebnis sind (Cook-Levin, Christofides) — benannt nach dem Ergebnis, nicht der Nummer. + +### M3 — Fragment-Filter nachschärfen +1. `_is_named_statement` (`blocks.py:1355`) verengen: Reduktions-Syntax mit Konstruktions-Suffix (Doppelpunkt/Zusatz NACH der Relation, z. B. `…≤ K-COLOR: G=(V,E) Konstruktion`) ist NICHT geschützt, sondern fragment-verdächtig. +2. Recheck-Overturn erschweren: `ueberstimmt` nur bei einstimmigem Recheck-Panel statt Mehrheit (`board_inventory.py:1031`). Beleg: 15 von 17 zurückgeholten Karten waren spätere QA-Dubletten. +3. Parameter-Varianten als Verdacht markieren: Titel-Paare, die sich nur durch ein Parameter-Token unterscheiden (`via k`/`via n`/`via |E|`), in `_filter_suspect` bzw. als eigene Familien-Heuristik aufnehmen → Filter-/Dedup-Kandidat. + +### M4 — Dedup-Recall erhöhen (ohne Fehlmerge-Risiko aufzugeben) +1. Die drei offenen Diffs (Richtungs-Guard `_direction_conflict`, Judge-Kanal-Guard, Dedup-Prompt-Zeile) BEHALTEN — sie sind Teil der Lösung. +2. Tiebreak statt Veto: bei 1-1-Judge-Split (`uneinig`, `board_inventory.py:1200`) einen dritten Judge entscheiden lassen. +3. Familien-Kollaps: Cluster von ≥3 Titeln, die sich nur im Parameter-Token unterscheiden, als EIN Merge-Fall (ein Judge-Call für die Familie) statt n² Einzelpaare. +4. `_canonical_key` (`blocks.py:1118`): Kompositum-Suffix „…problem" auch verklebt abtrennen („Cliquenproblem" → Stamm + Stopwort), damit solche Paare Kandidaten werden. Generisch halten (Suffix-Regel, keine Wortliste pro Domäne). +5. Type-Gate der Gruppierung (`_GROUP_STANDALONE`-Pfad) lockern: Umbrella-Merge nicht allein wegen gemischter Content-Typen (Definition+Satz+Algorithmus) verwerfen, wenn das Judge-Panel die Gruppe bestätigt hat. Die 17 verworfenen Fälle aus `inventar-gruppierung.json` sind der Prüfmaßstab. +6. NICHT anfassen: die Varianten-Regel „k-X ≠ X / Max-X ≠ X" im Dedup-Prompt und das 2-Judge-Panel für klare Fälle. Bisher 0 Fehlmerges — das muss so bleiben. + +### M5 — Lücken schließen +1. `clarify`-Quorum für Einzel-Reader-Funde von Einstimmigkeit auf Mehrheit senken (`board_inventory.py:657`) ODER abgelehnte Funde als Fragment aufbewahren statt `rejected`, damit `gap_check`/Supplement sie wiederfinden kann. +2. Gap-Check konzeptbasiert ergänzen: benannte Ergebnisse des Korpus (Sätze/Definitionen mit eigener Aussage, Abschnittsüberschriften) gegen das Inventar prüfen, nicht nur Token-Abdeckung. Ein benanntes Kernresultat ohne zugeordneten Baustein = Lücke → Supplement-Kandidat. +3. Prüffälle für aak (müssen nach einem Neulauf existieren): SAT/Cook-Levin, MGA, Sahni-PTAS Ak, Satz von Lawler, P=NP-Kriterium (Satz 6.16). + +### M6 — QA ehrlich machen +1. `dubletten_verdacht` in `NOTE_GEWICHTE` aufnehmen (`qa.py:40`), damit die Note das Nutzerproblem abbildet. +2. Optional: konzeptbasierte Lücken (aus M5.2) als eigene QA-Quote ausweisen. + +## Leitplanken + +- **Generisch bleiben**: keine aak-/Komplexitätstheorie-Sonderregeln in Pipeline oder Prompts. aak ist nur der Prüffall. Alle Heuristiken müssen für beliebige Themen tragen. +- **YAGNI**: kleine, gezielte Diffs; bestehende Muster (Judge-Panels, Schema-Parser, Kanban-Stages) wiederverwenden; nichts Neues bauen, was eine Prompt-Zeile löst. +- **Offene Diffs behalten**: `backend/blocks.py`, `backend/board_inventory.py`, `templates/Prompt/Blocks-Dedup.md` enthalten uncommittete Änderungen (Richtungs-Guard). Nicht verwerfen, darauf aufbauen. +- **Niemals `git commit` oder `git push`** — der Nutzer committet selbst. +- **Fehlmerge-Bilanz**: Recall-Erhöhungen (M4) dürfen keine falschen Merges einführen. Jede Lockerung braucht einen Judge-Beleg-Pfad; deterministische Auto-Merges bleiben streng. +- **Keine Echtläufe ohne Freigabe**: `make qa TOPIC=aak` mit LLM-Judges und komplette Neuläufe kosten Tokens — nur nach Rückfrage beim Nutzer starten. + +## Validierung (in dieser Reihenfolge) + +1. `pytest` in `backend/` — bestehende Tests dürfen nicht brechen; für M1 (Titel-Split, Referenz-Regex) und M4.4 (`_canonical_key`) gezielte Unit-Tests ergänzen (Fixtures: die realen Schadensfälle aus diesem Auftrag). +2. `make test-e2e` — Fake-E2E (`backend/fake_agents.py`) muss grün bleiben; Störfall-Matrix ggf. um Referenz-Titel und unbalancierte Klammern erweitern. +3. QA-Fehler-Injektion (Muster siehe QA-Modul-Tests): injizierte Satz-Nummern-Titel, Parameter-Familien und Konzeptlücken müssen von den geänderten Checks erkannt werden. +4. Erst nach Nutzer-Freigabe: Neulauf aak + `make qa TOPIC=aak`. Erfolgskriterien: + - Baustein-Anzahl ~60–70 (statt 108), + - `dubletten_verdacht` < 0.10 (statt 0.361), + - die 5 Prüffall-Konzepte aus M5.3 existieren, + - kein Titel matcht die erweiterte Referenz-Erkennung, keine unbalancierten Klammern, + - Lücken-/Fremd-Quote nicht schlechter als der Referenzlauf `20260705-201016.json`. diff --git a/backend/blocks.py b/backend/blocks.py index 7343d73..0205536 100644 --- a/backend/blocks.py +++ b/backend/blocks.py @@ -1192,6 +1192,15 @@ def _relation_conflict(title_a: str, title_b: str) -> bool: return a is not None and b is not None and a != b +def _direction_conflict(title_a: str, title_b: str) -> bool: + """Nur der DETERMINISTISCH sichere Teil des Relation-Guards: dieselben Operanden in + GETAUSCHTER Richtung („A ≤ B" vs „B ≤ A") — immer eine andere Reduktion. Ein Operand-Unterschied + (z. B. „CLIQUE" vs „k-CLIQUE" — Synonym? oder echt verschieden?) ist eine Bedeutungsfrage und + bleibt dem (belegten) Judge überlassen, statt per String-Vergleich blind blockiert zu werden.""" + a, b = _relation_operands(title_a), _relation_operands(title_b) + return a is not None and b is not None and a != b and set(a) == set(b) + + def _filter_schema(data) -> dict[int, int] | None: diff --git a/backend/board_inventory.py b/backend/board_inventory.py index f8902e5..8e375f9 100644 --- a/backend/board_inventory.py +++ b/backend/board_inventory.py @@ -44,7 +44,7 @@ from blocks import ( _completion_schema, _containment_parent, _crawl_index, _file_payload, _filter_schema, _filter_suspect, _is_artifact, _is_named_statement, _is_parentless_noise, _is_reference, _pairs_schema, _read, - _relation_conflict, _root, _supplement_schema, _text_sections, _umbrella_schema, + _direction_conflict, _relation_conflict, _root, _supplement_schema, _text_sections, _umbrella_schema, _aspect_marker, _title_variants, _corpus_files, _evidence_pack, _sink_json, source_folder, ) from config import (QA_GATE_NOTE, QA_GATE_LLM, @@ -423,7 +423,8 @@ async def _pair_one(ctx: GenContext, flow: Flow, c): for ci, chunk in enumerate(chunks): verdict = _pairs_schema(_json_file(flow.work_dir / f"pair-{cid}-{h}-c{ci}.json")) or {} for j, (a, b) in enumerate(chunk): - if verdict.get(j + 1) and not _relation_conflict(rows[a]["title"], rows[b]["title"]): + # Judge entscheidet Operanden-Identität selbst; nur getauschte Richtung bleibt geblockt + if verdict.get(j + 1) and not _direction_conflict(rows[a]["title"], rows[b]["title"]): edges.append((a, b)) if sims_title is not None: # auto recall net: near-identical titles merge without the judge for a, b in ordered: @@ -1182,7 +1183,10 @@ async def _proc_dedup(ctx: GenContext, flow: Flow, cards): detail: dict[tuple[int, int], str] = {} def _edge(a, b, kanal): - if _relation_conflict(allrows[a]["title"], allrows[b]["title"]): + # Auto-Merges (kein Judge): voller String-Guard. Judge-Kanal ("ja"): nur der deterministische + # Richtungs-Guard — Operanden-Identität (Synonym vs. echt verschieden) hat der Judge entschieden. + guard = _direction_conflict if kanal == "ja" else _relation_conflict + if guard(allrows[a]["title"], allrows[b]["title"]): detail[(a, b)] = "guard_veto" else: edges.append((a, b)) diff --git a/templates/Prompt/Blocks-Dedup.md b/templates/Prompt/Blocks-Dedup.md index 0928288..49def5f 100644 --- a/templates/Prompt/Blocks-Dedup.md +++ b/templates/Prompt/Blocks-Dedup.md @@ -1,5 +1,7 @@ The FINAL block inventory for the topic "{topic}" was assembled from several sources. Despite earlier filtering it can still carry duplicates: the same concept listed under two names. For EACH pair below, decide: do A and B denote the SAME block → **ja**, or TWO DIFFERENT blocks → **nein**? +Decide on the underlying CONCEPT — from title and description — not on wording alone. Merge only when A and B are EQUIVALENT (the same statement, mutually interchangeable); a special case, a restriction, or one direction of a relation is NOT equivalent → **nein**. + THE MOST COMMON ERROR is merging a named VARIANT with its base entity. A variant is NEVER its base: "3-X" ≠ "X", "Max-X" ≠ "Max-3-X", "Modified X" ≠ "X", "k-X" ≠ "X". A digit or qualifier prefix that restricts the entity makes it a DIFFERENT entity → **nein**. PAIRS: