feat: переработать уровни SLM

This commit is contained in:
2026-07-30 18:45:54 +03:00
parent c0956ed2a0
commit d3b37eb4bd
44 changed files with 1239 additions and 1449 deletions

View File

@@ -0,0 +1,26 @@
# Доменные пакеты Level 2
Доменный пакет заменяет корневой доменный модуль Level 1. Он объединяет SLM-модули одной предметной области, но сам не является модулем, Group или публичным API.
```text
domains/auth/
├── business/
├── presets/
├── adapters/
└── react/
├── session/
└── login-form/
```
## Основные границы
- [Доменный пакет](./domain-package.md) определяет предметную и структурную границу.
- [Business](./business.md) владеет `DomainApi`, фабрикой и доменными ошибками.
- [Фабрика, зависимости и adapters](./factory-ports-adapters.md) отделяют business от технической реализации.
- [Presets](./presets.md) собирают один API для нужных окружений.
- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу.
- [Миграция auth](./auth-example.md) показывает переход с Level 1.
- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей.
Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md).

View File

@@ -0,0 +1,101 @@
# Миграция домена auth с Level 1
> Проверочный пример перехода от доменного модуля к доменному пакету.
## Связанное правило
- [`SLM-L2-MIGRATION-A017`](../../rules/level-2.md#slm-l2-migration-a017)
## Исходная форма Level 1
```text
domains/auth/ # Доменный модуль
├── hooks/
├── services/
├── stores/
├── ui/
└── index.ts # Общий API модуля
```
Level 1 разрешает business-сценариям, framework hooks, state adapter и локальной сборке находиться внутри одного модуля.
## Целевая форма Level 2
```text
domains/auth/ # Доменный пакет
├── README.md
├── business/ # SLM-модуль
│ ├── errors/
│ ├── lib/
│ ├── services/
│ ├── types/
│ └── index.ts
├── presets/ # Group
│ ├── browser/ # SLM-модуль
│ │ ├── adapters/
│ │ └── index.ts
│ └── request/ # SLM-модуль
│ └── index.ts
└── react/ # Framework Group
├── session/ # SLM-модуль
│ └── index.ts
└── login-form/ # SLM-модуль
└── index.ts
```
Корневой `domains/auth/index.ts` удаляется. Потребители переходят на публичные API конкретных модулей.
## Перенос ответственности
| Исходная часть | Владелец Level 2 |
|---|---|
| Сценарии, предметные типы, единый API | `auth/business` |
| Коды, тип и guard ошибок | `auth/business` |
| Browser storage и HTTP adapters | `auth/presets/browser` |
| Cookies, request data и server adapters | `auth/presets/request` |
| Provider и session hooks | `auth/react/session` |
| Переиспользуемая форма | `auth/react/login-form` |
| Страница, текст и redirect | `compositions` |
## Новые импорты
```ts
import { authFactory, isAuthError } from '@/domains/auth/business'
import { createBrowserAuth } from '@/domains/auth/presets/browser'
import { AuthSessionProvider } from '@/domains/auth/react/session'
import { LoginForm } from '@/domains/auth/react/login-form'
```
## Cross-domain граф
Если User зависит от Auth, оба связанных домена сначала переводятся в пакеты. Затем User получает только type-only контракт:
```ts
import type { AuthApi } from '@/domains/auth/business'
export type UserDeps = {
auth: Pick<AuthApi, 'getSession'>
}
```
Место сборки графа создаёт экземпляры:
```ts
const authApi = createBrowserAuth()
const userApi = createBrowserUser({ authApi })
```
User не импортирует runtime-код Auth, а его React-модули не импортируют `useAuthSession` или Auth components.
## Порядок перехода
1. Определить dependency-connected набор доменов, который нужно мигрировать вместе.
2. Выделить `business` и одну фабрику без environment-specific import-графа.
3. Зафиксировать `DomainApi`, error codes, error type и runtime guard.
4. Перенести browser/server wiring в нужные presets и adapters.
5. Разделить React-ответственности на модули внутри Group `react`.
6. Перенести страницы, redirects и multi-domain UI в `compositions`.
7. Перевести внешние импорты на module-specific paths.
8. Удалить старый root `index.ts` и проверить import-граф.
Простые доменные модули могут оставаться в SLM root только как временное миграционное состояние. Они не создают прямые runtime- или type-only зависимости с уже переведёнными пакетами.

View File

@@ -0,0 +1,122 @@
# Модуль business
> Пояснение единственного runtime-источника доменных данных и результатов.
## Связанные правила
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009)
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
## Роль
`business` является обязательным SLM-модулем доменного пакета. Он владеет:
- публичными предметными сценариями;
- единым контрактом `DomainApi`;
- одной публичной фабрикой;
- типом явных зависимостей фабрики;
- предметными типами и детерминированными правилами;
- кодами, типом и runtime guard доменных ошибок;
- публичным представлением доменных данных и состояния.
Приложение получает runtime-данные, состояние и результаты домена только через экземпляр `DomainApi`. Adapter, preset или framework binding module не открывает параллельный источник доменных данных.
## Публичный API модуля
```ts
export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error'
export { authFactory } from './auth.factory'
export type {
AuthApi,
AuthDeps,
AuthError,
AuthErrorCode,
AuthFactory,
AuthState,
} from './types'
```
Фабрика, error contract и типы экспортируются для presets, adapters, framework-модулей и мест сборки графа. Предметные validators, normalizers, внутренние преобразователи исходных ошибок, constructors, mutable store и технические DTO остаются закрытыми и используются публичными сценариями `DomainApi`.
## Один DomainApi
```ts
export type AuthApi = {
getSnapshot: () => AuthState
requestPhoneOtp: (phone: string) => Promise<void>
verifyPhoneOtp: (code: string) => Promise<void>
}
export type AuthFactory = (deps: AuthDeps) => AuthApi
```
Все presets вызывают одну `authFactory` и создают `AuthApi` этого контракта. Preset может использовать другую техническую реализацию, но не добавляет метод и не меняет семантику сценария.
Точная модель хранения, initial state, подписки и SSR snapshot пока остаётся открытым вопросом. Нормативной уже является публичная граница: приложение наблюдает доменное состояние через `DomainApi`, а не напрямую через adapter или framework store.
## Обязательный контракт ошибок
Каждый `business` объявляет устойчивые коды, безопасную readonly-форму и runtime guard:
```ts
export const AUTH_ERROR_CODES = {
PHONE_INVALID: 'AUTH_PHONE_INVALID',
OTP_REQUEST_FAILED: 'AUTH_OTP_REQUEST_FAILED',
OTP_CODE_INVALID: 'AUTH_OTP_CODE_INVALID',
} as const
export type AuthErrorCode =
typeof AUTH_ERROR_CODES[keyof typeof AUTH_ERROR_CODES]
export type AuthError = Readonly<{
code: AuthErrorCode
}>
const authErrorCodes = new Set<string>(Object.values(AUTH_ERROR_CODES))
export const isAuthError = (value: unknown): value is AuthError => {
if (typeof value !== 'object' || value === null) {
return false
}
const prototype = Object.getPrototypeOf(value)
const keys = Reflect.ownKeys(value)
if (
(prototype !== Object.prototype && prototype !== null)
|| keys.length !== 1
|| keys[0] !== 'code'
|| !('code' in value)
) {
return false
}
return typeof value.code === 'string' && authErrorCodes.has(value.code)
}
```
Способ передачи ошибки, exception или discriminated `Result`, пока не закреплён. В обоих вариантах публичный сценарий сообщает ожидаемый сбой только через собственный `AuthError`.
## Изоляция технических ошибок
Ошибки SDK, HTTP, database, storage и adapters не пересекают `DomainApi` в форме, доступной приложению. `business` преобразует обрабатываемый технический сбой в собственный код:
```text
SDK error
→ adapter failure
→ business mapping
→ AuthErrorCode
→ приложение
```
Публичная форма не включает исходные `message`, `status`, `payload`, `cause`, SDK class или сам объект ошибки. Диагностические данные могут сохраняться только во внутреннем механизме observability, контракт которого будет определён отдельно.
То же относится к cross-domain вызову. Если `UserApi` использует `AuthApi`, сбой, который становится результатом публичного сценария User, представлен собственным `UserError`, а не `AuthError`.
Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки. Способ отличить их от ожидаемых cross-domain ошибок при exception-модели и их диагностическая политика остаются открытыми вопросами; исходная ошибка при этом не добавляется в публичную форму DomainError.

