Corpus semantic embedding + prove-local weight-hash cache (+ S5 on real KVM, verified) #34
Merged
dcharlot
merged 2 commits from 2026-07-20 19:51:33 -04:00
corpus-embedder-s5 into main 2026-07-20
sandbox-proof: weight-hash cache — stop re-BLAKE3-ing 13 GiB every run
prove-local streams the full weight file into a BLAKE3 to bind the proof
to the exact bytes. Correct, but on a 13 GiB model that is a ~13 GiB read
on every single run — pure waste when the file has not changed.
Add an opt-in cache keyed on the file's (size, mtime) fingerprint. A hit
returns the stored hash and skips the stream; any miss (absent entry,
changed size, changed mtime) re-hashes and updates the cache.
The honesty cost is stated plainly, because this is a proof input. The
cell binds to the BLAKE3 of the bytes; a cache that skips the re-hash
decides the bytes are unchanged *without reading them*, exactly as build
systems do — an unchanged (size, mtime) is taken as evidence the contents
are unchanged. That is a freshness heuristic, not a proof. So:
- The library default is `hash_cache: None` — full stream every run,
preserving the strong bytes-bound guarantee. Caching is something the
operator opts into knowingly.
- The cache is never a correctness dependency: an unreadable or corrupt
cache file degrades to a full hash, it never fails the proof.
- The prover is loud about which applied: it prints `[hashed]` or
`[cache hit: trusts size+mtime]` on every run, so a reader always
knows which guarantee held.
The CLI turns the cache on by default at a temp path (the ergonomics
live here, not in the library) so the every-run pain is solved out of the
box; `SANDBOX_WEIGHTS_HASH_CACHE=off` forces the full stream.
Verified on the real Devstral-Small-2-24B Q4_K_M (13.35 GiB): run 1 cold
= 15.72s wall, printed `[hashed]`; run 2 warm = 6.58s wall, printed
`[cache hit]` — the eliminated ~9s is exactly the 13 GiB BLAKE3 stream,
and both runs bind to the identical hash b3:60fc77b9fcc1…, so the proof
is unchanged. 4 tests: hit-matches-full-hash, miss-on-change,
no-cache-always-streams, corrupt-cache-degrades. Test temp dirs are
process+counter unique so they never collide under parallelism.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
jouleclaw-corpus: semantic embedding path — capture what a query asked, not just its shape
The corpus so far encoded receipt *structure* — intent class, retrieval
depth, counter honesty. Useful, but blind to meaning: two differently
worded questions of the same shape embedded identically. This adds the
semantic path.
The obstacle is real and is why this is not a one-liner: a receipt
records a BLAKE3 of its input, never the input. An embedder that
captures what a query *means* cannot read it off the receipt — it needs
the text, which only the deployment that logged both the receipts and
the queries can supply. So the type system now says so:
- `TextEmbedder` — embeds query text, the semantic counterpart to the
structural `Embedder`.
- `CorpusBuilder::build_with_text(&[(Receipt, String)])` — takes that
(receipt, query) join explicitly. The join is the caller's; it is
not silently verifiable here (the receipt hash is over a
normalisation this crate does not know).
- rows are `[semantic ‖ structural]` by default — meaning and shape
are complementary signals, so a row carries both; the
`with_append_structural(false)` toggle gives a pure-semantic vector.
Either way the width is constant across the corpus, as the trainer
requires.
The real embedder is behind the `candle` feature so the default build
stays light and offline: `CandleTextEmbedder` wraps the fleet's own
on-device BERT/BGE embedder (jouleclaw-embed-candle) — the same L2-tier
backend the cascade already uses, now feeding the corpus that trains the
router above it. The skip/negative semantics are unchanged: L1Lawful
still has no EOC analogue and is counted, not guessed.
Verified end to end with a real model, not a mock: MiniLM (384-dim) over
five real queries → a 399-dim composite corpus (384 semantic + 15
structural), one L1Lawful receipt skipped, and `train_logreg` +
`train_mf` both fit a router on the result. 19 tests (5 new for the text
path, driven by a deterministic mock so the logic stays offline-testable);
the real model runs in the `emit_corpus_semantic` example behind the
`candle` feature.
Co-Authored-By: Claude Opus 4.8 <[email protected]>