Files
rar-autopass/docs/prd/prd-5.21-embedding-cache-sqlite.md
Claude Agent 705ad030fe feat(embeddings): cache persistent de vectori in SQLite + warmup in fundal
Vectorii corpusului k-NN persista in tabela embedding_cache (PK model+text_hash,
blob float32 LE); la warmup se vectorizeaza doar textele lipsa din cache, deci
restartul cu corpus neschimbat nu mai plateste ~1-2 min de embed (embed=0).

- app/embedding_cache.py: serializare array('f'), load/save/purge chunk 500 cu
  BEGIN/COMMIT explicit (conexiuni autocommit), validare dimensiune la scriere
  si citire, orchestrare sync_corpus_vectors cu embed_fn injectat
- index_corpus(vectors=): vectori precalculati cu validare aliniere; mismatch
  -> fallback embed complet
- ensure_embeddings_corpus: warmup in thread la startup (block=True), calea de
  request ne-blocanta (acquire non-blocking pe lock; warmup in curs -> return
  imediat); purjare orfane + modele vechi doar dupa indexare reusita
- log warmup: cache=N embed=M in Xs
- 31 teste noi (cold/warm/incremental, model schimbat, concurenta, ranking
  exact, echivalenta float32); suita completa 1596 passed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:57:49 +00:00

28 KiB

PRD: Cache persistent de vectori embeddings in SQLite

1. Introducere

Vectorii corpusului k-NN (17.181 exemple etichetate din mapping_suggestions) traiesc doar in RAM si se recalculeaza integral la fiecare pornire a API-ului (~1-2 minute de CPU), desi textele nu s-au schimbat. Warmup-ul in fundal (deja implementat) tine pagina Mapari rapida, dar lasa o fereastra de 1-2 minute dupa restart in care sugestiile embeddings lipsesc. Acest feature persista vectorii intr-o tabela SQLite si vectorizeaza incremental doar textele noi/modificate, reducand fereastra la doar incarcarea modelului (~10-20s).

2. Obiective

Obiectiv Principal

  • Elimina re-vectorizarea integrala a corpusului la fiecare restart: vectorii se calculeaza o singura data si se refolosesc.

Obiective Secundare

  • Fereastra fara sugestii embeddings dupa restart scade de la ~1-2 minute la ~10-20s (doar incarcarea modelului ONNX).
  • Cresterea corpusului SILVER nu mai scumpeste restarturile (cost proportional doar cu randurile NOI).
  • Cache reconstruibil oricand (DELETE FROM embedding_cache + restart), fara alte efecte.

Metrici de Succes

  • La corpus neschimbat: zero apeluri embed() pe corpus la startup; indexarea din cache < 5s.
  • La N randuri noi in corpus: exact N texte vectorizate la urmatorul warmup.
  • Sugestiile embeddings (sursa "similaritate") functioneaza identic inainte/dupa (acelasi cod sugerat pentru acelasi query).

3. User Stories

US-001: Tabela embedding_cache in schema

Ca sistem Vreau o tabela dedicata pentru vectorii corpusului Pentru ca vectorii sa supravietuiasca restartului fara sa ating schema mapping_suggestions.

Acceptance Criteria:

  • app/schema.sql contine tabela embedding_cache cu: text_hash TEXT NOT NULL (SHA-256 al textului normalizat), model TEXT NOT NULL (numele modelului fastembed), vector BLOB NOT NULL (float32 little-endian, 384 valori), created_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (model, text_hash).
  • init_db() creeaza tabela idempotent (CREATE TABLE IF NOT EXISTS), pe baza de date existenta si pe una noua.
  • python3 -m pytest -q trece.

US-002: Serializare + acces cache (functii pure)

Ca developer Vreau functii de citire/scriere vectori in cache Pentru ca logica de (de)serializare sa fie testabila izolat.

