Skip to content

fix(chunker): Outline-first Kapitel-Split (#345)#348

Merged
TillQuandel merged 1 commit into
masterfrom
fix/345-outline-chapter-split
Jul 18, 2026
Merged

fix(chunker): Outline-first Kapitel-Split (#345)#348
TillQuandel merged 1 commit into
masterfrom
fix/345-outline-chapter-split

Conversation

@TillQuandel

Copy link
Copy Markdown
Owner

Closes #345 (PR 1 des Implementierungsplans #345/#346, Revision 4).

Architektur-Kurzfassung

split_by_chapters(text, pdf_path) bekommt einen Outline-first-Pfad: Kapitelgrenze = fitz-aufgelöste PDF-Lesezeichen-Zielseite statt fragilem Titel-Matching im Volltext.

  1. Outline lesen (get_toc(simple=False), Zielseite aus dem fitz-aufgelösten Tupel-Element; NUL-Padding-Bereinigung).
  2. Hauptebene wählen — Front-/Backmatter-Filter (_FRONTMATTER_RE als SSoT + verzeichnis-Substring + Plan-Termliste), Root-Descend (genau 1 Wurzel → eine Ebene tiefer, Moser-Muster), Duplikat-Merge (Seitenabstand ≤1, längerer Titel gewinnt), Sanity ≥2/≤60.
  3. Physisch→Marker-Seiten-Map, zwei Zweige: PageLabels nutzbar → Label je physischer Seite (Issue-Zitier-Seite bei PDFs ohne /PageLabels = physischer Index statt gedruckter Seite (Offset-Sonderdrucke) #95-Klasse); sonst Zählung nicht-leerer Seiten (Drift real 3–39 Seiten). physical_pages_by_anchor bleibt VERBOTEN ([MITTEL] Seitenanker systematisch um ~4-6 PDF-Seiten verschoben — auch bei perfekten Eval-Werten #342-Erbe).
  4. Validierung statt Matching — Titel-Wort-Overlap ≥50 % im ~400-Zeichen-Fenster ab Marker; Fehlschlag → HiPS-Kaskade (Substring → Fuzzy 80 %); bleibt es dabei → Grenze verwerfen + ins Vorsegment mergen; >50 % Fehlschläge → Outline verwerfen.
  5. Degenerations-Guards (Median-Segment <400 Wörter, Riesensegment >70 %) → Normalpfad. Offset-Voting-Kreuzcheck (Plateau-Tie-Break Richtung Map) als Confidence-Flag. Diagnose-Zeile N Kapitel erkannt (Quelle: outline, Validierung M/N, K Grenzen verworfen).
  6. Ohne nutzbare Outline transparenter Fallback auf exakt das bisherige Verhalten. Ohne pdf_path (Alt-Aufrufer/eval) unverändert.

Dazu: _TOC_TRAIL_RE nur erweitert (gespacte Dot-Leader, römische Seitenzahlen — _CHAPTER_RE/_is_real_chapter_match unangetastet); Chunk.source; extract_overview(text, chapters=chunks)-Injektion (ein Split pro Lauf); orchestrator-Aufrufstellen + harter by-chapter-Abbruch bei degeneriertem Split (der 15,5-h-/32-Mio-Token-/0-Notes-Lauf) statt Warnung.

Geänderte Dateien

  • generative/pipeline/pdf_chunker.py — Outline-Logik, _TOC_TRAIL_RE, Chunk.source, extract_overview(chapters=).
  • generative/config.pyMAX_SANE_OUTLINE_CHAPTERS, MIN_CHAPTER_SEGMENT_WORDS.
  • generative/orchestrator.pysplit_by_chapters(pdf_path=…), extract_overview(chapters=…), by-chapter-Abbruch-Guard.
  • generative/tests/test_outline_chapter_split.py, test_by_chapter_degeneration_abort.py (neu).

RED-Nachweis (vor dem Fix)

Akzeptanzkriterien v4 PR 1

  • (a) 5-Bücher-Fixturetest_real_book_outline_split (Klingenberg/DAMA/Kuhlen/Gantert/Hobohm): Quelle outline, Kapitelzahl im Band, 100 % der Grenzen passieren das Validierungs-Gate. Kuhlen (39-Seiten-Drift, kein PageLabels) = eigener Test, 6 Teile A–F. Echte PDFs sind Tills Privatbestand → pytest.skip ohne ATOMIC_AGENT_TEST_BOOK_DIR (in CI übersprungen); lokal 8/8 grün verifiziert.
  • (b) Klingenberg 8–10 Kapitel (real 9), Quelle outline.
  • (c) Fallbacktest_no_outline_falls_back_to_normal_path (synthetisch, läuft überall) + test_no_pdf_path_is_backward_compatible.
  • (d) TOC-Trail-Negativtests aus echten Klingenberg-Zeilen (5 parametrisiert + römisch + Nicht-Über-Filterung).
  • (e) Overview-Budgettest_overview_budget_from_chapters ≤1700 Wörter (real Klingenberg 1554 vs. 88.725 Volltext; N2-Baseline war ~21k).
  • (f) Bestehende Suite grün (inkl. unverändertes test_chapter_regex.py).
  • (g) synthetisches PageLabels-Fixturetest_labels_branch_maps_to_printed_label (Druckseite = phys+100 → Grenze mappt auf [S. 103], nicht den phys. Index).
  • (h) Moser-Single-Root-Fixturetest_root_descend_single_root (1 Wurzel, 3 L2-Kinder).

Suite-Zahlen

uv run pytest generative lib/decision_engine/tests shared/tests -q6268 passed, 11 skipped (davon 8 echte Buch-Tests ohne gesetztes ATOMIC_AGENT_TEST_BOOK_DIR), 9 deselected, 0 failed (418 s). Mit gesetztem Buch-Verzeichnis zusätzlich 8/8 echte Buch-Tests grün (197 s). ruff check + ruff format --check sauber.

Abweichungen vom Plan (mit Begründung)

  • Skip-Termliste exakt an Plan §1 ausgerichtet (nicht an die breitere Probe-Regex): bare literatur/abbildung als Substring droppten fälschlich Ganterts echtes Kapitel „Zweiter Teil. … Literatur, Bücher, Medien". Regressionstest test_gantert_keeps_literatur_content_chapter. Folge: einzelne Backmatter-Sektionen (z. B. „Weiterführende Literatur") können als validierte Chunks durchrutschen — harmlos (echte Dokumentsektionen), kein Verlust von Inhaltskapiteln.
  • Offset-Voting als Confidence-Flag implementiert (verändert den Split nicht): Plateau-Tie-Break „Map-Marker ∈ Treffermenge" statt naiver Erst-Fundstelle — sonst feuerte „map-confidence niedrig" durch Running-Heads auf jedem Buch (Kolumnentitel-Plateau). Der per-Kapitel-Validierungs-Merge bleibt der bestimmende Akzeptanzmechanismus.
  • Real-Buch-Tests referenzieren die PDFs über ATOMIC_AGENT_TEST_BOOK_DIR (Default config.LITERATURE_DIR) und skippen bei Abwesenheit — keine hartcodierten Pfade, CI-tauglich; die synthetischen Fixtures (c/d/e/g/h) sind die dauerhafte Abdeckung.
  • Validierungs-Test nutzt _validate_boundary (die echte Gate-Funktion inkl. Fuzzy) statt nacktem Wort-Overlap: Klingenberg-Body hat Encoding-Mojibake („Datenqualit�t"), Hobohm-Teil-Divider zeigt auf die erste Inhaltsseite („Informationsforschung") — beide korrekte Grenzen, die reiner Overlap fälschlich abgelehnt hätte.

split_by_chapters() bekommt mit pdf_path einen Outline-first-Pfad: Kapitelgrenze
= fitz-aufgeloeste PDF-Lesezeichen-Zielseite, ueber eine exakte physisch->Marker-
Seiten-Map (zwei Zweige: PageLabels-Label-Map bzw. Zaehl-Map nicht-leerer Seiten)
auf den [S. N]-Marker gemappt und per Titel-Wort-Overlap validiert (>=50 %, sonst
HiPS-Kaskade Substring/Fuzzy-80). Fehlgeschlagene Grenzen werden verworfen und ins
Vorsegment gemergt; >50 % Fehlschlaege oder Degenerations-Guards (Median-Segment
< 400 Woerter, Riesensegment > 70 %) -> Outline verwerfen -> heuristischer
Normalpfad. Front-/Backmatter-Filter, Root-Descend (Single-Root -> Ebene tiefer),
Duplikat-Merge (Seitenabstand <=1), Offset-Voting-Kreuzcheck (Confidence-Flag),
Diagnose-Zeile. Ohne nutzbare Outline exakt bisheriges Verhalten; ohne pdf_path
(Alt-Aufrufer/eval) unveraendert.

- _TOC_TRAIL_RE nur erweitert: gespacte Dot-Leader (`. . . 9`) + roemische
  Seitenzahlen. _CHAPTER_RE/_is_real_chapter_match unangetastet, test_chapter_regex
  bleibt gruen. (Alte Regex liess reale Klingenberg-TOC-Zeilen als Kapitel durch.)
- Chunk.source ("outline"/"heuristic"/"words").
- extract_overview(text, chapters=chunks): ein Split pro Lauf, outline-basiert.
- orchestrator: split_by_chapters(text, pdf_path) + extract_overview(chapters),
  Degenerations-Guard mit hartem by-chapter-Abbruch statt Warnung (der 15,5-h-/
  32-Mio-Token-/0-Notes-Lauf).

Empirie: Klingenberg 9, DAMA 17, Kuhlen 6 (39-Seiten-Drift), Gantert, Hobohm 12 —
alle Quelle outline, 100 % Validierung. Heuristik-Pfad auf Klingenberg: 117 Chunks.
@TillQuandel
TillQuandel merged commit 4ef2847 into master Jul 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MITTEL] split_by_chapters erkennt keine echten Kapitel — TOC-Eintraege werden Mikro-Kapitel (by-chapter unbrauchbar)

2 participants