mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
sync
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
## Миграция
|
## Миграция
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 зависимости с уже переведёнными пакетами.
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код.
|
||||||
|
|||||||
@@ -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`:
|
||||||
|
|||||||
@@ -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 не импортируются.
|
||||||
|
|||||||
@@ -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'>
|
||||||
|
|||||||
@@ -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-тест не заменяет автоматическую проверку или архитектурное ревью.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.
|
||||||
|
|||||||
@@ -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` и тесты.
|
||||||
|
|||||||
@@ -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}`.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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 пока остаётся открытым вопросом.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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 пока требует отдельных примеров.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
Reference in New Issue
Block a user