Acceptance Criteria:

  • Functii in app/embeddings.py (sau modul nou app/embedding_cache.py): vector_to_blob(list[float]) -> bytes si blob_to_vector(bytes) -> list[float] (float32 LE, array('f') din stdlib, fara dependinta noua).
  • load_cached_vectors(conn, model, hashes) -> dict[hash, vector] — un singur SELECT (chunk-uit la limita de parametri SQLite), intoarce doar hash-urile gasite.
  • save_vectors(conn, model, items: list[(hash, vector)]) — INSERT OR REPLACE batch, commit la final.
  • Round-trip exact: blob_to_vector(vector_to_blob(v)) == pytest.approx(v) pentru vectori de 384 float.
  • python3 -m pytest -q trece.

US-003: ensure_embeddings_corpus foloseste cache-ul

Ca operator Vreau ca warmup-ul sa vectorizeze doar textele lipsa din cache Pentru ca restartul sa nu mai coste 1-2 minute de CPU.

Acceptance Criteria:

  • ensure_embeddings_corpus(conn, block=True): calculeaza hash per denumire_normalizata, citeste vectorii existenti din embedding_cache pentru modelul curent, apeleaza embed() DOAR pe textele fara vector in cache, salveaza vectorii noi in cache, apoi indexeaza corpusul complet (cache + noi) in EmbeddingEngine.
  • EmbeddingEngine.index_corpus accepta vectori precalculati (parametru nou vectors sau metoda index_corpus_precomputed) fara sa apeleze backend-ul pentru ei; comportamentul existent (fara vectori -> embed tot) ramane pentru compatibilitate.
  • Semantica semnaturii corpusului ramane neschimbata: acelasi corpus SILVER -> nu se reindexeaza; corpus schimbat -> reindexare (dar embed doar pe diferenta).
  • Calea de request (block=False) ramane non-blocanta, neatinsa.
  • Degradare gratioasa pastrata: orice eroare de cache (tabela lipsa, blob corupt) -> fallback pe embed complet, fara exceptie propagata.
  • Blob corupt (lungime gresita) e ignorat si re-vectorizat, nu crapa indexarea.
  • python3 -m pytest -q trece.

US-004: Invalidare pe model + curatare orfane

Ca sistem Vreau cache-ul legat de numele modelului si curatat de intrari moarte Pentru ca schimbarea modelului sa nu serveasca vectori incompatibili, iar cache-ul sa nu creasca nelimitat.

Acceptance Criteria:

  • Cheia de cache include model (= FASTEMBED_MODEL): la schimbarea constantei, vectorii vechi NU sunt folositi (se re-vectorizeaza sub noul model, intrarile vechi raman inerte).
  • La finalul unei indexari reusite, intrarile embedding_cache ale modelului curent cu text_hash care nu mai exista in corpus sunt sterse (DELETE orfane).
  • Test: schimbarea modelului (mock cu nume diferit) -> zero hit-uri din cache; stergerea unui rand din corpus -> intrarea orfana dispare dupa reindexare.
  • python3 -m pytest -q trece.

US-005: Teste de flux cold/warm start

Ca developer Vreau teste care fixeaza comportamentul incremental Pentru ca regresiile de performanta la startup sa fie prinse de suita.

Acceptance Criteria:

  • Test cold start: cache gol + corpus de N randuri -> backend mock primeste exact N texte, cache-ul contine N intrari dupa.
  • Test warm start: al doilea proces (engine nou, acelasi conn) -> backend mock primeste ZERO texte de corpus, has_corpus() True, suggest_nearest functioneaza cu vectorii din cache.
  • Test incremental: se adauga 1 rand in mapping_suggestions -> backend mock primeste exact 1 text la reindexare.
  • Testele folosesc backend mock (fara fastembed real), ruleaza in suita implicita (fara marker live).
  • python3 -m pytest -q trece.

US-006: Verificare end-to-end pe instanta reala

Ca operator Vreau confirmarea pe serverul real ca restartul e ieftin Pentru ca metrica de succes sa fie masurata, nu presupusa.

