refactor: rename Mode variants C/A to Lexical/Hybrid (closes #2)
The retrieval-mode enum used opaque single letters `Mode { C, A }`;
neither the identifier nor the operator-facing `--mode a` token
conveyed what the mode does. Rename to self-describing names that map
directly onto the glossary definitions:
Mode::C -> Mode::Lexical (BM25 + tag filter, offline, no IONOS)
Mode::A -> Mode::Hybrid (lexical u semantic -> RRF -> rerank)
Decisions taken (the issue's open questions, resolved):
- CLI canonical tokens are now `lexical` / `hybrid`; the default is
`lexical`. The legacy `c` / `a` tokens stay accepted as
case-insensitive input aliases (zero cost; protects hand-typed
invocations). A unit test pins the alias contract.
- The diagnostics `mode` string (Debug-derived) now emits
"Lexical"/"Hybrid". Verified safe: no downstream consumer parses it.
doctate is the future integrator and embeds the library (the `Mode`
enum), not the JSON string (design spec §103).
- The reserved, out-of-scope generative tier (spec's "Mode B") keeps
conceptual room: a future `Mode::Generative` does not collide with
the retrieval-strategy names Lexical/Hybrid.
Surface: enum + FromStr, CLI defaults and `eval --mode both`
expansion, the private `collect_mode_c`/`collect_mode_a` methods (now
`collect_lexical`/`collect_hybrid`) and their comments, the two
pipeline mode test files (renamed), the eval CLI test, the glossary
(old letter forms demoted to Avoid lists), and the spec CLI synopsis.
closes #2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,3 +12,21 @@ fn cli_suggest_outputs_json_with_codes() {
|
||||
assert!(s.contains("\"icd_code\""));
|
||||
assert!(s.contains("E11"));
|
||||
}
|
||||
|
||||
/// The renamed canonical CLI token `--mode lexical` is accepted and the
|
||||
/// diagnostics line reports the new variant name `Lexical` (not the legacy `C`),
|
||||
/// confirming the enum rename reaches observable CLI output.
|
||||
#[test]
|
||||
fn cli_suggest_accepts_canonical_lexical_token() {
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_alpha-id"))
|
||||
.args(["suggest", "--mode", "lexical", "-"])
|
||||
.arg("--config").arg("config/default.toml")
|
||||
.env("ALPHA_ID_STDIN", "Diabetes mellitus Typ 2")
|
||||
.output().unwrap();
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(stdout.contains("E11"));
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("mode Lexical"),
|
||||
"diagnostics should report canonical mode name; stderr: {stderr}");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn eval_mode_c_runs_and_reports_billable_recall() {
|
||||
fn eval_lexical_runs_and_reports_billable_recall() {
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_alpha-id"))
|
||||
.args(["eval", "--mode", "c", "--cases", "30"])
|
||||
.args(["eval", "--mode", "lexical", "--cases", "30"])
|
||||
.arg("--config").arg("config/default.toml")
|
||||
.output().unwrap();
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
@@ -15,7 +15,7 @@ fn eval_mode_c_runs_and_reports_billable_recall() {
|
||||
fn eval_is_deterministic_same_seed() {
|
||||
let run = || {
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_alpha-id"))
|
||||
.args(["eval", "--mode", "c", "--cases", "40", "--seed", "42"])
|
||||
.args(["eval", "--mode", "lexical", "--cases", "40", "--seed", "42"])
|
||||
.arg("--config").arg("config/default.toml")
|
||||
.output().unwrap();
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
|
||||
+11
-2
@@ -15,6 +15,15 @@ fn filter_default_accepts_everything() {
|
||||
|
||||
#[test]
|
||||
fn mode_parses_from_str() {
|
||||
assert_eq!("a".parse::<Mode>().unwrap(), Mode::A);
|
||||
assert_eq!("c".parse::<Mode>().unwrap(), Mode::C);
|
||||
assert_eq!("lexical".parse::<Mode>().unwrap(), Mode::Lexical);
|
||||
assert_eq!("hybrid".parse::<Mode>().unwrap(), Mode::Hybrid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_letter_aliases_still_parse_to_new_variants() {
|
||||
// The legacy `c`/`a` CLI tokens remain accepted aliases (case-insensitive)
|
||||
// for the renamed canonical `lexical`/`hybrid` modes.
|
||||
assert_eq!("c".parse::<Mode>().unwrap(), Mode::Lexical);
|
||||
assert_eq!("a".parse::<Mode>().unwrap(), Mode::Hybrid);
|
||||
assert_eq!("A".parse::<Mode>().unwrap(), Mode::Hybrid);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ use alpha_id::pipeline::Pipeline;
|
||||
use alpha_id::model::{Config, Filter, Mode};
|
||||
|
||||
#[test]
|
||||
fn mode_a_degrades_to_c_when_ionos_unreachable() {
|
||||
fn hybrid_degrades_to_lexical_when_ionos_unreachable() {
|
||||
let mut cfg = Config::load("config/default.toml").unwrap();
|
||||
cfg.ionos_base_url = "http://127.0.0.1:1".into(); // refused
|
||||
let p = Pipeline::load(&cfg).unwrap();
|
||||
let res = p.suggest("Diabetes mellitus Typ 2", Mode::A, &Filter::default(), 10);
|
||||
let res = p.suggest("Diabetes mellitus Typ 2", Mode::Hybrid, &Filter::default(), 10);
|
||||
assert!(res.diagnostics.degraded); // fell back
|
||||
assert!(!res.suggestions.is_empty()); // still answered
|
||||
assert!(res.suggestions.iter().any(|s| s.icd_code.starts_with("E11")));
|
||||
@@ -6,23 +6,23 @@ fn cfg() -> Config {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_c_end_to_end_real_data() {
|
||||
fn lexical_end_to_end_real_data() {
|
||||
let p = Pipeline::load(&cfg()).unwrap();
|
||||
let dictation = "Patient mit Diabetes mellitus Typ 2\n\nArterielle Hypertonie";
|
||||
let res = p.suggest(dictation, Mode::C, &Filter::default(), 10);
|
||||
let res = p.suggest(dictation, Mode::Lexical, &Filter::default(), 10);
|
||||
assert!(!res.suggestions.is_empty());
|
||||
assert!(res.suggestions.len() <= 10);
|
||||
assert!(res.suggestions.iter().any(|s| s.icd_code.starts_with("E11")));
|
||||
assert!(res.suggestions.iter().any(|s| s.icd_code.starts_with("I10")));
|
||||
assert_eq!(res.diagnostics.segment_count, 2);
|
||||
assert_eq!(res.diagnostics.mode, "C");
|
||||
assert_eq!(res.diagnostics.mode, "Lexical");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn billable_only_filter_excludes_three_digit_v_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("Cholera", Mode::C, &f, 10);
|
||||
let res = p.suggest("Cholera", Mode::Lexical, &f, 10);
|
||||
assert!(res.suggestions.iter().all(|s| s.tags.billable && s.tags.valid));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ fn billable_only_filter_excludes_three_digit_v_codes() {
|
||||
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);
|
||||
let res = p.suggest("Diabetes mellitus Typ 2", Mode::Lexical, &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<_>>());
|
||||
Reference in New Issue
Block a user