Skip to content

fix(eval): CID-Font-Artefakte (U+0231/U+022C) im Quelltext zurückmappen — Grounder bestraft OCR-degradierte PDFs nicht mehr (#278)#298

Merged
TillQuandel merged 1 commit into
masterfrom
fix/278-eval-ocr-retrieval
Jul 15, 2026
Merged

fix(eval): CID-Font-Artefakte (U+0231/U+022C) im Quelltext zurückmappen — Grounder bestraft OCR-degradierte PDFs nicht mehr (#278)#298
TillQuandel merged 1 commit into
masterfrom
fix/278-eval-ocr-retrieval

Conversation

@TillQuandel

@TillQuandel TillQuandel commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Problem

Issue #278: Der Eval-Grounder scheitert am extrahierten Quelltext des Jockisch-2010-PDFs — quellentreue Notes werden fälschlich als Halluzination markiert:

Ursache

Per PyMuPDF direkt am Original-PDF verifiziert (nicht angenommen — der im Issue vermutete generische „Whitespace-Kollaps" ist beim Nachmessen präziser einzugrenzen): Die eingebettete Schriftart des PDFs mappt ihr Leerzeichen-Glyph auf U+0231 („ȱ", 5173×) und ihren Trennstrich auf U+022C („Ȭ", 240×) statt auf ASCII-Space/-Hyphen. Der von eval_common._extract_page_text/_raw_page_lines gelesene Text besteht dadurch fast nur aus zusammengeschriebenen Wörtern:

DieȱAkzeptanzȱistȱGegenstandȱzahlreicherȱwissenschaftlicherȱUntersuchungen.

Alle anderen Nicht-ASCII-Zeichen im PDF (echte Umlaute, vereinzelte Formel-/Griechisch-Glyphen ≤8×) sind Größenordnungen seltener und ohne belegte Wirkung auf das Symptom.

Folge: Cosine-Chunk-Retrieval (_retrieve_claim_contexts) und Zitat-Fuzzy-Verifikation (_verify_evidence_normalized, Schwelle 0.90) scheitern an einer Quelle, die das generierende LLM korrekt gelesen hat — der saubere Note-Claim erreicht gegen den korrupten Corpus nur Score 0.622 (RED-Test unten).

Fix

Rückmapping U+022C → "-" und U+0231 → " " am Anfang von eval_common._normalize, vor Silbentrennungs- und Whitespace-Normalisierung (der korrupte Trennstrich wird so von der bestehenden Silbentrennungs-Regex direkt mitrepariert). _normalize ist der gemeinsame Trichter für Chunk-Texte, Embeddings, Judge-Kontext-Pool und Evidence-Corpus (_normalize_for_evidence ruft es auf) — der Fix wirkt damit konsistent auf Retrieval UND Zitat-Verifikation, ohne neue Codepfade. PDFs ohne diese Artefakte bleiben byte-identisch (Regressionstest; die zwei Codepoints kommen in gesundem Text nicht vor).

Bewusst NICHT in diesem PR: der im Issue zusätzlich genannte CID-Filter vor der Akronym-Expansion (Pipeline-Seite, tanzmodellȱ-Durchschlag in den Note-Body) — das ist Generierungs-, nicht Eval-Scope und überschneidet sich mit #279/PR #295 (H1-/Akronym-Fix). Falls gewünscht, als Folge-Issue.

Auswirkung auf die Messung — warum das ehrlicher ist (nicht schöngerechnet)

Was sich ändert: Bei PDFs mit diesen CID-Artefakten sieht der Judge erstmals lesbaren Quelltext; die Zitat-Verifikation vergleicht sauberen gegen sauberen Text. hallucination_rate sinkt und coverage_rate steigt auf solchen PDFs — nicht, weil die Messlatte gesenkt wurde, sondern weil bisher gegen einen Quelltext gemessen wurde, den die Quelle so nicht enthält. Der Beleg, dass die alten Werte Artefakte waren (nicht echte Halluzinationen), ist extern und unabhängig vom Fix: Opus-Tiefenreview mit 10/16/14 geprüften Ankern, mehrere wörtliche Belege, 0 echte Halluzinationen.

Kein Masking-Risiko-Zuwachs: Der Fix injiziert keinen Kontext, ändert keine Labels, keine Schwellen und keine Decision-Logik — er repariert deterministisch zwei Glyph-Codepoints im Quelltext-Kanal. Eine echte Halluzination gegen ein Jockisch-artiges PDF wird nach dem Fix strenger geprüft als vorher, weil der Judge die Quelle jetzt tatsächlich lesen kann (vorher war auf solchen PDFs jedes Urteil Rauschen — in beide Richtungen). Für alle anderen PDFs: keinerlei Verhaltensänderung.

Testnachweis (TDD)

RED (vor Fix), Testdaten sind wörtliche PyMuPDF-Extrakte aus dem Original-PDF:

FAILED test_normalize_repairs_cid_space_artifact - AssertionError:
  assert 'DieȱAkzeptan...tersuchungen.' == 'Die Akzeptan...tersuchungen.'
FAILED test_normalize_repairs_cid_hyphenation_artifact - AssertionError:
  assert 'aufȱdieȱwahr...ȱ integriert.' == 'auf die wahr...l integriert.'
FAILED test_normalize_for_evidence_matches_corrupted_corpus - AssertionError:
  Erwartet verifiziert, war False (Score=0.622)
3 failed, 1 passed

GREEN (nach Fix): 4 passed. End-to-End-Stichprobe: _extract_page_text auf Jockisch S. 6 liefert jetzt lesbares Deutsch, 0× U+0231 / 0× U+022C (vorher dominierten sie die Seite).

Volle Suite (nach Fix inkl. EVAL_VERSION-Bump): 5940 passed, 3 skipped, 8 deselected in 329 s. ruff check + ruff format --check auf allen 4 geänderten Dateien sauber.

EVAL_VERSION 4.2 → 4.3 (gebumpt, Begründung)

Die im Code dokumentierte Konvention (Kommentar am 4.1→4.2-Bump + EVAL_CACHE_NAMESPACE-Docstring) verlangt den Bump hier zwingend: Der Judge-Kontext-Pool ändert sich für betroffene PDFs (dieselbe Methodik-Änderungs-Klasse wie #232), und der content-adressierte Eval-Cache ist versions-gescoped — ohne Bump würde eine inhaltlich unveränderte Jockisch-Note das alte Artefakt-Ergebnis aus dem eval-v4.2-Cache ziehen (exakt die „stille Staleness", vor der der Namespace-Mechanismus laut Docstring schützt). Kosten: Cache-Invalidierung auch für nicht betroffene PDFs (deren Ergebnisse bleiben inhaltlich identisch, werden aber einmal frisch gerechnet). Snapshot in test_audit_double_call.py nachgezogen (gleiche Stelle wie beim 4.2-Bump). Falls Till stattdessen 4.2 behalten will: einzelner Revert von zwei Zeilen, keine weiteren Abhängigkeiten.

Offene Entscheidungen / Grenzen

  • Re-Eval-Sweep über den Baseline-Korpus folgt als separate Aufgabe (nicht Teil dieses PRs); Bestandsdaten (quality_history/runs/baseline/note_evals) wurden nicht angefasst. Der 4.2/4.3-Skew bleibt gewollt sichtbar.
  • Der Fix behandelt genau die zwei empirisch dominanten Codepoints dieses Font-Typs. Andere CID-korrupte Fonts mit anderen Fehl-Mappings werden nicht generisch erkannt — das generische Textqualitäts-Gate + OCR-Fallback bleibt G6: PDF-Textqualitaets-Gate + OCR-Fallback (pdf_chunker) #27.
  • Seltene Rest-Artefakte im Jockisch-PDF (ĺ, Έ u. ä., ≤8×) bleiben unbehandelt — kein belegter Beitrag zum Coverage-Symptom.

Fixes #278


Adversariale Kontrolle (Opus, vor Merge)

Verdikt: Freigabe. Alle Zahlen unabhängig reproduziert: U+0231 = 5173×, U+022C = 240× am Original-PDF (beide Extraktionswege); RED-Score 0.6216 → 0.622 bestätigt; Bump-Konvention identisch zum 4.1→4.2-Muster (git show 70fee82), kein hängender 4.2-Literal; Mapping idempotent und vor Silbentrennung/Lowercasing korrekt platziert; Normalisierung wirkt symmetrisch auf beide Vergleichsseiten (kein Masking-Mechanismus durch diesen Fix).

Framing-Präzisierung aus der Kontrolle: Der Fix bringt das Jockisch-PDF auf Parität mit allen anderen PDFs (vorher: Evidence-Check bei „reject-all" festgenagelt). Die danach greifende, pre-existing systemische Schwäche — token_set_ratio gegen den Gesamt-Volltext lässt kurze In-Domain-Rekombinationen mit Score 1.0 durch — gilt für jedes PDF und ist nicht Teil dieses PRs (Folge-Issue-Kandidat: Evidence-Verifikation gegen kontiguierliches Fenster/Chunk). Randnotiz: U+022C/U+0231 haben legitime Vorkommen in livischer Orthographie bzw. phonetischer Transkription — für diesen Korpus (DE/EN-Wissenschaftstexte) praktisch irrelevant, und das Mapping wirkt ausschließlich im Eval-Matching, nie auf Nutzer-Text.

…ppen — quellentreue Notes nicht mehr als Halluzination markiert (#278)

Root Cause (per PyMuPDF am Jockisch-2010-PDF verifiziert): Die eingebettete
Schriftart mappt ihr Leerzeichen-Glyph auf U+0231 (ȱ, 5173x) und ihren
Trennstrich auf U+022C (Ȭ, 240x). Der extrahierte Quelltext besteht dadurch
fast nur aus zusammengeschriebenen Woertern (DieȱAkzeptanzȱistȱ...) — Cosine-
Chunk-Retrieval und Zitat-Fuzzy-Verifikation scheitern an einer Quelle, die
das generierende LLM korrekt gelesen hat. Ergebnis: False-Positive-
Halluzinationen (57,1%/42,9%) und 0,0% Coverage bei laut Opus-Tiefenreview
~100% quellentreuen Notes (Repro-Runs 20260714-185639, 20260714-215345).

Fix: Rueckmapping U+022C→'-' und U+0231→' ' am Anfang von
eval_common._normalize, VOR Silbentrennungs- und Whitespace-Normalisierung —
wirkt damit konsistent auf Chunks, Embeddings, Judge-Kontext-Pool und
Evidence-Corpus (_normalize_for_evidence). PDFs ohne diese Artefakte bleiben
byte-identisch (Regressionstest).

EVAL_VERSION 4.2 → 4.3: Der Judge-Kontext-Pool aendert sich fuer betroffene
PDFs — dieselbe Methodik-Aenderungs-Klasse wie der 4.1→4.2-Bump (#232). Ohne
Bump wuerde der content-adressierte Eval-Cache fuer unveraenderte Notes das
alte Artefakt-Ergebnis liefern (stille Staleness, genau der dokumentierte
Schutzzweck des Bumps). Snapshot in test_audit_double_call nachgezogen.

Tests: TDD (3 RED → GREEN) mit woertlichen PyMuPDF-Extrakten aus dem
Original-PDF; volle Suite 5940 passed.
@TillQuandel TillQuandel merged commit a720399 into master Jul 15, 2026
4 checks passed
TillQuandel added a commit that referenced this pull request Jul 15, 2026
…achen (#303)

Ist-Zustand (Dry-Run auf master a720399): "Zu evaluieren: 0 Notes" — alle 14
Baseline-Ordner in .cache/eval/baseline/ wurden uebersprungen.

Zwei Bugs:
1. _already_done() pruefte hartkodiert eval_version='4.1'. Nach dem
   EVAL_VERSION-Bump auf 4.3 (#298) markierte das jede historisch mit 4.1
   evaluierte Note als "schon erledigt" — obwohl deren Re-Evaluierung unter
   der aktuellen Version der Zweck des Sweeps ist.
2. PDF_MAP war veraltet (verwies auf nicht-existierende Dateinamen, kannte
   die meisten der 14 Ordner gar nicht) und nutzte Praefix-Matching
   (folder.startswith(prefix)) — das kollidiert real: "Porst-2014-Auszug-
   S1-40-bak-preclean-20260530" beginnt mit "Porst-2014-Auszug-S1-40" und
   haette denselben Eintrag geerbt, obwohl der Backup-Ordner ausgeschlossen
   sein soll.

Fix:
- Guard scopt auf _eq.EVAL_VERSION (additiv — Alt-Zeilen bleiben, Resume
  innerhalb einer Version bleibt erhalten).
- PDF_MAP komplett rekonstruiert aus der historischen Note->PDF-Zuordnung in
  atomic_analytics.db (note_evals.pdf je vault__*.md-Datei, eval_version=4.1)
  + Existenz-Check gegen LITERATURE_DIR (Config-Konstante statt hartkodierter
  OneDrive-Pfade — CLAUDE.md-Pfadregel). Exakter Ordnername statt Praefix.
  6 Ordner gemappt, 8 explizit ausgeschlossen (dokumentiert im Code-Kommentar
  je Eintrag): leerer Alt-Ordner, Backup, 3x Test-/Struktur-Fixture, 2x
  fehlendes historisches PDF (bates-2017.pdf, zettelkasten-primer.pdf), 1x
  Auszug-PDF fehlt (Porst — bewusst NICHT durchs Voll-PDF ersetzt, Score-
  Verwaesserung durch mehr Chunks; offener Punkt s. PR-Body).
- Neue --baseline-dir-Option (Default ROOT-relativ) fuer Funktionsnachweise
  aus einem isolierten Worktree ohne eigenes .cache.

Dry-Run danach (--baseline-dir gegen das Haupt-Repo-Cache, read-only): 27
Notes ueber 6 Ordner zu evaluieren, 0 uebersprungen, 8 Ordner sauber geskippt.

Tests: TDD (9 RED -> GREEN, test_reeval_baseline_guard.py neu). Volle Suite
(generative + lib/decision_engine/tests + shared/tests): 5969 passed, 3
skipped, 8 deselected.

Co-authored-by: TillQuandel <tillq@live.de>
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.

[HOCH] Eval-Retrieval scheitert an OCR-korruptem pdftotext-Quelltext — quellentreue Notes fälschlich als Halluzination markiert

2 participants