diff --git a/DRAFT/README.md b/DRAFT/README.md index 1f8fd12..1902f06 100644 --- a/DRAFT/README.md +++ b/DRAFT/README.md @@ -4,9 +4,8 @@ ## Материалы -- [Первый уровень](./level-1/README.md) - базовые слои, модули и зависимости. -- [Второй уровень](./level-2/README.md) - доменный слой и доменные модули. -- [Третий уровень](./level-3/README.md) - строгая внутренняя архитектура домена и границы сред выполнения. +- [Первый уровень](./level-1/README.md) - слои, доменные модули, публичные API и зависимости. +- [Второй уровень](./level-2/README.md) - доменные пакеты, единый `DomainApi`, presets и Framework Groups. - [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки. ## Соглашение diff --git a/DRAFT/index.md b/DRAFT/index.md index e4410ae..c61f205 100644 --- a/DRAFT/index.md +++ b/DRAFT/index.md @@ -5,7 +5,7 @@ title: SLM Design hero: name: SLM Design text: Последовательная архитектура фронтенд-приложений - tagline: Начните со слоёв и модулей, затем добавьте доменные границы и строгие ограничения сред выполнения только при реальной сложности. + tagline: Начните со слоёв и доменных модулей, затем переходите к доменным пакетам и строгим границам сред выполнения только при реальной сложности. image: src: /logo.svg alt: SLM Design @@ -16,26 +16,21 @@ hero: - theme: alt text: Читать Level 2 link: /level-2/ - - theme: alt - text: Читать Level 3 - link: /level-3/ - theme: alt text: Реестр правил link: /rules/ features: - title: Level 1 · Архитектурная база - details: Слои, модули, публичные API, зависимости, вложенные границы и жизненный цикл ресурсов. - - title: Level 2 · Доменные модули - details: Новый слой domains локализует модели, правила, сценарии и продуктовое состояние без обязательной внутренней структуры домена. - - title: Level 3 · Строгие домены - details: Домен объединяет бизнес-логику, порты, адаптеры, типовые сборки и модули фреймворков с явными границами сред выполнения и жизненного цикла. + details: Шесть слоёв, доменные модули, публичные API, зависимости, вложенные границы и жизненный цикл ресурсов. + - title: Level 2 · Доменные пакеты + details: Business, единый DomainApi, presets, adapters и Framework Groups с явными cross-domain и environment boundaries. - title: Канонические правила details: Точные блокирующие требования отделены от определений, рекомендаций и примеров и собраны по уровням. --- ## Что опубликовано -Сайт содержит рабочие черновики трёх уровней SLM и их канонические реестры правил. Монорепозитории и дополнительные режимы архитектуры пока не входят в опубликованную документацию. +Сайт содержит рабочие черновики двух уровней SLM и их канонические реестры правил. Монорепозитории и дополнительные режимы архитектуры пока не входят в опубликованную документацию. Определения выбранного уровня нормативны внутри черновика. Точные формулировки блокирующих требований находятся только в [реестре правил](/rules/). diff --git a/DRAFT/level-1/README.md b/DRAFT/level-1/README.md index 027d238..670f83f 100644 --- a/DRAFT/level-1/README.md +++ b/DRAFT/level-1/README.md @@ -2,25 +2,24 @@ > Статус: рабочий черновик. Документы в этой папке не являются спецификацией. -Level 1 задаёт основу SLM для лёгких проектов, которым нужна понятная организация без отдельной доменной архитектуры. +Level 1 задаёт полную структурную основу SLM: слои, модули, доменные модули, публичные границы, граф зависимостей и владение жизненным циклом. ## Место в уровнях SLM | Уровень | Назначение | |---|---| -| Level 1 | Слои, зависимости, структурные сущности и жизненный цикл ресурсов | -| Level 2 | Слой `domains` и доменные модули без строгой внутренней формы | -| Level 3 | Строгие роли и границы сред выполнения внутри доменов | +| Level 1 | Слои, доменные модули, зависимости, структурные сущности и жизненный цикл ресурсов | +| Level 2 | Доменные пакеты, единый `DomainApi`, сборки и явные границы сред выполнения | Повышение уровня может требовать рефакторинга, но базовые понятия Level 1 сохраняются. ## Область Level 1 -Level 1 описывает слои, модули, группы, сегменты, компоненты, публичный API, граф зависимостей и владение жизненным циклом ресурсов. +Level 1 описывает слои, доменные модули, группы, сегменты, компоненты, публичный API, граф зависимостей и владение жизненным циклом ресурсов. -Level 1 не описывает домены, фабрики, порты, адаптеры, обязательный поток данных, монорепозитории, соглашения об именовании и файловый стайлгайд. +Level 1 не задаёт обязательную внутреннюю форму доменного модуля, фабрики, порты, адаптеры, presets, обязательный поток данных, монорепозитории, соглашения об именовании и файловый стайлгайд. -Появление самостоятельной доменной логики является сигналом рассмотреть [Level 2](../level-2/). +Появление нескольких сред выполнения, устойчивого `DomainApi` или необходимости разделить бизнес-логику и технические сборки является сигналом рассмотреть [Level 2](../level-2/). ## Виды утверждений @@ -35,7 +34,7 @@ Level 1 не описывает домены, фабрики, порты, ада ## Основная идея -Модуль является основной архитектурной единицей SLM. Слой задаёт роль и направление зависимостей. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю. +Модуль является основной архитектурной единицей SLM. Слой задаёт роль и направление зависимостей. Доменный модуль представляет одну предметную область. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю. Level 1 требует отдельную папку и единый публичный API модуля. Имена этих элементов, внутренняя файловая форма модулей, сегментов и компонентов определяются стайлгайдом проекта. @@ -43,6 +42,7 @@ Level 1 требует отдельную папку и единый публи - [Терминология](./terminology.md) - [Слои](./layers.md) +- [Доменные модули](./domains.md) - [Зависимости](./dependencies.md) - [Модули](./modules.md) - [Группы](./groups.md) diff --git a/DRAFT/level-1/dependencies.md b/DRAFT/level-1/dependencies.md index 4557e3a..6cb7225 100644 --- a/DRAFT/level-1/dependencies.md +++ b/DRAFT/level-1/dependencies.md @@ -22,6 +22,15 @@ - Модули одного слоя могут импортировать друг друга. - Промежуточный слой не является обязательным посредником. +Доменный модуль Level 1 может импортировать публичный API другого доменного модуля. Runtime- и type-only импорты одинаково создают ребро графа, поэтому общий граф обязан оставаться ацикличным. + +```ts +// domains/orders +import type { Product } from '@/domains/catalog' +``` + +Level 1 не требует отдельного механизма междоменной инъекции. Более строгая модель cross-domain зависимостей задаётся Level 2. + Направление слоёв определено в [Слоях](./layers.md). ## Связанные правила diff --git a/DRAFT/level-1/domains.md b/DRAFT/level-1/domains.md new file mode 100644 index 0000000..357b66e --- /dev/null +++ b/DRAFT/level-1/domains.md @@ -0,0 +1,61 @@ +# Доменные модули Level 1 + +> Пояснение базовой модели предметных областей без обязательной внутренней архитектуры. + +## Связанные правила + +- [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) +- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) +- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) +- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) + +## Один домен, один модуль + +Связная предметная область получает один доменный модуль. Level 1 не требует выделять business, adapters, presets или framework bindings в самостоятельные соседние модули. + +```text +domains/auth/ +├── hooks/ +├── services/ +├── stores/ +├── types/ +├── ui/ +└── index.ts +``` + +Показанные каталоги являются возможными сегментами, а не обязательным каркасом. Доменный модуль может содержать предметные типы, сценарии, состояние, framework-код, локальные адаптеры, компоненты и вложенные модули. + +## Публичный API + +Внешний код использует домен через обычный публичный API модуля: + +```ts +import { signOut, useSession } from '@/domains/auth' +``` + +Глубокий импорт во внутренний сегмент нарушает модульную границу: + +```ts +import { useSession } from '@/domains/auth/hooks/use-session' +``` + +## Groups + +При большом количестве доменных модулей слой `domains` может содержать обычные навигационные Groups: + +```text +domains/ +├── shop/ # Group +│ ├── catalog/ # Доменный модуль +│ └── orders/ # Доменный модуль +└── cabinet/ # Group + └── profile/ # Доменный модуль +``` + +Group не имеет `index.ts`, реализации, состояния или публичного API. Она не создаёт dependency boundary и не реэкспортирует содержащиеся в ней домены. + +## Переход на Level 2 + +Доменный модуль переводится в доменный пакет Level 2, когда ему нужны устойчивый `DomainApi`, одна фабрика, несколько сред выполнения или независимые SLM-модули сборок и framework-интеграции. + +Такой переход изменяет структурную границу: корневой доменный модуль исчезает, а его предметная ответственность переходит обязательному модулю `business` внутри доменного пакета. diff --git a/DRAFT/level-1/layers.md b/DRAFT/level-1/layers.md index 3685948..18b690a 100644 --- a/DRAFT/level-1/layers.md +++ b/DRAFT/level-1/layers.md @@ -8,6 +8,7 @@ src/ ├── app/ ├── compositions/ +├── domains/ ├── infra/ ├── ui/ └── shared/ @@ -29,6 +30,12 @@ src/ `compositions` содержит продуктовый интерфейс: страницы, макеты, экраны, виджеты, точки входа и другие композиционные модули. Внутреннюю группировку слоя определяет проект. +### Domains + +`domains` содержит предметные области приложения: их модели, правила, сценарии и продуктовое состояние. Каждая самостоятельная предметная область представлена [доменным модулем](./domains.md). + +Доменная логика не принадлежит устройству одной страницы или маршрута. Конкретный visual outcome и сборка нескольких доменов остаются в `compositions`. + ### Infra `infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной доменной модели. @@ -52,6 +59,8 @@ app ↓ compositions ↓ +domains + ↓ infra ↓ ui @@ -63,8 +72,9 @@ shared | Слой | Может импортировать нижние слои | |---|---| -| `app` | `compositions`, `infra`, `ui`, `shared` | -| `compositions` | `infra`, `ui`, `shared` | +| `app` | `compositions`, `domains`, `infra`, `ui`, `shared` | +| `compositions` | `domains`, `infra`, `ui`, `shared` | +| `domains` | `infra`, `ui`, `shared` | | `infra` | `ui`, `shared` | | `ui` | `shared` | | `shared` | Нет | @@ -80,6 +90,6 @@ shared ## Граница Level 1 -Разрешённый импорт не переносит владение. Например, `infra` может использовать `ui`, но продуктовый интерфейс по-прежнему принадлежит `compositions`. +Разрешённый импорт не переносит владение. Например, доменный модуль может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев. -Самостоятельная доменная модель или сценарий являются сигналом рассмотреть [Level 2](../level-2/), а не расширять ответственность `shared` или `infra`. +Если код определяет продуктовую модель, правило или сценарий, он принадлежит `domains`, а не `shared` или `infra`. Строгая внутренняя форма домена появляется только на [Level 2](../level-2/). diff --git a/DRAFT/level-1/terminology.md b/DRAFT/level-1/terminology.md index 892a22f..99bb9f1 100644 --- a/DRAFT/level-1/terminology.md +++ b/DRAFT/level-1/terminology.md @@ -46,26 +46,37 @@ Полный линейный порядок слоёв выбранного уровня SLM. Он определяет, какой слой является нижним для проверки зависимостей. -Для Level 1 нормативным является порядок `app → compositions → infra → ui → shared`. Более высокий уровень может добавить новые роли и задаёт собственный полный порядок, сохраняя направление от верхних слоёв к нижним. +Для Level 1 нормативным является порядок `app → compositions → domains → infra → ui → shared`. Level 2 сохраняет этот порядок и уточняет внутреннюю форму слоя `domains`. ### Слой -Одна из пяти верхнеуровневых ролей внутри SLM root: +Одна из шести верхнеуровневых ролей внутри SLM root: | Слой | Роль | |---|---| | `app` | Связь приложения с фреймворком: запуск, маршруты и преобразование входных данных | | `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции | +| `domains` | Предметные области, их модели, правила, сценарии и продуктовое состояние | | `infra` | Технические сервисы и возможности приложения | | `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции | | `shared` | Независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода | -Слои образуют линейный порядок `app → compositions → infra → ui → shared`. Нижним считается любой слой справа от исходного; промежуточный слой не является обязательным посредником. +Слои образуют линейный порядок `app → compositions → domains → infra → ui → shared`. Нижним считается любой слой справа от исходного; промежуточный слой не является обязательным посредником. ### Модуль Минимальная самостоятельная архитектурная единица Level 1. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API. +### Доменная ответственность + +Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Количество экранов, endpoint-ов, hooks или файлов само по себе не определяет её границу. + +### Доменный модуль + +Обычный SLM-модуль слоя `domains`, представляющий одну доменную ответственность. Он подчиняется всем правилам модулей, предоставляет единый публичный API и является узлом графа зависимостей. + +Доменный модуль может содержать сегменты, компоненты и вложенные модули. Level 1 не требует разделять его внутреннее содержимое по техническим ролям. + ### Группа Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или узла графа зависимостей. @@ -104,7 +115,7 @@ SLM root ├── app │ └── точка входа фреймворка -├── compositions | infra | ui +├── compositions | domains | infra | ui │ ├── группа │ │ └── модуль │ └── модуль diff --git a/DRAFT/level-1/validation.md b/DRAFT/level-1/validation.md index 85375b1..a61715d 100644 --- a/DRAFT/level-1/validation.md +++ b/DRAFT/level-1/validation.md @@ -4,7 +4,7 @@ ## Автоматическая проверка -Проект, заявляющий соответствие Level 1, сопоставляет физические пути с SLM root, слоями, модулями, вложенными модулями, публичными точками входа, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей. +Проект, заявляющий соответствие Level 1, сопоставляет физические пути с SLM root, слоями, доменными модулями, остальными модулями, Groups, вложенными модулями, публичными точками входа, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей. Каждое правило класса `A` должно быть реализовано проверкой проекта и блокировать её при нарушении. Level 1 не навязывает конкретный инструмент. @@ -17,7 +17,18 @@ Правила класса `R` проверяются вручную. Статический анализ может обнаружить подозрительный код, но не способен окончательно определить: - ответственность и её владельца; +- связность предметной области доменного модуля; - соответствие кода роли слоя; - необходимость экспортов публичного API; - область жизни ресурса и достаточность очистки; - наличие самостоятельной границы у компонента, группы или сегмента. + +## Проверка доменных модулей + +На ревью определяется: + +- представляет ли доменный модуль одну связную предметную область; +- не разделена ли одна область на соседние модули без самостоятельных владельцев; +- не объединены ли в одном модуле несвязанные предметные области; +- остаются ли страницы, маршруты и multi-domain UI в `compositions`; +- остаются ли самостоятельные технические сервисы без предметной модели в `infra`. diff --git a/DRAFT/level-2/README.md b/DRAFT/level-2/README.md index 183eaee..cc824b6 100644 --- a/DRAFT/level-2/README.md +++ b/DRAFT/level-2/README.md @@ -2,48 +2,77 @@ > Статус: рабочий черновик. Документы в этой папке не являются спецификацией. -Level 2 расширяет архитектурную базу Level 1 слоем `domains`. Он предназначен для проектов, в которых появились самостоятельные предметные области, но ещё не требуется строгая внутренняя архитектура доменов. +Level 2 предназначен для приложений с устойчивыми доменными API, несколькими способами сборки или самостоятельными framework-модулями домена. Он сохраняет слои Level 1, но заменяет простой доменный модуль доменным пакетом с явными владельцами ролей. ## Наследование Level 1 -Проект Level 2 соблюдает все определения и правила Level 1, если терминология Level 2 не задаёт расширение для нового слоя. Модуль, Group, сегмент, компонент, вложенный модуль, публичный API, граф зависимостей и владение жизненным циклом сохраняют смысл Level 1. +Level 2 соблюдает определения и правила Level 1, кроме явно заменённых положений: -Канонический набор требований образуют два реестра: - -- [правила Level 1](../rules/level-1.md); -- [дополнительные правила Level 2](../rules/level-2.md). - -## Место в уровнях SLM - -| Уровень | Назначение | +| Положение Level 1 | Статус в Level 2 | |---|---| -| Level 1 | Слои, зависимости, структурные сущности и жизненный цикл ресурсов | -| Level 2 | Доменный слой и доменные модули без строгой внутренней формы | -| Level 3 | Строгая внутренняя архитектура и границы сред выполнения доменов | +| Порядок `app → compositions → domains → infra → ui → shared` | Сохраняется | +| Модуль, Group, сегмент, компонент, публичный API и граф зависимостей | Сохраняют смысл | +| Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются доменным пакетом | +| Навигационная Group слоя `domains` | Может содержать доменные пакеты | +| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups | -## Основная идея +Канонический набор требований образуют [правила Level 1](../rules/level-1.md) и [правила Level 2](../rules/level-2.md). -Домен Level 2 является обычным SLM-модулем слоя `domains`. Он владеет одной связной предметной областью и может содержать всю необходимую ей реализацию, сегменты, компоненты и вложенные модули. +## Когда выбирать Level 2 -По умолчанию доменные модули размещаются непосредственно в слое. При большом количестве доменов слой также может содержать обычные навигационные Groups Level 1. +Level 2 оправдан, когда предметной области нужны один устойчивый `DomainApi`, разные сборки для браузера и сервера, несколько технических интеграций или самостоятельные domain-specific модули React, Vue либо другого фреймворка. + +Уровень выбирается для всего SLM root. Проект, в котором всем предметным областям достаточно простых доменных модулей, остаётся на Level 1. После завершённого перехода на Level 2 каждая предметная область представлена доменным пакетом, даже если отдельный пакет имеет только `business`. + +Размер каталога сам по себе не требует перехода. + +## Базовая форма ```text src/domains/ -├── auth/ # Доменный модуль -├── catalog/ # Доменный модуль -└── orders/ # Доменный модуль +└── auth/ # Доменный пакет + ├── README.md # Необязательная metadata + ├── business/ # Обязательный SLM-модуль + │ └── index.ts + ├── presets/ # Необязательная Group + │ ├── browser/ # SLM-модуль + │ └── request/ # SLM-модуль + ├── adapters/ # Необязательная Group + │ └── identity-provider/ # SLM-модуль + └── react/ # Необязательная framework Group + ├── session/ # SLM-модуль + └── login-form/ # SLM-модуль ``` -## Область Level 2 +Корень пакета не является модулем и не имеет `index.ts`. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своим публичным API. -Level 2 описывает роль слоя `domains`, границу доменного модуля, опциональную группировку и зависимости с участием нового слоя. +## Публичные границы -Level 2 не задаёт обязательные внутренние роли, каталоги или способ сборки доменного модуля. При появлении устойчивого контракта бизнес-логики, нескольких сред выполнения или сложного жизненного цикла следует рассмотреть [Level 3](/level-3/). +Приложение получает данные, состояние и результаты домена через готовый экземпляр `DomainApi`. Технический код импортирует только API конкретного модуля: + +```ts +import { authFactory, isAuthError } from '@/domains/auth/business' +import { createBrowserAuth } from '@/domains/auth/presets/browser' +import { AuthSessionProvider } from '@/domains/auth/react/session' +import { LoginForm } from '@/domains/auth/react/login-form' +``` + +Общие импорты `@/domains/auth` и `@/domains/auth/react` запрещены: пакет и Groups не имеют агрегирующих API. + +## Миграция + +Доменные модули Level 1 могут временно сосуществовать с пакетами Level 2 только во время перехода. Такое состояние не является завершённым соответствием Level 2. По [`SLM-L2-MIGRATION-A017`](../rules/level-2.md#slm-l2-migration-a017) старые модули и новые пакеты не создают прямых runtime- или type-only зависимостей; связанные части графа мигрируют вместе. ## Карта черновика - [Терминология](./terminology.md) -- [Слои](./layers.md) -- [Домены](./domains.md) +- [Доменный пакет](./domains/domain-package.md) +- [Модуль business](./domains/business.md) +- [Фабрика, зависимости и адаптеры](./domains/factory-ports-adapters.md) +- [Presets и среды выполнения](./domains/presets.md) +- [Framework Groups и модули](./domains/framework-bindings.md) - [Зависимости](./dependencies.md) +- [Тестирование](./domains/testing.md) - [Проверка](./validation.md) +- [Миграция auth](./domains/auth-example.md) +- [Открытые вопросы](./domains/open-questions.md) diff --git a/DRAFT/level-2/dependencies.md b/DRAFT/level-2/dependencies.md index 6133234..0738072 100644 --- a/DRAFT/level-2/dependencies.md +++ b/DRAFT/level-2/dependencies.md @@ -1,50 +1,71 @@ # Зависимости Level 2 -> Расширение графа зависимостей Level 1 слоем `domains`. - -Level 2 не вводит отдельный вид зависимости. Доменные модули являются обычными узлами графа модулей, а Groups не участвуют в графе. - -## Направление - -Модуль слоя `domains` может импортировать: - -- публичные API других доменных модулей; -- публичные API модулей `infra`, `ui` и `shared`; -- нормативные ресурсы `shared`. - -`infra`, `ui` и `shared` не импортируют `domains`. `compositions` и `app` могут использовать публичные API доменных модулей как код нижнего слоя. - -## Междоменные зависимости - -Импорт между доменными модулями разрешён независимо от их Group: - -```ts -// domains/orders -import type { Product } from '@/domains/catalog' -``` - -Он создаёт обычное ребро графа модулей: - -```text -orders → catalog -``` - -Обратная runtime- или type-only зависимость, создающая цикл, запрещена общим правилом Level 1. - -## Groups и зависимости - -Путь опциональной Group участвует в адресе модуля, но сама Group не является импортируемой сущностью. Размещение в разных Groups не запрещает импорт и не задаёт его направление. - -Если двум бизнес-приложениям нужна гарантированная архитектурная изоляция, одной Group недостаточно: такая граница требует отдельных SLM roots или правил более высокого проектного уровня. - -## Вложенные модули - -Вложенный модуль домена остаётся внутренним для родительской границы. Другой домен не импортирует его напрямую и получает необходимые экспорты через API корневого доменного модуля. +> Уточнение графа зависимостей внутри и между доменными пакетами. ## Связанные правила -- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) +- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007) +- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012) +- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013) - [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) -- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) -- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010) + +## Направление внутри пакета + +| Исходный модуль | Допустимые зависимости | +|---|---| +| `business` | Собственные сегменты, объявленный нейтральный `shared`, объявленные business-safe внешние пакеты, type-only публичные business-контракты других доменов | +| Adapter | Собственный `business`, `infra`, конкретная техническая реализация, `shared` | +| Preset | Собственный `business`, закрытые или самостоятельные adapters, type-only API других доменов | +| Framework binding module | Собственный `business`, публичные API framework-модулей своего домена, фреймворк, `ui`, `shared` | +| Место сборки графа | Публичные API presets и framework-модулей всех входящих в граф доменов | + +`business` не достигает adapters, presets, framework-модулей, product SDK, storage, API браузера или Node.js. Проверяется весь транзитивный import-граф его публичной точки входа. + +## Междоменные импорты + +Модуль одного доменного пакета не импортирует runtime-экспорты другого доменного пакета. Разрешён только type-only импорт публичного контракта его `business`, по возможности суженный через `Pick`. + +```ts +import type { AuthApi } from '@/domains/auth/business' + +export type UserDeps = { + auth: Pick +} +``` + +Type-only импорт остаётся архитектурным ребром. Runtime- и type-only зависимости образуют единый DAG и не могут создавать цикл. + +Pure function, hook, Provider, context, component или framework state другого домена являются runtime-экспортами и не образуют исключение. Независимая общая функция переносится в `shared`, а UI нескольких доменов собирается в `compositions`. + +## Runtime-инъекция API + +Готовый API другого домена передаётся preset-модулю аргументом. Preset не импортирует его runtime-фабрику или сборку: + +```text +createAuthForRequest() + → AuthApi + → createUserForRequest({ authApi }) + → UserApi +``` + +Место сборки графа создаёт независимые домены раньше зависимых. Если сбой `AuthApi` становится результатом публичного сценария `UserApi`, приложению доступна только собственная доменная ошибка User. Точный механизм различения ошибок при exception-модели остаётся открытым вопросом. + +## Framework-состояние + +Framework binding module использует framework API только своего доменного пакета: + +```ts +// Допустимо внутри domains/auth/react/login-form +import { useAuthSession } from '@/domains/auth/react/session' + +// Недопустимо внутри domains/user/react/profile +import { useAuthSession } from '@/domains/auth/react/session' +``` + +Во втором случае композиционный модуль читает состояния обоих доменов и передаёт необходимые данные или callbacks через публичные свойства компонентов. + +## Границы сред + +Каждый client-, server- или shared-entry point имеет совместимый транзитивный import-граф. Серверный preset или adapter не реэкспортируется через `business`, Framework Group или клиентский preset. + +Tree shaking не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода. diff --git a/DRAFT/level-2/domains.md b/DRAFT/level-2/domains.md deleted file mode 100644 index 684d3f7..0000000 --- a/DRAFT/level-2/domains.md +++ /dev/null @@ -1,117 +0,0 @@ -# Домены Level 2 - -> Пояснение модели доменных модулей без строгой внутренней архитектуры Level 3. - -Домен Level 2 является обычным SLM-модулем. Новый уровень добавляет доменную роль и место в порядке слоёв, но не вводит отдельную структурную сущность поверх модуля. - -## Связанные правила - -- [`SLM-L2-DOMAIN-R001`](../rules/level-2.md#slm-l2-domain-r001) -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) -- [`SLM-L1-MODULE-A014`](../rules/level-1.md#slm-l1-module-a014) -- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) -- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) -- [`SLM-L1-MODULE-R012`](../rules/level-1.md#slm-l1-module-r012) -- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) -- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010) -- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013) - -## Один домен, один модуль - -Связная предметная область получает один корневой доменный модуль. Вся логика авторизации может находиться внутри `auth` без обязательного выделения `session`, `phone-login` или каждого сценария в соседний доменный модуль. - -```text -domains/auth/ -├── hooks/ -├── services/ -│ ├── session.service.ts -│ └── phone-login.service.ts -├── stores/ -├── types/ -├── ui/ -└── index.ts -``` - -Названия и набор сегментов определяет стайлгайд проекта. Level 2 не требует показанный каркас. - -## Внутренняя свобода - -Доменный модуль может содержать всё, что нужно его ответственности: - -- доменные типы, модели, правила и сценарии; -- продуктовое состояние и управление его жизненным циклом; -- framework hooks и domain-specific компоненты; -- вызовы переданных или импортированных технических сервисов; -- локальные adapters, mappers и интеграционный код; -- сегменты и вложенные модули. - -Если техническая реализация становится самостоятельным сервисом без доменной семантики, она переносится в `infra` по общим правилам назначения слоёв. - -## Когда нужен вложенный модуль - -Часть домена становится вложенным модулем только при появлении самостоятельной ответственности, публичного API внутри родительской границы, собственных зависимостей или области жизни. - -```text -domains/auth/ -├── parts/ -│ ├── auth-form/ -│ │ ├── auth-form.tsx -│ │ └── index.ts -│ └── registration-form/ -│ ├── registration-form.tsx -│ └── index.ts -├── auth.ts -└── index.ts -``` - -Внешний код по-прежнему получает `auth-form` и `registration-form` только через публичный API `auth`. Само наличие нескольких файлов или отдельного пользовательского сценария не требует вложенного модуля. - -## Опциональная группировка - -Если количество доменов затрудняет навигацию, Groups внутри `domains` могут классифицировать их по принадлежности к разным бизнес-приложениям или продуктовым областям. - -```text -domains/ -├── shop/ # Group -│ ├── auth/ # Доменный модуль Shop Auth -│ ├── catalog/ # Доменный модуль -│ └── orders/ # Доменный модуль -└── cabinet/ # Group - ├── auth/ # Отдельный доменный модуль Cabinet Auth - ├── profile/ # Доменный модуль - └── documents/ # Доменный модуль -``` - -`shop` и `cabinet` не имеют `index.ts`, состояния, реализации или публичного API. Они могут содержать только доменные модули и другие Groups. - -Одинаковое имя модуля в разных Groups допустимо, если это разные владельцы и разные предметные области. Если авторизация действительно общая, ей нужен один общий модуль-владелец, а не две копии. - -Group не создаёт dependency boundary. Импорт между модулями разных Groups проверяется так же, как любой импорт внутри слоя `domains`. - -## Публичный API - -Внешний код использует домен через обычный публичный API модуля: - -```ts -import { signOut, useSession } from '@/domains/auth' -``` - -Deep import остаётся нарушением: - -```ts -import { useSession } from '@/domains/auth/hooks/use-session' -``` - -Group не предоставляет агрегирующий API и не реэкспортирует содержащиеся в ней домены. - -## Граница с другими слоями - -| Ответственность | Владелец | -|---|---| -| Доменная модель, правило, сценарий или продуктовое состояние | Доменный модуль | -| Страница, маршрут, экран и конкретный visual outcome | Модуль `compositions` | -| Самостоятельный технический сервис без предметной модели | Модуль `infra` | -| Универсальный интерфейс без продуктовой семантики | Модуль `ui` | -| Независимая детерминированная утилита | `shared` или локальный владелец | - -Число потребителей не является единственным критерием. Самостоятельная доменная ответственность может принадлежать `domains`, даже если сегодня используется одной композицией. diff --git a/DRAFT/level-2/domains/README.md b/DRAFT/level-2/domains/README.md new file mode 100644 index 0000000..fb3ae64 --- /dev/null +++ b/DRAFT/level-2/domains/README.md @@ -0,0 +1,26 @@ +# Доменные пакеты Level 2 + +Доменный пакет заменяет корневой доменный модуль Level 1. Он объединяет SLM-модули одной предметной области, но сам не является модулем, Group или публичным API. + +```text +domains/auth/ +├── business/ +├── presets/ +├── adapters/ +└── react/ + ├── session/ + └── login-form/ +``` + +## Основные границы + +- [Доменный пакет](./domain-package.md) определяет предметную и структурную границу. +- [Business](./business.md) владеет `DomainApi`, фабрикой и доменными ошибками. +- [Фабрика, зависимости и adapters](./factory-ports-adapters.md) отделяют business от технической реализации. +- [Presets](./presets.md) собирают один API для нужных окружений. +- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка. +- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу. +- [Миграция auth](./auth-example.md) показывает переход с Level 1. +- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей. + +Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md). diff --git a/DRAFT/level-2/domains/auth-example.md b/DRAFT/level-2/domains/auth-example.md new file mode 100644 index 0000000..ae9a9a7 --- /dev/null +++ b/DRAFT/level-2/domains/auth-example.md @@ -0,0 +1,101 @@ +# Миграция домена auth с Level 1 + +> Проверочный пример перехода от доменного модуля к доменному пакету. + +## Связанное правило + +- [`SLM-L2-MIGRATION-A017`](../../rules/level-2.md#slm-l2-migration-a017) + +## Исходная форма Level 1 + +```text +domains/auth/ # Доменный модуль +├── hooks/ +├── services/ +├── stores/ +├── ui/ +└── index.ts # Общий API модуля +``` + +Level 1 разрешает business-сценариям, framework hooks, state adapter и локальной сборке находиться внутри одного модуля. + +## Целевая форма Level 2 + +```text +domains/auth/ # Доменный пакет +├── README.md +├── business/ # SLM-модуль +│ ├── errors/ +│ ├── lib/ +│ ├── services/ +│ ├── types/ +│ └── index.ts +├── presets/ # Group +│ ├── browser/ # SLM-модуль +│ │ ├── adapters/ +│ │ └── index.ts +│ └── request/ # SLM-модуль +│ └── index.ts +└── react/ # Framework Group + ├── session/ # SLM-модуль + │ └── index.ts + └── login-form/ # SLM-модуль + └── index.ts +``` + +Корневой `domains/auth/index.ts` удаляется. Потребители переходят на публичные API конкретных модулей. + +## Перенос ответственности + +| Исходная часть | Владелец Level 2 | +|---|---| +| Сценарии, предметные типы, единый API | `auth/business` | +| Коды, тип и guard ошибок | `auth/business` | +| Browser storage и HTTP adapters | `auth/presets/browser` | +| Cookies, request data и server adapters | `auth/presets/request` | +| Provider и session hooks | `auth/react/session` | +| Переиспользуемая форма | `auth/react/login-form` | +| Страница, текст и redirect | `compositions` | + +## Новые импорты + +```ts +import { authFactory, isAuthError } from '@/domains/auth/business' +import { createBrowserAuth } from '@/domains/auth/presets/browser' +import { AuthSessionProvider } from '@/domains/auth/react/session' +import { LoginForm } from '@/domains/auth/react/login-form' +``` + +## Cross-domain граф + +Если User зависит от Auth, оба связанных домена сначала переводятся в пакеты. Затем User получает только type-only контракт: + +```ts +import type { AuthApi } from '@/domains/auth/business' + +export type UserDeps = { + auth: Pick +} +``` + +Место сборки графа создаёт экземпляры: + +```ts +const authApi = createBrowserAuth() +const userApi = createBrowserUser({ authApi }) +``` + +User не импортирует runtime-код Auth, а его React-модули не импортируют `useAuthSession` или Auth components. + +## Порядок перехода + +1. Определить dependency-connected набор доменов, который нужно мигрировать вместе. +2. Выделить `business` и одну фабрику без environment-specific import-графа. +3. Зафиксировать `DomainApi`, error codes, error type и runtime guard. +4. Перенести browser/server wiring в нужные presets и adapters. +5. Разделить React-ответственности на модули внутри Group `react`. +6. Перенести страницы, redirects и multi-domain UI в `compositions`. +7. Перевести внешние импорты на module-specific paths. +8. Удалить старый root `index.ts` и проверить import-граф. + +Простые доменные модули могут оставаться в SLM root только как временное миграционное состояние. Они не создают прямые runtime- или type-only зависимости с уже переведёнными пакетами. diff --git a/DRAFT/level-2/domains/business.md b/DRAFT/level-2/domains/business.md new file mode 100644 index 0000000..8d65b99 --- /dev/null +++ b/DRAFT/level-2/domains/business.md @@ -0,0 +1,122 @@ +# Модуль business + +> Пояснение единственного runtime-источника доменных данных и результатов. + +## Связанные правила + +- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005) +- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006) +- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007) +- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008) +- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009) +- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010) +- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018) + +## Роль + +`business` является обязательным SLM-модулем доменного пакета. Он владеет: + +- публичными предметными сценариями; +- единым контрактом `DomainApi`; +- одной публичной фабрикой; +- типом явных зависимостей фабрики; +- предметными типами и детерминированными правилами; +- кодами, типом и runtime guard доменных ошибок; +- публичным представлением доменных данных и состояния. + +Приложение получает runtime-данные, состояние и результаты домена только через экземпляр `DomainApi`. Adapter, preset или framework binding module не открывает параллельный источник доменных данных. + +## Публичный API модуля + +```ts +export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error' +export { authFactory } from './auth.factory' + +export type { + AuthApi, + AuthDeps, + AuthError, + AuthErrorCode, + AuthFactory, + AuthState, +} from './types' +``` + +Фабрика, error contract и типы экспортируются для presets, adapters, framework-модулей и мест сборки графа. Предметные validators, normalizers, внутренние преобразователи исходных ошибок, constructors, mutable store и технические DTO остаются закрытыми и используются публичными сценариями `DomainApi`. + +## Один DomainApi + +```ts +export type AuthApi = { + getSnapshot: () => AuthState + requestPhoneOtp: (phone: string) => Promise + verifyPhoneOtp: (code: string) => Promise +} + +export type AuthFactory = (deps: AuthDeps) => AuthApi +``` + +Все presets вызывают одну `authFactory` и создают `AuthApi` этого контракта. Preset может использовать другую техническую реализацию, но не добавляет метод и не меняет семантику сценария. + +Точная модель хранения, initial state, подписки и SSR snapshot пока остаётся открытым вопросом. Нормативной уже является публичная граница: приложение наблюдает доменное состояние через `DomainApi`, а не напрямую через adapter или framework store. + +## Обязательный контракт ошибок + +Каждый `business` объявляет устойчивые коды, безопасную readonly-форму и runtime guard: + +```ts +export const AUTH_ERROR_CODES = { + PHONE_INVALID: 'AUTH_PHONE_INVALID', + OTP_REQUEST_FAILED: 'AUTH_OTP_REQUEST_FAILED', + OTP_CODE_INVALID: 'AUTH_OTP_CODE_INVALID', +} as const + +export type AuthErrorCode = + typeof AUTH_ERROR_CODES[keyof typeof AUTH_ERROR_CODES] + +export type AuthError = Readonly<{ + code: AuthErrorCode +}> + +const authErrorCodes = new Set(Object.values(AUTH_ERROR_CODES)) + +export const isAuthError = (value: unknown): value is AuthError => { + if (typeof value !== 'object' || value === null) { + return false + } + + const prototype = Object.getPrototypeOf(value) + const keys = Reflect.ownKeys(value) + + if ( + (prototype !== Object.prototype && prototype !== null) + || keys.length !== 1 + || keys[0] !== 'code' + || !('code' in value) + ) { + return false + } + + return typeof value.code === 'string' && authErrorCodes.has(value.code) +} +``` + +Способ передачи ошибки, exception или discriminated `Result`, пока не закреплён. В обоих вариантах публичный сценарий сообщает ожидаемый сбой только через собственный `AuthError`. + +## Изоляция технических ошибок + +Ошибки SDK, HTTP, database, storage и adapters не пересекают `DomainApi` в форме, доступной приложению. `business` преобразует обрабатываемый технический сбой в собственный код: + +```text +SDK error + → adapter failure + → business mapping + → AuthErrorCode + → приложение +``` + +Публичная форма не включает исходные `message`, `status`, `payload`, `cause`, SDK class или сам объект ошибки. Диагностические данные могут сохраняться только во внутреннем механизме observability, контракт которого будет определён отдельно. + +То же относится к cross-domain вызову. Если `UserApi` использует `AuthApi`, сбой, который становится результатом публичного сценария User, представлен собственным `UserError`, а не `AuthError`. + +Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки. Способ отличить их от ожидаемых cross-domain ошибок при exception-модели и их диагностическая политика остаются открытыми вопросами; исходная ошибка при этом не добавляется в публичную форму DomainError. diff --git a/DRAFT/level-2/domains/domain-package.md b/DRAFT/level-2/domains/domain-package.md new file mode 100644 index 0000000..10f460b --- /dev/null +++ b/DRAFT/level-2/domains/domain-package.md @@ -0,0 +1,87 @@ +# Граница доменного пакета + +> Пояснение новой контейнерной сущности Level 2. + +## Связанные правила + +- [`SLM-L2-DOMAIN-R002`](../../rules/level-2.md#slm-l2-domain-r002) +- [`SLM-L2-DOMAIN-A003`](../../rules/level-2.md#slm-l2-domain-a003) +- [`SLM-L2-GROUP-R004`](../../rules/level-2.md#slm-l2-group-r004) +- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005) + +## Предметная граница + +Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать business-сценарии авторизации, её browser/server presets и React-модули, но не страницу профиля или общий database client. + +Пакет не владеет исполняемой ответственностью. Конкретными API, состоянием, зависимостями и lifecycle владеют модули внутри него. + +## Корень пакета + +```text +domains/auth/ +├── README.md +├── business/ +├── presets/ +├── adapters/ +└── react/ +``` + +В корне разрешены: + +- документация; +- ownership metadata; +- декларативный manifest или декларативная конфигурация архитектурной проверки; +- обязательный модуль `business`; +- Groups допустимых ролей. + +В корне запрещены: + +- `index.ts` или другой агрегирующий executable entry point; +- runtime-файлы и side effects; +- изменяемое состояние и ресурсы lifecycle; +- реэкспорт API внутренних модулей; +- page-specific компоненты или сборка нескольких доменов. + +Metadata содержит только статические данные, не исполняется приложением, build tooling или проверяющим инструментом и не становится скрытым API пакета. + +## Модули и Groups + +`business` размещается непосредственно в пакете. Presets и самостоятельные adapters размещаются в Groups `presets` и `adapters`. Framework Group называется по фреймворку: `react`, `vue` и аналогично. + +```text +auth/ +├── business/ # SLM-модуль +├── presets/ # Group +│ └── browser/ # SLM-модуль +└── react/ # Framework Group + ├── session/ # SLM-модуль + └── login-form/ # SLM-модуль +``` + +Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/presets/browser`, `auth/react/session`, но не `auth`, `auth/presets` или `auth/react`. + +## Навигационные Groups + +Слой `domains` может содержать навигационные Groups с пакетами: + +```text +domains/ +└── commerce/ # Навигационная Group + ├── catalog/ # Доменный пакет + └── orders/ # Доменный пакет +``` + +Такая Group отличается от Group внутри пакета только допустимым составом: она содержит доменные пакеты и другие navigation Groups, а не модули. + +## Границы соседних слоёв + +| Ответственность | Владелец | +|---|---| +| Предметные сценарии, `DomainApi`, доменные ошибки | `business` | +| Техническая реализация зависимости одного домена | Adapter внутри пакета | +| Универсальный технический сервис | `infra` | +| Domain-specific framework API | Модуль внутри `react`, `vue` и аналогичной Group | +| Страница, маршрут, redirect, продуктовый текст | `compositions` | +| UI, объединяющий несколько доменов | `compositions` | + +Зависимость от React сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности. diff --git a/DRAFT/level-2/domains/factory-ports-adapters.md b/DRAFT/level-2/domains/factory-ports-adapters.md new file mode 100644 index 0000000..d0a2b07 --- /dev/null +++ b/DRAFT/level-2/domains/factory-ports-adapters.md @@ -0,0 +1,86 @@ +# Фабрика, зависимости и adapters + +> Пояснение границы между `business` и технической средой. + +## Связанные правила + +- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007) +- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008) +- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010) +- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) + +## Одна фабрика + +```text +явные зависимости + business factory → DomainApi +``` + +Модуль `business` предоставляет одну публичную фабрику. Она получает все runtime-возможности явными аргументами и создаёт API одного контракта независимо от выбранного preset. + +```ts +export type AuthFactory = (deps: AuthDeps) => AuthApi +``` + +Рекомендуется сохранять сам вызов фабрики чистым: он создаёт объекты и closures, но не читает скрытое окружение и не выбирает конкретную техническую реализацию. Детальный lifecycle ресурсов будет нормирован позже. + +## Технические зависимости + +Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы. + +```ts +export type AuthPhoneDependency = { + requestCode: (phone: string) => Promise + verifyCode: (code: string) => Promise +} +``` + +`unknown` допустим на границе непроверенного внешнего результата. `business` проверяет его до превращения в предметные данные или состояние. + +Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics. + +## Cross-domain API dependency + +Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом: + +```ts +import type { AuthApi } from '@/domains/auth/business' + +export type UserDeps = { + auth: Pick +} +``` + +Runtime-значение передаёт место сборки графа через preset. `user/business` не импортирует executable API, factory или preset Auth. + +## Adapter + +Adapter соединяет явную зависимость фабрики с технической системой: + +```text +business dependency ← adapter → SDK / storage / platform / request data +``` + +Adapter преобразует аргументы и технический результат к контракту зависимости. Он не определяет публичный метод `DomainApi`, предметный fallback или код доменной ошибки. + +Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение никогда не строит поведение по HTTP status, SDK error class или storage exception. + +## Размещение adapter + +Одноразовый adapter остаётся закрытым сегментом preset-модуля: + +```text +auth/presets/browser/ +├── adapters/ +│ └── phone.adapter.ts +└── index.ts +``` + +Adapter становится самостоятельным SLM-модулем, когда нужен нескольким presets или имеет отдельную integration responsibility: + +```text +auth/adapters/ +└── identity-provider/ + └── index.ts +``` + +Самостоятельный adapter сохраняет минимальный публичный API и не становится альтернативным источником доменных данных для приложения. diff --git a/DRAFT/level-2/domains/framework-bindings.md b/DRAFT/level-2/domains/framework-bindings.md new file mode 100644 index 0000000..daec429 --- /dev/null +++ b/DRAFT/level-2/domains/framework-bindings.md @@ -0,0 +1,115 @@ +# Framework Groups и модули + +> Пояснение domain-specific framework-кода на примере React. + +## Связанные правила + +- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) +- [`SLM-L2-FRAMEWORK-R014`](../../rules/level-2.md#slm-l2-framework-r014) +- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015) + +## Framework Group + +Папка для domain-specific React binding modules называется `react`: + +```text +domains/auth/react/ # Framework Group +├── session/ # SLM-модуль +│ ├── hooks/ +│ ├── providers/ +│ └── index.ts +└── login-form/ # SLM-модуль + ├── components/ + └── index.ts +``` + +`react` является Group, а не модулем. У неё нет `index.ts`, состояния, реализации, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`. + +Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. Он не является вложенным модулем, потому что родительская граница `react` является Group. + +## Framework binding module + +Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании готового `DomainApi` одного домена с конкретным framework. Сам факт зависимости от framework недостаточен: framework-specific preset остаётся в `presets`, а page-specific модуль остаётся в `compositions`. + +Framework binding module может: + +- передавать готовый `DomainApi` через Provider и context; +- предоставлять domain-specific hooks; +- отображать состояние и безопасные ошибки домена; +- реализовывать переиспользуемую domain-specific форму или guard; +- связывать framework lifecycle с публичным API домена. + +Он не вызывает business-фабрику или preset, не выбирает adapters и не создаёт новые предметные сценарии. + +## Модуль session + +`auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthApi`: + +```tsx +'use client' + +type AuthSessionProviderProps = PropsWithChildren<{ + api: AuthApi +}> + +export const AuthSessionProvider = ({ + api, + children, +}: AuthSessionProviderProps) => { + return ( + + {children} + + ) +} +``` + +Публичный путь модуля: + +```ts +import { + AuthSessionProvider, + useAuthSession, +} from '@/domains/auth/react/session' +``` + +Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API. + +## Модуль login-form + +`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с `AuthApi`, AuthState и AuthError. Она может использовать публичный API соседнего `auth/react/session`, если зависимость остаётся ацикличной. + +Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Поэтому domain-owned `AuthGuard` может решить, разрешён ли доступ, но политика перехода на `/login` остаётся у route composition. + +## Запрет cross-domain framework imports + +Framework binding module не импортирует hooks, contexts, Providers, components или framework state другого доменного пакета: + +```ts +// Недопустимо: domains/user/react/profile +import { useAuthSession } from '@/domains/auth/react/session' +``` + +Cross-domain UI собирается в `compositions`: + +```tsx +const session = useAuthSession() + +return ( + +) +``` + +Передача через props является границей композиции, но не требует prop drilling внутри домена: каждый пакет может использовать собственный Provider и context. Если User business постоянно нуждается в Auth, готовый `AuthApi` передаётся User factory при сборке графа, а User framework module работает уже со своим `UserApi`. + +## Публичные API + +```ts +import { AuthSessionProvider } from '@/domains/auth/react/session' +import { LoginForm } from '@/domains/auth/react/login-form' +``` + +Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена. diff --git a/DRAFT/level-2/domains/open-questions.md b/DRAFT/level-2/domains/open-questions.md new file mode 100644 index 0000000..676f1ee --- /dev/null +++ b/DRAFT/level-2/domains/open-questions.md @@ -0,0 +1,43 @@ +# Открытые вопросы Level 2 + +> Эти вопросы не являются правилами и не отменяют зафиксированные границы. + +## Зафиксированные решения + +- Level 1 включает слой `domains` и простые доменные модули. +- Level 2 заменяет доменный модуль доменным пакетом. +- Корень пакета содержит только metadata, модули и Groups и не имеет executable API. +- `business` предоставляет одну фабрику и один `DomainApi`. +- Приложение получает доменные данные, состояние и результаты только через `DomainApi`. +- Каждый `business` экспортирует коды, тип и runtime guard доменных ошибок. +- Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена. +- Количество presets определяется реальными окружениями; универсальный preset не обязателен. +- Runtime cross-domain imports запрещены; type-only business contracts разрешены и входят в DAG. +- Framework Group называется по фреймворку и содержит самостоятельные SLM-модули. +- Cross-domain framework state, hooks, contexts и components не импортируются. + +## Владение состоянием + +Нужно определить создание initial state, применение transitions, persistence и внешний event input. Нормативным уже является только то, что приложение читает состояние через `DomainApi`, но точная граница между business state machine и storage adapter пока не выбрана. + +Отдельно требуется проверить SSR snapshot, hydration, concurrent rendering, reset и поведение после завершения request scope. + +## Передача ошибок + +Нужно выбрать общую политику exception или discriminated `Result`, определить cancellation и unexpected failures, а также сериализацию ошибок через RPC и server actions. + +Для exception-модели отдельно нужно определить, как зависимый business отличает ожидаемую ошибку другого домена без runtime-импорта его guard: через переданный discriminator, wrapper места сборки или другой явный контракт. + +## Технические порты + +Нужно определить, является ли consumer-owned port обязательной формой каждой технической зависимости и какие behavioral guarantees он обязан описывать: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup. + +Cross-domain `Pick` уже зафиксирован как отдельный вид API dependency и не зависит от этого решения. + +## Lifecycle сборки + +Нужно определить контракты `start` и `dispose`, async cleanup, rollback частичного старта, repeated disposal, request abort и поведение API после завершения scope. До решения применяется только общее владение lifecycle Level 1. + +## Автоматическая проверка + +Нужно выбрать machine-readable формат для domain packages, metadata, модулей, Groups, entry points, environment labels и запрещённых транзитивных импортов. diff --git a/DRAFT/level-2/domains/presets.md b/DRAFT/level-2/domains/presets.md new file mode 100644 index 0000000..155cbbe --- /dev/null +++ b/DRAFT/level-2/domains/presets.md @@ -0,0 +1,101 @@ +# Presets и среды выполнения + +> Пояснение повторяемых сборок одного `DomainApi`. + +## Связанные правила + +- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008) +- [`SLM-L2-PRESET-R011`](../../rules/level-2.md#slm-l2-preset-r011) +- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) +- [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013) + +## Назначение + +Preset является SLM-модулем в Group `presets`. Он создаёт API одной business-фабрики для конкретного повторяемого контекста выполнения. + +```text +authFactory +├── presets/browser → AuthApi в браузере +├── presets/request → AuthApi одного server request +└── presets/server-action → AuthApi server action +``` + +Архитектура не требует обязательный `base` или изоморфный preset и не ограничивает количество presets. Проект создаёт только те сборки, которые нужны его реальным средам и областям использования. + +Если фабрика используется в одном месте и отдельная повторяемая конфигурация не возникает, место сборки графа может вызвать её напрямую. + +## Один контракт API + +Каждый preset выбирает технические реализации, но вызывает одну и ту же фабрику и возвращает один контракт `DomainApi`: + +```ts +export const createBrowserAuth = (): AuthApi => { + return authFactory({ + phone: createHttpPhoneAdapter(), + session: createBrowserSessionAdapter(), + }) +} +``` + +```ts +export const createAuthForRequest = ( + input: AuthRequestInput, +): AuthApi => { + return authFactory({ + phone: createServerPhoneAdapter(input), + session: createRequestSessionAdapter(input), + }) +} +``` + +Server preset может обращаться к database напрямую через adapter, а browser preset реализует тот же сценарий через HTTP или RPC. Preset не добавляет server-only метод к `AuthApi` и не меняет доменные ошибки. + +Если полный `DomainApi` невозможно корректно создать в некоторой среде, пакет просто не предоставляет preset для этой среды. Метод, намеренно падающий только потому, что среда не поддерживается, не считается реализацией контракта. + +## Cross-domain input + +Preset зависимого домена принимает готовый API аргументом: + +```ts +import type { AuthApi } from '@/domains/auth/business' + +export type CreateUserForRequestInput = { + authApi: Pick + request: UserRequestInput +} + +export const createUserForRequest = ({ + authApi, + request, +}: CreateUserForRequestInput): UserApi => { + return userFactory({ + auth: authApi, + profile: createUserProfileAdapter(request), + }) +} +``` + +Preset делает только type-only импорт `AuthApi`. Runtime-фабрику, preset или instance Auth он не импортирует. + +Место сборки графа выполняет сборку: + +```ts +const authApi = createAuthForRequest(authInput) +const userApi = createUserForRequest({ authApi, request: userInput }) +``` + +## Environment entry points + +Server preset имеет отдельный публичный entry point и marker выбранного framework или bundler: + +```ts +import 'server-only' + +export { createAuthForRequest } from './create-auth-for-request' +``` + +Server entry point не реэкспортируется через `business`, Framework Group, browser preset или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость. + +## Lifecycle + +Preset может создавать ресурсы, которым потребуется запуск или cleanup, но точная форма `start`, `dispose`, rollback и request abort пока не нормирована. До принятия решения действует общее правило владения lifecycle Level 1. diff --git a/DRAFT/level-2/domains/testing.md b/DRAFT/level-2/domains/testing.md new file mode 100644 index 0000000..9d670d8 --- /dev/null +++ b/DRAFT/level-2/domains/testing.md @@ -0,0 +1,56 @@ +# Тестирование доменного пакета + +> Проверка владельцев и публичных границ Level 2. + +## Связанное правило + +- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016) + +## Размещение + +Тест находится рядом с модулем-владельцем проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`. + +| Проверяемая граница | Владелец теста | +|---|---| +| Предметные сценарии, `DomainApi`, данные и ошибки | `business` | +| Техническое преобразование | Adapter | +| Выбор зависимостей и environment boundary | Preset | +| Provider, hook, form или guard | Соответствующий framework binding module | +| Граф нескольких доменов | Модуль `composition`, точка входа `app` или другое место сборки | + +## Business через фабрику + +Каждый публичный предметный сценарий проверяется через единственную фабрику с управляемыми зависимостями: + +```ts +const api = authFactory(createAuthTestDeps({ + requestCode: async () => ({ ok: true }), +})) + +await api.requestPhoneOtp('+79991112233') +``` + +Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от будущего решения `throw` или `Result`, но наружу всегда проверяется только AuthErrorCode. + +Business-тест не использует React, реальный SDK, database или production preset. + +## Остальные модули + +Adapter-тест проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки. + +Preset-тест проверяет выбранные реализации, вызов одной фабрики, контракт возвращённого `DomainApi` и отсутствие несовместимого environment-кода. + +Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthApi` и проверяет Provider, hook или component. `login-form` не повторяет полный набор business-сценариев. + +## Архитектурные проверки + +Отдельная import-graph проверка подтверждает: + +- отсутствие root API доменного пакета и Framework Groups; +- отсутствие runtime cross-domain imports; +- отсутствие type-only импортов из чужих presets, adapters и framework-модулей; +- отсутствие cross-domain framework hooks, contexts и components; +- отсутствие server-only достижимости из client modules; +- отсутствие runtime- и type-only циклов. + +Runtime-тест не заменяет эти проверки. diff --git a/DRAFT/level-2/layers.md b/DRAFT/level-2/layers.md deleted file mode 100644 index b7a72bf..0000000 --- a/DRAFT/level-2/layers.md +++ /dev/null @@ -1,74 +0,0 @@ -# Слои Level 2 - -> Пояснение нормативной модели слоёв Level 2. - -Level 2 добавляет `domains` между продуктовой композицией и техническими сервисами приложения. - -## Базовая структура - -```text -src/ -├── app/ -├── compositions/ -├── domains/ -├── infra/ -├── ui/ -└── shared/ -``` - -Отсутствующая роль не требует пустой папки. Проект без самостоятельной доменной ответственности может оставаться на Level 1. - -## Роли слоёв - -| Слой | Роль | -|---|---| -| `app` | Запуск, маршруты, преобразование входных данных и подключение готовых публичных API | -| `compositions` | Страницы, макеты, экраны, виджеты и конкретные продуктовые композиции | -| `domains` | Предметные области, их модели, правила, сценарии и продуктовое состояние | -| `infra` | Технические сервисы и возможности приложения без самостоятельной предметной модели | -| `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции | -| `shared` | Независимый детерминированный фундамент без знания о продукте, состояния и ввода-вывода | - -## Порядок слоёв - -```text -app - ↓ -compositions - ↓ -domains - ↓ -infra - ↓ -ui - ↓ -shared -``` - -Код слоя может импортировать модули своего или любого нижнего слоя. Промежуточные слои можно пропускать. - -| Слой | Может импортировать другие слои | -|---|---| -| `app` | `compositions`, `domains`, `infra`, `ui`, `shared` | -| `compositions` | `domains`, `infra`, `ui`, `shared` | -| `domains` | `infra`, `ui`, `shared` | -| `infra` | `ui`, `shared` | -| `ui` | `shared` | -| `shared` | Нет | - -Импорты между модулями одного слоя разрешены. Поэтому один доменный модуль может зависеть от публичного API другого доменного модуля, если общий граф остаётся ацикличным. - -## Границы ролей - -`compositions` определяет устройство конкретной страницы, маршрута или визуального результата. `domains` определяет повторяемую предметную семантику, которая не принадлежит одной композиции. - -`infra` предоставляет техническую возможность. Если код определяет продуктовую модель, правило или сценарий поверх этой возможности, владельцем такого кода является доменный модуль. - -Разрешённый импорт не переносит владение. Доменный модуль может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев. - -## Связанные правила - -- [`SLM-L1-LAYER-R001`](../rules/level-1.md#slm-l1-layer-r001) -- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) -- [`SLM-L1-LAYER-R003`](../rules/level-1.md#slm-l1-layer-r003) -- [`SLM-L2-DOMAIN-R001`](../rules/level-2.md#slm-l2-domain-r001) diff --git a/DRAFT/level-2/terminology.md b/DRAFT/level-2/terminology.md index ddcfdbc..8ced6ca 100644 --- a/DRAFT/level-2/terminology.md +++ b/DRAFT/level-2/terminology.md @@ -2,53 +2,97 @@ > Нормативные определения рабочего черновика. Этот раздел не объявляет правила. -Level 2 наследует терминологию Level 1 и добавляет определения, необходимые слою `domains`. Структурные сущности Level 1 не меняют смысл. +Level 2 наследует терминологию Level 1, сохраняет порядок `app → compositions → domains → infra → ui → shared` и заменяет доменный модуль новой контейнерной сущностью. -## Нормативный порядок слоёв +## Доменный пакет -Для Level 2 нормативным является полный порядок: +Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла графа зависимостей. -```text -app → compositions → domains → infra → ui → shared -``` +Корень пакета может содержать только декларативную metadata, обязательный модуль `business` и допустимые Groups. Metadata хранит статические данные о пакете, владении либо конфигурации проверки и не содержит кода, выполняемого приложением, сборщиком или проверяющим инструментом. -Нижним считается любой слой справа от исходного. Промежуточный слой не является обязательным посредником. +### Навигационная Group слоя `domains` -## Слой `domains` +Group, размещённая непосредственно в слое `domains` или другой такой Group. На Level 2 она классифицирует доменные пакеты и другие навигационные Groups, но не содержит исполняемый код и не образует dependency boundary. -Слой предметных областей приложения. Он содержит доменные модули и Groups, которые классифицируют эти модули. +### Модуль доменного пакета -Код слоя выражает продуктовые понятия, правила, сценарии или продуктовое состояние, которые не принадлежат устройству одной конкретной страницы, маршрута или визуальной композиции. +Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: business, preset, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел графа зависимостей. -## Доменная ответственность +Модуль внутри Group пакета не является вложенным модулем, потому что его ближайшая внешняя граница не является модулем. -Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Наличие каждого из этих элементов не является обязательным. +## Business -Количество экранов, endpoint-ов, хуков или файлов само по себе не определяет границу доменной ответственности. +### Модуль business -## Доменный модуль +Обязательный SLM-модуль `business`, который определяет публичные предметные сценарии, `DomainApi`, одну фабрику, типы зависимостей и публичный контракт доменных ошибок. -Обычный SLM-модуль слоя `domains`, представляющий одну доменную ответственность. Его ближайшей внешней структурной границей является слой `domains` или Group этого слоя, а не другой модуль. +`business` является единственным runtime-источником, через который приложение получает доменные данные, состояние и результаты сценариев. Он не зависит от конкретного фреймворка, среды или технической реализации. -Доменный модуль подчиняется всем правилам модулей Level 1: имеет отдельную папку, одного владельца, единый публичный API, собственный узел графа зависимостей и определённый жизненный цикл ресурсов. +### Business-safe внешний пакет -Доменный модуль может содержать корневые файлы, сегменты, компоненты и вложенные модули. Вложенный модуль внутри него остаётся обычным вложенным модулем и не становится самостоятельным доменным модулем. +Внешняя библиотека, допустимая в import-графе `business`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, framework и техническая интеграция не становятся business-safe только из-за совместимости с несколькими средами. -## Group слоя `domains` +### DomainApi -Обычная Group Level 1, которая классифицирует доменные модули по принадлежности к бизнес-приложению, продуктовой области или другому понятному проекту признаку. +Единый публичный runtime-контракт домена, экземпляр которого создаёт фабрика `business`. Все presets одной предметной области создают API этого контракта и не добавляют собственные предметные методы. -Такая Group не является доменом, владельцем ответственности или узлом графа зависимостей. Она не задаёт отдельного направления импортов и не изолирует содержащиеся в ней модули от других Groups. +### Фабрика business + +Единственная публичная функция `business`, которая получает явные зависимости и создаёт экземпляр `DomainApi`. Фабрика не выбирает конкретный preset и не определяет среду выполнения. + +### Доменная ошибка + +Безопасная публичная форма ожидаемого сбоя предметного сценария. Модуль `business` объявляет устойчивые коды, тип ошибки и runtime guard. Ошибки SDK, транспорта, storage, адаптера или другого домена не являются доменными ошибками текущего API. + +## Техническая сборка + +### Adapter + +Код, который связывает явную зависимость фабрики с SDK, storage, API платформы, данными запроса или техническим сервисом. Adapter может быть закрытым сегментом preset-модуля либо самостоятельным модулем в Group `adapters`. + +Точная обязательная форма технических портов пока не определена и остаётся открытым вопросом Level 2. + +### Preset + +SLM-модуль в Group `presets`, который создаёт `DomainApi` для одного именованного контекста выполнения: браузера, запроса, server action или другого реального окружения. Он выбирает технические реализации и передаёт фабрике готовые runtime-зависимости. + +Архитектура не устанавливает минимальное или максимальное количество presets и не требует универсального изоморфного preset. + +## Framework binding + +### Framework Group + +Group доменного пакета, названная по конкретному фреймворку: `react`, `vue` и аналогично. Она содержит framework binding modules, не имеет собственного `index.ts`, реализации, состояния, жизненного цикла или публичного API. + +### Framework binding module + +SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией с фреймворком. Например, `react/session` может владеть Provider и hooks сессии, а `react/login-form` — переиспользуемой формой авторизации. + +Framework binding module получает готовый `DomainApi`, не вызывает фабрику или preset и не импортирует framework-состояние, hooks или компоненты другого доменного пакета. + +## Сборка графа + +### Место сборки графа + +Код модуля `composition`, точки входа `app`, request handler или test setup, который создаёт нужные экземпляры `DomainApi` в ацикличном порядке и передаёт уже созданные API последующим presets. Место сборки не становится владельцем предметных или технических ответственностей модулей. Подробный lifecycle собранного графа пока не нормирован Level 2. + +### Граница среды выполнения + +Граница между import-графами, предназначенными для клиента, сервера или обеих сред. Она определяется транзитивной достижимостью импортов, а не только именем папки или tree shaking. ## Структурная модель ```text SLM root └── domains - ├── доменный модуль - │ ├── сегменты - │ └── вложенные модули - └── доменный модуль + └── доменный пакет + ├── metadata + ├── модуль business + ├── Group presets + │ └── preset-модуль + ├── Group adapters + │ └── adapter-модуль + └── Framework Group react + ├── модуль session + └── модуль login-form ``` - -Путь помогает определить структурную границу, но не доказывает корректность предметной декомпозиции. Решение о том, является ли ответственность самостоятельным доменом, требует понимания продукта. diff --git a/DRAFT/level-2/validation.md b/DRAFT/level-2/validation.md index cdcc754..5312524 100644 --- a/DRAFT/level-2/validation.md +++ b/DRAFT/level-2/validation.md @@ -1,48 +1,58 @@ # Проверка Level 2 -> Проверка расширенной модели слоёв и доменных границ. +> Граница автоматической проверки, архитектурного ревью и тестирования Level 2. -Проект Level 2 выполняет все автоматические проверки и архитектурное ревью Level 1, используя нормативный порядок из шести слоёв. +## Конфигурация проекта -## Сопоставление структуры +Конфигурация проверки сопоставляет физические пути с доменными пакетами, metadata, SLM-модулями, Groups, публичными точками входа, метками сред выполнения и allowlist внешних пакетов, объявленных business-safe. Она отдельно распознаёт navigation Groups слоя `domains` и Groups внутри пакета. -Конфигурация проверки проекта дополнительно определяет: - -- физический путь слоя `domains`; -- доменные модули непосредственно в слое и внутри Groups; -- Groups слоя `domains`; -- вложенные модули внутри доменных модулей. - -Сопоставление путей не определяет предметный смысл. Оно позволяет проверить направление импортов, публичные API, циклы и доступ к вложенным модулям. +Формат такой конфигурации пока не выбран. Независимо от формата проверка должна анализировать import-граф и объявленные границы, а не угадывать сущность только по имени папки. ## Автоматическая проверка -Новых автоматических правил Level 2 не требуется. Структурные инварианты обеспечивают правила Level 1: +Автоматическая проверка должна блокировать: -- порядок `app → compositions → domains → infra → ui → shared`; -- импорт модулей только через публичные API; -- отсутствие циклов в графе модулей; -- отсутствие прямого доступа к вложенным модулям извне родителя; -- отсутствие реализации и API у Groups. +- исполняемый файл, root `index.ts`, состояние или реэкспорт в корне доменного пакета; +- отсутствие `business` или несколько модулей `business` в одном пакете; +- deep imports во внутренние части модулей; +- runtime- или type-only достижимость framework-, adapter-, preset-, infra- или environment-specific кода из `business`; +- runtime-импорт любого экспорта другого доменного пакета; +- type-only импорт не из публичной точки входа `business` другого доменного пакета; +- импорт framework state, hooks, contexts или components другого домена; +- достижимость server-only кода из client-entry point и обратное несовместимое направление; +- runtime- или type-only циклы в графе модулей. ## Архитектурное ревью -Дополнительное правило Level 2 проверяется на ревью. Нужно определить: +На ревью определяется: -- представляет ли доменный модуль одну связную предметную область; -- не разделена ли одна область на соседние доменные модули без самостоятельных владельцев; -- не объединены ли в одном модуле несвязанные предметные области; -- принадлежат ли модели, правила, сценарии и продуктовое состояние правильному домену; -- остаётся ли Group только навигационной классификацией; -- не размещены ли page-specific composition или самостоятельный технический сервис в `domains`. +- представляет ли пакет одну связную предметную область; +- является ли `DomainApi` единственным runtime-источником доменных данных и результатов для приложения; +- принадлежат ли коды, тип и guard доменных ошибок модулю `business`; +- преобразует ли business ожидаемые технические и cross-domain сбои в собственные ошибки; +- представляет ли каждый preset один реальный контекст выполнения и сохраняет ли контракт фабрики; +- принадлежит ли каждый framework binding module домену, а не странице или multi-domain сценарию; +- соответствует ли каждый объявленный business-safe внешний пакет ограничениям детерминированной библиотеки без runtime capability; +- остаются ли Framework Groups и другие Groups без реализации и агрегирующего API. + +## Тестирование + +Business-сценарии проверяются через фабрику с управляемыми зависимостями. Preset проверяет выбор реализаций и границу среды. Framework binding module проверяет собственный Provider, hook или component без повторения всего набора business-сценариев. + +Import-graph checks не заменяются runtime-тестами. + +## Миграционное состояние + +Наличие доменных модулей Level 1 рядом с пакетами Level 2 допускается только как незавершённая миграция. Проверка полного соответствия Level 2 завершается ошибкой, пока в выбранном SLM root остаются простые доменные модули. Во время перехода отдельно проверяется отсутствие runtime- и type-only импортов между двумя формами. ## Связанные правила -- [`SLM-L2-DOMAIN-R001`](../rules/level-2.md#slm-l2-domain-r001) -- [`SLM-L1-LAYER-R001`](../rules/level-1.md#slm-l1-layer-r001) -- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) -- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) -- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) -- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) +- [`SLM-L2-DOMAIN-A003`](../rules/level-2.md#slm-l2-domain-a003) +- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007) +- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012) +- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013) +- [`SLM-L2-MIGRATION-A017`](../rules/level-2.md#slm-l2-migration-a017) +- [`SLM-L2-BUSINESS-R018`](../rules/level-2.md#slm-l2-business-r018) +- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) -Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не определяет предметные границы приложения. +Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения. diff --git a/DRAFT/level-3/README.md b/DRAFT/level-3/README.md deleted file mode 100644 index b2bc2d1..0000000 --- a/DRAFT/level-3/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# SLM Level 3 - -> Статус: рабочий черновик. Документы в этой папке не являются спецификацией. - -Level 3 предназначен для приложений со сложной предметной логикой, несколькими средами выполнения или длительным сроком поддержки. Он не добавляет новый слой, а задаёт явное и проверяемое устройство доменов внутри слоя `domains`. - -## Когда выбирать Level 3 - -Level 3 оправдан, когда предметная область имеет устойчивый контракт бизнес-логики, несколько технических интеграций, разные способы сборки для браузера и сервера либо сложный жизненный цикл ресурсов. - -Количество файлов или размер проекта сами по себе не требуют перехода. Предметная область без такой сложности оформляется доменным модулем Level 2. - -## Наследование предыдущих уровней - -Проект Level 3 соблюдает определения и правила Level 1 и Level 2, кроме явно заменённых положений. - -| Положение | Статус в Level 3 | -|---|---| -| Порядок `app → compositions → domains → infra → ui → shared` | Сохраняется | -| Модуль, группа, сегмент, компонент, публичный API и жизненный цикл | Сохраняют смысл Level 1 | -| Доменный модуль Level 2 и [`SLM-L2-DOMAIN-R001`](../rules/level-2.md#slm-l2-domain-r001) | Заменяются доменом Level 3 | -| Группа внутри `domains` | Может содержать домены, оставаясь навигационной папкой | -| Прямые дочерние модули домена | Не являются вложенными, потому что домен сам не является модулем | - -Домен не содержит исполняемого кода и не отменяет правило Level 1 о модульном владельце. Он задаёт предметную границу, а конкретной ответственностью, публичным API и жизненным циклом по-прежнему владеет модуль. - -## Основная идея - -```text -Домен задаёт предметную границу. -Модуль бизнес-логики определяет правила, сценарии и публичный контракт. -Порты описывают возможности, которые нужны бизнес-логике. -Адаптеры реализуют порты в конкретной среде. -Типовые сборки повторяемо создают API. -Модуль фреймворка связывает готовый API с React, Vue или другим фреймворком. -Владелец графа удерживает экземпляр API и завершает его жизненный цикл. -``` - -## Базовая форма домена - -```text -src/domains/ -└── auth/ # домен - ├── business/ # обязательный модуль - │ ├── errors/ - │ ├── lib/ - │ ├── ports/ - │ ├── services/ - │ ├── types/ - │ └── index.ts - ├── presets/ # необязательная группа - │ └── application/ # модуль типовой сборки - │ ├── adapters/ - │ └── index.ts - ├── adapters/ # необязательная группа - │ └── identity-provider/ # самостоятельный модуль адаптера - │ └── index.ts - └── react/ # модуль фреймворка - ├── hooks/ - ├── providers/ - └── index.ts -``` - -Модуль `business` обязателен. Группы `presets` и `adapters`, а также модули фреймворков появляются только при реальной потребности. Каталоги `types`, `errors`, `lib`, `services`, `tests`, `ui`, `client` и `server` не становятся самостоятельными корневыми ветками домена. - -Домен может находиться непосредственно в `domains` или внутри навигационной группы. Группа не меняет его границы, направление зависимостей и доступность модулей домена. - -## Публичные границы - -У корня домена нет общей точки входа для исполняемого кода. Внешний код импортирует публичный API конкретного модуля: - -```ts -import { authFactory, isAuthError } from '@/domains/auth/business' -import { createApplicationAuth } from '@/domains/auth/presets/application' -import { AuthProvider, useAuth } from '@/domains/auth/react' -``` - -`@/domains/auth/business` является публичным API отдельного модуля, а не глубоким импортом. Пути вида `@/domains/auth/business/services/...` и общий импорт `@/domains/auth` нарушают границу. - -## Карта черновика - -- [Терминология](./terminology.md) -- [Граница домена](./domains/domain.md) -- [Модуль бизнес-логики](./domains/business.md) -- [Фабрика, порты и адаптеры](./domains/factory-ports-adapters.md) -- [Типовые сборки и SSR](./domains/presets.md) -- [Модуль React](./domains/framework-bindings.md) -- [Зависимости](./dependencies.md) -- [Тестирование](./domains/testing.md) -- [Проверка](./validation.md) -- [Пример переноса домена](./domains/auth-example.md) -- [Открытые вопросы](./domains/open-questions.md) - -## Канонические правила - -Level 3 использует правила Level 1 и Level 2, а также [дополнительный реестр Level 3](../rules/level-3.md). Тематические документы объясняют правила, но не объявляют их повторно. diff --git a/DRAFT/level-3/dependencies.md b/DRAFT/level-3/dependencies.md deleted file mode 100644 index 385b44e..0000000 --- a/DRAFT/level-3/dependencies.md +++ /dev/null @@ -1,50 +0,0 @@ -# Зависимости Level 3 - -> Уточнение графа зависимостей Level 2 внутри домена. - -## Связанные правила - -- [`SLM-L3-BUSINESS-A004`](../rules/level-3.md#slm-l3-business-a004) -- [`SLM-L3-DEPENDENCY-R011`](../rules/level-3.md#slm-l3-dependency-r011) -- [`SLM-L3-ENVIRONMENT-A012`](../rules/level-3.md#slm-l3-environment-a012) -- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) - -## Направление внутри домена - -| Исходный модуль | Допустимые зависимости | -|---|---| -| `business` | Собственные сегменты, нейтральные ресурсы `shared`, в ограниченных случаях — публичные типы другого модуля `business` | -| Адаптер | Контракты `business`, модули `infra`, конкретная техническая реализация и нейтральные ресурсы `shared` | -| Модуль группы `presets` | Фабрика и контракты `business`, закрытые или самостоятельные адаптеры | -| `react` | Контракты `business`, готовый API и React | -| Модуль-владелец графа | Публичные API модулей `business`, типовых сборок и модулей фреймворков, входящих в граф | - -Модуль `business` не импортирует адаптеры, сборки, модули фреймворков, `infra`, продуктовые SDK, хранилища, API браузера или Node.js и конфигурацию среды. Адаптер не импортирует сборку или модуль фреймворка. Модуль сборки не импортирует модуль фреймворка. - -## Междоменные зависимости - -Модуль `business` одного домена не создаёт фабрику другого домена и не вызывает его исполняемый API напрямую. Если домену `orders` нужны сведения об авторизации, `orders/business` описывает собственный минимальный порт, а владелец графа передаёт его реализацию поверх уже созданного `AuthApi`. - -```text -сборка auth - → AuthApi - → сборка orders - → OrdersApi -``` - -Импорт публичного типа из модуля `business` другого домена допустим только при реальной ацикличной зависимости. Такой импорт не разрешает вызывать API другого домена. - -Прямой импорт даже чистой функции не служит обходом порта. Независимое общее правило принадлежит `shared`, а предметная возможность другого домена передаётся через порт. - -## Границы сред выполнения - -Серверная сборка или адаптер получает отдельную публичную точку входа и предусмотренную фреймворком либо сборщиком метку: - -```text -business # подходит клиенту и серверу -presets/application # подходит клиенту, если совместимы адаптеры -presets/request # только сервер -react # клиентский модуль React -``` - -Серверная точка входа не реэкспортируется через `business`, `react`, клиентскую сборку или корень домена. Путь `server/` или `client/` сам по себе ничего не доказывает: проверяется весь граф импортов, достижимый из точки входа. diff --git a/DRAFT/level-3/domains/README.md b/DRAFT/level-3/domains/README.md deleted file mode 100644 index d4aae1f..0000000 --- a/DRAFT/level-3/domains/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Домены Level 3 - -> Пояснение строгой внутренней архитектуры домена. - -Level 3 заменяет доменный модуль Level 2 немодульной предметной границей — доменом. Внутри неё размещаются модули с разными техническими ролями. Это не новый слой и не обязательный каркас для каждого проекта. - -## Связанные правила - -- [`SLM-L3-DOMAIN-R001`](../../rules/level-3.md#slm-l3-domain-r001) -- [`SLM-L3-DOMAIN-A002`](../../rules/level-3.md#slm-l3-domain-a002) -- [`SLM-L3-BUSINESS-R003`](../../rules/level-3.md#slm-l3-business-r003) -- [`SLM-L1-MODULE-A004`](../../rules/level-1.md#slm-l1-module-a004) - -## Роли внутри домена - -```text -Модуль бизнес-логики определяет поведение и публичный контракт. -Порты описывают возможности, которые нужны бизнес-логике. -Адаптеры реализуют порты в конкретной среде. -Типовые сборки повторяемо создают API. -Модуль React связывает готовый API с React. -Владелец графа удерживает экземпляр API и завершает его жизненный цикл. -``` - -| Роль | Структурный вид | Когда появляется | -|---|---|---| -| Бизнес-логика | Обязательный модуль `business` | Всегда | -| Типовая сборка | Модуль внутри `presets` | Нужен повторяемый способ сборки | -| Адаптер | Закрытый сегмент сборки или модуль внутри `adapters` | Нужна техническая интеграция | -| Связь с React | Модуль `react` непосредственно в домене | Домен предоставляет API для React | - -## Форма домена - -```text -domains/auth/ -├── business/ -│ ├── errors/ -│ ├── lib/ -│ ├── ports/ -│ ├── services/ -│ ├── types/ -│ └── index.ts -├── presets/ -│ └── application/ -│ ├── adapters/ -│ └── index.ts -├── adapters/ -│ └── identity-provider/ -│ └── index.ts -└── react/ - ├── hooks/ - ├── providers/ - └── index.ts -``` - -Модуль `business` обязателен; остальные ветки появляются по необходимости. `presets` и `adapters` являются группами без собственного исполняемого кода и API. Каталоги `errors`, `lib`, `ports`, `services`, `types`, `hooks` и `providers` являются сегментами соответствующих модулей-владельцев. - -Навигационные группы в слое `domains` допустимы, но не являются доменами и не меняют их публичные границы. Основные примеры Level 3 показывают домены непосредственно в `domains`. - -## Публичные API модулей - -Корень домена не имеет `index.ts` и не реэкспортирует дочерние модули. Внешний потребитель использует публичную точку входа нужного модуля: - -```ts -import { authFactory, type AuthApi } from '@/domains/auth/business' -import { createApplicationAuth } from '@/domains/auth/presets/application' -import { AuthProvider, useAuth } from '@/domains/auth/react' -``` - -Закрытый адаптер внутри `presets/application/adapters` не получает внешней точки входа. Адаптер, оформленный самостоятельным модулем, предоставляет минимальный публичный API. Доступ к нему за пределами домена допускается только как явно объявленная точка расширения интеграции. - -## Карта раздела - -- [Граница домена](./domain.md) -- [Модуль бизнес-логики](./business.md) -- [Фабрика, порты и адаптеры](./factory-ports-adapters.md) -- [Типовые сборки и SSR](./presets.md) -- [Модуль React](./framework-bindings.md) -- [Тестирование](./testing.md) -- [Пример переноса домена](./auth-example.md) -- [Открытые вопросы](./open-questions.md) diff --git a/DRAFT/level-3/domains/auth-example.md b/DRAFT/level-3/domains/auth-example.md deleted file mode 100644 index ae17b2e..0000000 --- a/DRAFT/level-3/domains/auth-example.md +++ /dev/null @@ -1,96 +0,0 @@ -# Перенос домена `auth` - -> Проверочный пример Level 3. Он показывает направление изменений, а не обязательный каркас. - -## Исходная проблема - -В более ранней форме SLM контракт бизнес-логики домена `auth` и его техническая сборка могли находиться в разных местах: - -```text -business/auth/ -├── auth.factory.ts -├── errors/ -├── hooks/ -├── services/ -├── types/ -└── index.ts - -compositions/business/auth/ -├── adapters/ -├── create-auth-business.ts -└── index.ts -``` - -Такое устройство отделяет бизнес-логику от конкретной среды, но разносит одну предметную область по разным архитектурным местам. Level 3 размещает эти части внутри одного домена, сохраняя границы между их ролями. - -## Целевая форма - -```text -domains/auth/ -├── business/ -│ ├── auth.factory.ts -│ ├── errors/ -│ ├── lib/ -│ ├── ports/ -│ ├── services/ -│ ├── types/ -│ └── index.ts -├── presets/ -│ ├── application/ -│ │ ├── adapters/ -│ │ └── index.ts -│ └── request/ -│ └── index.ts -└── react/ - ├── hooks/ - ├── providers/ - └── index.ts -``` - -## Разделение обязанностей - -| Исходная часть | Назначение в Level 3 | -|---|---| -| `auth.factory.ts`, сценарии, проверки и ошибки домена | `domains/auth/business` | -| SDK, хранилище и конкретная система управления состоянием | Закрытые адаптеры выбранной сборки | -| Повторяемая сборка для браузерного приложения | `domains/auth/presets/application` | -| Файлы cookie, заголовки и клиент одного запроса | `domains/auth/presets/request` | -| React-хуки, провайдер и интерфейс домена | `domains/auth/react` | -| Текст страницы, перенаправление и устройство экрана | Модуль-потребитель в `compositions` | - -## Проверка границ - -`authFactory` не импортирует `useAuth`, `'use client'`, SDK или хранилище. React-хук строится поверх готового `AuthApi`, например через независимые от фреймворка методы `getSnapshot` и `subscribe`. - -Нормализация номера телефона может быть публичной чистой функцией бизнес-логики: - -```ts -import { - normalizeAuthPhone, - validateAuthPhone, -} from '@/domains/auth/business' -``` - -Интерфейс использует её для ранней подсказки, но `requestPhoneOtp` повторно проверяет значение внутри предметного сценария. - -## Контракт ошибок - -`AuthBusinessError` остаётся закрытой реализацией. Потребитель получает только устойчивый контракт: - -```ts -import { - AUTH_ERROR_CODES, - isAuthError, -} from '@/domains/auth/business' -``` - -Так композиция React может выбрать сообщение или поведение повторной попытки по `code`, не зная класс ошибки SDK, статус HTTP или закрытый конструктор. - -## Порядок перехода - -1. Выделить точку входа `business` и убедиться, что её полный граф импортов не зависит от среды. -2. Перенести конкретные технические реализации в адаптеры выбранной сборки. -3. Оформить повторяемую сборку как `presets/application`. -4. Перенести хуки и провайдер в `react`, передавая им готовый API. -5. Сохранить интерфейс конкретной страницы и владение общим графом в `compositions`. -6. Добавить тесты фабрики, адаптеров, сборки и границы React до удаления старого пути. diff --git a/DRAFT/level-3/domains/business.md b/DRAFT/level-3/domains/business.md deleted file mode 100644 index 9140074..0000000 --- a/DRAFT/level-3/domains/business.md +++ /dev/null @@ -1,91 +0,0 @@ -# Модуль бизнес-логики внутри домена - -> Пояснение смыслового центра домена. - -## Связанные правила - -- [`SLM-L3-BUSINESS-R003`](../../rules/level-3.md#slm-l3-business-r003) -- [`SLM-L3-BUSINESS-A004`](../../rules/level-3.md#slm-l3-business-a004) -- [`SLM-L3-PORT-R007`](../../rules/level-3.md#slm-l3-port-r007) -- [`SLM-L1-MODULE-A004`](../../rules/level-1.md#slm-l1-module-a004) - -## Роль - -`business` — единственный обязательный модуль домена. Он владеет: - -- публичными предметными сценариями и `DomainApi`; -- фабрикой, типом зависимостей `Deps` и портами; -- предметными типами и контрактами; -- детерминированными правилами, проверкой и нормализацией данных; -- публичным контрактом ошибок предметной области; -- моделью состояния, командами и средствами чтения этого состояния. - -Модуль `business` не владеет SDK, реализацией хранилища, API браузера или Node.js, связью с фреймворком, конфигурацией среды и конкретной системой управления состоянием. - -## Публичный API - -Точка входа `business` открывает только контракт, необходимый потребителям, сборкам и адаптерам: - -```ts -export { authFactory } from './auth.factory' -export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error' -export { normalizeAuthPhone, validateAuthPhone } from './lib/auth-phone' - -export type { - AuthApi, - AuthDeps, - AuthError, - AuthErrorCode, - AuthFactory, - AuthPhonePort, - AuthSessionPort, - AuthState, -} from './types' -``` - -Типы портов экспортируются, потому что сборки и самостоятельные адаптеры реализуют эти контракты. Сервисы, внутренние преобразователи, конструктор ошибки, преобразование исходной ошибки, ключ хранения и конкретный механизм состояния остаются закрытыми. - -## Типы и чистые функции - -Каталоги `types`, `errors`, `lib`, `ports`, `services` и `tests` являются сегментами модуля `business`, а не отдельными API домена. Тип размещается у владельца: - -| Контракт | Владелец | -|---|---| -| `AuthApi`, `AuthDeps`, `AuthState`, порты | `business` | -| DTO SDK и транспортная ошибка | Адаптер или `infra` | -| Свойства React-провайдера | `react` | -| Модель представления экрана | Модуль-потребитель в `compositions` | - -Чистая предметная функция может быть публичной, только если выражает предметное правило и нужна реальному внешнему потребителю. Она получает все данные аргументами, детерминирована и не использует `Deps`, состояние, часы, генератор случайных значений, окружение или фреймворк. - -Потребитель может применять `validateAuthPhone` для ранней подсказки в интерфейсе, но публичный сценарий повторно проверяет данные на собственной границе. - -## Ошибки предметной области - -При сбое публичный сценарий выдаёт только ошибку из контракта домена. Исходная ошибка, класс SDK, статус HTTP, тело ответа и транспортный код не становятся API потребителя. - -```ts -export const AUTH_ERROR_CODES = { - PHONE_OTP_PHONE_INVALID: 'AUTH_PHONE_OTP_PHONE_INVALID', - PHONE_OTP_REQUEST_FAILED: 'AUTH_PHONE_OTP_REQUEST_FAILED', - PHONE_OTP_VERIFY_CODE_INVALID: 'AUTH_PHONE_OTP_VERIFY_CODE_INVALID', - PHONE_OTP_RESEND_TOO_SOON: 'AUTH_PHONE_OTP_RESEND_TOO_SOON', -} as const - -export type AuthError = Readonly<{ - code: AuthErrorCode - retryAfterSeconds: number | null -}> - -export const isAuthError = (value: unknown): value is AuthError => { - // Проверка публичной формы ошибки во время выполнения. -} -``` - -Если публичный API использует исключения, точка входа экспортирует проверку типа, коды и доступную только для чтения форму ошибки, но не её конструктор или преобразователь исходной ошибки. Если проект выбирает размеченный тип `Result`, тот же контракт выражается в ветви результата. Один API не смешивает оба способа для одинаковых сценариев. - -## Состояние домена - -Модуль `business` определяет форму `AuthState`, начальное состояние, допустимые переходы и публичный способ наблюдения. Конкретное хранилище, сохранение данных, источник подписки и хук фреймворка реализуются снаружи через порты и адаптеры. - -Независимый от фреймворка интерфейс наблюдения может состоять из `getSnapshot` и `subscribe`. Это часть API бизнес-логики, а не React-хук или `StoreApi` конкретной библиотеки. diff --git a/DRAFT/level-3/domains/domain.md b/DRAFT/level-3/domains/domain.md deleted file mode 100644 index 99f15cb..0000000 --- a/DRAFT/level-3/domains/domain.md +++ /dev/null @@ -1,63 +0,0 @@ -# Граница домена - -> Пояснение предметной и структурной границы Level 3. - -## Связанные правила - -- [`SLM-L3-DOMAIN-R001`](../../rules/level-3.md#slm-l3-domain-r001) -- [`SLM-L3-DOMAIN-A002`](../../rules/level-3.md#slm-l3-domain-a002) -- [`SLM-L3-BUSINESS-R003`](../../rules/level-3.md#slm-l3-business-r003) -- [`SLM-L1-MODULE-R011`](../../rules/level-1.md#slm-l1-module-r011) -- [`SLM-L1-GROUP-R007`](../../rules/level-1.md#slm-l1-group-r007) - -## Предметная граница - -Домен представляет одну связную предметную область: `auth`, `catalog`, `orders` или `checkout`. Он объединяет её бизнес-логику, технические интеграции, повторяемые сборки и модули фреймворков, но не превращается в большой модуль со смешанными ролями. - -Домен является предметной границей, а не владельцем исполняемого кода в смысле Level 1. Каждый сценарий, адаптер и способ сборки принадлежит конкретному модулю. Поэтому одна предметная область может иметь несколько публичных API, не нарушая правило о единственном владельце ответственности. - -## Структурные виды и роли - -| Путь | Роль | Структурный вид | -|---|---|---| -| `domains/auth` | Предметная область авторизации | Домен | -| `domains/auth/business` | Бизнес-логика | Модуль | -| `domains/auth/business/ports` | Необходимые бизнес-логике возможности | Сегмент | -| `domains/auth/presets` | Навигация по типовым сборкам | Группа | -| `domains/auth/presets/application` | Сборка уровня приложения | Модуль | -| `domains/auth/presets/application/adapters` | Закрытые адаптеры сборки | Сегмент | -| `domains/auth/adapters` | Навигация по самостоятельным адаптерам | Группа | -| `domains/auth/adapters/identity-provider` | Повторно используемый адаптер | Модуль | -| `domains/auth/react` | Связь с React | Модуль | - -Роль отвечает на вопрос, что делает код. Структурный вид определяет, какую архитектурную границу он образует. Имя папки само по себе не доказывает ни роль, ни структурный вид. - -## Корень домена - -Корень домена не содержит реализацию, состояние, ресурсы жизненного цикла, `index.ts` или общий файл реэкспортов. Его прямыми детьми могут быть модуль `business`, группы `presets` и `adapters`, а также модули фреймворков, например `react`. - -Корневые ветки `model`, `types`, `errors`, `lib`, `ui`, `client`, `server` или `tests` не создаются автоматически. Такой каталог должен быть либо сегментом модуля-владельца, либо самостоятельным модулем с одной из допустимых ролей домена. - -## Публичная граница - -```text -@/domains/auth/business -@/domains/auth/presets/application -@/domains/auth/react -``` - -Эти пути являются публичными API отдельных модулей. Корневого пути `@/domains/auth` для исполняемого кода не существует: он не должен объединять независимый от среды модуль `business`, клиентский React и серверную сборку через `export *`. - -## Граница с другими слоями - -| Ответственность | Владелец | -|---|---| -| Предметные сценарии, контракты, модель состояния и ошибки | `domains/auth/business` | -| Технический адаптер одной сборки | Сегмент соответствующего модуля в `presets` | -| Повторно используемая интеграция авторизации | Самостоятельный модуль адаптера | -| Повторяемая сборка `AuthApi` | Модуль в `presets` | -| Провайдер, хук и относящийся к домену интерфейс React | `domains/auth/react` | -| Страница, маршрут, перенаправление, экран и конкретный визуальный результат | Модуль `compositions` | -| Обёртка над SDK или технический сервис без семантики авторизации | Модуль `infra` | - -Зависимость от фреймворка сама по себе не делает интерфейс частью домена. Компонент принадлежит `react`, только когда работает с контрактом домена и не определяет страницу, маршрут или продуктовую композицию. diff --git a/DRAFT/level-3/domains/factory-ports-adapters.md b/DRAFT/level-3/domains/factory-ports-adapters.md deleted file mode 100644 index bae0f57..0000000 --- a/DRAFT/level-3/domains/factory-ports-adapters.md +++ /dev/null @@ -1,88 +0,0 @@ -# Фабрика, порты и адаптеры - -> Пояснение границы между бизнес-логикой и средой выполнения. - -## Связанные правила - -- [`SLM-L3-FACTORY-R005`](../../rules/level-3.md#slm-l3-factory-r005) -- [`SLM-L3-FACTORY-R006`](../../rules/level-3.md#slm-l3-factory-r006) -- [`SLM-L3-PORT-R007`](../../rules/level-3.md#slm-l3-port-r007) -- [`SLM-L3-ADAPTER-R008`](../../rules/level-3.md#slm-l3-adapter-r008) -- [`SLM-L3-BUSINESS-A004`](../../rules/level-3.md#slm-l3-business-a004) - -## Фабрика и экземпляр API - -```text -фабрика + реализации портов → экземпляр API бизнес-логики -``` - -Фабрика принадлежит модулю `business`, получает полный набор `AuthDeps` и возвращает `AuthApi`: - -```ts -export type AuthFactory = (deps: AuthDeps) => AuthApi -``` - -Все типовые сборки одной фабрики предоставляют полный набор портов и получают API одного контракта. Браузер, обработчик запроса и серверное действие не требуют разных фабрик только из-за среды выполнения. Сборка может открыть потребителю более узкое представление API, но не меняет контракт фабрики. - -Вызов фабрики создаёт только объекты и замыкания без побочных эффектов. Он не выполняет запросы, не читает файлы cookie, хранилище или переменные окружения, не запускает подписки и таймеры, не обращается к API платформы, не выбирает адаптер и не выполняет операции жизненного цикла фреймворка. - -## Независимый от среды граф импортов - -Проверяется весь граф рабочего кода, достижимый из точки входа `business`, а не только файл фабрики. Он не должен достигать: - -- React, Vue, Next.js и служебных меток фреймворка; -- границ `client-only`, `server-only`, API браузера или Node.js; -- SDK, сгенерированного клиента, реализации хранилища или конкретной библиотеки состояния; -- адаптеров, сборок, модулей фреймворков и конфигурации среды. - -Удаление неиспользуемого кода при сборке не доказывает изоляцию. Импорт только типов из конкретной реализации создаёт ту же архитектурную зависимость и также запрещён. - -## Порты - -Порт принадлежит бизнес-логике и описывает возможность на языке предметной области: - -```ts -export type AuthPhonePort = { - requestCode: (phone: string) => Promise - verifyCode: (data: VerifyPhoneOtpData) => Promise -} - -export type AuthSessionPort = { - getSnapshot: () => AuthState - subscribe: (listener: () => void) => () => void - setToken: (token: string | null) => void -} -``` - -Порт не принимает клиент SDK, сгенерированную операцию, `Request`, `Window`, React-хук, `StoreApi` или тип конкретной среды. Он отделяет контракт от реализации, а не скрывает отсутствие возможности. Необязательный порт или метод, который намеренно падает в одной из сред, нарушает контракт фабрики. - -`unknown` допустим только на границе непроверенного внешнего результата. Бизнес-логика обязана проверить такое значение до преобразования в предметный результат, состояние или ошибку. Если адаптер уже может вернуть устойчивый предметный результат, порт описывает этот результат, а не DTO конкретного транспорта. - -## Адаптеры - -Адаптер соединяет порт с конкретной технической реализацией: - -```text -порт business ← адаптер → SDK / хранилище / платформа / данные запроса -``` - -Адаптер может преобразовать предметные аргументы в транспортные, вызвать внешний источник, привести технический результат к контракту порта и вернуть исходный сбой. Он не определяет код ошибки домена, резервное предметное поведение, инвариант или публичный метод `AuthApi`. - -По умолчанию адаптер является закрытым сегментом минимальной типовой сборки: - -```text -domains/auth/presets/application/ -├── adapters/ -│ └── auth-phone.adapter.ts -└── index.ts -``` - -Если адаптер нужен нескольким сборкам или имеет самостоятельную ответственность интеграции, он становится отдельным модулем: - -```text -domains/auth/adapters/ -└── identity-provider/ - └── index.ts -``` - -Самостоятельный адаптер сохраняет минимальный публичный API. Его появление не делает конкретный SDK частью публичного контракта `business`. diff --git a/DRAFT/level-3/domains/framework-bindings.md b/DRAFT/level-3/domains/framework-bindings.md deleted file mode 100644 index e46c03d..0000000 --- a/DRAFT/level-3/domains/framework-bindings.md +++ /dev/null @@ -1,71 +0,0 @@ -# Модуль React внутри домена - -> Пояснение границы фреймворка на примере React. - -## Связанные правила - -- [`SLM-L3-FRAMEWORK-R013`](../../rules/level-3.md#slm-l3-framework-r013) -- [`SLM-L3-BUSINESS-A004`](../../rules/level-3.md#slm-l3-business-a004) -- [`SLM-L3-ASSEMBLY-R010`](../../rules/level-3.md#slm-l3-assembly-r010) - -## Имя и место модуля - -Зависящий от фреймворка модуль находится непосредственно в домене и называется именем фреймворка: - -```text -domains/auth/react/ -├── components/ -├── hooks/ -├── providers/ -├── types/ -└── index.ts -``` - -Имя `react` точно обозначает зависимость и не требует пустой промежуточной группы `framework/react` или `bindings/react`. Если домен действительно поддерживает другой фреймворк, он получает отдельный соседний модуль, например `vue`. - -## Роль модуля React - -Модуль React может: - -- передавать готовый `AuthApi` через контекст и провайдер; -- предоставлять хук доступа к API или состоянию; -- связывать жизненный цикл React с подпиской; -- реализовывать относящийся к домену React-компонент. - -Он не меняет предметные правила, не создаёт ошибки домена, не выбирает адаптеры и не вызывает фабрику или типовую сборку. Сборка остаётся у модуля-владельца графа; модуль React получает готовый экземпляр. - -```tsx -type AuthProviderProps = PropsWithChildren<{ - api: AuthApi -}> - -export const AuthProvider = ({ api, children }: AuthProviderProps) => { - return {children} -} -``` - -## Наблюдение за состоянием - -Если `AuthApi` предоставляет независимый от фреймворка интерфейс `getSnapshot` и `subscribe`, модуль React может использовать `useSyncExternalStore`: - -```tsx -'use client' - -export const useAuthState = () => { - const api = useAuth() - - return useSyncExternalStore( - api.subscribe, - api.getSnapshot, - api.getSnapshot, - ) -} -``` - -Модуль `business` не импортирует React и не возвращает React-хук как единственный способ наблюдать состояние. Подпиской и её очисткой управляет `useSyncExternalStore`. - -## Интерфейс домена и композиции - -Компонент принадлежит `react`, если его ответственность ограничена контрактом домена: он работает с `AuthApi`, состоянием и устойчивыми ошибками домена. Он не владеет страницей, маршрутом, перенаправлением, продуктовым текстом или композицией нескольких доменов. - -Экран, результат маршрута, локальный текст ошибки, перенаправление и интерфейс конкретной страницы остаются в `compositions`. Зависимость от React сама по себе не доказывает принадлежность домену. diff --git a/DRAFT/level-3/domains/open-questions.md b/DRAFT/level-3/domains/open-questions.md deleted file mode 100644 index dde7c22..0000000 --- a/DRAFT/level-3/domains/open-questions.md +++ /dev/null @@ -1,24 +0,0 @@ -# Открытые вопросы Level 3 - -> Эти вопросы не являются правилами и не отменяют уже принятые границы. - -## Зафиксированные решения - -- Домен является сущностью только Level 3; в Level 2 предметная область остаётся одним доменным модулем. -- Корень домена не имеет общей точки входа для исполняемого кода. -- Модуль фреймворка называется его именем и размещается непосредственно в домене: `domains/auth/react`. -- Модуль фреймворка получает готовый API и не выполняет сборку. -- Взаимодействие бизнес-логики разных доменов во время выполнения проходит через порт потребителя и владельца графа. -- Навигационные группы допустимы в `domains`, но не являются частью базовых примеров. - -## Форма передачи ошибок - -Level 3 требует устойчивый контракт ошибок домена, но не навязывает единый способ передачи: исключение с проверкой типа во время выполнения или размеченный `Result`. Нужно проверить, нужна ли общая политика для всех доменов одного приложения и как она влияет на серверные действия и сериализацию RPC. - -## Наблюдение за состоянием - -На реальном примере SSR и гидратации нужно проверить точную форму независимого от фреймворка интерфейса наблюдения: начальный снимок, параллельный рендеринг, сброс данных, очистку подписки и поведение после завершения запроса. Методы `getSnapshot` и `subscribe` пока служат иллюстрацией, а не обязательной файловой формой. - -## Автоматическая проверка архитектуры - -Нужно выбрать формат конфигурации проекта для автоматической проверки корней доменов, их модулей, публичных точек входа, меток сред и запрещённых транзитивных импортов. Проверка должна опираться на граф и описание структуры, а не только на имена папок. diff --git a/DRAFT/level-3/domains/presets.md b/DRAFT/level-3/domains/presets.md deleted file mode 100644 index 0000481..0000000 --- a/DRAFT/level-3/domains/presets.md +++ /dev/null @@ -1,89 +0,0 @@ -# Типовые сборки и SSR - -> Пояснение повторяемой сборки домена. - -## Связанные правила - -- [`SLM-L3-PRESET-R009`](../../rules/level-3.md#slm-l3-preset-r009) -- [`SLM-L3-ASSEMBLY-R010`](../../rules/level-3.md#slm-l3-assembly-r010) -- [`SLM-L3-ENVIRONMENT-A012`](../../rules/level-3.md#slm-l3-environment-a012) -- [`SLM-L3-FACTORY-R006`](../../rules/level-3.md#slm-l3-factory-r006) - -## Роль типовой сборки - -Модуль в группе `presets` задаёт именованный повторяемый способ создания API одной фабрики для конкретного контекста выполнения. Он выбирает реализации портов, создаёт `AuthApi` и передаёт вызывающему коду операции жизненного цикла, определённые модулями-владельцами ресурсов. - -```text -authFactory -├── presets/application → AuthApi уровня приложения -├── presets/request → AuthApi одного запроса -└── presets/server-action → AuthApi серверного действия -``` - -Среда определяется выбранной сборкой и её адаптерами, а не параметром `mode` внутри фабрики. Тесты создают отдельную сборку напрямую через фабрику и не требуют общего модуля `presets/testing`. - -## Структура и публичный API - -```text -domains/auth/presets/application/ -├── adapters/ -├── create-application-auth.ts -├── create-application-auth.test.ts -└── index.ts -``` - -`application` — только пример имени. Модуль называется по контексту выполнения или устойчивому назначению: `application`, `request`, `server-action`. Временный потребитель не должен давать имя повторно используемой конфигурации. - -```ts -export const createApplicationAuth = (): AuthApi => { - return authFactory({ - phone: createApplicationAuthPhoneAdapter(), - session: createApplicationAuthSessionAdapter(), - }) -} -``` - -Типовая сборка не добавляет сценарии, не меняет преобразование ошибок и не скрывает предметные правила. Одноразовый владелец графа может вызвать фабрику напрямую, если сам выбирает все порты и отвечает за жизненный цикл результата. - -## Область жизни - -Модуль сборки объявляет ожидаемую область жизни экземпляра API. Сборка `application` используется в течение жизни приложения, а `request` создаёт новый экземпляр для каждого запроса. Владелец графа не хранит данные одного запроса в общем экземпляре приложения. - -Если сборка создаёт ресурс жизненного цикла, вызывающий код получает явную операцию очистки: - -```ts -export type AuthRequestAssembly = { - api: AuthApi - dispose: () => void | Promise -} - -export const createAuthForRequest = ( - input: AuthRequestInput, -): AuthRequestAssembly => { - const session = createRequestSessionAdapter(input) - - return { - api: authFactory({ - phone: createRequestAuthPhoneAdapter(input), - session, - }), - dispose: session.dispose, - } -} -``` - -Создание API через фабрику или типовую сборку не запускает ввод-вывод и подписки. Если ресурс нужно запустить явно, модуль-владелец предоставляет отдельную операцию. Владелец графа вызывает её после начала своей области жизни и выполняет очистку при завершении. - -## Серверная граница - -Серверная сборка имеет отдельную точку входа и служебную метку выбранного фреймворка или сборщика: - -```ts -import 'server-only' - -export { createAuthForRequest } from './create-auth-for-request' -``` - -Эта точка входа не реэкспортируется через `business`, `react` или клиентскую сборку. Серверный адаптер также может иметь собственную метку, защищающую от ошибочного прямого импорта. - -Модуль фреймворка не вызывает сборку и не создаёт фабрику. Он получает готовый `AuthApi` от владельца графа, поэтому жизненный цикл React не смешивается с технической сборкой зависимостей. diff --git a/DRAFT/level-3/domains/testing.md b/DRAFT/level-3/domains/testing.md deleted file mode 100644 index a87e5fd..0000000 --- a/DRAFT/level-3/domains/testing.md +++ /dev/null @@ -1,70 +0,0 @@ -# Тестирование домена - -> Проверка границ и поведения Level 3. - -## Связанные правила - -- [`SLM-L3-TEST-R014`](../../rules/level-3.md#slm-l3-test-r014) -- [`SLM-L3-FACTORY-R006`](../../rules/level-3.md#slm-l3-factory-r006) -- [`SLM-L3-ASSEMBLY-R010`](../../rules/level-3.md#slm-l3-assembly-r010) - -## Принцип размещения - -Тест находится рядом с модулем-владельцем проверяемой ответственности. У домена нет общей корневой папки `tests/`. - -| Проверяемая граница | Владелец теста | -|---|---| -| Предметные сценарии, состояние и ошибки домена | `business` | -| Чистая функция бизнес-логики | Соответствующий сегмент `business` | -| Реализация порта | Адаптер | -| Выбор зависимостей, область жизни и очистка | Модуль в `presets` | -| Провайдер, хук и жизненный цикл React | `react` | -| Граф нескольких доменов | Модуль-владелец графа | -| Полный пользовательский поток | Точка входа сквозного теста приложения | - -## Тесты через фабрику - -Тесты через фабрику являются главным доказательством публичного поведения бизнес-логики. Они импортируют только публичный API `business` и передают управляемые тестовые реализации портов: - -```ts -import { - AUTH_ERROR_CODES, - authFactory, - isAuthError, -} from '@/domains/auth/business' - -it('maps source failure to domain error', async () => { - const requestCode = vi.fn().mockRejectedValue(new Error('Network failed')) - const api = authFactory(createAuthTestDeps({ requestCode })) - - await expect(api.requestPhoneOtp('+79991112233')).rejects.toMatchObject({ - code: AUTH_ERROR_CODES.PHONE_OTP_REQUEST_FAILED, - }) -}) -``` - -Такой набор тестов проверяет форму публичного API, отсутствие побочных эффектов при создании, успешные и ошибочные сценарии, проверку входных данных, переходы состояния и порядок внешних операций. - -Тест `business` не использует React, реальный SDK, хранилище или типовую сборку приложения. Если сценарий нельзя проверить без них, техническая зависимость проникла внутрь бизнес-логики. - -## Вспомогательная тестовая сборка - -Закрытая тестовая функция уменьшает повторение, но не является модулем в `presets`: - -```ts -const { api, ports, state } = createAuthTestHarness({ requestCode }) -``` - -Она создаёт новый экземпляр для каждого теста, допускает нужные сценарию замены и не экспортируется через рабочую точку входа. Модуль `presets/testing` по умолчанию не создаётся. - -## Тесты остальных ролей - -Тест адаптера проверяет вызванную техническую операцию, переданные данные, преобразование аргументов, результат или ошибку согласно контракту порта и очистку подписки. Он не повторяет преобразование ошибок домена и полный набор предметных сценариев. - -Тест типовой сборки проверяет полный набор портов, выбор адаптеров, отсутствие ввода-вывода при создании, область жизни экземпляра, передачу операции очистки и границу клиента и сервера. Он не повторяет успешные предметные сценарии. - -Тест React получает тестовый `AuthApi` и проверяет провайдер, хук доступа, обновление по `subscribe`, очистку после размонтирования и поведение в `StrictMode`. Минимальный интеграционный тест с настоящей фабрикой добавляется только при отдельном риске интеграции. - -## Минимальный набор - -Тест создаётся в ответ на реальный риск, а не ради заполнения каркаса. При этом публичный предметный сценарий требует теста через фабрику, типовая сборка приложения — теста сборки, адаптер с нетривиальным преобразованием данных — теста адаптера, а модуль React с поведением жизненного цикла — теста фреймворка. diff --git a/DRAFT/level-3/terminology.md b/DRAFT/level-3/terminology.md deleted file mode 100644 index f726111..0000000 --- a/DRAFT/level-3/terminology.md +++ /dev/null @@ -1,87 +0,0 @@ -# Терминология Level 3 - -> Нормативные определения рабочего черновика. Этот раздел не объявляет правила. - -Level 3 наследует терминологию Level 1 и Level 2 и заменяет доменный модуль Level 2 новой структурной сущностью — доменом Level 3. - -## Домен Level 3 - -### Домен - -Немодульная предметная граница слоя `domains`, представляющая одну самостоятельную предметную область. Домен объединяет модули и группы этой области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла графа зависимостей. - -Домен не является ни модулем, ни группой. Он может находиться непосредственно в слое `domains` или внутри навигационной группы этого слоя. Такая группа вправе содержать домены, но сохраняет остальные свойства группы Level 1. - -Модуль, расположенный непосредственно внутри домена или одной из его групп, не считается вложенным: его ближайшая внешняя граница не является модулем. - -### Модуль домена - -Модуль внутри домена, ответственность которого соответствует одной технической роли: бизнес-логике, типовой сборке, адаптеру или связи с фреймворком. Каждый модуль домена остаётся обычным модулем Level 1 со своим публичным API и узлом графа зависимостей. - -### Модуль бизнес-логики - -Обязательный модуль `business` внутри домена. Он определяет публичные предметные сценарии и контракты, фабрику, порты, ошибки предметной области, детерминированные правила и модель состояния. - -Модуль `business` не зависит от конкретной среды выполнения, технической реализации или фреймворка. - -### Порт - -Минимальный контракт возможности, которая нужна бизнес-логике для выполнения предметного сценария. Порт принадлежит модулю `business`, использует язык предметной области и не раскрывает SDK, сгенерированные DTO, хранилище, хук, объект платформы или другую техническую реализацию. - -### Фабрика - -Функция модуля `business`, которая получает полный набор портов и создаёт экземпляр публичного API бизнес-логики. Фабрика не выбирает реализации портов и не является готовой сборкой для конкретной среды. - -### Адаптер - -Код, который реализует один или несколько портов поверх конкретного SDK, хранилища, API платформы, данных запроса, системы управления состоянием или технического сервиса. Адаптер может быть закрытым сегментом модуля сборки либо самостоятельным модулем в группе `adapters`. - -### Типовая сборка - -Модуль в группе `presets`, который повторяемо создаёт API одной фабрики для именованного контекста выполнения. Он выбирает реализации портов и возвращает вызывающему коду операции, необходимые для управления жизненным циклом созданного экземпляра. - -### Модуль фреймворка - -Модуль домена, который связывает публичный API бизнес-логики с конкретным фреймворком. Он размещается непосредственно в домене и называется именем фреймворка: `react`, `vue` и аналогично. Такой модуль получает готовый API, но не вызывает фабрику и не реализует технические адаптеры. - -### Место сборки - -Место, которое вызывает фабрику, передаёт полный набор портов и получает экземпляр API. Повторяемое место сборки оформляется модулем в группе `presets`; одноразовая сборка принадлежит явному владельцу графа. Модуль фреймворка не является местом сборки. - -### Владелец графа - -Код модуля, который удерживает собранный граф и его экземпляры API в пределах объявленной области жизни, а также вызывает предоставленные операции запуска и очистки. Владелец графа не заменяет владельца ресурса: контракт жизненного цикла определяет модуль, которому принадлежит ресурс, по правилам Level 1. - -Владельцем графа может быть модуль приложения, маршрута, страницы, запроса или теста. - -### Граница среды выполнения - -Граница между графами импортов, предназначенными только для клиента, только для сервера или для обеих сред. Она определяется достижимостью импортов, а не названием папки или удалением неиспользуемого кода при сборке. - -## Виды владения - -Level 3 разделяет три разных вопроса: - -| Вопрос | Ответственный | -|---|---| -| Какая предметная область и словарь объединяют код | Домен | -| Кто владеет самостоятельной ответственностью и публичным API | Конкретный модуль | -| Кто удерживает экземпляры API и завершает их жизненный цикл | Владелец графа | - -Например, модуль `business` владеет моделью `AuthState` и допустимыми переходами между её состояниями. Модуль, содержащий адаптер, владеет конкретным механизмом хранения и определяет контракт его жизненного цикла. Владелец графа удерживает созданный `AuthApi` в допустимой области жизни и вызывает очистку. - -## Структурная модель - -```text -корень SLM -└── domains - └── домен - ├── модуль business - ├── группа presets - │ └── модуль типовой сборки - ├── группа adapters - │ └── модуль адаптера - └── модуль react -``` - -Группы `presets` и `adapters` существуют только при наличии соответствующих модулей. Каталоги `errors`, `ports`, `services`, `types`, `hooks` и `providers` являются сегментами своих модулей-владельцев, если сами не образуют самостоятельный модуль. diff --git a/DRAFT/level-3/validation.md b/DRAFT/level-3/validation.md deleted file mode 100644 index 596e03a..0000000 --- a/DRAFT/level-3/validation.md +++ /dev/null @@ -1,49 +0,0 @@ -# Проверка Level 3 - -> Граница автоматической проверки, архитектурного ревью и тестирования Level 3. - -## Конфигурация проекта - -Конфигурация проверки сопоставляет физические пути с доменами, их модулями, группами, публичными точками входа и метками сред выполнения. Она также определяет, какие точки входа предназначены только для клиента, только для сервера или для обеих сред. - -Сопоставление путей не определяет предметный смысл домена. Оно позволяет проверить его форму, публичные API модулей, граф импортов, циклы и совместимость сред. - -## Автоматическая проверка - -Автоматическая проверка должна блокировать: - -- отсутствие модуля `business` или несколько таких модулей в одном домене; -- исполняемый код или общую точку входа в корне домена; -- глубокие импорты во внутренние сегменты модулей домена; -- достижимость кода фреймворка, конкретной среды, адаптеров или сборок из точки входа `business`; -- достижимость несовместимой среды из клиентской, серверной или общей точки входа; -- циклы между модулями по общему правилу Level 1. - -## Архитектурное ревью - -На ревью определяется: - -- представляет ли домен одну связную предметную область; -- принадлежат ли предметные сценарии, контракт ошибок и модель состояния модулю `business`; -- описывает ли порт минимальную предметную возможность без типов конкретной реализации; -- остаётся ли адаптер техническим преобразователем без предметных правил и преобразования ошибок в ошибки домена; -- представляет ли модуль группы `presets` повторяемую сборку для одного контекста выполнения; -- определены ли владелец ресурса, владелец графа, область жизни экземпляра, запуск и очистка; -- проходит ли связь между доменами во время выполнения через порт потребителя; -- принадлежит ли интерфейс React домену, а не отдельной странице или маршруту. - -## Тестирование - -Бизнес-логика проверяется через публичный API фабрики с управляемыми тестовыми реализациями портов. Тест адаптера проверяет техническую границу, тест сборки — выбор зависимостей, область жизни и совместимость среды, тест модуля React — провайдер, подписки и жизненный цикл фреймворка. Полный междоменный граф проверяется у его владельца. - -Тесты не заменяют автоматическую проверку импортов и архитектурное ревью. Они подтверждают поведение уже выбранной границы. - -## Связанные правила - -- [`SLM-L3-DOMAIN-A002`](../rules/level-3.md#slm-l3-domain-a002) -- [`SLM-L3-BUSINESS-R003`](../rules/level-3.md#slm-l3-business-r003) -- [`SLM-L3-BUSINESS-A004`](../rules/level-3.md#slm-l3-business-a004) -- [`SLM-L3-ENVIRONMENT-A012`](../rules/level-3.md#slm-l3-environment-a012) -- [`SLM-L3-TEST-R014`](../rules/level-3.md#slm-l3-test-r014) -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) -- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) diff --git a/DRAFT/rules/README.md b/DRAFT/rules/README.md index f046682..3b65a9e 100644 --- a/DRAFT/rules/README.md +++ b/DRAFT/rules/README.md @@ -53,6 +53,7 @@ SLM-L{level}-{group}-{class}{number} | `DOMAIN` | Домены | | `BUSINESS` | Контракты бизнес-логики | | `FACTORY` | Фабрики бизнес-логики | +| `ERROR` | Ошибки домена | | `PORT` | Порты бизнес-логики | | `ADAPTER` | Адаптеры | | `PRESET` | Типовые сборки | @@ -60,6 +61,7 @@ SLM-L{level}-{group}-{class}{number} | `ENVIRONMENT` | Границы сред выполнения | | `FRAMEWORK` | Модули фреймворков | | `TEST` | Тестирование | +| `MIGRATION` | Переход между архитектурными формами | Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования. @@ -126,4 +128,3 @@ SLM-L{level}-{group}-{class}{number} - [Первый уровень](./level-1.md) - [Второй уровень](./level-2.md) -- [Третий уровень](./level-3.md) diff --git a/DRAFT/rules/level-1.md b/DRAFT/rules/level-1.md index 26f43b7..34cc211 100644 --- a/DRAFT/rules/level-1.md +++ b/DRAFT/rules/level-1.md @@ -100,3 +100,11 @@ > **Жизненный цикл ресурсов** > > Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни. + +## Граница доменных модулей + +### SLM-L1-DOMAIN-R015 + +> **Доменный модуль** +> +> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие ей модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей. diff --git a/DRAFT/rules/level-2.md b/DRAFT/rules/level-2.md index 46e4877..b122ad4 100644 --- a/DRAFT/rules/level-2.md +++ b/DRAFT/rules/level-2.md @@ -1,11 +1,119 @@ # Правила SLM второго уровня -Проект Level 2 соблюдает все правила Level 1 и дополнительные правила этого реестра. +Проект Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. `SLM-L1-DOMAIN-R015` заменяется правилами доменного пакета. `SLM-L1-GROUP-R007` сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Ранее использовавшийся номер `SLM-L2-DOMAIN-R001` не переиспользуется после изменения модели уровней. -## Граница доменов +## Граница доменного пакета -### SLM-L2-DOMAIN-R001 +### SLM-L2-DOMAIN-R002 -> **Граница домена** +> **Предметная граница пакета** > -> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие этой области доменные модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей. +> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным пакетом; все его модули относятся только к этой предметной области. + +### SLM-L2-DOMAIN-A003 + +> **Корень доменного пакета** +> +> Корень доменного пакета содержит только декларативную metadata, модуль `business` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов. + +### SLM-L2-GROUP-R004 + +> **Навигационная Group доменов** +> +> Group, расположенная непосредственно в слое `domains` или другой такой Group, содержит только доменные пакеты и навигационные Groups и не владеет реализацией, состоянием, жизненным циклом или публичным API. + +## Business и DomainApi + +### SLM-L2-BUSINESS-R005 + +> **Модуль business** +> +> Каждый доменный пакет содержит ровно один модуль `business`, который владеет публичными предметными сценариями и контрактом `DomainApi` этой предметной области. + +### SLM-L2-BUSINESS-R006 + +> **Единственный runtime-источник домена** +> +> Приложение получает доменные данные, состояние и результаты предметных сценариев только через экземпляр `DomainApi`; adapters, presets и framework binding modules не предоставляют параллельный runtime-источник этих данных или результатов. + +### SLM-L2-BUSINESS-A007 + +> **Импортная замкнутость business** +> +> Все runtime- и type-only импорты `business`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как business-safe. + +### SLM-L2-FACTORY-R008 + +> **Единая фабрика DomainApi** +> +> Модуль `business` предоставляет ровно одну публичную фабрику, которая получает явные runtime-зависимости и создаёт `DomainApi` одного контракта независимо от preset и среды выполнения. + +## Ошибки домена + +### SLM-L2-ERROR-R009 + +> **Публичный контракт ошибок** +> +> Модуль `business` экспортирует устойчивые коды доменных ошибок, именованный readonly-тип безопасной публичной формы и runtime guard этой формы независимо от выбранного способа передачи ошибки. + +### SLM-L2-ERROR-R010 + +> **Изоляция исходных ошибок** +> +> Сбой adapter, SDK, транспорта, storage или другого домена, доступный приложению через `DomainApi`, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause. + +## Presets и зависимости + +### SLM-L2-PRESET-R011 + +> **Роль preset** +> +> Каждый preset является SLM-модулем одного именованного контекста выполнения, выбирает реализации явных зависимостей, вызывает единственную business-фабрику и не добавляет предметные сценарии, методы `DomainApi` или собственные доменные ошибки. + +### SLM-L2-DEPENDENCY-A012 + +> **Междоменные импорты** +> +> Модуль одного доменного пакета не импортирует runtime-экспорты другого пакета, включая функции, состояние, hooks, contexts, Providers и components; разрешён только type-only импорт публичного business-контракта, который остаётся ребром общего ацикличного графа. + +### SLM-L2-ENVIRONMENT-A013 + +> **Совместимость среды выполнения** +> +> Публичная точка входа, обозначенная как client-, server- или shared-entry point, не импортирует и не реэкспортирует код несовместимой среды выполнения во всём достижимом графе импортов. + +## Framework Groups и тестирование + +### SLM-L2-FRAMEWORK-R014 + +> **Framework Group домена** +> +> Framework binding modules доменного пакета размещаются в Group, названной по фреймворку, и каждый прямой дочерний элемент этой Group является framework binding module. + +### SLM-L2-FRAMEWORK-R015 + +> **Framework binding module** +> +> Модуль внутри Framework Group владеет одной domain-specific framework-ответственностью, получает готовый `DomainApi` и не выполняет business-сборку, не выбирает adapters и не владеет страницей, маршрутом или multi-domain композицией. + +### SLM-L2-TEST-R016 + +> **Проверка владельцев Level 2** +> +> Каждый публичный предметный сценарий проверяется через business-фабрику, а основные тесты adapter, preset и framework binding module проверяют только собственные публичные границы и не повторяют полный набор предметных сценариев. + +## Миграция + +### SLM-L2-MIGRATION-A017 + +> **Изоляция форм во время миграции** +> +> Пока SLM root содержит одновременно доменные модули Level 1 и доменные пакеты Level 2, модули этих двух форм не создают между собой runtime- или type-only импортов. + +## Внешние библиотеки business + +### SLM-L2-BUSINESS-R018 + +> **Business-safe внешний пакет** +> +> Внешний пакет объявляется business-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state manager, framework или другой технической интеграцией. diff --git a/DRAFT/rules/level-3.md b/DRAFT/rules/level-3.md deleted file mode 100644 index 42c80e3..0000000 --- a/DRAFT/rules/level-3.md +++ /dev/null @@ -1,97 +0,0 @@ -# Правила SLM третьего уровня - -Проект Level 3 соблюдает правила Level 1 и Level 2, кроме заменённого `SLM-L2-DOMAIN-R001` и расширенного состава групп внутри слоя `domains`, а также дополнительные правила этого реестра. - -## Граница домена - -### SLM-L3-DOMAIN-R001 - -> **Предметная граница домена** -> -> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменом; его модули относятся только к этой области. - -### SLM-L3-DOMAIN-A002 - -> **Корень домена** -> -> Корень домена не содержит файлов реализации, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов и содержит только допустимые модули домена и группы. - -## Бизнес-логика и фабрика - -### SLM-L3-BUSINESS-R003 - -> **Контракт бизнес-логики** -> -> Каждый домен содержит ровно один модуль `business`, который определяет публичные предметные сценарии и контракты, ошибки предметной области и модель её состояния. - -### SLM-L3-BUSINESS-A004 - -> **Независимость бизнес-логики от среды** -> -> Граф импортов, достижимый из публичной точки входа `business`, не достигает кода фреймворков, зависимых от среды точек входа, API платформы, технических реализаций, адаптеров, сборок или модулей фреймворков. - -### SLM-L3-FACTORY-R005 - -> **Контракт фабрики** -> -> Фабрика бизнес-логики получает полный набор собственных портов и создаёт API одного и того же контракта независимо от среды выполнения; различия сред не выражаются режимом, необязательным портом или методом, намеренно недоступным в части сред. - -### SLM-L3-FACTORY-R006 - -> **Создание экземпляра API** -> -> Вызов фабрики не выполняет ввод-вывод, не читает скрытое окружение, не запускает ресурс жизненного цикла, не выбирает конкретный адаптер и не выполняет операции жизненного цикла фреймворка. - -### SLM-L3-PORT-R007 - -> **Порт бизнес-логики** -> -> Каждая возможность среды, необходимая бизнес-логике во время выполнения, описывается минимальным портом этой бизнес-логики; публичный контракт порта не раскрывает конкретную реализацию, фреймворк или типы среды. - -## Сборка - -### SLM-L3-ADAPTER-R008 - -> **Ответственность адаптера** -> -> Адаптер реализует порт бизнес-логики поверх конкретной технической системы и не определяет предметный инвариант, резервное поведение или ошибку предметной области. - -### SLM-L3-PRESET-R009 - -> **Роль типовой сборки** -> -> Модуль группы `presets` собирает API бизнес-логики для одного именованного контекста выполнения, выбирая реализации портов, и не изменяет контракт или предметные сценарии. - -### SLM-L3-ASSEMBLY-R010 - -> **Жизненный цикл собранного API** -> -> Владелец графа удерживает экземпляр API только в области жизни, объявленной модулем-владельцем, и вызывает предоставленные операции жизненного цикла; модуль-владелец определяет создание, область жизни, число экземпляров и очистку ресурса по правилам Level 1. - -## Междоменные зависимости и среды выполнения - -### SLM-L3-DEPENDENCY-R011 - -> **Междоменная связь во время выполнения** -> -> Модуль `business` одного домена не создаёт и не импортирует исполняемый API другого домена; необходимая возможность описывается собственным портом и передаётся владельцем графа при сборке. - -### SLM-L3-ENVIRONMENT-A012 - -> **Совместимость графа импортов** -> -> Публичная точка входа, обозначенная как клиентская, серверная или общая для обеих сред, не импортирует и не реэкспортирует код несовместимой среды выполнения. - -## Фреймворки и тестирование - -### SLM-L3-FRAMEWORK-R013 - -> **Модуль фреймворка домена** -> -> Зависящий от конкретного фреймворка код находится в модуле домена, названном именем фреймворка, получает готовый API бизнес-логики и не реализует предметные решения или технические адаптеры. - -### SLM-L3-TEST-R014 - -> **Проверка контракта бизнес-логики** -> -> Каждый публичный предметный сценарий проверяется через фабрику с управляемыми тестовыми реализациями портов; основные тесты адаптера, сборки и модуля фреймворка проверяют собственные границы и не повторяют набор предметных сценариев. diff --git a/README.md b/README.md index b295dcb..833e360 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Структура -- `DRAFT/` - рабочая документация Levels 1-3 и источник содержимого сайта. +- `DRAFT/` - рабочая документация Levels 1-2 и источник содержимого сайта. - `site/` - VitePress-конфигурация, тема и статические ресурсы. - `docs/` и `docs-v3/` - архивные версии документации, не используемые сайтом. - `old-docs/` - действующая legacy-документация для текущего skill. diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs index e8f9c89..7d5f668 100644 --- a/scripts/check-site.mjs +++ b/scripts/check-site.mjs @@ -9,7 +9,6 @@ const siteBase = '/slm-design/' const ruleRegistries = [ { source: path.join(repositoryRoot, 'DRAFT', 'rules', 'level-1.md'), route: 'rules/level-1' }, { source: path.join(repositoryRoot, 'DRAFT', 'rules', 'level-2.md'), route: 'rules/level-2' }, - { source: path.join(repositoryRoot, 'DRAFT', 'rules', 'level-3.md'), route: 'rules/level-3' }, ] const expectedPages = [ @@ -18,6 +17,7 @@ const expectedPages = [ 'level-1/index.html', 'level-1/terminology.html', 'level-1/layers.html', + 'level-1/domains.html', 'level-1/dependencies.html', 'level-1/modules.html', 'level-1/groups.html', @@ -28,27 +28,20 @@ const expectedPages = [ 'level-1/validation.html', 'level-2/index.html', 'level-2/terminology.html', - 'level-2/layers.html', - 'level-2/domains.html', 'level-2/dependencies.html', 'level-2/validation.html', - 'level-3/index.html', - 'level-3/terminology.html', - 'level-3/dependencies.html', - 'level-3/validation.html', - 'level-3/domains/index.html', - 'level-3/domains/domain.html', - 'level-3/domains/business.html', - 'level-3/domains/factory-ports-adapters.html', - 'level-3/domains/presets.html', - 'level-3/domains/framework-bindings.html', - 'level-3/domains/testing.html', - 'level-3/domains/auth-example.html', - 'level-3/domains/open-questions.html', + 'level-2/domains/index.html', + 'level-2/domains/domain-package.html', + 'level-2/domains/business.html', + 'level-2/domains/factory-ports-adapters.html', + 'level-2/domains/presets.html', + 'level-2/domains/framework-bindings.html', + 'level-2/domains/testing.html', + 'level-2/domains/auth-example.html', + 'level-2/domains/open-questions.html', 'rules/index.html', 'rules/level-1.html', 'rules/level-2.html', - 'rules/level-3.html', ].sort() async function collectHtmlFiles(directory, prefix = '') { diff --git a/site/.vitepress/config.mts b/site/.vitepress/config.mts index 5628b18..ba8cbd0 100644 --- a/site/.vitepress/config.mts +++ b/site/.vitepress/config.mts @@ -26,6 +26,7 @@ const documentationSidebar = [ { text: 'Обзор', link: '/level-1/' }, { text: 'Терминология', link: '/level-1/terminology' }, { text: 'Слои', link: '/level-1/layers' }, + { text: 'Доменные модули', link: '/level-1/domains' }, { text: 'Зависимости', link: '/level-1/dependencies' }, { text: 'Модули', link: '/level-1/modules' }, { text: 'Группы', link: '/level-1/groups' }, @@ -41,27 +42,17 @@ const documentationSidebar = [ items: [ { text: 'Обзор', link: '/level-2/' }, { text: 'Терминология', link: '/level-2/terminology' }, - { text: 'Слои', link: '/level-2/layers' }, - { text: 'Домены', link: '/level-2/domains' }, + { text: 'Доменные пакеты', link: '/level-2/domains/' }, + { text: 'Граница пакета', link: '/level-2/domains/domain-package' }, + { text: 'Business module', link: '/level-2/domains/business' }, + { text: 'Factory и adapters', link: '/level-2/domains/factory-ports-adapters' }, + { text: 'Presets и среды', link: '/level-2/domains/presets' }, + { text: 'Framework Groups', link: '/level-2/domains/framework-bindings' }, { text: 'Зависимости', link: '/level-2/dependencies' }, + { text: 'Тестирование', link: '/level-2/domains/testing' }, { text: 'Проверка', link: '/level-2/validation' }, - ], - }, - { - text: 'SLM Level 3', - items: [ - { text: 'Обзор', link: '/level-3/' }, - { text: 'Терминология', link: '/level-3/terminology' }, - { text: 'Domain', link: '/level-3/domains/' }, - { text: 'Business module', link: '/level-3/domains/business' }, - { text: 'Factory, ports и adapters', link: '/level-3/domains/factory-ports-adapters' }, - { text: 'Presets и SSR', link: '/level-3/domains/presets' }, - { text: 'React module', link: '/level-3/domains/framework-bindings' }, - { text: 'Зависимости', link: '/level-3/dependencies' }, - { text: 'Тестирование', link: '/level-3/domains/testing' }, - { text: 'Проверка', link: '/level-3/validation' }, - { text: 'Auth как пример миграции', link: '/level-3/domains/auth-example' }, - { text: 'Открытые вопросы', link: '/level-3/domains/open-questions' }, + { text: 'Миграция auth', link: '/level-2/domains/auth-example' }, + { text: 'Открытые вопросы', link: '/level-2/domains/open-questions' }, ], }, { @@ -70,7 +61,6 @@ const documentationSidebar = [ { text: 'Как устроены правила', link: '/rules/' }, { text: 'Реестр Level 1', link: '/rules/level-1' }, { text: 'Реестр Level 2', link: '/rules/level-2' }, - { text: 'Реестр Level 3', link: '/rules/level-3' }, ], }, ] @@ -81,8 +71,7 @@ export default defineConfig({ rewrites: { 'level-1/README.md': 'level-1/index.md', 'level-2/README.md': 'level-2/index.md', - 'level-3/README.md': 'level-3/index.md', - 'level-3/domains/README.md': 'level-3/domains/index.md', + 'level-2/domains/README.md': 'level-2/domains/index.md', 'rules/README.md': 'rules/index.md', }, title: 'SLM Design', @@ -112,7 +101,6 @@ export default defineConfig({ nav: [ { text: 'Level 1', link: '/level-1/' }, { text: 'Level 2', link: '/level-2/' }, - { text: 'Level 3', link: '/level-3/' }, { text: 'Правила', link: '/rules/' }, ], sidebar: documentationSidebar, @@ -168,7 +156,7 @@ export default defineConfig({ returnToTopLabel: 'Наверх', skipToContentLabel: 'Перейти к содержанию', footer: { - message: 'SLM Levels 1-3', + message: 'SLM Levels 1-2', copyright: 'Рабочий черновик архитектуры.', }, }, diff --git a/site/README.md b/site/README.md index 8848193..d1d8a7a 100644 --- a/site/README.md +++ b/site/README.md @@ -2,18 +2,16 @@ `site/` содержит конфигурацию, тему и статические ресурсы VitePress. -Источником опубликованной документации служит [`DRAFT`](../DRAFT/index.md). Сайт включает рабочие черновики Levels 1-3 и их правила. +Источником опубликованной документации служит [`DRAFT`](../DRAFT/index.md). Сайт включает рабочие черновики Levels 1-2 и их правила. ## Маршруты - `/` - главная страница; - `/level-1/` - документация Level 1; - `/level-2/` - документация Level 2; -- `/level-3/` - документация Level 3; - `/rules/` - устройство правил; - `/rules/level-1` - канонический реестр Level 1. - `/rules/level-2` - канонический реестр Level 2. -- `/rules/level-3` - канонический реестр Level 3. ## Локальный запуск