Files
alpha-id/docs/superpowers/specs/2026-05-18-alpha-id-icd-suggestion-design.md
T
Brummel 7cec295d99 docs: input-contract revision — extracted phrases instead of dictation paragraphs
Source-string analysis showed the Hypertonie miss is not a bug but a
structural consequence of embedding whole noisy Anamnese paragraphs
against terse Alpha-ID corpus strings. Resolution (user-decided): the
caller (doctate's LLM) extracts diagnostic phrases; this project still
contains no LLM — it just receives a cleaner input contract.

Spec: new §1a (rationale + generous extraction contract + the honest
extractor-is-recall-bottleneck trade-off); §1/§2/§3/§6/§8/§9/status
updated; §6 reworks eval (representativeness shift, paraphrase axis,
hand-labeled phrase-list set, separated extraction-ceiling vs matcher
recall). Plan: Goal/Architecture, revision banner, Task 6 rewritten
(one non-empty line = one unit; split_segments name/fields retained,
minimal-churn decision), Task 11/19 revision notes.

Code not yet changed (segment.rs still blank-line splits) — next step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:26:30 +02:00

21 KiB
Raw Blame History

Design-Spec: alpha-id — ICD-Code-Vorschläge aus Diktat

Datum: 2026-05-18 Status: PoC implementiert; Input-Vertrag revidiert 2026-05-18 (siehe §1a) — Eingabe ist nicht mehr das leerzeilen-segmentierte Diktat, sondern eine Liste vom Caller extrahierter Diagnose-Formulierungen. Stack: Rust — Core-Library + dünne CLI (PoC)


1. Ziel & Kontext

Aus einem (bereits transkribierten, fehlerbehafteten) ärztlichen Diktat eine einzige priorisierte Liste von max. 10 ICD-10-GM-Codes mit Beschreibung erzeugen, mit Filterung nach Tags.

Zwei Zwecke:

  1. Vergessene abrechenbare Codes aufdecken — im Stress vergessene Codes kosten Erlös. Das ist primär ein Recall-Problem. Leitmetrik: Recall@10 abrechenbarer Codes. Mit dem revidierten Input-Vertrag (§1a) ist der Recall dieser Komponente nach oben durch den Extraktions-Recall begrenzt: was der Caller nicht als Formulierung übergibt, kann der Matcher nicht retten. Der Ziel-1-Nutzen bleibt nur erhalten, wenn der Extraktionsvertrag großzügig ist (jede Diagnose-Erwähnung extrahieren, auch beiläufige in Anamnese/Verlauf — nicht nur das, was der Arzt bereits explizit als Diagnose kodiert).
  2. Hilfreiche klinische Impulse — die bloße Sichtbarkeit verwandter Einträge (ICD-Hierarchie-Geschwister, Alpha-ID-Synonyme) erweitert das Bild und wirkt absichernd.

Das System ist ein PoC. Übernahme in das Schwesterprojekt doctate erfolgt nur, wenn es nachweisbar taugt (gemessen am Eval-Harness).

I/O-Vertrag (revidiert 2026-05-18)

  • Eingabe: eine Liste vom Caller extrahierter Diagnose-Formulierungen, eine pro Zeile (newline-getrennt; eine Zeile = eine Retrieval-Einheit). Nicht mehr das komplette, leerzeilen-segmentierte Diktat. Die Extraktion leistet das LLM in doctate; dieses Projekt enthält weiterhin keinen generativen LLM — es bekommt nur einen saubereren Input. Begründung siehe §1a.
  • Ausgabe: eine priorisierte Liste von max. 10 ICD-Codes für die gesamte Eingabe — nicht pro Zeile. Pro Formulierung wird einzeln retrievet/gerankt, danach global fusioniert/dedupliziert.

1a. Warum extrahierte Formulierungen statt Diktat-Absätze (Revision 2026-05-18)

Belegter Befund (Source-String-Analyse, 2026-05-18): Der Match-Korpus (Alpha-ID Feld 8) besteht aus knappen Diagnose-Strings (24 Wörter, z. B. I10.90 Arterielle Hypertonie). Ein realer Befund-Absatz ist eine 1525-Wort-Anamnese-Prosa, in der die Diagnose von nicht-diagnostischen Tokens (Alter, Medikament, Messwerte) umgeben ist. bge-m3 mittelt den ganzen Absatz → der Query-Vektor wird vom Kontext dominiert, nicht vom Diagnosekern; die knappe Ziel-Phrase liegt zwar im Korpus, wird aber von Mess-/Kontext-„Fallen" (z. B. R03.0 Erhöhter Blutdruckwert ohne Diagnose) verdrängt. Dies ist kein Bug, sondern eine strukturelle Folge der Absatz-als-Einheit-Repräsentation — durch keine Fusions-/Reranker-Verbesserung behebbar.

Auflösung: Der Caller (doctate-LLM) extrahiert die relevanten Diagnose-Formulierungen; nur diese knappen Phrasen werden gematcht. Das stellt die Kurz↔Kurz-Symmetrie her — genau das Regime, in dem dichtes Retrieval + Reranker stark sind. Scope bleibt sauber: kein Mode B, kein LLM in diesem Projekt, nur ein präziserer Eingabevertrag. Der Pipeline-Kern (Lexik Semantik → RRF → Reranker → Cross-Einheit-Dedupe → Tag-Filter → Top-10) bleibt unverändert; lediglich die Einheiten-Bildung wechselt von „Split an Leerzeilen" zu „eine Formulierung pro Zeile".

Extraktionsvertrag (Verantwortung von doctate, nicht dieses Projekts):

  • Großzügig: jede Diagnose-/Befund-Erwähnung aus dem gesamten Diktat (Anamnese, Verlauf, Beurteilung) als eigene Zeile — auch beiläufige und Nebendiagnosen. Ziel 1 lebt von genau den nur beiläufig erwähnten, nicht kodierten Diagnosen.
  • Knapp & diagnosezentriert: die Diagnose-Nominalphrase, ohne Anamnese-Floskeln, Medikamente, Messwerte, Datumsangaben. Negationen/Ausschlüsse („kein Hinweis auf …") nicht als Diagnose extrahieren (das Projekt verarbeitet Negation nicht).
  • Wörtlich genug: nahe an der Diktatformulierung; leichte Transkriptionsfehler sind toleriert (Lexik/Semantik sind dafür robust, Umlaut-Faltung ist implementiert).
  • Keine Codes, keine Wertung: doctate liefert Text, nicht ICD; Priorisierung/Abrechenbarkeit entscheidet diese Komponente.

Ehrlicher Trade-off (vom Nutzer bewusst akzeptiert): Der Extraktor wird zum alleinigen Recall-Flaschenhals (§1, Ziel 1). Konsequenz für das Eval (§6): der Harness muss Extraktor-Verhalten modellieren (Auslassungen, Paraphrasen), nicht nur Transkriptionsfehler.

Datenquellen (vorhanden in /home/brummel/dev/alpha-id/)

  • data/icd10gm2026_alphaidse_edvtxt_20250926.txt — Alpha-ID-SE, 90.898 Sätze, 8 |-getrennte Felder (UTF-8). Feld 8 = ~90k natürlichsprachliche Diagnose-Formulierungen inkl. Synonyme (primärer Match-Korpus). Feld 1 = Gültigkeit (0/1). Felder 36 = ICD-Codes (Primär/Stern/Ausrufezeichen/Primär2). Feld 7 = Orpha.
  • icd-claml/Klassifikationsdateien/icd10gm2026syst_claml_20250912.xml — ICD-10-GM 2026 als ClaML 2.0.0 (14 MB): Beschreibungen, Hierarchie, <Meta>-Tags (Para295/Para301, Kapitel/Gruppe, Exotic, Sex, Age, IfSG, Content).

Modelle / Infrastruktur

IONOS AI Model Hub (https://openai.inference.de-txl.ionos.com/v1), Token ~/.ionos_token. Nur zwei Modelle:

  • Embeddings: BAAI/bge-m3 (multilingual, deutscher Medizintext)
  • Reranker: Qwen/Qwen3-VL-Reranker-8B (Cross-Encoder)

Kein generativer Chat-LLM. IONOS-Client-Pattern aus doctate/server/src/analyze/backend.rs und doctate/docs/ionos-llm-api.md wiederverwenden (Token-Handling, Retry, Quirks).


2. Architektur

Zwei Modi über Config-Schalter, gleiche Schnittstelle, im selben Eval-Harness vergleichbar:

  • Modus C — Lexik-Baseline: BM25/Trigramm + Tag-Filter. Kein IONOS, offline, sofort. Die Baseline, die A schlagen muss.
  • Modus A — Hybrid + Reranker (Kern): Lexik Semantik (bge-m3) → Qwen3-VL-Reranker-8B → Fusion → Tag-Filter → Top-10.

Modus B (generativer LLM-Rerank/-Vorschlag) ist explizit NICHT Scope. Embedding- und Reranker-Modelle sind keine generativen LLMs und bleiben in A.

Datenfluss

Eingabe = extrahierte Diagnose-Formulierungen (eine pro Zeile)
  │
  ▼ [1] Parser           → Einheiten (eine nicht-leere Zeile = eine
  │                         Formulierung; im Code weiterhin „segment" genannt)
  ▼ [2] Normalisierung   → pro Einheit: lowercasing, Medizin-Abkürzungen,
  │                         Umlaut-Faltung, Tokenisierung (kein LLM)
  ├─►[3a] Lexik (tantivy BM25/Trigramm)              ┐ pro Einheit:
  └─►[3b] Semantik (bge-m3-Query-Embedding vs.       ┘ Pool ~50100 (RRF-Mix)
          vorab persistierter Vektorindex)             [A; in C nur 3a]
  ▼ [4] Reranker         → Qwen3-VL-Reranker-8B: (Formulierung, Alpha-ID-Text)
  │                         paarweise → rerank-Score   [nur A]
  ▼ [5] Fusion           → alle Einheiten-Kandidaten in EINE Liste; je
  │                         normalisiertem ICD-Code dedupliziert
  │                         (score=max, alle Herkünfte erhalten)
  ▼ [6] Tag-Filter       → harte Constraints: Abrechenbarkeit (Para295/301),
  │                         Gültigkeit (Feld 1), Kapitel/Gruppe,
  │                         klinische Marker (Exot/Sex/Alter/IfSG/Content)
  ▼ [7] Top-10           → globales Budget; je Treffer: Code, Beschreibung,
                            Score, Quell-Phrase, Quell-Segmente, Tags

Module (klar abgegrenzt)

Modul Aufgabe Abhängigkeit
corpus Alpha-ID parsen → In-Memory-Index (ICD↔Phrasen, Gültigkeit, Orpha) data/
claml ClaML-XML einmal parsen → code → IcdMeta icd-claml/
embed IONOS bge-m3-Client (Batch, Cache, Retry, Resume) IONOS
rerank IONOS Qwen3-VL-Reranker-8B-Client (Paar-Scoring, Batch, Retry) IONOS
index Lexik-Index (tantivy) + persistierter Vektorindex (ANN) bauen/laden corpus,embed
tags Filter-Prädikate aus ClaML-Meta + Alpha-ID-Gültigkeit claml
pipeline Orchestrierung [1][7], Modus C/A per Config alle obigen
eval Gold-Set-Generator + Recall@k-Metrik pipeline
bin (CLI) dünner Wrapper: index, suggest, eval pipeline,eval

Jedes Modul ist für sich testbar; pipeline ist die einzige Stelle, die Modi kennt. Die Core-Library bleibt frei von CLI-Belangen, damit doctate sie später unverändert einbinden kann.


3. Datenmodell (Kerntypen)

AlphaIdEntry { alpha_id, valid: bool, icd_primary, icd_star,
               icd_addon, icd_primary2, orpha, text }
               // icd_addon = Zusatzschlüssel (Feld 5, „!"); NICHT ICD-Exklusiva
IcdMeta      { code, description, chapter, group, para295, para301,
               exotic, sex, age_low, age_high, ifsg, content }
Candidate    { icd_code, alpha_text, segment_idx,
               lexical: Option<f32>, semantic: Option<f32>,
               rerank: Option<f32> }
Suggestion   { icd_code, description, score, matched_phrase,
               source_segments: Vec<usize>, tags: Tags }
Filter       { billable_only, valid_only, chapters: Option<Set>,
               exclude_exotic, … }
Config       { mode: C|A, top_k, pool_size, filter, ionos }

Dedupe-Schlüssel = normalisierter ICD-Code (ohne Kreuz +, Stern *, Ausrufezeichen !). Ein getroffener Alpha-ID-Eintrag steuert seinen Primärcode bei; Stern-/!-Codes werden als ergänzend markiert. Abrechenbarkeit entscheidet der Tag-Filter (Schritt 6), nicht der Match.

Terminologie-Hinweis (Revision 2026-05-18): Eine Einheit = eine extrahierte Diagnose-Formulierung (eine Eingabezeile). Die Code-Feldnamen segment_idx / source_segments (sowie split_segments) bleiben aus DRY-/Churn-Gründen unverändert; „segment" bezeichnet im Code ab dieser Revision diese Formulierungs-Einheit, nicht mehr einen leerzeilen-getrennten Befund-Absatz. Bewusste Entscheidung: der Vertragswechsel ist semantisch groß, der Code-Eingriff minimal (nur die Einheiten-Bildung in segment.rs).

Fusion (Schritt 5, präzisiert)

  • Innerhalb eines Segments: Kandidatenpool = Lexik-Top-N Semantik-Top-N, gemischt per Reciprocal Rank Fusion (RRF). In Modus C ist die RRF-Ordnung die finale Vor-Filter-Reihung; in A folgt der Reranker.
  • Über Segmente: dedupliziert je normalisiertem ICD-Code, score = max über alle Vorkommen, alle Herkunfts-(Segment, Phrase) erhalten. Zahl getroffener Segmente = Tiebreaker und „Breiten"-Signal für Ziel 2.

3a. ICD-Code-Granularität & ClaML-Subklassifikation (Revision 2026-05-18)

Befund (an Echtdaten belegt): ~26,5 % der gültigen Alpha-ID-Primärcodes sind 6-stellig (5. ICD-Stelle, z. B. E11.72, I10.91). ClaML modelliert die 4./5. Stelle vieler Gruppen — u. a. alle Diabetes-Codes E10E14nicht als eigene <Class kind="category">, sondern über <Modifier>/<ModifierClass> (Subklassifikation), referenziert per <ModifiedBy code=… all=…> am übergeordneten <Class>. Ein naiver meta.get(code) (bzw. Strip-/Root-Fallback) klassifiziert dadurch 1.193 distinkte gültige Codes (≈ 4.394 Alpha-ID-Zeilen) fälschlich als nicht-abrechenbar (Rückfall auf 3-Steller mit Para295=V) → unter --billable-only werden real abrechenbare Codes still verworfen. Das untergräbt Ziel 1.

Entschiedene Auflösung (Nutzer, 2026-05-18): korrekte ClaML-Modifier-Expansion. claml.rs wertet zusätzlich <Modifier> (mit <SubClass>), <ModifierClass> (mit <SuperClass>, preferred <Rubric><Label>, ggf. eigenem <ModifiedBy> zur Verkettung der nächsten Stelle) und <ModifiedBy> an <Class> aus. Für jeden <Class> mit ModifiedBy-Referenzen werden die terminalen Stellen-Kombinationen synthetisiert (4. dann 5. Stelle, der ClaML-Subklassifikations-Algorithmus) und als zusätzliche IcdMeta unter dem kombinierten Code (z. B. E11+.7+2E11.72) in die Map eingetragen. Regeln:

  • Explizite <Class>-Einträge haben Vorrang; Synthese füllt nur fehlende Schlüssel (additiv, kein Überschreiben).
  • Abrechenbarkeit: Terminale synthetisierte 5.-Stellen-Codes dieser modifizierten Gruppen sind Para295=P/Para301=P (der bare 3-Steller bleibt V). Content des Parent (J = „erfordert Subklassifikation") ist das Signal, dass erst die terminale Stelle kodierbar ist.
  • Beschreibung: zusammengesetzt aus Parent-preferred-Label + 4.-Stellen-ModifierClass-Label + 5.-Stellen-ModifierClass-Label.
  • chapter/exotic/ifsg vom Parent geerbt, sofern der Modifier nichts Spezifischeres trägt.
  • Keine kartesische Explosion über fremde Modifier: nur die vom jeweiligen <Class> per ModifiedBy (in Reihenfolge) referenzierten Modifier kombinieren; ModifierClass-eigene ModifiedBy verketten die nächste Stelle.

Alle nötigen Informationen liegen allein in der ClaML-XML (die gelöschte Meta-TXT ist nicht erforderlich). Konsequenz im Plan: eigener Task „ClaML-Modifier-Expansion" + Revision des Mode-C-Pipeline-Lookups (kein Strip-/Root-Hack mehr) + Abrechenbarkeits-Regressionstest (z. B. E11.x unter --billable-only sichtbar & billable=true).


4. Einmaliges, kostensicheres Embedding (kritische Anforderung)

Das Embedden von ~90k Texten via IONOS kostet Geld und soll genau einmal erfolgen. Maßnahmen:

  1. Content-Hash-gekeyter, persistenter Cache. Jeder Alpha-ID-Text → Embedding, gekeyt durch einen stabilen Hash (z. B. SHA-256 von normalisiertem Text + Modellname + Modellversion). Persistenz on-disk (z. B. index/embeddings.db o. ä.). Vor jedem Embed-Aufruf Cache-Lookup; nur fehlende Keys werden gesendet.
  2. Resumierbar / idempotent. Bricht der Full-Run ab (Crash, Netz, Timeout), setzt der nächste Lauf bei den fehlenden Keys fort — kein Neustart von 0, kein doppelt bezahltes Token.
  3. Cost-Guard vor dem Full-Run. index build schätzt zuerst Items, Tokens und Kosten und zeigt sie an; der Full-Run erfordert explizite Bestätigung (--confirm Flag oder interaktiv).
  4. Verpflichtender Smoke-Test zuerst. index build --sample N (Default klein, z. B. 50) embedded nur ein Sample, validiert: HTTP-OK, korrekte Vektordimension, Cache schreibt/liest, ANN-Index baut, eine Beispiel-suggest-Abfrage liefert plausible Treffer. Erst nach grünem Sample wird der Full-Run freigegeben.
  5. Delta-fähig. Ändert sich künftig der Korpus (neue Alpha-ID-Jahresversion), werden nur geänderte/neue Texte nachembedded — der Hash-Key macht das automatisch.

Reihenfolge im Staging: C zuerst (braucht kein Embedding, sofort lauffähig + Eval-Harness), dann A-Embedding-Pfad am Sample verifiziert, dann ein einziger Full-Embedding-Run.


5. Fehlerbehandlung

Leitprinzip: degradieren, nie hart scheitern — fehlende Codes kosten Geld; der Arzt bekommt immer eine Liste.

  • IONOS Timeout/5xx: Retry mit Backoff (doctate-Pattern). Dauerhafter Ausfall in Modus A → automatischer Fallback auf Modus C; Ergebnis trägt degraded: true.
  • Token ~/.ionos_token fehlt → klare Meldung; A nicht verfügbar, C läuft.
  • Leeres/kaputtes Diktat → leeres Ergebnis mit Begründung. Segment ohne Kandidaten → übersprungen, in diagnostics vermerkt.
  • Korpus-/ClaML-Ladefehler → harter Startfehler (ohne Daten kein Betrieb).
  • Keine Halluzination per Konstruktion: Treffer stammen ausschließlich aus Korpus-Einträgen; der Reranker sortiert nur um, erfindet nichts.

6. Eval-Harness (erstklassige Komponente)

Kein gelabeltes Datenmaterial vorhanden → der Harness ist Teil des PoC, kein Nachgedanke. Der Alpha-ID-Korpus ist sein eigenes Gold-Set (jeder Text hat einen bekannten ICD-Code).

Repräsentativitäts-Konsequenz des Vertragswechsels (wichtig, ehrlich): Unter dem alten Diktat-Absatz-Vertrag war der synthetische Eval nicht repräsentativ — er verrauschte kurze Korpus-Strings und matchte gegen denselben Korpus (Kurz↔Kurz), während der reale Input lange Prosa war; die so gemessene Zahl (erster Lauf: Modus C 0,520 vs. A 0,995 BillableRecall@10) überschätzte die Realität (Self-Retrieval-Kontamination). Unter dem neuen Vertrag ist der reale Input selbst eine knappe Formulierung → das Kurz↔Kurz-Regime des Harness wird legitim repräsentativ für die Rausch-/Varianten-Robustheit. Damit verbleibt als dominante Unbekannte nicht mehr die Längen-Asymmetrie, sondern das Extraktor-Verhalten.

  • Generator (Rausch- & Varianten-Achse): sampelt gültige, abrechenbare Alpha-ID-Einträge → (phrase, erwarteter ICD). Injiziert deterministisch (Seed): (a) Transkriptionsfehler (deutsche Medizin-Homophone, Wortgrenzen-Merge/Split, Zifferndreher, Umlaut↔ae/oe/ue, ASR-Verwechslungen) und (neu) (b) Extraktor-Paraphrase: Synonym-/Wortstellungs-/Knappheits-Varianten der Formulierung (nicht nur die wörtliche Korpus-Phrase), damit nicht die exakte Korpus-Zeichenkette getestet wird.
  • Plus ein kleines, handgelabeltes Set: realistische, extrahierte Formulierungs-Listen (eine Phrase pro Zeile — die neue Eingabeform), gepaart mit dem Gold-Satz abrechenbarer ICD-Codes, den ein Kodierer dafür vergäbe. Zu jedem Beispiel wird zusätzlich das Quelldiktat abgelegt, um die Extraktions-Recall-Decke zu dokumentieren (= Anteil der Gold-Diagnosen, die ein großzügiger Extraktor aus dem Diktat überhaupt als Zeile liefern würde). Diese Decke ist doctates Verantwortung, nicht die dieser Komponente — wird gemessen/berichtet, aber nicht hier optimiert.
  • Metriken: Recall@1/5/10 gesamt; gesondert Recall@10 abrechenbarer Codes (Leitzahl Ziel 1) — gemessen gegeben die extrahierten Formulierungen; Modus C vs. A nebeneinander; separat ausgewiesen: Extraktions-Recall-Decke (Diktat→Formulierungen) vs. Matcher-Recall (Formulierungen→Codes), damit die zwei Recall-Verluste nicht vermischt werden; Latenz + IONOS-Calls/Kosten; Breiten-Maß (distinkte assoziierte Codes) für Ziel 2.

„Taugt's" ist eine Go/No-Go-Entscheidung des Nutzers anhand der Eval-Tabelle: A vs. C bei Recall@10 abrechenbarer Codes auf dem rausch-+varianten-injizierten Gold-Set und gegen das handgelabelte Formulierungs-Set, abgewogen gegen Latenz/IONOS-Kosten und die separat berichtete Extraktions-Decke. Eine feste numerische Schwelle wird bewusst nicht erfunden; sie wird nach dem ersten Eval-Lauf gemeinsam festgelegt.


7. CLI-Oberfläche

alpha-id index build [--sample N | --full] [--confirm]
                                  # Smoke-Test (Sample) bzw. einmaliger Full-Run;
                                  #   Korpus+ClaML parsen, bge-m3-Embeddings,
                                  #   Lexik-+Vektorindex persistieren (resumierbar)
alpha-id suggest [FILE|-]         # Formulierungs-Liste (eine pro Zeile) → Top-10
   --mode c|a  --top 10  --billable-only  --valid-only
   --chapters I,J  --exclude-exotic  --json|--table
alpha-id eval --mode c|a --cases N --seed S [--gold PATH]
  • Config-TOML: IONOS-Endpoint/Modellnamen/Pool-Größen/Pfad-Defaults, per Flags überschreibbar.
  • suggest-Ausgabe (JSON) je Treffer: icd_code, description, score, matched_phrase, source_segments, tags{billable, chapter, exotic, …} + Top-Level diagnostics{ mode, degraded, timings, segment_count, ionos_calls }.

8. Abgrenzung (Out of Scope)

  • Generativer Chat-LLM in der Pipeline (Modus B).
  • Extraktion der Diagnose-Formulierungen aus dem Rohdiktat — Verantwortung des Callers (doctate-LLM) gemäß Extraktionsvertrag §1a; dieses Projekt segmentiert/extrahiert nicht.
  • Negationsverarbeitung („kein Hinweis auf …") — Negierte Diagnosen werden laut Vertrag gar nicht erst extrahiert; der Matcher modelliert Negation nicht.
  • Eigene/projektspezifische Tag-Whitelists (nur ClaML-abgeleitete Filter).
  • Produktions-Deployment / doctate-Integration (erst nach erfolgreichem PoC).
  • UI (CLI genügt für den PoC).

9. Offene Punkte / Annahmen

  • Genaues IONOS-Antwortformat von bge-m3 (Vektordimension) und Qwen3-VL-Reranker-8B (Score-Skala, Batch-API) wird im Sample-Smoke-Test (Abschnitt 4.4) empirisch verifiziert, bevor der Full-Run läuft.
  • Wahl der konkreten Rust-Crates (tantivy für Lexik; ANN-Lib z. B. usearch/hnsw_rs; Embedding-Persistenz) wird im Implementierungsplan fixiert.
  • Revision 2026-05-18 (Input-Vertrag): Eingabe = extrahierte Diagnose-Formulierungen statt Diktat-Absätze (§1a). Plan-Konsequenz: Task 6 (segment.rs) wechselt von „Split an Leerzeilen" zu „eine nicht-leere Zeile = eine Einheit"; Eval-Tasks (11/19) ergänzen Paraphrase-Injektion + handgelabeltes Formulierungs-Set + getrennte Berichterstattung Extraktions-Decke vs. Matcher-Recall. Pipeline-Kern unverändert.
  • Offener Punkt: Der Extraktor (doctate-LLM) ist der alleinige Recall-Flaschenhals für Ziel 1; sein Real-Recall ist erst mit dem handgelabelten Diktat→Formulierungs-Set quantifizierbar und gehört nicht in dieses Projekt — wird aber getrennt berichtet (§6).
  • Aufgelöst 2026-05-18: Korpus↔ClaML-Code-Granularität (5. Stelle nur via <ModifierClass>) — siehe §3a; korrekte ClaML-Modifier-Expansion beschlossen und in den Plan aufgenommen.
  • Aufgelöst 2026-05-18: Git-Repository initialisiert; PoC implementiert, auf main gemergt, Umlaut-Faltung gefixt, einmaliger IONOS-Embedding-Lauf abgeschlossen (90.896 Vektoren).