Acceptance Criteria:

  • Log la finalul warmup-ului: numar vectori din cache / numar vectori calculati / durata totala (ex. embeddings: warmup ok cache=17181 embed=0 in 12.3s).
  • Verify in browser: dupa ./start.sh stop && ./start.sh test both --send, primul restart populeaza cache-ul; la al DOILEA restart, log-ul arata embed=0 si sugestia "similaritate" apare in preview-ul regulilor text in < 30s de la pornire.
  • Dimensiunea bazei creste cu ~26-30MB (17k x 384 float32) — documentat in log/PRD, acceptat.

4. Cerinte Functionale

  1. [REQ-001] Sistemul trebuie sa persiste vectorii corpusului SILVER in tabela embedding_cache, cheie (model, text_hash).
  2. [REQ-002] La warmup, sistemul trebuie sa vectorizeze DOAR textele fara intrare in cache pentru modelul curent.
  3. [REQ-003] Cand corpusul SILVER se schimba (semnatura diferita), sistemul reindexeaza folosind cache-ul si vectorizeaza doar diferenta.
  4. [REQ-004] Cand modelul se schimba, cache-ul vechi nu este folosit; vectorii se recalculeaza integral sub noua cheie de model.
  5. [REQ-005] Orice eroare legata de cache degradeaza la comportamentul actual (embed complet), fara a bloca API-ul sau a arunca in request.
  6. [REQ-006] Intrarile orfane (texte disparute din corpus) se curata la reindexare.
  7. [REQ-007] Vectorii query (textul operatiei/pattern-ului cautat) NU se cacheaza — se calculeaza la cerere (milisecunde).

5. Non-Goals (Ce NU facem)

  • NU introducem vector store extern (sqlite-vec, Qdrant, FAISS) — cosine in Python peste 17k vectori ramane suficient.
  • NU cacheam embeddings pentru query-uri (texte noi cautate de utilizatori).
  • NU modificam pipeline-ul de etichetare (tools/mapare-llm/) sau formatul seedului app/data/operatii-etichetate.json.
  • NU vectorizam GOLD (shared_mappings) sau nomenclatorul — sursa corpusului ramane exclusiv mapping_suggestions.
  • NU schimbam modelul de embeddings, pragul EMB_MIN_SIMILARITATE sau precedenta GOLD > SILVER > embeddings.
  • NU eliminam warmup-ul din fundal — ramane, doar devine ieftin.

6. Consideratii Tehnice

Stack/Tehnologii

  • SQLite existent (acelasi fisier data/autopass.db, WAL), stdlib array/hashlib — zero dependinte noi.
  • fastembed/ONNX ramane sursa vectorilor la miss.

Patterns de Urmat

  • Degradare gratioasa ca in ensure_embeddings_corpus/enrich_suggestions (except -> pass, corpus gol e acceptabil).
  • Schema idempotenta in app/schema.sql + init_db() (CREATE IF NOT EXISTS), ca restul tabelelor.
  • Semnatura corpusului (_corpus_signature_silver) ramane mecanismul de decizie "reindexez sau nu".
  • Backend injectabil in EmbeddingEngine pentru teste (mock, fara model real).

Dependente

  • Warmup-ul din fundal existent (app/main.py::_warmup_embeddings, ensure_embeddings_corpus(block=True)).
  • mapping_suggestions populat la init_db din seedul comis (app/operatii_seed.py).

Riscuri Tehnice

  • Scriere concurenta API/worker pe SQLite: scrierile de cache se fac in tranzactii scurte, batch, doar din thread-ul de warmup al API-ului (worker-ul NU incarca modelul — invariant existent).
  • Blob corupt/lungime gresita: tratat ca miss (re-embed), nu ca eroare.
  • Crestere DB ~26-30MB: acceptata; curatarea orfanelor previne cresterea nelimitata.
  • Hash-ul textului trebuie calculat pe denumire_normalizata EXACT cum intra in index_corpus — altfel miss permanent si cache inutil (test dedicat in US-005).

7. Consideratii UI/UX

Fara schimbari de UI. Efect indirect: sugestia "similaritate" (editor mapari + preview reguli text) devine disponibila la ~10-20s dupa restart in loc de 1-2 minute.

