chore: Зафиксироваче черновик доменов отдельно

This commit is contained in:
2026-07-30 11:58:46 +03:00
parent bd479b346f
commit 8241694fd5
9 changed files with 1601 additions and 0 deletions

86
DRAFT_domains/README.md Normal file
View File

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

View File

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

174
DRAFT_domains/business.md Normal file
View File

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

206
DRAFT_domains/domain.md Normal file
View File

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

View File

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

View File

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

View File

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

141
DRAFT_domains/presets.md Normal file
View File

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

402
DRAFT_domains/testing.md Normal file
View File

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