feat: add example

This commit is contained in:
2026-08-01 09:31:08 +03:00
parent 15805e28df
commit 26b59686a5
434 changed files with 34975 additions and 4995 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
# SLM Design
Этот каталог содержит действующую legacy-документацию по SLM-архитектуре.
Используй эту документацию как источник истины для задач по SLM Design: слоям, модулям, сегментам, публичному API, business factory, composition modules и монорепозиториям.
## Структура
- `canons/` - основные каноны SLM Design.
- `examples/` - дополнительные примеры реализации.
Точка входа: `canons/index.md`.

View File

@@ -1,331 +0,0 @@
---
title: Business-фабрика
description: Контракт business-модуля, runtime-зависимости, logic API, доменные ошибки и место сборки фабрик
---
# Business-фабрика
Раздел фиксирует архитектурный контракт business-модуля: фабрика описывает доменный logic API приложения и не знает о конкретном backend, SDK, storage, state/query runtime, browser API или React tree.
## Главный принцип
Business-модуль описывает домен приложения, а не форму backend API, SDK, storage, external hook, state manager или внешнего сервиса.
Фабрика должна отвечать на вопрос: какой API нужен приложению для работы с доменом. Она не должна отвечать на вопрос: какие методы есть у конкретного REST-клиента, SDK или browser API.
## Когда создавать business-модуль
Полный контракт business-модуля — фабрика, `deps`, доменные типы, доменные ошибки, adapters, сборка в `compositions/business/{domain}`, обязательные factory/assembly tests и colocated tests для внутренней runtime-safe логики. Он применяется к любому модулю, созданному в `business`.
Создавай business-модуль, когда выполняется хотя бы одно условие:
- сценарии нужны нескольким страницам, composition modules или другим доменам;
- у логики есть собственная доменная модель и доменные ошибки, которые нельзя выразить типами внешнего API;
- доменный сценарий зависит от внешних runtime-capabilities (backend, SDK, product storage, source hook, domain store, event, browser API), которые нужно изолировать за `deps`;
- домен нужно тестировать независимо от UI и конкретного backend.
Не создавай business-модуль заранее, если логика нужна одной странице, не имеет доменной модели, product I/O и domain state. Presentation-only store или browser interaction, принадлежащие UI scope, сами по себе не создают business-домен. Такая page-local orchestration живёт в соответствующем composition module.
Наличие product I/O отменяет page-local исключение. Даже если источник нужен одной странице, consumer composition не обращается к нему напрямую: объяви business-контракт, dependency adapter и domain errors.
«Облегчённого» business-модуля не существует: если модуль создан в `business/`, контракт применяется целиком. Подъём вызревшей логики из composition module в business-модуль — обычный рефакторинг: объяви доменные типы и `deps`, перенеси сценарии в services и hooks, собери фабрику и покрой её factory-level тестами.
## Обязательные правила
- Фабрика лежит в корне business-модуля: `business/{domain}/{domain}.factory.ts`.
- Фабрика принимает runtime-зависимости только через `deps`.
- Фабрика возвращает только logic API домена: hooks, selectors, command/query methods, scenario services.
- Фабрика не возвращает React-компоненты, layouts, guards, boundaries, providers или page-level wrappers.
- Business-модуль не содержит React-компоненты. UI-решения домена размещаются в `compositions`; полностью универсальные UI-контролы размещаются в `ui`.
- Business-модуль не импортирует реальные SDK, generated operations, HTTP-клиенты, storage, env, browser API или composition-сборку.
- Business-модуль не импортирует React state/effect runtime, SWR/query runtime, Zustand/Redux/MobX store runtime или event bus implementation.
- Source/query hooks, domain state stores, subscriptions, clock, random и technical services передаются через business-owned `deps`.
- Public contract business-модуля использует собственные доменные типы, а не DTO внешнего API.
- `index.ts` business-модуля экспортирует только фабрику и type-only экспорты. Исключений нет.
- Runtime-сборка business-фабрики выполняется вне `business`, обычно в `compositions/business/{domain}`.
- Для каждой runtime-capability создаётся явный dependency adapter. Adapter не пишется inline внутри builder.
- Из public API business выходят только собственные domain errors со стабильным `code`.
- Factory-level и assembly tests обязательны и входят в критерий завершения модуля.
## Проектирование фабрики
Проектирование начинается со сценариев домена, а не с API-клиента.
Порядок работы:
1. Опиши публичные сценарии, которые нужны приложению.
2. Опиши доменные типы, которыми должен оперировать UI и другие business-модули.
3. Проведи inventory всех runtime-capabilities: data sources, hooks, stores, events, browser APIs, env, clock и cross-domain APIs.
4. Опиши минимальные внешние возможности в `{Domain}Deps`.
5. Назови внешние возможности бизнес-языком, а не языком backend endpoint'ов и библиотек.
6. Опиши domain error codes для каждого публичного сценария.
7. Собери фабрику из внутренних services, hook wrappers, mappers и helpers поверх переданных deps.
8. Верни наружу только `{Domain}Api`.
9. Реализуй каждую dependency отдельным adapter в `compositions/business/{domain}`.
Правильные вопросы:
- Не `какой endpoint дернуть?`, а `какой бизнес-сценарий выполняем?`.
- Не `какой DTO пришёл?`, а `какую доменную модель отдаём наружу?`.
- Не `как называется метод SDK?`, а `какая внешняя возможность нужна домену?`.
- Не `как устроен backend сейчас?`, а `какой стабильный контракт нужен приложению?`.
## Контракт зависимостей
`{Domain}Deps` описывает не технические инструменты, а возможности, которые нужны домену.
Правила:
- имя зависимости отражает бизнес-возможность: `phoneAuth`, `session`, `profile`, `agreements`, `notifications`;
- методы внутри зависимости называют сценарное действие без повторения endpoint names: `phoneAuth.requestCode`, `phoneAuth.verifyCode`, `profile.getCurrentUser`, `agreements.saveUserAgreements`;
- параметры используют доменные типы business-модуля;
- результат внешней границы принимается как `unknown`, если данные требуют runtime-нормализации;
- пустой успешный ответ описывается как `Promise<void>`, если body не нужен;
- dependency не должна возвращать generated DTO как доменный тип;
- mapper, normalizer или domain error не передаётся через `deps`, если это часть доменной логики.
- source/query hook описывается business-owned result type, без `SWRConfiguration`, `UseQueryResult` и других library types;
- domain state описывается business-owned port, без `StoreApi` и concrete state manager types;
- subscription возвращает cleanup function;
- недетерминированные clock/random/id capabilities передаются явно;
Плохо:
```ts
import type { ApiRequestClient, BoundApi } from '@vendor/api-sdk'
import type { v1PatientProfileList } from '@vendor/api-sdk/operations'
type UserApiTree = {
patient: {
profile: typeof v1PatientProfileList
}
}
export type UserDeps = {
api: BoundApi<UserApiTree, ApiRequestClient>
}
```
Проблема: business-модуль знает про SDK, generated operation и форму внешнего клиента.
Хорошо:
```ts
import type { AuthState } from './auth-state.type'
import type { VerifyPhoneCodeData } from './verify-phone-code-data.type'
export type AuthDeps = {
phoneAuth: {
requestCode: (phone: string) => Promise<unknown>
resendCode: (challengeId: string) => Promise<unknown>
verifyCode: (data: VerifyPhoneCodeData) => Promise<unknown>
}
session: {
setToken: (token?: string | null) => void
useToken: () => string | null | undefined
}
sessionEvents: {
onInvalidated: (listener: () => void) => () => void
}
state: {
create: (initialState: AuthState) => {
get: () => AuthState
set: (state: AuthState) => void
useState: () => AuthState
}
}
}
```
Конкретный SDK, storage, source hook, store или mock подключается в `compositions/business/auth` через адаптер. Business-модуль знает только о своём `AuthDeps`.
## Публичный API фабрики
`{Domain}Api` описывает logic API домена, который фабрика возвращает наружу в runtime.
Правила:
- API говорит на языке домена;
- API не повторяет endpoint names;
- API не отдаёт DTO внешнего сервиса;
- API не раскрывает внутренние `create*` services;
- API остаётся стабильным при замене backend, SDK или storage;
- API может возвращать hooks и selectors, созданные поверх переданных dependency hooks/state ports; business не импортирует concrete hook/store runtime;
- API не возвращает React-компоненты, UI-компоненты, layouts, guards, boundaries или page-level wrappers.
Плохо:
```ts
export type AuthApi = {
AuthGuard: ComponentType<AuthGuardProps>
LoginButton: ComponentType<LoginButtonProps>
useAuth: ReturnType<typeof createAuthHook>
}
```
Проблема: factory output смешивает доменную логику с UI и начинает собирать React tree.
Хорошо:
```ts
export type AuthApi = {
requestPhoneCode: ReturnType<typeof createRequestPhoneCode>
resendPhoneCode: ReturnType<typeof createResendPhoneCode>
verifyPhoneCode: ReturnType<typeof createVerifyPhoneCode>
useAuth: ReturnType<typeof createAuthHook>
useIsAuthenticated: ReturnType<typeof createIsAuthenticatedHook>
startSessionInvalidationTracking: () => () => void
}
```
Такой API сообщает composition-слою доменное состояние и действия, но не навязывает UI-решение.
## Реализация фабрики
Фабрика связывает внутренние части business-модуля с переданными зависимостями.
```ts
import { createCurrentUserHook } from './hooks/use-current-user.hook'
import { createStoredUserAgreementsGetter } from './services/get-stored-user-agreements.service'
import { createUpdateCurrentUserProfile } from './services/update-current-user-profile.service'
import type { UserFactory } from './types/user-factory.type'
export const userFactory: UserFactory = (deps) => {
const { authApi, profile, storage } = deps
return {
getStoredUserAgreements: createStoredUserAgreementsGetter(storage),
updateCurrentUserProfile: createUpdateCurrentUserProfile(profile),
useCurrentUser: createCurrentUserHook({ authApi, profile }),
}
}
```
Фабрика не должна:
- создавать API-клиент;
- выбирать backend endpoint;
- читать env;
- обращаться к browser storage напрямую;
- делать запросы при создании API;
- импортировать composition-сборку;
- подстраивать свой API под конкретный внешний сервис;
- создавать или возвращать React-компоненты;
- импортировать React state/effect APIs;
- импортировать SWR, query library или state manager runtime;
- создавать concrete store, query client или event bus;
- подписываться на external event без dependency contract и явного cleanup.
## Runtime-границы и ошибки
Любая dependency фабрики считается ненадёжной runtime-границей.
Business-модуль защищает публичный контракт от таких случаев:
- dependency вернула `null`, `undefined`, пустой body или объект неправильной формы;
- dependency вернула rejected promise;
- dependency синхронно выбросила исключение;
- storage содержит устаревшие или битые данные;
- внешний сервис поменял форму ответа без изменения TypeScript-типов.
Защита выполняется внутри business-модуля через mappers, normalizers, type guards и domain errors. Fallback допустим только для валидного доменного исхода, явно представленного dependency contract, а не для technical failure или malformed response.
Наружу не должны протекать ошибки SDK, HTTP-клиента, storage, query hook, store, generated API или browser API. Потребитель business API всегда получает только собственную domain error со стабильным `code`, а не `message`, `status`, `response`, `stack` или форму внешней ошибки.
```ts
if (isDomainError<AuthErrorCode>(error) && error.code === 'AUTH_PHONE_CODE_VERIFY_FAILED') {
showInvalidCodeMessage()
}
```
Business всегда преобразует rejected promise, synchronous throw, source hook error и невалидную успешную структуру в собственную domain error.
## Сборка business-фабрики
Реальные runtime-зависимости подключаются в composition module `compositions/business/{domain}`.
```text
src/
├── business/
│ └── auth/
│ ├── auth.factory.ts
│ ├── types/
│ └── index.ts
└── compositions/
└── business/
└── auth/
├── create-auth-business.ts
├── adapters/
├── types/
└── index.ts
```
Если business-домены сгруппированы, сборка повторяет тот же относительный путь: `business/app/auth` соответствует `compositions/business/app/auth`, `business/cms/content` соответствует `compositions/business/cms/content`. Группировка не является default-структурой.
Правила:
- `business/{domain}` объявляет фабрику и контракт `deps`.
- `compositions/business/{domain}` отдельными adapters адаптирует SDK, storage, infra-клиенты, source hooks, stores, events и другие runtime-capabilities к этому контракту.
- Builder явно создаёт или получает scoped runtime instances без I/O, создаёт adapters поверх них, передаёт adapters фабрике и возвращает API; integration logic не пишется inline.
- `create{Domain}Business()` возвращает готовый `{Domain}Api`.
- Browser/application `create{Domain}Business()` принимает аргументы только для API других уже собранных business-фабрик.
- SDK, storage, env, HTTP-клиенты и browser API не передаются в `create{Domain}Business()` как deps.
- Если домен не зависит от других business API, `create{Domain}Business()` вызывается без аргументов.
- Request-scoped builder отделяет cross-domain API от `requestScopeInput`. В input находятся только framework/request data; concrete client factory импортируется внутри integration module. Request input используется только для создания adapters и не передаётся factory как raw dependency.
- Конечный граф API собирается в месте, которое владеет lifecycle: page composition, route composition, provider, request scope или test setup.
- `infra` не собирает business-фабрики, потому что не должен импортировать `business`.
- `app` не реализует business-сборку, а только подключает готовые composition modules к фреймворку.
```ts
import { createAuthBusiness } from '@/compositions/business/auth'
import { createUserBusiness } from '@/compositions/business/user'
const authApi = createAuthBusiness()
const userApi = createUserBusiness({ authApi })
```
Подробный пример см. в [Business composition](../examples/business-composition.md).
## Public API файла index.ts
Жёсткое правило без исключений: `business/{domain}/index.ts` экспортирует только фабрику и type-only экспорты.
```ts
export { userFactory } from './user.factory'
export type { User } from './types/user.type'
export type { UserApi } from './types/user-api.type'
export type { UserDeps } from './types/user-deps.type'
export type { UserFactory } from './types/user-factory.type'
export type { UserErrorCode } from './types/user-error-code.type'
```
## Тестирование
Business-модуль тестируется через public API фабрики. Factory-level тесты обязательны, импортируют модуль только через `business/{domain}` и не используют deep imports во внутренние `services`, `hooks`, `mappers` или `lib`.
Сборка в `compositions/business/{domain}` обязательно тестируется отдельно: эти тесты проверяют adapters, корректность передачи deps, отсутствие I/O при создании API и lifecycle cleanup, но не заменяют сценарные тесты business-модуля.
Colocated unit tests обязательны там, где есть mappers, normalizers, type guards, domain errors или другая внутренняя runtime-safe логика. Они являются дополнительным уровнем, а не заменой factory-level и assembly tests.
Подробный пример см. в [Business testing](../examples/business-testing.md).
## Чеклист
- Фабрика лежит в корне business-модуля.
- Фабрика принимает все runtime-зависимости через `deps`.
- В `deps` нет SDK, generated operations, HTTP-клиентов, backend DTO, StoreApi и query-library types.
- Контракты зависимостей названы бизнес-языком.
- Source/query hooks, stores, events и platform APIs переданы через `deps`.
- Business не импортирует React/SWR/query/store runtime.
- Все public types принадлежат business-модулю.
- Внешние ответы нормализуются перед попаданием в public API.
- Все source errors без исключений заменяются собственными domain errors.
- Public API фабрики не повторяет внешний API.
- Public API фабрики не возвращает React-компоненты.
- Business-модуль не содержит React-компоненты.
- `index.ts` экспортирует только фабрику и type-only экспорты, без исключений.
- Runtime-сборка живёт в `compositions/business/{domain}`.
- Для каждой runtime-capability существует private adapter.
- Builder не содержит inline integration logic.
- Business-модуль можно подключить к другому backend через адаптер без изменения фабрики.
- Factory-level и assembly tests созданы и выполняются.

View File

@@ -1,293 +0,0 @@
---
title: Runtime-граница business
description: Строгая изоляция business-фабрики от источников данных, state/query runtime, infra, browser API и внешних ошибок
---
# Runtime-граница business
## Главный инвариант
Business-модуль выполняет доменную композицию только над:
- собственными типами и детерминированной логикой;
- capabilities, переданными фабрике через `{Domain}Deps`.
Business не вызывает runtime-возможность, если она не была передана фабрике. Это относится не только к данным и `infra`, но и к hooks, stores, subscriptions, browser API и другим concrete runtime-механизмам.
Фабрика отвечает на вопрос «какой стабильный доменный API нужен приложению», а не «какими библиотеками и источниками он реализован».
## Что разрешено внутри business
Business может напрямую использовать:
- собственные domain types;
- собственные services, mappers, normalizers, validators и type guards;
- собственные domain errors;
- детерминированные вычисления без I/O, runtime-state и скрытого окружения;
- чистые библиотеки вроде schema validators, decimal/date utilities, если результат определяется только явными аргументами и типы библиотеки не становятся public contract;
- type-only контракты других business API для cross-domain dependencies.
## Что передаётся через deps
Через `{Domain}Deps` передавай любую runtime-capability:
| Capability | Примеры concrete implementation |
|---|---|
| Product source | REST SDK, GraphQL client, CMS, storage |
| Source/query hook | SWR, TanStack Query, Apollo hook |
| Domain state runtime | Zustand, Redux, MobX, RxJS store |
| Technical service | logger, telemetry, notifications, i18n engine |
| Platform API | `window`, navigation, clipboard, geolocation |
| Lifecycle event | unauthorized event, socket event, subscription |
| Environment | env/config provider, feature runtime configuration |
| Nondeterminism | clock, timer, random, ID generator |
| Cross-domain behavior | ограниченный API другой business-фабрики |
Не импортируй concrete implementation в business даже в том случае, если библиотека используется только в одном внутреннем файле.
## Запрещённые imports
Production-код `business/**` не импортирует напрямую ни runtime values, ни types из concrete runtimes:
- `infra`, `compositions`, `app`;
- SDK, generated operations и HTTP clients;
- storage implementation;
- React state/effect APIs;
- SWR, TanStack Query, Apollo и другие query runtimes;
- Zustand, Redux, MobX, RxJS stores;
- browser и framework runtime APIs;
- event bus implementation;
- env и process-specific configuration.
Запрет нельзя обойти через `import type`, alias, barrel, type cast или helper в `shared`. Type-only разрешены собственные contracts, детерминированный `shared`, чистые libraries и суженные public API других business-доменов.
## Доменный шлюз данных
Business API является единственной продуктовой границей для потребительского кода.
```text
page / layout / screen / widget
→ {Domain}Api
→ business scenario
→ {Domain}Deps
→ private dependency adapter
→ infra client / SDK / storage / external source
```
Внешний сервис остаётся физическим источником данных. Business является единственным источником доменной истины: он определяет модель, сценарий, нормализацию, fallback и ошибки.
Обычный consumer composition не создаёт параллельный продуктовый контракт поверх DTO или client. Единственная зона concrete product integration внутри `compositions``compositions/business/{domain}`.
## Business-owned deps
`{Domain}Deps` принадлежит business-модулю и описывает необходимые возможности доменным языком.
Требования:
- группируй методы по capability, а не по имени SDK/client;
- принимай доменные аргументы;
- принимай внешние результаты как `unknown`, если нужна runtime-проверка;
- описывай собственную минимальную форму dependency hook/result;
- описывай собственный state port, а не `StoreApi` конкретной библиотеки;
- возвращай cleanup из subscription capability;
- не передавай mapper, normalizer или domain error через deps;
- не используй generated DTO как доменную модель;
- не передавай целый client, если домену нужны два конкретных действия.
Плохо:
```ts
import type { StoreApi } from 'zustand'
import type { AdminApiClient } from '@vendor/admin-sdk'
export type AuthDeps = {
api: AdminApiClient
store: StoreApi<AuthState>
}
```
Хорошо:
```ts
import type { AuthState } from './auth-state.type'
import type { VerifyPhoneCodeData } from './verify-phone-code-data.type'
export type SourceHookResult = {
data: unknown
error: unknown
isLoading: boolean
refresh: () => Promise<void>
}
export type AuthDeps = {
phoneAuth: {
requestCode: (phone: string) => Promise<unknown>
resendCode: (challengeId: string) => Promise<unknown>
verifyCode: (data: VerifyPhoneCodeData) => Promise<unknown>
}
session: {
setToken: (token?: string | null) => void
useToken: () => string | null | undefined
}
sessionEvents: {
onInvalidated: (listener: () => void) => () => void
}
state: {
create: (initialState: AuthState) => {
get: () => AuthState
set: (state: AuthState) => void
useState: () => AuthState
}
}
}
```
## Dependency adapters
Adapter находится снаружи business и реализует конкретную часть `{Domain}Deps`.
```text
compositions/business/auth/
├── create-auth-business.ts
├── adapters/
│ ├── admin-auth-session.adapter.ts
│ ├── browser-auth-navigation.adapter.ts
│ ├── zustand-auth-state.adapter.ts
│ └── admin-auth-session-events.adapter.ts
└── index.ts
```
Правила adapter:
- импортирует type-only business contract;
- импортирует concrete infra/runtime implementation;
- преобразует доменные аргументы в transport arguments;
- возвращает raw/unknown runtime result, если business должен его проверить;
- пробрасывает source error без создания domain error;
- не реализует бизнес-правило;
- не выбирает доменный fallback;
- не экспортируется через public API integration module;
- тестируется на wiring, payload и соответствие dependency contract.
Каждая runtime-capability получает явный adapter. Не скрывай несколько разных integrations как inline-функции внутри builder.
## Dependency hooks
Если business должен предоставить hook, concrete hook передаётся через deps.
```text
SWR/TanStack hook
→ dependency adapter
→ business-owned source hook contract
→ business wrapper hook
→ domain result / domain error
```
Business wrapper может:
- вызвать переданный dependency hook;
- нормализовать `data` в доменную модель;
- заменить source error доменной ошибкой;
- вычислить domain state и selectors;
- вернуть собственный стабильный result type.
Dependency hook обязан быть non-throwing и non-Suspense: technical failure возвращается в `error: unknown`, а не выбрасывается во время вызова hook. Business wrapper обязан заменить этот `error` собственной domain error. Публичные callbacks dependency result, например `refresh`, также оборачиваются business и не могут выдать source error наружу.
Business wrapper не импортирует query library. В public contract не протекают `SWRConfiguration`, `UseQueryResult`, query keys, cache implementation или library-specific mutate API без отдельного domain contract.
## Domain state
Business владеет:
- моделью доменного state;
- допустимыми переходами;
- commands и selectors;
- реакцией на dependency results и events.
Business не владеет concrete state manager. Business выбирает initial domain state и передаёт его в `deps.state.create(initialState)`; adapter только создаёт concrete store с переданным значением.
Zustand/Redux/MobX adapter реализует business-owned state adapter factory и создаётся в `compositions/business/{domain}`. Business-фабрика получает adapter factory через `deps`, передаёт initial domain state и получает concrete port.
Локальный UI-state является другим случаем. Состояние раскрытия sidebar, выбранной вкладки или шага локального UI-flow может использовать concrete state manager непосредственно внутри владеющего composition module, если оно не подменяет доменное состояние и product data boundary.
## Доменные ошибки
Из любого публичного метода, command, query или hook business-модуля выходят только собственные доменные ошибки.
Business обязан преобразовать:
- rejected promise dependency;
- synchronous throw dependency;
- source hook error;
- ошибку store/storage/browser API;
- невалидный успешный ответ;
- неизвестную runtime-ошибку.
Domain error содержит стабильный `code`. Исходная ошибка может сохраняться только в `cause` для диагностики.
Потребитель не ориентируется на:
- `message` внешней ошибки;
- HTTP status;
- `response`;
- SDK error class;
- stack или transport code.
Adapter не импортирует и не создаёт domain error. Error mapping всегда выполняет business.
Rejected promise, synchronous throw, source error и malformed response всегда превращаются в domain error. Fallback допустим только для валидного доменного исхода, явно представленного dependency contract, например корректного отсутствия данных. Fallback не используется для поглощения technical failure или contract drift.
## Чистый builder
`compositions/business/{domain}/create-{domain}-business.ts` только:
1. явно создаёт или получает runtime instances нужного lifecycle без I/O;
2. создаёт adapters поверх этих instances;
3. передаёт adapters и API других доменов в factory;
4. возвращает готовый `{Domain}Api`.
Builder не содержит:
- inline SDK calls;
- `window`/storage operations;
- domain mapping;
- domain errors;
- Zustand/SWR setup вперемешку с другими dependencies;
- product request при создании API;
- скрытую subscription без cleanup contract.
Если capability имеет lifecycle, business API предоставляет domain-level `start`/`subscribe` operation, которая использует dependency и возвращает cleanup wrapper. Business преобразует ошибки регистрации, callback и cleanup в собственные domain errors. Builder остаётся без side effects, а владелец scope запускает operation после mount/commit и вызывает cleanup при завершении lifecycle.
Browser/application builder без cross-domain dependencies вызывается без аргументов. Для request scope integration module определяет отдельный `requestScopeInput` только с framework/request data, например headers, cookies, request ID и abort signal. Concrete client factory импортируется и вызывается внутри integration module. Request input используется только для создания adapters, не передаётся в business как raw client и не экспортируется consumer compositions.
## Graph и lifecycle
Per-domain builder не является владельцем полного graph.
Владелец graph:
- выбирает application-lifetime composition/route/page/request/test scope;
- собирает домены в ацикличном порядке;
- создаёт ровно необходимый набор API;
- использует точный graph type;
- управляет cleanup subscriptions/resources;
- не повторяет adapter wiring;
- не импортирует raw infra для «досборки» домена.
Не используй generic `Partial<Business>` с последующим `as Business`. Если scope содержит только Auth, его contract должен обещать только Auth.
## Cross-domain dependencies
Один business-домен не импортирует runtime другого домена. Он объявляет нужную capability в своих `Deps` через type-only API другого домена, по возможности суженный `Pick`.
Готовый API передаётся builder-у при сборке graph:
```text
createAuthBusiness()
→ createUserBusiness({ authApi })
→ createOrdersBusiness({ userApi })
```
Runtime-цикл означает ошибочную границу доменов. Не скрывай цикл service locator, lazy import или глобальным event bus.
Подробный контракт фабрики находится в [Business-фабрике](./business-factory.md). Практическая сборка показана в [Business composition](../examples/business-composition.md).

View File

