27e6a67388
Cycle-close tidy for the packed-versioned-index-bundle cycle (issues #3 + #4, both shipped and closed). Architect drift review over d83b10f..HEAD against the spec + glossary (no paths.design_ledger / project CLAUDE.md configured, so spec + glossary are the only references; the regression gate is empty — commands.regression: [] — so architect was the sole cycle-close gate). Architect verdict: drift_found, minor — no spec contract violated. Format, header schema, on-disk layout, and the load_packed_or_store validation order (n_rows → model → corpus hash) match the spec exactly; both #3 and #4 acceptance criteria are test-evidenced; no glossary term drift; the IndexStatus / degraded orthogonality is honored. Resolutions: - [medium] Missing positional-alignment test. The spec § Testing strategy promised a "row i == store.get(entries[i].text)" guard, and it had not shipped — the load-bearing invariant that licenses dropping the per-row SHA256 key (row i = corpus entry i) was unprotected. FIXED: added tests/pack_cli_tests.rs::cli_packed_rows_are_in_corpus_order, which packs a store with order-revealing orthogonal vectors via the CLI, reads the packed file back, and asserts the rows follow corpus-load order (swapped order fails the test). - [low] Stale IndexStatus doc comments still said "structurally valid/invalid" though the variants now also carry semantic model/corpus mismatch. FIXED: refreshed the Ok / Mismatch comments in src/model.rs. - [low] The `index --pack` writer logic lives inline in main rather than in the packed module, leaving its header construction outside library test reach. CARRY-ON: filed as backlog issue #5 (idea/debt) — behaviour is correct and CLI-test-covered; the refactor is a self-contained testability improvement, not forced into this tidy. - [note] No design ledger / project CLAUDE.md configured; drift is measurable only against spec + glossary. Recorded as an infrastructure observation. Regression: no scripts configured (no-op). Full suite green (68 tests, +1 for the alignment guard). cycle packed-versioned-bundle tidy (drift-clean). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
157 lines
6.6 KiB
Rust
157 lines
6.6 KiB
Rust
use alpha_id::model::{AlphaIdEntry, Config, IndexStatus};
|
|
use alpha_id::pipeline::load_packed_or_store;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
// Write a minimal config TOML pointing the corpus + index dir at temp paths.
|
|
// The `--pack` branch only reads `alpha_id_path` (corpus) and `index_dir`
|
|
// (store); `claml_path` etc. are present for parse but unused on this path.
|
|
fn write_config(dir: &Path, corpus: &Path, index_dir: &Path) -> std::path::PathBuf {
|
|
let toml = format!(
|
|
r#"ionos_base_url = "http://127.0.0.1:1"
|
|
token_path = "/nonexistent"
|
|
embed_model = "test/model"
|
|
rerank_model = "test/rerank"
|
|
alpha_id_path = "{corpus}"
|
|
claml_path = "/nonexistent.xml"
|
|
index_dir = "{index}"
|
|
pool_size = 10
|
|
top_k = 10
|
|
"#,
|
|
corpus = corpus.to_str().unwrap(),
|
|
index = index_dir.to_str().unwrap(),
|
|
);
|
|
let p = dir.join("config.toml");
|
|
std::fs::write(&p, toml).unwrap();
|
|
p
|
|
}
|
|
|
|
fn corpus_line(text: &str) -> String {
|
|
// Alpha-ID format: valid|alpha_id|primary|star|addon|primary2|orpha|text
|
|
format!("1|A0001|A00|||||{text}")
|
|
}
|
|
|
|
#[test]
|
|
fn pack_refuses_incomplete_store_exit_2() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let corpus = dir.path().join("corpus.txt");
|
|
std::fs::write(&corpus, format!("{}\n{}\n", corpus_line("alpha"), corpus_line("beta"))).unwrap();
|
|
let index_dir = dir.path().join("index");
|
|
std::fs::create_dir_all(&index_dir).unwrap();
|
|
let config = write_config(dir.path(), &corpus, &index_dir);
|
|
|
|
let out = Command::new(env!("CARGO_BIN_EXE_alpha-id"))
|
|
.args(["--config", config.to_str().unwrap(), "index", "--pack"])
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(out.status.code(), Some(2), "incomplete store must exit 2");
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(stderr.contains("store incomplete"), "stderr was: {stderr}");
|
|
}
|
|
|
|
#[test]
|
|
fn pack_writes_file_from_complete_store() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let corpus = dir.path().join("corpus.txt");
|
|
std::fs::write(&corpus, format!("{}\n{}\n", corpus_line("alpha"), corpus_line("beta"))).unwrap();
|
|
let index_dir = dir.path().join("index");
|
|
std::fs::create_dir_all(&index_dir).unwrap();
|
|
|
|
// Populate the per-file store for both entry texts so the pack succeeds.
|
|
let store = alpha_id::embed::EmbeddingStore::open(index_dir.to_str().unwrap(), "test/model").unwrap();
|
|
store.put("alpha", &[1.0f32, 0.0]).unwrap();
|
|
store.put("beta", &[0.0f32, 1.0]).unwrap();
|
|
|
|
let config = write_config(dir.path(), &corpus, &index_dir);
|
|
let out = Command::new(env!("CARGO_BIN_EXE_alpha-id"))
|
|
.args(["--config", config.to_str().unwrap(), "index", "--pack"])
|
|
.output()
|
|
.unwrap();
|
|
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
|
let packed = index_dir.join("packed.test_model.bin");
|
|
assert!(packed.exists(), "packed file should be written at {}", packed.display());
|
|
}
|
|
|
|
fn 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(),
|
|
}
|
|
}
|
|
|
|
/// End-to-end seam: a packed file produced by the `index --pack` CLI loads back
|
|
/// through the production `load_packed_or_store` path as `IndexStatus::Ok` with a
|
|
/// usable index, with no per-file store consulted. This pins the cross-boundary
|
|
/// round trip (CLI writer ↔ pipeline reader) that the two halves' unit tests
|
|
/// each cover only one side of.
|
|
#[test]
|
|
fn cli_packed_file_loads_back_as_ok_index() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let corpus = dir.path().join("corpus.txt");
|
|
std::fs::write(&corpus, format!("{}\n{}\n", corpus_line("alpha"), corpus_line("beta"))).unwrap();
|
|
let index_dir = dir.path().join("index");
|
|
std::fs::create_dir_all(&index_dir).unwrap();
|
|
|
|
let store = alpha_id::embed::EmbeddingStore::open(index_dir.to_str().unwrap(), "test/model").unwrap();
|
|
store.put("alpha", &[1.0f32, 0.0]).unwrap();
|
|
store.put("beta", &[0.0f32, 1.0]).unwrap();
|
|
|
|
let config = write_config(dir.path(), &corpus, &index_dir);
|
|
let out = Command::new(env!("CARGO_BIN_EXE_alpha-id"))
|
|
.args(["--config", config.to_str().unwrap(), "index", "--pack"])
|
|
.output()
|
|
.unwrap();
|
|
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
|
|
|
// Load the CLI-written packed file through the production path. The store
|
|
// directory is removed first to prove the index comes from the packed file
|
|
// alone, not the per-file store.
|
|
std::fs::remove_dir_all(index_dir.join("embeddings")).unwrap();
|
|
let mut cfg = Config::load(config.to_str().unwrap()).unwrap();
|
|
cfg.index_dir = index_dir.to_str().unwrap().to_string();
|
|
let entries = vec![entry("alpha"), entry("beta")];
|
|
let (vector, status) = load_packed_or_store(&cfg, &entries);
|
|
assert!(vector.is_some(), "packed file written by the CLI must yield an index");
|
|
assert_eq!(status, IndexStatus::Ok);
|
|
}
|
|
|
|
/// Positional alignment (spec § Testing strategy): the packed matrix stores one
|
|
/// row per corpus entry in corpus-load order, so row `i` is the embedding of
|
|
/// `entries[i].text`. The two entries carry order-revealing orthogonal vectors;
|
|
/// if packing did not preserve corpus order the rows would come back swapped.
|
|
#[test]
|
|
fn cli_packed_rows_are_in_corpus_order() {
|
|
use alpha_id::packed::read_packed;
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let corpus = dir.path().join("corpus.txt");
|
|
// alpha is line 1 (corpus entry 0), beta is line 2 (corpus entry 1).
|
|
std::fs::write(&corpus, format!("{}\n{}\n", corpus_line("alpha"), corpus_line("beta"))).unwrap();
|
|
let index_dir = dir.path().join("index");
|
|
std::fs::create_dir_all(&index_dir).unwrap();
|
|
|
|
let store = alpha_id::embed::EmbeddingStore::open(index_dir.to_str().unwrap(), "test/model").unwrap();
|
|
store.put("alpha", &[1.0f32, 0.0]).unwrap();
|
|
store.put("beta", &[0.0f32, 1.0]).unwrap();
|
|
|
|
let config = write_config(dir.path(), &corpus, &index_dir);
|
|
let out = Command::new(env!("CARGO_BIN_EXE_alpha-id"))
|
|
.args(["--config", config.to_str().unwrap(), "index", "--pack"])
|
|
.output()
|
|
.unwrap();
|
|
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
|
|
|
let (_header, rows) = read_packed(&index_dir.join("packed.test_model.bin")).unwrap();
|
|
// Row i is the embedding of corpus entry i: alpha→[1,0] at row 0, beta→[0,1] at row 1.
|
|
assert_eq!(
|
|
rows,
|
|
vec![vec![1.0f32, 0.0], vec![0.0f32, 1.0]],
|
|
"packed rows must follow corpus-load order"
|
|
);
|
|
}
|