View File

@@ -0,0 +1,87 @@
# Граница доменного пакета
> Пояснение новой контейнерной сущности Level 2.
## Связанные правила
- [`SLM-L2-DOMAIN-R002`](../../rules/level-2.md#slm-l2-domain-r002)
- [`SLM-L2-DOMAIN-A003`](../../rules/level-2.md#slm-l2-domain-a003)
- [`SLM-L2-GROUP-R004`](../../rules/level-2.md#slm-l2-group-r004)
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
## Предметная граница
Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать business-сценарии авторизации, её browser/server presets и React-модули, но не страницу профиля или общий database client.
Пакет не владеет исполняемой ответственностью. Конкретными API, состоянием, зависимостями и lifecycle владеют модули внутри него.
## Корень пакета
```text
domains/auth/
├── README.md
├── business/
├── presets/
├── adapters/
└── react/
```
В корне разрешены:
- документация;
- ownership metadata;
- декларативный manifest или декларативная конфигурация архитектурной проверки;
- обязательный модуль `business`;
- Groups допустимых ролей.
В корне запрещены:
- `index.ts` или другой агрегирующий executable entry point;
- runtime-файлы и side effects;
- изменяемое состояние и ресурсы lifecycle;
- реэкспорт API внутренних модулей;
- page-specific компоненты или сборка нескольких доменов.
Metadata содержит только статические данные, не исполняется приложением, build tooling или проверяющим инструментом и не становится скрытым API пакета.
## Модули и Groups
`business` размещается непосредственно в пакете. Presets и самостоятельные adapters размещаются в Groups `presets` и `adapters`. Framework Group называется по фреймворку: `react`, `vue` и аналогично.
```text
auth/
├── business/ # SLM-модуль
├── presets/ # Group
│ └── browser/ # SLM-модуль
└── react/ # Framework Group
├── session/ # SLM-модуль
└── login-form/ # SLM-модуль
```
Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/presets/browser`, `auth/react/session`, но не `auth`, `auth/presets` или `auth/react`.
## Навигационные Groups
Слой `domains` может содержать навигационные Groups с пакетами:
```text
domains/
└── commerce/ # Навигационная Group
├── catalog/ # Доменный пакет
└── orders/ # Доменный пакет
```
Такая Group отличается от Group внутри пакета только допустимым составом: она содержит доменные пакеты и другие navigation Groups, а не модули.
## Границы соседних слоёв
| Ответственность | Владелец |
|---|---|
| Предметные сценарии, `DomainApi`, доменные ошибки | `business` |
| Техническая реализация зависимости одного домена | Adapter внутри пакета |
| Универсальный технический сервис | `infra` |
| Domain-specific framework API | Модуль внутри `react`, `vue` и аналогичной Group |
| Страница, маршрут, redirect, продуктовый текст | `compositions` |
| UI, объединяющий несколько доменов | `compositions` |
Зависимость от React сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.

View File

@@ -0,0 +1,86 @@
# Фабрика, зависимости и adapters
> Пояснение границы между `business` и технической средой.
## Связанные правила
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
## Одна фабрика
```text
явные зависимости + business factory → DomainApi
```
Модуль `business` предоставляет одну публичную фабрику. Она получает все runtime-возможности явными аргументами и создаёт API одного контракта независимо от выбранного preset.
```ts
export type AuthFactory = (deps: AuthDeps) => AuthApi
```
Рекомендуется сохранять сам вызов фабрики чистым: он создаёт объекты и closures, но не читает скрытое окружение и не выбирает конкретную техническую реализацию. Детальный lifecycle ресурсов будет нормирован позже.
## Технические зависимости
Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы.
```ts
export type AuthPhoneDependency = {
requestCode: (phone: string) => Promise<unknown>
verifyCode: (code: string) => Promise<unknown>
}
```
`unknown` допустим на границе непроверенного внешнего результата. `business` проверяет его до превращения в предметные данные или состояние.
Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics.
## Cross-domain API dependency
Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом:
```ts
import type { AuthApi } from '@/domains/auth/business'
export type UserDeps = {
auth: Pick<AuthApi, 'getSession'>
}
```
Runtime-значение передаёт место сборки графа через preset. `user/business` не импортирует executable API, factory или preset Auth.
## Adapter
Adapter соединяет явную зависимость фабрики с технической системой:
```text
business dependency ← adapter → SDK / storage / platform / request data
```
Adapter преобразует аргументы и технический результат к контракту зависимости. Он не определяет публичный метод `DomainApi`, предметный fallback или код доменной ошибки.
Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение никогда не строит поведение по HTTP status, SDK error class или storage exception.
## Размещение adapter
Одноразовый adapter остаётся закрытым сегментом preset-модуля:
```text
auth/presets/browser/
├── adapters/
│ └── phone.adapter.ts
└── index.ts
```
Adapter становится самостоятельным SLM-модулем, когда нужен нескольким presets или имеет отдельную integration responsibility:
```text
auth/adapters/
└── identity-provider/
└── index.ts
```
Самостоятельный adapter сохраняет минимальный публичный API и не становится альтернативным источником доменных данных для приложения.

View File

@@ -0,0 +1,115 @@
# Framework Groups и модули
> Пояснение domain-specific framework-кода на примере React.
## Связанные правила
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-FRAMEWORK-R014`](../../rules/level-2.md#slm-l2-framework-r014)
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
## Framework Group
Папка для domain-specific React binding modules называется `react`:
```text
domains/auth/react/ # Framework Group
├── session/ # SLM-модуль
│ ├── hooks/
│ ├── providers/
│ └── index.ts
└── login-form/ # SLM-модуль
├── components/
└── index.ts
```
`react` является Group, а не модулем. У неё нет `index.ts`, состояния, реализации, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. Он не является вложенным модулем, потому что родительская граница `react` является Group.
## Framework binding module
Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании готового `DomainApi` одного домена с конкретным framework. Сам факт зависимости от framework недостаточен: framework-specific preset остаётся в `presets`, а page-specific модуль остаётся в `compositions`.
Framework binding module может:
- передавать готовый `DomainApi` через Provider и context;
- предоставлять domain-specific hooks;
- отображать состояние и безопасные ошибки домена;
- реализовывать переиспользуемую domain-specific форму или guard;
- связывать framework lifecycle с публичным API домена.
Он не вызывает business-фабрику или preset, не выбирает adapters и не создаёт новые предметные сценарии.
## Модуль session
`auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthApi`:
```tsx
'use client'
type AuthSessionProviderProps = PropsWithChildren<{
api: AuthApi
}>
export const AuthSessionProvider = ({
api,
children,
}: AuthSessionProviderProps) => {
return (
<AuthSessionContext.Provider value={api}>
{children}
</AuthSessionContext.Provider>
)
}
```
Публичный путь модуля:
```ts
import {
AuthSessionProvider,
useAuthSession,
} from '@/domains/auth/react/session'
```
Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API.
## Модуль login-form
`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с `AuthApi`, AuthState и AuthError. Она может использовать публичный API соседнего `auth/react/session`, если зависимость остаётся ацикличной.
Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Поэтому domain-owned `AuthGuard` может решить, разрешён ли доступ, но политика перехода на `/login` остаётся у route composition.
## Запрет cross-domain framework imports
Framework binding module не импортирует hooks, contexts, Providers, components или framework state другого доменного пакета:
```ts
// Недопустимо: domains/user/react/profile
import { useAuthSession } from '@/domains/auth/react/session'
```
Cross-domain UI собирается в `compositions`:
```tsx
const session = useAuthSession()
return (
<UserProfile
userId={session.userId}
canEdit={session.isAuthenticated}
/>
)
```
Передача через props является границей композиции, но не требует prop drilling внутри домена: каждый пакет может использовать собственный Provider и context. Если User business постоянно нуждается в Auth, готовый `AuthApi` передаётся User factory при сборке графа, а User framework module работает уже со своим `UserApi`.
## Публичные API
```ts
import { AuthSessionProvider } from '@/domains/auth/react/session'
import { LoginForm } from '@/domains/auth/react/login-form'
```
Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена.

View File

@@ -0,0 +1,43 @@
# Открытые вопросы Level 2
> Эти вопросы не являются правилами и не отменяют зафиксированные границы.
## Зафиксированные решения
- Level 1 включает слой `domains` и простые доменные модули.
- Level 2 заменяет доменный модуль доменным пакетом.
- Корень пакета содержит только metadata, модули и Groups и не имеет executable API.
- `business` предоставляет одну фабрику и один `DomainApi`.
- Приложение получает доменные данные, состояние и результаты только через `DomainApi`.
- Каждый `business` экспортирует коды, тип и runtime guard доменных ошибок.
- Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена.
- Количество presets определяется реальными окружениями; универсальный preset не обязателен.
- Runtime cross-domain imports запрещены; type-only business contracts разрешены и входят в DAG.
- Framework Group называется по фреймворку и содержит самостоятельные SLM-модули.
- Cross-domain framework state, hooks, contexts и components не импортируются.
## Владение состоянием
Нужно определить создание initial state, применение transitions, persistence и внешний event input. Нормативным уже является только то, что приложение читает состояние через `DomainApi`, но точная граница между business state machine и storage adapter пока не выбрана.
Отдельно требуется проверить SSR snapshot, hydration, concurrent rendering, reset и поведение после завершения request scope.
## Передача ошибок
Нужно выбрать общую политику exception или discriminated `Result`, определить cancellation и unexpected failures, а также сериализацию ошибок через RPC и server actions.
Для exception-модели отдельно нужно определить, как зависимый business отличает ожидаемую ошибку другого домена без runtime-импорта его guard: через переданный discriminator, wrapper места сборки или другой явный контракт.
## Технические порты
Нужно определить, является ли consumer-owned port обязательной формой каждой технической зависимости и какие behavioral guarantees он обязан описывать: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
Cross-domain `Pick<OtherDomainApi>` уже зафиксирован как отдельный вид API dependency и не зависит от этого решения.
## Lifecycle сборки
Нужно определить контракты `start` и `dispose`, async cleanup, rollback частичного старта, repeated disposal, request abort и поведение API после завершения scope. До решения применяется только общее владение lifecycle Level 1.
## Автоматическая проверка
Нужно выбрать machine-readable формат для domain packages, metadata, модулей, Groups, entry points, environment labels и запрещённых транзитивных импортов.

View File

@@ -0,0 +1,101 @@
# Presets и среды выполнения
> Пояснение повторяемых сборок одного `DomainApi`.
## Связанные правила
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-PRESET-R011`](../../rules/level-2.md#slm-l2-preset-r011)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013)
## Назначение
Preset является SLM-модулем в Group `presets`. Он создаёт API одной business-фабрики для конкретного повторяемого контекста выполнения.
```text
authFactory
├── presets/browser → AuthApi в браузере
├── presets/request → AuthApi одного server request
└── presets/server-action → AuthApi server action
```
Архитектура не требует обязательный `base` или изоморфный preset и не ограничивает количество presets. Проект создаёт только те сборки, которые нужны его реальным средам и областям использования.
Если фабрика используется в одном месте и отдельная повторяемая конфигурация не возникает, место сборки графа может вызвать её напрямую.
## Один контракт API
Каждый preset выбирает технические реализации, но вызывает одну и ту же фабрику и возвращает один контракт `DomainApi`:
```ts
export const createBrowserAuth = (): AuthApi => {
return authFactory({
phone: createHttpPhoneAdapter(),
session: createBrowserSessionAdapter(),
})
}
```
```ts
export const createAuthForRequest = (
input: AuthRequestInput,
): AuthApi => {
return authFactory({
phone: createServerPhoneAdapter(input),
session: createRequestSessionAdapter(input),
})
}
```
Server preset может обращаться к database напрямую через adapter, а browser preset реализует тот же сценарий через HTTP или RPC. Preset не добавляет server-only метод к `AuthApi` и не меняет доменные ошибки.
Если полный `DomainApi` невозможно корректно создать в некоторой среде, пакет просто не предоставляет preset для этой среды. Метод, намеренно падающий только потому, что среда не поддерживается, не считается реализацией контракта.
## Cross-domain input
Preset зависимого домена принимает готовый API аргументом:
```ts
import type { AuthApi } from '@/domains/auth/business'
export type CreateUserForRequestInput = {
authApi: Pick<AuthApi, 'getSession'>
request: UserRequestInput
}
export const createUserForRequest = ({
authApi,
request,
}: CreateUserForRequestInput): UserApi => {
return userFactory({
auth: authApi,
profile: createUserProfileAdapter(request),
})
}
```
Preset делает только type-only импорт `AuthApi`. Runtime-фабрику, preset или instance Auth он не импортирует.
Место сборки графа выполняет сборку:
```ts
const authApi = createAuthForRequest(authInput)
const userApi = createUserForRequest({ authApi, request: userInput })
```
## Environment entry points
Server preset имеет отдельный публичный entry point и marker выбранного framework или bundler:
```ts
import 'server-only'
export { createAuthForRequest } from './create-auth-for-request'
```
Server entry point не реэкспортируется через `business`, Framework Group, browser preset или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость.
## Lifecycle
Preset может создавать ресурсы, которым потребуется запуск или cleanup, но точная форма `start`, `dispose`, rollback и request abort пока не нормирована. До принятия решения действует общее правило владения lifecycle Level 1.

View File

@@ -0,0 +1,56 @@
# Тестирование доменного пакета
> Проверка владельцев и публичных границ Level 2.
## Связанное правило
- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016)
## Размещение
Тест находится рядом с модулем-владельцем проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`.
| Проверяемая граница | Владелец теста |
|---|---|
| Предметные сценарии, `DomainApi`, данные и ошибки | `business` |
| Техническое преобразование | Adapter |
| Выбор зависимостей и environment boundary | Preset |
| Provider, hook, form или guard | Соответствующий framework binding module |
| Граф нескольких доменов | Модуль `composition`, точка входа `app` или другое место сборки |
## Business через фабрику
Каждый публичный предметный сценарий проверяется через единственную фабрику с управляемыми зависимостями:
```ts
const api = authFactory(createAuthTestDeps({
requestCode: async () => ({ ok: true }),
}))
await api.requestPhoneOtp('+79991112233')
```
Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от будущего решения `throw` или `Result`, но наружу всегда проверяется только AuthErrorCode.
Business-тест не использует React, реальный SDK, database или production preset.
## Остальные модули
Adapter-тест проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки.
Preset-тест проверяет выбранные реализации, вызов одной фабрики, контракт возвращённого `DomainApi` и отсутствие несовместимого environment-кода.
Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthApi` и проверяет Provider, hook или component. `login-form` не повторяет полный набор business-сценариев.
## Архитектурные проверки
Отдельная import-graph проверка подтверждает:
- отсутствие root API доменного пакета и Framework Groups;
- отсутствие runtime cross-domain imports;
- отсутствие type-only импортов из чужих presets, adapters и framework-модулей;
- отсутствие cross-domain framework hooks, contexts и components;
- отсутствие server-only достижимости из client modules;
- отсутствие runtime- и type-only циклов.
Runtime-тест не заменяет эти проверки.