@@ -1,216 +0,0 @@
---
title: Процесс архитектурного решения
description: Обязательный порядок классификации задачи, выбора владельца, слоя, scope и стратегии изменений
---
# Процесс архитектурного решения
Не изменяй файлы, пока не принято архитектурное решение. Название папки, существующий похожий код и удобный импорт не доказывают правильность размещения.
## Карточка решения
Перед реализацией определи:
| Вопрос | Что зафиксировать |
|---|---|
| Роль изменения | Framework wiring, продуктовый сценарий, интеграция, композиция интерфейса, технический сервис, UI или чистый фундамент |
| Владелец | Домен, route/page scope, composition module, infra-модуль, UI-модуль или локальный consumer |
| Данные | Продуктовые данные, техническое состояние, framework input, локальное UI-state или отсутствуют |
| Runtime-возможности | Источники данных, hooks, stores, SDK, browser API, events, clock, random, env и другие внешние capabilities |
| Место | Приложение или package, слой, модуль, вложенный модуль и сегмент |
| Публичная граница | Что действительно нужно экспортировать и кто будет consumer |
| Путь данных | От consumer до business API, dependency adapter и конкретного источника |
| Lifecycle | Кто создаёт instance, сколько instances допустимо и кто выполняет cleanup |
| Стратегия | Локальная правка, новый модуль, новый business-контракт, adapter, перенос или исправление public API |
| Проверки | Typecheck, тесты, import graph, public API, lifecycle и архитектурные инварианты |
Карточку не обязательно выводить пользователю, если решение очевидно. Но агент обязан уметь обосновать каждый пункт до изменения файлов.
## Сбор контекста
Перед выбором места:
1. Прочитай локальные инструкции приложения или package.
2. Найди фактическую границу SLM: `src/`, `apps/{app}/src` или другой локальный root.
3. Проверь существующие слои, группы и соседние модули. Не создавай новую параллельную структуру без необходимости.
4. Проверь aliases, package exports и реальную разрешимость импортов.
5. Найди текущих consumers, public API и runtime import graph изменяемой ответственности.
6. Проверь существующие templates или generators после архитектурного выбора. Шаблон не принимает решение за SLM.
7. Отдельно найди product I/O, hooks, stores, subscriptions, browser API и другие runtime-возможности.
8. Проверь, нет ли уже business-домена, которому принадлежит сценарий.
Не считай неиспользуемый provider, пустой context, тип будущего graph или ссылку на несуществующий домен готовой архитектурой. Решение должно быть достижимо из runtime entry point и иметь реальных consumers.
## Выбор роли
Классифицируй ответственность в следующем порядке.
### Framework wiring
Если код существует только из-за фреймворка, размести его в `app`:
- route-файл;
- bootstrap;
- framework error entry;
- подключение глобальных ресурсов;
- тонкое подключение готового composition module.
`app` не реализует продуктовую композицию, business graph, store, provider или экран.
### Продуктовый сценарий
Если код определяет пользовательский сценарий, доменную модель, продуктовый state, бизнес-правило, нормализацию внешних данных, error mapping или доменный переход после ошибки, владелец находится в `business/{domain}`.
Визуальная реакция на готовый domain error принадлежит consumer composition: сообщение, error screen, redirect, retry control и UI fallback выбираются по стабильному доменному `code`.
Любой новый внешний источник продуктовых данных требует business-контракта. Колокация внешних вызовов в page/screen/widget services не является допустимым упрощением.
### Интеграция business-домена
Если код реализует `{Domain}Deps` через SDK, HTTP, storage, browser API, state/query runtime, event bus или другой concrete runtime, размести его в `compositions/business/{domain}`.
Это интеграционный composition module, а не business-домен и не обычная page/screen/widget composition.
### Продуктовая композиция
Если код собирает route/page/layout/screen/widget, управляет UI-state, provider scope или lifecycle готового business graph, размести его в соответствующем composition module.
Потребительский composition module получает продуктовые данные только через `{Domain}Api`. Он не импортирует product SDK, generated operations, product storage adapter или конкретный источник.
### Технический сервис
Если код предоставляет техническую возможность без продуктовой модели и сценариев, размести его в `infra`:
- HTTP client;
- SDK wrapper;
- logger;
- theme engine;
- i18n engine;
- telemetry transport;
- технический realtime client.
Composition может использовать технический infra-сервис напрямую, если сервис не становится обходным путём к продуктовым данным. Если capability нужна business, она всё равно передаётся через business-owned `deps` и adapter.
### Универсальный UI
Если сущность отображает интерфейс, не знает продуктовый сценарий и применима независимо от конкретной composition, размести её в `ui`.
### Чистый фундамент
Если код детерминирован, не имеет runtime-state, не знает продукт и переиспользуется несколькими владельцами, рассмотри `shared`. По умолчанию оставляй код рядом с первым владельцем.
## Выбор scope
Выбирай минимальный scope, который полностью владеет ответственностью:
1. Нужен одному component/module и не имеет самостоятельной ответственности: оставь внутри владельца.
2. Нужен как самостоятельная часть одного module: создай nested module в `parts/`.
3. Нужен нескольким частям одной page/route ветки: подними в общий composition scope этой ветки.
4. Нужен нескольким composition modules и остаётся продуктовой композицией: создай отдельный composition module.
5. Является доменным сценарием или product data boundary: создай или расширь `business/{domain}`.
6. Является техническим сервисом: создай или расширь `infra/{service}`.
7. Является универсальным UI: создай или расширь `ui/{module}`.
8. Выноси в package только `ui`, `infra` или `shared` код с реальным вторым consumer либо явно зафиксированным межприложенческим ownership/reuse-контрактом.
Не поднимай код выше ради короткого импорта. Не создавай `shared`, общий provider, generic business context или package «на будущее».
## Component, module и group
Применяй решение последовательно:
1. Только отображает готовые props и не владеет зависимостями: component в `ui/` родительского module.
2. Владеет сценарием, данными, state, dependency, lifecycle или внутренней декомпозицией: самостоятельный module.
3. Самостоятельный module, локальный для владельца: nested module в `parts/`.
4. Папка только классифицирует конечные modules: group без `index.ts`, state и runtime logic.
5. `ui/`, `parts/`, `hooks/`, `types/`, `services/` и другие служебные папки внутри module: segments, а не modules.
Если component начинает получать данные, выбирать источник, вызывать сценарный hook или управлять процессом, не добавляй логику в component. Измени архитектурную форму сущности.
## Выбор стратегии
### Новый продуктовый сценарий
1. Найди домен-владелец.
2. Спроектируй `{Domain}Api`, доменные типы и доменные ошибки.
3. Опиши минимальные runtime-capabilities в `{Domain}Deps`.
4. Реализуй детерминированную доменную логику.
5. Создай отдельные adapters в `compositions/business/{domain}`.
6. Собери фабрику чистым builder.
7. Подключи API во владельце lifecycle graph.
8. Используй API из потребительских compositions.
9. Добавь factory-level и assembly tests.
### Прямой product I/O вне business boundary
Не расширяй существующее нарушение.
1. Определи сценарий и домен.
2. Перенеси контракт данных в business-owned `Deps`.
3. Перенеси нормализацию, fallback и error mapping в business.
4. Оставь concrete source call в dependency adapter.
5. Замени прямой вызов на `{Domain}Api`.
6. Закрой adapter и source details из public API.
### Новый store или dependency hook
Сначала определи, является state локальным UI-state или доменным state.
- State является локальным UI-state, если сбрасывается вместе с UI scope, управляет только представлением и не хранит продуктовый факт или product data cache. Такой state может принадлежать composition module и использовать выбранный state manager внутри владельца.
- State является доменным, если выражает продуктовый факт, инвариант, доступен через business API или участвует в бизнес-сценарии.
- Доменный state принадлежит business-контракту. Фабрика получает state adapter factory через `deps`, выбирает initial domain state и создаёт concrete port через adapter.
- Source/query hook реализуется adapter-ом; business вызывает только dependency hook и возвращает собственный доменный hook/result.
### Сборка graph
1. Собери каждый домен отдельным `compositions/business/{domain}` builder.
2. Определи DAG cross-domain зависимостей.
3. Выбери один явный lifecycle scope: application-lifetime composition, route, page, request или test. Слой `app` только подключает application composition.
4. Создавай graph у владельца scope, а не в случайном screen/widget или на module scope без обоснования.
5. Передавай consumers точный graph type. Не используй `Partial<Graph>` с приведением к полному типу.
6. Для subscriptions, timers и resources зафиксируй cleanup/dispose.
### Архитектурное ревью
Проверяй не только пути файлов, но и семантику:
- business-shaped код вне `business`;
- product graph в `infra`;
- type-only imports, которые фактически переносят ownership;
- provider, который не создаёт и не получает instance от явного владельца;
- orphan modules и providers, недостижимые из entry point;
- public API, раскрывающий raw store, context, adapter или generated types;
- отсутствующие tests обязательного business-контракта.
## Условия остановки
Останови реализацию и сначала исправь решение, если:
- владелец ответственности не определён;
- один state или source имеет несколько конкурирующих владельцев;
- business требует прямого runtime или type-only import concrete runtime;
- graph создаёт runtime-цикл;
- lifecycle instance или cleanup не определён;
- public API нужен только для обхода границы;
- шаблон генерирует архитектуру, противоречащую принятому решению;
- изменение требует незапрошенной миграции нескольких независимых областей.
## Локальные материалы
Основной процесс достаточен для типового решения. Открывай только материал, который нужен текущей ветке задачи.
| Ситуация | Материал |
|---|---|
| Нужна полная карта допустимых файлов, root entries, segments и tests | [Атлас файлов SLM](./file-atlas.md) |
| Задача затрагивает product I/O, source hook, domain store, event, lifecycle или external errors | [Runtime-граница business](./business-runtime-boundary.md) |
| Выполняется архитектурное ревью или финальная проверка реализации | [Архитектурная проверка](./validation.md) |
| Неясен layer, направление import или роль `app/compositions/business/infra/ui/shared` | [Слои](./layers.md) |
| Нужно отличить module, component, group, nested module или спроектировать public API | [Модули](./modules.md) |
| Проектируется factory, Api, Deps, domain error или сборка домена | [Business-фабрика](./business-factory.md) |
| Неясно размещение hook/store/service/mapper/provider/type/style | [Сегменты](./segments.md) |
| Решается вынос из `apps/*/src` в `packages/*` | [Монорепозитории](./monorepo.md) |
| Нужен полный пример adapters, builder, state runtime и graph lifecycle | [Business composition](../examples/business-composition.md) |
| Нужна матрица factory-level, assembly и colocated tests | [Тестирование business-модулей](../examples/business-testing.md) |
| Нужен page/route provider, локальный UI store и доступ к готовому graph | [Композиция через Provider](../examples/react/composition-provider.md) |
| Команда выбирает организацию groups внутри `compositions` | [Структуры compositions](../examples/react/composition-structures.md) |
Не используй карту как scaffold checklist. Наличие возможной папки не означает, что её нужно создать.

View File

@@ -1,508 +0,0 @@
---
title: Атлас файлов SLM
description: Карта слоёв, типов modules, root files, segments, public API, tests и запрещённых файловых сочетаний
---
# Атлас файлов SLM
Используй атлас после того, как определены роль изменения и владелец ответственности. Не выбирай архитектуру по желаемому имени файла.
Атлас исчерпывает стандартные архитектурные роли SLM, но не является закрытым списком framework-файлов. Команда может добавить локальный segment или suffix, если его ответственность не дублирует существующую, не нарушает направление зависимостей и закреплена в локальных инструкциях.
## Единицы структуры
| Единица | Что означает | Имеет public API | Владеет runtime |
|---|---|---|---|
| Layer | Верхнеуровневая область ответственности внутри SLM root | Нет общего требования | Зависит от layer |
| Group | Навигационная папка для modules или других groups | Нет, `index.ts` запрещён | Нет |
| Module | Самостоятельный владелец одной ответственности | Да | Может |
| Segment | Папка внутри module по назначению файлов | Нет отдельного внешнего API | Только как часть владельца |
| Component | Презентационная часть родительского module в `ui/` | Локальный `index.ts` допустим | Нет архитектурного runtime |
| Root file | Главный entry/contract конкретного module | Экспортируется module `index.ts` | Зависит от типа module |
Сначала определи module, затем root file, затем необходимые segments. Не создавай все папки из атласа заранее.
## Карта SLM root
```text
src/
├── app/ # framework wiring
├── compositions/ # product tree, graph owners и business integrations
├── business/ # доменные контракты и сценарии
├── infra/ # технические runtime-сервисы
├── ui/ # универсальные UI modules
└── shared/ # детерминированный фундамент
```
В monorepo вместо `src/` границей приложения обычно является `apps/{app}/src/`. Packages находятся выше SLM root и не являются дополнительными слоями.
## Root files modules
| Pattern | Роль | Где допустим |
|---|---|---|
| `{name}.page.tsx` | Готовая page composition | `compositions` |
| `{name}.layout.tsx` | Product layout composition | `compositions` |
| `{name}.screen.tsx` | Уникальный screen leaf/branch | `compositions` |
| `{name}.widget.tsx` | Самостоятельный composition block | `compositions` |
| `{name}.route.tsx` | Route composition и route lifecycle | `compositions` |
| `{name}.entry.tsx` | Готовая точка подключения product tree | `compositions` |
| `{scope}-business-composition.ts` | Non-visual сборка business graph/scope | `compositions` |
| `{name}.ts` | Другой non-visual root, названный по ответственности | `compositions`, nested modules |
| `{name}.tsx` | Root UI/module component без специальной роли | `compositions`, `ui`, nested modules |
| `{domainName}.factory.ts` | Единственный runtime entry business-домена | `business/{domainPath}` |
| `create-{domainName}-business.ts` | Builder одной business-фабрики | `compositions/business/{domainPath}` |
| `{name}.client.ts` | Технический client | `infra/{service}` |
| `{name}.service.ts` | Технический root service, если service и есть module entry | `infra/{service}` |
| `index.ts` | Public API конечного module | В module; запрещён у group |
Root suffix не определяет owner автоматически. Например, `profile.store.ts` остаётся domain store или page UI store в зависимости от смысла state.
## Layer App
`app` содержит только файлы, требуемые framework/runtime entry.
Возможные файлы:
| Файл или pattern | Назначение |
|---|---|
| `main.tsx`, `bootstrap.tsx` | Запуск приложения |
| `app.tsx` | Тонкое подключение application entry/providers |
| `app-router.tsx`, `router.tsx` | Framework route registry |
| `page.tsx`, `layout.tsx`, `route.ts`, `error.tsx`, `not-found.tsx` | Framework-convention files |
| `middleware.ts` | Framework middleware boundary |
| framework metadata/config files | Только framework contract |
Правила:
- framework file импортирует готовый entry/route/page composition через public API;
- product tree собирается в `compositions`, не в `app`;
- business graph, domain store, screen, widget и product provider в `app` запрещены;
- у `app` нет SLM modules и общего `index.ts`;
- framework-specific server/client правила определяет профильный framework skill.
Минимальный пример:
```text
src/app/
├── app-router.tsx
├── app.tsx
└── main.tsx
src/compositions/entries/profile/
├── profile.entry.tsx
└── index.ts
```
## Consumer composition module
Consumer composition собирает product UI и использует готовые `{Domain}Api`.
```text
compositions/{group}/{name}/
├── {name}.{page|layout|screen|widget|route|entry}.tsx # visual entry, если нужен
├── {name}-business-composition.ts # business graph, если module владеет им
├── {name}.ts # другой non-visual entry, если нужен
├── ui/ # presentation components
├── parts/ # nested modules
├── providers/ # provider владельца scope
├── guards/ # route/UI guards над готовым DomainApi
├── hooks/ # access/orchestration hooks
├── stores/ # только локальный UI-state
├── services/ # orchestration готовых API
├── mappers/ # domain result -> ViewModel
├── types/ # module-owned types
├── errors/ # только composition/UI errors, не domain errors
├── lib/ # локальные deterministic helpers
├── config/ # module configuration
├── styles/ # styles module
├── tests/ # scope/integration tests при необходимости
└── index.ts # public API
```
Не все segments обязательны. Создавай только используемые.
Composition module не обязан иметь `.tsx`: request/application business graph использует `{scope}-business-composition.ts`, а другая non-visual orchestration может иметь root `.ts`, названный по ответственности, и public `index.ts`.
Guard принадлежит composition scope, использует готовый `{Domain}Api` и выбирает route/UI outcome. Guard не получает product data напрямую, не создаёт business graph и не импортирует source runtime.
Consumer composition не содержит:
- product SDK/client/generated operation;
- product storage adapter;
- source/query adapter домена;
- domain store implementation;
- domain mapper/error;
- business factory implementation.
Product data поступает только через `{Domain}Api`. `stores/` хранит presentation-only state: sidebar, tab, local step, transient form UI. Product entities и domain state туда не копируются.
### Page/route graph owner
Если composition владеет business graph и lifecycle, возможна структура:
```text
compositions/routes/profile/
├── profile.route.tsx
├── profile-business-composition.ts
├── providers/
│ ├── profile-business.provider.tsx
│ └── profile-business.context.ts
├── hooks/
│ └── use-profile-business.hook.ts
├── types/
│ └── profile-business.type.ts
├── tests/
│ └── profile-business-lifecycle.test.tsx
└── index.ts
```
Правила graph owner:
- graph type перечисляет только реально доступные API;
- `Partial<Business>` с cast к полному graph запрещён;
- graph создаётся в lifecycle владельца;
- domain lifecycle operation запускается после commit и возвращает cleanup;
- raw SDK/client/event bus не импортируется для досборки домена;
- screen/widget не создаёт тот же graph повторно.
## Integration module business
`compositions/business/{domainPath}` является единственным module, который знает одновременно business dependency contract и concrete runtime.
`{domainPath}` означает полный относительный путь конечного business module, а `{domainName}` — имя его последней папки. При groups integration path зеркалирует business path:
```text
business/app/auth/
compositions/business/app/auth/
business/cms/content/
compositions/business/cms/content/
```
```text
compositions/business/{domainPath}/
├── create-{domainName}-business.ts # обязательный builder
├── create-{domainName}-business.test.ts # обязательный assembly test
├── adapters/ # обязательны при runtime dependencies
│ ├── {source}.adapter.ts
│ ├── {storage}.adapter.ts
│ ├── {query}-hook.adapter.ts
│ ├── {state-manager}-state.adapter.ts
│ ├── {event-source}-events.adapter.ts
│ └── {platform}-navigation.adapter.ts
├── types/ # builder/cross-domain/request input types
│ ├── create-{domainName}-business-deps.type.ts
│ └── create-{domainName}-business-request-input.type.ts
├── testing/ # assembly fixtures, не public API
└── index.ts # builder и type-only integration inputs
```
Builder:
1. Явно создаёт или получает runtime instances нужного lifecycle без I/O.
2. Создаёт adapters поверх runtime instances.
3. Передаёт adapters и cross-domain API фабрике.
4. Возвращает готовый `{Domain}Api`.
Browser/application builder без cross-domain dependencies вызывается без аргументов. Request-scoped builder отдельно принимает `requestScopeInput` с request data; concrete client factory импортируется integration module.
Integration module не содержит:
- domain mapper/normalizer;
- domain error;
- business scenario;
- React provider/layout/screen/widget;
- full application graph;
- exported private adapter.
## Business module
Каждый `business/{domainPath}` имеет полный factory contract.
```text
business/{domainPath}/
├── {domainName}.factory.ts # обязательно
├── index.ts # обязательно
├── types/ # обязательно для contracts
│ ├── {domainName}-api.type.ts
│ ├── {domainName}-deps.type.ts
│ ├── {domainName}-factory.type.ts
│ ├── {domainName}-error.type.ts
│ ├── {domainName}-error-code.type.ts
│ ├── {entity}.type.ts
│ ├── {source}-hook-result.type.ts
│ └── {domainName}-state.type.ts
├── errors/
│ └── {domainName}-business.error.ts
├── services/
│ └── {scenario}.service.ts
├── hooks/
│ └── use-{scenario}.hook.ts # wrapper над dependency hook
├── mappers/
│ └── map-{entity}.ts # unknown -> domain
├── lib/
│ └── {domain-helper}.ts
├── config/
│ └── {domainName}.config.ts # deterministic domain constants
└── tests/
└── {domainName}-factory/
├── public-api.test.ts
├── {scenario}.test.ts
└── testing/
└── create-{domainName}-deps.mock.ts
```
Обязательный минимум:
- `{domainName}.factory.ts`;
- `{Domain}Api`, `{Domain}Deps`, `{Domain}Factory`;
- domain error codes и собственная domain error для runtime failure;
- `index.ts` с одним runtime export фабрики и type-only exports;
- factory-level tests каждого public runtime operation;
- integration builder и assembly test для каждого business-модуля, включая dependency-free factory.
Условные файлы:
- `services/` только при выделенном сценарии;
- `hooks/` только для wrapper над dependency hook;
- `mappers/`/`normalizers/`/type guards при внешнем `unknown`;
- `errors/` при runtime operations;
- colocated tests обязательны для каждого mapper, normalizer, type guard и domain error;
- colocated tests services/hooks обязательны при самостоятельной branching, race или другой runtime-safe логике.
В business запрещены:
- `ui/`, React components, providers, layouts и guards;
- concrete `stores/` Zustand/Redux/MobX;
- `adapters/` concrete runtime;
- SDK/client/generated DTO;
- SWR/TanStack Query/Apollo runtime или types;
- React state/effect runtime или types;
- storage/browser/event/env implementation;
- raw external error в public API.
## Infra module
Infra module владеет техническим сервисом без продуктовой модели.
```text
infra/{service}/
├── {service}.client.ts # если module является client
├── {service}.service.ts # если module является service
├── client.ts # допустимый technical entry
├── config/
├── clients/
├── services/
├── transports/
├── hooks/ # technical hooks
├── providers/ # provider technical service
├── ui/ # только technical UI, например theme tooling
├── errors/ # transport/technical errors
├── types/ # technical contracts/DTO
├── lib/
├── tests/
└── index.ts
```
Различай два вида adapter:
| Вид | Где живёт | Что знает |
|---|---|---|
| Transport adapter/client | `infra` | Протокол, SDK, HTTP, transport types |
| Domain dependency adapter | `compositions/business/{domainPath}` | Business `Deps` и конкретный infra runtime |
Infra не содержит business graph, domain state, product provider или domain error. Type-only imports business API не дают infra права агрегировать product graph.
## UI module
UI module предоставляет универсальный UI без business logic и product I/O.
```text
ui/{name}/
├── {name}.tsx # обязательный root component
├── ui/ # внутренние presentation components
├── parts/ # nested UI modules при самостоятельной роли
├── hooks/ # presentation behavior
├── stores/ # только локальный UI-state
├── providers/ # UI scope provider
├── types/ # props и UI contracts
├── styles/
├── lib/ # presentation helpers
├── tests/
└── index.ts
```
UI module может строиться на других UI modules и `shared`. Он не импортирует business, infra или compositions, не получает product data самостоятельно и не выбирает источник.
## Shared
`shared` содержит только детерминированный фундамент без знания о продукте и runtime-state.
```text
shared/
├── lib/
│ └── {utility}/
│ ├── {utility}.ts
│ ├── {utility}.test.ts
│ └── index.ts
├── types/
├── styles/
├── config/ # только product-agnostic constants
├── assets/ # product-agnostic assets при локальном соглашении
└── sprites/ # специализированная группа assets, если используется
```
Shared не содержит:
- product/domain types;
- stores и mutable singletons;
- SDK/client wrappers;
- React providers;
- environment-dependent services;
- imports из других SLM layers.
## Components внутри ui segment
Presentation component родительского module имеет плоскую структуру:
```text
{module}/ui/{component}/
├── {component}.tsx
├── types/
│ └── {component}-props.type.ts
├── styles/
│ └── {component}.module.css
└── index.ts
```
В папке component запрещены:
- `hooks/`, `stores/`, `services/`, `providers/`, `parts/`;
- source calls и scenario hooks;
- imports project code вне parent module, кроме разрешённых UI modules;
- nested components как отдельные architectural folders.
Если это требуется, сущность становится module и перемещается в `parts/` либо на общий composition/UI уровень.
## Nested modules в parts
Каждый элемент `parts/` является полноценным module:
```text
{parent}/parts/{part}/
├── {part}.tsx # visual root, если нужен
├── {part}.ts # non-visual root, если нужен
├── ui/
├── parts/
├── hooks/
├── stores/ # только state ответственности part
├── types/
├── styles/
├── tests/
└── index.ts
```
Одиночные `.tsx`, `.ts` или style files непосредственно в `parts/` запрещены. Если nested module нужен за пределами parent, подними его в минимальный общий scope.
## Segment matrix
| Segment | Consumer composition | Business integration | Business | Infra | UI | Shared |
|---|---|---|---|---|---|---|
| `ui/` | Да | Нет | Нет | Условно, technical UI | Да | Нет |
| `parts/` | Да | Нет | Нет | Условно | Да | Нет |
| `providers/` | Да | Нет | Нет | Да | Условно | Нет |
| `guards/` | Да, над готовым DomainApi | Нет | Нет | Нет | Нет | Нет |
| `hooks/` | Да | Нет | Только wrappers над deps | Technical hooks | Presentation hooks | Нет |
| `stores/` | Только UI-state | Нет, используй `adapters/` | Нет concrete stores | Technical state | Только UI-state | Нет |
| `services/` | Orchestration готовых API | Нет, используй `adapters/` | Domain scenarios над deps | Technical services | Presentation-only | Нет |
| `adapters/` | Нет product adapters | Да | Нет | Только transport adapters по локальному соглашению | Нет | Нет |
| `mappers/` | Domain -> ViewModel | Нет, transport adaptation остаётся в adapter | `unknown` -> domain | Transport mapping | View mapping | Чистые generic transforms |
| `errors/` | UI/composition errors | Нет domain errors | Domain errors | Technical/transport errors | UI errors | Только generic errors |
| `types/` | Module contracts | Integration input types | Domain contracts | Technical contracts/DTO | Props/UI contracts | Product-agnostic types |
| `styles/` | Да | Нет | Нет | Условно | Да | Global/foundation styles |
| `lib/` | Local helpers | Assembly helpers | Domain deterministic helpers | Technical helpers | Presentation helpers | Generic deterministic helpers |
| `config/` | Composition constants | Runtime assembly config | Domain constants | Technical config/env | UI constants | Product-agnostic constants |
| `tests/` | Scope/integration tests | Обязательные assembly tests | Обязательные factory tests | Technical tests | UI tests | Unit tests |
## Имена обычных файлов
| Pattern | Назначение |
|---|---|
| `{name}.type.ts` | Module-owned type |
| `{name}-props.type.ts` | Component props |
| `{name}.hook.ts`, `use-{name}.hook.ts` | Hook владельца |
| `{name}.store.ts` | Concrete store только допустимого owner/scope |
| `{name}.service.ts` | Scenario или technical service по layer |
| `{name}.adapter.ts` | Adapter с явно определённым видом |
| `map-{name}.ts`, `normalize-{name}.ts` | Mapper/normalizer владельца |
| `{name}.provider.tsx` | Provider владельца scope |
| `{name}.guard.tsx` | Route/UI guard consumer composition |
| `{name}.context.ts`, `{name}.context.tsx` | Private context implementation |
| `{name}.error.ts` | Error соответствующего layer |
| `{name}.config.ts` | Configuration владельца |
| `{name}.constant.ts` | Константа владельца |
| `{name}.module.css` | Styles конкретного module/component |
| `{name}.test.ts`, `{name}.test.tsx` | Colocated test |
Suffix описывает техническую форму, но не переносит ownership. `user.type.ts` не становится shared только потому, что это type; `auth.store.ts` не становится infra только потому, что использует Zustand.
## Public API по типам modules
| Module | Runtime exports | Type exports | Не экспортировать |
|---|---|---|---|
| Consumer composition | Entry/provider/access hooks | Props, state/view types | Raw context/store factory/internal parts |
| Business integration | `create{Domain}Business` | Cross-domain/request input types | Adapters, clients, mocks |
| Business | Только `{domainName}Factory` | Api, Deps, Factory, domain/error types | Services, hooks, mappers, error class |
| Infra | Минимальный technical API | Technical contracts | Mutable internals и generated tree без необходимости |
| UI | Root component и доказанные UI helpers | Props/UI types | Internal components/store/context |
| Nested module | Root entry | Props/module types | Parent internals |
Если дочерние layout/screen/widget импортируют access hooks владельца scope, не экспортируй из того же public API готовый entry, который импортирует эти дочерние modules. Раздели scope API и ready entry на отдельные composition modules, чтобы не создать runtime-цикл.
## Tests map
| Проверяемая граница | Размещение | Обязательность |
|---|---|---|
| Business public contract | `business/{domainPath}/tests/{domainName}-factory/` | Обязательно |
| Business mapper/normalizer/type guard/domain error | Рядом с файлом | Обязательно для каждого такого файла |
| Business service/hook wrapper | Рядом с файлом | При самостоятельной branching/race/runtime-safe логике |
| Adapter и builder wiring | `compositions/business/{domainPath}/*.test.ts` | Обязательно для каждого business-модуля |
| Graph lifecycle/provider | Tests graph owner module | При state/subscriptions/resources |
| Consumer composition | Рядом или `tests/` module | По поведению scope |
| Infra transport/service | Внутри infra module | По technical contract |
| UI module/component | Внутри UI module | По интерактивному поведению |
## Запрещённые структуры
```text
business/auth/ui/ # React UI внутри business
business/auth/stores/auth.store.ts # concrete Zustand/Redux store
business/auth/adapters/backend.adapter.ts # concrete runtime adapter
business/auth/types/sdk-response.type.ts # generated/external DTO contract
compositions/pages/profile/services/api.ts # прямой product source
compositions/screens/profile/store.ts # product/domain cache в screen
infra/business/ # product graph/provider в infra
shared/user.type.ts # product domain type в shared
business/app/index.ts # group с public API
compositions/pages/index.ts # group с public API
{module}/parts/hero.tsx # файл вместо nested module
{module}/ui/card/hooks/ # component с собственной логикой
```
## Новый тип файла или segment
Если нужной роли нет в атласе:
1. Назови ответственность файла без технического suffix.
2. Определи module-владельца и допустимые зависимости.
3. Проверь, не является ли файл существующим `service`, `adapter`, `mapper`, `provider` или nested module.
4. Создай новый segment только для нескольких файлов с одной устойчивой ролью.
5. Не создавай global segment на уровне `src/`.
6. Зафиксируй локальное соглашение, если pattern будет повторяться.
7. Обнови template, если новый pattern стал обязательной повторяемой структурой.
Не подгоняй ответственность под красивое дерево. Минимальный корректный module лучше полного scaffold без реального поведения.

View File

