feat: уточнить архитектурные границы SLM

This commit is contained in:
2026-07-30 21:44:47 +03:00
parent 4ce6bae61c
commit 15805e28df
28 changed files with 1006 additions and 553 deletions

View File

@@ -1,61 +1,90 @@
# Зависимости Level 2
> Уточнение графа зависимостей внутри и между доменными пакетами.
> Уточнение графа зависимостей внутри и между доменными границами.
## Связанные правила
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
## Направление внутри пакета
## Матрица внутри пакета
| Исходный модуль | Допустимые зависимости |
|---|---|
| `business` | Собственные файлы, объявленный нейтральный `shared`, business-safe внешние пакеты, type-only `business` других доменов |
| Adapter module | Type-only barrel собственного `business`, `infra`, конкретная техническая реализация, `shared` |
| Preset | Type-only barrel и `factory` собственного `business`, публичные adapter-модули своего домена, type-only API других доменов |
| Framework binding module | Type-only barrel и `error` собственного `business`, публичные framework-модули своего домена, фреймворк, `ui`, `shared` |
| Место сборки графа | Presets либо `business/factory` и adapter-модули, `business/error`, framework-модули входящих в граф доменов |
| `business` | Собственные файлы, объявленные environment-neutral ресурсы `shared`, business-safe packages, type-only контракты и `business/runtime` других доменов |
| Adapter module | Type-only barrel собственного `business`, `infra`, concrete technical runtime, `shared` |
| Assembly | Type-only barrel, `factory` и при необходимости `runtime` собственного `business`, публичные adapters своего домена, type-only API других доменов, `shared` |
| Framework binding module | Type-only barrel и `runtime` собственного `business`, публичные framework-модули своего домена, framework/state/query runtime, `ui`, `shared` |
| Место сборки графа | Assemblies либо `business/factory` и adapter-модули, `business/runtime`, framework-модули входящих в граф доменов, а также разрешённые матрицей `infra`, `ui` и `shared` |
`business` не достигает adapters, presets, framework-модулей, product SDK, storage, API браузера или Node.js. Проверяется весь транзитивный import-граф трёх публичных фасетов.
`business` не достигает adapters, assemblies, framework-модулей, product SDK, storage, state/query manager, API браузера или Node.js. Проверяется весь транзитивный import-граф публичных фасетов.
Adapter module импортирует из собственного `business` только типы технических зависимостей. Он не импортирует `business/factory` или `business/error`, потому что не собирает API и не создаёт доменные ошибки.
Adapter module импортирует из собственного `business` только типы технических зависимостей. Он не импортирует `business/factory`, потому что не собирает API. Публичный deterministic runtime обычно также не нужен adapter: внешний результат возвращается business для проверки и error mapping.
Preset не содержит inline adapters. Он импортирует production implementations через публичные API конкретных модулей `adapters/*`.
Assembly не содержит inline adapters. Она импортирует production implementations через публичные API конкретных модулей `adapters/*`.
## Междоменные импорты
Модуль одного доменного пакета не импортирует runtime-экспорты другого доменного пакета. Разрешён только type-only импорт корневого barrel его `business`, по возможности суженный через `Pick`. Чужие `business/factory` и `business/error` являются runtime entry points и запрещены.
При связи, пересекающей границу пакета Level 2, разрешены два вида статического импорта из другого домена:
```ts
import type { AuthApi } from '@/domains/auth/business'
export type UserDeps = {
auth: Pick<AuthApi, 'getSession'>
}
import type { AuthSessionApi } from '@/domains/auth/business'
import { isAuthError } from '@/domains/auth/business/runtime'
```
Type-only импорт остаётся архитектурным ребром. Runtime- и type-only зависимости образуют единый DAG и не могут создавать цикл.
Первый импорт предоставляет type-only контракт для runtime-инъекции. Второй предоставляет только публичную детерминированную функцию или значение. Оба импорта являются рёбрами общего DAG.
Pure function, hook, Provider, context, component или framework state другого домена являются runtime-экспортами и не образуют исключение. Независимая общая функция переносится в `shared`, а UI нескольких доменов собирается в `compositions`.
Запрещено импортировать из другого домена:
- `business/factory`;
- готовый API instance или singleton;
- assembly;
- adapter;
- framework state, hook, context, Provider или component;
- любой внутренний путь `business`.
Такая модель действует и между двумя пакетами Level 2, и между модулем Level 1 и пакетом Level 2. Между двумя простыми доменными модулями продолжают действовать правила Level 1.
## Детерминированный runtime
`business/runtime` закрывает случай общей продуктовой pure-функции, которую нельзя переносить в `shared` из-за знания о предметной области:
```ts
import {
normalizeAuthIdentifier,
} from '@/domains/auth/business/runtime'
```
Функция остаётся собственностью Auth, а зависимый домен получает явное runtime-ребро к её публичному контракту. Если связь создаёт цикл, границы доменов или владелец общего понятия пересматриваются.
Перенос в `shared` допустим только после устранения продуктового знания, а не как обход cross-domain правила.
## Runtime-инъекция API
Готовый API другого домена передаётся preset-модулю или одноразовому месту сборки аргументом. Код зависимого доменного пакета не импортирует его runtime-фабрику или сборку:
Готовый API другого домена передаётся assembly или одноразовому месту сборки аргументом. Код зависимого домена не импортирует его runtime-фабрику или сборку:
```text
createAuthForRequest()
→ AuthApi
→ createUserForRequest({ authApi })
→ UserApi
→ AuthSessionApi
→ createUserForRequest({ auth })
→ UserProfileApi
```
Место сборки графа создаёт независимые домены раньше зависимых. Если сбой `AuthApi` становится результатом публичного сценария `UserApi`, приложению доступна только собственная доменная ошибка User. Точный механизм различения ошибок при exception-модели остаётся открытым вопросом.
Место сборки графа создаёт независимые домены раньше зависимых. Если сбой Auth становится результатом публичного сценария User, наружу выходит только собственная ошибка User. При exception-модели User может распознать ожидаемый Auth error через публичный guard из `auth/business/runtime`.
## Совместное применение Level 1 и Level 2
Один SLM root может постоянно содержать обе формы. Каждая предметная область объявляется либо модулем, либо пакетом, поэтому checker однозначно определяет применимые правила.
Runtime-взаимодействие с пакетом Level 2 собирается за пределами доменного кода. Если существующий модуль Level 1 должен принимать готовый API, его собственный публичный контракт явно предоставляет такую точку инъекции. Если это невозможно без скрытого singleton или обратного импорта, зависимый модуль рефакторится либо переводится на Level 2.
Runtime-значение из модуля Level 1, необходимое пакету Level 2, также передаётся внешним graph owner как API, callback или другой явно типизированный аргумент. Код пакета не импортирует executable API модуля Level 1 напрямую.
## Framework-состояние
@@ -69,10 +98,12 @@ import { useAuthSession } from '@/domains/auth/react/session'
import { useAuthSession } from '@/domains/auth/react/session'
```
Во втором случае композиционный модуль читает состояния обоих доменов и передаёт необходимые данные или callbacks через публичные свойства компонентов.
Во втором случае composition читает состояния обоих доменов и передаёт необходимые данные или callbacks через публичные свойства компонентов.
State/query cache внутри framework binding не создаёт исключение для cross-domain imports. Он работает поверх переданного API своего домена.
## Границы сред
Каждый client-, server- или shared-entry point имеет совместимый транзитивный import-граф. Серверный preset или adapter не реэкспортируется через `business`, Framework Group или клиентский preset.
Каждый client-, server- или shared-entry point имеет совместимый транзитивный import-граф. Серверная assembly или adapter не реэкспортируется через `business`, Framework Group или клиентскую assembly.
Tree shaking не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода.