mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
feat: переработать уровни SLM
This commit is contained in:
@@ -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) - канонические наборы, формат и правила формулировки.
|
||||
|
||||
## Соглашение
|
||||
|
||||
@@ -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/).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
61
DRAFT/level-1/domains.md
Normal 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` внутри доменного пакета.
|
||||
@@ -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/).
|
||||
|
||||
@@ -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
|
||||
│ ├── группа
|
||||
│ │ └── модуль
|
||||
│ └── модуль
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода.
|
||||
|
||||
@@ -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`, даже если сегодня используется одной композицией.
|
||||
26
DRAFT/level-2/domains/README.md
Normal file
26
DRAFT/level-2/domains/README.md
Normal 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).
|
||||
101
DRAFT/level-2/domains/auth-example.md
Normal file
101
DRAFT/level-2/domains/auth-example.md
Normal 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 зависимости с уже переведёнными пакетами.
|
||||
122
DRAFT/level-2/domains/business.md
Normal file
122
DRAFT/level-2/domains/business.md
Normal 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.
|
||||
87
DRAFT/level-2/domains/domain-package.md
Normal file
87
DRAFT/level-2/domains/domain-package.md
Normal 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 сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.
|
||||
86
DRAFT/level-2/domains/factory-ports-adapters.md
Normal file
86
DRAFT/level-2/domains/factory-ports-adapters.md
Normal 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 и не становится альтернативным источником доменных данных для приложения.
|
||||
115
DRAFT/level-2/domains/framework-bindings.md
Normal file
115
DRAFT/level-2/domains/framework-bindings.md
Normal 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` в скрытый корневой модуль домена.
|
||||
43
DRAFT/level-2/domains/open-questions.md
Normal file
43
DRAFT/level-2/domains/open-questions.md
Normal 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 и запрещённых транзитивных импортов.
|
||||
101
DRAFT/level-2/domains/presets.md
Normal file
101
DRAFT/level-2/domains/presets.md
Normal 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.
|
||||
56
DRAFT/level-2/domains/testing.md
Normal file
56
DRAFT/level-2/domains/testing.md
Normal 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-тест не заменяет эти проверки.
|
||||
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Путь помогает определить структурную границу, но не доказывает корректность предметной декомпозиции. Решение о том, является ли ответственность самостоятельным доменом, требует понимания продукта.
|
||||
|
||||
@@ -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` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.
|
||||
|
||||
@@ -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). Тематические документы объясняют правила, но не объявляют их повторно.
|
||||
@@ -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/` сам по себе ничего не доказывает: проверяется весь граф импортов, достижимый из точки входа.
|
||||
@@ -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)
|
||||
@@ -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 до удаления старого пути.
|
||||
@@ -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` конкретной библиотеки.
|
||||
@@ -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`, только когда работает с контрактом домена и не определяет страницу, маршрут или продуктовую композицию.
|
||||
@@ -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`.
|
||||
@@ -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 сама по себе не доказывает принадлежность домену.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Открытые вопросы Level 3
|
||||
|
||||
> Эти вопросы не являются правилами и не отменяют уже принятые границы.
|
||||
|
||||
## Зафиксированные решения
|
||||
|
||||
- Домен является сущностью только Level 3; в Level 2 предметная область остаётся одним доменным модулем.
|
||||
- Корень домена не имеет общей точки входа для исполняемого кода.
|
||||
- Модуль фреймворка называется его именем и размещается непосредственно в домене: `domains/auth/react`.
|
||||
- Модуль фреймворка получает готовый API и не выполняет сборку.
|
||||
- Взаимодействие бизнес-логики разных доменов во время выполнения проходит через порт потребителя и владельца графа.
|
||||
- Навигационные группы допустимы в `domains`, но не являются частью базовых примеров.
|
||||
|
||||
## Форма передачи ошибок
|
||||
|
||||
Level 3 требует устойчивый контракт ошибок домена, но не навязывает единый способ передачи: исключение с проверкой типа во время выполнения или размеченный `Result`. Нужно проверить, нужна ли общая политика для всех доменов одного приложения и как она влияет на серверные действия и сериализацию RPC.
|
||||
|
||||
## Наблюдение за состоянием
|
||||
|
||||
На реальном примере SSR и гидратации нужно проверить точную форму независимого от фреймворка интерфейса наблюдения: начальный снимок, параллельный рендеринг, сброс данных, очистку подписки и поведение после завершения запроса. Методы `getSnapshot` и `subscribe` пока служат иллюстрацией, а не обязательной файловой формой.
|
||||
|
||||
## Автоматическая проверка архитектуры
|
||||
|
||||
Нужно выбрать формат конфигурации проекта для автоматической проверки корней доменов, их модулей, публичных точек входа, меток сред и запрещённых транзитивных импортов. Проверка должна опираться на граф и описание структуры, а не только на имена папок.
|
||||
@@ -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 не смешивается с технической сборкой зависимостей.
|
||||
@@ -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 с поведением жизненного цикла — теста фреймворка.
|
||||
@@ -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` являются сегментами своих модулей-владельцев, если сами не образуют самостоятельный модуль.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -100,3 +100,11 @@
|
||||
> **Жизненный цикл ресурсов**
|
||||
>
|
||||
> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
|
||||
|
||||
## Граница доменных модулей
|
||||
|
||||
### SLM-L1-DOMAIN-R015
|
||||
|
||||
> **Доменный модуль**
|
||||
>
|
||||
> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие ей модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей.
|
||||
|
||||
@@ -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 или другой технической интеграцией.
|
||||
|
||||
@@ -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
|
||||
|
||||
> **Проверка контракта бизнес-логики**
|
||||
>
|
||||
> Каждый публичный предметный сценарий проверяется через фабрику с управляемыми тестовыми реализациями портов; основные тесты адаптера, сборки и модуля фреймворка проверяют собственные границы и не повторяют набор предметных сценариев.
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Структура
|
||||
|
||||
- `DRAFT/` - рабочая документация Levels 1-3 и источник содержимого сайта.
|
||||
- `DRAFT/` - рабочая документация Levels 1-2 и источник содержимого сайта.
|
||||
- `site/` - VitePress-конфигурация, тема и статические ресурсы.
|
||||
- `docs/` и `docs-v3/` - архивные версии документации, не используемые сайтом.
|
||||
- `old-docs/` - действующая legacy-документация для текущего skill.
|
||||
|
||||
@@ -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 = '') {
|
||||
|
||||
@@ -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: 'Рабочий черновик архитектуры.',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Локальный запуск
|
||||
|
||||
|
||||
Reference in New Issue
Block a user