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