refactor: move the index --pack writer into the packed module (closes #5)
The `alpha-id index --pack` writer logic — store-completeness check, row
collection in corpus order, header derivation (dim/corpus_name/corpus_sha256/
n_rows), and the write — lived inline in main, leaving the header construction
outside library test reach (covered only end-to-end via the pack CLI tests).
This extracts it into packed::pack_from_store, returning
PackOutcome { Packed(PackedHeader), Incomplete { cached, total } }. main's pack
branch now just loads entries, calls the fn, and matches the outcome onto the
same stderr messages and exit code 2 as before.
Behaviour is unchanged: the stderr lines ("store incomplete: N of M entries
embedded; run `index --full --confirm` first" and "packed N rows × D dims →
path") and the exit-2 refusal are byte-identical, and the four existing pack
CLI tests (pack_refuses_incomplete_store_exit_2, pack_writes_file_from_complete_store,
cli_packed_file_loads_back_as_ok_index, cli_packed_rows_are_in_corpus_order)
stay green unchanged as the behaviour-parity guard. Two new unit tests now
exercise the header construction directly without spawning the binary: a
complete store yields PackOutcome::Packed with the expected n_rows/dim/
embed_model/corpus_sha256 and writes the file; an incomplete store yields
PackOutcome::Incomplete { cached, total } and writes nothing.
Minor improvement folded in: the old inline `store.get(...).expect("cached
vector")` is now a propagated AppError inside pack_from_store rather than a
panic (the branch stays unreachable — the complete-store precondition is
checked first — so parity holds).
This is the debt tidy the cycle-close audit deferred (architect [low] finding).
87 tests green.
closes #5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
use alpha_id::packed::{corpus_sha256, read_packed, write_packed, PackedHeader};
|
||||
use std::io::Write;
|
||||
|
||||
use alpha_id::embed::EmbeddingStore;
|
||||
use alpha_id::model::AlphaIdEntry;
|
||||
use alpha_id::packed::{pack_from_store, packed_path, PackOutcome};
|
||||
|
||||
fn header(n_rows: u32, dim: u32) -> PackedHeader {
|
||||
PackedHeader {
|
||||
n_rows,
|
||||
@@ -44,3 +48,79 @@ fn read_packed_rejects_bad_magic() {
|
||||
f.write_all(b"XXXX____________").unwrap();
|
||||
assert!(read_packed(&path).is_err());
|
||||
}
|
||||
|
||||
fn mk_entry(text: &str) -> AlphaIdEntry {
|
||||
AlphaIdEntry {
|
||||
alpha_id: "A0001".to_string(),
|
||||
valid: true,
|
||||
icd_primary: "A00".to_string(),
|
||||
icd_star: String::new(),
|
||||
icd_addon: String::new(),
|
||||
icd_primary2: String::new(),
|
||||
orpha: String::new(),
|
||||
text: text.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_from_store_complete_builds_header_and_writes_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let index_dir = dir.path().join("index");
|
||||
std::fs::create_dir_all(&index_dir).unwrap();
|
||||
let corpus = dir.path().join("corpus.txt");
|
||||
std::fs::write(&corpus, b"corpus bytes").unwrap();
|
||||
|
||||
let store = EmbeddingStore::open(index_dir.to_str().unwrap(), "test/model").unwrap();
|
||||
store.put("alpha", &[1.0f32, 0.0, 0.0]).unwrap();
|
||||
store.put("beta", &[0.0f32, 1.0, 0.0]).unwrap();
|
||||
let entries = vec![mk_entry("alpha"), mk_entry("beta")];
|
||||
|
||||
let outcome = pack_from_store(
|
||||
index_dir.to_str().unwrap(),
|
||||
"test/model",
|
||||
corpus.to_str().unwrap(),
|
||||
&entries,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
match outcome {
|
||||
PackOutcome::Packed(h) => {
|
||||
assert_eq!(h.n_rows, 2);
|
||||
assert_eq!(h.dim, 3);
|
||||
assert_eq!(h.embed_model, "test/model");
|
||||
assert_eq!(
|
||||
h.corpus_sha256,
|
||||
alpha_id::packed::corpus_sha256(corpus.to_str().unwrap()).unwrap()
|
||||
);
|
||||
}
|
||||
PackOutcome::Incomplete { .. } => panic!("a complete store must pack"),
|
||||
}
|
||||
assert!(packed_path(index_dir.to_str().unwrap(), "test/model").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_from_store_incomplete_reports_counts_and_writes_nothing() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let index_dir = dir.path().join("index");
|
||||
std::fs::create_dir_all(&index_dir).unwrap();
|
||||
let corpus = dir.path().join("corpus.txt");
|
||||
std::fs::write(&corpus, b"corpus bytes").unwrap();
|
||||
|
||||
let store = EmbeddingStore::open(index_dir.to_str().unwrap(), "test/model").unwrap();
|
||||
store.put("alpha", &[1.0f32, 0.0]).unwrap(); // only 1 of 2 entries cached
|
||||
let entries = vec![mk_entry("alpha"), mk_entry("beta")];
|
||||
|
||||
let outcome = pack_from_store(
|
||||
index_dir.to_str().unwrap(),
|
||||
"test/model",
|
||||
corpus.to_str().unwrap(),
|
||||
&entries,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(outcome, PackOutcome::Incomplete { cached: 1, total: 2 }));
|
||||
assert!(
|
||||
!packed_path(index_dir.to_str().unwrap(), "test/model").exists(),
|
||||
"an incomplete store must not write a packed file"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user