Motor generic pentru procese cu mai multi pasi care nu incap intr-o
tranzactie. Garantia nu e atomicitatea, ci compensarea in ordine inversa.
- saga-registry: definitii versionate in cod, backoff exponential plafonat
- saga-runner: corelator outbox->instanta, step runner, timeout, retry,
compensare; revendicare cu FOR UPDATE SKIP LOCKED ca doua procese sa nu
avanseze aceeasi saga simultan
- idempotenta la pornire prin index unic (saga, versiune, trigger_event),
nu prin SELECT-apoi-INSERT care ar avea race
- compensarea are propriul retry: o compensare esuata lasa sistemul mai rau
decat esecul original
- monitor /v1/sagas cu instante blocate si retry manual care NU sare pasi
- prima saga reala: imbogatire research brief, fara pas AI (un apel AI
automat per brief ar schimba profilul de cost)
Aritmetica ferestrei care traverseaza miezul noptii era netestata si prinsa
intr-o metoda privata. Extrasa in quiet-hours.ts cu 10 teste care acopera
fereastra simpla, traversarea miezului noptii si politica de bypass CRITICAL.
Documenteaza explicit ca agregarea nu reevalueaza quiet hours: incrementarea
unui contor pe o notificare deja vizibila nu e o intrerupere noua.
Motorul NU trimite o notificare per eveniment. Decide daca merita notificat,
cui, ce severitate, daca se agrega, ce canal si cand.
- notifications / notification_preferences / notification_processed_events.
Ultimul da idempotency: aceeasi regula nu proceseaza acelasi eveniment de
doua ori (unique rule_id + event_id).
- Reguli in COD, nu in DB cu condition_expression evaluat dinamic. Un
evaluator de expresii e o suprafata de atac si un limbaj in plus, fara ca
nimeni sa administreze inca reguli per tenant. Contractul ramane acelasi
cand vor migra in DB.
- Destinatarii se rezolva server-side din memberships, niciodata din payload.
Implicit nu notificam actorul despre propria actiune.
- Deduplicare pe cheia tenant+recipient+categorie+entitate+versiune regula.
- Agregare: 47 de taskuri intr-o ora devin o notificare cu aggregated_count,
nu 47 de notificari. Testul verifica invariantul ca fereastra de agregare
<= fereastra de dedup, altfel s-ar crea una noua inainte sa se agrege.
- Quiet hours cu fereastra care poate traversa miezul noptii. CRITICAL le
depaseste DOAR daca politica userului permite, nu implicit.
- Inbox: filtre (unread/action_required/critical/intelligence/system),
mark-read, acknowledge (opreste escaladarea), dismiss, snooze.
- Preferinte per user+tenant cu UPSERT (NULLS NOT DISTINCT pe category, ca
randul implicit "toate categoriile" sa fie unic).
Repara criteriul de acceptare "dashboardurile citesc read models, nu
interogheaza haotic toate modulele".
- projection_processed_events (unique: name+version+event_id) = garantia de
idempotency. Checkpointul e doar optimizare de scanare, nu corectitudine:
rescanam cu un safety lag de 60s si sarim ce s-a aplicat deja, ca sa nu
pierdem evenimente comise dupa unul cu created_at mai mare.
- ProjectionRegistry cu ProjectionDefinition (name, version, subscribedEvents,
rebuildStrategy, apply, rebuildTenant).
- executive_dashboard foloseste rebuildStrategy 'canonical-tables':
recalculeaza contorii din tabelele canonice, nu incrementeaza. Contorii
incrementali pot deriva daca un eveniment se pierde sau se dubleaza;
recalcularea e corecta prin constructie si idempotenta natural. Costul e o
interogare per eveniment relevant -- ok la volumul actual.
- GET /v1/dashboard/executive citeste proiectia. Cand proiectia inca n-a rulat
pentru workspace, intoarce status 'building' explicit, NU zerouri care ar
parea date reale.
- POST /v1/dashboard/executive/rebuild -- owner/admin, auditat.
- Serviciile de taskuri/organizatii/segmente emit acum evenimente in outbox
(in aceeasi tranzactie cu scrierea). Fara ele proiectia era cod mort.
- Campurile din spec care depind de module neconstruite (documents,
transactions, approvals) raman 0 explicit, nu inventate.
Specul de arhitectura cere Platform Kernel INAINTEA modulelor de domeniu.
Modulele A1/A2 au fost construite peste un kernel caruia ii lipseau exact
piesele astea. Le adaug acum, aditiv, fara sa rup ce merge.
- workspaces: tenant != workspace. Workspace-ul e contextul de lucru DIN
tenant. Backfill: fiecare tenant existent primeste workspace implicit,
altfel SessionGuard i-ar respinge toate requesturile.
- memberships.workspace_id + valid_from/valid_until: rol per workspace si
acces delegat cu expirare (contabil pana la o data). SessionGuard respinge
membership expirat si membership legat de alt workspace.
- ExecutionContext inlocuieste sesiunea subtire (userId+tenantId+role):
requestId, correlationId, workspaceId, membershipId, roles, permissions,
purpose, timezone, source. Tipul vechi ramane exportat sub acelasi nume,
ca sa nu ating ~15 module de domeniu doar pentru o redenumire.
- GET /v1/navigation: menu registry mutat in backend. Filtreaza pe rol, tip
de workspace, permisiuni si feature flags; intoarce doar itemii autorizati.
Ramane UX, nu securitate -- fiecare endpoint verifica din nou.
- event envelope: workspace_id, occurred_at, actor_id, aggregate_type,
causation_id, classification, provenance
- audit envelope: workspace_id, actor_type, purpose, changed_fields,
before/after hash, session_id
- AiGatewayService: punct unic de acces la modele (LiteLLM -> OpenRouter).
Clasele de actiuni din blueprint 14.3 sunt aplicate hard: high_risk blocat
permanent, material blocat pana exista coada de aprobare + idempotency;
doar read_only si draft ruleaza.
- Spotlighting (design doc Strat 1): continutul untrusted (date Apollo) e
impachetat in <untrusted-data> cu instructiune de sistem ca e DATE, nu
comenzi -- plus redactie PII deterministica (email/telefon/CNP) inainte
sa plece catre provider extern.
- ai_requests: adaugat action_class + context_manifest complet (nu doar
hash) pentru audit; cost_usd_minor_units (integer) inlocuit cu
cost_usd numeric(12,6) -- costurile reale sunt fractiuni de cent.
- research-briefs: POST /draft genereaza un rezumat AI din datele Apollo
fara sa salveze nimic (clasa draft -- userul revizuieste, apoi salveaza).
- briefing: aiExplanation devine narativ real peste prioritizarea
determinista; esecul AI intoarce null, nu strica briefingul.
- teste: clase de actiuni blocate, spotlighting, redactie PII.
- IntelligenceModule: proxy tenant-scoped catre intelligence-api
(/companies/search, /companies/{id}); ceo-web nu vorbeste niciodata
direct cu intelligence-api (blueprint 10.3/16).
- SavedSegments: salveaza o cautare Apollo si o ruleaza din nou oricand.
- ResearchBriefs: dovezi curatate manual per companie, cu sursa Apollo
implicita plus surse suplimentare -- explicit fara rezumat generat de
AI (AI Gateway inca neconstruit, blueprint 14).
- BriefingService: /v1/briefing/today, agregare deterministica de
taskuri restante/viitoare + activitate saptamanala; campul
aiExplanation ramane null si vizibil in raspuns, nu simulat.
- SessionGuard resolves Supabase JWT (local HS256 verify, GoTrue fallback)
and loads the tenant membership from x-tenant-id; TenantGuard keeps
deny-by-default and rejects client-supplied tenant_id (blueprint 11.3).
- New bootstrap routes: GET /v1/me, POST/GET /v1/tenants, tenant member
management (list/add/remove) with owner/admin RBAC.
- Organizations and Tasks modules: full CRUD scoped to session.tenantId,
soft delete, audit log + outbox events on every write.
- AuditService (global) for blueprint 3.4 "100% audit on material ops".
- jest + tenant.guard.spec covering deny-by-default and anti-IDOR cases.