feat(nous-real): cache de embeddings + write-through al CAS de arje

Cierra el ciclo del feedback: el modelo real (fastembed-allMiniLML6V2,
~1-50ms por archivo) era invocado ciegamente en cada re-cluster del
watcher. Ahora se cachea por sha256(bytes-vistos) + model_id, con
write-through al CAS de arje.

Pipeline en handle_file:
1. Lee primeros 8 KiB del archivo (igual que antes).
2. file_sha = ente_cas::sha256_of(buf) — hash de los bytes que el
   modelo *realmente* verá. Garantiza que un archivo creciendo mas
   alla de la ventana sin tocar la cabeza siga sirviendo cache hits.
3. Cache lookup -> HIT: respuesta en us, sin invocar fastembed.
4. MISS: ente_cas::store(&buf) (write-through, no-fatal si falla) ->
   backend.embed_one(text) -> cache.put(...).

Backend de cache: sled local en
$XDG_CACHE_HOME/brahman/nouser-nous-real-embed-cache.sled. Tree
versionado embed_cache_v1; el MODEL_ID viaja en la key, asi que
cambiar de modelo invalida el cache implicitamente. Override por env
NOUSER_NOUS_REAL_CACHE.

Encoding compacto: cada Vec<f32> se serializa como bytes little-endian
(4B por f32, sin overhead). Para 384-d son 1.5 KiB por entry. Decode
tolera bytes corruptos (longitud no-multiplo de 4 -> None, no panic).

Por que sled y no ente-cas directo: el CAS de arje es flat
sha256-keyed; la cache necesita un mapeo (file_sha, model_id) ->
embedding, no expresable como entry CAS. El write-through a CAS queda
como registro consultable + futura GC.

Mock NO se modifica — su embedding pseudo-32d es metadata-hashing puro,
sin costo. Cachearlo seria overhead.

Tests: 5 unitarios verdes (roundtrip, miss, model collision, content
collision, corrupted value). Stub mode (sin feature) sigue compilando
sin tocar cache.
This commit is contained in:
Sergio
2026-05-09 02:57:55 +00:00
parent 79d42aba28
commit b23ddf2980
6 changed files with 334 additions and 7 deletions
+49
View File
@@ -6,6 +6,55 @@ ratio/diff ver `git show <sha>`.
## 2026-05-09
### feat(nous-real): cache de embeddings + write-through al CAS de arje
Cierra el ciclo de la crítica del usuario: "Si un archivo no ha
cambiado su hash en el CAS, Nouser ni siquiera debería pedirle al
LLM que re-genere el embedding". El modelo real
(`fastembed-allMiniLML6V2-384d`, ~1-50ms por archivo) era invocado
ciegamente en cada re-cluster del watcher. Ahora se cachea por
`sha256(bytes-vistos) + model_id`.
Pipeline en `handle_file`:
1. Lee primeros 8 KiB (igual que antes).
2. `file_sha = ente_cas::sha256_of(buf)` — hash de los bytes que el
modelo *realmente* verá (no del archivo completo). Garantiza
que un archivo creciendo más allá de la ventana sin tocar la
cabeza siga sirviendo cache hits.
3. Cache lookup: HIT → respuesta en ~µs.
4. MISS → `ente_cas::store(&buf)` (write-through al CAS de arje,
no-fatal si falla) → `backend.embed_one(text)``cache.put(...)`.
Backend de cache: sled local en
`$XDG_CACHE_HOME/brahman/nouser-nous-real-embed-cache.sled`. Tree
versionado `embed_cache_v1`; el `MODEL_ID` viaja en la key, así que
cambiar de modelo invalida el cache implícitamente. Override por env
`NOUSER_NOUS_REAL_CACHE`.
Encoding compacto: cada `Vec<f32>` se serializa como bytes
little-endian (4B por f32, sin overhead). Para el modelo default
(384-d) son 1.5 KiB por entry. Decode tolera bytes corruptos
(longitud no-múltiplo de 4 → `None`, no panic).
Por qué sled y no `ente-cas` directo: el CAS de arje es flat
sha256-keyed; la cache necesita un mapeo `(file_sha, model_id) →
embedding`, no expresable como entry CAS. El write-through a CAS
queda como registro consultable + futura GC.
API:
- `EmbedCache::open()` → abre sled, idempotente.
- `EmbedCache::open_at(dir)` para tests.
- `EmbedCache::get(sha, model)``Option<Vec<f32>>`.
- `EmbedCache::put(sha, model, &[f32])` → no-fatal en error.
- `EmbedCache::len()` → contador para logs (best-effort).
Mock NO se modifica — su embedding pseudo-32d es metadata-hashing
puro, sin costo. Cachearlo sería overhead.
Tests: 5 unitarios (`roundtrip_returns_same_vector`, `miss_returns_none`,
`different_models_do_not_collide`, `different_content_different_keys`,
`corrupted_value_returns_none`). Verdes con `--features embeddings`;
stub mode (sin feature) sigue compilando sin tocar cache.
### chore(nakui): alinear `nakui-core` con `[workspace.package]` y deps compartidas
Cleanup de drift de convenciones: `nakui-core` era el único crate del
monorepo que mantenía `version = "0.1.0"` / `edition = "2021"` /