feat(s31): deterministic .npy+jsonl vector store with fail-fast shape check

This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 06:23:22 +02:00
commit 6548828fe1
2 changed files with 160 additions and 0 deletions

View file

@ -13,6 +13,7 @@ Pattern: tests/test_verdicts.py (determinism) + tests/test_retrieval.py (fixture
from __future__ import annotations
import json
import subprocess
import sys
import types
@ -27,6 +28,8 @@ from portfolio_optimiser.semretrieval import (
HybridRanker,
StructuralRetriever,
cosine,
load_vector_store,
save_vector_store,
)
from portfolio_optimiser.verdicts import (
ProposalFeatures,
@ -267,3 +270,82 @@ def test_hybrid_ranker_at_weight_zero_equals_the_structural_ranking() -> None:
assert [v.id for v in hybrid.rank(_QUERY, store.verdicts, 3)] == [
v.id for v in structural.rank(_QUERY, store.verdicts, 3)
]
# --- SC3 / SC7: the vector store is byte-deterministic and fails fast on a row/line mismatch ---
def test_vector_store_is_byte_identical_regardless_of_insertion_order(tmp_path: Path) -> None:
"""SC3 — the store sorts by id before embedding, so the same verdicts written in a different
order must produce byte-identical artifacts. Pattern:
tests/test_ledger.py::test_save_is_byte_identical_regardless_of_order."""
store, _ = _store_with_true_match_and_decoys()
forward, reverse = tmp_path / "forward", tmp_path / "reverse"
save_vector_store(forward, store.verdicts, FakeEmbedder())
save_vector_store(reverse, list(reversed(store.verdicts)), FakeEmbedder())
assert (forward / "vectors.npy").read_bytes() == (reverse / "vectors.npy").read_bytes()
assert (forward / "vectors.jsonl").read_bytes() == (reverse / "vectors.jsonl").read_bytes()
def test_vector_store_round_trips_ids_sorted(tmp_path: Path) -> None:
store, _ = _store_with_true_match_and_decoys()
save_vector_store(tmp_path, store.verdicts, FakeEmbedder())
loaded = load_vector_store(tmp_path)
assert loaded is not None
ids, matrix = loaded
assert ids == sorted(v.id for v in store.verdicts)
assert matrix.shape == (len(ids), EMBED_DIM)
assert matrix.dtype == np.dtype("<f8")
embedder = FakeEmbedder()
by_id = {v.id: v for v in store.verdicts}
for row, verdict_id in enumerate(ids):
assert np.array_equal(matrix[row], embedder(by_id[verdict_id].proposal_features))
def test_vector_store_jsonl_is_one_sorted_object_per_line(tmp_path: Path) -> None:
"""The id sidecar follows the repo's deterministic on-disk idiom: sorted keys, LF endings,
trailing newline so a diff of two runs is empty rather than noisy."""
store, _ = _store_with_true_match_and_decoys()
save_vector_store(tmp_path, store.verdicts, FakeEmbedder())
raw = (tmp_path / "vectors.jsonl").read_bytes().decode("utf-8")
assert raw.endswith("\n")
assert "\r" not in raw
assert [json.loads(line) for line in raw.splitlines()] == [
{"id": v.id} for v in sorted(store.verdicts, key=lambda v: v.id)
]
def test_ranking_is_stable_over_twenty_repetitions() -> None:
"""The NFR asks for >=20 repetitions, not a single re-run: BLAS reduction order is the thing
that would make this drift, and it is pinned at module import."""
base, _ = _store_with_true_match_and_decoys()
ranker = HybridRanker(FakeEmbedder(), similarity)
first = [v.id for v in ranker.rank(_QUERY, base.verdicts, 3)]
for _ in range(20):
assert [v.id for v in ranker.rank(_QUERY, base.verdicts, 3)] == first
def test_missing_vector_store_loads_as_none(tmp_path: Path) -> None:
"""Tolerant on absence — the store is a rebuildable cache, so a caller with no store simply
degrades to structural ranking rather than crashing."""
assert load_vector_store(tmp_path / "never-written") is None
assert load_vector_store(tmp_path) is None # directory exists but holds no artifacts
def test_vector_store_row_line_mismatch_fails_fast(tmp_path: Path) -> None:
"""SC7 — a truncated sidecar would silently mis-map every vector to the wrong verdict id.
That must raise, not rank garbage (mirrors SavingsLedger.load's fail-fast posture)."""
store, _ = _store_with_true_match_and_decoys()
save_vector_store(tmp_path, store.verdicts, FakeEmbedder())
sidecar = tmp_path / "vectors.jsonl"
kept = sidecar.read_text(encoding="utf-8").splitlines()[:-1]
sidecar.write_text("".join(line + "\n" for line in kept), encoding="utf-8")
with pytest.raises(ValueError, match="row"):
load_vector_store(tmp_path)