agent-ecosystem/docs/research/codex-dashboard-recent-projects-plan.md

1260 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Codex Dashboard Recent Projects Plan
**Дата**: 2026-04-14
**Статус**: Reference-quality architecture plan
**Goal**: сделать `Dashboard -> Recent Projects` эталонной feature по `SOLID`, `Clean Architecture`, `Ports/Adapters`, `DRY`
**Canonical standard**: этот документ фиксирует рекомендуемый shape для будущих feature в проекте
## Executive Summary
Выбранный вариант:
`Full vertical slice in src/features with core separated from process adapters`
`🎯 9 🛡️ 10 🧠 8`
Примерно `700-1000` строк изменений
Это решение фиксируется как **предпочтительный architectural template для будущих feature**, если feature:
- затрагивает более одного process boundary
- содержит заметную бизнес-логику
- имеет отдельный use case, который хочется развивать независимо
### Что это значит
- создаём feature `src/features/recent-projects`
- внутри feature есть **свои слои**:
- `contracts`
- `core/domain`
- `core/application`
- `main/composition`
- `main/adapters`
- `main/infrastructure`
- `preload`
- `renderer`
- app shell только **собирает** feature, но не владеет её логикой
### Главная корректировка относительно прошлого плана
Прошлая версия уже была неплохой, но для "эталонной" фичи ей не хватало жёсткости в четырёх местах:
1. use case был описан слишком близко к DTO, а не как application core
2. adapters и infrastructure были недостаточно разведены
3. не был явно сформулирован набор архитектурных запретов
4. inner circles были слишком близко привязаны к `main`, хотя это не main-specific business logic
Этот документ исправляет именно это.
---
## 1. Top 3 Architecture Options
### 1. Full vertical slice + strict Clean Architecture inside the feature
`🎯 9 🛡️ 10 🧠 8`
Примерно `700-1000` строк
Идея:
- feature lives in one place
- core/domain and core/application isolated from process details
- ports explicit
- adapters explicit
- process boundaries preserved inside the feature
Почему это лучший вариант:
- лучший long-term shape
- легко расширять новыми providers
- легче удерживать логику фичи в одном bounded context
- это реально можно показывать как reference implementation
### 2. Vertical slice, но без жёсткого разделения `application / adapters / infrastructure`
`🎯 7 🛡️ 7 🧠 6`
Примерно `500-750` строк
Плюсы:
- меньше файлов
- быстрее в реализации
Минусы:
- быстро поплывут ответственности
- adapters начнут смешиваться с use case
- через несколько итераций feature станет "папкой со всем подряд"
### 3. Renderer feature + main service outside feature
`🎯 8 🛡️ 8 🧠 6`
Примерно `450-700` строк
Плюсы:
- проще
- достаточно хорошо для одной задачи
Минусы:
- хуже ощущается как единая feature
- логика снова размазывается по repo
### Final choice
Берём **вариант 1**.
---
## 2. Product Goal
Когда пользователь открывает homepage, он должен увидеть **последние проекты, где недавно реально работал**, включая:
- Claude activity
- native Codex activity
- merged карточки, если это один repo или folder
### Acceptance criteria
1. Claude-only пользователь не видит поведенческой регрессии.
2. Codex-only пользователь видит свои проекты на homepage.
3. Claude + Codex в одном repo не создают дубликатов.
4. Карточка показывает provider logos.
5. Если standalone `codex` отсутствует или `codex app-server` не стартует, homepage спокойно деградирует в Claude-only.
6. В `ssh` context локальные native Codex проекты не подмешиваются.
7. `DashboardView` остаётся экраном-компоновщиком, а не бизнес-слоем.
---
## 3. Non-Goals For Phase 1
Не делаем:
- native Codex sessions в sidebar
- native Codex session detail opening
- model badges
- file-based Codex session index
- persistent `codex app-server`
- live notifications from Codex
- global provider-agnostic session index
- injection Codex data into `repositoryGroups`
---
## 4. Architecture Standards For This Feature
## 4.1 Single Responsibility
Каждый модуль меняется по одной причине:
- `domain` - business rules recent projects
- `application` - orchestration use case
- `adapters` - translation in/out
- `infrastructure` - конкретные технологии и внешние системы
- `input adapters` - IPC/HTTP wiring
- `preload` - renderer bridge
- `renderer/ui` - rendering
- `renderer/hooks` - interaction orchestration
## 4.2 Open / Closed
Новый provider должен подключаться новым source adapter, а не переписыванием use case.
## 4.3 Liskov
Любой source adapter должен удовлетворять одному и тому же порту:
- Claude source
- Codex source
- будущий Gemini source
## 4.4 Interface Segregation
Порты должны быть узкими:
- `RecentProjectsSourcePort`
- `RecentProjectsCachePort`
- `ClockPort`
- `LoggerPort`
- `ListDashboardRecentProjectsOutputPort`
Не должно быть толстых универсальных сервисов.
## 4.5 Dependency Inversion
Use case зависит только от портов.
Use case **не знает** про:
- `ipcMain`
- `Fastify`
- `ipcRenderer`
- `child_process`
- `electron`
- Zustand
- React
## 4.6 DRY
Нельзя дублировать:
- merge rules
- provider presentation mapping
- navigation fallback flow
- task/team aggregation by associated paths
- Codex thread fetch policy
## 4.7 Clean Architecture Rule
Допустимое направление зависимостей:
`main/adapters/input -> core/application -> core/domain`
`main/adapters/output -> core/application ports`
`main/infrastructure -> core/application ports`
`renderer/hooks -> renderer/adapters -> contracts`
`preload -> contracts`
Недопустимо:
- `core/domain -> core/application`
- `core/application -> main/infrastructure`
- `core/application -> main/adapters`
- `renderer/ui -> store/api`
- `shared/app-shell -> feature internals`
---
## 5. Hard Architectural Corrections
Это места, где нужно быть особенно аккуратным.
### Correction 1 - Feature contracts are not the same thing as app shell contracts
Ошибка, которую легко сделать:
- положить DTO в feature
- а потом заставить весь `src/shared/types/api.ts` зависеть от feature internals как от обычного shared-layer
Правильнее:
- `feature contracts` живут внутри feature и описывают форму recent-projects
- `app shell` может **композировать** feature API fragment, но не должен владеть feature моделью
Это тонкая, но важная разница.
### Correction 2 - Use case must not return infrastructure-shaped data
Use case работает с `core/application response model`, а не с raw app-server rows и не с renderer card model.
### Correction 3 - UI must not own navigation policy
`RecentProjectCard` не должен знать, как работает:
- worktree match
- refresh repository groups
- add custom path
- synthetic fallback group
Это responsibility interaction adapter/hook.
### Correction 4 - `DashboardView` must become a composition root for the screen
Он не должен:
- ходить в API
- мержить данные
- решать navigation flow
- агрегировать task/team decorations
---
## 6. Recommended Feature Structure
```text
src/features/recent-projects/
contracts/
index.ts
dto.ts
channels.ts
api.ts
core/
domain/
RecentProjectCandidate.ts
RecentProjectAggregate.ts
ProviderId.ts
policies/
mergeRecentProjectCandidates.ts
application/
use-cases/
ListDashboardRecentProjectsUseCase.ts
ports/
ClockPort.ts
LoggerPort.ts
RecentProjectsCachePort.ts
ListDashboardRecentProjectsOutputPort.ts
RecentProjectsSourcePort.ts
models/
ListDashboardRecentProjectsResponse.ts
main/
index.ts
composition/
createRecentProjectsFeature.ts
adapters/
input/
ipc/
registerRecentProjectsIpc.ts
http/
registerRecentProjectsHttp.ts
output/
presenters/
DashboardRecentProjectsPresenter.ts
sources/
ClaudeRecentProjectsSourceAdapter.ts
CodexRecentProjectsSourceAdapter.ts
infrastructure/
cache/
InMemoryRecentProjectsCache.ts
identity/
RecentProjectIdentityResolver.ts
codex/
CodexBinaryResolver.ts
CodexAppServerClient.ts
JsonRpcStdioClient.ts
preload/
index.ts
createRecentProjectsBridge.ts
renderer/
index.ts
adapters/
RecentProjectsSectionAdapter.ts
hooks/
useRecentProjectsSection.ts
useOpenRecentProject.ts
ui/
RecentProjectsSection.tsx
RecentProjectCard.tsx
utils/
navigation.ts
projectDecorations.ts
```
### Why this structure is the cleanest
- `contracts` - cross-process public contract
- `core/domain` - invariant business rules
- `core/application` - use case orchestration through ports
- `main/adapters/input` - driving adapters
- `main/adapters/output` - driven adapters
- `main/infrastructure` - concrete implementations
- `main/composition` - feature composition root
- `preload` - isolated renderer bridge
- `renderer` - feature presentation
Это уже не просто "feature folder", а полноценный vertical slice.
### Canonical template for future features
Для будущих feature в проекте фиксируем такой шаблон как базовый:
```text
src/features/<feature-name>/
contracts/
core/
domain/
application/
main/
composition/
adapters/
input/
output/
infrastructure/
preload/
renderer/
```
Использовать этот шаблон по умолчанию **для feature среднего и большого размера**.
Если feature маленькая и:
- не имеет отдельного use case
- не вводит новый bridge/API
- не содержит сложной business logic
то допускается упрощённый вариант без полного `core/main/preload` набора.
### What is mandatory
- `contracts/`
- `core/domain/`
- `core/application/`
- `main/composition/`
- хотя бы один из:
- `main/adapters/input/`
- `main/adapters/output/`
- `renderer/`, если у feature есть UI
### What is optional
- `preload/`, если feature не требует нового bridge/API
- `main/infrastructure/`, если feature не ходит во внешние runtime dependencies
- `renderer/adapters/`, если feature очень маленькая
- `renderer/hooks/`, если feature purely presentational
### When `core/` is required
`core/` обязателен, если feature:
- содержит независимые business rules
- имеет merge/filter/decision policy
- имеет хотя бы один use case, который хочется тестировать изолированно
- потенциально будет расширяться новыми providers / sources / policies
### When `core/` may be skipped
`core/` можно не создавать, если feature:
- purely presentational
- только прокидывает данные без трансформации правил
- не имеет собственного use case
- по сути является thin UI wrapper around existing app logic
### What should not be used as the default standard
- `main/domain/application/presentation`
- `preload/domain/application/presentation`
- `ui/domain/application/presentation`
Причина:
- это смешивает process axis и architecture axis
- это создаёт ложную симметрию
- это размывает единственный business core
---
## 7. Dependency Matrix
### Allowed imports
#### `contracts`
Can import:
- nothing app-specific
- TS built-ins only
#### `core/domain`
Can import:
- `contracts` if types are pure and stable
- other `core/domain` files
Cannot import:
- `core/application`
- `adapters`
- `infrastructure`
- `electron`
- `fastify`
- `@main/*`
#### `core/application`
Can import:
- `core/domain`
- `core/application/ports`
- `contracts`
Cannot import:
- `ipcMain`
- `Fastify`
- `child_process`
- `@renderer/*`
#### `main/adapters/input`
Can import:
- `core/application`
- `core/domain`
- `contracts`
Cannot import:
- renderer code
#### `main/adapters/output`
Can import:
- `core/application`
- `core/domain`
- `contracts`
- `main/infrastructure`
Cannot import:
- renderer code
#### `main/infrastructure`
Can import:
- `core/application/ports`
- `core/domain`
- `contracts`
- technology-specific libs
#### `main/composition`
Can import:
- `core/application`
- `main/adapters/input`
- `main/adapters/output`
- `main/infrastructure`
- `contracts`
- `@main/*`
#### `preload`
Can import:
- `contracts`
- feature preload helpers
- Electron preload APIs
Cannot import:
- `main/*`
- renderer code
#### `renderer/ui`
Can import:
- renderer hooks
- renderer adapters
- simple feature-local UI utilities
Cannot import:
- store directly
- API directly
- main code
#### `renderer/hooks`
Can import:
- `api`
- `useStore`
- feature contracts
- feature adapters/utils
---
## 8. Ports And Adapters Design
## 8.1 Domain Model
### `RecentProjectCandidate`
Это provider-agnostic внутренний объект, из которого потом строится response.
```ts
export interface RecentProjectCandidate {
identity: string;
displayName: string;
primaryPath: string;
associatedPaths: string[];
lastActivityAt: number;
providerIds: ProviderId[];
sourceKind: 'claude' | 'codex';
openTarget:
| { type: 'existing-worktree'; repositoryId: string; worktreeId: string }
| { type: 'synthetic-path'; path: string };
branchName?: string;
}
```
### `RecentProjectAggregate`
Это уже merged domain shape до presenter mapping.
```ts
export interface RecentProjectAggregate {
identity: string;
displayName: string;
primaryPath: string;
associatedPaths: string[];
lastActivityAt: number;
providerIds: ProviderId[];
source: 'claude' | 'codex' | 'mixed';
openTarget:
| { type: 'existing-worktree'; repositoryId: string; worktreeId: string }
| { type: 'synthetic-path'; path: string };
branchName?: string;
}
```
### Merge policy
`core/domain/policies/mergeRecentProjectCandidates.ts`
Это чистая функция без side effects.
Именно тут живёт истинная business rule:
- dedupe by identity
- prefer `existing-worktree`
- merge provider ids
- max by recency
- unset conflicting branch
---
## 8.2 Application Ports
### `RecentProjectsSourcePort`
```ts
export interface RecentProjectsSourcePort {
list(): Promise<RecentProjectCandidate[]>;
}
```
### `RecentProjectsCachePort`
```ts
export interface RecentProjectsCachePort<T> {
get(key: string): Promise<T | null>;
set(key: string, value: T, ttlMs: number): Promise<void>;
}
```
### `ClockPort`
```ts
export interface ClockPort {
now(): number;
}
```
### `LoggerPort`
```ts
export interface LoggerPort {
info(message: string, meta?: Record<string, unknown>): void;
warn(message: string, meta?: Record<string, unknown>): void;
error(message: string, meta?: Record<string, unknown>): void;
}
```
### `ListDashboardRecentProjectsOutputPort`
```ts
export interface ListDashboardRecentProjectsOutputPort<TViewModel> {
present(aggregates: RecentProjectAggregate[]): TViewModel;
}
```
Это важный момент.
Если хотим действительно clean use case, use case не должен знать финальный transport DTO напрямую.
---
## 8.3 Application Use Case
`core/application/use-cases/ListDashboardRecentProjectsUseCase.ts`
```ts
export interface ListDashboardRecentProjectsDeps<TViewModel> {
sources: RecentProjectsSourcePort[];
cache: RecentProjectsCachePort<TViewModel>;
output: ListDashboardRecentProjectsOutputPort<TViewModel>;
clock: ClockPort;
logger: LoggerPort;
}
export class ListDashboardRecentProjectsUseCase<TViewModel> {
constructor(private readonly deps: ListDashboardRecentProjectsDeps<TViewModel>) {}
async execute(cacheKey: string): Promise<TViewModel> {
const cached = await this.deps.cache.get(cacheKey);
if (cached) {
return cached;
}
const batches = await Promise.all(this.deps.sources.map((s) => s.list()));
const aggregates = mergeRecentProjectCandidates(batches.flat());
const viewModel = this.deps.output.present(aggregates);
await this.deps.cache.set(cacheKey, viewModel, 10_000);
return viewModel;
}
}
```
Почему это лучше прошлого варианта:
- use case orchestration не знает ни про renderer, ни про IPC DTO
- output model отдаётся через presenter port
- cache тоже внешний порт, а не private field in use case
Это уже настоящий `ports/adapters` shape.
---
## 8.4 Adapters
### `DashboardRecentProjectsPresenter`
Responsibility:
- `RecentProjectAggregate[] -> DashboardRecentProject[]`
Это output adapter, реализующий `ListDashboardRecentProjectsOutputPort`.
### `ClaudeRecentProjectsSourceAdapter`
Responsibility:
- взять active Claude context
- получить grouped repos
- превратить их в `RecentProjectCandidate[]`
Это driven/output adapter, реализующий `RecentProjectsSourcePort`.
### `CodexRecentProjectsSourceAdapter`
Responsibility:
- если context не local -> `[]`
- если `codex` missing -> `[]`
- через `codex app-server` достать recent thread summaries
- применить identity resolver
- превратить это в `RecentProjectCandidate[]`
Это тоже driven/output adapter, реализующий `RecentProjectsSourcePort`.
### Why adapters are separate from infrastructure
Потому что adapter переводит **между моделями и портами**, а infrastructure решает **как технически достать данные**.
Пример:
- `CodexAppServerClient` - infrastructure
- `CodexRecentProjectsSourceAdapter` - adapter
Это разные причины для изменения.
---
## 8.5 Infrastructure
### `CodexBinaryResolver`
Только определяет доступен ли standalone `codex`.
### `CodexAppServerClient`
Только транспорт к `codex app-server`.
### `JsonRpcStdioClient`
Только generic stdio JSON-RPC plumbing.
### `RecentProjectIdentityResolver`
Хотя он помогает бизнес-логике, по природе он infrastructure helper, потому что зависит на git/path environment semantics.
### `InMemoryRecentProjectsCache`
Concrete cache adapter.
---
## 9. Contracts Layer
Feature contracts должны содержать только:
- публичные DTO
- API fragment interface
- channel constants
### Recommended files
```text
src/features/recent-projects/contracts/
dto.ts
api.ts
channels.ts
index.ts
```
### DTO
```ts
export type DashboardProviderId = 'anthropic' | 'codex' | 'gemini';
export type DashboardRecentProjectOpenTarget =
| { type: 'existing-worktree'; repositoryId: string; worktreeId: string }
| { type: 'synthetic-path'; path: string };
export interface DashboardRecentProject {
id: string;
name: string;
primaryPath: string;
associatedPaths: string[];
mostRecentActivity: number;
providerIds: DashboardProviderId[];
source: 'claude' | 'codex' | 'mixed';
openTarget: DashboardRecentProjectOpenTarget;
primaryBranch?: string;
}
```
### API fragment
```ts
export interface RecentProjectsElectronApi {
getDashboardRecentProjects(): Promise<DashboardRecentProject[]>;
}
```
### Channel constant
```ts
export const GET_DASHBOARD_RECENT_PROJECTS = 'get-dashboard-recent-projects';
```
### Important shell rule
`src/shared/types/api.ts` не должен "владеть" этой feature.
Он может только **композировать** feature fragment:
```ts
import type { RecentProjectsElectronApi } from '@features/recent-projects/contracts';
export interface ElectronAPI extends RecentProjectsElectronApi {
// existing app methods...
}
```
Это намного чище, чем дублировать feature method shape внутри `shared/types/api.ts`.
---
## 10. Public API, Input Adapters And Composition Root
## 10.1 Feature Main Public API
`src/features/recent-projects/main/index.ts`
Должен экспортировать только:
- use case factory/composer
- input adapter registration helpers
- necessary public ports if really needed
Лучше не экспортировать наружу все внутренние классы поштучно без необходимости.
### Better pattern
```ts
export { createRecentProjectsFeature } from './composition/createRecentProjectsFeature';
export { registerRecentProjectsIpc } from './adapters/input/ipc/registerRecentProjectsIpc';
export { registerRecentProjectsHttp } from './adapters/input/http/registerRecentProjectsHttp';
```
### Why factory is better than many exports
Это уменьшает coupling composition root к внутреннему строению feature.
## 10.2 Feature Composition
Добавить:
```text
src/features/recent-projects/main/composition/createRecentProjectsFeature.ts
```
```ts
export function createRecentProjectsFeature(deps: {
getActiveContext: () => ServiceContext;
getLocalContext: () => ServiceContext | undefined;
logger: LoggerPort;
}) {
// instantiate infrastructure
// instantiate adapters
// instantiate presenter
// instantiate cache
// instantiate use case
// return entrypoint-facing surface
}
```
Именно это делает feature self-contained и масштабируемой.
## 10.3 IPC input adapter
`main/adapters/input/ipc/registerRecentProjectsIpc.ts`
Он не должен знать, как строится Codex client, cache или merge policy.
Он должен только:
- принять feature facade
- зарегистрировать handler
- прокинуть `cacheKey`
## 10.4 HTTP input adapter
То же самое для Fastify route.
## 10.5 Preload bridge
`preload/createRecentProjectsBridge.ts`
Bridge должен возвращать только feature API fragment:
```ts
export function createRecentProjectsBridge(): RecentProjectsElectronApi {
return {
getDashboardRecentProjects: () =>
ipcRenderer.invoke(GET_DASHBOARD_RECENT_PROJECTS),
};
}
```
Это хороший пример thin interface adapter.
---
## 11. Renderer Architecture
### 11.1 Renderer public entrypoint
`src/features/recent-projects/renderer/index.ts`
```ts
export { RecentProjectsSection } from './ui/RecentProjectsSection';
```
Внешний мир должен импортировать только это.
### 11.2 `useRecentProjectsSection`
Responsibility:
- загрузить `DashboardRecentProject[]`
- взять decorations из store
- через adapter получить card models
- отдать state for UI
### 11.3 `RecentProjectsSectionAdapter`
Responsibility:
- `DashboardRecentProject[] + task/team decorations -> RecentProjectCardModel[]`
Он не должен:
- выполнять fetch
- открывать проекты
### 11.4 `useOpenRecentProject`
Responsibility:
- encapsulate navigation policy
Это interaction adapter, а не UI helper.
Сюда выносится:
- `findMatchingWorktree`
- refresh repository groups
- `addCustomProjectPath`
- synthetic fallback group
### 11.5 `RecentProjectCard`
Responsibility:
- чистая presentation
Не импортирует:
- `useStore`
- `api`
- navigation utils
### 11.6 `RecentProjectsSection`
Responsibility:
- layout
- error/loading/empty states
- search filtering
- cards grid
---
## 12. DRY Rules For This Feature
### Rule 1
Одна merge policy в `domain/policies/mergeRecentProjectCandidates.ts`
### Rule 2
Одна navigation policy в `renderer/hooks/useOpenRecentProject.ts`
### Rule 3
Одна provider presentation mapping function в `renderer/utils/projectDecorations.ts`
### Rule 4
Одна Codex fetch policy в `CodexRecentProjectsSourceAdapter`
### Rule 5
Одна cache policy в `InMemoryRecentProjectsCache`
---
## 13. Tech Decisions For Codex
### Decision
Для phase 1 использовать:
- ephemeral `codex app-server`
- only `sourceKinds: ['vscode', 'cli']`
- include live + archived
- no file parsing
### Why
- этого достаточно для homepage use case
- не тянем слишком рискованную нативную storage-зависимость
- уменьшаем complexity
### Timeouts
- initialize: `3_000 ms`
- each list call: `3_000 ms`
- total Codex fetch: `8_000 ms`
- cache TTL: `10_000 ms`
---
## 14. Guard Rails
Если хотим, чтобы feature правда была эталонной, нужны guard rails.
### Recommended rule set
1. Outside feature import only public feature entrypoints:
- `@features/recent-projects/contracts`
- `@features/recent-projects/main`
- `@features/recent-projects/preload`
- `@features/recent-projects/renderer`
2. No deep imports from one process subtree into another.
3. `renderer/ui` components cannot import store or API directly.
4. `core/application` cannot import `electron`, `fastify`, `child_process`.
5. `core/domain` must remain side-effect free.
### Optional but recommended later
- add `no-restricted-imports` rules to enforce this automatically
---
## 15. Implementation Sequence
### Step 1
Create feature tree:
- `src/features/recent-projects`
### Step 2
Add alias support:
- `tsconfig.json`
- `tsconfig.node.json`
- `electron.vite.config.ts`
### Step 3
Create contracts:
- DTO
- API fragment
- channel constant
### Step 4
Create domain:
- candidate
- aggregate
- provider id
- merge policy
### Step 5
Create application layer:
- use case
- ports
- response model
### Step 6
Create output adapters:
- Claude source adapter
- Codex source adapter
- presenter
### Step 7
Create infrastructure:
- cache
- identity resolver
- Codex binary/client/json-rpc
### Step 8
Create feature composition root:
- `createRecentProjectsFeature`
### Step 9
Create driving input adapters:
- IPC
- HTTP
### Step 10
Create preload bridge
### Step 11
Create renderer:
- section adapter
- section hook
- open hook
- card UI
- section UI
### Step 12
Integrate into app shell:
- `src/main/index.ts`
- `src/main/ipc/handlers.ts`
- `src/main/http/index.ts`
- `src/preload/index.ts`
- `src/renderer/components/dashboard/DashboardView.tsx`
### Step 13
Add tests and architecture review pass
---
## 16. Test Strategy
### Domain tests
- merge policy
- branch conflict behaviour
- openTarget preference
- provider dedupe
### Application tests
- use case orchestrates sources + cache + presenter
- cache hit path
- cache miss path
- degraded Codex path still returns Claude result
### Adapter tests
- Claude source mapping
- Codex source mapping
- presenter mapping
- renderer section adapter mapping
- `useOpenRecentProject` navigation policy
### Infrastructure tests
- in-memory cache TTL
- Codex binary resolver success/failure
- app-server client request/timeout handling
### Renderer tests
- loading / empty / error states
- provider logos shown
- task/team decorations aggregated by `associatedPaths`
### Manual checks
1. local context without `codex`
2. local context with `codex`
3. mixed Claude + Codex repo
4. Codex-only non-git folder
5. ssh context
---
## 17. Anti-Patterns To Avoid
Не делать:
- одну папку feature без внутренних слоёв
- use case, который напрямую знает про `CodexAppServerClient`
- presenter logic inside React components
- store/API imports inside `RecentProjectCard`
- дублирование navigation fallback
- прямой import feature internals из app shell
- подмешивание recent-projects logic обратно в `DashboardView`
---
## 18. Final Recommendation
Если эта feature должна быть эталоном, то лучший shape такой:
- **feature-центричная структура**
- **строгий ports/adapters**
- **composition root внутри самой feature**
- **тонкий app shell**
- **тонкие input adapters и preload bridge**
- **чистый renderer UI без бизнес-логики**
Итоговый выбранный подход:
`Full vertical slice in src/features with core separated from process adapters`
`🎯 9 🛡️ 10 🧠 8`
Примерно `700-1000` строк
Это дороже, чем прагматичный hybrid, но это уже действительно можно считать reference implementation по `SOLID`, `Clean Architecture`, `Ports/Adapters` и `DRY`.