This commit is contained in:
2026-07-30 20:48:05 +03:00
parent d3b37eb4bd
commit 4ce6bae61c
23 changed files with 340 additions and 1694 deletions

View File

@@ -13,6 +13,7 @@ Level 2 соблюдает определения и правила Level 1, к
| Порядок `app → compositions → domains → infra → ui → shared` | Сохраняется | | Порядок `app → compositions → domains → infra → ui → shared` | Сохраняется |
| Модуль, Group, сегмент, компонент, публичный API и граф зависимостей | Сохраняют смысл | | Модуль, Group, сегмент, компонент, публичный API и граф зависимостей | Сохраняют смысл |
| Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются доменным пакетом | | Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются доменным пакетом |
| Единый публичный API модуля `business` | Представлен тремя объявленными фасетами одного логического API |
| Навигационная Group слоя `domains` | Может содержать доменные пакеты | | Навигационная Group слоя `domains` | Может содержать доменные пакеты |
| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups | | Group внутри доменного пакета | Содержит обычные SLM-модули и Groups |
@@ -22,7 +23,7 @@ Level 2 соблюдает определения и правила Level 1, к
Level 2 оправдан, когда предметной области нужны один устойчивый `DomainApi`, разные сборки для браузера и сервера, несколько технических интеграций или самостоятельные domain-specific модули React, Vue либо другого фреймворка. Level 2 оправдан, когда предметной области нужны один устойчивый `DomainApi`, разные сборки для браузера и сервера, несколько технических интеграций или самостоятельные domain-specific модули React, Vue либо другого фреймворка.
Уровень выбирается для всего SLM root. Проект, в котором всем предметным областям достаточно простых доменных модулей, остаётся на Level 1. После завершённого перехода на Level 2 каждая предметная область представлена доменным пакетом, даже если отдельный пакет имеет только `business`. Уровень выбирается для всего SLM root. Проект, в котором всем предметным областям достаточно простых доменных модулей, остаётся на Level 1. После завершённого перехода на Level 2 каждая предметная область представлена доменным пакетом как минимум с `business` и одним preset.
Размер каталога сам по себе не требует перехода. Размер каталога сам по себе не требует перехода.
@@ -33,11 +34,13 @@ src/domains/
└── auth/ # Доменный пакет └── auth/ # Доменный пакет
├── README.md # Необязательная metadata ├── README.md # Необязательная metadata
├── business/ # Обязательный SLM-модуль ├── business/ # Обязательный SLM-модуль
── index.ts ── index.ts # Только public types
├── presets/ # Необязательная Group │ ├── factory.ts # Public factory entry
│ └── error.ts # Public error runtime entry
├── presets/ # Обязательная непустая Group
│ ├── browser/ # SLM-модуль │ ├── browser/ # SLM-модуль
│ └── request/ # SLM-модуль │ └── request/ # SLM-модуль
├── adapters/ # Необязательная Group ├── adapters/ # При наличии technical dependencies
│ └── identity-provider/ # SLM-модуль │ └── identity-provider/ # SLM-модуль
└── react/ # Необязательная framework Group └── react/ # Необязательная framework Group
├── session/ # SLM-модуль ├── session/ # SLM-модуль
@@ -51,13 +54,15 @@ src/domains/
Приложение получает данные, состояние и результаты домена через готовый экземпляр `DomainApi`. Технический код импортирует только API конкретного модуля: Приложение получает данные, состояние и результаты домена через готовый экземпляр `DomainApi`. Технический код импортирует только API конкретного модуля:
```ts ```ts
import { authFactory, isAuthError } from '@/domains/auth/business' import type { AuthApi, AuthError } from '@/domains/auth/business'
import { authFactory } from '@/domains/auth/business/factory'
import { isAuthError } from '@/domains/auth/business/error'
import { createBrowserAuth } from '@/domains/auth/presets/browser' import { createBrowserAuth } from '@/domains/auth/presets/browser'
import { AuthSessionProvider } from '@/domains/auth/react/session' import { AuthSessionProvider } from '@/domains/auth/react/session'
import { LoginForm } from '@/domains/auth/react/login-form' import { LoginForm } from '@/domains/auth/react/login-form'
``` ```
Общие импорты `@/domains/auth` и `@/domains/auth/react` запрещены: пакет и Groups не имеют агрегирующих API. Общие импорты `@/domains/auth` и `@/domains/auth/react` запрещены: пакет и Groups не имеют агрегирующих API. Другие пути внутри `business`, кроме `business`, `business/factory` и `business/error`, являются deep imports.
## Миграция ## Миграция

View File

