This commit is contained in:
2026-01-11 13:04:33 +01:00
parent 6f2ab9fc56
commit 971aa7288b
44 changed files with 2168 additions and 1240 deletions
+244
View File
@@ -0,0 +1,244 @@
CQRS Light with Clean Architecture
This document defines CQRS Light as a pragmatic, production-ready approach that integrates cleanly with Clean Architecture.
It is intentionally non-dogmatic, avoids event-sourcing overhead, and focuses on clarity, performance, and maintainability.
⸻
1. What CQRS Light Is
CQRS Light separates how the system writes data from how it reads data — without changing the core architecture.
Key properties:
• Commands and Queries are separated logically, not infrastructurally
• Same database is allowed
• No event bus required
• No eventual consistency by default
CQRS Light is an optimization, not a foundation.
⸻
2. What CQRS Light Is NOT
CQRS Light explicitly does not include:
• Event Sourcing
• Message brokers
• Projections as a hard requirement
• Separate databases
• Microservices
Those can be added later if needed.
⸻
3. Why CQRS Light Exists
Without CQRS:
• Reads are forced through domain aggregates
• Aggregates grow unnaturally large
• Reporting logic pollutes the domain
• Performance degrades due to object loading
CQRS Light solves this by allowing:
• Strict domain logic on writes
• Flexible, optimized reads
⸻
4. Core Architectural Principle
Writes protect invariants. Reads optimize information access.
Therefore:
• Commands enforce business rules
• Queries are allowed to be pragmatic and denormalized
⸻
5. Placement in Clean Architecture
CQRS Light does not introduce new layers.
It reorganizes existing ones.
core/
└── <context>/
└── application/
├── commands/ # Write side (Use Cases)
└── queries/ # Read side (Query Use Cases)
Domain remains unchanged.
⸻
6. Command Side (Write Model)
Purpose
• Modify state
• Enforce invariants
• Emit outcomes
Characteristics
• Uses Domain Entities and Value Objects
• Uses Repositories
• Uses Output Ports
• Transactional
Example Structure
core/racing/application/commands/
├── CreateLeagueUseCase.ts
├── ApplyPenaltyUseCase.ts
└── RegisterForRaceUseCase.ts
⸻
7. Query Side (Read Model)
Purpose
• Read state
• Aggregate data
• Serve UI efficiently
Characteristics
• No domain entities
• No invariants
• No side effects
• May use SQL/ORM directly
Example Structure
core/racing/application/queries/
├── GetLeagueStandingsQuery.ts
├── GetDashboardOverviewQuery.ts
└── GetDriverStatsQuery.ts
Queries are still Use Cases, just read-only ones.
⸻
8. Repositories in CQRS Light
Write Repositories
• Used by command use cases
• Work with domain entities
• Enforce consistency
core/racing/domain/repositories/
└── LeagueRepositoryPort.ts
⸻
Read Repositories
• Used by query use cases
• Return flat, optimized data
• Not domain repositories
core/racing/application/ports/
└── LeagueStandingsReadPort.ts
Implementation lives in adapters.
⸻
9. Performance Benefits (Why It Matters)
Without CQRS Light
• Aggregate loading
• N+1 queries
• Heavy object graphs
• CPU and memory overhead
With CQRS Light
• Single optimized queries
• Minimal data transfer
• Database does aggregation
• Lower memory footprint
This results in:
• Faster endpoints
• Simpler code
• Easier scaling
⸻
10. Testing Strategy
Commands
• Unit tests
• Mock repositories and ports
• Verify output port calls
Queries
• Simple unit tests
• Input → Output verification
• No mocks beyond data source
CQRS Light reduces the need for complex integration tests.
⸻
11. When CQRS Light Is a Good Fit
Use CQRS Light when:
• Read complexity is high
• Write logic must stay strict
• Dashboards or analytics exist
• Multiple clients consume the system
Avoid CQRS Light when:
• Application is CRUD-only
• Data volume is small
• Read/write patterns are identical
⸻
12. Migration Path
CQRS Light allows incremental adoption:
1. Start with classic Clean Architecture
2. Separate commands and queries logically
3. Optimize read paths as needed
4. Introduce events or projections later (optional)
No rewrites required.
⸻
13. Key Rules (Non-Negotiable)
• Commands MUST use the domain
• Queries MUST NOT modify state
• Queries MUST NOT enforce invariants
• Domain MUST NOT depend on queries
• Core remains framework-agnostic
⸻
14. Mental Model
Think of CQRS Light as:
One core truth, two access patterns.
The domain defines truth.
Queries define convenience.
⸻
15. Final Summary
CQRS Light provides:
• Cleaner domain models
• Faster reads
• Reduced complexity
• Future scalability
Without:
• Infrastructure overhead
• Event sourcing complexity
• Premature optimization
It is the safest way to gain CQRS benefits while staying true to Clean Architecture.
+264
View File
@@ -0,0 +1,264 @@
Domain Objects Design Guide (Clean Architecture)
This document defines all domain object types used in the Core and assigns strict responsibilities and boundaries.
Its goal is to remove ambiguity between:
• Entities
• Value Objects
• Aggregate Roots
• Domain Services
• Domain Events
The rules in this document are non-negotiable.
⸻
Core Principle
Domain objects represent business truth.
They:
• outlive APIs and UIs
• must remain stable over time
• must not depend on technical details
If a class answers a business question, it belongs here.
⸻
1. Entities
Definition
An Entity is a domain object that:
• has a stable identity
• changes over time
• represents a business concept
Identity matters more than attributes.
⸻
Responsibilities
Entities MUST:
• own their identity
• enforce invariants on state changes
• expose behavior, not setters
Entities MUST NOT:
• depend on DTOs or transport models
• access repositories or services
• perform IO
• know about frameworks
⸻
Creation Rules
• New entities are created via create()
• Existing entities are reconstructed via rehydrate()
core/<context>/domain/entities/
⸻
Example
• League
• Season
• Race
• Driver
⸻
2. Value Objects
Definition
A Value Object is a domain object that:
• has no identity
• is immutable
• is defined by its value
⸻
Responsibilities
Value Objects MUST:
• validate their own invariants
• be immutable
• be comparable by value
Value Objects MUST NOT:
• contain business workflows
• reference entities
• perform IO
⸻
Creation Rules
• create() for new domain meaning
• fromX() for interpreting external formats
core/<context>/domain/value-objects/
⸻
Example
• Money
• LeagueName
• RaceTimeOfDay
• SeasonSchedule
⸻
3. Aggregate Roots
Definition
An Aggregate Root is an entity that:
• acts as the consistency boundary
• protects invariants across related entities
All access to the aggregate happens through the root.
⸻
Responsibilities
Aggregate Roots MUST:
• enforce consistency rules
• control modifications of child entities
Aggregate Roots MUST NOT:
• expose internal collections directly
• allow partial updates bypassing rules
⸻
Example
• League (root)
• Season (root)
⸻
4. Domain Services
Definition
A Domain Service encapsulates domain logic that:
• does not naturally belong to a single entity
• involves multiple domain objects
⸻
Responsibilities
Domain Services MAY:
• coordinate entities
• calculate derived domain values
Domain Services MUST:
• operate only on domain types
• remain stateless
Domain Services MUST NOT:
• access repositories
• orchestrate use cases
• perform IO
core/<context>/domain/services/
⸻
Example
• SeasonConfigurationFactory
• ChampionshipAggregator
• StrengthOfFieldCalculator
⸻
5. Domain Events
Definition
A Domain Event represents something that:
• has already happened
• is important to the business
⸻
Responsibilities
Domain Events MUST:
• be immutable
• carry minimal information
Domain Events MUST NOT:
• contain behavior
• perform side effects
core/<context>/domain/events/
⸻
Example
• RaceCompleted
• SeasonActivated
⸻
6. What Does NOT Belong in Domain Objects
❌ DTOs
❌ API Models
❌ View Models
❌ Repositories
❌ Framework Types
❌ Logging
❌ Configuration
If it depends on infrastructure, it does not belong here.
⸻
7. Dependency Rules
Entities → Value Objects
Entities → Domain Services
Domain Services → Entities
Reverse dependencies are forbidden.
⸻
8. Testing Requirements
Domain Objects MUST:
• have unit tests for invariants
• be tested without mocks
Domain Services MUST:
• have deterministic tests
⸻
Mental Model (Final)
Entities protect state.
Value Objects protect meaning.
Aggregate Roots protect consistency.
Domain Services protect cross-entity rules.
Domain Events describe facts.
⸻
Final Summary
• Domain objects represent business truth
• They are pure and framework-free
• They form the most stable part of the system
If domain objects are clean, everything else becomes easier.
+339
View File
@@ -0,0 +1,339 @@
Enums in Clean Architecture (Strict & Final)
This document defines how enums are modeled, placed, and used in a strict Clean Architecture setup.
Enums are one of the most common sources of architectural leakage. This guide removes all ambiguity.
⸻
1. Core Principle
Enums represent knowledge.
Knowledge must live where it is true.
Therefore:
• Not every enum is a domain enum
• Enums must never cross architectural boundaries blindly
• Ports must remain neutral
⸻
2. Enum Categories (Authoritative)
There are four and only four valid enum categories:
1. Domain Enums
2. Application (Workflow) Enums
3. Transport Enums (API)
4. UI Enums (Frontend)
Each category has strict placement and usage rules.
⸻
3. Domain Enums
Definition
A Domain Enum represents a business concept that:
• has meaning in the domain
• affects rules or invariants
• is part of the ubiquitous language
Examples:
• LeagueVisibility
• MembershipRole
• RaceStatus
• SponsorshipTier
• PenaltyType
⸻
Placement
core/<context>/domain/
├── value-objects/
│ └── LeagueVisibility.ts
└── entities/
Preferred: model domain enums as Value Objects instead of enum keywords.
⸻
Example (Value Object)
export class LeagueVisibility {
private constructor(private readonly value: 'public' | 'private') {}
static from(value: string): LeagueVisibility {
if (value !== 'public' && value !== 'private') {
throw new DomainError('Invalid LeagueVisibility');
}
return new LeagueVisibility(value);
}
isPublic(): boolean {
return this.value === 'public';
}
}
⸻
Usage Rules
Allowed:
• Domain
• Use Cases
Forbidden:
• Ports
• Adapters
• API DTOs
• Frontend
Domain enums must never cross a Port boundary.
⸻
4. Application Enums (Workflow Enums)
Definition
Application Enums represent internal workflow or state coordination.
They are not business truth and must not leak.
Examples:
• LeagueSetupStep
• ImportPhase
• ProcessingState
⸻
Placement
core/<context>/application/internal/
└── LeagueSetupStep.ts
⸻
Example
export enum LeagueSetupStep {
CreateLeague,
CreateSeason,
AssignOwner,
Notify
}
⸻
Usage Rules
Allowed:
• Application Services
• Use Cases
Forbidden:
• Domain
• Ports
• Adapters
• Frontend
These enums must remain strictly internal.
⸻
5. Transport Enums (API DTOs)
Definition
Transport Enums describe allowed values in HTTP contracts.
They exist purely to constrain transport data, not to encode business rules.
Naming rule:
Transport enums MUST end with Enum.
This makes enums immediately recognizable in code reviews and prevents silent leakage.
Examples:
• LeagueVisibilityEnum
• SponsorshipStatusEnum
• PenaltyTypeEnum
⸻
Placement
apps/api/<feature>/dto/
└── LeagueVisibilityEnum.ts
Website mirrors the same naming:
apps/website/lib/dtos/
└── LeagueVisibilityEnum.ts
⸻
Example
export enum LeagueVisibilityEnum {
Public = 'public',
Private = 'private'
}
⸻
Usage Rules
Allowed:
• API Controllers
• API Presenters
• Website API DTOs
Forbidden:
• Core Domain
• Use Cases
Transport enums are copies, never reexports of domain enums.
⸻
Placement
apps/api/<feature>/dto/
└── LeagueVisibilityDto.ts
or inline as union types in DTOs.
⸻
Example
export type LeagueVisibilityDto = 'public' | 'private';
⸻
Usage Rules
Allowed:
• API Controllers
• API Presenters
• Website API DTOs
Forbidden:
• Core Domain
• Use Cases
Transport enums are copies, never reexports of domain enums.
⸻
6. UI Enums (Frontend)
Definition
UI Enums describe presentation or interaction state.
They have no business meaning.
Examples:
• WizardStep
• SortOrder
• ViewMode
• TabKey
⸻
Placement
apps/website/lib/ui/
└── LeagueWizardStep.ts
⸻
Example
export enum LeagueWizardStep {
Basics,
Structure,
Scoring,
Review
}
⸻
Usage Rules
Allowed:
• Frontend only
Forbidden:
• Core
• API
⸻
7. Absolute Prohibitions
❌ Enums in Ports
// ❌ forbidden
export interface CreateLeagueInputPort {
visibility: LeagueVisibility;
}
✅ Correct
export interface CreateLeagueInputPort {
visibility: 'public' | 'private';
}
Mapping happens inside the Use Case:
const visibility = LeagueVisibility.from(input.visibility);
⸻
8. Decision Checklist
Ask these questions:
1. Does changing this enum change business rules?
• Yes → Domain Enum
• No → continue
2. Is it only needed for internal workflow coordination?
• Yes → Application Enum
• No → continue
3. Is it part of an HTTP contract?
• Yes → Transport Enum
• No → continue
4. Is it purely for UI state?
• Yes → UI Enum
⸻
9. Summary Table
Enum Type Location May Cross Ports Scope
Domain Enum core/domain ❌ No Business rules
Application Enum core/application ❌ No Workflow only
Transport Enum apps/api + website ❌ No HTTP contracts
UI Enum apps/website ❌ No Presentation only
⸻
10. Final Rule (Non-Negotiable)
If an enum crosses a boundary, it is in the wrong place.
This rule alone prevents most long-term architectural decay.
+513
View File
@@ -0,0 +1,513 @@
Use Case Architecture Guide
This document defines the correct structure and responsibilities of Application Use Cases
according to Clean Architecture, in a NestJS-based system.
The goal is:
• strict separation of concerns
• correct terminology (no fake "ports")
• minimal abstractions
• long-term consistency
This is the canonical reference for all use cases in this codebase.
~
1. Core Concepts (Authoritative Definitions)
Use Case
• Encapsulates application-level business logic
• Is the Input Port
• Is injected via DI
• Knows no API, no DTOs, no transport
• Coordinates domain objects and infrastructure
The public execute() method is the input port.
~
Input
• Pure data
• Not a port
• Not an interface
• May be omitted if the use case has no parameters
type GetSponsorsInput = {
leagueId: LeagueId
}
~
Result
• The business outcome of a use case
• May contain Entities and Value Objects
• Not a DTO
• Never leaves the core directly
type GetSponsorsResult = {
sponsors: Sponsor[]
}
~
Output Port
• A behavioral boundary
• Defines how the core communicates outward
• Never a data structure
• Lives in the Application Layer
export interface UseCaseOutputPort<T> {
present(data: T): void
}
~
Presenter
• Implements UseCaseOutputPort<T>
• Lives in the API / UI layer
• Translates Result → ViewModel / DTO
• Holds internal state
• Is pulled by the controller after execution
~
2. Canonical Use Case Structure
Application Layer
Use Case
@Injectable()
export class GetSponsorsUseCase {
constructor(
private readonly sponsorRepository: ISponsorRepository,
private readonly output: UseCaseOutputPort<GetSponsorsResult>,
) {}
async execute(): Promise<Result<void, ApplicationError>> {
const sponsors = await this.sponsorRepository.findAll()
this.output.present({ sponsors })
return Result.ok(undefined)
}
}
Rules:
• execute() is the Input Port
• The use case does not return result data
• All output flows through the OutputPort
• The return value signals success or failure only
### ⚠️ ARCHITECTURAL VIOLATION ALERT
**The pattern shown above is INCORRECT and violates Clean Architecture.**
#### ❌ WRONG PATTERN (What NOT to do)
```typescript
@Injectable()
export class GetSponsorsUseCase {
constructor(
private readonly sponsorRepository: ISponsorRepository,
private readonly output: UseCaseOutputPort<GetSponsorsResult>,
) {}
async execute(): Promise<Result<void, ApplicationError>> {
const sponsors = await this.sponsorRepository.findAll()
this.output.present({ sponsors }) // ❌ WRONG: Use case calling presenter
return Result.ok(undefined)
}
}
```
**Why this violates Clean Architecture:**
- Use cases **know about presenters** and how to call them
- Creates **tight coupling** between application logic and presentation
- Makes use cases **untestable** without mocking presenters
- Violates the **Dependency Rule** (inner layer depending on outer layer behavior)
#### ✅ CORRECT PATTERN (Clean Architecture)
```typescript
@Injectable()
export class GetSponsorsUseCase {
constructor(
private readonly sponsorRepository: ISponsorRepository,
// NO output port needed in constructor
) {}
async execute(): Promise<Result<GetSponsorsResult, ApplicationError>> {
const sponsors = await this.sponsorRepository.findAll()
return Result.ok({ sponsors })
// ✅ Returns Result, period. No .present() call.
}
}
```
**The Controller (in API layer) handles the wiring:**
```typescript
@Controller('/sponsors')
export class SponsorsController {
constructor(
private readonly useCase: GetSponsorsUseCase,
private readonly presenter: GetSponsorsPresenter,
) {}
@Get()
async getSponsors() {
// 1. Execute use case
const result = await this.useCase.execute()
if (result.isErr()) {
throw mapApplicationError(result.unwrapErr())
}
// 2. Wire to presenter
this.presenter.present(result.value)
// 3. Return ViewModel
return this.presenter.getViewModel()
}
}
```
**This is the ONLY pattern that respects Clean Architecture.**
~
Result Model
type GetSponsorsResult = {
sponsors: Sponsor[]
}
Rules:
• Domain objects are allowed
• No DTOs
• No interfaces
• No transport concerns
~
3. API Layer
API Services / Controllers (Thin Orchestration)
The API layer is a transport boundary. It MUST delegate business logic to `./core`:
• orchestrate auth + authorization checks (actor/session/roles)
• collect/validate transport input (DTOs at the boundary)
• execute a Core use case (entities/value objects live here)
• map Result → DTO / ViewModel via a Presenter (presenter owns mapping)
Rules:
• Controllers stay thin: no business rules, no domain validation, no decision-making
• API services orchestrate: auth + use case execution + presenter mapping
• Domain objects never cross the API boundary un-mapped
Presenter
@Injectable()
export class GetSponsorsPresenter
implements UseCaseOutputPort<GetSponsorsResult>
{
private viewModel!: GetSponsorsViewModel
present(result: GetSponsorsResult): void {
this.viewModel = {
sponsors: result.sponsors.map(s => ({
id: s.id.value,
name: s.name,
websiteUrl: s.websiteUrl,
})),
}
}
getViewModel(): GetSponsorsViewModel {
return this.viewModel
}
}
~
Controller
@Controller('/sponsors')
export class SponsorsController {
constructor(
private readonly useCase: GetSponsorsUseCase,
private readonly presenter: GetSponsorsPresenter,
) {}
@Get()
async getSponsors() {
const result = await this.useCase.execute()
if (result.isErr()) {
throw mapApplicationError(result.unwrapErr())
}
return this.presenter.getViewModel()
}
}
~
Payments Example
Application Layer
Use Case
@Injectable()
export class CreatePaymentUseCase {
constructor(
private readonly paymentRepository: IPaymentRepository,
private readonly output: UseCaseOutputPort<CreatePaymentResult>,
) {}
async execute(input: CreatePaymentInput): Promise<Result<void, ApplicationErrorCode<CreatePaymentErrorCode>>> {
// business logic
const payment = await this.paymentRepository.create(payment);
this.output.present({ payment });
return Result.ok(undefined);
}
}
Result Model
type CreatePaymentResult = {
payment: Payment;
};
API Layer
Presenter
@Injectable()
export class CreatePaymentPresenter
implements UseCaseOutputPort<CreatePaymentResult>
{
private viewModel: CreatePaymentViewModel | null = null;
present(result: CreatePaymentResult): void {
this.viewModel = {
payment: this.mapPaymentToDto(result.payment),
};
}
getViewModel(): CreatePaymentViewModel | null {
return this.viewModel;
}
reset(): void {
this.viewModel = null;
}
private mapPaymentToDto(payment: Payment): PaymentDto {
return {
id: payment.id,
// ... other fields
};
}
}
Controller
@Controller('/payments')
export class PaymentsController {
constructor(
private readonly useCase: CreatePaymentUseCase,
private readonly presenter: CreatePaymentPresenter,
) {}
@Post()
async createPayment(@Body() input: CreatePaymentInput) {
const result = await this.useCase.execute(input);
if (result.isErr()) {
throw mapApplicationError(result.unwrapErr());
}
return this.presenter.getViewModel();
}
}
~
4. Module Wiring (Composition Root)
@Module({
providers: [
GetSponsorsUseCase,
GetSponsorsPresenter,
{
provide: USE_CASE_OUTPUT_PORT,
useExisting: GetSponsorsPresenter,
},
],
})
export class SponsorsModule {}
Rules:
• The use case depends only on the OutputPort interface
• The presenter is bound as the OutputPort implementation
• process.env is not used inside the use case
~
5. Explicitly Forbidden
❌ DTOs in use cases
❌ Domain objects returned directly to the API
❌ Output ports used as data structures
❌ present() returning a value
❌ Input data named InputPort
❌ Mapping logic inside use cases
❌ Environment access inside the core
~
Do / Don’t (Boundary Examples)
✅ DO: Keep pages/components consuming ViewModels returned by website services (DTOs stop at the service boundary), e.g. [LeagueAdminSchedulePage()](apps/website/app/leagues/[id]/schedule/admin/page.tsx:12).
✅ DO: Keep controllers/services thin and delegating, e.g. [LeagueController.createLeagueSeasonScheduleRace()](apps/api/src/domain/league/LeagueController.ts:291).
❌ DON’T: Put business rules in the API layer; rules belong in `./core` use cases/entities/value objects, e.g. [CreateLeagueSeasonScheduleRaceUseCase.execute()](core/racing/application/use-cases/CreateLeagueSeasonScheduleRaceUseCase.ts:38).
~
6. Optional Extensions
Custom Output Ports
Only introduce a dedicated OutputPort interface if:
• multiple presentation paths exist
• streaming or progress updates are required
• more than one output method is needed
interface ComplexOutputPort {
presentSuccess(...)
presentFailure(...)
}
~
Input Port Interfaces
Only introduce an explicit InputPort interface if:
• multiple implementations of the same use case exist
• feature flags or A/B variants are required
• the use case itself must be substituted
Otherwise:
The use case class itself is the input port.
~
7. Key Rules (Memorize These)
Use cases answer what.
Presenters answer how.
Ports have behavior.
Data does not.
The core produces truth.
The API interprets it.
~
TL;DR
• Use cases are injected via DI
• execute() is the Input Port
• Outputs flow only through Output Ports
• Results are business models, not DTOs
• Interfaces exist only for behavior variability
### 🚨 CRITICAL CLEAN ARCHITECTURE CORRECTION
**The examples in this document (sections 2, 3, and the Payments Example) demonstrate the WRONG pattern that violates Clean Architecture.**
#### The Fundamental Problem
The current architecture shows use cases **calling presenters directly**:
```typescript
// ❌ WRONG - This violates Clean Architecture
this.output.present({ sponsors })
```
**This is architecturally incorrect.** Use cases must **never** know about presenters or call `.present()`.
#### The Correct Clean Architecture Pattern
**Use cases return Results. Controllers wire them to presenters.**
```typescript
// ✅ CORRECT - Use case returns data
@Injectable()
export class GetSponsorsUseCase {
constructor(private readonly sponsorRepository: ISponsorRepository) {}
async execute(): Promise<Result<GetSponsorsResult, ApplicationError>> {
const sponsors = await this.sponsorRepository.findAll()
return Result.ok({ sponsors })
// NO .present() call!
}
}
// ✅ CORRECT - Controller handles wiring
@Controller('/sponsors')
export class SponsorsController {
constructor(
private readonly useCase: GetSponsorsUseCase,
private readonly presenter: GetSponsorsPresenter,
) {}
@Get()
async getSponsors() {
const result = await this.useCase.execute()
if (result.isErr()) {
throw mapApplicationError(result.unwrapErr())
}
this.presenter.present(result.value)
return this.presenter.getViewModel()
}
}
```
#### Why This Matters
1. **Dependency Rule**: Inner layers (use cases) cannot depend on outer layers (presenters)
2. **Testability**: Use cases can be tested without mocking presenters
3. **Flexibility**: Same use case can work with different presenters
4. **Separation of Concerns**: Use cases do business logic, presenters do transformation
#### What Must Be Fixed
**All use cases in the codebase must be updated to:**
1. **Remove** the `output: UseCaseOutputPort<T>` constructor parameter
2. **Return** `Result<T, E>` directly from `execute()`
3. **Remove** all `this.output.present()` calls
**All controllers must be updated to:**
1. **Call** the use case and get the Result
2. **Pass** `result.value` to the presenter's `.present()` method
3. **Return** the presenter's `.getViewModel()`
This is the **single source of truth** for correct Clean Architecture in this project.