Files
alpha-id/docs/specs/2026-05-18-alpha-id-icd-suggestion-design.md
T
Brummel ca352aafc7 docs: retire superpowers/ layout — keep design spec under docs/specs/
The superpowers workflow methodology is no longer in use. Move the
methodology-neutral design spec to docs/specs/ as the project's design
ledger, drop the superpowers-bound implementation plan (it referenced
superpowers sub-skills), and remove the now-dead /docs/superpowers/
.gitignore entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:02:38 +02:00

229 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 E10E14****nicht** 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`+`2``E11.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).