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>
335 lines
28 KiB
Markdown
335 lines
28 KiB
Markdown
# 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
|
|
|
|
- [x] 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.
|
|
- [x] 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):
|
|
|
|
8. **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).
|
|
9. **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).
|
|
10. **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.
|
|
11. **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.
|
|
12. **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).
|
|
13. **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.
|
|
14. **A14** Validare `len(vector) == 384` si la SCRIERE (nu doar la citire) — un backend defect nu umple DB-ul cu blob-uri arbitrare.
|
|
15. **A15** Orchestrarea hash->load->embed->save->purge se extrage intr-o functie in `embedding_cache.py` care primeste `embed_fn` — `ensure_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
|
|
|
|
<!-- AUTONOMOUS DECISION LOG -->
|
|
|
|
| # | 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
|