mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 15:30:16 +03:00
feat: level-3 черновик
This commit is contained in:
96
DRAFT/level-3/README.md
Normal file
96
DRAFT/level-3/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# SLM Level 3
|
||||
|
||||
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
|
||||
|
||||
Level 3 предназначен для приложений с существенной доменной логикой, несколькими execution contexts или длительным сроком поддержки. Он не добавляет новый слой: он делает внутреннюю форму слоя `domains` явной и проверяемой.
|
||||
|
||||
## Когда выбирать Level 3
|
||||
|
||||
Level 3 оправдан, когда предметная область имеет устойчивый business contract, несколько concrete integrations, отдельные browser/request/server assemblies, сложный lifecycle или независимую долгую поддержку.
|
||||
|
||||
Количество файлов или размер проекта сами по себе не требуют перехода. Домен без такой сложности остаётся доменным модулем Level 2.
|
||||
|
||||
## Наследование предыдущих уровней
|
||||
|
||||
Проект Level 3 соблюдает определения и правила Levels 1-2, кроме явно заменённых положений.
|
||||
|
||||
| Положение | Статус в Level 3 |
|
||||
|---|---|
|
||||
| Порядок `app → compositions → domains → infra → ui → shared` | Сохраняется |
|
||||
| Модуль, Group, segment, component, public API и lifecycle | Сохраняют смысл Level 1 |
|
||||
| Доменный модуль Level 2 и [`SLM-L2-DOMAIN-R001`](../rules/level-2.md#slm-l2-domain-r001) | Заменяются Domain Level 3 |
|
||||
| Group внутри `domains` | Может содержать Domain, оставаясь только навигационной папкой |
|
||||
| Прямые дочерние modules Domain | Не являются вложенными modules, потому что Domain не является module |
|
||||
|
||||
Domain не содержит исполняемого кода, поэтому не отменяет правило Level 1 о модульном владельце. Он задаёт предметную границу; конкретной ответственностью, public API и lifecycle по-прежнему владеет module.
|
||||
|
||||
## Основная идея
|
||||
|
||||
```text
|
||||
Domain задаёт предметную границу.
|
||||
Business определяет contract и поведение.
|
||||
Ports описывают нужные business capabilities.
|
||||
Adapters связывают ports с concrete runtime.
|
||||
Presets собирают API для execution scope.
|
||||
Framework module адаптирует готовый API к framework.
|
||||
Graph owner удерживает API instance и выполняет lifecycle contract module-владельца.
|
||||
```
|
||||
|
||||
## Базовая форма Domain
|
||||
|
||||
```text
|
||||
src/domains/
|
||||
└── auth/ # Domain
|
||||
├── business/ # обязательный module
|
||||
│ ├── errors/
|
||||
│ ├── lib/
|
||||
│ ├── ports/
|
||||
│ ├── services/
|
||||
│ ├── types/
|
||||
│ └── index.ts
|
||||
├── presets/ # optional Group
|
||||
│ └── application/ # preset module
|
||||
│ ├── adapters/
|
||||
│ └── index.ts
|
||||
├── adapters/ # optional Group
|
||||
│ └── identity-provider/ # promoted adapter module
|
||||
│ └── index.ts
|
||||
└── react/ # framework module
|
||||
├── hooks/
|
||||
├── providers/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`business` обязателен. `presets`, `adapters` и framework modules появляются только при реальной ответственности. `types`, `errors`, `lib`, `services`, `tests`, `ui`, `client` и `server` не являются самостоятельными корневыми ветками Domain.
|
||||
|
||||
Domain может быть размещён непосредственно в `domains` или внутри навигационной Group. Groups допустимы, но не участвуют в основных примерах и не меняют границы Domain, направление зависимостей или доступность его role modules.
|
||||
|
||||
## Публичные границы
|
||||
|
||||
Domain не имеет root runtime entrypoint. Внешний код импортирует public API конкретного role module:
|
||||
|
||||
```ts
|
||||
import { authFactory, isAuthError } from '@/domains/auth/business'
|
||||
import { createApplicationAuth } from '@/domains/auth/presets/application'
|
||||
import { AuthProvider, useAuth } from '@/domains/auth/react'
|
||||
```
|
||||
|
||||
`@/domains/auth/business` является public API отдельного module, а не deep import. Напротив, `@/domains/auth/business/services/...` и root import `@/domains/auth` нарушают границу.
|
||||
|
||||
## Карта черновика
|
||||
|
||||
- [Терминология](./terminology.md)
|
||||
- [Граница Domain](./domains/domain.md)
|
||||
- [Business module](./domains/business.md)
|
||||
- [Factory, ports и adapters](./domains/factory-ports-adapters.md)
|
||||
- [Presets и SSR](./domains/presets.md)
|
||||
- [React module](./domains/framework-bindings.md)
|
||||
- [Зависимости](./dependencies.md)
|
||||
- [Тестирование](./domains/testing.md)
|
||||
- [Проверка](./validation.md)
|
||||
- [Auth как пример миграции](./domains/auth-example.md)
|
||||
- [Открытые вопросы](./domains/open-questions.md)
|
||||
|
||||
## Канонические правила
|
||||
|
||||
Level 3 использует правила Levels 1-2 и [дополнительный реестр Level 3](../rules/level-3.md). Тематические документы объясняют правила, но не объявляют их повторно.
|
||||
48
DRAFT/level-3/dependencies.md
Normal file
48
DRAFT/level-3/dependencies.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Зависимости Level 3
|
||||
|
||||
> Уточнение графа зависимостей Level 2 внутри Domain.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L3-BUSINESS-A004`](../rules/level-3.md#slm-l3-business-a004)
|
||||
- [`SLM-L3-DEPENDENCY-R011`](../rules/level-3.md#slm-l3-dependency-r011)
|
||||
- [`SLM-L3-ENVIRONMENT-A012`](../rules/level-3.md#slm-l3-environment-a012)
|
||||
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
|
||||
|
||||
## Направление внутри Domain
|
||||
|
||||
| Исходный role module | Допустимые зависимости |
|
||||
|---|---|
|
||||
| `business` | Собственные segments, нейтральный `shared`, ограниченные type-only contracts другого business module |
|
||||
| Adapter | Business contracts, `infra`, concrete runtime и neutral `shared` |
|
||||
| Preset | Business factory/contracts, private или promoted adapters |
|
||||
| `react` | Business contracts, готовый business API и React runtime |
|
||||
| Composition graph owner | Public preset, framework module и готовые API instances |
|
||||
|
||||
Business не импортирует adapters, presets, framework modules, `infra`, product SDK, storage, framework runtime, browser/Node API или environment configuration. Adapter не импортирует preset или framework module. Preset не импортирует framework module.
|
||||
|
||||
## Междоменные зависимости
|
||||
|
||||
Business одного Domain не создаёт factory другого Domain и не вызывает другой Domain runtime API напрямую. Если `orders` нужна capability авторизации, `orders/business` описывает собственный минимальный port, а graph owner передаёт реализацию над уже собранным `AuthApi`.
|
||||
|
||||
```text
|
||||
Auth preset
|
||||
→ AuthApi
|
||||
→ orders assembly
|
||||
→ OrdersApi
|
||||
```
|
||||
|
||||
Type-only import public business contract другого Domain допустим только при реальной ацикличной зависимости. Он не даёт права вызвать другой Domain runtime API. Direct runtime import даже pure function не является обходом port boundary: независимое общее правило должно принадлежать `shared`, а предметная capability передаётся через port.
|
||||
|
||||
## Environment boundaries
|
||||
|
||||
Server-only preset или adapter получает отдельный public entrypoint и framework/build marker. Он не реэкспортируется через `business`, `react`, client-compatible preset или root Domain.
|
||||
|
||||
```text
|
||||
business # isomorphic
|
||||
presets/application # client-compatible, если выбранные adapters совместимы
|
||||
presets/request # server-only
|
||||
react # client framework module
|
||||
```
|
||||
|
||||
Путь `server/` или `client/` сам по себе ничего не доказывает. Проверяется transitive import graph entrypoint.
|
||||
@@ -1,86 +1,81 @@
|
||||
# Domains: рабочие заметки
|
||||
# Домены Level 3
|
||||
|
||||
> Статус: исследовательский черновик. Материалы в этой папке не являются спецификацией и пока не задают обязательных правил SLM.
|
||||
> Пояснение строгой внутренней архитектуры Domain.
|
||||
|
||||
Эта папка фиксирует текущую гипотезу о новой сущности `Domain`, business-модуле внутри неё, framework-neutral factory, ports, adapters, presets и framework bindings.
|
||||
Level 3 превращает доменный module Level 2 в Domain: немодульную предметную границу с несколькими modules разных технических ролей. Это не новый слой и не обязательный scaffold для каждого проекта.
|
||||
|
||||
Level 3 развивает доменный модуль Level 2 в строгую доменную границу с несколькими модулями разных ролей. Такой переход может потребовать рефакторинга, но сохраняет предметного владельца и базовые модульные правила.
|
||||
## Связанные правила
|
||||
|
||||
Идентификаторы вида `DOM-N001` и `FAC-N001` являются стабильными якорями заметок. Они нужны для обсуждения и последующего переноса решений в спецификацию, но не являются идентификаторами нормативных правил.
|
||||
- [`SLM-L3-DOMAIN-R001`](../../rules/level-3.md#slm-l3-domain-r001)
|
||||
- [`SLM-L3-DOMAIN-A002`](../../rules/level-3.md#slm-l3-domain-a002)
|
||||
- [`SLM-L3-BUSINESS-R003`](../../rules/level-3.md#slm-l3-business-r003)
|
||||
- [`SLM-L1-MODULE-A004`](../../rules/level-1.md#slm-l1-module-a004)
|
||||
|
||||
## Основная формула
|
||||
## Роли внутри Domain
|
||||
|
||||
```text
|
||||
Business определяет ЧТО делать.
|
||||
Ports описывают ЧТО business нужно.
|
||||
Factory создаёт business API из ports.
|
||||
Adapters реализуют ports в конкретной среде.
|
||||
Preset выбирает adapters, scope и lifecycle.
|
||||
Framework binding подключает готовый API к React, Vue, Next.js и другим фреймворкам.
|
||||
Business определяет поведение и contract.
|
||||
Ports описывают runtime capabilities business.
|
||||
Adapters реализуют ports поверх concrete runtime.
|
||||
Presets собирают API для execution context.
|
||||
React module адаптирует готовый API к React.
|
||||
Graph owner удерживает конкретный instance и выполняет lifecycle contract module-владельца.
|
||||
```
|
||||
|
||||
Краткая схема:
|
||||
| Роль | Структурный вид | Когда появляется |
|
||||
|---|---|---|
|
||||
| `business` | Обязательный module | Всегда |
|
||||
| Preset | Module внутри `presets` | Нужна повторяемая assembly |
|
||||
| Adapter | Private segment preset или module внутри `adapters` | Нужна concrete integration |
|
||||
| `react` | Framework module непосредственно в Domain | Domain имеет React integration |
|
||||
|
||||
## Форма Domain
|
||||
|
||||
```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
|
||||
domains/auth/
|
||||
├── business/
|
||||
│ ├── errors/
|
||||
│ ├── lib/
|
||||
│ ├── ports/
|
||||
│ ├── services/
|
||||
│ ├── types/
|
||||
│ └── index.ts
|
||||
├── presets/
|
||||
│ └── application/
|
||||
│ ├── adapters/
|
||||
│ └── index.ts
|
||||
├── adapters/
|
||||
│ └── identity-provider/
|
||||
│ └── index.ts
|
||||
└── react/
|
||||
├── hooks/
|
||||
├── providers/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Зафиксированные гипотезы
|
||||
`business` обязателен; остальные ветки появляются по необходимости. `presets` и `adapters` являются Groups без собственного runtime/API. `errors`, `lib`, `ports`, `services`, `types`, `hooks` и `providers` являются segments соответствующих module-владельцев.
|
||||
|
||||
### DOM-N001: Domain является отдельной архитектурной сущностью
|
||||
Navigation Groups в слое `domains` допустимы, но не являются Domain и не изменяют его import boundary. Основные примеры Level 3 намеренно показывают Domain непосредственно в `domains`.
|
||||
|
||||
Domain является границей владения одной предметной областью. Он содержит modules и logical groups с разной технической ролью, но общей доменной принадлежностью.
|
||||
## Public API modules
|
||||
|
||||
### DOM-N002: Business внутри Domain является модулем
|
||||
Domain root не имеет `index.ts` и не реэкспортирует роли. Внешний consumer использует только public entrypoint нужного module:
|
||||
|
||||
`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/
|
||||
```ts
|
||||
import { authFactory, type AuthApi } from '@/domains/auth/business'
|
||||
import { createApplicationAuth } from '@/domains/auth/presets/application'
|
||||
import { AuthProvider, useAuth } from '@/domains/auth/react'
|
||||
```
|
||||
|
||||
`domains/` пока рассматривается как новая верхнеуровневая область, заменяющая разнесение одной доменной ответственности между `business/{domain}` и `compositions/business/{domain}`.
|
||||
Private adapter внутри `presets/application/adapters` не получает external entrypoint. Promoted adapter module получает собственный API для modules Domain; доступ за пределами Domain допускается только как явно объявленная integration extension point.
|
||||
|
||||
## Карта раздела
|
||||
|
||||
- [Граница Domain](./domain.md)
|
||||
- [Business module](./business.md)
|
||||
- [Factory, ports и adapters](./factory-ports-adapters.md)
|
||||
- [Presets и SSR](./presets.md)
|
||||
- [React module](./framework-bindings.md)
|
||||
- [Тестирование](./testing.md)
|
||||
- [Auth как пример миграции](./auth-example.md)
|
||||
- [Открытые вопросы](./open-questions.md)
|
||||
|
||||
@@ -1,78 +1,68 @@
|
||||
# Auth как проверочный пример
|
||||
# Auth как пример миграции
|
||||
|
||||
> Рабочая заметка на основе реального модуля `/home/gromov/projects/biocad/newbiocadru/apps/web/src/business/auth`. Код проекта не изменялся.
|
||||
> Проверочный пример Level 3. Он показывает направление декомпозиции, а не обязательный scaffold.
|
||||
|
||||
Цель примера: проверить гипотезы Domain на существующем SLM business-модуле, а не предложить немедленную миграцию.
|
||||
## Исходная проблема
|
||||
|
||||
## Текущее устройство
|
||||
В более ранней форме SLM business contract Auth и concrete assembly могли находиться отдельно:
|
||||
|
||||
```text
|
||||
business/auth/
|
||||
├── auth.factory.ts
|
||||
├── errors/
|
||||
├── hooks/
|
||||
├── mappers/
|
||||
├── services/
|
||||
├── tests/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Runtime-сборка находится отдельно:
|
||||
|
||||
```text
|
||||
compositions/business/knv/auth/
|
||||
compositions/business/auth/
|
||||
├── adapters/
|
||||
├── create-knv-auth-business.ts
|
||||
├── create-auth-business.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Новая сущность Domain может колоцировать обе ответственности без смешивания ролей:
|
||||
Такая форма отделяет pure business от concrete runtime, но разносит одну предметную область по разным архитектурным местам. Level 3 колоцирует их внутри Domain, не смешивая роли.
|
||||
|
||||
## Целевая форма
|
||||
|
||||
```text
|
||||
domains/auth/
|
||||
├── business/
|
||||
│ ├── auth.factory.ts
|
||||
│ ├── errors/
|
||||
│ ├── lib/
|
||||
│ ├── ports/
|
||||
│ ├── services/
|
||||
│ ├── types/
|
||||
│ └── index.ts
|
||||
├── presets/
|
||||
│ └── {preset-name}/
|
||||
│ └── adapters/
|
||||
└── {framework-binding}/
|
||||
│ ├── application/
|
||||
│ │ ├── adapters/
|
||||
│ │ └── index.ts
|
||||
│ └── request/
|
||||
│ └── index.ts
|
||||
└── react/
|
||||
├── hooks/
|
||||
├── providers/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Factory и client boundary
|
||||
## Разделение обязанностей
|
||||
|
||||
### AUTH-N001: Текущий AuthApi содержит client-oriented hook
|
||||
| Исходная часть | Назначение в Level 3 |
|
||||
|---|---|
|
||||
| `auth.factory.ts`, scenarios, validators, domain errors | `domains/auth/business` |
|
||||
| SDK, storage и state manager integration | Private adapters выбранного preset |
|
||||
| Повторяемый browser builder | `domains/auth/presets/application` |
|
||||
| Request-specific cookies, headers и client | `domains/auth/presets/request` |
|
||||
| React hooks, provider и domain UI | `domains/auth/react` |
|
||||
| Page text, redirect и screen outcome | Consumer composition |
|
||||
|
||||
`auth.factory.ts` импортирует `createAuthHook`, а `hooks/use-auth.hook.ts` содержит `'use client'`. Кроме того, `AuthDeps.session` описывает `useToken`.
|
||||
## Проверка границ
|
||||
|
||||
Текущий transitive graph:
|
||||
`authFactory` не импортирует `useAuth`, `'use client'`, SDK или storage. React hook строится поверх готового `AuthApi`, например через framework-neutral `getSnapshot` и `subscribe`.
|
||||
|
||||
```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:
|
||||
Нормализация номера телефона может быть public pure business function:
|
||||
|
||||
```ts
|
||||
import {
|
||||
@@ -81,93 +71,26 @@ import {
|
||||
} from '@/domains/auth/business'
|
||||
```
|
||||
|
||||
Business service и UI могут использовать одну семантику. Business service всё равно повторно валидирует вход независимо от UI-проверки.
|
||||
|
||||
Существующий `business/user` показывает другой workaround: pure validators возвращаются через собранный `userFactory` API. Прямой pure export позволит не требовать assembly для детерминированной функции.
|
||||
UI использует её для feedback, но `requestPhoneOtp` повторно валидирует значение внутри business scenario.
|
||||
|
||||
## 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.
|
||||
|
||||
Предварительное исправление границы:
|
||||
`AuthBusinessError` остаётся private implementation. Consumer получает только stable contract:
|
||||
|
||||
```ts
|
||||
// Public business API.
|
||||
export { AUTH_ERROR_CODES, isAuthError }
|
||||
export type { AuthError, AuthErrorCode }
|
||||
|
||||
// Business-private implementation.
|
||||
class AuthBusinessError extends Error {}
|
||||
const createAuthBusinessError = (...) => {}
|
||||
import {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from '@/domains/auth/business'
|
||||
```
|
||||
|
||||
Consumer получает безопасный observation contract, но не получает constructor и source mapping.
|
||||
Так React composition может выбрать сообщение или retry behavior по `code`, не зная SDK error, HTTP status или constructor private ошибки.
|
||||
|
||||
## Presets
|
||||
## Migration order
|
||||
|
||||
### AUTH-N004: Текущий createKnvAuthBusiness является preset
|
||||
|
||||
`createKnvAuthBusiness()` выбирает `knvAuthPhoneAdapter` и `appAuthSessionAdapter`, затем вызывает `authFactory`.
|
||||
|
||||
В новой терминологии это application preset, внутри которого могут оставаться KNV-specific adapters:
|
||||
|
||||
```text
|
||||
domains/auth/presets/application/create-application-auth.ts
|
||||
```
|
||||
|
||||
Он не является единственно допустимым assembly site. Tests, SSR request composition и другой product preset могут напрямую вызвать ту же `authFactory`.
|
||||
|
||||
## SSR-вариант
|
||||
|
||||
Одна factory позволяет получить request-scoped API без второй реализации business:
|
||||
|
||||
```ts
|
||||
import 'server-only'
|
||||
|
||||
export const createAuthForRequest = (input: AuthRequestInput) => {
|
||||
return authFactory({
|
||||
authPhone: createKnvServerAuthPhoneAdapter(input),
|
||||
session: createRequestAuthSessionAdapter(input),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Browser preset использует другую реализацию тех же ports. Factory, business types, pure functions и error contract остаются общими.
|
||||
|
||||
## Предварительная целевая структура
|
||||
|
||||
```text
|
||||
domains/auth/
|
||||
├── business/
|
||||
│ ├── auth.factory.ts
|
||||
│ ├── errors/
|
||||
│ ├── lib/
|
||||
│ ├── mappers/
|
||||
│ ├── services/
|
||||
│ ├── tests/
|
||||
│ ├── types/
|
||||
│ └── index.ts
|
||||
├── presets/
|
||||
│ └── application/
|
||||
│ ├── adapters/
|
||||
│ ├── create-application-auth.ts
|
||||
│ ├── create-application-auth.test.ts
|
||||
│ └── index.ts
|
||||
└── {framework-binding}/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Это только проверочная структура. Она не фиксирует обязательность всех папок и не должна использоваться как scaffold checklist.
|
||||
|
||||
Server-only/request preset может быть добавлен отдельным module при реальной потребности. Он не образует обязательную `server`-ветку Domain.
|
||||
|
||||
Tests не используют общий testing preset. Business tests выполняют per-test assembly напрямую через `authFactory`, а production presets тестируются рядом с собственной реализацией только на wiring, scope и lifecycle.
|
||||
1. Выделить `business` entrypoint и убедиться, что его transitive graph isomorphic.
|
||||
2. Перенести concrete runtime в adapters выбранного preset.
|
||||
3. Оформить повторяемую assembly как `presets/application`.
|
||||
4. Перенести hooks и Provider в `react`, передавая им готовый API.
|
||||
5. Сохранить page-specific UI и graph ownership в `compositions`.
|
||||
6. Добавить factory, adapter, preset и React boundary tests до удаления старого пути.
|
||||
|
||||
@@ -1,50 +1,35 @@
|
||||
# Business module внутри Domain
|
||||
|
||||
> Рабочая заметка. Не является нормативным разделом спецификации.
|
||||
> Пояснение semantic core Domain.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L3-BUSINESS-R003`](../../rules/level-3.md#slm-l3-business-r003)
|
||||
- [`SLM-L3-BUSINESS-A004`](../../rules/level-3.md#slm-l3-business-a004)
|
||||
- [`SLM-L3-PORT-R007`](../../rules/level-3.md#slm-l3-port-r007)
|
||||
- [`SLM-L1-MODULE-A004`](../../rules/level-1.md#slm-l1-module-a004)
|
||||
|
||||
## Роль
|
||||
|
||||
### BUS-N001: Business является семантическим ядром Domain
|
||||
`business` -- единственный обязательный module Domain. Он владеет:
|
||||
|
||||
Business-модуль владеет:
|
||||
|
||||
- публичными бизнес-сценариями;
|
||||
- public business scenarios и `DomainApi`;
|
||||
- factory, `Deps` и ports;
|
||||
- business-owned types и contracts;
|
||||
- business API;
|
||||
- factory и ports;
|
||||
- детерминированными доменными правилами;
|
||||
- доменным error contract;
|
||||
- преобразованием внешних результатов в доменные результаты.
|
||||
- детерминированными rules, validation и normalization;
|
||||
- domain error contract;
|
||||
- семантикой domain state, commands и selectors.
|
||||
|
||||
Business не владеет concrete runtime, environment wiring и framework integration.
|
||||
Business не владеет SDK, storage implementation, browser/Node API, framework integration, environment wiring или concrete state manager.
|
||||
|
||||
## Public API business-модуля
|
||||
## Public API
|
||||
|
||||
### 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:
|
||||
Business entrypoint открывает только contract, нужный consumers, presets и adapters:
|
||||
|
||||
```ts
|
||||
export { authFactory } from './auth.factory'
|
||||
|
||||
export {
|
||||
AUTH_ERROR_CODES,
|
||||
isAuthError,
|
||||
} from './errors/auth-error'
|
||||
|
||||
export {
|
||||
normalizeAuthPhone,
|
||||
validateAuthPhone,
|
||||
} from './lib/auth-phone'
|
||||
export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error'
|
||||
export { normalizeAuthPhone, validateAuthPhone } from './lib/auth-phone'
|
||||
|
||||
export type {
|
||||
AuthApi,
|
||||
@@ -52,123 +37,55 @@ export type {
|
||||
AuthError,
|
||||
AuthErrorCode,
|
||||
AuthFactory,
|
||||
AuthPhonePort,
|
||||
AuthSessionPort,
|
||||
AuthState,
|
||||
}
|
||||
} from './types'
|
||||
```
|
||||
|
||||
## Types
|
||||
Port types экспортируются, потому что preset и promoted adapter реализуют именно эти contracts. `services`, private mappers, error constructor, source mapper, persistence key и concrete state runtime остаются закрытыми.
|
||||
|
||||
### BUS-N003: Business contracts остаются внутри business
|
||||
## Types и pure functions
|
||||
|
||||
Отдельный `model` submodule пока не требуется. Типы размещаются по ownership:
|
||||
`types`, `errors`, `lib`, `ports`, `services` и `tests` -- segments business module, а не отдельные Domain APIs. Type размещается у владельца:
|
||||
|
||||
| Тип | Место |
|
||||
| Contract | Владелец |
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `AuthApi`, `AuthDeps`, `AuthState`, ports | `business` |
|
||||
| SDK DTO и transport error | Adapter или `infra` |
|
||||
| React provider props | `react` |
|
||||
| View model screen | Consumer composition |
|
||||
|
||||
`types/` является segment business-модуля, а не самостоятельным общим хранилищем Domain.
|
||||
Pure domain function может быть public, только если она выражает business rule и имеет реального external consumer. Она получает все данные аргументами, детерминирована, не использует `Deps`, state, clock, random, environment или framework runtime.
|
||||
|
||||
## 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-модуля.
|
||||
Consumer может применять `validateAuthPhone` для раннего UX feedback, но public business scenario повторяет validation на своей границе.
|
||||
|
||||
## Domain errors
|
||||
|
||||
### BUS-N006: Создание и наблюдение ошибки являются разными контрактами
|
||||
|
||||
Business создаёт domain error. Consumer только распознаёт ошибку и читает поля, от которых зависит его поведение.
|
||||
|
||||
Public observation contract:
|
||||
Каждый public runtime scenario выдаёт только domain failure contract. Source error, SDK class, HTTP status, response body и transport code не становятся consumer API.
|
||||
|
||||
```ts
|
||||
export const AUTH_ERROR_CODES = {
|
||||
PHONE_OTP_PHONE_INVALID: 'AUTH_PHONE_OTP_PHONE_INVALID',
|
||||
PHONE_OTP_REQUEST_FAILED: 'AUTH_PHONE_OTP_REQUEST_FAILED',
|
||||
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.
|
||||
// Runtime validation of the public observation shape.
|
||||
}
|
||||
```
|
||||
|
||||
Private creation contract:
|
||||
Если public API использует exceptions, entrypoint экспортирует domain-specific guard, codes и read-only observation shape, но не constructor или source error mapper. Если проект выбирает discriminated `Result`, тот же contract должен быть выражен в result branch. Один business API не смешивает оба способа для одинаковых scenario.
|
||||
|
||||
```ts
|
||||
class AuthBusinessError extends Error implements AuthError {
|
||||
// Constructor, cause и source diagnostics.
|
||||
}
|
||||
## Domain state
|
||||
|
||||
const createAuthBusinessError = (...) => {
|
||||
// Source error mapping.
|
||||
}
|
||||
```
|
||||
Business определяет форму `AuthState`, начальное состояние, допустимые transitions и public observation contract. Concrete store, persistence, subscription source и framework hook реализуются снаружи business через ports/adapters.
|
||||
|
||||
### BUS-N007: Error constructor не является consumer API
|
||||
|
||||
Consumer не должен создавать `AuthBusinessError`, выбирать source mapping или подделывать business failure. Поэтому наружу предполагается экспортировать:
|
||||
|
||||
- stable error code values;
|
||||
- error code type;
|
||||
- read-only observable error shape;
|
||||
- runtime guard или parser.
|
||||
|
||||
Наружу не предполагается экспортировать:
|
||||
|
||||
- error constructor;
|
||||
- error factory;
|
||||
- source error mapper;
|
||||
- transport-specific error data;
|
||||
- internal fallback selection.
|
||||
|
||||
### BUS-N008: Одних типов недостаточно при throw-based API
|
||||
|
||||
TypeScript не описывает checked exceptions. Для сигнатуры
|
||||
|
||||
```ts
|
||||
(data: VerifyPhoneOtpData) => Promise<void>
|
||||
```
|
||||
|
||||
значение в `catch` всё равно имеет тип `unknown`. Если consumer различает ошибки по `code`, business должен предоставить runtime discriminator либо перейти на typed `Result`.
|
||||
|
||||
Выбор между throw + guard и typed `Result` пока не закрыт окончательно. Текущий минимальный путь совместимости: throw + public observation contract.
|
||||
Framework-neutral observation может иметь форму `getSnapshot` и `subscribe`. Это protocol business API, а не React hook или `StoreApi` конкретной библиотеки.
|
||||
|
||||
@@ -1,206 +1,63 @@
|
||||
# Domain
|
||||
# Граница Domain
|
||||
|
||||
> Рабочая заметка. Не является нормативным разделом спецификации.
|
||||
> Пояснение предметной и структурной границы Level 3.
|
||||
|
||||
## Определение
|
||||
## Связанные правила
|
||||
|
||||
### DOM-N003: Domain является границей владения предметной областью
|
||||
- [`SLM-L3-DOMAIN-R001`](../../rules/level-3.md#slm-l3-domain-r001)
|
||||
- [`SLM-L3-DOMAIN-A002`](../../rules/level-3.md#slm-l3-domain-a002)
|
||||
- [`SLM-L3-BUSINESS-R003`](../../rules/level-3.md#slm-l3-business-r003)
|
||||
- [`SLM-L1-MODULE-R011`](../../rules/level-1.md#slm-l1-module-r011)
|
||||
- [`SLM-L1-GROUP-R007`](../../rules/level-1.md#slm-l1-group-r007)
|
||||
|
||||
Domain группирует business-контракт, concrete integrations, готовые presets и framework-specific bindings одной предметной области.
|
||||
## Предметная граница
|
||||
|
||||
Примеры Domain:
|
||||
Domain представляет одну связную предметную область: `auth`, `catalog`, `orders` или `checkout`. Он собирает её business contract, concrete integrations, повторяемые assemblies и framework bindings, но не становится большим module со смешанными ролями.
|
||||
|
||||
- `auth`;
|
||||
- `user`;
|
||||
- `catalog`;
|
||||
- `orders`;
|
||||
- `checkout`.
|
||||
Domain является предметной границей, а не владельцем runtime-кода в смысле Level 1. Каждый scenario, adapter, preset и framework binding остаётся ответственностью конкретного module. Такое разделение позволяет одной области иметь несколько public module APIs без нарушения правила о единственном владельце ответственности.
|
||||
|
||||
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 |
|
||||
| `domains/auth/business/ports` | Business capabilities | Segment |
|
||||
| `domains/auth/presets` | Навигация assemblies | Group |
|
||||
| `domains/auth/presets/application` | Application preset | Module |
|
||||
| `domains/auth/presets/application/adapters` | Private integrations preset | Segment |
|
||||
| `domains/auth/adapters` | Навигация promoted adapters | Group |
|
||||
| `domains/auth/adapters/identity-provider` | Reusable adapter | Module |
|
||||
| `domains/auth/react` | React binding | Module |
|
||||
|
||||
## Публичные границы
|
||||
Role отвечает на вопрос, что делает код. Structural kind отвечает на вопрос, какую архитектурную границу он образует. Имя папки само по себе не доказывает ни роль, ни structural kind.
|
||||
|
||||
### DOM-N005: Domain предоставляет отдельные public submodules
|
||||
## Корень Domain
|
||||
|
||||
Предварительно Domain не имеет обязательного общего facade. Каждый public module предоставляет собственный entrypoint:
|
||||
Корень Domain не содержит реализацию, state, lifecycle resources, `index.ts` или общий barrel. Его прямыми детьми могут быть `business`, Groups `presets` и `adapters`, а также framework modules с именем framework, например `react`.
|
||||
|
||||
```ts
|
||||
import { authFactory, validateAuthPhone } from '@/domains/auth/business'
|
||||
import { createApplicationAuth } from '@/domains/auth/presets/application'
|
||||
import { AuthProvider, useAuth } from '@/domains/auth/react'
|
||||
```
|
||||
Не создаются автоматически корневые ветки `model`, `types`, `errors`, `lib`, `ui`, `client`, `server` или `tests`. Такая ветка должна либо быть segment module-владельца, либо иметь самостоятельную module responsibility, выраженную одной из ролей Domain.
|
||||
|
||||
`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
|
||||
@/domains/auth/presets/application
|
||||
@/domains/auth/react
|
||||
```
|
||||
|
||||
Private adapters внутри preset не получают собственного внешнего entrypoint.
|
||||
Эти пути являются public API role modules. Root path `@/domains/auth` не существует как runtime boundary. Он не должен объединять isomorphic business, client React и server-only preset через `export *`.
|
||||
|
||||
### 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 |
|
||||
| Business scenarios, contracts, state semantics и errors | `domains/auth/business` |
|
||||
| Concrete adapter одной assembly | Segment соответствующего preset |
|
||||
| Reusable auth integration | Promoted adapter module |
|
||||
| Повторяемая assembly `AuthApi` | Preset module |
|
||||
| React provider, hook и domain-specific React UI | `domains/auth/react` |
|
||||
| Page, route, redirect, screen и конкретный visual outcome | Module `compositions` |
|
||||
| SDK wrapper или технический сервис без Auth semantics | Module `infra` |
|
||||
|
||||
Граница domain-specific UI пока остаётся открытым вопросом.
|
||||
Framework dependency сама по себе не делает UI частью Domain. Component принадлежит `react` только когда он работает с domain contract и не определяет page, route или product composition.
|
||||
|
||||
@@ -1,114 +1,52 @@
|
||||
# Factory, ports и adapters
|
||||
|
||||
> Рабочая заметка. Не является нормативным разделом спецификации.
|
||||
> Пояснение runtime boundary business module.
|
||||
|
||||
## Терминология
|
||||
## Связанные правила
|
||||
|
||||
### FAC-N003: Собирается API instance, а не factory
|
||||
- [`SLM-L3-FACTORY-R005`](../../rules/level-3.md#slm-l3-factory-r005)
|
||||
- [`SLM-L3-FACTORY-R006`](../../rules/level-3.md#slm-l3-factory-r006)
|
||||
- [`SLM-L3-PORT-R007`](../../rules/level-3.md#slm-l3-port-r007)
|
||||
- [`SLM-L3-ADAPTER-R008`](../../rules/level-3.md#slm-l3-adapter-r008)
|
||||
- [`SLM-L3-BUSINESS-A004`](../../rules/level-3.md#slm-l3-business-a004)
|
||||
|
||||
## Factory и API instance
|
||||
|
||||
```text
|
||||
Factory + Deps implementations → business API instance
|
||||
Factory + implementations ports -> 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.
|
||||
Factory принадлежит `business`, получает полный `AuthDeps` и возвращает `AuthApi`:
|
||||
|
||||
```ts
|
||||
export type AuthFactory = (deps: AuthDeps) => AuthApi
|
||||
```
|
||||
|
||||
### FAC-N005: Factory имеет стабильную форму результата
|
||||
Все presets одной factory предоставляют полный набор ports и получают одинаковый business API. Browser, request и server action не создают разные factory только из-за среды. Preset может открыть consumer суженный view API, но не меняет contract самой factory.
|
||||
|
||||
Все presets одной factory создают один и тот же business API contract. Среда не выбирается через аргумент `mode`, а форма API не зависит от наличия optional dependency.
|
||||
Factory construction создаёт только deterministic services и closures. Она не делает request, не читает cookies/storage/env, не запускает subscription/timer, не обращается к platform API, не выбирает adapter и не запускает framework lifecycle.
|
||||
|
||||
Не рекомендуется:
|
||||
## Isomorphic import graph
|
||||
|
||||
```ts
|
||||
authFactory({
|
||||
mode: 'server',
|
||||
serverAdminClient: optionalClient,
|
||||
})
|
||||
```
|
||||
Проверяется весь production graph, достижимый из `business` entrypoint, а не только файл factory. Он не должен достигать:
|
||||
|
||||
Не рекомендуется возвращать методы, которые существуют в общем API, но намеренно падают в одной из сред.
|
||||
- React, Vue, Next.js и framework markers;
|
||||
- browser-only, Node-only, `client-only` или `server-only` boundary;
|
||||
- SDK, generated client, storage implementation или concrete state/query runtime;
|
||||
- adapters, presets, framework modules и environment configuration.
|
||||
|
||||
### 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 не используется как доказательство изоляции.
|
||||
Tree shaking не является доказательством изоляции. Type-only import concrete runtime создаёт ту же архитектурную зависимость и также запрещён.
|
||||
|
||||
## Ports
|
||||
|
||||
### PORT-N001: Port принадлежит business
|
||||
|
||||
Port описывает capability на языке business, а не форму concrete implementation.
|
||||
Port принадлежит business и описывает capability на business language:
|
||||
|
||||
```ts
|
||||
export type AuthPhonePort = {
|
||||
requestCode: (phone: string) => Promise<unknown>
|
||||
verifyCode: (data: VerifyPhoneOtpData) => Promise<unknown>
|
||||
}
|
||||
```
|
||||
|
||||
Port не должен раскрывать SDK client, generated operation, `Request`, `Window`, React hook, Zustand `StoreApi` и другие environment/framework types.
|
||||
|
||||
### PORT-N002: Ports абстрагируют implementation, но не доступность capability
|
||||
|
||||
Одна factory возможна, пока каждый preset способен реализовать одинаковые ports.
|
||||
|
||||
Server capability может остаться общим port, если browser adapter реализует её через безопасный HTTP/RPC boundary. Если capability принципиально невозможно реализовать в одной из поддерживаемых сред, её нельзя маскировать optional dependency общего API.
|
||||
|
||||
### PORT-N003: Reactive port должен быть framework-neutral
|
||||
|
||||
Client hook в `Deps` делает контракт client-oriented. Вместо `useToken` базовый port может описывать framework-neutral observation protocol:
|
||||
|
||||
```ts
|
||||
export type AuthSessionPort = {
|
||||
getSnapshot: () => AuthState
|
||||
subscribe: (listener: () => void) => () => void
|
||||
@@ -116,85 +54,35 @@ export type AuthSessionPort = {
|
||||
}
|
||||
```
|
||||
|
||||
React binding может построить `useAuth` поверх `getSnapshot` и `subscribe`. Vue binding использует тот же port через собственный lifecycle.
|
||||
Port не принимает SDK client, generated operation, `Request`, `Window`, React hook, `StoreApi` или environment-specific type. Он абстрагирует implementation, а не доступность capability: optional port и method, который намеренно падает в одной среде, нарушают factory contract.
|
||||
|
||||
Точная форма reactive ports требует отдельной проверки на реальном state manager.
|
||||
`unknown` допустим только на границе непроверенного external result. Business обязан валидировать его до превращения в domain result, state или error. Если adapter уже может представить устойчивый business-owned result, port описывает именно этот result, а не concrete DTO.
|
||||
|
||||
## Adapters
|
||||
|
||||
### ADP-N001: Adapter реализует business port
|
||||
|
||||
Adapter знает одновременно business contract и concrete runtime:
|
||||
Adapter соединяет business port и concrete runtime:
|
||||
|
||||
```text
|
||||
business port ← adapter → SDK / storage / browser / request
|
||||
business port <- adapter -> SDK / storage / platform / request input
|
||||
```
|
||||
|
||||
Adapter может:
|
||||
Adapter может преобразовать domain argument в transport argument, вызвать concrete source, нормализовать техническую форму к port contract и вернуть source failure. Он не определяет domain error code, business fallback, invariant или public method `AuthApi`.
|
||||
|
||||
- преобразовать 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:
|
||||
Default location -- private segment минимального preset owner:
|
||||
|
||||
```text
|
||||
domains/auth/presets/{preset-name}/
|
||||
domains/auth/presets/application/
|
||||
├── adapters/
|
||||
├── create-auth.ts
|
||||
│ └── auth-phone.adapter.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Возможный promotion переиспользуемого adapter:
|
||||
Если один adapter имеет несколько assembly consumers или самостоятельную integration responsibility, он становится promoted module:
|
||||
|
||||
```text
|
||||
domains/auth/adapters/ # optional logical group
|
||||
└── {adapter-name}/ # adapter module
|
||||
domains/auth/adapters/
|
||||
└── identity-provider/
|
||||
└── 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.
|
||||
Promoted adapter сохраняет минимальный public API. Его появление не делает concrete SDK частью public business contract.
|
||||
|
||||
@@ -1,109 +1,71 @@
|
||||
# Framework bindings внутри Domain
|
||||
# React module внутри Domain
|
||||
|
||||
> Рабочая заметка. Не является нормативным разделом спецификации.
|
||||
> Пояснение framework boundary Domain на примере React.
|
||||
|
||||
## Определение
|
||||
## Связанные правила
|
||||
|
||||
### FW-N001: Framework code определяется зависимостью от framework
|
||||
- [`SLM-L3-FRAMEWORK-R013`](../../rules/level-3.md#slm-l3-framework-r013)
|
||||
- [`SLM-L3-BUSINESS-A004`](../../rules/level-3.md#slm-l3-business-a004)
|
||||
- [`SLM-L3-ASSEMBLY-R010`](../../rules/level-3.md#slm-l3-assembly-r010)
|
||||
|
||||
К framework code относится код, существующий из-за React, Vue, Next.js или другого framework/runtime contract:
|
||||
## Имя и место module
|
||||
|
||||
- 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.
|
||||
|
||||
## Возможная структура
|
||||
Framework-specific module находится непосредственно в Domain и называется именем framework:
|
||||
|
||||
```text
|
||||
domains/auth/{framework-binding}/
|
||||
├── providers/
|
||||
├── hooks/
|
||||
domains/auth/react/
|
||||
├── components/
|
||||
├── hooks/
|
||||
├── providers/
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`{framework-binding}` является placeholder. SLM пока не выбирает между `react`, `bindings/react`, `framework/react` и другим локальным соглашением. Чёткая граница определяется самостоятельным module и отдельным public entrypoint, а не обязательным именем родительской папки.
|
||||
`react` точно обозначает framework и не создаёт пустую промежуточную Group вроде `framework/react` или `bindings/react`. Если Domain действительно поддерживает другой framework, он получает отдельный sibling module, например `vue`.
|
||||
|
||||
Если одна папка предоставляет cohesive framework API, она является module. Если папка только классифицирует несколько самостоятельных binding modules, она является logical group и не имеет собственного `index.ts`.
|
||||
## Роль React module
|
||||
|
||||
## Reactive state
|
||||
React module может:
|
||||
|
||||
### FW-N003: Framework hook строится снаружи business
|
||||
- передать готовый `AuthApi` через context/provider;
|
||||
- создать hook доступа к API или framework-neutral state;
|
||||
- связать React lifecycle с subscription API;
|
||||
- реализовать domain-specific React component.
|
||||
|
||||
Если business предоставляет framework-neutral `getSnapshot` и `subscribe`, React binding может использовать `useSyncExternalStore`:
|
||||
Он не меняет business rules, не создаёт domain errors, не выбирает concrete adapters и не вызывает factory или preset. Сборка остаётся у composition graph owner; React module получает уже готовый instance.
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
```tsx
|
||||
type AuthProviderProps = PropsWithChildren<{
|
||||
api: AuthApi
|
||||
}>
|
||||
|
||||
export const createUseAuth = (authApi: AuthApi) => {
|
||||
return () => {
|
||||
return useSyncExternalStore(
|
||||
authApi.subscribeAuthState,
|
||||
authApi.getAuthState,
|
||||
authApi.getAuthState,
|
||||
)
|
||||
}
|
||||
export const AuthProvider = ({ api, children }: AuthProviderProps) => {
|
||||
return <AuthContext.Provider value={api}>{children}</AuthContext.Provider>
|
||||
}
|
||||
```
|
||||
|
||||
Это только иллюстрация направления. Финальная форма state port должна учитывать реальный state/query runtime.
|
||||
## Reactive state
|
||||
|
||||
Business при таком подходе не импортирует React и не возвращает React hook как единственный способ чтения состояния.
|
||||
Если `AuthApi` предоставляет framework-neutral protocol `getSnapshot` и `subscribe`, React module может использовать `useSyncExternalStore`:
|
||||
|
||||
## Framework module как assembly site
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
### FW-N004: Provider может владеть API instance
|
||||
export const useAuthState = () => {
|
||||
const api = useAuth()
|
||||
|
||||
Provider вправе вызвать preset или factory, если provider является явным владельцем scope и lifecycle:
|
||||
|
||||
```text
|
||||
AuthProvider
|
||||
→ createBrowserAuth preset
|
||||
→ AuthApi instance
|
||||
→ context
|
||||
→ access hooks
|
||||
return useSyncExternalStore(
|
||||
api.subscribe,
|
||||
api.getSnapshot,
|
||||
api.getSnapshot,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Provider construction не должен запускать I/O или subscription до framework commit/effect. Cleanup выполняется владельцем lifecycle.
|
||||
Business не импортирует React и не возвращает React hook как единственный способ наблюдать state. React module не создаёт subscription до commit и возвращает cleanup через protocol `useSyncExternalStore`.
|
||||
|
||||
Framework module не обязан собирать API. Он также может получить готовый instance от route/page/application graph owner.
|
||||
## Domain UI и compositions
|
||||
|
||||
## Framework-neutral и environment-neutral
|
||||
Component принадлежит `react`, если его responsibility ограничена domain contract: он работает с `AuthApi`, domain state и stable domain errors. Он не владеет page, route, redirect, product copy или composition нескольких domains.
|
||||
|
||||
Эти свойства различаются:
|
||||
|
||||
| Свойство | Запрещённая зависимость |
|
||||
|---|---|
|
||||
| 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 пока требует отдельных примеров.
|
||||
Screen, route outcome, локальный текст ошибки, redirect и page-specific UI остаются в `compositions`. Dependency от React сама по себе не доказывает принадлежность Domain.
|
||||
|
||||
@@ -1,110 +1,24 @@
|
||||
# Открытые вопросы Domains
|
||||
# Открытые вопросы Level 3
|
||||
|
||||
> Эти вопросы намеренно не сформулированы как правила.
|
||||
> Эти вопросы не являются правилами и не отменяют уже принятые границы.
|
||||
|
||||
## Ошибки
|
||||
## Зафиксированные решения
|
||||
|
||||
### OPEN-N001: Throw или typed Result
|
||||
- Domain является сущностью только Level 3; в Level 2 предметная область остаётся одним domain module.
|
||||
- Domain root не имеет общего runtime barrel.
|
||||
- Framework module называется именем framework и размещается непосредственно в Domain: `domains/auth/react`.
|
||||
- Framework module получает готовый API и не выполняет assembly.
|
||||
- Runtime-взаимодействие business разных Domains проходит через consumer-owned port и graph owner.
|
||||
- Navigation Groups допустимы в `domains`, но не являются частью базовых примеров.
|
||||
|
||||
Нужно решить, остаются ли ожидаемые domain failures исключениями с public runtime guard или business API возвращает discriminated `Result<T, DomainError>`.
|
||||
## Failure transport
|
||||
|
||||
Текущий совместимый вариант: throw + `isDomainError`. Typed Result потребует изменения формы всех scenario methods.
|
||||
Level 3 требует stable domain failure contract, но не навязывает проекту единый transport: exception с domain-specific runtime guard или discriminated `Result`. Нужно проверить, нужна ли общая политика для всех Domain одного приложения и как она влияет на server actions/RPC serialization.
|
||||
|
||||
### OPEN-N002: Универсальный или domain-specific error guard
|
||||
## Reactive state protocol
|
||||
|
||||
Нужно определить, достаточно ли общего `isDomainError`, либо каждый business-модуль экспортирует `isAuthError`, `isUserError` и собственную проверку code set.
|
||||
Нужно проверить на реальном SSR/hydration кейсе точную форму framework-neutral observation protocol: initial snapshot, concurrent rendering, invalidation, subscription cleanup и поведение после request boundary. `getSnapshot` и `subscribe` пока являются базовой иллюстрацией, а не обязательной файловой формой.
|
||||
|
||||
## Domain structure
|
||||
## Architecture lint
|
||||
|
||||
### 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.
|
||||
Нужно выбрать формат project configuration для автоматической проверки Domain roots, role modules, public entrypoints, environment labels и запрещённых transitive imports. Проверка должна опираться на graph и metadata, а не только на имена папок.
|
||||
|
||||
@@ -1,141 +1,89 @@
|
||||
# Presets и SSR
|
||||
|
||||
> Рабочая заметка. Не является нормативным разделом спецификации.
|
||||
> Пояснение повторяемой assembly Domain.
|
||||
|
||||
## Определение
|
||||
## Связанные правила
|
||||
|
||||
### PRE-N003: Preset является готовым вариантом assembly
|
||||
- [`SLM-L3-PRESET-R009`](../../rules/level-3.md#slm-l3-preset-r009)
|
||||
- [`SLM-L3-ASSEMBLY-R010`](../../rules/level-3.md#slm-l3-assembly-r010)
|
||||
- [`SLM-L3-ENVIRONMENT-A012`](../../rules/level-3.md#slm-l3-environment-a012)
|
||||
- [`SLM-L3-FACTORY-R006`](../../rules/level-3.md#slm-l3-factory-r006)
|
||||
|
||||
Preset выбирает implementations ports и создаёт API одной business factory для конкретного execution context.
|
||||
## Роль preset
|
||||
|
||||
Preset -- именованная повторяемая assembly одной business factory для конкретного execution context. Он выбирает concrete implementations ports, создаёт `AuthApi` и передаёт caller lifecycle operations, определённые module-владельцами resources.
|
||||
|
||||
```text
|
||||
authFactory
|
||||
├── presets/application -> browser-compatible AuthApi
|
||||
├── presets/request -> request-scoped AuthApi
|
||||
└── presets/server-action -> server action AuthApi
|
||||
```
|
||||
|
||||
Среда определяется preset и adapters, а не `mode` внутри factory. Tests создают per-test assembly напрямую через factory и не требуют общего `presets/testing`.
|
||||
|
||||
## Структура и public API
|
||||
|
||||
```text
|
||||
domains/auth/presets/application/
|
||||
├── adapters/
|
||||
├── create-application-auth.ts
|
||||
├── create-application-auth.test.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`application` -- пример имени. Preset называется по execution scope или устойчивому назначению: `application`, `request`, `server-action`. Он не называется по temporary consumer, если configuration не предназначена для повторного использования.
|
||||
|
||||
```ts
|
||||
export const createKnvAuthBusiness = (): AuthApi => {
|
||||
export const createApplicationAuth = (): AuthApi => {
|
||||
return authFactory({
|
||||
authPhone: knvAuthPhoneAdapter,
|
||||
session: appAuthSessionAdapter,
|
||||
phone: createApplicationAuthPhoneAdapter(),
|
||||
session: createApplicationAuthSessionAdapter(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`createKnvAuthBusiness` является preset builder, а не второй factory и не единственно допустимое место сборки.
|
||||
Preset не добавляет scenario, не меняет error mapping и не скрывает business rule. Он также не становится монополией на factory: явный composition graph owner может собрать одноразовый graph, если он принимает на себя все обязанности assembly.
|
||||
|
||||
## Несколько presets одной factory
|
||||
## Scope и lifecycle
|
||||
|
||||
```text
|
||||
authFactory
|
||||
├── createBrowserAuth
|
||||
├── createAuthForRequest
|
||||
├── createAuthForServerAction
|
||||
└── другие production presets
|
||||
Preset объявляет ожидаемый scope API instance. Application preset используется в application scope; request preset создаёт новый instance для каждого request. Graph owner удерживает instance только в этом scope и не хранит request data в application singleton.
|
||||
|
||||
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:
|
||||
Если assembly создаёт lifecycle resource, caller получает явный cleanup handle:
|
||||
|
||||
```ts
|
||||
export type AuthSsrApi = Pick<AuthApi, 'resolveSession'>
|
||||
export type AuthRequestAssembly = {
|
||||
api: AuthApi
|
||||
dispose: () => void | Promise<void>
|
||||
}
|
||||
|
||||
export const createAuthForRequest = (
|
||||
input: AuthRequestInput,
|
||||
): AuthSsrApi => {
|
||||
const authApi = authFactory(createRequestAuthDeps(input))
|
||||
): AuthRequestAssembly => {
|
||||
const session = createRequestSessionAdapter(input)
|
||||
|
||||
return {
|
||||
resolveSession: authApi.resolveSession,
|
||||
api: authFactory({
|
||||
phone: createRequestAuthPhoneAdapter(input),
|
||||
session,
|
||||
}),
|
||||
dispose: session.dispose,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Это ограничивает 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.
|
||||
Factory и preset construction не запускают I/O или subscriptions. Если domain lifecycle должен начать resource, module-владелец выражает это отдельной API operation; graph owner вызывает её после начала scope и выполняет предоставленный cleanup при его завершении.
|
||||
|
||||
## Server-only boundary
|
||||
|
||||
### PRE-N008: Environment-specific preset может иметь отдельный public entrypoint
|
||||
|
||||
Если preset должен быть недостижим из client graph, проект может выделить для него отдельный entrypoint и использовать framework/build marker. Имя и физическая группировка preset не задаются SLM.
|
||||
Server preset имеет отдельный entrypoint и marker выбранного framework/build system:
|
||||
|
||||
```ts
|
||||
// Один из возможных server-only preset entrypoints.
|
||||
import 'server-only'
|
||||
|
||||
export { createAuthForRequest } from './create-auth-for-request'
|
||||
```
|
||||
|
||||
Этот entrypoint не реэкспортируется через:
|
||||
Этот entrypoint не реэкспортируется через `business`, `react` или client-compatible preset. Server adapter может иметь собственный marker для защиты от ошибочного прямого import.
|
||||
|
||||
- `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.
|
||||
Framework module не вызывает preset и не создаёт factory. Он получает готовый `AuthApi` от graph owner, поэтому React lifecycle не смешивается с concrete assembly.
|
||||
|
||||
@@ -1,402 +1,70 @@
|
||||
# Тестирование Domain
|
||||
|
||||
> Рабочая заметка. Не является нормативным разделом спецификации.
|
||||
> Verification границ и behavior Level 3.
|
||||
|
||||
## Главный принцип
|
||||
## Связанное правило
|
||||
|
||||
### TST-N001: Тест размещается у владельца проверяемой ответственности
|
||||
- [`SLM-L3-TEST-R014`](../../rules/level-3.md#slm-l3-test-r014)
|
||||
- [`SLM-L3-FACTORY-R006`](../../rules/level-3.md#slm-l3-factory-r006)
|
||||
- [`SLM-L3-ASSEMBLY-R010`](../../rules/level-3.md#slm-l3-assembly-r010)
|
||||
|
||||
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
|
||||
```
|
||||
Тест живёт у module-владельца проверяемой ответственности. У Domain нет общей корневой папки `tests/`.
|
||||
|
||||
## Матрица покрытия
|
||||
| Проверяемая граница | Владелец теста |
|
||||
|---|---|
|
||||
| Business scenarios, state и domain errors | `business` |
|
||||
| Pure rule, mapper, parser или guard | Colocated segment `business` |
|
||||
| Concrete port implementation | Adapter |
|
||||
| Wiring, scope и cleanup assembly | Preset |
|
||||
| Provider, hook и React lifecycle | `react` |
|
||||
| Cross-domain graph | Composition graph owner |
|
||||
| Полный пользовательский поток | E2E entry приложения |
|
||||
|
||||
| Граница | Предварительная обязательность | Что проверяется |
|
||||
|---|---|---|
|
||||
| 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 | По продуктовой потребности | Полный пользовательский поток |
|
||||
## Factory-level tests
|
||||
|
||||
## Предварительная структура
|
||||
|
||||
```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-модуля:
|
||||
Factory-level tests являются главным доказательством public business behavior. Они импортируют только public API `business` и передают controlled ports:
|
||||
|
||||
```ts
|
||||
import {
|
||||
authFactory,
|
||||
AUTH_ERROR_CODES,
|
||||
authFactory,
|
||||
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 })
|
||||
const requestCode = vi.fn().mockRejectedValue(new Error('Network failed'))
|
||||
const api = authFactory(createAuthTestDeps({ requestCode }))
|
||||
|
||||
await expect(api.requestPhoneOtp(phone)).rejects.toMatchObject({
|
||||
await expect(api.requestPhoneOtp('+79991112233')).rejects.toMatchObject({
|
||||
code: AUTH_ERROR_CODES.PHONE_OTP_REQUEST_FAILED,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Другой test case создаёт независимую assembly:
|
||||
Factory-level suite проверяет форму public API, отсутствие side effects при construction, happy path, input validation, malformed port result, rejected promise, synchronous throw, domain error code, порядок effects, state transitions и значимые concurrent calls.
|
||||
|
||||
Business test не использует React, production SDK, storage или production preset. Если scenario нельзя проверить без них, runtime boundary проникла внутрь business.
|
||||
|
||||
## Test harness
|
||||
|
||||
Private test harness уменьшает boilerplate, но не является preset:
|
||||
|
||||
```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()
|
||||
})
|
||||
const { api, ports, state } = createAuthTestHarness({ requestCode })
|
||||
```
|
||||
|
||||
### TST-N004: Test harness не является preset
|
||||
Harness создаёт новый instance на каждый test case, допускает scenario-specific overrides и не экспортируется через production entrypoint. `presets/testing` не создаётся по умолчанию.
|
||||
|
||||
Test harness является private test utility, которая уменьшает boilerplate и предоставляет observability:
|
||||
## Тесты остальных ролей
|
||||
|
||||
```ts
|
||||
const { api, ports, state } = createAuthTestHarness(overrides)
|
||||
```
|
||||
Adapter test проверяет concrete operation, transport payload, mapping аргументов, raw result/error согласно port contract и subscription cleanup. Он не повторяет domain error mapping или scenario matrix.
|
||||
|
||||
Test harness:
|
||||
Preset test проверяет полный набор ports, выбор adapters, отсутствие I/O при construction, scope instance, передачу cleanup handle и server/client import boundary. Он не повторяет happy path business.
|
||||
|
||||
- private для конкретной test suite;
|
||||
- не экспортируется production entrypoint;
|
||||
- допускает произвольные scenario-specific overrides;
|
||||
- создаёт новый API instance для каждого test case;
|
||||
- не представляет устойчивую application environment;
|
||||
- не имеет собственного production lifecycle;
|
||||
- не размещается в `presets/`.
|
||||
React test получает fake `AuthApi` и проверяет Provider, access hook, update по `subscribe`, cleanup после unmount и поведение в Strict Mode. Smoke test с real factory добавляется только при отдельном integration risk.
|
||||
|
||||
Общий `test preset` по умолчанию не создаётся. Если Storybook, demo application или e2e environment получают устойчивую именованную конфигурацию, это отдельный application preset, а не универсальная конфигурация unit tests.
|
||||
## Минимальный набор
|
||||
|
||||
Предварительное имя helper:
|
||||
|
||||
```text
|
||||
business/tests/factory/testing/create-auth-test-harness.ts
|
||||
```
|
||||
|
||||
## Public API tests
|
||||
|
||||
### TST-N005: Business public API проверяется отдельно
|
||||
|
||||
Runtime public exports фиксируются тестом entrypoint:
|
||||
|
||||
```ts
|
||||
import * as authBusiness from '.'
|
||||
|
||||
expect(Object.keys(authBusiness).sort()).toEqual([
|
||||
'AUTH_ERROR_CODES',
|
||||
'authFactory',
|
||||
'isAuthError',
|
||||
'normalizeAuthPhone',
|
||||
'validateAuthPhone',
|
||||
])
|
||||
```
|
||||
|
||||
Этот тест обнаруживает случайный runtime export, но не видит type-only exports. Полная проверка type surface должна выполняться будущим architecture lint или TypeScript API check.
|
||||
|
||||
Форма API instance также фиксируется factory-level test:
|
||||
|
||||
```ts
|
||||
expect(Object.keys(authFactory(ports)).sort()).toEqual([
|
||||
'requestPhoneOtp',
|
||||
'resendPhoneOtp',
|
||||
'signOut',
|
||||
'verifyPhoneOtp',
|
||||
])
|
||||
```
|
||||
|
||||
## Pure domain functions
|
||||
|
||||
### TST-N006: Pure functions тестируются рядом с реализацией
|
||||
|
||||
```text
|
||||
business/lib/auth-phone.ts
|
||||
business/lib/auth-phone.test.ts
|
||||
```
|
||||
|
||||
Проверяются:
|
||||
|
||||
- canonical values;
|
||||
- boundary values;
|
||||
- malformed input;
|
||||
- normalization;
|
||||
- invariants;
|
||||
- отсутствие mutation входа;
|
||||
- детерминированность результата.
|
||||
|
||||
```ts
|
||||
describe('normalizeAuthPhone', () => {
|
||||
it.each([
|
||||
['8 (999) 111-22-33', '+79991112233'],
|
||||
['+7 999 111 22 33', '+79991112233'],
|
||||
['123', null],
|
||||
])('normalizes %s', (input, expected) => {
|
||||
expect(normalizeAuthPhone(input)).toBe(expected)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Business scenario повторно применяет то же правило на своей границе. UI validation не заменяет business validation.
|
||||
|
||||
## Internal tests
|
||||
|
||||
### TST-N007: Colocated tests дополняют public contract tests
|
||||
|
||||
Colocated tests оправданы для:
|
||||
|
||||
- mappers и normalizers;
|
||||
- runtime guards и parsers;
|
||||
- private error implementation;
|
||||
- сложного branching;
|
||||
- race/concurrency algorithms;
|
||||
- reusable internal pure functions.
|
||||
|
||||
Отдельный test каждого service не требуется автоматически. Factory-level tests остаются главным доказательством, что внутренняя реализация подключена к public scenario правильно.
|
||||
|
||||
Service test добавляется, если он существенно упрощает проверку сложного внутреннего алгоритма и не дублирует целиком factory-level matrix.
|
||||
|
||||
## Domain errors
|
||||
|
||||
### TST-N008: Consumer contract ошибки тестируется без public constructor
|
||||
|
||||
Factory-level test проверяет observable contract:
|
||||
|
||||
```ts
|
||||
try {
|
||||
await api.verifyPhoneOtp(data)
|
||||
} catch (error) {
|
||||
expect(isAuthError(error)).toBe(true)
|
||||
|
||||
if (isAuthError(error)) {
|
||||
expect(error.code).toBe(
|
||||
AUTH_ERROR_CODES.PHONE_OTP_VERIFY_CODE_INVALID,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Consumer-level test не использует private `AuthBusinessError` constructor и не зависит от `instanceof` internal class.
|
||||
|
||||
Colocated test error implementation может отдельно проверить:
|
||||
|
||||
- private constructor;
|
||||
- `cause`;
|
||||
- source code mapping;
|
||||
- source metadata normalization;
|
||||
- защиту от malformed error values.
|
||||
|
||||
## Adapter tests
|
||||
|
||||
### TST-N009: Adapter test проверяет port boundary, а не business behavior
|
||||
|
||||
Adapter test размещается рядом с adapter и проверяет:
|
||||
|
||||
- правильную concrete operation;
|
||||
- transport payload;
|
||||
- преобразование domain arguments в concrete arguments;
|
||||
- raw/unknown result согласно port contract;
|
||||
- проброс source error без создания domain error;
|
||||
- subscription cleanup;
|
||||
- отсутствие лишних SDK operations в минимальном client;
|
||||
- environment boundary, если она проверяема build/lint средствами.
|
||||
|
||||
Adapter test не повторяет domain error mapping, business fallback и scenario orchestration.
|
||||
|
||||
Если несколько adapters реализуют один нетривиальный behavioral port contract, позднее можно выделить reusable contract test suite. Она остаётся test-only utility и не становится preset.
|
||||
|
||||
## Preset tests
|
||||
|
||||
### TST-N010: Production preset test проверяет assembly risk
|
||||
|
||||
Preset test размещается рядом с production preset и проверяет:
|
||||
|
||||
- выбор правильных adapters;
|
||||
- передачу полного `Deps` в factory;
|
||||
- exact narrowed API view, если preset его задаёт;
|
||||
- отсутствие I/O при construction;
|
||||
- отсутствие import-time subscriptions и storage reads;
|
||||
- scope API instance;
|
||||
- передачу lifecycle/dispose handles caller;
|
||||
- изоляцию двух request-scoped instances;
|
||||
- server/client import boundary.
|
||||
|
||||
Preset test не повторяет happy path и error matrix business scenarios. Эти гарантии принадлежат factory-level tests.
|
||||
|
||||
## Framework binding tests
|
||||
|
||||
### TST-N011: Framework binding тестируется через fake business API
|
||||
|
||||
Framework unit test по умолчанию получает fake API, а не собирает реальную factory:
|
||||
|
||||
```tsx
|
||||
const authApi = createAuthApiFake()
|
||||
|
||||
render(
|
||||
<AuthProvider api={authApi}>
|
||||
<Consumer />
|
||||
</AuthProvider>,
|
||||
)
|
||||
```
|
||||
|
||||
Проверяются:
|
||||
|
||||
- Provider предоставляет переданный instance;
|
||||
- access hook возвращает правильный API;
|
||||
- использование без Provider даёт предсказуемую ошибку;
|
||||
- изменение framework-neutral state вызывает framework update;
|
||||
- subscriptions запускаются в правильной lifecycle phase;
|
||||
- cleanup выполняется после unmount;
|
||||
- Strict Mode не запускает construction side effects;
|
||||
- server snapshot и hydration согласованы, если binding участвует в SSR.
|
||||
|
||||
Отдельный smoke test с real factory и memory ports добавляется только при самостоятельном integration risk. Такой тест принадлежит framework module либо graph owner, который действительно собирает эту связку.
|
||||
|
||||
## UI tests
|
||||
|
||||
### TST-N012: Domain UI тестируется при наличии значимого поведения
|
||||
|
||||
Компонент не требует test только потому, что он существует. Test оправдан, если Domain-owned UI:
|
||||
|
||||
- содержит interaction;
|
||||
- отображает несколько domain states;
|
||||
- реагирует на domain error code;
|
||||
- управляет focus или keyboard navigation;
|
||||
- имеет значимый accessibility contract;
|
||||
- использует framework lifecycle;
|
||||
- содержит регрессионно опасную presentation logic.
|
||||
|
||||
Проверяются observable behavior и accessibility semantics, а не внутренняя структура JSX/Vue template.
|
||||
|
||||
Snapshot-only tests не являются обязательным доказательством. Визуальные различия при необходимости проверяются отдельным visual regression инструментом.
|
||||
|
||||
Universal UI module тестируется в слое `ui`, а page/screen/composition UI тестируется у соответствующего composition owner. Наличие React/Vue само по себе не переносит ownership теста в Domain.
|
||||
|
||||
## Graph и E2E tests
|
||||
|
||||
### TST-N013: Cross-domain graph тестируется у graph owner
|
||||
|
||||
Проверяются:
|
||||
|
||||
- topological assembly order;
|
||||
- передача собранных API в dependent factories;
|
||||
- exact graph type;
|
||||
- отсутствие повторной assembly без нужного scope;
|
||||
- ownership instance;
|
||||
- lifecycle start и cleanup;
|
||||
- request/application/page isolation.
|
||||
|
||||
Business modules не содержат tests полного application graph.
|
||||
|
||||
### TST-N014: E2E дополняет, но не заменяет Domain tests
|
||||
|
||||
E2E проверяет пользовательский поток через реальный application entry. Он не заменяет factory-level tests, потому что не способен дешёво и детерминированно перебрать malformed responses, synchronous throws, races и все domain error mappings.
|
||||
|
||||
## Чего избегать
|
||||
|
||||
### TST-N015: Test suite не повторяет одну ответственность на всех уровнях
|
||||
|
||||
Не рекомендуется:
|
||||
|
||||
- повторять одну scenario matrix в service, factory, preset и framework tests;
|
||||
- тестировать business через production SDK;
|
||||
- использовать общий mutable API instance между tests;
|
||||
- экспортировать test harness из production public API;
|
||||
- создавать `presets/testing` как default-механизм unit tests;
|
||||
- проверять private implementation из factory-level tests;
|
||||
- считать type-only файл требующим runtime unit test;
|
||||
- использовать real network или process env в business tests.
|
||||
|
||||
Минимальная правильная граница предпочтительнее большого количества дублирующих tests.
|
||||
Файл test создаётся вместе с реальным risk, а не ради scaffold. Однако public business scenario не считается завершённым без factory-level tests; production preset без assembly test; adapter с нетривиальным transport mapping без adapter test; React binding с lifecycle behavior без framework test.
|
||||
|
||||
85
DRAFT/level-3/terminology.md
Normal file
85
DRAFT/level-3/terminology.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Терминология Level 3
|
||||
|
||||
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
|
||||
|
||||
Level 3 наследует терминологию Levels 1-2 и заменяет структурную модель доменного модуля Level 2 новой сущностью Domain.
|
||||
|
||||
## Domain
|
||||
|
||||
### Domain
|
||||
|
||||
Немодульная предметная граница слоя `domains`, представляющая одну самостоятельную предметную область. Domain объединяет role modules и Groups этой области, но не содержит собственного runtime-кода, состояния, lifecycle, public API или узла графа зависимостей.
|
||||
|
||||
Domain не является module и не является Group. Он может находиться непосредственно в слое `domains` или внутри навигационной Group этого слоя. В этом частном случае Group вправе содержать Domain, но сохраняет все остальные свойства Group Level 1.
|
||||
|
||||
Module, расположенный непосредственно внутри Domain или внутри его Group, не является вложенным module: ближайшая внешняя граница Domain не является module.
|
||||
|
||||
### Role module
|
||||
|
||||
Module внутри Domain, чья ответственность определяется одной технической ролью: `business`, preset, adapter или framework binding. Каждый role module остаётся обычным module Level 1 со своим public API и узлом графа зависимостей.
|
||||
|
||||
### Business module
|
||||
|
||||
Обязательный module `business` внутри Domain. Он определяет public business scenarios, business contracts, factory, ports, domain errors, детерминированные правила и семантику domain state.
|
||||
|
||||
`business` не зависит от concrete runtime, execution environment или framework.
|
||||
|
||||
### Port
|
||||
|
||||
Минимальный business-owned contract runtime capability, которая нужна business для выполнения scenario. Port описывается языком предметной области и не раскрывает SDK, generated DTO, store, hook, platform object или другой concrete runtime.
|
||||
|
||||
### Factory
|
||||
|
||||
Функция business module, которая получает полный набор ports и создаёт business API instance. Factory не является assembly и не выбирает concrete implementation ports.
|
||||
|
||||
### Adapter
|
||||
|
||||
Код, который реализует один или несколько business ports поверх concrete runtime: SDK, storage, platform API, request input, state manager или технического сервиса. Adapter может быть private segment preset module либо самостоятельным promoted adapter module.
|
||||
|
||||
### Preset
|
||||
|
||||
Module с именованной повторяемой assembly одной business factory для execution context. Preset выбирает implementations ports и сообщает caller, как владеть созданным API instance.
|
||||
|
||||
### Framework module
|
||||
|
||||
Role module, который существует из-за contract конкретного framework. Такой module размещается непосредственно в Domain и называется именем framework: `react`, `vue` и аналогично. Он получает готовый business API, но не собирает factory и не реализует concrete adapter.
|
||||
|
||||
### Assembly site
|
||||
|
||||
Место, которое вызывает business factory, передаёт полный набор ports и получает API instance. Reusable assembly оформляется preset module; одноразовая assembly принадлежит явному composition graph owner. Framework module не является assembly site.
|
||||
|
||||
### Graph owner
|
||||
|
||||
Код, который удерживает конкретный runtime graph и API instances в execution scope и вызывает предоставленные start/cleanup operations. Graph owner не заменяет module-владельца lifecycle resource; contract создания, области жизни, числа instances и cleanup определяет module по правилам Level 1. Graph owner может быть application, route, page, request или test scope.
|
||||
|
||||
### Environment boundary
|
||||
|
||||
Граница между client-only, server-only и isomorphic import graphs. Она определяется достижимостью import graph, а не названием папки или надеждой на tree shaking.
|
||||
|
||||
## Виды владения
|
||||
|
||||
Level 3 различает три вопроса, которые в обычной речи могут называться владением:
|
||||
|
||||
| Вопрос | Ответственный |
|
||||
|---|---|
|
||||
| Какая предметная область и словарь объединяют код | Domain |
|
||||
| Кто владеет самостоятельной ответственностью и public API | Конкретный module |
|
||||
| Кто удерживает API instance в execution scope и вызывает lifecycle operations | Graph owner |
|
||||
|
||||
Например, `business` владеет моделью `AuthState` и её допустимыми переходами. Adapter владеет concrete state runtime и его lifecycle contract. Graph owner удерживает конкретный `AuthApi` instance в допустимом scope и вызывает его cleanup.
|
||||
|
||||
## Структурная модель
|
||||
|
||||
```text
|
||||
SLM root
|
||||
└── domains
|
||||
└── Domain
|
||||
├── business module
|
||||
├── presets Group
|
||||
│ └── preset module
|
||||
├── adapters Group
|
||||
│ └── adapter module
|
||||
└── react framework module
|
||||
```
|
||||
|
||||
`presets` и `adapters` являются Groups только при наличии соответствующих modules. `errors`, `ports`, `services`, `types`, `hooks` и `providers` являются segments своих module-владельцев, если сами не образуют отдельный module.
|
||||
49
DRAFT/level-3/validation.md
Normal file
49
DRAFT/level-3/validation.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Проверка Level 3
|
||||
|
||||
> Граница автоматической проверки, архитектурного ревью и verification Level 3.
|
||||
|
||||
## Конфигурация проекта
|
||||
|
||||
Конфигурация проверки сопоставляет физические пути с Domain, role modules, Groups, public entrypoints и environment labels. Она также определяет, какие entrypoints считаются client-only, server-only или isomorphic.
|
||||
|
||||
Сопоставление путей не определяет предметный смысл Domain. Оно позволяет проверить форму Domain, public API modules, import graph, циклы и environment boundaries.
|
||||
|
||||
## Автоматическая проверка
|
||||
|
||||
Автоматическая проверка должна блокировать:
|
||||
|
||||
- отсутствие или множественность `business` module в Domain;
|
||||
- runtime-код или root entrypoint у Domain;
|
||||
- deep imports в segments role modules;
|
||||
- достижение framework, concrete runtime, environment markers, adapters или presets из `business` entrypoint;
|
||||
- достижение incompatible environment graph из client-only, server-only или isomorphic entrypoint;
|
||||
- циклы между modules по общему правилу Level 1.
|
||||
|
||||
## Архитектурное ревью
|
||||
|
||||
На ревью определяется:
|
||||
|
||||
- является ли Domain одной связной предметной областью;
|
||||
- принадлежит ли business scenario, error contract и state semantics `business` module;
|
||||
- описывает ли port минимальную business capability без concrete types;
|
||||
- остаётся ли adapter техническим bridge без domain fallback и error mapping;
|
||||
- является ли preset повторяемой assembly конкретного scope;
|
||||
- определены ли module-владелец lifecycle contract, graph owner, scope instance, start и cleanup;
|
||||
- проходит ли междоменная runtime-связь через consumer-owned port;
|
||||
- принадлежит ли React UI Domain, а не конкретной page или route composition.
|
||||
|
||||
## Verification
|
||||
|
||||
Business проверяется factory-level tests с controlled ports. Adapter проверяется на transport/wiring boundary, preset -- на assembly, scope и environment boundary, React module -- на provider, subscriptions и framework lifecycle. Полная cross-domain assembly проверяется у graph owner.
|
||||
|
||||
Тесты не заменяют автоматические import checks и архитектурное ревью. Они доказывают runtime behavior на уже выбранной границе.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L3-DOMAIN-A002`](../rules/level-3.md#slm-l3-domain-a002)
|
||||
- [`SLM-L3-BUSINESS-R003`](../rules/level-3.md#slm-l3-business-r003)
|
||||
- [`SLM-L3-BUSINESS-A004`](../rules/level-3.md#slm-l3-business-a004)
|
||||
- [`SLM-L3-ENVIRONMENT-A012`](../rules/level-3.md#slm-l3-environment-a012)
|
||||
- [`SLM-L3-TEST-R014`](../rules/level-3.md#slm-l3-test-r014)
|
||||
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
|
||||
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
|
||||
Reference in New Issue
Block a user