@@ -7,23 +7,30 @@
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007) - [`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-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013) - [`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-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) - [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
## Направление внутри пакета ## Направление внутри пакета
| Исходный модуль | Допустимые зависимости | | Исходный модуль | Допустимые зависимости |
|---|---| |---|---|
| `business` | Собственные сегменты, объявленный нейтральный `shared`, объявленные business-safe внешние пакеты, type-only публичные business-контракты других доменов | | `business` | Собственные файлы, объявленный нейтральный `shared`, business-safe внешние пакеты, type-only `business` других доменов |
| Adapter | Собственный `business`, `infra`, конкретная техническая реализация, `shared` | | Adapter module | Type-only barrel собственного `business`, `infra`, конкретная техническая реализация, `shared` |
| Preset | Собственный `business`, закрытые или самостоятельные adapters, type-only API других доменов | | Preset | Type-only barrel и `factory` собственного `business`, публичные adapter-модули своего домена, type-only API других доменов |
| Framework binding module | Собственный `business`, публичные API framework-модулей своего домена, фреймворк, `ui`, `shared` | | Framework binding module | Type-only barrel и `error` собственного `business`, публичные framework-модули своего домена, фреймворк, `ui`, `shared` |
| Место сборки графа | Публичные API presets и framework-модулей всех входящих в граф доменов | | Место сборки графа | Presets либо `business/factory` и adapter-модули, `business/error`, framework-модули входящих в граф доменов |
`business` не достигает adapters, presets, framework-модулей, product SDK, storage, API браузера или Node.js. Проверяется весь транзитивный import-граф его публичной точки входа. `business` не достигает adapters, presets, framework-модулей, product SDK, storage, API браузера или Node.js. Проверяется весь транзитивный import-граф трёх публичных фасетов.
Adapter module импортирует из собственного `business` только типы технических зависимостей. Он не импортирует `business/factory` или `business/error`, потому что не собирает API и не создаёт доменные ошибки.
Preset не содержит inline adapters. Он импортирует production implementations через публичные API конкретных модулей `adapters/*`.
## Междоменные импорты ## Междоменные импорты
Модуль одного доменного пакета не импортирует runtime-экспорты другого доменного пакета. Разрешён только type-only импорт публичного контракта его `business`, по возможности суженный через `Pick`. Модуль одного доменного пакета не импортирует runtime-экспорты другого доменного пакета. Разрешён только type-only импорт корневого barrel его `business`, по возможности суженный через `Pick`. Чужие `business/factory` и `business/error` являются runtime entry points и запрещены.
```ts ```ts
import type { AuthApi } from '@/domains/auth/business' import type { AuthApi } from '@/domains/auth/business'
@@ -39,7 +46,7 @@ Pure function, hook, Provider, context, component или framework state дру
## Runtime-инъекция API ## Runtime-инъекция API
Готовый API другого домена передаётся preset-модулю аргументом. Preset не импортирует его runtime-фабрику или сборку: Готовый API другого домена передаётся preset-модулю или одноразовому месту сборки аргументом. Код зависимого доменного пакета не импортирует его runtime-фабрику или сборку:
```text ```text
createAuthForRequest() createAuthForRequest()

View File

@@ -5,8 +5,8 @@
```text ```text
domains/auth/ domains/auth/
├── business/ ├── business/
├── presets/ ├── presets/ # Обязательная Group
├── adapters/ ├── adapters/ # При наличии technical dependencies
└── react/ └── react/
├── session/ ├── session/
└── login-form/ └── login-form/
@@ -15,9 +15,9 @@ domains/auth/
## Основные границы ## Основные границы
- [Доменный пакет](./domain-package.md) определяет предметную и структурную границу. - [Доменный пакет](./domain-package.md) определяет предметную и структурную границу.
- [Business](./business.md) владеет `DomainApi`, фабрикой и доменными ошибками. - [Business](./business.md) владеет `DomainApi` и разделяет public types, factory и error runtime по трём фасетам.
- [Фабрика, зависимости и adapters](./factory-ports-adapters.md) отделяют business от технической реализации. - [Фабрика, зависимости и adapters](./factory-ports-adapters.md) требуют отдельный SLM-модуль для каждой production adapter implementation.
- [Presets](./presets.md) собирают один API для нужных окружений. - [Presets](./presets.md) обязательны и собирают один API для нужных окружений.
- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка. - [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу. - [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу.
- [Миграция auth](./auth-example.md) показывает переход с Level 1. - [Миграция auth](./auth-example.md) показывает переход с Level 1.

View File

@@ -5,6 +5,10 @@
## Связанное правило ## Связанное правило
- [`SLM-L2-MIGRATION-A017`](../../rules/level-2.md#slm-l2-migration-a017) - [`SLM-L2-MIGRATION-A017`](../../rules/level-2.md#slm-l2-migration-a017)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-PRESET-A020`](../../rules/level-2.md#slm-l2-preset-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
## Исходная форма Level 1 ## Исходная форма Level 1
@@ -29,10 +33,18 @@ domains/auth/ # Доменный пакет
│ ├── lib/ │ ├── lib/
│ ├── services/ │ ├── services/
│ ├── types/ │ ├── types/
│ ├── index.ts # Только public types
│ ├── factory.ts # Public factory entry
│ └── error.ts # Public error runtime entry
├── adapters/ # Group
│ ├── phone-http/ # SLM-модуль
│ │ └── index.ts
│ ├── browser-session/ # SLM-модуль
│ │ └── index.ts
│ └── request-session/ # SLM-модуль
│ └── index.ts │ └── index.ts
├── presets/ # Group ├── presets/ # Обязательная Group
│ ├── browser/ # SLM-модуль │ ├── browser/ # SLM-модуль
│ │ ├── adapters/
│ │ └── index.ts │ │ └── index.ts
│ └── request/ # SLM-модуль │ └── request/ # SLM-модуль
│ └── index.ts │ └── index.ts
@@ -47,20 +59,35 @@ domains/auth/ # Доменный пакет
## Перенос ответственности ## Перенос ответственности
| Исходная часть | Владелец Level 2 | | Исходная часть | Владелец Level 2 | Публичный путь |
|---|---| |---|---|---|
| Сценарии, предметные типы, единый API | `auth/business` | | Сценарии и public types | `auth/business` | `auth/business` |
| Коды, тип и guard ошибок | `auth/business` | | Runtime-фабрика | `auth/business` | `auth/business/factory` |
| Browser storage и HTTP adapters | `auth/presets/browser` | | Коды и guards ошибок | `auth/business` | `auth/business/error` |
| Cookies, request data и server adapters | `auth/presets/request` | | Browser storage и HTTP adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
| Provider и session hooks | `auth/react/session` | | Cookies, request data и server adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
| Переиспользуемая форма | `auth/react/login-form` | | Выбор browser implementations | `auth/presets/browser` | `auth/presets/browser` |
| Страница, текст и redirect | `compositions` | | Выбор request implementations | `auth/presets/request` | `auth/presets/request` |
| Provider и session hooks | `auth/react/session` | `auth/react/session` |
| Переиспользуемая форма | `auth/react/login-form` | `auth/react/login-form` |
| Страница, текст и redirect | `compositions` | API конкретной composition |
## Новые импорты ## Новые импорты
```ts ```ts
import { authFactory, isAuthError } from '@/domains/auth/business' import type {
AuthApi,
AuthError,
AuthErrorCode,
} from '@/domains/auth/business'
import { authFactory } from '@/domains/auth/business/factory'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/error'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserAuth } from '@/domains/auth/presets/browser' import { createBrowserAuth } from '@/domains/auth/presets/browser'
import { AuthSessionProvider } from '@/domains/auth/react/session' import { AuthSessionProvider } from '@/domains/auth/react/session'
import { LoginForm } from '@/domains/auth/react/login-form' import { LoginForm } from '@/domains/auth/react/login-form'
@@ -90,12 +117,13 @@ User не импортирует runtime-код Auth, а его React-модул
## Порядок перехода ## Порядок перехода
1. Определить dependency-connected набор доменов, который нужно мигрировать вместе. 1. Определить dependency-connected набор доменов, который нужно мигрировать вместе.
2. Выделить `business` и одну фабрику без environment-specific import-графа. 2. Выделить `business` и три публичных фасета: type-only barrel, `factory` и `error`.
3. Зафиксировать `DomainApi`, error codes, error type и runtime guard. 3. Зафиксировать `DomainApi`, error codes, error type и runtime guard.
4. Перенести browser/server wiring в нужные presets и adapters. 4. Оформить каждую production implementation отдельным модулем `adapters/*`.
5. Разделить React-ответственности на модули внутри Group `react`. 5. Создать минимум один preset и перенести туда повторяемый выбор adapter-модулей.
6. Перенести страницы, redirects и multi-domain UI в `compositions`. 6. Разделить React-ответственности на модули внутри Group `react`.
7. Перевести внешние импорты на module-specific paths. 7. Перенести страницы, redirects и multi-domain UI в `compositions`.
8. Удалить старый root `index.ts` и проверить import-граф. 8. Перевести внешние импорты на разрешённые public paths.
9. Удалить старый root `index.ts` и проверить import-граф.
Простые доменные модули могут оставаться в SLM root только как временное миграционное состояние. Они не создают прямые runtime- или type-only зависимости с уже переведёнными пакетами. Простые доменные модули могут оставаться в SLM root только как временное миграционное состояние. Они не создают прямые runtime- или type-only зависимости с уже переведёнными пакетами.

View File

@@ -11,6 +11,8 @@
- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009) - [`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-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-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)
## Роль ## Роль
@@ -28,10 +30,13 @@
## Публичный API модуля ## Публичный API модуля
```ts Один логический API `business` разделён на три фиксированных фасета.
export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error'
export { authFactory } from './auth.factory'
### Type-only barrel
Корневой `business/index.ts` экспортирует только типы:
```ts
export type { export type {
AuthApi, AuthApi,
AuthDeps, AuthDeps,
@@ -42,7 +47,57 @@ export type {
} from './types' } from './types'
``` ```
Фабрика, error contract и типы экспортируются для presets, adapters, framework-модулей и мест сборки графа. Предметные validators, normalizers, внутренние преобразователи исходных ошибок, constructors, mutable store и технические DTO остаются закрытыми и используются публичными сценариями `DomainApi`. Потребитель использует этот путь только через `import type`:
```ts
import type {
AuthApi,
AuthError,
AuthErrorCode,
} from '@/domains/auth/business'
```
### Factory entry
`business/factory.ts` экспортирует только runtime-фабрику:
```ts
export { authFactory } from './auth.factory'
```
```ts
import { authFactory } from '@/domains/auth/business/factory'
```
### Error entry
`business/error.ts` экспортирует только runtime-коды и guards:
```ts
export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error'
```
```ts
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/error'
```
`AuthError` и `AuthErrorCode` не реэкспортируются из `business/error`: все public types имеют один канонический путь через type-only barrel. Предметные validators, normalizers, constructors ошибок, source-error mappers, mutable store и технические DTO остаются закрытыми.
Другие внешние пути внутри `business` являются deep imports. Файлы `factory.ts` и `error.ts` являются фасетами одного SLM-модуля, а не сегментами или вложенными модулями.
## Потребители фасетов
| Потребитель | `business` | `business/factory` | `business/error` |
|---|---|---|---|
| Adapter module своего домена | Type-only | Нет | Нет |
| Preset своего домена | Type-only | Да | Нет |
| Framework binding module своего домена | Type-only | Нет | Да |
| `composition` или `app` | Type-only | Да | Да |
| Модуль другого доменного пакета | Type-only | Нет | Нет |
| Тест | Type-only | По границе тестируемого владельца | По границе тестируемого владельца |
## Один DomainApi ## Один DomainApi
@@ -56,13 +111,13 @@ export type AuthApi = {
export type AuthFactory = (deps: AuthDeps) => AuthApi export type AuthFactory = (deps: AuthDeps) => AuthApi
``` ```
Все presets вызывают одну `authFactory` и создают `AuthApi` этого контракта. Preset может использовать другую техническую реализацию, но не добавляет метод и не меняет семантику сценария. Все presets вызывают одну `authFactory` и создают `AuthApi` этого контракта. Preset может использовать другую техническую реализацию, но не добавляет метод и не меняет семантику сценария. Одноразовое место сборки в `composition` также может вызвать `business/factory`, используя публичные adapter-модули пакета, если фабрика имеет технические зависимости.
Точная модель хранения, initial state, подписки и SSR snapshot пока остаётся открытым вопросом. Нормативной уже является публичная граница: приложение наблюдает доменное состояние через `DomainApi`, а не напрямую через adapter или framework store. Точная модель хранения, initial state, подписки и SSR snapshot пока остаётся открытым вопросом. Нормативной уже является публичная граница: приложение наблюдает доменное состояние через `DomainApi`, а не напрямую через adapter или framework store.
## Обязательный контракт ошибок ## Обязательный контракт ошибок
Каждый `business` объявляет устойчивые коды, безопасную readonly-форму и runtime guard: Каждый `business` объявляет устойчивые коды, безопасную readonly-форму и runtime guard. Типы публикуются через `business`, а runtime symbols через `business/error`:
```ts ```ts
export const AUTH_ERROR_CODES = { export const AUTH_ERROR_CODES = {

View File

@@ -8,6 +8,10 @@
- [`SLM-L2-DOMAIN-A003`](../../rules/level-2.md#slm-l2-domain-a003) - [`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-GROUP-R004`](../../rules/level-2.md#slm-l2-group-r004)
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005) - [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-PRESET-A020`](../../rules/level-2.md#slm-l2-preset-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)
## Предметная граница ## Предметная граница
@@ -32,7 +36,9 @@ domains/auth/
- ownership metadata; - ownership metadata;
- декларативный manifest или декларативная конфигурация архитектурной проверки; - декларативный manifest или декларативная конфигурация архитектурной проверки;
- обязательный модуль `business`; - обязательный модуль `business`;
- Groups допустимых ролей. - обязательная непустая Group `presets`;
- непустая Group `adapters`, если фабрика имеет технические зависимости;
- Framework Groups при наличии соответствующих модулей.
В корне запрещены: В корне запрещены:
@@ -46,19 +52,24 @@ Metadata содержит только статические данные, не
## Модули и Groups ## Модули и Groups
`business` размещается непосредственно в пакете. Presets и самостоятельные adapters размещаются в Groups `presets` и `adapters`. Framework Group называется по фреймворку: `react`, `vue` и аналогично. `business` размещается непосредственно в пакете и предоставляет три публичных фасета: type-only barrel, `factory` и `error`. Presets размещаются в обязательной Group `presets`. Все production adapters являются самостоятельными модулями Group `adapters` и не определяются в других частях production-графа. Framework Group называется по фреймворку: `react`, `vue` и аналогично.
```text ```text
auth/ auth/
├── business/ # SLM-модуль ├── business/ # SLM-модуль
├── presets/ # Group │ ├── index.ts # Только public types
│ ├── factory.ts # Public factory entry
│ └── error.ts # Public error runtime entry
├── adapters/ # Group при наличии technical dependencies
│ └── phone-http/ # SLM-модуль
├── presets/ # Обязательная Group
│ └── browser/ # SLM-модуль │ └── browser/ # SLM-модуль
└── react/ # Framework Group └── react/ # Framework Group
├── session/ # SLM-модуль ├── session/ # SLM-модуль
└── login-form/ # SLM-модуль └── login-form/ # SLM-модуль
``` ```
Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/presets/browser`, `auth/react/session`, но не `auth`, `auth/presets` или `auth/react`. Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/business/factory`, `auth/business/error`, `auth/adapters/phone-http`, `auth/presets/browser` и `auth/react/session`, но не `auth`, `auth/adapters`, `auth/presets` или `auth/react`.
## Навигационные Groups ## Навигационные Groups

View File

@@ -8,6 +8,9 @@
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008) - [`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-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) - [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`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)
## Одна фабрика ## Одна фабрика
@@ -18,9 +21,17 @@
Модуль `business` предоставляет одну публичную фабрику. Она получает все runtime-возможности явными аргументами и создаёт API одного контракта независимо от выбранного preset. Модуль `business` предоставляет одну публичную фабрику. Она получает все runtime-возможности явными аргументами и создаёт API одного контракта независимо от выбранного preset.
```ts ```ts
import type { AuthApi, AuthDeps } from '@/domains/auth/business'
export type AuthFactory = (deps: AuthDeps) => AuthApi export type AuthFactory = (deps: AuthDeps) => AuthApi
``` ```
Runtime-фабрика импортируется только через отдельный entry point:
```ts
import { authFactory } from '@/domains/auth/business/factory'
```
Рекомендуется сохранять сам вызов фабрики чистым: он создаёт объекты и closures, но не читает скрытое окружение и не выбирает конкретную техническую реализацию. Детальный lifecycle ресурсов будет нормирован позже. Рекомендуется сохранять сам вызов фабрики чистым: он создаёт объекты и closures, но не читает скрытое окружение и не выбирает конкретную техническую реализацию. Детальный lifecycle ресурсов будет нормирован позже.
## Технические зависимости ## Технические зависимости
@@ -50,11 +61,11 @@ export type UserDeps = {
} }
``` ```
Runtime-значение передаёт место сборки графа через preset. `user/business` не импортирует executable API, factory или preset Auth. Runtime-значение место сборки графа передаёт через preset либо напрямую зависимой business-фабрике. `user/business` не импортирует executable API, factory или preset Auth.
## Adapter ## Adapter module
Adapter соединяет явную зависимость фабрики с технической системой: Adapter module соединяет явную техническую зависимость фабрики с конкретной системой:
```text ```text
business dependency ← adapter → SDK / storage / platform / request data business dependency ← adapter → SDK / storage / platform / request data
@@ -64,23 +75,40 @@ Adapter преобразует аргументы и технический ре
Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение никогда не строит поведение по HTTP status, SDK error class или storage exception. Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение никогда не строит поведение по HTTP status, SDK error class или storage exception.
## Размещение adapter ## Размещение adapters
Одноразовый adapter остаётся закрытым сегментом preset-модуля: Каждая production-реализация является отдельным SLM-модулем в Group `adapters`, даже если пока используется одним preset:
```text
auth/presets/browser/
├── adapters/
│ └── phone.adapter.ts
└── index.ts
```
Adapter становится самостоятельным SLM-модулем, когда нужен нескольким presets или имеет отдельную integration responsibility:
```text ```text
auth/adapters/ auth/adapters/
── identity-provider/ ── phone-http/
│ └── index.ts
└── browser-session/
└── index.ts └── index.ts
``` ```
Самостоятельный adapter сохраняет минимальный публичный API и не становится альтернативным источником доменных данных для приложения. Adapter module имеет собственные ответственность, публичный API, environment label и тестовую границу. Group `adapters` не имеет `index.ts` и не реэкспортирует дочерние модули.
Production adapter запрещено определять:
- закрытым сегментом preset;
- inline-функцией в `composition` или `app`;
- частью framework binding module;
- скрытой реализацией внутри `business`.
Preset и одноразовое место сборки импортируют конкретные adapter-модули через их публичные API:
```ts
import { authFactory } from '@/domains/auth/business/factory'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserSessionAdapter } from '@/domains/auth/adapters/browser-session'
const authApi = authFactory({
phone: createPhoneHttpAdapter(),
session: createBrowserSessionAdapter(),
})
```
Если фабрика не имеет технических зависимостей, Group `adapters` не обязательна. Cross-domain API dependency не считается adapter и передаётся отдельно.
Локальные fake implementations в business-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код.

View File

@@ -7,6 +7,8 @@
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) - [`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-R014`](../../rules/level-2.md#slm-l2-framework-r014)
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015) - [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
## Framework Group ## Framework Group
@@ -41,6 +43,22 @@ Framework binding module может:
Он не вызывает business-фабрику или preset, не выбирает adapters и не создаёт новые предметные сценарии. Он не вызывает business-фабрику или preset, не выбирает adapters и не создаёт новые предметные сценарии.
Framework binding module импортирует типы и runtime error contract через разные фасеты:
```ts
import type {
AuthApi,
AuthError,
} from '@/domains/auth/business'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/error'
```
Импорт `business/factory` из Framework Group запрещён: готовый `DomainApi` передаётся модулю извне.
## Модуль session ## Модуль session
`auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthApi`: `auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthApi`:

View File

@@ -8,10 +8,14 @@
- Level 2 заменяет доменный модуль доменным пакетом. - Level 2 заменяет доменный модуль доменным пакетом.
- Корень пакета содержит только metadata, модули и Groups и не имеет executable API. - Корень пакета содержит только metadata, модули и Groups и не имеет executable API.
- `business` предоставляет одну фабрику и один `DomainApi`. - `business` предоставляет одну фабрику и один `DomainApi`.
- Публичный API `business` разделён на type-only barrel, `business/factory` и `business/error`; другие пути запрещены.
- Приложение получает доменные данные, состояние и результаты только через `DomainApi`. - Приложение получает доменные данные, состояние и результаты только через `DomainApi`.
- Каждый `business` экспортирует коды, тип и runtime guard доменных ошибок. - Каждый `business` экспортирует коды, тип и runtime guard доменных ошибок.
- Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена. - Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена.
- Количество presets определяется реальными окружениями; универсальный preset не обязателен. - Каждый доменный пакет содержит минимум один preset; универсальный изоморфный preset не обязателен.
- При наличии технических зависимостей Group `adapters` обязательна, а каждая production implementation является отдельным SLM-модулем.
- Все production consumers используют публичные adapter-модули; inline adapter implementations вне Group `adapters` запрещены.
- Одноразовая composition может вызвать `business/factory` напрямую; это не отменяет обязательный preset пакета.
- Runtime cross-domain imports запрещены; type-only business contracts разрешены и входят в DAG. - Runtime cross-domain imports запрещены; type-only business contracts разрешены и входят в DAG.
- Framework Group называется по фреймворку и содержит самостоятельные SLM-модули. - Framework Group называется по фреймворку и содержит самостоятельные SLM-модули.
- Cross-domain framework state, hooks, contexts и components не импортируются. - Cross-domain framework state, hooks, contexts и components не импортируются.

View File

@@ -8,10 +8,14 @@
- [`SLM-L2-PRESET-R011`](../../rules/level-2.md#slm-l2-preset-r011) - [`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-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013) - [`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-PRESET-A020`](../../rules/level-2.md#slm-l2-preset-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)
## Назначение ## Назначение
Preset является SLM-модулем в Group `presets`. Он создаёт API одной business-фабрики для конкретного повторяемого контекста выполнения. Preset является SLM-модулем в Group `presets`. Он создаёт API одной business-фабрики для конкретного повторяемого контекста выполнения. Каждый доменный пакет содержит минимум один preset-модуль.
```text ```text
authFactory authFactory
@@ -20,29 +24,39 @@ authFactory
└── presets/server-action → AuthApi server action └── presets/server-action → AuthApi server action
``` ```
Архитектура не требует обязательный `base` или изоморфный preset и не ограничивает количество presets. Проект создаёт только те сборки, которые нужны его реальным средам и областям использования. Архитектура не требует `base` или изоморфный preset и не ограничивает максимальное количество presets. Обязательный preset должен соответствовать реальному поддерживаемому контексту, а не существовать только для заполнения структуры.
Если фабрика используется в одном месте и отдельная повторяемая конфигурация не возникает, место сборки графа может вызвать её напрямую. Место сборки графа в `composition` может вызвать фабрику напрямую для одноразовой конфигурации. Такая сборка не отменяет обязательный preset пакета и при наличии технических зависимостей использует публичные adapter-модули, а не inline implementations.
## Один контракт API ## Один контракт API
Каждый preset выбирает технические реализации, но вызывает одну и ту же фабрику и возвращает один контракт `DomainApi`: Каждый preset вызывает одну и ту же фабрику и возвращает один контракт `DomainApi`. При наличии технических зависимостей preset выбирает их публичные adapter-модули:
```ts ```ts
import type { AuthApi } from '@/domains/auth/business'
import { authFactory } from '@/domains/auth/business/factory'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserSessionAdapter } from '@/domains/auth/adapters/browser-session'
export const createBrowserAuth = (): AuthApi => { export const createBrowserAuth = (): AuthApi => {
return authFactory({ return authFactory({
phone: createHttpPhoneAdapter(), phone: createPhoneHttpAdapter(),
session: createBrowserSessionAdapter(), session: createBrowserSessionAdapter(),
}) })
} }
``` ```
```ts ```ts
import type { AuthApi } from '@/domains/auth/business'
import { authFactory } from '@/domains/auth/business/factory'
import { createRequestPhoneAdapter } from '@/domains/auth/adapters/request-phone'
import { createRequestSessionAdapter } from '@/domains/auth/adapters/request-session'
export const createAuthForRequest = ( export const createAuthForRequest = (
input: AuthRequestInput, input: AuthRequestInput,
): AuthApi => { ): AuthApi => {
return authFactory({ return authFactory({
phone: createServerPhoneAdapter(input), phone: createRequestPhoneAdapter(input),
session: createRequestSessionAdapter(input), session: createRequestSessionAdapter(input),
}) })
} }
@@ -54,10 +68,13 @@ Server preset может обращаться к database напрямую че
## Cross-domain input ## Cross-domain input
Preset зависимого домена принимает готовый API аргументом: Preset зависимого домена принимает готовый API аргументом. Cross-domain API не является adapter и не размещается в Group `adapters`:
```ts ```ts
import type { AuthApi } from '@/domains/auth/business' import type { AuthApi } from '@/domains/auth/business'
import type { UserApi } from '@/domains/user/business'
import { userFactory } from '@/domains/user/business/factory'
import { createUserProfileAdapter } from '@/domains/user/adapters/profile'
export type CreateUserForRequestInput = { export type CreateUserForRequestInput = {
authApi: Pick<AuthApi, 'getSession'> authApi: Pick<AuthApi, 'getSession'>

View File

@@ -2,9 +2,13 @@
> Проверка владельцев и публичных границ Level 2. > Проверка владельцев и публичных границ Level 2.
## Связанное правило ## Связанные правила
- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016) - [`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-PRESET-A020`](../../rules/level-2.md#slm-l2-preset-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)
## Размещение ## Размещение
@@ -23,7 +27,14 @@
Каждый публичный предметный сценарий проверяется через единственную фабрику с управляемыми зависимостями: Каждый публичный предметный сценарий проверяется через единственную фабрику с управляемыми зависимостями:
```ts ```ts
const api = authFactory(createAuthTestDeps({ import type { AuthApi } from '@/domains/auth/business'
import { authFactory } from '@/domains/auth/business/factory'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/error'
const api: AuthApi = authFactory(createAuthTestDeps({
requestCode: async () => ({ ok: true }), requestCode: async () => ({ ok: true }),
})) }))
@@ -32,25 +43,32 @@ await api.requestPhoneOtp('+79991112233')
Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от будущего решения `throw` или `Result`, но наружу всегда проверяется только AuthErrorCode. Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от будущего решения `throw` или `Result`, но наружу всегда проверяется только AuthErrorCode.
Business-тест не использует React, реальный SDK, database или production preset. Business-тест не использует React, реальный SDK, database или production preset. Локальная fake implementation допустима в тесте и не становится adapter-модулем, потому что не входит в production graph.
## Остальные модули ## Остальные модули
Adapter-тест проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки. Тест каждого adapter-модуля проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки.
Preset-тест проверяет выбранные реализации, вызов одной фабрики, контракт возвращённого `DomainApi` и отсутствие несовместимого environment-кода. Тест обязательного preset проверяет вызов `business/factory`, контракт возвращённого `DomainApi` и отсутствие несовместимого environment-кода. Если фабрика имеет технические зависимости, тест также проверяет выбранные публичные adapter-модули; adapterless preset проверяет корректную сборку без Group `adapters`.
Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthApi` и проверяет Provider, hook или component. `login-form` не повторяет полный набор business-сценариев. Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthApi` и проверяет Provider, hook или component. `login-form` не повторяет полный набор business-сценариев.
## Архитектурные проверки ## Автоматические структурные проверки
Отдельная import-graph проверка подтверждает: Проверка файлов, exports и import-графа подтверждает:
- отсутствие root API доменного пакета и Framework Groups; - отсутствие root API доменного пакета и Framework Groups;
- наличие ровно трёх фасетов `business`, type-only exports в корневом barrel и отсутствие type exports в runtime-фасетах;
- соблюдение матрицы потребителей `business`, `business/factory` и `business/error`;
- наличие непосредственно в корне пакета непустой Group `presets` с объявленными модульными границами;
- отсутствие runtime cross-domain imports; - отсутствие runtime cross-domain imports;
- отсутствие type-only импортов из чужих presets, adapters и framework-модулей; - отсутствие type-only импортов из чужих presets, adapters и framework-модулей;
- отсутствие cross-domain framework hooks, contexts и components; - отсутствие cross-domain framework hooks, contexts и components;
- отсутствие server-only достижимости из client modules; - отсутствие server-only достижимости из client modules;
- отсутствие runtime- и type-only циклов. - отсутствие runtime- и type-only циклов.
Runtime-тест не заменяет эти проверки. ## Архитектурное ревью
На ревью проверяется, что `business/factory` экспортирует только фабрику, а `business/error` только error codes и guards. Для каждой технической зависимости рассматриваются все production implementations: каждая должна принадлежать отдельному модулю Group `adapters`, даже если используется один раз. Inline implementations во всём production-графе запрещены, а test-only fakes из этой проверки исключены.
Runtime-тест не заменяет автоматическую проверку или архитектурное ревью.

View File

@@ -28,6 +28,18 @@ Group, размещённая непосредственно в слое `domain
`business` является единственным runtime-источником, через который приложение получает доменные данные, состояние и результаты сценариев. Он не зависит от конкретного фреймворка, среды или технической реализации. `business` является единственным runtime-источником, через который приложение получает доменные данные, состояние и результаты сценариев. Он не зависит от конкретного фреймворка, среды или технической реализации.
### Публичные фасеты business
Три объявленных entry points одного логического публичного API модуля `business`:
| Путь | Содержимое |
|---|---|
| `business` | Только public types, включая `DomainApi`, зависимости, factory type, DomainError и DomainErrorCode |
| `business/factory` | Единственная runtime-фабрика `DomainApi` |
| `business/error` | Runtime-коды и guards доменных ошибок |
Фасеты не являются сегментами, вложенными модулями или самостоятельными узлами графа. Любой другой внешний путь внутрь `business` является deep import.
### Business-safe внешний пакет ### Business-safe внешний пакет
Внешняя библиотека, допустимая в import-графе `business`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, framework и техническая интеграция не становятся business-safe только из-за совместимости с несколькими средами. Внешняя библиотека, допустимая в import-графе `business`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, framework и техническая интеграция не становятся business-safe только из-за совместимости с несколькими средами.
@@ -46,17 +58,23 @@ Group, размещённая непосредственно в слое `domain
## Техническая сборка ## Техническая сборка
### Техническая зависимость
Явная runtime-возможность, необходимая business-фабрике и требующая production-реализации поверх SDK, storage, API платформы, данных запроса или технического сервиса. Type-only cross-domain API dependency, неизменяемая конфигурация и аргумент отдельного предметного сценария не являются техническими зависимостями.
### Adapter ### Adapter
Код, который связывает явную зависимость фабрики с SDK, storage, API платформы, данными запроса или техническим сервисом. Adapter может быть закрытым сегментом preset-модуля либо самостоятельным модулем в Group `adapters`. SLM-модуль в Group `adapters`, который реализует одну или несколько связанных технических зависимостей фабрики поверх SDK, storage, API платформы, данных запроса или технического сервиса. Каждая production-реализация принадлежит adapter-модулю и не размещается внутри preset или composition.
Group `adapters` обязательна и непуста, если фабрика имеет техническую зависимость. Фабрика без технических зависимостей не требует создания этой Group.
Точная обязательная форма технических портов пока не определена и остаётся открытым вопросом Level 2. Точная обязательная форма технических портов пока не определена и остаётся открытым вопросом Level 2.
### Preset ### Preset
SLM-модуль в Group `presets`, который создаёт `DomainApi` для одного именованного контекста выполнения: браузера, запроса, server action или другого реального окружения. Он выбирает технические реализации и передаёт фабрике готовые runtime-зависимости. SLM-модуль в Group `presets`, который создаёт `DomainApi` для одного именованного контекста выполнения: браузера, запроса, server action или другого реального окружения. При наличии технических зависимостей он выбирает их adapter-модули и передаёт фабрике готовые runtime-зависимости.
Архитектура не устанавливает минимальное или максимальное количество presets и не требует универсального изоморфного preset. Каждый доменный пакет содержит минимум один preset. Архитектура не ограничивает их максимальное количество и не требует универсального изоморфного preset.
## Framework binding ## Framework binding
@@ -74,7 +92,7 @@ Framework binding module получает готовый `DomainApi`, не вы
### Место сборки графа ### Место сборки графа
Код модуля `composition`, точки входа `app`, request handler или test setup, который создаёт нужные экземпляры `DomainApi` в ацикличном порядке и передаёт уже созданные API последующим presets. Место сборки не становится владельцем предметных или технических ответственностей модулей. Подробный lifecycle собранного графа пока не нормирован Level 2. Код модуля `composition`, точки входа `app`, request handler или test setup, который создаёт нужные экземпляры `DomainApi` в ацикличном порядке и передаёт уже созданные API preset-модулям либо напрямую зависимым business-фабрикам. Место сборки не становится владельцем предметных или технических ответственностей модулей. Подробный lifecycle собранного графа пока не нормирован Level 2.
### Граница среды выполнения ### Граница среды выполнения
@@ -88,9 +106,9 @@ SLM root
└── доменный пакет └── доменный пакет
├── metadata ├── metadata
├── модуль business ├── модуль business
├── Group presets ├── обязательная Group presets
│ └── preset-модуль │ └── preset-модуль
├── Group adapters ├── Group adapters при наличии технических зависимостей
│ └── adapter-модуль │ └── adapter-модуль
└── Framework Group react └── Framework Group react
├── модуль session ├── модуль session

View File

@@ -4,7 +4,7 @@
## Конфигурация проекта ## Конфигурация проекта
Конфигурация проверки сопоставляет физические пути с доменными пакетами, metadata, SLM-модулями, Groups, публичными точками входа, метками сред выполнения и allowlist внешних пакетов, объявленных business-safe. Она отдельно распознаёт navigation Groups слоя `domains` и Groups внутри пакета. Конфигурация проверки сопоставляет физические пути с доменными пакетами, metadata, SLM-модулями, Groups, тремя фасетами `business`, техническими зависимостями, adapter-модулями, публичными точками входа, метками сред выполнения и allowlist внешних пакетов, объявленных business-safe. Она отдельно распознаёт navigation Groups слоя `domains` и Groups внутри пакета.
Формат такой конфигурации пока не выбран. Независимо от формата проверка должна анализировать import-граф и объявленные границы, а не угадывать сущность только по имени папки. Формат такой конфигурации пока не выбран. Независимо от формата проверка должна анализировать import-граф и объявленные границы, а не угадывать сущность только по имени папки.
@@ -14,6 +14,9 @@
- исполняемый файл, root `index.ts`, состояние или реэкспорт в корне доменного пакета; - исполняемый файл, root `index.ts`, состояние или реэкспорт в корне доменного пакета;
- отсутствие `business` или несколько модулей `business` в одном пакете; - отсутствие `business` или несколько модулей `business` в одном пакете;
- отсутствие любого из трёх entry points `business`, `business/factory`, `business/error`, runtime export из корневого barrel, type export из runtime-фасета, другой публичный путь либо deep import внутри `business`;
- импорт фасета `business` потребителем, которому этот фасет не разрешён;
- отсутствие непосредственно в корне пакета непустой Group `presets` или наличие в ней прямого дочернего элемента без объявленной модульной границы;
- deep imports во внутренние части модулей; - deep imports во внутренние части модулей;
- runtime- или type-only достижимость framework-, adapter-, preset-, infra- или environment-specific кода из `business`; - runtime- или type-only достижимость framework-, adapter-, preset-, infra- или environment-specific кода из `business`;
- runtime-импорт любого экспорта другого доменного пакета; - runtime-импорт любого экспорта другого доменного пакета;
@@ -28,8 +31,11 @@
- представляет ли пакет одну связную предметную область; - представляет ли пакет одну связную предметную область;
- является ли `DomainApi` единственным runtime-источником доменных данных и результатов для приложения; - является ли `DomainApi` единственным runtime-источником доменных данных и результатов для приложения;
- принадлежат ли коды, тип и guard доменных ошибок модулю `business`; - является ли фабрика единственным runtime-экспортом `business/factory`;
- содержит ли `business/error` только runtime-коды и guards, а type-only barrel именованные типы DomainError и DomainErrorCode;
- преобразует ли business ожидаемые технические и cross-domain сбои в собственные ошибки; - преобразует ли business ожидаемые технические и cross-domain сбои в собственные ошибки;
- является ли каждая production-реализация технической зависимости отдельным модулем Group `adapters`, включая реализации, используемые только в одном месте;
- отсутствуют ли production adapters вне Group `adapters` во всём production-графе; test-only fakes не участвуют в этой проверке;
- представляет ли каждый preset один реальный контекст выполнения и сохраняет ли контракт фабрики; - представляет ли каждый preset один реальный контекст выполнения и сохраняет ли контракт фабрики;
- принадлежит ли каждый framework binding module домену, а не странице или multi-domain сценарию; - принадлежит ли каждый framework binding module домену, а не странице или multi-domain сценарию;
- соответствует ли каждый объявленный business-safe внешний пакет ограничениям детерминированной библиотеки без runtime capability; - соответствует ли каждый объявленный business-safe внешний пакет ограничениям детерминированной библиотеки без runtime capability;
@@ -37,7 +43,7 @@
## Тестирование ## Тестирование
Business-сценарии проверяются через фабрику с управляемыми зависимостями. Preset проверяет выбор реализаций и границу среды. Framework binding module проверяет собственный Provider, hook или component без повторения всего набора business-сценариев. Business-сценарии проверяются через `business/factory` с управляемыми test fakes. Adapter module проверяет техническое преобразование. Preset проверяет границу среды и, при наличии технических зависимостей, выбор adapter-модулей. Framework binding module проверяет собственный Provider, hook или component без повторения всего набора business-сценариев.
Import-graph checks не заменяются runtime-тестами. Import-graph checks не заменяются runtime-тестами.
@@ -53,6 +59,10 @@ Import-graph checks не заменяются runtime-тестами.
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013) - [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
- [`SLM-L2-MIGRATION-A017`](../rules/level-2.md#slm-l2-migration-a017) - [`SLM-L2-MIGRATION-A017`](../rules/level-2.md#slm-l2-migration-a017)
- [`SLM-L2-BUSINESS-R018`](../rules/level-2.md#slm-l2-business-r018) - [`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-PRESET-A020`](../rules/level-2.md#slm-l2-preset-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-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) - [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения. Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.

View File

@@ -1,6 +1,6 @@
# Правила SLM второго уровня # Правила SLM второго уровня
Проект Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. `SLM-L1-DOMAIN-R015` заменяется правилами доменного пакета. `SLM-L1-GROUP-R007` сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Ранее использовавшийся номер `SLM-L2-DOMAIN-R001` не переиспользуется после изменения модели уровней. Проект Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. `SLM-L1-DOMAIN-R015` заменяется правилами доменного пакета. `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` не переиспользуется после изменения модели уровней.
## Граница доменного пакета ## Граница доменного пакета
@@ -46,7 +46,7 @@
> **Единая фабрика DomainApi** > **Единая фабрика DomainApi**
> >
> Модуль `business` предоставляет ровно одну публичную фабрику, которая получает явные runtime-зависимости и создаёт `DomainApi` одного контракта независимо от preset и среды выполнения. > Модуль `business` предоставляет ровно одну публичную фабрику, которая получает явные runtime-зависимости, создаёт `DomainApi` одного контракта независимо от preset и среды выполнения и является единственным runtime-экспортом фасета `business/factory`.
## Ошибки домена ## Ошибки домена
@@ -54,7 +54,7 @@
> **Публичный контракт ошибок** > **Публичный контракт ошибок**
> >
> Модуль `business` экспортирует устойчивые коды доменных ошибок, именованный readonly-тип безопасной публичной формы и runtime guard этой формы независимо от выбранного способа передачи ошибки. > Модуль `business` экспортирует через type-only barrel именованные readonly-типы DomainError и DomainErrorCode, а через `business/error` только устойчивые runtime-коды и guards безопасной публичной формы независимо от выбранного способа передачи ошибки.
### SLM-L2-ERROR-R010 ### SLM-L2-ERROR-R010
@@ -68,7 +68,7 @@
> **Роль preset** > **Роль preset**
> >
> Каждый preset является SLM-модулем одного именованного контекста выполнения, выбирает реализации явных зависимостей, вызывает единственную business-фабрику и не добавляет предметные сценарии, методы `DomainApi` или собственные доменные ошибки. > Каждый preset является SLM-модулем одного именованного контекста выполнения, при наличии технических зависимостей выбирает их adapter-модули, вызывает единственную business-фабрику и не добавляет предметные сценарии, методы `DomainApi` или собственные доменные ошибки.
### SLM-L2-DEPENDENCY-A012 ### SLM-L2-DEPENDENCY-A012
@@ -117,3 +117,31 @@
> **Business-safe внешний пакет** > **Business-safe внешний пакет**
> >
> Внешний пакет объявляется business-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state manager, framework или другой технической интеграцией. > Внешний пакет объявляется business-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state manager, framework или другой технической интеграцией.
## Публичные фасеты business
### SLM-L2-BUSINESS-A019
> **Публичные фасеты business**
>
> Публичный API `business` состоит ровно из трёх entry points: корневой barrel содержит только type exports, а `business/factory` и `business/error` содержат только runtime exports; другие публичные пути и deep imports запрещены.
## Обязательные роли сборки
### SLM-L2-PRESET-A020
> **Обязательная Group presets**
>
> Корень каждого доменного пакета содержит ровно одну непустую Group `presets`, каждый прямой дочерний элемент которой является объявленной границей SLM-модуля.
### SLM-L2-ADAPTER-R021
> **Модули production adapters**
>
> Если business-фабрика имеет хотя бы одну техническую зависимость, корень доменного пакета содержит непустую Group `adapters`, а каждая production-реализация такой зависимости является отдельным adapter-модулем этой Group и не определяется в другом месте production-графа.
### SLM-L2-BUSINESS-A022
> **Потребители фасетов business**
>
> Корневой `business` импортируется извне только через `import type`; `business/factory` импортируют только presets своего домена, `app`, `compositions` и тесты, а `business/error` импортируют только framework binding modules своего домена, `app`, `compositions` и тесты.

View File

@@ -1,86 +0,0 @@
# Domains: рабочие заметки
> Статус: исследовательский черновик. Материалы в этой папке не являются спецификацией и пока не задают обязательных правил SLM.
Эта папка фиксирует текущую гипотезу о новой сущности `Domain`, business-модуле внутри неё, framework-neutral factory, ports, adapters, presets и framework bindings.
Level 3 развивает доменный модуль Level 2 в строгую доменную границу с несколькими модулями разных ролей. Такой переход может потребовать рефакторинга, но сохраняет предметного владельца и базовые модульные правила.
Идентификаторы вида `DOM-N001` и `FAC-N001` являются стабильными якорями заметок. Они нужны для обсуждения и последующего переноса решений в спецификацию, но не являются идентификаторами нормативных правил.
## Основная формула
```text
Business определяет ЧТО делать.
Ports описывают ЧТО business нужно.
Factory создаёт business API из ports.
Adapters реализуют ports в конкретной среде.
Preset выбирает adapters, scope и lifecycle.
Framework binding подключает готовый API к React, Vue, Next.js и другим фреймворкам.
```
Краткая схема:
```text
┌─ browser preset
├─ SSR request preset
Business factory + ports ├─ server action preset
├─ per-test assembly
└─ другой application preset
готовый business API instance
├─ framework bindings
├─ compositions
└─ другие business factories через ports
```
## Зафиксированные гипотезы
### DOM-N001: Domain является отдельной архитектурной сущностью
Domain является границей владения одной предметной областью. Он содержит modules и logical groups с разной технической ролью, но общей доменной принадлежностью.
### DOM-N002: Business внутри Domain является модулем
`business` имеет собственную ответственность и public API, поэтому это module, а не segment. `types/`, `services/`, `errors/` и `lib/` внутри business остаются segments.
### FAC-N001: Один business-контракт имеет одну factory
Разные среды выполнения не требуют разных factories, если они предоставляют один и тот же API. Различия среды выражаются ports, adapters и presets.
### PRE-N001: Одна factory допускает несколько presets
Browser, SSR, server action, tests и другие контексты могут собирать одну factory с разными реализациями ports.
### FAC-N002: Business и factory нейтральны к framework и environment
Изоморфный business import graph не достигает React, Vue, Next.js, browser-only, server-only, SDK, storage implementations и других concrete runtimes.
### PRE-N002: Среда является свойством preset
Client/server/request различия определяются preset и выбранными adapters, а не `mode` внутри factory. Tests создают отдельную per-test assembly напрямую через factory и не требуют общего test preset.
## Карта заметок
- [Domain](./domain.md) - роль новой сущности, структура и публичные границы.
- [Business](./business.md) - ответственность business-модуля, types, pure functions и errors.
- [Factory, ports и adapters](./factory-ports-adapters.md) - контракт factory и требования изоморфности.
- [Presets и SSR](./presets.md) - варианты сборки, lifecycle и защита server-only кода.
- [Framework bindings](./framework-bindings.md) - React/Vue/Next-код внутри Domain.
- [Тестирование](./testing.md) - границы тестов, factory-level contract, harness, adapters, presets, framework и UI.
- [Auth как проверочный пример](./auth-example.md) - применение гипотез к реальному модулю.
- [Открытые вопросы](./open-questions.md) - решения, которые ещё нельзя превращать в правила.
## Предварительная структура приложения
```text
src/
├── app/
├── compositions/
├── domains/
├── infra/
├── ui/
└── shared/
```
`domains/` пока рассматривается как новая верхнеуровневая область, заменяющая разнесение одной доменной ответственности между `business/{domain}` и `compositions/business/{domain}`.

View File

@@ -1,173 +0,0 @@
# Auth как проверочный пример
> Рабочая заметка на основе реального модуля `/home/gromov/projects/biocad/newbiocadru/apps/web/src/business/auth`. Код проекта не изменялся.
Цель примера: проверить гипотезы Domain на существующем SLM business-модуле, а не предложить немедленную миграцию.
## Текущее устройство
```text
business/auth/
├── auth.factory.ts
├── errors/
├── hooks/
├── mappers/
├── services/
├── tests/
├── types/
└── index.ts
```
Runtime-сборка находится отдельно:
```text
compositions/business/knv/auth/
├── adapters/
├── create-knv-auth-business.ts
└── index.ts
```
Новая сущность Domain может колоцировать обе ответственности без смешивания ролей:
```text
domains/auth/
├── business/
├── presets/
│ └── {preset-name}/
│ └── adapters/
└── {framework-binding}/
```
## Factory и client boundary
### AUTH-N001: Текущий AuthApi содержит client-oriented hook
`auth.factory.ts` импортирует `createAuthHook`, а `hooks/use-auth.hook.ts` содержит `'use client'`. Кроме того, `AuthDeps.session` описывает `useToken`.
Текущий transitive graph:
```text
authFactory
→ createAuthHook
→ 'use client'
```
Это практический пример того, почему neutral factory должна проверяться по всему transitive import graph, а framework hooks должны находиться в отдельном framework module Domain. Точный путь этого module пока не выбран.
Возможное направление:
```text
business AuthApi
→ framework-neutral state observation
React binding
→ useAuth над готовым AuthApi
```
Финальный state contract пока не выбран.
## Pure phone logic
### AUTH-N002: Нормализация телефона уже дублируется
Business содержит private `normalizePhoneOtpPhone`, а auth-widget содержит отдельный `getPhoneDigits` и собственный `PHONE_DIGITS_LENGTH`.
Это кандидат на public pure business function:
```ts
import {
normalizeAuthPhone,
validateAuthPhone,
} from '@/domains/auth/business'
```
Business service и UI могут использовать одну семантику. Business service всё равно повторно валидирует вход независимо от UI-проверки.
Существующий `business/user` показывает другой workaround: pure validators возвращаются через собранный `userFactory` API. Прямой pure export позволит не требовать assembly для детерминированной функции.
## Error contract
### AUTH-N003: Error contract фактически публичен, но описан не полностью
Business создаёт `AuthBusinessError` с `code` и `retryAfterSeconds`, но public `index.ts` экспортирует только type `AuthErrorCode`.
Consumer auth-widget поэтому:
- повторяет строковые error codes в message map;
- создаёт локальный `AuthErrorData`;
- вручную проверяет `code` и `retryAfterSeconds` в `unknown`;
- самостоятельно нормализует форму caught error.
Предварительное исправление границы:
```ts
// Public business API.
export { AUTH_ERROR_CODES, isAuthError }
export type { AuthError, AuthErrorCode }
// Business-private implementation.
class AuthBusinessError extends Error {}
const createAuthBusinessError = (...) => {}
```
Consumer получает безопасный observation contract, но не получает constructor и source mapping.
## Presets
### AUTH-N004: Текущий createKnvAuthBusiness является preset
`createKnvAuthBusiness()` выбирает `knvAuthPhoneAdapter` и `appAuthSessionAdapter`, затем вызывает `authFactory`.
В новой терминологии это application preset, внутри которого могут оставаться KNV-specific adapters:
```text
domains/auth/presets/application/create-application-auth.ts
```
Он не является единственно допустимым assembly site. Tests, SSR request composition и другой product preset могут напрямую вызвать ту же `authFactory`.
## SSR-вариант
Одна factory позволяет получить request-scoped API без второй реализации business:
```ts
import 'server-only'
export const createAuthForRequest = (input: AuthRequestInput) => {
return authFactory({
authPhone: createKnvServerAuthPhoneAdapter(input),
session: createRequestAuthSessionAdapter(input),
})
}
```
Browser preset использует другую реализацию тех же ports. Factory, business types, pure functions и error contract остаются общими.
## Предварительная целевая структура
```text
domains/auth/
├── business/
│ ├── auth.factory.ts
│ ├── errors/
│ ├── lib/
│ ├── mappers/
│ ├── services/
│ ├── tests/
│ ├── types/
│ └── index.ts
├── presets/
│ └── application/
│ ├── adapters/
│ ├── create-application-auth.ts
│ ├── create-application-auth.test.ts
│ └── index.ts
└── {framework-binding}/
└── index.ts
```
Это только проверочная структура. Она не фиксирует обязательность всех папок и не должна использоваться как scaffold checklist.
Server-only/request preset может быть добавлен отдельным module при реальной потребности. Он не образует обязательную `server`-ветку Domain.
Tests не используют общий testing preset. Business tests выполняют per-test assembly напрямую через `authFactory`, а production presets тестируются рядом с собственной реализацией только на wiring, scope и lifecycle.

View File

@@ -1,174 +0,0 @@
# Business module внутри Domain
> Рабочая заметка. Не является нормативным разделом спецификации.
## Роль
### BUS-N001: Business является семантическим ядром Domain
Business-модуль владеет:
- публичными бизнес-сценариями;
- business-owned types и contracts;
- business API;
- factory и ports;
- детерминированными доменными правилами;
- доменным error contract;
- преобразованием внешних результатов в доменные результаты.
Business не владеет concrete runtime, environment wiring и framework integration.
## Public API business-модуля
### BUS-N002: Business может экспортировать четыре категории сущностей
| Категория | Примеры |
|---|---|
| Factory | `authFactory` |
| Types и contracts | `AuthApi`, `AuthDeps`, `AuthState`, `AuthErrorCode` |
| Pure domain functions | `normalizeAuthPhone`, `validateAuthPhone` |
| Error observation contract | `AUTH_ERROR_CODES`, `AuthError`, `isAuthError` |
Это заменяет старую гипотезу, что business `index.ts` может экспортировать в runtime только factory.
Предварительный public API:
```ts
export { authFactory } from './auth.factory'
export {
AUTH_ERROR_CODES,
isAuthError,
} from './errors/auth-error'
export {
normalizeAuthPhone,
validateAuthPhone,
} from './lib/auth-phone'
export type {
AuthApi,
AuthDeps,
AuthError,
AuthErrorCode,
AuthFactory,
AuthState,
}
```
## Types
### BUS-N003: Business contracts остаются внутри business
Отдельный `model` submodule пока не требуется. Типы размещаются по ownership:
| Тип | Место |
|---|---|
| `AuthApi`, `AuthDeps`, `AuthState` | `domains/auth/business/types` |
| `AuthError`, `AuthErrorCode` | `domains/auth/business/types` или `errors` |
| SDK DTO | Adapter или infra runtime |
| React provider props | Выбранный React binding module Domain |
| View model конкретного screen | Consumer composition |
`types/` является segment business-модуля, а не самостоятельным общим хранилищем Domain.
## Pure domain functions
### BUS-N004: Детерминированная доменная функция может экспортироваться напрямую
Pure domain function:
- получает все данные через аргументы;
- возвращает результат только на основе аргументов;
- не использует `Deps`;
- не выполняет I/O;
- не читает mutable runtime state;
- не зависит от clock, random, env или platform API;
- не импортирует React, Vue, Next.js или state manager;
- использует business language и реализует доменное правило.
Примеры:
```ts
normalizeAuthPhone(value)
validateAuthPhone(value)
calculateOrderTotal(order)
hasRequiredUserAgreements(user)
```
Consumer может использовать такую функцию для раннего UX feedback. Business scenario всё равно обязан повторно проверить вход на своей границе.
### BUS-N005: Не каждая pure function становится public
Функция остаётся private, если она нужна только одному service или является технической деталью реализации. Public export оправдан доменной семантикой и реальным внешним либо межмодульным consumer.
Папки `domain/shared` и `domain/public` не создаются только ради видимости. Public contract определяется entrypoint business-модуля.
## Domain errors
### BUS-N006: Создание и наблюдение ошибки являются разными контрактами
Business создаёт domain error. Consumer только распознаёт ошибку и читает поля, от которых зависит его поведение.
Public observation contract:
```ts
export const AUTH_ERROR_CODES = {
PHONE_OTP_PHONE_INVALID: 'AUTH_PHONE_OTP_PHONE_INVALID',
PHONE_OTP_VERIFY_CODE_INVALID: 'AUTH_PHONE_OTP_VERIFY_CODE_INVALID',
PHONE_OTP_RESEND_TOO_SOON: 'AUTH_PHONE_OTP_RESEND_TOO_SOON',
} as const
export type AuthErrorCode =
(typeof AUTH_ERROR_CODES)[keyof typeof AUTH_ERROR_CODES]
export type AuthError = Readonly<{
code: AuthErrorCode
retryAfterSeconds: number | null
}>
export const isAuthError = (value: unknown): value is AuthError => {
// Structural runtime validation.
}
```
Private creation contract:
```ts
class AuthBusinessError extends Error implements AuthError {
// Constructor, cause и source diagnostics.
}
const createAuthBusinessError = (...) => {
// Source error mapping.
}
```
### BUS-N007: Error constructor не является consumer API
Consumer не должен создавать `AuthBusinessError`, выбирать source mapping или подделывать business failure. Поэтому наружу предполагается экспортировать:
- stable error code values;
- error code type;
- read-only observable error shape;
- runtime guard или parser.
Наружу не предполагается экспортировать:
- error constructor;
- error factory;
- source error mapper;
- transport-specific error data;
- internal fallback selection.
### BUS-N008: Одних типов недостаточно при throw-based API
TypeScript не описывает checked exceptions. Для сигнатуры
```ts
(data: VerifyPhoneOtpData) => Promise<void>
```
значение в `catch` всё равно имеет тип `unknown`. Если consumer различает ошибки по `code`, business должен предоставить runtime discriminator либо перейти на typed `Result`.
Выбор между throw + guard и typed `Result` пока не закрыт окончательно. Текущий минимальный путь совместимости: throw + public observation contract.

View File

@@ -1,206 +0,0 @@
# Domain
> Рабочая заметка. Не является нормативным разделом спецификации.
## Определение
### DOM-N003: Domain является границей владения предметной областью
Domain группирует business-контракт, concrete integrations, готовые presets и framework-specific bindings одной предметной области.
Примеры Domain:
- `auth`;
- `user`;
- `catalog`;
- `orders`;
- `checkout`.
Domain не является одним большим module. Он является границей, внутри которой могут находиться modules и logical groups с заданным направлением зависимостей.
```text
Domain
├── business module
├── presets group
│ └── preset modules
├── framework binding module или group
└── optional reusable adapters group
└── adapter modules
```
## Предварительная структура
```text
domains/auth/
├── business/
│ ├── auth.factory.ts
│ ├── errors/
│ ├── lib/
│ ├── services/
│ ├── tests/
│ ├── types/
│ └── index.ts
├── presets/
│ └── {preset-name}/
│ ├── adapters/
│ ├── create-auth.ts
│ ├── create-auth.test.ts
│ └── index.ts
└── {framework-binding}/
├── hooks/
├── providers/
├── tests/
├── ui/
└── index.ts
```
`{preset-name}` и `{framework-binding}` являются placeholders, а не обязательными именами папок. Preset называется по своему scope или назначению. Framework binding может быть оформлен как `react`, `bindings/react`, `framework/react` или по другому локальному соглашению.
Environment-specific preset, включая server-only вариант, может быть добавлен отдельным preset module. SLM не требует заранее делить `presets` или `adapters` на `browser`, `server` и другие технические категории.
## Возможные ветки Domain
### DOM-N007: Domain не имеет фиксированного набора верхних папок
| Роль | Типичная форма | Статус |
|---|---|---|
| Business | Один business module | Основная гипотеза Domain |
| Presets | Logical group с preset modules | По наличию повторяемых assemblies |
| Framework bindings | Module или logical group | По наличию framework integration |
| Reusable adapters | Logical group с adapter modules | Только после promotion из владельца |
| Tests | Segment конкретного module | Не создаётся в корне Domain |
`model`, `types`, `errors`, `lib`, `ui`, `client` и `server` не становятся верхними Domain-разделами автоматически. Они размещаются внутри module-владельца либо появляются как локальное соглашение с отдельным обоснованием.
## Иерархия сущностей
### DOM-N004: Роль и структурный вид являются независимыми характеристиками
Архитектурная роль отвечает на вопрос «какую ответственность выполняет код»:
- business;
- preset;
- framework binding;
- adapter.
Структурный вид отвечает на вопрос «как оформлена граница кода»:
- Domain;
- module;
- group;
- segment;
- file.
```text
Domain
├── Module
│ ├── Segment
│ │ └── File
│ └── File
└── Group
├── Module
└── Group
└── Module
```
Правила структурных видов:
- Module владеет самостоятельной ответственностью и public API.
- Group является logical directory для навигации, не имеет `index.ts`, runtime и собственных файлов реализации.
- Segment существует внутри module, группирует его файлы по назначению и не имеет отдельного внешнего API.
- Имя папки само по себе не доказывает её структурный вид.
Пример классификации:
| Путь | Роль | Структурный вид |
|---|---|---|
| `domains/auth` | Предметная область Auth | Domain |
| `domains/auth/business` | Business | Module |
| `domains/auth/business/services` | Business scenarios | Segment |
| `domains/auth/business/tests` | Business tests | Segment |
| `domains/auth/presets` | Навигация presets | Group |
| `domains/auth/presets/{preset-name}` | Preset | Module |
| `domains/auth/presets/{preset-name}/adapters` | Private adapters preset | Segment |
| `domains/auth/{framework-binding}` | Framework binding | Module или Group по фактической границе |
| `domains/auth/adapters` | Навигация promoted adapters | Optional group |
| `domains/auth/adapters/{adapter-name}` | Reusable adapter | Module |
## Публичные границы
### DOM-N005: Domain предоставляет отдельные public submodules
Предварительно Domain не имеет обязательного общего facade. Каждый public module предоставляет собственный entrypoint:
```ts
import { authFactory, validateAuthPhone } from '@/domains/auth/business'
import { createApplicationAuth } from '@/domains/auth/presets/application'
import { AuthProvider, useAuth } from '@/domains/auth/react'
```
`application` и `react` здесь являются только примерами пользовательских имён. Отдельные entrypoints не смешивают business, concrete assembly и framework code в одном import graph.
Возможные public entrypoints:
```text
@/domains/auth/business
@/domains/auth/presets/{preset-name}
@/domains/auth/{framework-binding}
@/domains/auth/adapters/{adapter-name} # только для promoted adapter module
```
Private adapters внутри preset не получают собственного внешнего entrypoint.
### DOM-N006: Omnibus barrel для всего Domain опасен
Такой entrypoint может связать изоморфный, client-only и server-only graphs:
```ts
// Не использовать как default-подход.
export * from './business'
export * from './presets/application'
export * from './react'
```
Tree shaking не считается security boundary. Server-only submodule не должен быть достижим из изоморфного или client entrypoint даже через re-export.
## Предварительное направление зависимостей
```text
business
preset + private adapters
готовый business API instance
framework bindings / compositions
```
Более точная схема импортов:
```text
business -/→ adapters | presets | framework | infra concrete runtime
preset-private adapters → business contracts + concrete runtime
promoted adapter module → business contracts + concrete runtime
presets → business factory + private or promoted adapters
framework → business contracts + ready API or preset
compositions → ready business API + framework bindings
```
Framework module может одновременно быть assembly site, если он явно владеет lifecycle API instance. Наличие папки `presets/` не даёт ей монополию на вызов factory.
## Domain и compositions
Domain владеет повторяемой доменной ответственностью. Composition по-прежнему владеет страницей, route tree, экраном и конкретным пользовательским outcome.
Предварительная граница:
| Ответственность | Владелец |
|---|---|
| Auth scenarios и contracts | `domains/auth/business` |
| Private auth adapters одной assembly | Segment внутри соответствующего preset module |
| Reusable auth adapter | Optional adapter module после promotion |
| Повторяемая сборка AuthApi | Конкретный preset module или другой assembly site |
| Auth React provider/access hook | Выбранный framework binding module |
| Текст ошибки, redirect, экран и route outcome | Consumer composition |
Граница domain-specific UI пока остаётся открытым вопросом.

View File

@@ -1,200 +0,0 @@
# Factory, ports и adapters
> Рабочая заметка. Не является нормативным разделом спецификации.
## Терминология
### FAC-N003: Собирается API instance, а не factory
```text
Factory + Deps implementations → business API instance
```
- Factory является функцией создания.
- Ports являются business-owned контрактами capabilities.
- `Deps` группирует ports, нужные factory.
- Adapters реализуют ports в concrete runtime.
- Assembly site вызывает factory и получает API instance.
- Preset является готовой конфигурацией assembly.
Формулировка «собранная фабрика» неточна. Factory конфигурируется зависимостями и создаёт собранный API.
## Business factory
### FAC-N004: Factory является framework-neutral и environment-neutral
Factory не знает, где будет использована:
- в browser;
- во время SSR;
- в server action;
- в background process;
- в unit test;
- в React, Vue или другом framework.
```ts
export type AuthFactory = (deps: AuthDeps) => AuthApi
```
### FAC-N005: Factory имеет стабильную форму результата
Все presets одной factory создают один и тот же business API contract. Среда не выбирается через аргумент `mode`, а форма API не зависит от наличия optional dependency.
Не рекомендуется:
```ts
authFactory({
mode: 'server',
serverAdminClient: optionalClient,
})
```
Не рекомендуется возвращать методы, которые существуют в общем API, но намеренно падают в одной из сред.
### FAC-N006: Factory construction не выполняет side effects
Вызов factory не должен:
- выполнять network request;
- читать cookies, storage или env;
- запускать subscription или timer;
- обращаться к browser либо Node API;
- создавать скрытый application singleton;
- выбирать concrete adapter;
- выполнять framework lifecycle.
Factory может синхронно создать детерминированные services и связать их с переданными ports.
## Гигиена import graph
### FAC-N007: Весь достижимый из business import graph должен быть изоморфным
Недостаточно проверить только файл `{domain}.factory.ts`. Ни один production import, достижимый из business public entrypoint, не должен приводить к:
- React, Vue, Next.js и другим frameworks;
- `'use client'`, `client-only` или `server-only` boundary;
- browser API;
- Node-only API;
- concrete SDK/client;
- concrete storage;
- state/query runtime;
- adapters и presets;
- environment configuration.
Tree shaking не используется как доказательство изоляции.
## Ports
### PORT-N001: Port принадлежит business
Port описывает capability на языке business, а не форму concrete implementation.
```ts
export type AuthPhonePort = {
requestCode: (phone: string) => Promise<unknown>
verifyCode: (data: VerifyPhoneOtpData) => Promise<unknown>
}
```
Port не должен раскрывать SDK client, generated operation, `Request`, `Window`, React hook, Zustand `StoreApi` и другие environment/framework types.
### PORT-N002: Ports абстрагируют implementation, но не доступность capability
Одна factory возможна, пока каждый preset способен реализовать одинаковые ports.
Server capability может остаться общим port, если browser adapter реализует её через безопасный HTTP/RPC boundary. Если capability принципиально невозможно реализовать в одной из поддерживаемых сред, её нельзя маскировать optional dependency общего API.
### PORT-N003: Reactive port должен быть framework-neutral
Client hook в `Deps` делает контракт client-oriented. Вместо `useToken` базовый port может описывать framework-neutral observation protocol:
```ts
export type AuthSessionPort = {
getSnapshot: () => AuthState
subscribe: (listener: () => void) => () => void
setToken: (token: string | null) => void
}
```
React binding может построить `useAuth` поверх `getSnapshot` и `subscribe`. Vue binding использует тот же port через собственный lifecycle.
Точная форма reactive ports требует отдельной проверки на реальном state manager.
## Adapters
### ADP-N001: Adapter реализует business port
Adapter знает одновременно business contract и concrete runtime:
```text
business port ← adapter → SDK / storage / browser / request
```
Adapter может:
- преобразовать domain arguments в transport arguments;
- вызвать concrete source;
- привести concrete runtime к минимальной форме port;
- управлять техническими деталями конкретной integration.
Adapter не должен:
- определять business error code;
- выбирать domain fallback;
- менять business invariant;
- расширять public business API методами concrete client.
### ADP-N002: Adapter размещается у минимального владельца
SLM не задаёт обязательную структуру `adapters/browser`, `adapters/server` или другую техническую классификацию.
Adapter может быть:
- private файлом или segment конкретного preset module;
- самостоятельным Domain module после появления нескольких assembly consumers;
- частью пользовательской logical group, если она действительно упрощает навигацию.
Default colocation для adapter, принадлежащего одной assembly:
```text
domains/auth/presets/{preset-name}/
├── adapters/
├── create-auth.ts
└── index.ts
```
Возможный promotion переиспользуемого adapter:
```text
domains/auth/adapters/ # optional logical group
└── {adapter-name}/ # adapter module
└── index.ts
```
Environment-specific code не должен быть достижим из entrypoint, объявленного framework-neutral или environment-neutral. Способ физической изоляции выбирает проект. Группировка по `browser/server` допустима как локальное соглашение, но не является требованием SLM.
## Assembly sites
### ASM-N001: Вызов factory определяет роль assembler
Factory может быть вызвана в preset, provider, route/request composition, test setup или другом месте. Путь сам по себе не запрещает сборку.
Assembly site обязан:
- предоставить полный `Deps`;
- выбрать concrete adapters;
- определить предполагаемый scope API instance;
- вернуть необходимые lifecycle/dispose handles;
- не скрывать создание graph от фактического владельца.
После возврата результата lifecycle принадлежит caller/graph owner, который удерживает API instance. Например, request владеет request-scoped instance, а Provider владеет instance до unmount. Preset описывает создание и передачу ownership, но не становится долгоживущим владельцем только из-за своего расположения.
### ASM-N002: Consumer использует готовый API
Screen, component или service, который только выполняет business-сценарий, получает готовый business API, например `AuthApi`. Если такой consumer вызывает factory, он становится assembler и должен удовлетворять всем требованиям assembly role.
### ASM-N003: Cross-domain dependency получает собранный API
Business одного Domain не создаёт factory другого Domain внутри себя. Он описывает необходимую capability через свой `Deps`, а graph owner передаёт уже собранный API.
Tests вправе напрямую вызывать factory с mocks и fakes. Это один из основных сценариев существования factory.

View File

@@ -1,109 +0,0 @@
# Framework bindings внутри Domain
> Рабочая заметка. Не является нормативным разделом спецификации.
## Определение
### FW-N001: Framework code определяется зависимостью от framework
К framework code относится код, существующий из-за React, Vue, Next.js или другого framework/runtime contract:
- components;
- providers и contexts;
- framework hooks;
- framework lifecycle;
- directives и framework entrypoints;
- framework-specific types;
- server/client component boundaries.
Такой код может принадлежать Domain по смыслу, но не размещается внутри framework-neutral business.
## Роль binding
### FW-N002: Framework binding адаптирует готовый business API
Framework binding может:
- предоставить готовый business API через context/provider;
- построить React/Vue hook доступа;
- связать framework lifecycle с domain subscription;
- предоставить domain-specific framework component;
- получить API instance через props, context или preset.
Framework binding не изменяет business rules и не реализует source adapter вместо Domain preset/adapters.
## Возможная структура
```text
domains/auth/{framework-binding}/
├── providers/
├── hooks/
├── components/
├── types/
└── index.ts
```
`{framework-binding}` является placeholder. SLM пока не выбирает между `react`, `bindings/react`, `framework/react` и другим локальным соглашением. Чёткая граница определяется самостоятельным module и отдельным public entrypoint, а не обязательным именем родительской папки.
Если одна папка предоставляет cohesive framework API, она является module. Если папка только классифицирует несколько самостоятельных binding modules, она является logical group и не имеет собственного `index.ts`.
## Reactive state
### FW-N003: Framework hook строится снаружи business
Если business предоставляет framework-neutral `getSnapshot` и `subscribe`, React binding может использовать `useSyncExternalStore`:
```ts
'use client'
export const createUseAuth = (authApi: AuthApi) => {
return () => {
return useSyncExternalStore(
authApi.subscribeAuthState,
authApi.getAuthState,
authApi.getAuthState,
)
}
}
```
Это только иллюстрация направления. Финальная форма state port должна учитывать реальный state/query runtime.
Business при таком подходе не импортирует React и не возвращает React hook как единственный способ чтения состояния.
## Framework module как assembly site
### FW-N004: Provider может владеть API instance
Provider вправе вызвать preset или factory, если provider является явным владельцем scope и lifecycle:
```text
AuthProvider
→ createBrowserAuth preset
→ AuthApi instance
→ context
→ access hooks
```
Provider construction не должен запускать I/O или subscription до framework commit/effect. Cleanup выполняется владельцем lifecycle.
Framework module не обязан собирать API. Он также может получить готовый instance от route/page/application graph owner.
## Framework-neutral и environment-neutral
Эти свойства различаются:
| Свойство | Запрещённая зависимость |
|---|---|
| Framework-neutral | React, Vue, Next lifecycle и types |
| Environment-neutral | Browser-only, Node-only, server-only, env/runtime globals |
Business factory должна удовлетворять обоим свойствам. Framework binding по определению framework-specific, а preset по определению может быть environment-specific.
## Domain UI
### FW-N005: Framework принадлежность не доказывает Domain ownership
React component размещается внутри Domain только если его ответственность принадлежит Domain. Page, screen, route outcome, локальный текст ошибки и продуктовая композиция могут остаться в `compositions`.
Граница между domain-specific components и consumer compositions пока требует отдельных примеров.

View File

@@ -1,110 +0,0 @@
# Открытые вопросы Domains
> Эти вопросы намеренно не сформулированы как правила.
## Ошибки
### OPEN-N001: Throw или typed Result
Нужно решить, остаются ли ожидаемые domain failures исключениями с public runtime guard или business API возвращает discriminated `Result<T, DomainError>`.
Текущий совместимый вариант: throw + `isDomainError`. Typed Result потребует изменения формы всех scenario methods.
### OPEN-N002: Универсальный или domain-specific error guard
Нужно определить, достаточно ли общего `isDomainError`, либо каждый business-модуль экспортирует `isAuthError`, `isUserError` и собственную проверку code set.
## Domain structure
### OPEN-N003: Имена framework modules
Варианты:
```text
domains/auth/framework/react
domains/auth/bindings/react
domains/auth/react
```
`framework/react` явно классифицирует роль, `react` сокращает import path, а `bindings/react` подчёркивает adapter-like назначение границы. Выбор пока не сделан.
### OPEN-N004: Нужен ли root Domain entrypoint
Статус: предварительно закрыт в пользу нескольких entrypoints.
Каждый public module Domain предоставляет собственную точку входа: business, конкретный preset, framework binding и promoted adapter. Обязательный root runtime barrel не создаётся, потому что он может смешать isomorphic, client-only и server-only graphs.
### OPEN-N005: Public adapters
Текущая гипотеза: adapter начинается как private segment минимального владельца, обычно preset module. При появлении самостоятельной ответственности или нескольких assembly consumers он может быть поднят в отдельный adapter module с собственным entrypoint.
Открытым остаётся точный promotion criterion; фиксированная числовая граница пока не выбрана.
## Factory и ports
### OPEN-N006: Гранулярность одной factory
Одна factory может возвращать большой API, хотя конкретному SSR scope нужны два метода. Нужно проверить, достаточно ли narrowed preset view, или крупные contracts требуют нескольких business modules/factories.
Предварительный принцип: одна factory на один связный business API contract; разные environments сами по себе не создают новую factory.
### OPEN-N007: Reactive state contract
Нужно проверить на реальном Zustand/React/SSR кейсе форму framework-neutral state port:
- `getSnapshot` + `subscribe`;
- commands/selectors;
- initial server snapshot;
- hydration;
- cleanup;
- concurrent rendering.
## Framework boundary
### OPEN-N008: Domain-specific UI
Нужно решить, какие auth components принадлежат выбранному Auth framework binding module, а какие остаются composition widgets/screens.
Framework dependency сама по себе не доказывает Domain ownership.
## Cross-domain dependencies
### OPEN-N009: Прямой импорт pure functions другого Domain
Нужно определить, может ли business одного Domain напрямую импортировать pure function другого Domain или cross-domain связь всегда должна проходить через `Deps`.
Возможный компромисс:
- type-only contracts разрешены;
- runtime API передаётся через ports;
- pure function import разрешён только как явно зафиксированная ацикличная Domain dependency.
## Уровни архитектуры
### OPEN-N010: На каком уровне появляется Domain
Статус: предварительно закрыт в пользу трёх уровней. Более высокий уровень добавляет требования и может потребовать структурного рефакторинга без изменения предметного владельца.
Текущая шкала:
```text
Level 1: базовые слои и модули
Level 2: доменные модули без строгой внутренней формы
Level 3: business, factories, ports, adapters, presets и verification внутри Domain
```
## Проверяемость
### OPEN-N011: Architecture lint
Будущие проверки могут контролировать:
- запрещённые imports из `business/**`;
- отсутствие server-only graph в isomorphic entrypoint;
- отсутствие client framework в factory graph;
- разрешённые категории exports business public API;
- запрет `export *` на environment boundaries;
- cycles между Domain modules;
- preset lifecycle declarations.
Семантическую чистоту функции нельзя надёжно доказать только по имени export. Для этого потребуется сочетание folder conventions, import restrictions, AST checks и public API tests.

View File

@@ -1,141 +0,0 @@
# Presets и SSR
> Рабочая заметка. Не является нормативным разделом спецификации.
## Определение
### PRE-N003: Preset является готовым вариантом assembly
Preset выбирает implementations ports и создаёт API одной business factory для конкретного execution context.
```ts
export const createKnvAuthBusiness = (): AuthApi => {
return authFactory({
authPhone: knvAuthPhoneAdapter,
session: appAuthSessionAdapter,
})
}
```
`createKnvAuthBusiness` является preset builder, а не второй factory и не единственно допустимое место сборки.
## Несколько presets одной factory
```text
authFactory
├── createBrowserAuth
├── createAuthForRequest
├── createAuthForServerAction
└── другие production presets
tests и custom graph owners могут вызывать authFactory напрямую
```
### PRE-N004: Presets могут отличаться adapters и lifecycle
Browser preset может использовать browser storage и query runtime. Request preset может использовать cookies, headers и request-scoped client. Tests вместо общего preset создают локальную per-test assembly с memory ports, mocks или fakes.
Business rules и форма создаваемого `AuthApi` при этом не меняются.
### PRE-N005: Preset может предоставлять суженный API view
Preset может не раскрывать consumer все методы созданного API:
```ts
export type AuthSsrApi = Pick<AuthApi, 'resolveSession'>
export const createAuthForRequest = (
input: AuthRequestInput,
): AuthSsrApi => {
const authApi = authFactory(createRequestAuthDeps(input))
return {
resolveSession: authApi.resolveSession,
}
}
```
Это ограничивает contract конкретного scope, но не создаёт новую business factory.
## SSR
### PRE-N006: Request владеет instance, созданным request preset
Если API зависит от cookies, headers, tenant, locale, request ID или abort signal, preset создаёт новый instance для каждого request и передаёт ownership вызывающему request scope.
Application singleton для request data недопустим, потому что может смешать состояния независимых запросов. Если preset создаёт disposable resource, результат должен позволить request owner выполнить cleanup.
Предварительная форма:
```ts
import 'server-only'
export const createAuthForRequest = (
input: AuthRequestInput,
): AuthApi => {
return authFactory({
authPhone: createServerAuthPhoneAdapter(input),
session: createRequestSessionAdapter(input),
})
}
```
### PRE-N007: SSR использует тот же business contract
Преимущества одной factory:
- одинаковые business rules в browser и на server;
- одинаковые domain types и errors;
- request adapters не протекают в business;
- factory тестируется без Next.js;
- backend, cookies и headers заменяются независимо;
- server rendering не требует второй реализации business.
## Server-only boundary
### PRE-N008: Environment-specific preset может иметь отдельный public entrypoint
Если preset должен быть недостижим из client graph, проект может выделить для него отдельный entrypoint и использовать framework/build marker. Имя и физическая группировка preset не задаются SLM.
```ts
// Один из возможных server-only preset entrypoints.
import 'server-only'
export { createAuthForRequest } from './create-auth-for-request'
```
Этот entrypoint не реэкспортируется через:
- `domains/auth/business`;
- browser preset;
- React client binding;
- общий Domain barrel.
Server adapters также могут иметь собственный `server-only` marker для защиты от ошибочного прямого импорта.
### PRE-N009: Isomorphic factory не импортирует server-only marker
`server-only` относится к preset/framework boundary, а не к business factory. Это позволяет вызывать factory в unit tests, другом server framework или browser preset.
## Browser boundary
### PRE-N010: Client-compatible preset не достигает server-only graph
Client-compatible preset импортирует только isomorphic business и совместимые с ним adapters. Secrets, privileged SDK и Node-only modules не должны входить в его transitive import graph.
Framework marker `'use client'` размещается в framework binding или client entrypoint, а не в business.
## Preset не является обязательным посредником
### PRE-N011: Custom assembly остаётся допустимой
Graph owner может напрямую вызвать factory:
```ts
const authApi = authFactory({
authPhone: customAuthPhoneAdapter,
session: memorySessionAdapter,
})
```
Preset нужен для повторяемой готовой конфигурации. Он не ограничивает DI-возможности factory.

View File

@@ -1,402 +0,0 @@
# Тестирование Domain
> Рабочая заметка. Не является нормативным разделом спецификации.
## Главный принцип
### TST-N001: Тест размещается у владельца проверяемой ответственности
Domain не получает одну общую папку `tests/` для всего кода. Business behavior, adapter wiring, preset lifecycle, framework bindings и UI имеют разных владельцев и тестируются рядом с ними.
```text
business behavior → business tests
pure domain rule → colocated business test
adapter behavior → adapter test
preset assembly → preset test
framework lifecycle → framework binding test
UI interaction → UI owner test
cross-domain graph → graph owner test
```
## Матрица покрытия
| Граница | Предварительная обязательность | Что проверяется |
|---|---|---|
| Business factory | Главная, обязательная | Scenarios, state, errors, ports, порядок effects |
| Public pure business functions | Обязательная | Validation, normalization, invariants и edge cases |
| Internal runtime-safe logic | По сложности | Mappers, guards, parsers, races и branching |
| Domain error implementation | Обязательная при runtime errors | Codes, guard, observable fields и source isolation |
| Adapters | Обязательная при наличии | Port contract, payload, raw result/error и cleanup |
| Production presets | Обязательная при наличии | Wiring, scope, ownership transfer и construction safety |
| Framework bindings | При наличии поведения | Provider, hooks, reactivity, lifecycle и hydration |
| Domain-owned UI | При наличии значимого поведения | States, interactions и accessibility contract |
| Cross-domain graph | При наличии graph | Assembly order, API handoff, scope и cleanup |
| E2E | По продуктовой потребности | Полный пользовательский поток |
## Предварительная структура
```text
domains/auth/
├── business/
│ ├── auth.factory.ts
│ ├── index.ts
│ ├── index.test.ts
│ ├── errors/
│ │ ├── auth-error.ts
│ │ └── auth-error.test.ts
│ ├── lib/
│ │ ├── auth-phone.ts
│ │ └── auth-phone.test.ts
│ ├── services/
│ ├── types/
│ └── tests/
│ └── factory/
│ ├── public-api.test.ts
│ ├── request-phone-otp.test.ts
│ ├── resend-phone-otp.test.ts
│ ├── verify-phone-otp.test.ts
│ └── testing/
│ └── create-auth-test-harness.ts
├── presets/
│ └── {preset-name}/
│ ├── adapters/
│ │ ├── auth-source.adapter.ts
│ │ └── auth-source.adapter.test.ts
│ ├── create-auth.ts
│ ├── create-auth.test.ts
│ └── index.ts
└── {framework-binding}/
├── auth.provider.tsx
├── auth.provider.test.tsx
├── use-auth.ts
└── use-auth.test.tsx
```
Это карта возможных тестов, а не обязательный scaffold. Файл создаётся только вместе с реальным поведением, которое требуется проверить.
## Business tests
### TST-N002: Factory-level tests являются главными тестами Domain behavior
Business factory тестируется как black box через public API business-модуля:
```ts
import {
authFactory,
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business'
```
Factory-level tests не зависят от React, Next.js, production SDK, real storage или production presets. Все runtime capabilities заменяются test ports, mocks, stubs или in-memory fakes.
Обязательная матрица для public scenarios:
- форма возвращаемого API;
- отсутствие side effects при вызове factory;
- happy path;
- input validation;
- нормализация результатов ports;
- nullable, empty и malformed results;
- rejected promise dependency;
- synchronous throw dependency;
- stable domain error code;
- отсутствие raw source error как consumer contract;
- порядок side effects;
- остановка следующих effects после failure;
- state transitions;
- repeated и concurrent calls, если они влияют на контракт;
- lifecycle operations и cleanup, если они входят в public business API.
Если business behavior невозможно проверить без React, Vue, Next.js или concrete SDK, это сигнал о проникновении framework/runtime ответственности внутрь business.
### TST-N003: Factory-level test использует per-test assembly
Каждый test case создаёт factory с нужной именно ему конфигурацией ports:
```ts
it('maps source failure to domain error', async () => {
const cause = new Error('Network failed')
const requestCode = vi.fn().mockRejectedValue(cause)
const { api } = createAuthTestHarness({ requestCode })
await expect(api.requestPhoneOtp(phone)).rejects.toMatchObject({
code: AUTH_ERROR_CODES.PHONE_OTP_REQUEST_FAILED,
})
})
```
Другой test case создаёт независимую assembly:
```ts
it('does not call source for invalid phone', async () => {
const requestCode = vi.fn()
const { api } = createAuthTestHarness({ requestCode })
await expect(api.requestPhoneOtp('123')).rejects.toMatchObject({
code: AUTH_ERROR_CODES.PHONE_OTP_PHONE_INVALID,
})
expect(requestCode).not.toHaveBeenCalled()
})
```
### TST-N004: Test harness не является preset
Test harness является private test utility, которая уменьшает boilerplate и предоставляет observability:
```ts
const { api, ports, state } = createAuthTestHarness(overrides)
```
Test harness:
- private для конкретной test suite;
- не экспортируется production entrypoint;
- допускает произвольные scenario-specific overrides;
- создаёт новый API instance для каждого test case;
- не представляет устойчивую application environment;
- не имеет собственного production lifecycle;
- не размещается в `presets/`.
Общий `test preset` по умолчанию не создаётся. Если Storybook, demo application или e2e environment получают устойчивую именованную конфигурацию, это отдельный application preset, а не универсальная конфигурация unit tests.
Предварительное имя helper:
```text
business/tests/factory/testing/create-auth-test-harness.ts
```
## Public API tests
### TST-N005: Business public API проверяется отдельно
Runtime public exports фиксируются тестом entrypoint:
```ts
import * as authBusiness from '.'
expect(Object.keys(authBusiness).sort()).toEqual([
'AUTH_ERROR_CODES',
'authFactory',
'isAuthError',
'normalizeAuthPhone',
'validateAuthPhone',
])
```
Этот тест обнаруживает случайный runtime export, но не видит type-only exports. Полная проверка type surface должна выполняться будущим architecture lint или TypeScript API check.
Форма API instance также фиксируется factory-level test:
```ts
expect(Object.keys(authFactory(ports)).sort()).toEqual([
'requestPhoneOtp',
'resendPhoneOtp',
'signOut',
'verifyPhoneOtp',
])
```
## Pure domain functions
### TST-N006: Pure functions тестируются рядом с реализацией
```text
business/lib/auth-phone.ts
business/lib/auth-phone.test.ts
```
Проверяются:
- canonical values;
- boundary values;
- malformed input;
- normalization;
- invariants;
- отсутствие mutation входа;
- детерминированность результата.
```ts
describe('normalizeAuthPhone', () => {
it.each([
['8 (999) 111-22-33', '+79991112233'],
['+7 999 111 22 33', '+79991112233'],
['123', null],
])('normalizes %s', (input, expected) => {
expect(normalizeAuthPhone(input)).toBe(expected)
})
})
```
Business scenario повторно применяет то же правило на своей границе. UI validation не заменяет business validation.
## Internal tests
### TST-N007: Colocated tests дополняют public contract tests
Colocated tests оправданы для:
- mappers и normalizers;
- runtime guards и parsers;
- private error implementation;
- сложного branching;
- race/concurrency algorithms;
- reusable internal pure functions.
Отдельный test каждого service не требуется автоматически. Factory-level tests остаются главным доказательством, что внутренняя реализация подключена к public scenario правильно.
Service test добавляется, если он существенно упрощает проверку сложного внутреннего алгоритма и не дублирует целиком factory-level matrix.
## Domain errors
### TST-N008: Consumer contract ошибки тестируется без public constructor
Factory-level test проверяет observable contract:
```ts
try {
await api.verifyPhoneOtp(data)
} catch (error) {
expect(isAuthError(error)).toBe(true)
if (isAuthError(error)) {
expect(error.code).toBe(
AUTH_ERROR_CODES.PHONE_OTP_VERIFY_CODE_INVALID,
)
}
}
```
Consumer-level test не использует private `AuthBusinessError` constructor и не зависит от `instanceof` internal class.
Colocated test error implementation может отдельно проверить:
- private constructor;
- `cause`;
- source code mapping;
- source metadata normalization;
- защиту от malformed error values.
## Adapter tests
### TST-N009: Adapter test проверяет port boundary, а не business behavior
Adapter test размещается рядом с adapter и проверяет:
- правильную concrete operation;
- transport payload;
- преобразование domain arguments в concrete arguments;
- raw/unknown result согласно port contract;
- проброс source error без создания domain error;
- subscription cleanup;
- отсутствие лишних SDK operations в минимальном client;
- environment boundary, если она проверяема build/lint средствами.
Adapter test не повторяет domain error mapping, business fallback и scenario orchestration.
Если несколько adapters реализуют один нетривиальный behavioral port contract, позднее можно выделить reusable contract test suite. Она остаётся test-only utility и не становится preset.
## Preset tests
### TST-N010: Production preset test проверяет assembly risk
Preset test размещается рядом с production preset и проверяет:
- выбор правильных adapters;
- передачу полного `Deps` в factory;
- exact narrowed API view, если preset его задаёт;
- отсутствие I/O при construction;
- отсутствие import-time subscriptions и storage reads;
- scope API instance;
- передачу lifecycle/dispose handles caller;
- изоляцию двух request-scoped instances;
- server/client import boundary.
Preset test не повторяет happy path и error matrix business scenarios. Эти гарантии принадлежат factory-level tests.
## Framework binding tests
### TST-N011: Framework binding тестируется через fake business API
Framework unit test по умолчанию получает fake API, а не собирает реальную factory:
```tsx
const authApi = createAuthApiFake()
render(
<AuthProvider api={authApi}>
<Consumer />
</AuthProvider>,
)
```
Проверяются:
- Provider предоставляет переданный instance;
- access hook возвращает правильный API;
- использование без Provider даёт предсказуемую ошибку;
- изменение framework-neutral state вызывает framework update;
- subscriptions запускаются в правильной lifecycle phase;
- cleanup выполняется после unmount;
- Strict Mode не запускает construction side effects;
- server snapshot и hydration согласованы, если binding участвует в SSR.
Отдельный smoke test с real factory и memory ports добавляется только при самостоятельном integration risk. Такой тест принадлежит framework module либо graph owner, который действительно собирает эту связку.
## UI tests
### TST-N012: Domain UI тестируется при наличии значимого поведения
Компонент не требует test только потому, что он существует. Test оправдан, если Domain-owned UI:
- содержит interaction;
- отображает несколько domain states;
- реагирует на domain error code;
- управляет focus или keyboard navigation;
- имеет значимый accessibility contract;
- использует framework lifecycle;
- содержит регрессионно опасную presentation logic.
Проверяются observable behavior и accessibility semantics, а не внутренняя структура JSX/Vue template.
Snapshot-only tests не являются обязательным доказательством. Визуальные различия при необходимости проверяются отдельным visual regression инструментом.
Universal UI module тестируется в слое `ui`, а page/screen/composition UI тестируется у соответствующего composition owner. Наличие React/Vue само по себе не переносит ownership теста в Domain.
## Graph и E2E tests
### TST-N013: Cross-domain graph тестируется у graph owner
Проверяются:
- topological assembly order;
- передача собранных API в dependent factories;
- exact graph type;
- отсутствие повторной assembly без нужного scope;
- ownership instance;
- lifecycle start и cleanup;
- request/application/page isolation.
Business modules не содержат tests полного application graph.
### TST-N014: E2E дополняет, но не заменяет Domain tests
E2E проверяет пользовательский поток через реальный application entry. Он не заменяет factory-level tests, потому что не способен дешёво и детерминированно перебрать malformed responses, synchronous throws, races и все domain error mappings.
## Чего избегать
### TST-N015: Test suite не повторяет одну ответственность на всех уровнях
Не рекомендуется:
- повторять одну scenario matrix в service, factory, preset и framework tests;
- тестировать business через production SDK;
- использовать общий mutable API instance между tests;
- экспортировать test harness из production public API;
- создавать `presets/testing` как default-механизм unit tests;
- проверять private implementation из factory-level tests;
- считать type-only файл требующим runtime unit test;
- использовать real network или process env в business tests.
Минимальная правильная граница предпочтительнее большого количества дублирующих tests.