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

30 KiB
Raw Permalink Blame History

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

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 в проекте фиксируем такой шаблон как базовый:

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.

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.

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

export interface RecentProjectsSourcePort {
  list(): Promise<RecentProjectCandidate[]>;
}

RecentProjectsCachePort

export interface RecentProjectsCachePort<T> {
  get(key: string): Promise<T | null>;
  set(key: string, value: T, ttlMs: number): Promise<void>;
}

ClockPort

export interface ClockPort {
  now(): number;
}

LoggerPort

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

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

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
src/features/recent-projects/contracts/
  dto.ts
  api.ts
  channels.ts
  index.ts

DTO

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

export interface RecentProjectsElectronApi {
  getDashboardRecentProjects(): Promise<DashboardRecentProject[]>;
}

Channel constant

export const GET_DASHBOARD_RECENT_PROJECTS = 'get-dashboard-recent-projects';

Important shell rule

src/shared/types/api.ts не должен "владеть" этой feature.
Он может только композировать feature fragment:

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

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

Добавить:

src/features/recent-projects/main/composition/createRecentProjectsFeature.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:

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

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.

  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.

  • 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.