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.
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.
Extends AbilityFactory with a risk-stratified capability model for Hermes
MCP tools (economic-data/capability-reasoning/legislation-search), ported
from Open.Jarvis's plugin permission system: non-owner/admin roles only
get low/medium risk tools by default, high/critical stay reserved. Adds
maskSensitiveValue as a reusable secret/PII redaction utility for future
outbox/audit logging, reinforcing the existing ai_requests hash-only
storage principle.
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.
Per Blueprint v4.0 §12 canonical data models:
- Business Engine: organizations extended (legal_name, country, registry_id,
domain, external_ids), transactions (minor-unit amounts, evidence_status),
documents (metadata only -- binary/OCR stays in Paperless via paperless_id)
- Life Engine: goals (horizon/metric/target/milestones)
- Intelligence Engine: decisions (context/options/assumptions/evidence),
opportunities, ai_requests (context_manifest_hash for audit without
storing sensitive payload content in Postgres)
- Trust Engine: observations (generic subject_type/subject_id so any
entity -- user, org, device -- can feed reputation/trust scores)
Applied directly to Supabase staging Postgres (14 tables total now).
This is also the first drizzle/ migration actually committed -- the
earlier one from the Identity Engine work never made it into git.
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).