docs: spec/plan revision — ClaML Modifier expansion (§3a) + eval-credibility notes
Documents the corpus↔ClaML 5th-digit gap, the decided Modifier/ ModifierClass expansion fix (Goal 1), and the deterministic / seeded-sample eval corrections from the final review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1084,8 +1084,81 @@ git commit -m "feat: RRF and cross-segment max-score dedupe"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Task 9b: ClaML Modifier/ModifierClass expansion (Revision 2026-05-18, spec §3a)
|
||||||
|
|
||||||
|
**Why:** ~26.5% of valid Alpha-ID primary codes are 6-char 5th-digit codes (e.g. `E11.72`, `I10.91`). ClaML models the 4th/5th digit of many groups (incl. all E10–E14 diabetes) only via `<Modifier>`/`<ModifierClass>`, not as `<Class kind="category">`. Task 4's `claml::load` returns no entry for these, so billability/description are wrong (fall back to the 3-digit `Para295=V` root) — `--billable-only` silently drops ~1,193 distinct billable codes, breaking Goal 1. Decided resolution: expand modifiers correctly from the ClaML file alone.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/claml.rs` (extend `load` to also expand subclassification)
|
||||||
|
- Test: `tests/claml_modifier_tests.rs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing test** — `tests/claml_modifier_tests.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use alpha_id::claml;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expands_diabetes_5th_digit_codes_as_billable() {
|
||||||
|
let map = claml::load("icd-claml/Klassifikationsdateien/icd10gm2026syst_claml_20250912.xml").unwrap();
|
||||||
|
// E11.x 5th-digit codes are NOT explicit <Class>; must be synthesized.
|
||||||
|
let e1190 = map.get("E11.90").expect("E11.90 synthesized from ModifierClass");
|
||||||
|
assert_eq!(e1190.para295, "P", "terminal 5th-digit diabetes code must be billable");
|
||||||
|
assert!(e1190.description.to_lowercase().contains("diabetes"),
|
||||||
|
"composed description from parent + modifier labels: {:?}", e1190.description);
|
||||||
|
let e1172 = map.get("E11.72").expect("E11.72 synthesized");
|
||||||
|
assert_eq!(e1172.para295, "P");
|
||||||
|
// Explicit <Class> entries still win and are unchanged.
|
||||||
|
let a010 = map.get("A01.0").expect("A01.0 explicit");
|
||||||
|
assert_eq!(a010.para295, "P");
|
||||||
|
assert!(a010.description.contains("Typhus abdominalis"));
|
||||||
|
// The bare 3-digit root stays non-billable (V).
|
||||||
|
assert_eq!(map.get("E11").expect("E11 root").para295, "V");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run** `cargo test --test claml_modifier_tests` → FAIL (`E11.90` absent).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the ClaML subclassification expansion in `src/claml.rs`.**
|
||||||
|
|
||||||
|
This is the standard ClaML modifier algorithm. There is no verbatim code block here because the exact element/attribute handling must be derived against the real XML; the implementer (capable model) must:
|
||||||
|
- During the stream parse, additionally collect: for each `<Class kind="category">`, its ordered `<ModifiedBy code=… all=…>` references; the full set of `<Modifier code=…>` (each with its `<SubClass code=".X"/>` children) and `<ModifierClass code=… modifier=…>` (each with its preferred `<Rubric><Label>`, its `<SuperClass>`, and its own optional `<ModifiedBy>` that chains the next digit).
|
||||||
|
- After the main parse, for every `<Class kind="category">` that carries `ModifiedBy` references, generate the terminal code combinations by applying the first modifier (4th digit) then, per the resulting ModifierClass's own `ModifiedBy` (or the Class's second `ModifiedBy`), the next modifier (5th digit) — i.e. walk the modifier chain to terminal leaves. The combined code key is `parentcode` + modifierclass codes concatenated in ICD order (e.g. `E11` + `.9` + `0` → `E11.90`; `E11` + `.7` + `2` → `E11.72`). Match the exact concatenation to the corpus code format (verify against `data/...txt` field 3 for a few E11/I10 codes — they look like `E11.90`, `E11.72`, `I10.91`).
|
||||||
|
- Synthesized `IcdMeta`: `code` = combined; `para295`/`para301` = **"P"** for terminal leaves of a modifier-expanded group (the bare root is "V"; terminal subdivision is the billable primary — verified in the investigation that these groups have no per-ModifierClass Para295, and the corpus treats the 5th-digit code as the codeable primary); `description` = parent preferred label + " - " + 4th-digit ModifierClass label + " - " + 5th-digit ModifierClass label (trim/join sensibly, skip empty); `chapter` = parent chapter; `exotic`/`ifsg`/`content` inherited from parent.
|
||||||
|
- **Additive only:** if a key already exists from an explicit `<Class>`, do NOT overwrite it (explicit wins).
|
||||||
|
- Only combine modifiers actually referenced by that Class (and chained ModifierClass `ModifiedBy`). No cartesian product across unrelated modifiers.
|
||||||
|
- Keep `load`'s signature and the existing explicit-Class behavior (Tasks 4 tests must still pass).
|
||||||
|
|
||||||
|
The implementer should inspect the real XML (`grep`/`xmllint`) for `Modifier`, `ModifierClass`, `ModifiedBy`, the `S04E10_4`/`S04E10_5` diabetes modifiers, and a couple of corpus E11/I10 codes, and derive the precise concatenation/label-composition that makes the Step-1 assertions pass honestly. Do NOT hardcode `E11.*` — the expansion must be generic over all ClaML modifier groups.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run** `cargo test --test claml_modifier_tests` and `cargo test --test claml_tests` → both PASS (new expansion correct; original explicit-Class tests incl. `mixed_content_label_not_truncated` unchanged & green). Then full `cargo test` + `cargo clippy --lib` clean.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/claml.rs tests/claml_modifier_tests.rs
|
||||||
|
git commit -m "feat: expand ClaML Modifier/ModifierClass into terminal 5th-digit codes"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Task 10: Pipeline — Mode C
|
### Task 10: Pipeline — Mode C
|
||||||
|
|
||||||
|
> **Revision 2026-05-18:** With Task 9b, `claml::load` returns the synthesized 5th-digit codes. The ad-hoc `meta_lookup` strip-zero/3-char-root fallback added during the first Task 10 implementation must be **removed** (it caused the misclassification). Use a direct `self.meta.get(code)` with at most a single exact-then-5-char-parent fallback (no 3-char-root fallback, since that is exactly what corrupted billability). Additionally **add a billable regression test** to `tests/pipeline_mode_c_tests.rs`:
|
||||||
|
>
|
||||||
|
> ```rust
|
||||||
|
> #[test]
|
||||||
|
> fn billable_only_keeps_diabetes_5th_digit_codes() {
|
||||||
|
> let p = Pipeline::load(&cfg()).unwrap();
|
||||||
|
> let f = Filter { billable_only: true, valid_only: true, chapters: None, exclude_exotic: false };
|
||||||
|
> let res = p.suggest("Diabetes mellitus Typ 2", Mode::C, &f, 10);
|
||||||
|
> assert!(res.suggestions.iter().any(|s| s.icd_code.starts_with("E11") && s.tags.billable),
|
||||||
|
> "billable E11.x must survive --billable-only; got {:?}",
|
||||||
|
> res.suggestions.iter().map(|s| (&s.icd_code, s.tags.billable)).collect::<Vec<_>>());
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> The original two Task 10 tests remain. Re-run full suite + clippy. Re-review after the revision.
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `src/pipeline.rs`
|
- Modify: `src/pipeline.rs`
|
||||||
- Test: `tests/pipeline_mode_c_tests.rs`
|
- Test: `tests/pipeline_mode_c_tests.rs`
|
||||||
|
|||||||
@@ -115,6 +115,22 @@ Config { mode: C|A, top_k, pool_size, filter, ionos }
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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 E10–E14** — **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)
|
## 4. Einmaliges, kostensicheres Embedding (kritische Anforderung)
|
||||||
|
|
||||||
Das Embedden von ~90k Texten via IONOS kostet Geld und soll **genau einmal** erfolgen. Maßnahmen:
|
Das Embedden von ~90k Texten via IONOS kostet Geld und soll **genau einmal** erfolgen. Maßnahmen:
|
||||||
@@ -186,3 +202,4 @@ 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.
|
- 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.
|
- 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`.
|
- Das Repo ist derzeit **kein** Git-Repository; vor dem Commit dieses Specs ggf. `git init`.
|
||||||
|
- **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.
|
||||||
|
|||||||
Reference in New Issue
Block a user