Commit Graph

10 Commits

Author SHA1 Message Date
Brummel 43b1b6fc25 feat: packed single-file embedding index (closes #3)
The semantic index was ~90 896 per-entry files (index/embeddings/<safe-model>/
<sha256>.f32), a deployment liability: the file count — not the 356 MB — is
hostile to container layers, object storage, and Git-LFS, and Pipeline::load
opened every one individually. This packs a complete per-file store into one
self-describing matrix file loaded in a single read.

New src/packed.rs owns the format: 4-byte magic "AIPK", u32 LE format_version,
u32 LE header length, a JSON PackedHeader {n_rows, dim, embed_model,
corpus_sha256, corpus_name}, pad to a 4-byte boundary, then row-major
little-endian f32 payload in corpus order (row i = corpus entry i). write_packed
is atomic (temp+rename); read_packed rejects a wrong magic, a newer-than-
supported version, a malformed header, or a payload length inconsistent with
n_rows×dim. corpus_sha256 hashes the raw corpus file bytes.

Pipeline::load now calls a free fn load_packed_or_store(cfg, entries) that
prefers the packed file and falls back to today's all-or-nothing per-file store,
reporting which path it took via a new IndexStatus {Ok, Absent, Mismatch}
carried into Diagnostics. `alpha-id index --pack` writes the file from a
complete store (refusing an incomplete one with exit 2), recording corpus_sha256
+ embed_model in the header as the artifact-binding data.

Iteration boundary (deliberate): this is iteration 1 of the packed-versioned-
bundle cycle. load_packed_or_store validates the packed file STRUCTURALLY only
(magic/version/payload length via read_packed, plus n_rows == entries.len());
it does NOT yet compare the header's corpus_sha256/embed_model against the live
config, and IndexStatus::Mismatch fires only on structural corruption. The
load-time mismatch warning, the index= token on the diagnostics line, and the
Hybrid-degrade-names-reason wiring are iteration 2 (issue #4), which consumes
the binding data this commit writes. The per-file store is untouched — it stays
the build/resume path; the packed file is an additive serve-path artifact.

mmap was considered and rejected: the brute-force full-scan cosine search
touches every row per query, so memory-mapping yields no runtime benefit over a
single read and would add an unsafe dependency (spec § Out of scope).

Verification: cargo test green (61 tests; 9 new — 3 format round-trip/hash/bad-
magic, 4 load-path incl. packed-beats-store and structural n_rows mismatch, 2
--pack CLI incl. exit-2 refusal). An end-to-end seam test writes the packed
file via the CLI, removes the per-file store, and asserts the pipeline builds
its index from the packed file alone as IndexStatus::Ok (#3 acceptance b).

Spec: docs/specs/2026-05-31-packed-versioned-index-bundle.md
Plan: docs/plans/2026-06-01-packed-versioned-bundle-iter1.md

closes #3

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 00:42:08 +02:00
Brummel d83b10f939 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>
2026-05-31 19:15:08 +02:00
Brummel c9363c6675 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>
2026-05-31 19:03:51 +02:00
Brummel 752558629f fix: deterministic fusion tie-order + seeded random eval sampling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:40:08 +02:00
Brummel 16fd91fa68 polish: drop redundant Filter import; document equal eval metrics 2026-05-18 19:29:50 +02:00
Brummel 6179d89931 feat: C-vs-A eval comparison; go/no-go metric
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:24:26 +02:00
Brummel 2934bbc8f4 fix: --sample 0 no longer panics; avoid needless corpus clone 2026-05-18 19:06:56 +02:00
Brummel d08cd293a0 feat: cost-guarded resumable index build (sample smoke + full)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:00:47 +02:00
Brummel 41debc4f60 feat: CLI suggest/eval/index; Mode C baseline runnable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:31:35 +02:00
Brummel 70be6449db chore: scaffold alpha-id crate skeleton 2026-05-18 16:12:59 +02:00