8. Success Metrics

  • Apeluri embed() pe corpus la restart cu corpus neschimbat: 0 (log embed=0).
  • Durata warmup la warm start: < 30s total (dominata de incarcarea modelului), fata de ~1-2 min acum.
  • Zero regresii in suita: python3 -m pytest -q verde.

9. Open Questions

  • Vrem o comanda CLI de administrare (python3 -m tools.embcache rebuild|clear|stats)? DECIS (/autoplan): manual (DELETE FROM embedding_cache) suficient in v1; CLI deferat la TODOS.md.
  • Purjarea orfanelor sterge si intrarile modelelor VECHI? DECIS (/autoplan): DA, la aceeasi trecere — simplu si sigur.

10. Amendamente /autoplan (2026-07-06)

Acceptate in scope (auto-decizii, vezi Decision Audit Trail):

  1. A1 Codul de cache traieste in modul NOU app/embedding_cache.py (functiile cu conn); app/embeddings.py ramane fara acces DB. EmbeddingEngine.index_corpus primeste parametru nou vectors: list[list[float]] | None, aliniat pozitional cu items; None = comportamentul actual (embed tot).
  2. A2 save_vectors scrie in tranzactii chunk-uite de 500 randuri/commit — AMENDEAZA AC-ul US-002 "commit la final". Motiv: un commit unic de ~26MB tine write-lock in timp ce worker-ul face BEGIN IMMEDIATE pe submissions.
  3. A3 Purjarea orfanelor: diff in Python (set hash-uri corpus vs set hash-uri cache), apoi DELETE pe chunk-uri de 500 pe PK. Include intrarile modelelor vechi (model != curent). Motiv: tranzactii scurte; (limita de parametri SQLite e 32766 pe 3.32+, nu 999 — chunking-ul e pentru durata lock-ului).
  4. A4 Orice eroare a cailor de cache (SELECT/INSERT/blob corupt) se logheaza cu log.warning INAINTE de fallback-ul pe embed complet. Degradarea existenta (except -> pass pe indexare) ramane neschimbata — scoping strict pe codul nou.
  5. A5 Metrica "acelasi cod sugerat" se verifica cu toleranta float32 (round-trip float64->float32 poate muta scoruri la a 7-a zecimala): test de echivalenta a RANKING-ULUI, nu egalitate stricta de scor.
  6. A6 Teste suplimentare la US-005: (a) chunking >500 randuri (scriere multi-tranzactie completa), (b) idempotenta la cache partial (populare intrerupta -> urmatorul warmup completeaza, fara duplicate, INSERT OR REPLACE).
  7. A7 Nota bounded acceptata: purjarea ruleaza doar la finalul unei indexari reusite; cu embeddings dezactivat sau warmup esuat permanent, cache-ul mort ramane pana la reset manual (DELETE FROM embedding_cache).

