From 1edf8cb225dee5a7cb366169c8b6493e7541cbdb Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 22 Aug 2026 21:05:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=85=D0=BE=D0=B4=20=D0=BA=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8E:=20=D1=87=D0=B8=D1=81=D1=82=D0=B0=D1=8F=20=D0=B0?= =?UTF-8?q?=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D1=82=D0=B0=D0=BA=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D1=8B=20DDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - architecture.md: раздел «Слои и модель домена» — таблица слоёв с колонкой «чего не знает», шаблоны со своим сегодняшним предметом в коде, запрет анемичной модели - CLAUDE.md: раздел «Проектирование» с подходом и ссылкой на раздел архитектуры - названо, что чистоту домена не держит ни одно правило archrules --- CLAUDE.md | 17 ++++++++++++++ docs/architecture.md | 55 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6cdeee9..ec383aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,23 @@ Storage, gRPC-клиент Yandex SpeechKit v3, Prometheus, `slog`. Прилож его Biome и юнит-тесты Vue. Сборка — Taskfile, образ — Docker, выкладка — Ansible из `pet-project-server`. +## Проектирование + +**Чистая архитектура.** Зависимости направлены внутрь, к домену: домен не знает +ни хранилища, ни транспорта, знание о внешнем мире приходит интерфейсом порта, а +реализацию подставляет точка входа. Направления держат тесты-сканеры +`internal/archrules`, а не договорённость. + +**Модель домена ведётся тактическими шаблонами DDD** — сущность и корень +агрегата, объект-значение, доменное событие, репозиторий, служба домена, +фабрика. Анемичной модели не заводим: поведение записи живёт в домене, а +прикладной слой назначает порядок шагов, а не правила. + +Слои, их дома, что каждому знать нельзя и чем шаблон занят сегодня — +[docs/architecture.md](docs/architecture.md), «Слои и модель домена». Здесь это +не повторяется: перечень растёт вместе с моделью, и вторая копия разошлась бы с +ним молча. + ## Инварианты Что нарушать нельзя. diff --git a/docs/architecture.md b/docs/architecture.md index 61172a1..e7bc721 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -77,9 +77,11 @@ задача возвращается в работу, нормирует [pipeline](../openspec/specs/pipeline/spec.md), «Брошенная задача возвращается в работу»; здесь это принцип письма шага, а не описание поведения. -- **Ядро зависит от интерфейсов.** `internal/service` знает только +- **Чистая архитектура.** Зависимости направлены внутрь, к домену: внутренний + слой не знает внешнего никогда. `internal/service` знает только `internal/contract`; ffmpeg, Yandex и хранилище подставляются в точке входа - `cmd/transcriber`. Правило механизировано тестами-сканерами `internal/archrules`, и + `cmd/transcriber`. Слои, их дома и словарь модели — раздел «Слои и модель + домена» ниже. Правило механизировано тестами-сканерами `internal/archrules`, и они же держат обратные направления: транспорты не знают друг о друге, адаптер не знает ни ядра, ни транспортов. *Изъятие:* транспорт **вправе** знать адаптер хранилища — `controller/http` @@ -87,6 +89,55 @@ роутер этого хранилища, а не наш сервер поверх него. Правила на это направление нет намеренно. +## Слои и модель домена + +**Подход — чистая архитектура.** Зависимость идёт только внутрь: домен не знает +ни хранилища, ни транспорта, а знание о внешнем мире живёт интерфейсом в портах +и реализацией в инфраструктуре. + +| Слой | Дом | Что живёт | Чего не знает | +| --- | --- | --- | --- | +| Домен | `internal/entity` | сущности, объекты-значения, доменные события, инварианты значениями | ничего, кроме стандартной библиотеки и единой точки времени `internal/clock` | +| Порты | `internal/contract` | интерфейсы репозиториев и внешних служб, типизированные ошибки | реализаций | +| Прикладной слой | `internal/service` | шаги конвейера: порядок, повтор, приговор | адаптеров и входов | +| Инфраструктура | `internal/adapter` | репозитории, ffmpeg, Yandex, шаги схемы | ядра и входов | +| Входы | `internal/controller` | HTTP и пул воркеров | друг друга | +| Сборка | `cmd/transcriber` | подстановка реализаций в порты, подъём сервера и пула | — | + +Направления держат тесты-сканеры `internal/archrules` — все, кроме чистоты +самого домена. **Её не держит ничто**: правила смотрят ядро, входы и адаптеры, а +импорт внешней библиотеки в `internal/entity` сегодня пройдёт молча. + +**Модель домена ведётся тактическими шаблонами DDD.** Шаблон называется здесь +вместе со своим сегодняшним предметом — перечень растёт вместе с моделью: + +- **Сущность** — `entity.AudioRecord`: у неё идентичность и поведение + (`MoveToState`, `Halt`, `Resume`, `Postpone`), а не набор полей при сервисе; +- **корень агрегата** — она же: файлы, тексты, структура, попытки распознавания и + журнал событий принадлежат записи и живут её идентификатором, а правит агрегат + держатель захвата; +- **объект-значение** — `entity.Stage` со своими сроками, `entity.StuckLimits`, + `entity.Replica`, `entity.RecognitionResult`: сравниваются по значению и своей + идентичности не имеют; +- **доменное событие** — `entity.RecordEvent`: что случилось с записью, чьей + рукой и чем кончилось; +- **репозиторий** — интерфейсы `internal/contract`, реализации под + `internal/adapter/repo`; +- **служба домена** — правило, не принадлежащее одной сущности, живёт функцией + пакета домена (`entity.WorkingStages`, `entity.SanitizeOriginalFilename`); +- **фабрика** — `entity.NewInProgressResult` и соседи: значение приходит + согласованным, а не заполняется полями снаружи. + +**Анемичной модели не заводим.** Новое поведение записи ищет дом сначала в +домене; прикладной слой назначает порядок шагов, а не правила. Признак нарушения +наблюдаем: правило о записи, записанное в `internal/service` условием над её +полями, принадлежит `internal/entity`. + +Место, где подход нарушен сегодня, названо изъятием в «Принципах»: транспорт +знает адаптер хранилища, потому что HTTP-поверхность и есть роутер этого +хранилища. Изъятие снимает задача `storage-without-pocketbase` — своя отдача +файла и свои маршруты возвращают транспорту независимость от инфраструктуры. + ## Компоненты Каждый — строкой со ссылкой на capability, а не пересказом её требований.