@@ -1,148 +0,0 @@
---
title: SLM Design
description: Назначение архитектуры, ключевые принципы и карта разделов документации
---
# Основы SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
## Рабочий алгоритм
1. Заполни архитектурную карточку из [процесса принятия решения](./decision-process.md): роль, владелец, данные, runtime-capabilities, место, public API, lifecycle и проверки.
2. Сначала выбери слой ответственности, затем module scope, затем segments и только после этого конкретные файлы.
3. Для product data и domain state примени [runtime-границу business](./business-runtime-boundary.md).
4. Выбирай минимальное корректное место и не создавай общий provider, store, package или business-контракт «на будущее».
5. После реализации пройди [архитектурную проверку](./validation.md). Задача не завершена без обязательных business и assembly tests.
## Дополнительные примеры
Каноны ниже достаточны для базового архитектурного решения. Если нужен подробный пример реализации, открой конкретный файл:
- [Композиция через Provider](../examples/react/composition-provider.md) — page-level provider, store и business composition.
- [Структуры compositions](../examples/react/composition-structures.md) — допустимые структуры слоя `compositions`.
- [Business composition](../examples/business-composition.md) — runtime-сборка business-фабрик в `compositions/business/{domain}`.
- [Тестирование business-модулей](../examples/business-testing.md) — factory-level тесты и тесты сборки.
## Разделы спецификации
Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше:
- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя.
- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента.
- [Атлас файлов SLM](./file-atlas.md) — root files, segments, структуры всех типов modules, public API и tests.
- [Business-фабрика](./business-factory.md) — контракт business-модуля, logic API, deps, доменные ошибки и сборка фабрик.
- [Runtime-граница business](./business-runtime-boundary.md) — capabilities фабрики, adapters, hooks, stores и безусловные domain errors.
- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов.
- [Монорепозитории](./monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business/compositions.
- [Архитектурная проверка](./validation.md) — блокирующие gates создания, рефакторинга и ревью.
Рекомендуемый порядок чтения: процесс решения → runtime-граница business → архитектурная проверка → атлас или подробности только нужной ветки.
## Преимущества
### Единый слой композиции
Страницы, маршруты и крупные продуктовые части интерфейса собираются в `compositions`. Слой не навязывает жёсткую структуру: команда может использовать `pages/layouts/screens/widgets` или другую организацию под свой фреймворк и продукт.
### Вертикальная организация домена
Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы, hooks, services и mappers живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: доменная логика локализована.
### Dependency Injection без фреймворков
Runtime-зависимости business-модуля реализуются через фабрики — модуль декларирует что ему нужно, а composition-сборка предоставляет зависимости. Домены изолированы от SDK, storage и backend-клиентов без DI-контейнеров и шин событий.
### Разделение ответственности без перегрузки слоёв
Композиция приложения (`compositions/`), сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — разные слои с разной природой. Ни один слой не превращается в свалку разнородного кода.
### Графовая композиция там, где она нужна
Внутри `compositions` допускается граф импортов через публичный API. Это позволяет page-level store, provider или сборку business-фабрик использовать одновременно в layout, screen и widget, не перенося продуктовый runtime-state в `infra` или `shared`.
### Горизонтальная инкапсуляция
Вложенные модули (`parts/`) и публичные API позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга.
### Колокация по умолчанию
Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями.
### Масштабирование через группировку
При росте проекта слои не теряют структуру — модули группируются по естественным признакам: композиции по страницам и маршрутам, бизнес-домены по субдоменам, UI-компоненты по уровню абстракции.
### Адаптация к монорепозиториям
SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. `compositions` и бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы.
## Происхождение
SLM Design вырос на основе:
- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей
- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое
- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию
- **Colocation Principle** — код живёт рядом с местом использования
## Пример структуры проекта
```text
src/
├── app/
├── compositions/
│ ├── business/
│ │ ├── auth/
│ │ └── user/
│ ├── pages/
│ │ ├── home/
│ │ ├── profile/
│ │ └── product-detail/
│ ├── layouts/
│ │ ├── main/
│ │ └── dashboard/
│ ├── screens/
│ │ ├── home/
│ │ └── profile/
│ └── widgets/
│ ├── page-heading/
│ └── promo-banner/
├── business/
│ ├── auth/
│ ├── catalog/
│ ├── orders/
│ └── chat/
├── infra/
│ ├── theme/
│ ├── i18n/
│ ├── backend-api/
│ └── logger/
├── ui/
│ ├── button/
│ ├── input/
│ ├── modal/
│ ├── toast/
│ └── dropdown/
└── shared/
├── lib/
├── types/
└── styles/
```
## Принципы
- **Композиция — отдельный слой.** Страницы, маршруты и крупные продуктовые части интерфейса собираются в `compositions`.
- **Структура композиции свободна.** Команда сама выбирает организацию внутри `compositions`; базовая рекомендация — `pages/layouts/screens/widgets`.
- **Домен — единое целое.** Доменная модель, сценарии, типы, services и mappers живут в одном business-модуле. Concrete data/state/query hooks передаются фабрике через adapters.
- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости.
- **Зависимости однонаправлены за пределами compositions.** `app` подключает `compositions`; `compositions` связывает `business`, `infra`, `ui` и `shared`; `business` вызывает concrete runtime-возможности только через переданные фабрике `deps`.
- **Product data проходит через business.** Page, layout, screen и widget не обращаются к product source напрямую.
- **Ошибки принадлежат домену.** Из business API выходят только собственные domain errors со стабильным `code`.
- **Внутри compositions допустим граф.** Composition modules могут импортировать друг друга через public API.
- **Архитектура — каркас, не клетка.** Правила фиксируют границы ответственности и public API, а внутреннюю форму композиции определяет команда.

View File

@@ -1,285 +0,0 @@
---
title: Слои
description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя
---
# Слои
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом.
## Определение
**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.**
## Группы слоёв
Слои делятся на три группы:
| Группа | Слои | Описание |
|--------|------|----------|
| Композиция | `app`, `compositions` | Подключают приложение к фреймворку и собирают страницы, маршруты и крупные продуктовые части интерфейса |
| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит |
| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги |
## Направление зависимостей
Любой импорт между модулями — только через публичный API.
```text
app → compositions
compositions → business | infra | ui | shared
business → shared
infra → infra | shared
ui → ui | shared
shared -/→ SLM-слои
```
- `app` подключает приложение к фреймворку и импортирует готовые composition modules
- `compositions` импортирует `business`, `infra`, `ui`, `shared`
- `business` импортирует только собственные файлы, детерминированный `shared`, чистые библиотеки и type-only контракты других business-модулей
- `infra` импортирует `infra` и `shared`
- `ui` импортирует `ui` и `shared`
- `shared` не импортирует другие SLM-слои
- `business`, `infra`, `ui`, `shared` не импортируют `compositions`
- Внутри `compositions` направление импортов между composition modules не фиксируется, но импорты разрешены только через публичный API и не должны создавать runtime-циклы
- Модули `business` вызывают любую runtime-capability только через `deps` фабрики; source/query hooks, stores, events и platform APIs также являются dependencies
- `import type` не разрешает переносить ownership: business не импортирует generated DTO, SDK/client/store/query types, а infra не объявляет product graph через type-only business imports
- Pure deterministic third-party libraries допустимы внутри business, если не имеют I/O/state/hidden environment и не протекают в public contract
## Слой App
Точка входа приложения. Отвечает за запуск, роутинг и подключение composition modules к фреймворку.
В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации.
### Требования
- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация
- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов
- Провайдеры, guards, layouts, screens и страницы — только подключает готовые из `compositions`, не реализует
- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы
- Никем не импортируется
## Слой Compositions
`compositions/` — слой сборки страниц, маршрутов и крупных продуктовых частей интерфейса.
На этом слое собираются page, layout, screen, widget и другие composition modules. Они связываются между собой и с нижними слоями: `business`, `infra`, `ui`, `shared`.
SLM не фиксирует жёсткую структуру внутри `compositions`. Команда выбирает организацию под фреймворк, роутинг, CMS и продуктовую задачу.
Базовая рекомендация:
```text
src/compositions/
├── business/
├── pages/
├── layouts/
├── screens/
└── widgets/
```
`business`, `pages`, `layouts`, `screens` и `widgets` внутри `compositions` не являются отдельными SLM-слоями. Это группы composition modules.
`compositions/business/{domain}` используется для runtime-сборки business-фабрик с реальными зависимостями приложения. Это не business-слой, а composition module, который адаптирует `infra`, SDK, storage и browser API к `deps` business-фабрики.
Внутри `compositions` различай три роли:
| Роль | Ответственность |
|---|---|
| Integration module | `compositions/business/{domain}` реализует adapters и собирает одну business-фабрику |
| Graph owner | Page/route/provider/request scope собирает готовые business API и управляет lifecycle |
| Consumer composition | Page/layout/screen/widget вызывает `{Domain}Api` и не знает concrete product source |
Consumer composition получает продуктовые данные только через business API. Прямые SDK/HTTP/generated/storage вызовы разрешены только dependency adapters интеграционного модуля домена.
Технический infra-сервис без product data можно использовать в composition напрямую. Если такой сервис нужен business, он всё равно описывается business-owned capability и передаётся фабрике через adapter.
Если business-домены сгруппированы, `compositions/business` повторяет ту же группировку: `business/app/auth` соответствует `compositions/business/app/auth`, `business/cms/content` соответствует `compositions/business/cms/content`. Это не default-структура, а способ сохранить навигацию в крупных проектах.
Composition module может содержать обычные сегменты SLM: `ui/`, `parts/`, `hooks/`, `stores/`, `services/`, `mappers/`, `types/`, `styles/`, `lib/`, `config/`, `providers/`.
Page-level store, provider, guard или business composition размещаются внутри page composition module, если они нужны всей странице.
```text
compositions/pages/profile/
├── profile.page.tsx
├── profile-business-composition.ts
├── providers/
├── hooks/
├── stores/
├── types/
└── index.ts
```
Layout, screen и widget могут получать через public API page composition локальный UI-state и готовые `{Domain}Api`. Product data не копируется в page store как параллельный источник истины.
```ts
import { useProfilePageStore } from '@/compositions/pages/profile'
```
Внутри `compositions` направление импортов между composition modules не фиксируется. Допустим граф, но все импорты идут только через public API.
```ts
// Хорошо
import { useProfilePageStore } from '@/compositions/pages/profile'
// Плохо
import { useProfilePageStore } from '@/compositions/pages/profile/hooks/use-profile-page-store.hook'
```
### Требования
- `compositions` содержит composition modules страниц, маршрутов и крупных продуктовых частей интерфейса
- Структура внутри `compositions` выбирается командой
- Базовая рекомендация: `business/`, `pages/`, `layouts/`, `screens/`, `widgets/`
- `business`, `pages`, `layouts`, `screens`, `widgets` внутри `compositions` являются группами composition modules
- `compositions/business/{domain}` собирает конкретную business-фабрику с runtime-зависимостями; при группировке используется зеркальный путь `compositions/business/{group}/{domain}`
- Concrete product sources, source/query hooks, domain stores, browser APIs и events подключаются только adapters интеграционного module
- Page/layout/screen/widget используют product data только через `{Domain}Api`
- Builder одной фабрики явно создаёт или получает scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters factory; integration logic не пишется inline
- Providers, stores, guards и business composition размещаются внутри того composition module, которому они принадлежат
- Внутри `compositions` импорты между composition modules разрешены в любую сторону, но только через public API
- Runtime-циклы между composition modules запрещены
- Deep imports внутрь composition modules запрещены
- `business`, `infra`, `ui` и `shared` не импортируют `compositions`
## Слой Business
Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, hooks, services, mappers и доменной логикой.
Слой входит в группу «Ядро». Импортирует собственные файлы, детерминированный `shared/`, чистые библиотеки и type-only контракты других business-модулей. Каждый бизнес-модуль создаёт публичный API фабрики в корне. Любые runtime-capabilities передаются через аргументы фабрики.
Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель и dependency contracts, `hooks/` и `services/` — wrappers над переданными capabilities, `mappers/` — доменная нормализация, `lib/` — детерминированные доменные утилиты.
Business-модуль не содержит React-компоненты, layouts, guards, providers и page-level wrappers. Визуальный fallback, route boundary и привязка logic API к React tree размещаются в `compositions`; error mapping и допустимый доменный fallback остаются в business.
```text
src/business/
├── auth/
├── catalog/
├── orders/
├── checkout/
└── chat/
```
Когда количество доменов затрудняет навигацию — можно ввести группировку по крупным предметным областям. Группа — папка для организации, не модуль и не public API.
```text
src/business/
├── app/
│ ├── auth/
│ ├── profile/
│ └── orders/
└── cms/
├── content/
├── media/
└── navigation/
```
`app` и `cms` здесь являются группами доменов. Модулями остаются конечные папки: `auth`, `profile`, `content`, `media`, `navigation`.
### Требования
- Один модуль = один бизнес-домен
- Business-домены можно группировать при необходимости; группа не является модулем и не содержит `index.ts`
- Циклические зависимости между доменами запрещены
- Публичный API фабрики — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты, без исключений
- Фабрика возвращает только logic API: hooks, selectors, command/query methods, scenario services
- Business-модуль не содержит React-компоненты и не возвращает компоненты из фабрики
- Любые runtime-capabilities — только через `deps` фабрики: product sources, hooks, stores, events, technical services, env и browser APIs
- Business не импортирует React/SWR/query/store runtime; concrete hooks и stores реализуются adapters
- Реальные SDK, API-клиенты, storage, state/query runtime, env и browser API подключаются в `compositions/business/{domain}` или зеркальном пути при группировке
- Business нормализует внешние результаты и подставляет только собственные domain errors
- Доменные типы (`User`, `Product`) живут здесь, не в `shared/`
## Слой infra
Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль.
Слой входит в группу «Ядро». Импортирует `infra/` и `shared/`.
Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
```text
src/infra/
├── theme/
├── i18n/
├── backend-api/
├── maps-api/
├── logger/
├── feature-flags/
└── realtime/
```
### Требования
- Один модуль = один техсервис
- Импортирует `infra/` и `shared/`
- Не содержит продуктовые composition modules конкретных страниц или маршрутов
## Слой UI
UI-кит без бизнес-логики: button, carousel, toast, modal.
Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`.
Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`.
```text
src/ui/
├── button/
├── input/
├── icon/
├── carousel/
├── modal/
├── toast/
├── dropdown/
├── tabs/
└── tooltip/
```
Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах.
```text
src/ui/
├── primitives/
│ ├── button/
│ ├── input/
│ ├── icon/
│ └── badge/
└── composites/
├── carousel/
├── modal/
├── dropdown/
├── tabs/
└── tooltip/
```
### Требования
- Не содержит бизнес-логику
- Импортирует только `ui/` и `shared/`
## Слой Shared
Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене.
Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует.
Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги).
Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь.
```text
src/shared/
├── lib/
├── types/
├── styles/
└── sprites/
```
### Требования
- Не имеет runtime-состояния
- Не знает о продуктовых composition modules

View File

@@ -1,300 +0,0 @@
---
title: Модули
description: Структура модуля, типы (композиционный, UI, бизнес, инфра), публичный API, отличие модуля от компонента
---
# Модули
Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.
## Определение
**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.**
Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.
Модуль не обязан быть UI-блоком. Это может быть page composition, layout composition, screen composition, widget composition, бизнес-домен, инфраструктурный сервис или UI-kit сущность.
Главная граница модуля — не папка, а ответственность.
## Компонент
**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.**
Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.
> Компонент отображает. Модуль организует.
Компонент не может:
- Импортировать код проекта за пределами родительского модуля. Единственное исключение — компоненты слоя `ui`, если правила слоёв разрешают родительскому модулю импортировать `ui`.
- Владеть архитектурными зависимостями.
- Содержать вложенные компоненты: папка компонента включает только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
- Содержать вложенные модули.
- Делать внешние запросы.
- Самостоятельно получать данные.
- Выбирать источник данных.
- Композировать данные.
- Вызывать сценарные хуки.
- Оркестрировать сценарий.
- Композировать модули.
- Решать, как устроен процесс.
- Содержать бизнес-логику.
- Содержать сценарную логику.
Компонент может рендерить другие компоненты: соседние компоненты из `ui/` своего модуля, компоненты слоя `ui` и элементы, переданные через props. Запрет «содержать» относится к структуре папки, а не к JSX-разметке: декомпозиция компонентов остаётся плоской внутри `ui/` родительского модуля.
Если компоненту требуется что-то из запрещённого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.
```text
auth/
├── ui/
│ └── logout-button/
│ ├── logout-button.tsx
│ ├── styles/
│ │ └── logout-button.module.css
│ ├── types/
│ │ └── logout-button-props.type.ts
│ └── index.ts
└── index.ts
```
## Что считается модулем
Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.
## Группы модулей
Слой может содержать не только модули, но и группы модулей. По умолчанию модуль лежит прямо в слое; группу вводят только когда модулей много и нужна явная классификация.
Группа — навигационная папка внутри слоя или другой группы. Она классифицирует модули по типу, предметной области, продуктовой зоне или runtime-назначению, но сама не является модулем.
Жёсткие правила группы:
- группа не имеет public API;
- группа не содержит `index.ts`;
- группа не импортируется внешним кодом;
- группа не владеет логикой, состоянием, deps или runtime-сборкой;
- группа может содержать другие группы и конечные модули.
Модулем считается конечная папка с самостоятельной ответственностью и публичной границей.
```text
src/business/
├── app/ # группа
│ ├── auth/ # business-модуль
│ └── profile/ # business-модуль
└── cms/ # группа
├── content/ # business-модуль
└── media/ # business-модуль
```
Примеры модулей:
- `compositions/pages/home/` — модуль page composition.
- `compositions/layouts/main/` — модуль layout composition.
- `compositions/screens/profile/` — модуль screen composition.
- `compositions/widgets/page-heading/` — модуль widget composition.
- `business/auth/` — модуль бизнес-домена.
- `infra/theme/` — модуль инфраструктурного сервиса.
- `ui/button/` — модуль UI-kit сущности.
- `compositions/pages/home/parts/hero-section/` — вложенный модуль page composition.
Не считаются модулями:
- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/`, `providers/` — это сегменты.
- `compositions/pages/`, `business/app/`, `business/cms/` — это группы, если в них нет `index.ts`.
- `compositions/pages/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента.
## Типы модулей
Тип модуля определяет обязательный корневой файл и стартовую структуру.
### Композиционный модуль
Композиционный модуль — модуль внутри `compositions`, который участвует в сборке страниц, маршрутов и крупных продуктовых частей интерфейса.
Он может быть page, layout, screen, widget, block, entry-point, CMS-entry, route segment или другим типом композиции, выбранным командой.
```text
compositions/pages/profile/
├── profile.page.tsx
├── profile-business-composition.ts
├── providers/
├── hooks/
├── stores/
├── parts/
├── types/
└── index.ts
```
Композиционный модуль может импортировать другие composition modules через public API. Это отличие слоя `compositions`: внутри него допускается графовая композиция.
При этом deep imports запрещены.
```ts
// Хорошо
import { useProfilePageStore } from '@/compositions/pages/profile'
// Плохо
import { useProfilePageStore } from '@/compositions/pages/profile/hooks/use-profile-page-store.hook'
```
### UI-модуль
Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне:
```text
button/
├── button.tsx
└── index.ts
```
`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу.
### Бизнес-модуль
Бизнес-модуль — модуль, который строится вокруг публичного API фабрики.
Business-модуль содержит доменную логику, типы, hooks, services, mappers и helpers. Он не содержит React-компоненты и не возвращает компоненты из фабрики.
Бизнес-модуль обязан иметь фабрику в корне:
```text
auth/
├── auth.factory.ts
├── index.ts
└── types/
```
Фабрика возвращает публичный API модуля для использования в runtime.
### Инфраструктурный модуль
Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.
Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.
```text
theme/
├── index.ts
├── config/
├── hooks/
├── styles/
└── ui/
```
```text
backend-api/
├── backend-api.client.ts
├── config/
├── types/
└── index.ts
```
## Структура
Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.
```text
{module-name}/
├── {module-name}.factory.ts # фабрика (для business-модулей)
├── {module-name}.tsx # корневой файл модуля (опционален)
├── ui/ # компоненты модуля, кроме business-модулей
├── parts/ # вложенные модули
├── providers/ # провайдеры модуля
├── hooks/ # хуки
├── stores/ # сторы состояния
├── services/ # сценарии и операции модуля
├── mappers/ # трансформация на границе ответственности
├── types/ # типы
├── styles/ # стили
├── lib/ # утилиты модуля
├── config/ # константы и конфигурация
└── index.ts # публичный API
```
Подробное описание сегментов — в разделе [Сегменты](./segments.md).
## Публичный API
Внешний код импортирует модуль только через публичный API.
```ts
// Хорошо
import { customerFactory } from '@/business/customer'
import type { Customer } from '@/business/customer'
```
```ts
// Плохо
import { validateToken } from '@/business/auth/lib/tokens'
```
`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.
Внутренние сегменты модуля остаются деталями реализации.
Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Это жёсткое правило без исключений.
```ts
// business/customer/index.ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerDeps } from './types/customer-deps.type'
export type { CustomerFactory } from './types/customer-factory.type'
```
Composition module экспортирует через `index.ts` только безопасный контракт, который нужен другим composition modules или `app`: page/layout/screen/widget, provider, hooks доступа, типы. Внутренние stores, context objects и функции создания состояния не экспортируются без необходимости.
Stateful module по умолчанию не экспортирует raw context, `StoreApi`, mutable singleton, persistence key или concrete adapter. Экспортируй domain/technical commands, selectors и access hooks. Каждый mutable export требует реального внешнего consumer и отдельного обоснования.
Если layout, screen или widget импортируют hooks из page composition, не смешивайте в одном public API готовую page composition и hooks для дочерних модулей: это может создать runtime-цикл.
```ts
// compositions/pages/profile/index.ts
export { ProfilePageProvider } from './providers/profile-page.provider'
export { useProfilePageStore } from './hooks/use-profile-page-store.hook'
export { useProfileBusinessComposition } from './hooks/use-profile-business-composition.hook'
export type { ProfilePageState } from './types/profile-page-state.type'
```
## Фабрика
Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный logic API фабрики.
Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.
Фабрика не возвращает React-компоненты, layouts, guards, boundaries, providers или page-level wrappers. Business-модуль не содержит React-компоненты: UI-решения домена размещаются в `compositions`, а полностью универсальные UI-компоненты — в `ui`.
Модуль без runtime-capabilities экспортирует фабрику без аргументов. Модуль с зависимостями экспортирует фабрику, принимающую `deps`: API других доменов и внешние возможности, описанные бизнес-языком. Source/query hooks, state stores, subscriptions, browser API, clock и другие runtime-механизмы также считаются dependencies. Типы всегда экспортируются напрямую через `export type`, но `import type` не разрешает протащить в business generated DTO, SDK/store/query contracts.
Runtime-сборка фабрики с реальными SDK, storage, infra-клиентами, source hooks, stores, events и browser API происходит в `compositions/business/{domain}` через отдельные adapters. Builder явно создаёт или получает scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters factory. Конечный граф business API собирается в месте, которое владеет lifecycle: page composition, route composition, application-lifetime composition provider, request scope или test setup.
### Примеры
Подробные правила фабрики см. в [Business-фабрика](./business-factory.md).
Пример runtime-сборки business-фабрик см. в [Business composition](../examples/business-composition.md).
Пример page-level Provider в React см. в [Композиция через Provider](../examples/react/composition-provider.md).
Примеры разных структур слоя `compositions` см. в [Структуры compositions](../examples/react/composition-structures.md).
## Жизненный цикл
Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.
- Нужен одной странице, route branch или крупной продуктовой части интерфейса → внутри соответствующего composition module.
- Нужен нескольким частям одной страницы → внутри page composition или другого общего composition scope.
- Нужен нескольким страницам или маршрутам → отдельный composition module внутри `compositions`.
- Абстрактный UI без бизнес-логики → `ui/`.
- Сценарий, product data contract, domain state, тип или доменная логика → `business/{domain}/`.
- Concrete implementation business dependency → adapter в `compositions/business/{domain}/`.
- Технический сервис → `infra/`.
- Общая чистая утилита → `shared/`.
Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.

View File