Adaugate de review-ul Eng (subagent independent, toate verificate in cod):

  1. A8 index_corpus(items, signature, vectors) VALIDEAZA len(vectors) == len(items) si absenta None-urilor; mismatch => log.warning + fallback pe embed complet. Test obligatoriu de ranking EXACT la warm start: >=2 query-uri distincte care aserteaza CODUL vecinului cel mai apropiat (nu doar non-empty) — singurul test care prinde o permutare de aliniere (misalignment = coduri gresite sugerate, silentios).
  2. A9 Secventa hash->load->embed->save->purge->index e protejata de un lock de modul (threading.Lock, pattern _engine_lock existent). CORECTEAZA riscul din sectiunea 6: "scrierile doar din thread-ul de warmup" e FALS — calea de request (block=False) trece de gate-ul is_loaded() (mapping.py:675) dupa incarcarea modelului si ar scrie/purja concurent cu warmup-ul (purjare falsa de randuri noi).
  3. A10 Conexiunile sunt autocommit (isolation_level=None, db.py:16): save_vectors/purge_stale emit EXPLICIT BEGIN/COMMIT per chunk — conn.commit() singur e no-op si ar produce silentios o tranzactie per rand (17k commit-uri), anuland exact motivul A2.
  4. A11 Numele modelului = PARAMETRU explicit pe tot lantul (inclusiv ensure_embeddings_corpus); nu se hardcodeaza FASTEMBED_MODEL in adancime — altfel testele cu mock scriu sub cheia modelului real si US-004 nu are unde injecta numele.
  5. A12 Vectorii din cache raman array('f') end-to-end (fara conversie la list[float]) — RAM ~8x mai mic; prag documentat: designul curent (cosine pur Python) e validat pana la ~50k randuri de corpus; peste, vezi TODOS (numpy).
  6. A13 Teste suplimentare obligatorii: (a) purjarea NU ruleaza cand indexarea a esuat (altfel un esec golește cache-ul permanent); (b) len(vectors) != len(items) => fallback gratios; (c) save partial esuat => indexarea continua din vectorii din RAM; (d) TOATE testele noi seteaza explicit AUTOPASS_EMBEDDINGS_ENABLED=1 — gate-ul din mapping.py:671 face altfel testele vacuos-verzi.
  7. A14 Validare len(vector) == 384 si la SCRIERE (nu doar la citire) — un backend defect nu umple DB-ul cu blob-uri arbitrare.
  8. A15 Orchestrarea hash->load->embed->save->purge se extrage intr-o functie in embedding_cache.py care primeste embed_fnensure_embeddings_corpus ramane citibila (~30 linii, nu ~80 cu 6 responsabilitati).

