fix: normalize --chapters filter input to padded chapter numbers

The --chapters filter compared the raw CLI token by exact string
equality against the zero-padded chapter tags ("01".."22") that
the corpus carries (roman_to_padded, src/claml.rs). The CLI only
split and trimmed the input, so the intuitive forms — `--chapters 4`
or `--chapters E,I` — never matched any candidate and the suggestion
list came back empty, indistinguishable from "no clinical match".

Add a pure `normalize_chapters` helper that zero-pads bare 1-2 digit
chapter numbers to two digits and partitions the token list into
known chapters (1..=22) and unrecognised tokens. The Suggest arm now
feeds the padded set into the filter and emits a stderr warning for
each unrecognised token, so misuse is visible instead of silent.

`--chapters 4` and `--chapters 04` now behave identically. ICD
code-letter mapping (E -> chapter) is deliberately left out of scope:
under this contract a letter is simply an unrecognised token and
triggers the warning.

closes #1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 19:03:51 +02:00
parent 1fded48142
commit c9363c6675
2 changed files with 32 additions and 2 deletions
+9 -2
View File
@@ -1,4 +1,4 @@
use alpha_id::model::{Config, Filter, Mode};
use alpha_id::model::{normalize_chapters, Config, Filter, Mode};
use alpha_id::pipeline::Pipeline;
use clap::{Parser, Subcommand};
use std::io::Read;
@@ -126,9 +126,16 @@ fn main() {
Cmd::Suggest { input, mode, top, billable_only, valid_only, exclude_exotic, chapters, json } => {
let p = Pipeline::load(&cfg).expect("load");
let m: Mode = mode.parse().expect("mode");
let chapters = chapters.map(|c| {
let (padded, unknown) = normalize_chapters(&c);
for tok in &unknown {
eprintln!("warning: --chapters token '{tok}' matches no known chapter (01..22); ignored");
}
padded
});
let filter = Filter {
billable_only, valid_only, exclude_exotic,
chapters: chapters.map(|c| c.split(',').map(|s| s.trim().to_string()).collect()),
chapters,
};
let text = read_input(&input);
let res = p.suggest(&text, m, &filter, top);
+23
View File
@@ -42,6 +42,29 @@ pub struct Filter {
pub exclude_exotic: bool,
}
/// Normalize a raw `--chapters` token list to the zero-padded chapter-tag form
/// ("01".."22") that `Filter::accepts` compares against. Splits on ',', trims
/// each token, and classifies: a token that parses as one of the 22 known
/// ICD-10-GM chapters (1..=22) goes — zero-padded to two digits — into the first
/// returned vec (in input order); every other non-empty token goes verbatim
/// (trimmed) into the second (unrecognised) vec. Empty-after-trim tokens are
/// skipped. Both "4" and "04" yield "04".
pub fn normalize_chapters(raw: &str) -> (Vec<String>, Vec<String>) {
let mut known = Vec::new();
let mut unknown = Vec::new();
for tok in raw.split(',') {
let tok = tok.trim();
if tok.is_empty() {
continue;
}
match tok.parse::<u32>() {
Ok(n) if (1..=22).contains(&n) => known.push(format!("{n:02}")),
_ => unknown.push(tok.to_string()),
}
}
(known, unknown)
}
impl Filter {
pub fn accepts(&self, t: &Tags) -> bool {
if self.billable_only && !t.billable { return false; }