@@ -1,229 +0,0 @@
---
title: Монорепозитории
description: Правила применения SLM Design для frontend-проектов, находящихся в монорепозитории
---
# Монорепозитории
Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов.
## Определение
**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.**
## Базовая структура
Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`.
```text
repo/
├── apps/
│ ├── web/
│ │ └── src/
│ │ ├── app/
│ │ ├── compositions/
│ │ ├── business/
│ │ ├── infra/
│ │ ├── ui/
│ │ └── shared/
│ └── admin/
│ └── src/
│ └── ...
└── packages/
├── ui/
│ ├── button/ # самостоятельный пакет UI-модуля
│ ├── input/ # самостоятельный пакет UI-модуля
│ └── modal/ # самостоятельный пакет UI-модуля
├── infra/
│ ├── theme/ # самостоятельный пакет infra-модуля
│ ├── backend-api/ # самостоятельный пакет infra-модуля
│ └── logger/ # самостоятельный пакет infra-модуля
└── shared/ # единый shared-пакет
├── package.json
└── src/
├── lib/ # переиспользуемые утилиты
├── helpers/ # переиспользуемые helpers
└── index.ts
```
`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои.
## Группировка frontend-пакетов
Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`.
```text
packages/ui/* # пакеты UI-модулей
packages/infra/* # пакеты infra-модулей
packages/shared # единый shared-пакет
```
Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM.
## Пакет и модуль
Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации.
В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению.
```text
packages/ui/button/
packages/ui/modal/
packages/infra/theme/
packages/infra/backend-api/
packages/shared/
```
## Что остаётся в приложении
Слои `app`, `compositions` и `business` остаются внутри конкретного приложения.
```text
apps/web/src/app/
apps/web/src/compositions/
apps/web/src/business/
```
`app` привязан к фреймворку и entry points приложения.
`compositions` привязан к страницам, маршрутам и крупным продуктовым частям интерфейса конкретного приложения. Этот слой не выносится в `packages/*`, потому что отражает продуктовую сборку приложения.
`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой.
## Что можно выносить
В пакеты выносится только код из `ui`, `infra` и `shared`, который уже нужен двум фронтенд-приложениям либо имеет явно зафиксированный межприложенческий ownership/reuse-контракт.
| Группа | Что выносить | Пример |
|--------|--------------|--------|
| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` |
| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` |
| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` |
По умолчанию начинай внутри приложения. Пакет можно создать до второго consumer только при явном архитектурном решении, известном consumer и стабильном межприложенческом контракте. Абстрактная «общая природа» сама по себе недостаточна.
## UI-пакеты
В `packages/ui/*` размещаются переиспользуемые UI-модули.
```text
packages/ui/button/
├── package.json
└── src/
├── button.tsx
├── styles/
├── types/
└── index.ts
```
UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц.
## Infra-пакеты
В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули.
```text
packages/infra/backend-api/
├── package.json
└── src/
├── clients/
├── config/
├── types/
└── index.ts
```
Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок.
## Shared-пакет
`packages/shared` является единым пакетом.
```text
packages/shared/
├── package.json
└── src/
├── lib/
├── helpers/
└── index.ts
```
В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте.
Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся.
## Имена пакетов и импорты
Путь импорта задаётся `name` в `package.json`, а не расположением директории.
```json
{
"name": "@repo/theme"
}
```
```text
packages/infra/theme/package.json
```
```ts
import { ThemeProvider } from '@repo/theme'
```
Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены.
```ts
// Хорошо
import { Button } from '@repo/button'
// Плохо
import { Button } from '@repo/button/src/button'
```
## Зависимости
На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений.
```text
apps → packages
packages -/→ apps
```
Внутри приложения продолжает действовать обычное направление зависимостей SLM: `app` подключает `compositions`, `compositions` связывает `business`, `infra`, `ui` и `shared`, а `business` вызывает concrete runtime-capabilities только через переданные фабрике `deps`.
Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях.
## Когда не выносить
Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API.
Фактическое использование в одном приложении допустимо для package только при зафиксированном втором consumer или внешнем ownership/reuse-контракте. Иначе сохрани локальную колокацию.
```text
# Плохо
apps/web/src/compositions/pages/home/parts/promo-section/
packages/ui/promo-section/
```
Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным composition module.
## Конфигурационные пакеты
Конфигурационные пакеты не относятся к SLM-архитектуре.
Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`.
## Правила
- SLM применяется внутри каждого `apps/{app}/src`.
- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`.
- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями.
- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей.
- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей.
- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers.
- Модуль можно размещать в package при реальном втором consumer или явном межприложенческом ownership/reuse-контракте.
- `app`, `compositions` и `business` не выносятся в пакеты.
- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`.
- Пакеты не импортируют приложения.
- Межпакетные импорты идут только через публичный API.
- Deep imports внутрь пакетов запрещены.
- Локальная колокация важнее преждевременного выноса в `packages/*`.

View File

@@ -1,222 +0,0 @@
---
title: Сегменты
description: Сегменты внутри модуля (ui/, parts/, hooks/ и др.), назначение и правила размещения файлов
---
# Сегменты
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит.
## Определение
**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.**
## Обзор
| Сегмент | Содержимое |
|---------|------------|
| `ui/` | Презентационные компоненты родительского модуля |
| `parts/` | Вложенные модули со своими сегментами |
| `providers/` | Провайдеры модуля |
| `hooks/` | React-хуки |
| `stores/` | Сторы состояния |
| `services/` | Сценарии и операции владельца module |
| `mappers/` | Трансформация данных между форматами |
| `types/` | TypeScript-типы и интерфейсы |
| `styles/` | Стили |
| `lib/` | Утилиты и хелперы модуля |
| `config/` | Константы и конфигурация |
Сегменты не являются обязательными. Например, `providers/` нужен только модулю, который владеет провайдерами. Если provider, store или guard относится к конкретной странице или маршруту, он размещается внутри соответствующего composition module, а не в `infra` или `shared`.
Business-модули не используют `ui/` для React-компонентов. Доменный logic API живёт в factory/services/hook wrappers/mappers. React tree, guards, layouts и visual fallbacks размещаются в consumer compositions; concrete domain source hooks/stores — в adapters `compositions/business/{domain}`.
## Сегмент ui/
Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля.
Компонент в `ui/`:
- Находится в собственной папке.
- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`.
- Не содержит вложенные компоненты и модули — папка компонента остаётся плоской.
- Может рендерить соседние компоненты из `ui/` своего модуля и компоненты слоя `ui`, если правила слоёв разрешают родительскому модулю импортировать `ui`.
- Не импортирует другой код проекта за пределами родительского модуля.
- Не делает внешние запросы.
- Не вызывает сценарные хуки.
- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные.
- Не содержит бизнес-логику или сценарную логику.
Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент).
Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`.
```text
user/
├── ui/
│ ├── user-avatar/
│ │ ├── user-avatar.tsx
│ │ ├── styles/
│ │ │ └── user-avatar.module.css
│ │ ├── types/
│ │ │ └── user-avatar-props.type.ts
│ │ └── index.ts
│ └── user-status/
│ ├── user-status.tsx
│ └── index.ts
├── types/
├── hooks/
├── user.tsx
└── index.ts
```
Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`.
## Сегмент parts/
Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются.
```text
compositions/pages/home/
├── parts/
│ ├── hero-section/
│ │ ├── hero-section.tsx
│ │ ├── styles/
│ │ ├── parts/
│ │ │ └── top-banner/
│ │ │ ├── top-banner.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ └── features-section/
│ ├── features-section.tsx
│ ├── hooks/
│ └── index.ts
├── home.page.tsx
└── index.ts
```
Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности.
Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке.
Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше.
## Сегмент providers/
Провайдеры модуля: React Context providers, провайдеры scope-состояния, провайдеры композиции фабрик или другие обёртки, которые принадлежат модулю.
```text
providers/
├── profile-page.provider.tsx
└── profile-business-composition.provider.tsx
```
Provider размещается в том модуле, который владеет соответствующим состоянием или композицией. Page-level provider живёт в page composition module; application-level provider, завязанный на фреймворк, подключается в `app`, но реализуется в нижнем подходящем слое.
## Сегмент hooks/
React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты.
```text
hooks/
├── use-auth.hook.ts
├── use-session.hook.ts
└── use-permissions.hook.ts
```
В business-модуле `hooks/` содержит только wrappers, созданные поверх dependency hooks, переданных фабрике. Business не импортирует React state/effect APIs, SWR, TanStack Query, Apollo или другой hook runtime напрямую.
Concrete source hook реализуется adapter-ом в `compositions/business/{domain}` и возвращает business-owned result type. Business wrapper нормализует данные и заменяет source error собственной domain error.
## Сегмент stores/
Сторы состояния composition/infra/UI module. Конкретная реализация зависит от выбранного state manager (Zustand, MobX, Redux и т.д.).
```text
stores/
├── auth.store.ts
└── session.store.ts
```
Если состояние нужно всей странице, concrete store живёт в page composition module. Если состояние относится к бизнес-домену, business владеет state model, transitions и state port. Concrete Zustand/Redux/MobX adapter factory реализуется в `compositions/business/{domain}`, передаётся через `deps`, принимает initial domain state от business-фабрики и возвращает concrete port.
Для каждого store определи creator, scope, количество instances и cleanup. Module singleton допустим только для явно доказанного application/process lifetime.
## Сегмент services/
Сценарии и операции module. Содержимое зависит от слоя и владельца.
```text
services/
├── auth.service.ts
└── token.service.ts
```
Правила по слоям:
- `business/services` реализует доменные сценарии только поверх `deps` фабрики;
- `compositions/business/{domain}/adapters` реализует concrete product/runtime dependencies, а не `services/` обычной composition;
- composition `services/` может оркестрировать готовые business API и технические infra-сервисы, но не обращаться к product source напрямую;
- `infra/services` реализует технический сервис или transport;
- `ui` и `shared` не выполняют product I/O.
Business service не импортирует SDK, generated API, HTTP-клиент, storage, env, browser API, React/SWR/query runtime или concrete store напрямую.
## Сегмент mappers/
Функции трансформации данных на границе ответственности module.
```text
mappers/
├── map-user.ts
├── map-product.ts
└── map-order-to-dto.ts
```
В business-модулях mappers защищают public contract от ненадёжной runtime-границы: преобразуют `unknown` в доменную модель, отклоняют невалидные структуры и не импортируют concrete DTO SDK.
Dependency adapter может преобразовать доменные аргументы в transport payload, но не создаёт доменную модель из ответа, domain error или business fallback. Domain-to-ViewModel mapping принадлежит потребительской composition, если описывает только представление.
## Сегмент types/
TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов.
```text
types/
├── user.type.ts
└── session.type.ts
```
В business-модулях `types/` содержит собственные доменные типы, `{Domain}Api`, `{Domain}Deps`, `{Domain}Factory`, dependency hook/state result types и доменные error codes. Generated DTO, SDK-типы, `StoreApi`, query-library types и типы HTTP-клиента не входят в контракт business-модуля.
## Сегмент styles/
Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.).
```text
styles/
├── auth.module.css
└── login-form.module.css
```
## Сегмент lib/
Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов.
```text
lib/
├── validate-email.ts
└── format-phone.ts
```
Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`.
## Сегмент config/
Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения.
```text
config/
├── routes.ts
└── constants.ts
```

View File

@@ -1,214 +0,0 @@
---
title: Архитектурная проверка
description: Блокирующие проверки SLM перед завершением создания, рефакторинга и архитектурного ревью
---
# Архитектурная проверка
Не считай задачу завершённой только потому, что код компилируется или UI отображается. Проверь архитектурное решение, runtime-цепочку и обязательные тестовые границы.
## Проверка владельца
- У каждой ответственности один явный владелец.
- Product state и сценарии принадлежат business-домену.
- Page/route UI-state принадлежит соответствующему composition module.
- Concrete technical service принадлежит infra-модулю.
- Concrete business dependency adapter принадлежит `compositions/business/{domain}`.
- Provider находится у владельца scope, а не в generic infra-модуле.
- Код не поднят в общий слой или package без реального consumer.
## Проверка runtime-цепочки
Для каждого `{Domain}Api` проследи цепочку сборки:
```text
graph owner
→ per-domain builder
→ dependency adapters + business factory
→ {Domain}Api
→ provider/entry
```
И отдельно цепочку вызова:
```text
consumer
→ {Domain}Api
→ business scenario
→ {Domain}Deps
→ dependency adapter
→ source/runtime
```
Если отсутствует хотя бы одно необходимое звено, домен не подключён. Тип будущего API, пустой provider или неиспользуемый builder не заменяет runtime-сборку.
Для каждого route/page entry проверь, что он достигает готового composition module. `app` не должен реализовывать screen/layout/product wiring самостоятельно.
## Проверка business imports
В production-коде `business/**` не должно быть прямых runtime или type-only imports из concrete runtimes:
- `infra`, `compositions`, `app`;
- SDK/generated clients;
- SWR/TanStack Query/Apollo;
- Zustand/Redux/MobX/RxJS store runtime;
- React state/effect runtime;
- storage/browser API;
- env/event bus/clock/random implementations.
Разрешены собственные файлы, type-only business contracts, `shared` без runtime-capabilities и чистые детерминированные библиотеки.
Проверь не только import paths, но и re-export/barrel/helper, который может скрывать запрещённую зависимость.
## Проверка deps
- Каждая runtime-capability присутствует в `{Domain}Deps`.
- Контракт принадлежит business и назван доменным языком.
- В `Deps` нет client/SDK/generated operation/StoreApi/query-library types.
- Dependency hook возвращает business-owned source result.
- State dependency использует business-owned state port.
- External result имеет `unknown`, если требует runtime validation.
- Subscription возвращает cleanup.
- Cross-domain API сужен до реально используемых методов.
## Проверка adapters
- Для каждой runtime-capability есть явный adapter.
- Adapter находится в `compositions/business/{domain}`.
- Builder не содержит inline integration logic.
- Adapter не формирует domain error.
- Adapter не выполняет domain normalization.
- Adapter не выбирает business fallback.
- Private adapters отсутствуют в public `index.ts`.
- Concrete transport imports не протекают в обычные consumer compositions.
## Проверка product data
- Page/layout/screen/widget получает product data через `{Domain}Api`.
- Нет прямого вызова SDK/client/generated operation из потребительской composition.
- Нет product storage access в UI/component/composition service.
- DTO не используется как domain/view contract без business normalization.
- Один источник не имеет параллельного прямого и business-пути.
## Проверка ошибок
Для каждого public runtime operation проверь:
- rejected dependency;
- synchronous throw dependency;
- `undefined`/`null`/empty body;
- объект неправильной формы;
- source hook error;
- invalid state/storage value;
- неизвестную runtime-ошибку.
Во всех случаях наружу выходит только domain error со стабильным `code`. Source error сохраняется в `cause`, но не становится consumer contract. Technical failure и malformed response нельзя превращать в fallback; fallback разрешён только для валидного доменного исхода.
Не оставляй формулировку «domain error, если контракт это обещает». Business public contract всегда обещает только domain errors.
## Проверка state и hooks
- Business владеет domain state model, но не concrete state manager.
- Zustand/Redux/MobX store создаётся adapter-ом, не factory.
- SWR/Query hook создаётся adapter-ом, не business-модулем.
- Dependency hook является non-throwing/non-Suspense и передаёт technical error через business-owned result.
- Business wrapper вызывает dependency hook и возвращает собственный result type.
- Business wrapper преобразует error result и ошибки callbacks в domain errors.
- Store/query library types отсутствуют в public API и `Deps`.
- Определены creator, scope, количество instances и cleanup.
- Module singleton используется только при явно доказанном application/process lifetime.
- Provider не создаёт ложное впечатление владения instance, созданным на module scope.
## Проверка graph
- Per-domain builders собирают только свои фабрики.
- Graph owner назван и соответствует lifecycle.
- Домены создаются в топологическом порядке.
- Runtime-циклы отсутствуют.
- Один и тот же graph не копируется по нескольким providers без обоснования scope.
- Screen/widget не собирает graph самостоятельно.
- Provider value имеет точный тип.
- Нет `Partial<Graph>` с unchecked cast к полному graph.
- Subscription, timer, socket и другие resources имеют cleanup/dispose.
- Pending operation не может записать stale state после invalidation/unmount без явно принятой политики.
## Проверка public API
- Межмодульные импорты идут через реальный public entrypoint.
- Import alias/package export существует физически.
- Group не имеет `index.ts`.
- Business `index.ts` экспортирует runtime только factory, остальное через `export type`.
- Integration module экспортирует builder и необходимые type-only integration input contracts.
- Raw context, raw store, mutable singleton, adapter, generated operation и persistence key закрыты.
- Каждый export имеет реального внешнего consumer.
- Deep imports отсутствуют, включая tests уровня public contract.
## Проверка тестов
Business-модуль не завершён без factory-level tests.
Обязательный минимум:
1. Форма public API фабрики.
2. Happy path каждого runtime method/hook.
3. Invalid dependency response.
4. Rejected dependency.
5. Synchronous throw каждого обычного method/callback/state/lifecycle dependency. Dependency hooks проверяются отдельным non-throwing contract.
6. Domain error `code` и `cause`.
7. Порядок side effects и остановка после ошибки.
8. State transitions и concurrent calls.
`compositions/business/{domain}` не завершён без assembly tests:
1. Factory получает правильные adapters.
2. Каждый adapter вызывает нужный runtime source с правильным payload.
3. Builder не делает I/O при создании API.
4. Cross-domain API передан в правильном виде.
5. Private adapters не раскрыты public API.
6. Adapter пробрасывает source error без создания domain error.
7. Dependency hook не бросает и не использует Suspense/throw-on-error mode.
8. Lifecycle cleanup проверен, если есть subscriptions/resources.
Colocated tests обязательны для mappers, normalizers, type guards, domain errors и другой внутренней runtime-safe логики. Они дополняют, но не заменяют factory-level tests. Подробная матрица находится в [Тестировании business-модулей](../examples/business-testing.md).
Проверь наличие исполняемого test script и test runner именно в изменяемом workspace. Root task без локального script не является выполненной тестовой инфраструктурой.
## Проверка целостности репозитория
- Все imports разрешаются.
- Все упомянутые modules и public entrypoints существуют.
- Direct runtime packages объявлены в package текущего workspace.
- Старый provider/store/source path удалён после миграции, если больше не используется.
- Нет speculative scaffold с пустым graph, несуществующими доменами или placeholder contracts.
- Template исправлен, если именно он системно создаёт нарушение.
- Выполнены доступные typecheck, tests, lint/build и `git diff --check`.
## Формат архитектурного ревью
Для каждого нарушения укажи:
1. Путь и строку.
2. Нарушенный invariant.
3. Runtime или maintenance риск.
4. Минимальную корректную границу.
5. Необходимые tests.
Отделяй обязательное нарушение от необязательного улучшения. Не предлагай большую миграцию, если нарушение можно устранить локально без создания второй архитектуры.
## Финальный gate
Перед завершением ответь «да» на все вопросы:
- Архитектурная роль изменения определена?
- Владелец ответственности и state определён?
- Все runtime-capabilities проходят через правильную границу?
- Product data проходит через business API?
- Business вызывает только переданные deps и собственную детерминированную логику?
- Наружу выходят только domain errors?
- Adapters существуют и закрыты?
- Graph и lifecycle определены?
- Public API минимален и разрешим?
- Обязательные tests созданы и запущены?
- Изменение не оставило старый параллельный путь?
Если хотя бы один ответ «нет», задача не завершена.

View File

@@ -0,0 +1,15 @@
# Черновики SLM
> Материалы в `DRAFT` являются рабочими черновиками и не задают нормативную спецификацию SLM.
## Материалы
- [Первый уровень](./level-1/README.md) - слои, доменные модули, публичные API и зависимости.
- [Второй уровень](./level-2/README.md) - доменные пакеты, именованные Domain API, assemblies и Framework Groups.
- [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки.
## Соглашение
Черновики могут содержать определения, правила, рекомендации, примеры и открытые вопросы.
Нормативные определения задаются терминологией соответствующего уровня. Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки.

View File

@@ -0,0 +1,53 @@
# SLM Level 1
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
Level 1 задаёт полную структурную основу SLM: слои, модули, доменные модули, публичные границы, граф зависимостей и владение жизненным циклом.
## Место в уровнях SLM
| Уровень | Назначение |
|---|---|
| Level 1 | Слои, доменные модули, зависимости, структурные сущности и жизненный цикл ресурсов |
| Level 2 | Опциональная пакетная форма отдельных доменов, именованные API, assemblies и явные границы сред выполнения |
Переход отдельного домена на Level 2 может требовать рефакторинга, но базовые понятия Level 1 сохраняются. Остальные домены того же SLM root могут оставаться модулями Level 1.
## Область Level 1
Level 1 описывает слои, доменные модули, группы, сегменты, компоненты, публичный API, граф зависимостей и владение жизненным циклом ресурсов.
Level 1 не задаёт обязательную внутреннюю форму доменного модуля, фабрики, порты, адаптеры, assemblies, обязательный поток данных, монорепозитории, соглашения об именовании и файловый стайлгайд.
Появление нескольких сред выполнения, нескольких независимо собираемых API или необходимости разделить бизнес-логику и технические сборки является сигналом перевести конкретный домен на [Level 2](../level-2/).
## Виды утверждений
- **Определение** нормативно задаёт смысл архитектурного термина, но не получает код правила.
- **Правило** задаёт блокирующий архитектурный инвариант и объявляется в каноническом реестре.
- **Рекомендация** помогает принять решение, но не делает архитектуру невалидной.
- **Пример** иллюстрирует модель и не задаёт обязательную структуру.
Нормативные определения находятся в [терминологии](./terminology.md). Канонический набор правил: [правила SLM Level 1](../rules/level-1.md). Формат и требования к ним: [Правила SLM](../rules/).
Остальные документы Level 1 объясняют и иллюстрируют модель, но не владеют точными формулировками определений и правил.
## Основная идея
Модуль является основной архитектурной единицей SLM. Слой задаёт роль и направление зависимостей. Доменный модуль представляет одну предметную область. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю.
Level 1 требует отдельную папку и единый публичный API модуля. Имена этих элементов, внутренняя файловая форма модулей, сегментов и компонентов определяются стайлгайдом проекта.
## Карта черновика
- [Терминология](./terminology.md)
- [Слои](./layers.md)
- [Доменные модули](./domains.md)
- [Зависимости](./dependencies.md)
- [Модули](./modules.md)
- [Группы](./groups.md)
- [Сегменты](./segments.md)
- [Компоненты](./components.md)
- [Вложенные модули](./nested-modules.md)
- [Жизненный цикл](./lifecycle.md)
- [Проверка](./validation.md)

View File

@@ -0,0 +1,65 @@
# Компоненты Level 1
> Пояснение нормативной модели компонентов Level 1.
Компонент является строительным элементом интерфейса, а не самостоятельной архитектурной единицей.
## Связанные правила
- [`SLM-L1-COMPONENT-R009`](../rules/level-1.md#slm-l1-component-r009)
- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013)
## Файловая форма
Файловую форму компонента определяет стайлгайд. Компонент может быть одним файлом фреймворка или каталогом со вспомогательными файлами.
```text
landing/
└── ui/
└── hero.tsx
```
```text
landing/
└── ui/
└── hero/
├── hero.tsx
├── styles/
│ └── hero.module.css
└── types/
└── hero-props.type.ts
```
Наличие каталога, типов, стилей или локального `index.ts` не превращает компонент в модуль.
## Реализация
Компонент может отображать входные данные, вызывать переданные обработчики, условно строить интерфейс и хранить локальное состояние представления.
Level 1 не вводит отдельного запрета на импорты, выполняемые кодом компонента. Каждый такой импорт считается зависимостью родительского модуля и должен соблюдать направление слоёв, публичные API и запрет циклов.
Доступ к данным, состояние, контекст или код жизненного цикла внутри компонента сами по себе не создают новую архитектурную границу. Их источники, зависимости и область жизни определяет родительский модуль.
Провайдер может технически реализовывать контекст и жизненный цикл фреймворка, но владельцем состояния и ресурсов остаётся родительский модуль.
Файл в `app` может технически быть компонентом React или Vue. Архитектурно он является точкой входа фреймворка, а не компонентом SLM.
## Компонент и модуль
| Признак | Компонент | Модуль |
|---|---|---|
| Самостоятельная ответственность | Нет | Да |
| Собственный публичный API | Нет | Да |
| Собственная граница зависимостей | Нет | Да |
| Вспомогательные файлы | Может иметь | Может иметь |
| Сегменты и вложенные модули | Нет | Может иметь |
Модуль может состоять всего из одного корневого компонента. Различие определяется владением, а не количеством файлов.
## Когда нужен вложенный модуль
Если часть интерфейса получает самостоятельную ответственность, публичный API, собственную границу зависимостей, область жизни или внутреннюю модульную декомпозицию, она является модулем. При локальном использовании такой модуль может размещаться как вложенный.

View File

@@ -0,0 +1,59 @@
# Зависимости Level 1
> Пояснение нормативной модели зависимостей Level 1.
Матрица слоёв задаёт допустимые связи, а модули образуют граф зависимостей.
## Что считается зависимостью
- Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
- Импорт между файлами одного модуля не пересекает модульную границу и не создаёт отдельный узел графа.
- Импорт любого внутреннего файла, сегмента или компонента считается зависимостью ближайшего модуля-владельца.
- Вложенный модуль является обычным самостоятельным узлом графа.
- Группы, сегменты и компоненты не являются самостоятельными узлами графа.
Точка входа фреймворка не является модулем. Её импорты участвуют в проверке направления слоёв, но не образуют исходящий узел графа модулей.
Ресурс `shared` также не является модулем. Его прямой импорт участвует в проверке направления слоёв, но не нарушает требование о публичном API модуля.
## Допустимые связи
- Модуль может импортировать модули своего слоя и слоёв, разрешённых нормативной матрицей.
- Модули одного слоя могут импортировать друг друга.
- Промежуточный слой не является обязательным посредником.
- `infra` и `ui` не импортируют друг друга; их связывает владелец из `domains`, `compositions` или `app`.
Доменный модуль Level 1 может импортировать публичный API другого доменного модуля. Runtime- и type-only импорты одинаково создают ребро графа, поэтому общий граф обязан оставаться ацикличным.
```ts
// domains/orders
import type { Product } from '@/domains/catalog'
```
Level 1 не требует отдельного механизма междоменной инъекции. Более строгая модель cross-domain зависимостей задаётся Level 2.
Матрица слоёв определена в [Слоях](./layers.md).
## Связанные правила
- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010)
## Публичный API
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимо
import { Button } from '@/ui/button/button'
```
## Циклы
```text
ui/modal → ui/button → ui/icon
ui/icon -/→ ui/modal
```

View File

@@ -0,0 +1,61 @@
# Доменные модули Level 1
> Пояснение базовой модели предметных областей без обязательной внутренней архитектуры.
## Связанные правила
- [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006)
- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007)
## Один домен, один модуль
Связная предметная область получает один доменный модуль. Level 1 не требует выделять business, adapters, assemblies или framework bindings в самостоятельные соседние модули.
```text
domains/auth/
├── hooks/
├── services/
├── stores/
├── types/
├── ui/
└── index.ts
```
Показанные каталоги являются возможными сегментами, а не обязательным каркасом. Доменный модуль может содержать предметные типы, сценарии, состояние, framework-код, локальные адаптеры, компоненты и вложенные модули.
## Публичный API
Внешний код использует домен через обычный публичный API модуля:
```ts
import { signOut, useSession } from '@/domains/auth'
```
Глубокий импорт во внутренний сегмент нарушает модульную границу:
```ts
import { useSession } from '@/domains/auth/hooks/use-session'
```
## Groups
При большом количестве доменных модулей слой `domains` может содержать обычные навигационные Groups:
```text
domains/
├── shop/ # Group
│ ├── catalog/ # Доменный модуль
│ └── orders/ # Доменный модуль
└── cabinet/ # Group
└── profile/ # Доменный модуль
```
Group не имеет `index.ts`, реализации, состояния или публичного API. Она не создаёт dependency boundary и не реэкспортирует содержащиеся в ней домены.
## Переход на Level 2
Доменный модуль переводится в доменный пакет Level 2, когда ему нужны устойчивые API, несколько фабрик, разные среды выполнения или независимые SLM-модули сборок и framework-интеграции.
Такой переход изменяет только форму выбранного домена: корневой доменный модуль исчезает, а его предметная ответственность переходит обязательному модулю `business` внутри доменного пакета. Другие предметные области SLM root не обязаны переходить вместе с ним.

View File

@@ -0,0 +1,25 @@
# Группы Level 1
> Пояснение нормативной модели групп Level 1.
Группа помогает ориентироваться в большом количестве модулей. Она классифицирует структуру, но ничего не реализует и не образует узел графа зависимостей.
## Связанное правило
- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
## Пример
```text
compositions/
├── pages/ # Группа
│ ├── landing/ # Модуль
│ └── contacts/ # Модуль
└── layouts/ # Группа
└── main/ # Модуль
```
`pages` и `layouts` являются возможной группировкой проекта, а не обязательной структурой Level 1.
Рекомендуется создавать группу только при реальной навигационной потребности. Если папка начинает владеть файлами реализации, состоянием, жизненным циклом или публичным API, она является модулем и должна получить модульную границу.

View File

@@ -0,0 +1,95 @@
# Слои Level 1
> Пояснение нормативной модели слоёв Level 1.
## Базовая структура
```text
src/
├── app/
├── compositions/
├── domains/
├── infra/
├── ui/
└── shared/
```
`src/` здесь является примером SLM root. Фактическую границу определяет устройство приложения.
Отсутствующая в проекте роль не требует пустой папки. Слой является доступной архитектурной ролью, а не обязательным элементом каркаса.
## Роли слоёв
### App
`app` связывает приложение с фреймворком: запускает его, объявляет маршруты, преобразует входные данные и подключает публичные API модулей разрешённых слоёв или ресурсы `shared`. Файлы `app` являются точками входа фреймворка, а не модулями SLM.
Точка входа может напрямую использовать `compositions`, `domains`, `infra`, `ui` или `shared`, если зависимость разрешена матрицей слоёв. Такое использование не переносит ответственность импортируемого модуля в `app`.
### Compositions
`compositions` содержит продуктовый интерфейс: страницы, макеты, экраны, виджеты, точки входа и другие композиционные модули. Внутреннюю группировку слоя определяет проект.
### Domains
`domains` содержит предметные области приложения: их модели, правила, сценарии и продуктовое состояние. Каждая самостоятельная предметная область представлена [доменным модулем](./domains.md).
Доменная логика не принадлежит устройству одной страницы или маршрута. Конкретный visual outcome и сборка нескольких доменов остаются в `compositions`.
### Infra
`infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной доменной модели.
### UI
`ui` содержит универсальные модули интерфейса, которые не зависят от конкретной страницы или продуктовой композиции.
### Shared
`shared` содержит независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода.
В `shared` допускаются обычные модули и специальные немодульные ресурсы: небольшие чистые утилиты, общие типы, стили, конфигурация и статические файлы. Ресурс не имеет самостоятельной ответственности, публичного API или жизненного цикла и может импортироваться напрямую по пути, установленному стайлгайдом.
Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль. Каталог ресурсов не реэкспортирует модули и не используется для обхода их публичных API.
## Матрица зависимостей
```text
app
|
compositions
|
domains
/ \
infra ui
\ /
shared
```
Код слоя может импортировать модули своего слоя и слоёв, разрешённых строкой матрицы. Разрешённая зависимость может пропускать промежуточные роли.
| Слой | Может импортировать |
|---|---|
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
`infra` и `ui` не импортируют друг друга. Универсальный UI получает локализованный текст, тему, callbacks аналитики и другие возможности через входной контракт либо связывается с ними в `domains` или `compositions`. Если UI-модулю необходимо напрямую знать конкретный технический сервис приложения, такой код не является универсальным UI.
Импорты внутри слоя, публичный API и циклы описаны отдельно в [Зависимостях](./dependencies.md).
## Связанные правила
- [`SLM-L1-LAYER-R001`](../rules/level-1.md#slm-l1-layer-r001)
- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002)
- [`SLM-L1-LAYER-R003`](../rules/level-1.md#slm-l1-layer-r003)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
## Граница Level 1
Разрешённый импорт не переносит владение. Например, доменный модуль может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев.
Если код определяет продуктовую модель, правило или сценарий, он принадлежит `domains`, а не `shared` или `infra`. Строгая внутренняя форма домена появляется только на [Level 2](../level-2/).

View File

@@ -0,0 +1,31 @@
# Жизненный цикл Level 1
> Пояснение нормативной модели владения ресурсами Level 1.
Жизненный цикл является частью ответственности модуля. Файл, компонент, провайдер или точка входа фреймворка могут технически создавать и останавливать ресурс, но архитектурным владельцем остаётся модуль.
## Связанные правила
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013)
## Граница ресурса
Для ресурса определяются:
- модуль-владелец;
- место создания;
- момент начала работы;
- область жизни;
- допустимое число экземпляров;
- способ остановки и очистки.
Ресурс начинает работу не раньше начала своей области жизни и не остаётся активным после её завершения. Подписки, слушатели, таймеры, наблюдатели, запросы и соединения рассматриваются одинаково, если требуют явного завершения или отмены.
## Реализация
Очистку может выполнять сам модуль, компонент, провайдер или фреймворк. Способ реализации не меняет владельца и не переносит ответственность в технический файл.
Одиночный экземпляр на всё приложение допустим только тогда, когда модуль действительно владеет областью жизни приложения или процесса. Размещение экземпляра на уровне файла само по себе этого не доказывает.
Точка входа `app` может запускать или подключать ресурс через публичный API импортируемого модуля, но не становится его владельцем.

View File

@@ -0,0 +1,43 @@
# Модули Level 1
> Пояснение нормативной модели модулей Level 1.
Модуль является основной архитектурной единицей SLM. Он размещается в отдельной папке, но может состоять только из публичной точки входа и одного файла реализации.
## Связанные правила
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-MODULE-A014`](../rules/level-1.md#slm-l1-module-a014)
- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006)
- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011)
- [`SLM-L1-MODULE-R012`](../rules/level-1.md#slm-l1-module-r012)
## Владение
Каждая самостоятельная ответственность имеет одного модуля-владельца. Модуль определяет её публичный API, зависимости, состояние, область жизни и внутреннее устройство независимо от того, в каком файле выполняется конкретный код.
Точки входа `app` и нормативные ресурсы `shared` являются единственными немодульными исключениями. Остальной код внутри SLM root либо принадлежит существующему модулю, либо образует новый модуль.
## Публичный API
Модуль предоставляет один логический публичный API. Конкретное имя точки входа и механизм экспорта определяет стайлгайд проекта.
Внешний код использует модуль только через публичный API. Сам API открывает только контракт, необходимый реальным внешним потребителям; внутренние механизмы, изменяемое состояние и детали жизненного цикла остаются закрытыми.
## Внутреннее устройство
Модуль может содержать корневые файлы, сегменты, компоненты и [вложенные модули](./nested-modules.md). Внутри своей границы он может использовать относительные импорты и не обязан обращаться к собственному публичному API; точную форму внутренних импортов определяет стайлгайд.
SLM не требует полного каркаса или обязательного каталога сегментов.
## Визуальный модуль
Визуальный модуль обычно имеет корневой компонент, который экспортируется через публичный API.
```text
button/
├── button.tsx
└── index.ts
```
Корневой компонент остаётся компонентом, а владельцем ответственности является модуль `button`.

View File

@@ -0,0 +1,30 @@
# Вложенные модули Level 1
> Пояснение нормативной модели вложенных модулей Level 1.
Вложенный модуль является обычным модулем, размещённым внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей.
## Связанные правила
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010)
## Пример
```text
landing/
├── landing.page.tsx
├── parts/
│ └── hero/
│ ├── hero.tsx
│ └── index.ts
└── index.ts
```
`parts/` здесь является примером сегмента, а не обязательным именем.
Код родительского модуля использует вложенный модуль через его собственный публичный API. Код за пределами родительского модуля получает доступ только через публичный API родителя.
Если вложенный модуль становится нужен за пределами родителя, рекомендуется перенести его в минимальную общую область без изменения внутренней формы. Доступ через API родителя при этом остаётся допустимым и сам по себе не требует переноса.

View File

@@ -0,0 +1,29 @@
# Сегменты Level 1
> Пояснение нормативной модели сегментов Level 1.
Сегмент организует внутреннее содержимое модуля. Level 1 определяет роль сегмента, но не задаёт обязательный список имён.
## Связанное правило
- [`SLM-L1-SEGMENT-R008`](../rules/level-1.md#slm-l1-segment-r008)
- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
## Файловая форма
Названия, набор и содержимое сегментов определяет стайлгайд проекта. Сегмент может группировать файлы, компоненты или вложенные модули.
Файлы и компоненты сегмента принадлежат родительскому модулю. Вложенный модуль внутри сегмента сохраняет собственные ответственность, публичный API и узел графа зависимостей.
## Пример
```text
landing/ # Модуль
└── ui/ # Сегмент модуля
└── hero/ # Каталог компонента
├── hero.tsx
├── styles/ # Вспомогательный каталог компонента
└── types/ # Вспомогательный каталог компонента
```
`styles/` и `types/` внутри каталога компонента не обязаны считаться сегментами SLM. Их форму определяет стайлгайд компонентов.

View File

@@ -0,0 +1,143 @@
# Терминология Level 1
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
Определения Level 1 задают обязательный смысл архитектурных терминов и используются при толковании всех правил. Код получают только блокирующие требования, а не сами определения.
## Базовые понятия
### SLM root
Граница структурной архитектуры одного приложения. Внутри неё определяются слои, модули и граф зависимостей Level 1. Монорепозиторий, пакеты и отношения между несколькими SLM root находятся за пределами Level 1.
### Ответственность
Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственные публичный API, зависимости, состояние или область жизни.
### Владелец
Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннее устройство. Место выполнения кода не переносит владение.
### Публичный API
Единая логическая точка внешнего доступа к модулю. Публичный API скрывает внутреннее устройство; конкретное имя файла и механизм экспорта определяет стайлгайд проекта.
### Зависимость
Статическая связь внутри одного SLM root, которую импорт или реэкспорт создаёт между архитектурными границами. Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
Зависимость любого внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу и становится отдельным узлом графа зависимостей.
### Область жизни
Период, в течение которого принадлежащие модулю состояние или долгоживущий ресурс должны оставаться активными.
### Ресурс жизненного цикла
Ресурс, работа которого продолжается после первоначального вызова и требует остановки, отмены, отписки или освобождения. Например, подписка, слушатель событий, таймер, наблюдатель, запрос или соединение.
### Очистка
Гарантированное прекращение работы ресурса не позже завершения его области жизни. Автоматическая очистка фреймворка считается очисткой владельца, если модуль устанавливает и контролирует соответствующую границу.
## Структурные сущности
### Нормативная матрица слоёв
Отношение допустимой зависимости между слоями одного SLM root. Матрица определяет, код каких слоёв может импортировать исходный слой; она не обязана образовывать линейный порядок.
Для Level 1 нормативно отношение `app → compositions → domains → { infra, ui } → shared`. `infra` и `ui` являются независимыми ветвями: они не импортируют друг друга. Промежуточный слой не является обязательным посредником.
### Слой
Одна из шести верхнеуровневых ролей внутри SLM root:
| Слой | Роль |
|---|---|
| `app` | Связь приложения с фреймворком: запуск, маршруты и преобразование входных данных |
| `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции |
| `domains` | Предметные области, их модели, правила, сценарии и продуктовое состояние |
| `infra` | Технические сервисы и возможности приложения |
| `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции |
| `shared` | Независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода |
Полная матрица допустимых зависимостей:
| Исходный слой | Допустимые целевые слои |
|---|---|
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
### Модуль
Минимальная самостоятельная архитектурная единица Level 1. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API.
### Доменная ответственность
Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Количество экранов, endpoint-ов, hooks или файлов само по себе не определяет её границу.
### Доменный модуль
Обычный SLM-модуль слоя `domains`, представляющий одну доменную ответственность. Он подчиняется всем правилам модулей, предоставляет единый публичный API и является узлом графа зависимостей.
Доменный модуль может содержать сегменты, компоненты и вложенные модули. Level 1 не требует разделять его внутреннее содержимое по техническим ролям.
### Группа
Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или узла графа зависимостей.
### Сегмент
Внутренняя часть одного модуля, которая группирует его содержимое по назначению. Сегмент не является самостоятельным владельцем, публичным API или узлом графа зависимостей.
### Компонент
Сущность фреймворка, которая реализует часть интерфейса родительского модуля. Компонент не образует собственного владельца, публичного API или узла графа зависимостей.
Импорты, состояние, доступ к данным и код жизненного цикла компонента принадлежат родительскому модулю. Их наличие само по себе не создаёт новый модуль; решающим признаком является самостоятельная ответственность.
### Вложенный модуль
Обычный модуль, размещённый внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей.
Публичный API вложенного модуля доступен коду родительской границы. Для кода за пределами родительского модуля вложенный модуль остаётся внутренней реализацией родителя.
### Точка входа фреймворка
Специальная немодульная единица слоя `app`, которая непосредственно связывает приложение с фреймворком. Её импорты участвуют в проверке направления слоёв, но сама точка входа не является узлом графа модулей.
### Ресурс shared
Специальная немодульная единица слоя `shared`: небольшая детерминированная утилита, общий тип, стиль, конфигурация или статический ресурс без продуктового знания, изменяемого состояния, ввода-вывода, области жизни и собственного публичного API.
Ресурс `shared` может импортироваться напрямую по пути, установленному стайлгайдом, и не является узлом графа модулей. Доступный по этому пути файл является всей единицей и не скрывает отдельное внутреннее устройство.
Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль.
## Структурная модель
```text
SLM root
├── app
│ └── точка входа фреймворка
├── compositions | domains | infra | ui
│ ├── группа
│ │ └── модуль
│ └── модуль
│ ├── корневые файлы
│ ├── сегмент
│ │ ├── файлы
│ │ ├── компоненты
│ │ └── вложенные модули
│ └── вложенный модуль
└── shared
├── группа
├── модуль
└── ресурс shared
```
Путь и имя папки сами по себе не определяют сущность. Её определяют ответственность, владелец и публичная граница. Физическое сопоставление путей с сущностями задаётся стайлгайдом или конфигурацией проверки проекта.

View File

@@ -0,0 +1,34 @@
# Проверка Level 1
> Граница автоматической проверки и архитектурного ревью Level 1.
## Автоматическая проверка
Проект, заявляющий соответствие Level 1, сопоставляет физические пути с SLM root, слоями, доменными модулями, остальными модулями, Groups, вложенными модулями, публичными точками входа, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей.
Каждое правило класса `A` должно быть реализовано проверкой проекта и блокировать её при нарушении. Level 1 не навязывает конкретный инструмент.
Скрипт `draft-rules.js` проверяет только целостность документов: формат и уникальность кодов, ссылки и наличие тематических упоминаний. Он не проверяет архитектуру приложения.
Актуальный список правил скрипт получает из [канонического реестра](../rules/level-1.md).
## Архитектурное ревью
Правила класса `R` проверяются вручную. Статический анализ может обнаружить подозрительный код, но не способен окончательно определить:
- ответственность и её владельца;
- связность предметной области доменного модуля;
- соответствие кода роли слоя;
- необходимость экспортов публичного API;
- область жизни ресурса и достаточность очистки;
- наличие самостоятельной границы у компонента, группы или сегмента.
## Проверка доменных модулей
На ревью определяется:
- представляет ли доменный модуль одну связную предметную область;
- не разделена ли одна область на соседние модули без самостоятельных владельцев;
- не объединены ли в одном модуле несвязанные предметные области;
- остаются ли страницы, маршруты и multi-domain UI в `compositions`;
- остаются ли самостоятельные технические сервисы без предметной модели в `infra`.

View File

@@ -0,0 +1,102 @@
# SLM Level 2
> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
Level 2 предназначен для отдельных предметных областей с устойчивыми API, несколькими способами сборки или самостоятельными framework-модулями. Он сохраняет структурную базу Level 1, но заменяет выбранный доменный модуль доменным пакетом с явными владельцами ролей.
## Наследование Level 1
Доменный пакет Level 2 соблюдает определения и правила Level 1, кроме явно заменённых положений:
| Положение Level 1 | Статус в Level 2 |
|---|---|
| Матрица `app → compositions → domains → { infra, ui } → shared` | Сохраняется |
| Модуль, Group, сегмент, компонент, публичный API и граф зависимостей | Сохраняют смысл |
| Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются только для предметной области в пакетной форме |
| Единый публичный API модуля `business` | Представлен обязательными type-only и factory-фасетами и необязательным runtime-фасетом |
| Навигационная Group слоя `domains` | Может одновременно содержать доменные модули и пакеты |
| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups |
Канонический набор требований образуют [правила Level 1](../rules/level-1.md) и [правила Level 2](../rules/level-2.md).
## Когда выбирать Level 2
Level 2 оправдан, когда одной предметной области нужны несколько независимо собираемых Domain API, разные browser/server assemblies, несколько технических интеграций или самостоятельные domain-specific модули React, Vue либо другого фреймворка.
Level 2 выбирается для конкретной предметной области. Один SLM root может корректно содержать простые доменные модули Level 1 и доменные пакеты Level 2. Размер каталога сам по себе не требует перехода.
## Цена Level 2
Пакетная форма намеренно дороже простого доменного модуля. Она добавляет отдельные business-фасеты, минимум одну assembly, явную runtime-инъекцию cross-domain API и adapter-модули для production capabilities. Concrete state/query runtime, SDK и недетерминизм не импортируются напрямую в `business`.
Эта цена оправдана независимой сборкой API, несколькими средами, строгой environment boundary или самостоятельными framework bindings. Если домену не нужны такие свойства, он остаётся модулем Level 1.
## Базовая форма
```text
src/domains/
├── catalog/ # Доменный модуль Level 1
└── auth/ # Доменный пакет Level 2
├── README.md # Необязательная metadata
├── business/ # Обязательный SLM-модуль
│ ├── index.ts # Только public types
│ ├── factory.ts # Public factories entry
│ └── runtime.ts # Необязательный deterministic runtime
├── assemblies/ # Обязательная непустая Group
│ ├── browser/ # SLM-модуль
│ └── request/ # SLM-модуль
├── adapters/ # При наличии technical dependencies
│ └── identity-provider/ # SLM-модуль
└── react/ # Необязательная framework Group
├── session/ # SLM-модуль
└── login-form/ # SLM-модуль
```
Корень пакета не является модулем и не имеет `index.ts`. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своим публичным API.
## Публичные границы
`business` может объявить несколько API и соответствующих фабрик. Assembly создаёт только нужный для своего контекста именованный граф:
```ts
import type {
AuthAdministrationApi,
AuthSessionApi,
} from '@/domains/auth/business'
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/runtime'
import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
import { AuthSessionProvider } from '@/domains/auth/react/session'
```
`business/runtime` существует только при наличии реальных внешних потребителей детерминированных runtime-экспортов. Общие импорты `@/domains/auth` и `@/domains/auth/react` запрещены: пакет и Groups не имеют агрегирующих API.
## Совместное применение форм
Простой доменный модуль и доменный пакет могут постоянно сосуществовать в одном SLM root, но одна предметная область имеет только одну форму. При связи, пересекающей границу пакета Level 2, доменный код использует type-only публичный контракт либо детерминированный `business/runtime`; готовые API передаются runtime-аргументами в месте сборки графа.
Если доменному модулю Level 1 нужна runtime-инъекция, которой его текущий публичный API не поддерживает, этот потребитель рефакторится или сам переводится в пакетную форму. Level 2 не создаёт скрытый механизм внедрения в старый модуль.
## Карта черновика
- [Терминология](./terminology.md)
- [Доменный пакет](./domains/domain-package.md)
- [Модуль business](./domains/business.md)
- [Фабрики, зависимости и adapters](./domains/factory-ports-adapters.md)
- [Assemblies и среды выполнения](./domains/assemblies.md)
- [Состояние и кэш](./domains/state-cache.md)
- [Framework Groups и модули](./domains/framework-bindings.md)
- [Зависимости](./dependencies.md)
- [Тестирование](./domains/testing.md)
- [Проверка](./validation.md)
- [Переход auth](./domains/auth-example.md)
- [Открытые вопросы](./domains/open-questions.md)

View File

@@ -0,0 +1,109 @@
# Зависимости Level 2
> Уточнение графа зависимостей внутри и между доменными границами.
## Связанные правила
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
## Матрица внутри пакета
| Исходный модуль | Допустимые зависимости |
|---|---|
| `business` | Собственные файлы, объявленные environment-neutral ресурсы `shared`, business-safe packages, type-only контракты и `business/runtime` других доменов |
| Adapter module | Type-only barrel собственного `business`, `infra`, concrete technical runtime, `shared` |
| Assembly | Type-only barrel, `factory` и при необходимости `runtime` собственного `business`, публичные adapters своего домена, type-only API других доменов, `shared` |
| Framework binding module | Type-only barrel и `runtime` собственного `business`, публичные framework-модули своего домена, framework/state/query runtime, `ui`, `shared` |
| Место сборки графа | Assemblies либо `business/factory` и adapter-модули, `business/runtime`, framework-модули входящих в граф доменов, а также разрешённые матрицей `infra`, `ui` и `shared` |
`business` не достигает adapters, assemblies, framework-модулей, product SDK, storage, state/query manager, API браузера или Node.js. Проверяется весь транзитивный import-граф публичных фасетов.
Adapter module импортирует из собственного `business` только типы технических зависимостей. Он не импортирует `business/factory`, потому что не собирает API. Публичный deterministic runtime обычно также не нужен adapter: внешний результат возвращается business для проверки и error mapping.
Assembly не содержит inline adapters. Она импортирует production implementations через публичные API конкретных модулей `adapters/*`.
## Междоменные импорты
При связи, пересекающей границу пакета Level 2, разрешены два вида статического импорта из другого домена:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import { isAuthError } from '@/domains/auth/business/runtime'
```
Первый импорт предоставляет type-only контракт для runtime-инъекции. Второй предоставляет только публичную детерминированную функцию или значение. Оба импорта являются рёбрами общего DAG.
Запрещено импортировать из другого домена:
- `business/factory`;
- готовый API instance или singleton;
- assembly;
- adapter;
- framework state, hook, context, Provider или component;
- любой внутренний путь `business`.
Такая модель действует и между двумя пакетами Level 2, и между модулем Level 1 и пакетом Level 2. Между двумя простыми доменными модулями продолжают действовать правила Level 1.
## Детерминированный runtime
`business/runtime` закрывает случай общей продуктовой pure-функции, которую нельзя переносить в `shared` из-за знания о предметной области:
```ts
import {
normalizeAuthIdentifier,
} from '@/domains/auth/business/runtime'
```
Функция остаётся собственностью Auth, а зависимый домен получает явное runtime-ребро к её публичному контракту. Если связь создаёт цикл, границы доменов или владелец общего понятия пересматриваются.
Перенос в `shared` допустим только после устранения продуктового знания, а не как обход cross-domain правила.
## Runtime-инъекция API
Готовый API другого домена передаётся assembly или одноразовому месту сборки аргументом. Код зависимого домена не импортирует его runtime-фабрику или сборку:
```text
createAuthForRequest()
→ AuthSessionApi
→ createUserForRequest({ auth })
→ UserProfileApi
```
Место сборки графа создаёт независимые домены раньше зависимых. Если сбой Auth становится результатом публичного сценария User, наружу выходит только собственная ошибка User. При exception-модели User может распознать ожидаемый Auth error через публичный guard из `auth/business/runtime`.
## Совместное применение Level 1 и Level 2
Один SLM root может постоянно содержать обе формы. Каждая предметная область объявляется либо модулем, либо пакетом, поэтому checker однозначно определяет применимые правила.
Runtime-взаимодействие с пакетом Level 2 собирается за пределами доменного кода. Если существующий модуль Level 1 должен принимать готовый API, его собственный публичный контракт явно предоставляет такую точку инъекции. Если это невозможно без скрытого singleton или обратного импорта, зависимый модуль рефакторится либо переводится на Level 2.
Runtime-значение из модуля Level 1, необходимое пакету Level 2, также передаётся внешним graph owner как API, callback или другой явно типизированный аргумент. Код пакета не импортирует executable API модуля Level 1 напрямую.
## Framework-состояние
Framework binding module использует framework API только своего доменного пакета:
```ts
// Допустимо внутри domains/auth/react/login-form
import { useAuthSession } from '@/domains/auth/react/session'
// Недопустимо внутри domains/user/react/profile
import { useAuthSession } from '@/domains/auth/react/session'
```
Во втором случае composition читает состояния обоих доменов и передаёт необходимые данные или callbacks через публичные свойства компонентов.
State/query cache внутри framework binding не создаёт исключение для cross-domain imports. Он работает поверх переданного API своего домена.
## Границы сред
Каждый client-, server- или shared-entry point имеет совместимый транзитивный import-граф. Серверная assembly или adapter не реэкспортируется через `business`, Framework Group или клиентскую assembly.
Tree shaking не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода.

View File

@@ -0,0 +1,29 @@
# Доменные пакеты Level 2
Доменный пакет заменяет один выбранный доменный модуль Level 1. Он объединяет SLM-модули одной предметной области, но сам не является модулем, Group или публичным API.
```text
domains/auth/
├── business/
├── assemblies/ # Обязательная Group
├── adapters/ # При наличии technical dependencies
└── react/
├── session/
└── login-form/
```
Другие предметные области того же SLM root могут оставаться доменными модулями Level 1.
## Основные границы
- [Доменный пакет](./domain-package.md) определяет предметную и структурную policy boundary.
- [Business](./business.md) владеет Domain API и разделяет public types, factories и необязательный deterministic runtime.
- [Фабрики, зависимости и adapters](./factory-ports-adapters.md) изолируют runtime-capabilities, включая state/query runtime и недетерминизм.
- [Assemblies](./assemblies.md) обязательны и собирают именованный граф нужных API для реального контекста.
- [Состояние и кэш](./state-cache.md) разделяет предметную власть business и технические проекции.
- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу.
- [Переход auth](./auth-example.md) показывает локальный переход с Level 1 без big-bang всего root.
- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей.
Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md).

View File

@@ -0,0 +1,170 @@
# Assemblies и среды выполнения
> Пояснение повторяемой сборки именованного графа Domain API.
## Связанные правила
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ASSEMBLY-R011`](../../rules/level-2.md#slm-l2-assembly-r011)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
## Назначение
Assembly является SLM-модулем в Group `assemblies`. Она создаёт явный граф одного или нескольких Domain API пакета для конкретного повторяемого контекста выполнения. Каждый доменный пакет содержит минимум одну assembly.
```text
business/factory
├── assemblies/browser → { session: AuthSessionApi }
├── assemblies/request → { session, administration }
└── assemblies/server-action → { administration }
```
Архитектура не требует `base` или изоморфную assembly и не ограничивает их максимальное количество. Обязательная assembly соответствует реальному поддерживаемому контексту, а не существует только для заполнения структуры.
Место сборки графа в `composition` может вызвать фабрики напрямую для одноразовой конфигурации. Такая сборка не отменяет обязательную assembly пакета и при наличии технических зависимостей использует публичные adapter-модули, а не inline implementations.
## Именованный граф API
Browser assembly импортирует только фабрики и adapters нужных ей API:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import { authSessionFactory } from '@/domains/auth/business/factory'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserSessionAdapter } from '@/domains/auth/adapters/browser-session'
export type AuthBrowserGraph = Readonly<{
session: AuthSessionApi
}>
export const createBrowserAuth = (): AuthBrowserGraph => {
const session = authSessionFactory({
phone: createPhoneHttpAdapter(),
session: createBrowserSessionAdapter(),
})
return { session }
}
```
Request assembly может собрать дополнительный API, которого нет в браузере:
```ts
import type {
AuthAdministrationApi,
AuthSessionApi,
} from '@/domains/auth/business'
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
export type AuthRequestGraph = Readonly<{
administration: AuthAdministrationApi
session: AuthSessionApi
}>
```
Assembly не добавляет методы к этим контрактам и не создаёт общий `AuthApi`. Именованный объект только сообщает, какие независимые API доступны в контексте.
## Cross-domain input
Assembly зависимого домена принимает готовый API аргументом. Cross-domain API не является adapter и не размещается в Group `adapters`:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import type { UserProfileApi } from '@/domains/user/business'
import { userProfileFactory } from '@/domains/user/business/factory'
import { createUserProfileAdapter } from '@/domains/user/adapters/profile'
export type CreateUserForRequestInput = {
auth: Pick<AuthSessionApi, 'getSnapshot'>
request: UserRequestInput
}
export type UserRequestGraph = Readonly<{
profile: UserProfileApi
}>
export const createUserForRequest = ({
auth,
request,
}: CreateUserForRequestInput): UserRequestGraph => {
const profile = userProfileFactory({
auth,
profile: createUserProfileAdapter(request),
})
return { profile }
}
```
Assembly делает только type-only импорт `AuthSessionApi`. Runtime-фабрику, assembly или instance Auth она не импортирует.
Место сборки графа выполняет runtime-связь:
```ts
const auth = createAuthForRequest(authInput)
const user = createUserForRequest({
auth: auth.session,
request: userInput,
})
```
## Environment entry points
Server assembly имеет отдельный публичный entry point и marker выбранного framework или bundler:
```ts
import 'server-only'
export { createAuthForRequest } from './create-auth-for-request'
```
Server entry point не реэкспортируется через `business`, Framework Group, browser assembly или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость.
## Lifecycle
Factory не запускает запрос, subscription, timer или другую скрытую долгоживущую работу во время создания API. Assembly может активировать технический ресурс только с явной передачей cleanup своему caller. Операция Domain API, которая запускает ресурс позже, сама возвращает cleanup:
```ts
const stop = auth.session.startInvalidationTracking()
try {
// Scope использует API.
} finally {
await stop()
}
```
Если assembly сама создаёт ресурс, принадлежащий всему возвращённому графу, результат дополнительно предоставляет cleanup handle:
```ts
export type AuthRequestAssembly = Readonly<{
apis: AuthRequestGraph
dispose: () => Promise<void>
}>
```
```ts
const auth = createAuthForRequest(input)
try {
return await handleRequest(auth.apis)
} finally {
await auth.dispose()
}
```
Assembly без собственного ресурса не обязана возвращать пустой `dispose`. Graph owner вызывает каждый реально предоставленный cleanup не позже завершения application, route, request или test scope.
Одноразовое место сборки, которое вызывает фабрики напрямую, подчиняется той же границе: оно не запускает скрытый ресурс в constructor и сохраняет cleanup любого созданного adapter-ресурса до завершения своего scope.
Рекомендуется делать `dispose` идемпотентным, освобождать частично созданные ресурсы при ошибке assembly и закрывать зависимые ресурсы раньше их зависимостей. Детальная политика rollback и поведения API после cleanup остаётся открытым вопросом.

View File

@@ -0,0 +1,139 @@
# Переход домена auth с Level 1
> Проверочный пример локального перехода от доменного модуля к доменному пакету.
## Связанные правила
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
## Исходная форма Level 1
```text
domains/
├── auth/ # Доменный модуль
│ ├── hooks/
│ ├── services/
│ ├── stores/
│ ├── ui/
│ └── index.ts # Общий API модуля
└── catalog/ # Независимый доменный модуль
└── index.ts
```
Level 1 разрешает business-сценариям, framework hooks, state adapter и локальной сборке Auth находиться внутри одного модуля.
## Целевая форма Auth
```text
domains/
├── auth/ # Доменный пакет Level 2
│ ├── README.md
│ ├── business/ # Один SLM-модуль
│ │ ├── errors/
│ │ ├── factories/
│ │ ├── services/
│ │ ├── types/
│ │ ├── index.ts # Только public types нескольких API
│ │ ├── factory.ts # Public factories entry
│ │ └── runtime.ts # Error codes, guards, public pure runtime
│ ├── adapters/ # Group
│ │ ├── phone-http/ # SLM-модуль
│ │ ├── browser-session/ # SLM-модуль
│ │ └── request-session/ # SLM-модуль
│ ├── assemblies/ # Обязательная Group
│ │ ├── browser/ # Только AuthSessionApi
│ │ └── request/ # Session + Administration API
│ └── react/ # Framework Group
│ ├── session/ # SLM-модуль
│ └── login-form/ # SLM-модуль
└── catalog/ # По-прежнему модуль Level 1
└── index.ts
```
Корневой `domains/auth/index.ts` удаляется. Потребители переходят на публичные API конкретных модулей. `catalog` и остальные домены не меняют форму только из-за перехода Auth.
## Перенос ответственности
| Исходная часть | Владелец Level 2 | Публичный путь |
|---|---|---|
| Session-сценарии и public types | `auth/business` | `auth/business` |
| Administration-сценарии и public types | `auth/business` | `auth/business` |
| Runtime-фабрики API | `auth/business` | `auth/business/factory` |
| Коды, guards и public pure-функции | `auth/business` | `auth/business/runtime` |
| Browser storage и HTTP adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
| Cookies, request data и server adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
| Browser graph | `auth/assemblies/browser` | `auth/assemblies/browser` |
| Request graph | `auth/assemblies/request` | `auth/assemblies/request` |
| Provider и session hooks | `auth/react/session` | `auth/react/session` |
| Переиспользуемая форма | `auth/react/login-form` | `auth/react/login-form` |
| Страница, текст и redirect | `compositions` | API конкретной composition |
## Новые импорты
```ts
import type {
AuthAdministrationApi,
AuthError,
AuthErrorCode,
AuthSessionApi,
} from '@/domains/auth/business'
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/runtime'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
import { AuthSessionProvider } from '@/domains/auth/react/session'
import { LoginForm } from '@/domains/auth/react/login-form'
```
## Cross-domain граф
Если User package зависит от Auth, он получает только type-only API contract и при необходимости deterministic runtime:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import { isAuthError } from '@/domains/auth/business/runtime'
export type UserDeps = {
auth: Pick<AuthSessionApi, 'getSnapshot'>
}
```
Место сборки создаёт instances:
```ts
const auth = createBrowserAuth()
const user = createBrowserUser({ auth: auth.session })
```
User не импортирует Auth factory или assembly, а его React-модули не импортируют `useAuthSession` или Auth components.
Если User остаётся модулем Level 1, runtime-связь также выполняется снаружи доменных границ. Для этого его собственный API должен иметь явную точку передачи нужного Auth behavior; иначе именно User требуется рефакторинг или переход, но несвязанные домены не затрагиваются.
## Порядок перехода
1. Выбрать один доменный модуль и зафиксировать его внешние consumers.
2. Объявить `business` с type-only и factory entry points.
3. Разделить сценарии на независимо собираемые Domain API без дублирования методов.
4. Добавить `business/runtime`, только если внешним consumers нужны codes, guards или pure-функции.
5. Оформить каждую связную production implementation модулем `adapters/*`.
6. Создать минимум одну assembly и перенести туда повторяемый выбор API и adapters.
7. Разделить React-ответственности на модули внутри Group `react`.
8. Перенести страницы, redirects и multi-domain UI в `compositions`.
9. Перевести внешние импорты на разрешённые public paths.
10. Удалить старый root `index.ts` Auth и объявить пакетную форму checker-у.
Завершённость перехода определяется только границей Auth. Наличие других доменных модулей Level 1 не является миграционным долгом.

View File

@@ -0,0 +1,218 @@
# Модуль business
> Пояснение предметного владельца, нескольких Domain API и публичного deterministic runtime.
## Связанные правила
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009)
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
- [`SLM-L2-BUSINESS-R025`](../../rules/level-2.md#slm-l2-business-r025)
## Роль
`business` является обязательным SLM-модулем доменного пакета. Он владеет:
- публичными предметными сценариями;
- одним или несколькими именованными Domain API;
- одной публичной фабрикой для каждого API;
- типами явных зависимостей фабрик;
- предметными типами и детерминированными правилами;
- контрактами ожидаемых доменных ошибок;
- публичным представлением доменных данных и состояния.
Несколько API остаются частью одного модуля, пока относятся к одной связной предметной области. Они нужны не для копирования use cases, а для независимой сборки разных наборов сценариев, зависимостей и сред.
## Публичные фасеты
Один логический API модуля `business` имеет два обязательных и один необязательный entry point.
### Type-only barrel
Корневой `business/index.ts` экспортирует только типы:
```ts
export type {
AuthAdministrationApi,
AuthAdministrationDeps,
AuthAdministrationFactory,
AuthError,
AuthErrorCode,
AuthSessionApi,
AuthSessionDeps,
AuthSessionFactory,
AuthState,
} from './types'
```
Потребитель использует этот путь только через `import type`:
```ts
import type {
AuthSessionApi,
AuthState,
} from '@/domains/auth/business'
```
### Factory entry
`business/factory.ts` экспортирует только именованные runtime-фабрики:
```ts
export { authAdministrationFactory } from './factories/auth-administration.factory'
export { authSessionFactory } from './factories/auth-session.factory'
```
```ts
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
```
Каждому API соответствует одна фабрика. Фасет не экспортирует готовые instances, adapters или environment-specific assembly.
### Runtime entry
Необязательный `business/runtime.ts` экспортирует только публичные детерминированные значения и функции:
```ts
export {
AUTH_ERROR_CODES,
isAuthError,
} from './errors/auth-error'
export { normalizeAuthIdentifier } from './lib/normalize-auth-identifier'
```
Здесь допустимы error codes и guards, validators, value constructors, чистые продуктовые функции и immutable-константы. Все public types по-прежнему импортируются из корневого type-only barrel.
`business/runtime` не содержит:
- фабрики и готовые API instances;
- I/O или изменяемое состояние;
- state/query runtime;
- чтение clock, random, environment или platform API;
- сценарии, которым нужны runtime-зависимости.
Если внешним потребителям не нужен детерминированный runtime, файл `runtime.ts` не создаётся. Другие внешние пути внутри `business` являются deep imports.
## Несколько Domain API
```ts
export type AuthSessionApi = {
getCurrentSession: () => Promise<AuthState>
getSnapshot: () => AuthState
requestPhoneOtp: (phone: string) => Promise<void>
startInvalidationTracking: () => () => Promise<void>
verifyPhoneOtp: (code: string) => Promise<void>
}
export type AuthAdministrationApi = {
revokeUserSessions: (userId: string) => Promise<void>
}
```
`AuthSessionApi` может собираться в browser и request contexts, а `AuthAdministrationApi` только в доверенной server assembly. Browser assembly не получает метод-заглушку и не импортирует adapters административного API.
Общий `business/factory` является публичным фасетом, а не гарантией отдельного business chunk для каждой фабрики. Разделение API устраняет обязательное создание лишних adapters и instances. Если самой business-логике нужны независимо поставляемые bundle boundaries, требуется отдельный build/package mechanism за пределами текущего Level 2, а не искусственное разделение предметной области.
Один публичный сценарий принадлежит ровно одному API. Если два API постоянно требуют одинаковых методов, состояния и lifecycle, их граница пересматривается вместо дублирования.
Assembly может вернуть именованный граф нескольких API:
```ts
export type AuthBrowserGraph = Readonly<{
session: AuthSessionApi
}>
export type AuthRequestGraph = Readonly<{
administration: AuthAdministrationApi
session: AuthSessionApi
}>
```
Такой граф является составом готовых контрактов для контекста, а не новым предметным API.
## Предметная власть и состояние
Business API остаются единственной границей, определяющей предметную модель, validation, переходы и семантику результатов. Это не означает, что только `business` физически хранит байты.
Adapter или framework binding может использовать Zustand, TanStack Query, SWR, Apollo либо другой runtime для хранения и доставки значений. Такое хранилище является технической реализацией или проекцией, если:
- значения получены или проверены business API либо `business/runtime`;
- предметные переходы выполняются через business API;
- внешний DTO не становится публичной моделью напрямую;
- optimistic value создаётся или проверяется предметным владельцем;
- библиотечные cache/store types не становятся Domain API.
Подробная граница описана в [Состоянии и кэше](./state-cache.md).
## Потребители фасетов
| Потребитель | `business` | `business/factory` | `business/runtime` |
|---|---|---|---|
| Adapter своего домена | Type-only | Нет | Обычно нет |
| Assembly своего домена | Type-only | Да | При необходимости |
| Framework binding своего домена | Type-only | Нет | Да, если нужен public runtime |
| `composition` или `app` | Type-only | Для одноразовой сборки | Да |
| Код другого домена при связи с Level 2 | Type-only | Нет | Да |
| Тест | Type-only | По границе тестируемого API | По границе тестируемого владельца |
Runtime-импорт `business/runtime` другого домена остаётся архитектурным ребром и участвует в общей проверке циклов.
## Контракт ошибок
Ожидаемая ошибка имеет устойчивую безопасную readonly-форму:
```ts
export type AuthErrorCode =
| 'AUTH_PHONE_INVALID'
| 'AUTH_OTP_REQUEST_FAILED'
| 'AUTH_OTP_CODE_INVALID'
export type AuthError = Readonly<{
code: AuthErrorCode
}>
```
Если runtime-потребителям нужны constants или guard, они публикуются через `business/runtime`:
```ts
export const AUTH_ERROR_CODES = {
PHONE_INVALID: 'AUTH_PHONE_INVALID',
OTP_REQUEST_FAILED: 'AUTH_OTP_REQUEST_FAILED',
OTP_CODE_INVALID: 'AUTH_OTP_CODE_INVALID',
} as const
export const isAuthError = (value: unknown): value is AuthError => {
return isSafeCodedError(value, Object.values(AUTH_ERROR_CODES))
}
```
Guard не является обязательной частью каждого домена. При discriminated `Result` типизированному потребителю может быть достаточно error union; при exception, RPC или неизвестной runtime-границе guard часто нужен. Выбор `throw` или `Result` не меняет набор обязательных фасетов.
## Изоляция технических и чужих ошибок
Ошибки SDK, HTTP, database, storage и adapters не пересекают Domain API в форме, доступной приложению. `business` преобразует обрабатываемый технический сбой в собственный код:
```text
SDK error
→ adapter failure
→ business mapping
→ AuthErrorCode
→ приложение
```
Публичная форма не включает исходные `message`, `status`, `payload`, `cause`, SDK class или сам объект ошибки. Диагностические данные остаются во внутреннем observability-механизме.
То же относится к cross-domain вызову. Если User API использует Auth API, сбой, который становится результатом публичного сценария User, представлен собственным `UserError`. При exception-модели зависимый business может импортировать `isAuthError` и error codes из `auth/business/runtime`, после чего преобразовать ожидаемую ошибку в собственный контракт.
Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки.

View File

@@ -0,0 +1,114 @@
# Граница доменного пакета
> Пояснение контейнерной сущности Level 2 и её совместного использования с доменными модулями Level 1.
## Связанные правила
- [`SLM-L2-DOMAIN-R002`](../../rules/level-2.md#slm-l2-domain-r002)
- [`SLM-L2-DOMAIN-A003`](../../rules/level-2.md#slm-l2-domain-a003)
- [`SLM-L2-GROUP-R004`](../../rules/level-2.md#slm-l2-group-r004)
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
## Предметная граница
Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать business-сценарии авторизации, её browser/server assemblies и React-модули, но не страницу профиля или общий database client.
Пакет не владеет исполняемой ответственностью. Конкретными API, состоянием, зависимостями и lifecycle владеют модули внутри него.
Level 2 применяется к пакету, а не ко всему SLM root. Например, `auth` может быть пакетом Level 2, пока `catalog` и `news` остаются доменными модулями Level 1.
## Корень пакета
```text
domains/auth/
├── README.md
├── business/
├── assemblies/
├── adapters/
└── react/
```
В корне разрешены:
- документация;
- ownership metadata;
- декларативный manifest или декларативная конфигурация архитектурной проверки;
- обязательный модуль `business`;
- обязательная непустая Group `assemblies`;
- непустая Group `adapters`, если хотя бы одна фабрика имеет технические зависимости;
- Framework Groups при наличии соответствующих модулей.
В корне запрещены:
- `index.ts` или другой агрегирующий executable entry point;
- runtime-файлы и side effects;
- изменяемое состояние и ресурсы lifecycle;
- реэкспорт API внутренних модулей;
- page-specific компоненты или сборка нескольких доменов.
Metadata содержит только статические данные. Проверяющий инструмент может читать её декларативно, но она не исполняется и не становится скрытым API пакета.
## Policy boundary
Доменный пакет не является узлом import-графа. Его техническая граница состоит из объявленного проверке набора дочерних модулей и правил, применяемых ко всем связям через эту границу.
Отсутствие root barrel намеренно:
- client- и server-entry points не агрегируются в один импорт;
- каждый модуль сохраняет отдельную ответственность и environment boundary;
- переименование публичного модуля является изменением его собственного контракта, а не скрывается пакетом;
- versioning целого publishable package остаётся за пределами Level 2.
## Модули и Groups
`business` размещается непосредственно в пакете. Assemblies находятся в обязательной Group `assemblies`. Production adapters являются самостоятельными модулями Group `adapters`. Framework Group называется по фреймворку: `react`, `vue` и аналогично.
```text
auth/
├── business/ # SLM-модуль
│ ├── index.ts # Только public types
│ ├── factory.ts # Public factories entry
│ └── runtime.ts # Необязательный deterministic runtime
├── adapters/ # Group при наличии technical dependencies
│ └── phone-http/ # SLM-модуль
├── assemblies/ # Обязательная Group
│ └── browser/ # SLM-модуль
└── react/ # Framework Group
├── session/ # SLM-модуль
└── login-form/ # SLM-модуль
```
Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/business/factory`, опциональный `auth/business/runtime`, `auth/adapters/phone-http`, `auth/assemblies/browser` и `auth/react/session`, но не `auth`, `auth/adapters`, `auth/assemblies` или `auth/react`.
## Навигационные Groups
Слой `domains` может содержать Groups с обеими формами домена:
```text
domains/
└── commerce/ # Навигационная Group
├── catalog/ # Доменный модуль Level 1
└── orders/ # Доменный пакет Level 2
```
Такая Group отличается от Group внутри пакета допустимым составом: она содержит доменные модули, доменные пакеты и другие navigation Groups, а не внутренние модули пакета.
Одна предметная область не представляется одновременно модулем и пакетом. Переход завершается удалением старой границы именно этого домена, а не переводом всех соседних областей.
## Границы соседних слоёв
| Ответственность | Владелец |
|---|---|
| Предметные сценарии, Domain API, доменные ошибки | `business` |
| Техническая реализация зависимости одного домена | Adapter внутри пакета |
| Сборка API для именованного контекста | Assembly внутри пакета |
| Универсальный технический сервис | `infra` |
| Domain-specific framework API | Модуль внутри `react`, `vue` и аналогичной Group |
| Страница, маршрут, redirect, продуктовый текст | `compositions` |
| UI, объединяющий несколько доменов | `compositions` |
Зависимость от React сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.

View File

@@ -0,0 +1,158 @@
# Фабрики, зависимости и adapters
> Пояснение границы между `business` и технической средой.
## Связанные правила
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
## Одна фабрика на API
```text
явные зависимости + business factory → один Domain API
```
Модуль `business` предоставляет одну именованную фабрику для каждого объявленного API. Фабрики экспортируются через общий runtime-фасет `business/factory`:
```ts
import type {
AuthAdministrationApi,
AuthAdministrationDeps,
AuthSessionApi,
AuthSessionDeps,
} from '@/domains/auth/business'
export type AuthSessionFactory = (
deps: AuthSessionDeps,
) => AuthSessionApi
export type AuthAdministrationFactory = (
deps: AuthAdministrationDeps,
) => AuthAdministrationApi
```
```ts
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
```
Фабрика не выбирает environment, assembly или конкретную production-реализацию зависимости. Разные API могут иметь разные наборы deps и собираться независимо.
## Технические зависимости
Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы.
```ts
export type AuthPhoneDependency = {
requestCode: (phone: string) => Promise<unknown>
verifyCode: (code: string) => Promise<unknown>
}
```
`unknown` допустим на границе непроверенного внешнего результата. `business` проверяет его до превращения в предметные данные или состояние.
Техническими зависимостями также являются:
- concrete state/query runtime;
- subscription и event source;
- browser, Node.js и framework capabilities;
- request data и abort signal;
- текущее время и timer;
- random и ID generator;
- environment и runtime configuration provider.
```ts
export type VerificationDeps = {
clock: { now: () => number }
ids: { create: () => string }
timer: { delay: (ms: number) => Promise<void> }
}
```
`Date.now()`, `new Date()` без аргумента, `Math.random()`, `crypto.randomUUID()`, скрытый env и глобальный timer не читаются напрямую business-кодом, если влияют на поведение сценария. Детерминированная арифметика над переданным timestamp остаётся business-safe.
Cancellation также описывается business-owned контрактом. Concrete `AbortSignal` или другой platform type остаётся внутри adapter, пока не принят отдельный общий публичный контракт.
Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics.
## Cross-domain API dependency
Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
export type UserDeps = {
auth: Pick<AuthSessionApi, 'getSnapshot'>
}
```
Runtime-значение место сборки графа передаёт через assembly либо напрямую зависимой business-фабрике. `user/business` не импортирует executable factory, assembly или instance Auth.
Если exception-модели нужен runtime guard чужой ожидаемой ошибки, зависимый business может импортировать его из детерминированного `auth/business/runtime`. Этот импорт остаётся обычным ребром DAG.
## Adapter module
Adapter module соединяет явную техническую зависимость фабрики с конкретной системой:
```text
business dependency ← adapter → SDK / query runtime / platform / request data
```
Adapter преобразует аргументы и технический результат к контракту зависимости. Он не определяет публичный метод Domain API, предметный fallback или код доменной ошибки.
Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение не строит поведение по HTTP status, SDK error class или storage exception.
Adapter может использовать TanStack Query, Apollo, Zustand или другой state/query runtime, если реализует business-owned dependency. Library types, query keys и mutable clients не протекают в public types `business`.
## Размещение adapters
Каждая связная production-реализация является отдельным SLM-модулем в Group `adapters`:
```text
auth/adapters/
├── phone-http/
│ └── index.ts
├── browser-session/
│ └── index.ts
└── browser-runtime/
└── index.ts
```
Один adapter-модуль может реализовать несколько тесно связанных зависимостей одного technical provider. Например, `browser-runtime` может предоставить clock, timer и ID generator. Правило не требует отдельного модуля для каждого метода deps.
Adapter module имеет собственные ответственность, публичный API, environment label и тестовую границу. Group `adapters` не имеет `index.ts` и не реэкспортирует дочерние модули.
Production adapter запрещено определять:
- закрытым сегментом assembly;
- inline-функцией в `composition` или `app`;
- частью framework binding module;
- скрытой реализацией внутри `business`.
Assembly и одноразовое место сборки импортируют конкретные adapter-модули через их публичные API:
```ts
import { authSessionFactory } from '@/domains/auth/business/factory'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserRuntimeAdapter } from '@/domains/auth/adapters/browser-runtime'
const session = authSessionFactory({
phone: createPhoneHttpAdapter(),
runtime: createBrowserRuntimeAdapter(),
})
```
Если ни одна фабрика не имеет технических зависимостей, Group `adapters` не обязательна. Cross-domain API dependency не считается adapter и передаётся отдельно.
Локальные fake implementations в business-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код.

View File

@@ -0,0 +1,156 @@
# Framework Groups и модули
> Пояснение domain-specific framework-кода на примере React.
## Связанные правила
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-FRAMEWORK-R014`](../../rules/level-2.md#slm-l2-framework-r014)
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
## Framework Group
Папка для domain-specific React binding modules называется `react`:
```text
domains/auth/react/ # Framework Group
├── session/ # SLM-модуль
│ ├── hooks/
│ ├── providers/
│ └── index.ts
├── queries/ # SLM-модуль
│ └── index.ts
└── login-form/ # SLM-модуль
├── components/
└── index.ts
```
`react` является Group, а не модулем. У неё нет `index.ts`, состояния, реализации, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. Он не является вложенным модулем, потому что родительская граница `react` является Group.
## Framework binding module
Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании одного или нескольких готовых Domain API своего домена с конкретным framework. Сам факт зависимости от framework недостаточен: framework-specific assembly остаётся в `assemblies`, а page-specific модуль остаётся в `compositions`.
Framework binding module может:
- передавать готовые API через Provider и context;
- предоставлять domain-specific hooks;
- отображать состояние и безопасные ошибки домена;
- использовать framework-compatible state/query runtime;
- реализовывать переиспользуемую domain-specific форму или guard;
- связывать framework lifecycle с явными операциями Domain API.
Он не вызывает business-фабрику или assembly, не выбирает adapters и не создаёт новые предметные сценарии.
Framework binding импортирует типы и deterministic runtime через разные фасеты:
```ts
import type {
AuthError,
AuthSessionApi,
} from '@/domains/auth/business'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/runtime'
```
Импорт `business/factory` запрещён: готовые API передаются модулю извне.
## Модуль session
`auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthSessionApi`:
```tsx
'use client'
type AuthSessionProviderProps = PropsWithChildren<{
api: AuthSessionApi
}>
export const AuthSessionProvider = ({
api,
children,
}: AuthSessionProviderProps) => {
return (
<AuthSessionContext.Provider value={api}>
{children}
</AuthSessionContext.Provider>
)
}
```
Публичный путь модуля:
```ts
import {
AuthSessionProvider,
useAuthSession,
} from '@/domains/auth/react/session'
```
Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API.
## State/query runtime
`auth/react/queries` может использовать TanStack Query, SWR или другой React runtime поверх готового `AuthSessionApi`. Query cache остаётся framework projection, пока данные и transitions проходят через API, а optimistic values создаются или проверяются business-владельцем.
```ts
export const useAuthSessionQuery = () => {
const api = useAuthSession()
return useQuery({
queryKey: ['auth', 'session'],
queryFn: api.getCurrentSession,
})
}
```
Server prefetch и client hook могут быть разными модулями Framework Group с совместимыми environment markers. Общая query policy выносится в отдельный framework-модуль при наличии нескольких потребителей, а не дублируется в compositions.
Подробности описаны в [Состоянии и кэше](./state-cache.md).
## Модуль login-form
`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с Auth API, состоянием и ошибками своего домена. Она может использовать публичный API соседнего `auth/react/session`, если зависимость остаётся ацикличной.
Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Domain-owned guard может решить, разрешено ли действие, но политика перехода на `/login` остаётся у route composition.
## Запрет cross-domain framework imports
Framework binding module не импортирует hooks, contexts, Providers, components или framework state другого домена:
```ts
// Недопустимо: domains/user/react/profile
import { useAuthSession } from '@/domains/auth/react/session'
```
Cross-domain UI собирается в `compositions`:
```tsx
const session = useAuthSession()
return (
<UserProfile
userId={session.userId}
canEdit={session.isAuthenticated}
/>
)
```
Передача через props является границей композиции, но не требует prop drilling внутри домена: каждый пакет может использовать собственный Provider и context. Если User business постоянно нуждается в Auth, готовый `AuthSessionApi` передаётся User factory при сборке графа, а User framework module работает уже со своим API.
## Публичные API
```ts
import { AuthSessionProvider } from '@/domains/auth/react/session'
import { LoginForm } from '@/domains/auth/react/login-form'
```
Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена.

View File

@@ -0,0 +1,52 @@
# Открытые вопросы Level 2
> Эти вопросы не являются правилами и не отменяют зафиксированные границы.
## Зафиксированные решения
- Level 1 является общей базой, а Level 2 применяется отдельно к выбранным предметным областям.
- Доменный модуль Level 1 и доменный пакет Level 2 могут постоянно сосуществовать в одном SLM root.
- Одна предметная область имеет только одну форму.
- Корень пакета содержит только metadata, модуль `business` и допустимые Groups и не имеет executable API.
- `business` может объявлять несколько независимо собираемых Domain API и одну фабрику для каждого API.
- Публичный API `business` имеет обязательные type-only `business` и runtime `business/factory`, а также необязательный deterministic `business/runtime`.
- Приложение получает предметную модель, transitions и результаты через Domain API; technical и framework cache могут хранить и проецировать эти значения.
- Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена в исходной форме.
- Каждый доменный пакет содержит минимум одну assembly; универсальная изоморфная assembly не обязательна.
- Assembly может создавать именованный граф нескольких API и не добавляет к ним сценарии.
- При наличии технических зависимостей Group `adapters` обязательна, а каждая связная production implementation принадлежит adapter-модулю.
- Runtime cross-domain imports через пакетную границу разрешены только из `business/runtime`; type-only public contracts разрешены и входят в DAG.
- Framework Group называется по фреймворку и содержит самостоятельные SLM-модули.
- Cross-domain framework state, hooks, contexts и components не импортируются.
- Clock, timer, random, ID generator и environment являются явными dependencies business.
- Assembly без собственного lifecycle-ресурса не обязана возвращать пустой `dispose`.
## Владение состоянием
Нужно подробнее определить создание initial state, применение transitions, persistence и внешний event input. Нормативным уже является то, что предметную модель и переходы определяет `business`, а concrete state manager реализует business-owned port либо framework projection.
Отдельно требуется проверить SSR snapshot, hydration, concurrent rendering, reset и поведение после завершения request scope.
## Передача ошибок
Нужно выбрать общую рекомендацию exception или discriminated `Result`, определить cancellation и unexpected failures, а также сериализацию ошибок через RPC и server actions.
Структура публичных фасетов от этого решения больше не зависит: type errors публикуются через `business`, а необходимые runtime codes и guards через опциональный `business/runtime`.
## Технические порты
Нужно определить, является ли consumer-owned port обязательной формой каждой технической зависимости и какие behavioral guarantees он описывает: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
Cross-domain API dependency уже зафиксирована как отдельный вид зависимости и не зависит от этого решения.
## Lifecycle сборки
Уже зафиксировано, что явная операция, запускающая ресурс, возвращает cleanup, а assembly с собственным ресурсом предоставляет cleanup handle результата. Ещё нужно определить async cleanup, rollback частичной сборки, repeated disposal, request abort и поведение API после завершения scope.
## Cache hydration
Нужно проверить единый способ разделять framework-neutral cache policy, client hooks, server prefetch и serialization boundary без утечки query-library types в Domain API.
## Автоматическая проверка
Нужно выбрать machine-readable формат для форм домена, metadata, модулей, Groups, entry points, environment labels и запрещённых транзитивных импортов.

View File

@@ -0,0 +1,114 @@
# Состояние и кэш
> Пояснение границы между предметной властью business и техническими state/query runtimes.
## Связанные правила
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
## Библиотеки не запрещены
Запрет state/query manager в import-графе `business` не запрещает TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX или RxJS в доменном пакете. Он запрещает concrete runtime становиться предметным контрактом.
Такая библиотека может находиться:
- в adapter-модуле, если реализует техническую зависимость business-фабрики;
- в framework binding module, если доставляет готовый Domain API конкретному framework;
- в composition, если состояние принадлежит только её UI scope и не подменяет доменную модель.
## Три вида состояния
### Предметное состояние
Модель фактов и переходов домена. `business` определяет initial value, validation, допустимые команды, transitions и selectors. Concrete store может быть передан фабрике как business-owned port:
```ts
export type AuthStateDependency = {
create: (initial: AuthState) => {
get: () => AuthState
set: (state: AuthState) => void
subscribe: (listener: () => void) => () => void
}
}
```
Adapter поверх Zustand или другого manager реализует этот контракт, но не выбирает initial state и не добавляет переходы.
### Technical source cache
Кэш SDK, HTTP, GraphQL или storage-вызовов. Adapter может использовать QueryClient, Apollo cache или иной runtime для deduplication, transport retry и хранения внешних результатов. Business по-прежнему проверяет и преобразует результат до публикации предметной модели.
Query keys и библиотечные result types остаются внутри adapter. Domain API не экспортирует `UseQueryResult`, `ApolloError`, raw DTO или mutable QueryClient.
Если technical cache создаёт timers, subscriptions или другой lifecycle-ресурс для всего графа, владеющая им assembly передаёт cleanup graph owner. Cache без такого ресурса не требует искусственного `dispose`.
### Framework projection cache
Кэш, который framework binding строит поверх вызовов готового Domain API для rendering, Suspense, revalidation или UI coordination.
```ts
const useProfile = () => {
const api = useUserApi()
return useQuery({
queryKey: ['user', 'profile'],
queryFn: api.getProfile,
})
}
```
Такой cache не является параллельным предметным источником, пока его значения происходят из Domain API и все предметные изменения выполняются через API.
## Invalidation и retry
Не каждая cache policy является бизнес-правилом.
| Политика | Обычный владелец |
|---|---|
| Query key, stale time, deduplication, background refetch | Adapter или framework binding |
| Rendering stale data, Suspense, polling UI | Framework binding или composition |
| Transport retry безопасного запроса | Adapter |
| Запрет повторной предметной команды | `business` |
| Cooldown, лимит попыток, допустимый transition | `business` |
| Freshness, влияющая на корректность сценария | `business` через явный контракт |
Если после команды требуется invalidation, framework binding может связать успешный результат API с техническими keys. Если выбор invalidation выражает предметную семантику, business возвращает устойчивый outcome/event либо публикует чистое правило через `business/runtime`; binding только отображает его на библиотечные операции.
## Optimistic updates
Framework binding не конструирует произвольную предметную модель из input формы или transport DTO. Optimistic update допустим, когда предполагаемое значение:
- возвращено командой Domain API как безопасная projection;
- создано отдельным pure-методом Domain API;
- создано или проверено публичной функцией `business/runtime`.
```ts
const optimisticProfile = projectProfileUpdate(currentProfile, command)
queryClient.setQueryData(profileKey, optimisticProfile)
```
Здесь `projectProfileUpdate` принадлежит `business/runtime`, а `setQueryData` остаётся технической операцией framework binding.
Запись raw form data или DTO напрямую в публичный product cache обходит предметного владельца и нарушает границу.
## Browser, SSR и RSC
Client hook, server prefetch и hydrate/dehydrate могут принадлежать разным framework binding modules с совместимыми environment entry points. Общая framework-specific cache policy выносится в отдельный модуль той же Framework Group, если у неё несколько реальных потребителей.
`compositions` вызывает публичный server binding для prefetch и публичный client binding для rendering. Она не повторяет query keys и mapping доменных результатов.
## Проверка на ревью
Для каждого state/query runtime определяется:
- является ли он adapter, framework projection или локальным UI state;
- откуда поступают значения;
- кто определяет transition и optimistic projection;
- где находятся library-specific types и keys;
- как invalidation соотносится с результатами Domain API;
- соответствует ли cache lifecycle области жизни API и framework scope.

View File

@@ -0,0 +1,91 @@
# Тестирование доменного пакета
> Проверка владельцев и публичных границ Level 2.
## Связанные правила
- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016)
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
## Размещение
Тест находится рядом с модулем-владельцем проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`.
| Проверяемая граница | Владелец теста |
|---|---|
| Сценарии, Domain API, данные и ошибки | `business` |
| Публичная pure-функция или guard | `business/runtime` внутри тестов модуля business |
| Техническое преобразование | Adapter |
| Выбор API, dependencies и environment boundary | Assembly |
| Provider, hook, query integration, form или guard | Соответствующий framework binding module |
| Граф нескольких доменов | Модуль `composition`, точка входа `app` или другое место сборки |
## Business через фабрику
Каждый публичный сценарий проверяется через фабрику владеющего им API с управляемыми dependencies:
```ts
import type { AuthSessionApi } from '@/domains/auth/business'
import { authSessionFactory } from '@/domains/auth/business/factory'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/runtime'
const api: AuthSessionApi = authSessionFactory(createAuthSessionTestDeps({
clock: { now: () => 1_700_000_000_000 },
phone: { requestCode: async () => ({ ok: true }) },
}))
await api.requestPhoneOtp('+79991112233')
```
Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от `throw` или `Result`, но наружу проверяется только собственный AuthErrorCode.
Business-тест не использует React, реальный SDK, database, production assembly или системные clock/random. Локальная fake implementation допустима в тесте и не становится adapter-модулем, потому что не входит в production graph.
Если `business` объявляет несколько API, каждый тестируется через свою фабрику. Общая внутренняя pure-логика не требует повторять одинаковые cases на уровне всех API.
## Остальные модули
Тест adapter-модуля проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки.
Тест обязательной assembly проверяет:
- вызов только нужных business-фабрик;
- точный именованный состав возвращённого графа;
- выбор публичных adapter-модулей;
- отсутствие несовместимого environment-кода;
- передачу cross-domain API аргументом, а не импортом;
- cleanup handle, если assembly создаёт ресурс жизненного цикла.
Assembly без собственного lifecycle-ресурса не тестирует пустой `dispose`, потому что не обязана его предоставлять.
Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthSessionApi` и проверяет Provider, hook или component. Query binding дополнительно проверяет keys, invalidation и отсутствие raw DTO в публичном результате, но не повторяет полный набор business-сценариев.
## Автоматические структурные проверки
Проверка файлов, exports и import-графа подтверждает:
- отсутствие root API доменного пакета и Framework Groups;
- обязательные `business` и `business/factory`, type-only exports в корневом barrel и допустимый опциональный `business/runtime`;
- соблюдение матрицы потребителей фасетов business;
- наличие непосредственно в корне пакета непустой Group `assemblies` с объявленными модульными границами;
- отсутствие запрещённых runtime cross-domain imports;
- отсутствие type-only импортов из чужих assemblies, adapters и framework-модулей;
- отсутствие cross-domain framework hooks, contexts и components;
- отсутствие server-only достижимости из client modules;
- отсутствие runtime- и type-only циклов.
## Архитектурное ревью
На ревью проверяется, что `business/factory` экспортирует только объявленные фабрики, а `business/runtime` при наличии содержит только публичный deterministic runtime. Для каждой технической зависимости рассматриваются все production implementations: каждая связная implementation должна принадлежать одному модулю Group `adapters`, но один такой модуль может реализовать несколько тесно связанных capabilities одного provider.
Отдельно проверяются прямые обращения business к `Date.now`, `Math.random`, `crypto.randomUUID`, timers, env и другим скрытым runtime-capabilities.
Runtime-тест не заменяет автоматическую проверку или архитектурное ревью.

View File

@@ -0,0 +1,147 @@
# Терминология Level 2
> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
Level 2 наследует терминологию и матрицу слоёв Level 1. Он применяется отдельно к предметным областям, которым нужна пакетная форма, и не требует переводить остальные доменные модули того же SLM root.
## Формы домена
### Форма домена
Структурное представление одной доменной ответственности. На Level 1 предметная область представлена доменным модулем. Для предметной области, использующей Level 2, этот модуль заменяется доменным пакетом. Одна предметная область не использует обе формы одновременно.
### Доменный пакет
Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла графа зависимостей.
Корень пакета может содержать только декларативную metadata, обязательный модуль `business` и допустимые Groups. Metadata хранит статические данные о пакете, владении либо конфигурации проверки и не содержит кода, выполняемого приложением.
Доменный пакет является policy boundary: проверка использует объявленную принадлежность модулей пакету для контроля структуры и междоменных связей. Публичными dependency boundaries кода остаются модули внутри пакета, а не пакет целиком.
### Навигационная Group слоя `domains`
Group, размещённая непосредственно в слое `domains` или другой такой Group. Она классифицирует доменные модули Level 1, доменные пакеты Level 2 и другие навигационные Groups, но не содержит исполняемый код и не образует dependency boundary.
### Модуль доменного пакета
Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: business, assembly, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел графа зависимостей.
Модуль внутри Group пакета не является вложенным модулем, потому что его ближайшая внешняя граница не является модулем.
## Business
### Модуль business
Обязательный SLM-модуль `business`, который определяет публичные предметные сценарии, один или несколько именованных Domain API, соответствующие им фабрики, типы зависимостей и публичные контракты ожидаемых ошибок.
`business` является предметным владельцем данных, моделей, переходов и результатов сценариев домена. Он не зависит от конкретного фреймворка, среды или технической реализации.
### Публичные фасеты business
Объявленные entry points одного логического публичного API модуля `business`:
| Путь | Статус | Содержимое |
|---|---|---|
| `business` | Обязательный | Только public types, включая Domain API, зависимости, factory types и error types |
| `business/factory` | Обязательный | Только именованные runtime-фабрики Domain API |
| `business/runtime` | Необязательный | Только публичные детерминированные runtime-значения и функции |
Фасет `business/runtime` может публиковать устойчивые error codes и guards, validators, value constructors, предметные константы и чистые функции, если они нужны реальным внешним потребителям. Он не содержит фабрики, изменяемое состояние, ввод-вывод, сценарии с runtime-зависимостями или environment-specific код.
Фасеты не являются сегментами, вложенными модулями или самостоятельными узлами графа. Любой другой внешний путь внутрь `business` является deep import.
### Business-safe внешний пакет
Внешняя библиотека, допустимая в import-графе `business`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, query runtime, framework и техническая интеграция не становятся business-safe только из-за совместимости с несколькими средами.
### Domain API
Именованный публичный runtime-контракт связного набора предметных сценариев внутри одного домена. Модуль `business` может объявить несколько Domain API, если они независимо собираются, имеют разные технические зависимости или нужны разным средам и потребителям.
Разделение на API не создаёт новые доменные пакеты и не разрешает дублировать один сценарий в нескольких контрактах. Каждый публичный сценарий принадлежит ровно одному Domain API.
### Фабрика business
Публичная функция фасета `business/factory`, которая получает явные зависимости и создаёт экземпляр одного объявленного Domain API. Каждому Domain API соответствует ровно одна публичная фабрика. Фабрика не выбирает assembly или среду выполнения.
### Предметная власть business
Право определять публичную доменную модель, допустимые переходы, валидацию внешних значений и семантику результатов. Adapter, assembly или framework binding может хранить, кэшировать и проецировать значения Domain API, но не становится независимым источником предметной модели или переходов.
### Доменная ошибка
Безопасная публичная форма ожидаемого сбоя предметного сценария. Модуль `business` объявляет устойчивый readonly-тип с кодом; при необходимости runtime-коды и guards публикуются через `business/runtime`. Ошибки SDK, транспорта, storage, adapter или другого домена не являются доменными ошибками текущего API.
Способ передачи ошибки, например exception или discriminated `Result`, не изменяет владение и публичную безопасность ошибки.
## Техническая сборка
### Техническая зависимость
Явная runtime-возможность, необходимая business-фабрике и требующая production-реализации. К техническим зависимостям относятся источники данных, SDK, storage, API платформы, state/query runtime, технические сервисы, данные request scope, clock, timer, random, ID generator и другие источники ввода-вывода, состояния или недетерминизма.
Type-only cross-domain API dependency, неизменяемая конфигурация и аргумент отдельного предметного сценария не являются техническими зависимостями.
### Adapter
SLM-модуль в Group `adapters`, который реализует одну или несколько связанных технических зависимостей business-фабрик поверх SDK, storage, state/query runtime, API платформы, данных запроса или технического сервиса. Одна связная production-реализация принадлежит одному adapter-модулю и не размещается внутри assembly или composition.
Group `adapters` обязательна и непуста, если хотя бы одна business-фабрика имеет техническую зависимость. Фабрики без технических зависимостей не требуют создания этой Group.
Точная обязательная поведенческая форма технических портов пока не определена и остаётся открытым вопросом Level 2.
### Assembly
SLM-модуль в Group `assemblies`, который создаёт явный именованный граф одного или нескольких Domain API пакета для одного реального контекста выполнения: браузера, запроса, server action или другого окружения. При наличии технических зависимостей assembly выбирает их adapter-модули и передаёт фабрикам готовые runtime-зависимости.
Assembly может принимать готовые API других доменов аргументами. Она не добавляет предметные сценарии, методы API или собственные доменные ошибки.
Каждый доменный пакет содержит минимум одну assembly. Архитектура не ограничивает их максимальное количество и не требует универсальной изоморфной assembly.
### Ресурс assembly
Ресурс жизненного цикла, который assembly сама создаёт для возвращаемого графа API. Создание фабрики или assembly не запускает скрытую долгоживущую работу. Если технический ресурс необходимо активировать при сборке, публичный результат assembly предоставляет cleanup handle; если ресурс запускается явной операцией API, эта операция предоставляет cleanup.
Assembly без собственного ресурса жизненного цикла не обязана возвращать пустой `dispose`.
## Framework binding
### Framework Group
Group доменного пакета, названная по конкретному фреймворку: `react`, `vue` и аналогично. Она содержит framework binding modules, не имеет собственного `index.ts`, реализации, состояния, жизненного цикла или публичного API.
### Framework binding module
SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией с фреймворком. Например, `react/session` может владеть Provider и hooks сессии, а `react/login-form` - переиспользуемой формой авторизации.
Framework binding module получает готовые Domain API, не вызывает фабрику или assembly и не импортирует framework-состояние, hooks или компоненты другого домена. Он может использовать совместимые с его ролью state/query libraries и технически кэшировать результаты Domain API, не создавая новую предметную модель.
## Сборка графа
### Место сборки графа
Код модуля `composition`, точки входа `app`, request handler или test setup, который создаёт assemblies либо напрямую вызывает фабрики в ацикличном порядке и передаёт уже созданные API зависимым сборкам.
Место сборки владеет областью жизни совокупного графа и вызывает предоставленные cleanup handles не позже её завершения. Это координационное владение не переносит к нему предметные или технические ответственности внутренних модулей.
### Граница среды выполнения
Граница между import-графами, предназначенными для клиента, сервера или обеих сред. Она определяется транзитивной достижимостью импортов, а не только именем папки или tree shaking.
## Структурная модель
```text
SLM root
└── domains
├── доменный модуль Level 1
└── доменный пакет Level 2
├── metadata
├── модуль business
├── обязательная Group assemblies
│ └── assembly-модуль
├── Group adapters при наличии технических зависимостей
│ └── adapter-модуль
└── Framework Group react
├── модуль session
└── модуль login-form
```

View File

@@ -0,0 +1,81 @@
# Проверка Level 2
> Граница автоматической проверки, архитектурного ревью и тестирования Level 2.
## Конфигурация проекта
Конфигурация проверки сопоставляет физические пути с доменными модулями Level 1, доменными пакетами Level 2, metadata, SLM-модулями, Groups, фасетами `business`, техническими зависимостями, adapter-модулями, assemblies, публичными точками входа, метками сред выполнения и allowlist внешних business-safe пакетов.
Формат такой конфигурации пока не выбран. Независимо от формата проверка анализирует объявленные границы и import-граф, а не угадывает сущность только по имени папки.
## Автоматическая проверка
Автоматическая проверка блокирует:
- одновременное объявление одной предметной области доменным модулем и пакетом;
- исполняемый файл, root `index.ts`, состояние или реэкспорт в корне доменного пакета;
- отсутствие `business` или несколько модулей `business` в одном пакете;
- отсутствие `business` либо `business/factory`, runtime export из корневого barrel, export не-фабрики из `business/factory`, type export из `business/runtime`, другой публичный путь либо deep import внутри `business`;
- импорт фасета `business` потребителем, которому этот фасет не разрешён;
- отсутствие непосредственно в корне пакета непустой Group `assemblies` или наличие в ней прямого дочернего элемента без объявленной модульной границы;
- deep imports во внутренние части модулей;
- runtime- или type-only достижимость framework-, adapter-, assembly-, infra- или environment-specific кода из `business`;
- запрещённый runtime-импорт через границу пакета Level 2;
- type-only импорт не из публичной точки входа владельца;
- импорт framework state, hooks, contexts или components другого домена;
- достижимость server-only кода из client-entry point и обратное несовместимое направление;
- runtime- или type-only циклы в графе модулей.
Статический анализ может отдельно находить прямые вызовы `Date.now`, `Math.random`, `crypto.randomUUID`, timers и чтение env внутри `business`. Окончательное решение о скрытом недетерминизме остаётся за ревью.
## Архитектурное ревью
На ревью определяется:
- представляет ли пакет одну связную предметную область;
- принадлежит ли каждая соседняя область ровно одной форме независимо от форм других доменов;
- принадлежат ли публичные сценарии ровно одному из именованных Domain API;
- оправдано ли разделение API разными consumers, dependencies или assemblies, а не техническим дроблением;
- остаются ли модель, validation и transitions под предметной властью `business`;
- не создаёт ли state/query cache параллельную продуктовую модель или raw DTO boundary;
- соответствует ли каждой фабрике ровно один API и остаётся ли она environment-neutral;
- содержит ли `business/runtime` только реально публичные deterministic values и functions;
- преобразует ли business ожидаемые technical и cross-domain сбои в собственные ошибки;
- является ли каждая связная production-реализация технических dependencies отдельным модулем Group `adapters`;
- представляет ли каждая assembly один реальный контекст выполнения и возвращает ли точный именованный граф;
- не запускают ли фабрики и assemblies скрытую долгоживущую работу при создании графа;
- предоставляет ли assembly cleanup только для действительно созданного ею lifecycle-ресурса и вызывает ли graph owner этот cleanup;
- принадлежит ли каждый framework binding module домену, а не странице или multi-domain сценарию;
- соответствует ли каждый объявленный business-safe внешний пакет ограничениям детерминированной библиотеки без runtime capability;
- остаются ли Framework Groups и другие Groups без реализации и агрегирующего API.
## Тестирование
Business-сценарии проверяются через соответствующие фабрики с управляемыми test fakes, включая fake clock/random/id при необходимости. Adapter module проверяет technical transformation. Assembly проверяет состав графа, выбор adapters, environment boundary и условный cleanup. Framework binding module проверяет собственный Provider, hook, cache integration или component без повторения полного набора business-сценариев.
Import-graph checks не заменяются runtime-тестами.
## Смешанный SLM root
Наличие доменных модулей Level 1 рядом с пакетами Level 2 является завершённым допустимым состоянием. Проверка применяет правила формы отдельно к каждой предметной области и правила Level 2 ко всем статическим связям, пересекающим пакетную границу.
Переход одного домена завершается, когда его старая модульная граница удалена и checker видит только пакет. Другие домены не входят в критерий завершения.
## Связанные правила
- [`SLM-L2-DOMAIN-A003`](../rules/level-2.md#slm-l2-domain-a003)
- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-a007)
- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013)
- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026)
- [`SLM-L2-BUSINESS-R018`](../rules/level-2.md#slm-l2-business-r018)
- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
- [`SLM-L2-ASSEMBLY-A020`](../rules/level-2.md#slm-l2-assembly-a020)
- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
- [`SLM-L2-ASSEMBLY-R023`](../rules/level-2.md#slm-l2-assembly-r023)
- [`SLM-L2-BUSINESS-R024`](../rules/level-2.md#slm-l2-business-r024)
- [`SLM-L2-BUSINESS-R025`](../rules/level-2.md#slm-l2-business-r025)
- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.

View File

@@ -0,0 +1,128 @@
# Правила SLM
> Статус: системный черновик. Не является нормативной спецификацией.
Эта директория является единственным местом объявления правил SLM. Остальные черновики объясняют архитектуру и ссылаются на канонические коды, но не повторяют формулировки правил.
## Что считается правилом
Правило задаёт один блокирующий архитектурный инвариант.
Определения, рекомендации, разрешения, примеры и открытые вопросы не получают код правила.
Нормативные определения объявляются в терминологии соответствующего уровня. Они обязательны для толкования правил, но нормативность определения сама по себе не превращает его в правило.
## Код правила
```text
SLM-L{level}-{group}-{class}{number}
```
| Часть | Значение |
|---|---|
| `SLM` | Принадлежность архитектуре SLM |
| `L{level}` | Уровень архитектуры |
| `group` | Раздел правил |
| `class` | Способ проверки: `A` или `R` |
| `number` | Трёхзначный номер внутри уровня |
## Способы проверки
### `A`: автоматическая проверка
Всё правило можно однозначно проверить программно без понимания предметного смысла кода. Нарушение такого правила должно блокировать автоматическую проверку.
### `R`: проверка на ревью
Для окончательного решения требуется понимание ответственности, владения или смысла зависимости. Линтер может проверять отдельные признаки, но не заменяет решение на ревью.
Одно правило не разделяется на автоматическую и ручную копии только из-за разных способов проверки. Если существенная часть инварианта требует смыслового решения, всё правило получает класс `R`.
## Разделы правил
| Код | Раздел |
|---|---|
| `LAYER` | Слои |
| `DEPENDENCY` | Зависимости |
| `MODULE` | Модули |
| `GROUP` | Группы |
| `SEGMENT` | Сегменты |
| `COMPONENT` | Компоненты |
| `NESTED_MODULE` | Вложенные модули |
| `LIFECYCLE` | Жизненный цикл |
| `DOMAIN` | Домены |
| `BUSINESS` | Контракты бизнес-логики |
| `FACTORY` | Фабрики бизнес-логики |
| `ERROR` | Ошибки домена |
| `PORT` | Порты бизнес-логики |
| `ADAPTER` | Адаптеры |
| `ASSEMBLY` | Сборка API и жизненный цикл |
| `ENVIRONMENT` | Границы сред выполнения |
| `FRAMEWORK` | Модули фреймворков |
| `TEST` | Тестирование |
Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования.
## Формат записи
```md
### SLM-L1-MODULE-A004
> **Публичный API модуля**
>
> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API.
```
Код является заголовком третьего уровня и автоматически получает адрес для ссылки `#slm-l1-module-a004`.
Название и описание входят в одну цитату. Название выделяется жирным и служит кратким именем правила. Описание полностью формулирует требование и занимает одну физическую строку.
Ссылка из тематического черновика:
```md
[`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004)
```
## Как формулировать правила
1. Правило понятно без чтения тематической главы и опирается только на нормативные термины своего уровня.
2. Правило защищает один архитектурный инвариант.
3. Один инвариант получает один код независимо от числа участников и способов проверки.
4. Название является кратким и устойчивым именем правила.
5. Название обозначает предмет правила, а описание полностью формулирует требование.
6. Описание объясняет допустимую границу и то, что считается нарушением.
7. Описание раскрывает названный инвариант и не вводит второе независимое требование.
8. Описание использует нормативные определения и не пересказывает их без необходимости.
9. Название и описание используют человеческий язык и только необходимые архитектурные термины.
10. Обоснование, подробности, примеры и исключения размещаются в тематическом черновике, а не в описании.
11. Правило не создаётся отдельно с позиции владельца и потребителя, если обе формулировки защищают одну границу.
12. Перед добавлением правила реестр проверяется на дубли и противоречия.
13. Код присваивается после проверки правила на примерах и контрпримерах.
## Нумерация
1. Номер уникален внутри уровня независимо от раздела и способа проверки.
2. Номер не обозначает важность или порядок выполнения.
3. Удалённый номер не переиспользуется для другого правила.
4. При изменении способа проверки номер сохраняется, но меняется полный код.
## Проверка качества
Перед принятием правила нужно ответить «да»:
- Понятно, о чём правило?
- Название кратко и однозначно называет правило?
- Понятно, что оно требует?
- Понятно, что является нарушением?
- Нельзя ли объединить его с существующим правилом?
- Не содержит ли оно рекомендацию или разрешение?
- Соответствует ли класс способу окончательной проверки?
## Проверка документов
Корневой скрипт `draft-rules.js` читает объявления только из этой директории, проверяет формат и уникальность кодов, валидирует ссылки из остальных черновиков и выводит правила разделами «Автоматические» и «Для ревью». Скрипт проверяет документы, а не архитектуру приложения.
## Наборы правил
- [Первый уровень](./level-1.md)
- [Второй уровень](./level-2.md)

View File

@@ -0,0 +1,110 @@
# Правила SLM первого уровня
Здесь собраны правила первого уровня. Это единственное место, где они формулируются; тематические черновики объясняют их и ссылаются на коды.
## Размещение кода по слоям
### SLM-L1-LAYER-R001
> **Назначение слоёв**
>
> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода.
### SLM-L1-LAYER-A002
> **Направление зависимостей**
>
> Внутри одного SLM root код каждого слоя может зависеть только от кода целевых слоёв, разрешённых для него нормативной матрицей слоёв.
### SLM-L1-LAYER-R003
> **Граница слоя `app`**
>
> В `app` размещаются только точки входа фреймворка для запуска, маршрутов, преобразования входных данных и подключения публичных API модулей разрешённых слоёв или ресурсов `shared`; ответственности этих модулей остаются за пределами `app`.
## Границы модулей
### SLM-L1-MODULE-A004
> **Публичный API модуля**
>
> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API.
### SLM-L1-MODULE-A014
> **Папка модуля**
>
> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки.
### SLM-L1-MODULE-R006
> **Ответственность модуля**
>
> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях.
### SLM-L1-MODULE-R011
> **Владелец ответственности**
>
> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`.
### SLM-L1-MODULE-R012
> **Состав публичного API**
>
> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми.
## Зависимости между модулями
### SLM-L1-DEPENDENCY-A005
> **Циклические зависимости**
>
> Граф зависимостей модулей внутри одного SLM root, включая вложенные модули, не содержит циклов.
## Назначение групп
### SLM-L1-GROUP-R007
> **Назначение группы**
>
> Группа содержит только модули и другие группы, не владеет файлами реализации, состоянием, жизненным циклом или публичным API и не импортируется внешним кодом.
## Назначение сегментов
### SLM-L1-SEGMENT-R008
> **Граница сегмента**
>
> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или узла графа зависимостей.
## Ответственность компонентов
### SLM-L1-COMPONENT-R009
> **Ответственность компонента**
>
> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу.
## Границы вложенных модулей
### SLM-L1-NESTED_MODULE-A010
> **Доступ к вложенному модулю**
>
> Код за пределами родительского модуля не импортирует вложенный модуль напрямую и получает его экспорты только через публичный API родителя.
## Жизненный цикл
### SLM-L1-LIFECYCLE-R013
> **Жизненный цикл ресурсов**
>
> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
## Граница доменных модулей
### SLM-L1-DOMAIN-R015
> **Доменный модуль**
>
> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие ей модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей.

View File

@@ -0,0 +1,171 @@
# Правила SLM второго уровня
Доменный пакет Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. Для предметной области в пакетной форме `SLM-L1-DOMAIN-R015` заменяется правилами доменного пакета; остальные предметные области того же SLM root могут сохранять форму доменного модуля Level 1. `SLM-L1-GROUP-R007` сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Для модуля `business` правило `SLM-L1-MODULE-A004` уточняется правилом `SLM-L2-BUSINESS-A019`: объявленные фасеты вместе образуют один логический публичный API и не считаются deep imports. Ранее использовавшиеся номера `SLM-L2-DOMAIN-R001` и `SLM-L2-MIGRATION-A017` не переиспользуются после изменения модели уровней и перехода к постоянному совместному применению форм.
## Граница доменного пакета
### SLM-L2-DOMAIN-R002
> **Предметная граница пакета**
>
> Каждая предметная область, использующая пакетную форму Level 2, представлена ровно одним доменным пакетом; все его модули относятся только к этой предметной области.
### SLM-L2-DOMAIN-A003
> **Корень доменного пакета**
>
> Корень доменного пакета содержит только декларативную metadata, модуль `business` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов.
### SLM-L2-GROUP-R004
> **Навигационная Group доменов**
>
> Group, расположенная непосредственно в слое `domains` или другой такой Group, содержит только доменные модули Level 1, доменные пакеты Level 2 и навигационные Groups и не владеет реализацией, состоянием, жизненным циклом или публичным API.
## Business и Domain API
### SLM-L2-BUSINESS-R005
> **Модуль business**
>
> Каждый доменный пакет содержит ровно один модуль `business`, который владеет публичными предметными сценариями и объявляет один или несколько именованных Domain API этой предметной области.
### SLM-L2-BUSINESS-R006
> **Предметная власть business**
>
> Adapters, assemblies и framework binding modules транспортируют, кэшируют или проецируют доменные данные только в форме, произведённой или проверенной публичным API либо детерминированным runtime модуля `business`, и не определяют независимую предметную модель, переход или результат сценария.
### SLM-L2-BUSINESS-A007
> **Импортная замкнутость business**
>
> Все runtime- и type-only импорты `business`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как business-safe.
### SLM-L2-FACTORY-R008
> **Фабрики Domain API**
>
> Каждому публичному Domain API соответствует ровно одна именованная фабрика фасета `business/factory`; фабрика получает явные runtime-зависимости, создаёт только этот API и не выбирает assembly или среду выполнения.
## Ошибки домена
### SLM-L2-ERROR-R009
> **Публичный контракт ошибок**
>
> Каждый ожидаемый сбой публичного предметного сценария представлен именованным readonly-типом собственной доменной ошибки с устойчивым кодом; тип экспортируется через type-only barrel, а необходимые внешним потребителям runtime-коды и guards только через `business/runtime`.
### SLM-L2-ERROR-R010
> **Изоляция исходных ошибок**
>
> Сбой adapter, SDK, транспорта, storage или другого домена, доступный приложению через Domain API, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause.
## Assemblies и зависимости
### SLM-L2-ASSEMBLY-R011
> **Роль assembly**
>
> Каждая assembly является SLM-модулем одного именованного контекста выполнения, выбирает необходимые adapter-модули, вызывает одну или несколько business-фабрик и возвращает явный именованный граф их API, не добавляя предметные сценарии, методы API или собственные доменные ошибки.
### SLM-L2-DEPENDENCY-A012
> **Междоменные импорты Level 2**
>
> Статическая связь между разными доменными границами, хотя бы одна из которых является пакетом Level 2, допускает только type-only импорт публичного API доменного модуля Level 1 или корневого `business` пакета Level 2 либо runtime-импорт `business/runtime` пакета Level 2; все такие связи входят в общий ацикличный граф, а остальные runtime-экспорты другого домена не импортируются.
### SLM-L2-ENVIRONMENT-A013
> **Совместимость среды выполнения**
>
> Публичная точка входа, обозначенная как client-, server- или shared-entry point, не импортирует и не реэкспортирует код несовместимой среды выполнения во всём достижимом графе импортов.
## Framework Groups и тестирование
### SLM-L2-FRAMEWORK-R014
> **Framework Group домена**
>
> Framework binding modules доменного пакета размещаются в Group, названной по фреймворку, и каждый прямой дочерний элемент этой Group является framework binding module.
### SLM-L2-FRAMEWORK-R015
> **Framework binding module**
>
> Модуль внутри Framework Group владеет одной domain-specific framework-ответственностью, получает готовые Domain API и не выполняет business-сборку, не выбирает adapters и не владеет страницей, маршрутом или multi-domain композицией.
### SLM-L2-TEST-R016
> **Проверка владельцев Level 2**
>
> Каждый публичный предметный сценарий проверяется через фабрику владеющего им Domain API, а основные тесты adapter, assembly и framework binding module проверяют только собственные публичные границы и не повторяют полный набор предметных сценариев.
## Совместное применение форм
### SLM-L2-DOMAIN-A026
> **Однозначная форма домена**
>
> Каждая предметная область объявлена ровно в одной форме: как доменный модуль Level 1 либо как доменный пакет Level 2; один SLM root может одновременно содержать разные предметные области обеих форм.
## Внешние библиотеки business
### SLM-L2-BUSINESS-R018
> **Business-safe внешний пакет**
>
> Внешний пакет объявляется business-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state/query manager, framework или другой технической интеграцией.
## Публичные фасеты business
### SLM-L2-BUSINESS-A019
> **Публичные фасеты business**
>
> Публичный API `business` имеет обязательные entry points `business` только с type exports и `business/factory` только с именованными runtime-фабриками, может иметь `business/runtime` только с runtime exports и не имеет других публичных путей или deep imports.
## Обязательные роли сборки
### SLM-L2-ASSEMBLY-A020
> **Обязательная Group assemblies**
>
> Корень каждого доменного пакета содержит ровно одну непустую Group `assemblies`, каждый прямой дочерний элемент которой является объявленной границей SLM-модуля.
### SLM-L2-ADAPTER-R021
> **Модули production adapters**
>
> Если хотя бы одна business-фабрика имеет техническую зависимость, корень доменного пакета содержит непустую Group `adapters`, а каждая связная production-реализация одной или нескольких таких зависимостей принадлежит ровно одному adapter-модулю этой Group и не определяется в другом месте production-графа.
### SLM-L2-BUSINESS-A022
> **Потребители фасетов business**
>
> Корневой `business` импортируется извне только через `import type`; `business/factory` импортируют только assemblies своего домена, `app`, `compositions` и тесты, а `business/runtime` импортируется только через объявленный публичный путь с соблюдением правил слоёв и междоменных зависимостей.
## Жизненный цикл assembly
### SLM-L2-ASSEMBLY-R023
> **Cleanup ресурса assembly**
>
> Создание Domain API фабрикой или assembly не запускает скрытый ресурс жизненного цикла; ресурс запускается явной операцией с cleanup, а технический ресурс, который assembly обязана создать для графа, сопровождается публичным cleanup handle, вызываемым не позже завершения области жизни графа.
## Недетерминизм business
### SLM-L2-BUSINESS-R024
> **Явные источники недетерминизма**
>
> Business-сценарий получает текущее время, timer, random, ID generator, environment и другие источники недетерминизма только через явные зависимости фабрики и не читает их из скрытого runtime-окружения.
## Публичный runtime business
### SLM-L2-BUSINESS-R025
> **Детерминированный runtime business**
>
> Фасет `business/runtime` экспортирует только необходимые реальным внешним потребителям детерминированные значения и функции без ввода-вывода, изменяемого состояния, runtime capability или environment-specific поведения.

View File

@@ -1,390 +0,0 @@
---
title: Business composition
description: Пример runtime-сборки business-фабрик в compositions/business
---
# Business composition
`compositions/business/{domain}` — composition module, который собирает конкретную business-фабрику с реальными runtime-зависимостями приложения.
Этот модуль не является бизнес-доменом. Он находится на слое `compositions`, потому что связывает `business`, `infra`, SDK, storage, browser API и другие внешние runtime-источники.
Это единственная integration-зона concrete product dependencies. Page, layout, screen и widget не импортируют SDK/client/storage напрямую и получают product data только через готовый `{Domain}Api`.
## Структура
```text
src/compositions/business/
├── auth/
│ ├── create-auth-business.ts
│ ├── create-auth-business.test.ts
│ ├── adapters/
│ │ ├── phone-auth.adapter.ts
│ │ ├── session.adapter.ts
│ │ ├── auth-session-events.adapter.ts
│ │ └── zustand-auth-state.adapter.ts
│ └── index.ts
├── user/
│ ├── create-user-business.ts
│ ├── create-user-business.test.ts
│ ├── adapters/
│ │ ├── user-profile.adapter.ts
│ │ └── user-storage.adapter.ts
│ ├── types/
│ │ └── create-user-business-deps.type.ts
│ └── index.ts
└── content/
├── create-content-business.ts
├── create-content-business.test.ts
├── adapters/
│ └── content-api.adapter.ts
└── index.ts
```
Если business-домены сгруппированы, `compositions/business` повторяет тот же относительный путь. Например: `business/app/auth` соответствует `compositions/business/app/auth`, `business/cms/content` соответствует `compositions/business/cms/content`.
Сегменты добавляются только по необходимости, но каждая concrete business dependency всегда оформляется отдельным файлом в `adapters/`. Не оставляй короткий adapter inline внутри builder.
## Ответственность
`compositions/business/{domain}` отвечает за adapter composition:
- создаёт или получает внешние клиенты из `infra`;
- отдельными adapters адаптирует SDK, API, storage, source/query hooks, state managers, events и browser API к `deps` business-модуля;
- вызывает business-фабрику;
- принимает API других business-модулей, если текущий домен зависит от них;
- экспортирует готовый `{Domain}Api` через builder-функцию;
- тестирует сборку и корректность адаптеров.
`compositions/business/{domain}` не должен содержать доменную логику. Если код описывает бизнес-правило, маппинг доменной модели, доменную ошибку или сценарий, он должен жить в соответствующем `business`-модуле.
`compositions/business/{domain}` не должен содержать React-компоненты, layouts, guards, providers и page-level wrappers. Применение logic API в React tree выполняется в обычных composition modules страниц, layouts, screens или widgets.
Builder не реализует dependencies inline. Он явно создаёт scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters фабрике.
## Business-контракт
Business-модуль объявляет dependency contract.
```ts
// business/auth/types/auth-deps.type.ts
import type { AuthState } from './auth-state.type'
import type { VerifyPhoneCodeData } from './verify-phone-code-data.type'
export type AuthDeps = {
phoneAuth: {
requestCode: (phone: string) => Promise<unknown>
resendCode: (challengeId: string) => Promise<unknown>
verifyCode: (data: VerifyPhoneCodeData) => Promise<unknown>
}
session: {
setToken: (token?: string | null) => void
useToken: () => string | null | undefined
}
sessionEvents: {
onInvalidated: (listener: () => void) => () => void
}
state: {
create: (initialState: AuthState) => {
get: () => AuthState
set: (state: AuthState) => void
useState: () => AuthState
}
}
}
```
Business-модуль не знает, через какой SDK, backend или storage реализованы эти возможности.
## Adapter composition
Composition-адаптер знает про конкретный runtime и приводит его к business-контракту.
```ts
// compositions/business/auth/adapters/phone-auth.adapter.ts
import type { AuthDeps } from '@/business/auth'
import type { AuthApiClient } from '@/infra/backend-api'
export const createPhoneAuthAdapter = (authApiClient: AuthApiClient): AuthDeps['phoneAuth'] => ({
requestCode: (phone) => {
return authApiClient.authOtp.phoneStart({ body: { phone } })
},
resendCode: (challengeId) => {
return authApiClient.authOtp.phoneResend({ body: { challengeId } })
},
verifyCode: (data) => {
return authApiClient.authOtp.phoneVerify({ body: data })
},
})
```
Адаптер не формирует доменные ошибки и не выбирает доменный `code`. Он может вернуть результат внешнего вызова или пробросить ошибку dependency. Решение о доменном коде принимает business-модуль.
Плохо:
```ts
export const createVerifyPhoneCode = (
authApiClient: AuthApiClient,
): AuthDeps['phoneAuth']['verifyCode'] => async (data) => {
try {
return await authApiClient.authOtp.phoneVerify({ body: data })
} catch (error) {
throw new AuthBusinessError('AUTH_PHONE_CODE_VERIFY_FAILED', error)
}
}
```
Проблема: composition-адаптер начал владеть доменной ошибкой.
Хорошо:
```ts
export const createVerifyPhoneCode = (
authApiClient: AuthApiClient,
): AuthDeps['phoneAuth']['verifyCode'] => (data) => {
return authApiClient.authOtp.phoneVerify({ body: data })
}
```
## State adapter
Доменное состояние принадлежит business-контракту, но concrete state manager остаётся снаружи business.
```ts
// compositions/business/auth/adapters/zustand-auth-state.adapter.ts
import { useStore } from 'zustand'
import { createStore } from 'zustand/vanilla'
import type { AuthDeps, AuthState } from '@/business/auth'
export const authStateAdapter: AuthDeps['state'] = {
create: (initialState) => {
const store = createStore<AuthState>()(() => initialState)
return {
get: store.getState,
set: (state) => store.setState(state),
useState: () => useStore(store),
}
},
}
```
`authFactory` выбирает initial domain state и вызывает `deps.state.create(initialState)`. Он не импортирует Zustand и не раскрывает `StoreApi` через public contract. Adapter только создаёт concrete store с переданным состоянием и не выбирает доменную политику.
Для SWR, TanStack Query и других source hooks действует то же правило: adapter реализует business-owned hook contract, business wrapper нормализует `data`, заменяет source `error` собственной domain error и возвращает собственный result type.
## Lifecycle adapter
External event также передаётся через business-owned contract.
```ts
// compositions/business/auth/adapters/auth-session-events.adapter.ts
import type { AuthDeps } from '@/business/auth'
import { onAuthSessionInvalidated } from '@/infra/backend-api'
export const authSessionEventsAdapter: AuthDeps['sessionEvents'] = {
onInvalidated: onAuthSessionInvalidated,
}
```
Business API предоставляет domain-level operation `startSessionInvalidationTracking()`. Внутри business она вызывает `deps.sessionEvents.onInvalidated`, выполняет доменный state transition и возвращает cleanup wrapper. Ошибки регистрации, callback и cleanup заменяются `AuthBusinessError`.
Graph owner запускает operation после commit и вызывает возвращённый cleanup при unmount, как показано в полном provider ниже. Provider не импортирует raw infra event и не связывает его с business command самостоятельно.
## Builder одного домена
Builder собирает одну business-фабрику.
```ts
// compositions/business/auth/create-auth-business.ts
import { authFactory } from '@/business/auth'
import { createBackendApiClient } from '@/infra/backend-api'
import { authSessionEventsAdapter } from './adapters/auth-session-events.adapter'
import { authStateAdapter } from './adapters/zustand-auth-state.adapter'
import { createPhoneAuthAdapter } from './adapters/phone-auth.adapter'
import { createSessionAdapter } from './adapters/session.adapter'
export const createAuthBusiness = () => {
const authApiClient = createBackendApiClient()
return authFactory({
phoneAuth: createPhoneAuthAdapter(authApiClient),
session: createSessionAdapter(),
sessionEvents: authSessionEventsAdapter,
state: authStateAdapter,
})
}
```
Browser/application builder без cross-domain зависимостей вызывается без аргументов. Он явно создаёт runtime instances и передаёт их private adapter factories. Client/adapter constructors не выполняют I/O, не читают storage/env неявно и не запускают subscriptions; lifecycle каждого instance соответствует lifecycle builder result.
Request-scoped builder принимает отдельный `requestScopeInput` только с request data, а concrete client factory импортирует сам. Не используй application singleton для request credentials, cookies или tenant context.
## Cross-domain зависимости
Если один business-модуль зависит от API другого business-модуля, builder принимает уже собранный API.
```ts
// compositions/business/user/types/create-user-business-deps.type.ts
import type { AuthApi } from '@/business/auth'
export type CreateUserBusinessDeps = {
authApi: Pick<AuthApi, 'useAuth'>
}
```
```ts
// compositions/business/user/create-user-business.ts
import { userFactory } from '@/business/user'
import { createBackendApiClient } from '@/infra/backend-api'
import { createUserProfileAdapter } from './adapters/user-profile.adapter'
import { createUserStorageAdapter } from './adapters/user-storage.adapter'
import type { CreateUserBusinessDeps } from './types/create-user-business-deps.type'
export const createUserBusiness = (deps: CreateUserBusinessDeps) => {
const apiClient = createBackendApiClient()
return userFactory({
authApi: deps.authApi,
profile: createUserProfileAdapter(apiClient),
storage: createUserStorageAdapter(),
})
}
```
Правила:
- сначала создаются независимые домены;
- затем создаются домены, которым нужны API уже созданных доменов;
- browser/application builder deps содержат только API других business-фабрик;
- request-scoped builder отделяет cross-domain API от `requestScopeInput` с request data;
- зависимость сужается через `Pick`, если нужен один метод;
- циклические runtime-зависимости между business API запрещены;
- если появляется цикл, нужно пересмотреть границы доменов или вынести общий сценарий в отдельный домен.
## Сборка графа в месте использования
Конечный граф создаётся там, где понятен lifecycle: page provider, route composition, application-lifetime composition provider, request scope или test setup. Слой `app` только подключает готовую composition.
```tsx
// compositions/routes/profile/providers/profile-business.provider.tsx
'use client'
import { createContext, useEffect, useState, type ReactNode } from 'react'
import { createAuthBusiness } from '@/compositions/business/auth'
import { createUserBusiness } from '@/compositions/business/user'
type ProfileBusiness = {
authApi: ReturnType<typeof createAuthBusiness>
userApi: ReturnType<typeof createUserBusiness>
}
export const ProfileBusinessContext = createContext<ProfileBusiness | null>(null)
const createProfileBusiness = (): ProfileBusiness => {
const authApi = createAuthBusiness()
const userApi = createUserBusiness({ authApi })
return { authApi, userApi }
}
export const ProfileBusinessProvider = ({ children }: { children: ReactNode }) => {
const [business] = useState(createProfileBusiness)
useEffect(() => {
return business.authApi.startSessionInvalidationTracking()
}, [business.authApi])
return (
<ProfileBusinessContext.Provider value={business}>
{children}
</ProfileBusinessContext.Provider>
)
}
```
Route-level `ProfileBusinessProvider` владеет lifecycle graph. `compositions/business/*` только предоставляет чистые функции сборки. React Strict Mode может повторно вызвать lazy initializer в development, поэтому factory, builder и adapter constructors не выполняют I/O и не запускают subscriptions.
Graph owner импортирует builders, но не raw SDK/client/event bus для «досборки» конкретного домена. Если external event влияет на domain state, event subscription является частью `{Domain}Deps`; business API предоставляет domain-level lifecycle operation, которую provider запускает в effect и cleanup которой вызывает при unmount. Registration и cleanup errors преобразуются business-модулем в domain errors.
## Public API
`index.ts` composition-модуля экспортирует builder и type-only deps, если builder зависит от других business API.
```ts
// compositions/business/auth/index.ts
export { createAuthBusiness } from './create-auth-business'
```
```ts
// compositions/business/user/index.ts
export { createUserBusiness } from './create-user-business'
export type { CreateUserBusinessDeps } from './types/create-user-business-deps.type'
```
Не экспортируй из public API:
- внутренние SDK-клиенты;
- generated operation trees;
- private adapters;
- test mocks;
- helpers, которые нужны только для сборки.
Если адаптер нужен нескольким composition-модулям, сначала проверь, не является ли это infra-сервисом. Не поднимай адаптер в `shared` только ради удобного импорта.
## Как не превратить сборку в кашу
Признаки плохой сборки:
- один файл создаёт все API-клиенты, все dependency-адаптеры и все фабрики;
- рядом лежат unrelated helpers для разных доменов;
- dependency-адаптеры смешаны с domain mappers;
- Zustand/SWR/SDK logic написана прямо внутри builder;
- graph owner напрямую связывает raw infra event с business command;
- business-правила реализованы в `compositions/business`;
- public API экспортирует внутренние адаптеры;
- невозможно протестировать сборку одного домена отдельно.
Что делать вместо этого:
- один домен runtime-сборки — один composition module;
- dependency-адаптеры держать рядом с конкретной сборкой домена;
- большие dependency-адаптеры выносить в `adapters/`;
- типы сборщика выносить в `types/`, если они перестали быть локальными;
- доменные mappers оставлять в `business/{domain}/mappers`;
- тестировать сборку домена отдельно от полной сборки приложения.
## Тестирование сборки
Тесты `compositions/business/{domain}` не заменяют factory-level тесты business-модуля.
Они проверяют только composition-риск:
- правильные dependency-адаптеры переданы в фабрику;
- API другой фабрики передан в нужном виде;
- SDK operation вызывается с ожидаемым payload;
- storage/browser adapter соответствует dependency-контракту;
- state/query adapter соответствует business-owned contract и не раскрывает library types;
- сборка не делает запросы во время создания business API;
- client/adapter constructors не выполняют import-time I/O, storage access или subscriptions;
- lifecycle operation запускается владельцем scope и вызывает cleanup;
- минимальный API-клиент не тянет лишние generated-операции.
Factory-level поведение самого домена тестируется в `business/{domain}/tests/{domain}-factory`.
## Чеклист
- Runtime-сборка находится в `compositions/business/{domain}`.
- Business-модуль не импортирует реальные SDK, API или storage.
- Dependency-адаптер реализован на composition-слое.
- State/query runtime реализован adapter-ом, а не импортирован business-модулем.
- Файлы внутри модуля сборки разнесены по ответственности.
- Runtime-зависимости между доменами передаются через builder deps.
- Builder deps содержат только API других собранных business-фабрик.
- Request-scoped builder отделяет cross-domain API от `requestScopeInput`.
- Public API composition-модуля не раскрывает internal adapters.
- Builder не содержит inline integration logic.
- Lifecycle operation запускается после commit и имеет cleanup.
- Сборка покрыта тестами на корректность связки deps и адаптеров.
- Business-поведение покрыто factory-level тестами в business-модуле.

View File

@@ -1,364 +0,0 @@
---
title: Тестирование business-модулей
description: Factory-level и colocated unit-тесты для business-фабрик SLM
---
# Тестирование business-модулей
Business-модуль тестируется как доменный контракт приложения. Главный контракт business-модуля — фабрика и API, который она возвращает.
Factory-level тесты обязательны для каждого business-модуля. Assembly tests обязательны для `compositions/business/{domain}`. Внутренние colocated unit-тесты добавляются для runtime-safe логики и не заменяют проверку public API фабрики.
## Уровни тестов
Полное изменение домена проверяется на трёх границах:
1. Factory-level тесты.
2. Assembly tests dependency adapters и builder.
3. Colocated unit-тесты внутренней runtime-safe логики.
Factory-level тесты отвечают на вопрос: работает ли домен снаружи через публичный API фабрики.
Assembly tests отвечают на вопрос: правильно ли concrete runtime реализует `Deps` и передан фабрике.
Colocated unit-тесты отвечают на вопрос: надёжна ли внутренняя runtime-safe механика, на которой держится публичный контракт.
## Factory-level тесты
Размещение:
```text
business/{domain}/tests/{domain}-factory/
```
Пример:
```text
business/user/tests/user-factory/
├── public-api.test.tsx
├── use-current-user.test.tsx
├── update-current-user-profile.test.ts
├── get-stored-user-agreements.test.ts
└── testing/
└── create-user-deps.mock.ts
```
Factory-level тесты импортируют модуль только через public API.
```ts
import { userFactory } from '@/business/user'
```
Factory-level тесты не импортируют:
- `services/*`;
- `hooks/*`;
- `mappers/*`;
- `lib/*`;
- `errors/*`;
- любые deep imports business-модуля.
## Что покрывать на factory-level
Каждый runtime-метод, который возвращает фабрика, должен иметь factory-level тесты.
Обязательно проверяются:
- полный публичный runtime API фабрики;
- happy path каждого метода;
- edge cases публичного контракта;
- корректные ответы DI-зависимостей;
- пустые ответы DI-зависимостей;
- невалидные ответы DI-зависимостей;
- rejected promise от dependency;
- синхронное исключение dependency;
- преобразование внешних ошибок в доменные ошибки;
- преобразование ошибок source hooks, stores и subscriptions в доменные ошибки;
- стабильный доменный `code` для каждой ошибки public contract;
- сохранение исходной ошибки в `cause`;
- отсутствие зависимости public contract от `status`, `message`, `response` и других внешних полей ошибки;
- порядок side effects;
- отсутствие следующих side effects после ошибки;
- hooks, если фабрика возвращает hooks.
Пример:
```ts
const deps = createUserDepsMock({
profile: {
useCurrent: createCurrentUserSourceHookMock({ data: sourceUser }),
},
})
const userApi = userFactory(deps)
await userApi.updateCurrentUserProfile(data)
const result = renderHook(() => userApi.useCurrentUser())
```
Тест проверяет поведение `userApi`, а не внутреннее устройство `createUpdateCurrentUserProfile`.
## Public API тест
У каждого business-модуля должен быть тест, который фиксирует публичный runtime API фабрики.
```ts
it('returns stable user business API', () => {
const userApi = userFactory(createUserDepsMock())
expect(Object.keys(userApi).sort()).toEqual([
'getStoredUserAgreements',
'updateCurrentUserProfile',
'useCurrentUser',
])
})
```
Такой тест не заменяет сценарные тесты методов. Он только фиксирует форму API и защищает от случайного удаления или переименования методов.
## DI-границы
Любая dependency фабрики считается ненадёжной runtime-границей.
Для каждой dependency нужно проверить минимум:
- корректный успешный ответ;
- `undefined`;
- `null`;
- пустой объект;
- объект неправильной формы;
- rejected promise;
- синхронный throw обычного method/callback/state/lifecycle dependency;
- повторные вызовы;
- смену результата dependency hook или domain state.
Если dependency является callback'ом, проверяется порядок вызовов и payload.
Если dependency работает с storage, проверяются битые, устаревшие и отсутствующие данные.
## Hooks через фабрику
Hooks, которые возвращает фабрика, тестируются через factory API.
Проверяй:
- hook не делает запрос без готовых входных данных;
- dependency hook получает ожидаемые доменные аргументы;
- hook корректно обрабатывает смену dependency result;
- `data` имеет доменную модель;
- `error` имеет доменный контракт;
- loading/refresh state соответствует собственному API модуля;
- невалидный dependency response не попадает наружу как валидная доменная модель.
```ts
const useCurrent = createCurrentUserSourceHookMock({ data: sourceUser })
const deps = createUserDepsMock({ profile: { useCurrent } })
const userApi = userFactory(deps)
const { result } = renderHook(() => userApi.useCurrentUser())
```
Не тестируй hook business-модуля как отдельную публичную сущность, если он не является public API фабрики.
SWR/Query cache keys, provider wrapper и library-specific revalidation тестируются в assembly tests dependency adapter, а не в business factory tests.
## Command-сценарии
Для command-методов вроде `save`, `update`, `change`, `request`, `verify` проверяются:
- payload передаётся во внешнюю dependency в ожидаемой форме;
- входной payload не мутируется;
- пустой успешный ответ считается успехом, если body не нужен;
- ошибка dependency превращается в доменную ошибку;
- потребитель может принять решение по доменному `code`;
- side effects выполняются в правильном порядке;
- при ошибке одного шага следующие side effects не выполняются;
- повторный вызов не использует устаревшее состояние, если это важно для сценария.
Если сценарий использует несколько зависимостей, тест должен явно фиксировать порядок.
## Colocated unit-тесты
Colocated unit-тесты размещаются рядом с файлом, который владеет runtime-логикой.
```text
business/{domain}/
├── errors/
│ ├── {domain}-business.error.ts
│ └── {domain}-business.error.test.ts
├── lib/
│ ├── normalize-{entity}.ts
│ └── normalize-{entity}.test.ts
├── mappers/
│ ├── map-{entity}.ts
│ └── map-{entity}.test.ts
├── services/
│ ├── update-{entity}.service.ts
│ └── update-{entity}.service.test.ts
└── hooks/
├── use-{scenario}.hook.ts
└── use-{scenario}.hook.test.tsx
```
Colocated unit-тесты нужны для:
- mappers;
- normalizers;
- type guards;
- runtime-safe helpers;
- domain errors;
- сложных services;
- hook wrappers со сложной нормализацией domain result/error;
- storage parsers;
- fallback-логики.
Colocated unit-тесты не нужны для:
- type-only файлов;
- `index.ts` без runtime-логики;
- простых re-export файлов;
- статических config-файлов без branching;
- типов, которые проверяются typecheck'ом.
## Почему colocated тесты не заменяют factory-level
Colocated тест может доказать, что mapper работает правильно, но он не доказывает, что фабрика использует этот mapper в публичном сценарии.
Colocated тест может доказать, что service обрабатывает ошибку, но он не доказывает, что service реально попал в public API фабрики.
Factory-level тест проверяет интеграцию внутренних частей business-модуля как чёрный ящик.
Правило:
- сначала покрывай public API фабрики;
- затем усиливай покрытие colocated тестами там, где есть runtime-safe логика.
## Маппинг и runtime safety
Если business-модуль получает данные с любой dependency boundary, тестируй не только happy path. Boundary включает методы, source hooks, stores, events и browser capabilities.
Проверяй:
- отсутствующие обязательные поля;
- nullable-поля;
- поля неправильного runtime-типа;
- пустые строки;
- пробельные строки;
- странные идентификаторы;
- пустые массивы;
- не-массив вместо массива;
- частично валидные объекты;
- дефолтные значения;
- domain error для malformed response, если модель не может быть безопасно построена.
Fallback допустим только для валидного доменного исхода, например корректно представленного отсутствия данных. Rejection, synchronous throw, source error и malformed response всегда дают domain error.
## Доменные ошибки
Business-модуль никогда не отдаёт наружу сырые ошибки SDK, HTTP-клиента, source hook, store, storage или browser API. Public contract всегда содержит только собственные domain errors.
Factory-level тесты должны доказывать, что потребитель может работать только с доменным `code` и не знает форму внешней ошибки.
Проверяй:
- `error.name`;
- стабильный `error.code`;
- сохранение `cause`;
- отсутствие утечки DTO/HTTP-specific деталей в public contract;
- rejected promise от разных dependencies маппится в ожидаемый доменный код;
- синхронный throw dependency маппится в ожидаемый доменный код;
- невалидный успешный ответ превращается в доменный код ошибки;
- разные технические ошибки дают один код, если для потребителя это один бизнес-сценарий;
- разные пользовательские сценарии дают разные коды, если UI должен реагировать по-разному;
- safe fallback только для валидного доменного исхода, явно представленного dependency contract.
UI и i18n должны ориентироваться на `code`, а не на `message` внешней ошибки.
Пример factory-level проверки:
```ts
it('throws domain error code when phone code verification fails', async () => {
const externalError = new Error('Request failed with status code 500')
const authApi = authFactory({
phoneAuth: {
requestCode: vi.fn(),
resendCode: vi.fn(),
verifyCode: vi.fn().mockRejectedValue(externalError),
},
session,
sessionEvents: createAuthSessionEventsMock(),
state: createAuthStateAdapterMock(),
})
await expect(authApi.verifyPhoneCode(data)).rejects.toMatchObject({
name: 'AuthBusinessError',
code: 'AUTH_PHONE_CODE_VERIFY_FAILED',
cause: externalError,
})
})
```
Не проверяй в потребительских сценариях `externalError.message`, HTTP status или тип ошибки SDK как ожидаемое поведение business API.
## Тестирование compositions/business
Тесты `compositions/business/{domain}` проверяют сборку, а не бизнес-поведение.
Проверяй:
- builder вызывает нужную business-фабрику;
- adapter вызывает SDK operation с ожидаемым payload;
- storage/browser adapter соответствует dependency contract;
- API другого домена передаётся в нужном виде;
- builder deps содержат только API других собранных business-фабрик;
- builder/client/adapter constructors не выполняют I/O, storage/env reads или subscriptions во время создания API;
- state/query runtime находится в adapter и не импортируется business-модулем;
- dependency hook работает без Suspense/throw-on-error и возвращает technical error через result;
- adapter пробрасывает source error без создания domain error;
- lifecycle subscription возвращает и вызывает cleanup;
- public API composition-модуля не экспортирует внутренние adapters.
Не проверяй здесь domain errors, fallback'и и маппинг доменной модели. Это ответственность factory-level и colocated тестов в `business/{domain}`.
## Что не тестировать unit-тестами business-модуля
Unit-тесты business-модуля не проверяют:
- реальные REST-запросы;
- generated-клиенты;
- настоящий backend;
- Next.js routing;
- визуальную вёрстку;
- интеграцию с production storage;
- реальные внешние сервисы;
- e2e-поток целого приложения.
Эти проверки относятся к `infra`, `compositions`, integration или e2e уровням.
## Архитектурные импорты
Проверяй production import graph business-модуля. В `business/**` не должно быть runtime или type-only imports из concrete runtimes:
- SDK/client/infra;
- SWR/TanStack Query/Apollo;
- Zustand/Redux/MobX;
- React state/effect APIs;
- storage/browser/event implementations.
Factory-level test обязан импортировать фабрику через public API business-модуля. Deep import `../../{domain}.factory` не доказывает корректность public boundary.
## Чеклист
- Каждый runtime-метод фабрики имеет factory-level тесты.
- Factory-level тесты импортируют модуль только через public API.
- Public API фабрики зафиксирован отдельным тестом.
- DI-зависимости проверены на корректные, пустые, невалидные и ошибочные ответы.
- Hooks тестируются через API, который вернула фабрика.
- Command-сценарии проверяют порядок side effects.
- После ошибки не выполняются лишние side effects.
- Runtime-safe mappers, normalizers и guards покрыты colocated unit-тестами.
- Type-only файлы не покрываются бессмысленными unit-тестами.
- Доменные ошибки имеют стабильный `code`, сохраняют `cause` и не раскрывают внешнюю ошибку как public contract.
- Тесты `compositions/business/{domain}` проверяют сборку, а не бизнес-логику.
- Business production imports не содержат concrete state/query/source runtime.
- Тесты не требуют backend, network, env и долгоживущих процессов.

View File

@@ -1,346 +0,0 @@
---
title: Композиция через Provider
description: Пример page-level Provider для composition modules в React-проекте
---
# Композиция через Provider
Раздел показывает, как page composition может владеть provider, store и business composition, которые нужны layout, screen и другим composition modules.
## Идея
Page composition хранит состояние и композицию бизнес-доменов на уровне страницы. Layout и screen не импортируют друг друга: они получают доступ к page-level данным через публичный API page composition.
В примере `ProfilePageState` — только локальное UI-state страницы. Это не domain state и не product data cache. Доменное состояние описывается business-модулем, а concrete store/query hook передаётся его фабрике через adapter в `compositions/business/{domain}`.
В примере page composition владеет scope-контрактом страницы, но не экспортирует готовый `ProfilePage`, потому что layout и screen импортируют hooks из `pages/profile`. Дерево страницы собирается в отдельном entry-point composition module, который слой `app` только подключает.
## Принципы
1. **Владение.** Page-level store, provider и business composition принадлежат page composition module.
2. **Обычные сегменты.** Provider, hooks, stores и types лежат в обычных сегментах модуля: `providers/`, `hooks/`, `stores/`, `types/`.
3. **Публичный контракт.** Page composition экспортирует только безопасные hooks, provider и типы, которые нужны другим composition modules.
4. **Сборка снаружи business.** Business-модули не используют page-level providers. Page composition вызывает builders из `compositions/business/{domain}` и владеет lifecycle готового графа.
5. **Без deep imports.** Layout и screen импортируют hooks только из public API page composition.
## Структура модулей
```text
compositions/pages/profile/
├── profile-business-composition.ts
├── providers/
│ └── profile-page.provider.tsx
├── hooks/
│ ├── use-profile-page-store.hook.ts
│ └── use-profile-business-composition.hook.ts
├── stores/
│ └── profile-page.store.ts
├── types/
│ └── profile-page-state.type.ts
└── index.ts
compositions/layouts/profile-main/
├── profile-main.layout.tsx
└── index.ts
compositions/screens/profile/
├── profile.screen.tsx
├── ui/
│ ├── profile-error/
│ ├── profile-summary/
│ └── profile-summary-skeleton/
└── index.ts
```
## Тип состояния страницы
Файл: `compositions/pages/profile/types/profile-page-state.type.ts`.
```ts
export type ProfilePageState = {
title: string
isSidebarOpen: boolean
setSidebarOpen: (value: boolean) => void
}
```
## Store страницы
Файл: `compositions/pages/profile/stores/profile-page.store.ts`.
```ts
import { createStore } from 'zustand/vanilla'
import type { ProfilePageState } from '../types/profile-page-state.type'
export const createProfilePageStore = () =>
createStore<ProfilePageState>((set) => ({
title: 'Profile',
isSidebarOpen: false,
setSidebarOpen: (value) => set({ isSidebarOpen: value }),
}))
```
`createProfilePageStore` не экспортируется через public API модуля. Это внутренняя деталь создания состояния.
## Business composition страницы
Файл: `compositions/pages/profile/profile-business-composition.ts`.
```ts
import { createAuthBusiness } from '@/compositions/business/auth'
import { createProfileBusiness } from '@/compositions/business/profile'
export const createProfileBusinessComposition = () => {
const authApi = createAuthBusiness()
const profileApi = createProfileBusiness({ authApi })
return { authApi, profileApi }
}
```
Page composition собирает нужный для страницы граф из per-domain builders. Реальные runtime-зависимости остаются в `compositions/business/{domain}`, а не внутри `business`.
Page composition не импортирует SDK, product storage, source hook или raw infra event для дополнительной настройки домена. Такое wiring принадлежит соответствующему integration module.
## Provider страницы
Файл: `compositions/pages/profile/providers/profile-page.provider.tsx`.
```tsx
'use client'
import { createContext, useEffect, useState, type ReactNode } from 'react'
import type { StoreApi } from 'zustand/vanilla'
import { createProfileBusinessComposition } from '../profile-business-composition'
import { createProfilePageStore } from '../stores/profile-page.store'
import type { ProfilePageState } from '../types/profile-page-state.type'
type ProfileBusinessComposition = ReturnType<typeof createProfileBusinessComposition>
type ProfilePageProviderValue = {
store: StoreApi<ProfilePageState>
business: ProfileBusinessComposition
}
export const ProfilePageContext = createContext<ProfilePageProviderValue | null>(null)
type Props = {
children: ReactNode
}
const createProfilePageProviderValue = (): ProfilePageProviderValue => ({
store: createProfilePageStore(),
business: createProfileBusinessComposition(),
})
export const ProfilePageProvider = ({ children }: Props) => {
const [value] = useState(createProfilePageProviderValue)
useEffect(() => {
return value.business.authApi.startSessionInvalidationTracking()
}, [value.business.authApi])
return (
<ProfilePageContext.Provider value={value}>
{children}
</ProfilePageContext.Provider>
)
}
```
Context object остаётся технической деталью provider и не должен использоваться внешними модулями напрямую. Наружу экспортируются hooks доступа.
Lazy initializer может быть повторно вызван React Strict Mode в development. Поэтому store/business constructors не выполняют I/O и не запускают subscriptions. Domain-level lifecycle operations запускаются отдельно в effect и возвращают cleanup.
## Hooks доступа
Файл: `compositions/pages/profile/hooks/use-profile-page-store.hook.ts`.
```ts
'use client'
import { useContext } from 'react'
import { useStore } from 'zustand'
import { ProfilePageContext } from '../providers/profile-page.provider'
import type { ProfilePageState } from '../types/profile-page-state.type'
export const useProfilePageStore = <T,>(selector: (state: ProfilePageState) => T) => {
const ctx = useContext(ProfilePageContext)
if (!ctx) {
throw new Error('useProfilePageStore must be used within ProfilePageProvider')
}
return useStore(ctx.store, selector)
}
```
Файл: `compositions/pages/profile/hooks/use-profile-business-composition.hook.ts`.
```ts
'use client'
import { useContext } from 'react'
import { ProfilePageContext } from '../providers/profile-page.provider'
export const useProfileBusinessComposition = () => {
const ctx = useContext(ProfilePageContext)
if (!ctx) {
throw new Error('useProfileBusinessComposition must be used within ProfilePageProvider')
}
return ctx.business
}
```
## Layout использует page-level store
Файл: `compositions/layouts/profile-main/profile-main.layout.tsx`.
```tsx
'use client'
import type { ReactNode } from 'react'
import { useProfilePageStore } from '@/compositions/pages/profile'
type Props = {
children: ReactNode
}
export const ProfileMainLayout = ({ children }: Props) => {
const title = useProfilePageStore((state) => state.title)
const isSidebarOpen = useProfilePageStore((state) => state.isSidebarOpen)
return (
<div data-sidebar-open={isSidebarOpen}>
<header>{title}</header>
<main>{children}</main>
</div>
)
}
```
Layout импортирует hook из public API page composition. Он не импортирует screen и не лезет во внутренние файлы `pages/profile`.
## Screen использует business composition
Файл: `compositions/screens/profile/profile.screen.tsx`.
```tsx
'use client'
import { useProfileBusinessComposition } from '@/compositions/pages/profile'
import { ProfileError } from './ui/profile-error'
import { ProfileSummary } from './ui/profile-summary'
import { ProfileSummarySkeleton } from './ui/profile-summary-skeleton'
export const ProfileScreen = () => {
const { profileApi } = useProfileBusinessComposition()
const currentProfile = profileApi.useCurrentProfile()
if (currentProfile.isLoading) {
return <ProfileSummarySkeleton />
}
if (currentProfile.error) {
return <ProfileError code={currentProfile.error.code} />
}
return currentProfile.data ? <ProfileSummary profile={currentProfile.data} /> : null
}
```
Screen получает готовые доменные API из page composition и не собирает граф фабрик самостоятельно. `ProfileSummary` — компонент screen composition, а не часть `business/profile`.
## Публичный API page composition
Файл: `compositions/pages/profile/index.ts`.
```ts
export { ProfilePageProvider } from './providers/profile-page.provider'
export { useProfilePageStore } from './hooks/use-profile-page-store.hook'
export { useProfileBusinessComposition } from './hooks/use-profile-business-composition.hook'
export type { ProfilePageState } from './types/profile-page-state.type'
```
Внутренние `createProfilePageStore`, `createProfileBusinessComposition` и `ProfilePageContext` не экспортируются через public API.
Готовое дерево собирай в отдельном entry-point composition module. Не смешивай в одном public API готовую page composition и hooks, которые импортируют её дочерние layout/screen modules: это может создать runtime-цикл.
## Подключение в app
Entry composition связывает provider, layout и screen:
```tsx
// compositions/entries/profile/profile.entry.tsx
'use client'
import { ProfilePageProvider } from '@/compositions/pages/profile'
import { ProfileMainLayout } from '@/compositions/layouts/profile-main'
import { ProfileScreen } from '@/compositions/screens/profile'
export const ProfileEntry = () => (
<ProfilePageProvider>
<ProfileMainLayout>
<ProfileScreen />
</ProfileMainLayout>
</ProfilePageProvider>
)
```
React Router config только подключает готовый entry:
```tsx
import { ProfileEntry } from '@/compositions/entries/profile'
export const profileRoute = {
path: '/profile',
element: <ProfileEntry />,
}
```
Для Next App Router создай готовые layout/page entries в `compositions`, а framework files только подключают их.
```tsx
// compositions/entries/profile/profile-layout.entry.tsx
'use client'
import { ProfilePageProvider } from '@/compositions/pages/profile'
import { ProfileMainLayout } from '@/compositions/layouts/profile-main'
import type { ReactNode } from 'react'
export const ProfileLayoutEntry = ({ children }: { children: ReactNode }) => {
return (
<ProfilePageProvider>
<ProfileMainLayout>{children}</ProfileMainLayout>
</ProfilePageProvider>
)
}
```
```tsx
// compositions/entries/profile/profile-page.entry.tsx
'use client'
import { ProfileScreen } from '@/compositions/screens/profile'
export const ProfilePageEntry = () => <ProfileScreen />
```
```tsx
// app/(profile)/layout.tsx
import { ProfileLayoutEntry } from '@/compositions/entries/profile'
export default ProfileLayoutEntry
```
```tsx
// app/(profile)/page.tsx
import { ProfilePageEntry } from '@/compositions/entries/profile'
export default ProfilePageEntry
```
`app` размещает готовые entry composition modules по правилам фреймворка, но не реализует product tree внутри себя.

View File

@@ -1,83 +0,0 @@
---
title: Структуры compositions
description: Примеры организации слоя compositions под разные способы сборки React-приложения
---
# Структуры compositions
Раздел показывает, что SLM не фиксирует жёсткую структуру внутри `compositions`. Команда выбирает организацию под фреймворк, роутинг, CMS и продуктовую задачу.
## Базовая рекомендация
Подходит для большинства приложений, где есть явные страницы, layouts, screens и переиспользуемые композиционные блоки.
```text
src/compositions/
├── business/
│ ├── auth/
│ └── user/
├── pages/
│ ├── home/
│ └── profile/
├── layouts/
│ ├── main/
│ └── dashboard/
├── screens/
│ ├── home/
│ └── profile/
└── widgets/
├── page-heading/
└── promo-banner/
```
`business`, `pages`, `layouts`, `screens` и `widgets` здесь не являются отдельными SLM-слоями. Это группы composition modules внутри одного слоя `compositions`.
`compositions/business/{domain}` используется для runtime-сборки business-фабрик. Он не заменяет `business/{domain}` и не содержит доменную логику.
Только эта группа integration modules знает одновременно business dependency contract и concrete product runtime. Остальные composition modules являются graph owners или consumers готовых business API.
## Entry-points и blocks
Подходит для проектов, где точка сборки не всегда является страницей: CMS registry, embedded UI, route entries, feature entries.
```text
src/compositions/
├── entry-points/
│ ├── cms-profile/
│ └── embedded-checkout/
├── pages/
│ └── profile/
├── layouts/
│ └── profile-main/
├── screens/
│ └── profile/
└── blocks/
├── profile-summary/
└── recommended-products/
```
## Группировка вокруг продукта
Подходит, когда удобнее держать все части одной крупной области рядом.
```text
src/compositions/
└── profile/
├── page/
├── layout/
├── screen/
└── blocks/
```
## Главное правило
Любая структура допустима, если соблюдаются границы слоя:
- `app` подключает готовые composition modules к фреймворку.
- `compositions` может импортировать `business`, `infra`, `ui`, `shared`.
- `compositions/business/{domain}` отдельными adapters собирает конкретную business-фабрику с runtime-зависимостями.
- Page/layout/screen/widget получают product data только через `{Domain}Api`.
- Graph owner импортирует builders, но не raw product SDK/client/event для досборки домена.
- `business`, `infra`, `ui`, `shared` не импортируют `compositions`.
- Импорты между composition modules идут только через public API.
- Deep imports внутрь composition modules запрещены.