feat: переработать уровни SLM

This commit is contained in:
2026-07-30 18:45:54 +03:00
parent c0956ed2a0
commit d3b37eb4bd
44 changed files with 1239 additions and 1449 deletions

View File

@@ -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) - канонические наборы, формат и правила формулировки.
## Соглашение

View File

@@ -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/).

View File

@@ -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)

View File

@@ -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).
## Связанные правила

61
DRAFT/level-1/domains.md Normal file
View File

@@ -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` внутри доменного пакета.

View File

@@ -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/).

View File

@@ -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
│ ├── группа
│ │ └── модуль
│ └── модуль

View File

@@ -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`.

View File

@@ -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)

View File

@@ -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<AuthApi, 'getSession'>
}
```
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 не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода.

View File

@@ -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`, даже если сегодня используется одной композицией.

View File

@@ -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).

View File

@@ -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<AuthApi, 'getSession'>
}
```
Место сборки графа создаёт экземпляры:
```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 зависимости с уже переведёнными пакетами.

View File

@@ -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<void>
verifyPhoneOtp: (code: string) => Promise<void>
}
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<string>(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.

View File

@@ -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 сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.

View File

@@ -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<unknown>
verifyCode: (code: string) => Promise<unknown>
}
```
`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<AuthApi, 'getSession'>
}
```
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 и не становится альтернативным источником доменных данных для приложения.

View File

@@ -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 (
<AuthSessionContext.Provider value={api}>
{children}
</AuthSessionContext.Provider>
)
}
```
Публичный путь модуля:
```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 (
<UserProfile
userId={session.userId}
canEdit={session.isAuthenticated}
/>
)
```
Передача через 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` в скрытый корневой модуль домена.

View File

@@ -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<OtherDomainApi>` уже зафиксирован как отдельный вид 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 и запрещённых транзитивных импортов.

View File

@@ -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<AuthApi, 'getSession'>
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.

View File

@@ -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-тест не заменяет эти проверки.

View File

@@ -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)

View File

@@ -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
```
Путь помогает определить структурную границу, но не доказывает корректность предметной декомпозиции. Решение о том, является ли ответственность самостоятельным доменом, требует понимания продукта.

View File

@@ -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` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.

View File

@@ -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). Тематические документы объясняют правила, но не объявляют их повторно.

View File

@@ -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/` сам по себе ничего не доказывает: проверяется весь граф импортов, достижимый из точки входа.

View File

@@ -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)

View File

@@ -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 до удаления старого пути.

View File

@@ -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` конкретной библиотеки.

View File

@@ -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`, только когда работает с контрактом домена и не определяет страницу, маршрут или продуктовую композицию.

View File

@@ -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<unknown>
verifyCode: (data: VerifyPhoneOtpData) => Promise<unknown>
}
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`.

View File

@@ -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 <AuthContext.Provider value={api}>{children}</AuthContext.Provider>
}
```
## Наблюдение за состоянием
Если `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 сама по себе не доказывает принадлежность домену.

View File

@@ -1,24 +0,0 @@
# Открытые вопросы Level 3
> Эти вопросы не являются правилами и не отменяют уже принятые границы.
## Зафиксированные решения
- Домен является сущностью только Level 3; в Level 2 предметная область остаётся одним доменным модулем.
- Корень домена не имеет общей точки входа для исполняемого кода.
- Модуль фреймворка называется его именем и размещается непосредственно в домене: `domains/auth/react`.
- Модуль фреймворка получает готовый API и не выполняет сборку.
- Взаимодействие бизнес-логики разных доменов во время выполнения проходит через порт потребителя и владельца графа.
- Навигационные группы допустимы в `domains`, но не являются частью базовых примеров.
## Форма передачи ошибок
Level 3 требует устойчивый контракт ошибок домена, но не навязывает единый способ передачи: исключение с проверкой типа во время выполнения или размеченный `Result`. Нужно проверить, нужна ли общая политика для всех доменов одного приложения и как она влияет на серверные действия и сериализацию RPC.
## Наблюдение за состоянием
На реальном примере SSR и гидратации нужно проверить точную форму независимого от фреймворка интерфейса наблюдения: начальный снимок, параллельный рендеринг, сброс данных, очистку подписки и поведение после завершения запроса. Методы `getSnapshot` и `subscribe` пока служат иллюстрацией, а не обязательной файловой формой.
## Автоматическая проверка архитектуры
Нужно выбрать формат конфигурации проекта для автоматической проверки корней доменов, их модулей, публичных точек входа, меток сред и запрещённых транзитивных импортов. Проверка должна опираться на граф и описание структуры, а не только на имена папок.

View File

@@ -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<void>
}
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 не смешивается с технической сборкой зависимостей.

View File

@@ -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 с поведением жизненного цикла — теста фреймворка.

View File

@@ -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` являются сегментами своих модулей-владельцев, если сами не образуют самостоятельный модуль.

View File

@@ -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)

View File

@@ -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)

View File

@@ -100,3 +100,11 @@
> **Жизненный цикл ресурсов**
>
> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
## Граница доменных модулей
### SLM-L1-DOMAIN-R015
> **Доменный модуль**
>
> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие ей модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей.

View File

@@ -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 или другой технической интеграцией.

View File

@@ -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
> **Проверка контракта бизнес-логики**
>
> Каждый публичный предметный сценарий проверяется через фабрику с управляемыми тестовыми реализациями портов; основные тесты адаптера, сборки и модуля фреймворка проверяют собственные границы и не повторяют набор предметных сценариев.

View File

@@ -4,7 +4,7 @@
## Структура
- `DRAFT/` - рабочая документация Levels 1-3 и источник содержимого сайта.
- `DRAFT/` - рабочая документация Levels 1-2 и источник содержимого сайта.
- `site/` - VitePress-конфигурация, тема и статические ресурсы.
- `docs/` и `docs-v3/` - архивные версии документации, не используемые сайтом.
- `old-docs/` - действующая legacy-документация для текущего skill.

View File

@@ -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 = '') {

View File

@@ -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: 'Рабочий черновик архитектуры.',
},
},

View File

@@ -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.
## Локальный запуск