diff --git a/DRAFT/level-2/README.md b/DRAFT/level-2/README.md index cc824b6..aa129dd 100644 --- a/DRAFT/level-2/README.md +++ b/DRAFT/level-2/README.md @@ -13,6 +13,7 @@ Level 2 соблюдает определения и правила Level 1, к | Порядок `app → compositions → domains → infra → ui → shared` | Сохраняется | | Модуль, Group, сегмент, компонент, публичный API и граф зависимостей | Сохраняют смысл | | Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются доменным пакетом | +| Единый публичный API модуля `business` | Представлен тремя объявленными фасетами одного логического API | | Навигационная Group слоя `domains` | Может содержать доменные пакеты | | Group внутри доменного пакета | Содержит обычные SLM-модули и Groups | @@ -22,7 +23,7 @@ Level 2 соблюдает определения и правила Level 1, к 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/ # Доменный пакет ├── README.md # Необязательная metadata ├── business/ # Обязательный SLM-модуль - │ └── index.ts - ├── presets/ # Необязательная Group + │ ├── index.ts # Только public types + │ ├── factory.ts # Public factory entry + │ └── error.ts # Public error runtime entry + ├── presets/ # Обязательная непустая Group │ ├── browser/ # SLM-модуль │ └── request/ # SLM-модуль - ├── adapters/ # Необязательная Group + ├── adapters/ # При наличии technical dependencies │ └── identity-provider/ # SLM-модуль └── react/ # Необязательная framework Group ├── session/ # SLM-модуль @@ -51,13 +54,15 @@ src/domains/ Приложение получает данные, состояние и результаты домена через готовый экземпляр `DomainApi`. Технический код импортирует только API конкретного модуля: ```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 { AuthSessionProvider } from '@/domains/auth/react/session' 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. ## Миграция diff --git a/DRAFT/level-2/dependencies.md b/DRAFT/level-2/dependencies.md index 0738072..e8bb83a 100644 --- a/DRAFT/level-2/dependencies.md +++ b/DRAFT/level-2/dependencies.md @@ -7,23 +7,30 @@ - [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007) - [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012) - [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013) +- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019) +- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021) +- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022) - [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) ## Направление внутри пакета | Исходный модуль | Допустимые зависимости | |---|---| -| `business` | Собственные сегменты, объявленный нейтральный `shared`, объявленные business-safe внешние пакеты, type-only публичные business-контракты других доменов | -| Adapter | Собственный `business`, `infra`, конкретная техническая реализация, `shared` | -| Preset | Собственный `business`, закрытые или самостоятельные adapters, type-only API других доменов | -| Framework binding module | Собственный `business`, публичные API framework-модулей своего домена, фреймворк, `ui`, `shared` | -| Место сборки графа | Публичные API presets и framework-модулей всех входящих в граф доменов | +| `business` | Собственные файлы, объявленный нейтральный `shared`, business-safe внешние пакеты, type-only `business` других доменов | +| Adapter module | Type-only barrel собственного `business`, `infra`, конкретная техническая реализация, `shared` | +| Preset | Type-only barrel и `factory` собственного `business`, публичные adapter-модули своего домена, type-only API других доменов | +| Framework binding module | Type-only barrel и `error` собственного `business`, публичные framework-модули своего домена, фреймворк, `ui`, `shared` | +| Место сборки графа | Presets либо `business/factory` и adapter-модули, `business/error`, framework-модули входящих в граф доменов | -`business` не достигает 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 import type { AuthApi } from '@/domains/auth/business' @@ -39,7 +46,7 @@ Pure function, hook, Provider, context, component или framework state дру ## Runtime-инъекция API -Готовый API другого домена передаётся preset-модулю аргументом. Preset не импортирует его runtime-фабрику или сборку: +Готовый API другого домена передаётся preset-модулю или одноразовому месту сборки аргументом. Код зависимого доменного пакета не импортирует его runtime-фабрику или сборку: ```text createAuthForRequest() diff --git a/DRAFT/level-2/domains/README.md b/DRAFT/level-2/domains/README.md index fb3ae64..163e2f6 100644 --- a/DRAFT/level-2/domains/README.md +++ b/DRAFT/level-2/domains/README.md @@ -5,8 +5,8 @@ ```text domains/auth/ ├── business/ -├── presets/ -├── adapters/ +├── presets/ # Обязательная Group +├── adapters/ # При наличии technical dependencies └── react/ ├── session/ └── login-form/ @@ -15,9 +15,9 @@ domains/auth/ ## Основные границы - [Доменный пакет](./domain-package.md) определяет предметную и структурную границу. -- [Business](./business.md) владеет `DomainApi`, фабрикой и доменными ошибками. -- [Фабрика, зависимости и adapters](./factory-ports-adapters.md) отделяют business от технической реализации. -- [Presets](./presets.md) собирают один API для нужных окружений. +- [Business](./business.md) владеет `DomainApi` и разделяет public types, factory и error runtime по трём фасетам. +- [Фабрика, зависимости и adapters](./factory-ports-adapters.md) требуют отдельный SLM-модуль для каждой production adapter implementation. +- [Presets](./presets.md) обязательны и собирают один API для нужных окружений. - [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка. - [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу. - [Миграция auth](./auth-example.md) показывает переход с Level 1. diff --git a/DRAFT/level-2/domains/auth-example.md b/DRAFT/level-2/domains/auth-example.md index ae9a9a7..a50072b 100644 --- a/DRAFT/level-2/domains/auth-example.md +++ b/DRAFT/level-2/domains/auth-example.md @@ -5,6 +5,10 @@ ## Связанное правило - [`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 @@ -29,10 +33,18 @@ domains/auth/ # Доменный пакет │ ├── lib/ │ ├── services/ │ ├── types/ -│ └── index.ts -├── presets/ # Group +│ ├── 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 +├── presets/ # Обязательная Group │ ├── browser/ # SLM-модуль -│ │ ├── adapters/ │ │ └── index.ts │ └── request/ # SLM-модуль │ └── index.ts @@ -47,20 +59,35 @@ domains/auth/ # Доменный пакет ## Перенос ответственности -| Исходная часть | Владелец Level 2 | -|---|---| -| Сценарии, предметные типы, единый API | `auth/business` | -| Коды, тип и guard ошибок | `auth/business` | -| Browser storage и HTTP adapters | `auth/presets/browser` | -| Cookies, request data и server adapters | `auth/presets/request` | -| Provider и session hooks | `auth/react/session` | -| Переиспользуемая форма | `auth/react/login-form` | -| Страница, текст и redirect | `compositions` | +| Исходная часть | Владелец Level 2 | Публичный путь | +|---|---|---| +| Сценарии и public types | `auth/business` | `auth/business` | +| Runtime-фабрика | `auth/business` | `auth/business/factory` | +| Коды и guards ошибок | `auth/business` | `auth/business/error` | +| Browser storage и HTTP adapters | Соответствующий adapter-модуль | `auth/adapters/*` | +| Cookies, request data и server adapters | Соответствующий adapter-модуль | `auth/adapters/*` | +| Выбор browser implementations | `auth/presets/browser` | `auth/presets/browser` | +| Выбор 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 -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 { AuthSessionProvider } from '@/domains/auth/react/session' import { LoginForm } from '@/domains/auth/react/login-form' @@ -90,12 +117,13 @@ User не импортирует runtime-код Auth, а его React-модул ## Порядок перехода 1. Определить dependency-connected набор доменов, который нужно мигрировать вместе. -2. Выделить `business` и одну фабрику без environment-specific import-графа. +2. Выделить `business` и три публичных фасета: type-only barrel, `factory` и `error`. 3. Зафиксировать `DomainApi`, error codes, error type и runtime guard. -4. Перенести browser/server wiring в нужные presets и adapters. -5. Разделить React-ответственности на модули внутри Group `react`. -6. Перенести страницы, redirects и multi-domain UI в `compositions`. -7. Перевести внешние импорты на module-specific paths. -8. Удалить старый root `index.ts` и проверить import-граф. +4. Оформить каждую production implementation отдельным модулем `adapters/*`. +5. Создать минимум один preset и перенести туда повторяемый выбор adapter-модулей. +6. Разделить React-ответственности на модули внутри Group `react`. +7. Перенести страницы, redirects и multi-domain UI в `compositions`. +8. Перевести внешние импорты на разрешённые public paths. +9. Удалить старый root `index.ts` и проверить import-граф. Простые доменные модули могут оставаться в SLM root только как временное миграционное состояние. Они не создают прямые runtime- или type-only зависимости с уже переведёнными пакетами. diff --git a/DRAFT/level-2/domains/business.md b/DRAFT/level-2/domains/business.md index 8d65b99..0b4d675 100644 --- a/DRAFT/level-2/domains/business.md +++ b/DRAFT/level-2/domains/business.md @@ -11,6 +11,8 @@ - [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009) - [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010) - [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018) +- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019) +- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022) ## Роль @@ -28,10 +30,13 @@ ## Публичный API модуля -```ts -export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error' -export { authFactory } from './auth.factory' +Один логический API `business` разделён на три фиксированных фасета. +### Type-only barrel + +Корневой `business/index.ts` экспортирует только типы: + +```ts export type { AuthApi, AuthDeps, @@ -42,7 +47,57 @@ export type { } 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 @@ -56,13 +111,13 @@ export type 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. ## Обязательный контракт ошибок -Каждый `business` объявляет устойчивые коды, безопасную readonly-форму и runtime guard: +Каждый `business` объявляет устойчивые коды, безопасную readonly-форму и runtime guard. Типы публикуются через `business`, а runtime symbols через `business/error`: ```ts export const AUTH_ERROR_CODES = { diff --git a/DRAFT/level-2/domains/domain-package.md b/DRAFT/level-2/domains/domain-package.md index 10f460b..1efb4b9 100644 --- a/DRAFT/level-2/domains/domain-package.md +++ b/DRAFT/level-2/domains/domain-package.md @@ -8,6 +8,10 @@ - [`SLM-L2-DOMAIN-A003`](../../rules/level-2.md#slm-l2-domain-a003) - [`SLM-L2-GROUP-R004`](../../rules/level-2.md#slm-l2-group-r004) - [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005) +- [`SLM-L2-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; - декларативный manifest или декларативная конфигурация архитектурной проверки; - обязательный модуль `business`; -- Groups допустимых ролей. +- обязательная непустая Group `presets`; +- непустая Group `adapters`, если фабрика имеет технические зависимости; +- Framework Groups при наличии соответствующих модулей. В корне запрещены: @@ -46,19 +52,24 @@ Metadata содержит только статические данные, не ## Модули и 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 auth/ ├── 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-модуль └── react/ # Framework Group ├── session/ # SLM-модуль └── login-form/ # SLM-модуль ``` -Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/presets/browser`, `auth/react/session`, но не `auth`, `auth/presets` или `auth/react`. +Groups не имеют `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 diff --git a/DRAFT/level-2/domains/factory-ports-adapters.md b/DRAFT/level-2/domains/factory-ports-adapters.md index d0a2b07..f563b4c 100644 --- a/DRAFT/level-2/domains/factory-ports-adapters.md +++ b/DRAFT/level-2/domains/factory-ports-adapters.md @@ -8,6 +8,9 @@ - [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008) - [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010) - [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) +- [`SLM-L2-BUSINESS-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. ```ts +import type { AuthApi, AuthDeps } from '@/domains/auth/business' + export type AuthFactory = (deps: AuthDeps) => AuthApi ``` +Runtime-фабрика импортируется только через отдельный entry point: + +```ts +import { authFactory } from '@/domains/auth/business/factory' +``` + Рекомендуется сохранять сам вызов фабрики чистым: он создаёт объекты и 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 business dependency ← adapter → SDK / storage / platform / request data @@ -64,23 +75,40 @@ Adapter преобразует аргументы и технический ре Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение никогда не строит поведение по HTTP status, SDK error class или storage exception. -## Размещение adapter +## Размещение adapters -Одноразовый adapter остаётся закрытым сегментом preset-модуля: - -```text -auth/presets/browser/ -├── adapters/ -│ └── phone.adapter.ts -└── index.ts -``` - -Adapter становится самостоятельным SLM-модулем, когда нужен нескольким presets или имеет отдельную integration responsibility: +Каждая production-реализация является отдельным SLM-модулем в Group `adapters`, даже если пока используется одним preset: ```text auth/adapters/ -└── identity-provider/ +├── phone-http/ +│ └── index.ts +└── browser-session/ └── 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-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код. diff --git a/DRAFT/level-2/domains/framework-bindings.md b/DRAFT/level-2/domains/framework-bindings.md index daec429..55d763b 100644 --- a/DRAFT/level-2/domains/framework-bindings.md +++ b/DRAFT/level-2/domains/framework-bindings.md @@ -7,6 +7,8 @@ - [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) - [`SLM-L2-FRAMEWORK-R014`](../../rules/level-2.md#slm-l2-framework-r014) - [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015) +- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019) +- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022) ## Framework Group @@ -41,6 +43,22 @@ Framework binding module может: Он не вызывает 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 `auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthApi`: diff --git a/DRAFT/level-2/domains/open-questions.md b/DRAFT/level-2/domains/open-questions.md index 676f1ee..cf46b54 100644 --- a/DRAFT/level-2/domains/open-questions.md +++ b/DRAFT/level-2/domains/open-questions.md @@ -8,10 +8,14 @@ - Level 2 заменяет доменный модуль доменным пакетом. - Корень пакета содержит только metadata, модули и Groups и не имеет executable API. - `business` предоставляет одну фабрику и один `DomainApi`. +- Публичный API `business` разделён на type-only barrel, `business/factory` и `business/error`; другие пути запрещены. - Приложение получает доменные данные, состояние и результаты только через `DomainApi`. - Каждый `business` экспортирует коды, тип и runtime guard доменных ошибок. - Ожидаемые ошибки 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. - Framework Group называется по фреймворку и содержит самостоятельные SLM-модули. - Cross-domain framework state, hooks, contexts и components не импортируются. diff --git a/DRAFT/level-2/domains/presets.md b/DRAFT/level-2/domains/presets.md index 155cbbe..707c625 100644 --- a/DRAFT/level-2/domains/presets.md +++ b/DRAFT/level-2/domains/presets.md @@ -8,10 +8,14 @@ - [`SLM-L2-PRESET-R011`](../../rules/level-2.md#slm-l2-preset-r011) - [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) - [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013) +- [`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 authFactory @@ -20,29 +24,39 @@ authFactory └── presets/server-action → AuthApi server action ``` -Архитектура не требует обязательный `base` или изоморфный preset и не ограничивает количество presets. Проект создаёт только те сборки, которые нужны его реальным средам и областям использования. +Архитектура не требует `base` или изоморфный preset и не ограничивает максимальное количество presets. Обязательный preset должен соответствовать реальному поддерживаемому контексту, а не существовать только для заполнения структуры. -Если фабрика используется в одном месте и отдельная повторяемая конфигурация не возникает, место сборки графа может вызвать её напрямую. +Место сборки графа в `composition` может вызвать фабрику напрямую для одноразовой конфигурации. Такая сборка не отменяет обязательный preset пакета и при наличии технических зависимостей использует публичные adapter-модули, а не inline implementations. ## Один контракт API -Каждый preset выбирает технические реализации, но вызывает одну и ту же фабрику и возвращает один контракт `DomainApi`: +Каждый preset вызывает одну и ту же фабрику и возвращает один контракт `DomainApi`. При наличии технических зависимостей preset выбирает их публичные adapter-модули: ```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 => { return authFactory({ - phone: createHttpPhoneAdapter(), + phone: createPhoneHttpAdapter(), session: createBrowserSessionAdapter(), }) } ``` ```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 = ( input: AuthRequestInput, ): AuthApi => { return authFactory({ - phone: createServerPhoneAdapter(input), + phone: createRequestPhoneAdapter(input), session: createRequestSessionAdapter(input), }) } @@ -54,10 +68,13 @@ Server preset может обращаться к database напрямую че ## Cross-domain input -Preset зависимого домена принимает готовый API аргументом: +Preset зависимого домена принимает готовый API аргументом. Cross-domain API не является adapter и не размещается в Group `adapters`: ```ts 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 = { authApi: Pick diff --git a/DRAFT/level-2/domains/testing.md b/DRAFT/level-2/domains/testing.md index 9d670d8..be601d7 100644 --- a/DRAFT/level-2/domains/testing.md +++ b/DRAFT/level-2/domains/testing.md @@ -2,9 +2,13 @@ > Проверка владельцев и публичных границ Level 2. -## Связанное правило +## Связанные правила - [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016) +- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019) +- [`SLM-L2-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 -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 }), })) @@ -32,25 +43,32 @@ await api.requestPhoneOtp('+79991112233') Набор проверяет успешные и ожидаемые ошибочные результаты, 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-сценариев. -## Архитектурные проверки +## Автоматические структурные проверки -Отдельная import-graph проверка подтверждает: +Проверка файлов, exports и import-графа подтверждает: - отсутствие root API доменного пакета и Framework Groups; +- наличие ровно трёх фасетов `business`, type-only exports в корневом barrel и отсутствие type exports в runtime-фасетах; +- соблюдение матрицы потребителей `business`, `business/factory` и `business/error`; +- наличие непосредственно в корне пакета непустой Group `presets` с объявленными модульными границами; - отсутствие runtime cross-domain imports; - отсутствие type-only импортов из чужих presets, adapters и framework-модулей; - отсутствие cross-domain framework hooks, contexts и components; - отсутствие server-only достижимости из client modules; - отсутствие runtime- и type-only циклов. -Runtime-тест не заменяет эти проверки. +## Архитектурное ревью + +На ревью проверяется, что `business/factory` экспортирует только фабрику, а `business/error` только error codes и guards. Для каждой технической зависимости рассматриваются все production implementations: каждая должна принадлежать отдельному модулю Group `adapters`, даже если используется один раз. Inline implementations во всём production-графе запрещены, а test-only fakes из этой проверки исключены. + +Runtime-тест не заменяет автоматическую проверку или архитектурное ревью. diff --git a/DRAFT/level-2/terminology.md b/DRAFT/level-2/terminology.md index 8ced6ca..978eb6d 100644 --- a/DRAFT/level-2/terminology.md +++ b/DRAFT/level-2/terminology.md @@ -28,6 +28,18 @@ Group, размещённая непосредственно в слое `domain `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 внешний пакет Внешняя библиотека, допустимая в 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 -Код, который связывает явную зависимость фабрики с SDK, storage, API платформы, данными запроса или техническим сервисом. Adapter может быть закрытым сегментом preset-модуля либо самостоятельным модулем в Group `adapters`. +SLM-модуль в Group `adapters`, который реализует одну или несколько связанных технических зависимостей фабрики поверх SDK, storage, API платформы, данных запроса или технического сервиса. Каждая production-реализация принадлежит adapter-модулю и не размещается внутри preset или composition. + +Group `adapters` обязательна и непуста, если фабрика имеет техническую зависимость. Фабрика без технических зависимостей не требует создания этой Group. Точная обязательная форма технических портов пока не определена и остаётся открытым вопросом Level 2. ### Preset -SLM-модуль в Group `presets`, который создаёт `DomainApi` для одного именованного контекста выполнения: браузера, запроса, server action или другого реального окружения. Он выбирает технические реализации и передаёт фабрике готовые runtime-зависимости. +SLM-модуль в Group `presets`, который создаёт `DomainApi` для одного именованного контекста выполнения: браузера, запроса, server action или другого реального окружения. При наличии технических зависимостей он выбирает их adapter-модули и передаёт фабрике готовые runtime-зависимости. -Архитектура не устанавливает минимальное или максимальное количество presets и не требует универсального изоморфного preset. +Каждый доменный пакет содержит минимум один preset. Архитектура не ограничивает их максимальное количество и не требует универсального изоморфного preset. ## 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 ├── модуль business - ├── Group presets + ├── обязательная Group presets │ └── preset-модуль - ├── Group adapters + ├── Group adapters при наличии технических зависимостей │ └── adapter-модуль └── Framework Group react ├── модуль session diff --git a/DRAFT/level-2/validation.md b/DRAFT/level-2/validation.md index 5312524..903d133 100644 --- a/DRAFT/level-2/validation.md +++ b/DRAFT/level-2/validation.md @@ -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-граф и объявленные границы, а не угадывать сущность только по имени папки. @@ -14,6 +14,9 @@ - исполняемый файл, root `index.ts`, состояние или реэкспорт в корне доменного пакета; - отсутствие `business` или несколько модулей `business` в одном пакете; +- отсутствие любого из трёх entry points `business`, `business/factory`, `business/error`, runtime export из корневого barrel, type export из runtime-фасета, другой публичный путь либо deep import внутри `business`; +- импорт фасета `business` потребителем, которому этот фасет не разрешён; +- отсутствие непосредственно в корне пакета непустой Group `presets` или наличие в ней прямого дочернего элемента без объявленной модульной границы; - deep imports во внутренние части модулей; - runtime- или type-only достижимость framework-, adapter-, preset-, infra- или environment-specific кода из `business`; - runtime-импорт любого экспорта другого доменного пакета; @@ -28,8 +31,11 @@ - представляет ли пакет одну связную предметную область; - является ли `DomainApi` единственным runtime-источником доменных данных и результатов для приложения; -- принадлежат ли коды, тип и guard доменных ошибок модулю `business`; +- является ли фабрика единственным runtime-экспортом `business/factory`; +- содержит ли `business/error` только runtime-коды и guards, а type-only barrel именованные типы DomainError и DomainErrorCode; - преобразует ли business ожидаемые технические и cross-domain сбои в собственные ошибки; +- является ли каждая production-реализация технической зависимости отдельным модулем Group `adapters`, включая реализации, используемые только в одном месте; +- отсутствуют ли production adapters вне Group `adapters` во всём production-графе; test-only fakes не участвуют в этой проверке; - представляет ли каждый preset один реальный контекст выполнения и сохраняет ли контракт фабрики; - принадлежит ли каждый framework binding module домену, а не странице или multi-domain сценарию; - соответствует ли каждый объявленный 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-тестами. @@ -53,6 +59,10 @@ Import-graph checks не заменяются runtime-тестами. - [`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-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) Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения. diff --git a/DRAFT/rules/level-2.md b/DRAFT/rules/level-2.md index b122ad4..95d2db8 100644 --- a/DRAFT/rules/level-2.md +++ b/DRAFT/rules/level-2.md @@ -1,6 +1,6 @@ # Правила 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** > -> Модуль `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 @@ -68,7 +68,7 @@ > **Роль preset** > -> Каждый preset является SLM-модулем одного именованного контекста выполнения, выбирает реализации явных зависимостей, вызывает единственную business-фабрику и не добавляет предметные сценарии, методы `DomainApi` или собственные доменные ошибки. +> Каждый preset является SLM-модулем одного именованного контекста выполнения, при наличии технических зависимостей выбирает их adapter-модули, вызывает единственную business-фабрику и не добавляет предметные сценарии, методы `DomainApi` или собственные доменные ошибки. ### SLM-L2-DEPENDENCY-A012 @@ -117,3 +117,31 @@ > **Business-safe внешний пакет** > > Внешний пакет объявляется 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` и тесты. diff --git a/DRAFT_domains/README.md b/DRAFT_domains/README.md deleted file mode 100644 index 8b430c4..0000000 --- a/DRAFT_domains/README.md +++ /dev/null @@ -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}`. diff --git a/DRAFT_domains/auth-example.md b/DRAFT_domains/auth-example.md deleted file mode 100644 index 3f8f0a4..0000000 --- a/DRAFT_domains/auth-example.md +++ /dev/null @@ -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. diff --git a/DRAFT_domains/business.md b/DRAFT_domains/business.md deleted file mode 100644 index d8f4912..0000000 --- a/DRAFT_domains/business.md +++ /dev/null @@ -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 -``` - -значение в `catch` всё равно имеет тип `unknown`. Если consumer различает ошибки по `code`, business должен предоставить runtime discriminator либо перейти на typed `Result`. - -Выбор между throw + guard и typed `Result` пока не закрыт окончательно. Текущий минимальный путь совместимости: throw + public observation contract. diff --git a/DRAFT_domains/domain.md b/DRAFT_domains/domain.md deleted file mode 100644 index 759eb42..0000000 --- a/DRAFT_domains/domain.md +++ /dev/null @@ -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 пока остаётся открытым вопросом. diff --git a/DRAFT_domains/factory-ports-adapters.md b/DRAFT_domains/factory-ports-adapters.md deleted file mode 100644 index 6f0bec3..0000000 --- a/DRAFT_domains/factory-ports-adapters.md +++ /dev/null @@ -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 - verifyCode: (data: VerifyPhoneOtpData) => Promise -} -``` - -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. diff --git a/DRAFT_domains/framework-bindings.md b/DRAFT_domains/framework-bindings.md deleted file mode 100644 index 9555fa1..0000000 --- a/DRAFT_domains/framework-bindings.md +++ /dev/null @@ -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 пока требует отдельных примеров. diff --git a/DRAFT_domains/open-questions.md b/DRAFT_domains/open-questions.md deleted file mode 100644 index 6856f33..0000000 --- a/DRAFT_domains/open-questions.md +++ /dev/null @@ -1,110 +0,0 @@ -# Открытые вопросы Domains - -> Эти вопросы намеренно не сформулированы как правила. - -## Ошибки - -### OPEN-N001: Throw или typed Result - -Нужно решить, остаются ли ожидаемые domain failures исключениями с public runtime guard или business API возвращает discriminated `Result`. - -Текущий совместимый вариант: 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. diff --git a/DRAFT_domains/presets.md b/DRAFT_domains/presets.md deleted file mode 100644 index bb24ba9..0000000 --- a/DRAFT_domains/presets.md +++ /dev/null @@ -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 - -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. diff --git a/DRAFT_domains/testing.md b/DRAFT_domains/testing.md deleted file mode 100644 index 5ebde0e..0000000 --- a/DRAFT_domains/testing.md +++ /dev/null @@ -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( - - - , -) -``` - -Проверяются: - -- 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.