This commit is contained in:
S. Gromov
2026-08-10 09:12:22 +03:00
parent b5db9e5158
commit 691069af8e
55 changed files with 1519 additions and 3383 deletions

View File

@@ -5,9 +5,9 @@ on:
branches: [master]
paths:
- '.github/workflows/docs.yml'
- 'DRAFT/**'
- 'docs/**'
- 'site/**'
- 'draft-rules.js'
- 'scripts/check-docs.mjs'
- 'scripts/check-site.mjs'
- 'scripts/lib/slugify-heading.mjs'
- 'package.json'
@@ -15,9 +15,9 @@ on:
pull_request:
paths:
- '.github/workflows/docs.yml'
- 'DRAFT/**'
- 'docs/**'
- 'site/**'
- 'draft-rules.js'
- 'scripts/check-docs.mjs'
- 'scripts/check-site.mjs'
- 'scripts/lib/slugify-heading.mjs'
- 'package.json'

View File

@@ -6,6 +6,7 @@ on:
paths:
- '.github/workflows/skill.yml'
- 'DRAFT/**'
- 'docs/**'
- 'src-skills/**'
- 'skills/**'
- 'scripts/build-skill.mjs'
@@ -14,13 +15,14 @@ on:
- 'scripts/lib/slugify-heading.mjs'
- 'tests/skill-bundle.test.mjs'
- 'site/.vitepress/config.mts'
- 'draft-rules.js'
- 'scripts/check-docs.mjs'
- 'package.json'
- 'package-lock.json'
pull_request:
paths:
- '.github/workflows/skill.yml'
- 'DRAFT/**'
- 'docs/**'
- 'src-skills/**'
- 'skills/**'
- 'scripts/build-skill.mjs'
@@ -29,7 +31,7 @@ on:
- 'scripts/lib/slugify-heading.mjs'
- 'tests/skill-bundle.test.mjs'
- 'site/.vitepress/config.mts'
- 'draft-rules.js'
- 'scripts/check-docs.mjs'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
@@ -54,7 +56,7 @@ jobs:
run: npm ci
- name: Check rules and skill
run: npm run test:skill && npm run check:draft-rules && npm run check:skill
run: npm run test:skill && npm run check:docs && npm run check:skill
- name: Rebuild skill deterministically
run: mv skills/slm-design "$RUNNER_TEMP/slm-design-expected" && npm run build:skill && diff -qr "$RUNNER_TEMP/slm-design-expected" skills/slm-design && git diff --exit-code -- skills/slm-design

View File

