записан подход к проектированию: чистая архитектура и тактические шаблоны DDD

- architecture.md: раздел «Слои и модель домена» — таблица слоёв с колонкой «чего не знает», шаблоны со своим сегодняшним предметом в коде, запрет анемичной модели
- CLAUDE.md: раздел «Проектирование» с подходом и ссылкой на раздел архитектуры
- названо, что чистоту домена не держит ни одно правило archrules
This commit is contained in:
av
2026-08-22 21:05:13 +03:00
parent ad5b5e377f
commit 1edf8cb225
2 changed files with 70 additions and 2 deletions
+53 -2
View File
@@ -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, а не пересказом её требований.