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)
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
- 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.
Ports the SKILL.md/frontmatter pattern from skills-claude (fork of
anthropics/skills) to define user-facing "niches" (legal, financial,
business-capability) that each group one or more AGENT_TOOL_REGISTRY
entries. NicheRegistryService loads and validates src/agent-niches/
definitions/*.md at boot, failing loudly on unknown tool references so a
niche can never silently grant access to an undeclared capability. This
is the data model for the planned dashboard where memberships access
Hermes agents scoped by niche instead of by raw tool name.
Both guards were fully implemented but never actually wired in -- Nest
doesn't enforce a guard just because its module is imported, it needs
an explicit APP_GUARD registration. Registered both globally so every
new controller is deny-by-default and rate-limited unless it opts out.
Added a @Public() decorator (checked via Reflector in TenantGuard) for
routes that legitimately have no session, applied it to /health so the
global guard doesn't break it.
CORS was wide open (enableCors() with no origin restriction, effectively
allow-any-origin). Now reads an explicit CORS_ORIGINS allowlist from env,
defaulting to localhost:3000 for local dev.
Adds tenants/memberships/consent_records tables, a CASL AbilityFactory
keyed on membership role, and a TenantGuard that derives tenant_id from
session only (never client-supplied), per blueprint 8.2/8.3/11.3.
Adds outbox_events + audit_log tables, an OutboxService for transactional
writes, and a Cron-based OutboxDispatcher that publishes pending events
to a BullMQ queue, per blueprint 9.1 (events before intelligence).