mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
feat: доменный API
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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-механики, если архитектурная граница уже определена и не меняется."
|
||||
description: "Используй при проектировании, реализации, миграции и архитектурном ревью по SLM: когда нужно определить SLM root, ответственность, владельца, слой, модульную границу, публичный API, форму домена Level 1 или Level 2, направление зависимостей, Domain API, ports, factories, adapters, default assembly, framework state/cache, realtime, errors или lifecycle. Не используй для локального coding, debugging, форматирования, framework- или SDK-механики, если архитектурная граница уже определена и не меняется."
|
||||
---
|
||||
|
||||
# SLM Design
|
||||
@@ -103,31 +103,33 @@ Navigation Group непосредственно в `domains` может соде
|
||||
```text
|
||||
domains/<domain>/
|
||||
├── metadata # optional, declarative only
|
||||
├── business/ # required SLM module
|
||||
├── api/ # required SLM module
|
||||
├── assemblies/ # required non-empty Group
|
||||
├── adapters/ # when factories have technical dependencies
|
||||
│ └── default/ # required baseline production assembly
|
||||
├── adapters/ # when factories have dependency ports
|
||||
└── react|vue|... # when domain-specific bindings exist
|
||||
```
|
||||
|
||||
Доменный пакет является policy boundary, но не module, Group, public API или graph node. В его корне нет executable files, state, lifecycle, barrel или реэкспортов. Исполняемыми владельцами являются модули внутри пакета.
|
||||
|
||||
`business` является единственным предметным владельцем пакета. Его публичный API состоит из фасетов:
|
||||
`api` является единственным семантическим шлюзом пакета. Его публичный API состоит из фасетов:
|
||||
|
||||
| Путь | Содержимое |
|
||||
|---|---|
|
||||
| `business` | Только public types: Domain API, dependencies, factory types, error types |
|
||||
| `business/factory` | Только именованные runtime factories, по одной на Domain API |
|
||||
| `business/runtime` | Только реально нужные внешним consumers детерминированные runtime values/functions |
|
||||
| `api` | Только consumer-facing public types: Domain API, models, outcomes, errors |
|
||||
| `api/ports` | Implementer-facing types при наличии dependency ports |
|
||||
| `api/factory` | Только именованные runtime factories, по одной на Domain API |
|
||||
| `api/runtime` | Только реально нужные внешним consumers детерминированные runtime values/functions |
|
||||
|
||||
Другой публичный путь внутрь `business` является deep import. `business/runtime` не создавай для симметрии.
|
||||
Другой публичный путь внутрь `api` является deep import. `api/ports` и `api/runtime` не создавай без реальной границы или consumer.
|
||||
|
||||
Роли 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.
|
||||
- `api` определяет Domain API, public models, validation, outcomes, dependency ports и expected domain errors, но не framework state/cache.
|
||||
- Adapter module реализует связанные dependency ports поверх SDK, storage, platform API, transport или другого provider runtime.
|
||||
- `assemblies/default` выбирает штатные adapters, вызывает factories и возвращает baseline graph готовых API; дополнительные assemblies представляют отличающиеся production contexts.
|
||||
- Framework binding module получает готовые Domain API и владеет domain-specific state, cache, hydration и framework integration.
|
||||
- Composition, `app`, request handler или test setup вызывает assemblies в ацикличном порядке и владеет общим scope graph.
|
||||
|
||||
## Универсальный цикл решения
|
||||
|
||||
@@ -144,7 +146,7 @@ domains/<domain>/
|
||||
- места создания graph и instances;
|
||||
- subscriptions, timers, requests, connections и cleanup;
|
||||
- тесты и команды проверки затрагиваемых owners.
|
||||
- architecture mapping, metadata, environment declarations и business-safe allowlists, если проект их использует.
|
||||
- architecture mapping, assembly contexts, environment declarations и API-safe allowlists, если проект их использует.
|
||||
|
||||
Считай type-only import и reexport архитектурным ребром. Для runtime-графа дополнительно ищи arguments factories, callbacks, registries, event buses, service locators и singletons: фактическая зависимость может не иметь прямого runtime import.
|
||||
|
||||
@@ -277,8 +279,10 @@ Framework UI entity не имеет самостоятельной ответс
|
||||
Рассматривай Level 2, когда конкретному домену действительно нужны:
|
||||
|
||||
- несколько независимо собираемых Domain API;
|
||||
- разные browser/server/request assemblies;
|
||||
- собственные public models и stable errors поверх provider contracts;
|
||||
- baseline `assemblies/default` и дополнительные production contexts;
|
||||
- несколько production technical integrations;
|
||||
- HTTP, storage или realtime behind consumer-owned ports;
|
||||
- строгие environment boundaries;
|
||||
- самостоятельные domain-specific framework modules.
|
||||
|
||||
@@ -289,13 +293,13 @@ Framework UI entity не имеет самостоятельной ответс
|
||||
1. Перечисли реальных внешних consumers.
|
||||
2. Для каждого запиши минимально необходимый contract.
|
||||
3. Удали exports, которым нет consumer.
|
||||
4. Не включай mutable internals, concrete clients, stores, contexts, adapter implementations или lifecycle internals в API домена, `business` или parent module.
|
||||
4. Не включай mutable internals, concrete clients, stores, contexts, adapter implementations или lifecycle internals в Domain API, модуль `api` или parent module.
|
||||
5. Для обычного module оставь одну логическую external entry point.
|
||||
6. Для `business` используй только объявленные facets.
|
||||
6. Для модуля `api` используй только `api`, `api/factory`, optional `api/ports` и optional `api/runtime`.
|
||||
7. Удали deep imports и обнови package exports/aliases при необходимости.
|
||||
8. Не открывай nested module напрямую за пределы parent boundary.
|
||||
|
||||
Каждый adapter остаётся обычным SLM-модулем и предоставляет собственный минимальный public API, через который assembly получает production implementation. Запрещён не public API adapter-модуля, а его реэкспорт через `business`, корень пакета, Domain API или другой несвязанный owner.
|
||||
Каждый adapter остаётся обычным SLM-модулем и предоставляет собственный минимальный public API, через который assembly получает production implementation. Запрещён не public API adapter-модуля, а его реэкспорт через `api`, корень пакета, Domain API или другой несвязанный owner.
|
||||
|
||||
Для Level 2 разделяй Domain API по устойчивым различиям consumers, dependencies или environments, а не по внутренним техническим папкам. Каждый публичный scenario принадлежит ровно одному Domain API; каждому API соответствует одна factory. Именованный graph assembly не является новым Domain API.
|
||||
|
||||
@@ -309,7 +313,7 @@ Framework UI entity не имеет самостоятельной ответс
|
||||
4. Проверь матрицу слоёв.
|
||||
5. Если edge пересекает Level 2 package boundary, примени более строгую cross-domain модель.
|
||||
6. Проверь transitive environment compatibility.
|
||||
7. Добавь edge в общий module DAG и проверь цикл.
|
||||
7. Добавь edge в общий module DAG и runtime graph и проверь цикл, включая callbacks и mixed L1/L2 construction.
|
||||
|
||||
Между двумя доменными модулями Level 1 допустим обычный runtime-import публичного API при соблюдении layer matrix и DAG. Не навязывай им runtime injection Level 2.
|
||||
|
||||
@@ -317,11 +321,11 @@ Framework UI entity не имеет самостоятельной ответс
|
||||
|
||||
```ts
|
||||
import type { LevelOneDomainApi } from '.../level-one-domain'
|
||||
import type { LevelTwoDomainApi } from '.../level-two-domain/business'
|
||||
import { deterministicValue } from '.../level-two-domain/business/runtime'
|
||||
import type { LevelTwoDomainApi } from '.../level-two-domain/api'
|
||||
import { deterministicValue } from '.../level-two-domain/api/runtime'
|
||||
```
|
||||
|
||||
Готовый runtime API, связь с которым пересекает Level 2 package boundary, создаёт внешний graph owner и передаёт assembly или factory аргументом. Через такую границу не импортируй чужую factory, assembly, adapter, API singleton, framework state, hook, context, Provider, component или внутренний путь `business`.
|
||||
Готовый runtime API, связь с которым пересекает Level 2 package boundary, создаёт внешний graph owner и передаёт assembly зависимого пакета Level 2 либо явной construction point/public callback модуля Level 1. Через такую границу не импортируй чужие `api/ports`, factory, assembly, adapter, API singleton, framework state, hook, context, Provider, component или внутренний путь `api`.
|
||||
|
||||
Не скрывай cross-domain dependency локальным structural interface, callback, global registry или event bus. Установи владельца контракта и отрази фактический runtime edge в graph, иначе можно пропустить цикл.
|
||||
|
||||
@@ -330,31 +334,31 @@ import { deterministicValue } from '.../level-two-domain/business/runtime'
|
||||
| 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 |
|
||||
| Concrete state/query runtime для materialized domain values | Framework binding или composition |
|
||||
| Clock, timer, random, ID, environment | Dependency port с 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.
|
||||
Adapter переводит provider arguments, records и expected failures в consumer-owned port. Он не объявляет Domain API scenario, domain fallback, transition или public domain error. Production implementation port не прячь inline в assembly или composition.
|
||||
|
||||
Assembly выбирает public adapters своего домена, вызывает factories и возвращает точный именованный graph API. Она не добавляет scenarios, методы API или собственные domain errors. Framework binding получает готовые API и не вызывает factory/assembly и не выбирает adapter.
|
||||
`assemblies/default` выбирает штатные adapters, вызывает factories и возвращает baseline graph API. Дополнительная assembly представляет реально отличающийся production context. Они не добавляют scenarios, методы API или собственные domain errors. Framework binding получает готовые API и не вызывает factory/assembly и не выбирает adapter.
|
||||
|
||||
Даже если готовый `infra` API структурно совпадает с technical dependency, текущие правила Level 2 требуют production implementation в adapter-модуле домена. Сделай его public API минимальным и не добавляй фиктивные преобразования, но не обходи обязательную adapter boundary прямой передачей `infra` capability в factory.
|
||||
|
||||
Перед новым external import в `business`:
|
||||
Перед новым external import в `api`:
|
||||
|
||||
1. Определи реально resolved package entry и resolver conditions нужных environments.
|
||||
2. Проверь transitive runtime graph, side effects, I/O, mutable state и runtime capabilities.
|
||||
3. Убедись, что package соответствует business-safe критериям, и обнови project allowlist/declaration.
|
||||
3. Убедись, что package соответствует API-safe критериям, и обнови project allowlist/declaration.
|
||||
4. Если доказательства нет, вынеси capability в factory dependency и реализуй production binding через adapter.
|
||||
|
||||
### State и cache
|
||||
|
||||
```text
|
||||
Domain facts, validation, transitions, commands, scenario outcomes
|
||||
-> business authority
|
||||
Domain models, validation, transitions, commands, scenario outcomes
|
||||
-> api authority
|
||||
|
||||
Transport/source cache
|
||||
-> adapter
|
||||
@@ -366,19 +370,26 @@ State только текущей UI composition
|
||||
-> composition owner
|
||||
```
|
||||
|
||||
Raw DTO, query-library result и mutable client не являются Domain API. Private source cache внутри adapter может хранить raw transport data, если DTO и library types не выходят в Domain API. Framework projection и любые публикуемые domain values используют только форму, произведённую или проверенную `business`, и не создают параллельную предметную модель.
|
||||
Raw DTO, query-library result и mutable client не являются Domain API. Private source cache внутри adapter может хранить provider records, если DTO и library types не выходят в Domain API. Binding может владеть framework metadata и local UI state, но domain payload projection использует только public values, outcomes и events, произведённые или проверенные `api`, и не создаёт параллельную предметную модель.
|
||||
|
||||
При optimistic или concurrent mutations не придумывай универсальный rollback. Сначала установи owner политики ordering, versioning, rebase/rollback и authoritative refresh.
|
||||
При optimistic или concurrent mutations не придумывай универсальный rollback. Предметные ordering, versioning, rebase/rollback и reconciliation определяет операция Domain API либо deterministic `api/runtime`; иначе binding invalidates projection и получает authoritative snapshot через API.
|
||||
|
||||
### Errors
|
||||
|
||||
- Каждый expected failure публичного scenario, включая собственный domain rejection, представлен именованным readonly error type текущего домена со stable code.
|
||||
- Expected technical или foreign-domain failure, доступный через текущий Domain API, преобразуется текущим `business` в такой собственный domain error.
|
||||
- Expected provider failure проходит через adapter и closed port failure, после чего текущий `api` преобразует его в собственный domain error.
|
||||
- Expected foreign-domain outcome или error поступает через готовый публичный API другого домена и преобразуется текущим `api` напрямую, без автоматического local port.
|
||||
- Source object, SDK class, message, status, payload и `cause` не входят в public domain contract.
|
||||
- Type errors экспортируются через `business`; необходимые runtime codes и guards - только через реально нужный `business/runtime`.
|
||||
- Type errors экспортируются через `api`; необходимые runtime codes и guards - только через реально нужный `api/runtime`.
|
||||
- Не выбирай exception или discriminated `Result` как правило SLM: сохрани project policy и архитектурное владение.
|
||||
- Не маскируй programming defect под expected domain outcome. Если меняется публичный failure channel, выясни политику unexpected failures, cancellation и serialization.
|
||||
|
||||
### Realtime
|
||||
|
||||
Realtime transport остаётся внутри adapter. Domain API публикует только проверенные events, outcomes, statuses и stable errors. Для command-response protocol установи correlation scope, ACK semantics, timeout, cancellation и `OUTCOME_UNKNOWN`; без correlation не обещай индивидуальный result.
|
||||
|
||||
Для каждой subscription установи ordering, duplicate delivery, reconnect, gap detection, resync, shared connection ownership и момент, после которого cleanup гарантирует отсутствие callbacks. Framework binding materializes events через API-owned transition либо invalidates cache и повторно запрашивает snapshot.
|
||||
|
||||
### Lifecycle и environment
|
||||
|
||||
Для каждого request, subscription, listener, timer, observer, connection или другого долгоживущего ресурса зафиксируй:
|
||||
@@ -393,11 +404,11 @@ Owned or borrowed:
|
||||
Cleanup:
|
||||
```
|
||||
|
||||
Factory или assembly не должна запускать неучтённую долгоживущую работу. Явная операция, запускающая ресурс, предоставляет cleanup. Если assembly обязана создать resource для graph, её публичный result предоставляет cleanup handle; graph owner вызывает его не позже конца scope. Assembly без собственного ресурса не возвращает пустой `dispose` для симметрии.
|
||||
Factory не запускает долгоживущую работу. Явная операция, запускающая resource, предоставляет cleanup. У каждого resource один owner: adapter-owned resource экспортирует handle для aggregate cleanup, assembly-owned resource передаётся adapter как borrowed capability. Assembly немедленно регистрирует cleanup каждого owned resource и полученный adapter lifecycle handle; при любом obligation возвращает идемпотентный aggregate cleanup.
|
||||
|
||||
Спроектируй и failure path любой сборки graph. Если assembly, composition, `app`, request handler или test setup уже создали owned resources и следующий шаг завершился ошибкой до возврата готового graph, текущий graph owner очищает созданное в обратном dependency order. Если rollback, async cleanup или repeated disposal имеют значимую семантику, не придумывай её молча: зафиксируй решение и покрой partial-acquisition test.
|
||||
Спроектируй failure path assembly. Если следующий шаг завершился ошибкой до возврата graph, assembly выполняет все зарегистрированные cleanup obligations в обратном dependency order. После awaited cleanup callbacks запрещены. Покрой partial acquisition, adapter handles, repeated disposal и cleanup errors тестами.
|
||||
|
||||
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`.
|
||||
Environment определяется transitive import graph, а не именем файла или tree shaking. Для RSC, Server Actions, workers, edge runtime и conditional exports установи executable edges, framework references и runtime capabilities. Для SSR-enabled Client Component отдельно проверь server prerender graph, browser hydration graph и framework-deferred browser effects.
|
||||
|
||||
## Рабочие процедуры
|
||||
|
||||
@@ -420,13 +431,13 @@ Environment определяется transitive import graph, а не имене
|
||||
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.
|
||||
4. Для Level 2 сначала реализуй consumer types, `api/ports` при наличии dependency ports, errors, operations и `api/factory`.
|
||||
5. Затем реализуй production adapters, обязательную `assemblies/default`, дополнительные assemblies и framework bindings.
|
||||
6. В graph owner вызывай assembly builders пакетов Level 2 и явные construction points/public callbacks модулей Level 1, передавая им готовые cross-domain API.
|
||||
7. Переведи всех затронутых consumers на public paths.
|
||||
8. Обнови architecture mapping, metadata, package exports, environment declarations и business-safe allowlists, затронутые новой границей.
|
||||
8. Обнови architecture mapping, assembly contexts, package exports, environment declarations и API-safe allowlists, затронутые новой границей.
|
||||
9. Удали obsolete exports, deep imports и старые boundaries в согласованном scope.
|
||||
10. Добавь tests рядом с owners, включая cleanup failure paths для assemblies и одноразовых graph roots, создающих resources.
|
||||
10. Добавь tests рядом с owners, включая adapter contract tests, realtime guarantees и cleanup failure paths assemblies.
|
||||
11. Запусти доступные structural, type, unit, integration и architecture checks и убедись, что новые пути входят в анализ.
|
||||
|
||||
Не оставляй заведомо промежуточную смешанную границу как завершённый результат. Backward compatibility добавляй только для реального внешнего consumer, persisted contract или явно согласованной phased migration.
|
||||
@@ -437,11 +448,11 @@ Environment определяется transitive import graph, а не имене
|
||||
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.
|
||||
5. Перенеси public models, validation, transitions, outcomes и errors под authority `api`.
|
||||
6. Объяви consumer-owned ports, closed port failures и по одной factory на Domain API.
|
||||
7. Оформи production implementations ports как adapter modules и добавь contract tests.
|
||||
8. Создай обязательную `assemblies/default` для baseline production context и дополнительные assemblies только при реальном отличии graph.
|
||||
9. Перенеси domain-specific state, cache, hydration и framework responsibilities в Framework Group.
|
||||
10. Оставь pages, routes и multi-domain UI в `compositions`.
|
||||
11. Переключи external consumers и graph roots.
|
||||
12. Обнови declarations формы домена, модулей, facets, environments и public entry points в project architecture mapping.
|
||||
@@ -456,11 +467,12 @@ Environment определяется transitive import graph, а не имене
|
||||
2. Построй фактическую карту owners, public boundaries, imports и runtime injection.
|
||||
3. Проверь structural правила класса `A` по наблюдаемым evidence.
|
||||
4. Отдельно проверь смысловые правила класса `R`; отсутствие lint error не доказывает их соблюдение.
|
||||
5. Проверь transitive `business` closure, resolved external package entries и business-safe declarations.
|
||||
6. Проверь environment graph и полноту architecture mapping: неизвестные executable paths не должны выпадать из анализа.
|
||||
7. Проверь state/cache/error/lifecycle ownership, включая cleanup частично созданной assembly.
|
||||
8. Проверь, что tests находятся у правильных owners и не дублируют весь behavior на каждом уровне.
|
||||
9. Сначала сообщи findings по severity, затем краткий verdict и остаточные gaps.
|
||||
5. Проверь transitive `api` closure, resolved external package entries и API-safe declarations.
|
||||
6. Проверь importer matrix `api/ports`, `api/factory`, concrete adapters и assemblies.
|
||||
7. Проверь environment graph, resolver conditions и framework reference edges.
|
||||
8. Проверь state/cache/error/realtime/lifecycle ownership, включая cleanup частично созданной assembly.
|
||||
9. Проверь, что tests находятся у правильных owners и не дублируют весь behavior на каждом уровне.
|
||||
10. Сначала сообщи findings по severity, затем краткий verdict и остаточные gaps.
|
||||
|
||||
Каждый finding содержит:
|
||||
|
||||
@@ -481,14 +493,14 @@ Confidence:
|
||||
|
||||
| Ответственность | Основная 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, success cleanup и partial-failure cleanup | Assembly module или одноразовый graph owner |
|
||||
| Domain scenarios, validation, models, outcomes и expected errors | `api` через соответствующую factory |
|
||||
| Deterministic runtime/guards | `api` |
|
||||
| Port mapping и provider behavior | Adapter module |
|
||||
| Graph composition, adapter selection, environment, success cleanup и partial-failure 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.
|
||||
Не повторяй полный Domain API scenario suite в adapter, assembly и framework tests. Проверяй в каждой границе только принадлежащий ей behavior и integration contract.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
@@ -503,17 +515,18 @@ Confidence:
|
||||
|
||||
### Public boundaries
|
||||
|
||||
- Deep imports во внутренности module или `business`.
|
||||
- Deep imports во внутренности module или `api`.
|
||||
- Root barrel доменного пакета или Group.
|
||||
- Reexport adapter implementation через `business`, package root или Domain API вместо public API самого adapter-модуля.
|
||||
- Reexport adapter implementation через `api`, package root или Domain API вместо public API самого adapter-модуля.
|
||||
- Reexport client и server entry points через общий barrel.
|
||||
- Создавать `business/runtime` без внешнего consumer.
|
||||
- Создавать `api/ports` без dependency port или `api/runtime` без внешнего consumer.
|
||||
|
||||
### Business и runtime
|
||||
### Domain API и runtime
|
||||
|
||||
- Импортировать SDK, storage, framework, state/query manager, platform API или hidden nondeterminism в `business`.
|
||||
- Импортировать SDK, storage, framework, state/query manager, platform API или hidden nondeterminism в `api`.
|
||||
- Обходить boundary через helper, `shared` или type alias.
|
||||
- Публиковать raw DTO или library-specific cache/store types в Domain API.
|
||||
- Экспортировать port contracts через consumer-facing `api` вместо `api/ports`.
|
||||
- Позволять adapter определять domain fallback, transition или error semantics.
|
||||
- Прятать production adapter inline в assembly/composition.
|
||||
- Позволять factory выбирать environment или assembly.
|
||||
@@ -522,19 +535,23 @@ Confidence:
|
||||
|
||||
- Добавлять scenario или API method в assembly.
|
||||
- Вызывать factory/assembly из framework binding.
|
||||
- Импортировать `api/factory` или concrete adapter из production graph owner в обход assembly.
|
||||
- При пересечении Level 2 package boundary импортировать framework state, hooks или components другого домена.
|
||||
- При пересечении Level 2 package boundary импортировать чужую factory, assembly, adapter или API singleton.
|
||||
- При пересечении Level 2 package boundary импортировать чужие ports, factory, assembly, adapter или API singleton.
|
||||
- Прятать runtime dependency в service locator, mutable registry или event bus.
|
||||
- Передавать production `infra` capability напрямую в Level 2 factory в обход обязательного adapter-модуля.
|
||||
- Считать `assemblies/default` изоморфной только из-за имени или runtime branch.
|
||||
|
||||
### State и lifecycle
|
||||
|
||||
- Делать cache параллельной domain model.
|
||||
- Строить optimistic domain value из raw form/DTO без business validation.
|
||||
- Строить optimistic domain value из raw form/DTO без API validation.
|
||||
- Использовать file-level singleton без доказанного application scope.
|
||||
- Запускать скрытую subscription/timer при создании API.
|
||||
- Оставлять resource без scope или cleanup.
|
||||
- Возвращать пустой `dispose` только для одинаковой формы assemblies.
|
||||
- Вызывать callbacks после завершившегося cleanup.
|
||||
- Повторять realtime command без idempotency guarantee после `OUTCOME_UNKNOWN`.
|
||||
|
||||
### Процесс
|
||||
|
||||
@@ -569,7 +586,7 @@ Confidence:
|
||||
| Новая 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 |
|
||||
| Store, persistence или external events | Projection owner, API validation, hydration, resync и authoritative source |
|
||||
| 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 |
|
||||
@@ -583,7 +600,7 @@ Confidence:
|
||||
### До изменения файлов
|
||||
|
||||
- [ ] Найден SLM root и path mapping.
|
||||
- [ ] Найдены architecture declarations, metadata и environment/business-safe allowlists проекта.
|
||||
- [ ] Найдены architecture declarations, assembly contexts, environment/API-safe allowlists проекта.
|
||||
- [ ] Прочитаны локальные инструкции.
|
||||
- [ ] Сформулирована responsibility.
|
||||
- [ ] Назначен один owner.
|
||||
@@ -602,16 +619,18 @@ Confidence:
|
||||
- [ ] Нет deep imports и package/Group barrels.
|
||||
- [ ] Layer matrix соблюдена.
|
||||
- [ ] Общий module graph ацикличен.
|
||||
- [ ] `business` import closure environment-neutral и technical-runtime-free.
|
||||
- [ ] `api` import closure environment-neutral и technical-runtime-free.
|
||||
- [ ] Runtime APIs, пересекающие Level 2 package boundary, передаются аргументами; L1 -> L1 использует public API.
|
||||
- [ ] Новые external imports `business` доказанно business-safe и объявлены в allowlist.
|
||||
- [ ] Новые external imports `api` доказанно API-safe и объявлены в allowlist.
|
||||
- [ ] Client/server graphs не содержат несовместимый executable code.
|
||||
- [ ] Facets `business` имеют допустимое содержимое и consumers.
|
||||
- [ ] Production technical dependencies принадлежат нужным adapters.
|
||||
- [ ] Assembly или одноразовый graph owner возвращает точный graph и очищает owned resources после успеха и partial failure.
|
||||
- [ ] Обязательные и фактически существующие optional facets `api` имеют допустимое содержимое и consumers.
|
||||
- [ ] Production implementations ports принадлежат нужным adapters.
|
||||
- [ ] `assemblies/default` представляет объявленный baseline context и не имеет import side effects.
|
||||
- [ ] Assemblies возвращают точный graph и выполняют все owned и adapter-provided cleanup obligations после успеха и partial failure.
|
||||
- [ ] Framework bindings получают готовые APIs.
|
||||
- [ ] Все expected scenario failures имеют собственный stable domain error; technical и foreign errors не протекают наружу.
|
||||
- [ ] Cache не подменяет business authority.
|
||||
- [ ] Framework projection не подменяет authority `api`.
|
||||
- [ ] Realtime ports определяют correlation, ordering, resync, outcome uncertainty и cleanup.
|
||||
- [ ] Architecture mapping, exports, facets и environment declarations соответствуют новым путям.
|
||||
- [ ] Tests проверяют behavior соответствующих owners.
|
||||
- [ ] После migration удалена старая form/boundary.
|
||||
@@ -636,10 +655,11 @@ Confidence:
|
||||
| Нужна точная формулировка правила | [`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) |
|
||||
| Package, Domain API, ports, 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) |
|
||||
| Realtime messages и subscriptions | [`realtime.md`](./reference/draft/level-2/domains/realtime.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) |
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
## Один домен, один модуль
|
||||
|
||||
Связная предметная область получает один доменный модуль. Level 1 не требует выделять business, adapters, assemblies или framework bindings в самостоятельные соседние модули.
|
||||
Связная предметная область получает один доменный модуль. Level 1 не требует выделять Domain API, ports, adapters, assemblies или framework bindings в самостоятельные соседние модули.
|
||||
|
||||
```text
|
||||
domains/auth/
|
||||
@@ -58,4 +58,4 @@ Group не имеет `index.ts`, реализации, состояния ил
|
||||
|
||||
Доменный модуль переводится в доменный пакет Level 2, когда ему нужны устойчивые API, несколько фабрик, разные среды выполнения или независимые SLM-модули сборок и framework-интеграции.
|
||||
|
||||
Такой переход изменяет только форму выбранного домена: корневой доменный модуль исчезает, а его предметная ответственность переходит обязательному модулю `business` внутри доменного пакета. Другие предметные области SLM root не обязаны переходить вместе с ним.
|
||||
Такой переход изменяет только форму выбранного домена: корневой доменный модуль исчезает, а его публичные модели и операции переходят обязательному модулю `api` внутри доменного пакета. Другие предметные области SLM root не обязаны переходить вместе с ним, но входящие imports и production composition roots выбранного домена обновляются.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
|
||||
|
||||
Level 2 предназначен для отдельных предметных областей с устойчивыми API, несколькими способами сборки или самостоятельными framework-модулями. Он сохраняет структурную базу Level 1, но заменяет выбранный доменный модуль доменным пакетом с явными владельцами ролей.
|
||||
Level 2 предназначен для отдельных предметных областей, которым нужен устойчивый Domain API поверх нескольких внешних источников, сред выполнения или самостоятельных framework-модулей. Он сохраняет структурную базу Level 1, но заменяет выбранный доменный модуль доменным пакетом с обязательными `api`, production adapters и штатной сборкой `assemblies/default`.
|
||||
|
||||
## Наследование Level 1
|
||||
|
||||
@@ -11,90 +11,143 @@ Level 2 предназначен для отдельных предметных
|
||||
| Положение Level 1 | Статус в Level 2 |
|
||||
|---|---|
|
||||
| Матрица `app → compositions → domains → { infra, ui } → shared` | Сохраняется |
|
||||
| Модуль, Group, сегмент, компонент, публичный API и граф зависимостей | Сохраняют смысл |
|
||||
| Модуль, Group, сегмент, компонент, публичный API и статический граф зависимостей | Сохраняют смысл |
|
||||
| Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются только для предметной области в пакетной форме |
|
||||
| Единый публичный API модуля `business` | Представлен обязательными type-only и factory-фасетами и необязательным runtime-фасетом |
|
||||
| Единый публичный API модуля `api` | Представлен обязательными consumer type и factory-фасетами, implementer-фасетом ports при необходимости и необязательным runtime-фасетом |
|
||||
| Навигационная Group слоя `domains` | Может одновременно содержать доменные модули и пакеты |
|
||||
| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups |
|
||||
|
||||
Канонический набор требований образуют [правила Level 1](../rules/level-1.md) и [правила Level 2](../rules/level-2.md).
|
||||
|
||||
## Основная идея
|
||||
|
||||
Для прикладного consumer предметная область существует как Domain API:
|
||||
|
||||
```text
|
||||
framework / composition
|
||||
│
|
||||
▼
|
||||
Domain API
|
||||
│
|
||||
▼
|
||||
dependency ports
|
||||
▲
|
||||
│
|
||||
production adapters
|
||||
│
|
||||
▼
|
||||
SDK / backend / storage / realtime
|
||||
```
|
||||
|
||||
Модуль `api` владеет публичными моделями, validation, операциями, outcomes и стабильными ошибками. Он объявляет consumer-owned ports и получает их реализации через фабрику. Adapter знает конкретный provider, assembly выбирает adapters, а framework binding получает готовый API и организует state, cache, reactivity и hydration средствами своего framework.
|
||||
|
||||
Приложение не обращается к предметному внешнему источнику в обход Domain API. Это не запрещает самостоятельные технические сервисы `infra`, universal UI или framework-only SDK для получения opaque input; запрет относится к данным и операциям конкретного домена.
|
||||
|
||||
## Когда выбирать Level 2
|
||||
|
||||
Level 2 оправдан, когда одной предметной области нужны несколько независимо собираемых Domain API, разные browser/server assemblies, несколько технических интеграций или самостоятельные domain-specific модули React, Vue либо другого фреймворка.
|
||||
Level 2 оправдан, когда предметной области нужны:
|
||||
|
||||
Level 2 выбирается для конкретной предметной области. Один SLM root может корректно содержать простые доменные модули Level 1 и доменные пакеты Level 2. Размер каталога сам по себе не требует перехода.
|
||||
- собственная модель, отличающаяся от backend DTO;
|
||||
- стабильные ошибки независимо от SDK и транспорта;
|
||||
- несколько production sources или providers;
|
||||
- HTTP, storage, realtime или platform integrations за одной предметной границей;
|
||||
- разные baseline и специальные assemblies;
|
||||
- строгие client/server/RSC/worker boundaries;
|
||||
- самостоятельные domain-specific framework bindings;
|
||||
- изолированные tests Domain API через fake ports и contract tests adapters.
|
||||
|
||||
Level 2 выбирается для конкретной предметной области. Один SLM root может корректно содержать простые доменные модули Level 1 и доменные пакеты Level 2. Размер каталога, один endpoint или один hook сами по себе не требуют перехода.
|
||||
|
||||
## Цена Level 2
|
||||
|
||||
Пакетная форма намеренно дороже простого доменного модуля. Она добавляет отдельные business-фасеты, минимум одну assembly, явную runtime-инъекцию cross-domain API и adapter-модули для production capabilities. Concrete state/query runtime, SDK и недетерминизм не импортируются напрямую в `business`.
|
||||
Пакетная форма намеренно дороже простого доменного модуля. Она добавляет фасеты `api`, dependency ports, production adapters, обязательную штатную assembly, mapping внешних records и failures, а также отдельные test boundaries.
|
||||
|
||||
Эта цена оправдана независимой сборкой API, несколькими средами, строгой environment boundary или самостоятельными framework bindings. Если домену не нужны такие свойства, он остаётся модулем Level 1.
|
||||
Эта цена окупается, когда Domain API действительно изолирует приложение от внешней модели, ошибок, provider и runtime. Если фабрика только переименовывает один метод SDK и возвращает тот же DTO и error, домену обычно достаточно Level 1.
|
||||
|
||||
Импорт assembly не создаёт граф и не запускает side effects. Composition root вызывает только assemblies dependency-connected доменов, нужных текущему route, request, worker или application scope; глобальная eager-сборка всех `default` не является требованием Level 2.
|
||||
|
||||
## Базовая форма
|
||||
|
||||
```text
|
||||
src/domains/
|
||||
├── catalog/ # Доменный модуль Level 1
|
||||
└── auth/ # Доменный пакет Level 2
|
||||
├── README.md # Необязательная metadata
|
||||
├── 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-модуль
|
||||
├── catalog/ # Доменный модуль Level 1
|
||||
└── auth/ # Доменный пакет Level 2
|
||||
├── README.md # Необязательная metadata
|
||||
├── api/ # Обязательный SLM-модуль
|
||||
│ ├── index.ts # Только consumer-facing public types
|
||||
│ ├── factory.ts # Public factories
|
||||
│ ├── ports.ts # При наличии dependency ports
|
||||
│ └── runtime.ts # Необязательный deterministic runtime
|
||||
├── adapters/ # При наличии dependency ports
|
||||
│ ├── identity-rest/ # SLM-модуль
|
||||
│ └── identity-realtime/ # SLM-модуль
|
||||
├── assemblies/ # Обязательная Group
|
||||
│ ├── default/ # Обязательная штатная assembly
|
||||
│ └── administration/ # Дополнительная assembly
|
||||
└── react/ # Необязательная Framework Group
|
||||
├── session/ # SLM-модуль
|
||||
└── queries/ # SLM-модуль
|
||||
```
|
||||
|
||||
Корень пакета не является модулем и не имеет `index.ts`. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своим публичным API.
|
||||
Корень пакета не является модулем и не имеет `index.ts`. Groups также не имеют агрегирующих API. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своей публичной границей.
|
||||
|
||||
## Публичные границы
|
||||
|
||||
`business` может объявить несколько API и соответствующих фабрик. Assembly создаёт только нужный для своего контекста именованный граф:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthAdministrationApi,
|
||||
AuthError,
|
||||
AuthSession,
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/business'
|
||||
} from '@/domains/auth/api'
|
||||
|
||||
import {
|
||||
authAdministrationFactory,
|
||||
authSessionFactory,
|
||||
} from '@/domains/auth/business/factory'
|
||||
import type {
|
||||
AuthIdentityPort,
|
||||
AuthIdentityPortFailure,
|
||||
} from '@/domains/auth/api/ports'
|
||||
|
||||
import {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
import { createAuthSessionApi } from '@/domains/auth/api/factory'
|
||||
import { isAuthError } from '@/domains/auth/api/runtime'
|
||||
|
||||
import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
|
||||
import { createAuth } from '@/domains/auth/assemblies/default'
|
||||
import { AuthSessionProvider } from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
`business/runtime` существует только при наличии реальных внешних потребителей детерминированных runtime-экспортов. Общие импорты `@/domains/auth` и `@/domains/auth/react` запрещены: пакет и Groups не имеют агрегирующих API.
|
||||
Обычный прикладной consumer импортирует типы `api`, при необходимости deterministic `api/runtime`, готовую production-сборку и framework bindings. Фасет `api/ports` предназначен для adapters, assemblies и tests. Фасет `api/factory` в production импортируют только assemblies своего домена.
|
||||
|
||||
Общие импорты `@/domains/auth`, `@/domains/auth/adapters`, `@/domains/auth/assemblies` и `@/domains/auth/react` запрещены: пакет и Groups не имеют публичного API.
|
||||
|
||||
## Штатная assembly
|
||||
|
||||
Каждый пакет содержит `assemblies/default`. Она создаёт канонический production-граф одного baseline capability context, объявленного проектом.
|
||||
|
||||
`default` может быть browser-only в React + Vite или действительно изоморфной в Next.js. Имя не является доказательством совместимости: проверяется executable import-граф для заявленных resolver conditions. Если RSC, administration, worker или realtime session требуют другого набора API, dependencies, trust или lifecycle, появляется дополнительная именованная assembly.
|
||||
|
||||
## State и framework
|
||||
|
||||
Domain API не является framework store. TanStack Query, SWR, Zustand, Redux, Pinia, Signals и аналогичные runtimes находятся в framework bindings или compositions. Они могут владеть framework metadata и UI-state, но их domain payload состоит только из public values, outcomes и events Domain API.
|
||||
|
||||
Server и client создают разные API instances и caches. Через RSC boundary передаются сериализуемые public values или hydration payload, но не фабрики, API objects, ports или mutable clients.
|
||||
|
||||
## Realtime
|
||||
|
||||
Realtime transport остаётся внутри adapter. Domain API может предоставлять command methods и subscriptions, но публикует только проверенные events, outcomes и stable domain errors. Correlation, acknowledgement, ordering, reconnect, duplicate delivery, resync, outcome uncertainty и cleanup задаются контрактом realtime port и не выводятся из поведения конкретного WebSocket SDK.
|
||||
|
||||
## Совместное применение форм
|
||||
|
||||
Простой доменный модуль и доменный пакет могут постоянно сосуществовать в одном SLM root, но одна предметная область имеет только одну форму. При связи, пересекающей границу пакета Level 2, доменный код использует type-only публичный контракт либо детерминированный `business/runtime`; готовые API передаются runtime-аргументами в месте сборки графа.
|
||||
Простой доменный модуль и доменный пакет могут постоянно сосуществовать в одном SLM root, но одна предметная область имеет только одну форму. При связи, пересекающей пакет Level 2, доменный код использует type-only публичный контракт либо deterministic `api/runtime`; готовые API передаются runtime-аргументами assemblies пакетов Level 2 либо явным construction points модулей Level 1.
|
||||
|
||||
Если доменному модулю Level 1 нужна runtime-инъекция, которой его текущий публичный API не поддерживает, этот потребитель рефакторится или сам переводится в пакетную форму. Level 2 не создаёт скрытый механизм внедрения в старый модуль.
|
||||
Переход одного домена изменяет его входящие dependency edges и composition roots, но не требует переводить несвязанные соседние домены на Level 2.
|
||||
|
||||
## Карта черновика
|
||||
|
||||
- [Терминология](./terminology.md)
|
||||
- [Доменный пакет](./domains/domain-package.md)
|
||||
- [Модуль business](./domains/business.md)
|
||||
- [Фабрики, зависимости и adapters](./domains/factory-ports-adapters.md)
|
||||
- [Assemblies и среды выполнения](./domains/assemblies.md)
|
||||
- [Модуль api и Domain API](./domains/domain-api.md)
|
||||
- [Фабрики, ports и adapters](./domains/factory-ports-adapters.md)
|
||||
- [Assemblies и default](./domains/assemblies.md)
|
||||
- [Состояние и кэш](./domains/state-cache.md)
|
||||
- [Framework Groups и модули](./domains/framework-bindings.md)
|
||||
- [Realtime](./domains/realtime.md)
|
||||
- [Зависимости](./dependencies.md)
|
||||
- [Тестирование](./domains/testing.md)
|
||||
- [Проверка](./validation.md)
|
||||
|
||||
@@ -1,109 +1,152 @@
|
||||
# Зависимости Level 2
|
||||
|
||||
> Уточнение графа зависимостей внутри и между доменными границами.
|
||||
> Уточнение статического import-графа и runtime injection graph внутри и между доменными границами.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-API-A007`](../rules/level-2.md#slm-l2-api-a007)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
|
||||
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
|
||||
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
|
||||
- [`SLM-L2-API-A019`](../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-API-A022`](../rules/level-2.md#slm-l2-api-a022)
|
||||
- [`SLM-L2-PORT-R027`](../rules/level-2.md#slm-l2-port-r027)
|
||||
- [`SLM-L2-ASSEMBLY-R030`](../rules/level-2.md#slm-l2-assembly-r030)
|
||||
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
|
||||
|
||||
## Матрица внутри пакета
|
||||
## Статическая матрица внутри пакета
|
||||
|
||||
| Исходный модуль | Допустимые зависимости |
|
||||
|---|---|
|
||||
| `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` |
|
||||
| `api` | Собственные файлы, объявленные environment-neutral ресурсы `shared`, API-safe packages, type-only Domain API и `api/runtime` других доменов |
|
||||
| Adapter module | `api/ports` своего домена, `infra`, concrete provider runtime, `shared` |
|
||||
| Assembly | `api`, `api/ports`, `api/factory`, при необходимости `api/runtime` своего домена, публичные adapters своего домена, type-only Domain API других доменов, `shared` |
|
||||
| Framework binding module | `api` и `api/runtime` своего домена, публичные framework modules своего домена, framework/state/query runtime, `ui`, `shared` |
|
||||
| Graph owner | Assemblies и framework modules входящих в граф доменов, а также разрешённые матрицей `infra`, `ui` и `shared` |
|
||||
|
||||
`business` не достигает adapters, assemblies, framework-модулей, product SDK, storage, state/query manager, API браузера или Node.js. Проверяется весь транзитивный import-граф публичных фасетов.
|
||||
Модуль `api` не достигает adapters, assemblies, framework modules, product SDK, storage, state/query manager, DOM, Node.js API или других environment-specific capabilities. Проверяется весь транзитивный executable и type graph его фасетов.
|
||||
|
||||
Adapter module импортирует из собственного `business` только типы технических зависимостей. Он не импортирует `business/factory`, потому что не собирает API. Публичный deterministic runtime обычно также не нужен adapter: внешний результат возвращается business для проверки и error mapping.
|
||||
Adapter импортирует contract только через `api/ports`. Он не импортирует factory и consumer-facing runtime, потому что не создаёт API и не выбирает публичный domain outcome.
|
||||
|
||||
Assembly не содержит inline adapters. Она импортирует production implementations через публичные API конкретных модулей `adapters/*`.
|
||||
Assembly импортирует только adapters собственного домена. Production graph owner не импортирует concrete adapters или `api/factory`: он вызывает готовые assembly builders.
|
||||
|
||||
## Междоменные импорты
|
||||
## Публичные фасеты
|
||||
|
||||
При связи, пересекающей границу пакета Level 2, разрешены два вида статического импорта из другого домена:
|
||||
```text
|
||||
api
|
||||
→ import type прикладных contracts
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
import { isAuthError } from '@/domains/auth/business/runtime'
|
||||
api/ports
|
||||
→ import type adapters, assemblies и tests
|
||||
|
||||
api/factory
|
||||
→ runtime import assemblies и API tests
|
||||
|
||||
api/runtime
|
||||
→ runtime import реальных consumers
|
||||
```
|
||||
|
||||
Первый импорт предоставляет type-only контракт для runtime-инъекции. Второй предоставляет только публичную детерминированную функцию или значение. Оба импорта являются рёбрами общего DAG.
|
||||
Символьная type-проверка ports может быть строже обычного path allowlist. Проект объявляет, какие files и modules считаются adapters, assemblies и test boundaries.
|
||||
|
||||
## Междоменные статические импорты
|
||||
|
||||
Если связь пересекает границу пакета Level 2, разрешены:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/api'
|
||||
|
||||
import {
|
||||
isAuthError,
|
||||
} from '@/domains/auth/api/runtime'
|
||||
```
|
||||
|
||||
Для доменного модуля Level 1 используется type-only импорт его обычного публичного API.
|
||||
|
||||
Запрещено импортировать из другого домена:
|
||||
|
||||
- `business/factory`;
|
||||
- готовый API instance или singleton;
|
||||
- `api/factory`;
|
||||
- `api/ports`;
|
||||
- готовый API singleton;
|
||||
- assembly;
|
||||
- adapter;
|
||||
- framework state, hook, context, Provider или component;
|
||||
- любой внутренний путь `business`.
|
||||
- любой внутренний путь `api`.
|
||||
|
||||
Такая модель действует и между двумя пакетами Level 2, и между модулем Level 1 и пакетом Level 2. Между двумя простыми доменными модулями продолжают действовать правила Level 1.
|
||||
Runtime-импорт `api/runtime` остаётся статическим ребром общего DAG. Если он создаёт цикл, границы доменов или владелец pure-функции пересматриваются.
|
||||
|
||||
## Детерминированный runtime
|
||||
## Runtime-инъекция cross-domain API
|
||||
|
||||
`business/runtime` закрывает случай общей продуктовой pure-функции, которую нельзя переносить в `shared` из-за знания о предметной области:
|
||||
|
||||
```ts
|
||||
import {
|
||||
normalizeAuthIdentifier,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
```
|
||||
|
||||
Функция остаётся собственностью Auth, а зависимый домен получает явное runtime-ребро к её публичному контракту. Если связь создаёт цикл, границы доменов или владелец общего понятия пересматриваются.
|
||||
|
||||
Перенос в `shared` допустим только после устранения продуктового знания, а не как обход cross-domain правила.
|
||||
|
||||
## Runtime-инъекция API
|
||||
|
||||
Готовый API другого домена передаётся assembly или одноразовому месту сборки аргументом. Код зависимого домена не импортирует его runtime-фабрику или сборку:
|
||||
Готовый API другого домена передаётся assembly аргументом:
|
||||
|
||||
```text
|
||||
createAuthForRequest()
|
||||
createAuth()
|
||||
→ AuthSessionApi
|
||||
→ createUserForRequest({ auth })
|
||||
→ createUser({ auth })
|
||||
→ UserProfileApi
|
||||
```
|
||||
|
||||
Место сборки графа создаёт независимые домены раньше зависимых. Если сбой Auth становится результатом публичного сценария User, наружу выходит только собственная ошибка User. При exception-модели User может распознать ожидаемый Auth error через публичный guard из `auth/business/runtime`.
|
||||
User assembly передаёт `auth` своей factory. Она не импортирует runtime instance Auth.
|
||||
|
||||
Cross-domain API не превращается автоматически в local port. Bridge port нужен только при реальном translation contract. Structural copy чужого API скрывает owner и затрудняет обнаружение runtime-цикла.
|
||||
|
||||
## Runtime dependency graph
|
||||
|
||||
Статический DAG импортов не показывает все runtime edges, передаваемые аргументами. Architecture mapping объявляет либо review явно восстанавливает:
|
||||
|
||||
- assembly inputs;
|
||||
- создаваемые Domain API;
|
||||
- public APIs и construction points доменных модулей Level 1;
|
||||
- передаваемые factories dependencies;
|
||||
- callbacks и late-bound capabilities, пересекающие Level 2 boundary;
|
||||
- scope и multiplicity;
|
||||
- cleanup order.
|
||||
|
||||
Graph owner создаёт независимые APIs раньше зависимых и освобождает их в обратном порядке. Цикл `A API → B API → A API` запрещён, даже если одна сторона является модулем Level 1, а callback, lazy holder или local structural type сохраняет статически ацикличный import graph.
|
||||
|
||||
Lazy provider или registry не является автоматическим исключением. Для него требуется отдельный readiness, lifecycle и failure contract, а сам runtime edge остаётся частью graph review.
|
||||
|
||||
## Совместное применение Level 1 и Level 2
|
||||
|
||||
Один SLM root может постоянно содержать обе формы. Каждая предметная область объявляется либо модулем, либо пакетом, поэтому checker однозначно определяет применимые правила.
|
||||
Один SLM root может постоянно содержать обе формы. Между двумя доменными модулями Level 1 продолжают действовать обычные правила Level 1.
|
||||
|
||||
Runtime-взаимодействие с пакетом Level 2 собирается за пределами доменного кода. Если существующий модуль Level 1 должен принимать готовый API, его собственный публичный контракт явно предоставляет такую точку инъекции. Если это невозможно без скрытого singleton или обратного импорта, зависимый модуль рефакторится либо переводится на Level 2.
|
||||
Если хотя бы одна сторона является пакетом Level 2, runtime API создаёт внешний graph owner и передаёт assembly зависимого пакета Level 2 либо явной construction point/public callback зависимого модуля Level 1. Если у модуля Level 1 такой точки нет и связь невозможна без global singleton или обратного импорта, модуль рефакторится либо переводится на Level 2.
|
||||
|
||||
Runtime-значение из модуля Level 1, необходимое пакету Level 2, также передаётся внешним graph owner как API, callback или другой явно типизированный аргумент. Код пакета не импортирует executable API модуля Level 1 напрямую.
|
||||
Переход формы остаётся локальным для предметной ответственности, но change radius включает все входящие imports и composition roots выбранного домена.
|
||||
|
||||
## Framework-состояние
|
||||
## Framework state
|
||||
|
||||
Framework binding module использует framework API только своего доменного пакета:
|
||||
Framework binding использует 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'
|
||||
// Допустимо внутри domains/auth/react/queries
|
||||
import {
|
||||
useAuthApi,
|
||||
} from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
Во втором случае composition читает состояния обоих доменов и передаёт необходимые данные или callbacks через публичные свойства компонентов.
|
||||
```ts
|
||||
// Недопустимо внутри domains/user/react/profile
|
||||
import {
|
||||
useAuthApi,
|
||||
} from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
State/query cache внутри framework binding не создаёт исключение для cross-domain imports. Он работает поверх переданного API своего домена.
|
||||
Во втором случае composition читает projections обоих доменов и передаёт values или callbacks через публичные props. Если User Domain API зависит от Auth, связь выполняется assemblies на runtime graph level.
|
||||
|
||||
## Границы сред
|
||||
## Границы сред и RSC
|
||||
|
||||
Каждый client-, server- или shared-entry point имеет совместимый транзитивный import-граф. Серверная assembly или adapter не реэкспортируется через `business`, Framework Group или клиентскую assembly.
|
||||
Каждая declared client, server, edge, worker или shared entry point проверяется под реальными resolver conditions. Название `assemblies/default` не объявляет environment compatibility.
|
||||
|
||||
Tree shaking не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода.
|
||||
Tree shaking и runtime condition не доказывают изоляцию. Server-only adapter не достигается из client entry, даже если ветка считается неиспользуемой.
|
||||
|
||||
Checker различает:
|
||||
|
||||
- executable import edge;
|
||||
- type-only import edge;
|
||||
- framework reference edge;
|
||||
- dynamic import с объявленным target capability set.
|
||||
|
||||
Server Component выполняется в server scope. Ссылка на Client Component и invocation Server Action анализируются как framework references, а не как обычное совместное выполнение. Для SSR-enabled Client Component отдельно проверяются server prerender graph, browser hydration graph и объявленные framework-deferred browser edges. Необъявленный или неанализируемый dynamic import запрещается либо явно allowlist-ится project policy.
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# Доменные пакеты Level 2
|
||||
|
||||
Доменный пакет заменяет один выбранный доменный модуль Level 1. Он объединяет SLM-модули одной предметной области, но сам не является модулем, Group или публичным API.
|
||||
Доменный пакет заменяет один выбранный доменный модуль Level 1. Он объединяет SLM-модули одной предметной области вокруг контролируемого Domain API, но сам не является модулем, Group или публичным API.
|
||||
|
||||
```text
|
||||
domains/auth/
|
||||
├── business/
|
||||
├── assemblies/ # Обязательная Group
|
||||
├── adapters/ # При наличии technical dependencies
|
||||
├── api/ # Обязательный модуль
|
||||
├── adapters/ # При наличии dependency ports
|
||||
├── assemblies/
|
||||
│ └── default/ # Обязательная штатная assembly
|
||||
└── react/
|
||||
├── session/
|
||||
└── login-form/
|
||||
└── queries/
|
||||
```
|
||||
|
||||
Другие предметные области того же SLM root могут оставаться доменными модулями Level 1.
|
||||
@@ -17,12 +18,13 @@ domains/auth/
|
||||
## Основные границы
|
||||
|
||||
- [Доменный пакет](./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 и технические проекции.
|
||||
- [Domain API](./domain-api.md) является единственным семантическим шлюзом к данным и операциям домена.
|
||||
- [Фабрики, ports и adapters](./factory-ports-adapters.md) изолируют SDK, backend, storage, runtime capabilities и provider failures.
|
||||
- [Assemblies](./assemblies.md) содержат обязательную штатную сборку `default` и дополнительные production-контексты.
|
||||
- [Состояние и кэш](./state-cache.md) принадлежат framework bindings или compositions: они могут хранить framework metadata и UI-state, но materialize domain payload только из значений Domain API.
|
||||
- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
|
||||
- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу.
|
||||
- [Realtime](./realtime.md) задаёт messages, subscriptions, correlation, resync, errors и cleanup.
|
||||
- [Тестирование](./testing.md) проверяет Domain API через фабрики, adapters через port contracts и assemblies через production wiring.
|
||||
- [Переход auth](./auth-example.md) показывает локальный переход с Level 1 без big-bang всего root.
|
||||
- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Assemblies и среды выполнения
|
||||
# Assemblies и production-граф
|
||||
|
||||
> Пояснение повторяемой сборки именованного графа Domain API.
|
||||
> Пояснение обязательной штатной сборки, дополнительных контекстов, environment compatibility и lifecycle.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
@@ -8,163 +8,257 @@
|
||||
- [`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-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
|
||||
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
|
||||
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
|
||||
- [`SLM-L2-ASSEMBLY-R030`](../../rules/level-2.md#slm-l2-assembly-r030)
|
||||
- [`SLM-L2-ASSEMBLY-R031`](../../rules/level-2.md#slm-l2-assembly-r031)
|
||||
|
||||
## Назначение
|
||||
|
||||
Assembly является SLM-модулем в Group `assemblies`. Она создаёт явный граф одного или нескольких Domain API пакета для конкретного повторяемого контекста выполнения. Каждый доменный пакет содержит минимум одну assembly.
|
||||
Assembly является SLM-модулем Group `assemblies`. Она выбирает production adapters своего домена, вызывает фабрики `api` и возвращает готовый именованный граф Domain API для одного объявленного production-контекста.
|
||||
|
||||
```text
|
||||
business/factory
|
||||
├── assemblies/browser → { session: AuthSessionApi }
|
||||
├── assemblies/request → { session, administration }
|
||||
└── assemblies/server-action → { administration }
|
||||
api/factory + adapters + cross-domain APIs
|
||||
→ assembly
|
||||
→ named Domain API graph
|
||||
```
|
||||
|
||||
Архитектура не требует `base` или изоморфную assembly и не ограничивает их максимальное количество. Обязательная assembly соответствует реальному поддерживаемому контексту, а не существует только для заполнения структуры.
|
||||
Assembly не добавляет предметные методы, модели, transitions или ошибки. Она также не владеет framework state: готовый API передаётся framework binding или composition.
|
||||
|
||||
Место сборки графа в `composition` может вызвать фабрики напрямую для одноразовой конфигурации. Такая сборка не отменяет обязательную assembly пакета и при наличии технических зависимостей использует публичные adapter-модули, а не inline implementations.
|
||||
Импорт assembly не запускает side effects. Граф появляется только после вызова builder.
|
||||
|
||||
## Именованный граф API
|
||||
## Обязательная default assembly
|
||||
|
||||
Browser assembly импортирует только фабрики и adapters нужных ей API:
|
||||
Каждый пакет содержит модуль `assemblies/default`:
|
||||
|
||||
```text
|
||||
auth/assemblies/
|
||||
├── default/
|
||||
│ └── index.ts
|
||||
└── administration/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`default` является штатной production-сборкой домена для одного baseline capability set, объявленного проектом. Она может быть browser-only, server-only, worker-compatible или действительно isomorphic. Имя не сообщает environment compatibility.
|
||||
|
||||
Пример metadata:
|
||||
|
||||
```yaml
|
||||
assemblies:
|
||||
default:
|
||||
capabilities: [fetch, web-crypto]
|
||||
conditions: [browser, import]
|
||||
administration:
|
||||
capabilities: [node, server-secrets]
|
||||
conditions: [node, import]
|
||||
```
|
||||
|
||||
Формат metadata не нормирован, но checker должен получать capability set и resolver conditions из явного project mapping, а не угадывать их по имени `default`.
|
||||
|
||||
## Дополнительные assemblies
|
||||
|
||||
Дополнительная assembly появляется, когда отличается реальная production-граница:
|
||||
|
||||
- набор Domain API;
|
||||
- dependencies или providers;
|
||||
- trust boundary;
|
||||
- environment capabilities;
|
||||
- scope или lifecycle;
|
||||
- способ аутентификации;
|
||||
- realtime guarantees.
|
||||
|
||||
Хорошие имена описывают контекст: `administration`, `realtime-session`, `worker`, `rsc`. Имя `rsc` оправдано только при отличающемся RSC wiring; само наличие Server Component не требует отдельной assembly.
|
||||
|
||||
Не создаётся assembly-заглушка с методами, бросающими `NOT_SUPPORTED`. Контекст возвращает только реально доступные API.
|
||||
|
||||
## Штатный граф
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
import { authSessionFactory } from '@/domains/auth/business/factory'
|
||||
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
|
||||
import { createBrowserSessionAdapter } from '@/domains/auth/adapters/browser-session'
|
||||
import type {
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/api'
|
||||
|
||||
export type AuthBrowserGraph = Readonly<{
|
||||
import {
|
||||
createAuthSessionApi,
|
||||
} from '@/domains/auth/api/factory'
|
||||
|
||||
import {
|
||||
createAuthRestAdapter,
|
||||
} from '@/domains/auth/adapters/identity-rest'
|
||||
|
||||
export type AuthGraph = Readonly<{
|
||||
session: AuthSessionApi
|
||||
}>
|
||||
|
||||
export const createBrowserAuth = (): AuthBrowserGraph => {
|
||||
const session = authSessionFactory({
|
||||
phone: createPhoneHttpAdapter(),
|
||||
session: createBrowserSessionAdapter(),
|
||||
export const createAuth = (): AuthGraph => {
|
||||
const session = createAuthSessionApi({
|
||||
identity: createAuthRestAdapter(),
|
||||
})
|
||||
|
||||
return { session }
|
||||
}
|
||||
```
|
||||
|
||||
Request assembly может собрать дополнительный API, которого нет в браузере:
|
||||
Обычный graph owner импортирует только production builder:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthAdministrationApi,
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/business'
|
||||
|
||||
import {
|
||||
authAdministrationFactory,
|
||||
authSessionFactory,
|
||||
} from '@/domains/auth/business/factory'
|
||||
createAuth,
|
||||
} from '@/domains/auth/assemblies/default'
|
||||
|
||||
export type AuthRequestGraph = Readonly<{
|
||||
administration: AuthAdministrationApi
|
||||
session: AuthSessionApi
|
||||
}>
|
||||
const auth = createAuth()
|
||||
```
|
||||
|
||||
Assembly не добавляет методы к этим контрактам и не создаёт общий `AuthApi`. Именованный объект только сообщает, какие независимые API доступны в контексте.
|
||||
Factory и concrete adapter остаются construction details assembly. Тесты API и adapters импортируют соответствующие границы напрямую.
|
||||
|
||||
## React + Vite и Next.js
|
||||
|
||||
В React + Vite `default` часто использует browser adapters:
|
||||
|
||||
```text
|
||||
assemblies/default
|
||||
→ browser REST adapter
|
||||
→ browser WebSocket adapter
|
||||
```
|
||||
|
||||
В Next.js та же `default` может считаться isomorphic только при совместимом executable graph под всеми заявленными conditions. Runtime branch не делает импорт безопасным:
|
||||
|
||||
```ts
|
||||
// Недостаточное доказательство изоморфности.
|
||||
if (typeof window === 'undefined') {
|
||||
return createServerAdapter()
|
||||
}
|
||||
|
||||
return createBrowserAdapter()
|
||||
```
|
||||
|
||||
Если server и client требуют разных concrete dependencies, используются разные assemblies или framework-specific resolver entries, проверяемые отдельно.
|
||||
|
||||
## RSC boundary
|
||||
|
||||
RSC не переносит API instance с сервера в браузер:
|
||||
|
||||
```text
|
||||
Server Component
|
||||
→ request-scoped server assembly
|
||||
→ server Domain API instance
|
||||
→ public serializable value
|
||||
→ Client Component boundary
|
||||
→ separate client assembly
|
||||
→ separate client Domain API instance
|
||||
```
|
||||
|
||||
Server Component исполняется в server scope. Его импорт Client Component является framework reference, а не обычным executable edge RSC graph. При включённом SSR или prerender сам Client Component дополнительно исполняется в отдельном server render graph, а затем в browser hydration graph; обе фазы проверяются, а browser-only effects объявляются как framework-deferred edges. Server Action создаёт и очищает собственный request graph на каждый вызов.
|
||||
|
||||
Через boundary не передаются functions, API objects, ports, adapters, mutable cache clients или request secrets.
|
||||
|
||||
## Cross-domain input
|
||||
|
||||
Assembly зависимого домена принимает готовый API аргументом. Cross-domain API не является adapter и не размещается в Group `adapters`:
|
||||
Assembly зависимого домена принимает готовый API аргументом:
|
||||
|
||||
```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'
|
||||
import type {
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/api'
|
||||
|
||||
export type CreateUserForRequestInput = {
|
||||
auth: Pick<AuthSessionApi, 'getSnapshot'>
|
||||
request: UserRequestInput
|
||||
}
|
||||
|
||||
export type UserRequestGraph = Readonly<{
|
||||
profile: UserProfileApi
|
||||
export type CreateUserInput = Readonly<{
|
||||
auth: Pick<AuthSessionApi, 'getSession'>
|
||||
}>
|
||||
|
||||
export const createUserForRequest = ({
|
||||
export const createUser = ({
|
||||
auth,
|
||||
request,
|
||||
}: CreateUserForRequestInput): UserRequestGraph => {
|
||||
const profile = userProfileFactory({
|
||||
}: CreateUserInput): UserGraph => {
|
||||
const profile = createUserProfileApi({
|
||||
auth,
|
||||
profile: createUserProfileAdapter(request),
|
||||
profile: createUserProfileRestAdapter(),
|
||||
})
|
||||
|
||||
return { profile }
|
||||
}
|
||||
```
|
||||
|
||||
Assembly делает только type-only импорт `AuthSessionApi`. Runtime-фабрику, assembly или instance Auth она не импортирует.
|
||||
|
||||
Место сборки графа выполняет runtime-связь:
|
||||
Graph owner выполняет runtime-связь:
|
||||
|
||||
```ts
|
||||
const auth = createAuthForRequest(authInput)
|
||||
const user = createUserForRequest({
|
||||
const auth = createAuth()
|
||||
const user = createUser({
|
||||
auth: auth.session,
|
||||
request: userInput,
|
||||
})
|
||||
```
|
||||
|
||||
## Environment entry points
|
||||
User assembly делает только type-only импорт Auth API. Она не импортирует Auth factory, adapter или assembly. Общий runtime dependency graph остаётся ацикличным.
|
||||
|
||||
Server assembly имеет отдельный публичный entry point и marker выбранного framework или bundler:
|
||||
## Dependency-connected graph
|
||||
|
||||
```ts
|
||||
import 'server-only'
|
||||
Наличие `assemblies/default` у каждого Level 2 package не требует eager-сборки всех доменов:
|
||||
|
||||
export { createAuthForRequest } from './create-auth-for-request'
|
||||
```text
|
||||
route A
|
||||
→ auth/default
|
||||
→ user/default
|
||||
|
||||
route B
|
||||
→ catalog/default
|
||||
```
|
||||
|
||||
Server entry point не реэкспортируется через `business`, Framework Group, browser assembly или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость.
|
||||
Graph owner вызывает только builders, необходимые текущему scope. Module-level вызов `createAuth()` и global registry готовых APIs нарушают явное владение scope.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Factory не запускает запрос, subscription, timer или другую скрытую долгоживущую работу во время создания API. Assembly может активировать технический ресурс только с явной передачей cleanup своему caller. Операция Domain API, которая запускает ресурс позже, сама возвращает cleanup:
|
||||
Factory не запускает запрос, socket, subscription или timer во время создания API. Явная операция, которая позже запускает ресурс, возвращает cleanup:
|
||||
|
||||
```ts
|
||||
const stop = auth.session.startInvalidationTracking()
|
||||
const subscription = await chat.subscribe(observer)
|
||||
|
||||
try {
|
||||
// Scope использует API.
|
||||
await runScope()
|
||||
} finally {
|
||||
await stop()
|
||||
await subscription.close()
|
||||
}
|
||||
```
|
||||
|
||||
Если assembly сама создаёт ресурс, принадлежащий всему возвращённому графу, результат дополнительно предоставляет cleanup handle:
|
||||
Если assembly создаёт owned resource или получает lifecycle handle adapter-owned resource, результат предоставляет aggregate cleanup:
|
||||
|
||||
```ts
|
||||
export type AuthRequestAssembly = Readonly<{
|
||||
apis: AuthRequestGraph
|
||||
export type ChatAssembly = Readonly<{
|
||||
apis: ChatGraph
|
||||
dispose: () => Promise<void>
|
||||
}>
|
||||
```
|
||||
|
||||
```ts
|
||||
const auth = createAuthForRequest(input)
|
||||
Cleanup является идемпотентным. После завершившегося cleanup resource не вызывает callbacks.
|
||||
|
||||
try {
|
||||
return await handleRequest(auth.apis)
|
||||
} finally {
|
||||
await auth.dispose()
|
||||
У каждого resource ровно один owner. Adapter, который сам создаёт connection или source cache, остаётся владельцем и экспортирует lifecycle handle; assembly только включает этот handle в aggregate cleanup. Если connection создаёт assembly, adapter получает borrowed capability и не закрывает её самостоятельно.
|
||||
|
||||
## Частичная ошибка сборки
|
||||
|
||||
Assembly регистрирует cleanup сразу после создания каждого owned resource и сразу после получения adapter lifecycle handle. Если следующий шаг завершается ошибкой, все зарегистрированные obligations выполняются до передачи ошибки caller-у:
|
||||
|
||||
```ts
|
||||
export const createChat = async (): Promise<ChatAssembly> => {
|
||||
const cleanups: Array<() => Promise<void>> = []
|
||||
|
||||
try {
|
||||
const connection = await createRealtimeConnection()
|
||||
cleanups.push(connection.close)
|
||||
|
||||
const history = createHistoryAdapter(connection)
|
||||
const messages = createMessagesApi({ history })
|
||||
|
||||
return {
|
||||
apis: { messages },
|
||||
dispose: createIdempotentReverseCleanup(cleanups),
|
||||
}
|
||||
} catch (error) {
|
||||
await runReverseCleanup(cleanups)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Assembly без собственного ресурса не обязана возвращать пустой `dispose`. Graph owner вызывает каждый реально предоставленный cleanup не позже завершения application, route, request или test scope.
|
||||
Реализация helper не нормирована. Нормативны достижимость cleanup на failure path, обратный dependency order и отсутствие callbacks после завершения disposal.
|
||||
|
||||
Одноразовое место сборки, которое вызывает фабрики напрямую, подчиняется той же границе: оно не запускает скрытый ресурс в constructor и сохраняет cleanup любого созданного adapter-ресурса до завершения своего scope.
|
||||
|
||||
Рекомендуется делать `dispose` идемпотентным, освобождать частично созданные ресурсы при ошибке assembly и закрывать зависимые ресурсы раньше их зависимостей. Детальная политика rollback и поведения API после cleanup остаётся открытым вопросом.
|
||||
Assembly без cleanup obligations возвращает только API graph и не добавляет пустой `dispose` для симметрии. Наличие adapter-owned resource с переданным handle уже является cleanup obligation, даже если assembly не считается его владельцем.
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
# Переход домена auth с Level 1
|
||||
|
||||
> Проверочный пример локального перехода от доменного модуля к доменному пакету.
|
||||
> Проверочный пример локального перехода от доменного модуля к пакету с Domain API, ports, adapters и default assembly.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`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-API-R005`](../../rules/level-2.md#slm-l2-api-r005)
|
||||
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
|
||||
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
|
||||
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
|
||||
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
|
||||
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
|
||||
|
||||
## Исходная форма Level 1
|
||||
|
||||
@@ -25,7 +28,7 @@ domains/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Level 1 разрешает business-сценариям, framework hooks, state adapter и локальной сборке Auth находиться внутри одного модуля.
|
||||
Level 1 разрешает external calls, framework hooks, state и Auth scenarios внутри одной module boundary.
|
||||
|
||||
## Целевая форма Auth
|
||||
|
||||
@@ -33,107 +36,169 @@ Level 1 разрешает business-сценариям, framework hooks, state a
|
||||
domains/
|
||||
├── auth/ # Доменный пакет Level 2
|
||||
│ ├── README.md
|
||||
│ ├── business/ # Один SLM-модуль
|
||||
│ ├── api/ # Один SLM-модуль
|
||||
│ │ ├── errors/
|
||||
│ │ ├── factories/
|
||||
│ │ ├── services/
|
||||
│ │ ├── types/
|
||||
│ │ ├── index.ts # Только public types нескольких API
|
||||
│ │ ├── factory.ts # Public factories entry
|
||||
│ │ └── runtime.ts # Error codes, guards, public pure runtime
|
||||
│ │ ├── models/
|
||||
│ │ ├── operations/
|
||||
│ │ ├── ports/
|
||||
│ │ ├── index.ts # Consumer-facing types
|
||||
│ │ ├── ports.ts # Implementer-facing types
|
||||
│ │ ├── factory.ts # Domain API factories
|
||||
│ │ └── runtime.ts # Guards и public pure runtime
|
||||
│ ├── adapters/ # Group
|
||||
│ │ ├── phone-http/ # SLM-модуль
|
||||
│ │ ├── browser-session/ # SLM-модуль
|
||||
│ │ ├── identity-rest/ # SLM-модуль
|
||||
│ │ ├── identity-realtime/ # SLM-модуль
|
||||
│ │ └── request-session/ # SLM-модуль
|
||||
│ ├── assemblies/ # Обязательная Group
|
||||
│ │ ├── browser/ # Только AuthSessionApi
|
||||
│ │ └── request/ # Session + Administration API
|
||||
│ │ ├── default/ # Штатный Auth graph
|
||||
│ │ └── administration/ # Специальный trusted graph
|
||||
│ └── react/ # Framework Group
|
||||
│ ├── session/ # SLM-модуль
|
||||
│ └── login-form/ # SLM-модуль
|
||||
│ ├── session/ # Provider готового API
|
||||
│ ├── queries/ # Query/cache projection
|
||||
│ └── login-form/ # Переиспользуемый domain UI
|
||||
└── catalog/ # По-прежнему модуль Level 1
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Корневой `domains/auth/index.ts` удаляется. Потребители переходят на публичные API конкретных модулей. `catalog` и остальные домены не меняют форму только из-за перехода Auth.
|
||||
Корневой `domains/auth/index.ts` удаляется. `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` |
|
||||
| Session operations и public models | `auth/api` | `auth/api` |
|
||||
| Port contracts и failures | `auth/api` | `auth/api/ports` |
|
||||
| Runtime factories | `auth/api` | `auth/api/factory` |
|
||||
| Error guards и public pure-функции | `auth/api` | `auth/api/runtime` |
|
||||
| REST provider mapping | `auth/adapters/identity-rest` | Adapter API для assembly |
|
||||
| Realtime protocol и correlation | `auth/adapters/identity-realtime` | Adapter API для assembly |
|
||||
| Request cookies mapping | `auth/adapters/request-session` | Adapter API для assembly |
|
||||
| Штатный production graph | `auth/assemblies/default` | `auth/assemblies/default` |
|
||||
| Trusted administration graph | `auth/assemblies/administration` | `auth/assemblies/administration` |
|
||||
| Provider и session hooks | `auth/react/session` | `auth/react/session` |
|
||||
| Query/cache/hydration | `auth/react/queries` | `auth/react/queries` |
|
||||
| Переиспользуемая форма | `auth/react/login-form` | `auth/react/login-form` |
|
||||
| Страница, текст и redirect | `compositions` | API конкретной composition |
|
||||
|
||||
## Новые импорты
|
||||
## Domain API и port
|
||||
|
||||
```ts
|
||||
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'>
|
||||
export type AuthSessionApi = {
|
||||
getSession: () => Promise<AuthSession>
|
||||
requestPhoneOtp: (
|
||||
command: RequestPhoneOtpCommand,
|
||||
) => Promise<RequestPhoneOtpOutcome>
|
||||
verifyPhoneOtp: (
|
||||
command: VerifyPhoneOtpCommand,
|
||||
) => Promise<AuthSession>
|
||||
}
|
||||
```
|
||||
|
||||
Место сборки создаёт instances:
|
||||
|
||||
```ts
|
||||
const auth = createBrowserAuth()
|
||||
const user = createBrowserUser({ auth: auth.session })
|
||||
export type AuthIdentityPort = {
|
||||
requestPhoneOtp: (
|
||||
command: AuthIdentityPortCommand,
|
||||
) => Promise<AuthIdentityPortResult>
|
||||
verifyPhoneOtp: (
|
||||
command: VerifyIdentityPortCommand,
|
||||
) => Promise<VerifyIdentityPortResult>
|
||||
}
|
||||
```
|
||||
|
||||
User не импортирует Auth factory или assembly, а его React-модули не импортируют `useAuthSession` или Auth components.
|
||||
REST adapter реализует этот port поверх generated client. API проверяет records и преобразует port failures в `AuthError`.
|
||||
|
||||
Если User остаётся модулем Level 1, runtime-связь также выполняется снаружи доменных границ. Для этого его собственный API должен иметь явную точку передачи нужного Auth behavior; иначе именно User требуется рефакторинг или переход, но несвязанные домены не затрагиваются.
|
||||
## Штатная сборка
|
||||
|
||||
```ts
|
||||
import {
|
||||
createAuthSessionApi,
|
||||
} from '@/domains/auth/api/factory'
|
||||
|
||||
import {
|
||||
createIdentityRestAdapter,
|
||||
} from '@/domains/auth/adapters/identity-rest'
|
||||
|
||||
export const createAuth = (): AuthGraph => ({
|
||||
session: createAuthSessionApi({
|
||||
identity: createIdentityRestAdapter(),
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
Обычный production consumer использует:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createAuth,
|
||||
} from '@/domains/auth/assemblies/default'
|
||||
```
|
||||
|
||||
Он не импортирует factory или adapter напрямую.
|
||||
|
||||
## Framework state
|
||||
|
||||
Старый `auth/stores` не переносится в `api`. React query/store projection принадлежит `auth/react/queries`:
|
||||
|
||||
```ts
|
||||
export const useAuthSessionQuery = () => {
|
||||
const api = useAuthApi()
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['auth', 'session'],
|
||||
queryFn: api.getSession,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
При Vue или другом framework та же модель и errors Domain API материализуются его собственными средствами.
|
||||
|
||||
## Realtime
|
||||
|
||||
`identity-realtime` скрывает socket protocol, operation IDs, acknowledgements и reconnect. Domain API возвращает обычный command outcome и публикует проверенные Auth events.
|
||||
|
||||
Если disconnect произошёл до acknowledgement, API не утверждает ложный отказ и может вернуть `AUTH_OPERATION_OUTCOME_UNKNOWN`. После gap binding получает `RESYNC_REQUIRED` и повторно вызывает `getSession()`.
|
||||
|
||||
## RSC
|
||||
|
||||
Server Component создаёт request-scoped Auth graph и передаёт Client Component только сериализуемый `AuthSession` или hydration payload. Client Component создаёт отдельный client graph; при SSR его render должен быть совместим с server prerender, а browser-only capabilities остаются в deferred effects.
|
||||
|
||||
`assemblies/default` используется в обоих местах только если её executable graph действительно совместим со всеми declared conditions. Иначе появляется отдельная assembly, например `auth/assemblies/rsc`.
|
||||
|
||||
## Cross-domain graph
|
||||
|
||||
Если User package зависит от Auth, он импортирует только type contract:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/api'
|
||||
```
|
||||
|
||||
User assembly принимает готовый API:
|
||||
|
||||
```ts
|
||||
const auth = createAuth()
|
||||
const user = createUser({
|
||||
auth: auth.session,
|
||||
})
|
||||
```
|
||||
|
||||
User не импортирует Auth factory, port, adapter, assembly или React hooks. Если User остаётся модулем Level 1, его public API должен иметь явную точку передачи нужного Auth behavior.
|
||||
|
||||
## Порядок перехода
|
||||
|
||||
1. Выбрать один доменный модуль и зафиксировать его внешние consumers.
|
||||
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-у.
|
||||
1. Зафиксировать consumers, external sources, state, errors и lifecycle исходного Auth module.
|
||||
2. Объявить consumer-facing Domain API и public models.
|
||||
3. Объявить dependency ports, records и closed failures.
|
||||
4. Реализовать factory и проверить Domain API через fake ports.
|
||||
5. Оформить каждую production implementation модулем `adapters/*` и добавить contract tests.
|
||||
6. Создать `assemblies/default` для штатного production context.
|
||||
7. Добавить специальные assemblies только для реально отличающихся graphs.
|
||||
8. Перенести framework state, cache и hydration в modules Group `react`.
|
||||
9. Перенести страницы, redirects и multi-domain UI в `compositions`.
|
||||
10. Перевести внешние imports на разрешённые public paths.
|
||||
11. Обновить dependency-connected graph owners и cross-domain inputs.
|
||||
12. Удалить старый root `index.ts` Auth и объявить package checker-у.
|
||||
|
||||
Завершённость перехода определяется только границей Auth. Наличие других доменных модулей Level 1 не является миграционным долгом.
|
||||
Завершённость перехода определяется одной формой Auth и отсутствием обходных imports. Наличие других доменных модулей Level 1 не является миграционным долгом.
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
# Модуль 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`, после чего преобразовать ожидаемую ошибку в собственный контракт.
|
||||
|
||||
Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки.
|
||||
260
skills/slm-design/reference/draft/level-2/domains/domain-api.md
Normal file
260
skills/slm-design/reference/draft/level-2/domains/domain-api.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Модуль api и Domain API
|
||||
|
||||
> Пояснение семантического шлюза домена, его публичных фасетов, моделей, операций и ошибок.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-API-R005`](../../rules/level-2.md#slm-l2-api-r005)
|
||||
- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
|
||||
- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-a007)
|
||||
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
|
||||
- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009)
|
||||
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
|
||||
- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018)
|
||||
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
|
||||
- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024)
|
||||
- [`SLM-L2-API-R025`](../../rules/level-2.md#slm-l2-api-r025)
|
||||
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
|
||||
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
|
||||
|
||||
## Роль
|
||||
|
||||
`api` является обязательным SLM-модулем доменного пакета. Для прикладного consumer предметная область доступна только через объявленные им Domain API, public models, outcomes и errors.
|
||||
|
||||
Модуль `api` владеет:
|
||||
|
||||
- именованными Domain API;
|
||||
- публичными командами, запросами и подписками;
|
||||
- public domain models;
|
||||
- validation внешних и port values;
|
||||
- семантикой outcomes и expected errors;
|
||||
- dependency ports и port failures;
|
||||
- одной фабрикой для каждого Domain API;
|
||||
- необходимыми consumers deterministic guards и pure-функциями.
|
||||
|
||||
Модуль не владеет framework store, query cache, hydration runtime, SDK, transport client или production adapter. Он может координировать одну операцию и замыкать переданные ports, но не хранит скрытую mutable projection данных приложения между вызовами.
|
||||
|
||||
## Domain API как шлюз
|
||||
|
||||
```text
|
||||
consumer command
|
||||
→ Domain API
|
||||
→ dependency port
|
||||
→ adapter
|
||||
→ provider
|
||||
|
||||
provider record/failure
|
||||
→ adapter mapping
|
||||
→ port record/failure
|
||||
→ Domain API validation and semantics
|
||||
→ public model/outcome/error
|
||||
→ consumer
|
||||
```
|
||||
|
||||
Framework hook, store или composition не импортирует concrete SDK и не читает предметный внешний источник напрямую. Это позволяет менять endpoint, provider и transport, сохраняя публичный контракт, пока не изменилась продуктовая семантика.
|
||||
|
||||
Domain API не обязан скрывать реальное предметное изменение. Если backend изменил правило, которое влияет на публичный outcome приложения, контракт домена пересматривается явно.
|
||||
|
||||
## Публичные фасеты
|
||||
|
||||
Один логический публичный API модуля `api` разделён по аудиториям.
|
||||
|
||||
### Consumer types
|
||||
|
||||
Корневой `api/index.ts` экспортирует только типы, необходимые прикладным consumers:
|
||||
|
||||
```ts
|
||||
export type {
|
||||
AuthError,
|
||||
AuthErrorCode,
|
||||
AuthSession,
|
||||
AuthSessionApi,
|
||||
RequestPhoneOtpCommand,
|
||||
VerifyPhoneOtpCommand,
|
||||
} from './types'
|
||||
```
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthSession,
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/api'
|
||||
```
|
||||
|
||||
Port contracts, factory dependencies, provider records и technical failures не входят в consumer-facing barrel.
|
||||
|
||||
### Implementer types
|
||||
|
||||
`api/ports.ts` существует только при наличии dependency ports и экспортирует implementer-facing contracts:
|
||||
|
||||
```ts
|
||||
export type {
|
||||
AuthIdentityPort,
|
||||
AuthIdentityPortFailure,
|
||||
AuthIdentityRecord,
|
||||
AuthSessionApiDependencies,
|
||||
} from './ports'
|
||||
```
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthIdentityPort,
|
||||
} from '@/domains/auth/api/ports'
|
||||
```
|
||||
|
||||
Этим фасетом пользуются adapters своего домена, assemblies и tests. Прикладной consumer не строит поведение по port records или failures.
|
||||
|
||||
### Factory entry
|
||||
|
||||
`api/factory.ts` экспортирует только именованные runtime-фабрики:
|
||||
|
||||
```ts
|
||||
export {
|
||||
createAuthAdministrationApi,
|
||||
createAuthSessionApi,
|
||||
} from './factories'
|
||||
```
|
||||
|
||||
```ts
|
||||
import {
|
||||
createAuthSessionApi,
|
||||
} from '@/domains/auth/api/factory'
|
||||
```
|
||||
|
||||
В production этот фасет импортируют только assemblies текущего домена. API-тесты используют его с fake ports.
|
||||
|
||||
### Runtime entry
|
||||
|
||||
Необязательный `api/runtime.ts` экспортирует только публичный детерминированный runtime:
|
||||
|
||||
```ts
|
||||
export {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
projectSessionEvent,
|
||||
} from './runtime'
|
||||
```
|
||||
|
||||
Здесь допустимы error codes и guards, validators, value constructors, pure transitions, reconciliation functions и immutable-константы. Фасет не содержит фабрики, API instances, ports, I/O, subscriptions, mutable state или environment-specific код.
|
||||
|
||||
Если runtime-потребителей нет, файл не создаётся. Другие внешние пути внутри `api` являются deep imports.
|
||||
|
||||
## Stateless runtime boundary
|
||||
|
||||
Domain API управляет смыслом данных, а не способом их materialization. Query и command возвращают public values или outcomes, которые framework binding может сохранить в TanStack Query, Zustand, Pinia или другом runtime:
|
||||
|
||||
```ts
|
||||
export type AuthSessionApi = {
|
||||
getSession: () => Promise<AuthSession>
|
||||
requestPhoneOtp: (
|
||||
command: RequestPhoneOtpCommand,
|
||||
) => Promise<RequestPhoneOtpOutcome>
|
||||
verifyPhoneOtp: (
|
||||
command: VerifyPhoneOtpCommand,
|
||||
) => Promise<AuthSession>
|
||||
signOut: () => Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
API не экспортирует `getState`, mutable store, QueryClient или framework subscription. Operation-local correlation, cancellation и validation допустимы; canonical cache приложения остаётся у framework consumer.
|
||||
|
||||
Если клиентский workflow имеет предметное состояние, framework хранит readonly value, а API определяет переход:
|
||||
|
||||
```ts
|
||||
const nextCheckout = checkoutApi.applyCommand(
|
||||
currentCheckout,
|
||||
command,
|
||||
)
|
||||
```
|
||||
|
||||
Или consumer использует pure-функцию `api/runtime`. Framework не применяет предметный merge самостоятельно.
|
||||
|
||||
## Несколько Domain API
|
||||
|
||||
```ts
|
||||
export type AuthSessionApi = {
|
||||
getSession: () => Promise<AuthSession>
|
||||
signIn: (command: SignInCommand) => Promise<AuthSession>
|
||||
signOut: () => Promise<void>
|
||||
}
|
||||
|
||||
export type AuthAdministrationApi = {
|
||||
revokeUserSessions: (
|
||||
command: RevokeUserSessionsCommand,
|
||||
) => Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
`AuthSessionApi` и `AuthAdministrationApi` могут иметь разные ports, trust boundaries и assemblies. Один публичный сценарий принадлежит ровно одному API.
|
||||
|
||||
Разделение не используется только ради файловой декомпозиции. Если APIs не могут быть созданы независимо из-за общей atomicity, состояния или lifecycle, они объединяются либо получают один явно созданный shared capability через assembly.
|
||||
|
||||
Assembly возвращает именованный граф готовых контрактов:
|
||||
|
||||
```ts
|
||||
export type AuthGraph = Readonly<{
|
||||
session: AuthSessionApi
|
||||
}>
|
||||
```
|
||||
|
||||
Такой граф сообщает доступный набор API, но не является новым предметным API.
|
||||
|
||||
## Errors и failure algebra
|
||||
|
||||
Ожидаемая публичная ошибка имеет устойчивую readonly сериализуемую форму:
|
||||
|
||||
```ts
|
||||
export type AuthErrorCode =
|
||||
| 'AUTH_IDENTITY_INVALID'
|
||||
| 'AUTH_RATE_LIMITED'
|
||||
| 'AUTH_SERVICE_UNAVAILABLE'
|
||||
|
||||
export type AuthError = Readonly<{
|
||||
code: AuthErrorCode
|
||||
}>
|
||||
```
|
||||
|
||||
Внешний failure проходит две границы:
|
||||
|
||||
```text
|
||||
provider error
|
||||
→ adapter
|
||||
→ closed port failure
|
||||
→ api
|
||||
→ stable domain error
|
||||
```
|
||||
|
||||
Например, adapter переводит HTTP `429`, SDK class или socket error frame в `AuthIdentityPortFailure` с типом `RATE_LIMITED`. Domain API решает, что публичная операция завершается `AUTH_RATE_LIMITED`.
|
||||
|
||||
Port failure не содержит raw provider object в публично доступной форме. Domain error не включает status, SDK class, source message, payload или `cause`. Диагностические данные остаются в observability-механизме adapter или infra.
|
||||
|
||||
Cancellation и `OUTCOME_UNKNOWN` не объединяются с обычным failure, если приложение должно различать их. Ошибка программирования и нарушенный внутренний инвариант не маскируются под expected domain error.
|
||||
|
||||
Выбор exception или discriminated `Result` остаётся policy проекта. Архитектурная цепочка provider failure → port failure → domain error не зависит от канала передачи.
|
||||
|
||||
## Недетерминизм
|
||||
|
||||
Clock, timer, random, ID generator и environment передаются как dependency ports:
|
||||
|
||||
```ts
|
||||
export type AuthRuntimePort = {
|
||||
now: () => number
|
||||
createId: () => string
|
||||
}
|
||||
```
|
||||
|
||||
Модуль `api` не читает `Date.now`, `Math.random`, env или platform globals напрямую, если они влияют на результат операции. Это сохраняет детерминированность API-тестов и явную environment boundary.
|
||||
|
||||
## Потребители фасетов
|
||||
|
||||
| Потребитель | `api` | `api/ports` | `api/factory` | `api/runtime` |
|
||||
|---|---|---|---|---|
|
||||
| Adapter своего домена | Нет | Type-only | Нет | Нет |
|
||||
| Assembly своего домена | Type-only | Type-only | Да | При необходимости |
|
||||
| Framework binding своего домена | Type-only | Нет | Нет | При необходимости |
|
||||
| `composition` или `app` | Type-only | Нет | Нет | При необходимости |
|
||||
| Код другого домена | Type-only | Нет | Нет | При необходимости |
|
||||
| API-тест | Type-only | Type-only | Да | По тестируемой границе |
|
||||
|
||||
Прикладной production graph создаётся assemblies. `app`, compositions и framework bindings не импортируют factory или concrete adapters.
|
||||
@@ -7,17 +7,17 @@
|
||||
- [`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-API-R005`](../../rules/level-2.md#slm-l2-api-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-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
|
||||
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
|
||||
|
||||
## Предметная граница
|
||||
|
||||
Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать business-сценарии авторизации, её browser/server assemblies и React-модули, но не страницу профиля или общий database client.
|
||||
Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать Domain API авторизации, production adapters её providers, assemblies и React bindings, но не страницу профиля, общий database client или multi-domain navigation policy.
|
||||
|
||||
Пакет не владеет исполняемой ответственностью. Конкретными API, состоянием, зависимостями и lifecycle владеют модули внутри него.
|
||||
Пакет не владеет исполняемой ответственностью. Domain API, adapters, production graph, framework projection и lifecycle принадлежат конкретным модулям внутри него.
|
||||
|
||||
Level 2 применяется к пакету, а не ко всему SLM root. Например, `auth` может быть пакетом Level 2, пока `catalog` и `news` остаются доменными модулями Level 1.
|
||||
|
||||
@@ -26,9 +26,9 @@ Level 2 применяется к пакету, а не ко всему SLM root
|
||||
```text
|
||||
domains/auth/
|
||||
├── README.md
|
||||
├── business/
|
||||
├── assemblies/
|
||||
├── api/
|
||||
├── adapters/
|
||||
├── assemblies/
|
||||
└── react/
|
||||
```
|
||||
|
||||
@@ -36,17 +36,18 @@ domains/auth/
|
||||
|
||||
- документация;
|
||||
- ownership metadata;
|
||||
- декларативный manifest или декларативная конфигурация архитектурной проверки;
|
||||
- обязательный модуль `business`;
|
||||
- обязательная непустая Group `assemblies`;
|
||||
- непустая Group `adapters`, если хотя бы одна фабрика имеет технические зависимости;
|
||||
- декларативный manifest архитектурной проверки;
|
||||
- объявления environment capability sets;
|
||||
- обязательный модуль `api`;
|
||||
- обязательная непустая Group `assemblies` с модулем `default`;
|
||||
- непустая Group `adapters`, если хотя бы одна фабрика имеет dependency port;
|
||||
- Framework Groups при наличии соответствующих модулей.
|
||||
|
||||
В корне запрещены:
|
||||
|
||||
- `index.ts` или другой агрегирующий executable entry point;
|
||||
- runtime-файлы и side effects;
|
||||
- изменяемое состояние и ресурсы lifecycle;
|
||||
- изменяемое состояние и lifecycle resources;
|
||||
- реэкспорт API внутренних модулей;
|
||||
- page-specific компоненты или сборка нескольких доменов.
|
||||
|
||||
@@ -58,31 +59,33 @@ Metadata содержит только статические данные. Пр
|
||||
|
||||
Отсутствие root barrel намеренно:
|
||||
|
||||
- client- и server-entry points не агрегируются в один импорт;
|
||||
- каждый модуль сохраняет отдельную ответственность и environment boundary;
|
||||
- переименование публичного модуля является изменением его собственного контракта, а не скрывается пакетом;
|
||||
- versioning целого publishable package остаётся за пределами Level 2.
|
||||
- client, server, RSC и worker entry points не агрегируются в один импорт;
|
||||
- каждый модуль сохраняет отдельные ответственность и environment boundary;
|
||||
- concrete adapters не становятся частью Domain API;
|
||||
- Groups не превращаются в скрытые modules;
|
||||
- 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-модуль
|
||||
├── api/ # SLM-модуль
|
||||
│ ├── index.ts # Consumer-facing types
|
||||
│ ├── ports.ts # Implementer-facing types
|
||||
│ ├── factory.ts # Runtime factories
|
||||
│ └── runtime.ts # Необязательный deterministic runtime
|
||||
├── adapters/ # Group при наличии ports
|
||||
│ ├── identity-rest/ # SLM-модуль
|
||||
│ └── identity-realtime/ # SLM-модуль
|
||||
├── assemblies/ # Обязательная Group
|
||||
│ ├── default/ # Обязательный SLM-модуль
|
||||
│ └── administration/ # Дополнительный SLM-модуль
|
||||
└── react/ # Framework Group
|
||||
├── session/ # SLM-модуль
|
||||
└── queries/ # SLM-модуль
|
||||
```
|
||||
|
||||
Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/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 не имеют `index.ts`. Публичными путями являются `auth/api`, `auth/api/ports`, `auth/api/factory`, опциональный `auth/api/runtime`, `auth/adapters/identity-rest`, `auth/assemblies/default` и `auth/react/session`, но не `auth`, `auth/adapters`, `auth/assemblies` или `auth/react`.
|
||||
|
||||
## Навигационные Groups
|
||||
|
||||
@@ -97,18 +100,20 @@ domains/
|
||||
|
||||
Такая Group отличается от Group внутри пакета допустимым составом: она содержит доменные модули, доменные пакеты и другие navigation Groups, а не внутренние модули пакета.
|
||||
|
||||
Одна предметная область не представляется одновременно модулем и пакетом. Переход завершается удалением старой границы именно этого домена, а не переводом всех соседних областей.
|
||||
Одна предметная область не представляется одновременно модулем и пакетом. Переход завершается удалением старой границы выбранного домена, но требует обновить все его входящие imports и production composition roots.
|
||||
|
||||
## Границы соседних слоёв
|
||||
|
||||
| Ответственность | Владелец |
|
||||
|---|---|
|
||||
| Предметные сценарии, Domain API, доменные ошибки | `business` |
|
||||
| Техническая реализация зависимости одного домена | Adapter внутри пакета |
|
||||
| Сборка API для именованного контекста | Assembly внутри пакета |
|
||||
| Публичные модели, Domain API, validation и domain errors | `api` |
|
||||
| Контракт external capability | `api/ports` |
|
||||
| Production-реализация dependency port | Adapter внутри пакета |
|
||||
| Штатный production-граф | `assemblies/default` |
|
||||
| Специальный production-граф | Дополнительная assembly |
|
||||
| Domain-specific framework state, cache и bindings | Модуль внутри `react`, `vue` и аналогичной Group |
|
||||
| Универсальный технический сервис | `infra` |
|
||||
| Domain-specific framework API | Модуль внутри `react`, `vue` и аналогичной Group |
|
||||
| Страница, маршрут, redirect, продуктовый текст | `compositions` |
|
||||
| UI, объединяющий несколько доменов | `compositions` |
|
||||
|
||||
Зависимость от React сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.
|
||||
Зависимость от React, WebSocket или SDK сама по себе не определяет владельца. Решающими остаются предметная ответственность, направление dependency inversion и публичная граница.
|
||||
|
||||
@@ -1,158 +1,235 @@
|
||||
# Фабрики, зависимости и adapters
|
||||
# Фабрики, ports и adapters
|
||||
|
||||
> Пояснение границы между `business` и технической средой.
|
||||
> Пояснение dependency inversion между Domain API и внешними runtime-возможностями.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-a007)
|
||||
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
|
||||
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
|
||||
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
|
||||
- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018)
|
||||
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
|
||||
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
|
||||
- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024)
|
||||
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
|
||||
|
||||
## Одна фабрика на API
|
||||
## Одна фабрика на Domain API
|
||||
|
||||
```text
|
||||
явные зависимости + business factory → один Domain API
|
||||
явные ports + cross-domain APIs + factory → один Domain API
|
||||
```
|
||||
|
||||
Модуль `business` предоставляет одну именованную фабрику для каждого объявленного API. Фабрики экспортируются через общий runtime-фасет `business/factory`:
|
||||
Модуль `api` предоставляет одну именованную фабрику для каждого объявленного Domain API:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthAdministrationApi,
|
||||
AuthAdministrationDeps,
|
||||
AuthSessionApi,
|
||||
AuthSessionDeps,
|
||||
} from '@/domains/auth/business'
|
||||
} from '@/domains/auth/api'
|
||||
|
||||
export type AuthSessionFactory = (
|
||||
deps: AuthSessionDeps,
|
||||
import type {
|
||||
AuthIdentityPort,
|
||||
AuthRuntimePort,
|
||||
} from '@/domains/auth/api/ports'
|
||||
|
||||
export type AuthSessionApiDependencies = Readonly<{
|
||||
identity: AuthIdentityPort
|
||||
runtime: AuthRuntimePort
|
||||
}>
|
||||
|
||||
export type AuthSessionApiFactory = (
|
||||
dependencies: AuthSessionApiDependencies,
|
||||
) => AuthSessionApi
|
||||
|
||||
export type AuthAdministrationFactory = (
|
||||
deps: AuthAdministrationDeps,
|
||||
) => AuthAdministrationApi
|
||||
```
|
||||
|
||||
```ts
|
||||
import {
|
||||
authAdministrationFactory,
|
||||
authSessionFactory,
|
||||
} from '@/domains/auth/business/factory'
|
||||
createAuthSessionApi,
|
||||
} from '@/domains/auth/api/factory'
|
||||
```
|
||||
|
||||
Фабрика не выбирает environment, assembly или конкретную production-реализацию зависимости. Разные API могут иметь разные наборы deps и собираться независимо.
|
||||
Фабрика не выбирает environment, provider, adapter или assembly. Она не открывает connection, не запускает subscription и не создаёт framework state. Разные Domain API могут иметь разные dependency sets и собираться независимо.
|
||||
|
||||
## Технические зависимости
|
||||
## Consumer-owned ports
|
||||
|
||||
Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы.
|
||||
Port описывает capability с позиции модуля `api`, а не повторяет конкретный provider:
|
||||
|
||||
```ts
|
||||
export type AuthPhoneDependency = {
|
||||
requestCode: (phone: string) => Promise<unknown>
|
||||
verifyCode: (code: string) => Promise<unknown>
|
||||
export type AuthIdentityRecord = Readonly<{
|
||||
expiresAt: number
|
||||
subject: string
|
||||
}>
|
||||
|
||||
export type AuthIdentityPortFailure =
|
||||
| Readonly<{ type: 'FORBIDDEN' }>
|
||||
| Readonly<{ type: 'RATE_LIMITED' }>
|
||||
| Readonly<{ type: 'UNAVAILABLE' }>
|
||||
|
||||
export type AuthIdentityPortResult =
|
||||
| Readonly<{
|
||||
ok: true
|
||||
value: AuthIdentityRecord
|
||||
}>
|
||||
| Readonly<{
|
||||
ok: false
|
||||
failure: AuthIdentityPortFailure
|
||||
}>
|
||||
|
||||
export type AuthIdentityPort = {
|
||||
signIn: (
|
||||
command: AuthIdentityPortCommand,
|
||||
) => Promise<AuthIdentityPortResult>
|
||||
}
|
||||
```
|
||||
|
||||
`unknown` допустим на границе непроверенного внешнего результата. `business` проверяет его до превращения в предметные данные или состояние.
|
||||
Port не экспортирует generated DTO, SDK error class, HTTP status или concrete client. `AuthIdentityRecord` не становится `AuthSession`: модуль `api` проверяет record и создаёт публичную модель.
|
||||
|
||||
Техническими зависимостями также являются:
|
||||
Не каждый port обязан использовать `Result`. Exception, callback или async iterable допустимы при project policy, если expected failures, cancellation, outcome uncertainty и cleanup остаются типизированными и проверяемыми.
|
||||
|
||||
- concrete state/query runtime;
|
||||
- subscription и event source;
|
||||
- browser, Node.js и framework capabilities;
|
||||
- request data и abort signal;
|
||||
- текущее время и timer;
|
||||
- random и ID generator;
|
||||
- environment и runtime configuration provider.
|
||||
## Гранулярность ports
|
||||
|
||||
```ts
|
||||
export type VerificationDeps = {
|
||||
clock: { now: () => number }
|
||||
ids: { create: () => string }
|
||||
timer: { delay: (ms: number) => Promise<void> }
|
||||
}
|
||||
Port соответствует связной capability, а не каждому endpoint и не всему SDK:
|
||||
|
||||
```text
|
||||
AuthIdentityPort
|
||||
├── requestCode
|
||||
├── verifyCode
|
||||
└── revokeSession
|
||||
```
|
||||
|
||||
`Date.now()`, `new Date()` без аргумента, `Math.random()`, `crypto.randomUUID()`, скрытый env и глобальный timer не читаются напрямую business-кодом, если влияют на поведение сценария. Детерминированная арифметика над переданным timestamp остаётся business-safe.
|
||||
Допустимо разделить capability, если операции имеют разные trust boundaries, lifecycle или providers. Запрещено создавать десятки pass-through ports только ради зеркала transport operations.
|
||||
|
||||
Cancellation также описывается business-owned контрактом. Concrete `AbortSignal` или другой platform type остаётся внутри adapter, пока не принят отдельный общий публичный контракт.
|
||||
Clock, timer, random, ID generator и environment также являются ports, если влияют на результат Domain API. Materialized framework state и query cache ports не являются: они принадлежат framework binding.
|
||||
|
||||
Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics.
|
||||
## Failure algebra
|
||||
|
||||
## Cross-domain API dependency
|
||||
Expected failure проходит две явные стадии:
|
||||
|
||||
Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом:
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
|
||||
export type UserDeps = {
|
||||
auth: Pick<AuthSessionApi, 'getSnapshot'>
|
||||
}
|
||||
```text
|
||||
provider-specific failure
|
||||
→ adapter mapping
|
||||
→ closed port failure
|
||||
→ api mapping
|
||||
→ stable domain error or outcome
|
||||
```
|
||||
|
||||
Runtime-значение место сборки графа передаёт через assembly либо напрямую зависимой business-фабрике. `user/business` не импортирует executable factory, assembly или instance Auth.
|
||||
Port failure должен сохранять различия, которые нужны Domain API. Если adapter сводит `FORBIDDEN`, `CONFLICT` и `UNAVAILABLE` к `unknown`, API не может выбрать корректную публичную семантику. Если adapter передаёт HTTP status или SDK error, concrete provider протекает внутрь API.
|
||||
|
||||
Если exception-модели нужен runtime guard чужой ожидаемой ошибки, зависимый business может импортировать его из детерминированного `auth/business/runtime`. Этот импорт остаётся обычным ребром DAG.
|
||||
Unexpected exception не обязана превращаться в expected failure. Cancellation объявляется отдельно от failure, если caller управляет ею. Disconnect или timeout после отправки неидемпотентной команды может означать `OUTCOME_UNKNOWN`, а не доказанный отказ.
|
||||
|
||||
## Adapter module
|
||||
|
||||
Adapter module соединяет явную техническую зависимость фабрики с конкретной системой:
|
||||
Adapter соединяет port с concrete provider:
|
||||
|
||||
```text
|
||||
business dependency ← adapter → SDK / query runtime / platform / request data
|
||||
api-owned port ← adapter → SDK / REST / storage / platform / realtime
|
||||
```
|
||||
|
||||
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'
|
||||
import type {
|
||||
AuthIdentityPort,
|
||||
} from '@/domains/auth/api/ports'
|
||||
|
||||
const session = authSessionFactory({
|
||||
phone: createPhoneHttpAdapter(),
|
||||
runtime: createBrowserRuntimeAdapter(),
|
||||
export const createAuthRestAdapter = (
|
||||
client: IdentityClient,
|
||||
): AuthIdentityPort => ({
|
||||
async signIn(command) {
|
||||
try {
|
||||
const response = await client.signIn({
|
||||
login: command.identifier,
|
||||
password: command.secret,
|
||||
})
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
expiresAt: response.expires_at,
|
||||
subject: response.user_id,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return mapIdentityProviderFailure(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Если ни одна фабрика не имеет технических зависимостей, Group `adapters` не обязательна. Cross-domain API dependency не считается adapter и передаётся отдельно.
|
||||
Adapter преобразует protocol arguments, records и expected failures, но не решает, какой `AuthError` получит приложение, не добавляет предметный fallback и не объявляет метод Domain API.
|
||||
|
||||
Локальные fake implementations в business-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код.
|
||||
## Размещение adapters
|
||||
|
||||
Каждая связная production-реализация является отдельным SLM-модулем Group `adapters`:
|
||||
|
||||
```text
|
||||
auth/adapters/
|
||||
├── identity-rest/
|
||||
│ └── index.ts
|
||||
├── identity-realtime/
|
||||
│ └── index.ts
|
||||
└── session-cookie/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Один adapter-модуль может реализовать несколько тесно связанных ports одного provider. Group `adapters` не имеет `index.ts` и не реэкспортирует дочерние модули.
|
||||
|
||||
Production adapter запрещено определять:
|
||||
|
||||
- внутри `api`;
|
||||
- закрытым сегментом assembly;
|
||||
- inline-функцией в `app` или composition;
|
||||
- частью framework binding;
|
||||
- mutable registry или service locator.
|
||||
|
||||
Concrete adapters в production импортируют только assemblies своего домена. Adapter tests импортируют соответствующий module напрямую.
|
||||
|
||||
## Универсальный infra service
|
||||
|
||||
Adapter может использовать публичный API `infra`, если concrete technical service является универсальным для приложения:
|
||||
|
||||
```text
|
||||
auth adapter
|
||||
→ infra/http-client
|
||||
→ external identity provider
|
||||
```
|
||||
|
||||
Совпадение сигнатур `infra` API и port не переносит ownership port в `infra`. Adapter остаётся явной границей provider mapping, failures и environment. Он может быть тонким, но не добавляет фиктивные преобразования ради объёма кода.
|
||||
|
||||
## Cross-domain API dependency
|
||||
|
||||
Готовый API другого домена не является technical port:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/api'
|
||||
|
||||
export type UserProfileApiDependencies = Readonly<{
|
||||
auth: Pick<AuthSessionApi, 'getSession'>
|
||||
profile: UserProfilePort
|
||||
}>
|
||||
```
|
||||
|
||||
Graph owner создаёт Auth раньше User и передаёт `auth.session` в User assembly. User не объявляет structural copy чужого API и не создаёт bridge adapter без реального преобразования контракта.
|
||||
|
||||
Если expected Auth failure становится публичным outcome User, User API преобразует его в собственную `UserError`. При exception-модели он может использовать публичный guard из `auth/api/runtime`.
|
||||
|
||||
## Framework-only SDK
|
||||
|
||||
Некоторые SDK доступны только как framework Provider, hook или component, например CAPTCHA или payment element. Framework binding может получить opaque token или operation input через такой SDK и передать его команде Domain API:
|
||||
|
||||
```text
|
||||
framework SDK
|
||||
→ opaque token
|
||||
→ Domain API command
|
||||
→ port
|
||||
→ provider adapter
|
||||
```
|
||||
|
||||
Binding не вызывает предметную provider operation напрямую, SDK type не входит в public Domain API, а generic technical UI при необходимости разделяется между `infra`, `ui` и composition.
|
||||
|
||||
## Tests и fake ports
|
||||
|
||||
Локальные fake implementations в API-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри test boundary и позволяют детерминированно задавать records, failures, cancellation и realtime события.
|
||||
|
||||
Adapter contract tests отдельно доказывают, что concrete provider действительно реализует port. API-тест с идеальным fake не заменяет эту проверку.
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# Framework Groups и модули
|
||||
|
||||
> Пояснение domain-specific framework-кода на примере React.
|
||||
> Пояснение domain-specific framework-кода, materialized state и RSC boundaries на примере React.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
|
||||
- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-FRAMEWORK-R014`](../../rules/level-2.md#slm-l2-framework-r014)
|
||||
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
|
||||
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
|
||||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
|
||||
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
|
||||
|
||||
## Framework Group
|
||||
|
||||
@@ -28,44 +29,44 @@ domains/auth/react/ # Framework Group
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`react` является Group, а не модулем. У неё нет `index.ts`, состояния, реализации, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
|
||||
`react` является Group, а не модулем. У неё нет `index.ts`, реализации, state, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
|
||||
|
||||
Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. Он не является вложенным модулем, потому что родительская граница `react` является Group.
|
||||
Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа.
|
||||
|
||||
## Framework binding module
|
||||
|
||||
Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании одного или нескольких готовых Domain API своего домена с конкретным framework. Сам факт зависимости от framework недостаточен: framework-specific assembly остаётся в `assemblies`, а page-specific модуль остаётся в `compositions`.
|
||||
Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании готового Domain API своего домена с конкретным framework.
|
||||
|
||||
Framework binding module может:
|
||||
Framework binding может:
|
||||
|
||||
- передавать готовые API через Provider и context;
|
||||
- передавать готовый API через Provider и context;
|
||||
- предоставлять domain-specific hooks;
|
||||
- отображать состояние и безопасные ошибки домена;
|
||||
- использовать framework-compatible state/query runtime;
|
||||
- хранить framework projection в query cache или store;
|
||||
- отображать public models, outcomes и domain errors;
|
||||
- реализовывать SSR prefetch и client hydration;
|
||||
- реализовывать переиспользуемую domain-specific форму или guard;
|
||||
- связывать framework lifecycle с явными операциями Domain API.
|
||||
- связывать framework lifecycle с явной realtime subscription.
|
||||
|
||||
Он не вызывает business-фабрику или assembly, не выбирает adapters и не создаёт новые предметные сценарии.
|
||||
Он не вызывает `api/factory` или assembly, не выбирает adapters, не импортирует SDK предметного external source и не определяет новые предметные операции.
|
||||
|
||||
Framework binding импортирует типы и deterministic runtime через разные фасеты:
|
||||
Framework binding импортирует consumer types и deterministic runtime через разные фасеты:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthError,
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/business'
|
||||
} from '@/domains/auth/api'
|
||||
|
||||
import {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
} from '@/domains/auth/api/runtime'
|
||||
```
|
||||
|
||||
Импорт `business/factory` запрещён: готовые API передаются модулю извне.
|
||||
Импорты `api/ports`, `api/factory` и `adapters/*` запрещены.
|
||||
|
||||
## Модуль session
|
||||
## Готовый API
|
||||
|
||||
`auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthSessionApi`:
|
||||
`auth/react/session` может владеть Provider для уже созданного `AuthSessionApi`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -91,66 +92,120 @@ export const AuthSessionProvider = ({
|
||||
```ts
|
||||
import {
|
||||
AuthSessionProvider,
|
||||
useAuthSession,
|
||||
useAuthApi,
|
||||
} from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API.
|
||||
|
||||
## State/query runtime
|
||||
## Query и store projection
|
||||
|
||||
`auth/react/queries` может использовать TanStack Query, SWR или другой React runtime поверх готового `AuthSessionApi`. Query cache остаётся framework projection, пока данные и transitions проходят через API, а optimistic values создаются или проверяются business-владельцем.
|
||||
`auth/react/queries` может использовать TanStack Query, SWR, Zustand или другой React runtime поверх готового API:
|
||||
|
||||
```ts
|
||||
export const useAuthSessionQuery = () => {
|
||||
const api = useAuthSession()
|
||||
const api = useAuthApi()
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['auth', 'session'],
|
||||
queryFn: api.getCurrentSession,
|
||||
queryFn: api.getSession,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Server prefetch и client hook могут быть разными модулями Framework Group с совместимыми environment markers. Общая query policy выносится в отдельный framework-модуль при наличии нескольких потребителей, а не дублируется в compositions.
|
||||
Query keys, stale time, pending status и hydration принадлежат binding. Значения и ошибки поступают через Domain API. Framework types не становятся частью `AuthSessionApi`.
|
||||
|
||||
Подробности описаны в [Состоянии и кэше](./state-cache.md).
|
||||
Framework projection не импортируется другим доменом. Cross-domain UI собирается в `compositions`.
|
||||
|
||||
## Модуль login-form
|
||||
## Realtime binding
|
||||
|
||||
`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с Auth API, состоянием и ошибками своего домена. Она может использовать публичный API соседнего `auth/react/session`, если зависимость остаётся ацикличной.
|
||||
Binding может запускать subscription готового API в framework lifecycle:
|
||||
|
||||
Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Domain-owned guard может решить, разрешено ли действие, но политика перехода на `/login` остаётся у route composition.
|
||||
```text
|
||||
component/provider scope
|
||||
→ Domain API subscribe
|
||||
→ verified domain events
|
||||
→ query invalidation or API-owned projection
|
||||
→ cleanup on scope end
|
||||
```
|
||||
|
||||
Binding не импортирует WebSocket client и не разбирает frames. После cleanup он не принимает late callbacks. Если reconnect создаёт gap, binding обрабатывает публичный `RESYNC_REQUIRED` outcome и повторно загружает snapshot через Domain API.
|
||||
|
||||
## Domain-specific UI
|
||||
|
||||
`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с Auth API, public models и errors своего домена. Она может использовать публичный API соседнего `auth/react/session`, если статический граф остаётся ацикличным.
|
||||
|
||||
Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Domain API может вернуть `AUTH_REQUIRED`, но переход на `/login` выбирает composition.
|
||||
|
||||
## Framework-only SDK
|
||||
|
||||
SDK, доступный только через Provider, hook или component, может использоваться binding для получения opaque operation input:
|
||||
|
||||
```text
|
||||
CAPTCHA React component
|
||||
→ opaque token
|
||||
→ AuthApi command
|
||||
```
|
||||
|
||||
Binding не использует SDK для самостоятельной предметной операции, не превращает SDK response в public domain model и не экспортирует SDK type через Domain API. Если SDK предоставляет reusable technical UI без предметной модели, его generic integration может принадлежать `infra` и `ui`, а composition связывает её с доменом.
|
||||
|
||||
## Запрет cross-domain framework imports
|
||||
|
||||
Framework binding module не импортирует hooks, contexts, Providers, components или framework state другого домена:
|
||||
Framework binding module не импортирует hooks, contexts, Providers, stores или components другого домена:
|
||||
|
||||
```ts
|
||||
// Недопустимо: domains/user/react/profile
|
||||
import { useAuthSession } from '@/domains/auth/react/session'
|
||||
import {
|
||||
useAuthSessionQuery,
|
||||
} from '@/domains/auth/react/queries'
|
||||
```
|
||||
|
||||
Cross-domain UI собирается в `compositions`:
|
||||
|
||||
```tsx
|
||||
const session = useAuthSession()
|
||||
const session = useAuthSessionQuery()
|
||||
|
||||
return (
|
||||
<UserProfile
|
||||
userId={session.userId}
|
||||
canEdit={session.isAuthenticated}
|
||||
userId={session.data?.userId}
|
||||
/>
|
||||
)
|
||||
```
|
||||
|
||||
Передача через props является границей композиции, но не требует prop drilling внутри домена: каждый пакет может использовать собственный Provider и context. Если User business постоянно нуждается в Auth, готовый `AuthSessionApi` передаётся User factory при сборке графа, а User framework module работает уже со своим API.
|
||||
Если User Domain API постоянно нуждается в Auth, готовый `AuthSessionApi` передаётся User assembly при сборке runtime-графа. User framework binding работает уже со своим API.
|
||||
|
||||
## SSR, RSC и client boundary
|
||||
|
||||
Server prefetch и client hooks могут принадлежать разным modules Framework Group с совместимыми entry points. Они не разделяют API instance или mutable cache:
|
||||
|
||||
```text
|
||||
server binding
|
||||
→ server API instance
|
||||
→ prefetch
|
||||
→ hydration payload
|
||||
|
||||
client binding
|
||||
→ client API instance
|
||||
→ hydrate
|
||||
→ rendering
|
||||
```
|
||||
|
||||
Server Component не передаёт API object в Client Component. Client reference и Server Action reference объявляются checker-у отдельно от executable imports. Если Client Component участвует в SSR или prerender, его server render graph проверяется отдельно от browser hydration graph; browser-only capability используется только через объявленную framework-deferred boundary.
|
||||
|
||||
## Публичные API
|
||||
|
||||
```ts
|
||||
import { AuthSessionProvider } from '@/domains/auth/react/session'
|
||||
import { LoginForm } from '@/domains/auth/react/login-form'
|
||||
import {
|
||||
AuthSessionProvider,
|
||||
} from '@/domains/auth/react/session'
|
||||
|
||||
import {
|
||||
useAuthSessionQuery,
|
||||
} from '@/domains/auth/react/queries'
|
||||
|
||||
import {
|
||||
LoginForm,
|
||||
} from '@/domains/auth/react/login-form'
|
||||
```
|
||||
|
||||
Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена.
|
||||
|
||||
@@ -7,46 +7,69 @@
|
||||
- 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`.
|
||||
- Корень package содержит только metadata, модуль `api` и допустимые Groups и не имеет executable API.
|
||||
- Модуль `api` является единственным семантическим шлюзом данных и операций домена.
|
||||
- Публичные фасеты разделяют consumer types, implementer ports, factories и optional deterministic runtime.
|
||||
- Каждый Domain API имеет одну factory; production factories импортируют только assemblies своего домена.
|
||||
- Dependency ports принадлежат `api`, а production adapters являются отдельными modules Group `adapters`.
|
||||
- Provider errors проходят через closed port failures и преобразуются в stable domain errors.
|
||||
- Каждый package содержит `assemblies/default` для одного baseline production context.
|
||||
- Имя `default` не определяет environment или isomorphic compatibility.
|
||||
- Дополнительная assembly появляется только для отличающегося graph, dependencies, trust, capabilities или lifecycle.
|
||||
- Framework bindings владеют state, cache, reactivity и hydration и не обращаются к предметному external source в обход Domain API.
|
||||
- Server и client используют разные API instances и caches; через RSC boundary проходят только serializable values.
|
||||
- Realtime transport скрыт adapter, а messages и subscriptions доступны через Domain API.
|
||||
- Realtime port объявляет correlation, ACK, ordering, duplicate, reconnect, resync, cancellation и cleanup semantics.
|
||||
- Assembly rollback выполняет cleanup собственных resources и полученных adapter lifecycle handles; successful aggregate cleanup идемпотентен и прекращает callbacks.
|
||||
- Cross-domain Domain API является отдельной runtime dependency, а не автоматически local port.
|
||||
- Runtime assembly graph остаётся ацикличным.
|
||||
|
||||
## Владение состоянием
|
||||
## Канал ошибок
|
||||
|
||||
Нужно подробнее определить создание initial state, применение transitions, persistence и внешний event input. Нормативным уже является то, что предметную модель и переходы определяет `business`, а concrete state manager реализует business-owned port либо framework projection.
|
||||
Нужно выбрать project-wide recommendation между exceptions и discriminated `Result`, определить форму cancellation и unexpected failures, а также сериализацию domain errors через RPC и Server Actions.
|
||||
|
||||
Отдельно требуется проверить SSR snapshot, hydration, concurrent rendering, reset и поведение после завершения request scope.
|
||||
Архитектурная цепочка provider failure → port failure → domain error от выбора канала не зависит.
|
||||
|
||||
## Передача ошибок
|
||||
## Port semantics
|
||||
|
||||
Нужно выбрать общую рекомендацию exception или discriminated `Result`, определить cancellation и unexpected failures, а также сериализацию ошибок через RPC и server actions.
|
||||
Нужно определить минимальный machine-readable способ объявлять behavioral guarantees ports: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
|
||||
|
||||
Структура публичных фасетов от этого решения больше не зависит: type errors публикуются через `business`, а необходимые runtime codes и guards через опциональный `business/runtime`.
|
||||
Не все ports требуют все поля, но существенная для корректности semantics не должна существовать только в комментарии adapter implementation.
|
||||
|
||||
## Технические порты
|
||||
## Environment metadata
|
||||
|
||||
Нужно определить, является ли consumer-owned port обязательной формой каждой технической зависимости и какие behavioral guarantees он описывает: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
|
||||
Нужно выбрать формат для capability sets, resolver conditions, executable edges, framework reference edges, dynamic imports и API-safe package declarations.
|
||||
|
||||
Cross-domain API dependency уже зафиксирована как отдельный вид зависимости и не зависит от этого решения.
|
||||
Особенно требуется проверить Next.js RSC, Server Actions, edge runtime, workers и conditional exports внешних packages.
|
||||
|
||||
## Lifecycle сборки
|
||||
## Runtime dependency graph
|
||||
|
||||
Уже зафиксировано, что явная операция, запускающая ресурс, возвращает cleanup, а assembly с собственным ресурсом предоставляет cleanup handle результата. Ещё нужно определить async cleanup, rollback частичной сборки, repeated disposal, request abort и поведение API после завершения scope.
|
||||
Нужно выбрать machine-readable формат assembly inputs и создаваемых API, чтобы автоматически обнаруживать runtime cycles, скрытые static structural ports и неверный cleanup order.
|
||||
|
||||
## Cache hydration
|
||||
До появления формата runtime graph остаётся обязательной review boundary.
|
||||
|
||||
Нужно проверить единый способ разделять framework-neutral cache policy, client hooks, server prefetch и serialization boundary без утечки query-library types в Domain API.
|
||||
## Lifecycle
|
||||
|
||||
## Автоматическая проверка
|
||||
Гарантии rollback, reverse cleanup, idempotence и отсутствия callbacks после disposal зафиксированы. Ещё нужно определить aggregate cleanup errors, retry failed cleanup, request abort, deadline disposal и поведение API после завершения scope.
|
||||
|
||||
Нужно выбрать machine-readable формат для форм домена, metadata, модулей, Groups, entry points, environment labels и запрещённых транзитивных импортов.
|
||||
## Hydration payload
|
||||
|
||||
Нужно выбрать рекомендации по versioning, schema validation, stale persisted cache, partial hydration и защите request-specific или sensitive values.
|
||||
|
||||
Hydration payload остаётся framework-owned и не может содержать API instance или mutable client.
|
||||
|
||||
## Multiple APIs и shared capabilities
|
||||
|
||||
Нужно проверить рекомендуемую форму для нескольких Domain API, которые используют один shared connection, transaction coordinator или framework-neutral operation context, не перенося предметную семантику в adapter или assembly.
|
||||
|
||||
Если independent factories не сохраняют atomicity, APIs должны объединяться; точный критерий требует дополнительных примеров.
|
||||
|
||||
## Framework-only SDK
|
||||
|
||||
Нужно проверить React/Vue SDK, которые предоставляют capability только через Provider, hook или component: payment elements, CAPTCHA, maps и identity widgets.
|
||||
|
||||
Зафиксировано, что binding может передать Domain API только opaque operation input и не выполняет предметную provider operation напрямую. Требуются проверочные примеры для `infra` + `ui` + composition.
|
||||
|
||||
## Масштаб production graph
|
||||
|
||||
Нужно проверить lazy и route-scoped сборку на SLM root с десятками Level 2 packages. Импорт assemblies остаётся side-effect-free, а graph owner создаёт только dependency-connected часть graph; конкретный registry или lazy-loading mechanism пока не нормирован.
|
||||
|
||||
217
skills/slm-design/reference/draft/level-2/domains/realtime.md
Normal file
217
skills/slm-design/reference/draft/level-2/domains/realtime.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Realtime messages и subscriptions
|
||||
|
||||
> Пояснение Domain API поверх WebSocket, SSE, GraphQL subscriptions и provider SDK.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
|
||||
- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009)
|
||||
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
|
||||
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
|
||||
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
|
||||
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
|
||||
- [`SLM-L2-REALTIME-R029`](../../rules/level-2.md#slm-l2-realtime-r029)
|
||||
|
||||
## Граница транспорта
|
||||
|
||||
Realtime transport находится внутри adapter:
|
||||
|
||||
```text
|
||||
WebSocket / SSE / GraphQL / SDK
|
||||
→ adapter
|
||||
→ realtime port
|
||||
→ Domain API
|
||||
→ domain event/outcome/error
|
||||
→ framework projection
|
||||
```
|
||||
|
||||
Domain API не экспортирует `WebSocket`, `MessageEvent`, raw frames, SDK subscription, provider error или transport close code. Port также не должен быть generic socket API с `send(frame)` и `onMessage(frame)`: он описывает capability, необходимую конкретному домену.
|
||||
|
||||
## Realtime-команда
|
||||
|
||||
Публичная команда может выглядеть как обычный Promise независимо от транспорта:
|
||||
|
||||
```ts
|
||||
export type ChatApi = {
|
||||
sendMessage: (
|
||||
command: SendMessageCommand,
|
||||
) => Promise<ChatMessage>
|
||||
}
|
||||
```
|
||||
|
||||
Port возвращает типизированный technical outcome:
|
||||
|
||||
```ts
|
||||
export type SendMessagePortFailure =
|
||||
| Readonly<{ type: 'FORBIDDEN' }>
|
||||
| Readonly<{ type: 'RATE_LIMITED' }>
|
||||
| Readonly<{ type: 'UNAVAILABLE' }>
|
||||
| Readonly<{ type: 'OUTCOME_UNKNOWN' }>
|
||||
|
||||
export type ChatRealtimePort = {
|
||||
sendMessage: (
|
||||
command: SendMessagePortCommand,
|
||||
) => Promise<PortResult<ChatMessageRecord, SendMessagePortFailure>>
|
||||
}
|
||||
```
|
||||
|
||||
Domain API преобразует port result в `ChatMessage` или собственную `ChatError`. Для прикладного consumer transport остаётся незаметным.
|
||||
|
||||
## Correlation
|
||||
|
||||
`socket.send()` подтверждает только локальную отправку frame. Чтобы завершить `sendMessage()` результатом server command, protocol должен сопоставить command и acknowledgement:
|
||||
|
||||
```text
|
||||
Domain API command
|
||||
→ adapter assigns operationId
|
||||
→ transport frame
|
||||
→ server ACK or ERROR with operationId
|
||||
→ adapter settles pending port operation
|
||||
→ Domain API maps outcome
|
||||
```
|
||||
|
||||
Adapter владеет protocol registry pending operations и не бросает error из async `onmessage`, который невозможно поймать вокруг исходного `send`. Он завершает соответствующую Promise или другой объявленный operation channel.
|
||||
|
||||
Correlation contract фиксирует:
|
||||
|
||||
- источник и scope уникальности operation ID;
|
||||
- момент, когда команда считается принятой или выполненной;
|
||||
- поведение при duplicate и late acknowledgement;
|
||||
- timeout и cancellation;
|
||||
- очистку pending operation при disconnect;
|
||||
- связь command outcome с последующими domain events.
|
||||
|
||||
Если provider не возвращает correlation metadata, API не обещает индивидуальный результат. Такая операция является fire-and-forget, а поздний отказ публикуется отдельным domain event либо доступна только общая ошибка transport scope.
|
||||
|
||||
## Outcome uncertainty и idempotency
|
||||
|
||||
Disconnect после отправки и до acknowledgement не доказывает, что command не выполнена:
|
||||
|
||||
```text
|
||||
frame sent
|
||||
→ connection lost
|
||||
→ server may have committed command
|
||||
→ acknowledgement unknown
|
||||
```
|
||||
|
||||
Port возвращает `OUTCOME_UNKNOWN`, если это различие нужно Domain API. Автоматический retry безопасен только при provider guarantee или idempotency key. Domain API не преобразует неопределённый outcome в ложное `MESSAGE_NOT_SENT`.
|
||||
|
||||
## Subscription
|
||||
|
||||
Публичная subscription предоставляет проверенные events и явный cleanup:
|
||||
|
||||
```ts
|
||||
export type ChatEvent =
|
||||
| Readonly<{
|
||||
type: 'MESSAGE_CREATED'
|
||||
message: ChatMessage
|
||||
revision: number
|
||||
}>
|
||||
| Readonly<{
|
||||
type: 'MESSAGE_REMOVED'
|
||||
messageId: string
|
||||
revision: number
|
||||
}>
|
||||
|
||||
export type ChatSubscription = Readonly<{
|
||||
close: () => Promise<void>
|
||||
}>
|
||||
|
||||
export type ChatObserver = Readonly<{
|
||||
onEvent: (event: ChatEvent) => void
|
||||
onError: (error: ChatRealtimeError) => void
|
||||
onStatus: (status: ChatRealtimeStatus) => void
|
||||
}>
|
||||
|
||||
export type ChatApi = {
|
||||
subscribe: (
|
||||
observer: ChatObserver,
|
||||
) => Promise<ChatSubscription>
|
||||
}
|
||||
```
|
||||
|
||||
Callback, async iterable или другой project-wide channel допустимы. Обязательны типизированные domain events/errors, определённый lifecycle и cleanup.
|
||||
|
||||
## Stable errors и statuses
|
||||
|
||||
Начальная ошибка подключения может завершить `subscribe()` domain error. Ошибка после успешного запуска приходит через stream channel.
|
||||
|
||||
Не каждый transport failure становится domain error. Adapter может восстановить соединение и опубликовать только устойчивый status:
|
||||
|
||||
```ts
|
||||
export type ChatRealtimeStatus =
|
||||
| Readonly<{ type: 'CONNECTED' }>
|
||||
| Readonly<{ type: 'RECONNECTING' }>
|
||||
| Readonly<{ type: 'RESYNC_REQUIRED' }>
|
||||
| Readonly<{ type: 'CLOSED' }>
|
||||
```
|
||||
|
||||
Публичные errors описывают реакции приложения, например `CHAT_REALTIME_UNAVAILABLE`, `CHAT_FORBIDDEN` или `CHAT_SESSION_EXPIRED`. Close codes, provider messages и SDK classes остаются внутри adapter.
|
||||
|
||||
Caller-initiated close не является domain error.
|
||||
|
||||
## Ordering, duplicates и resync
|
||||
|
||||
Realtime port явно объявляет:
|
||||
|
||||
- гарантируется ли порядок событий;
|
||||
- возможна ли at-least-once delivery;
|
||||
- кто устраняет duplicates;
|
||||
- содержит ли event revision или sequence;
|
||||
- как обнаруживается gap после reconnect;
|
||||
- откуда загружается authoritative snapshot.
|
||||
|
||||
Если adapter не может доказать непрерывность, Domain API публикует `RESYNC_REQUIRED`. Framework binding invalidates projection и получает snapshot через query Domain API.
|
||||
|
||||
Binding не применяет raw delta к публичной модели. Если безопасный merge содержит предметную семантику, его выполняет операция Domain API или pure-функция `api/runtime`.
|
||||
|
||||
## Shared connection
|
||||
|
||||
Один adapter может multiplex несколько ports и subscriptions через физическое соединение. Connection имеет явные owner, scope, multiplicity и cleanup:
|
||||
|
||||
```text
|
||||
assembly-owned connection
|
||||
├── chat messages port
|
||||
├── presence port
|
||||
└── notification port
|
||||
```
|
||||
|
||||
Cleanup отдельной subscription снимает её lease. Cleanup assembly закрывает shared connection после завершения всех принадлежащих графу operations. После awaited cleanup новые callbacks запрещены.
|
||||
|
||||
Если создание connection завершилось успешно, а следующий шаг assembly упал, connection закрывается на rollback path до возврата ошибки.
|
||||
|
||||
## Framework materialization
|
||||
|
||||
Framework binding выбирает техническую реакцию на domain event:
|
||||
|
||||
```text
|
||||
MESSAGE_CREATED
|
||||
→ update query cache verified full model
|
||||
|
||||
RESYNC_REQUIRED
|
||||
→ invalidate query
|
||||
→ fetch snapshot through Domain API
|
||||
```
|
||||
|
||||
Zustand, QueryClient, Pinia или другой store не импортирует socket adapter и не интерпретирует protocol frame. Он хранит только public values, events, statuses и errors Domain API.
|
||||
|
||||
## SSR, RSC и workers
|
||||
|
||||
Browser assembly может включать realtime adapter, а request/RSC assembly — только query API. Отсутствующий realtime API не заменяется throwing stub.
|
||||
|
||||
Server process или worker получает отдельную assembly и scope, если ему действительно нужна долгоживущая subscription. Server Component не открывает connection, которая переживает request, без отдельного owner вне request scope.
|
||||
|
||||
## Тестовые границы
|
||||
|
||||
API-тест с fake realtime port проверяет mapping records, failures, stable errors и public events. Adapter contract test проверяет protocol frames, correlation, timeout, disconnect, duplicate acknowledgement, reconnect, resync и cleanup. Framework test проверяет materialization и invalidation. Assembly test проверяет shared connection, rollback и отсутствие callbacks после disposal.
|
||||
|
||||
Контрольные случаи:
|
||||
|
||||
- acknowledgement приходит после timeout;
|
||||
- duplicate acknowledgement приходит после reconnect;
|
||||
- event приходит раньше command acknowledgement;
|
||||
- disconnect происходит после send и до ACK;
|
||||
- unsubscribe завершается во время pending callback;
|
||||
- adapter получает malformed payload;
|
||||
- следующий resource assembly падает после открытия connection.
|
||||
@@ -1,114 +1,172 @@
|
||||
# Состояние и кэш
|
||||
# Состояние, cache и hydration
|
||||
|
||||
> Пояснение границы между предметной властью business и техническими state/query runtimes.
|
||||
> Пояснение границы между семантической властью Domain API и framework-owned materialization.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`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-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
|
||||
- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-a007)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
|
||||
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
|
||||
- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018)
|
||||
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
|
||||
|
||||
## Библиотеки не запрещены
|
||||
## Основная граница
|
||||
|
||||
Запрет state/query manager в import-графе `business` не запрещает TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX или RxJS в доменном пакете. Он запрещает concrete runtime становиться предметным контрактом.
|
||||
Модуль `api` определяет форму и семантику доменных значений, но не выбирает способ их хранения и реактивной доставки. TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX, Pinia, Signals и RxJS остаются в framework bindings или compositions.
|
||||
|
||||
Такая библиотека может находиться:
|
||||
|
||||
- в 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
|
||||
}
|
||||
}
|
||||
```text
|
||||
Domain API
|
||||
→ public model/outcome/event
|
||||
→ framework projection
|
||||
→ rendering
|
||||
```
|
||||
|
||||
Adapter поверх Zustand или другого manager реализует этот контракт, но не выбирает initial state и не добавляет переходы.
|
||||
Concrete state/query runtime не импортируется модулем `api`, не является dependency port фабрики и не входит в публичный Domain API.
|
||||
|
||||
### Technical source cache
|
||||
## Виды materialization
|
||||
|
||||
Кэш SDK, HTTP, GraphQL или storage-вызовов. Adapter может использовать QueryClient, Apollo cache или иной runtime для deduplication, transport retry и хранения внешних результатов. Business по-прежнему проверяет и преобразует результат до публикации предметной модели.
|
||||
### Source cache
|
||||
|
||||
Query keys и библиотечные result types остаются внутри adapter. Domain API не экспортирует `UseQueryResult`, `ApolloError`, raw DTO или mutable QueryClient.
|
||||
Технический cache внешнего provider внутри adapter. Он может отвечать за transport deduplication, connection state, provider retry и хранение port records.
|
||||
|
||||
Если technical cache создаёт timers, subscriptions или другой lifecycle-ресурс для всего графа, владеющая им assembly передаёт cleanup graph owner. Cache без такого ресурса не требует искусственного `dispose`.
|
||||
Source cache не публикует raw DTO, query keys, mutable client или library result через Domain API. Если adapter создаёт timers, subscriptions или connection, он остаётся единственным владельцем и экспортирует lifecycle handle, который assembly только агрегирует. Если resource создаёт assembly, adapter использует его как borrowed capability и не закрывает самостоятельно.
|
||||
|
||||
### Framework projection cache
|
||||
### Framework projection
|
||||
|
||||
Кэш, который framework binding строит поверх вызовов готового Domain API для rendering, Suspense, revalidation или UI coordination.
|
||||
State или cache, который framework binding строит из готового Domain API:
|
||||
|
||||
```ts
|
||||
const useProfile = () => {
|
||||
const api = useUserApi()
|
||||
export const useAuthSession = () => {
|
||||
const api = useAuthApi()
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['user', 'profile'],
|
||||
queryFn: api.getProfile,
|
||||
queryKey: ['auth', 'session'],
|
||||
queryFn: api.getSession,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Такой cache не является параллельным предметным источником, пока его значения происходят из Domain API и все предметные изменения выполняются через API.
|
||||
Query key, stale time, pending/retry status, Suspense, rendering stale data и техническая invalidation принадлежат binding. `AuthSession` и `AuthError` принадлежат `api`.
|
||||
|
||||
### Composition state
|
||||
|
||||
Состояние конкретной страницы или multi-domain flow принадлежит composition: выбранная вкладка, открытый modal, draft формы, route transition и координация нескольких API.
|
||||
|
||||
Если draft приобретает самостоятельную доменную семантику, Domain API предоставляет validation или transition, но framework по-прежнему хранит возвращаемое readonly value.
|
||||
|
||||
## Domain API не является store
|
||||
|
||||
Публичный Domain API не экспортирует:
|
||||
|
||||
- mutable store;
|
||||
- `getState` и `setState` framework runtime;
|
||||
- QueryClient;
|
||||
- Zustand `StoreApi`;
|
||||
- framework hook;
|
||||
- глобальный singleton данных;
|
||||
- универсальный state port.
|
||||
|
||||
API methods возвращают значения и outcomes. Framework consumer решает, как долго их хранить и когда повторно запросить.
|
||||
|
||||
Это не означает, что framework определяет предметные transitions. Он материализует только то, что произвёл или проверил API.
|
||||
|
||||
## Invalidation и retry
|
||||
|
||||
Не каждая cache policy является бизнес-правилом.
|
||||
|
||||
| Политика | Обычный владелец |
|
||||
|---|---|
|
||||
| Query key, stale time, deduplication, background refetch | Adapter или framework binding |
|
||||
| Rendering stale data, Suspense, polling UI | Framework binding или composition |
|
||||
| Query key, stale time, deduplication, background refetch | Framework binding |
|
||||
| Transport retry безопасного запроса | Adapter |
|
||||
| Запрет повторной предметной команды | `business` |
|
||||
| Cooldown, лимит попыток, допустимый transition | `business` |
|
||||
| Freshness, влияющая на корректность сценария | `business` через явный контракт |
|
||||
| Rendering stale data, Suspense, polling UI | Framework binding или composition |
|
||||
| Запрет повторной предметной команды | Domain API |
|
||||
| Cooldown, лимит попыток, допустимый transition | Domain API |
|
||||
| Freshness, влияющая на корректность сценария | Domain API через operation contract |
|
||||
|
||||
Если после команды требуется invalidation, framework binding может связать успешный результат API с техническими keys. Если выбор invalidation выражает предметную семантику, business возвращает устойчивый outcome/event либо публикует чистое правило через `business/runtime`; binding только отображает его на библиотечные операции.
|
||||
После успешной команды binding может технически invalidировать известные query keys. Если выбор invalidation выражает предметную семантику, Domain API возвращает устойчивый outcome/event, а binding только отображает его на framework cache.
|
||||
|
||||
## Optimistic updates
|
||||
|
||||
Framework binding не конструирует произвольную предметную модель из input формы или transport DTO. Optimistic update допустим, когда предполагаемое значение:
|
||||
Framework binding не конструирует произвольную публичную модель из form input, raw DTO или текущего cache. Optimistic projection допустима, когда предполагаемое значение:
|
||||
|
||||
- возвращено командой Domain API как безопасная projection;
|
||||
- создано отдельным pure-методом Domain API;
|
||||
- создано или проверено публичной функцией `business/runtime`.
|
||||
- возвращено командой Domain API;
|
||||
- создано отдельной операцией Domain API;
|
||||
- создано или проверено pure-функцией `api/runtime`.
|
||||
|
||||
```ts
|
||||
const optimisticProfile = projectProfileUpdate(currentProfile, command)
|
||||
import {
|
||||
projectProfileUpdate,
|
||||
} from '@/domains/user/api/runtime'
|
||||
|
||||
const optimisticProfile = projectProfileUpdate(
|
||||
currentProfile,
|
||||
command,
|
||||
)
|
||||
|
||||
queryClient.setQueryData(profileKey, optimisticProfile)
|
||||
```
|
||||
|
||||
Здесь `projectProfileUpdate` принадлежит `business/runtime`, а `setQueryData` остаётся технической операцией framework binding.
|
||||
`projectProfileUpdate` владеет предметным transition, а `setQueryData` остаётся framework operation.
|
||||
|
||||
Запись raw form data или DTO напрямую в публичный product cache обходит предметного владельца и нарушает границу.
|
||||
## Concurrent mutations и realtime
|
||||
|
||||
## Browser, SSR и RSC
|
||||
При нескольких optimistic commands и realtime events binding не выбирает самостоятельно ordering, versioning, rollback или rebase. Domain API возвращает correlation/version metadata либо предоставляет deterministic reconciliation:
|
||||
|
||||
Client hook, server prefetch и hydrate/dehydrate могут принадлежать разным framework binding modules с совместимыми environment entry points. Общая framework-specific cache policy выносится в отдельный модуль той же Framework Group, если у неё несколько реальных потребителей.
|
||||
```ts
|
||||
const nextProjection = reconcileProfile({
|
||||
current,
|
||||
event,
|
||||
pendingCommands,
|
||||
})
|
||||
```
|
||||
|
||||
`compositions` вызывает публичный server binding для prefetch и публичный client binding для rendering. Она не повторяет query keys и mapping доменных результатов.
|
||||
Если API не объявляет безопасный merge, binding invalidates cache и получает authoritative snapshot через Domain API. Это предпочтительнее скрытого применения неполного delta.
|
||||
|
||||
## Persistence
|
||||
|
||||
Framework cache может технически сохраняться между reloads, но persisted value не становится источником предметной истины. После восстановления значение:
|
||||
|
||||
- используется как stale projection до revalidation;
|
||||
- либо проверяется публичным validator `api/runtime`;
|
||||
- либо отбрасывается и загружается через Domain API.
|
||||
|
||||
Если storage является самостоятельным предметным внешним источником, доступ к нему оформляется dependency port и adapter. Автоматический framework middleware не обходит API validation и transitions.
|
||||
|
||||
## SSR и hydration
|
||||
|
||||
Server и client имеют разные API instances и caches:
|
||||
|
||||
```text
|
||||
server request
|
||||
→ request assembly
|
||||
→ server Domain API
|
||||
→ server framework cache
|
||||
→ serializable hydration payload
|
||||
|
||||
browser
|
||||
→ client assembly
|
||||
→ client Domain API
|
||||
→ hydrated client cache
|
||||
```
|
||||
|
||||
Hydration payload принадлежит framework binding и содержит только public domain values и framework metadata. API object, functions, ports, adapters, mutable clients и request secrets не сериализуются.
|
||||
|
||||
Server cache создаётся на каждый request и не хранится в module singleton. Client cache создаётся на согласованный application или route scope.
|
||||
|
||||
## RSC и Server Actions
|
||||
|
||||
Server Component вызывает server Domain API и передаёт Client Component только сериализуемые values или hydration payload. Client Component создаёт или получает отдельный client API instance; при SSR его render отдельно проверяется в server prerender graph до browser hydration.
|
||||
|
||||
Server Action создаёт request-scoped production graph на каждый вызов, выполняет Domain API command и гарантированно выполняет все cleanup obligations графа. Client invocation Server Action является framework reference edge, а не передачей server API в browser.
|
||||
|
||||
## Проверка на ревью
|
||||
|
||||
Для каждого state/query runtime определяется:
|
||||
|
||||
- является ли он adapter, framework projection или локальным UI state;
|
||||
- откуда поступают значения;
|
||||
- кто определяет transition и optimistic projection;
|
||||
- является ли он source cache, framework projection или composition state;
|
||||
- откуда поступают public domain values;
|
||||
- кто определяет validation и transition;
|
||||
- где находятся library-specific types и keys;
|
||||
- как invalidation соотносится с результатами Domain API;
|
||||
- соответствует ли cache lifecycle области жизни API и framework scope.
|
||||
- как invalidation связана с Domain API outcomes;
|
||||
- как обрабатываются optimistic concurrency и realtime events;
|
||||
- что сериализуется при SSR/RSC;
|
||||
- соответствует ли cache scope области жизни API graph.
|
||||
|
||||
@@ -1,91 +1,189 @@
|
||||
# Тестирование доменного пакета
|
||||
|
||||
> Проверка владельцев и публичных границ Level 2.
|
||||
> Проверка Domain API, port contracts, production wiring и framework projections 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-API-A019`](../../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
|
||||
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||||
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
|
||||
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
|
||||
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
|
||||
- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024)
|
||||
- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027)
|
||||
- [`SLM-L2-REALTIME-R029`](../../rules/level-2.md#slm-l2-realtime-r029)
|
||||
- [`SLM-L2-ASSEMBLY-R030`](../../rules/level-2.md#slm-l2-assembly-r030)
|
||||
|
||||
## Размещение
|
||||
|
||||
Тест находится рядом с модулем-владельцем проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`.
|
||||
Тест находится рядом с module-owner проверяемой ответственности. У доменного пакета нет общей корневой папки `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` или другое место сборки |
|
||||
| Domain API operations, models, outcomes и errors | `api` через factory |
|
||||
| Deterministic runtime и guards | `api` |
|
||||
| Реализация dependency port | Adapter |
|
||||
| Default и специальный production graph | Assembly |
|
||||
| Provider, hook, query/store integration и hydration | Framework binding |
|
||||
| Cross-domain graph | Graph owner |
|
||||
|
||||
## Business через фабрику
|
||||
## Domain API через фабрику
|
||||
|
||||
Каждый публичный сценарий проверяется через фабрику владеющего им API с управляемыми dependencies:
|
||||
Каждый публичный сценарий проверяется через фабрику владеющего им Domain API с управляемыми fake ports:
|
||||
|
||||
```ts
|
||||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||||
import { authSessionFactory } from '@/domains/auth/business/factory'
|
||||
import type {
|
||||
AuthSessionApi,
|
||||
} from '@/domains/auth/api'
|
||||
|
||||
import type {
|
||||
AuthIdentityPort,
|
||||
} from '@/domains/auth/api/ports'
|
||||
|
||||
import {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from '@/domains/auth/business/runtime'
|
||||
createAuthSessionApi,
|
||||
} from '@/domains/auth/api/factory'
|
||||
|
||||
const api: AuthSessionApi = authSessionFactory(createAuthSessionTestDeps({
|
||||
clock: { now: () => 1_700_000_000_000 },
|
||||
phone: { requestCode: async () => ({ ok: true }) },
|
||||
}))
|
||||
const identity: AuthIdentityPort = createIdentityPortFake({
|
||||
signIn: {
|
||||
ok: true,
|
||||
value: {
|
||||
expiresAt: 1_700_000_000_000,
|
||||
subject: 'user-1',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await api.requestPhoneOtp('+79991112233')
|
||||
const api: AuthSessionApi = createAuthSessionApi({
|
||||
identity,
|
||||
runtime: {
|
||||
createId: () => 'id-1',
|
||||
now: () => 1_700_000_000_000,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от `throw` или `Result`, но наружу проверяется только собственный AuthErrorCode.
|
||||
API suite проверяет:
|
||||
|
||||
Business-тест не использует React, реальный SDK, database, production assembly или системные clock/random. Локальная fake implementation допустима в тесте и не становится adapter-модулем, потому что не входит в production graph.
|
||||
- public models и outcomes;
|
||||
- validation commands и port records;
|
||||
- mapping каждого expected port failure;
|
||||
- отсутствие raw provider details в domain errors;
|
||||
- cancellation и outcome uncertainty при наличии;
|
||||
- pure transitions и reconciliation;
|
||||
- каждый API отдельно при нескольких factories.
|
||||
|
||||
Если `business` объявляет несколько API, каждый тестируется через свою фабрику. Общая внутренняя pure-логика не требует повторять одинаковые cases на уровне всех API.
|
||||
API-тест не использует React, production assembly, реальный SDK, backend, system clock или module singleton.
|
||||
|
||||
## Остальные модули
|
||||
## Adapter contract test
|
||||
|
||||
Тест adapter-модуля проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки.
|
||||
Adapter test доказывает, что concrete provider реализует port:
|
||||
|
||||
Тест обязательной assembly проверяет:
|
||||
- правильно преобразует arguments;
|
||||
- валидно читает provider record;
|
||||
- возвращает port record, а не raw DTO;
|
||||
- различает закрытые port failures;
|
||||
- не создаёт public domain error;
|
||||
- соблюдает cancellation, timeout и lifecycle contract;
|
||||
- использует заявленный environment capability set.
|
||||
|
||||
- вызов только нужных business-фабрик;
|
||||
- точный именованный состав возвращённого графа;
|
||||
- выбор публичных adapter-модулей;
|
||||
- отсутствие несовместимого environment-кода;
|
||||
- передачу cross-domain API аргументом, а не импортом;
|
||||
- cleanup handle, если assembly создаёт ресурс жизненного цикла.
|
||||
Fake port в API-тесте не заменяет adapter contract test. Идеальный fake может соответствовать типу, пока реальный endpoint или SDK уже изменился.
|
||||
|
||||
Assembly без собственного lifecycle-ресурса не тестирует пустой `dispose`, потому что не обязана его предоставлять.
|
||||
## Default assembly
|
||||
|
||||
Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthSessionApi` и проверяет Provider, hook или component. Query binding дополнительно проверяет keys, invalidation и отсутствие raw DTO в публичном результате, но не повторяет полный набор business-сценариев.
|
||||
Тест `assemblies/default` проверяет:
|
||||
|
||||
- вызов только нужных API factories;
|
||||
- выбор штатных adapter modules;
|
||||
- точный именованный состав graph;
|
||||
- объявленный baseline capability set;
|
||||
- отсутствие module-import side effects;
|
||||
- передачу cross-domain API аргументом;
|
||||
- отсутствие factory/adapter leakage наружу;
|
||||
- aggregate cleanup, если assembly создаёт owned resource или получает adapter lifecycle handle.
|
||||
|
||||
Каждая дополнительная assembly тестирует отличие своего production context, а не повторяет полный API suite.
|
||||
|
||||
## Partial construction и cleanup
|
||||
|
||||
Assembly test моделирует ошибку после регистрации каждого cleanup obligation, включая adapter-owned handle:
|
||||
|
||||
```text
|
||||
resource A created
|
||||
resource B creation failed
|
||||
→ cleanup A awaited
|
||||
→ original failure propagated
|
||||
```
|
||||
|
||||
Проверяются reverse dependency order, idempotent repeated disposal, попытка очистить все resources и отсутствие callbacks после завершившегося cleanup.
|
||||
|
||||
Assembly без cleanup obligations не тестирует пустой `dispose`, потому что не обязана его предоставлять. Если adapter передал lifecycle handle, obligation существует независимо от resource ownership.
|
||||
|
||||
## Framework binding
|
||||
|
||||
Framework test получает fake готового Domain API и проверяет собственную responsibility:
|
||||
|
||||
- Provider и hook;
|
||||
- query keys, stale policy и invalidation;
|
||||
- store projection;
|
||||
- optimistic update через API-owned function;
|
||||
- hydration payload;
|
||||
- public domain errors;
|
||||
- subscription cleanup;
|
||||
- отсутствие direct SDK/external source access.
|
||||
|
||||
Framework test не повторяет validation и failure mapping всех API operations.
|
||||
|
||||
## Realtime
|
||||
|
||||
API realtime test с fake port проверяет public events, stable errors, acknowledgement semantics и `OUTCOME_UNKNOWN` mapping.
|
||||
|
||||
Adapter realtime contract test проверяет:
|
||||
|
||||
- command correlation;
|
||||
- duplicate и late acknowledgement;
|
||||
- disconnect до ACK;
|
||||
- ordering и sequence gaps;
|
||||
- reconnect и resync;
|
||||
- malformed frames;
|
||||
- cancellation и unsubscribe;
|
||||
- отсутствие callbacks после cleanup.
|
||||
|
||||
Assembly test отдельно проверяет shared connection, multiplexing, rollback и graph-level disposal. Framework test проверяет только materialization events и invalidation.
|
||||
|
||||
## SSR, RSC и Server Actions
|
||||
|
||||
Environment tests подтверждают:
|
||||
|
||||
- request-scoped API и cache не разделяются между users;
|
||||
- API instance не входит в hydration payload;
|
||||
- Client Component создаёт отдельный client graph;
|
||||
- SSR-enabled Client Component проверяется в server prerender и browser hydration graphs;
|
||||
- browser-only effect не выполняется во время server render;
|
||||
- Server Action создаёт и очищает graph на каждый вызов;
|
||||
- framework reference edge не превращается в executable client/server leak;
|
||||
- `default` проверяется под всеми объявленными resolver conditions.
|
||||
|
||||
## Cross-domain graph
|
||||
|
||||
Graph owner test создаёт assemblies и construction points модулей Level 1 в dependency order и проверяет runtime inputs и callbacks. Отдельно проверяется невозможность mixed L1/L2 циклической сборки и reverse cleanup order.
|
||||
|
||||
Не достаточно проверить только статический import DAG: runtime dependencies, передаваемые arguments, должны быть представлены architecture mapping или review evidence.
|
||||
|
||||
## Автоматические структурные проверки
|
||||
|
||||
Проверка файлов, exports и import-графа подтверждает:
|
||||
Import и export checks подтверждают:
|
||||
|
||||
- отсутствие 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 циклов.
|
||||
- отсутствие root API пакета и Groups;
|
||||
- обязательные `api`, `api/factory` и `assemblies/default`;
|
||||
- `api/ports` только при наличии declared ports;
|
||||
- допустимые exports каждого фасета;
|
||||
- importer matrix factories, ports и concrete adapters;
|
||||
- отсутствие deep imports;
|
||||
- отсутствие SDK, framework и state/query runtime в graph `api`;
|
||||
- отсутствие запрещённых cross-domain imports;
|
||||
- environment compatibility под configured conditions;
|
||||
- отсутствие статических cycles.
|
||||
|
||||
## Архитектурное ревью
|
||||
|
||||
На ревью проверяется, что `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-тест не заменяет автоматическую проверку или архитектурное ревью.
|
||||
Runtime tests не заменяют import-graph checks и architecture review.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
|
||||
|
||||
Level 2 наследует терминологию и матрицу слоёв Level 1. Он применяется отдельно к предметным областям, которым нужна пакетная форма, и не требует переводить остальные доменные модули того же SLM root.
|
||||
Level 2 наследует терминологию и матрицу слоёв Level 1. Он применяется отдельно к предметным областям, которым нужна пакетная форма с контролируемым Domain API, dependency ports, production adapters, штатной assembly и самостоятельными framework bindings.
|
||||
|
||||
## Формы домена
|
||||
|
||||
@@ -12,11 +12,11 @@ Level 2 наследует терминологию и матрицу слоёв
|
||||
|
||||
### Доменный пакет
|
||||
|
||||
Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла графа зависимостей.
|
||||
Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла статического графа зависимостей.
|
||||
|
||||
Корень пакета может содержать только декларативную metadata, обязательный модуль `business` и допустимые Groups. Metadata хранит статические данные о пакете, владении либо конфигурации проверки и не содержит кода, выполняемого приложением.
|
||||
Корень пакета может содержать только декларативную metadata, обязательный модуль `api` и допустимые Groups. Metadata хранит статические данные о пакете, владении, environment capability sets и конфигурации проверки и не содержит кода, выполняемого приложением.
|
||||
|
||||
Доменный пакет является policy boundary: проверка использует объявленную принадлежность модулей пакету для контроля структуры и междоменных связей. Публичными dependency boundaries кода остаются модули внутри пакета, а не пакет целиком.
|
||||
Доменный пакет является policy boundary: проверка использует объявленную принадлежность модулей пакету для контроля структуры, внешних источников и междоменных связей. Публичными dependency boundaries кода остаются модули внутри пакета, а не пакет целиком.
|
||||
|
||||
### Навигационная Group слоя `domains`
|
||||
|
||||
@@ -24,87 +24,146 @@ Group, размещённая непосредственно в слое `domain
|
||||
|
||||
### Модуль доменного пакета
|
||||
|
||||
Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: business, assembly, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел графа зависимостей.
|
||||
Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: Domain API, assembly, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел статического графа зависимостей.
|
||||
|
||||
Модуль внутри Group пакета не является вложенным модулем, потому что его ближайшая внешняя граница не является модулем.
|
||||
|
||||
## Business
|
||||
## Доменный API
|
||||
|
||||
### Модуль business
|
||||
### Модуль `api`
|
||||
|
||||
Обязательный SLM-модуль `business`, который определяет публичные предметные сценарии, один или несколько именованных Domain API, соответствующие им фабрики, типы зависимостей и публичные контракты ожидаемых ошибок.
|
||||
Обязательный SLM-модуль `api`, который является семантическим шлюзом предметной области для приложения. Он объявляет публичные модели, один или несколько именованных Domain API, соответствующие фабрики, dependency ports, ожидаемые доменные ошибки и необходимый внешним потребителям детерминированный runtime.
|
||||
|
||||
`business` является предметным владельцем данных, моделей, переходов и результатов сценариев домена. Он не зависит от конкретного фреймворка, среды или технической реализации.
|
||||
Модуль `api` определяет смысл данных и операций, но не является framework store или query cache. Он не импортирует SDK, transport client, storage implementation, framework, state/query manager или platform I/O. Его экземпляры замыкают переданные ports и могут координировать отдельную операцию, но не служат скрытым изменяемым источником данных приложения между вызовами.
|
||||
|
||||
### Публичные фасеты 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 только из-за совместимости с несколькими средами.
|
||||
Термин **публичный API модуля `api`** обозначает фасеты SLM-модуля. Термин **Domain API** обозначает именованный runtime-контракт предметных операций. Эти понятия не взаимозаменяемы.
|
||||
|
||||
### Domain API
|
||||
|
||||
Именованный публичный runtime-контракт связного набора предметных сценариев внутри одного домена. Модуль `business` может объявить несколько Domain API, если они независимо собираются, имеют разные технические зависимости или нужны разным средам и потребителям.
|
||||
Именованный публичный runtime-контракт связного набора предметных команд, запросов или подписок внутри одного домена. Потребитель вызывает Domain API и получает только публичные модели, outcomes и ошибки предметной области, не зная provider, endpoint, SDK или transport protocol.
|
||||
|
||||
Разделение на API не создаёт новые доменные пакеты и не разрешает дублировать один сценарий в нескольких контрактах. Каждый публичный сценарий принадлежит ровно одному Domain API.
|
||||
Модуль `api` может объявить несколько Domain API, если они независимо собираются, имеют разные ports, trust boundaries или реальные consumers. Каждый публичный сценарий принадлежит ровно одному Domain API. APIs с общим неразделимым состоянием, atomicity или lifecycle образуют один контракт либо получают один явно созданный shared capability через assembly.
|
||||
|
||||
### Фабрика business
|
||||
### Публичная доменная модель
|
||||
|
||||
Публичная функция фасета `business/factory`, которая получает явные зависимости и создаёт экземпляр одного объявленного Domain API. Каждому Domain API соответствует ровно одна публичная фабрика. Фабрика не выбирает assembly или среду выполнения.
|
||||
Readonly-форма данных, которую Domain API принимает или возвращает внешнему потребителю. Публичная доменная модель принадлежит модулю `api`, не является backend DTO, cache record или framework view model и экспортируется только при наличии реального consumer.
|
||||
|
||||
### Предметная власть business
|
||||
Внутренняя модель модуля `api`, port record и framework view model могут иметь другую форму и не становятся публичными только из-за принадлежности тому же домену.
|
||||
|
||||
Право определять публичную доменную модель, допустимые переходы, валидацию внешних значений и семантику результатов. Adapter, assembly или framework binding может хранить, кэшировать и проецировать значения Domain API, но не становится независимым источником предметной модели или переходов.
|
||||
### Семантическая власть Domain API
|
||||
|
||||
Право определять публичную доменную модель, validation внешних значений, допустимые предметные transitions, семантику операций, outcomes и ожидаемых ошибок. Adapter, assembly или framework binding может транспортировать, хранить, кэшировать и отображать значения, но не становится независимым источником этих решений.
|
||||
|
||||
### Публичные фасеты `api`
|
||||
|
||||
Объявленные entry points одного логического публичного API модуля `api`:
|
||||
|
||||
| Путь | Статус | Содержимое |
|
||||
|---|---|---|
|
||||
| `api` | Обязательный | Только consumer-facing types: Domain API, public models, commands, outcomes и domain errors |
|
||||
| `api/factory` | Обязательный | Только именованные runtime-фабрики Domain API |
|
||||
| `api/ports` | При наличии dependency ports | Только implementer-facing types: ports, port records, port failures и factory dependency types |
|
||||
| `api/runtime` | Необязательный | Только публичные детерминированные runtime-значения и функции |
|
||||
|
||||
Фасет `api/runtime` может публиковать устойчивые error codes и guards, validators, value constructors, pure transitions, reconciliation functions, предметные константы и чистые projections, если они нужны реальным внешним потребителям. Он не содержит фабрики, изменяемое состояние, ввод-вывод, подписки, сценарии с runtime-зависимостями или environment-specific код.
|
||||
|
||||
Фасеты не являются сегментами, вложенными модулями или самостоятельными узлами графа. Любой другой внешний путь внутрь `api` является deep import.
|
||||
|
||||
### API-safe внешний пакет
|
||||
|
||||
Внешняя библиотека, допустимая в import-графе модуля `api`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, query runtime, framework и техническая интеграция не становятся API-safe только из-за совместимости с несколькими средами.
|
||||
|
||||
### Фабрика Domain API
|
||||
|
||||
Публичная функция фасета `api/factory`, которая получает явные dependency ports и cross-domain API dependencies и создаёт экземпляр одного объявленного Domain API. Каждому Domain API соответствует ровно одна публичная фабрика.
|
||||
|
||||
Фабрика не выбирает concrete adapter, assembly, environment или framework, не создаёт framework state и не запускает запрос, socket, subscription, timer или другую долгоживущую работу во время создания API.
|
||||
|
||||
## Ports, adapters и ошибки
|
||||
|
||||
### Dependency port
|
||||
|
||||
Consumer-owned контракт runtime-возможности, которая нужна модулю `api` и требует production-реализации. Port определяет минимальные операции, success values, закрытые expected failures и существенные behavioral guarantees со стороны потребителя capability, а не копирует API конкретного provider.
|
||||
|
||||
К ports относятся источники данных, external command gateways, storage, platform capabilities, clock, timer, random, ID generator и realtime event sources. Framework state/query manager, materialized cache и готовый API другого домена не являются dependency ports.
|
||||
|
||||
Port и связанные implementer-facing types принадлежат модулю `api` и публикуются через type-only фасет `api/ports`. Они не содержат SDK classes, generated DTO, HTTP status, `WebSocket`, framework hooks или другие concrete provider types.
|
||||
|
||||
### Port record
|
||||
|
||||
Технически нейтральная форма значения на границе port, достаточная модулю `api` для validation и преобразования в публичную доменную модель. Port record принадлежит implementer-facing контракту и не является публичной моделью приложения или raw provider DTO.
|
||||
|
||||
### Port failure
|
||||
|
||||
Закрытый implementer-facing набор ожидаемых сбоев dependency port, достаточный модулю `api` для выбора собственного outcome или domain error. Adapter преобразует provider-specific failure в port failure; модуль `api` преобразует port failure в публичную семантику.
|
||||
|
||||
Cancellation и неопределённый результат операции объявляются отдельно, если потребитель способен различать их. Unexpected programming failure не маскируется под expected port failure.
|
||||
|
||||
### Доменная ошибка
|
||||
|
||||
Безопасная публичная форма ожидаемого сбоя предметного сценария. Модуль `business` объявляет устойчивый readonly-тип с кодом; при необходимости runtime-коды и guards публикуются через `business/runtime`. Ошибки SDK, транспорта, storage, adapter или другого домена не являются доменными ошибками текущего API.
|
||||
Безопасная публичная форма ожидаемого сбоя операции Domain API. Модуль `api` объявляет устойчивый readonly сериализуемый тип с кодом; при необходимости runtime-коды и guards публикуются через `api/runtime`.
|
||||
|
||||
Способ передачи ошибки, например exception или discriminated `Result`, не изменяет владение и публичную безопасность ошибки.
|
||||
Ошибки provider, SDK, транспорта, storage, adapter или другого домена не являются доменными ошибками текущего API. Публичная ошибка не содержит исходные `message`, status, payload, class, stack или `cause`. Способ передачи ошибки, например exception или discriminated `Result`, не изменяет её владельца.
|
||||
|
||||
## Техническая сборка
|
||||
### Cross-domain API dependency
|
||||
|
||||
### Техническая зависимость
|
||||
Готовый публичный API доменного модуля Level 1 или Domain API пакета Level 2, необходимый операции текущего Domain API. Это отдельный вид runtime-зависимости, а не dependency port и не adapter. Graph owner создаёт независимый API раньше зависимого и передаёт готовое значение assembly, которая передаёт его фабрике.
|
||||
|
||||
Явная runtime-возможность, необходимая business-фабрике и требующая production-реализации. К техническим зависимостям относятся источники данных, SDK, storage, API платформы, state/query runtime, технические сервисы, данные request scope, clock, timer, random, ID generator и другие источники ввода-вывода, состояния или недетерминизма.
|
||||
|
||||
Type-only cross-domain API dependency, неизменяемая конфигурация и аргумент отдельного предметного сценария не являются техническими зависимостями.
|
||||
Локальный bridge port вводится только при реальном переводе чужого контракта, а не автоматически для каждого междоменного ребра.
|
||||
|
||||
### Adapter
|
||||
|
||||
SLM-модуль в Group `adapters`, который реализует одну или несколько связанных технических зависимостей business-фабрик поверх SDK, storage, state/query runtime, API платформы, данных запроса или технического сервиса. Одна связная production-реализация принадлежит одному adapter-модулю и не размещается внутри assembly или composition.
|
||||
SLM-модуль в Group `adapters`, который реализует один или несколько связанных dependency ports поверх SDK, generated client, storage, transport, platform API, данных запроса или технического сервиса. Одна связная production-реализация принадлежит одному adapter-модулю и не размещается внутри assembly, framework binding или composition.
|
||||
|
||||
Group `adapters` обязательна и непуста, если хотя бы одна business-фабрика имеет техническую зависимость. Фабрики без технических зависимостей не требуют создания этой Group.
|
||||
Adapter знает concrete provider и переводит его arguments, records и expected failures в контракт port. Он не объявляет операции Domain API, публичные доменные модели, предметные fallbacks или domain errors.
|
||||
|
||||
Точная обязательная поведенческая форма технических портов пока не определена и остаётся открытым вопросом Level 2.
|
||||
### Source cache
|
||||
|
||||
Технический cache внешнего источника внутри adapter: transport deduplication, connection state, provider retry или хранение port records. Source cache не является публичной доменной моделью и не передаёт наружу library-specific keys, clients или result types. Adapter является владельцем созданного им cache и экспортирует lifecycle handle; assembly может агрегировать этот cleanup, не становясь вторым владельцем. Если resource создаёт assembly, adapter получает его как borrowed capability.
|
||||
|
||||
## Assemblies и runtime-граф
|
||||
|
||||
### Assembly
|
||||
|
||||
SLM-модуль в Group `assemblies`, который создаёт явный именованный граф одного или нескольких Domain API пакета для одного реального контекста выполнения: браузера, запроса, server action или другого окружения. При наличии технических зависимостей assembly выбирает их adapter-модули и передаёт фабрикам готовые runtime-зависимости.
|
||||
SLM-модуль в Group `assemblies`, который создаёт явный именованный граф одного или нескольких Domain API пакета для одного объявленного production-контекста. Assembly выбирает публичные adapter-модули своего домена, вызывает фабрики и может принимать готовые API других доменов аргументами.
|
||||
|
||||
Assembly может принимать готовые API других доменов аргументами. Она не добавляет предметные сценарии, методы API или собственные доменные ошибки.
|
||||
Assembly не добавляет предметные операции, модели или ошибки. Импорт assembly не создаёт API и не запускает side effects; граф появляется только при явном вызове её builder.
|
||||
|
||||
Каждый доменный пакет содержит минимум одну assembly. Архитектура не ограничивает их максимальное количество и не требует универсальной изоморфной assembly.
|
||||
### Default assembly
|
||||
|
||||
Обязательный модуль `assemblies/default`, который создаёт штатный production-граф домена для одного baseline capability set, объявленного проектом. Имя `default` означает каноническую сборку проекта, но не означает browser-, server-, shared- или isomorphic-совместимость.
|
||||
|
||||
Для React + Vite `default` может быть browser-only. Для Next.js она может быть действительно изоморфной, только если каждый executable import совместим со всеми заявленными resolver conditions. Отличающийся набор API, dependencies, trust, runtime capabilities или lifecycle получает отдельную именованную assembly, например `rsc`, `administration` или `realtime-session`.
|
||||
|
||||
### Дополнительная assembly
|
||||
|
||||
Assembly, отличная от `default` и представляющая реальный дополнительный production-контекст. Имя может отражать environment только тогда, когда environment действительно определяет wiring; наличие RSC, server action или worker само по себе не требует отдельной assembly при неизменном совместимом графе.
|
||||
|
||||
### Graph owner
|
||||
|
||||
Composition root уровня `app`, `composition`, request handler, worker entry или test setup, который вызывает assemblies в ацикличном порядке, передаёт готовые cross-domain API зависимым assemblies и владеет областью жизни совокупного графа.
|
||||
|
||||
Graph owner импортирует production builders assemblies, но не `api/factory` или concrete adapters. Он создаёт только dependency-connected часть графа, необходимую текущему application, route, request, worker или test scope.
|
||||
|
||||
### Ресурс assembly
|
||||
|
||||
Ресурс жизненного цикла, который assembly сама создаёт для возвращаемого графа API. Создание фабрики или assembly не запускает скрытую долгоживущую работу. Если технический ресурс необходимо активировать при сборке, публичный результат assembly предоставляет cleanup handle; если ресурс запускается явной операцией API, эта операция предоставляет cleanup.
|
||||
Ресурс жизненного цикла, владельцем которого является assembly и который она обязана создать для возвращаемого графа. Adapter-owned resource сохраняет adapter owner и передаёт assembly только lifecycle handle для aggregate cleanup; borrowed resource не закрывается получателем.
|
||||
|
||||
Assembly без собственного ресурса жизненного цикла не обязана возвращать пустой `dispose`.
|
||||
Assembly немедленно регистрирует каждое cleanup obligation: cleanup собственного resource и полученный adapter lifecycle handle. При частичной ошибке все зарегистрированные obligations выполняются в обратном порядке. Успешный результат с хотя бы одним obligation предоставляет идемпотентный aggregate async cleanup, после завершения которого resources не вызывают callbacks. Только graph без cleanup obligations не возвращает пустой `dispose`.
|
||||
|
||||
## Framework binding
|
||||
## State, cache и framework
|
||||
|
||||
### Framework projection
|
||||
|
||||
Материализованное состояние или cache, которое framework binding строит из public models, outcomes и events Domain API для rendering, revalidation, optimistic UI и координации интерфейса. Concrete runtime может быть TanStack Query, SWR, Apollo, Zustand, Redux, Pinia, Signals или механизм конкретного framework.
|
||||
|
||||
Framework projection принадлежит binding или composition, а не модулю `api`. Она может хранить значения и технические статусы, но не определяет параллельную предметную модель. Предметный optimistic merge, ordering, rollback, reconciliation или transition производится операцией Domain API либо детерминированной функцией `api/runtime`.
|
||||
|
||||
### Hydration payload
|
||||
|
||||
Сериализуемая framework-owned форма переноса projection между server и client scopes. Payload содержит только разрешённые публичные доменные значения и framework metadata и не содержит API instances, functions, mutable cache clients, ports, adapters или request secrets.
|
||||
|
||||
Server и client создают отдельные API instances и framework caches. RSC передаёт через client boundary только сериализуемые значения или hydration payload; Server Action создаёт собственный request-scoped граф на каждый вызов.
|
||||
|
||||
### Framework Group
|
||||
|
||||
@@ -112,21 +171,43 @@ Group доменного пакета, названная по конкретн
|
||||
|
||||
### Framework binding module
|
||||
|
||||
SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией с фреймворком. Например, `react/session` может владеть Provider и hooks сессии, а `react/login-form` - переиспользуемой формой авторизации.
|
||||
SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией готового Domain API с конкретным framework. Он может владеть Provider, hooks, query policy, framework projection, hydration и переиспользуемым domain-specific UI.
|
||||
|
||||
Framework binding module получает готовые Domain API, не вызывает фабрику или assembly и не импортирует framework-состояние, hooks или компоненты другого домена. Он может использовать совместимые с его ролью state/query libraries и технически кэшировать результаты Domain API, не создавая новую предметную модель.
|
||||
Framework binding module получает готовый Domain API, не вызывает его фабрику или assembly и не выбирает adapters. Он не импортирует framework state, hooks или components другого домена и не обращается к предметному external source в обход Domain API.
|
||||
|
||||
## Сборка графа
|
||||
Framework-only SDK допустим внутри binding только для получения opaque operation input, например token от CAPTCHA или payment element; предметная операция всё равно выполняется через Domain API, а SDK type не пересекает его публичную границу.
|
||||
|
||||
### Место сборки графа
|
||||
## Realtime
|
||||
|
||||
Код модуля `composition`, точки входа `app`, request handler или test setup, который создаёт assemblies либо напрямую вызывает фабрики в ацикличном порядке и передаёт уже созданные API зависимым сборкам.
|
||||
### Realtime port
|
||||
|
||||
Место сборки владеет областью жизни совокупного графа и вызывает предоставленные cleanup handles не позже её завершения. Это координационное владение не переносит к нему предметные или технические ответственности внутренних модулей.
|
||||
Dependency port для двусторонних сообщений или подписок поверх WebSocket, SSE, GraphQL subscription, provider SDK или другого push-транспорта. Realtime port описывает предметно необходимую capability и проверяемые guarantees, но не публикует transport frames или concrete client.
|
||||
|
||||
### Граница среды выполнения
|
||||
### Realtime-команда
|
||||
|
||||
Граница между import-графами, предназначенными для клиента, сервера или обеих сред. Она определяется транзитивной достижимостью импортов, а не только именем папки или tree shaking.
|
||||
Операция Domain API, отправляемая через realtime port и имеющая объявленный момент подтверждения. Если приложение должно получить индивидуальный outcome, protocol adapter сопоставляет command, acknowledgement и failure посредством correlation metadata.
|
||||
|
||||
Разрыв соединения после отправки и до подтверждения может означать неопределённый outcome. Без idempotency key или provider guarantee такой исход не объявляется безопасным failure или автоматически повторяемой командой.
|
||||
|
||||
### Realtime subscription
|
||||
|
||||
Явная операция Domain API, которая публикует только проверенные domain events, statuses и errors и предоставляет cleanup. Realtime port определяет ordering, duplicate delivery, reconnect, gap detection, resync, cancellation и момент, после которого завершившийся cleanup гарантирует отсутствие новых callbacks.
|
||||
|
||||
Shared physical connection принадлежит adapter или assembly с явными scope, multiplicity и cleanup. Framework binding решает, как материализовать domain events: обновить projection, применить API-owned transition либо invalidировать cache и повторно запросить snapshot через Domain API.
|
||||
|
||||
## Environment
|
||||
|
||||
### Environment capability set
|
||||
|
||||
Явно объявленный набор runtime-возможностей, доступных конкретной точке входа или assembly: DOM, cookies, filesystem, worker API, edge API, framework server runtime и аналогично. Название папки не определяет capability set.
|
||||
|
||||
Совместимость проверяется по executable import-графу для каждого поддерживаемого набора resolver conditions и framework execution phase, включая server prerender и browser hydration. Runtime branching и tree shaking не доказывают изоляцию несовместимых импортов.
|
||||
|
||||
### Framework reference edge
|
||||
|
||||
Связь, которую framework преобразует в ссылку на другой executable graph вместо обычного runtime-вызова, например ссылка Server Component на Client Component или client invocation Server Action. Такая связь объявляется конфигурации проверки и анализируется отдельно от executable и type-only edges, но не отменяет проверку всех сред, в которых target graph исполняется самостоятельно.
|
||||
|
||||
RSC не является универсальной третьей средой рядом с browser и server. Server Component выполняется в server scope. Client Component участвует в browser hydration и, при включённом SSR или prerender, также исполняется в отдельном server render graph; framework-deferred browser effects проверяются отдельно. Между RSC и client graph проходит serialization/reference boundary.
|
||||
|
||||
## Структурная модель
|
||||
|
||||
@@ -136,12 +217,17 @@ SLM root
|
||||
├── доменный модуль Level 1
|
||||
└── доменный пакет Level 2
|
||||
├── metadata
|
||||
├── модуль business
|
||||
├── модуль api
|
||||
│ ├── api
|
||||
│ ├── api/factory
|
||||
│ ├── api/ports при наличии ports
|
||||
│ └── api/runtime при наличии consumers
|
||||
├── обязательная Group assemblies
|
||||
│ └── assembly-модуль
|
||||
├── Group adapters при наличии технических зависимостей
|
||||
│ ├── модуль default
|
||||
│ └── дополнительные assembly-модули
|
||||
├── Group adapters при наличии ports
|
||||
│ └── adapter-модуль
|
||||
└── Framework Group react
|
||||
├── модуль session
|
||||
└── модуль login-form
|
||||
└── модуль queries
|
||||
```
|
||||
|
||||
@@ -1,81 +1,126 @@
|
||||
# Проверка Level 2
|
||||
|
||||
> Граница автоматической проверки, архитектурного ревью и тестирования Level 2.
|
||||
> Граница автоматической проверки, architecture review и contract tests Level 2.
|
||||
|
||||
## Конфигурация проекта
|
||||
|
||||
Конфигурация проверки сопоставляет физические пути с доменными модулями Level 1, доменными пакетами Level 2, metadata, SLM-модулями, Groups, фасетами `business`, техническими зависимостями, adapter-модулями, assemblies, публичными точками входа, метками сред выполнения и allowlist внешних business-safe пакетов.
|
||||
Конфигурация проверки сопоставляет физические пути с:
|
||||
|
||||
Формат такой конфигурации пока не выбран. Независимо от формата проверка анализирует объявленные границы и import-граф, а не угадывает сущность только по имени папки.
|
||||
- доменными модулями Level 1 и пакетами Level 2;
|
||||
- metadata, SLM-модулями и Groups;
|
||||
- фасетами `api`;
|
||||
- dependency ports и adapter modules;
|
||||
- assemblies и их baseline/special contexts;
|
||||
- public entry points;
|
||||
- executable, type-only, framework reference и deferred edges;
|
||||
- environment capability sets, resolver conditions и framework execution phases;
|
||||
- API-safe external packages;
|
||||
- runtime assembly inputs и создаваемыми API, если проект автоматизирует runtime DAG.
|
||||
|
||||
Формат такой конфигурации пока не выбран. Проверка анализирует объявленные boundaries и resolved graphs, а не угадывает сущность только по имени папки.
|
||||
|
||||
## Автоматическая проверка
|
||||
|
||||
Автоматическая проверка блокирует:
|
||||
Каждое правило класса `A` реализуется блокирующей проверкой проекта. Автоматическая проверка обнаруживает:
|
||||
|
||||
- одновременное объявление одной предметной области доменным модулем и пакетом;
|
||||
- исполняемый файл, 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 циклы в графе модулей.
|
||||
- одновременное объявление одной предметной области module и package;
|
||||
- executable file, root `index.ts`, state или reexport в корне package;
|
||||
- отсутствие `api` или несколько модулей `api`;
|
||||
- отсутствие `api` либо `api/factory`;
|
||||
- недопустимый type/runtime export kind фасетов `api`, `api/ports`, `api/factory` и `api/runtime`;
|
||||
- другой public path или deep import внутри `api`;
|
||||
- отсутствие Group `assemblies` или модуля `assemblies/default`;
|
||||
- прямой дочерний элемент `assemblies` или `adapters` без module boundary;
|
||||
- нарушение importer matrix ports, factories и concrete adapters;
|
||||
- достижимость adapter, assembly, framework, SDK, storage, state/query runtime или environment-specific code из `api`;
|
||||
- запрещённый cross-domain import;
|
||||
- type-only import не из public facet владельца;
|
||||
- import framework state, hooks, contexts или components другого домена;
|
||||
- несовместимую executable reachability под каждым configured resolver condition set;
|
||||
- runtime- или type-only cycles статического module graph.
|
||||
|
||||
Статический анализ может отдельно находить прямые вызовы `Date.now`, `Math.random`, `crypto.randomUUID`, timers и чтение env внутри `business`. Окончательное решение о скрытом недетерминизме остаётся за ревью.
|
||||
Проверка external package reachability использует project allowlist API-safe packages. Решение о том, соответствует ли package критериям API-safe, принимается на review; автоматизация проверяет объявленный label и фактически resolved entries.
|
||||
|
||||
## Архитектурное ревью
|
||||
Неанализируемые dynamic imports запрещаются или явно allowlist-ятся project policy с target capability set.
|
||||
|
||||
На ревью определяется:
|
||||
## Architecture review
|
||||
|
||||
- представляет ли пакет одну связную предметную область;
|
||||
- принадлежит ли каждая соседняя область ровно одной форме независимо от форм других доменов;
|
||||
- принадлежат ли публичные сценарии ровно одному из именованных 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.
|
||||
На review определяется:
|
||||
|
||||
## Тестирование
|
||||
- представляет ли package одну связную предметную область;
|
||||
- является ли `api` единственным семантическим шлюзом домена;
|
||||
- соответствуют ли exports `api` реальным consumer contracts, `api/ports` implementer contracts, а `api/factory` объявленным Domain API factories;
|
||||
- отличаются ли public models от raw provider DTO там, где это необходимо;
|
||||
- принадлежат ли operations ровно одному Domain API;
|
||||
- оправдано ли разделение нескольких Domain API независимой сборкой, trust или consumers;
|
||||
- описывают ли ports consumer-owned capabilities, а не endpoints конкретного SDK;
|
||||
- достаточна ли closed failure algebra для выбора domain outcomes;
|
||||
- преобразуются ли provider и foreign-domain failures в собственные errors;
|
||||
- является ли каждая production implementation отдельным adapter module;
|
||||
- не выполняют ли framework bindings предметные external operations в обход API;
|
||||
- не создаёт ли framework projection параллельную модель;
|
||||
- определены ли optimistic ordering, versioning и reconciliation модулем `api`;
|
||||
- представляет ли `default` один реальный baseline capability context;
|
||||
- оправданы ли дополнительные assemblies реальным отличием graph;
|
||||
- остаётся ли runtime assembly graph ацикличным;
|
||||
- создаётся ли только dependency-connected часть production graph;
|
||||
- полностью ли определены lifecycle и cleanup failure paths;
|
||||
- соответствует ли каждый API-safe package ограничениям;
|
||||
- остаются ли Groups без implementation и aggregate API.
|
||||
|
||||
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-сценариев.
|
||||
## Environment review
|
||||
|
||||
Import-graph checks не заменяются runtime-тестами.
|
||||
Для каждого public entry point рассматриваются реальные executable imports под заявленными conditions. Отдельно проверяются:
|
||||
|
||||
- RSC server execution;
|
||||
- Client Component references;
|
||||
- server prerender graph Client Components при включённом SSR;
|
||||
- browser hydration graph Client Components;
|
||||
- framework-deferred browser effects;
|
||||
- Server Action references;
|
||||
- browser, Node.js, edge и worker capabilities;
|
||||
- conditional exports external packages;
|
||||
- dynamic imports;
|
||||
- serialization boundaries.
|
||||
|
||||
Название `default`, `rsc`, `server` или `client` не является доказательством совместимости. Tree shaking и runtime branching также не являются доказательством.
|
||||
|
||||
## Realtime review
|
||||
|
||||
Для каждого realtime port фиксируются:
|
||||
|
||||
- correlation scope и ACK semantics;
|
||||
- ordering и duplicate policy;
|
||||
- disconnect, timeout и `OUTCOME_UNKNOWN`;
|
||||
- idempotency и retry;
|
||||
- reconnect, gap detection и resync;
|
||||
- cancellation;
|
||||
- shared connection owner;
|
||||
- cleanup и запрет callbacks после disposal.
|
||||
|
||||
Без этих guarantees adapter нельзя считать проверяемой реализацией port.
|
||||
|
||||
## Testing
|
||||
|
||||
Domain API проверяется через factory с fake ports. Adapter проверяется contract tests concrete provider. Assembly проверяет production wiring, capabilities, partial construction и cleanup. Framework binding проверяет projection, hydration и lifecycle с fake API.
|
||||
|
||||
Import-graph checks не заменяются runtime tests, а API fake не заменяет adapter contract test.
|
||||
|
||||
## Смешанный SLM root
|
||||
|
||||
Наличие доменных модулей Level 1 рядом с пакетами Level 2 является завершённым допустимым состоянием. Проверка применяет правила формы отдельно к каждой предметной области и правила Level 2 ко всем статическим связям, пересекающим пакетную границу.
|
||||
Наличие доменных модулей Level 1 рядом с пакетами Level 2 является завершённым допустимым состоянием. Проверка применяет правила формы отдельно к каждой предметной области и правила Level 2 ко всем статическим связям, пересекающим package boundary.
|
||||
|
||||
Переход одного домена завершается, когда его старая модульная граница удалена и checker видит только пакет. Другие домены не входят в критерий завершения.
|
||||
Переход одного домена завершается, когда его старая module boundary удалена и checker видит только package. Другие домены не входят в критерий формы, но dependency-connected consumers и graph owners входят в change radius.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-DOMAIN-A003`](../rules/level-2.md#slm-l2-domain-a003)
|
||||
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-API-A007`](../rules/level-2.md#slm-l2-api-a007)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
|
||||
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
|
||||
- [`SLM-L2-BUSINESS-R018`](../rules/level-2.md#slm-l2-business-r018)
|
||||
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
|
||||
- [`SLM-L2-API-A019`](../rules/level-2.md#slm-l2-api-a019)
|
||||
- [`SLM-L2-ASSEMBLY-A020`](../rules/level-2.md#slm-l2-assembly-a020)
|
||||
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
|
||||
- [`SLM-L2-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)
|
||||
- [`SLM-L2-API-A022`](../rules/level-2.md#slm-l2-api-a022)
|
||||
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
|
||||
|
||||
Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.
|
||||
|
||||
@@ -51,14 +51,16 @@ SLM-L{level}-{group}-{class}{number}
|
||||
| `NESTED_MODULE` | Вложенные модули |
|
||||
| `LIFECYCLE` | Жизненный цикл |
|
||||
| `DOMAIN` | Домены |
|
||||
| `BUSINESS` | Контракты бизнес-логики |
|
||||
| `FACTORY` | Фабрики бизнес-логики |
|
||||
| `API` | Доменный API |
|
||||
| `FACTORY` | Фабрики Domain API |
|
||||
| `ERROR` | Ошибки домена |
|
||||
| `PORT` | Порты бизнес-логики |
|
||||
| `PORT` | Dependency ports |
|
||||
| `ADAPTER` | Адаптеры |
|
||||
| `ASSEMBLY` | Сборка API и жизненный цикл |
|
||||
| `ENVIRONMENT` | Границы сред выполнения |
|
||||
| `FRAMEWORK` | Модули фреймворков |
|
||||
| `STATE` | Материализация состояния |
|
||||
| `REALTIME` | Realtime-взаимодействие |
|
||||
| `TEST` | Тестирование |
|
||||
|
||||
Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Правила 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` не переиспользуются после изменения модели уровней и перехода к постоянному совместному применению форм.
|
||||
Доменный пакет Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. Для предметной области в пакетной форме [`SLM-L1-DOMAIN-R015`](./level-1.md#slm-l1-domain-r015) заменяется правилами доменного пакета; остальные предметные области того же SLM root могут сохранять форму доменного модуля Level 1. [`SLM-L1-GROUP-R007`](./level-1.md#slm-l1-group-r007) сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Для модуля `api` правило [`SLM-L1-MODULE-A004`](./level-1.md#slm-l1-module-a004) уточняется `SLM-L2-API-A019`: объявленные фасеты вместе образуют один логический публичный API и не считаются deep imports. Ранее использовавшиеся номера `SLM-L2-DOMAIN-R001` и `SLM-L2-MIGRATION-A017` не переиспользуются.
|
||||
|
||||
## Граница доменного пакета
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
> **Корень доменного пакета**
|
||||
>
|
||||
> Корень доменного пакета содержит только декларативную metadata, модуль `business` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов.
|
||||
> Корень доменного пакета содержит только декларативную metadata, модуль `api` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов.
|
||||
|
||||
### SLM-L2-GROUP-R004
|
||||
|
||||
@@ -22,31 +22,31 @@
|
||||
>
|
||||
> Group, расположенная непосредственно в слое `domains` или другой такой Group, содержит только доменные модули Level 1, доменные пакеты Level 2 и навигационные Groups и не владеет реализацией, состоянием, жизненным циклом или публичным API.
|
||||
|
||||
## Business и Domain API
|
||||
## Доменный API
|
||||
|
||||
### SLM-L2-BUSINESS-R005
|
||||
### SLM-L2-API-R005
|
||||
|
||||
> **Модуль business**
|
||||
> **Модуль api**
|
||||
>
|
||||
> Каждый доменный пакет содержит ровно один модуль `business`, который владеет публичными предметными сценариями и объявляет один или несколько именованных Domain API этой предметной области.
|
||||
> Каждый доменный пакет содержит ровно один модуль `api`, который объявляет один или несколько именованных Domain API, их публичные модели, результаты, ошибки, dependency ports и фабрики.
|
||||
|
||||
### SLM-L2-BUSINESS-R006
|
||||
### SLM-L2-API-R006
|
||||
|
||||
> **Предметная власть business**
|
||||
> **Семантическая власть Domain API**
|
||||
>
|
||||
> Adapters, assemblies и framework binding modules транспортируют, кэшируют или проецируют доменные данные только в форме, произведённой или проверенной публичным API либо детерминированным runtime модуля `business`, и не определяют независимую предметную модель, переход или результат сценария.
|
||||
> Доступные приложению доменные данные, модели, validation, семантика команд и запросов, результаты и ожидаемые ошибки производятся или проверяются модулем `api`; adapters, assemblies и framework bindings не определяют параллельную предметную модель или переход.
|
||||
|
||||
### SLM-L2-BUSINESS-A007
|
||||
### SLM-L2-API-A007
|
||||
|
||||
> **Импортная замкнутость business**
|
||||
> **Импортная замкнутость api**
|
||||
>
|
||||
> Все runtime- и type-only импорты `business`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как business-safe.
|
||||
> Все runtime- и type-only импорты модуля `api`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как API-safe.
|
||||
|
||||
### SLM-L2-FACTORY-R008
|
||||
|
||||
> **Фабрики Domain API**
|
||||
>
|
||||
> Каждому публичному Domain API соответствует ровно одна именованная фабрика фасета `business/factory`; фабрика получает явные runtime-зависимости, создаёт только этот API и не выбирает assembly или среду выполнения.
|
||||
> Каждому публичному Domain API соответствует ровно одна именованная фабрика фасета `api/factory`; фабрика получает явные ports и cross-domain API, создаёт только этот Domain API, не выбирает adapter или assembly и не запускает скрытые ресурсы жизненного цикла.
|
||||
|
||||
## Ошибки домена
|
||||
|
||||
@@ -54,13 +54,13 @@
|
||||
|
||||
> **Публичный контракт ошибок**
|
||||
>
|
||||
> Каждый ожидаемый сбой публичного предметного сценария представлен именованным readonly-типом собственной доменной ошибки с устойчивым кодом; тип экспортируется через type-only barrel, а необходимые внешним потребителям runtime-коды и guards только через `business/runtime`.
|
||||
> Каждый ожидаемый сбой публичной операции Domain API представлен именованным readonly сериализуемым типом собственной доменной ошибки с устойчивым кодом; тип экспортируется через `api`, а необходимые внешним потребителям runtime-коды и guards только через `api/runtime`.
|
||||
|
||||
### SLM-L2-ERROR-R010
|
||||
|
||||
> **Изоляция исходных ошибок**
|
||||
>
|
||||
> Сбой adapter, SDK, транспорта, storage или другого домена, доступный приложению через Domain API, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause.
|
||||
> Сбой provider, adapter, SDK, транспорта, storage или другого домена, доступный приложению через Domain API, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause.
|
||||
|
||||
## Assemblies и зависимости
|
||||
|
||||
@@ -68,19 +68,19 @@
|
||||
|
||||
> **Роль assembly**
|
||||
>
|
||||
> Каждая assembly является SLM-модулем одного именованного контекста выполнения, выбирает необходимые adapter-модули, вызывает одну или несколько business-фабрик и возвращает явный именованный граф их API, не добавляя предметные сценарии, методы API или собственные доменные ошибки.
|
||||
> Каждая assembly является SLM-модулем одного объявленного production-контекста, выбирает adapter-модули, вызывает одну или несколько фабрик своего `api` и возвращает явный именованный граф готовых Domain API, не добавляя предметные операции, модели или ошибки.
|
||||
|
||||
### SLM-L2-DEPENDENCY-A012
|
||||
|
||||
> **Междоменные импорты Level 2**
|
||||
>
|
||||
> Статическая связь между разными доменными границами, хотя бы одна из которых является пакетом Level 2, допускает только type-only импорт публичного API доменного модуля Level 1 или корневого `business` пакета Level 2 либо runtime-импорт `business/runtime` пакета Level 2; все такие связи входят в общий ацикличный граф, а остальные runtime-экспорты другого домена не импортируются.
|
||||
> Статическая связь между разными доменными границами, хотя бы одна из которых является пакетом Level 2, допускает только type-only импорт публичного API доменного модуля Level 1 или фасета `api` пакета Level 2 либо runtime-импорт `api/runtime` пакета Level 2; остальные публичные и внутренние пути другого домена не импортируются.
|
||||
|
||||
### SLM-L2-ENVIRONMENT-A013
|
||||
|
||||
> **Совместимость среды выполнения**
|
||||
>
|
||||
> Публичная точка входа, обозначенная как client-, server- или shared-entry point, не импортирует и не реэкспортирует код несовместимой среды выполнения во всём достижимом графе импортов.
|
||||
> Для каждой объявленной точки входа, поддерживаемого набора resolver conditions и framework execution phase её достижимый executable import-граф не содержит несовместимых runtime capabilities; type-only связи, framework reference и deferred edges проверяются отдельно и не считаются обычным выполнением.
|
||||
|
||||
## Framework Groups и тестирование
|
||||
|
||||
@@ -94,13 +94,13 @@
|
||||
|
||||
> **Framework binding module**
|
||||
>
|
||||
> Модуль внутри Framework Group владеет одной domain-specific framework-ответственностью, получает готовые Domain API и не выполняет business-сборку, не выбирает adapters и не владеет страницей, маршрутом или multi-domain композицией.
|
||||
> Модуль внутри Framework Group владеет одной domain-specific framework-ответственностью, получает готовые Domain API, материализует их значения средствами фреймворка и не вызывает фабрики, не выбирает adapters, не обращается к предметному внешнему источнику в обход Domain API и не владеет страницей, маршрутом или multi-domain композицией.
|
||||
|
||||
### SLM-L2-TEST-R016
|
||||
|
||||
> **Проверка владельцев Level 2**
|
||||
>
|
||||
> Каждый публичный предметный сценарий проверяется через фабрику владеющего им Domain API, а основные тесты adapter, assembly и framework binding module проверяют только собственные публичные границы и не повторяют полный набор предметных сценариев.
|
||||
> Каждый публичный сценарий проверяется через фабрику владеющего им Domain API, каждый adapter — по контракту реализуемого port, а основные тесты assembly и framework binding module проверяют только собственные публичные границы и не повторяют полный набор сценариев Domain API.
|
||||
|
||||
## Совместное применение форм
|
||||
|
||||
@@ -110,62 +110,100 @@
|
||||
>
|
||||
> Каждая предметная область объявлена ровно в одной форме: как доменный модуль Level 1 либо как доменный пакет Level 2; один SLM root может одновременно содержать разные предметные области обеих форм.
|
||||
|
||||
## Внешние библиотеки business
|
||||
## Внешние библиотеки api
|
||||
|
||||
### SLM-L2-BUSINESS-R018
|
||||
### SLM-L2-API-R018
|
||||
|
||||
> **Business-safe внешний пакет**
|
||||
> **API-safe внешний пакет**
|
||||
>
|
||||
> Внешний пакет объявляется business-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state/query manager, framework или другой технической интеграцией.
|
||||
> Внешний пакет объявляется API-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state/query manager, framework или другой технической интеграцией.
|
||||
|
||||
## Публичные фасеты business
|
||||
## Публичные фасеты api
|
||||
|
||||
### SLM-L2-BUSINESS-A019
|
||||
### SLM-L2-API-A019
|
||||
|
||||
> **Публичные фасеты business**
|
||||
> **Публичные фасеты api**
|
||||
>
|
||||
> Публичный API `business` имеет обязательные entry points `business` только с type exports и `business/factory` только с именованными runtime-фабриками, может иметь `business/runtime` только с runtime exports и не имеет других публичных путей или deep imports.
|
||||
> Публичный API модуля `api` имеет обязательные entry points `api` только с type exports и `api/factory` только с runtime exports, может иметь `api/ports` только при наличии объявленного dependency port и только с type exports и `api/runtime` только с runtime exports и не имеет других публичных путей или deep imports.
|
||||
|
||||
## Обязательные роли сборки
|
||||
## Обязательная штатная сборка
|
||||
|
||||
### SLM-L2-ASSEMBLY-A020
|
||||
|
||||
> **Обязательная Group assemblies**
|
||||
> **Обязательная assembly default**
|
||||
>
|
||||
> Корень каждого доменного пакета содержит ровно одну непустую Group `assemblies`, каждый прямой дочерний элемент которой является объявленной границей SLM-модуля.
|
||||
> Корень каждого доменного пакета содержит ровно одну непустую Group `assemblies` с ровно одним прямым модулем `default`; каждый другой прямой дочерний элемент Group также является объявленной границей assembly-модуля.
|
||||
|
||||
### SLM-L2-ADAPTER-R021
|
||||
|
||||
> **Модули production adapters**
|
||||
>
|
||||
> Если хотя бы одна business-фабрика имеет техническую зависимость, корень доменного пакета содержит непустую Group `adapters`, а каждая связная production-реализация одной или нескольких таких зависимостей принадлежит ровно одному adapter-модулю этой Group и не определяется в другом месте production-графа.
|
||||
> Если хотя бы одна фабрика имеет dependency port, корень доменного пакета содержит непустую Group `adapters`, а каждая связная production-реализация одного или нескольких ports принадлежит ровно одному adapter-модулю этой Group и не определяется в другом месте production-графа.
|
||||
|
||||
### SLM-L2-BUSINESS-A022
|
||||
### SLM-L2-API-A022
|
||||
|
||||
> **Потребители фасетов business**
|
||||
> **Потребители фасетов и сборочных модулей**
|
||||
>
|
||||
> Корневой `business` импортируется извне только через `import type`; `business/factory` импортируют только assemblies своего домена, `app`, `compositions` и тесты, а `business/runtime` импортируется только через объявленный публичный путь с соблюдением правил слоёв и междоменных зависимостей.
|
||||
> Фасет `api` импортируется извне только через `import type`, `api/ports` импортируют только adapters своего домена, assemblies и тесты, `api/factory` и concrete adapters в production импортируют только assemblies своего домена, а `api/runtime` не импортируют adapters и используют только через объявленный публичный путь с соблюдением правил слоёв и междоменных зависимостей.
|
||||
|
||||
## Жизненный цикл assembly
|
||||
|
||||
### SLM-L2-ASSEMBLY-R023
|
||||
|
||||
> **Cleanup ресурса assembly**
|
||||
> **Транзакционный lifecycle assembly**
|
||||
>
|
||||
> Создание Domain API фабрикой или assembly не запускает скрытый ресурс жизненного цикла; ресурс запускается явной операцией с cleanup, а технический ресурс, который assembly обязана создать для графа, сопровождается публичным cleanup handle, вызываемым не позже завершения области жизни графа.
|
||||
> Assembly не запускает скрытую долгоживущую работу; cleanup каждого созданного ею ресурса и каждого полученного adapter lifecycle handle немедленно регистрируется, при частичной ошибке выполняется в обратном порядке, а успешный результат с cleanup obligations предоставляет идемпотентный aggregate cleanup, после завершения которого resources не вызывают callbacks.
|
||||
|
||||
## Недетерминизм business
|
||||
## Недетерминизм api
|
||||
|
||||
### SLM-L2-BUSINESS-R024
|
||||
### SLM-L2-API-R024
|
||||
|
||||
> **Явные источники недетерминизма**
|
||||
>
|
||||
> Business-сценарий получает текущее время, timer, random, ID generator, environment и другие источники недетерминизма только через явные зависимости фабрики и не читает их из скрытого runtime-окружения.
|
||||
> Операция Domain API получает текущее время, timer, random, ID generator, environment и другие источники недетерминизма только через явные ports фабрики и не читает их из скрытого runtime-окружения.
|
||||
|
||||
## Публичный runtime business
|
||||
## Публичный runtime api
|
||||
|
||||
### SLM-L2-BUSINESS-R025
|
||||
### SLM-L2-API-R025
|
||||
|
||||
> **Детерминированный runtime business**
|
||||
> **Детерминированный runtime api**
|
||||
>
|
||||
> Фасет `business/runtime` экспортирует только необходимые реальным внешним потребителям детерминированные значения и функции без ввода-вывода, изменяемого состояния, runtime capability или environment-specific поведения.
|
||||
> Фасет `api/runtime` экспортирует только необходимые реальным внешним потребителям детерминированные значения и функции без ввода-вывода, изменяемого состояния, runtime capability или environment-specific поведения.
|
||||
|
||||
## Dependency ports
|
||||
|
||||
### SLM-L2-PORT-R027
|
||||
|
||||
> **Consumer-owned port**
|
||||
>
|
||||
> Каждый dependency port принадлежит модулю `api`, описывает минимальную необходимую ему capability и закрытый набор ожидаемых port failures без concrete provider, SDK, framework или transport types; adapter реализует этот контракт, но не определяет его семантику.
|
||||
|
||||
## Материализация состояния
|
||||
|
||||
### SLM-L2-STATE-R028
|
||||
|
||||
> **Framework-owned materialization**
|
||||
>
|
||||
> Framework binding или composition может владеть framework metadata и собственным UI-state, но материализует доменный payload только из values, outcomes и events, произведённых или проверенных Domain API, и применяет предметный optimistic merge, reconciliation или transition только через операцию либо детерминированный runtime модуля `api`.
|
||||
|
||||
## Realtime
|
||||
|
||||
### SLM-L2-REALTIME-R029
|
||||
|
||||
> **Проверяемый realtime-контракт**
|
||||
>
|
||||
> Каждый realtime port явно определяет correlation, момент подтверждения команды, ordering, duplicate, reconnect, resync, cancellation и cleanup semantics; adapter скрывает transport protocol, а Domain API публикует только проверенные события, outcomes и собственные стабильные ошибки.
|
||||
|
||||
## Runtime-граф assemblies
|
||||
|
||||
### SLM-L2-ASSEMBLY-R030
|
||||
|
||||
> **Ацикличная runtime-сборка**
|
||||
>
|
||||
> Runtime-граф публичных API доменных модулей Level 1 и Domain API пакетов Level 2, включая assembly inputs, factory dependencies и передаваемые callbacks, не содержит циклов, а graph owner создаёт независимые API раньше зависимых и очищает их в обратном порядке.
|
||||
|
||||
### SLM-L2-ASSEMBLY-R031
|
||||
|
||||
> **Контекст default assembly**
|
||||
>
|
||||
> `assemblies/default` представляет один объявленный штатный production-набор API, dependencies, runtime capabilities и lifecycle; имя `default` само по себе не означает browser-, server- или isomorphic-совместимость, а отличающийся контекст получает отдельную именованную assembly.
|
||||
|
||||
Reference in New Issue
Block a user