@@ -4,12 +4,11 @@
## Материалы
- [Первый уровень](./level-1/README.md) - слои, доменные модули, публичные API и зависимости.
- [Второй уровень](./level-2/README.md) - доменные пакеты, именованные Domain API, assemblies и Framework Groups.
- [Архитектура](./architecture/README.md) - слои, модули, публичные API, фасеты и зависимости.
- [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки.
## Соглашение
Черновики могут содержать определения, правила, рекомендации, примеры и открытые вопросы.
Нормативные определения задаются терминологией соответствующего уровня. Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки.
Нормативные определения задаются [терминологией](./architecture/terminology.md). Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки.

View File

@@ -0,0 +1,42 @@
# Архитектура SLM
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
SLM задаёт структурную основу приложения: слои, модули, публичные границы, зависимости, фасеты сред выполнения и владение жизненным циклом.
## Область SLM
Черновик описывает слои, модули, группы, сегменты, компоненты, публичный API, зависимости и владение жизненным циклом ресурсов.
SLM не задаёт обязательную внутреннюю форму модуля, обязательный поток данных, правила монорепозиториев или полный файловый стайлгайд. Форма модуля следует его ответственности и реальным потребителям.
## Виды утверждений
- **Определение** нормативно задаёт смысл архитектурного термина, но не получает код правила.
- **Правило** задаёт блокирующий архитектурный инвариант и объявляется в каноническом реестре.
- **Рекомендация** помогает принять решение, но не делает архитектуру невалидной.
- **Пример** иллюстрирует модель и не задаёт обязательную структуру.
Нормативные определения находятся в [терминологии](./terminology.md). Канонический набор требований находится в [реестре правил SLM](../rules/registry.md). Формат и требования к правилам описаны отдельно в разделе [Правила SLM](../rules/).
Остальные документы объясняют и иллюстрируют модель, но не владеют точными формулировками определений и правил.
## Основная идея
Модуль является основной архитектурной единицей SLM. Слой задаёт его роль и направление зависимостей: модули слоя `domains` владеют предметными ответственностями на тех же основаниях, что и остальные модули. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю.
SLM требует отдельную папку и единый логический публичный API модуля. По умолчанию API представлен корневым `index`; при необходимости модуль добавляет environment-фасеты `client`, `browser` и `server`. Внутренняя файловая форма модулей, сегментов и компонентов определяется стайлгайдом проекта.
## Карта черновика
- [Терминология](./terminology.md)
- [Слои](./layers.md)
- [Модули слоя domains](./domains.md)
- [Зависимости](./dependencies.md)
- [Модули](./modules.md)
- [Группы](./groups.md)
- [Сегменты](./segments.md)
- [Компоненты](./components.md)
- [Вложенные модули](./nested-modules.md)
- [Жизненный цикл](./lifecycle.md)
- [Проверка](./validation.md)

View File

@@ -1,17 +1,17 @@
# Компоненты Level 1
# Компоненты SLM
> Пояснение нормативной модели компонентов Level 1.
> Пояснение нормативной модели компонентов SLM.
Компонент является строительным элементом интерфейса, а не самостоятельной архитектурной единицей.
## Связанные правила
- [`SLM-L1-COMPONENT-R009`](../rules/level-1.md#slm-l1-component-r009)
- [`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-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013)
- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009)
- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
- [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013)
## Файловая форма
@@ -40,7 +40,7 @@ landing/
Компонент может отображать входные данные, вызывать переданные обработчики, условно строить интерфейс и хранить локальное состояние представления.
Level 1 не вводит отдельного запрета на импорты, выполняемые кодом компонента. Каждый такой импорт считается зависимостью родительского модуля и должен соблюдать направление слоёв, публичные API и запрет циклов.
SLM не вводит отдельного запрета на импорты, выполняемые кодом компонента. Каждый такой импорт считается зависимостью родительского модуля и должен соблюдать направление слоёв, публичные фасеты и запрет циклов.
Доступ к данным, состояние, контекст или код жизненного цикла внутри компонента сами по себе не создают новую архитектурную границу. Их источники, зависимости и область жизни определяет родительский модуль.

View File

@@ -0,0 +1,87 @@
# Зависимости SLM
> Пояснение нормативной модели зависимостей SLM.
Матрица слоёв задаёт допустимые зависимости между модулями.
## Что считается зависимостью
- Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
- Импорт между файлами одного модуля не пересекает модульную границу.
- Импорт любого внутреннего файла, сегмента или компонента считается зависимостью ближайшего модуля-владельца.
- Вложенный модуль имеет собственную границу зависимостей.
- Группы, сегменты и компоненты не имеют самостоятельных границ зависимостей.
Точка входа фреймворка не является модулем. Её импорты участвуют в проверке направления слоёв.
Ресурс `shared` также не является модулем. Его прямой импорт участвует в проверке направления слоёв, но не нарушает требование о публичном API модуля.
## Допустимые связи
- Модуль может импортировать модули своего слоя и слоёв, разрешённых нормативной матрицей.
- Модули одного слоя могут импортировать друг друга.
- Промежуточный слой не является обязательным посредником.
- `infra` может импортировать `ui` для визуального представления технической возможности; `ui` не импортирует `infra`.
Модуль слоя `domains` может импортировать публичный API другого разрешённого модуля. Runtime- и type-only импорты одинаково создают архитектурную зависимость, а циклические зависимости запрещены.
```ts
// domains/orders
import type { Product } from '@/domains/catalog'
```
SLM не требует отдельного механизма инъекции между модулями слоя `domains`.
Матрица слоёв определена в [Слоях](./layers.md).
## Связанные правила
- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)
- [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016)
- [`SLM-ENVIRONMENT-R017`](../rules/registry.md#slm-environment-r017)
- [`SLM-ENVIRONMENT-R018`](../rules/registry.md#slm-environment-r018)
- [`SLM-ENVIRONMENT-R019`](../rules/registry.md#slm-environment-r019)
## Публичный API
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимо
import { Button } from '@/ui/button/button'
```
Если модуль объявляет специализированный фасет, его путь также является публичным:
```ts
import type { Session } from '@/domains/auth'
import { AuthProvider } from '@/domains/auth/client'
import { getServerSession } from '@/domains/auth/server'
```
## Фасеты сред выполнения
Ограничения фасета распространяются на все его runtime-импорты и реэкспорты, включая транзитивные. Type-only import остаётся архитектурной зависимостью, но не добавляет исполняемый код в среду фасета.
`index` не импортирует и не реэкспортирует `client`, `browser` или `server`. `client` может использовать универсальную внутреннюю реализацию модуля, но не импортирует `browser` или `server`. `browser` и `server` могут использовать универсальную внутреннюю реализацию, но не импортируют друг друга.
```ts
// Browser-only фасет подключается только за границей без SSR.
const BrowserEditor = dynamic(
() => import('@/domains/editor/browser').then(({ BrowserEditor }) => BrowserEditor),
{ ssr: false },
)
```
Обычный статический импорт `@/domains/editor/browser` запрещён даже внутри Client Component. Tree shaking, проверка `typeof window` и обещание не вызывать экспорт при SSR не заменяют динамическую границу с отключённым SSR.
## Циклы
```text
ui/modal → ui/button → ui/icon
ui/icon -/→ ui/modal
```

View File

@@ -0,0 +1,59 @@
# Модули слоя domains
> Пояснение размещения предметных ответственностей в обычных SLM-модулях.
## Связанные правила
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007)
- [`SLM-LAYER-R001`](../rules/registry.md#slm-layer-r001)
## Обычные модули SLM
Модуль слоя `domains` является обычным SLM-модулем. Он отличается от модулей других слоёв только предметной ответственностью, а не отдельной архитектурной сущностью или обязательной файловой формой.
```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/ # SLM-модуль
│ └── orders/ # SLM-модуль
└── cabinet/ # Group
└── profile/ # SLM-модуль
```
Group не имеет `index.ts`, реализации, состояния или публичного API. Она не создаёт dependency boundary и не реэкспортирует содержащиеся в ней модули.
## Среды выполнения
Если части публичного API модуля предназначены для разных сред выполнения, модуль сохраняет одну ответственность и разделяет доступ фасетами `client`, `browser` и `server`.

View File

@@ -1,13 +1,13 @@
# Группы Level 1
# Группы SLM
> Пояснение нормативной модели групп Level 1.
> Пояснение нормативной модели групп SLM.
Группа помогает ориентироваться в большом количестве модулей. Она классифицирует структуру, но ничего не реализует и не образует узел графа зависимостей.
Группа помогает ориентироваться в большом количестве модулей. Она классифицирует структуру, но ничего не реализует и не образует самостоятельную границу зависимостей.
## Связанное правило
- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
## Пример
@@ -20,6 +20,6 @@ compositions/
└── main/ # Модуль
```
`pages` и `layouts` являются возможной группировкой проекта, а не обязательной структурой Level 1.
`pages` и `layouts` являются возможной группировкой проекта, а не обязательной структурой SLM.
Рекомендуется создавать группу только при реальной навигационной потребности. Если папка начинает владеть файлами реализации, состоянием, жизненным циклом или публичным API, она является модулем и должна получить модульную границу.

View File

@@ -1,6 +1,6 @@
# Слои Level 1
# Слои SLM
> Пояснение нормативной модели слоёв Level 1.
> Пояснение нормативной модели слоёв SLM.
## Базовая структура
@@ -32,13 +32,13 @@ src/
### Domains
`domains` содержит предметные области приложения: их модели, правила, сценарии и продуктовое состояние. Каждая самостоятельная предметная область представлена [доменным модулем](./domains.md).
`domains` содержит обычные SLM-модули, владеющие предметными ответственностями приложения: моделями, правилами, сценариями и продуктовым состоянием.
Доменная логика не принадлежит устройству одной страницы или маршрута. Конкретный visual outcome и сборка нескольких доменов остаются в `compositions`.
Предметная логика не принадлежит устройству одной страницы или маршрута. Конкретный visual outcome и сборка нескольких предметных ответственностей остаются в `compositions`.
### Infra
`infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной доменной модели.
`infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной предметной модели.
### UI
@@ -60,9 +60,11 @@ app
compositions
|
domains
/ \
infra ui
\ /
|
infra
|
ui
|
shared
```
@@ -73,23 +75,25 @@ shared
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `shared` |
| `infra` | `infra`, `ui`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
`infra` и `ui` не импортируют друг друга. Универсальный UI получает локализованный текст, тему, callbacks аналитики и другие возможности через входной контракт либо связывается с ними в `domains` или `compositions`. Если UI-модулю необходимо напрямую знать конкретный технический сервис приложения, такой код не является универсальным UI.
`infra` может импортировать `ui`, когда технической возможности требуется визуальное представление, например CAPTCHA, платёжный виджет, карта, uploader, уведомление или инструмент разработчика. Такое представление остаётся частью технической ответственности и не переносит в `infra` страницы, продуктовые тексты или композицию нескольких модулей.
`ui` не импортирует `infra`. Универсальный UI получает локализованный текст, тему, callbacks аналитики и другие технические возможности через входной контракт. Если UI-модулю необходимо напрямую знать конкретный технический сервис приложения, интеграция размещается в `infra`, `domains` или `compositions`, а универсальная часть остаётся в `ui`.
Импорты внутри слоя, публичный API и циклы описаны отдельно в [Зависимостях](./dependencies.md).
## Связанные правила
- [`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-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-LAYER-R001`](../rules/registry.md#slm-layer-r001)
- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
- [`SLM-LAYER-R003`](../rules/registry.md#slm-layer-r003)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
## Граница Level 1
## Граница слоя
Разрешённый импорт не переносит владение. Например, доменный модуль может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев.
Разрешённый импорт не переносит владение. Например, модуль слоя `domains` может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев.
Если код определяет продуктовую модель, правило или сценарий, он принадлежит `domains`, а не `shared` или `infra`. Строгая внутренняя форма домена появляется только на [Level 2](../level-2/).
Если код определяет продуктовую модель, правило или сценарий, он принадлежит `domains`, а не `shared` или `infra`. Внутренняя форма домена определяется его ответственностью и реальными потребителями.

View File

@@ -1,13 +1,13 @@
# Жизненный цикл Level 1
# Жизненный цикл
> Пояснение нормативной модели владения ресурсами Level 1.
> Пояснение нормативной модели владения ресурсами SLM.
Жизненный цикл является частью ответственности модуля. Файл, компонент, провайдер или точка входа фреймворка могут технически создавать и останавливать ресурс, но архитектурным владельцем остаётся модуль.
## Связанные правила
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
- [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013)
## Граница ресурса

View File

@@ -0,0 +1,66 @@
# Модули SLM
> Пояснение нормативной модели модулей SLM.
Модуль является основной архитектурной единицей SLM. Он размещается в отдельной папке, но может состоять только из публичной точки входа и одного файла реализации.
## Связанные правила
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-MODULE-A014`](../rules/registry.md#slm-module-a014)
- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
- [`SLM-MODULE-R012`](../rules/registry.md#slm-module-r012)
- [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016)
- [`SLM-ENVIRONMENT-R017`](../rules/registry.md#slm-environment-r017)
- [`SLM-ENVIRONMENT-R018`](../rules/registry.md#slm-environment-r018)
- [`SLM-ENVIRONMENT-R019`](../rules/registry.md#slm-environment-r019)
## Владение
Каждая самостоятельная ответственность имеет одного модуля-владельца. Модуль определяет её публичный API, зависимости, состояние, область жизни и внутреннее устройство независимо от того, в каком файле выполняется конкретный код.
Точки входа `app` и нормативные ресурсы `shared` являются единственными немодульными исключениями. Остальной код внутри SLM root либо принадлежит существующему модулю, либо образует новый модуль.
## Публичный API
Модуль предоставляет один логический публичный API. По умолчанию он представлен корневым `index`, который служит основным barrel. Если реальные потребители требуют разделить несовместимые среды выполнения, модуль добавляет один или несколько фасетов `client`, `browser` и `server`.
Объявленные фасеты вместе образуют один публичный API и не считаются deep imports. Внешний код использует модуль только через них. Сам API открывает только контракт, необходимый реальным внешним потребителям; внутренние механизмы, изменяемое состояние и детали жизненного цикла остаются закрытыми.
```text
auth/
├── index.ts # Универсальный фасет
├── client.ts # Необязательная клиентская framework-граница
├── browser.ts # Необязательная browser-only граница
├── server.ts # Необязательная server-only граница
└── ... # Внутренняя реализация
```
`index` экспортирует публичные типы и runtime-код, который одинаково допустимо выполнять при серверном рендеринге, включая RSC, и в клиентском runtime. Он не реэкспортирует специализированные фасеты.
`client` экспортирует Client Components, hooks, Providers и другой код, который не может выполняться как RSC. Такой код может быть отмечен директивой вроде `use client`.
`browser` экспортирует browser-only возможности и lazy-функциональность. Потребитель подключает этот фасет только динамически через поддерживаемую фреймворком границу с отключённым SSR.
`server` экспортирует только server-only возможности. `index`, `client` и `browser` не импортируют и не реэкспортируют его код.
Фасет не создаётся для симметрии или будущей потребности. Один публичный runtime-export размещается в минимально подходящем фасете и не дублируется между фасетами.
## Внутреннее устройство
Модуль может содержать корневые файлы, сегменты, компоненты и [вложенные модули](./nested-modules.md). Внутри своей границы он может использовать относительные импорты и не обязан обращаться к собственному публичному API; точную форму внутренних импортов определяет стайлгайд.
SLM не требует полного каркаса или обязательного каталога сегментов. Файлы фасетов являются публичными точками входа, а не сегментами или самостоятельными модулями.
## Визуальный модуль
Визуальный модуль обычно имеет корневой компонент, который экспортируется через публичный API.
```text
button/
├── button.tsx
└── index.ts
```
Корневой компонент остаётся компонентом, а владельцем ответственности является модуль `button`.

View File

@@ -1,15 +1,15 @@
# Вложенные модули Level 1
# Вложенные модули
> Пояснение нормативной модели вложенных модулей Level 1.
> Пояснение нормативной модели вложенных модулей SLM.
Вложенный модуль является обычным модулем, размещённым внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей.
Вложенный модуль является обычным модулем, размещённым внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется всем общим правилам модулей.
## Связанные правила
- [`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-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010)
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)
## Пример

View File

@@ -1,19 +1,19 @@
# Сегменты Level 1
# Сегменты SLM
> Пояснение нормативной модели сегментов Level 1.
> Пояснение нормативной модели сегментов SLM.
Сегмент организует внутреннее содержимое модуля. Level 1 определяет роль сегмента, но не задаёт обязательный список имён.
Сегмент организует внутреннее содержимое модуля. SLM определяет роль сегмента, но не задаёт обязательный список имён.
## Связанное правило
- [`SLM-L1-SEGMENT-R008`](../rules/level-1.md#slm-l1-segment-r008)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-SEGMENT-R008`](../rules/registry.md#slm-segment-r008)
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
## Файловая форма
Названия, набор и содержимое сегментов определяет стайлгайд проекта. Сегмент может группировать файлы, компоненты или вложенные модули.
Файлы и компоненты сегмента принадлежат родительскому модулю. Вложенный модуль внутри сегмента сохраняет собственные ответственность, публичный API и узел графа зависимостей.
Файлы и компоненты сегмента принадлежат родительскому модулю. Вложенный модуль внутри сегмента сохраняет собственные ответственность, публичный API и границу зависимостей.
## Пример

View File

@@ -1,14 +1,14 @@
# Терминология Level 1
# Терминология SLM
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
Определения Level 1 задают обязательный смысл архитектурных терминов и используются при толковании всех правил. Код получают только блокирующие требования, а не сами определения.
Определения задают обязательный смысл архитектурных терминов и используются при толковании всех правил. Код получают только блокирующие требования, а не сами определения.
## Базовые понятия
### SLM root
Граница структурной архитектуры одного приложения. Внутри неё определяются слои, модули и граф зависимостей Level 1. Монорепозиторий, пакеты и отношения между несколькими SLM root находятся за пределами Level 1.
Граница структурной архитектуры одного приложения. Внутри неё определяются слои, модули и зависимости SLM. Монорепозиторий, пакеты и отношения между несколькими SLM root находятся за пределами текущего черновика.
### Ответственность
@@ -20,13 +20,29 @@
### Публичный API
Единая логическая точка внешнего доступа к модулю. Публичный API скрывает внутреннее устройство; конкретное имя файла и механизм экспорта определяет стайлгайд проекта.
Единая логическая граница внешнего доступа к модулю. Публичный API скрывает внутреннее устройство и состоит из обязательного корневого фасета `index` и только реально необходимых environment-фасетов `client`, `browser` и `server`.
### Фасет
Объявленная публичная точка входа модуля, которая открывает часть его единого логического API для определённой среды или способа выполнения. Импорт объявленного фасета не является deep import. Любой другой путь внутрь модуля остаётся внутренним.
Корневой фасет `index` является основным barrel модуля. Он экспортирует публичные типы и runtime-код, совместимый как с серверным рендерингом, включая React Server Components, так и с клиентским выполнением.
Необязательные environment-фасеты имеют следующий нормативный смысл:
| Фасет | Среда и способ выполнения |
|---|---|
| `client` | Клиентская framework-граница, которая может участвовать в server prerender и затем выполняться при hydration и в браузере |
| `browser` | Browser-only код, подключаемый только динамически с отключённым SSR |
| `server` | Server-only код, недоступный через универсальный, клиентский и браузерный фасеты |
Client Component, импортированный Server Component, не становится универсальным кодом и не экспортируется через `index`. Совместимость фасета со средой определяется всеми его runtime-импортами и реэкспортами, включая транзитивные.
### Зависимость
Статическая связь внутри одного SLM root, которую импорт или реэкспорт создаёт между архитектурными границами. Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
Зависимость любого внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу и становится отдельным узлом графа зависимостей.
Зависимость любого внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
### Область жизни
@@ -46,7 +62,7 @@
Отношение допустимой зависимости между слоями одного SLM root. Матрица определяет, код каких слоёв может импортировать исходный слой; она не обязана образовывать линейный порядок.
Для Level 1 нормативно отношение `app → compositions → domains → { infra, ui } → shared`. `infra` и `ui` являются независимыми ветвями: они не импортируют друг друга. Промежуточный слой не является обязательным посредником.
Для SLM нормативно отношение `app → compositions → domains → infra ui → shared`. `infra` может импортировать `ui`, а `ui` не импортирует `infra`. Промежуточный слой не является обязательным посредником.
### Слой
@@ -68,53 +84,47 @@
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `shared` |
| `infra` | `infra`, `ui`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
### Модуль
Минимальная самостоятельная архитектурная единица Level 1. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API.
Минимальная самостоятельная архитектурная единица SLM. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API.
### Доменная ответственность
### Предметная ответственность
Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Количество экранов, endpoint-ов, hooks или файлов само по себе не определяет её границу.
### Доменный модуль
Обычный SLM-модуль слоя `domains`, представляющий одну доменную ответственность. Он подчиняется всем правилам модулей, предоставляет единый публичный API и является узлом графа зависимостей.
Доменный модуль может содержать сегменты, компоненты и вложенные модули. Level 1 не требует разделять его внутреннее содержимое по техническим ролям.
### Группа
Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или узла графа зависимостей.
Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или границы зависимостей.
### Сегмент
Внутренняя часть одного модуля, которая группирует его содержимое по назначению. Сегмент не является самостоятельным владельцем, публичным API или узлом графа зависимостей.
Внутренняя часть одного модуля, которая группирует его содержимое по назначению. Сегмент не является самостоятельным владельцем, публичным API или границей зависимостей.
### Компонент
Сущность фреймворка, которая реализует часть интерфейса родительского модуля. Компонент не образует собственного владельца, публичного API или узла графа зависимостей.
Сущность фреймворка, которая реализует часть интерфейса родительского модуля. Компонент не образует собственного владельца, публичного API или границы зависимостей.
Импорты, состояние, доступ к данным и код жизненного цикла компонента принадлежат родительскому модулю. Их наличие само по себе не создаёт новый модуль; решающим признаком является самостоятельная ответственность.
### Вложенный модуль
Обычный модуль, размещённый внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей.
Обычный модуль, размещённый внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется всем общим правилам модулей.
Публичный API вложенного модуля доступен коду родительской границы. Для кода за пределами родительского модуля вложенный модуль остаётся внутренней реализацией родителя.
### Точка входа фреймворка
Специальная немодульная единица слоя `app`, которая непосредственно связывает приложение с фреймворком. Её импорты участвуют в проверке направления слоёв, но сама точка входа не является узлом графа модулей.
Специальная немодульная единица слоя `app`, которая непосредственно связывает приложение с фреймворком. Её импорты участвуют в проверке направления слоёв, но сама точка входа не является модулем.
### Ресурс shared
Специальная немодульная единица слоя `shared`: небольшая детерминированная утилита, общий тип, стиль, конфигурация или статический ресурс без продуктового знания, изменяемого состояния, ввода-вывода, области жизни и собственного публичного API.
Ресурс `shared` может импортироваться напрямую по пути, установленному стайлгайдом, и не является узлом графа модулей. Доступный по этому пути файл является всей единицей и не скрывает отдельное внутреннее устройство.
Ресурс `shared` может импортироваться напрямую по пути, установленному стайлгайдом, и не является модулем. Доступный по этому пути файл является всей единицей и не скрывает отдельное внутреннее устройство.
Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль.

View File

@@ -0,0 +1,51 @@
# Проверка SLM
> Граница автоматической проверки и архитектурного ревью SLM.
## Автоматическая проверка
Проект, заявляющий соответствие SLM, сопоставляет физические пути с SLM root, слоями, модулями, Groups, вложенными модулями, публичными фасетами, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей.
Каждое правило класса `A` должно быть реализовано проверкой проекта и блокировать её при нарушении. SLM не навязывает конкретный инструмент.
Скрипт `draft-rules.js` проверяет только целостность документов: формат и уникальность кодов, ссылки и наличие тематических упоминаний. Он не проверяет архитектуру приложения.
Актуальный список правил скрипт получает из [канонического реестра](../rules/registry.md).
## Архитектурное ревью
Правила класса `R` проверяются вручную. Статический анализ может обнаружить подозрительный код, но не способен окончательно определить:
- ответственность и её владельца;
- связность ответственности модуля;
- соответствие кода роли слоя;
- необходимость экспортов публичного API;
- область жизни ресурса и достаточность очистки;
- наличие самостоятельной границы у компонента, группы или сегмента.
## Проверка фасетов
Автоматическая проверка сопоставляет публичные пути модуля с фасетами `index`, `client`, `browser` и `server`, запрещает остальные внешние пути и проверяет их runtime-импорты и реэкспорты, включая транзитивные.
Для проверки сред инструмент различает runtime imports, type-only imports и dynamic imports. Окончательное решение о соответствии экспортируемого кода назначению фасета принимается на ревью.
На ревью проверяется:
- экспортирует ли `index` только универсальные типы и runtime-код;
- остаётся ли `client` совместимым с server prerender и browser hydration;
- достигается ли `browser` только через dynamic boundary с отключённым SSR;
- остаётся ли `server` недоступным через `index`, `client` и `browser`;
- существует ли каждый специализированный фасет ради реального потребителя;
- не дублируется ли один runtime-export между фасетами.
Название файла, директива `use client`, tree shaking или локальная проверка `typeof window` сами по себе не доказывают совместимость кода со средой выполнения.
## Проверка слоя domains
На ревью определяется:
- соответствует ли ответственность модуля предметной роли слоя `domains`;
- не разделена ли одна область на соседние модули без самостоятельных владельцев;
- не объединены ли в одном модуле несвязанные предметные области;
- остаются ли страницы, маршруты и UI нескольких предметных ответственностей в `compositions`;
- остаются ли самостоятельные технические сервисы без предметной модели в `infra`.

View File

@@ -5,32 +5,29 @@ title: SLM Design
hero:
name: SLM Design
text: Последовательная архитектура фронтенд-приложений
tagline: Начните со слоёв и доменных модулей, затем переводите отдельные сложные домены в пакетную форму со строгими runtime-границами.
tagline: Слои, модули и явные публичные границы для приложений с несколькими средами выполнения.
image:
src: /logo.svg
alt: SLM Design
actions:
- theme: brand
text: Читать Level 1
link: /level-1/
- theme: alt
text: Читать Level 2
link: /level-2/
text: Читать архитектуру
link: /architecture/
- theme: alt
text: Реестр правил
link: /rules/
features:
- title: Level 1 · Архитектурная база
details: Шесть слоёв, доменные модули, публичные API, зависимости, вложенные границы и жизненный цикл ресурсов.
- title: Level 2 · Доменные API
details: Domain API, consumer-owned ports, production adapters, default assemblies и Framework Groups с явными runtime-границами.
- title: Архитектурная база
details: Шесть слоёв, модули, публичные API, зависимости, вложенные границы и жизненный цикл ресурсов.
- title: Фасеты сред выполнения
details: Универсальный index и специализированные client, browser и server фасеты для явного разделения сред выполнения.
- title: Канонические правила
details: Точные блокирующие требования отделены от определений, рекомендаций и примеров и собраны по уровням.
details: Точные блокирующие требования отделены от определений, рекомендаций и примеров и собраны в едином реестре.
---
## Что опубликовано
Сайт содержит рабочие черновики двух уровней SLM и их канонические реестры правил. Level 2 применяется к отдельным доменам поверх общей базы Level 1. Монорепозитории пока не входят в опубликованную документацию.
Сайт содержит рабочий черновик архитектуры SLM и канонический реестр правил. Монорепозитории пока не входят в опубликованную документацию.
Определения применяемого уровня нормативны внутри черновика. Точные формулировки блокирующих требований находятся только в [реестре правил](/rules/).
Определения терминологии нормативны внутри черновика. Точные формулировки блокирующих требований находятся только в [реестре правил](/rules/registry).

View File

@@ -1,53 +0,0 @@
# SLM Level 1
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
Level 1 задаёт полную структурную основу SLM: слои, модули, доменные модули, публичные границы, граф зависимостей и владение жизненным циклом.
## Место в уровнях SLM
| Уровень | Назначение |
|---|---|
| Level 1 | Слои, доменные модули, зависимости, структурные сущности и жизненный цикл ресурсов |
| Level 2 | Опциональная пакетная форма отдельных доменов, именованные API, assemblies и явные границы сред выполнения |
Переход отдельного домена на Level 2 может требовать рефакторинга, но базовые понятия Level 1 сохраняются. Остальные домены того же SLM root могут оставаться модулями Level 1.
## Область Level 1
Level 1 описывает слои, доменные модули, группы, сегменты, компоненты, публичный API, граф зависимостей и владение жизненным циклом ресурсов.
Level 1 не задаёт обязательную внутреннюю форму доменного модуля, фабрики, порты, адаптеры, assemblies, обязательный поток данных, монорепозитории, соглашения об именовании и файловый стайлгайд.
Появление нескольких сред выполнения, нескольких независимо собираемых API или необходимости разделить бизнес-логику и технические сборки является сигналом перевести конкретный домен на [Level 2](../level-2/).
## Виды утверждений
- **Определение** нормативно задаёт смысл архитектурного термина, но не получает код правила.
- **Правило** задаёт блокирующий архитектурный инвариант и объявляется в каноническом реестре.
- **Рекомендация** помогает принять решение, но не делает архитектуру невалидной.
- **Пример** иллюстрирует модель и не задаёт обязательную структуру.
Нормативные определения находятся в [терминологии](./terminology.md). Канонический набор правил: [правила SLM Level 1](../rules/level-1.md). Формат и требования к ним: [Правила SLM](../rules/).
Остальные документы Level 1 объясняют и иллюстрируют модель, но не владеют точными формулировками определений и правил.
## Основная идея
Модуль является основной архитектурной единицей SLM. Слой задаёт роль и направление зависимостей. Доменный модуль представляет одну предметную область. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю.
Level 1 требует отдельную папку и единый публичный API модуля. Имена этих элементов, внутренняя файловая форма модулей, сегментов и компонентов определяются стайлгайдом проекта.
## Карта черновика
- [Терминология](./terminology.md)
- [Слои](./layers.md)
- [Доменные модули](./domains.md)
- [Зависимости](./dependencies.md)
- [Модули](./modules.md)
- [Группы](./groups.md)
- [Сегменты](./segments.md)
- [Компоненты](./components.md)
- [Вложенные модули](./nested-modules.md)
- [Жизненный цикл](./lifecycle.md)
- [Проверка](./validation.md)

View File

@@ -1,59 +0,0 @@
# Зависимости Level 1
> Пояснение нормативной модели зависимостей Level 1.
Матрица слоёв задаёт допустимые связи, а модули образуют граф зависимостей.
## Что считается зависимостью
- Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
- Импорт между файлами одного модуля не пересекает модульную границу и не создаёт отдельный узел графа.
- Импорт любого внутреннего файла, сегмента или компонента считается зависимостью ближайшего модуля-владельца.
- Вложенный модуль является обычным самостоятельным узлом графа.
- Группы, сегменты и компоненты не являются самостоятельными узлами графа.
Точка входа фреймворка не является модулем. Её импорты участвуют в проверке направления слоёв, но не образуют исходящий узел графа модулей.
Ресурс `shared` также не является модулем. Его прямой импорт участвует в проверке направления слоёв, но не нарушает требование о публичном API модуля.
## Допустимые связи
- Модуль может импортировать модули своего слоя и слоёв, разрешённых нормативной матрицей.
- Модули одного слоя могут импортировать друг друга.
- Промежуточный слой не является обязательным посредником.
- `infra` и `ui` не импортируют друг друга; их связывает владелец из `domains`, `compositions` или `app`.
Доменный модуль Level 1 может импортировать публичный API другого доменного модуля. Runtime- и type-only импорты одинаково создают ребро графа, поэтому общий граф обязан оставаться ацикличным.
```ts
// domains/orders
import type { Product } from '@/domains/catalog'
```
Level 1 не требует отдельного механизма междоменной инъекции. Более строгая модель cross-domain зависимостей задаётся Level 2.
Матрица слоёв определена в [Слоях](./layers.md).
## Связанные правила
- [`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-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010)
## Публичный API
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимо
import { Button } from '@/ui/button/button'
```
## Циклы
```text
ui/modal → ui/button → ui/icon
ui/icon -/→ ui/modal
```

View File

@@ -1,61 +0,0 @@
# Доменные модули 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 не требует выделять Domain API, ports, adapters, assemblies или 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, когда ему нужны устойчивые API, несколько фабрик, разные среды выполнения или независимые SLM-модули сборок и framework-интеграции.
Такой переход изменяет только форму выбранного домена: корневой доменный модуль исчезает, а его публичные модели и операции переходят обязательному модулю `api` внутри доменного пакета. Другие предметные области SLM root не обязаны переходить вместе с ним, но входящие imports и production composition roots выбранного домена обновляются.

View File

@@ -1,43 +0,0 @@
# Модули Level 1
> Пояснение нормативной модели модулей Level 1.
Модуль является основной архитектурной единицей SLM. Он размещается в отдельной папке, но может состоять только из публичной точки входа и одного файла реализации.
## Связанные правила
- [`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)
## Владение
Каждая самостоятельная ответственность имеет одного модуля-владельца. Модуль определяет её публичный API, зависимости, состояние, область жизни и внутреннее устройство независимо от того, в каком файле выполняется конкретный код.
Точки входа `app` и нормативные ресурсы `shared` являются единственными немодульными исключениями. Остальной код внутри SLM root либо принадлежит существующему модулю, либо образует новый модуль.
## Публичный API
Модуль предоставляет один логический публичный API. Конкретное имя точки входа и механизм экспорта определяет стайлгайд проекта.
Внешний код использует модуль только через публичный API. Сам API открывает только контракт, необходимый реальным внешним потребителям; внутренние механизмы, изменяемое состояние и детали жизненного цикла остаются закрытыми.
## Внутреннее устройство
Модуль может содержать корневые файлы, сегменты, компоненты и [вложенные модули](./nested-modules.md). Внутри своей границы он может использовать относительные импорты и не обязан обращаться к собственному публичному API; точную форму внутренних импортов определяет стайлгайд.
SLM не требует полного каркаса или обязательного каталога сегментов.
## Визуальный модуль
Визуальный модуль обычно имеет корневой компонент, который экспортируется через публичный API.
```text
button/
├── button.tsx
└── index.ts
```
Корневой компонент остаётся компонентом, а владельцем ответственности является модуль `button`.

View File

@@ -1,34 +0,0 @@
# Проверка Level 1
> Граница автоматической проверки и архитектурного ревью Level 1.
## Автоматическая проверка
Проект, заявляющий соответствие Level 1, сопоставляет физические пути с SLM root, слоями, доменными модулями, остальными модулями, Groups, вложенными модулями, публичными точками входа, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей.
Каждое правило класса `A` должно быть реализовано проверкой проекта и блокировать её при нарушении. Level 1 не навязывает конкретный инструмент.
Скрипт `draft-rules.js` проверяет только целостность документов: формат и уникальность кодов, ссылки и наличие тематических упоминаний. Он не проверяет архитектуру приложения.
Актуальный список правил скрипт получает из [канонического реестра](../rules/level-1.md).
## Архитектурное ревью
Правила класса `R` проверяются вручную. Статический анализ может обнаружить подозрительный код, но не способен окончательно определить:
- ответственность и её владельца;
- связность предметной области доменного модуля;
- соответствие кода роли слоя;
- необходимость экспортов публичного API;
- область жизни ресурса и достаточность очистки;
- наличие самостоятельной границы у компонента, группы или сегмента.
## Проверка доменных модулей
На ревью определяется:
- представляет ли доменный модуль одну связную предметную область;
- не разделена ли одна область на соседние модули без самостоятельных владельцев;
- не объединены ли в одном модуле несвязанные предметные области;
- остаются ли страницы, маршруты и multi-domain UI в `compositions`;
- остаются ли самостоятельные технические сервисы без предметной модели в `infra`.

View File

@@ -1,155 +0,0 @@
# SLM Level 2
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
Level 2 предназначен для отдельных предметных областей, которым нужен устойчивый Domain API поверх нескольких внешних источников, сред выполнения или самостоятельных framework-модулей. Он сохраняет структурную базу Level 1, но заменяет выбранный доменный модуль доменным пакетом с обязательными `api`, production adapters и штатной сборкой `assemblies/default`.
## Наследование Level 1
Доменный пакет Level 2 соблюдает определения и правила Level 1, кроме явно заменённых положений:
| Положение Level 1 | Статус в Level 2 |
|---|---|
| Матрица `app → compositions → domains → { infra, ui } → shared` | Сохраняется |
| Модуль, Group, сегмент, компонент, публичный API и статический граф зависимостей | Сохраняют смысл |
| Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются только для предметной области в пакетной форме |
| Единый публичный API модуля `api` | Представлен обязательными consumer type и factory-фасетами, implementer-фасетом ports при необходимости и необязательным runtime-фасетом |
| Навигационная Group слоя `domains` | Может одновременно содержать доменные модули и пакеты |
| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups |
Канонический набор требований образуют [правила Level 1](../rules/level-1.md) и [правила Level 2](../rules/level-2.md).
## Основная идея
Для прикладного consumer предметная область существует как Domain API:
```text
framework / composition
Domain API
dependency ports
production adapters
SDK / backend / storage / realtime
```
Модуль `api` владеет публичными моделями, validation, операциями, outcomes и стабильными ошибками. Он объявляет consumer-owned ports и получает их реализации через фабрику. Adapter знает конкретный provider, assembly выбирает adapters, а framework binding получает готовый API и организует state, cache, reactivity и hydration средствами своего framework.
Приложение не обращается к предметному внешнему источнику в обход Domain API. Это не запрещает самостоятельные технические сервисы `infra`, universal UI или framework-only SDK для получения opaque input; запрет относится к данным и операциям конкретного домена.
## Когда выбирать Level 2
Level 2 оправдан, когда предметной области нужны:
- собственная модель, отличающаяся от backend DTO;
- стабильные ошибки независимо от SDK и транспорта;
- несколько production sources или providers;
- HTTP, storage, realtime или platform integrations за одной предметной границей;
- разные baseline и специальные assemblies;
- строгие client/server/RSC/worker boundaries;
- самостоятельные domain-specific framework bindings;
- изолированные tests Domain API через fake ports и contract tests adapters.
Level 2 выбирается для конкретной предметной области. Один SLM root может корректно содержать простые доменные модули Level 1 и доменные пакеты Level 2. Размер каталога, один endpoint или один hook сами по себе не требуют перехода.
## Цена Level 2
Пакетная форма намеренно дороже простого доменного модуля. Она добавляет фасеты `api`, dependency ports, production adapters, обязательную штатную assembly, mapping внешних records и failures, а также отдельные test boundaries.
Эта цена окупается, когда Domain API действительно изолирует приложение от внешней модели, ошибок, provider и runtime. Если фабрика только переименовывает один метод SDK и возвращает тот же DTO и error, домену обычно достаточно Level 1.
Импорт assembly не создаёт граф и не запускает side effects. Composition root вызывает только assemblies dependency-connected доменов, нужных текущему route, request, worker или application scope; глобальная eager-сборка всех `default` не является требованием Level 2.
## Базовая форма
```text
src/domains/
├── catalog/ # Доменный модуль Level 1
└── auth/ # Доменный пакет Level 2
├── README.md # Необязательная metadata
├── api/ # Обязательный SLM-модуль
│ ├── index.ts # Только consumer-facing public types
│ ├── factory.ts # Public factories
│ ├── ports.ts # При наличии dependency ports
│ └── runtime.ts # Необязательный deterministic runtime
├── adapters/ # При наличии dependency ports
│ ├── identity-rest/ # SLM-модуль
│ └── identity-realtime/ # SLM-модуль
├── assemblies/ # Обязательная Group
│ ├── default/ # Обязательная штатная assembly
│ └── administration/ # Дополнительная assembly
└── react/ # Необязательная Framework Group
├── session/ # SLM-модуль
└── queries/ # SLM-модуль
```
Корень пакета не является модулем и не имеет `index.ts`. Groups также не имеют агрегирующих API. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своей публичной границей.
## Публичные границы
```ts
import type {
AuthError,
AuthSession,
AuthSessionApi,
} from '@/domains/auth/api'
import type {
AuthIdentityPort,
AuthIdentityPortFailure,
} from '@/domains/auth/api/ports'
import { createAuthSessionApi } from '@/domains/auth/api/factory'
import { isAuthError } from '@/domains/auth/api/runtime'
import { createAuth } from '@/domains/auth/assemblies/default'
import { AuthSessionProvider } from '@/domains/auth/react/session'
```
Обычный прикладной consumer импортирует типы `api`, при необходимости deterministic `api/runtime`, готовую production-сборку и framework bindings. Фасет `api/ports` предназначен для adapters, assemblies и tests. Фасет `api/factory` в production импортируют только assemblies своего домена.
Общие импорты `@/domains/auth`, `@/domains/auth/adapters`, `@/domains/auth/assemblies` и `@/domains/auth/react` запрещены: пакет и Groups не имеют публичного API.
## Штатная assembly
Каждый пакет содержит `assemblies/default`. Она создаёт канонический production-граф одного baseline capability context, объявленного проектом.
`default` может быть browser-only в React + Vite или действительно изоморфной в Next.js. Имя не является доказательством совместимости: проверяется executable import-граф для заявленных resolver conditions. Если RSC, administration, worker или realtime session требуют другого набора API, dependencies, trust или lifecycle, появляется дополнительная именованная assembly.
## State и framework
Domain API не является framework store. TanStack Query, SWR, Zustand, Redux, Pinia, Signals и аналогичные runtimes находятся в framework bindings или compositions. Они могут владеть framework metadata и UI-state, но их domain payload состоит только из public values, outcomes и events Domain API.
Server и client создают разные API instances и caches. Через RSC boundary передаются сериализуемые public values или hydration payload, но не фабрики, API objects, ports или mutable clients.
## Realtime
Realtime transport остаётся внутри adapter. Domain API может предоставлять command methods и subscriptions, но публикует только проверенные events, outcomes и stable domain errors. Correlation, acknowledgement, ordering, reconnect, duplicate delivery, resync, outcome uncertainty и cleanup задаются контрактом realtime port и не выводятся из поведения конкретного WebSocket SDK.
## Совместное применение форм
Простой доменный модуль и доменный пакет могут постоянно сосуществовать в одном SLM root, но одна предметная область имеет только одну форму. При связи, пересекающей пакет Level 2, доменный код использует type-only публичный контракт либо deterministic `api/runtime`; готовые API передаются runtime-аргументами assemblies пакетов Level 2 либо явным construction points модулей Level 1.
Переход одного домена изменяет его входящие dependency edges и composition roots, но не требует переводить несвязанные соседние домены на Level 2.
## Карта черновика
- [Терминология](./terminology.md)
- [Доменный пакет](./domains/domain-package.md)
- [Модуль api и Domain API](./domains/domain-api.md)
- [Фабрики, ports и adapters](./domains/factory-ports-adapters.md)
- [Assemblies и default](./domains/assemblies.md)
- [Состояние и кэш](./domains/state-cache.md)
- [Framework Groups и модули](./domains/framework-bindings.md)
- [Realtime](./domains/realtime.md)
- [Зависимости](./dependencies.md)
- [Тестирование](./domains/testing.md)
- [Проверка](./validation.md)
- [Переход auth](./domains/auth-example.md)
- [Открытые вопросы](./domains/open-questions.md)

View File

@@ -1,152 +0,0 @@
# Зависимости Level 2
> Уточнение статического import-графа и runtime injection graph внутри и между доменными границами.
## Связанные правила
- [`SLM-L2-API-A007`](../rules/level-2.md#slm-l2-api-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-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-API-A019`](../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-API-A022`](../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-PORT-R027`](../rules/level-2.md#slm-l2-port-r027)
- [`SLM-L2-ASSEMBLY-R030`](../rules/level-2.md#slm-l2-assembly-r030)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
## Статическая матрица внутри пакета
| Исходный модуль | Допустимые зависимости |
|---|---|
| `api` | Собственные файлы, объявленные environment-neutral ресурсы `shared`, API-safe packages, type-only Domain API и `api/runtime` других доменов |
| Adapter module | `api/ports` своего домена, `infra`, concrete provider runtime, `shared` |
| Assembly | `api`, `api/ports`, `api/factory`, при необходимости `api/runtime` своего домена, публичные adapters своего домена, type-only Domain API других доменов, `shared` |
| Framework binding module | `api` и `api/runtime` своего домена, публичные framework modules своего домена, framework/state/query runtime, `ui`, `shared` |
| Graph owner | Assemblies и framework modules входящих в граф доменов, а также разрешённые матрицей `infra`, `ui` и `shared` |
Модуль `api` не достигает adapters, assemblies, framework modules, product SDK, storage, state/query manager, DOM, Node.js API или других environment-specific capabilities. Проверяется весь транзитивный executable и type graph его фасетов.
Adapter импортирует contract только через `api/ports`. Он не импортирует factory и consumer-facing runtime, потому что не создаёт API и не выбирает публичный domain outcome.
Assembly импортирует только adapters собственного домена. Production graph owner не импортирует concrete adapters или `api/factory`: он вызывает готовые assembly builders.
## Публичные фасеты
```text
api
→ import type прикладных contracts
api/ports
→ import type adapters, assemblies и tests
api/factory
→ runtime import assemblies и API tests
api/runtime
→ runtime import реальных consumers
```
Символьная type-проверка ports может быть строже обычного path allowlist. Проект объявляет, какие files и modules считаются adapters, assemblies и test boundaries.
## Междоменные статические импорты
Если связь пересекает границу пакета Level 2, разрешены:
```ts
import type {
AuthSessionApi,
} from '@/domains/auth/api'
import {
isAuthError,
} from '@/domains/auth/api/runtime'
```
Для доменного модуля Level 1 используется type-only импорт его обычного публичного API.
Запрещено импортировать из другого домена:
- `api/factory`;
- `api/ports`;
- готовый API singleton;
- assembly;
- adapter;
- framework state, hook, context, Provider или component;
- любой внутренний путь `api`.
Runtime-импорт `api/runtime` остаётся статическим ребром общего DAG. Если он создаёт цикл, границы доменов или владелец pure-функции пересматриваются.
## Runtime-инъекция cross-domain API
Готовый API другого домена передаётся assembly аргументом:
```text
createAuth()
→ AuthSessionApi
→ createUser({ auth })
→ UserProfileApi
```
User assembly передаёт `auth` своей factory. Она не импортирует runtime instance Auth.
Cross-domain API не превращается автоматически в local port. Bridge port нужен только при реальном translation contract. Structural copy чужого API скрывает owner и затрудняет обнаружение runtime-цикла.
## Runtime dependency graph
Статический DAG импортов не показывает все runtime edges, передаваемые аргументами. Architecture mapping объявляет либо review явно восстанавливает:
- assembly inputs;
- создаваемые Domain API;
- public APIs и construction points доменных модулей Level 1;
- передаваемые factories dependencies;
- callbacks и late-bound capabilities, пересекающие Level 2 boundary;
- scope и multiplicity;
- cleanup order.
Graph owner создаёт независимые APIs раньше зависимых и освобождает их в обратном порядке. Цикл `A API → B API → A API` запрещён, даже если одна сторона является модулем Level 1, а callback, lazy holder или local structural type сохраняет статически ацикличный import graph.
Lazy provider или registry не является автоматическим исключением. Для него требуется отдельный readiness, lifecycle и failure contract, а сам runtime edge остаётся частью graph review.
## Совместное применение Level 1 и Level 2
Один SLM root может постоянно содержать обе формы. Между двумя доменными модулями Level 1 продолжают действовать обычные правила Level 1.
Если хотя бы одна сторона является пакетом Level 2, runtime API создаёт внешний graph owner и передаёт assembly зависимого пакета Level 2 либо явной construction point/public callback зависимого модуля Level 1. Если у модуля Level 1 такой точки нет и связь невозможна без global singleton или обратного импорта, модуль рефакторится либо переводится на Level 2.
Переход формы остаётся локальным для предметной ответственности, но change radius включает все входящие imports и composition roots выбранного домена.
## Framework state
Framework binding использует framework API только своего доменного пакета:
```ts
// Допустимо внутри domains/auth/react/queries
import {
useAuthApi,
} from '@/domains/auth/react/session'
```
```ts
// Недопустимо внутри domains/user/react/profile
import {
useAuthApi,
} from '@/domains/auth/react/session'
```
Во втором случае composition читает projections обоих доменов и передаёт values или callbacks через публичные props. Если User Domain API зависит от Auth, связь выполняется assemblies на runtime graph level.
## Границы сред и RSC
Каждая declared client, server, edge, worker или shared entry point проверяется под реальными resolver conditions. Название `assemblies/default` не объявляет environment compatibility.
Tree shaking и runtime condition не доказывают изоляцию. Server-only adapter не достигается из client entry, даже если ветка считается неиспользуемой.
Checker различает:
- executable import edge;
- type-only import edge;
- framework reference edge;
- dynamic import с объявленным target capability set.
Server Component выполняется в server scope. Ссылка на Client Component и invocation Server Action анализируются как framework references, а не как обычное совместное выполнение. Для SSR-enabled Client Component отдельно проверяются server prerender graph, browser hydration graph и объявленные framework-deferred browser edges. Необъявленный или неанализируемый dynamic import запрещается либо явно allowlist-ится project policy.

View File

@@ -1,31 +0,0 @@
# Доменные пакеты Level 2
Доменный пакет заменяет один выбранный доменный модуль Level 1. Он объединяет SLM-модули одной предметной области вокруг контролируемого Domain API, но сам не является модулем, Group или публичным API.
```text
domains/auth/
├── api/ # Обязательный модуль
├── adapters/ # При наличии dependency ports
├── assemblies/
│ └── default/ # Обязательная штатная assembly
└── react/
├── session/
└── queries/
```
Другие предметные области того же SLM root могут оставаться доменными модулями Level 1.
## Основные границы
- [Доменный пакет](./domain-package.md) определяет предметную и структурную policy boundary.
- [Domain API](./domain-api.md) является единственным семантическим шлюзом к данным и операциям домена.
- [Фабрики, ports и adapters](./factory-ports-adapters.md) изолируют SDK, backend, storage, runtime capabilities и provider failures.
- [Assemblies](./assemblies.md) содержат обязательную штатную сборку `default` и дополнительные production-контексты.
- [Состояние и кэш](./state-cache.md) принадлежат framework bindings или compositions: они могут хранить framework metadata и UI-state, но materialize domain payload только из значений Domain API.
- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
- [Realtime](./realtime.md) задаёт messages, subscriptions, correlation, resync, errors и cleanup.
- [Тестирование](./testing.md) проверяет Domain API через фабрики, adapters через port contracts и assemblies через production wiring.
- [Переход auth](./auth-example.md) показывает локальный переход с Level 1 без big-bang всего root.
- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей.
Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md).

View File

@@ -1,264 +0,0 @@
# Assemblies и production-граф
> Пояснение обязательной штатной сборки, дополнительных контекстов, environment compatibility и lifecycle.
## Связанные правила
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ASSEMBLY-R011`](../../rules/level-2.md#slm-l2-assembly-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)
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
- [`SLM-L2-ASSEMBLY-R030`](../../rules/level-2.md#slm-l2-assembly-r030)
- [`SLM-L2-ASSEMBLY-R031`](../../rules/level-2.md#slm-l2-assembly-r031)
## Назначение
Assembly является SLM-модулем Group `assemblies`. Она выбирает production adapters своего домена, вызывает фабрики `api` и возвращает готовый именованный граф Domain API для одного объявленного production-контекста.
```text
api/factory + adapters + cross-domain APIs
→ assembly
→ named Domain API graph
```
Assembly не добавляет предметные методы, модели, transitions или ошибки. Она также не владеет framework state: готовый API передаётся framework binding или composition.
Импорт assembly не запускает side effects. Граф появляется только после вызова builder.
## Обязательная default assembly
Каждый пакет содержит модуль `assemblies/default`:
```text
auth/assemblies/
├── default/
│ └── index.ts
└── administration/
└── index.ts
```
`default` является штатной production-сборкой домена для одного baseline capability set, объявленного проектом. Она может быть browser-only, server-only, worker-compatible или действительно isomorphic. Имя не сообщает environment compatibility.
Пример metadata:
```yaml
assemblies:
default:
capabilities: [fetch, web-crypto]
conditions: [browser, import]
administration:
capabilities: [node, server-secrets]
conditions: [node, import]
```
Формат metadata не нормирован, но checker должен получать capability set и resolver conditions из явного project mapping, а не угадывать их по имени `default`.
## Дополнительные assemblies
Дополнительная assembly появляется, когда отличается реальная production-граница:
- набор Domain API;
- dependencies или providers;
- trust boundary;
- environment capabilities;
- scope или lifecycle;
- способ аутентификации;
- realtime guarantees.
Хорошие имена описывают контекст: `administration`, `realtime-session`, `worker`, `rsc`. Имя `rsc` оправдано только при отличающемся RSC wiring; само наличие Server Component не требует отдельной assembly.
Не создаётся assembly-заглушка с методами, бросающими `NOT_SUPPORTED`. Контекст возвращает только реально доступные API.
## Штатный граф
```ts
import type {
AuthSessionApi,
} from '@/domains/auth/api'
import {
createAuthSessionApi,
} from '@/domains/auth/api/factory'
import {
createAuthRestAdapter,
} from '@/domains/auth/adapters/identity-rest'
export type AuthGraph = Readonly<{
session: AuthSessionApi
}>
export const createAuth = (): AuthGraph => {
const session = createAuthSessionApi({
identity: createAuthRestAdapter(),
})
return { session }
}
```
Обычный graph owner импортирует только production builder:
```ts
import {
createAuth,
} from '@/domains/auth/assemblies/default'
const auth = createAuth()
```
Factory и concrete adapter остаются construction details assembly. Тесты API и adapters импортируют соответствующие границы напрямую.
## React + Vite и Next.js
В React + Vite `default` часто использует browser adapters:
```text
assemblies/default
→ browser REST adapter
→ browser WebSocket adapter
```
В Next.js та же `default` может считаться isomorphic только при совместимом executable graph под всеми заявленными conditions. Runtime branch не делает импорт безопасным:
```ts
// Недостаточное доказательство изоморфности.
if (typeof window === 'undefined') {
return createServerAdapter()
}
return createBrowserAdapter()
```
Если server и client требуют разных concrete dependencies, используются разные assemblies или framework-specific resolver entries, проверяемые отдельно.
## RSC boundary
RSC не переносит API instance с сервера в браузер:
```text
Server Component
→ request-scoped server assembly
→ server Domain API instance
→ public serializable value
→ Client Component boundary
→ separate client assembly
→ separate client Domain API instance
```
Server Component исполняется в server scope. Его импорт Client Component является framework reference, а не обычным executable edge RSC graph. При включённом SSR или prerender сам Client Component дополнительно исполняется в отдельном server render graph, а затем в browser hydration graph; обе фазы проверяются, а browser-only effects объявляются как framework-deferred edges. Server Action создаёт и очищает собственный request graph на каждый вызов.
Через boundary не передаются functions, API objects, ports, adapters, mutable cache clients или request secrets.
## Cross-domain input
Assembly зависимого домена принимает готовый API аргументом:
```ts
import type {
AuthSessionApi,
} from '@/domains/auth/api'
export type CreateUserInput = Readonly<{
auth: Pick<AuthSessionApi, 'getSession'>
}>
export const createUser = ({
auth,
}: CreateUserInput): UserGraph => {
const profile = createUserProfileApi({
auth,
profile: createUserProfileRestAdapter(),
})
return { profile }
}
```
Graph owner выполняет runtime-связь:
```ts
const auth = createAuth()
const user = createUser({
auth: auth.session,
})
```
User assembly делает только type-only импорт Auth API. Она не импортирует Auth factory, adapter или assembly. Общий runtime dependency graph остаётся ацикличным.
## Dependency-connected graph
Наличие `assemblies/default` у каждого Level 2 package не требует eager-сборки всех доменов:
```text
route A
→ auth/default
→ user/default
route B
→ catalog/default
```
Graph owner вызывает только builders, необходимые текущему scope. Module-level вызов `createAuth()` и global registry готовых APIs нарушают явное владение scope.
## Lifecycle
Factory не запускает запрос, socket, subscription или timer во время создания API. Явная операция, которая позже запускает ресурс, возвращает cleanup:
```ts
const subscription = await chat.subscribe(observer)
try {
await runScope()
} finally {
await subscription.close()
}
```
Если assembly создаёт owned resource или получает lifecycle handle adapter-owned resource, результат предоставляет aggregate cleanup:
```ts
export type ChatAssembly = Readonly<{
apis: ChatGraph
dispose: () => Promise<void>
}>
```
Cleanup является идемпотентным. После завершившегося cleanup resource не вызывает callbacks.
У каждого resource ровно один owner. Adapter, который сам создаёт connection или source cache, остаётся владельцем и экспортирует lifecycle handle; assembly только включает этот handle в aggregate cleanup. Если connection создаёт assembly, adapter получает borrowed capability и не закрывает её самостоятельно.
## Частичная ошибка сборки
Assembly регистрирует cleanup сразу после создания каждого owned resource и сразу после получения adapter lifecycle handle. Если следующий шаг завершается ошибкой, все зарегистрированные obligations выполняются до передачи ошибки caller-у:
```ts
export const createChat = async (): Promise<ChatAssembly> => {
const cleanups: Array<() => Promise<void>> = []
try {
const connection = await createRealtimeConnection()
cleanups.push(connection.close)
const history = createHistoryAdapter(connection)
const messages = createMessagesApi({ history })
return {
apis: { messages },
dispose: createIdempotentReverseCleanup(cleanups),
}
} catch (error) {
await runReverseCleanup(cleanups)
throw error
}
}
```
Реализация helper не нормирована. Нормативны достижимость cleanup на failure path, обратный dependency order и отсутствие callbacks после завершения disposal.
Assembly без cleanup obligations возвращает только API graph и не добавляет пустой `dispose` для симметрии. Наличие adapter-owned resource с переданным handle уже является cleanup obligation, даже если assembly не считается его владельцем.

View File

@@ -1,204 +0,0 @@
# Переход домена auth с Level 1
> Проверочный пример локального перехода от доменного модуля к пакету с Domain API, ports, adapters и default assembly.
## Связанные правила
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-API-R005`](../../rules/level-2.md#slm-l2-api-r005)
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
## Исходная форма Level 1
```text
domains/
├── auth/ # Доменный модуль
│ ├── hooks/
│ ├── services/
│ ├── stores/
│ ├── ui/
│ └── index.ts # Общий API модуля
└── catalog/ # Независимый доменный модуль
└── index.ts
```
Level 1 разрешает external calls, framework hooks, state и Auth scenarios внутри одной module boundary.
## Целевая форма Auth
```text
domains/
├── auth/ # Доменный пакет Level 2
│ ├── README.md
│ ├── api/ # Один SLM-модуль
│ │ ├── errors/
│ │ ├── factories/
│ │ ├── models/
│ │ ├── operations/
│ │ ├── ports/
│ │ ├── index.ts # Consumer-facing types
│ │ ├── ports.ts # Implementer-facing types
│ │ ├── factory.ts # Domain API factories
│ │ └── runtime.ts # Guards и public pure runtime
│ ├── adapters/ # Group
│ │ ├── identity-rest/ # SLM-модуль
│ │ ├── identity-realtime/ # SLM-модуль
│ │ └── request-session/ # SLM-модуль
│ ├── assemblies/ # Обязательная Group
│ │ ├── default/ # Штатный Auth graph
│ │ └── administration/ # Специальный trusted graph
│ └── react/ # Framework Group
│ ├── session/ # Provider готового API
│ ├── queries/ # Query/cache projection
│ └── login-form/ # Переиспользуемый domain UI
└── catalog/ # По-прежнему модуль Level 1
└── index.ts
```
Корневой `domains/auth/index.ts` удаляется. `catalog` и остальные домены не меняют форму только из-за перехода Auth.
## Перенос ответственности
| Исходная часть | Владелец Level 2 | Публичный путь |
|---|---|---|
| Session operations и public models | `auth/api` | `auth/api` |
| Port contracts и failures | `auth/api` | `auth/api/ports` |
| Runtime factories | `auth/api` | `auth/api/factory` |
| Error guards и public pure-функции | `auth/api` | `auth/api/runtime` |
| REST provider mapping | `auth/adapters/identity-rest` | Adapter API для assembly |
| Realtime protocol и correlation | `auth/adapters/identity-realtime` | Adapter API для assembly |
| Request cookies mapping | `auth/adapters/request-session` | Adapter API для assembly |
| Штатный production graph | `auth/assemblies/default` | `auth/assemblies/default` |
| Trusted administration graph | `auth/assemblies/administration` | `auth/assemblies/administration` |
| Provider и session hooks | `auth/react/session` | `auth/react/session` |
| Query/cache/hydration | `auth/react/queries` | `auth/react/queries` |
| Переиспользуемая форма | `auth/react/login-form` | `auth/react/login-form` |
| Страница, текст и redirect | `compositions` | API конкретной composition |
## Domain API и port
```ts
export type AuthSessionApi = {
getSession: () => Promise<AuthSession>
requestPhoneOtp: (
command: RequestPhoneOtpCommand,
) => Promise<RequestPhoneOtpOutcome>
verifyPhoneOtp: (
command: VerifyPhoneOtpCommand,
) => Promise<AuthSession>
}
```
```ts
export type AuthIdentityPort = {
requestPhoneOtp: (
command: AuthIdentityPortCommand,
) => Promise<AuthIdentityPortResult>
verifyPhoneOtp: (
command: VerifyIdentityPortCommand,
) => Promise<VerifyIdentityPortResult>
}
```
REST adapter реализует этот port поверх generated client. API проверяет records и преобразует port failures в `AuthError`.
## Штатная сборка
```ts
import {
createAuthSessionApi,
} from '@/domains/auth/api/factory'
import {
createIdentityRestAdapter,
} from '@/domains/auth/adapters/identity-rest'
export const createAuth = (): AuthGraph => ({
session: createAuthSessionApi({
identity: createIdentityRestAdapter(),
}),
})
```
Обычный production consumer использует:
```ts
import {
createAuth,
} from '@/domains/auth/assemblies/default'
```
Он не импортирует factory или adapter напрямую.
## Framework state
Старый `auth/stores` не переносится в `api`. React query/store projection принадлежит `auth/react/queries`:
```ts
export const useAuthSessionQuery = () => {
const api = useAuthApi()
return useQuery({
queryKey: ['auth', 'session'],
queryFn: api.getSession,
})
}
```
При Vue или другом framework та же модель и errors Domain API материализуются его собственными средствами.
## Realtime
`identity-realtime` скрывает socket protocol, operation IDs, acknowledgements и reconnect. Domain API возвращает обычный command outcome и публикует проверенные Auth events.
Если disconnect произошёл до acknowledgement, API не утверждает ложный отказ и может вернуть `AUTH_OPERATION_OUTCOME_UNKNOWN`. После gap binding получает `RESYNC_REQUIRED` и повторно вызывает `getSession()`.
## RSC
Server Component создаёт request-scoped Auth graph и передаёт Client Component только сериализуемый `AuthSession` или hydration payload. Client Component создаёт отдельный client graph; при SSR его render должен быть совместим с server prerender, а browser-only capabilities остаются в deferred effects.
`assemblies/default` используется в обоих местах только если её executable graph действительно совместим со всеми declared conditions. Иначе появляется отдельная assembly, например `auth/assemblies/rsc`.
## Cross-domain graph
Если User package зависит от Auth, он импортирует только type contract:
```ts
import type {
AuthSessionApi,
} from '@/domains/auth/api'
```
User assembly принимает готовый API:
```ts
const auth = createAuth()
const user = createUser({
auth: auth.session,
})
```
User не импортирует Auth factory, port, adapter, assembly или React hooks. Если User остаётся модулем Level 1, его public API должен иметь явную точку передачи нужного Auth behavior.
## Порядок перехода
1. Зафиксировать consumers, external sources, state, errors и lifecycle исходного Auth module.
2. Объявить consumer-facing Domain API и public models.
3. Объявить dependency ports, records и closed failures.
4. Реализовать factory и проверить Domain API через fake ports.
5. Оформить каждую production implementation модулем `adapters/*` и добавить contract tests.
6. Создать `assemblies/default` для штатного production context.
7. Добавить специальные assemblies только для реально отличающихся graphs.
8. Перенести framework state, cache и hydration в modules Group `react`.
9. Перенести страницы, redirects и multi-domain UI в `compositions`.
10. Перевести внешние imports на разрешённые public paths.
11. Обновить dependency-connected graph owners и cross-domain inputs.
12. Удалить старый root `index.ts` Auth и объявить package checker-у.
Завершённость перехода определяется одной формой Auth и отсутствием обходных imports. Наличие других доменных модулей Level 1 не является миграционным долгом.

View File

@@ -1,260 +0,0 @@
# Модуль api и Domain API
> Пояснение семантического шлюза домена, его публичных фасетов, моделей, операций и ошибок.
## Связанные правила
- [`SLM-L2-API-R005`](../../rules/level-2.md#slm-l2-api-r005)
- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-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-API-R018`](../../rules/level-2.md#slm-l2-api-r018)
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024)
- [`SLM-L2-API-R025`](../../rules/level-2.md#slm-l2-api-r025)
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
## Роль
`api` является обязательным SLM-модулем доменного пакета. Для прикладного consumer предметная область доступна только через объявленные им Domain API, public models, outcomes и errors.
Модуль `api` владеет:
- именованными Domain API;
- публичными командами, запросами и подписками;
- public domain models;
- validation внешних и port values;
- семантикой outcomes и expected errors;
- dependency ports и port failures;
- одной фабрикой для каждого Domain API;
- необходимыми consumers deterministic guards и pure-функциями.
Модуль не владеет framework store, query cache, hydration runtime, SDK, transport client или production adapter. Он может координировать одну операцию и замыкать переданные ports, но не хранит скрытую mutable projection данных приложения между вызовами.
## Domain API как шлюз
```text
consumer command
→ Domain API
→ dependency port
→ adapter
→ provider
provider record/failure
→ adapter mapping
→ port record/failure
→ Domain API validation and semantics
→ public model/outcome/error
→ consumer
```
Framework hook, store или composition не импортирует concrete SDK и не читает предметный внешний источник напрямую. Это позволяет менять endpoint, provider и transport, сохраняя публичный контракт, пока не изменилась продуктовая семантика.
Domain API не обязан скрывать реальное предметное изменение. Если backend изменил правило, которое влияет на публичный outcome приложения, контракт домена пересматривается явно.
## Публичные фасеты
Один логический публичный API модуля `api` разделён по аудиториям.
### Consumer types
Корневой `api/index.ts` экспортирует только типы, необходимые прикладным consumers:
```ts
export type {
AuthError,
AuthErrorCode,
AuthSession,
AuthSessionApi,
RequestPhoneOtpCommand,
VerifyPhoneOtpCommand,
} from './types'
```
```ts
import type {
AuthSession,
AuthSessionApi,
} from '@/domains/auth/api'
```
Port contracts, factory dependencies, provider records и technical failures не входят в consumer-facing barrel.
### Implementer types
`api/ports.ts` существует только при наличии dependency ports и экспортирует implementer-facing contracts:
```ts
export type {
AuthIdentityPort,
AuthIdentityPortFailure,
AuthIdentityRecord,
AuthSessionApiDependencies,
} from './ports'
```
```ts
import type {
AuthIdentityPort,
} from '@/domains/auth/api/ports'
```
Этим фасетом пользуются adapters своего домена, assemblies и tests. Прикладной consumer не строит поведение по port records или failures.
### Factory entry
`api/factory.ts` экспортирует только именованные runtime-фабрики:
```ts
export {
createAuthAdministrationApi,
createAuthSessionApi,
} from './factories'
```
```ts
import {
createAuthSessionApi,
} from '@/domains/auth/api/factory'
```
В production этот фасет импортируют только assemblies текущего домена. API-тесты используют его с fake ports.
### Runtime entry
Необязательный `api/runtime.ts` экспортирует только публичный детерминированный runtime:
```ts
export {
AUTH_ERROR_CODES,
isAuthError,
projectSessionEvent,
} from './runtime'
```
Здесь допустимы error codes и guards, validators, value constructors, pure transitions, reconciliation functions и immutable-константы. Фасет не содержит фабрики, API instances, ports, I/O, subscriptions, mutable state или environment-specific код.
Если runtime-потребителей нет, файл не создаётся. Другие внешние пути внутри `api` являются deep imports.
## Stateless runtime boundary
Domain API управляет смыслом данных, а не способом их materialization. Query и command возвращают public values или outcomes, которые framework binding может сохранить в TanStack Query, Zustand, Pinia или другом runtime:
```ts
export type AuthSessionApi = {
getSession: () => Promise<AuthSession>
requestPhoneOtp: (
command: RequestPhoneOtpCommand,
) => Promise<RequestPhoneOtpOutcome>
verifyPhoneOtp: (
command: VerifyPhoneOtpCommand,
) => Promise<AuthSession>
signOut: () => Promise<void>
}
```
API не экспортирует `getState`, mutable store, QueryClient или framework subscription. Operation-local correlation, cancellation и validation допустимы; canonical cache приложения остаётся у framework consumer.
Если клиентский workflow имеет предметное состояние, framework хранит readonly value, а API определяет переход:
```ts
const nextCheckout = checkoutApi.applyCommand(
currentCheckout,
command,
)
```
Или consumer использует pure-функцию `api/runtime`. Framework не применяет предметный merge самостоятельно.
## Несколько Domain API
```ts
export type AuthSessionApi = {
getSession: () => Promise<AuthSession>
signIn: (command: SignInCommand) => Promise<AuthSession>
signOut: () => Promise<void>
}
export type AuthAdministrationApi = {
revokeUserSessions: (
command: RevokeUserSessionsCommand,
) => Promise<void>
}
```
`AuthSessionApi` и `AuthAdministrationApi` могут иметь разные ports, trust boundaries и assemblies. Один публичный сценарий принадлежит ровно одному API.
Разделение не используется только ради файловой декомпозиции. Если APIs не могут быть созданы независимо из-за общей atomicity, состояния или lifecycle, они объединяются либо получают один явно созданный shared capability через assembly.
Assembly возвращает именованный граф готовых контрактов:
```ts
export type AuthGraph = Readonly<{
session: AuthSessionApi
}>
```
Такой граф сообщает доступный набор API, но не является новым предметным API.
## Errors и failure algebra
Ожидаемая публичная ошибка имеет устойчивую readonly сериализуемую форму:
```ts
export type AuthErrorCode =
| 'AUTH_IDENTITY_INVALID'
| 'AUTH_RATE_LIMITED'
| 'AUTH_SERVICE_UNAVAILABLE'
export type AuthError = Readonly<{
code: AuthErrorCode
}>
```
Внешний failure проходит две границы:
```text
provider error
→ adapter
→ closed port failure
→ api
→ stable domain error
```
Например, adapter переводит HTTP `429`, SDK class или socket error frame в `AuthIdentityPortFailure` с типом `RATE_LIMITED`. Domain API решает, что публичная операция завершается `AUTH_RATE_LIMITED`.
Port failure не содержит raw provider object в публично доступной форме. Domain error не включает status, SDK class, source message, payload или `cause`. Диагностические данные остаются в observability-механизме adapter или infra.
Cancellation и `OUTCOME_UNKNOWN` не объединяются с обычным failure, если приложение должно различать их. Ошибка программирования и нарушенный внутренний инвариант не маскируются под expected domain error.
Выбор exception или discriminated `Result` остаётся policy проекта. Архитектурная цепочка provider failure → port failure → domain error не зависит от канала передачи.
## Недетерминизм
Clock, timer, random, ID generator и environment передаются как dependency ports:
```ts
export type AuthRuntimePort = {
now: () => number
createId: () => string
}
```
Модуль `api` не читает `Date.now`, `Math.random`, env или platform globals напрямую, если они влияют на результат операции. Это сохраняет детерминированность API-тестов и явную environment boundary.
## Потребители фасетов
| Потребитель | `api` | `api/ports` | `api/factory` | `api/runtime` |
|---|---|---|---|---|
| Adapter своего домена | Нет | Type-only | Нет | Нет |
| Assembly своего домена | Type-only | Type-only | Да | При необходимости |
| Framework binding своего домена | Type-only | Нет | Нет | При необходимости |
| `composition` или `app` | Type-only | Нет | Нет | При необходимости |
| Код другого домена | Type-only | Нет | Нет | При необходимости |
| API-тест | Type-only | Type-only | Да | По тестируемой границе |
Прикладной production graph создаётся assemblies. `app`, compositions и framework bindings не импортируют factory или concrete adapters.

View File

@@ -1,119 +0,0 @@
# Граница доменного пакета
> Пояснение контейнерной сущности Level 2 и её совместного использования с доменными модулями Level 1.
## Связанные правила
- [`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-API-R005`](../../rules/level-2.md#slm-l2-api-r005)
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
## Предметная граница
Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать Domain API авторизации, production adapters её providers, assemblies и React bindings, но не страницу профиля, общий database client или multi-domain navigation policy.
Пакет не владеет исполняемой ответственностью. Domain API, adapters, production graph, framework projection и lifecycle принадлежат конкретным модулям внутри него.
Level 2 применяется к пакету, а не ко всему SLM root. Например, `auth` может быть пакетом Level 2, пока `catalog` и `news` остаются доменными модулями Level 1.
## Корень пакета
```text
domains/auth/
├── README.md
├── api/
├── adapters/
├── assemblies/
└── react/
```
В корне разрешены:
- документация;
- ownership metadata;
- декларативный manifest архитектурной проверки;
- объявления environment capability sets;
- обязательный модуль `api`;
- обязательная непустая Group `assemblies` с модулем `default`;
- непустая Group `adapters`, если хотя бы одна фабрика имеет dependency port;
- Framework Groups при наличии соответствующих модулей.
В корне запрещены:
- `index.ts` или другой агрегирующий executable entry point;
- runtime-файлы и side effects;
- изменяемое состояние и lifecycle resources;
- реэкспорт API внутренних модулей;
- page-specific компоненты или сборка нескольких доменов.
Metadata содержит только статические данные. Проверяющий инструмент может читать её декларативно, но она не исполняется и не становится скрытым API пакета.
## Policy boundary
Доменный пакет не является узлом import-графа. Его техническая граница состоит из объявленного проверке набора дочерних модулей и правил, применяемых ко всем связям через эту границу.
Отсутствие root barrel намеренно:
- client, server, RSC и worker entry points не агрегируются в один импорт;
- каждый модуль сохраняет отдельные ответственность и environment boundary;
- concrete adapters не становятся частью Domain API;
- Groups не превращаются в скрытые modules;
- versioning publishable package остаётся за пределами Level 2.
## Модули и Groups
```text
auth/
├── api/ # SLM-модуль
│ ├── index.ts # Consumer-facing types
│ ├── ports.ts # Implementer-facing types
│ ├── factory.ts # Runtime factories
│ └── runtime.ts # Необязательный deterministic runtime
├── adapters/ # Group при наличии ports
│ ├── identity-rest/ # SLM-модуль
│ └── identity-realtime/ # SLM-модуль
├── assemblies/ # Обязательная Group
│ ├── default/ # Обязательный SLM-модуль
│ └── administration/ # Дополнительный SLM-модуль
└── react/ # Framework Group
├── session/ # SLM-модуль
└── queries/ # SLM-модуль
```
Groups не имеют `index.ts`. Публичными путями являются `auth/api`, `auth/api/ports`, `auth/api/factory`, опциональный `auth/api/runtime`, `auth/adapters/identity-rest`, `auth/assemblies/default` и `auth/react/session`, но не `auth`, `auth/adapters`, `auth/assemblies` или `auth/react`.
## Навигационные Groups
Слой `domains` может содержать Groups с обеими формами домена:
```text
domains/
└── commerce/ # Навигационная Group
├── catalog/ # Доменный модуль Level 1
└── orders/ # Доменный пакет Level 2
```
Такая Group отличается от Group внутри пакета допустимым составом: она содержит доменные модули, доменные пакеты и другие navigation Groups, а не внутренние модули пакета.
Одна предметная область не представляется одновременно модулем и пакетом. Переход завершается удалением старой границы выбранного домена, но требует обновить все его входящие imports и production composition roots.
## Границы соседних слоёв
| Ответственность | Владелец |
|---|---|
| Публичные модели, Domain API, validation и domain errors | `api` |
| Контракт external capability | `api/ports` |
| Production-реализация dependency port | Adapter внутри пакета |
| Штатный production-граф | `assemblies/default` |
| Специальный production-граф | Дополнительная assembly |
| Domain-specific framework state, cache и bindings | Модуль внутри `react`, `vue` и аналогичной Group |
| Универсальный технический сервис | `infra` |
| Страница, маршрут, redirect, продуктовый текст | `compositions` |
| UI, объединяющий несколько доменов | `compositions` |
Зависимость от React, WebSocket или SDK сама по себе не определяет владельца. Решающими остаются предметная ответственность, направление dependency inversion и публичная граница.

View File

@@ -1,235 +0,0 @@
# Фабрики, ports и adapters
> Пояснение dependency inversion между Domain API и внешними runtime-возможностями.
## Связанные правила
- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-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)
- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018)
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024)
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
## Одна фабрика на Domain API
```text
явные ports + cross-domain APIs + factory → один Domain API
```
Модуль `api` предоставляет одну именованную фабрику для каждого объявленного Domain API:
```ts
import type {
AuthSessionApi,
} from '@/domains/auth/api'
import type {
AuthIdentityPort,
AuthRuntimePort,
} from '@/domains/auth/api/ports'
export type AuthSessionApiDependencies = Readonly<{
identity: AuthIdentityPort
runtime: AuthRuntimePort
}>
export type AuthSessionApiFactory = (
dependencies: AuthSessionApiDependencies,
) => AuthSessionApi
```
```ts
import {
createAuthSessionApi,
} from '@/domains/auth/api/factory'
```
Фабрика не выбирает environment, provider, adapter или assembly. Она не открывает connection, не запускает subscription и не создаёт framework state. Разные Domain API могут иметь разные dependency sets и собираться независимо.
## Consumer-owned ports
Port описывает capability с позиции модуля `api`, а не повторяет конкретный provider:
```ts
export type AuthIdentityRecord = Readonly<{
expiresAt: number
subject: string
}>
export type AuthIdentityPortFailure =
| Readonly<{ type: 'FORBIDDEN' }>
| Readonly<{ type: 'RATE_LIMITED' }>
| Readonly<{ type: 'UNAVAILABLE' }>
export type AuthIdentityPortResult =
| Readonly<{
ok: true
value: AuthIdentityRecord
}>
| Readonly<{
ok: false
failure: AuthIdentityPortFailure
}>
export type AuthIdentityPort = {
signIn: (
command: AuthIdentityPortCommand,
) => Promise<AuthIdentityPortResult>
}
```
Port не экспортирует generated DTO, SDK error class, HTTP status или concrete client. `AuthIdentityRecord` не становится `AuthSession`: модуль `api` проверяет record и создаёт публичную модель.
Не каждый port обязан использовать `Result`. Exception, callback или async iterable допустимы при project policy, если expected failures, cancellation, outcome uncertainty и cleanup остаются типизированными и проверяемыми.
## Гранулярность ports
Port соответствует связной capability, а не каждому endpoint и не всему SDK:
```text
AuthIdentityPort
├── requestCode
├── verifyCode
└── revokeSession
```
Допустимо разделить capability, если операции имеют разные trust boundaries, lifecycle или providers. Запрещено создавать десятки pass-through ports только ради зеркала transport operations.
Clock, timer, random, ID generator и environment также являются ports, если влияют на результат Domain API. Materialized framework state и query cache ports не являются: они принадлежат framework binding.
## Failure algebra
Expected failure проходит две явные стадии:
```text
provider-specific failure
→ adapter mapping
→ closed port failure
→ api mapping
→ stable domain error or outcome
```
Port failure должен сохранять различия, которые нужны Domain API. Если adapter сводит `FORBIDDEN`, `CONFLICT` и `UNAVAILABLE` к `unknown`, API не может выбрать корректную публичную семантику. Если adapter передаёт HTTP status или SDK error, concrete provider протекает внутрь API.
Unexpected exception не обязана превращаться в expected failure. Cancellation объявляется отдельно от failure, если caller управляет ею. Disconnect или timeout после отправки неидемпотентной команды может означать `OUTCOME_UNKNOWN`, а не доказанный отказ.
## Adapter module
Adapter соединяет port с concrete provider:
```text
api-owned port ← adapter → SDK / REST / storage / platform / realtime
```
```ts
import type {
AuthIdentityPort,
} from '@/domains/auth/api/ports'
export const createAuthRestAdapter = (
client: IdentityClient,
): AuthIdentityPort => ({
async signIn(command) {
try {
const response = await client.signIn({
login: command.identifier,
password: command.secret,
})
return {
ok: true,
value: {
expiresAt: response.expires_at,
subject: response.user_id,
},
}
} catch (error) {
return mapIdentityProviderFailure(error)
}
},
})
```
Adapter преобразует protocol arguments, records и expected failures, но не решает, какой `AuthError` получит приложение, не добавляет предметный fallback и не объявляет метод Domain API.
## Размещение adapters
Каждая связная production-реализация является отдельным SLM-модулем Group `adapters`:
```text
auth/adapters/
├── identity-rest/
│ └── index.ts
├── identity-realtime/
│ └── index.ts
└── session-cookie/
└── index.ts
```
Один adapter-модуль может реализовать несколько тесно связанных ports одного provider. Group `adapters` не имеет `index.ts` и не реэкспортирует дочерние модули.
Production adapter запрещено определять:
- внутри `api`;
- закрытым сегментом assembly;
- inline-функцией в `app` или composition;
- частью framework binding;
- mutable registry или service locator.
Concrete adapters в production импортируют только assemblies своего домена. Adapter tests импортируют соответствующий module напрямую.
## Универсальный infra service
Adapter может использовать публичный API `infra`, если concrete technical service является универсальным для приложения:
```text
auth adapter
→ infra/http-client
→ external identity provider
```
Совпадение сигнатур `infra` API и port не переносит ownership port в `infra`. Adapter остаётся явной границей provider mapping, failures и environment. Он может быть тонким, но не добавляет фиктивные преобразования ради объёма кода.
## Cross-domain API dependency
Готовый API другого домена не является technical port:
```ts
import type {
AuthSessionApi,
} from '@/domains/auth/api'
export type UserProfileApiDependencies = Readonly<{
auth: Pick<AuthSessionApi, 'getSession'>
profile: UserProfilePort
}>
```
Graph owner создаёт Auth раньше User и передаёт `auth.session` в User assembly. User не объявляет structural copy чужого API и не создаёт bridge adapter без реального преобразования контракта.
Если expected Auth failure становится публичным outcome User, User API преобразует его в собственную `UserError`. При exception-модели он может использовать публичный guard из `auth/api/runtime`.
## Framework-only SDK
Некоторые SDK доступны только как framework Provider, hook или component, например CAPTCHA или payment element. Framework binding может получить opaque token или operation input через такой SDK и передать его команде Domain API:
```text
framework SDK
→ opaque token
→ Domain API command
→ port
→ provider adapter
```
Binding не вызывает предметную provider operation напрямую, SDK type не входит в public Domain API, а generic technical UI при необходимости разделяется между `infra`, `ui` и composition.
## Tests и fake ports
Локальные fake implementations в API-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри test boundary и позволяют детерминированно задавать records, failures, cancellation и realtime события.
Adapter contract tests отдельно доказывают, что concrete provider действительно реализует port. API-тест с идеальным fake не заменяет эту проверку.

View File

@@ -1,211 +0,0 @@
# Framework Groups и модули
> Пояснение domain-specific framework-кода, materialized state и RSC boundaries на примере React.
## Связанные правила
- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
- [`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)
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
## Framework Group
Папка для domain-specific React binding modules называется `react`:
```text
domains/auth/react/ # Framework Group
├── session/ # SLM-модуль
│ ├── hooks/
│ ├── providers/
│ └── index.ts
├── queries/ # SLM-модуль
│ └── index.ts
└── login-form/ # SLM-модуль
├── components/
└── index.ts
```
`react` является Group, а не модулем. У неё нет `index.ts`, реализации, state, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа.
## Framework binding module
Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании готового Domain API своего домена с конкретным framework.
Framework binding может:
- передавать готовый API через Provider и context;
- предоставлять domain-specific hooks;
- хранить framework projection в query cache или store;
- отображать public models, outcomes и domain errors;
- реализовывать SSR prefetch и client hydration;
- реализовывать переиспользуемую domain-specific форму или guard;
- связывать framework lifecycle с явной realtime subscription.
Он не вызывает `api/factory` или assembly, не выбирает adapters, не импортирует SDK предметного external source и не определяет новые предметные операции.
Framework binding импортирует consumer types и deterministic runtime через разные фасеты:
```ts
import type {
AuthError,
AuthSessionApi,
} from '@/domains/auth/api'
import {
isAuthError,
} from '@/domains/auth/api/runtime'
```
Импорты `api/ports`, `api/factory` и `adapters/*` запрещены.
## Готовый API
`auth/react/session` может владеть Provider для уже созданного `AuthSessionApi`:
```tsx
'use client'
type AuthSessionProviderProps = PropsWithChildren<{
api: AuthSessionApi
}>
export const AuthSessionProvider = ({
api,
children,
}: AuthSessionProviderProps) => {
return (
<AuthSessionContext.Provider value={api}>
{children}
</AuthSessionContext.Provider>
)
}
```
Публичный путь модуля:
```ts
import {
AuthSessionProvider,
useAuthApi,
} from '@/domains/auth/react/session'
```
Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API.
## Query и store projection
`auth/react/queries` может использовать TanStack Query, SWR, Zustand или другой React runtime поверх готового API:
```ts
export const useAuthSessionQuery = () => {
const api = useAuthApi()
return useQuery({
queryKey: ['auth', 'session'],
queryFn: api.getSession,
})
}
```
Query keys, stale time, pending status и hydration принадлежат binding. Значения и ошибки поступают через Domain API. Framework types не становятся частью `AuthSessionApi`.
Framework projection не импортируется другим доменом. Cross-domain UI собирается в `compositions`.
## Realtime binding
Binding может запускать subscription готового API в framework lifecycle:
```text
component/provider scope
→ Domain API subscribe
→ verified domain events
→ query invalidation or API-owned projection
→ cleanup on scope end
```
Binding не импортирует WebSocket client и не разбирает frames. После cleanup он не принимает late callbacks. Если reconnect создаёт gap, binding обрабатывает публичный `RESYNC_REQUIRED` outcome и повторно загружает snapshot через Domain API.
## Domain-specific UI
`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с Auth API, public models и errors своего домена. Она может использовать публичный API соседнего `auth/react/session`, если статический граф остаётся ацикличным.
Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Domain API может вернуть `AUTH_REQUIRED`, но переход на `/login` выбирает composition.
## Framework-only SDK
SDK, доступный только через Provider, hook или component, может использоваться binding для получения opaque operation input:
```text
CAPTCHA React component
→ opaque token
→ AuthApi command
```
Binding не использует SDK для самостоятельной предметной операции, не превращает SDK response в public domain model и не экспортирует SDK type через Domain API. Если SDK предоставляет reusable technical UI без предметной модели, его generic integration может принадлежать `infra` и `ui`, а composition связывает её с доменом.
## Запрет cross-domain framework imports
Framework binding module не импортирует hooks, contexts, Providers, stores или components другого домена:
```ts
// Недопустимо: domains/user/react/profile
import {
useAuthSessionQuery,
} from '@/domains/auth/react/queries'
```
Cross-domain UI собирается в `compositions`:
```tsx
const session = useAuthSessionQuery()
return (
<UserProfile
userId={session.data?.userId}
/>
)
```
Если User Domain API постоянно нуждается в Auth, готовый `AuthSessionApi` передаётся User assembly при сборке runtime-графа. User framework binding работает уже со своим API.
## SSR, RSC и client boundary
Server prefetch и client hooks могут принадлежать разным modules Framework Group с совместимыми entry points. Они не разделяют API instance или mutable cache:
```text
server binding
→ server API instance
→ prefetch
→ hydration payload
client binding
→ client API instance
→ hydrate
→ rendering
```
Server Component не передаёт API object в Client Component. Client reference и Server Action reference объявляются checker-у отдельно от executable imports. Если Client Component участвует в SSR или prerender, его server render graph проверяется отдельно от browser hydration graph; browser-only capability используется только через объявленную framework-deferred boundary.
## Публичные API
```ts
import {
AuthSessionProvider,
} from '@/domains/auth/react/session'
import {
useAuthSessionQuery,
} from '@/domains/auth/react/queries'
import {
LoginForm,
} from '@/domains/auth/react/login-form'
```
Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена.

View File

@@ -1,75 +0,0 @@
# Открытые вопросы Level 2
> Эти вопросы не являются правилами и не отменяют зафиксированные границы.
## Зафиксированные решения
- Level 1 является общей базой, а Level 2 применяется отдельно к выбранным предметным областям.
- Доменный модуль Level 1 и доменный пакет Level 2 могут постоянно сосуществовать в одном SLM root.
- Одна предметная область имеет только одну форму.
- Корень package содержит только metadata, модуль `api` и допустимые Groups и не имеет executable API.
- Модуль `api` является единственным семантическим шлюзом данных и операций домена.
- Публичные фасеты разделяют consumer types, implementer ports, factories и optional deterministic runtime.
- Каждый Domain API имеет одну factory; production factories импортируют только assemblies своего домена.
- Dependency ports принадлежат `api`, а production adapters являются отдельными modules Group `adapters`.
- Provider errors проходят через closed port failures и преобразуются в stable domain errors.
- Каждый package содержит `assemblies/default` для одного baseline production context.
- Имя `default` не определяет environment или isomorphic compatibility.
- Дополнительная assembly появляется только для отличающегося graph, dependencies, trust, capabilities или lifecycle.
- Framework bindings владеют state, cache, reactivity и hydration и не обращаются к предметному external source в обход Domain API.
- Server и client используют разные API instances и caches; через RSC boundary проходят только serializable values.
- Realtime transport скрыт adapter, а messages и subscriptions доступны через Domain API.
- Realtime port объявляет correlation, ACK, ordering, duplicate, reconnect, resync, cancellation и cleanup semantics.
- Assembly rollback выполняет cleanup собственных resources и полученных adapter lifecycle handles; successful aggregate cleanup идемпотентен и прекращает callbacks.
- Cross-domain Domain API является отдельной runtime dependency, а не автоматически local port.
- Runtime assembly graph остаётся ацикличным.
## Канал ошибок
Нужно выбрать project-wide recommendation между exceptions и discriminated `Result`, определить форму cancellation и unexpected failures, а также сериализацию domain errors через RPC и Server Actions.
Архитектурная цепочка provider failure → port failure → domain error от выбора канала не зависит.
## Port semantics
Нужно определить минимальный machine-readable способ объявлять behavioral guarantees ports: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
Не все ports требуют все поля, но существенная для корректности semantics не должна существовать только в комментарии adapter implementation.
## Environment metadata
Нужно выбрать формат для capability sets, resolver conditions, executable edges, framework reference edges, dynamic imports и API-safe package declarations.
Особенно требуется проверить Next.js RSC, Server Actions, edge runtime, workers и conditional exports внешних packages.
## Runtime dependency graph
Нужно выбрать machine-readable формат assembly inputs и создаваемых API, чтобы автоматически обнаруживать runtime cycles, скрытые static structural ports и неверный cleanup order.
До появления формата runtime graph остаётся обязательной review boundary.
## Lifecycle
Гарантии rollback, reverse cleanup, idempotence и отсутствия callbacks после disposal зафиксированы. Ещё нужно определить aggregate cleanup errors, retry failed cleanup, request abort, deadline disposal и поведение API после завершения scope.
## Hydration payload
Нужно выбрать рекомендации по versioning, schema validation, stale persisted cache, partial hydration и защите request-specific или sensitive values.
Hydration payload остаётся framework-owned и не может содержать API instance или mutable client.
## Multiple APIs и shared capabilities
Нужно проверить рекомендуемую форму для нескольких Domain API, которые используют один shared connection, transaction coordinator или framework-neutral operation context, не перенося предметную семантику в adapter или assembly.
Если independent factories не сохраняют atomicity, APIs должны объединяться; точный критерий требует дополнительных примеров.
## Framework-only SDK
Нужно проверить React/Vue SDK, которые предоставляют capability только через Provider, hook или component: payment elements, CAPTCHA, maps и identity widgets.
Зафиксировано, что binding может передать Domain API только opaque operation input и не выполняет предметную provider operation напрямую. Требуются проверочные примеры для `infra` + `ui` + composition.
## Масштаб production graph
Нужно проверить lazy и route-scoped сборку на SLM root с десятками Level 2 packages. Импорт assemblies остаётся side-effect-free, а graph owner создаёт только dependency-connected часть graph; конкретный registry или lazy-loading mechanism пока не нормирован.

View File

@@ -1,217 +0,0 @@
# Realtime messages и subscriptions
> Пояснение Domain API поверх WebSocket, SSE, GraphQL subscriptions и provider SDK.
## Связанные правила
- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
- [`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-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
- [`SLM-L2-REALTIME-R029`](../../rules/level-2.md#slm-l2-realtime-r029)
## Граница транспорта
Realtime transport находится внутри adapter:
```text
WebSocket / SSE / GraphQL / SDK
→ adapter
→ realtime port
→ Domain API
→ domain event/outcome/error
→ framework projection
```
Domain API не экспортирует `WebSocket`, `MessageEvent`, raw frames, SDK subscription, provider error или transport close code. Port также не должен быть generic socket API с `send(frame)` и `onMessage(frame)`: он описывает capability, необходимую конкретному домену.
## Realtime-команда
Публичная команда может выглядеть как обычный Promise независимо от транспорта:
```ts
export type ChatApi = {
sendMessage: (
command: SendMessageCommand,
) => Promise<ChatMessage>
}
```
Port возвращает типизированный technical outcome:
```ts
export type SendMessagePortFailure =
| Readonly<{ type: 'FORBIDDEN' }>
| Readonly<{ type: 'RATE_LIMITED' }>
| Readonly<{ type: 'UNAVAILABLE' }>
| Readonly<{ type: 'OUTCOME_UNKNOWN' }>
export type ChatRealtimePort = {
sendMessage: (
command: SendMessagePortCommand,
) => Promise<PortResult<ChatMessageRecord, SendMessagePortFailure>>
}
```
Domain API преобразует port result в `ChatMessage` или собственную `ChatError`. Для прикладного consumer transport остаётся незаметным.
## Correlation
`socket.send()` подтверждает только локальную отправку frame. Чтобы завершить `sendMessage()` результатом server command, protocol должен сопоставить command и acknowledgement:
```text
Domain API command
→ adapter assigns operationId
→ transport frame
→ server ACK or ERROR with operationId
→ adapter settles pending port operation
→ Domain API maps outcome
```
Adapter владеет protocol registry pending operations и не бросает error из async `onmessage`, который невозможно поймать вокруг исходного `send`. Он завершает соответствующую Promise или другой объявленный operation channel.
Correlation contract фиксирует:
- источник и scope уникальности operation ID;
- момент, когда команда считается принятой или выполненной;
- поведение при duplicate и late acknowledgement;
- timeout и cancellation;
- очистку pending operation при disconnect;
- связь command outcome с последующими domain events.
Если provider не возвращает correlation metadata, API не обещает индивидуальный результат. Такая операция является fire-and-forget, а поздний отказ публикуется отдельным domain event либо доступна только общая ошибка transport scope.
## Outcome uncertainty и idempotency
Disconnect после отправки и до acknowledgement не доказывает, что command не выполнена:
```text
frame sent
→ connection lost
→ server may have committed command
→ acknowledgement unknown
```
Port возвращает `OUTCOME_UNKNOWN`, если это различие нужно Domain API. Автоматический retry безопасен только при provider guarantee или idempotency key. Domain API не преобразует неопределённый outcome в ложное `MESSAGE_NOT_SENT`.
## Subscription
Публичная subscription предоставляет проверенные events и явный cleanup:
```ts
export type ChatEvent =
| Readonly<{
type: 'MESSAGE_CREATED'
message: ChatMessage
revision: number
}>
| Readonly<{
type: 'MESSAGE_REMOVED'
messageId: string
revision: number
}>
export type ChatSubscription = Readonly<{
close: () => Promise<void>
}>
export type ChatObserver = Readonly<{
onEvent: (event: ChatEvent) => void
onError: (error: ChatRealtimeError) => void
onStatus: (status: ChatRealtimeStatus) => void
}>
export type ChatApi = {
subscribe: (
observer: ChatObserver,
) => Promise<ChatSubscription>
}
```
Callback, async iterable или другой project-wide channel допустимы. Обязательны типизированные domain events/errors, определённый lifecycle и cleanup.
## Stable errors и statuses
Начальная ошибка подключения может завершить `subscribe()` domain error. Ошибка после успешного запуска приходит через stream channel.
Не каждый transport failure становится domain error. Adapter может восстановить соединение и опубликовать только устойчивый status:
```ts
export type ChatRealtimeStatus =
| Readonly<{ type: 'CONNECTED' }>
| Readonly<{ type: 'RECONNECTING' }>
| Readonly<{ type: 'RESYNC_REQUIRED' }>
| Readonly<{ type: 'CLOSED' }>
```
Публичные errors описывают реакции приложения, например `CHAT_REALTIME_UNAVAILABLE`, `CHAT_FORBIDDEN` или `CHAT_SESSION_EXPIRED`. Close codes, provider messages и SDK classes остаются внутри adapter.
Caller-initiated close не является domain error.
## Ordering, duplicates и resync
Realtime port явно объявляет:
- гарантируется ли порядок событий;
- возможна ли at-least-once delivery;
- кто устраняет duplicates;
- содержит ли event revision или sequence;
- как обнаруживается gap после reconnect;
- откуда загружается authoritative snapshot.
Если adapter не может доказать непрерывность, Domain API публикует `RESYNC_REQUIRED`. Framework binding invalidates projection и получает snapshot через query Domain API.
Binding не применяет raw delta к публичной модели. Если безопасный merge содержит предметную семантику, его выполняет операция Domain API или pure-функция `api/runtime`.
## Shared connection
Один adapter может multiplex несколько ports и subscriptions через физическое соединение. Connection имеет явные owner, scope, multiplicity и cleanup:
```text
assembly-owned connection
├── chat messages port
├── presence port
└── notification port
```
Cleanup отдельной subscription снимает её lease. Cleanup assembly закрывает shared connection после завершения всех принадлежащих графу operations. После awaited cleanup новые callbacks запрещены.
Если создание connection завершилось успешно, а следующий шаг assembly упал, connection закрывается на rollback path до возврата ошибки.
## Framework materialization
Framework binding выбирает техническую реакцию на domain event:
```text
MESSAGE_CREATED
→ update query cache verified full model
RESYNC_REQUIRED
→ invalidate query
→ fetch snapshot through Domain API
```
Zustand, QueryClient, Pinia или другой store не импортирует socket adapter и не интерпретирует protocol frame. Он хранит только public values, events, statuses и errors Domain API.
## SSR, RSC и workers
Browser assembly может включать realtime adapter, а request/RSC assembly — только query API. Отсутствующий realtime API не заменяется throwing stub.
Server process или worker получает отдельную assembly и scope, если ему действительно нужна долгоживущая subscription. Server Component не открывает connection, которая переживает request, без отдельного owner вне request scope.
## Тестовые границы
API-тест с fake realtime port проверяет mapping records, failures, stable errors и public events. Adapter contract test проверяет protocol frames, correlation, timeout, disconnect, duplicate acknowledgement, reconnect, resync и cleanup. Framework test проверяет materialization и invalidation. Assembly test проверяет shared connection, rollback и отсутствие callbacks после disposal.
Контрольные случаи:
- acknowledgement приходит после timeout;
- duplicate acknowledgement приходит после reconnect;
- event приходит раньше command acknowledgement;
- disconnect происходит после send и до ACK;
- unsubscribe завершается во время pending callback;
- adapter получает malformed payload;
- следующий resource assembly падает после открытия connection.

View File

@@ -1,172 +0,0 @@
# Состояние, cache и hydration
> Пояснение границы между семантической властью Domain API и framework-owned materialization.
## Связанные правила
- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-a007)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018)
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
## Основная граница
Модуль `api` определяет форму и семантику доменных значений, но не выбирает способ их хранения и реактивной доставки. TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX, Pinia, Signals и RxJS остаются в framework bindings или compositions.
```text
Domain API
→ public model/outcome/event
→ framework projection
→ rendering
```
Concrete state/query runtime не импортируется модулем `api`, не является dependency port фабрики и не входит в публичный Domain API.
## Виды materialization
### Source cache
Технический cache внешнего provider внутри adapter. Он может отвечать за transport deduplication, connection state, provider retry и хранение port records.
Source cache не публикует raw DTO, query keys, mutable client или library result через Domain API. Если adapter создаёт timers, subscriptions или connection, он остаётся единственным владельцем и экспортирует lifecycle handle, который assembly только агрегирует. Если resource создаёт assembly, adapter использует его как borrowed capability и не закрывает самостоятельно.
### Framework projection
State или cache, который framework binding строит из готового Domain API:
```ts
export const useAuthSession = () => {
const api = useAuthApi()
return useQuery({
queryKey: ['auth', 'session'],
queryFn: api.getSession,
})
}
```
Query key, stale time, pending/retry status, Suspense, rendering stale data и техническая invalidation принадлежат binding. `AuthSession` и `AuthError` принадлежат `api`.
### Composition state
Состояние конкретной страницы или multi-domain flow принадлежит composition: выбранная вкладка, открытый modal, draft формы, route transition и координация нескольких API.
Если draft приобретает самостоятельную доменную семантику, Domain API предоставляет validation или transition, но framework по-прежнему хранит возвращаемое readonly value.
## Domain API не является store
Публичный Domain API не экспортирует:
- mutable store;
- `getState` и `setState` framework runtime;
- QueryClient;
- Zustand `StoreApi`;
- framework hook;
- глобальный singleton данных;
- универсальный state port.
API methods возвращают значения и outcomes. Framework consumer решает, как долго их хранить и когда повторно запросить.
Это не означает, что framework определяет предметные transitions. Он материализует только то, что произвёл или проверил API.
## Invalidation и retry
| Политика | Обычный владелец |
|---|---|
| Query key, stale time, deduplication, background refetch | Framework binding |
| Transport retry безопасного запроса | Adapter |
| Rendering stale data, Suspense, polling UI | Framework binding или composition |
| Запрет повторной предметной команды | Domain API |
| Cooldown, лимит попыток, допустимый transition | Domain API |
| Freshness, влияющая на корректность сценария | Domain API через operation contract |
После успешной команды binding может технически invalidировать известные query keys. Если выбор invalidation выражает предметную семантику, Domain API возвращает устойчивый outcome/event, а binding только отображает его на framework cache.
## Optimistic updates
Framework binding не конструирует произвольную публичную модель из form input, raw DTO или текущего cache. Optimistic projection допустима, когда предполагаемое значение:
- возвращено командой Domain API;
- создано отдельной операцией Domain API;
- создано или проверено pure-функцией `api/runtime`.
```ts
import {
projectProfileUpdate,
} from '@/domains/user/api/runtime'
const optimisticProfile = projectProfileUpdate(
currentProfile,
command,
)
queryClient.setQueryData(profileKey, optimisticProfile)
```
`projectProfileUpdate` владеет предметным transition, а `setQueryData` остаётся framework operation.
## Concurrent mutations и realtime
При нескольких optimistic commands и realtime events binding не выбирает самостоятельно ordering, versioning, rollback или rebase. Domain API возвращает correlation/version metadata либо предоставляет deterministic reconciliation:
```ts
const nextProjection = reconcileProfile({
current,
event,
pendingCommands,
})
```
Если API не объявляет безопасный merge, binding invalidates cache и получает authoritative snapshot через Domain API. Это предпочтительнее скрытого применения неполного delta.
## Persistence
Framework cache может технически сохраняться между reloads, но persisted value не становится источником предметной истины. После восстановления значение:
- используется как stale projection до revalidation;
- либо проверяется публичным validator `api/runtime`;
- либо отбрасывается и загружается через Domain API.
Если storage является самостоятельным предметным внешним источником, доступ к нему оформляется dependency port и adapter. Автоматический framework middleware не обходит API validation и transitions.
## SSR и hydration
Server и client имеют разные API instances и caches:
```text
server request
→ request assembly
→ server Domain API
→ server framework cache
→ serializable hydration payload
browser
→ client assembly
→ client Domain API
→ hydrated client cache
```
Hydration payload принадлежит framework binding и содержит только public domain values и framework metadata. API object, functions, ports, adapters, mutable clients и request secrets не сериализуются.
Server cache создаётся на каждый request и не хранится в module singleton. Client cache создаётся на согласованный application или route scope.
## RSC и Server Actions
Server Component вызывает server Domain API и передаёт Client Component только сериализуемые values или hydration payload. Client Component создаёт или получает отдельный client API instance; при SSR его render отдельно проверяется в server prerender graph до browser hydration.
Server Action создаёт request-scoped production graph на каждый вызов, выполняет Domain API command и гарантированно выполняет все cleanup obligations графа. Client invocation Server Action является framework reference edge, а не передачей server API в browser.
## Проверка на ревью
Для каждого state/query runtime определяется:
- является ли он source cache, framework projection или composition state;
- откуда поступают public domain values;
- кто определяет validation и transition;
- где находятся library-specific types и keys;
- как invalidation связана с Domain API outcomes;
- как обрабатываются optimistic concurrency и realtime events;
- что сериализуется при SSR/RSC;
- соответствует ли cache scope области жизни API graph.

View File

@@ -1,189 +0,0 @@
# Тестирование доменного пакета
> Проверка Domain API, port contracts, production wiring и framework projections Level 2.
## Связанные правила
- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016)
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024)
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
- [`SLM-L2-REALTIME-R029`](../../rules/level-2.md#slm-l2-realtime-r029)
- [`SLM-L2-ASSEMBLY-R030`](../../rules/level-2.md#slm-l2-assembly-r030)
## Размещение
Тест находится рядом с module-owner проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`.
| Проверяемая граница | Владелец теста |
|---|---|
| Domain API operations, models, outcomes и errors | `api` через factory |
| Deterministic runtime и guards | `api` |
| Реализация dependency port | Adapter |
| Default и специальный production graph | Assembly |
| Provider, hook, query/store integration и hydration | Framework binding |
| Cross-domain graph | Graph owner |
## Domain API через фабрику
Каждый публичный сценарий проверяется через фабрику владеющего им Domain API с управляемыми fake ports:
```ts
import type {
AuthSessionApi,
} from '@/domains/auth/api'
import type {
AuthIdentityPort,
} from '@/domains/auth/api/ports'
import {
createAuthSessionApi,
} from '@/domains/auth/api/factory'
const identity: AuthIdentityPort = createIdentityPortFake({
signIn: {
ok: true,
value: {
expiresAt: 1_700_000_000_000,
subject: 'user-1',
},
},
})
const api: AuthSessionApi = createAuthSessionApi({
identity,
runtime: {
createId: () => 'id-1',
now: () => 1_700_000_000_000,
},
})
```
API suite проверяет:
- public models и outcomes;
- validation commands и port records;
- mapping каждого expected port failure;
- отсутствие raw provider details в domain errors;
- cancellation и outcome uncertainty при наличии;
- pure transitions и reconciliation;
- каждый API отдельно при нескольких factories.
API-тест не использует React, production assembly, реальный SDK, backend, system clock или module singleton.
## Adapter contract test
Adapter test доказывает, что concrete provider реализует port:
- правильно преобразует arguments;
- валидно читает provider record;
- возвращает port record, а не raw DTO;
- различает закрытые port failures;
- не создаёт public domain error;
- соблюдает cancellation, timeout и lifecycle contract;
- использует заявленный environment capability set.
Fake port в API-тесте не заменяет adapter contract test. Идеальный fake может соответствовать типу, пока реальный endpoint или SDK уже изменился.
## Default assembly
Тест `assemblies/default` проверяет:
- вызов только нужных API factories;
- выбор штатных adapter modules;
- точный именованный состав graph;
- объявленный baseline capability set;
- отсутствие module-import side effects;
- передачу cross-domain API аргументом;
- отсутствие factory/adapter leakage наружу;
- aggregate cleanup, если assembly создаёт owned resource или получает adapter lifecycle handle.
Каждая дополнительная assembly тестирует отличие своего production context, а не повторяет полный API suite.
## Partial construction и cleanup
Assembly test моделирует ошибку после регистрации каждого cleanup obligation, включая adapter-owned handle:
```text
resource A created
resource B creation failed
→ cleanup A awaited
→ original failure propagated
```
Проверяются reverse dependency order, idempotent repeated disposal, попытка очистить все resources и отсутствие callbacks после завершившегося cleanup.
Assembly без cleanup obligations не тестирует пустой `dispose`, потому что не обязана его предоставлять. Если adapter передал lifecycle handle, obligation существует независимо от resource ownership.
## Framework binding
Framework test получает fake готового Domain API и проверяет собственную responsibility:
- Provider и hook;
- query keys, stale policy и invalidation;
- store projection;
- optimistic update через API-owned function;
- hydration payload;
- public domain errors;
- subscription cleanup;
- отсутствие direct SDK/external source access.
Framework test не повторяет validation и failure mapping всех API operations.
## Realtime
API realtime test с fake port проверяет public events, stable errors, acknowledgement semantics и `OUTCOME_UNKNOWN` mapping.
Adapter realtime contract test проверяет:
- command correlation;
- duplicate и late acknowledgement;
- disconnect до ACK;
- ordering и sequence gaps;
- reconnect и resync;
- malformed frames;
- cancellation и unsubscribe;
- отсутствие callbacks после cleanup.
Assembly test отдельно проверяет shared connection, multiplexing, rollback и graph-level disposal. Framework test проверяет только materialization events и invalidation.
## SSR, RSC и Server Actions
Environment tests подтверждают:
- request-scoped API и cache не разделяются между users;
- API instance не входит в hydration payload;
- Client Component создаёт отдельный client graph;
- SSR-enabled Client Component проверяется в server prerender и browser hydration graphs;
- browser-only effect не выполняется во время server render;
- Server Action создаёт и очищает graph на каждый вызов;
- framework reference edge не превращается в executable client/server leak;
- `default` проверяется под всеми объявленными resolver conditions.
## Cross-domain graph
Graph owner test создаёт assemblies и construction points модулей Level 1 в dependency order и проверяет runtime inputs и callbacks. Отдельно проверяется невозможность mixed L1/L2 циклической сборки и reverse cleanup order.
Не достаточно проверить только статический import DAG: runtime dependencies, передаваемые arguments, должны быть представлены architecture mapping или review evidence.
## Автоматические структурные проверки
Import и export checks подтверждают:
- отсутствие root API пакета и Groups;
- обязательные `api`, `api/factory` и `assemblies/default`;
- `api/ports` только при наличии declared ports;
- допустимые exports каждого фасета;
- importer matrix factories, ports и concrete adapters;
- отсутствие deep imports;
- отсутствие SDK, framework и state/query runtime в graph `api`;
- отсутствие запрещённых cross-domain imports;
- environment compatibility под configured conditions;
- отсутствие статических cycles.
Runtime tests не заменяют import-graph checks и architecture review.

View File

@@ -1,233 +0,0 @@
# Терминология Level 2
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
Level 2 наследует терминологию и матрицу слоёв Level 1. Он применяется отдельно к предметным областям, которым нужна пакетная форма с контролируемым Domain API, dependency ports, production adapters, штатной assembly и самостоятельными framework bindings.
## Формы домена
### Форма домена
Структурное представление одной доменной ответственности. На Level 1 предметная область представлена доменным модулем. Для предметной области, использующей Level 2, этот модуль заменяется доменным пакетом. Одна предметная область не использует обе формы одновременно.
### Доменный пакет
Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла статического графа зависимостей.
Корень пакета может содержать только декларативную metadata, обязательный модуль `api` и допустимые Groups. Metadata хранит статические данные о пакете, владении, environment capability sets и конфигурации проверки и не содержит кода, выполняемого приложением.
Доменный пакет является policy boundary: проверка использует объявленную принадлежность модулей пакету для контроля структуры, внешних источников и междоменных связей. Публичными dependency boundaries кода остаются модули внутри пакета, а не пакет целиком.
### Навигационная Group слоя `domains`
Group, размещённая непосредственно в слое `domains` или другой такой Group. Она классифицирует доменные модули Level 1, доменные пакеты Level 2 и другие навигационные Groups, но не содержит исполняемый код и не образует dependency boundary.
### Модуль доменного пакета
Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: Domain API, assembly, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел статического графа зависимостей.
Модуль внутри Group пакета не является вложенным модулем, потому что его ближайшая внешняя граница не является модулем.
## Доменный API
### Модуль `api`
Обязательный SLM-модуль `api`, который является семантическим шлюзом предметной области для приложения. Он объявляет публичные модели, один или несколько именованных Domain API, соответствующие фабрики, dependency ports, ожидаемые доменные ошибки и необходимый внешним потребителям детерминированный runtime.
Модуль `api` определяет смысл данных и операций, но не является framework store или query cache. Он не импортирует SDK, transport client, storage implementation, framework, state/query manager или platform I/O. Его экземпляры замыкают переданные ports и могут координировать отдельную операцию, но не служат скрытым изменяемым источником данных приложения между вызовами.
Термин **публичный API модуля `api`** обозначает фасеты SLM-модуля. Термин **Domain API** обозначает именованный runtime-контракт предметных операций. Эти понятия не взаимозаменяемы.
### Domain API
Именованный публичный runtime-контракт связного набора предметных команд, запросов или подписок внутри одного домена. Потребитель вызывает Domain API и получает только публичные модели, outcomes и ошибки предметной области, не зная provider, endpoint, SDK или transport protocol.
Модуль `api` может объявить несколько Domain API, если они независимо собираются, имеют разные ports, trust boundaries или реальные consumers. Каждый публичный сценарий принадлежит ровно одному Domain API. APIs с общим неразделимым состоянием, atomicity или lifecycle образуют один контракт либо получают один явно созданный shared capability через assembly.
### Публичная доменная модель
Readonly-форма данных, которую Domain API принимает или возвращает внешнему потребителю. Публичная доменная модель принадлежит модулю `api`, не является backend DTO, cache record или framework view model и экспортируется только при наличии реального consumer.
Внутренняя модель модуля `api`, port record и framework view model могут иметь другую форму и не становятся публичными только из-за принадлежности тому же домену.
### Семантическая власть Domain API
Право определять публичную доменную модель, validation внешних значений, допустимые предметные transitions, семантику операций, outcomes и ожидаемых ошибок. Adapter, assembly или framework binding может транспортировать, хранить, кэшировать и отображать значения, но не становится независимым источником этих решений.
### Публичные фасеты `api`
Объявленные entry points одного логического публичного API модуля `api`:
| Путь | Статус | Содержимое |
|---|---|---|
| `api` | Обязательный | Только consumer-facing types: Domain API, public models, commands, outcomes и domain errors |
| `api/factory` | Обязательный | Только именованные runtime-фабрики Domain API |
| `api/ports` | При наличии dependency ports | Только implementer-facing types: ports, port records, port failures и factory dependency types |
| `api/runtime` | Необязательный | Только публичные детерминированные runtime-значения и функции |
Фасет `api/runtime` может публиковать устойчивые error codes и guards, validators, value constructors, pure transitions, reconciliation functions, предметные константы и чистые projections, если они нужны реальным внешним потребителям. Он не содержит фабрики, изменяемое состояние, ввод-вывод, подписки, сценарии с runtime-зависимостями или environment-specific код.
Фасеты не являются сегментами, вложенными модулями или самостоятельными узлами графа. Любой другой внешний путь внутрь `api` является deep import.
### API-safe внешний пакет
Внешняя библиотека, допустимая в import-графе модуля `api`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, query runtime, framework и техническая интеграция не становятся API-safe только из-за совместимости с несколькими средами.
### Фабрика Domain API
Публичная функция фасета `api/factory`, которая получает явные dependency ports и cross-domain API dependencies и создаёт экземпляр одного объявленного Domain API. Каждому Domain API соответствует ровно одна публичная фабрика.
Фабрика не выбирает concrete adapter, assembly, environment или framework, не создаёт framework state и не запускает запрос, socket, subscription, timer или другую долгоживущую работу во время создания API.
## Ports, adapters и ошибки
### Dependency port
Consumer-owned контракт runtime-возможности, которая нужна модулю `api` и требует production-реализации. Port определяет минимальные операции, success values, закрытые expected failures и существенные behavioral guarantees со стороны потребителя capability, а не копирует API конкретного provider.
К ports относятся источники данных, external command gateways, storage, platform capabilities, clock, timer, random, ID generator и realtime event sources. Framework state/query manager, materialized cache и готовый API другого домена не являются dependency ports.
Port и связанные implementer-facing types принадлежат модулю `api` и публикуются через type-only фасет `api/ports`. Они не содержат SDK classes, generated DTO, HTTP status, `WebSocket`, framework hooks или другие concrete provider types.
### Port record
Технически нейтральная форма значения на границе port, достаточная модулю `api` для validation и преобразования в публичную доменную модель. Port record принадлежит implementer-facing контракту и не является публичной моделью приложения или raw provider DTO.
### Port failure
Закрытый implementer-facing набор ожидаемых сбоев dependency port, достаточный модулю `api` для выбора собственного outcome или domain error. Adapter преобразует provider-specific failure в port failure; модуль `api` преобразует port failure в публичную семантику.
Cancellation и неопределённый результат операции объявляются отдельно, если потребитель способен различать их. Unexpected programming failure не маскируется под expected port failure.
### Доменная ошибка
Безопасная публичная форма ожидаемого сбоя операции Domain API. Модуль `api` объявляет устойчивый readonly сериализуемый тип с кодом; при необходимости runtime-коды и guards публикуются через `api/runtime`.
Ошибки provider, SDK, транспорта, storage, adapter или другого домена не являются доменными ошибками текущего API. Публичная ошибка не содержит исходные `message`, status, payload, class, stack или `cause`. Способ передачи ошибки, например exception или discriminated `Result`, не изменяет её владельца.
### Cross-domain API dependency
Готовый публичный API доменного модуля Level 1 или Domain API пакета Level 2, необходимый операции текущего Domain API. Это отдельный вид runtime-зависимости, а не dependency port и не adapter. Graph owner создаёт независимый API раньше зависимого и передаёт готовое значение assembly, которая передаёт его фабрике.
Локальный bridge port вводится только при реальном переводе чужого контракта, а не автоматически для каждого междоменного ребра.
### Adapter
SLM-модуль в Group `adapters`, который реализует один или несколько связанных dependency ports поверх SDK, generated client, storage, transport, platform API, данных запроса или технического сервиса. Одна связная production-реализация принадлежит одному adapter-модулю и не размещается внутри assembly, framework binding или composition.
Adapter знает concrete provider и переводит его arguments, records и expected failures в контракт port. Он не объявляет операции Domain API, публичные доменные модели, предметные fallbacks или domain errors.
### Source cache
Технический cache внешнего источника внутри adapter: transport deduplication, connection state, provider retry или хранение port records. Source cache не является публичной доменной моделью и не передаёт наружу library-specific keys, clients или result types. Adapter является владельцем созданного им cache и экспортирует lifecycle handle; assembly может агрегировать этот cleanup, не становясь вторым владельцем. Если resource создаёт assembly, adapter получает его как borrowed capability.
## Assemblies и runtime-граф
### Assembly
SLM-модуль в Group `assemblies`, который создаёт явный именованный граф одного или нескольких Domain API пакета для одного объявленного production-контекста. Assembly выбирает публичные adapter-модули своего домена, вызывает фабрики и может принимать готовые API других доменов аргументами.
Assembly не добавляет предметные операции, модели или ошибки. Импорт assembly не создаёт API и не запускает side effects; граф появляется только при явном вызове её builder.
### Default assembly
Обязательный модуль `assemblies/default`, который создаёт штатный production-граф домена для одного baseline capability set, объявленного проектом. Имя `default` означает каноническую сборку проекта, но не означает browser-, server-, shared- или isomorphic-совместимость.
Для React + Vite `default` может быть browser-only. Для Next.js она может быть действительно изоморфной, только если каждый executable import совместим со всеми заявленными resolver conditions. Отличающийся набор API, dependencies, trust, runtime capabilities или lifecycle получает отдельную именованную assembly, например `rsc`, `administration` или `realtime-session`.
### Дополнительная assembly
Assembly, отличная от `default` и представляющая реальный дополнительный production-контекст. Имя может отражать environment только тогда, когда environment действительно определяет wiring; наличие RSC, server action или worker само по себе не требует отдельной assembly при неизменном совместимом графе.
### Graph owner
Composition root уровня `app`, `composition`, request handler, worker entry или test setup, который вызывает assemblies в ацикличном порядке, передаёт готовые cross-domain API зависимым assemblies и владеет областью жизни совокупного графа.
Graph owner импортирует production builders assemblies, но не `api/factory` или concrete adapters. Он создаёт только dependency-connected часть графа, необходимую текущему application, route, request, worker или test scope.
### Ресурс assembly
Ресурс жизненного цикла, владельцем которого является assembly и который она обязана создать для возвращаемого графа. Adapter-owned resource сохраняет adapter owner и передаёт assembly только lifecycle handle для aggregate cleanup; borrowed resource не закрывается получателем.
Assembly немедленно регистрирует каждое cleanup obligation: cleanup собственного resource и полученный adapter lifecycle handle. При частичной ошибке все зарегистрированные obligations выполняются в обратном порядке. Успешный результат с хотя бы одним obligation предоставляет идемпотентный aggregate async cleanup, после завершения которого resources не вызывают callbacks. Только graph без cleanup obligations не возвращает пустой `dispose`.
## State, cache и framework
### Framework projection
Материализованное состояние или cache, которое framework binding строит из public models, outcomes и events Domain API для rendering, revalidation, optimistic UI и координации интерфейса. Concrete runtime может быть TanStack Query, SWR, Apollo, Zustand, Redux, Pinia, Signals или механизм конкретного framework.
Framework projection принадлежит binding или composition, а не модулю `api`. Она может хранить значения и технические статусы, но не определяет параллельную предметную модель. Предметный optimistic merge, ordering, rollback, reconciliation или transition производится операцией Domain API либо детерминированной функцией `api/runtime`.
### Hydration payload
Сериализуемая framework-owned форма переноса projection между server и client scopes. Payload содержит только разрешённые публичные доменные значения и framework metadata и не содержит API instances, functions, mutable cache clients, ports, adapters или request secrets.
Server и client создают отдельные API instances и framework caches. RSC передаёт через client boundary только сериализуемые значения или hydration payload; Server Action создаёт собственный request-scoped граф на каждый вызов.
### Framework Group
Group доменного пакета, названная по конкретному фреймворку: `react`, `vue` и аналогично. Она содержит framework binding modules, не имеет собственного `index.ts`, реализации, состояния, жизненного цикла или публичного API.
### Framework binding module
SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией готового Domain API с конкретным framework. Он может владеть Provider, hooks, query policy, framework projection, hydration и переиспользуемым domain-specific UI.
Framework binding module получает готовый Domain API, не вызывает его фабрику или assembly и не выбирает adapters. Он не импортирует framework state, hooks или components другого домена и не обращается к предметному external source в обход Domain API.
Framework-only SDK допустим внутри binding только для получения opaque operation input, например token от CAPTCHA или payment element; предметная операция всё равно выполняется через Domain API, а SDK type не пересекает его публичную границу.
## Realtime
### Realtime port
Dependency port для двусторонних сообщений или подписок поверх WebSocket, SSE, GraphQL subscription, provider SDK или другого push-транспорта. Realtime port описывает предметно необходимую capability и проверяемые guarantees, но не публикует transport frames или concrete client.
### Realtime-команда
Операция Domain API, отправляемая через realtime port и имеющая объявленный момент подтверждения. Если приложение должно получить индивидуальный outcome, protocol adapter сопоставляет command, acknowledgement и failure посредством correlation metadata.
Разрыв соединения после отправки и до подтверждения может означать неопределённый outcome. Без idempotency key или provider guarantee такой исход не объявляется безопасным failure или автоматически повторяемой командой.
### Realtime subscription
Явная операция Domain API, которая публикует только проверенные domain events, statuses и errors и предоставляет cleanup. Realtime port определяет ordering, duplicate delivery, reconnect, gap detection, resync, cancellation и момент, после которого завершившийся cleanup гарантирует отсутствие новых callbacks.
Shared physical connection принадлежит adapter или assembly с явными scope, multiplicity и cleanup. Framework binding решает, как материализовать domain events: обновить projection, применить API-owned transition либо invalidировать cache и повторно запросить snapshot через Domain API.
## Environment
### Environment capability set
Явно объявленный набор runtime-возможностей, доступных конкретной точке входа или assembly: DOM, cookies, filesystem, worker API, edge API, framework server runtime и аналогично. Название папки не определяет capability set.
Совместимость проверяется по executable import-графу для каждого поддерживаемого набора resolver conditions и framework execution phase, включая server prerender и browser hydration. Runtime branching и tree shaking не доказывают изоляцию несовместимых импортов.
### Framework reference edge
Связь, которую framework преобразует в ссылку на другой executable graph вместо обычного runtime-вызова, например ссылка Server Component на Client Component или client invocation Server Action. Такая связь объявляется конфигурации проверки и анализируется отдельно от executable и type-only edges, но не отменяет проверку всех сред, в которых target graph исполняется самостоятельно.
RSC не является универсальной третьей средой рядом с browser и server. Server Component выполняется в server scope. Client Component участвует в browser hydration и, при включённом SSR или prerender, также исполняется в отдельном server render graph; framework-deferred browser effects проверяются отдельно. Между RSC и client graph проходит serialization/reference boundary.
## Структурная модель
```text
SLM root
└── domains
├── доменный модуль Level 1
└── доменный пакет Level 2
├── metadata
├── модуль api
│ ├── api
│ ├── api/factory
│ ├── api/ports при наличии ports
│ └── api/runtime при наличии consumers
├── обязательная Group assemblies
│ ├── модуль default
│ └── дополнительные assembly-модули
├── Group adapters при наличии ports
│ └── adapter-модуль
└── Framework Group react
├── модуль session
└── модуль queries
```

View File

@@ -1,126 +0,0 @@
# Проверка Level 2
> Граница автоматической проверки, architecture review и contract tests Level 2.
## Конфигурация проекта
Конфигурация проверки сопоставляет физические пути с:
- доменными модулями Level 1 и пакетами Level 2;
- metadata, SLM-модулями и Groups;
- фасетами `api`;
- dependency ports и adapter modules;
- assemblies и их baseline/special contexts;
- public entry points;
- executable, type-only, framework reference и deferred edges;
- environment capability sets, resolver conditions и framework execution phases;
- API-safe external packages;
- runtime assembly inputs и создаваемыми API, если проект автоматизирует runtime DAG.
Формат такой конфигурации пока не выбран. Проверка анализирует объявленные boundaries и resolved graphs, а не угадывает сущность только по имени папки.
## Автоматическая проверка
Каждое правило класса `A` реализуется блокирующей проверкой проекта. Автоматическая проверка обнаруживает:
- одновременное объявление одной предметной области module и package;
- executable file, root `index.ts`, state или reexport в корне package;
- отсутствие `api` или несколько модулей `api`;
- отсутствие `api` либо `api/factory`;
- недопустимый type/runtime export kind фасетов `api`, `api/ports`, `api/factory` и `api/runtime`;
- другой public path или deep import внутри `api`;
- отсутствие Group `assemblies` или модуля `assemblies/default`;
- прямой дочерний элемент `assemblies` или `adapters` без module boundary;
- нарушение importer matrix ports, factories и concrete adapters;
- достижимость adapter, assembly, framework, SDK, storage, state/query runtime или environment-specific code из `api`;
- запрещённый cross-domain import;
- type-only import не из public facet владельца;
- import framework state, hooks, contexts или components другого домена;
- несовместимую executable reachability под каждым configured resolver condition set;
- runtime- или type-only cycles статического module graph.
Проверка external package reachability использует project allowlist API-safe packages. Решение о том, соответствует ли package критериям API-safe, принимается на review; автоматизация проверяет объявленный label и фактически resolved entries.
Неанализируемые dynamic imports запрещаются или явно allowlist-ятся project policy с target capability set.
## Architecture review
На review определяется:
- представляет ли package одну связную предметную область;
- является ли `api` единственным семантическим шлюзом домена;
- соответствуют ли exports `api` реальным consumer contracts, `api/ports` implementer contracts, а `api/factory` объявленным Domain API factories;
- отличаются ли public models от raw provider DTO там, где это необходимо;
- принадлежат ли operations ровно одному Domain API;
- оправдано ли разделение нескольких Domain API независимой сборкой, trust или consumers;
- описывают ли ports consumer-owned capabilities, а не endpoints конкретного SDK;
- достаточна ли closed failure algebra для выбора domain outcomes;
- преобразуются ли provider и foreign-domain failures в собственные errors;
- является ли каждая production implementation отдельным adapter module;
- не выполняют ли framework bindings предметные external operations в обход API;
- не создаёт ли framework projection параллельную модель;
- определены ли optimistic ordering, versioning и reconciliation модулем `api`;
- представляет ли `default` один реальный baseline capability context;
- оправданы ли дополнительные assemblies реальным отличием graph;
- остаётся ли runtime assembly graph ацикличным;
- создаётся ли только dependency-connected часть production graph;
- полностью ли определены lifecycle и cleanup failure paths;
- соответствует ли каждый API-safe package ограничениям;
- остаются ли Groups без implementation и aggregate API.
## Environment review
Для каждого public entry point рассматриваются реальные executable imports под заявленными conditions. Отдельно проверяются:
- RSC server execution;
- Client Component references;
- server prerender graph Client Components при включённом SSR;
- browser hydration graph Client Components;
- framework-deferred browser effects;
- Server Action references;
- browser, Node.js, edge и worker capabilities;
- conditional exports external packages;
- dynamic imports;
- serialization boundaries.
Название `default`, `rsc`, `server` или `client` не является доказательством совместимости. Tree shaking и runtime branching также не являются доказательством.
## Realtime review
Для каждого realtime port фиксируются:
- correlation scope и ACK semantics;
- ordering и duplicate policy;
- disconnect, timeout и `OUTCOME_UNKNOWN`;
- idempotency и retry;
- reconnect, gap detection и resync;
- cancellation;
- shared connection owner;
- cleanup и запрет callbacks после disposal.
Без этих guarantees adapter нельзя считать проверяемой реализацией port.
## Testing
Domain API проверяется через factory с fake ports. Adapter проверяется contract tests concrete provider. Assembly проверяет production wiring, capabilities, partial construction и cleanup. Framework binding проверяет projection, hydration и lifecycle с fake API.
Import-graph checks не заменяются runtime tests, а API fake не заменяет adapter contract test.
## Смешанный SLM root
Наличие доменных модулей Level 1 рядом с пакетами Level 2 является завершённым допустимым состоянием. Проверка применяет правила формы отдельно к каждой предметной области и правила Level 2 ко всем статическим связям, пересекающим package boundary.
Переход одного домена завершается, когда его старая module boundary удалена и checker видит только package. Другие домены не входят в критерий формы, но dependency-connected consumers и graph owners входят в change radius.
## Связанные правила
- [`SLM-L2-DOMAIN-A003`](../rules/level-2.md#slm-l2-domain-a003)
- [`SLM-L2-API-A007`](../rules/level-2.md#slm-l2-api-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-API-A019`](../rules/level-2.md#slm-l2-api-a019)
- [`SLM-L2-ASSEMBLY-A020`](../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-API-A022`](../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.

View File

@@ -10,21 +10,20 @@
Определения, рекомендации, разрешения, примеры и открытые вопросы не получают код правила.
Нормативные определения объявляются в терминологии соответствующего уровня. Они обязательны для толкования правил, но нормативность определения сама по себе не превращает его в правило.
Нормативные определения объявляются в [терминологии SLM](../architecture/terminology.md). Они обязательны для толкования правил, но нормативность определения сама по себе не превращает его в правило.
## Код правила
```text
SLM-L{level}-{group}-{class}{number}
SLM-{group}-{class}{number}
```
| Часть | Значение |
|---|---|
| `SLM` | Принадлежность архитектуре SLM |
| `L{level}` | Уровень архитектуры |
| `group` | Раздел правил |
| `class` | Способ проверки: `A` или `R` |
| `number` | Трёхзначный номер внутри уровня |
| `number` | Глобально уникальный трёхзначный номер правила |
## Способы проверки
@@ -50,44 +49,33 @@ SLM-L{level}-{group}-{class}{number}
| `COMPONENT` | Компоненты |
| `NESTED_MODULE` | Вложенные модули |
| `LIFECYCLE` | Жизненный цикл |
| `DOMAIN` | Домены |
| `API` | Доменный API |
| `FACTORY` | Фабрики Domain API |
| `ERROR` | Ошибки домена |
| `PORT` | Dependency ports |
| `ADAPTER` | Адаптеры |
| `ASSEMBLY` | Сборка API и жизненный цикл |
| `ENVIRONMENT` | Границы сред выполнения |
| `FRAMEWORK` | Модули фреймворков |
| `STATE` | Материализация состояния |
| `REALTIME` | Realtime-взаимодействие |
| `TEST` | Тестирование |
Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования.
## Формат записи
```md
### SLM-L1-MODULE-A004
### SLM-MODULE-A004
> **Публичный API модуля**
>
> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API.
> Каждый модуль предоставляет единый логический публичный API через обязательный корневой фасет `index` и, при необходимости, фасеты `client`, `browser` и `server`; код за пределами модуля импортирует его содержимое только через эти фасеты.
```
Код является заголовком третьего уровня и автоматически получает адрес для ссылки `#slm-l1-module-a004`.
Код является заголовком третьего уровня и автоматически получает адрес для ссылки `#slm-module-a004`.
Название и описание входят в одну цитату. Название выделяется жирным и служит кратким именем правила. Описание полностью формулирует требование и занимает одну физическую строку.
Ссылка из тематического черновика:
```md
[`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
[`SLM-MODULE-A004`](./registry.md#slm-module-a004)
```
## Как формулировать правила
1. Правило понятно без чтения тематической главы и опирается только на нормативные термины своего уровня.
1. Правило понятно без чтения тематической главы и опирается только на нормативные термины SLM.
2. Правило защищает один архитектурный инвариант.
3. Один инвариант получает один код независимо от числа участников и способов проверки.
4. Название является кратким и устойчивым именем правила.
@@ -103,7 +91,7 @@ SLM-L{level}-{group}-{class}{number}
## Нумерация
1. Номер уникален внутри уровня независимо от раздела и способа проверки.
1. Номер глобально уникален независимо от раздела и способа проверки.
2. Номер не обозначает важность или порядок выполнения.
3. Удалённый номер не переиспользуется для другого правила.
4. При изменении способа проверки номер сохраняется, но меняется полный код.
@@ -124,7 +112,6 @@ SLM-L{level}-{group}-{class}{number}
Корневой скрипт `draft-rules.js` читает объявления только из этой директории, проверяет формат и уникальность кодов, валидирует ссылки из остальных черновиков и выводит правила разделами «Автоматические» и «Для ревью». Скрипт проверяет документы, а не архитектуру приложения.
## Наборы правил
## Реестр
- [Первый уровень](./level-1.md)
- [Второй уровень](./level-2.md)
- [Единый реестр правил SLM](./registry.md)

View File

@@ -1,209 +0,0 @@
# Правила SLM второго уровня
Доменный пакет Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. Для предметной области в пакетной форме [`SLM-L1-DOMAIN-R015`](./level-1.md#slm-l1-domain-r015) заменяется правилами доменного пакета; остальные предметные области того же SLM root могут сохранять форму доменного модуля Level 1. [`SLM-L1-GROUP-R007`](./level-1.md#slm-l1-group-r007) сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Для модуля `api` правило [`SLM-L1-MODULE-A004`](./level-1.md#slm-l1-module-a004) уточняется `SLM-L2-API-A019`: объявленные фасеты вместе образуют один логический публичный API и не считаются deep imports. Ранее использовавшиеся номера `SLM-L2-DOMAIN-R001` и `SLM-L2-MIGRATION-A017` не переиспользуются.
## Граница доменного пакета
### SLM-L2-DOMAIN-R002
> **Предметная граница пакета**
>
> Каждая предметная область, использующая пакетную форму Level 2, представлена ровно одним доменным пакетом; все его модули относятся только к этой предметной области.
### SLM-L2-DOMAIN-A003
> **Корень доменного пакета**
>
> Корень доменного пакета содержит только декларативную metadata, модуль `api` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов.
### SLM-L2-GROUP-R004
> **Навигационная Group доменов**
>
> Group, расположенная непосредственно в слое `domains` или другой такой Group, содержит только доменные модули Level 1, доменные пакеты Level 2 и навигационные Groups и не владеет реализацией, состоянием, жизненным циклом или публичным API.
## Доменный API
### SLM-L2-API-R005
> **Модуль api**
>
> Каждый доменный пакет содержит ровно один модуль `api`, который объявляет один или несколько именованных Domain API, их публичные модели, результаты, ошибки, dependency ports и фабрики.
### SLM-L2-API-R006
> **Семантическая власть Domain API**
>
> Доступные приложению доменные данные, модели, validation, семантика команд и запросов, результаты и ожидаемые ошибки производятся или проверяются модулем `api`; adapters, assemblies и framework bindings не определяют параллельную предметную модель или переход.
### SLM-L2-API-A007
> **Импортная замкнутость api**
>
> Все runtime- и type-only импорты модуля `api`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как API-safe.
### SLM-L2-FACTORY-R008
> **Фабрики Domain API**
>
> Каждому публичному Domain API соответствует ровно одна именованная фабрика фасета `api/factory`; фабрика получает явные ports и cross-domain API, создаёт только этот Domain API, не выбирает adapter или assembly и не запускает скрытые ресурсы жизненного цикла.
## Ошибки домена
### SLM-L2-ERROR-R009
> **Публичный контракт ошибок**
>
> Каждый ожидаемый сбой публичной операции Domain API представлен именованным readonly сериализуемым типом собственной доменной ошибки с устойчивым кодом; тип экспортируется через `api`, а необходимые внешним потребителям runtime-коды и guards только через `api/runtime`.
### SLM-L2-ERROR-R010
> **Изоляция исходных ошибок**
>
> Сбой provider, adapter, SDK, транспорта, storage или другого домена, доступный приложению через Domain API, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause.
## Assemblies и зависимости
### SLM-L2-ASSEMBLY-R011
> **Роль assembly**
>
> Каждая assembly является SLM-модулем одного объявленного production-контекста, выбирает adapter-модули, вызывает одну или несколько фабрик своего `api` и возвращает явный именованный граф готовых Domain API, не добавляя предметные операции, модели или ошибки.
### SLM-L2-DEPENDENCY-A012
> **Междоменные импорты Level 2**
>
> Статическая связь между разными доменными границами, хотя бы одна из которых является пакетом Level 2, допускает только type-only импорт публичного API доменного модуля Level 1 или фасета `api` пакета Level 2 либо runtime-импорт `api/runtime` пакета Level 2; остальные публичные и внутренние пути другого домена не импортируются.
### SLM-L2-ENVIRONMENT-A013
> **Совместимость среды выполнения**
>
> Для каждой объявленной точки входа, поддерживаемого набора resolver conditions и framework execution phase её достижимый executable import-граф не содержит несовместимых runtime capabilities; type-only связи, framework reference и deferred edges проверяются отдельно и не считаются обычным выполнением.
## 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-ответственностью, получает готовые Domain API, материализует их значения средствами фреймворка и не вызывает фабрики, не выбирает adapters, не обращается к предметному внешнему источнику в обход Domain API и не владеет страницей, маршрутом или multi-domain композицией.
### SLM-L2-TEST-R016
> **Проверка владельцев Level 2**
>
> Каждый публичный сценарий проверяется через фабрику владеющего им Domain API, каждый adapter — по контракту реализуемого port, а основные тесты assembly и framework binding module проверяют только собственные публичные границы и не повторяют полный набор сценариев Domain API.
## Совместное применение форм
### SLM-L2-DOMAIN-A026
> **Однозначная форма домена**
>
> Каждая предметная область объявлена ровно в одной форме: как доменный модуль Level 1 либо как доменный пакет Level 2; один SLM root может одновременно содержать разные предметные области обеих форм.
## Внешние библиотеки api
### SLM-L2-API-R018
> **API-safe внешний пакет**
>
> Внешний пакет объявляется API-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state/query manager, framework или другой технической интеграцией.
## Публичные фасеты api
### SLM-L2-API-A019
> **Публичные фасеты api**
>
> Публичный API модуля `api` имеет обязательные entry points `api` только с type exports и `api/factory` только с runtime exports, может иметь `api/ports` только при наличии объявленного dependency port и только с type exports и `api/runtime` только с runtime exports и не имеет других публичных путей или deep imports.
## Обязательная штатная сборка
### SLM-L2-ASSEMBLY-A020
> **Обязательная assembly default**
>
> Корень каждого доменного пакета содержит ровно одну непустую Group `assemblies` с ровно одним прямым модулем `default`; каждый другой прямой дочерний элемент Group также является объявленной границей assembly-модуля.
### SLM-L2-ADAPTER-R021
> **Модули production adapters**
>
> Если хотя бы одна фабрика имеет dependency port, корень доменного пакета содержит непустую Group `adapters`, а каждая связная production-реализация одного или нескольких ports принадлежит ровно одному adapter-модулю этой Group и не определяется в другом месте production-графа.
### SLM-L2-API-A022
> **Потребители фасетов и сборочных модулей**
>
> Фасет `api` импортируется извне только через `import type`, `api/ports` импортируют только adapters своего домена, assemblies и тесты, `api/factory` и concrete adapters в production импортируют только assemblies своего домена, а `api/runtime` не импортируют adapters и используют только через объявленный публичный путь с соблюдением правил слоёв и междоменных зависимостей.
## Жизненный цикл assembly
### SLM-L2-ASSEMBLY-R023
> **Транзакционный lifecycle assembly**
>
> Assembly не запускает скрытую долгоживущую работу; cleanup каждого созданного ею ресурса и каждого полученного adapter lifecycle handle немедленно регистрируется, при частичной ошибке выполняется в обратном порядке, а успешный результат с cleanup obligations предоставляет идемпотентный aggregate cleanup, после завершения которого resources не вызывают callbacks.
## Недетерминизм api
### SLM-L2-API-R024
> **Явные источники недетерминизма**
>
> Операция Domain API получает текущее время, timer, random, ID generator, environment и другие источники недетерминизма только через явные ports фабрики и не читает их из скрытого runtime-окружения.
## Публичный runtime api
### SLM-L2-API-R025
> **Детерминированный runtime api**
>
> Фасет `api/runtime` экспортирует только необходимые реальным внешним потребителям детерминированные значения и функции без ввода-вывода, изменяемого состояния, runtime capability или environment-specific поведения.
## Dependency ports
### SLM-L2-PORT-R027
> **Consumer-owned port**
>
> Каждый dependency port принадлежит модулю `api`, описывает минимальную необходимую ему capability и закрытый набор ожидаемых port failures без concrete provider, SDK, framework или transport types; adapter реализует этот контракт, но не определяет его семантику.
## Материализация состояния
### SLM-L2-STATE-R028
> **Framework-owned materialization**
>
> Framework binding или composition может владеть framework metadata и собственным UI-state, но материализует доменный payload только из values, outcomes и events, произведённых или проверенных Domain API, и применяет предметный optimistic merge, reconciliation или transition только через операцию либо детерминированный runtime модуля `api`.
## Realtime
### SLM-L2-REALTIME-R029
> **Проверяемый realtime-контракт**
>
> Каждый realtime port явно определяет correlation, момент подтверждения команды, ordering, duplicate, reconnect, resync, cancellation и cleanup semantics; adapter скрывает transport protocol, а Domain API публикует только проверенные события, outcomes и собственные стабильные ошибки.
## Runtime-граф assemblies
### SLM-L2-ASSEMBLY-R030
> **Ацикличная runtime-сборка**
>
> Runtime-граф публичных API доменных модулей Level 1 и Domain API пакетов Level 2, включая assembly inputs, factory dependencies и передаваемые callbacks, не содержит циклов, а graph owner создаёт независимые API раньше зависимых и очищает их в обратном порядке.
### SLM-L2-ASSEMBLY-R031
> **Контекст default assembly**
>
> `assemblies/default` представляет один объявленный штатный production-набор API, dependencies, runtime capabilities и lifecycle; имя `default` само по себе не означает browser-, server- или isomorphic-совместимость, а отличающийся контекст получает отдельную именованную assembly.

View File

@@ -1,21 +1,22 @@
# Правила SLM первого уровня
Здесь собраны правила первого уровня. Это единственное место, где они формулируются; тематические черновики объясняют их и ссылаются на коды.
# Реестр правил SLM
Здесь собраны правила SLM. Это единственное место, где они формулируются; тематические черновики объясняют их и ссылаются на коды.
## Размещение кода по слоям
### SLM-L1-LAYER-R001
### SLM-LAYER-R001
> **Назначение слоёв**
>
> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода.
### SLM-L1-LAYER-A002
### SLM-LAYER-A002
> **Направление зависимостей**
>
> Внутри одного SLM root код каждого слоя может зависеть только от кода целевых слоёв, разрешённых для него нормативной матрицей слоёв.
### SLM-L1-LAYER-R003
### SLM-LAYER-R003
> **Граница слоя `app`**
>
@@ -23,31 +24,31 @@
## Границы модулей
### SLM-L1-MODULE-A004
### SLM-MODULE-A004
> **Публичный API модуля**
>
> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API.
> Каждый модуль предоставляет единый логический публичный API через обязательный корневой фасет `index` и, при необходимости, фасеты `client`, `browser` и `server`; код за пределами модуля импортирует его содержимое только через эти фасеты.
### SLM-L1-MODULE-A014
### SLM-MODULE-A014
> **Папка модуля**
>
> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки.
### SLM-L1-MODULE-R006
### SLM-MODULE-R006
> **Ответственность модуля**
>
> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях.
### SLM-L1-MODULE-R011
### SLM-MODULE-R011
> **Владелец ответственности**
>
> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`.
### SLM-L1-MODULE-R012
### SLM-MODULE-R012
> **Состав публичного API**
>
@@ -55,15 +56,15 @@
## Зависимости между модулями
### SLM-L1-DEPENDENCY-A005
### SLM-DEPENDENCY-A005
> **Циклические зависимости**
>
> Граф зависимостей модулей внутри одного SLM root, включая вложенные модули, не содержит циклов.
> Зависимости между модулями внутри одного SLM root, включая вложенные модули, не образуют циклов.
## Назначение групп
### SLM-L1-GROUP-R007
### SLM-GROUP-R007
> **Назначение группы**
>
@@ -71,15 +72,15 @@
## Назначение сегментов
### SLM-L1-SEGMENT-R008
### SLM-SEGMENT-R008
> **Граница сегмента**
>
> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или узла графа зависимостей.
> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или границы зависимостей.
## Ответственность компонентов
### SLM-L1-COMPONENT-R009
### SLM-COMPONENT-R009
> **Ответственность компонента**
>
@@ -87,7 +88,7 @@
## Границы вложенных модулей
### SLM-L1-NESTED_MODULE-A010
### SLM-NESTED_MODULE-A010
> **Доступ к вложенному модулю**
>
@@ -95,16 +96,34 @@
## Жизненный цикл
### SLM-L1-LIFECYCLE-R013
### SLM-LIFECYCLE-R013
> **Жизненный цикл ресурсов**
>
> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
## Граница доменных модулей
## Границы сред выполнения
### SLM-L1-DOMAIN-R015
### SLM-ENVIRONMENT-R016
> **Доменный модуль**
> **Универсальный фасет**
>
> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие ей модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей.
> Корневой фасет `index` экспортирует только публичный код, совместимый как с серверным рендерингом, включая RSC, так и с клиентским выполнением, и не импортирует или реэкспортирует код фасетов `client`, `browser` или `server` прямо либо транзитивно.
### SLM-ENVIRONMENT-R017
> **Клиентский фасет**
>
> Фасет `client` экспортирует только клиентский код, который не может выполняться как RSC, и не импортирует или реэкспортирует код фасетов `browser` или `server` прямо либо транзитивно.
### SLM-ENVIRONMENT-R018
> **Браузерный фасет**
>
> Фасет `browser` экспортирует только browser-only код, а потребители импортируют его только динамически с отключённым SSR.
### SLM-ENVIRONMENT-R019
> **Серверный фасет**
>
> Фасет `server` экспортирует только server-only код и не импортируется или реэкспортируется фасетами `index`, `client` или `browser` прямо либо транзитивно.

View File

@@ -4,9 +4,9 @@
## Структура
- `DRAFT/` - рабочая документация Levels 1-2, источник содержимого сайта и bundled references текущего skill.
- `site/` - VitePress-конфигурация, тема и статические ресурсы.
- `docs/` и `docs-v3/` - архивные версии документации, не используемые сайтом.
- `docs/` - документация SLM и единственный источник содержимого сайта.
- `site/` - VitePress-рендерер: конфигурация, тема и статические ресурсы без собственной копии документации.
- `DRAFT/` - прежний рабочий материал, не используемый сайтом.
- `old-docs/` - архив legacy-документации, не используемый текущим skill.
- `src-skills/` - исходники agent skills.
- `skills/` - собранные skills для установки через `npx skills`.
@@ -18,10 +18,12 @@
```bash
npm run build:skill
npm run check:skill
npm run check:docs
npm run check:site
npm run check
```
`npm run build:skill` детерминированно пересобирает `skills/slm-design/` из `src-skills/slm-design/` и `DRAFT/`. `npm run check:skill` ничего не изменяет и проверяет, что tracked-артефакт актуален, ссылки разрешаются, а legacy references отсутствуют. `npm run check` дополнительно проверяет правила и собирает сайт. Не редактируй собранные файлы вручную.
`npm run check:docs` проверяет правила и ссылки документации. `npm run check:site` собирает VitePress из `docs/` и проверяет опубликованные страницы. Skill пока собирается отдельно из `src-skills/slm-design/` и собственных reference-материалов; не редактируй собранные файлы вручную.
## Установка

75
docs/README.md Normal file
View File

@@ -0,0 +1,75 @@
---
layout: home
title: SLM Design
description: Архитектура фронтенд-приложений с явным владением ответственностями
hero:
name: SLM Design
text: Архитектура владения ответственностями
tagline: Сначала определяется ответственность и её владелец. Слои, группы, модули и сегменты выражают уже принятое архитектурное решение.
image:
src: /logo.svg
alt: SLM Design
actions:
- theme: brand
text: Изучить архитектуру
link: /architecture/
- theme: alt
text: Открыть правила
link: /rules/registry
features:
- title: Ответственность раньше структуры
details: Модуль появляется из самостоятельной ответственности, а не из размера каталога, количества файлов или выбранного фреймворка.
- title: Явная структурная модель
details: Слой определяет роль, группа классифицирует модули, модуль владеет ответственностью, сегмент организует реализацию.
- title: Проверяемые границы
details: Публичные API, направление зависимостей и правила жизненного цикла делают архитектурное решение наблюдаемым и проверяемым.
---
SLM Design (Scoped Layered Module Design) — структурная архитектура фронтенд-приложений, основанная на явном владении ответственностями.
Архитектурное решение начинается не с папки или имени файла. Сначала определяется ответственность, затем её владелец, роль владельца в приложении и только после этого физическое размещение кода.
## Основа
SLM использует четыре структурных понятия:
1. **Слой** классифицирует код по архитектурной роли и ограничивает направление зависимостей.
2. **Группа** помогает классифицировать модули внутри слоя, но ничего не реализует и ничем не владеет.
3. **Модуль** владеет самостоятельной ответственностью, её публичным API, зависимостями, состоянием и жизненным циклом.
4. **Сегмент** организует внутреннее содержимое одного модуля и не создаёт нового владельца.
```text
Слой → [Группа*] → Модуль → [Сегмент*]
```
Знак `*` означает, что элементов может не быть или их может быть несколько. Группы могут быть вложены друг в друга внутри одного слоя. Сегменты всегда остаются внутри одного модуля.
## Документация
### Архитектура
- [Обзор архитектуры](./architecture/)
- [Слои](./architecture/layers.md)
- [Модули](./architecture/modules.md)
- [Сегменты](./architecture/segments.md)
### Правила
- [Как устроены правила](./rules/)
- [Реестр правил](./rules/registry.md)
### Справочные материалы
- [Терминология](./reference/terminology.md)
- [Проверка архитектуры](./reference/validation.md)
## Порядок принятия решения
1. Сформулировать ответственность без упоминания папок, файлов и библиотек.
2. Назначить одного владельца ответственности.
3. Выбрать слой по роли владельца.
4. Определить публичный контракт, зависимости, состояние и жизненный цикл.
5. Организовать реализацию сегментами, если это упрощает навигацию.
6. Представить принятое решение папками, файлами и публичными точками входа.

View File

@@ -0,0 +1,84 @@
# Архитектура SLM
SLM описывает владение ответственностями внутри одного фронтенд-приложения. Слой определяет роль кода, группа классифицирует модули, модуль владеет ответственностью, а сегмент организует реализацию владельца.
## Владение как основа
**Ответственность** — связная часть приложения с одной причиной изменяться. Она становится самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни.
У каждой самостоятельной ответственности есть ровно один владелец. В SLM владельцем является модуль. Он определяет:
- какие возможности доступны внешним потребителям;
- от каких других возможностей зависит ответственность;
- кому принадлежат данные и изменяемое состояние;
- когда создаются и уничтожаются долгоживущие ресурсы;
- как устроена внутренняя реализация.
Место выполнения кода не меняет владельца. Компонент, провайдер, маршрут или точка запуска могут технически вызывать код ответственности, но не получают владение ею автоматически.
## Структурная модель
```text
SLM root
└── слой
├── модуль
│ └── сегмент
└── группа
├── модуль
└── группа
└── модуль
└── сегмент
```
| Сущность | Назначение | Владеет ответственностью |
|---|---|---|
| Слой | Классифицирует код по архитектурной роли | Нет |
| Группа | Классифицирует модули внутри слоя | Нет |
| Модуль | Реализует одну самостоятельную ответственность | Да |
| Сегмент | Организует внутренности одного модуля | Нет |
Слой и группа находятся снаружи модульной границы. Сегмент находится внутри неё. Компоненты, хуки, сервисы, хранилища и другие детали реализации принадлежат ближайшему модулю-владельцу, если сами не образуют вложенный модуль.
## Порядок проектирования
Архитектурное решение принимается от смысла к структуре:
1. Описать результат или поведение, за которое должен отвечать код.
2. Определить одну причину изменения этой ответственности.
3. Найти связанные данные, поведение, состояние и жизненный цикл.
4. Назначить модуль владельцем и определить его внешних потребителей.
5. Выбрать [слой](./layers.md) по роли ответственности.
6. Спроектировать публичный API и допустимые зависимости [модуля](./modules.md).
7. При необходимости организовать реализацию [сегментами](./segments.md).
8. Только после этого выбрать физические пути и имена файлов.
Если ответственность или владелец не определены, файловая структура не может исправить архитектурную неопределённость.
## Логическая и физическая границы
Модуль не определяется наличием папки, `index.ts` или нескольких файлов. Его определяет самостоятельная ответственность и владение её контрактом, зависимостями, состоянием и жизненным циклом.
После принятия решения модуль получает отдельную папку и публичные точки входа. Это обязательное физическое представление модульной границы, необходимое потребителям и автоматическим проверкам.
Верны обе формулировки:
- самостоятельная ответственность требует модульной границы;
- отдельная папка сама по себе не доказывает наличие модуля.
Пути сопоставляются со слоями, группами, модулями и сегментами в стайлгайде или конфигурации конкретного проекта. Имя пути не меняет нормативный смысл сущности.
## Область применения
SLM применяется внутри **SLM root** — границы структурной архитектуры одного приложения. Это может быть `src/` или другая область, установленная проектом.
Архитектура определяет:
- роли слоёв и допустимые направления зависимостей;
- владельцев самостоятельных ответственностей;
- публичные границы модулей;
- назначение групп и сегментов;
- владение состоянием и жизненным циклом ресурсов.
SLM не задаёт обязательный поток данных, полный файловый стайлгайд, фиксированный набор сегментов, правила монорепозиториев или обязательную внутреннюю форму каждого модуля.
Нормативный смысл терминов находится в [терминологии](../reference/terminology.md). Точные блокирующие требования объявлены только в [реестре правил](../rules/registry.md).

120
docs/architecture/layers.md Normal file
View File

@@ -0,0 +1,120 @@
# Слои
Слой классифицирует код по архитектурной роли и задаёт допустимые направления зависимостей. Слой не владеет ответственностью: владельцем остаётся модуль, размещённый в этом слое.
Слой выбирается после определения ответственности. Похожее имя папки или наличие зависимости от конкретной библиотеки не являются основанием для выбора слоя.
## Роли слоёв
SLM определяет шесть ролей:
| Слой | Роль |
|---|---|
| `app` | Связь приложения с фреймворком: запуск, маршруты, преобразование внешних входных данных и подключение готовых публичных API |
| `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции |
| `domains` | Предметные модели, правила, сценарии и продуктовое состояние |
| `infra` | Технические сервисы и возможности приложения без собственной предметной модели |
| `ui` | Универсальные интерфейсные модули без зависимости от конкретной продуктовой композиции |
| `shared` | Детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода |
Отсутствующая роль не требует пустой папки. Проект создаёт слой только тогда, когда в нём появляется соответствующая ответственность.
### App
`app` содержит точки связи с фреймворком: запуск, файлы маршрутов и преобразование внешних входных данных. Они подключают готовые публичные API других слоёв, но не присваивают их ответственность.
Точки входа `app` являются специальным немодульным исключением. Страница, макет, провайдер или другой самостоятельный продуктовый владелец реализуется в подходящем модуле и только подключается из `app`.
### Compositions
`compositions` содержит владельцев продуктовых композиций: страниц, макетов, экранов, виджетов, результатов маршрутов и интерфейса, объединяющего несколько ответственностей.
Конкретная организация слоя определяется продуктом и фреймворком. Названия `pages`, `layouts`, `screens` и `widgets` могут использоваться как группы, но не являются дополнительными слоями.
### Domains
`domains` содержит модули-владельцы предметных ответственностей: моделей, правил, сценариев и продуктового состояния.
Доменный модуль является обычным SLM-модулем. Ему не требуется отдельная архитектурная форма только потому, что он находится в `domains`.
### Infra
`infra` содержит технические возможности приложения: аналитику, локализацию, тему, телеметрию, интеграции с платформой и другие сервисы без собственной предметной модели.
Технический способ выполнения предметного сценария не переносит владение сценарием из `domains` в `infra`.
### UI
`ui` содержит универсальные интерфейсные модули, которые не знают о конкретной странице, маршруте или продуктовой композиции.
### Shared
`shared` содержит детерминированный фундамент, не зависящий от продукта и не имеющий ввода-вывода, изменяемого состояния или жизненного цикла.
В `shared` могут находиться обычные модули и небольшие немодульные ресурсы: чистые функции, общие типы, стили, декларативная конфигурация и статические файлы.
## Направление зависимостей
Матрица определяет, от каких слоёв может зависеть исходный слой:
| Исходный слой | Допустимые целевые слои |
|---|---|
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `ui`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
Разрешённая зависимость может пропускать промежуточные слои. Например, `compositions` может напрямую использовать модуль `ui`, не создавая посредника в `domains` или `infra`.
Обычный импорт, импорт типа (`import type`) и реэкспорт одинаково участвуют в архитектурном графе. Для связи между модулями дополнительно действуют их [публичные границы и запрет циклов](./modules.md#зависимости-между-модулями).
Разрешённое направление не переносит владение. Если модуль `domains` использует `infra`, предметный сценарий остаётся ответственностью доменного модуля, а техническая возможность — ответственностью инфраструктурного.
`infra` может использовать `ui`, когда технической возможности нужно собственное визуальное представление: CAPTCHA, uploader, карта или инструмент разработчика. `ui` не использует `infra`; необходимые технические возможности универсальный UI получает через входной контракт.
## Группировка модулей
Группа классифицирует модули внутри одного слоя или другой группы. Она нужна, когда плоский список модулей перестаёт быть понятным.
```text
compositions/
├── pages/ # Группа
│ ├── catalog/ # Модуль
│ └── profile/ # Модуль
├── layouts/ # Группа
│ └── main/ # Модуль
└── widgets/ # Группа
└── cart-summary/ # Модуль
```
Группа:
- содержит только модули и вложенные группы;
- не владеет ответственностью или реализацией;
- не имеет состояния и жизненного цикла;
- не предоставляет публичный API;
- не является узлом графа зависимостей;
- не реэкспортирует содержащиеся в ней модули.
Модуль может находиться непосредственно в слое. Группа вводится только ради реальной классификации, а её названия и глубину определяет проект.
Группа организует несколько владельцев внутри слоя. [Сегмент](./segments.md) организует код внутри одного владельца.
## Немодульные исключения
Внутри SLM root код по умолчанию принадлежит модулю. Исключения ограничены двумя случаями:
- точка входа `app` непосредственно связывает приложение с фреймворком;
- ресурс `shared` является небольшой самостоятельной детерминированной единицей без внутренней границы.
Если ресурсу `shared` нужны несколько файлов реализации, собственные архитектурные зависимости, изменяемое состояние, ввод-вывод или жизненный цикл, ему требуется модуль-владелец.
## Связанные правила
- [`SLM-LAYER-R001`](../rules/registry.md#slm-layer-r001)
- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
- [`SLM-LAYER-R003`](../rules/registry.md#slm-layer-r003)
- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)

View File

@@ -0,0 +1,176 @@
# Модули
Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или сущности фреймворка выполняется код.
## Ответственность и владелец
Перед созданием модуля ответственность формулируется без упоминания желаемой папки, файла или библиотеки.
Самостоятельность ответственности определяется вопросами:
- есть ли у неё отдельная причина изменяться;
- нужен ли внешним потребителям собственный контракт;
- есть ли у неё архитектурные зависимости;
- владеет ли она данными или изменяемым состоянием;
- нужна ли ей собственная область жизни;
- можно ли назвать её независимо от внутренней реализации.
Если ответственность самостоятельна, она получает ровно один модуль-владелец. Если несколько частей изменяются по несвязанным причинам, им нужны разные владельцы.
Размер не определяет модуль. Модуль может состоять из одного файла реализации, а большая папка может оставаться частью другого владельца.
## Граница владения
Модуль определяет:
- публичные возможности ответственности;
- допустимые внешние зависимости;
- модели и правила, принадлежащие ответственности;
- состояние и источник истины;
- создание и очистку долгоживущих ресурсов;
- устройство внутренней реализации.
Компонент, хук, провайдер, хранилище, сервис или маршрут могут выполнять часть этой работы технически. Владение остаётся у ближайшего модуля, пока часть кода не получает самостоятельную ответственность и собственную модульную границу.
## Публичный API
У модуля один логический публичный API. Он является контрактом между владельцем и внешними потребителями, а не перечнем всех внутренних файлов.
Публичный API:
- открывает только возможности, необходимые реальным внешним потребителям;
- скрывает детали реализации и изменяемые внутренние механизмы;
- не раскрывает внутренние сегменты;
- представлен объявленными публичными фасетами;
- является единственным способом доступа к модулю извне.
Внутри своей границы модуль может обращаться к собственным файлам напрямую. Требование публичного API действует при пересечении модульной границы.
### Фасеты
Фасет — публичная точка входа, открывающая часть единого API для определённой среды выполнения.
| Фасет | Назначение |
|---|---|
| `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением |
| `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере |
| `browser` | Код только для браузера, подключаемый динамически через границу с отключённым SSR |
| `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения |
`index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде.
Ограничение среды распространяется на все исполняемые импорты и реэкспорты фасета, включая транзитивные. Имя файла, директива `use client`, tree shaking или проверка `typeof window` сами по себе не доказывают совместимость.
Один исполняемый экспорт размещается в минимально подходящем фасете и не дублируется между ними. Универсальный фасет не реэкспортирует специализированные фасеты.
```text
auth/
├── index.ts # Обязательный универсальный фасет
├── client.ts # При необходимости
├── browser.ts # При необходимости
├── server.ts # При необходимости
└── ... # Внутренняя реализация
```
Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль.
## Зависимости между модулями
Зависимость связывает владельца исходного кода с владельцем импортируемого кода. Импорт внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
При пересечении модульной границы код использует только публичный фасет целевого модуля:
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимый глубокий импорт
import { Button } from '@/ui/button/button'
```
Для каждой связи выполняются три условия:
1. Направление разрешено [матрицей слоёв](./layers.md#направление-зависимостей).
2. Целевой модуль используется только через публичный API.
3. Общий граф модулей остаётся ацикличным.
Модули одного слоя могут зависеть друг от друга, если соблюдены публичные границы и не возникает цикл. SLM не требует обязательного посредника или отдельного механизма dependency injection для любой межмодульной связи.
## Компоненты
Компонент является сущностью фреймворка, реализующей часть интерфейса родительского модуля. Он не образует самостоятельного владельца только из-за наличия отдельного файла, каталога, props, локального состояния, доступа к данным или кода жизненного цикла.
Зависимости, состояние и ресурсы компонента относятся к родительскому модулю. Если UI-часть получает самостоятельную ответственность, публичный API, собственные зависимости или область жизни, ей требуется модульная граница.
Модуль может состоять из одного корневого компонента. В этом случае компонент реализует интерфейс, а модуль остаётся владельцем ответственности.
Компонент может быть оформлен отдельным каталогом и иметь локальный `index.ts`:
```text
button-submit/
├── button-submit.tsx
├── styles/
│ └── button-submit.module.css
├── types/
│ └── button-submit.types.ts
└── index.ts
```
Такой `index.ts` является внутренней точкой входа компонента. Он упрощает импорты внутри модуля, но не создаёт SLM public API, нового владельца или узел графа зависимостей.
| `index.ts` компонента | Публичный фасет модуля |
|---|---|
| Действует внутри ближайшего модуля | Действует при пересечении модульной границы |
| Экспортирует локальную реализацию и типы | Экспортирует контракт владельца |
| Не создаёт архитектурную границу | Представляет архитектурную границу |
| Не делает компонент модулем | Принадлежит уже определённому модулю |
Если компонент входит в публичный контракт владельца, корневой фасет модуля явно реэкспортирует его локальную точку входа. Внешний код по-прежнему импортирует модуль, а не внутренний путь компонента.
## Вложенные модули
Вложенный модуль — самостоятельный владелец, физически размещённый внутри родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется общим правилам модулей.
Код родительского модуля использует вложенный модуль через его публичный API. Код за пределами родительской границы не импортирует вложенный модуль напрямую и получает необходимые возможности через API родителя.
Если вложенный модуль становится нужен нескольким внешним владельцам, его можно перенести в минимальную общую область. Сам факт повторного использования внутри родителя переноса не требует.
## Состояние и жизненный цикл
Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в файл хранилища, провайдер или контекст фреймворка.
Для каждого долгоживущего ресурса модуль-владелец определяет:
- место создания;
- момент запуска;
- область жизни;
- допустимое число экземпляров;
- способ остановки, отмены или освобождения.
Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить компонент, провайдер или фреймворк, но ответственность за корректную границу остаётся у модуля.
Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем.
## Внутренняя организация
После определения ответственности и публичной границы модуль может организовать реализацию [сегментами](./segments.md), компонентами и вложенными модулями. SLM не требует полного каркаса и не задаёт обязательный набор внутренних папок.
Каждый модуль физически размещается в отдельной папке. Это делает логическую границу наблюдаемой для потребителей и автоматических проверок, но не заменяет решение о владельце.
Наличие `index.ts` не используется как единственный признак модуля. Модульную границу определяют ответственность, владелец, публичные потребители и сопоставление путей, принятое проектом.
## Связанные правила
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-MODULE-A014`](../rules/registry.md#slm-module-a014)
- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
- [`SLM-MODULE-R012`](../rules/registry.md#slm-module-r012)
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)
- [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013)
- [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016)
- [`SLM-ENVIRONMENT-R017`](../rules/registry.md#slm-environment-r017)
- [`SLM-ENVIRONMENT-R018`](../rules/registry.md#slm-environment-r018)
- [`SLM-ENVIRONMENT-R019`](../rules/registry.md#slm-environment-r019)

View File

@@ -0,0 +1,92 @@
# Сегменты
Сегмент организует внутреннее содержимое одного модуля по назначению. Он помогает ориентироваться в реализации владельца, но не создаёт новую ответственность или архитектурную границу.
## Место в модели
Сегмент появляется только внутри уже определённого модуля:
```text
Слой → [Группа*] → Модуль → [Сегмент*]
```
Группа классифицирует несколько модулей внутри слоя. Сегмент классифицирует код одного модуля. Ни группа, ни сегмент не являются владельцами.
Все файлы, компоненты, состояние, зависимости и код жизненного цикла сегмента принадлежат родительскому модулю. Исключением является только вложенный модуль, который начинает собственную границу владения.
## Назначение
Сегмент используется, когда группировка внутренних файлов по назначению упрощает навигацию. Набор и названия сегментов определяет проект.
Возможная структура:
```text
profile/
├── index.ts
├── profile.tsx
├── hooks/ # Возможный сегмент
├── services/ # Возможный сегмент
├── stores/ # Возможный сегмент
├── types/ # Возможный сегмент
└── ui/ # Возможный сегмент
```
Ни один из показанных сегментов не обязателен. Маленький модуль может хранить реализацию в корне без дополнительных каталогов.
Сегмент:
- не имеет самостоятельной ответственности;
- не предоставляет публичный API;
- не владеет состоянием или жизненным циклом;
- не является узлом графа зависимостей;
- не импортируется внешним кодом как отдельная архитектурная сущность.
Локальный `index.ts` может использоваться во внутренней единице сегмента, например в каталоге компонента. Он не превращает эту единицу или сегмент в модульную границу.
## Компоненты и вложенные модули
Сегмент может содержать компоненты и вспомогательные файлы родительского модуля. Компонент вправе иметь локальные `styles/`, `types/`, `tests/` и внутренний `index.ts`; всё это остаётся реализацией ближайшего модуля.
```text
header/ # Модуль
└── components/ # Сегмент
└── button-submit/ # Компонент
├── button-submit.tsx
├── styles/
│ └── button-submit.module.css
├── types/
│ └── button-submit.types.ts
└── index.ts # Внутренняя точка входа
```
Внешний код не импортирует `components/button-submit`. Если компонент нужен снаружи, модуль-владелец реэкспортирует его через собственный публичный фасет.
Сегмент также может содержать вложенные модули. В отличие от остальных файлов сегмента, каждый вложенный модуль сам владеет отдельной ответственностью и имеет публичный API и границу зависимостей.
```text
landing/ # Родительский модуль
└── parts/ # Сегмент
└── hero/ # Вложенный модуль
├── hero.tsx
└── index.ts
```
Имя `parts` является примером, а не обязательным соглашением SLM.
## Выбор границы
| Ситуация | Решение |
|---|---|
| Код относится к существующему владельцу и группируется только по назначению | Сегмент |
| Части нужна собственная ответственность, API, зависимости, состояние или область жизни | Модуль |
| Несколько модулей слоя нужно классифицировать для навигации | Группа |
| Нескольким файлам не нужна отдельная группировка | Оставить в корне модуля |
Размер каталога и количество файлов не определяют выбор между сегментом и модулем.
## Связанные правила
- [`SLM-SEGMENT-R008`](../rules/registry.md#slm-segment-r008)
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)

View File

@@ -0,0 +1,95 @@
# Терминология SLM
Этот документ задаёт нормативный смысл терминов. Определения используются при толковании архитектуры и правил, но сами по себе не являются отдельными правилами.
## Владение
### SLM root
Граница структурной архитектуры одного приложения. Внутри неё определяются владельцы ответственностей, слои, модули и их зависимости.
### Ответственность
Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственный публичный API, зависимости, состояние или область жизни.
### Владелец
Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннюю реализацию. Место выполнения кода не переносит владение.
## Структурные сущности
### Слой
Архитектурная роль кода внутри SLM root. Слой классифицирует владельцев по назначению и ограничивает допустимые направления зависимостей. Нормативные роли и матрица определены в разделе [Слои](../architecture/layers.md).
### Группа
Необязательный навигационный классификатор модулей внутри одного слоя или другой группы. Группа не является владельцем, публичным API или границей зависимостей.
### Модуль
Минимальная самостоятельная архитектурная единица SLM. Модуль владеет одной связной ответственностью, имеет публичный API и физически размещается в отдельной папке.
### Сегмент
Необязательная внутренняя часть одного модуля, группирующая его содержимое по назначению. Сегмент не является владельцем, публичным API или границей зависимостей.
### Компонент
Сущность фреймворка, реализующая часть интерфейса родительского модуля. Зависимости, состояние и жизненный цикл компонента принадлежат этому модулю и сами по себе не создают нового владельца. Компонент может иметь внутренний `index.ts`, который не является публичным фасетом SLM.
### Вложенный модуль
Обычный модуль, физически размещённый внутри родительского модуля. Он сам владеет отдельной ответственностью и имеет публичный API и границу зависимостей, но остаётся внутренней реализацией родителя для внешнего кода.
## Публичная граница
### Публичный API
Единый логический контракт внешнего доступа к модулю. Он скрывает внутреннюю реализацию и физически представлен обязательным фасетом `index` и только необходимыми фасетами `client`, `browser` и `server`.
### Фасет
Объявленная публичная точка входа модуля, открывающая часть его единого API для определённой среды выполнения. Импорт фасета не является глубоким импортом; любой другой внешний путь внутрь модуля остаётся внутренним.
### Глубокий импорт
Импорт или реэкспорт внутреннего пути чужого модуля, который не объявлен его публичным фасетом.
## Зависимости
### Зависимость
Направленная статическая связь между архитектурными границами внутри одного SLM root. Обычный импорт, импорт типа (`import type`) и реэкспорт одинаково создают архитектурную зависимость.
Зависимость внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
### Нормативная матрица слоёв
Отношение допустимой зависимости между слоями одного SLM root. Матрица определяет доступные целевые роли, но не требует проходить через каждый промежуточный слой.
## Жизненный цикл
### Область жизни
Период, в течение которого принадлежащие модулю состояние или долгоживущий ресурс должны оставаться активными.
### Ресурс жизненного цикла
Ресурс, работа которого продолжается после первоначального вызова и требует остановки, отмены, отписки или освобождения. Например, подписка, обработчик событий, таймер, наблюдатель, запрос или соединение.
### Очистка
Гарантированное прекращение работы ресурса не позже завершения его области жизни. Автоматическая очистка фреймворка считается очисткой владельца, если модуль устанавливает и контролирует соответствующую границу.
## Немодульные единицы
### Точка входа фреймворка
Специальная немодульная единица слоя `app`, которая запускает приложение, объявляет точку маршрута, преобразует внешние входные данные или подключает готовые публичные API.
### Ресурс shared
Небольшая детерминированная единица слоя `shared`, не зависящая от продукта и не скрывающая отдельного внутреннего устройства. У неё нет изменяемого состояния, ввода-вывода, области жизни или собственного публичного API.
Путь и имя сами по себе не определяют ни одну из перечисленных сущностей. Физическое сопоставление задаётся стайлгайдом или конфигурацией проверки после определения ответственности и владельца.

View File

@@ -0,0 +1,98 @@
# Проверка архитектуры
Проверка SLM подтверждает две разные стороны решения:
- смысловая проверка устанавливает ответственность, владельца и корректность границ;
- структурная проверка подтверждает, что решение правильно выражено путями, публичными фасетами и зависимостями.
Успешная сборка или корректно отображаемый интерфейс не доказывают архитектурную корректность.
## Карточка решения
Перед изменением структуры нужно ответить:
| Вопрос | Что зафиксировать |
|---|---|
| Ответственность | Какой результат или поведение изменяется как единое целое |
| Владелец | Какой модуль определяет контракт и внутреннюю реализацию |
| Слой | Какой архитектурной роли соответствует ответственность |
| Потребители | Кому действительно нужен публичный API |
| Зависимости | Какие другие владельцы и возможности необходимы |
| Состояние | Кто определяет смысл и допустимые изменения данных |
| Жизненный цикл | Кто создаёт ресурсы, какова их область жизни и очистка |
| Физическая форма | Какими путями и фасетами представлено принятое решение |
Если ответственность или владелец не определены, проверка путей откладывается: одинаковая файловая структура может представлять разные архитектурные решения.
## Архитектурное ревью
На ревью проверяется смысл кода, который нельзя надёжно вывести из файловой системы:
- одна ли связная ответственность находится внутри модуля;
- есть ли у каждой самостоятельной ответственности ровно один владелец;
- соответствует ли ответственность роли выбранного слоя;
- не стали ли группа, сегмент или компонент скрытыми владельцами;
- нужен ли каждый экспорт реальному внешнему потребителю;
- не раскрывает ли публичный API изменяемые внутренние механизмы;
- определены ли владелец, область жизни, число экземпляров и очистка каждого ресурса;
- не переносится ли владение из-за места вызова, провайдера фреймворка или точки маршрута.
Окончательные смысловые требования имеют класс `R` в [реестре правил](../rules/registry.md).
## Автоматическая проверка
Проект сопоставляет физические пути с SLM root, слоями, группами, модулями, вложенными модулями, сегментами, фасетами, точками входа `app` и ресурсами `shared`. Сопоставление задаётся локальной конфигурацией и не меняет смысл сущностей.
Наличие `index.ts` само по себе не объявляет модуль. Проверка отличает объявленные корни модулей и их публичные фасеты от локальных точек входа компонентов и других внутренних единиц.
Автоматически проверяются:
- допустимое направление импортов по матрице слоёв;
- отдельная папка каждого модуля;
- доступ к чужому модулю только через объявленные фасеты;
- отсутствие циклов между модулями;
- отсутствие прямого внешнего доступа к вложенным модулям;
- допустимый транзитивный граф исполняемых импортов каждого фасета среды выполнения;
- динамическое подключение `browser`-фасета с отключённым SSR.
Каждое правило класса `A` должно полностью блокировать проверку при нарушении. SLM не требует конкретного lint-инструмента.
## Проверка зависимостей
Для каждого внешнего импорта определяется:
1. Модуль-владелец исходного файла.
2. Модуль-владелец целевого файла.
3. Слои исходного и целевого владельцев.
4. Публичный фасет, через который выполнен импорт.
5. Отсутствие цикла после добавления связи.
Импорты типов (`import type`) и реэкспорты проверяются как архитектурные связи. Относительные импорты внутри одного модуля не пересекают модульную границу.
## Проверка фасетов
Совместимость фасета определяется всем достижимым исполняемым кодом, а не только его собственным файлом.
Проверка подтверждает:
- `index` не достигает `client`, `browser` или `server`;
- `client` не достигает `browser` или `server`;
- `browser` и `server` не достигают друг друга;
- `browser` доступен только через поддерживаемую динамическую границу без SSR;
- специализированный фасет существует ради реального потребителя;
- один исполняемый экспорт не дублируется между фасетами.
Импорт типа остаётся архитектурной зависимостью, но не добавляет исполняемый код в среду фасета.
## Критерий завершения
Изменение соответствует SLM, когда одновременно выполнены условия:
- ответственность и единственный владелец определены;
- роль слоя соответствует ответственности;
- публичный API минимален и используется всеми внешними потребителями;
- зависимости разрешены и не образуют циклов;
- группа и сегменты не подменяют модульную границу;
- состояние и ресурсы имеют владельца и корректную область жизни;
- физическая структура однозначно выражает принятое решение;
- применимые автоматические проверки и архитектурное ревью пройдены.

62
docs/rules/README.md Normal file
View File

@@ -0,0 +1,62 @@
# Правила SLM
Правило SLM задаёт один блокирующий архитектурный инвариант. Точные формулировки правил находятся только в [едином реестре](./registry.md); архитектурные главы объясняют модель и ссылаются на соответствующие коды.
## Виды утверждений
- **Определение** задаёт нормативный смысл термина.
- **Правило** задаёт блокирующее требование.
- **Рекомендация** помогает принять решение, но не является обязательной.
- **Пример** показывает один из вариантов реализации и не задаёт каркас проекта.
Определения собраны в [терминологии](../reference/terminology.md). Определение может быть обязательным для толкования правил, но не получает отдельный код.
## Код правила
```text
SLM-{group}-{class}{number}
```
| Часть | Значение |
|---|---|
| `SLM` | Принадлежность архитектуре SLM |
| `group` | Предмет правила |
| `class` | Способ окончательной проверки: `A` или `R` |
| `number` | Глобально уникальный трёхзначный номер |
Пример: [`SLM-MODULE-A004`](./registry.md#slm-module-a004).
## Способ проверки
### Автоматические правила (`A`)
Всё требование можно однозначно проверить программно по структуре проекта, публичным путям и графу импортов. Нарушение блокирует автоматическую проверку.
### Правила для ревью (`R`)
Для окончательного решения требуется понимание ответственности, владельца, потребителей или области жизни. Инструмент может найти подозрительный код, но не заменяет архитектурное решение.
## Разделы правил
| Код | Предмет |
|---|---|
| `LAYER` | Роль слоя и направление зависимостей |
| `MODULE` | Ответственность, владение и публичная граница модуля |
| `DEPENDENCY` | Граф зависимостей модулей |
| `GROUP` | Навигационная группировка модулей |
| `SEGMENT` | Внутренняя организация модуля |
| `COMPONENT` | Принадлежность компонента модулю |
| `NESTED_MODULE` | Доступ к вложенному модулю |
| `LIFECYCLE` | Владение долгоживущими ресурсами |
| `ENVIRONMENT` | Совместимость публичных фасетов со средами выполнения |
Раздел правила не создаёт одноимённую главу или дополнительный уровень архитектуры. Например, `GROUP` классифицирует правило о группировке, а сама группа остаётся необязательной частью слоя.
## Требования к реестру
- Одно правило защищает один инвариант.
- Точная формулировка не повторяется в тематических документах.
- Название кратко обозначает предмет, а описание полностью формулирует требование.
- Рекомендации, обоснования и примеры не входят в формулировку правила.
- Один инвариант не получает отдельные автоматическую и ручную копии.
- Номер правила не обозначает важность и не переиспользуется после удаления.

129
docs/rules/registry.md Normal file
View File

@@ -0,0 +1,129 @@
# Реестр правил SLM
Здесь собраны правила SLM. Это единственное место, где они формулируются; тематические документы объясняют архитектуру и ссылаются на коды.
## Размещение кода по слоям
### SLM-LAYER-R001
> **Назначение слоёв**
>
> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода.
### SLM-LAYER-A002
> **Направление зависимостей**
>
> Внутри одного SLM root код каждого слоя может зависеть только от кода целевых слоёв, разрешённых для него нормативной матрицей слоёв.
### SLM-LAYER-R003
> **Граница слоя `app`**
>
> В `app` размещаются только точки входа фреймворка для запуска, маршрутов, преобразования входных данных и подключения публичных API модулей разрешённых слоёв или ресурсов `shared`; ответственности этих модулей остаются за пределами `app`.
## Границы модулей
### SLM-MODULE-A004
> **Публичный API модуля**
>
> Каждый модуль предоставляет единый логический публичный API через обязательный корневой фасет `index` и, при необходимости, фасеты `client`, `browser` и `server`; код за пределами модуля импортирует его содержимое только через эти фасеты.
### SLM-MODULE-A014
> **Папка модуля**
>
> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки.
### SLM-MODULE-R006
> **Ответственность модуля**
>
> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях.
### SLM-MODULE-R011
> **Владелец ответственности**
>
> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`.
### SLM-MODULE-R012
> **Состав публичного API**
>
> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми.
## Зависимости между модулями
### SLM-DEPENDENCY-A005
> **Циклические зависимости**
>
> Зависимости между модулями внутри одного SLM root, включая вложенные модули, не образуют циклов.
## Назначение групп
### SLM-GROUP-R007
> **Назначение группы**
>
> Группа содержит только модули и другие группы, не владеет файлами реализации, состоянием, жизненным циклом или публичным API и не импортируется внешним кодом.
## Назначение сегментов
### SLM-SEGMENT-R008
> **Граница сегмента**
>
> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или границы зависимостей.
## Ответственность компонентов
### SLM-COMPONENT-R009
> **Ответственность компонента**
>
> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу.
## Границы вложенных модулей
### SLM-NESTED_MODULE-A010
> **Доступ к вложенному модулю**
>
> Код за пределами родительского модуля не импортирует вложенный модуль напрямую и получает его экспорты только через публичный API родителя.
## Жизненный цикл
### SLM-LIFECYCLE-R013
> **Жизненный цикл ресурсов**
>
> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
## Границы сред выполнения
### SLM-ENVIRONMENT-R016
> **Универсальный фасет**
>
> Корневой фасет `index` экспортирует только публичный код, совместимый как с серверным рендерингом, включая RSC, так и с клиентским выполнением, и не импортирует или реэкспортирует код фасетов `client`, `browser` или `server` прямо либо транзитивно.
### SLM-ENVIRONMENT-R017
> **Клиентский фасет**
>
> Фасет `client` экспортирует только клиентский код, который не может выполняться как RSC, и не импортирует или реэкспортирует код фасетов `browser` или `server` прямо либо транзитивно.
### SLM-ENVIRONMENT-R018
> **Браузерный фасет**
>
> Фасет `browser` экспортирует только browser-only код, а потребители импортируют его только динамически с отключённым SSR.
### SLM-ENVIRONMENT-R019
> **Серверный фасет**
>
> Фасет `server` экспортирует только server-only код и не импортируется или реэкспортируется фасетами `index`, `client` или `browser` прямо либо транзитивно.

View File

@@ -8,10 +8,8 @@
"check": "npm run test:skill && npm run check:skill && npm run check:site",
"check:skill": "node scripts/check-skill.mjs",
"test:skill": "node --test tests/skill-bundle.test.mjs",
"check:draft-rules": "node draft-rules.js",
"check:docs": "npm run check:draft-rules",
"check:docs-all": "npm run check:site",
"check:site": "npm run check:draft-rules && npm run docs:build && node scripts/check-site.mjs",
"check:docs": "node scripts/check-docs.mjs",
"check:site": "npm run check:docs && npm run docs:build && node scripts/check-site.mjs",
"docs:dev": "vitepress dev site",
"docs:build": "vitepress build site",
"docs:preview": "vitepress preview site"

View File

@@ -2,11 +2,11 @@ import { readdir, readFile } from 'node:fs/promises'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const rootDirectory = dirname(fileURLToPath(import.meta.url))
const draftDirectory = join(rootDirectory, 'DRAFT')
const rulesDirectory = join(draftDirectory, 'rules')
const ruleCodeSource = 'SLM-L\\d+-[A-Z][A-Z_]{1,31}-[AR]\\d{3}'
const ruleHeadingPattern = new RegExp(`^###\\s+(?<code>SLM-L(?<level>\\d+)-(?<group>[A-Z][A-Z_]{1,31})-(?<classification>[AR])(?<number>\\d{3}))$`)
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const docsDirectory = join(repositoryRoot, 'docs')
const rulesDirectory = join(docsDirectory, 'rules')
const ruleCodeSource = 'SLM-[A-Z][A-Z_]{1,31}-[AR]\\d{3}'
const ruleHeadingPattern = new RegExp(`^###\\s+(?<code>SLM-(?<group>[A-Z][A-Z_]{1,31})-(?<classification>[AR])(?<number>\\d{3}))$`)
const ruleCodePattern = new RegExp(`\\b${ruleCodeSource}\\b`, 'g')
const ruleReferencePattern = new RegExp(
'\\[`(?<code>' + ruleCodeSource + ')`\\]\\((?<target>[^)\\s]+)\\)',
@@ -68,7 +68,7 @@ const parseRules = async (file) => {
const match = line.match(ruleHeadingPattern)
if (match?.groups) {
const source = `${relative(rootDirectory, file)}:${index + 1}`
const source = `${relative(repositoryRoot, file)}:${index + 1}`
const titleMatch = lines[index + 2]?.match(ruleTitlePattern)
const descriptionMatch = lines[index + 4]?.match(ruleDescriptionPattern)
@@ -93,13 +93,12 @@ const parseRules = async (file) => {
classification: match.groups.classification,
description: descriptionMatch.groups.description.trim(),
file,
level: Number(match.groups.level),
number: Number(match.groups.number),
title: titleMatch.groups.title.trim(),
source,
})
} else if (/^#{1,6}\s+SLM-L/.test(line)) {
throw new Error(`Invalid SLM rule heading at ${relative(rootDirectory, file)}:${index + 1}`)
} else if (/^#{1,6}\s+SLM-/.test(line)) {
throw new Error(`Invalid SLM rule heading at ${relative(repositoryRoot, file)}:${index + 1}`)
}
})
@@ -112,10 +111,10 @@ const parseReferences = async (file) => {
const references = []
visitMarkdownLines(lines, (line, index) => {
const source = `${relative(rootDirectory, file)}:${index + 1}`
const source = `${relative(repositoryRoot, file)}:${index + 1}`
if (/^#{1,6}\s+SLM-L/.test(line)) {
throw new Error(`SLM rule declaration is only allowed in DRAFT/rules: ${source}`)
if (/^#{1,6}\s+SLM-/.test(line)) {
throw new Error(`SLM rule declaration is only allowed in docs/rules: ${source}`)
}
const codeMatches = [...line.matchAll(ruleCodePattern)]
@@ -167,11 +166,11 @@ const printSection = (title, rules) => {
}
const ruleFiles = await getMarkdownFiles(rulesDirectory)
const draftFiles = (await getMarkdownFiles(draftDirectory))
const documentationFiles = (await getMarkdownFiles(docsDirectory))
.filter((file) => !ruleFiles.includes(file))
const rules = (await Promise.all(ruleFiles.map(parseRules))).flat()
const rulesByCode = new Map()
const rulesByLevelNumber = new Map()
const rulesByNumber = new Map()
for (const rule of rules) {
const duplicate = rulesByCode.get(rule.code)
@@ -182,19 +181,18 @@ for (const rule of rules) {
rulesByCode.set(rule.code, rule)
const levelNumber = `${rule.level}:${rule.number}`
const duplicateNumber = rulesByLevelNumber.get(levelNumber)
const duplicateNumber = rulesByNumber.get(rule.number)
if (duplicateNumber) {
throw new Error(
`Duplicate SLM rule number L${rule.level}-${String(rule.number).padStart(3, '0')}: ${duplicateNumber.source}, ${rule.source}`,
`Duplicate SLM rule number ${String(rule.number).padStart(3, '0')}: ${duplicateNumber.source}, ${rule.source}`,
)
}
rulesByLevelNumber.set(levelNumber, rule)
rulesByNumber.set(rule.number, rule)
}
const references = (await Promise.all(draftFiles.map(parseReferences))).flat()
const references = (await Promise.all(documentationFiles.map(parseReferences))).flat()
const referencedCodes = new Set()
for (const reference of references) {
@@ -213,13 +211,12 @@ for (const reference of references) {
for (const rule of rules) {
if (!referencedCodes.has(rule.code)) {
throw new Error(`SLM rule ${rule.code} is not referenced by any draft`)
throw new Error(`SLM rule ${rule.code} is not referenced by any documentation page`)
}
}
rules.sort((left, right) => (
left.level - right.level
|| left.number - right.number
left.number - right.number
|| left.code.localeCompare(right.code)
))

View File

@@ -7,43 +7,20 @@ const distRoot = path.join(repositoryRoot, 'site', '.vitepress', 'dist')
const siteOrigin = 'https://site.test'
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, 'docs', 'rules', 'registry.md'), route: 'rules/registry' },
]
const expectedPages = [
'404.html',
'index.html',
'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',
'level-1/segments.html',
'level-1/components.html',
'level-1/nested-modules.html',
'level-1/lifecycle.html',
'level-1/validation.html',
'level-2/index.html',
'level-2/terminology.html',
'level-2/dependencies.html',
'level-2/validation.html',
'level-2/domains/index.html',
'level-2/domains/domain-package.html',
'level-2/domains/domain-api.html',
'level-2/domains/factory-ports-adapters.html',
'level-2/domains/assemblies.html',
'level-2/domains/state-cache.html',
'level-2/domains/framework-bindings.html',
'level-2/domains/realtime.html',
'level-2/domains/testing.html',
'level-2/domains/auth-example.html',
'level-2/domains/open-questions.html',
'architecture/index.html',
'architecture/layers.html',
'architecture/modules.html',
'architecture/segments.html',
'reference/terminology.html',
'reference/validation.html',
'rules/index.html',
'rules/level-1.html',
'rules/level-2.html',
'rules/registry.html',
].sort()
async function collectHtmlFiles(directory, prefix = '') {
@@ -135,7 +112,7 @@ let ruleCount = 0
for (const registry of ruleRegistries) {
const rulesMarkdown = await readFile(registry.source, 'utf8')
const ruleIds = [...rulesMarkdown.matchAll(/^### (SLM-L\d+-[A-Z_]+-[AR]\d{3})$/gm)]
const ruleIds = [...rulesMarkdown.matchAll(/^### (SLM-[A-Z_]+-[AR]\d{3})$/gm)]
.map((match) => match[1])
const rulesHtml = htmlByPage.get(`${registry.route}.html`)
@@ -156,12 +133,43 @@ for (const registry of ruleRegistries) {
}
const notFoundHtml = htmlByPage.get('404.html')
if (!notFoundHtml.includes('Страница не найдена')) {
if (!notFoundHtml.includes('Такой страницы нет')) {
throw new Error('404 page is not localized')
}
const homeHtml = htmlByPage.get('index.html')
if (!homeHtml.includes('Архитектура владения ответственностями')) {
throw new Error('Home page does not render the documentation-owned hero')
}
if ([...homeHtml.matchAll(/<h1\b/g)].length !== 1) {
throw new Error('Home page must have exactly one primary heading')
}
for (const [relativePath, html] of htmlByPage) {
for (const obsoleteText of [
'Последовательная архитектура',
'Рабочий черновик архитектуры',
'edit/master/DRAFT/',
]) {
if (html.includes(obsoleteText)) {
throw new Error(`${relativePath} contains obsolete copy: ${obsoleteText}`)
}
}
}
const sitemap = await readFile(path.join(distRoot, 'sitemap.xml'), 'utf8')
for (const forbiddenRoute of ['/ru/', '/specification/']) {
for (const forbiddenRoute of [
'/ru/',
'/specification/',
'/architecture/domains',
'/architecture/dependencies',
'/architecture/groups',
'/architecture/components',
'/architecture/nested-modules',
'/architecture/lifecycle',
'/architecture/validation',
]) {
if (sitemap.includes(forbiddenRoute)) {
throw new Error(`Sitemap contains archival or excluded route ${forbiddenRoute}`)
}

View File

@@ -7,63 +7,39 @@ const viteConfigPath = fileURLToPath(new URL('../vite.config.mts', import.meta.u
const documentationSidebar = [
{
text: 'SLM Level 1',
text: 'Архитектурная модель',
items: [
{ 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' },
{ text: 'Сегменты', link: '/level-1/segments' },
{ text: 'Компоненты', link: '/level-1/components' },
{ text: 'Вложенные модули', link: '/level-1/nested-modules' },
{ text: 'Жизненный цикл', link: '/level-1/lifecycle' },
{ text: 'Проверка', link: '/level-1/validation' },
{ text: 'Владение и структура', link: '/architecture/' },
{ text: 'Слои и группы', link: '/architecture/layers' },
{ text: 'Модули и границы', link: '/architecture/modules' },
{ text: 'Сегменты', link: '/architecture/segments' },
],
},
{
text: 'SLM Level 2',
text: 'Нормативная часть',
items: [
{ text: 'Обзор', link: '/level-2/' },
{ text: 'Терминология', link: '/level-2/terminology' },
{ text: 'Доменные пакеты', link: '/level-2/domains/' },
{ text: 'Граница пакета', link: '/level-2/domains/domain-package' },
{ text: 'Domain API', link: '/level-2/domains/domain-api' },
{ text: 'Factories, ports и adapters', link: '/level-2/domains/factory-ports-adapters' },
{ text: 'Assemblies и default', link: '/level-2/domains/assemblies' },
{ text: 'Состояние и кэш', link: '/level-2/domains/state-cache' },
{ text: 'Framework Groups', link: '/level-2/domains/framework-bindings' },
{ text: 'Realtime', link: '/level-2/domains/realtime' },
{ text: 'Зависимости', link: '/level-2/dependencies' },
{ text: 'Тестирование', link: '/level-2/domains/testing' },
{ text: 'Проверка', link: '/level-2/validation' },
{ text: 'Переход auth', link: '/level-2/domains/auth-example' },
{ text: 'Открытые вопросы', link: '/level-2/domains/open-questions' },
{ text: 'Устройство правил', link: '/rules/' },
{ text: 'Реестр правил', link: '/rules/registry' },
],
},
{
text: 'Правила',
text: 'Справочные материалы',
items: [
{ text: 'Как устроены правила', link: '/rules/' },
{ text: 'Реестр Level 1', link: '/rules/level-1' },
{ text: 'Реестр Level 2', link: '/rules/level-2' },
{ text: 'Терминология', link: '/reference/terminology' },
{ text: 'Проверка архитектуры', link: '/reference/validation' },
],
},
]
export default defineConfig({
srcDir: '../DRAFT',
srcExclude: ['README.md'],
srcDir: '../docs',
rewrites: {
'level-1/README.md': 'level-1/index.md',
'level-2/README.md': 'level-2/index.md',
'level-2/domains/README.md': 'level-2/domains/index.md',
'README.md': 'index.md',
'architecture/README.md': 'architecture/index.md',
'rules/README.md': 'rules/index.md',
},
title: 'SLM Design',
description: 'Последовательная архитектура фронтенд-приложений SLM',
description: 'Архитектура фронтенд-приложений с явным владением ответственностями',
lang: 'ru-RU',
base: '/slm-design/',
cleanUrls: true,
@@ -87,9 +63,9 @@ export default defineConfig({
logo: '/logo.svg',
siteTitle: 'SLM Design',
nav: [
{ text: 'Level 1', link: '/level-1/' },
{ text: 'Level 2', link: '/level-2/' },
{ text: 'Архитектура', link: '/architecture/' },
{ text: 'Правила', link: '/rules/' },
{ text: 'Проверка', link: '/reference/validation' },
],
sidebar: documentationSidebar,
socialLinks: [{ icon: 'github', link: repositoryUrl }],
@@ -100,14 +76,14 @@ export default defineConfig({
disableQueryPersistence: true,
translations: {
button: {
buttonText: 'Поиск по документации',
buttonAriaLabel: 'Поиск по документации или коду правила',
buttonText: 'Найти в SLM',
buttonAriaLabel: 'Искать по архитектуре, терминам и правилам',
},
modal: {
displayDetails: 'Показать подробности',
resetButtonTitle: 'Сбросить поиск',
backButtonTitle: 'Закрыть поиск',
noResultsText: 'Ничего не найдено',
displayDetails: 'Показать контекст',
resetButtonTitle: 'Очистить запрос',
backButtonTitle: 'Закрыть',
noResultsText: 'Совпадений нет',
footer: {
selectText: 'выбрать',
navigateText: 'перейти',
@@ -117,25 +93,25 @@ export default defineConfig({
},
},
},
outline: { level: [2, 3], label: 'На этой странице' },
outline: { level: [2, 3], label: 'В этом разделе' },
notFound: {
code: '404',
title: 'Страница не найдена',
quote: 'Запрошенная страница отсутствует в опубликованной документации SLM.',
linkLabel: 'Перейти на главную',
linkText: 'Вернуться к документации',
title: 'Такой страницы нет',
quote: 'Этот путь не относится к текущей структуре документации.',
linkLabel: 'Открыть главную страницу',
linkText: 'К началу документации',
},
editLink: {
pattern: `${repositoryUrl}/edit/master/DRAFT/:path`,
text: 'Предложить изменение',
pattern: `${repositoryUrl}/edit/master/docs/:path`,
text: 'Уточнить документацию',
},
lastUpdated: {
text: 'Обновлено',
text: 'Последнее изменение',
formatOptions: { dateStyle: 'medium' },
},
docFooter: {
prev: 'Предыдущая страница',
next: 'Следующая страница',
prev: 'Назад',
next: 'Далее',
},
darkModeSwitchLabel: 'Оформление',
lightModeSwitchTitle: 'Светлая тема',
@@ -144,8 +120,8 @@ export default defineConfig({
returnToTopLabel: 'Наверх',
skipToContentLabel: 'Перейти к содержанию',
footer: {
message: 'SLM Levels 1-2',
copyright: 'Рабочий черновик архитектуры.',
message: 'Документация SLM Design',
copyright: 'Открытая архитектурная модель',
},
},
})

View File

@@ -71,7 +71,7 @@ html {
font-size: clamp(2rem, 4vw, 2.65rem);
}
.vp-doc h3[id^='slm-l'] {
.vp-doc h3[id^='slm-'] {
margin-top: 34px;
scroll-margin-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height) + 24px);
color: var(--vp-c-brand-1);
@@ -81,7 +81,7 @@ html {
letter-spacing: -0.02em;
}
.vp-doc h3[id^='slm-l'] + blockquote {
.vp-doc h3[id^='slm-'] + blockquote {
position: relative;
margin: 10px 0 24px;
padding: 42px 20px 18px;
@@ -94,7 +94,7 @@ html {
line-height: 1.7;
}
.vp-doc h3[id^='slm-l'] + blockquote::before {
.vp-doc h3[id^='slm-'] + blockquote::before {
position: absolute;
top: 14px;
left: 20px;
@@ -106,23 +106,23 @@ html {
text-transform: uppercase;
}
.vp-doc h3[id^='slm-l'][id*='-a'] + blockquote::before {
content: 'Автоматическая проверка';
.vp-doc h3[id^='slm-'][id*='-a'] + blockquote::before {
content: 'Проверяется автоматически';
}
.vp-doc h3[id^='slm-l'][id*='-r'] + blockquote::before {
content: 'Проверка на ревью';
.vp-doc h3[id^='slm-'][id*='-r'] + blockquote::before {
content: 'Требует ревью';
}
.vp-doc h3[id^='slm-l'] + blockquote > p {
.vp-doc h3[id^='slm-'] + blockquote > p {
margin: 0;
}
.vp-doc h3[id^='slm-l'] + blockquote > p + p {
.vp-doc h3[id^='slm-'] + blockquote > p + p {
margin-top: 8px;
}
.vp-doc h3[id^='slm-l'] + blockquote strong:first-child {
.vp-doc h3[id^='slm-'] + blockquote strong:first-child {
color: var(--vp-c-text-1);
font-weight: 700;
}
@@ -132,7 +132,7 @@ html {
font-size: 16px;
}
.vp-doc h3[id^='slm-l'] + blockquote {
.vp-doc h3[id^='slm-'] + blockquote {
margin-inline: -8px;
padding: 42px 14px 14px;
}

View File

@@ -1,17 +1,20 @@
# Сайт SLM Design
`site/` содержит конфигурацию, тему и статические ресурсы VitePress.
`site/` содержит только VitePress-конфигурацию, тему и статические ресурсы. Сайт не владеет документацией и не хранит её содержимое.
Источником опубликованной документации служит [`DRAFT`](../DRAFT/index.md). Сайт включает рабочие черновики Levels 1-2 и их правила.
Владельцем опубликованного содержания является [`docs/`](../docs/README.md). VitePress читает Markdown-файлы непосредственно из этой папки и рендерит их без отдельной копии внутри `site/`.
## Маршруты
- `/` - главная страница;
- `/level-1/` - документация Level 1;
- `/level-2/` - документация Level 2;
- `/architecture/` - владение и структурная модель;
- `/architecture/layers` - слои и группы;
- `/architecture/modules` - модули и публичные границы;
- `/architecture/segments` - внутренняя организация модулей;
- `/rules/` - устройство правил;
- `/rules/level-1` - канонический реестр Level 1.
- `/rules/level-2` - канонический реестр Level 2.
- `/rules/registry` - единый реестр правил;
- `/reference/terminology` - нормативные определения;
- `/reference/validation` - проверка архитектуры.
## Локальный запуск
@@ -25,4 +28,4 @@ npm run docs:dev
npm run check:site
```
Команда проверяет реестр правил, собирает VitePress и проверяет опубликованные маршруты и якоря правил.
Команда проверяет документацию в `docs/`, собирает VitePress и валидирует опубликованные маршруты, ссылки, поисковый индекс и якоря правил.