This commit is contained in:
team3
2026-07-05 23:03:00 +02:00
parent ed5cf146c3
commit f4c5116abb
4 changed files with 116 additions and 3 deletions

98
AUFTRAG-inventar-fix.md Normal file
View File

@@ -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 **~6070** (davon ~4045 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; M1M3 = Ursachen, M4M5 = 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 ~6070 (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`.

View File

@@ -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:

View File

@@ -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))

View File

@@ -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: