clean arch violations identified
This commit is contained in:
@@ -361,6 +361,144 @@ Inner layers:
|
||||
|
||||
This keeps the **automation engine** stable and reusable across presentation surfaces (companion today, web or backend orchestrators later), while allowing infrastructure and UI details to evolve without rewriting business logic.
|
||||
|
||||
### 4.2 Critical Clean Architecture Principle: Use Cases Do NOT Call Presenters
|
||||
|
||||
**The most important rule in Clean Architecture is that use cases must remain completely independent of presentation concerns.**
|
||||
|
||||
#### ❌ WRONG PATTERN (What NOT to do)
|
||||
|
||||
```typescript
|
||||
// ❌ VIOLATES CLEAN ARCHITECTURE
|
||||
class GetRaceDetailUseCase {
|
||||
constructor(
|
||||
private repositories: any,
|
||||
private output: UseCaseOutputPort<GetRaceDetailResult>
|
||||
) {}
|
||||
|
||||
async execute(input: GetRaceDetailInput): Promise<Result<void, ApplicationError>> {
|
||||
const race = await this.raceRepository.findById(input.raceId);
|
||||
|
||||
if (!race) {
|
||||
// WRONG: Use case calling presenter
|
||||
const result = Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
|
||||
this.output.present(result); // ❌ DON'T DO THIS
|
||||
return result;
|
||||
}
|
||||
|
||||
// WRONG: Use case calling presenter
|
||||
const result = Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
|
||||
this.output.present(result); // ❌ DON'T DO THIS
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this violates Clean Architecture:**
|
||||
- Use cases now **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
|
||||
// ✅ CLEAN ARCHITECTURE - Use case returns data, period
|
||||
class GetRaceDetailUseCase {
|
||||
constructor(
|
||||
private repositories: any,
|
||||
private output: UseCaseOutputPort<GetRaceDetailResult>
|
||||
) {}
|
||||
|
||||
async execute(input: GetRaceDetailInput): Promise<Result<GetRaceDetailResult, ApplicationError>> {
|
||||
const race = await this.raceRepository.findById(input.raceId);
|
||||
|
||||
if (!race) {
|
||||
return Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
|
||||
// NO .present() call! Just returns the Result.
|
||||
}
|
||||
|
||||
return Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
|
||||
// NO .present() call! Just returns the Result.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**The Controller/Wiring Layer (Infrastructure/Presentation):**
|
||||
|
||||
```typescript
|
||||
// ✅ Controller wires use case to presenter
|
||||
class RaceController {
|
||||
constructor(
|
||||
private getRaceDetailUseCase: GetRaceDetailUseCase,
|
||||
private raceDetailPresenter: RaceDetailPresenter
|
||||
) {}
|
||||
|
||||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailDTO> {
|
||||
// 1. Execute use case
|
||||
const result = await this.getRaceDetailUseCase.execute(params);
|
||||
|
||||
// 2. Pass result to presenter (wiring happens here)
|
||||
this.raceDetailPresenter.present(result);
|
||||
|
||||
// 3. Get ViewModel from presenter
|
||||
return this.raceDetailPresenter.viewModel;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 The "Presenter Not Presented" Error Explained
|
||||
|
||||
Your current architecture has this error because:
|
||||
|
||||
1. **Use cases call `.present()`** (violating Clean Architecture)
|
||||
2. **Controllers expect presenters to have `.viewModel`**
|
||||
3. **But if use case returns early on error without calling `.present()`**, the presenter never gets data
|
||||
4. **Controller tries to access `.viewModel`** → throws "Presenter not presented"
|
||||
|
||||
**The fix is NOT to add more `.present()` calls to use cases. The fix is to remove ALL `.present()` calls from use cases.**
|
||||
|
||||
### 4.4 Your Adapter Pattern is a Smokescreen
|
||||
|
||||
Your current code uses adapter classes like:
|
||||
|
||||
```typescript
|
||||
class RaceDetailOutputAdapter implements UseCaseOutputPort<GetRaceDetailResult> {
|
||||
constructor(private presenter: RaceDetailPresenter) {}
|
||||
|
||||
present(result: GetRaceDetailResult): void {
|
||||
this.presenter.present(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**This is just hiding the crime.** The adapter still couples the use case to the presenter concept. The real Clean Architecture approach eliminates these adapters entirely and has controllers do the wiring.
|
||||
|
||||
### 4.5 The Real Clean Architecture Flow
|
||||
|
||||
```
|
||||
1. Controller receives HTTP request
|
||||
2. Controller calls UseCase.execute()
|
||||
3. UseCase returns Result<T, E> (no presenter knowledge)
|
||||
4. Controller passes Result to Presenter
|
||||
5. Presenter transforms Result → ViewModel
|
||||
6. Controller returns ViewModel to HTTP layer
|
||||
```
|
||||
|
||||
**Key insight:** The use case's `output` port should be **the Result itself**, not a presenter. The controller is responsible for taking that Result and passing it to the appropriate presenter.
|
||||
|
||||
### 4.6 What This Means for Your Codebase
|
||||
|
||||
**To achieve 100% Clean Architecture, you must:**
|
||||
|
||||
1. **Remove all `.present()` calls from use cases** - they should only return Results
|
||||
2. **Remove all adapter classes** - they're unnecessary coupling
|
||||
3. **Make controllers wire use cases to presenters** - this is where the "glue" belongs
|
||||
4. **Use cases return Results, period** - they don't know about presenters, viewmodels, or HTTP
|
||||
|
||||
**This is the ONLY way to achieve true Clean Architecture.** Any pattern where use cases call presenters is **not Clean Architecture**, regardless of how many adapter layers you add.
|
||||
|
||||
The "presenter not presented" error is a **symptom** of this architectural violation, not the root problem.
|
||||
|
||||
---
|
||||
|
||||
## 4. Layer-by-Layer Mapping
|
||||
@@ -717,11 +855,11 @@ This section describes how a typical hosted-session automation run flows through
|
||||
- The automation engine (implemented by [`AutomationEngineAdapter`](core/infrastructure/adapters/automation/engine/AutomationEngineAdapter.ts:1) and backed by [`PlaywrightAutomationAdapter`](core/infrastructure/adapters/automation/core/PlaywrightAutomationAdapter.ts:1)) proceeds through steps:
|
||||
|
||||
- Navigate to hosted sessions.
|
||||
- Open “Create a Race” and the hosted-session wizard.
|
||||
- Open "Create a Race" and the hosted-session wizard.
|
||||
- For each step (race information, server details, admins, cars, tracks, weather, race options, conditions):
|
||||
- Ensure the correct page is active using [`PageStateValidator`](core/domain/services/PageStateValidator.ts:1) and selectors from [`IRACING_SELECTORS`](core/infrastructure/adapters/automation/dom/IRacingSelectors.ts:1).
|
||||
- Fill fields and toggles using [`IRacingDomInteractor`](core/infrastructure/adapters/automation/dom/IRacingDomInteractor.ts:1).
|
||||
- Click the correct “Next” / “Create Race” / “New Race” buttons, guarded by [`SafeClickService`](core/infrastructure/adapters/automation/dom/SafeClickService.ts:1) and blocked-selector logic.
|
||||
- Click the correct "Next" / "Create Race" / "New Race" buttons, guarded by [`SafeClickService`](core/infrastructure/adapters/automation/dom/SafeClickService.ts:1) and blocked-selector logic.
|
||||
|
||||
- At each step, the Playwright adapter:
|
||||
- Updates the overlay via `updateOverlay(step, message)`.
|
||||
@@ -741,6 +879,60 @@ This section describes how a typical hosted-session automation run flows through
|
||||
- The companion renderer may present a [`RaceCreationResult`](core/domain/value-objects/RaceCreationResult.ts:1) via [`RaceCreationSuccessScreen`](apps/companion/renderer/components/RaceCreationSuccessScreen.tsx).
|
||||
- The browser context is closed or re-used based on mode and configuration; debug artifacts may be written by the Playwright adapter for failed runs.
|
||||
|
||||
### 5.2 Clean Architecture Flow Example
|
||||
|
||||
**The correct Clean Architecture flow for use cases:**
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - Current broken pattern
|
||||
class GetRaceDetailUseCase {
|
||||
async execute(input: GetRaceDetailInput): Promise<Result<void, ApplicationError>> {
|
||||
const race = await this.raceRepository.findById(input.raceId);
|
||||
|
||||
if (!race) {
|
||||
const result = Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
|
||||
this.output.present(result); // ❌ Use case calling presenter
|
||||
return result;
|
||||
}
|
||||
|
||||
const result = Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
|
||||
this.output.present(result); // ❌ Use case calling presenter
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECT - Clean Architecture
|
||||
class GetRaceDetailUseCase {
|
||||
async execute(input: GetRaceDetailInput): Promise<Result<GetRaceDetailResult, ApplicationError>> {
|
||||
const race = await this.raceRepository.findById(input.raceId);
|
||||
|
||||
if (!race) {
|
||||
return Result.err({ code: 'RACE_NOT_FOUND', details: {...} });
|
||||
// ✅ No .present() call - just returns Result
|
||||
}
|
||||
|
||||
return Result.ok({ race, league, registrations, drivers, userResult, isUserRegistered, canRegister });
|
||||
// ✅ No .present() call - just returns Result
|
||||
}
|
||||
}
|
||||
|
||||
// Controller wiring (in infrastructure/presentation layer)
|
||||
class RaceController {
|
||||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailDTO> {
|
||||
// 1. Call use case
|
||||
const result = await this.getRaceDetailUseCase.execute(params);
|
||||
|
||||
// 2. Wire to presenter
|
||||
this.raceDetailPresenter.present(result);
|
||||
|
||||
// 3. Return ViewModel
|
||||
return this.raceDetailPresenter.viewModel;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**This is the ONLY pattern that respects Clean Architecture.** Your current architecture violates this fundamental principle, which is why you have the "presenter not presented" problem.
|
||||
|
||||
---
|
||||
|
||||
## 6. How This Serves Admins, Drivers, Teams
|
||||
|
||||
Reference in New Issue
Block a user