mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 15:30:16 +03:00
feat: add example
This commit is contained in:
612
.opencode/skills/slm-design/SKILL.md
Normal file
612
.opencode/skills/slm-design/SKILL.md
Normal file
@@ -0,0 +1,612 @@
|
||||
---
|
||||
name: slm-design
|
||||
description: "Используй при проектировании, реализации, миграции и архитектурном ревью по SLM: когда нужно определить SLM root, ответственность, владельца, слой, модульную границу, публичный API, форму домена Level 1 или Level 2, направление зависимостей, Domain API, business factory, adapter, assembly, framework binding, state/cache/error/lifecycle ownership или исправить deep import, цикл и environment leak. Не используй для локального coding, debugging, форматирования, framework- или SDK-механики, если архитектурная граница уже определена и не меняется."
|
||||
---
|
||||
|
||||
# SLM Design
|
||||
|
||||
## Рабочий контракт
|
||||
|
||||
Применяй SLM как способ выполнить пользовательскую задачу, а не как тему для пересказа. После чтения этого файла ты должен уметь принять типовое архитектурное решение, реализовать его в запрошенном scope и проверить результат. Открывай references только для точной формулировки правила, редкого случая или неразрешённого вопроса.
|
||||
|
||||
Работай в таком порядке:
|
||||
|
||||
1. Исследуй существующий код и локальные правила проекта.
|
||||
2. Определи ответственность, владельца и минимальный scope.
|
||||
3. Выбери слой, архитектурную сущность и форму домена.
|
||||
4. Спроектируй публичную границу, зависимости, runtime-сборку и lifecycle.
|
||||
5. До редактирования проверь решение по применимым правилам.
|
||||
6. Если пользователь запросил реализацию, внеси изменения до завершённого состояния.
|
||||
7. Проверь импорты, exports, граф, среды, lifecycle и тесты.
|
||||
8. Кратко сообщи решение, сделанные изменения, проверки, assumptions и остаточные риски.
|
||||
|
||||
Не начинай широкое перемещение кода или генерацию каркаса до шагов 1-5. Не расширяй задачу до полного аудита SLM root, если локальное изменение можно корректно выполнить в меньшем scope.
|
||||
|
||||
## Источники и обязательность
|
||||
|
||||
Bundled DRAFT является рабочим источником истины для этой версии skill, но остаётся черновиком архитектуры. Используй источники в следующем порядке:
|
||||
|
||||
1. [`rules/level-1.md`](./reference/draft/rules/level-1.md) и [`rules/level-2.md`](./reference/draft/rules/level-2.md) - единственный источник блокирующих правил.
|
||||
2. [`level-1/terminology.md`](./reference/draft/level-1/terminology.md) и [`level-2/terminology.md`](./reference/draft/level-2/terminology.md) - обязательный смысл терминов.
|
||||
3. README уровней - область применения, наследование и замены правил.
|
||||
4. Тематические главы - объяснения, рекомендации и варианты проектирования.
|
||||
5. Примеры - иллюстрации, а не обязательный каркас.
|
||||
6. `open-questions.md` - нерешённые вопросы, а не требования.
|
||||
|
||||
Если тематическая глава строже реестра, не создавай из неё новое блокирующее правило. Предложи более строгую форму как рекомендацию или уточни локальную policy, если выбор влияет на API, ownership, стоимость или runtime. Если этот файл расходится с реестром или нормативной терминологией, следуй bundled DRAFT и отметь дефект skill.
|
||||
|
||||
При review различай:
|
||||
|
||||
- **Rule violation** - нарушено применимое правило с существующим кодом SLM.
|
||||
- **Definition mismatch** - реализация не соответствует нормативному смыслу сущности.
|
||||
- **Architectural risk** - есть доказуемый риск, но нет блокирующего правила.
|
||||
- **Decision required** - DRAFT или проект оставляет значимый выбор открытым.
|
||||
- **Recommendation** - улучшение, которое не является обязательным.
|
||||
- **Assumption** - обратимое рабочее допущение, явно указанное в результате.
|
||||
|
||||
Не придумывай коды правил. Перед ссылкой на нарушение открой соответствующий реестр и проверь точную формулировку.
|
||||
|
||||
## Минимальная рабочая модель
|
||||
|
||||
### SLM root и уровни
|
||||
|
||||
SLM root - граница структурной архитектуры одного приложения. Сначала найди фактический root, path aliases, локальный стайлгайд и конфигурацию архитектурной проверки. Не считай `src` root автоматически и не выводи сущность только из имени папки.
|
||||
|
||||
Level 1 действует во всём SLM root и задаёт слои, модули, публичные API, общий dependency DAG и владение lifecycle.
|
||||
|
||||
Level 2 применяется отдельно к выбранной предметной области и заменяет только её доменный модуль пакетной формой. Остальные домены могут постоянно оставаться на Level 1. Одна предметная область имеет ровно одну итоговую форму.
|
||||
|
||||
### Слои
|
||||
|
||||
| Исходный слой | Может зависеть от |
|
||||
|---|---|
|
||||
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
|
||||
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
|
||||
| `domains` | `domains`, `infra`, `ui`, `shared` |
|
||||
| `infra` | `infra`, `shared` |
|
||||
| `ui` | `ui`, `shared` |
|
||||
| `shared` | `shared` |
|
||||
|
||||
Матрица не требует проходить через каждый промежуточный слой. Разрешённый импорт не переносит владение ответственностью.
|
||||
|
||||
| Слой | Помещай сюда |
|
||||
|---|---|
|
||||
| `app` | Framework entry points: запуск, routes, преобразование внешнего input и подключение готовых API |
|
||||
| `compositions` | Pages, layouts, screens, widgets, route outcomes и multi-domain UI |
|
||||
| `domains` | Предметные модели, правила, сценарии и продуктовое состояние |
|
||||
| `infra` | Универсальные технические capabilities без собственной предметной модели |
|
||||
| `ui` | Универсальные UI-модули без зависимости от продуктовой композиции |
|
||||
| `shared` | Детерминированный product-agnostic фундамент без I/O, mutable state и lifecycle |
|
||||
|
||||
### Архитектурные сущности
|
||||
|
||||
| Признак | Сущность |
|
||||
|---|---|
|
||||
| Самостоятельная ответственность со своим API, dependencies, state или lifecycle | Module |
|
||||
| Только навигационно классифицирует modules и Groups | Group |
|
||||
| Организует внутренности одного module | Segment |
|
||||
| Framework UI entity, реализующая часть ответственности родителя | Component |
|
||||
| Самостоятельный module, скрытый внутри parent module | Nested module |
|
||||
| Framework bootstrap или route entry | Немодульная единица `app` |
|
||||
| Малый deterministic product-agnostic файл без внутренней границы | Shared resource |
|
||||
|
||||
Module является узлом dependency graph, размещается в отдельной папке и имеет единый логический публичный API. Group, segment и component не владеют API, состоянием или lifecycle. Наличие локального `index.ts`, нескольких файлов, hook, data access или lifecycle-кода само по себе не превращает component или segment в module: всё это принадлежит ближайшему module-owner.
|
||||
|
||||
Nested module имеет собственную ответственность, API и узел графа, но внешний код получает его exports только через публичный API parent module.
|
||||
|
||||
### Пакетная форма Level 2
|
||||
|
||||
Минимальная структура доменного пакета:
|
||||
|
||||
```text
|
||||
domains/<domain>/
|
||||
├── metadata # optional, declarative only
|
||||
├── business/ # required SLM module
|
||||
├── assemblies/ # required non-empty Group
|
||||
├── adapters/ # when factories have technical dependencies
|
||||
└── react|vue|... # when domain-specific bindings exist
|
||||
```
|
||||
|
||||
Доменный пакет является policy boundary, но не module, Group, public API или graph node. В его корне нет executable files, state, lifecycle, barrel или реэкспортов. Исполняемыми владельцами являются модули внутри пакета.
|
||||
|
||||
`business` является единственным предметным владельцем пакета. Его публичный API состоит из фасетов:
|
||||
|
||||
| Путь | Содержимое |
|
||||
|---|---|
|
||||
| `business` | Только public types: Domain API, dependencies, factory types, error types |
|
||||
| `business/factory` | Только именованные runtime factories, по одной на Domain API |
|
||||
| `business/runtime` | Только реально нужные внешним consumers детерминированные runtime values/functions |
|
||||
|
||||
Другой публичный путь внутрь `business` является deep import. `business/runtime` не создавай для симметрии.
|
||||
|
||||
Роли Level 2:
|
||||
|
||||
- `business` определяет Domain API, модели, validation, transitions, scenario results, dependency contracts и expected domain errors.
|
||||
- Adapter module реализует связанные technical dependencies поверх SDK, storage, platform API, state/query runtime или другого technical runtime.
|
||||
- Assembly module выбирает adapters, вызывает factories и возвращает именованный graph готовых API для одного execution context.
|
||||
- Framework binding module получает готовые Domain API и владеет одной domain-specific интеграцией с framework.
|
||||
- Composition, `app`, request handler или test setup собирает междоменный graph в ацикличном порядке и владеет его общим scope.
|
||||
|
||||
## Универсальный цикл решения
|
||||
|
||||
### 1. Discover
|
||||
|
||||
Перед решением найди только релевантный контекст:
|
||||
|
||||
- локальные инструкции и стайлгайд;
|
||||
- SLM root и mapping путей на слои и модули;
|
||||
- существующие public entry points и package exports;
|
||||
- внешних consumers затрагиваемой границы;
|
||||
- runtime- и type-only imports, реэкспорты и aliases;
|
||||
- state, I/O, SDK, framework runtime и источники недетерминизма;
|
||||
- места создания graph и instances;
|
||||
- subscriptions, timers, requests, connections и cleanup;
|
||||
- тесты и команды проверки затрагиваемых owners.
|
||||
|
||||
Считай type-only import и reexport архитектурным ребром. Для runtime-графа дополнительно ищи arguments factories, callbacks, registries, event buses, service locators и singletons: фактическая зависимость может не иметь прямого runtime import.
|
||||
|
||||
### 2. Classify
|
||||
|
||||
Сформулируй краткую внутреннюю карточку:
|
||||
|
||||
```text
|
||||
Task outcome:
|
||||
Responsibility:
|
||||
Owner:
|
||||
Layer:
|
||||
Entity:
|
||||
Domain form:
|
||||
Public consumers:
|
||||
Runtime dependencies:
|
||||
Environment:
|
||||
State and lifecycle:
|
||||
Change scope:
|
||||
```
|
||||
|
||||
Не обязан показывать карточку пользователю, если решение однозначно. Если одно из ключевых полей неизвестно и влияет на границу, сначала исследуй код, затем задай один конкретный вопрос.
|
||||
|
||||
### 3. Design boundary
|
||||
|
||||
Определи:
|
||||
|
||||
- один owner каждой самостоятельной ответственности;
|
||||
- минимальный публичный контракт для реальных consumers;
|
||||
- разрешённые static edges;
|
||||
- runtime injection и место сборки graph;
|
||||
- владельцев domain state, technical cache и framework projection;
|
||||
- безопасную форму expected errors;
|
||||
- environment entry points и их transitive reachability;
|
||||
- scope, multiplicity и cleanup каждого lifecycle resource;
|
||||
- тестовую границу каждого изменяемого owner.
|
||||
|
||||
### 4. Validate before edits
|
||||
|
||||
До изменения файлов ответь:
|
||||
|
||||
- Соответствует ли ответственность роли слоя?
|
||||
- Является ли выбранная сущность настоящим owner, а не удобной папкой?
|
||||
- Есть ли у domain одна форма?
|
||||
- Импортируется ли каждый чужой module через public API?
|
||||
- Разрешены ли layer и cross-domain edges?
|
||||
- Остаётся ли graph ацикличным?
|
||||
- Совместим ли transitive graph с environment entry point?
|
||||
- Есть ли owner, scope, multiplicity и cleanup у ресурсов?
|
||||
- Не требует ли решение незапрошенной миграции соседних owners?
|
||||
|
||||
### 5. Act and verify
|
||||
|
||||
Если пользователь просит код, не останавливайся на рекомендации. Реализуй согласованную границу, обнови consumers и tests, удали obsolete paths и проверь завершённое состояние. Если пользователь просит только анализ, план или review, не редактируй код.
|
||||
|
||||
## Алгоритмы выбора
|
||||
|
||||
### Ответственность и владелец
|
||||
|
||||
1. Опиши ответственность одним предложением без имени папки, файла или библиотеки.
|
||||
2. Назови одну причину её изменения.
|
||||
3. Найди данные, behavior и state, которые изменяются вместе с ней.
|
||||
4. Найди внешних consumers.
|
||||
5. Проверь, нужны ли ей собственные API, dependencies, state или lifecycle.
|
||||
6. Если самостоятельность доказана, назначь ровно один module-owner.
|
||||
7. Если ответственность нельзя сформулировать или у неё конкурирующие owners, остановись до структурных изменений.
|
||||
|
||||
Место выполнения не переносит владение. Provider, hook, controller, route и component могут запускать чужую ответственность, не становясь её owner.
|
||||
|
||||
### Выбор слоя
|
||||
|
||||
```text
|
||||
Только framework bootstrap, route entry или external input adaptation?
|
||||
-> app
|
||||
|
||||
Page/layout/screen/widget, route outcome или multi-domain UI?
|
||||
-> compositions
|
||||
|
||||
Domain model, scenario, validation, transition или product state?
|
||||
-> domains
|
||||
|
||||
Technical capability без собственной domain model?
|
||||
-> infra
|
||||
|
||||
Product-independent reusable UI?
|
||||
-> ui
|
||||
|
||||
Deterministic, product-agnostic, без I/O/state/lifecycle?
|
||||
-> shared
|
||||
|
||||
Иначе -> уточни ответственность, не выбирай папку по аналогии.
|
||||
```
|
||||
|
||||
Domain-specific framework integration над готовым API может принадлежать Framework Group пакета Level 2. Зависимость от React/Vue сама по себе не переносит domain behavior в `compositions` или `app`.
|
||||
|
||||
### Выбор сущности
|
||||
|
||||
```text
|
||||
Есть самостоятельный owner/API/dependencies/state/lifecycle?
|
||||
Да -> module.
|
||||
Нет -> часть текущего owner.
|
||||
|
||||
Module нужен только внутри одного parent module?
|
||||
Да -> nested module.
|
||||
|
||||
Папка только классифицирует modules/Groups?
|
||||
Да -> Group.
|
||||
|
||||
Папка только организует содержимое одного module?
|
||||
Да -> segment.
|
||||
|
||||
Framework UI entity не имеет самостоятельной ответственности?
|
||||
Да -> component parent module.
|
||||
```
|
||||
|
||||
Не создавай module только из-за размера, повторного использования внутреннего helper или желания получить отдельную папку. Не оставляй самостоятельную ответственность component-ом или segment-ом только ради меньшего diff.
|
||||
|
||||
### Выбор формы домена
|
||||
|
||||
По умолчанию используй доменный модуль Level 1. Level 1 не требует factory, ports, adapters, assemblies или разделения по техническим ролям.
|
||||
|
||||
Рассматривай Level 2, когда конкретному домену действительно нужны:
|
||||
|
||||
- несколько независимо собираемых Domain API;
|
||||
- разные browser/server/request assemblies;
|
||||
- несколько production technical integrations;
|
||||
- строгие environment boundaries;
|
||||
- самостоятельные domain-specific framework modules.
|
||||
|
||||
Не выбирай Level 2 из-за количества файлов, одного SDK, одного hook, желания унифицировать дерево или гипотетической будущей интеграции. Зафиксируй, какую реальную потребность окупает дополнительная стоимость package, facets, assembly и adapters.
|
||||
|
||||
### Публичная граница
|
||||
|
||||
1. Перечисли реальных внешних consumers.
|
||||
2. Для каждого запиши минимально необходимый contract.
|
||||
3. Удали exports, которым нет consumer.
|
||||
4. Не экспортируй mutable internals, concrete clients, stores, contexts, adapters или lifecycle implementation.
|
||||
5. Для обычного module оставь одну логическую external entry point.
|
||||
6. Для `business` используй только объявленные facets.
|
||||
7. Удали deep imports и обнови package exports/aliases при необходимости.
|
||||
8. Не открывай nested module напрямую за пределы parent boundary.
|
||||
|
||||
Для Level 2 разделяй Domain API по устойчивым различиям consumers, dependencies или environments, а не по внутренним техническим папкам. Каждый публичный scenario принадлежит ровно одному Domain API; каждому API соответствует одна factory. Именованный graph assembly не является новым Domain API.
|
||||
|
||||
### Проверка зависимости
|
||||
|
||||
Для каждого нового или изменённого edge:
|
||||
|
||||
1. Определи source owner и target owner.
|
||||
2. Определи их слои и формы доменов.
|
||||
3. Если owners различаются, импортируй target только через public API.
|
||||
4. Проверь матрицу слоёв.
|
||||
5. Если edge пересекает Level 2 package boundary, примени более строгую cross-domain модель.
|
||||
6. Проверь transitive environment compatibility.
|
||||
7. Добавь edge в общий module DAG и проверь цикл.
|
||||
|
||||
При пересечении границы Level 2 статически допустимы:
|
||||
|
||||
```ts
|
||||
import type { OtherDomainApi } from '.../other/business'
|
||||
import { deterministicValue } from '.../other/business/runtime'
|
||||
```
|
||||
|
||||
Готовый API другого домена создаёт внешний graph owner и передаёт assembly или factory аргументом. Не импортируй из другого домена его factory, assembly, adapter, API singleton, framework state, hook, context, Provider, component или внутренний путь `business`.
|
||||
|
||||
Не скрывай cross-domain dependency локальным structural interface, callback, global registry или event bus. Установи владельца контракта и отрази runtime edge в graph, иначе можно пропустить цикл.
|
||||
|
||||
### Runtime capabilities
|
||||
|
||||
| Capability | Размещение в Level 2 |
|
||||
|---|---|
|
||||
| SDK, HTTP/GraphQL source, storage, platform API | Adapter |
|
||||
| Concrete state/query runtime для business dependency | Adapter |
|
||||
| Clock, timer, random, ID, environment | Явная factory dependency с production implementation в adapter |
|
||||
| Готовый API другого домена | Cross-domain dependency, передаваемая graph owner |
|
||||
| Provider, hook или query projection готового Domain API | Framework binding module |
|
||||
| Page-local или multi-domain UI state | Владеющий composition module |
|
||||
| Универсальный technical service | `infra` module |
|
||||
|
||||
Adapter переводит technical arguments/results и реализует dependency contract. Он не объявляет Domain API scenario, domain fallback, transition или public domain error. Production implementation technical dependency не прячь inline в assembly или composition.
|
||||
|
||||
Assembly выбирает public adapters своего домена, вызывает factories и возвращает точный именованный graph API. Она не добавляет scenarios, методы API или собственные domain errors. Framework binding получает готовые API и не вызывает factory/assembly и не выбирает adapter.
|
||||
|
||||
### State и cache
|
||||
|
||||
```text
|
||||
Domain facts, validation, transitions, commands, scenario outcomes
|
||||
-> business authority
|
||||
|
||||
Transport/source cache
|
||||
-> adapter
|
||||
|
||||
Framework/query projection готового Domain API
|
||||
-> framework binding
|
||||
|
||||
State только текущей UI composition
|
||||
-> composition owner
|
||||
```
|
||||
|
||||
Raw DTO, query-library result и mutable client не являются Domain API. Technical и framework cache могут хранить и проецировать только значения, произведённые или проверенные `business`, и не создают параллельную предметную модель.
|
||||
|
||||
При optimistic или concurrent mutations не придумывай универсальный rollback. Сначала установи owner политики ordering, versioning, rebase/rollback и authoritative refresh.
|
||||
|
||||
### Errors
|
||||
|
||||
- Expected technical или foreign-domain failure, доступный через текущий Domain API, преобразуется текущим `business` в собственный readonly domain error со stable code.
|
||||
- Source object, SDK class, message, status, payload и `cause` не входят в public domain contract.
|
||||
- Type errors экспортируются через `business`; необходимые runtime codes и guards - только через реально нужный `business/runtime`.
|
||||
- Не выбирай exception или discriminated `Result` как правило SLM: сохрани project policy и архитектурное владение.
|
||||
- Не маскируй programming defect под expected domain outcome. Если меняется публичный failure channel, выясни политику unexpected failures, cancellation и serialization.
|
||||
|
||||
### Lifecycle и environment
|
||||
|
||||
Для каждого request, subscription, listener, timer, observer, connection или другого долгоживущего ресурса зафиксируй:
|
||||
|
||||
```text
|
||||
Owner:
|
||||
Created or started by:
|
||||
Scope:
|
||||
Multiplicity:
|
||||
Environment:
|
||||
Owned or borrowed:
|
||||
Cleanup:
|
||||
```
|
||||
|
||||
Factory или assembly не должна запускать неучтённую долгоживущую работу. Явная операция, запускающая ресурс, предоставляет cleanup. Если assembly обязана создать resource для graph, её публичный result предоставляет cleanup handle; graph owner вызывает его не позже конца scope. Assembly без собственного ресурса не возвращает пустой `dispose` для симметрии.
|
||||
|
||||
Environment определяется transitive import graph, а не именем файла или tree shaking. Для RSC, server actions, workers, edge runtime и conditional exports сначала установи реальные executable edges, framework reference edges и runtime capabilities; не объявляй environment safety только по метке `client`/`server`.
|
||||
|
||||
## Рабочие процедуры
|
||||
|
||||
### Проектирование
|
||||
|
||||
1. Ограничь scope пользовательской задачей.
|
||||
2. Найди SLM root, project mapping и существующие owners.
|
||||
3. Построй карту consumers и текущих public paths.
|
||||
4. Определи ответственность, layer, entity и domain form.
|
||||
5. Спроектируй target boundaries и минимальные public contracts.
|
||||
6. Классифицируй technical и cross-domain dependencies.
|
||||
7. Определи graph owner, environments, state, errors и lifecycle.
|
||||
8. Проверь правила и stop conditions.
|
||||
9. Выдай решение, target structure, dependencies и порядок реализации.
|
||||
|
||||
Не предлагай файловое дерево до определения owners и boundaries. Имена файлов и segments следуют локальному стайлгайду, а не задаются SLM.
|
||||
|
||||
### Реализация
|
||||
|
||||
1. Зафиксируй принятое решение и change scope.
|
||||
2. Изменяй код в dependency order: contracts и behavior раньше adapters и assembly, providers/consumers после готовых API.
|
||||
3. Для Level 1 не создавай отсутствующие роли Level 2.
|
||||
4. Для Level 2 сначала реализуй types, errors, behavior и factories `business`.
|
||||
5. Затем реализуй production adapters, assemblies и framework bindings, которые реально нужны задаче.
|
||||
6. Собери междоменный graph в composition, `app`, request handler или test setup.
|
||||
7. Переведи всех затронутых consumers на public paths.
|
||||
8. Удали obsolete exports, deep imports и старые boundaries в согласованном scope.
|
||||
9. Добавь tests рядом с owners.
|
||||
10. Запусти доступные structural, type, unit, integration и architecture checks.
|
||||
|
||||
Не оставляй заведомо промежуточную смешанную границу как завершённый результат. Backward compatibility добавляй только для реального внешнего consumer, persisted contract или явно согласованной phased migration.
|
||||
|
||||
### Миграция Level 1 -> Level 2
|
||||
|
||||
1. Выбери ровно один domain module и докажи потребность Level 2.
|
||||
2. Найди все consumers, exports, state, I/O, framework integration и lifecycle resources.
|
||||
3. Вычисли dependency-connected migration radius до редактирования.
|
||||
4. Спроектируй Domain API по scenarios и consumers, а не по текущим technical segments.
|
||||
5. Перенеси модели, validation, transitions, outcomes и errors под authority `business`.
|
||||
6. Объяви явные factory dependencies и по одной factory на API.
|
||||
7. Оформи production technical implementations как adapter modules.
|
||||
8. Создай минимум одну assembly для реального execution context.
|
||||
9. Перенеси domain-specific framework responsibilities в Framework Group.
|
||||
10. Оставь pages, routes и multi-domain UI в `compositions`.
|
||||
11. Переключи external consumers и graph roots.
|
||||
12. Удали прежний root API и старую форму домена.
|
||||
13. Проверь, что итог содержит одну форму и не требует миграции соседних доменов.
|
||||
|
||||
Временное физическое сосуществование старой и новой структуры допустимо только внутри незавершённого изменения. Не объявляй его conforming state. Если атомарный cutover невозможен, сначала согласуй ограниченную compatibility strategy и срок её удаления.
|
||||
|
||||
### Архитектурное ревью
|
||||
|
||||
1. Определи review scope, SLM root и формы затронутых доменов.
|
||||
2. Построй фактическую карту owners, public boundaries, imports и runtime injection.
|
||||
3. Проверь structural правила класса `A` по наблюдаемым evidence.
|
||||
4. Отдельно проверь смысловые правила класса `R`; отсутствие lint error не доказывает их соблюдение.
|
||||
5. Проверь transitive `business` closure и environment graph.
|
||||
6. Проверь state/cache/error/lifecycle ownership.
|
||||
7. Проверь, что tests находятся у правильных owners и не дублируют весь behavior на каждом уровне.
|
||||
8. Сначала сообщи findings по severity, затем краткий verdict и остаточные gaps.
|
||||
|
||||
Каждый finding содержит:
|
||||
|
||||
```text
|
||||
Location:
|
||||
Kind:
|
||||
Rule or definition:
|
||||
Evidence:
|
||||
Impact:
|
||||
Minimal remediation:
|
||||
Required tests:
|
||||
Confidence:
|
||||
```
|
||||
|
||||
Не называй рекомендацию нарушением. Не подтверждай полное SLM conformance, если не исследовал весь нужный graph или не знаешь project mapping.
|
||||
|
||||
### Тестирование по владельцам
|
||||
|
||||
| Ответственность | Основная test boundary |
|
||||
|---|---|
|
||||
| Domain scenarios, validation, state и expected errors | `business` через соответствующую factory |
|
||||
| Deterministic runtime/guards | `business` |
|
||||
| Technical mapping и provider behavior | Adapter module |
|
||||
| Graph composition, adapter selection, environment и cleanup | Assembly module |
|
||||
| Provider, hook, form или query projection | Framework binding module |
|
||||
| Multi-domain graph и lifecycle | Composition, `app` или другой graph owner |
|
||||
|
||||
Не повторяй полный business scenario suite в adapter, assembly и framework tests. Проверяй в каждой границе только принадлежащий ей behavior и integration contract.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
### Ownership и структура
|
||||
|
||||
- Выбирать слой или сущность по имени существующей папки.
|
||||
- Размещать domain model или scenario в `infra`/`shared`.
|
||||
- Оставлять page, route policy или multi-domain responsibility внутри домена.
|
||||
- Делать Group, segment или component скрытым owner.
|
||||
- Создавать общий module или Level 2 package на будущее.
|
||||
- Требовать от component быть stateless: локальные data/lifecycle details допустимы, пока ответственность принадлежит parent module.
|
||||
|
||||
### Public boundaries
|
||||
|
||||
- Deep imports во внутренности module или `business`.
|
||||
- Root barrel доменного пакета или Group.
|
||||
- Export mutable store, context, client, adapter или singleton.
|
||||
- Reexport client и server entry points через общий barrel.
|
||||
- Создавать `business/runtime` без внешнего consumer.
|
||||
|
||||
### Business и runtime
|
||||
|
||||
- Импортировать SDK, storage, framework, state/query manager, platform API или hidden nondeterminism в `business`.
|
||||
- Обходить boundary через helper, `shared` или type alias.
|
||||
- Публиковать raw DTO или library-specific cache/store types в Domain API.
|
||||
- Позволять adapter определять domain fallback, transition или error semantics.
|
||||
- Прятать production adapter inline в assembly/composition.
|
||||
- Позволять factory выбирать environment или assembly.
|
||||
|
||||
### Assembly, framework и cross-domain
|
||||
|
||||
- Добавлять scenario или API method в assembly.
|
||||
- Вызывать factory/assembly из framework binding.
|
||||
- Импортировать framework state, hooks или components другого домена.
|
||||
- Импортировать чужую factory, assembly, adapter или API singleton.
|
||||
- Прятать runtime dependency в service locator, mutable registry или event bus.
|
||||
- Создавать pass-through adapter автоматически без проверки project policy и реальной boundary value.
|
||||
|
||||
### State и lifecycle
|
||||
|
||||
- Делать cache параллельной domain model.
|
||||
- Строить optimistic domain value из raw form/DTO без business validation.
|
||||
- Использовать file-level singleton без доказанного application scope.
|
||||
- Запускать скрытую subscription/timer при создании API.
|
||||
- Оставлять resource без scope или cleanup.
|
||||
- Возвращать пустой `dispose` только для одинаковой формы assemblies.
|
||||
|
||||
### Процесс
|
||||
|
||||
- Выбирать Level 2 по размеру каталога.
|
||||
- Генерировать полный package scaffold без потребности.
|
||||
- Мигрировать соседние домены ради локального изменения.
|
||||
- Копировать пример как нормативное дерево.
|
||||
- Перечислять коды правил вместо анализа фактического graph и runtime.
|
||||
- Задавать пользователю все открытые вопросы независимо от задачи.
|
||||
|
||||
## Stop conditions и адресные вопросы
|
||||
|
||||
Остановись до изменения публичной или runtime-границы, если:
|
||||
|
||||
- ответственность или owner не определены;
|
||||
- одна ответственность имеет конкурирующих owners;
|
||||
- неизвестны consumers изменяемого API;
|
||||
- одна domain responsibility окажется в двух формах;
|
||||
- planned edge создаёт цикл;
|
||||
- environment compatibility нельзя установить;
|
||||
- resource scope, multiplicity или cleanup неизвестны;
|
||||
- изменение требует незапрошенной широкой миграции;
|
||||
- локальные инструкции противоречат выбранной SLM boundary;
|
||||
- корректность зависит от открытой semantics cancellation, concurrency, hydration или disposal;
|
||||
- задача требует правил монорепозитория, versioning или нескольких SLM roots, которых текущий DRAFT не задаёт.
|
||||
|
||||
Задавай вопрос только при наличии trigger:
|
||||
|
||||
| Trigger | Что выяснить |
|
||||
|---|---|
|
||||
| L1 -> L2 или удаление старого API | Полный migration radius, атомарный cutover или compatibility strategy |
|
||||
| Новая technical dependency | Ownership contract, timeout/retry/idempotency/order/subscription semantics |
|
||||
| Abort или cancellable operation | Кто владеет cancellation и как она связана с cleanup/outcome |
|
||||
| Публичные errors, RPC, server action | Expected failure, cancellation, unexpected defect и serialization policy |
|
||||
| Store, persistence или external events | Initial state, transitions, reset, persistence и owner |
|
||||
| Optimistic/concurrent mutations | Ordering, versioning, rollback/rebase и authoritative refresh |
|
||||
| Assembly, lazy graph или новый root | Scope, multiplicity, owned/borrowed resources и disposal |
|
||||
| SSR, hydration, RSC | Serialization boundary, validation/reset и executable/reference edges |
|
||||
| Worker, edge, conditional exports | Реальные capabilities и resolver conditions |
|
||||
| Готовый `infra` API совпадает с port | Нужен ли domain adapter или допустима прямая передача capability |
|
||||
|
||||
Можно продолжить с явным assumption только когда решение обратимо, не меняет owner/public API, не ослабляет environment boundary и не скрывает lifecycle.
|
||||
|
||||
## Проверочные списки
|
||||
|
||||
### До изменения файлов
|
||||
|
||||
- [ ] Найден SLM root и path mapping.
|
||||
- [ ] Прочитаны локальные инструкции.
|
||||
- [ ] Сформулирована responsibility.
|
||||
- [ ] Назначен один owner.
|
||||
- [ ] Выбраны layer и entity.
|
||||
- [ ] Для domain выбрана одна form.
|
||||
- [ ] Найдены реальные consumers.
|
||||
- [ ] Спроектирован минимальный public API.
|
||||
- [ ] Классифицированы static и runtime dependencies.
|
||||
- [ ] Проверены layer, cross-domain и environment edges.
|
||||
- [ ] Для resources определены scope и cleanup.
|
||||
- [ ] Нет активного stop condition.
|
||||
|
||||
### После реализации
|
||||
|
||||
- [ ] Каждый module имеет отдельную boundary и public API.
|
||||
- [ ] Нет deep imports и package/Group barrels.
|
||||
- [ ] Layer matrix соблюдена.
|
||||
- [ ] Общий module graph ацикличен.
|
||||
- [ ] `business` import closure environment-neutral и technical-runtime-free.
|
||||
- [ ] Cross-domain runtime APIs передаются аргументами.
|
||||
- [ ] Client/server graphs не содержат несовместимый executable code.
|
||||
- [ ] Facets `business` имеют допустимое содержимое и consumers.
|
||||
- [ ] Production technical dependencies принадлежат нужным adapters.
|
||||
- [ ] Assembly возвращает точный graph и cleanup, если владеет resource.
|
||||
- [ ] Framework bindings получают готовые APIs.
|
||||
- [ ] Technical и foreign errors не протекают наружу.
|
||||
- [ ] Cache не подменяет business authority.
|
||||
- [ ] Tests проверяют behavior соответствующих owners.
|
||||
- [ ] После migration удалена старая form/boundary.
|
||||
|
||||
## Формат результата
|
||||
|
||||
Не печатай полную внутреннюю карточку и все checklists без необходимости. Пользователю нужен результат задачи.
|
||||
|
||||
| Режим | Обязательный результат |
|
||||
|---|---|
|
||||
| Design | Decision, owner/layer/form, boundaries, public APIs, dependencies, lifecycle, implementation order, assumptions |
|
||||
| Implementation | Использованное решение, изменённые boundaries/files, API/import changes, tests/checks, отклонения и риски |
|
||||
| Migration | Source/target forms, consumer map, phases, cutover, удаление старой boundary и completion gate |
|
||||
| Review | Findings с evidence, verdict, remediation order, unresolved decisions и непроверенный scope |
|
||||
|
||||
Для однозначной локальной реализации достаточно кратко объяснить архитектурное решение и выполнить работу. Для дорогого, публично несовместимого или неоднозначного решения сначала покажи варианты и запроси выбор.
|
||||
|
||||
## Когда открывать references
|
||||
|
||||
| Ситуация | Reference |
|
||||
|---|---|
|
||||
| Нужна точная формулировка правила | [`rules/level-1.md`](./reference/draft/rules/level-1.md), [`rules/level-2.md`](./reference/draft/rules/level-2.md) |
|
||||
| Неясен смысл сущности | [`level-1/terminology.md`](./reference/draft/level-1/terminology.md), [`level-2/terminology.md`](./reference/draft/level-2/terminology.md) |
|
||||
| Сложный Level 1 module/dependency/lifecycle case | [`level-1/`](./reference/draft/level-1/README.md) |
|
||||
| Package, business, factory или adapters | [`level-2/domains/`](./reference/draft/level-2/domains/README.md) |
|
||||
| Cross-domain или environment edge | [`level-2/dependencies.md`](./reference/draft/level-2/dependencies.md) |
|
||||
| State, cache, SSR или hydration | [`state-cache.md`](./reference/draft/level-2/domains/state-cache.md), [`open-questions.md`](./reference/draft/level-2/domains/open-questions.md) |
|
||||
| Assembly lifecycle и cleanup | [`assemblies.md`](./reference/draft/level-2/domains/assemblies.md) |
|
||||
| Full architecture review | [`level-1/validation.md`](./reference/draft/level-1/validation.md), [`level-2/validation.md`](./reference/draft/level-2/validation.md) |
|
||||
| L1 -> L2 migration example | [`auth-example.md`](./reference/draft/level-2/domains/auth-example.md) |
|
||||
|
||||
Будущие project examples открывай только после архитектурной классификации. Используй их как evidence конкретной реализации для похожего stack/environment, но не копируй naming, дерево или дополнительные роли без потребности. Example никогда не переопределяет rule или terminology.
|
||||
15
.opencode/skills/slm-design/reference/draft/README.md
Normal file
15
.opencode/skills/slm-design/reference/draft/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Черновики SLM
|
||||
|
||||
> Материалы в `DRAFT` являются рабочими черновиками и не задают нормативную спецификацию SLM.
|
||||
|
||||
## Материалы
|
||||
|
||||
- [Первый уровень](./level-1/README.md) - слои, доменные модули, публичные API и зависимости.
|
||||
- [Второй уровень](./level-2/README.md) - доменные пакеты, именованные Domain API, assemblies и Framework Groups.
|
||||
- [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки.
|
||||
|
||||
## Соглашение
|
||||
|
||||
Черновики могут содержать определения, правила, рекомендации, примеры и открытые вопросы.
|
||||
|
||||
Нормативные определения задаются терминологией соответствующего уровня. Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки.
|
||||
@@ -0,0 +1,53 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,65 @@
|
||||
# Компоненты Level 1
|
||||
|
||||
> Пояснение нормативной модели компонентов Level 1.
|
||||
|
||||
Компонент является строительным элементом интерфейса, а не самостоятельной архитектурной единицей.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`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)
|
||||
|
||||
## Файловая форма
|
||||
|
||||
Файловую форму компонента определяет стайлгайд. Компонент может быть одним файлом фреймворка или каталогом со вспомогательными файлами.
|
||||
|
||||
```text
|
||||
landing/
|
||||
└── ui/
|
||||
└── hero.tsx
|
||||
```
|
||||
|
||||
```text
|
||||
landing/
|
||||
└── ui/
|
||||
└── hero/
|
||||
├── hero.tsx
|
||||
├── styles/
|
||||
│ └── hero.module.css
|
||||
└── types/
|
||||
└── hero-props.type.ts
|
||||
```
|
||||
|
||||
Наличие каталога, типов, стилей или локального `index.ts` не превращает компонент в модуль.
|
||||
|
||||
## Реализация
|
||||
|
||||
Компонент может отображать входные данные, вызывать переданные обработчики, условно строить интерфейс и хранить локальное состояние представления.
|
||||
|
||||
Level 1 не вводит отдельного запрета на импорты, выполняемые кодом компонента. Каждый такой импорт считается зависимостью родительского модуля и должен соблюдать направление слоёв, публичные API и запрет циклов.
|
||||
|
||||
Доступ к данным, состояние, контекст или код жизненного цикла внутри компонента сами по себе не создают новую архитектурную границу. Их источники, зависимости и область жизни определяет родительский модуль.
|
||||
|
||||
Провайдер может технически реализовывать контекст и жизненный цикл фреймворка, но владельцем состояния и ресурсов остаётся родительский модуль.
|
||||
|
||||
Файл в `app` может технически быть компонентом React или Vue. Архитектурно он является точкой входа фреймворка, а не компонентом SLM.
|
||||
|
||||
## Компонент и модуль
|
||||
|
||||
| Признак | Компонент | Модуль |
|
||||
|---|---|---|
|
||||
| Самостоятельная ответственность | Нет | Да |
|
||||
| Собственный публичный API | Нет | Да |
|
||||
| Собственная граница зависимостей | Нет | Да |
|
||||
| Вспомогательные файлы | Может иметь | Может иметь |
|
||||
| Сегменты и вложенные модули | Нет | Может иметь |
|
||||
|
||||
Модуль может состоять всего из одного корневого компонента. Различие определяется владением, а не количеством файлов.
|
||||
|
||||
## Когда нужен вложенный модуль
|
||||
|
||||
Если часть интерфейса получает самостоятельную ответственность, публичный API, собственную границу зависимостей, область жизни или внутреннюю модульную декомпозицию, она является модулем. При локальном использовании такой модуль может размещаться как вложенный.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Зависимости 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
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Доменные модули Level 1
|
||||
|
||||
> Пояснение базовой модели предметных областей без обязательной внутренней архитектуры.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015)
|
||||
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
|
||||
- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006)
|
||||
- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007)
|
||||
|
||||
## Один домен, один модуль
|
||||
|
||||
Связная предметная область получает один доменный модуль. Level 1 не требует выделять business, adapters, 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-интеграции.
|
||||
|
||||
Такой переход изменяет только форму выбранного домена: корневой доменный модуль исчезает, а его предметная ответственность переходит обязательному модулю `business` внутри доменного пакета. Другие предметные области SLM root не обязаны переходить вместе с ним.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Группы Level 1
|
||||
|
||||
> Пояснение нормативной модели групп Level 1.
|
||||
|
||||
Группа помогает ориентироваться в большом количестве модулей. Она классифицирует структуру, но ничего не реализует и не образует узел графа зависимостей.
|
||||
|
||||
## Связанное правило
|
||||
|
||||
- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007)
|
||||
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
|
||||
|
||||
## Пример
|
||||
|
||||
```text
|
||||
compositions/
|
||||
├── pages/ # Группа
|
||||
│ ├── landing/ # Модуль
|
||||
│ └── contacts/ # Модуль
|
||||
└── layouts/ # Группа
|
||||
└── main/ # Модуль
|
||||
```
|
||||
|
||||
`pages` и `layouts` являются возможной группировкой проекта, а не обязательной структурой Level 1.
|
||||
|
||||
Рекомендуется создавать группу только при реальной навигационной потребности. Если папка начинает владеть файлами реализации, состоянием, жизненным циклом или публичным API, она является модулем и должна получить модульную границу.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Слои Level 1
|
||||
|
||||
> Пояснение нормативной модели слоёв Level 1.
|
||||
|
||||
## Базовая структура
|
||||
|
||||
```text
|
||||
src/
|
||||
├── app/
|
||||
├── compositions/
|
||||
├── domains/
|
||||
├── infra/
|
||||
├── ui/
|
||||
└── shared/
|
||||
```
|
||||
|
||||
`src/` здесь является примером SLM root. Фактическую границу определяет устройство приложения.
|
||||
|
||||
Отсутствующая в проекте роль не требует пустой папки. Слой является доступной архитектурной ролью, а не обязательным элементом каркаса.
|
||||
|
||||
## Роли слоёв
|
||||
|
||||
### App
|
||||
|
||||
`app` связывает приложение с фреймворком: запускает его, объявляет маршруты, преобразует входные данные и подключает публичные API модулей разрешённых слоёв или ресурсы `shared`. Файлы `app` являются точками входа фреймворка, а не модулями SLM.
|
||||
|
||||
Точка входа может напрямую использовать `compositions`, `domains`, `infra`, `ui` или `shared`, если зависимость разрешена матрицей слоёв. Такое использование не переносит ответственность импортируемого модуля в `app`.
|
||||
|
||||
### Compositions
|
||||
|
||||
`compositions` содержит продуктовый интерфейс: страницы, макеты, экраны, виджеты, точки входа и другие композиционные модули. Внутреннюю группировку слоя определяет проект.
|
||||
|
||||
### Domains
|
||||
|
||||
`domains` содержит предметные области приложения: их модели, правила, сценарии и продуктовое состояние. Каждая самостоятельная предметная область представлена [доменным модулем](./domains.md).
|
||||
|
||||
Доменная логика не принадлежит устройству одной страницы или маршрута. Конкретный visual outcome и сборка нескольких доменов остаются в `compositions`.
|
||||
|
||||
### Infra
|
||||
|
||||
`infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной доменной модели.
|
||||
|
||||
### UI
|
||||
|
||||
`ui` содержит универсальные модули интерфейса, которые не зависят от конкретной страницы или продуктовой композиции.
|
||||
|
||||
### Shared
|
||||
|
||||
`shared` содержит независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода.
|
||||
|
||||
В `shared` допускаются обычные модули и специальные немодульные ресурсы: небольшие чистые утилиты, общие типы, стили, конфигурация и статические файлы. Ресурс не имеет самостоятельной ответственности, публичного API или жизненного цикла и может импортироваться напрямую по пути, установленному стайлгайдом.
|
||||
|
||||
Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль. Каталог ресурсов не реэкспортирует модули и не используется для обхода их публичных API.
|
||||
|
||||
## Матрица зависимостей
|
||||
|
||||
```text
|
||||
app
|
||||
|
|
||||
compositions
|
||||
|
|
||||
domains
|
||||
/ \
|
||||
infra ui
|
||||
\ /
|
||||
shared
|
||||
```
|
||||
|
||||
Код слоя может импортировать модули своего слоя и слоёв, разрешённых строкой матрицы. Разрешённая зависимость может пропускать промежуточные роли.
|
||||
|
||||
| Слой | Может импортировать |
|
||||
|---|---|
|
||||
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
|
||||
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
|
||||
| `domains` | `domains`, `infra`, `ui`, `shared` |
|
||||
| `infra` | `infra`, `shared` |
|
||||
| `ui` | `ui`, `shared` |
|
||||
| `shared` | `shared` |
|
||||
|
||||
`infra` и `ui` не импортируют друг друга. Универсальный UI получает локализованный текст, тему, callbacks аналитики и другие возможности через входной контракт либо связывается с ними в `domains` или `compositions`. Если UI-модулю необходимо напрямую знать конкретный технический сервис приложения, такой код не является универсальным 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)
|
||||
|
||||
## Граница Level 1
|
||||
|
||||
Разрешённый импорт не переносит владение. Например, доменный модуль может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев.
|
||||
|
||||
Если код определяет продуктовую модель, правило или сценарий, он принадлежит `domains`, а не `shared` или `infra`. Строгая внутренняя форма домена появляется только на [Level 2](../level-2/).
|
||||
@@ -0,0 +1,31 @@
|
||||
# Жизненный цикл Level 1
|
||||
|
||||
> Пояснение нормативной модели владения ресурсами Level 1.
|
||||
|
||||
Жизненный цикл является частью ответственности модуля. Файл, компонент, провайдер или точка входа фреймворка могут технически создавать и останавливать ресурс, но архитектурным владельцем остаётся модуль.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
|
||||
- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013)
|
||||
|
||||
## Граница ресурса
|
||||
|
||||
Для ресурса определяются:
|
||||
|
||||
- модуль-владелец;
|
||||
- место создания;
|
||||
- момент начала работы;
|
||||
- область жизни;
|
||||
- допустимое число экземпляров;
|
||||
- способ остановки и очистки.
|
||||
|
||||
Ресурс начинает работу не раньше начала своей области жизни и не остаётся активным после её завершения. Подписки, слушатели, таймеры, наблюдатели, запросы и соединения рассматриваются одинаково, если требуют явного завершения или отмены.
|
||||
|
||||
## Реализация
|
||||
|
||||
Очистку может выполнять сам модуль, компонент, провайдер или фреймворк. Способ реализации не меняет владельца и не переносит ответственность в технический файл.
|
||||
|
||||
Одиночный экземпляр на всё приложение допустим только тогда, когда модуль действительно владеет областью жизни приложения или процесса. Размещение экземпляра на уровне файла само по себе этого не доказывает.
|
||||
|
||||
Точка входа `app` может запускать или подключать ресурс через публичный API импортируемого модуля, но не становится его владельцем.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Модули 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`.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Вложенные модули Level 1
|
||||
|
||||
> Пояснение нормативной модели вложенных модулей Level 1.
|
||||
|
||||
Вложенный модуль является обычным модулем, размещённым внутри границы родительского модуля. Он имеет собственные ответственность, публичный 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)
|
||||
|
||||
## Пример
|
||||
|
||||
```text
|
||||
landing/
|
||||
├── landing.page.tsx
|
||||
├── parts/
|
||||
│ └── hero/
|
||||
│ ├── hero.tsx
|
||||
│ └── index.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`parts/` здесь является примером сегмента, а не обязательным именем.
|
||||
|
||||
Код родительского модуля использует вложенный модуль через его собственный публичный API. Код за пределами родительского модуля получает доступ только через публичный API родителя.
|
||||
|
||||
Если вложенный модуль становится нужен за пределами родителя, рекомендуется перенести его в минимальную общую область без изменения внутренней формы. Доступ через API родителя при этом остаётся допустимым и сам по себе не требует переноса.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Сегменты Level 1
|
||||
|
||||
> Пояснение нормативной модели сегментов Level 1.
|
||||
|
||||
Сегмент организует внутреннее содержимое модуля. Level 1 определяет роль сегмента, но не задаёт обязательный список имён.
|
||||
|
||||
## Связанное правило
|
||||
|
||||
- [`SLM-L1-SEGMENT-R008`](../rules/level-1.md#slm-l1-segment-r008)
|
||||
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
|
||||
|
||||
## Файловая форма
|
||||
|
||||
Названия, набор и содержимое сегментов определяет стайлгайд проекта. Сегмент может группировать файлы, компоненты или вложенные модули.
|
||||
|
||||
Файлы и компоненты сегмента принадлежат родительскому модулю. Вложенный модуль внутри сегмента сохраняет собственные ответственность, публичный API и узел графа зависимостей.
|
||||
|
||||
## Пример
|
||||
|
||||
```text
|
||||
landing/ # Модуль
|
||||
└── ui/ # Сегмент модуля
|
||||
└── hero/ # Каталог компонента
|
||||
├── hero.tsx
|
||||
├── styles/ # Вспомогательный каталог компонента
|
||||
└── types/ # Вспомогательный каталог компонента
|
||||
```
|
||||
|
||||
`styles/` и `types/` внутри каталога компонента не обязаны считаться сегментами SLM. Их форму определяет стайлгайд компонентов.
|
||||
@@ -0,0 +1,143 @@
|
||||
# Терминология Level 1
|
||||
|
||||
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
|
||||
|
||||
Определения Level 1 задают обязательный смысл архитектурных терминов и используются при толковании всех правил. Код получают только блокирующие требования, а не сами определения.
|
||||
|
||||
## Базовые понятия
|
||||
|
||||
### SLM root
|
||||
|
||||
Граница структурной архитектуры одного приложения. Внутри неё определяются слои, модули и граф зависимостей Level 1. Монорепозиторий, пакеты и отношения между несколькими SLM root находятся за пределами Level 1.
|
||||
|
||||
### Ответственность
|
||||
|
||||
Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственные публичный API, зависимости, состояние или область жизни.
|
||||
|
||||
### Владелец
|
||||
|
||||
Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннее устройство. Место выполнения кода не переносит владение.
|
||||
|
||||
### Публичный API
|
||||
|
||||
Единая логическая точка внешнего доступа к модулю. Публичный API скрывает внутреннее устройство; конкретное имя файла и механизм экспорта определяет стайлгайд проекта.
|
||||
|
||||
### Зависимость
|
||||
|
||||
Статическая связь внутри одного SLM root, которую импорт или реэкспорт создаёт между архитектурными границами. Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
|
||||
|
||||
Зависимость любого внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу и становится отдельным узлом графа зависимостей.
|
||||
|
||||
### Область жизни
|
||||
|
||||
Период, в течение которого принадлежащие модулю состояние или долгоживущий ресурс должны оставаться активными.
|
||||
|
||||
### Ресурс жизненного цикла
|
||||
|
||||
Ресурс, работа которого продолжается после первоначального вызова и требует остановки, отмены, отписки или освобождения. Например, подписка, слушатель событий, таймер, наблюдатель, запрос или соединение.
|
||||
|
||||
### Очистка
|
||||
|
||||
Гарантированное прекращение работы ресурса не позже завершения его области жизни. Автоматическая очистка фреймворка считается очисткой владельца, если модуль устанавливает и контролирует соответствующую границу.
|
||||
|
||||
## Структурные сущности
|
||||
|
||||
### Нормативная матрица слоёв
|
||||
|
||||
Отношение допустимой зависимости между слоями одного SLM root. Матрица определяет, код каких слоёв может импортировать исходный слой; она не обязана образовывать линейный порядок.
|
||||
|
||||
Для Level 1 нормативно отношение `app → compositions → domains → { infra, ui } → shared`. `infra` и `ui` являются независимыми ветвями: они не импортируют друг друга. Промежуточный слой не является обязательным посредником.
|
||||
|
||||
### Слой
|
||||
|
||||
Одна из шести верхнеуровневых ролей внутри SLM root:
|
||||
|
||||
| Слой | Роль |
|
||||
|---|---|
|
||||
| `app` | Связь приложения с фреймворком: запуск, маршруты и преобразование входных данных |
|
||||
| `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции |
|
||||
| `domains` | Предметные области, их модели, правила, сценарии и продуктовое состояние |
|
||||
| `infra` | Технические сервисы и возможности приложения |
|
||||
| `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции |
|
||||
| `shared` | Независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода |
|
||||
|
||||
Полная матрица допустимых зависимостей:
|
||||
|
||||
| Исходный слой | Допустимые целевые слои |
|
||||
|---|---|
|
||||
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
|
||||
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
|
||||
| `domains` | `domains`, `infra`, `ui`, `shared` |
|
||||
| `infra` | `infra`, `shared` |
|
||||
| `ui` | `ui`, `shared` |
|
||||
| `shared` | `shared` |
|
||||
|
||||
### Модуль
|
||||
|
||||
Минимальная самостоятельная архитектурная единица Level 1. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API.
|
||||
|
||||
### Доменная ответственность
|
||||
|
||||
Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Количество экранов, endpoint-ов, hooks или файлов само по себе не определяет её границу.
|
||||
|
||||
### Доменный модуль
|
||||
|
||||
Обычный SLM-модуль слоя `domains`, представляющий одну доменную ответственность. Он подчиняется всем правилам модулей, предоставляет единый публичный API и является узлом графа зависимостей.
|
||||
|
||||
Доменный модуль может содержать сегменты, компоненты и вложенные модули. Level 1 не требует разделять его внутреннее содержимое по техническим ролям.
|
||||
|
||||
### Группа
|
||||
|
||||
Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или узла графа зависимостей.
|
||||
|
||||
### Сегмент
|
||||
|
||||
Внутренняя часть одного модуля, которая группирует его содержимое по назначению. Сегмент не является самостоятельным владельцем, публичным API или узлом графа зависимостей.
|
||||
|
||||
### Компонент
|
||||
|
||||
Сущность фреймворка, которая реализует часть интерфейса родительского модуля. Компонент не образует собственного владельца, публичного API или узла графа зависимостей.
|
||||
|
||||
Импорты, состояние, доступ к данным и код жизненного цикла компонента принадлежат родительскому модулю. Их наличие само по себе не создаёт новый модуль; решающим признаком является самостоятельная ответственность.
|
||||
|
||||
### Вложенный модуль
|
||||
|
||||
Обычный модуль, размещённый внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей.
|
||||
|
||||
Публичный API вложенного модуля доступен коду родительской границы. Для кода за пределами родительского модуля вложенный модуль остаётся внутренней реализацией родителя.
|
||||
|
||||
### Точка входа фреймворка
|
||||
|
||||
Специальная немодульная единица слоя `app`, которая непосредственно связывает приложение с фреймворком. Её импорты участвуют в проверке направления слоёв, но сама точка входа не является узлом графа модулей.
|
||||
|
||||
### Ресурс shared
|
||||
|
||||
Специальная немодульная единица слоя `shared`: небольшая детерминированная утилита, общий тип, стиль, конфигурация или статический ресурс без продуктового знания, изменяемого состояния, ввода-вывода, области жизни и собственного публичного API.
|
||||
|
||||
Ресурс `shared` может импортироваться напрямую по пути, установленному стайлгайдом, и не является узлом графа модулей. Доступный по этому пути файл является всей единицей и не скрывает отдельное внутреннее устройство.
|
||||
|
||||
Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль.
|
||||
|
||||
## Структурная модель
|
||||
|
||||
```text
|
||||
SLM root
|
||||
├── app
|
||||
│ └── точка входа фреймворка
|
||||
├── compositions | domains | infra | ui
|
||||
│ ├── группа
|
||||
│ │ └── модуль
|
||||
│ └── модуль
|
||||
│ ├── корневые файлы
|
||||
│ ├── сегмент
|
||||
│ │ ├── файлы
|
||||
│ │ ├── компоненты
|
||||
│ │ └── вложенные модули
|
||||
│ └── вложенный модуль
|
||||
└── shared
|
||||
├── группа
|
||||
├── модуль
|
||||
└── ресурс shared
|
||||
```
|
||||
|
||||
Путь и имя папки сами по себе не определяют сущность. Её определяют ответственность, владелец и публичная граница. Физическое сопоставление путей с сущностями задаётся стайлгайдом или конфигурацией проверки проекта.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Проверка 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`.
|
||||
102
.opencode/skills/slm-design/reference/draft/level-2/README.md
Normal file
102
.opencode/skills/slm-design/reference/draft/level-2/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# SLM Level 2
|
||||
|
||||
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
|
||||
|
||||
Level 2 предназначен для отдельных предметных областей с устойчивыми API, несколькими способами сборки или самостоятельными framework-модулями. Он сохраняет структурную базу Level 1, но заменяет выбранный доменный модуль доменным пакетом с явными владельцами ролей.
|
||||
|
||||
## Наследование 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 модуля `business` | Представлен обязательными type-only и factory-фасетами и необязательным runtime-фасетом |
|
||||
| Навигационная Group слоя `domains` | Может одновременно содержать доменные модули и пакеты |
|
||||
| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups |
|
||||
|
||||
Канонический набор требований образуют [правила Level 1](../rules/level-1.md) и [правила Level 2](../rules/level-2.md).
|
||||
|
||||
## Когда выбирать Level 2
|
||||
|
||||
Level 2 оправдан, когда одной предметной области нужны несколько независимо собираемых Domain API, разные browser/server assemblies, несколько технических интеграций или самостоятельные domain-specific модули React, Vue либо другого фреймворка.
|
||||
|
||||
Level 2 выбирается для конкретной предметной области. Один SLM root может корректно содержать простые доменные модули Level 1 и доменные пакеты Level 2. Размер каталога сам по себе не требует перехода.
|
||||
|
||||
## Цена Level 2
|
||||
|
||||
Пакетная форма намеренно дороже простого доменного модуля. Она добавляет отдельные business-фасеты, минимум одну assembly, явную runtime-инъекцию cross-domain API и adapter-модули для production capabilities. Concrete state/query runtime, SDK и недетерминизм не импортируются напрямую в `business`.
|
||||
|
||||
Эта цена оправдана независимой сборкой API, несколькими средами, строгой environment boundary или самостоятельными framework bindings. Если домену не нужны такие свойства, он остаётся модулем Level 1.
|
||||
|
||||
## Базовая форма
|
||||
|
||||
```text
|
||||
src/domains/
|
||||
├── catalog/ # Доменный модуль Level 1
|
||||
└── auth/ # Доменный пакет Level 2
|
||||
├── README.md # Необязательная metadata
|
||||
├── business/ # Обязательный SLM-модуль
|
||||
│ ├── index.ts # Только public types
|
||||
│ ├── factory.ts # Public factories entry
|
||||
│ └── runtime.ts # Необязательный deterministic runtime
|
||||
├── assemblies/ # Обязательная непустая Group
|
||||
│ ├── browser/ # SLM-модуль
|
||||
│ └── request/ # SLM-модуль
|
||||
├── adapters/ # При наличии technical dependencies
|
||||
│ └── identity-provider/ # SLM-модуль
|
||||
└── react/ # Необязательная framework Group
|
||||
├── session/ # SLM-модуль
|
||||
└── login-form/ # SLM-модуль
|
||||
```
|
||||
|
||||
Корень пакета не является модулем и не имеет `index.ts`. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своим публичным API.
|
||||
|
||||
## Публичные границы
|
||||
|
||||
`business` может объявить несколько API и соответствующих фабрик. Assembly создаёт только нужный для своего контекста именованный граф:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthAdministrationApi,
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/business'
|
||||
|
||||
import {
|
||||
authAdministrationFactory,
|
||||
authSessionFactory,
|
||||
} from '@/domains/auth/business/factory'
|
||||
|
||||
import {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
|
||||
import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
|
||||
import { AuthSessionProvider } from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
`business/runtime` существует только при наличии реальных внешних потребителей детерминированных runtime-экспортов. Общие импорты `@/domains/auth` и `@/domains/auth/react` запрещены: пакет и Groups не имеют агрегирующих API.
|
||||
|
||||
## Совместное применение форм
|
||||
|
||||
Простой доменный модуль и доменный пакет могут постоянно сосуществовать в одном SLM root, но одна предметная область имеет только одну форму. При связи, пересекающей границу пакета Level 2, доменный код использует type-only публичный контракт либо детерминированный `business/runtime`; готовые API передаются runtime-аргументами в месте сборки графа.
|
||||
|
||||
Если доменному модулю Level 1 нужна runtime-инъекция, которой его текущий публичный API не поддерживает, этот потребитель рефакторится или сам переводится в пакетную форму. Level 2 не создаёт скрытый механизм внедрения в старый модуль.
|
||||
|
||||
## Карта черновика
|
||||
|
||||
- [Терминология](./terminology.md)
|
||||
- [Доменный пакет](./domains/domain-package.md)
|
||||
- [Модуль business](./domains/business.md)
|
||||
- [Фабрики, зависимости и adapters](./domains/factory-ports-adapters.md)
|
||||
- [Assemblies и среды выполнения](./domains/assemblies.md)
|
||||
- [Состояние и кэш](./domains/state-cache.md)
|
||||
- [Framework Groups и модули](./domains/framework-bindings.md)
|
||||
- [Зависимости](./dependencies.md)
|
||||
- [Тестирование](./domains/testing.md)
|
||||
- [Проверка](./validation.md)
|
||||
- [Переход auth](./domains/auth-example.md)
|
||||
- [Открытые вопросы](./domains/open-questions.md)
|
||||
@@ -0,0 +1,109 @@
|
||||
# Зависимости Level 2
|
||||
|
||||
> Уточнение графа зависимостей внутри и между доменными границами.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
|
||||
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
|
||||
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
|
||||
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
|
||||
|
||||
## Матрица внутри пакета
|
||||
|
||||
| Исходный модуль | Допустимые зависимости |
|
||||
|---|---|
|
||||
| `business` | Собственные файлы, объявленные environment-neutral ресурсы `shared`, business-safe packages, type-only контракты и `business/runtime` других доменов |
|
||||
| Adapter module | Type-only barrel собственного `business`, `infra`, concrete technical runtime, `shared` |
|
||||
| Assembly | Type-only barrel, `factory` и при необходимости `runtime` собственного `business`, публичные adapters своего домена, type-only API других доменов, `shared` |
|
||||
| Framework binding module | Type-only barrel и `runtime` собственного `business`, публичные framework-модули своего домена, framework/state/query runtime, `ui`, `shared` |
|
||||
| Место сборки графа | Assemblies либо `business/factory` и adapter-модули, `business/runtime`, framework-модули входящих в граф доменов, а также разрешённые матрицей `infra`, `ui` и `shared` |
|
||||
|
||||
`business` не достигает adapters, assemblies, framework-модулей, product SDK, storage, state/query manager, API браузера или Node.js. Проверяется весь транзитивный import-граф публичных фасетов.
|
||||
|
||||
Adapter module импортирует из собственного `business` только типы технических зависимостей. Он не импортирует `business/factory`, потому что не собирает API. Публичный deterministic runtime обычно также не нужен adapter: внешний результат возвращается business для проверки и error mapping.
|
||||
|
||||
Assembly не содержит inline adapters. Она импортирует production implementations через публичные API конкретных модулей `adapters/*`.
|
||||
|
||||
## Междоменные импорты
|
||||
|
||||
При связи, пересекающей границу пакета Level 2, разрешены два вида статического импорта из другого домена:
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
import { isAuthError } from '@/domains/auth/business/runtime'
|
||||
```
|
||||
|
||||
Первый импорт предоставляет type-only контракт для runtime-инъекции. Второй предоставляет только публичную детерминированную функцию или значение. Оба импорта являются рёбрами общего DAG.
|
||||
|
||||
Запрещено импортировать из другого домена:
|
||||
|
||||
- `business/factory`;
|
||||
- готовый API instance или singleton;
|
||||
- assembly;
|
||||
- adapter;
|
||||
- framework state, hook, context, Provider или component;
|
||||
- любой внутренний путь `business`.
|
||||
|
||||
Такая модель действует и между двумя пакетами Level 2, и между модулем Level 1 и пакетом Level 2. Между двумя простыми доменными модулями продолжают действовать правила Level 1.
|
||||
|
||||
## Детерминированный runtime
|
||||
|
||||
`business/runtime` закрывает случай общей продуктовой pure-функции, которую нельзя переносить в `shared` из-за знания о предметной области:
|
||||
|
||||
```ts
|
||||
import {
|
||||
normalizeAuthIdentifier,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
```
|
||||
|
||||
Функция остаётся собственностью Auth, а зависимый домен получает явное runtime-ребро к её публичному контракту. Если связь создаёт цикл, границы доменов или владелец общего понятия пересматриваются.
|
||||
|
||||
Перенос в `shared` допустим только после устранения продуктового знания, а не как обход cross-domain правила.
|
||||
|
||||
## Runtime-инъекция API
|
||||
|
||||
Готовый API другого домена передаётся assembly или одноразовому месту сборки аргументом. Код зависимого домена не импортирует его runtime-фабрику или сборку:
|
||||
|
||||
```text
|
||||
createAuthForRequest()
|
||||
→ AuthSessionApi
|
||||
→ createUserForRequest({ auth })
|
||||
→ UserProfileApi
|
||||
```
|
||||
|
||||
Место сборки графа создаёт независимые домены раньше зависимых. Если сбой Auth становится результатом публичного сценария User, наружу выходит только собственная ошибка User. При exception-модели User может распознать ожидаемый Auth error через публичный guard из `auth/business/runtime`.
|
||||
|
||||
## Совместное применение Level 1 и Level 2
|
||||
|
||||
Один SLM root может постоянно содержать обе формы. Каждая предметная область объявляется либо модулем, либо пакетом, поэтому checker однозначно определяет применимые правила.
|
||||
|
||||
Runtime-взаимодействие с пакетом Level 2 собирается за пределами доменного кода. Если существующий модуль Level 1 должен принимать готовый API, его собственный публичный контракт явно предоставляет такую точку инъекции. Если это невозможно без скрытого singleton или обратного импорта, зависимый модуль рефакторится либо переводится на Level 2.
|
||||
|
||||
Runtime-значение из модуля Level 1, необходимое пакету Level 2, также передаётся внешним graph owner как API, callback или другой явно типизированный аргумент. Код пакета не импортирует executable API модуля Level 1 напрямую.
|
||||
|
||||
## Framework-состояние
|
||||
|
||||
Framework binding module использует framework API только своего доменного пакета:
|
||||
|
||||
```ts
|
||||
// Допустимо внутри domains/auth/react/login-form
|
||||
import { useAuthSession } from '@/domains/auth/react/session'
|
||||
|
||||
// Недопустимо внутри domains/user/react/profile
|
||||
import { useAuthSession } from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
Во втором случае composition читает состояния обоих доменов и передаёт необходимые данные или callbacks через публичные свойства компонентов.
|
||||
|
||||
State/query cache внутри framework binding не создаёт исключение для cross-domain imports. Он работает поверх переданного API своего домена.
|
||||
|
||||
## Границы сред
|
||||
|
||||
Каждый client-, server- или shared-entry point имеет совместимый транзитивный import-граф. Серверная assembly или adapter не реэкспортируется через `business`, Framework Group или клиентскую assembly.
|
||||
|
||||
Tree shaking не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Доменные пакеты Level 2
|
||||
|
||||
Доменный пакет заменяет один выбранный доменный модуль Level 1. Он объединяет SLM-модули одной предметной области, но сам не является модулем, Group или публичным API.
|
||||
|
||||
```text
|
||||
domains/auth/
|
||||
├── business/
|
||||
├── assemblies/ # Обязательная Group
|
||||
├── adapters/ # При наличии technical dependencies
|
||||
└── react/
|
||||
├── session/
|
||||
└── login-form/
|
||||
```
|
||||
|
||||
Другие предметные области того же SLM root могут оставаться доменными модулями Level 1.
|
||||
|
||||
## Основные границы
|
||||
|
||||
- [Доменный пакет](./domain-package.md) определяет предметную и структурную policy boundary.
|
||||
- [Business](./business.md) владеет Domain API и разделяет public types, factories и необязательный deterministic runtime.
|
||||
- [Фабрики, зависимости и adapters](./factory-ports-adapters.md) изолируют runtime-capabilities, включая state/query runtime и недетерминизм.
|
||||
- [Assemblies](./assemblies.md) обязательны и собирают именованный граф нужных API для реального контекста.
|
||||
- [Состояние и кэш](./state-cache.md) разделяет предметную власть business и технические проекции.
|
||||
- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
|
||||
- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу.
|
||||
- [Переход auth](./auth-example.md) показывает локальный переход с Level 1 без big-bang всего root.
|
||||
- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей.
|
||||
|
||||
Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md).
|
||||
@@ -0,0 +1,170 @@
|
||||
# Assemblies и среды выполнения
|
||||
|
||||
> Пояснение повторяемой сборки именованного графа Domain API.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`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-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-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-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
|
||||
|
||||
## Назначение
|
||||
|
||||
Assembly является SLM-модулем в Group `assemblies`. Она создаёт явный граф одного или нескольких Domain API пакета для конкретного повторяемого контекста выполнения. Каждый доменный пакет содержит минимум одну assembly.
|
||||
|
||||
```text
|
||||
business/factory
|
||||
├── assemblies/browser → { session: AuthSessionApi }
|
||||
├── assemblies/request → { session, administration }
|
||||
└── assemblies/server-action → { administration }
|
||||
```
|
||||
|
||||
Архитектура не требует `base` или изоморфную assembly и не ограничивает их максимальное количество. Обязательная assembly соответствует реальному поддерживаемому контексту, а не существует только для заполнения структуры.
|
||||
|
||||
Место сборки графа в `composition` может вызвать фабрики напрямую для одноразовой конфигурации. Такая сборка не отменяет обязательную assembly пакета и при наличии технических зависимостей использует публичные adapter-модули, а не inline implementations.
|
||||
|
||||
## Именованный граф API
|
||||
|
||||
Browser assembly импортирует только фабрики и adapters нужных ей API:
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
import { authSessionFactory } from '@/domains/auth/business/factory'
|
||||
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
|
||||
import { createBrowserSessionAdapter } from '@/domains/auth/adapters/browser-session'
|
||||
|
||||
export type AuthBrowserGraph = Readonly<{
|
||||
session: AuthSessionApi
|
||||
}>
|
||||
|
||||
export const createBrowserAuth = (): AuthBrowserGraph => {
|
||||
const session = authSessionFactory({
|
||||
phone: createPhoneHttpAdapter(),
|
||||
session: createBrowserSessionAdapter(),
|
||||
})
|
||||
|
||||
return { session }
|
||||
}
|
||||
```
|
||||
|
||||
Request assembly может собрать дополнительный API, которого нет в браузере:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthAdministrationApi,
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/business'
|
||||
|
||||
import {
|
||||
authAdministrationFactory,
|
||||
authSessionFactory,
|
||||
} from '@/domains/auth/business/factory'
|
||||
|
||||
export type AuthRequestGraph = Readonly<{
|
||||
administration: AuthAdministrationApi
|
||||
session: AuthSessionApi
|
||||
}>
|
||||
```
|
||||
|
||||
Assembly не добавляет методы к этим контрактам и не создаёт общий `AuthApi`. Именованный объект только сообщает, какие независимые API доступны в контексте.
|
||||
|
||||
## Cross-domain input
|
||||
|
||||
Assembly зависимого домена принимает готовый API аргументом. Cross-domain API не является adapter и не размещается в Group `adapters`:
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
import type { UserProfileApi } from '@/domains/user/business'
|
||||
import { userProfileFactory } from '@/domains/user/business/factory'
|
||||
import { createUserProfileAdapter } from '@/domains/user/adapters/profile'
|
||||
|
||||
export type CreateUserForRequestInput = {
|
||||
auth: Pick<AuthSessionApi, 'getSnapshot'>
|
||||
request: UserRequestInput
|
||||
}
|
||||
|
||||
export type UserRequestGraph = Readonly<{
|
||||
profile: UserProfileApi
|
||||
}>
|
||||
|
||||
export const createUserForRequest = ({
|
||||
auth,
|
||||
request,
|
||||
}: CreateUserForRequestInput): UserRequestGraph => {
|
||||
const profile = userProfileFactory({
|
||||
auth,
|
||||
profile: createUserProfileAdapter(request),
|
||||
})
|
||||
|
||||
return { profile }
|
||||
}
|
||||
```
|
||||
|
||||
Assembly делает только type-only импорт `AuthSessionApi`. Runtime-фабрику, assembly или instance Auth она не импортирует.
|
||||
|
||||
Место сборки графа выполняет runtime-связь:
|
||||
|
||||
```ts
|
||||
const auth = createAuthForRequest(authInput)
|
||||
const user = createUserForRequest({
|
||||
auth: auth.session,
|
||||
request: userInput,
|
||||
})
|
||||
```
|
||||
|
||||
## Environment entry points
|
||||
|
||||
Server assembly имеет отдельный публичный entry point и marker выбранного framework или bundler:
|
||||
|
||||
```ts
|
||||
import 'server-only'
|
||||
|
||||
export { createAuthForRequest } from './create-auth-for-request'
|
||||
```
|
||||
|
||||
Server entry point не реэкспортируется через `business`, Framework Group, browser assembly или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Factory не запускает запрос, subscription, timer или другую скрытую долгоживущую работу во время создания API. Assembly может активировать технический ресурс только с явной передачей cleanup своему caller. Операция Domain API, которая запускает ресурс позже, сама возвращает cleanup:
|
||||
|
||||
```ts
|
||||
const stop = auth.session.startInvalidationTracking()
|
||||
|
||||
try {
|
||||
// Scope использует API.
|
||||
} finally {
|
||||
await stop()
|
||||
}
|
||||
```
|
||||
|
||||
Если assembly сама создаёт ресурс, принадлежащий всему возвращённому графу, результат дополнительно предоставляет cleanup handle:
|
||||
|
||||
```ts
|
||||
export type AuthRequestAssembly = Readonly<{
|
||||
apis: AuthRequestGraph
|
||||
dispose: () => Promise<void>
|
||||
}>
|
||||
```
|
||||
|
||||
```ts
|
||||
const auth = createAuthForRequest(input)
|
||||
|
||||
try {
|
||||
return await handleRequest(auth.apis)
|
||||
} finally {
|
||||
await auth.dispose()
|
||||
}
|
||||
```
|
||||
|
||||
Assembly без собственного ресурса не обязана возвращать пустой `dispose`. Graph owner вызывает каждый реально предоставленный cleanup не позже завершения application, route, request или test scope.
|
||||
|
||||
Одноразовое место сборки, которое вызывает фабрики напрямую, подчиняется той же границе: оно не запускает скрытый ресурс в constructor и сохраняет cleanup любого созданного adapter-ресурса до завершения своего scope.
|
||||
|
||||
Рекомендуется делать `dispose` идемпотентным, освобождать частично созданные ресурсы при ошибке assembly и закрывать зависимые ресурсы раньше их зависимостей. Детальная политика rollback и поведения API после cleanup остаётся открытым вопросом.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Переход домена auth с Level 1
|
||||
|
||||
> Проверочный пример локального перехода от доменного модуля к доменному пакету.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
|
||||
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-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-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
|
||||
## Исходная форма Level 1
|
||||
|
||||
```text
|
||||
domains/
|
||||
├── auth/ # Доменный модуль
|
||||
│ ├── hooks/
|
||||
│ ├── services/
|
||||
│ ├── stores/
|
||||
│ ├── ui/
|
||||
│ └── index.ts # Общий API модуля
|
||||
└── catalog/ # Независимый доменный модуль
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Level 1 разрешает business-сценариям, framework hooks, state adapter и локальной сборке Auth находиться внутри одного модуля.
|
||||
|
||||
## Целевая форма Auth
|
||||
|
||||
```text
|
||||
domains/
|
||||
├── auth/ # Доменный пакет Level 2
|
||||
│ ├── README.md
|
||||
│ ├── business/ # Один SLM-модуль
|
||||
│ │ ├── errors/
|
||||
│ │ ├── factories/
|
||||
│ │ ├── services/
|
||||
│ │ ├── types/
|
||||
│ │ ├── index.ts # Только public types нескольких API
|
||||
│ │ ├── factory.ts # Public factories entry
|
||||
│ │ └── runtime.ts # Error codes, guards, public pure runtime
|
||||
│ ├── adapters/ # Group
|
||||
│ │ ├── phone-http/ # SLM-модуль
|
||||
│ │ ├── browser-session/ # SLM-модуль
|
||||
│ │ └── request-session/ # SLM-модуль
|
||||
│ ├── assemblies/ # Обязательная Group
|
||||
│ │ ├── browser/ # Только AuthSessionApi
|
||||
│ │ └── request/ # Session + Administration API
|
||||
│ └── react/ # Framework Group
|
||||
│ ├── session/ # SLM-модуль
|
||||
│ └── login-form/ # SLM-модуль
|
||||
└── catalog/ # По-прежнему модуль Level 1
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Корневой `domains/auth/index.ts` удаляется. Потребители переходят на публичные API конкретных модулей. `catalog` и остальные домены не меняют форму только из-за перехода Auth.
|
||||
|
||||
## Перенос ответственности
|
||||
|
||||
| Исходная часть | Владелец Level 2 | Публичный путь |
|
||||
|---|---|---|
|
||||
| Session-сценарии и public types | `auth/business` | `auth/business` |
|
||||
| Administration-сценарии и public types | `auth/business` | `auth/business` |
|
||||
| Runtime-фабрики API | `auth/business` | `auth/business/factory` |
|
||||
| Коды, guards и public pure-функции | `auth/business` | `auth/business/runtime` |
|
||||
| Browser storage и HTTP adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
|
||||
| Cookies, request data и server adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
|
||||
| Browser graph | `auth/assemblies/browser` | `auth/assemblies/browser` |
|
||||
| Request graph | `auth/assemblies/request` | `auth/assemblies/request` |
|
||||
| Provider и session hooks | `auth/react/session` | `auth/react/session` |
|
||||
| Переиспользуемая форма | `auth/react/login-form` | `auth/react/login-form` |
|
||||
| Страница, текст и redirect | `compositions` | API конкретной composition |
|
||||
|
||||
## Новые импорты
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthAdministrationApi,
|
||||
AuthError,
|
||||
AuthErrorCode,
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/business'
|
||||
|
||||
import {
|
||||
authAdministrationFactory,
|
||||
authSessionFactory,
|
||||
} from '@/domains/auth/business/factory'
|
||||
|
||||
import {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
|
||||
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
|
||||
import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
|
||||
import { AuthSessionProvider } from '@/domains/auth/react/session'
|
||||
import { LoginForm } from '@/domains/auth/react/login-form'
|
||||
```
|
||||
|
||||
## Cross-domain граф
|
||||
|
||||
Если User package зависит от Auth, он получает только type-only API contract и при необходимости deterministic runtime:
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
import { isAuthError } from '@/domains/auth/business/runtime'
|
||||
|
||||
export type UserDeps = {
|
||||
auth: Pick<AuthSessionApi, 'getSnapshot'>
|
||||
}
|
||||
```
|
||||
|
||||
Место сборки создаёт instances:
|
||||
|
||||
```ts
|
||||
const auth = createBrowserAuth()
|
||||
const user = createBrowserUser({ auth: auth.session })
|
||||
```
|
||||
|
||||
User не импортирует Auth factory или assembly, а его React-модули не импортируют `useAuthSession` или Auth components.
|
||||
|
||||
Если User остаётся модулем Level 1, runtime-связь также выполняется снаружи доменных границ. Для этого его собственный API должен иметь явную точку передачи нужного Auth behavior; иначе именно User требуется рефакторинг или переход, но несвязанные домены не затрагиваются.
|
||||
|
||||
## Порядок перехода
|
||||
|
||||
1. Выбрать один доменный модуль и зафиксировать его внешние consumers.
|
||||
2. Объявить `business` с type-only и factory entry points.
|
||||
3. Разделить сценарии на независимо собираемые Domain API без дублирования методов.
|
||||
4. Добавить `business/runtime`, только если внешним consumers нужны codes, guards или pure-функции.
|
||||
5. Оформить каждую связную production implementation модулем `adapters/*`.
|
||||
6. Создать минимум одну assembly и перенести туда повторяемый выбор API и adapters.
|
||||
7. Разделить React-ответственности на модули внутри Group `react`.
|
||||
8. Перенести страницы, redirects и multi-domain UI в `compositions`.
|
||||
9. Перевести внешние импорты на разрешённые public paths.
|
||||
10. Удалить старый root `index.ts` Auth и объявить пакетную форму checker-у.
|
||||
|
||||
Завершённость перехода определяется только границей Auth. Наличие других доменных модулей Level 1 не является миграционным долгом.
|
||||
@@ -0,0 +1,218 @@
|
||||
# Модуль business
|
||||
|
||||
> Пояснение предметного владельца, нескольких Domain API и публичного deterministic runtime.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
|
||||
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
|
||||
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
|
||||
- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009)
|
||||
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
|
||||
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
|
||||
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
|
||||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
|
||||
- [`SLM-L2-BUSINESS-R025`](../../rules/level-2.md#slm-l2-business-r025)
|
||||
|
||||
## Роль
|
||||
|
||||
`business` является обязательным SLM-модулем доменного пакета. Он владеет:
|
||||
|
||||
- публичными предметными сценариями;
|
||||
- одним или несколькими именованными Domain API;
|
||||
- одной публичной фабрикой для каждого API;
|
||||
- типами явных зависимостей фабрик;
|
||||
- предметными типами и детерминированными правилами;
|
||||
- контрактами ожидаемых доменных ошибок;
|
||||
- публичным представлением доменных данных и состояния.
|
||||
|
||||
Несколько API остаются частью одного модуля, пока относятся к одной связной предметной области. Они нужны не для копирования use cases, а для независимой сборки разных наборов сценариев, зависимостей и сред.
|
||||
|
||||
## Публичные фасеты
|
||||
|
||||
Один логический API модуля `business` имеет два обязательных и один необязательный entry point.
|
||||
|
||||
### Type-only barrel
|
||||
|
||||
Корневой `business/index.ts` экспортирует только типы:
|
||||
|
||||
```ts
|
||||
export type {
|
||||
AuthAdministrationApi,
|
||||
AuthAdministrationDeps,
|
||||
AuthAdministrationFactory,
|
||||
AuthError,
|
||||
AuthErrorCode,
|
||||
AuthSessionApi,
|
||||
AuthSessionDeps,
|
||||
AuthSessionFactory,
|
||||
AuthState,
|
||||
} from './types'
|
||||
```
|
||||
|
||||
Потребитель использует этот путь только через `import type`:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthSessionApi,
|
||||
AuthState,
|
||||
} from '@/domains/auth/business'
|
||||
```
|
||||
|
||||
### Factory entry
|
||||
|
||||
`business/factory.ts` экспортирует только именованные runtime-фабрики:
|
||||
|
||||
```ts
|
||||
export { authAdministrationFactory } from './factories/auth-administration.factory'
|
||||
export { authSessionFactory } from './factories/auth-session.factory'
|
||||
```
|
||||
|
||||
```ts
|
||||
import {
|
||||
authAdministrationFactory,
|
||||
authSessionFactory,
|
||||
} from '@/domains/auth/business/factory'
|
||||
```
|
||||
|
||||
Каждому API соответствует одна фабрика. Фасет не экспортирует готовые instances, adapters или environment-specific assembly.
|
||||
|
||||
### Runtime entry
|
||||
|
||||
Необязательный `business/runtime.ts` экспортирует только публичные детерминированные значения и функции:
|
||||
|
||||
```ts
|
||||
export {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from './errors/auth-error'
|
||||
|
||||
export { normalizeAuthIdentifier } from './lib/normalize-auth-identifier'
|
||||
```
|
||||
|
||||
Здесь допустимы error codes и guards, validators, value constructors, чистые продуктовые функции и immutable-константы. Все public types по-прежнему импортируются из корневого type-only barrel.
|
||||
|
||||
`business/runtime` не содержит:
|
||||
|
||||
- фабрики и готовые API instances;
|
||||
- I/O или изменяемое состояние;
|
||||
- state/query runtime;
|
||||
- чтение clock, random, environment или platform API;
|
||||
- сценарии, которым нужны runtime-зависимости.
|
||||
|
||||
Если внешним потребителям не нужен детерминированный runtime, файл `runtime.ts` не создаётся. Другие внешние пути внутри `business` являются deep imports.
|
||||
|
||||
## Несколько Domain API
|
||||
|
||||
```ts
|
||||
export type AuthSessionApi = {
|
||||
getCurrentSession: () => Promise<AuthState>
|
||||
getSnapshot: () => AuthState
|
||||
requestPhoneOtp: (phone: string) => Promise<void>
|
||||
startInvalidationTracking: () => () => Promise<void>
|
||||
verifyPhoneOtp: (code: string) => Promise<void>
|
||||
}
|
||||
|
||||
export type AuthAdministrationApi = {
|
||||
revokeUserSessions: (userId: string) => Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
`AuthSessionApi` может собираться в browser и request contexts, а `AuthAdministrationApi` только в доверенной server assembly. Browser assembly не получает метод-заглушку и не импортирует adapters административного API.
|
||||
|
||||
Общий `business/factory` является публичным фасетом, а не гарантией отдельного business chunk для каждой фабрики. Разделение API устраняет обязательное создание лишних adapters и instances. Если самой business-логике нужны независимо поставляемые bundle boundaries, требуется отдельный build/package mechanism за пределами текущего Level 2, а не искусственное разделение предметной области.
|
||||
|
||||
Один публичный сценарий принадлежит ровно одному API. Если два API постоянно требуют одинаковых методов, состояния и lifecycle, их граница пересматривается вместо дублирования.
|
||||
|
||||
Assembly может вернуть именованный граф нескольких API:
|
||||
|
||||
```ts
|
||||
export type AuthBrowserGraph = Readonly<{
|
||||
session: AuthSessionApi
|
||||
}>
|
||||
|
||||
export type AuthRequestGraph = Readonly<{
|
||||
administration: AuthAdministrationApi
|
||||
session: AuthSessionApi
|
||||
}>
|
||||
```
|
||||
|
||||
Такой граф является составом готовых контрактов для контекста, а не новым предметным API.
|
||||
|
||||
## Предметная власть и состояние
|
||||
|
||||
Business API остаются единственной границей, определяющей предметную модель, validation, переходы и семантику результатов. Это не означает, что только `business` физически хранит байты.
|
||||
|
||||
Adapter или framework binding может использовать Zustand, TanStack Query, SWR, Apollo либо другой runtime для хранения и доставки значений. Такое хранилище является технической реализацией или проекцией, если:
|
||||
|
||||
- значения получены или проверены business API либо `business/runtime`;
|
||||
- предметные переходы выполняются через business API;
|
||||
- внешний DTO не становится публичной моделью напрямую;
|
||||
- optimistic value создаётся или проверяется предметным владельцем;
|
||||
- библиотечные cache/store types не становятся Domain API.
|
||||
|
||||
Подробная граница описана в [Состоянии и кэше](./state-cache.md).
|
||||
|
||||
## Потребители фасетов
|
||||
|
||||
| Потребитель | `business` | `business/factory` | `business/runtime` |
|
||||
|---|---|---|---|
|
||||
| Adapter своего домена | Type-only | Нет | Обычно нет |
|
||||
| Assembly своего домена | Type-only | Да | При необходимости |
|
||||
| Framework binding своего домена | Type-only | Нет | Да, если нужен public runtime |
|
||||
| `composition` или `app` | Type-only | Для одноразовой сборки | Да |
|
||||
| Код другого домена при связи с Level 2 | Type-only | Нет | Да |
|
||||
| Тест | Type-only | По границе тестируемого API | По границе тестируемого владельца |
|
||||
|
||||
Runtime-импорт `business/runtime` другого домена остаётся архитектурным ребром и участвует в общей проверке циклов.
|
||||
|
||||
## Контракт ошибок
|
||||
|
||||
Ожидаемая ошибка имеет устойчивую безопасную readonly-форму:
|
||||
|
||||
```ts
|
||||
export type AuthErrorCode =
|
||||
| 'AUTH_PHONE_INVALID'
|
||||
| 'AUTH_OTP_REQUEST_FAILED'
|
||||
| 'AUTH_OTP_CODE_INVALID'
|
||||
|
||||
export type AuthError = Readonly<{
|
||||
code: AuthErrorCode
|
||||
}>
|
||||
```
|
||||
|
||||
Если runtime-потребителям нужны constants или guard, они публикуются через `business/runtime`:
|
||||
|
||||
```ts
|
||||
export const AUTH_ERROR_CODES = {
|
||||
PHONE_INVALID: 'AUTH_PHONE_INVALID',
|
||||
OTP_REQUEST_FAILED: 'AUTH_OTP_REQUEST_FAILED',
|
||||
OTP_CODE_INVALID: 'AUTH_OTP_CODE_INVALID',
|
||||
} as const
|
||||
|
||||
export const isAuthError = (value: unknown): value is AuthError => {
|
||||
return isSafeCodedError(value, Object.values(AUTH_ERROR_CODES))
|
||||
}
|
||||
```
|
||||
|
||||
Guard не является обязательной частью каждого домена. При discriminated `Result` типизированному потребителю может быть достаточно error union; при exception, RPC или неизвестной runtime-границе guard часто нужен. Выбор `throw` или `Result` не меняет набор обязательных фасетов.
|
||||
|
||||
## Изоляция технических и чужих ошибок
|
||||
|
||||
Ошибки SDK, HTTP, database, storage и adapters не пересекают Domain API в форме, доступной приложению. `business` преобразует обрабатываемый технический сбой в собственный код:
|
||||
|
||||
```text
|
||||
SDK error
|
||||
→ adapter failure
|
||||
→ business mapping
|
||||
→ AuthErrorCode
|
||||
→ приложение
|
||||
```
|
||||
|
||||
Публичная форма не включает исходные `message`, `status`, `payload`, `cause`, SDK class или сам объект ошибки. Диагностические данные остаются во внутреннем observability-механизме.
|
||||
|
||||
То же относится к cross-domain вызову. Если User API использует Auth API, сбой, который становится результатом публичного сценария User, представлен собственным `UserError`. При exception-модели зависимый business может импортировать `isAuthError` и error codes из `auth/business/runtime`, после чего преобразовать ожидаемую ошибку в собственный контракт.
|
||||
|
||||
Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Граница доменного пакета
|
||||
|
||||
> Пояснение контейнерной сущности 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-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
|
||||
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
|
||||
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-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` может содержать business-сценарии авторизации, её browser/server assemblies и React-модули, но не страницу профиля или общий database client.
|
||||
|
||||
Пакет не владеет исполняемой ответственностью. Конкретными API, состоянием, зависимостями и lifecycle владеют модули внутри него.
|
||||
|
||||
Level 2 применяется к пакету, а не ко всему SLM root. Например, `auth` может быть пакетом Level 2, пока `catalog` и `news` остаются доменными модулями Level 1.
|
||||
|
||||
## Корень пакета
|
||||
|
||||
```text
|
||||
domains/auth/
|
||||
├── README.md
|
||||
├── business/
|
||||
├── assemblies/
|
||||
├── adapters/
|
||||
└── react/
|
||||
```
|
||||
|
||||
В корне разрешены:
|
||||
|
||||
- документация;
|
||||
- ownership metadata;
|
||||
- декларативный manifest или декларативная конфигурация архитектурной проверки;
|
||||
- обязательный модуль `business`;
|
||||
- обязательная непустая Group `assemblies`;
|
||||
- непустая Group `adapters`, если хотя бы одна фабрика имеет технические зависимости;
|
||||
- Framework Groups при наличии соответствующих модулей.
|
||||
|
||||
В корне запрещены:
|
||||
|
||||
- `index.ts` или другой агрегирующий executable entry point;
|
||||
- runtime-файлы и side effects;
|
||||
- изменяемое состояние и ресурсы lifecycle;
|
||||
- реэкспорт API внутренних модулей;
|
||||
- page-specific компоненты или сборка нескольких доменов.
|
||||
|
||||
Metadata содержит только статические данные. Проверяющий инструмент может читать её декларативно, но она не исполняется и не становится скрытым API пакета.
|
||||
|
||||
## Policy boundary
|
||||
|
||||
Доменный пакет не является узлом import-графа. Его техническая граница состоит из объявленного проверке набора дочерних модулей и правил, применяемых ко всем связям через эту границу.
|
||||
|
||||
Отсутствие root barrel намеренно:
|
||||
|
||||
- client- и server-entry points не агрегируются в один импорт;
|
||||
- каждый модуль сохраняет отдельную ответственность и environment boundary;
|
||||
- переименование публичного модуля является изменением его собственного контракта, а не скрывается пакетом;
|
||||
- versioning целого publishable package остаётся за пределами Level 2.
|
||||
|
||||
## Модули и Groups
|
||||
|
||||
`business` размещается непосредственно в пакете. Assemblies находятся в обязательной Group `assemblies`. Production adapters являются самостоятельными модулями Group `adapters`. Framework Group называется по фреймворку: `react`, `vue` и аналогично.
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── business/ # SLM-модуль
|
||||
│ ├── index.ts # Только public types
|
||||
│ ├── factory.ts # Public factories entry
|
||||
│ └── runtime.ts # Необязательный deterministic runtime
|
||||
├── adapters/ # Group при наличии technical dependencies
|
||||
│ └── phone-http/ # SLM-модуль
|
||||
├── assemblies/ # Обязательная Group
|
||||
│ └── browser/ # SLM-модуль
|
||||
└── react/ # Framework Group
|
||||
├── session/ # SLM-модуль
|
||||
└── login-form/ # SLM-модуль
|
||||
```
|
||||
|
||||
Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/business/factory`, опциональный `auth/business/runtime`, `auth/adapters/phone-http`, `auth/assemblies/browser` и `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, а не внутренние модули пакета.
|
||||
|
||||
Одна предметная область не представляется одновременно модулем и пакетом. Переход завершается удалением старой границы именно этого домена, а не переводом всех соседних областей.
|
||||
|
||||
## Границы соседних слоёв
|
||||
|
||||
| Ответственность | Владелец |
|
||||
|---|---|
|
||||
| Предметные сценарии, Domain API, доменные ошибки | `business` |
|
||||
| Техническая реализация зависимости одного домена | Adapter внутри пакета |
|
||||
| Сборка API для именованного контекста | Assembly внутри пакета |
|
||||
| Универсальный технический сервис | `infra` |
|
||||
| Domain-specific framework API | Модуль внутри `react`, `vue` и аналогичной Group |
|
||||
| Страница, маршрут, redirect, продуктовый текст | `compositions` |
|
||||
| UI, объединяющий несколько доменов | `compositions` |
|
||||
|
||||
Зависимость от React сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.
|
||||
@@ -0,0 +1,158 @@
|
||||
# Фабрики, зависимости и adapters
|
||||
|
||||
> Пояснение границы между `business` и технической средой.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
|
||||
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
|
||||
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
|
||||
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
|
||||
|
||||
## Одна фабрика на API
|
||||
|
||||
```text
|
||||
явные зависимости + business factory → один Domain API
|
||||
```
|
||||
|
||||
Модуль `business` предоставляет одну именованную фабрику для каждого объявленного API. Фабрики экспортируются через общий runtime-фасет `business/factory`:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthAdministrationApi,
|
||||
AuthAdministrationDeps,
|
||||
AuthSessionApi,
|
||||
AuthSessionDeps,
|
||||
} from '@/domains/auth/business'
|
||||
|
||||
export type AuthSessionFactory = (
|
||||
deps: AuthSessionDeps,
|
||||
) => AuthSessionApi
|
||||
|
||||
export type AuthAdministrationFactory = (
|
||||
deps: AuthAdministrationDeps,
|
||||
) => AuthAdministrationApi
|
||||
```
|
||||
|
||||
```ts
|
||||
import {
|
||||
authAdministrationFactory,
|
||||
authSessionFactory,
|
||||
} from '@/domains/auth/business/factory'
|
||||
```
|
||||
|
||||
Фабрика не выбирает environment, assembly или конкретную production-реализацию зависимости. Разные API могут иметь разные наборы deps и собираться независимо.
|
||||
|
||||
## Технические зависимости
|
||||
|
||||
Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы.
|
||||
|
||||
```ts
|
||||
export type AuthPhoneDependency = {
|
||||
requestCode: (phone: string) => Promise<unknown>
|
||||
verifyCode: (code: string) => Promise<unknown>
|
||||
}
|
||||
```
|
||||
|
||||
`unknown` допустим на границе непроверенного внешнего результата. `business` проверяет его до превращения в предметные данные или состояние.
|
||||
|
||||
Техническими зависимостями также являются:
|
||||
|
||||
- concrete state/query runtime;
|
||||
- subscription и event source;
|
||||
- browser, Node.js и framework capabilities;
|
||||
- request data и abort signal;
|
||||
- текущее время и timer;
|
||||
- random и ID generator;
|
||||
- environment и runtime configuration provider.
|
||||
|
||||
```ts
|
||||
export type VerificationDeps = {
|
||||
clock: { now: () => number }
|
||||
ids: { create: () => string }
|
||||
timer: { delay: (ms: number) => Promise<void> }
|
||||
}
|
||||
```
|
||||
|
||||
`Date.now()`, `new Date()` без аргумента, `Math.random()`, `crypto.randomUUID()`, скрытый env и глобальный timer не читаются напрямую business-кодом, если влияют на поведение сценария. Детерминированная арифметика над переданным timestamp остаётся business-safe.
|
||||
|
||||
Cancellation также описывается business-owned контрактом. Concrete `AbortSignal` или другой platform type остаётся внутри adapter, пока не принят отдельный общий публичный контракт.
|
||||
|
||||
Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics.
|
||||
|
||||
## Cross-domain API dependency
|
||||
|
||||
Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом:
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
|
||||
export type UserDeps = {
|
||||
auth: Pick<AuthSessionApi, 'getSnapshot'>
|
||||
}
|
||||
```
|
||||
|
||||
Runtime-значение место сборки графа передаёт через assembly либо напрямую зависимой business-фабрике. `user/business` не импортирует executable factory, assembly или instance Auth.
|
||||
|
||||
Если exception-модели нужен runtime guard чужой ожидаемой ошибки, зависимый business может импортировать его из детерминированного `auth/business/runtime`. Этот импорт остаётся обычным ребром DAG.
|
||||
|
||||
## Adapter module
|
||||
|
||||
Adapter module соединяет явную техническую зависимость фабрики с конкретной системой:
|
||||
|
||||
```text
|
||||
business dependency ← adapter → SDK / query runtime / platform / request data
|
||||
```
|
||||
|
||||
Adapter преобразует аргументы и технический результат к контракту зависимости. Он не определяет публичный метод Domain API, предметный fallback или код доменной ошибки.
|
||||
|
||||
Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение не строит поведение по HTTP status, SDK error class или storage exception.
|
||||
|
||||
Adapter может использовать TanStack Query, Apollo, Zustand или другой state/query runtime, если реализует business-owned dependency. Library types, query keys и mutable clients не протекают в public types `business`.
|
||||
|
||||
## Размещение adapters
|
||||
|
||||
Каждая связная production-реализация является отдельным SLM-модулем в Group `adapters`:
|
||||
|
||||
```text
|
||||
auth/adapters/
|
||||
├── phone-http/
|
||||
│ └── index.ts
|
||||
├── browser-session/
|
||||
│ └── index.ts
|
||||
└── browser-runtime/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Один adapter-модуль может реализовать несколько тесно связанных зависимостей одного technical provider. Например, `browser-runtime` может предоставить clock, timer и ID generator. Правило не требует отдельного модуля для каждого метода deps.
|
||||
|
||||
Adapter module имеет собственные ответственность, публичный API, environment label и тестовую границу. Group `adapters` не имеет `index.ts` и не реэкспортирует дочерние модули.
|
||||
|
||||
Production adapter запрещено определять:
|
||||
|
||||
- закрытым сегментом assembly;
|
||||
- inline-функцией в `composition` или `app`;
|
||||
- частью framework binding module;
|
||||
- скрытой реализацией внутри `business`.
|
||||
|
||||
Assembly и одноразовое место сборки импортируют конкретные adapter-модули через их публичные API:
|
||||
|
||||
```ts
|
||||
import { authSessionFactory } from '@/domains/auth/business/factory'
|
||||
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
|
||||
import { createBrowserRuntimeAdapter } from '@/domains/auth/adapters/browser-runtime'
|
||||
|
||||
const session = authSessionFactory({
|
||||
phone: createPhoneHttpAdapter(),
|
||||
runtime: createBrowserRuntimeAdapter(),
|
||||
})
|
||||
```
|
||||
|
||||
Если ни одна фабрика не имеет технических зависимостей, Group `adapters` не обязательна. Cross-domain API dependency не считается adapter и передаётся отдельно.
|
||||
|
||||
Локальные fake implementations в business-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Framework Groups и модули
|
||||
|
||||
> Пояснение domain-specific framework-кода на примере React.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-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-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
|
||||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
|
||||
## 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`, состояния, реализации, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
|
||||
|
||||
Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. Он не является вложенным модулем, потому что родительская граница `react` является Group.
|
||||
|
||||
## Framework binding module
|
||||
|
||||
Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании одного или нескольких готовых Domain API своего домена с конкретным framework. Сам факт зависимости от framework недостаточен: framework-specific assembly остаётся в `assemblies`, а page-specific модуль остаётся в `compositions`.
|
||||
|
||||
Framework binding module может:
|
||||
|
||||
- передавать готовые API через Provider и context;
|
||||
- предоставлять domain-specific hooks;
|
||||
- отображать состояние и безопасные ошибки домена;
|
||||
- использовать framework-compatible state/query runtime;
|
||||
- реализовывать переиспользуемую domain-specific форму или guard;
|
||||
- связывать framework lifecycle с явными операциями Domain API.
|
||||
|
||||
Он не вызывает business-фабрику или assembly, не выбирает adapters и не создаёт новые предметные сценарии.
|
||||
|
||||
Framework binding импортирует типы и deterministic runtime через разные фасеты:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthError,
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/business'
|
||||
|
||||
import {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
```
|
||||
|
||||
Импорт `business/factory` запрещён: готовые API передаются модулю извне.
|
||||
|
||||
## Модуль session
|
||||
|
||||
`auth/react/session` может владеть Provider и hooks доступа к уже созданному `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,
|
||||
useAuthSession,
|
||||
} from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API.
|
||||
|
||||
## State/query runtime
|
||||
|
||||
`auth/react/queries` может использовать TanStack Query, SWR или другой React runtime поверх готового `AuthSessionApi`. Query cache остаётся framework projection, пока данные и transitions проходят через API, а optimistic values создаются или проверяются business-владельцем.
|
||||
|
||||
```ts
|
||||
export const useAuthSessionQuery = () => {
|
||||
const api = useAuthSession()
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['auth', 'session'],
|
||||
queryFn: api.getCurrentSession,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Server prefetch и client hook могут быть разными модулями Framework Group с совместимыми environment markers. Общая query policy выносится в отдельный framework-модуль при наличии нескольких потребителей, а не дублируется в compositions.
|
||||
|
||||
Подробности описаны в [Состоянии и кэше](./state-cache.md).
|
||||
|
||||
## Модуль login-form
|
||||
|
||||
`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с Auth API, состоянием и ошибками своего домена. Она может использовать публичный API соседнего `auth/react/session`, если зависимость остаётся ацикличной.
|
||||
|
||||
Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Domain-owned guard может решить, разрешено ли действие, но политика перехода на `/login` остаётся у route composition.
|
||||
|
||||
## Запрет cross-domain framework imports
|
||||
|
||||
Framework binding module не импортирует hooks, contexts, Providers, components или framework state другого домена:
|
||||
|
||||
```ts
|
||||
// Недопустимо: domains/user/react/profile
|
||||
import { useAuthSession } from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
Cross-domain UI собирается в `compositions`:
|
||||
|
||||
```tsx
|
||||
const session = useAuthSession()
|
||||
|
||||
return (
|
||||
<UserProfile
|
||||
userId={session.userId}
|
||||
canEdit={session.isAuthenticated}
|
||||
/>
|
||||
)
|
||||
```
|
||||
|
||||
Передача через props является границей композиции, но не требует prop drilling внутри домена: каждый пакет может использовать собственный Provider и context. Если User business постоянно нуждается в Auth, готовый `AuthSessionApi` передаётся User factory при сборке графа, а User framework module работает уже со своим API.
|
||||
|
||||
## Публичные API
|
||||
|
||||
```ts
|
||||
import { AuthSessionProvider } from '@/domains/auth/react/session'
|
||||
import { LoginForm } from '@/domains/auth/react/login-form'
|
||||
```
|
||||
|
||||
Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Открытые вопросы Level 2
|
||||
|
||||
> Эти вопросы не являются правилами и не отменяют зафиксированные границы.
|
||||
|
||||
## Зафиксированные решения
|
||||
|
||||
- Level 1 является общей базой, а Level 2 применяется отдельно к выбранным предметным областям.
|
||||
- Доменный модуль Level 1 и доменный пакет Level 2 могут постоянно сосуществовать в одном SLM root.
|
||||
- Одна предметная область имеет только одну форму.
|
||||
- Корень пакета содержит только metadata, модуль `business` и допустимые Groups и не имеет executable API.
|
||||
- `business` может объявлять несколько независимо собираемых Domain API и одну фабрику для каждого API.
|
||||
- Публичный API `business` имеет обязательные type-only `business` и runtime `business/factory`, а также необязательный deterministic `business/runtime`.
|
||||
- Приложение получает предметную модель, transitions и результаты через Domain API; technical и framework cache могут хранить и проецировать эти значения.
|
||||
- Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена в исходной форме.
|
||||
- Каждый доменный пакет содержит минимум одну assembly; универсальная изоморфная assembly не обязательна.
|
||||
- Assembly может создавать именованный граф нескольких API и не добавляет к ним сценарии.
|
||||
- При наличии технических зависимостей Group `adapters` обязательна, а каждая связная production implementation принадлежит adapter-модулю.
|
||||
- Runtime cross-domain imports через пакетную границу разрешены только из `business/runtime`; type-only public contracts разрешены и входят в DAG.
|
||||
- Framework Group называется по фреймворку и содержит самостоятельные SLM-модули.
|
||||
- Cross-domain framework state, hooks, contexts и components не импортируются.
|
||||
- Clock, timer, random, ID generator и environment являются явными dependencies business.
|
||||
- Assembly без собственного lifecycle-ресурса не обязана возвращать пустой `dispose`.
|
||||
|
||||
## Владение состоянием
|
||||
|
||||
Нужно подробнее определить создание initial state, применение transitions, persistence и внешний event input. Нормативным уже является то, что предметную модель и переходы определяет `business`, а concrete state manager реализует business-owned port либо framework projection.
|
||||
|
||||
Отдельно требуется проверить SSR snapshot, hydration, concurrent rendering, reset и поведение после завершения request scope.
|
||||
|
||||
## Передача ошибок
|
||||
|
||||
Нужно выбрать общую рекомендацию exception или discriminated `Result`, определить cancellation и unexpected failures, а также сериализацию ошибок через RPC и server actions.
|
||||
|
||||
Структура публичных фасетов от этого решения больше не зависит: type errors публикуются через `business`, а необходимые runtime codes и guards через опциональный `business/runtime`.
|
||||
|
||||
## Технические порты
|
||||
|
||||
Нужно определить, является ли consumer-owned port обязательной формой каждой технической зависимости и какие behavioral guarantees он описывает: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
|
||||
|
||||
Cross-domain API dependency уже зафиксирована как отдельный вид зависимости и не зависит от этого решения.
|
||||
|
||||
## Lifecycle сборки
|
||||
|
||||
Уже зафиксировано, что явная операция, запускающая ресурс, возвращает cleanup, а assembly с собственным ресурсом предоставляет cleanup handle результата. Ещё нужно определить async cleanup, rollback частичной сборки, repeated disposal, request abort и поведение API после завершения scope.
|
||||
|
||||
## Cache hydration
|
||||
|
||||
Нужно проверить единый способ разделять framework-neutral cache policy, client hooks, server prefetch и serialization boundary без утечки query-library types в Domain API.
|
||||
|
||||
## Автоматическая проверка
|
||||
|
||||
Нужно выбрать machine-readable формат для форм домена, metadata, модулей, Groups, entry points, environment labels и запрещённых транзитивных импортов.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Состояние и кэш
|
||||
|
||||
> Пояснение границы между предметной властью business и техническими state/query runtimes.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
|
||||
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
|
||||
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
|
||||
|
||||
## Библиотеки не запрещены
|
||||
|
||||
Запрет state/query manager в import-графе `business` не запрещает TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX или RxJS в доменном пакете. Он запрещает concrete runtime становиться предметным контрактом.
|
||||
|
||||
Такая библиотека может находиться:
|
||||
|
||||
- в adapter-модуле, если реализует техническую зависимость business-фабрики;
|
||||
- в framework binding module, если доставляет готовый Domain API конкретному framework;
|
||||
- в composition, если состояние принадлежит только её UI scope и не подменяет доменную модель.
|
||||
|
||||
## Три вида состояния
|
||||
|
||||
### Предметное состояние
|
||||
|
||||
Модель фактов и переходов домена. `business` определяет initial value, validation, допустимые команды, transitions и selectors. Concrete store может быть передан фабрике как business-owned port:
|
||||
|
||||
```ts
|
||||
export type AuthStateDependency = {
|
||||
create: (initial: AuthState) => {
|
||||
get: () => AuthState
|
||||
set: (state: AuthState) => void
|
||||
subscribe: (listener: () => void) => () => void
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Adapter поверх Zustand или другого manager реализует этот контракт, но не выбирает initial state и не добавляет переходы.
|
||||
|
||||
### Technical source cache
|
||||
|
||||
Кэш SDK, HTTP, GraphQL или storage-вызовов. Adapter может использовать QueryClient, Apollo cache или иной runtime для deduplication, transport retry и хранения внешних результатов. Business по-прежнему проверяет и преобразует результат до публикации предметной модели.
|
||||
|
||||
Query keys и библиотечные result types остаются внутри adapter. Domain API не экспортирует `UseQueryResult`, `ApolloError`, raw DTO или mutable QueryClient.
|
||||
|
||||
Если technical cache создаёт timers, subscriptions или другой lifecycle-ресурс для всего графа, владеющая им assembly передаёт cleanup graph owner. Cache без такого ресурса не требует искусственного `dispose`.
|
||||
|
||||
### Framework projection cache
|
||||
|
||||
Кэш, который framework binding строит поверх вызовов готового Domain API для rendering, Suspense, revalidation или UI coordination.
|
||||
|
||||
```ts
|
||||
const useProfile = () => {
|
||||
const api = useUserApi()
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['user', 'profile'],
|
||||
queryFn: api.getProfile,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Такой cache не является параллельным предметным источником, пока его значения происходят из Domain API и все предметные изменения выполняются через API.
|
||||
|
||||
## Invalidation и retry
|
||||
|
||||
Не каждая cache policy является бизнес-правилом.
|
||||
|
||||
| Политика | Обычный владелец |
|
||||
|---|---|
|
||||
| Query key, stale time, deduplication, background refetch | Adapter или framework binding |
|
||||
| Rendering stale data, Suspense, polling UI | Framework binding или composition |
|
||||
| Transport retry безопасного запроса | Adapter |
|
||||
| Запрет повторной предметной команды | `business` |
|
||||
| Cooldown, лимит попыток, допустимый transition | `business` |
|
||||
| Freshness, влияющая на корректность сценария | `business` через явный контракт |
|
||||
|
||||
Если после команды требуется invalidation, framework binding может связать успешный результат API с техническими keys. Если выбор invalidation выражает предметную семантику, business возвращает устойчивый outcome/event либо публикует чистое правило через `business/runtime`; binding только отображает его на библиотечные операции.
|
||||
|
||||
## Optimistic updates
|
||||
|
||||
Framework binding не конструирует произвольную предметную модель из input формы или transport DTO. Optimistic update допустим, когда предполагаемое значение:
|
||||
|
||||
- возвращено командой Domain API как безопасная projection;
|
||||
- создано отдельным pure-методом Domain API;
|
||||
- создано или проверено публичной функцией `business/runtime`.
|
||||
|
||||
```ts
|
||||
const optimisticProfile = projectProfileUpdate(currentProfile, command)
|
||||
|
||||
queryClient.setQueryData(profileKey, optimisticProfile)
|
||||
```
|
||||
|
||||
Здесь `projectProfileUpdate` принадлежит `business/runtime`, а `setQueryData` остаётся технической операцией framework binding.
|
||||
|
||||
Запись raw form data или DTO напрямую в публичный product cache обходит предметного владельца и нарушает границу.
|
||||
|
||||
## Browser, SSR и RSC
|
||||
|
||||
Client hook, server prefetch и hydrate/dehydrate могут принадлежать разным framework binding modules с совместимыми environment entry points. Общая framework-specific cache policy выносится в отдельный модуль той же Framework Group, если у неё несколько реальных потребителей.
|
||||
|
||||
`compositions` вызывает публичный server binding для prefetch и публичный client binding для rendering. Она не повторяет query keys и mapping доменных результатов.
|
||||
|
||||
## Проверка на ревью
|
||||
|
||||
Для каждого state/query runtime определяется:
|
||||
|
||||
- является ли он adapter, framework projection или локальным UI state;
|
||||
- откуда поступают значения;
|
||||
- кто определяет transition и optimistic projection;
|
||||
- где находятся library-specific types и keys;
|
||||
- как invalidation соотносится с результатами Domain API;
|
||||
- соответствует ли cache lifecycle области жизни API и framework scope.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Тестирование доменного пакета
|
||||
|
||||
> Проверка владельцев и публичных границ Level 2.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016)
|
||||
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-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-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
|
||||
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
|
||||
|
||||
## Размещение
|
||||
|
||||
Тест находится рядом с модулем-владельцем проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`.
|
||||
|
||||
| Проверяемая граница | Владелец теста |
|
||||
|---|---|
|
||||
| Сценарии, Domain API, данные и ошибки | `business` |
|
||||
| Публичная pure-функция или guard | `business/runtime` внутри тестов модуля business |
|
||||
| Техническое преобразование | Adapter |
|
||||
| Выбор API, dependencies и environment boundary | Assembly |
|
||||
| Provider, hook, query integration, form или guard | Соответствующий framework binding module |
|
||||
| Граф нескольких доменов | Модуль `composition`, точка входа `app` или другое место сборки |
|
||||
|
||||
## Business через фабрику
|
||||
|
||||
Каждый публичный сценарий проверяется через фабрику владеющего им API с управляемыми dependencies:
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
import { authSessionFactory } from '@/domains/auth/business/factory'
|
||||
import {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
|
||||
const api: AuthSessionApi = authSessionFactory(createAuthSessionTestDeps({
|
||||
clock: { now: () => 1_700_000_000_000 },
|
||||
phone: { requestCode: async () => ({ ok: true }) },
|
||||
}))
|
||||
|
||||
await api.requestPhoneOtp('+79991112233')
|
||||
```
|
||||
|
||||
Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от `throw` или `Result`, но наружу проверяется только собственный AuthErrorCode.
|
||||
|
||||
Business-тест не использует React, реальный SDK, database, production assembly или системные clock/random. Локальная fake implementation допустима в тесте и не становится adapter-модулем, потому что не входит в production graph.
|
||||
|
||||
Если `business` объявляет несколько API, каждый тестируется через свою фабрику. Общая внутренняя pure-логика не требует повторять одинаковые cases на уровне всех API.
|
||||
|
||||
## Остальные модули
|
||||
|
||||
Тест adapter-модуля проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки.
|
||||
|
||||
Тест обязательной assembly проверяет:
|
||||
|
||||
- вызов только нужных business-фабрик;
|
||||
- точный именованный состав возвращённого графа;
|
||||
- выбор публичных adapter-модулей;
|
||||
- отсутствие несовместимого environment-кода;
|
||||
- передачу cross-domain API аргументом, а не импортом;
|
||||
- cleanup handle, если assembly создаёт ресурс жизненного цикла.
|
||||
|
||||
Assembly без собственного lifecycle-ресурса не тестирует пустой `dispose`, потому что не обязана его предоставлять.
|
||||
|
||||
Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthSessionApi` и проверяет Provider, hook или component. Query binding дополнительно проверяет keys, invalidation и отсутствие raw DTO в публичном результате, но не повторяет полный набор business-сценариев.
|
||||
|
||||
## Автоматические структурные проверки
|
||||
|
||||
Проверка файлов, exports и import-графа подтверждает:
|
||||
|
||||
- отсутствие root API доменного пакета и Framework Groups;
|
||||
- обязательные `business` и `business/factory`, type-only exports в корневом barrel и допустимый опциональный `business/runtime`;
|
||||
- соблюдение матрицы потребителей фасетов business;
|
||||
- наличие непосредственно в корне пакета непустой Group `assemblies` с объявленными модульными границами;
|
||||
- отсутствие запрещённых runtime cross-domain imports;
|
||||
- отсутствие type-only импортов из чужих assemblies, adapters и framework-модулей;
|
||||
- отсутствие cross-domain framework hooks, contexts и components;
|
||||
- отсутствие server-only достижимости из client modules;
|
||||
- отсутствие runtime- и type-only циклов.
|
||||
|
||||
## Архитектурное ревью
|
||||
|
||||
На ревью проверяется, что `business/factory` экспортирует только объявленные фабрики, а `business/runtime` при наличии содержит только публичный deterministic runtime. Для каждой технической зависимости рассматриваются все production implementations: каждая связная implementation должна принадлежать одному модулю Group `adapters`, но один такой модуль может реализовать несколько тесно связанных capabilities одного provider.
|
||||
|
||||
Отдельно проверяются прямые обращения business к `Date.now`, `Math.random`, `crypto.randomUUID`, timers, env и другим скрытым runtime-capabilities.
|
||||
|
||||
Runtime-тест не заменяет автоматическую проверку или архитектурное ревью.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Терминология Level 2
|
||||
|
||||
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
|
||||
|
||||
Level 2 наследует терминологию и матрицу слоёв Level 1. Он применяется отдельно к предметным областям, которым нужна пакетная форма, и не требует переводить остальные доменные модули того же SLM root.
|
||||
|
||||
## Формы домена
|
||||
|
||||
### Форма домена
|
||||
|
||||
Структурное представление одной доменной ответственности. На Level 1 предметная область представлена доменным модулем. Для предметной области, использующей Level 2, этот модуль заменяется доменным пакетом. Одна предметная область не использует обе формы одновременно.
|
||||
|
||||
### Доменный пакет
|
||||
|
||||
Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла графа зависимостей.
|
||||
|
||||
Корень пакета может содержать только декларативную metadata, обязательный модуль `business` и допустимые Groups. Metadata хранит статические данные о пакете, владении либо конфигурации проверки и не содержит кода, выполняемого приложением.
|
||||
|
||||
Доменный пакет является policy boundary: проверка использует объявленную принадлежность модулей пакету для контроля структуры и междоменных связей. Публичными dependency boundaries кода остаются модули внутри пакета, а не пакет целиком.
|
||||
|
||||
### Навигационная Group слоя `domains`
|
||||
|
||||
Group, размещённая непосредственно в слое `domains` или другой такой Group. Она классифицирует доменные модули Level 1, доменные пакеты Level 2 и другие навигационные Groups, но не содержит исполняемый код и не образует dependency boundary.
|
||||
|
||||
### Модуль доменного пакета
|
||||
|
||||
Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: business, assembly, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел графа зависимостей.
|
||||
|
||||
Модуль внутри Group пакета не является вложенным модулем, потому что его ближайшая внешняя граница не является модулем.
|
||||
|
||||
## Business
|
||||
|
||||
### Модуль business
|
||||
|
||||
Обязательный SLM-модуль `business`, который определяет публичные предметные сценарии, один или несколько именованных Domain API, соответствующие им фабрики, типы зависимостей и публичные контракты ожидаемых ошибок.
|
||||
|
||||
`business` является предметным владельцем данных, моделей, переходов и результатов сценариев домена. Он не зависит от конкретного фреймворка, среды или технической реализации.
|
||||
|
||||
### Публичные фасеты business
|
||||
|
||||
Объявленные entry points одного логического публичного API модуля `business`:
|
||||
|
||||
| Путь | Статус | Содержимое |
|
||||
|---|---|---|
|
||||
| `business` | Обязательный | Только public types, включая Domain API, зависимости, factory types и error types |
|
||||
| `business/factory` | Обязательный | Только именованные runtime-фабрики Domain API |
|
||||
| `business/runtime` | Необязательный | Только публичные детерминированные runtime-значения и функции |
|
||||
|
||||
Фасет `business/runtime` может публиковать устойчивые error codes и guards, validators, value constructors, предметные константы и чистые функции, если они нужны реальным внешним потребителям. Он не содержит фабрики, изменяемое состояние, ввод-вывод, сценарии с runtime-зависимостями или environment-specific код.
|
||||
|
||||
Фасеты не являются сегментами, вложенными модулями или самостоятельными узлами графа. Любой другой внешний путь внутрь `business` является deep import.
|
||||
|
||||
### Business-safe внешний пакет
|
||||
|
||||
Внешняя библиотека, допустимая в import-графе `business`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, query runtime, framework и техническая интеграция не становятся business-safe только из-за совместимости с несколькими средами.
|
||||
|
||||
### Domain API
|
||||
|
||||
Именованный публичный runtime-контракт связного набора предметных сценариев внутри одного домена. Модуль `business` может объявить несколько Domain API, если они независимо собираются, имеют разные технические зависимости или нужны разным средам и потребителям.
|
||||
|
||||
Разделение на API не создаёт новые доменные пакеты и не разрешает дублировать один сценарий в нескольких контрактах. Каждый публичный сценарий принадлежит ровно одному Domain API.
|
||||
|
||||
### Фабрика business
|
||||
|
||||
Публичная функция фасета `business/factory`, которая получает явные зависимости и создаёт экземпляр одного объявленного Domain API. Каждому Domain API соответствует ровно одна публичная фабрика. Фабрика не выбирает assembly или среду выполнения.
|
||||
|
||||
### Предметная власть business
|
||||
|
||||
Право определять публичную доменную модель, допустимые переходы, валидацию внешних значений и семантику результатов. Adapter, assembly или framework binding может хранить, кэшировать и проецировать значения Domain API, но не становится независимым источником предметной модели или переходов.
|
||||
|
||||
### Доменная ошибка
|
||||
|
||||
Безопасная публичная форма ожидаемого сбоя предметного сценария. Модуль `business` объявляет устойчивый readonly-тип с кодом; при необходимости runtime-коды и guards публикуются через `business/runtime`. Ошибки SDK, транспорта, storage, adapter или другого домена не являются доменными ошибками текущего API.
|
||||
|
||||
Способ передачи ошибки, например exception или discriminated `Result`, не изменяет владение и публичную безопасность ошибки.
|
||||
|
||||
## Техническая сборка
|
||||
|
||||
### Техническая зависимость
|
||||
|
||||
Явная runtime-возможность, необходимая business-фабрике и требующая production-реализации. К техническим зависимостям относятся источники данных, SDK, storage, API платформы, state/query runtime, технические сервисы, данные request scope, clock, timer, random, ID generator и другие источники ввода-вывода, состояния или недетерминизма.
|
||||
|
||||
Type-only cross-domain API dependency, неизменяемая конфигурация и аргумент отдельного предметного сценария не являются техническими зависимостями.
|
||||
|
||||
### Adapter
|
||||
|
||||
SLM-модуль в Group `adapters`, который реализует одну или несколько связанных технических зависимостей business-фабрик поверх SDK, storage, state/query runtime, API платформы, данных запроса или технического сервиса. Одна связная production-реализация принадлежит одному adapter-модулю и не размещается внутри assembly или composition.
|
||||
|
||||
Group `adapters` обязательна и непуста, если хотя бы одна business-фабрика имеет техническую зависимость. Фабрики без технических зависимостей не требуют создания этой Group.
|
||||
|
||||
Точная обязательная поведенческая форма технических портов пока не определена и остаётся открытым вопросом Level 2.
|
||||
|
||||
### Assembly
|
||||
|
||||
SLM-модуль в Group `assemblies`, который создаёт явный именованный граф одного или нескольких Domain API пакета для одного реального контекста выполнения: браузера, запроса, server action или другого окружения. При наличии технических зависимостей assembly выбирает их adapter-модули и передаёт фабрикам готовые runtime-зависимости.
|
||||
|
||||
Assembly может принимать готовые API других доменов аргументами. Она не добавляет предметные сценарии, методы API или собственные доменные ошибки.
|
||||
|
||||
Каждый доменный пакет содержит минимум одну assembly. Архитектура не ограничивает их максимальное количество и не требует универсальной изоморфной assembly.
|
||||
|
||||
### Ресурс assembly
|
||||
|
||||
Ресурс жизненного цикла, который assembly сама создаёт для возвращаемого графа API. Создание фабрики или assembly не запускает скрытую долгоживущую работу. Если технический ресурс необходимо активировать при сборке, публичный результат assembly предоставляет cleanup handle; если ресурс запускается явной операцией API, эта операция предоставляет cleanup.
|
||||
|
||||
Assembly без собственного ресурса жизненного цикла не обязана возвращать пустой `dispose`.
|
||||
|
||||
## Framework binding
|
||||
|
||||
### Framework Group
|
||||
|
||||
Group доменного пакета, названная по конкретному фреймворку: `react`, `vue` и аналогично. Она содержит framework binding modules, не имеет собственного `index.ts`, реализации, состояния, жизненного цикла или публичного API.
|
||||
|
||||
### Framework binding module
|
||||
|
||||
SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией с фреймворком. Например, `react/session` может владеть Provider и hooks сессии, а `react/login-form` - переиспользуемой формой авторизации.
|
||||
|
||||
Framework binding module получает готовые Domain API, не вызывает фабрику или assembly и не импортирует framework-состояние, hooks или компоненты другого домена. Он может использовать совместимые с его ролью state/query libraries и технически кэшировать результаты Domain API, не создавая новую предметную модель.
|
||||
|
||||
## Сборка графа
|
||||
|
||||
### Место сборки графа
|
||||
|
||||
Код модуля `composition`, точки входа `app`, request handler или test setup, который создаёт assemblies либо напрямую вызывает фабрики в ацикличном порядке и передаёт уже созданные API зависимым сборкам.
|
||||
|
||||
Место сборки владеет областью жизни совокупного графа и вызывает предоставленные cleanup handles не позже её завершения. Это координационное владение не переносит к нему предметные или технические ответственности внутренних модулей.
|
||||
|
||||
### Граница среды выполнения
|
||||
|
||||
Граница между import-графами, предназначенными для клиента, сервера или обеих сред. Она определяется транзитивной достижимостью импортов, а не только именем папки или tree shaking.
|
||||
|
||||
## Структурная модель
|
||||
|
||||
```text
|
||||
SLM root
|
||||
└── domains
|
||||
├── доменный модуль Level 1
|
||||
└── доменный пакет Level 2
|
||||
├── metadata
|
||||
├── модуль business
|
||||
├── обязательная Group assemblies
|
||||
│ └── assembly-модуль
|
||||
├── Group adapters при наличии технических зависимостей
|
||||
│ └── adapter-модуль
|
||||
└── Framework Group react
|
||||
├── модуль session
|
||||
└── модуль login-form
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
# Проверка Level 2
|
||||
|
||||
> Граница автоматической проверки, архитектурного ревью и тестирования Level 2.
|
||||
|
||||
## Конфигурация проекта
|
||||
|
||||
Конфигурация проверки сопоставляет физические пути с доменными модулями Level 1, доменными пакетами Level 2, metadata, SLM-модулями, Groups, фасетами `business`, техническими зависимостями, adapter-модулями, assemblies, публичными точками входа, метками сред выполнения и allowlist внешних business-safe пакетов.
|
||||
|
||||
Формат такой конфигурации пока не выбран. Независимо от формата проверка анализирует объявленные границы и import-граф, а не угадывает сущность только по имени папки.
|
||||
|
||||
## Автоматическая проверка
|
||||
|
||||
Автоматическая проверка блокирует:
|
||||
|
||||
- одновременное объявление одной предметной области доменным модулем и пакетом;
|
||||
- исполняемый файл, root `index.ts`, состояние или реэкспорт в корне доменного пакета;
|
||||
- отсутствие `business` или несколько модулей `business` в одном пакете;
|
||||
- отсутствие `business` либо `business/factory`, runtime export из корневого barrel, export не-фабрики из `business/factory`, type export из `business/runtime`, другой публичный путь либо deep import внутри `business`;
|
||||
- импорт фасета `business` потребителем, которому этот фасет не разрешён;
|
||||
- отсутствие непосредственно в корне пакета непустой Group `assemblies` или наличие в ней прямого дочернего элемента без объявленной модульной границы;
|
||||
- deep imports во внутренние части модулей;
|
||||
- runtime- или type-only достижимость framework-, adapter-, assembly-, infra- или environment-specific кода из `business`;
|
||||
- запрещённый runtime-импорт через границу пакета Level 2;
|
||||
- type-only импорт не из публичной точки входа владельца;
|
||||
- импорт framework state, hooks, contexts или components другого домена;
|
||||
- достижимость server-only кода из client-entry point и обратное несовместимое направление;
|
||||
- runtime- или type-only циклы в графе модулей.
|
||||
|
||||
Статический анализ может отдельно находить прямые вызовы `Date.now`, `Math.random`, `crypto.randomUUID`, timers и чтение env внутри `business`. Окончательное решение о скрытом недетерминизме остаётся за ревью.
|
||||
|
||||
## Архитектурное ревью
|
||||
|
||||
На ревью определяется:
|
||||
|
||||
- представляет ли пакет одну связную предметную область;
|
||||
- принадлежит ли каждая соседняя область ровно одной форме независимо от форм других доменов;
|
||||
- принадлежат ли публичные сценарии ровно одному из именованных Domain API;
|
||||
- оправдано ли разделение API разными consumers, dependencies или assemblies, а не техническим дроблением;
|
||||
- остаются ли модель, validation и transitions под предметной властью `business`;
|
||||
- не создаёт ли state/query cache параллельную продуктовую модель или raw DTO boundary;
|
||||
- соответствует ли каждой фабрике ровно один API и остаётся ли она environment-neutral;
|
||||
- содержит ли `business/runtime` только реально публичные deterministic values и functions;
|
||||
- преобразует ли business ожидаемые technical и cross-domain сбои в собственные ошибки;
|
||||
- является ли каждая связная production-реализация технических dependencies отдельным модулем Group `adapters`;
|
||||
- представляет ли каждая assembly один реальный контекст выполнения и возвращает ли точный именованный граф;
|
||||
- не запускают ли фабрики и assemblies скрытую долгоживущую работу при создании графа;
|
||||
- предоставляет ли assembly cleanup только для действительно созданного ею lifecycle-ресурса и вызывает ли graph owner этот cleanup;
|
||||
- принадлежит ли каждый framework binding module домену, а не странице или multi-domain сценарию;
|
||||
- соответствует ли каждый объявленный business-safe внешний пакет ограничениям детерминированной библиотеки без runtime capability;
|
||||
- остаются ли Framework Groups и другие Groups без реализации и агрегирующего API.
|
||||
|
||||
## Тестирование
|
||||
|
||||
Business-сценарии проверяются через соответствующие фабрики с управляемыми test fakes, включая fake clock/random/id при необходимости. Adapter module проверяет technical transformation. Assembly проверяет состав графа, выбор adapters, environment boundary и условный cleanup. Framework binding module проверяет собственный Provider, hook, cache integration или component без повторения полного набора business-сценариев.
|
||||
|
||||
Import-graph checks не заменяются runtime-тестами.
|
||||
|
||||
## Смешанный SLM root
|
||||
|
||||
Наличие доменных модулей Level 1 рядом с пакетами Level 2 является завершённым допустимым состоянием. Проверка применяет правила формы отдельно к каждой предметной области и правила Level 2 ко всем статическим связям, пересекающим пакетную границу.
|
||||
|
||||
Переход одного домена завершается, когда его старая модульная граница удалена и checker видит только пакет. Другие домены не входят в критерий завершения.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-DOMAIN-A003`](../rules/level-2.md#slm-l2-domain-a003)
|
||||
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
|
||||
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
|
||||
- [`SLM-L2-BUSINESS-R018`](../rules/level-2.md#slm-l2-business-r018)
|
||||
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-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-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-ASSEMBLY-R023`](../rules/level-2.md#slm-l2-assembly-r023)
|
||||
- [`SLM-L2-BUSINESS-R024`](../rules/level-2.md#slm-l2-business-r024)
|
||||
- [`SLM-L2-BUSINESS-R025`](../rules/level-2.md#slm-l2-business-r025)
|
||||
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
|
||||
|
||||
Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.
|
||||
128
.opencode/skills/slm-design/reference/draft/rules/README.md
Normal file
128
.opencode/skills/slm-design/reference/draft/rules/README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Правила SLM
|
||||
|
||||
> Статус: системный черновик. Не является нормативной спецификацией.
|
||||
|
||||
Эта директория является единственным местом объявления правил SLM. Остальные черновики объясняют архитектуру и ссылаются на канонические коды, но не повторяют формулировки правил.
|
||||
|
||||
## Что считается правилом
|
||||
|
||||
Правило задаёт один блокирующий архитектурный инвариант.
|
||||
|
||||
Определения, рекомендации, разрешения, примеры и открытые вопросы не получают код правила.
|
||||
|
||||
Нормативные определения объявляются в терминологии соответствующего уровня. Они обязательны для толкования правил, но нормативность определения сама по себе не превращает его в правило.
|
||||
|
||||
## Код правила
|
||||
|
||||
```text
|
||||
SLM-L{level}-{group}-{class}{number}
|
||||
```
|
||||
|
||||
| Часть | Значение |
|
||||
|---|---|
|
||||
| `SLM` | Принадлежность архитектуре SLM |
|
||||
| `L{level}` | Уровень архитектуры |
|
||||
| `group` | Раздел правил |
|
||||
| `class` | Способ проверки: `A` или `R` |
|
||||
| `number` | Трёхзначный номер внутри уровня |
|
||||
|
||||
## Способы проверки
|
||||
|
||||
### `A`: автоматическая проверка
|
||||
|
||||
Всё правило можно однозначно проверить программно без понимания предметного смысла кода. Нарушение такого правила должно блокировать автоматическую проверку.
|
||||
|
||||
### `R`: проверка на ревью
|
||||
|
||||
Для окончательного решения требуется понимание ответственности, владения или смысла зависимости. Линтер может проверять отдельные признаки, но не заменяет решение на ревью.
|
||||
|
||||
Одно правило не разделяется на автоматическую и ручную копии только из-за разных способов проверки. Если существенная часть инварианта требует смыслового решения, всё правило получает класс `R`.
|
||||
|
||||
## Разделы правил
|
||||
|
||||
| Код | Раздел |
|
||||
|---|---|
|
||||
| `LAYER` | Слои |
|
||||
| `DEPENDENCY` | Зависимости |
|
||||
| `MODULE` | Модули |
|
||||
| `GROUP` | Группы |
|
||||
| `SEGMENT` | Сегменты |
|
||||
| `COMPONENT` | Компоненты |
|
||||
| `NESTED_MODULE` | Вложенные модули |
|
||||
| `LIFECYCLE` | Жизненный цикл |
|
||||
| `DOMAIN` | Домены |
|
||||
| `BUSINESS` | Контракты бизнес-логики |
|
||||
| `FACTORY` | Фабрики бизнес-логики |
|
||||
| `ERROR` | Ошибки домена |
|
||||
| `PORT` | Порты бизнес-логики |
|
||||
| `ADAPTER` | Адаптеры |
|
||||
| `ASSEMBLY` | Сборка API и жизненный цикл |
|
||||
| `ENVIRONMENT` | Границы сред выполнения |
|
||||
| `FRAMEWORK` | Модули фреймворков |
|
||||
| `TEST` | Тестирование |
|
||||
|
||||
Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования.
|
||||
|
||||
## Формат записи
|
||||
|
||||
```md
|
||||
### SLM-L1-MODULE-A004
|
||||
|
||||
> **Публичный API модуля**
|
||||
>
|
||||
> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API.
|
||||
```
|
||||
|
||||
Код является заголовком третьего уровня и автоматически получает адрес для ссылки `#slm-l1-module-a004`.
|
||||
|
||||
Название и описание входят в одну цитату. Название выделяется жирным и служит кратким именем правила. Описание полностью формулирует требование и занимает одну физическую строку.
|
||||
|
||||
Ссылка из тематического черновика:
|
||||
|
||||
```md
|
||||
[`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
|
||||
```
|
||||
|
||||
## Как формулировать правила
|
||||
|
||||
1. Правило понятно без чтения тематической главы и опирается только на нормативные термины своего уровня.
|
||||
2. Правило защищает один архитектурный инвариант.
|
||||
3. Один инвариант получает один код независимо от числа участников и способов проверки.
|
||||
4. Название является кратким и устойчивым именем правила.
|
||||
5. Название обозначает предмет правила, а описание полностью формулирует требование.
|
||||
6. Описание объясняет допустимую границу и то, что считается нарушением.
|
||||
7. Описание раскрывает названный инвариант и не вводит второе независимое требование.
|
||||
8. Описание использует нормативные определения и не пересказывает их без необходимости.
|
||||
9. Название и описание используют человеческий язык и только необходимые архитектурные термины.
|
||||
10. Обоснование, подробности, примеры и исключения размещаются в тематическом черновике, а не в описании.
|
||||
11. Правило не создаётся отдельно с позиции владельца и потребителя, если обе формулировки защищают одну границу.
|
||||
12. Перед добавлением правила реестр проверяется на дубли и противоречия.
|
||||
13. Код присваивается после проверки правила на примерах и контрпримерах.
|
||||
|
||||
## Нумерация
|
||||
|
||||
1. Номер уникален внутри уровня независимо от раздела и способа проверки.
|
||||
2. Номер не обозначает важность или порядок выполнения.
|
||||
3. Удалённый номер не переиспользуется для другого правила.
|
||||
4. При изменении способа проверки номер сохраняется, но меняется полный код.
|
||||
|
||||
## Проверка качества
|
||||
|
||||
Перед принятием правила нужно ответить «да»:
|
||||
|
||||
- Понятно, о чём правило?
|
||||
- Название кратко и однозначно называет правило?
|
||||
- Понятно, что оно требует?
|
||||
- Понятно, что является нарушением?
|
||||
- Нельзя ли объединить его с существующим правилом?
|
||||
- Не содержит ли оно рекомендацию или разрешение?
|
||||
- Соответствует ли класс способу окончательной проверки?
|
||||
|
||||
## Проверка документов
|
||||
|
||||
Корневой скрипт `draft-rules.js` читает объявления только из этой директории, проверяет формат и уникальность кодов, валидирует ссылки из остальных черновиков и выводит правила разделами «Автоматические» и «Для ревью». Скрипт проверяет документы, а не архитектуру приложения.
|
||||
|
||||
## Наборы правил
|
||||
|
||||
- [Первый уровень](./level-1.md)
|
||||
- [Второй уровень](./level-2.md)
|
||||
110
.opencode/skills/slm-design/reference/draft/rules/level-1.md
Normal file
110
.opencode/skills/slm-design/reference/draft/rules/level-1.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Правила SLM первого уровня
|
||||
Здесь собраны правила первого уровня. Это единственное место, где они формулируются; тематические черновики объясняют их и ссылаются на коды.
|
||||
|
||||
## Размещение кода по слоям
|
||||
|
||||
### SLM-L1-LAYER-R001
|
||||
|
||||
> **Назначение слоёв**
|
||||
>
|
||||
> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода.
|
||||
|
||||
### SLM-L1-LAYER-A002
|
||||
|
||||
> **Направление зависимостей**
|
||||
>
|
||||
> Внутри одного SLM root код каждого слоя может зависеть только от кода целевых слоёв, разрешённых для него нормативной матрицей слоёв.
|
||||
|
||||
### SLM-L1-LAYER-R003
|
||||
|
||||
> **Граница слоя `app`**
|
||||
>
|
||||
> В `app` размещаются только точки входа фреймворка для запуска, маршрутов, преобразования входных данных и подключения публичных API модулей разрешённых слоёв или ресурсов `shared`; ответственности этих модулей остаются за пределами `app`.
|
||||
|
||||
## Границы модулей
|
||||
|
||||
### SLM-L1-MODULE-A004
|
||||
|
||||
> **Публичный API модуля**
|
||||
>
|
||||
> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API.
|
||||
|
||||
### SLM-L1-MODULE-A014
|
||||
|
||||
> **Папка модуля**
|
||||
>
|
||||
> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки.
|
||||
|
||||
### SLM-L1-MODULE-R006
|
||||
|
||||
> **Ответственность модуля**
|
||||
>
|
||||
> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях.
|
||||
|
||||
### SLM-L1-MODULE-R011
|
||||
|
||||
> **Владелец ответственности**
|
||||
>
|
||||
> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`.
|
||||
|
||||
### SLM-L1-MODULE-R012
|
||||
|
||||
> **Состав публичного API**
|
||||
>
|
||||
> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми.
|
||||
|
||||
## Зависимости между модулями
|
||||
|
||||
### SLM-L1-DEPENDENCY-A005
|
||||
|
||||
> **Циклические зависимости**
|
||||
>
|
||||
> Граф зависимостей модулей внутри одного SLM root, включая вложенные модули, не содержит циклов.
|
||||
|
||||
## Назначение групп
|
||||
|
||||
### SLM-L1-GROUP-R007
|
||||
|
||||
> **Назначение группы**
|
||||
>
|
||||
> Группа содержит только модули и другие группы, не владеет файлами реализации, состоянием, жизненным циклом или публичным API и не импортируется внешним кодом.
|
||||
|
||||
## Назначение сегментов
|
||||
|
||||
### SLM-L1-SEGMENT-R008
|
||||
|
||||
> **Граница сегмента**
|
||||
>
|
||||
> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или узла графа зависимостей.
|
||||
|
||||
## Ответственность компонентов
|
||||
|
||||
### SLM-L1-COMPONENT-R009
|
||||
|
||||
> **Ответственность компонента**
|
||||
>
|
||||
> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу.
|
||||
|
||||
## Границы вложенных модулей
|
||||
|
||||
### SLM-L1-NESTED_MODULE-A010
|
||||
|
||||
> **Доступ к вложенному модулю**
|
||||
>
|
||||
> Код за пределами родительского модуля не импортирует вложенный модуль напрямую и получает его экспорты только через публичный API родителя.
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
### SLM-L1-LIFECYCLE-R013
|
||||
|
||||
> **Жизненный цикл ресурсов**
|
||||
>
|
||||
> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
|
||||
|
||||
## Граница доменных модулей
|
||||
|
||||
### SLM-L1-DOMAIN-R015
|
||||
|
||||
> **Доменный модуль**
|
||||
>
|
||||
> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие ей модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей.
|
||||
171
.opencode/skills/slm-design/reference/draft/rules/level-2.md
Normal file
171
.opencode/skills/slm-design/reference/draft/rules/level-2.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Правила SLM второго уровня
|
||||
|
||||
Доменный пакет Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. Для предметной области в пакетной форме `SLM-L1-DOMAIN-R015` заменяется правилами доменного пакета; остальные предметные области того же SLM root могут сохранять форму доменного модуля Level 1. `SLM-L1-GROUP-R007` сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Для модуля `business` правило `SLM-L1-MODULE-A004` уточняется правилом `SLM-L2-BUSINESS-A019`: объявленные фасеты вместе образуют один логический публичный API и не считаются deep imports. Ранее использовавшиеся номера `SLM-L2-DOMAIN-R001` и `SLM-L2-MIGRATION-A017` не переиспользуются после изменения модели уровней и перехода к постоянному совместному применению форм.
|
||||
|
||||
## Граница доменного пакета
|
||||
|
||||
### SLM-L2-DOMAIN-R002
|
||||
|
||||
> **Предметная граница пакета**
|
||||
>
|
||||
> Каждая предметная область, использующая пакетную форму Level 2, представлена ровно одним доменным пакетом; все его модули относятся только к этой предметной области.
|
||||
|
||||
### SLM-L2-DOMAIN-A003
|
||||
|
||||
> **Корень доменного пакета**
|
||||
>
|
||||
> Корень доменного пакета содержит только декларативную metadata, модуль `business` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов.
|
||||
|
||||
### SLM-L2-GROUP-R004
|
||||
|
||||
> **Навигационная Group доменов**
|
||||
>
|
||||
> Group, расположенная непосредственно в слое `domains` или другой такой Group, содержит только доменные модули Level 1, доменные пакеты Level 2 и навигационные Groups и не владеет реализацией, состоянием, жизненным циклом или публичным API.
|
||||
|
||||
## Business и Domain API
|
||||
|
||||
### SLM-L2-BUSINESS-R005
|
||||
|
||||
> **Модуль business**
|
||||
>
|
||||
> Каждый доменный пакет содержит ровно один модуль `business`, который владеет публичными предметными сценариями и объявляет один или несколько именованных Domain API этой предметной области.
|
||||
|
||||
### SLM-L2-BUSINESS-R006
|
||||
|
||||
> **Предметная власть business**
|
||||
>
|
||||
> Adapters, assemblies и framework binding modules транспортируют, кэшируют или проецируют доменные данные только в форме, произведённой или проверенной публичным API либо детерминированным runtime модуля `business`, и не определяют независимую предметную модель, переход или результат сценария.
|
||||
|
||||
### SLM-L2-BUSINESS-A007
|
||||
|
||||
> **Импортная замкнутость business**
|
||||
>
|
||||
> Все runtime- и type-only импорты `business`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как business-safe.
|
||||
|
||||
### SLM-L2-FACTORY-R008
|
||||
|
||||
> **Фабрики Domain API**
|
||||
>
|
||||
> Каждому публичному Domain API соответствует ровно одна именованная фабрика фасета `business/factory`; фабрика получает явные runtime-зависимости, создаёт только этот API и не выбирает assembly или среду выполнения.
|
||||
|
||||
## Ошибки домена
|
||||
|
||||
### SLM-L2-ERROR-R009
|
||||
|
||||
> **Публичный контракт ошибок**
|
||||
>
|
||||
> Каждый ожидаемый сбой публичного предметного сценария представлен именованным readonly-типом собственной доменной ошибки с устойчивым кодом; тип экспортируется через type-only barrel, а необходимые внешним потребителям runtime-коды и guards только через `business/runtime`.
|
||||
|
||||
### SLM-L2-ERROR-R010
|
||||
|
||||
> **Изоляция исходных ошибок**
|
||||
>
|
||||
> Сбой adapter, SDK, транспорта, storage или другого домена, доступный приложению через Domain API, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause.
|
||||
|
||||
## Assemblies и зависимости
|
||||
|
||||
### SLM-L2-ASSEMBLY-R011
|
||||
|
||||
> **Роль assembly**
|
||||
>
|
||||
> Каждая assembly является SLM-модулем одного именованного контекста выполнения, выбирает необходимые adapter-модули, вызывает одну или несколько business-фабрик и возвращает явный именованный граф их API, не добавляя предметные сценарии, методы API или собственные доменные ошибки.
|
||||
|
||||
### SLM-L2-DEPENDENCY-A012
|
||||
|
||||
> **Междоменные импорты Level 2**
|
||||
>
|
||||
> Статическая связь между разными доменными границами, хотя бы одна из которых является пакетом Level 2, допускает только type-only импорт публичного API доменного модуля Level 1 или корневого `business` пакета Level 2 либо runtime-импорт `business/runtime` пакета Level 2; все такие связи входят в общий ацикличный граф, а остальные runtime-экспорты другого домена не импортируются.
|
||||
|
||||
### SLM-L2-ENVIRONMENT-A013
|
||||
|
||||
> **Совместимость среды выполнения**
|
||||
>
|
||||
> Публичная точка входа, обозначенная как client-, server- или shared-entry point, не импортирует и не реэкспортирует код несовместимой среды выполнения во всём достижимом графе импортов.
|
||||
|
||||
## Framework Groups и тестирование
|
||||
|
||||
### SLM-L2-FRAMEWORK-R014
|
||||
|
||||
> **Framework Group домена**
|
||||
>
|
||||
> Framework binding modules доменного пакета размещаются в Group, названной по фреймворку, и каждый прямой дочерний элемент этой Group является framework binding module.
|
||||
|
||||
### SLM-L2-FRAMEWORK-R015
|
||||
|
||||
> **Framework binding module**
|
||||
>
|
||||
> Модуль внутри Framework Group владеет одной domain-specific framework-ответственностью, получает готовые Domain API и не выполняет business-сборку, не выбирает adapters и не владеет страницей, маршрутом или multi-domain композицией.
|
||||
|
||||
### SLM-L2-TEST-R016
|
||||
|
||||
> **Проверка владельцев Level 2**
|
||||
>
|
||||
> Каждый публичный предметный сценарий проверяется через фабрику владеющего им Domain API, а основные тесты adapter, assembly и framework binding module проверяют только собственные публичные границы и не повторяют полный набор предметных сценариев.
|
||||
|
||||
## Совместное применение форм
|
||||
|
||||
### SLM-L2-DOMAIN-A026
|
||||
|
||||
> **Однозначная форма домена**
|
||||
>
|
||||
> Каждая предметная область объявлена ровно в одной форме: как доменный модуль Level 1 либо как доменный пакет Level 2; один SLM root может одновременно содержать разные предметные области обеих форм.
|
||||
|
||||
## Внешние библиотеки business
|
||||
|
||||
### SLM-L2-BUSINESS-R018
|
||||
|
||||
> **Business-safe внешний пакет**
|
||||
>
|
||||
> Внешний пакет объявляется business-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state/query manager, framework или другой технической интеграцией.
|
||||
|
||||
## Публичные фасеты business
|
||||
|
||||
### SLM-L2-BUSINESS-A019
|
||||
|
||||
> **Публичные фасеты business**
|
||||
>
|
||||
> Публичный API `business` имеет обязательные entry points `business` только с type exports и `business/factory` только с именованными runtime-фабриками, может иметь `business/runtime` только с runtime exports и не имеет других публичных путей или deep imports.
|
||||
|
||||
## Обязательные роли сборки
|
||||
|
||||
### SLM-L2-ASSEMBLY-A020
|
||||
|
||||
> **Обязательная Group assemblies**
|
||||
>
|
||||
> Корень каждого доменного пакета содержит ровно одну непустую Group `assemblies`, каждый прямой дочерний элемент которой является объявленной границей SLM-модуля.
|
||||
|
||||
### SLM-L2-ADAPTER-R021
|
||||
|
||||
> **Модули production adapters**
|
||||
>
|
||||
> Если хотя бы одна business-фабрика имеет техническую зависимость, корень доменного пакета содержит непустую Group `adapters`, а каждая связная production-реализация одной или нескольких таких зависимостей принадлежит ровно одному adapter-модулю этой Group и не определяется в другом месте production-графа.
|
||||
|
||||
### SLM-L2-BUSINESS-A022
|
||||
|
||||
> **Потребители фасетов business**
|
||||
>
|
||||
> Корневой `business` импортируется извне только через `import type`; `business/factory` импортируют только assemblies своего домена, `app`, `compositions` и тесты, а `business/runtime` импортируется только через объявленный публичный путь с соблюдением правил слоёв и междоменных зависимостей.
|
||||
|
||||
## Жизненный цикл assembly
|
||||
|
||||
### SLM-L2-ASSEMBLY-R023
|
||||
|
||||
> **Cleanup ресурса assembly**
|
||||
>
|
||||
> Создание Domain API фабрикой или assembly не запускает скрытый ресурс жизненного цикла; ресурс запускается явной операцией с cleanup, а технический ресурс, который assembly обязана создать для графа, сопровождается публичным cleanup handle, вызываемым не позже завершения области жизни графа.
|
||||
|
||||
## Недетерминизм business
|
||||
|
||||
### SLM-L2-BUSINESS-R024
|
||||
|
||||
> **Явные источники недетерминизма**
|
||||
>
|
||||
> Business-сценарий получает текущее время, timer, random, ID generator, environment и другие источники недетерминизма только через явные зависимости фабрики и не читает их из скрытого runtime-окружения.
|
||||
|
||||
## Публичный runtime business
|
||||
|
||||
### SLM-L2-BUSINESS-R025
|
||||
|
||||
> **Детерминированный runtime business**
|
||||
>
|
||||
> Фасет `business/runtime` экспортирует только необходимые реальным внешним потребителям детерминированные значения и функции без ввода-вывода, изменяемого состояния, runtime capability или environment-specific поведения.
|
||||
Reference in New Issue
Block a user