docs(5.15): criterii design din mockup-uri + revizie eng-review

Criterii noi (din docs/mockups/prd-5.15-mockups.html), facute AC testabile:
- antet: referinta vizuala obligatorie la mockup-uri
- US-002: doar tokeni CSS, fara hex hardcodat (AA pe teme luminoase)
- US-003: layout exact strip D6 (glife/copy/last-auth) + stari goale + ierarhie all-time
- US-004: 'Eroare VIN' ilustrativ; pills din labels.py
- US-007: referinta picker op<->cod (2 stari) + reveal odometru

Include si revizia /plan-eng-review care era in working tree necomisa
(E1-E8, US-011 authz, US-012 analytics, Val 0, raport eng CLEARED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-28 14:34:01 +00:00
parent 19f89ecd70
commit 9e42e7ed6f

View File

@@ -4,6 +4,10 @@
> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`.
> Sistemul de design al landing-ului: `app/web/templates/landing.html` (commit 41aa385), `DESIGN.md`.
> Mockup-uri piese fara design (REFERINTA VIZUALA OBLIGATORIE): `docs/mockups/prd-5.15-mockups.html`
> — strip sanatate D6 (stari rosu/verde), picker prestatii E4 (op<->cod), reveal odometru initial.
> Acopera exact piesele pe care mockup-urile landing nu le aratau si corecteaza contradictiile
> mockup<->PRD (VIN unic, contor all-time, culori prin tokeni).
> Starea trece: `draft -> aprobat -> in-executie -> verify-pass -> inchis`.
## 1. Obiectiv
@@ -49,6 +53,43 @@ Decizii din /plan-ceo-review (2026-06-28, mod SELECTIVE EXPANSION):
din chip (US-009), (b) bulk-fix din lista (US-010), (c) require dinamic odometruInitial la chip
R-ODO/I-ODO (US-007), (d) editare keyboard-first in form slim (US-007).
Decizii din /plan-eng-review (2026-06-28, model claude/opus; outside-voice = Claude subagent,
Codex a atins usage-limit). Fiecare confirmata cu userul:
- **E1** (ARCH, /repune nu mai sterge operatia): `/repune` face azi `p0.pop("cod_op_service")`
la `routes.py:1326` — sterge operatia cand se seteaza un cod direct, rupand D7 si US-009.
US-006 ELIMINA acel `pop` si pastreaza `cod_op_service`; test de regresie obligatoriu
(op_service supravietuieste unui /repune cu cod). (Rafineaza US-006.)
- **E2** (DRY teme, fisier fierbinte): config-ul de teme e duplicat in ~7 locuri in base.html
(anti-FOUC `VALID` la :22 + cinci literali paraleli `CYCLE`/`VALID`/`ICONS`/`LABELS`/`NEXT`
la :758-765). US-001 CONSOLIDEAZA intr-o singura structura sursa-de-adevar (`THEMES`
ordonata) din care se DERIVA ciclul/etichetele/iconitele + setul anti-FOUC. Adaugarea unei
teme = o intrare. (Rafineaza US-001.)
- **E3** (obs concat idempotent): la import, copierea denumirii operatiei in `obs` se face
DERIVE-ON-EMPTY (doar cand `obs` e gol) ca sa fie idempotenta la re-import/re-editare. Test
dedicat anti-dublu-concat ("Schimb ulei; Schimb ulei"). (Rafineaza US-005.)
- **E4** (binding operatie<->cod in chips — HIGH): chip-urile NU sunt o lista plata de coduri.
Cand exista operatii (`cod_op_service`), UI-ul randeaza UN picker PE operatie (eticheta op +
chip-ul ei de cod), pastrand perechea per-item pe care modelul o are deja; lista plata de
coduri libere DOAR pentru cazul fara operatie (corectie pura). Astfel US-009 citeste perechea
direct, iar deduparea e PER-ITEM (nu "dupa cod" — doua operatii distincte pot mapa legitim la
acelasi cod RAR). (Rafineaza US-006 AC2 + US-007 AC3 + US-009.)
- **E5** (serializare Val 3 pe routes.py): US-005 si US-006 rescriu ACEEASI functie
`post_corecteaza` (`routes.py:1120-1262`). Regula "un singur autor pe fisier fierbinte" se
EXTINDE la routes.py in Val 3: US-005 INAINTE de US-006 (secvential, nu paralel). (Rafineaza §6.)
- **E6** (US-007 HTMX server-driven PRIMARY): inversam abordarea — chips add/remove via `hx-post`
care re-randeaza partial-ul chips+form; reveal-ul conditional `odometruInitial` rezulta GRATIS
din re-randarea server; navigare tastatura = `<select>`/`<datalist>` nativ. JS custom DOAR ca
progressive enhancement (snappiness), nu calea principala. Elimina path-ul dublu JS/no-JS.
(Rafineaza US-007.)
- **E7** (contoare in timp local RO): `azi`/`luna asta` se bucketeaza in timp local RO (UTC+2/+3),
nu UTC — `updated_at` e `datetime('now')` UTC, deci `date(updated_at)` pur ar numara gresit
trimiterile dintre miezul noptii local si ~03:00. Folosim offset RO (ex. `date(updated_at,'+3 hours')`
cu aceeasi baza `now`) + test la granita de miez de noapte local. (Rafineaza US-003.)
- **E8** (interleave fix authz GET-listari — securitate): CLAUDE.md noteaza scurgere cross-cont
deschisa ("GET-urile de listare sunt globale + neprotejate"). Userul a ales sa INTERLEAVE
remedierea in 5.15 -> story noua **US-011** (account-scope pe GET-urile de listare + teste),
nu queue dupa polish-ul de teme.
Fapte verificate care fundamenteaza scope-ul (nu presupuneri):
- `vin` la RAR e **un singur camp** (17 car., MAJUSCULE, fara O/I/Q) — cerinta "fara 2 campuri
VIN" e deja respectata azi (`_form_editare.html` are un singur `vin`); ramane sa NU regresam.
@@ -90,6 +131,11 @@ acelasi produs, coerent vizual.
noi: `--text->--ink`, `--sub->--muted`, `--okt->--ok`, `--errt->--err`, `--infot->--accent`.
- [ ] Selectorul ciclic parcurge TOATE: light -> dark -> petrol -> grafit -> cobalt -> cupru ->
hartie -> Auto, afiseaza eticheta temei curente, persistenta `localStorage` (D2).
- [ ] **DRY (E2)**: config-ul de teme traieste intr-o SINGURA structura sursa-de-adevar
(`THEMES` ordonata, cu `{id,label,icon}`) din care se DERIVA `CYCLE`/`NEXT`/`ICONS`/`LABELS`
(azi 5 literali paraleli la base.html:758-765) SI setul anti-FOUC `VALID` (azi separat la
base.html:22). Adaugarea unei teme noi = o singura intrare; test ca derivatele acopera
toate temele (prinde o intrare ICONS/LABELS lipsa, nu doar token CSS lipsa).
- [ ] "Auto" pastrat: urmeaza `prefers-color-scheme`, rezolva la dark/grafit sau light/hartie
(decizie minora: Auto -> dark + hartie pentru light, sau dark/grafit — aliniaza cu I2).
- [ ] Script anti-FOUC in `<head>` seteaza `data-theme` sincron pre-paint pentru toate starile;
@@ -118,6 +164,12 @@ chips **pentru ca** dashboard-ul si formularul sa le consume DRY, identic cu moc
printr-un flag (`slim=True`), fara a rupe randarea actuala (default neschimbat).
- [ ] `.chips` + `.chip` (cu buton `×` de stergere) pentru prestatii multi-select; accesibil
(buton real cu `aria-label`), stilat ca in mockup (accent 18%, font 10-11px).
- [ ] **Doar tokeni, fara hex hardcodat (criteriu din mockup)**: toate culorile componentelor noi
(contor, lista slim, chips, strip, picker) folosesc EXCLUSIV variabile CSS
(`var(--errt)`/`var(--okt)`/`var(--accent)`/`var(--card2)`/`var(--line2)` etc.), NU hex literal
si NU inline-styles copiate ca-atare din `landing.html`. Cifra "De corectat" rosie = token
(`var(--errt)`), nu `#E05D5D` hardcodat, ca sa ramana AA pe temele luminoase (hartie/light).
Referinta: `docs/mockups/prd-5.15-mockups.html`.
- [ ] Zero regresie vizuala pe componentele existente (`.card/.pill/.act/.tabel-trimiteri`).
- **Verificare E2E**: pagina de proba/sandbox sau direct in US-003/004/007; vizual pe un esantion de teme + 390/1280.
@@ -136,8 +188,17 @@ chips **pentru ca** dashboard-ul si formularul sa le consume DRY, identic cu moc
oprit SAU RAR inaccesibil ("Blocat: worker oprit" / "Blocat: RAR inaccesibil"), cu ultima
autentificare RAR. Glife accesibile ✓/✗ (nu doar culoare). Invariant zero-silent-failures:
semnalul "declaratiile NU pleaca" e imposibil de ratat, NU ingropat sub volum.
**Layout exact (din mockup)**: strip full-width sus; glifa (✗ rosu / ✓ verde) + text bold la
stanga, "Ultima autentificare RAR: ..." mono muted la dreapta. Copy: rosu "Blocat: worker oprit
— declaratiile NU pleaca" (sau "... RAR inaccesibil"), verde "Declaratiile curg normal".
Referinta: `docs/mockups/prd-5.15-mockups.html`.
- [ ] Sub strip: card "Trimiteri RAR AUTOPASS" cu 3 contoare slim: **In coada** (queued, accent),
**Trimise** (sent, verde), **De corectat** (blocate = needs_data + needs_mapping + error, rosu).
- [ ] **Stari goale + ierarhie contor (criteriu din mockup)**: cifra principala a contorului "Trimise"
e **all-time** (cifra mare bold), iar "luna asta"/"azi" sunt o sub-linie mono secundara
(`luna {n} · azi {n}`) — NU "luna asta" ca cifra principala (corecteaza framing-ul din mockup-ul
landing). Contorul "De corectat" la 0 se afiseaza **muted, nu rosu** (rosu doar cand exista
blocate — pastreaza pattern-ul `_status.html:47`). Referinta: `docs/mockups/prd-5.15-mockups.html`.
- [ ] Cardul **Trimise** afiseaza trei valori temporale (D4): all-time (cifra principala) + "luna asta"
+ "azi" (sub-linie secundara). `_status_counts` extins cu `sent_today`/`sent_month`.
**Sursa de timp**: NU exista coloana `sent_at`; folosim `status='sent' AND date(updated_at)=...`.
@@ -145,6 +206,13 @@ chips **pentru ca** dashboard-ul si formularul sa le consume DRY, identic cu moc
la +90z (`purge_after` se seteaza in ACEEASI scriere care marcheaza `sent`), deci `updated_at`
== momentul trimiterii pentru randurile `sent` -> fara migrare de coloana (respecta Non-Goal).
Daca pe viitor apar scrieri post-`sent`, reevalueaza o coloana `sent_at` dedicata.
**Timezone (E7)**: `updated_at` e `datetime('now')` = UTC; bucketarea `azi`/`luna asta`
se face in TIMP LOCAL RO (ex. `date(updated_at,'+3 hours')`, aceeasi baza `now`), altfel
trimiterile dintre miezul noptii local si ~03:00 cad pe ziua precedenta si "luna asta" e
gresita in primele ore ale zilei de 1. Test la granita de miez de noapte local.
**Caveat reconcile (E6 outside-voice)**: pe reconciliere (raspuns pierdut) worker-ul
marcheaza `sent` cu `updated_at` = momentul reconcilierii, nu al inserarii RAR — pentru
randurile reconciliate (rare) `updated_at` poate diferi de momentul real al trimiterii.
- [ ] Navigarea existenta (Trimiteri/Mapari + badge needs_mapping) se pastreaza. Click pe contorul
**De corectat** deep-link-eaza in lista filtrata pe blocate (`?status=` existent din 5.x),
nu intr-o pagina noua.
@@ -169,7 +237,8 @@ si mai usor de scanat, pastrand filtrele si actiunile.
blocate (checkbox doar pe `gestionabil`) raman FUNCTIONALE.
- [ ] Click pe rand deschide `/_fragments/trimitere/{id}` in modal (neschimbat).
- [ ] Slim layout consistent desktop si <=1024px (cardurile responsive existente nu regreseaza).
- [ ] Pill-urile de stare folosesc maparea din `labels.py` (zero etichete noi).
- [ ] Pill-urile de stare folosesc maparea din `labels.py` (zero etichete noi). Eticheta "Eroare VIN"
din mockup-ul landing e DOAR ilustrativa — se foloseste `stare_scurt` existent (ex. "De corectat").
- **Verificare E2E**: browser — filtrare + paginare + click detaliu + bulk pe blocate, pe 4 teme,
pe 390/820/1280.
@@ -194,6 +263,11 @@ sa corectez/completez ce s-a facut, separat de codurile RAR.
daca fisierul NU are coloana Observatii, denumirea operatiei se **COPIAZA** (nu se muta) si in
`obs`; daca are coloana Observatii, se pastreaza textul ei. Format de concatenare definit
(denumiri separate prin "; "). Fluxul needs_mapping ramane neatins.
- [ ] **Idempotent (E3)**: copierea operatiei in `obs` e DERIVE-ON-EMPTY (doar cand `obs` e gol)
ca re-importul/re-editarea sa NU dubleze textul ("Schimb ulei; Schimb ulei"). Test dedicat
anti-dublu-concat.
- [ ] **Cuplaj preview-import**: `obs` se adauga in `EDIT_FIELDS` (`import_router.py:261`); `_merge_override`
il propaga (obs e free-text, cade pe ramura ne-canonicalizata — fara strip "0", doar trim).
- **Verificare E2E**: `POST /trimitere/{id}/corecteaza` cu `obs` -> persistat -> vizibil in detaliu;
optional proba live RAR ca `obs` apare in FINALIZATA.
@@ -216,6 +290,10 @@ comanda poate avea mai multe prestatii, asa cum accepta RAR.
(`routes.py:1146-1164`, `1288-1324`) — multi-select le rescrie ca: pastreaza itemii existenti
cu `cod_op_service`/`denumire` (invariant D7) si seteaza/adauga `cod_prestatie` pe ei.
`idPrezentare:null` se adauga in `payload.py` la construirea payload-ului, NU in itemul intern.
**E1 (CRITIC)**: `/repune` face azi `p0.pop("cod_op_service", None)` la `routes.py:1326`
ELIMINA acel `pop`: cand se seteaza un cod direct, `cod_op_service`/`denumire` RAMAN pe item
(altfel rupe D7 si US-009). **Test de regresie obligatoriu** (IRON RULE): op_service
supravietuieste unui /repune cu cod.
- [ ] **Pereche operatie<->cod definita**: cand exista operatii (`cod_op_service`), fiecare cod-chip
se ataseaza unei operatii (1 operatie -> 1 cod, ca azi, dar acum N operatii -> N coduri);
cand NU exista operatie (cod direct, ex. corectie pura), chip-urile sunt coduri libere intr-o
@@ -223,8 +301,9 @@ comanda poate avea mai multe prestatii, asa cum accepta RAR.
- [ ] Fiecare cod e validat fata de nomenclator (`valid_codes`); cod necunoscut -> respins cu
mesaj (NU se trimite raw — invariant ORA-12899 din CLAUDE.md/contract).
- [ ] Lista goala de coduri -> ramane `needs_mapping` (nu se trimite fara cod).
- [ ] **Coduri duplicate** (acelasi cod adaugat de 2x) -> deduplicate inainte de persistare
(cheia sorteaza deja dupa identitate, dar lista persistata nu trebuie sa contina duplicate).
- [ ] **Coduri duplicate** -> dedupare **PER-ITEM, nu "dupa cod"** (E4): doua operatii distincte
pot mapa legitim la acelasi cod RAR; deduparea naiva dupa cod ar sterge o operatie reala si
ar distruge contextul op->cod cerut de US-009. Dedup = acelasi (op, cod) de 2x, nu acelasi cod.
- [ ] Recalcul idempotenta dupa editare (mecanism existent), cu prinderea coliziunii ca azi.
- [ ] Se pastreaza regula `odometruInitial` obligatoriu cand lista contine `R-ODO`/`I-ODO`
(contract §payload) — validare existenta, doar verificata pe lista.
@@ -244,8 +323,10 @@ e compact si imi arata clar codurile RAR si observatiile, ca in mockup.
- [ ] Formularul foloseste varianta slim de camp (US-002): VIN, Data prestatiei, Nr. inmatriculare,
Observatii (textarea), prestatii (chips), Odometru — un SINGUR camp VIN (fara "Confirma VIN").
- [ ] Observatii = textarea liber, legat de `obs` (US-005).
- [ ] Prestatii = chips multi-select: fiecare cod ca chip cu `×`; un picker (dropdown din
nomenclator) adauga un cod nou; lista se trimite ca `cod_prestatie` multiplu (US-006).
- [ ] Prestatii = chips multi-select. **Binding op<->cod (E4)**: cand exista operatii
(`cod_op_service`), UN picker PE operatie (eticheta op + chip-ul ei de cod), pastrand
perechea per-item; lista plata de coduri libere DOAR pentru cazul fara operatie (corectie
pura). Fiecare cod ca chip cu `×`; lista se trimite ca `cod_prestatie` multiplu (US-006).
- [ ] Acelasi `_form_editare.html` slujeste ambele modale (detaliu `/corecteaza` si preview
`/editeaza`), fara duplicare; degradare fara JS rezonabila (chips ca lista, picker = select).
- [ ] **Require dinamic odometruInitial** (D10c): cand lista de chips contine `R-ODO` sau `I-ODO`,
@@ -255,10 +336,18 @@ e compact si imi arata clar codurile RAR si observatiile, ca in mockup.
navigheaza optiunile; Esc inchide modalul; focus-ul revine logic dupa adaugare/stergere.
- [ ] Stilizare fidela mockup-ului pe toate temele; tinte 44px pe mobil; a11y (label-uri, aria,
anunt de chip adaugat/sters pentru screen-reader).
- [ ] **Suprafata JS reala** (nota de efort): chips add/remove client-side + picker navigabil cu
tastatura + management focus + reveal conditional odometruInitial = JS ne-trivial intr-un app
HTMX/minimal-JS. Fallback fara JS: picker = `<select>` simplu (server valideaza R-ODO->odo,
deci no-JS da `needs_data` corect, nu date gresite) — multi-select se degradeaza la un cod/submit.
- [ ] **HTMX server-driven PRIMARY (E6)**: chips add/remove via `hx-post` care re-randeaza
partial-ul chips+form; reveal-ul conditional `odometruInitial` rezulta GRATIS din re-randarea
server (server computeaza din lista de chips, fara ramura JS); navigare tastatura =
`<select>`/`<datalist>` nativ. JS custom DOAR ca progressive enhancement (snappiness), nu
calea principala. Elimina path-ul dublu JS/no-JS pe care formularea anterioara il cerea.
- [ ] **Referinta vizuala (criteriu din mockup)**: `docs/mockups/prd-5.15-mockups.html` defineste
aspectul-tinta — VIN unic (FARA al doilea camp "Confirma VIN" din mockup-ul landing); Observatii
ca textarea slim; picker PE operatie cu DOUA stari vizuale: (a) operatie mapata = chip cod cu `×`
+ "+ alt cod" + link "salveaza regula op->cod" (US-009); (b) operatie ne-mapata = picker galben
"alege cod RAR" cu eticheta "lipsa cod". OdometruInitial: ascuns implicit (doar hint discret
"se cere doar pentru R-ODO/I-ODO") si DEZVALUIT cu bordura-stanga galbena + label "necesar pentru
R-ODO" cand lista de chips contine R-ODO/I-ODO.
- **Verificare E2E**: browser — editare trimitere needs_data: schimb VIN + scriu Observatii + adaug
2 coduri RAR (chips, cu tastatura) + adaug R-ODO (apare odometruInitial) + sterg un chip -> salvare
-> persistat; identic in preview import.
@@ -316,6 +405,43 @@ de corectat/zi nu vreau sa intru in fiecare individual.
- **Verificare E2E**: selectez 3 randuri needs_mapping + aplic un cod -> toate 3 -> `queued`.
- **Verificare E2E**: rulare completa documentata in Raportul VERIFY.
### US-011: Securitate — account-scope pe GET-urile de listare (interleave, E8)
**Ca** operator **vreau** ca listarile sa-mi arate DOAR trimiterile contului meu **pentru ca**
azi GET-urile de listare sunt globale + neprotejate (scurgere VIN/PII cross-cont, notata in CLAUDE.md).
- **Depinde de**: — (backend pur, independent de UI; ruleaza in paralel cu valurile de design)
- **Fisiere**: `app/web/routes.py` (GET-urile de listare trimiteri), `app/api/v1/router.py`
(GET-urile API de listare daca sunt globale), `app/auth.py` (refolosire scope existent),
`tests/test_web_scope.py`, `tests/test_api_scope.py` (~5 fisiere)
- **Test intai (RED)**: `test_get_listare_scoped_cont` — un cont NU vede randuri ale altui cont;
`test_get_listare_neautentificat_401`; `test_get_detaliu_scoped` (404-before-leak pe id strain).
- **Acceptance criteria**:
- [ ] GET-urile de listare (trimiteri + orice listare globala) devin account-scoped, refolosind
mecanismul de scope existent (ca POST-urile + bulk-delete: 404-before-409 pe id strain).
- [ ] Un cont nu poate enumera/citi VIN/PII al altui cont prin listare sau detaliu.
- [ ] Enforcement aliniat cu `AUTOPASS_REQUIRE_API_KEY` (dev vs prod), fara a rupe contul id=1
implicit in dev.
- [ ] Actualizeaza nota din CLAUDE.md ("GET-urile de listare ... de remediat") cand e inchis.
- **Verificare E2E**: doua conturi cu trimiteri; contul A nu vede niciun rand al contului B in
listare, filtre, paginare sau detaliu.
### US-012: Analytics device-mix (validare premisa mobil, in-PR)
**Ca** owner **vreau** sa stiu raportul desktop/mobil al operatorilor **pentru ca** sa decid daca
rafinarile mobil (390px) viitoare merita efortul (premisa nevalidata din TODOS 5.13/CEO-F1).
- **Depinde de**: — (instrumentare backend, independenta de UI)
- **Fisiere**: `app/web/routes.py` (sau middleware existent), `app/schema.sql` SAU `app_events`
(reuse tabela de evenimente existenta — fara coloana noua daca `app_events` poarta semnalul),
`tests/test_device_mix.py` (~3 fisiere)
- **Test intai (RED)**: `test_device_mix_inregistrat`, `test_device_mix_fara_pii`.
- **Acceptance criteria**:
- [ ] La acces dashboard, clasifica grosier viewport/UA in desktop/mobil si inregistreaza in
`app_events` (semnal agregat, FARA PII suplimentar). Reuse tabela existenta — fara migrare
daca `app_events` poarta semnalul.
- [ ] Un mod simplu de citire a raportului (query/admin), suficient pentru a decide investitia mobil.
- [ ] Zero PII nou; aliniat retentiei `app_events` existente.
- **Verificare E2E**: acces dashboard de pe doua viewport-uri -> doua evenimente clasificate corect.
## 4. Riscuri
- **base.html fisier fierbinte**: US-001/US-002 il ating amandoua + US-003/004/007 il citesc.
@@ -355,15 +481,22 @@ de corectat/zi nu vreau sa intru in fiecare individual.
## 6. Valuri de executie (graful de dependente)
```
Val 0: [US-011] authz GET-listari (backend pur; ruleaza in paralel cu orice val) ||
Val 1: [US-001] base.html teme + tokeni (autor unic pe base.html)
Val 2: [US-002] base.html componente (dupa US-001, autor unic pe base.html)
Val 3: [US-003] [US-004] dashboard + strip sanatate + lista (consuma US-002; disjuncte) ||
[US-005] [US-006] backend obs + prestatii (fisiere backend; in paralel cu UI)
[US-005] -> [US-006] backend obs APOI prestatii — SECVENTIAL (E5): ambele rescriu
ACEEASI functie post_corecteaza (routes.py:1120-1262), autor unic
Val 4: [US-007] formular slim cu chips (dupa US-002+US-005+US-006)
Val 5: [US-009] [US-010] salvare mapare din chip || bulk-fix (dupa US-006/007 resp. US-004)
— disjuncte la nivel de template (_form_editare vs _submissions)
Val 6: [US-008] regresie + E2E final (dupa toate)
```
> **Regula autor-unic extinsa (E5)**: pe langa base.html, `routes.py` are autor unic in Val 3:
> US-005 INAINTE de US-006 (ambele in `post_corecteaza`). US-009/US-010 in Val 5 sunt disjuncte
> la nivel de template; adauga rute noi separate in routes.py (regiuni diferite, mergeabile).
> Secventiere fata de alte PRD-uri (D9): **5.15 INAINTE de 5.14** (mapare LLM) — 5.15 fixeaza forma
> listei `prestatii` si UX-ul de confirmare; 5.14 umple codurile peste aceeasi forma.
@@ -416,7 +549,35 @@ extinderi acceptate (D10) -> US-007 imbogatit + US-009 (salvare mapare din chip)
obiectivul pur de propagare design. **Acceptat constient** (alegerea userului); ramane optiunea de
a le scoate intr-un PRD separat daca propagarea design e ce e urgent.
**VERDICT:** APROBAT cu modificari incorporate (+remedieri spec-review). Scope: 10 stories / 6 valuri.
Fara CODEX/cross-model in aceasta rulare (review uni-model). Gata de executie dupa confirmarea userului.
### /plan-eng-review — 2026-06-28 (model claude/opus; outside-voice = Claude subagent, Codex usage-limit)
| Review | Trigger | Why | Runs | Status | Findings |
|--------|---------|-----|------|--------|----------|
| CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | issues_open->remediat | 10 stories, 4 ext acceptate, spec-review remediat |
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | issues_open->remediat | 7 issues (2 HIGH), 1 regresie IRON-RULE, +2 stories noi |
| Outside Voice | Claude subagent | Independent 2nd opinion | 1 | issues_found | 10 findings; 2 HIGH absorbite, restul foldate |
**Step 0 scope:** acceptat ca-atare (10 stories). Gate de complexitate = breadth, nu depth (zero clase/servicii noi). User a confirmat pastrarea US-009/US-010.
**Constatari eng-review (toate confirmate cu userul si foldate in AC):**
- **E1 (ARCH, HIGH, conf 9/10)** `routes.py:1326` `/repune` face `p0.pop("cod_op_service")` — sterge operatia, rupe D7+US-009. US-006: elimina pop + test regresie (IRON RULE).
- **E2 (Code-quality, conf 9/10)** config teme duplicat ~7 locuri pe base.html (anti-FOUC + 5 literali). US-001: o singura structura `THEMES`, restul derivat.
- **E3 (Test, conf 7/10)** obs concat la import poate dubla textul la re-import. US-005: derive-on-empty + test anti-dublu.
- **E7 (Perf/Correctness, conf 8/10 — outside-voice)** `date(updated_at)` UTC numara gresit `azi`/`luna` peste granita local RO. US-003: bucketare timp local + test granita.
**Outside-voice (Claude subagent) — material absorbit:**
- **OV/E4 (HIGH, conf 9/10)** chip-uri lista plata fara binding op<->cod -> rupe US-009; dedup-dupa-cod sterge operatie legala. US-006/007: picker PE operatie cand exista op; flat doar fara op; dedup per-item.
- **OV/E5 (HIGH, conf 9/10)** Val 3 conflict same-function: US-005+US-006 rescriu `post_corecteaza`. §6: serializare US-005 -> US-006 pe routes.py.
- **OV/E6 (MED, conf 8/10)** US-007 supraestimeaza JS custom intr-un app HTMX. US-007: hx-post server-driven primary; reveal odo gratis; select/datalist nativ.
- **OV/E8 (MED securitate, conf 7/10)** GET-uri de listare globale neprotejate (scurgere VIN/PII cross-cont, CLAUDE.md). User a ales INTERLEAVE -> **US-011** (account-scope + teste).
- **OV minore foldate:** reconcile drift pe `updated_at` (caveat US-003); cost poll non-sargabil (notat, non-blocant); cuplaj `EDIT_FIELDS` pentru obs preview (US-005 AC).
**TODO (decizie user):** premisa mobil nevalidata -> user a ales BUILD-IN-PR -> **US-012** (analytics device-mix, fara PII, reuse `app_events`).
**Scope actualizat:** 10 -> **12 stories** (+US-011 authz, +US-012 analytics). Fara migrare de schema. Outside-voice a confirmat "no migration" TRUE.
**Failure modes — gap critic:** niciun gap critic ramas silent. Cel mai aproape: E1 (regresie tacuta op_service pe /repune) — acum acoperit de test obligatoriu. E7 (off-by-a-day tacut) — acum cu test de granita.
**VERDICT:** CEO + ENG CLEARED — gata de executie. 12 stories. Outside-voice absorbit (2 HIGH foldate). Fara migrare de schema.
NO UNRESOLVED DECISIONS