diff --git a/docs/superpowers/plans/2026-05-18-alpha-id-icd-suggestion.md b/docs/superpowers/plans/2026-05-18-alpha-id-icd-suggestion.md index 0d59688..f1b6ab7 100644 --- a/docs/superpowers/plans/2026-05-18-alpha-id-icd-suggestion.md +++ b/docs/superpowers/plans/2026-05-18-alpha-id-icd-suggestion.md @@ -2,14 +2,16 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** A Rust core library + thin CLI that turns a marker-segmented dictation into one prioritized list of ≤10 ICD-10-GM codes, with tag filtering, validated by a transcription-error eval harness. +**Goal:** A Rust core library + thin CLI that turns a list of caller-extracted diagnostic phrases (one per line) into one prioritized list of ≤10 ICD-10-GM codes, with tag filtering, validated by a noise+paraphrase eval harness. -**Architecture:** Two modes behind one interface. Mode C = lexical baseline (tantivy BM25 over the ~90k Alpha-ID phrase corpus) + ClaML tag filter. Mode A = adds IONOS `bge-m3` semantic retrieval (brute-force cosine over precomputed embeddings) fused with lexical via Reciprocal Rank Fusion, then IONOS `Qwen3-VL-Reranker-8B` cross-encoder rerank. Cross-segment fusion produces one global top-10. Embedding is one-time, hash-cached, resumable, cost-guarded. On IONOS failure mode A degrades to C. +**Architecture:** Two modes behind one interface. Mode C = lexical baseline (tantivy BM25 over the ~90k Alpha-ID phrase corpus) + ClaML tag filter. Mode A = adds IONOS `bge-m3` semantic retrieval (brute-force cosine over precomputed embeddings) fused with lexical via Reciprocal Rank Fusion, then IONOS `Qwen3-VL-Reranker-8B` cross-encoder rerank. Cross-unit fusion (one unit = one extracted phrase) produces one global top-10. Embedding is one-time, hash-cached, resumable, cost-guarded. On IONOS failure mode A degrades to C. **Tech Stack:** Rust 1.94, `clap` (CLI), `tantivy` (lexical), `quick-xml` (ClaML streaming parse), `reqwest` blocking (IONOS HTTP), `serde`/`serde_json`/`toml`, `sha2` (cache keys), `rand` (seeded error injection). No ANN crate — brute-force exact cosine over 90k×1024 f32 is <50 ms and removes a native-build dependency (YAGNI). Spec: `docs/superpowers/specs/2026-05-18-alpha-id-icd-suggestion-design.md`. Data lives in gitignored `data/` and `icd-claml/` (already present). +> **Revision 2026-05-18 (Input-Vertrag), spec §1a:** The input is no longer a blank-line-segmented dictation but a list of **caller-extracted diagnostic phrases, one per non-empty line** (extraction is doctate's LLM, never this project). Evidence-driven: terse corpus strings need terse query input; whole-paragraph embedding dilutes the diagnosis (see spec §1a). **Scope of code change is deliberately minimal:** only Task 6 (`segment.rs`) changes — split on newlines instead of blank lines; function name `split_segments` and the `segment_idx`/`source_segments` fields are retained (a "segment" now denotes one extracted phrase). The pipeline core (Tasks 9/10/18) is unchanged. Eval Tasks 11 & 19 gain paraphrase injection + a hand-labeled phrase-list set + separated reporting of extraction-recall ceiling vs. matcher recall. Where this plan still says "dictation/blank lines/Befund segment", read "phrase list / newlines / extracted-phrase unit". + --- ## File Structure @@ -22,7 +24,7 @@ src/model.rs # core types: AlphaIdEntry, IcdMeta, Tags, Cand src/corpus.rs # parse Alpha-ID-SE -> Vec + helpers src/claml.rs # stream-parse ClaML XML -> HashMap src/normalize.rs # text normalization + abbreviation expansion -src/segment.rs # split dictation on blank lines -> Vec +src/segment.rs # split phrase list: one non-empty line -> one unit (rev 2026-05-18) src/lexical.rs # tantivy index build/load/query over Alpha-ID texts src/tags.rs # build Tags from IcdMeta+validity; Filter predicate src/fusion.rs # RRF within segment; cross-segment dedupe(max) @@ -703,7 +705,13 @@ git commit -m "feat: text normalization with medical abbreviation expansion" --- -### Task 6: Dictation segmentation +### Task 6: Phrase-list splitting (rev 2026-05-18, spec §1a) + +Input is a caller-extracted phrase list: **one non-empty (trimmed) line = +one retrieval unit**. Blank/whitespace-only lines are skipped — they are no +longer paragraph delimiters. Function name `split_segments` and the +`Vec` shape are retained (minimal-churn decision, spec §3 note); a +"segment" now denotes one extracted phrase. **Files:** - Modify: `src/segment.rs` @@ -715,23 +723,31 @@ git commit -m "feat: text normalization with medical abbreviation expansion" use alpha_id::segment::split_segments; #[test] -fn splits_on_blank_lines() { - let d = "Befund eins\nmit Zeile zwei\n\nBefund zwei\n\n\nBefund drei\n"; - let segs = split_segments(d); - assert_eq!(segs, vec![ - "Befund eins\nmit Zeile zwei".to_string(), - "Befund zwei".to_string(), - "Befund drei".to_string(), +fn one_unit_per_nonempty_line() { + let input = "arterielle Hypertonie\nTyp-2-Diabetes mit Polyneuropathie\ngemischte Hyperlipidämie"; + assert_eq!(split_segments(input), vec![ + "arterielle Hypertonie".to_string(), + "Typ-2-Diabetes mit Polyneuropathie".to_string(), + "gemischte Hyperlipidämie".to_string(), ]); } #[test] -fn single_segment_when_no_blank_line() { - assert_eq!(split_segments("nur ein Befund"), vec!["nur ein Befund".to_string()]); +fn blank_and_whitespace_lines_are_skipped_and_each_line_trimmed() { + let input = " \n\n Kreuzschmerz \n \nGonarthrose rechts\n\n"; + assert_eq!(split_segments(input), vec![ + "Kreuzschmerz".to_string(), + "Gonarthrose rechts".to_string(), + ]); } #[test] -fn empty_input_yields_no_segments() { +fn single_phrase_is_one_unit() { + assert_eq!(split_segments("nur eine Diagnose"), vec!["nur eine Diagnose".to_string()]); +} + +#[test] +fn empty_or_whitespace_only_input_yields_no_units() { assert!(split_segments(" \n\n").is_empty()); } ``` @@ -739,43 +755,36 @@ fn empty_input_yields_no_segments() { - [ ] **Step 2: Run test to verify it fails** Run: `cargo test --test segment_tests` -Expected: FAIL — `split_segments` not found. +Expected: FAIL — old blank-line-paragraph behavior joins lines / new test names absent. - [ ] **Step 3: Write minimal implementation** — `src/segment.rs` ```rust -/// Split a dictation into Befund segments on Markdown paragraph -/// breaks (one or more blank lines). Trims each segment; drops empties. -pub fn split_segments(dictation: &str) -> Vec { - let mut segments = Vec::new(); - let mut current: Vec<&str> = Vec::new(); - for line in dictation.lines() { - if line.trim().is_empty() { - if !current.is_empty() { - segments.push(current.join("\n").trim().to_string()); - current.clear(); - } - } else { - current.push(line); - } - } - if !current.is_empty() { - segments.push(current.join("\n").trim().to_string()); - } - segments.into_iter().filter(|s| !s.is_empty()).collect() +/// Split the caller-extracted phrase list into retrieval units: one +/// non-empty (trimmed) input line = one unit. Blank/whitespace-only +/// lines are skipped. Rev 2026-05-18 (spec §1a): input is extracted +/// diagnostic phrases, not a blank-line-segmented dictation. Name and +/// signature kept; a "segment" now denotes one extracted phrase. +pub fn split_segments(phrases: &str) -> Vec { + phrases + .lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .map(|l| l.to_string()) + .collect() } ``` - [ ] **Step 4: Run test to verify it passes** Run: `cargo test --test segment_tests` -Expected: PASS (3 tests). +Expected: PASS (4 tests). - [ ] **Step 5: Commit** ```bash git add src/segment.rs tests/segment_tests.rs -git commit -m "feat: dictation segmentation on blank lines" +git commit -m "refactor: input contract — one extracted phrase per line (spec §1a)" ``` --- @@ -1321,6 +1330,17 @@ git commit -m "feat: Mode C pipeline end-to-end (lexical + tags + fusion)" ### Task 11: Eval harness — error injection + Recall@k +> **Revision 2026-05-18 (spec §6):** Two changes layered on this task. +> (1) `inject_errors` case "umlaut" must produce the **ASCII digraph** +> (ä→ae, ö→oe, ü→ue, ß→ss), not delete the umlaut — the realistic +> transcription variant the umlaut fold (Task 5 / `normalize.rs`) handles; +> already implemented on `main` (commit `8348e33`). +> (2) Add a **paraphrase-injection** axis: a seeded transform that yields +> a synonym/word-order/brevity variant of the corpus phrase (not only the +> verbatim string + char noise), so the eval no longer tests the exact +> corpus byte-string (addresses the self-retrieval over-estimation, spec +> §6). Keep determinism-by-seed and the existing Recall@k tests. + **Files:** - Modify: `src/eval.rs` - Test: `tests/eval_tests.rs` @@ -2351,6 +2371,20 @@ git commit -m "feat: Mode A hybrid pipeline with graceful degradation to C" ### Task 19: Eval comparison C vs A + smoke against real IONOS +> **Revision 2026-05-18 (spec §6):** First C-vs-A run done — Mode C 0.520 +> vs A **0.995** BillableRecall@10 (200 cases, seed 42). Under the *old* +> dictation-paragraph contract that 0.995 was self-retrieval-inflated; +> under the new phrase-list contract (spec §1a) the short↔short regime is +> legitimately representative for noise/paraphrase robustness. Add to this +> task: (a) a small **hand-labeled set** of realistic *extracted phrase +> lists* (one phrase/line — the new input form) paired with the gold +> billable-ICD set, **plus the source dictation** stored alongside to +> document the extraction-recall ceiling; (b) the eval report must show +> **extraction-recall ceiling (dictation→phrases) vs. matcher recall +> (phrases→codes) separately**, never conflated. The hand-labeled set +> needs realistic example dictations from the user (still an open input, +> spec §9) — flag as BLOCKED-pending-user-data if absent, don't fabricate. + **Files:** - Modify: `src/bin/alpha_id.rs` (Eval arm prints both modes when `--mode both`) - Test: `tests/eval_compare_tests.rs` diff --git a/docs/superpowers/specs/2026-05-18-alpha-id-icd-suggestion-design.md b/docs/superpowers/specs/2026-05-18-alpha-id-icd-suggestion-design.md index 1746f1c..33ec46a 100644 --- a/docs/superpowers/specs/2026-05-18-alpha-id-icd-suggestion-design.md +++ b/docs/superpowers/specs/2026-05-18-alpha-id-icd-suggestion-design.md @@ -1,7 +1,7 @@ # Design-Spec: alpha-id — ICD-Code-Vorschläge aus Diktat **Datum:** 2026-05-18 -**Status:** Entwurf zur Freigabe (Brainstorming abgeschlossen) +**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) --- @@ -12,15 +12,30 @@ Aus einem (bereits transkribierten, fehlerbehafteten) ärztlichen Diktat eine ** 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**. +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 +### I/O-Vertrag (revidiert 2026-05-18) -- **Eingabe:** das *komplette* Diktat in *einem* Aufruf, durch **leere Zeilen (Markdown-Absätze)** in Befund-Segmente getrennt. Die Segmentierung erfolgt upstream durch doctates LLM; **dieses Projekt segmentiert nicht und enthält keinen generativen LLM**. -- **Ausgabe:** **eine** priorisierte Liste von max. 10 ICD-Codes **für das gesamte Diktat** — nicht pro Segment. Marker werden intern genutzt (fokussierte Retrieval-Einheiten), danach global fusioniert. +- **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 (2–4 Wörter, z. B. `I10.90 Arterielle Hypertonie`). Ein realer Befund-Absatz ist eine 15–25-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/`) @@ -50,17 +65,18 @@ Zwei Modi über Config-Schalter, **gleiche Schnittstelle**, im selben Eval-Harne ### Datenfluss ``` -Komplettes Diktat (Befunde durch Leerzeilen getrennt) +Eingabe = extrahierte Diagnose-Formulierungen (eine pro Zeile) │ - ▼ [1] Parser → Segmente (Split an Leerzeilen) - ▼ [2] Normalisierung → pro Segment: lowercasing, Medizin-Abkürzungen, - │ Tokenisierung (kein LLM) - ├─►[3a] Lexik (tantivy BM25/Trigramm) ┐ pro Segment: + ▼ [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 ~50–100 (RRF-Mix) vorab persistierter Vektorindex) [A; in C nur 3a] - ▼ [4] Reranker → Qwen3-VL-Reranker-8B: (Segmenttext, Alpha-ID-Text) + ▼ [4] Reranker → Qwen3-VL-Reranker-8B: (Formulierung, Alpha-ID-Text) │ paarweise → rerank-Score [nur A] - ▼ [5] Fusion → alle Segment-Kandidaten in EINE Liste; je + ▼ [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), @@ -108,6 +124,8 @@ 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. @@ -161,11 +179,13 @@ Reihenfolge im Staging: **C zuerst** (braucht kein Embedding, sofort lauffähig 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). -- **Generator:** sampelt gültige, abrechenbare Alpha-ID-Einträge → `(text, erwarteter ICD)`. Injiziert **deterministisch (Seed)** realistische Transkriptionsfehler: deutsche Medizin-Homophone, Wortgrenzen-Merge/Split, Zifferndreher, fehlende Umlaute/Diakritika, typische ASR-Verwechslungen. Konfigurierbare Fehlerrate, mehrere Varianten/Eintrag. -- **Plus** ein kleines, handgelabeltes Set realistischer Mehr-Befund-Diktate (leerzeilengetrennt) — die echte Eingabeform. -- **Metriken:** Recall@1/5/10 gesamt; **gesondert Recall@10 abrechenbarer Codes** (Leitzahl Ziel 1); Modus C vs. A nebeneinander; Latenz + IONOS-Calls/Kosten; Breiten-Maß (distinkte assoziierte Codes) für Ziel 2. +**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**. -„Taugt's" ist eine **Go/No-Go-Entscheidung des Nutzers** anhand der Eval-Tabelle: A vs. C bei Recall@10 abrechenbarer Codes auf dem fehlerinjizierten Gold-Set, abgewogen gegen Latenz und IONOS-Kosten. Eine feste numerische Schwelle wird hier bewusst **nicht** erfunden; sie wird nach dem ersten Eval-Lauf gemeinsam festgelegt. +- **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. --- @@ -176,7 +196,7 @@ 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|-] # Diktat → Top-10 +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] @@ -190,7 +210,8 @@ alpha-id eval --mode c|a --cases N --seed S [--gold PATH] ## 8. Abgrenzung (Out of Scope) - Generativer Chat-LLM in der Pipeline (Modus B). -- Segmentierung des Diktats (kommt fertig aus doctate). +- **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). @@ -201,5 +222,7 @@ alpha-id eval --mode c|a --cases N --seed S [--gold PATH] - 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. -- Das Repo ist derzeit **kein** Git-Repository; vor dem Commit dieses Specs ggf. `git init`. +- **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 ``) — 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).