diff --git a/DRAFT_domains/README.md b/DRAFT_domains/README.md new file mode 100644 index 0000000..8b430c4 --- /dev/null +++ b/DRAFT_domains/README.md @@ -0,0 +1,86 @@ +# 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 new file mode 100644 index 0000000..3f8f0a4 --- /dev/null +++ b/DRAFT_domains/auth-example.md @@ -0,0 +1,173 @@ +# 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 new file mode 100644 index 0000000..d8f4912 --- /dev/null +++ b/DRAFT_domains/business.md @@ -0,0 +1,174 @@ +# 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 new file mode 100644 index 0000000..759eb42 --- /dev/null +++ b/DRAFT_domains/domain.md @@ -0,0 +1,206 @@ +# 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 new file mode 100644 index 0000000..6f0bec3 --- /dev/null +++ b/DRAFT_domains/factory-ports-adapters.md @@ -0,0 +1,200 @@ +# 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 new file mode 100644 index 0000000..9555fa1 --- /dev/null +++ b/DRAFT_domains/framework-bindings.md @@ -0,0 +1,109 @@ +# 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 new file mode 100644 index 0000000..6856f33 --- /dev/null +++ b/DRAFT_domains/open-questions.md @@ -0,0 +1,110 @@ +# Открытые вопросы 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 new file mode 100644 index 0000000..bb24ba9 --- /dev/null +++ b/DRAFT_domains/presets.md @@ -0,0 +1,141 @@ +# 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 new file mode 100644 index 0000000..5ebde0e --- /dev/null +++ b/DRAFT_domains/testing.md @@ -0,0 +1,402 @@ +# Тестирование 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.