Diagrama arhitectura

  API startup (lifespan)                     request path (neatins)
       |                                          |
  _warmup_embeddings (thread)                enrich_suggestions
       |                                          |
  ensure_embeddings_corpus(conn, block=True)  suggest_nearest (query embed la cerere)
       |
       |-- SELECT mapping_suggestions ----------- corpus SILVER (sursa)
       |-- _corpus_signature_silver ------------- decizia reindexarii (NESCHIMBATA)
       |-- [NOU] embedding_cache.load_cached_vectors(conn, model, hashes)
       |         (SELECT chunk-uit; blob corupt => miss)
       |-- embed() DOAR pe miss-uri (backend fastembed/mock)
       |-- [NOU] embedding_cache.save_vectors(conn, model, items_noi)
       |         (INSERT OR REPLACE, commit la 500)
       |-- [NOU] purge_stale(conn, model, hash-uri_corpus)
       |         (orfane + modele vechi, DELETE chunk-uit)
       `-- EmbeddingEngine.index_corpus(items, signature, vectors=toate)

  SQLite (WAL, busy_timeout 15s):  embedding_cache (model, text_hash) PK
  Worker: NU atinge embedding_cache (nu incarca modelul — invariant existent)

Data flow cu shadow paths

  corpus rows ─▶ filtrare denumire goala ─▶ hash sha256(text) ─▶ cache lookup ─▶ embed(miss) ─▶ save ─▶ index
      │                 │                        │                   │               │            │
   [gol? => return]  [toate goale? =>        [hash pe lista       [tabela lipsa/  [backend     [OperationalError
    cache neatins]    corpus gol, no-op]      FILTRATA, nu pe      DB locked =>    esueaza =>   la commit =>
                                              rows brute!]         warning +       warning +    warning + fallback;
                                                                   embed complet]  corpus gol]  cache partial OK]

Error & Rescue Registry

Codepath Ce poate esua Exceptie Rescued? Actiune User vede
load_cached_vectors DB locked/busy sqlite3.OperationalError DA log.warning + fallback embed complet nimic (warmup mai lung)
load_cached_vectors blob lungime gresita (validare explicita) DA tratat ca miss, re-embed, log count nimic
save_vectors lock la commit chunk sqlite3.OperationalError DA log.warning; cache partial ramane valid (idempotent) nimic
purge_stale lock la DELETE sqlite3.OperationalError DA log.warning; orfanele raman pana la urmatoarea trecere nimic
hash drift (normalizare schimbata) miss permanent — (nu e exceptie) DA (observabil) log warmup arata embed=N cand se astepta 0 (US-006) nimic
ensure_embeddings_corpus orice alta exceptie Exception (catch-all EXISTENT) DA pass (degradare documentata REQ-005) sugestii embeddings lipsesc

Failure Modes Registry

Codepath Failure mode Rescued? Test? User vede? Logged?
cold start cache gol DA (embed tot) US-005 fereastra 1-2 min (o data) DA (US-006)
warm start cache complet US-005 fereastra ~10-20s DA
incremental N texte noi DA US-005 DA
blob corupt re-embed silentios DA US-003 AC nimic DA (A4)
populare intrerupta cache partial DA (idempotent) A6b nimic DA
model schimbat zero hit, re-embed sub cheie noua DA US-004 fereastra 1-2 min (o data) DA
warmup esuat permanent cache mort ne-purjat partial (A7, manual) nimic (bounded, ~30MB) DA

Zero CRITICAL GAP (niciun rand cu Rescued=N + Test=N + Silent).

NOT in scope (deferate, cu motiv)

  • CLI tools/embcache — manual suficient in v1 (Open Q1).
  • Precompute vectori la build (artefact Docker) — ar face si primul start ieftin; separat.
  • Graceful reload / zero-downtime — ar elimina complet fereastra post-restart; infra, nu app.
  • Optimizare latenta per-query suggest_nearest (numpy) — de masurat intai; PRD-ul declara cosine Python suficient (non-goal).
  • Cache query-uri (REQ-007), vector store extern, panou admin stats — non-goals/skip.

What already exists (reuse)

  • _corpus_signature_silver (mapping.py:635) — decizia reindexarii, pastrata identic.
  • ensure_embeddings_corpus (mapping.py:649) — singurul punct de interceptie pe flux.
  • EmbeddingEngine(backend=...) (embeddings.py:84) — mock-ul de test existent acopera US-005.
  • Schema idempotenta schema.sql + init_db() — pattern-ul tabelei noi.
  • PRAGMA busy_timeout=15000 + WAL (db.py:18-20) — deja setate, sustin scrierile concurente.

Dream state delta

Planul duce sistemul de la "restart plateste intreg corpusul" la "restart plateste doar delta"; cheia per-text face re-etichetarile (cod/is_nul) GRATUITE la restart — exact traiectoria 12 luni (corpus SILVER in crestere organica). Ramane in afara idealului: fereastra de incarcare a modelului (~10-20s) si costul per-query al cosine-ului pur Python (ambele in TODOS).

Diagrama de acoperire teste (Eng S3)

CODE PATHS (toate NOI — teste planificate, nu existente)          ACOPERIT DE
[+] app/embedding_cache.py
  ├── vector_to_blob/blob_to_vector round-trip                    US-002 AC (+A5 toleranta)
  ├── blob corupt (lungime gresita) => miss                       US-003 AC
  ├── len(vector)!=384 la SCRIERE => respins                      A14
  ├── save_vectors chunk 500 + BEGIN/COMMIT explicit              A6a + A10
  ├── save partial esuat => indexare continua din RAM             A13c
  └── purge orfane + modele vechi; NU ruleaza la index esuat      US-004 + A13a
[+] app/embeddings.py :: index_corpus(vectors=)
  ├── len(vectors)!=len(items) => fallback embed complet          A8/A13b
  └── ranking EXACT la warm start (>=2 query-uri, cod asertat)    A8
[+] app/mapping.py :: ensure_embeddings_corpus
  ├── cold start: exact N texte la backend mock                   US-005
  ├── warm start: ZERO texte, has_corpus True, suggest ok         US-005
  ├── incremental: +1 rand => exact 1 embed                       US-005
  ├── model schimbat => zero hit-uri cache                        US-004
  ├── hash pe lista FILTRATA (denumire goala exclusa)             risc §6, test dedicat
  └── concurenta warmup/request sub lock                          A9 (test cu 2 threaduri)
TOATE testele: AUTOPASS_EMBEDDINGS_ENABLED=1 explicit             A13d (anti-vacuos)

COVERAGE PLANIFICAT: 15/15 cai identificate au test specificat | GAPS ramase: 0
E2E: verify pe instanta reala (US-006) — operational, in afara suitei

Paralelizare worktree (Eng)

Implementare secventiala, fara oportunitate de paralelizare: lantul schema.sql → embedding_cache.py → embeddings.py (vectors=) → mapping.py (wiring) → teste e strict dependent, toate in acelasi modul de suggestii. Un singur lane.

Implementation Tasks

Sintetizate din finding-urile review-ului. P1 blocheaza ship-ul.

  • T1 (P1, human: ~3h / CC: ~20min) — embedding_cache — Modul nou: serializare array('f') end-to-end, load/save chunk 500 cu BEGIN/COMMIT explicit, validare dim la scriere/citire, purge diff-Python (orfane + modele vechi), log.warning pe erori — Surfaced by: US-001/002/004 + A2/A3/A4/A10/A12/A14 — Files: app/schema.sql, app/embedding_cache.py — Verify: pytest teste noi serializare/chunk/purge
  • T2 (P1, human: ~2h / CC: ~15min) — embeddings — index_corpus(vectors=) cu validare aliniere + fallback; model ca parametru pe lant — Surfaced by: A8/A11 — Files: app/embeddings.py — Verify: test ranking exact + test mismatch
  • T3 (P1, human: ~2h / CC: ~15min) — mapping — Wiring ensure_embeddings_corpus: orchestrare extrasa cu embed_fn (A15), lock de modul (A9), hash pe lista filtrata — Surfaced by: US-003 + A9/A15 — Files: app/mapping.py, app/embedding_cache.py — Verify: teste cold/warm/incremental + test concurenta
  • T4 (P1, human: ~2h / CC: ~15min) — teste — Suita completa din diagrama (15 cai), toate cu AUTOPASS_EMBEDDINGS_ENABLED=1 — Surfaced by: US-005 + A6/A13 — Files: tests/ — Verify: python3 -m pytest -q
  • T5 (P2, human: ~30min / CC: ~5min) — observabilitate — Log warmup cache=N embed=M in Xs — Surfaced by: US-006 — Files: app/mapping.py sau app/main.py — Verify: manual pe ./start.sh, doua restarturi
  • T6 (P2, human: ~15min / CC: ~2min) — teste — Test echivalenta ranking cu toleranta float32 — Surfaced by: A5 — Files: tests/ — Verify: pytest

Decision Audit Trail

# Phase Decision Classification Principle Rationale Rejected
1 CEO 0C-bis Design per-text (model, text_hash), nu blob monolit pe semnatura Mechanical P1 Doar per-text da embed=0 la relabel (workflow activ: re-etichetare Haiku); semnatura include cod/is_nul, textul nu se schimba blob monolit (re-embed tot la orice relabel)
2 CEO 0E Modul nou app/embedding_cache.py; embeddings.py ramane fara DB Mechanical P5 Separare existenta curata (engine pur / mapping face DB) functii in embeddings.py
3 CEO 0D Open Q2: purjare include modelele vechi Mechanical P2/P3 ~5 linii, previne crestere moarta; propunerea PRD lasare inerta
4 CEO 0D CLI embcache deferat la TODOS Mechanical P3/P6 PRD propune manual in v1 build acum
5 CEO S2 log.warning pe erorile cailor de cache (A4) Mechanical P1 (zero silent failures) fallback-ul ramane, dar devine vizibil pass mut
6 CEO S6/S5 Metrica cu toleranta float32 + test echivalenta ranking (A5) Mechanical P1 egalitate stricta ar esua legitim pe round-trip float32 egalitate stricta
7 CEO S7/S1 Commit chunk-uit 500 (A2) + purjare diff-Python chunk-uita (A3) Mechanical P5 tranzactii scurte vs worker BEGIN IMMEDIATE commit unic ~26MB
8 CEO S6 Teste chunking + idempotenta cache partial (A6) Mechanical P1 fixeaza comportamentul multi-tranzactie doar testele US-005
9 CEO 0D Metrici Prometheus warmup TASTE log-ul US-006 acopera nevoia; counterul e nice-to-have — (la gate)
10 CEO OV Sidecar DB separat (voce externa) vs acelasi autopass.db TASTE user a confirmat P3 la D1; vocea externa ridica bloat backup + no-shrink fara VACUUM — (la gate)
11 CEO 0A Landscape check din cunostinte in-distributie (fara WebSearch) Mechanical P3 pattern Layer 1 arhicunoscut (cache pe (model, hash(text))) cautare web
12 CEO S4 Dublu-index teoretic warmup/request: no action Mechanical P6 SUPERSEDED de #14 — evaluarea era corecta doar pentru planul FARA cache; cu scrieri+DELETE, riscul devine real lock nou
13 Eng S1 A8: validare aliniere vectors/items + test ranking exact Mechanical P1 misalignment = coduri gresite silentios; zip nu detecteaza nimic (embeddings.py:158) liste paralele nevalidate
14 Eng S1 A9: lock de modul pe secventa de cache (supersedes #12) Mechanical P1 calea de request scrie si ea dupa is_loaded() (mapping.py:675); purjare concurenta = stergere falsa de randuri noi write-uri doar pe block=True (re-embed repetat pe request path)
15 Eng S2 A10: BEGIN/COMMIT explicit per chunk (autocommit db.py:16) Mechanical P5 conn.commit() e no-op in autocommit; chunking-ul A2 nu s-ar intampla de fapt commit() naiv
16 Eng S2 A11: model ca parametru explicit pe tot lantul Mechanical P5 testabilitate US-004; mock-urile nu trebuie sa scrie sub cheia modelului real FASTEMBED_MODEL hardcodat
17 Eng S4 A12: array('f') end-to-end + prag documentat ~50k Mechanical P1 RAM ~8x mai mic gratis; blob_to_vector trece oricum prin array('f') list[float] boxed (~2GB la 170k)
18 Eng S3 A13: 4 teste suplimentare (purge-dupa-esec, mismatch, save partial, env flag explicit) Mechanical P1 teste vacuos-verzi cu embeddings dezactivat = mai rau decat lipsa lor doar US-005+A6
19 Eng S3 A14: validare dimensiune vector la scriere Mechanical P1 o linie; previne umplerea DB cu blob-uri arbitrare validare doar la citire
20 Eng S2 A15: orchestrare extrasa cu embed_fn injectat Mechanical P5 ensure_embeddings_corpus ramane citibila functie de 80 linii cu 6 responsabilitati

GSTACK REVIEW REPORT

Review Trigger Why Runs Status Findings
CEO Review /plan-ceo-review Scope & strategy 1 issues_open (via /autoplan) 6 propuneri, 2 acceptate, 2 deferate, 2 taste la gate
Codex Review /codex review Independent 2nd opinion 1 issues_found (subagent-only) Codex indisponibil (usage limit); voce externa = subagent Claude, 6 finding-uri CEO + 8 Eng
Eng Review /plan-eng-review Architecture & tests (required) 1 clean (via /autoplan) 8 issues, 0 critical gaps — toate absorbite ca A8-A15
Design Review /plan-design-review UI/UX gaps 0 SKIPPED fara scope UI (grep 0 potriviri)
DX Review /plan-devex-review Developer experience gaps 0 SKIPPED fara suprafata developer-facing noua

CROSS-MODEL: indisponibil — Codex pe usage limit pana la 18 iul; toate vocile externe au rulat ca subagenti Claude independenti (context proaspat, fara istoricul review-ului).

VERDICT: CEO + ENG CLEARED — APPROVED la gate-ul /autoplan (2026-07-06): taste #9 = SKIP metrici Prometheus (log-ul US-006 suficient); taste #10 = cache ramane in autopass.db (sidecar respins; caveat VACUUM documentat). Ready to implement.

NO UNRESOLVED DECISIONS