You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
C4Context
title System Context — Evolith Tracker (Governance Control Plane)
Person(human, "Human Actor", "Governs, authorizes exceptions, and approves Gate Decisions")
Person(agent, "Autonomous Agents", "Executes bounded tasks and provides TechnicalEvaluationResults via MCP")
System(evolith_tracker, "Evolith Tracker", "E2E AI-Native SDLC Orchestrator. Owns canonical Gate Decisions and Evidence Graph.")
System_Ext(evolith_core, "Evolith Core", "Upstream Constitution (Rulesets, Schemas, Contracts)")
System_Ext(ums, "UMS SaaS", "AuthN/AuthZ, authorization graph")
System_Ext(providers, "External Providers", "SCM, CI/CD, Work Systems, Observability")
Rel(human, evolith_tracker, "Approves transitions")
Rel(agent, evolith_tracker, "Evaluates criteria (stateless)")
Rel(evolith_tracker, evolith_core, "Resolves policies")
Rel(evolith_tracker, ums, "Validates identity")
Rel(evolith_tracker, providers, "Ingests evidence via ACLs")
Loading
2. Architecture Positioning
Evolith Tracker is a Progressive Monolith — a single backend deployment unit with strict internal modularity. The 5 Phase Gate contexts (Discovery, Design, Construction, QA, Release) plus 4 supporting contexts (Governance, Artifacts, Metrics, Integration) are logical bounded contexts that can be extracted into independent services in future phases without rewriting domain logic. The frontend is a Microfrontend architecture (Module Federation) per T-002.
React Microfrontends (Shell Host + 7 remotes, PWA) ← §20
↓ (REST only — OpenAPI 3.0; see T-009)
API Gateway / BFF
↓
NestJS Monolith (9 bounded contexts)
├── Discovery Module (Phase Gate 1)
├── Design Module (Phase Gate 2)
├── Construction Module (Phase Gate 3)
├── QA Module (Phase Gate 4)
├── Release Module (Phase Gate 5)
├── Governance Module (Gate Decision Engine, Process Orchestrator)
├── Artifacts Module (Evidence Graph, Artifact schemas)
├── Metrics Module (Scorecards, DORA/SPACE)
├── Integration Module (Provider Ports & ACLs)
└── Shared Infrastructure (Unit of Work, Event Bus + Outbox, Tenant Context)
↓
PostgreSQL (schema-per-context — tracker_*, see T-008)
↓
Evolith Core SDK/CLI/MCP
UMS Auth API
External ACL Adapters
### 2.1 Gate Evaluation and Decision Separation
Following the **Governed Composition** vision, Tracker enforces a strict separation of concerns regarding who evaluates and who decides:
- **TechnicalEvaluationResult:** Produced by stateless evaluators (CLI, MCP, Agents) against versioned rules. It is merely an assessment of evidence (`compliant`, `non_compliant`, `error`).
- **GateDecision:** The canonical governance decision owned exclusively by Tracker (`approved`, `rejected`, `approved_with_exception`). A green pipeline or successful agent response cannot independently advance a phase; Tracker must combine the evaluations, tenant policies, and human approvals to produce the final `GateDecision`.
Geo / Master-Data is a supporting/generic subdomain (reference data: regionalization, localization, ubigeo, currencies, taxes) — not an SDLC phase context. It replaces the free-field TenantLocalization with validated master catalogs, and is accessed only through the IMasterDataDirectory port + a soft-reference snapshot (geoId), never a cross-context JOIN. It owns its schema geo so it can be extracted to a shared evolith_mms service (satellite + platform service, like UMS) in one step when a second SaaS consumer appears. See T-032 and the Geo diagram.
Part III — Domain Model
4. Discovery Bounded Context
Ubiquitous Language
Initiative: Business container that consolidates an idea before splitting into epics
Discovery Canvas: Structured registration of problem, ROI, KPIs, risks
Approval Gate: Go/No-Go validation checkpoint
Backlog: Prioritized inventory of User Stories from approved Initiative
Requirement Checklist: Dynamic workflow steps (mandatory/optional) per tenant
Aggregate Roots
Aggregate
Identity
Responsibilities
Child Entities
Initiative
initiativeId: UUID
Canvas lifecycle, approval workflow, backlog generation, state transitions
// Command patterninterfaceCommand{readonlycommandId: string;readonlyoccurredAt: Date;}interfaceCommandHandler<TCommandextendsCommand,TResult>{handle(command: TCommand): Promise<Result<TResult>>;}// Query patterninterfaceQuery<TResponse>{readonlyqueryId: string;}interfaceQueryHandler<TQueryextendsQuery<TResponse>,TResponse>{handle(query: TQuery): Promise<Result<TResponse>>;}// Command Bus (infrastructure) — wraps every command in a Unit of Work
@Injectable()exportclassCommandBus{constructor(privatereadonlyhandlerRegistry: HandlerRegistry,privatereadonlyuow: IUnitOfWork,privatereadonlyoutbox: TransactionalOutbox,privatereadonlyeventBus: DomainEventBus,){}asyncexecute<TCommandextendsCommand>(command: TCommand): Promise<Result<unknown>>{consthandler=this.handlerRegistry.get(command.constructor);// Unit of Work: the handler's writes AND the outbox inserts commit atomically// in ONE database transaction. Either everything persists, or nothing does.constresult=awaitthis.uow.runInTransaction(async(tx)=>{consthandlerResult=awaithandler.handle(command,tx);if(handlerResult.isSuccess){// Domain events are persisted to the outbox INSIDE the same transaction.for(consteventofhandlerResult.value.domainEvents){awaitthis.outbox.addEvent(event,tx);}}returnhandlerResult;// throwing/returning failure rolls back the whole UoW});// Best-effort, low-latency in-process dispatch AFTER commit. This is an// OPTIMIZATION, not a guarantee — delivery is guaranteed by the OutboxProcessor// (see §13 caveat). A failure here is logged and ignored; the outbox will retry.if(result.isSuccess){voidthis.eventBus.publish(result.value.domainEvents).catch((err)=>this.logger.warn({msg: 'in-process dispatch failed; outbox will deliver', err }));}returnresult;}}
12.1 Unit of Work (Transactional Boundary)
The Unit of Work is the explicit transactional boundary for a single command. It guarantees that all aggregate mutations produced by a command handler — and the domain events emitted by those mutations — are committed atomically.
// Unit of Work port (domain/application-facing abstraction — no ORM leakage)exportinterfaceIUnitOfWork{// Runs `work` inside a single DB transaction. Commits on success;// rolls back if `work` throws or the returned Result is a failure.runInTransaction<T>(work: (tx: TransactionContext)=>Promise<Result<T>>,): Promise<Result<T>>;}// TransactionContext is the handle passed to repositories so every write in a// command joins the SAME transaction. Backed by TypeORM's EntityManager.exporttypeTransactionContext=EntityManager;// Infrastructure adapter (TypeORM — ADR-0043)
@Injectable()exportclassTypeOrmUnitOfWorkimplementsIUnitOfWork{constructor(privatereadonlydataSource: DataSource){}asyncrunInTransaction<T>(work: (tx: EntityManager)=>Promise<Result<T>>,): Promise<Result<T>>{returnthis.dataSource.transaction(async(tx)=>{constresult=awaitwork(tx);if(result.isFailure){// Force rollback without surfacing an exception to the caller.thrownewRollbackSignal(result);}returnresult;}).catch((err)=>{if(errinstanceofRollbackSignal)returnerr.resultasResult<T>;throwerr;// genuine infrastructure error — propagate});}}
Unit of Work rules:
Rule
Rationale
One command = one transaction = one Unit of Work
A command is the atomic consistency unit; partial commits are never allowed
Repositories receive the TransactionContext (tx) from the handler
All writes of a command share the same transaction; no autonomous repository transactions
A command mutates one aggregate root by default
Honors the Small Aggregates pattern; multi-aggregate writes require explicit justification
Outbox inserts happen inside the UoW
Domain events and state mutations are committed atomically (no lost events, no phantom events)
Cross-context effects are never in the same transaction
Other bounded contexts react asynchronously via the outbox — see §13
The domain layer never sees EntityManager
TransactionContext is injected at the application/infrastructure boundary only
WARNING: Critical caveat: The in-memory DomainEventBus does NOT guarantee delivery across process restarts or crashes. If the process dies after a command's transaction commits but before its in-process dispatch completes, those in-memory handlers do not run. The in-memory bus is a latency optimization, never the source of truth for cross-context propagation.
The delivery guarantee comes from the Transactional Outbox, not the in-memory bus. The two work together:
Mechanism
Guarantee
Use
In-memory dispatch (CommandBus, post-commit)
None (best-effort)
Low-latency same-process reactions when the process stays alive
Transactional Outbox + OutboxProcessor
At-least-once
The durable, restart-safe propagation path for every domain event
Resulting contract for event consumers:
Concern
Rule
Idempotency
Every event handler MUST be idempotent — the outbox is at-least-once, so an event may be delivered more than once (e.g. if the process dies between eventBus.publish and the processedAt update). Consumers dedupe on eventId.
Ordering
The OutboxProcessor reads ORDER BY createdAt ASC. Strict global ordering is not guaranteed across aggregates; handlers must not assume cross-aggregate order.
Recovery
On restart, the OutboxProcessor picks up every row where processedAt IS NULL — no event is lost, even if the in-memory dispatch never ran.
Cross-context events
Always travel via the outbox. A context never reacts to another context's change through the in-memory bus alone.
Phase 1 → future broker
When a real broker (Kafka/RabbitMQ) is introduced, only the OutboxProcessor's publish target changes. The outbox-write-inside-the-UoW contract is unchanged, so domain/application code is untouched.
This makes the Phase 1 messaging design forward-compatible: the DomainEventBus interface and the outbox contract already model at-least-once semantics, so swapping the in-memory implementation for a distributed broker is an infrastructure-only change (consistent with the Progressive Monolith → distributed evolution in the Scale-Out Strategy).
Key Principle: Tracker does not copy or store Evolith Core rulesets as a second source of truth. It queries the Core at runtime (with strategic caching) and validates artifacts against live rules.
18. UMS Authentication and Authorization Integration
Per T-002 and T-002, the frontend is a Microfrontend architecture from day 1 using Module Federation (Vite plugin @module-federation/vite or @originjs/vite-plugin-federation). A Shell Host orchestrates independently deployable React remotes, one per Bounded Context UI. The backend remains a single deployment (Progressive Monolith); only the frontend is physically segmented.