From 192a8a185bc76fbfc8eb74e388b22b07a39f6917 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Fri, 24 Jul 2026 14:35:35 +0300 Subject: [PATCH] chore: init --- .gitignore | 2 + README.md | 28 + docs/README.md | 12 + docs/canons/business-factory.md | 331 ++++++++ docs/canons/business-runtime-boundary.md | 293 +++++++ docs/canons/decision-process.md | 216 ++++++ docs/canons/file-atlas.md | 508 +++++++++++++ docs/canons/index.md | 148 ++++ docs/canons/layers.md | 285 +++++++ docs/canons/modules.md | 300 ++++++++ docs/canons/monorepo.md | 229 ++++++ docs/canons/segments.md | 222 ++++++ docs/canons/validation.md | 214 ++++++ docs/examples/business-composition.md | 390 ++++++++++ docs/examples/business-testing.md | 364 +++++++++ docs/examples/react/composition-provider.md | 346 +++++++++ docs/examples/react/composition-structures.md | 83 ++ package.json | 14 + scripts/build-skill.mjs | 70 ++ scripts/check-skill.mjs | 99 +++ skills/slm-design/SKILL.md | 719 ++++++++++++++++++ skills/slm-design/reference/README.md | 12 + .../reference/canons/business-factory.md | 331 ++++++++ .../canons/business-runtime-boundary.md | 293 +++++++ .../reference/canons/decision-process.md | 216 ++++++ .../slm-design/reference/canons/file-atlas.md | 508 +++++++++++++ skills/slm-design/reference/canons/index.md | 148 ++++ skills/slm-design/reference/canons/layers.md | 285 +++++++ skills/slm-design/reference/canons/modules.md | 300 ++++++++ .../slm-design/reference/canons/monorepo.md | 229 ++++++ .../slm-design/reference/canons/segments.md | 222 ++++++ .../slm-design/reference/canons/validation.md | 214 ++++++ .../examples/business-composition.md | 390 ++++++++++ .../reference/examples/business-testing.md | 364 +++++++++ .../examples/react/composition-provider.md | 346 +++++++++ .../examples/react/composition-structures.md | 83 ++ src-skills/slm-design/SKILL.md | 7 + src-skills/slm-design/skill.config.mjs | 25 + 38 files changed, 8846 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/README.md create mode 100644 docs/canons/business-factory.md create mode 100644 docs/canons/business-runtime-boundary.md create mode 100644 docs/canons/decision-process.md create mode 100644 docs/canons/file-atlas.md create mode 100644 docs/canons/index.md create mode 100644 docs/canons/layers.md create mode 100644 docs/canons/modules.md create mode 100644 docs/canons/monorepo.md create mode 100644 docs/canons/segments.md create mode 100644 docs/canons/validation.md create mode 100644 docs/examples/business-composition.md create mode 100644 docs/examples/business-testing.md create mode 100644 docs/examples/react/composition-provider.md create mode 100644 docs/examples/react/composition-structures.md create mode 100644 package.json create mode 100644 scripts/build-skill.mjs create mode 100644 scripts/check-skill.mjs create mode 100644 skills/slm-design/SKILL.md create mode 100644 skills/slm-design/reference/README.md create mode 100644 skills/slm-design/reference/canons/business-factory.md create mode 100644 skills/slm-design/reference/canons/business-runtime-boundary.md create mode 100644 skills/slm-design/reference/canons/decision-process.md create mode 100644 skills/slm-design/reference/canons/file-atlas.md create mode 100644 skills/slm-design/reference/canons/index.md create mode 100644 skills/slm-design/reference/canons/layers.md create mode 100644 skills/slm-design/reference/canons/modules.md create mode 100644 skills/slm-design/reference/canons/monorepo.md create mode 100644 skills/slm-design/reference/canons/segments.md create mode 100644 skills/slm-design/reference/canons/validation.md create mode 100644 skills/slm-design/reference/examples/business-composition.md create mode 100644 skills/slm-design/reference/examples/business-testing.md create mode 100644 skills/slm-design/reference/examples/react/composition-provider.md create mode 100644 skills/slm-design/reference/examples/react/composition-structures.md create mode 100644 src-skills/slm-design/SKILL.md create mode 100644 src-skills/slm-design/skill.config.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2752eb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..05e6921 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# SLM Design + +Документация и agent skill для архитектуры Scoped Layered Module Design. + +## Структура + +- `docs/` — исходная документация и спецификация SLM Design. +- `src-skills/` — исходники agent skills. +- `skills/` — собранные skills для установки через `npx skills`. + +## Сборка + +Требуется Node.js 20 или новее. + +```bash +npm run build +npm run check +``` + +`npm run build` пересобирает `skills/slm-design/` из `docs/` и `src-skills/slm-design/`. Не редактируй собранные файлы вручную. + +## Установка + +После публикации репозитория: + +```bash +npx skills add /slm-design-new --skill slm-design +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d6f92b8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# SLM Design + +`docs/` - файлы документации по SLM-архитектуре. + +Используй эту документацию как источник истины для задач по SLM Design: слоям, модулям, сегментам, публичному API, business factory, composition modules и монорепозиториям. + +## Структура + +- `canons/` - основные каноны SLM Design. +- `examples/` - дополнительные примеры реализации. + +Точка входа: `canons/index.md`. diff --git a/docs/canons/business-factory.md b/docs/canons/business-factory.md new file mode 100644 index 0000000..a215cc3 --- /dev/null +++ b/docs/canons/business-factory.md @@ -0,0 +1,331 @@ +--- +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`, если 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 +} +``` + +Проблема: 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 + resendCode: (challengeId: string) => Promise + verifyCode: (data: VerifyPhoneCodeData) => Promise + } + 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 + LoginButton: ComponentType + useAuth: ReturnType +} +``` + +Проблема: factory output смешивает доменную логику с UI и начинает собирать React tree. + +Хорошо: + +```ts +export type AuthApi = { + requestPhoneCode: ReturnType + resendPhoneCode: ReturnType + verifyPhoneCode: ReturnType + useAuth: ReturnType + useIsAuthenticated: ReturnType + 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(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 созданы и выполняются. diff --git a/docs/canons/business-runtime-boundary.md b/docs/canons/business-runtime-boundary.md new file mode 100644 index 0000000..69bbf2c --- /dev/null +++ b/docs/canons/business-runtime-boundary.md @@ -0,0 +1,293 @@ +--- +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 +} +``` + +Хорошо: + +```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 +} + +export type AuthDeps = { + phoneAuth: { + requestCode: (phone: string) => Promise + resendCode: (challengeId: string) => Promise + verifyCode: (data: VerifyPhoneCodeData) => Promise + } + 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` с последующим `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). diff --git a/docs/canons/decision-process.md b/docs/canons/decision-process.md new file mode 100644 index 0000000..9c6989f --- /dev/null +++ b/docs/canons/decision-process.md @@ -0,0 +1,216 @@ +--- +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` с приведением к полному типу. +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. Наличие возможной папки не означает, что её нужно создать. diff --git a/docs/canons/file-atlas.md b/docs/canons/file-atlas.md new file mode 100644 index 0000000..b325189 --- /dev/null +++ b/docs/canons/file-atlas.md @@ -0,0 +1,508 @@ +--- +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` с 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 без реального поведения. diff --git a/docs/canons/index.md b/docs/canons/index.md new file mode 100644 index 0000000..ff74cb0 --- /dev/null +++ b/docs/canons/index.md @@ -0,0 +1,148 @@ +--- +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, а внутреннюю форму композиции определяет команда. diff --git a/docs/canons/layers.md b/docs/canons/layers.md new file mode 100644 index 0000000..29e291c --- /dev/null +++ b/docs/canons/layers.md @@ -0,0 +1,285 @@ +--- +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 diff --git a/docs/canons/modules.md b/docs/canons/modules.md new file mode 100644 index 0000000..b251314 --- /dev/null +++ b/docs/canons/modules.md @@ -0,0 +1,300 @@ +--- +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/`. + +Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/docs/canons/monorepo.md b/docs/canons/monorepo.md new file mode 100644 index 0000000..5b2f6fe --- /dev/null +++ b/docs/canons/monorepo.md @@ -0,0 +1,229 @@ +--- +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/*`. diff --git a/docs/canons/segments.md b/docs/canons/segments.md new file mode 100644 index 0000000..31ca007 --- /dev/null +++ b/docs/canons/segments.md @@ -0,0 +1,222 @@ +--- +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 +``` diff --git a/docs/canons/validation.md b/docs/canons/validation.md new file mode 100644 index 0000000..3eaa218 --- /dev/null +++ b/docs/canons/validation.md @@ -0,0 +1,214 @@ +--- +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` с 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 созданы и запущены? +- Изменение не оставило старый параллельный путь? + +Если хотя бы один ответ «нет», задача не завершена. diff --git a/docs/examples/business-composition.md b/docs/examples/business-composition.md new file mode 100644 index 0000000..6fd79e1 --- /dev/null +++ b/docs/examples/business-composition.md @@ -0,0 +1,390 @@ +--- +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 + resendCode: (challengeId: string) => Promise + verifyCode: (data: VerifyPhoneCodeData) => Promise + } + 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()(() => 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 +} +``` + +```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 + userApi: ReturnType +} + +export const ProfileBusinessContext = createContext(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 ( + + {children} + + ) +} +``` + +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-модуле. diff --git a/docs/examples/business-testing.md b/docs/examples/business-testing.md new file mode 100644 index 0000000..4ddee8d --- /dev/null +++ b/docs/examples/business-testing.md @@ -0,0 +1,364 @@ +--- +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 и долгоживущих процессов. diff --git a/docs/examples/react/composition-provider.md b/docs/examples/react/composition-provider.md new file mode 100644 index 0000000..71e98d2 --- /dev/null +++ b/docs/examples/react/composition-provider.md @@ -0,0 +1,346 @@ +--- +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((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 + +type ProfilePageProviderValue = { + store: StoreApi + business: ProfileBusinessComposition +} + +export const ProfilePageContext = createContext(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 ( + + {children} + + ) +} +``` + +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 = (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 ( +
+
{title}
+
{children}
+
+ ) +} +``` + +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 + } + + if (currentProfile.error) { + return + } + + return 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 = () => ( + + + + + +) +``` + +React Router config только подключает готовый entry: + +```tsx +import { ProfileEntry } from '@/compositions/entries/profile' + +export const profileRoute = { + path: '/profile', + element: , +} +``` + +Для 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 ( + + {children} + + ) +} +``` + +```tsx +// compositions/entries/profile/profile-page.entry.tsx +'use client' + +import { ProfileScreen } from '@/compositions/screens/profile' + +export const ProfilePageEntry = () => +``` + +```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 внутри себя. diff --git a/docs/examples/react/composition-structures.md b/docs/examples/react/composition-structures.md new file mode 100644 index 0000000..b884945 --- /dev/null +++ b/docs/examples/react/composition-structures.md @@ -0,0 +1,83 @@ +--- +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 запрещены. diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb077da --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "slm-design", + "private": true, + "type": "module", + "scripts": { + "build": "npm run build:skill", + "build:skill": "node scripts/build-skill.mjs", + "check": "npm run build && npm run check:skill", + "check:skill": "node scripts/check-skill.mjs" + }, + "engines": { + "node": ">=20" + } +} diff --git a/scripts/build-skill.mjs b/scripts/build-skill.mjs new file mode 100644 index 0000000..d2d99bd --- /dev/null +++ b/scripts/build-skill.mjs @@ -0,0 +1,70 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import skillConfig from '../src-skills/slm-design/skill.config.mjs'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const sourceDir = path.join(repoRoot, 'src-skills', skillConfig.name); +const sourcePath = path.join(sourceDir, skillConfig.source); +const docsDir = path.join(repoRoot, 'docs'); +const outputDir = path.join(repoRoot, 'skills', skillConfig.name); +const includePattern = //g; + +const isWithin = (filePath, parentPath) => { + const relativePath = path.relative(parentPath, filePath); + + return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); +}; + +const removeFrontmatter = (content) => { + return content.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---\r?\n?/, ''); +}; + +const shiftHeadings = (content) => { + return content.replace(/^(#{1,6})(?=\s)/gm, (heading) => '#'.repeat(Math.min(heading.length + 1, 6))); +}; + +const resolveIncludes = (content, baseDir) => { + return content.replace(includePattern, (match, includePath) => { + const resolvedPath = path.resolve(baseDir, includePath); + + if (!isWithin(resolvedPath, repoRoot) || !fs.existsSync(resolvedPath)) { + throw new Error(`Include file not found: ${includePath}`); + } + + return shiftHeadings(removeFrontmatter(fs.readFileSync(resolvedPath, 'utf8')).trim()); + }); +}; + +const rewriteLinks = (content) => { + return skillConfig.linkRewrites.reduce((result, { from, to }) => { + return result.split(`](${from})`).join(`](${to})`); + }, content); +}; + +const createFrontmatter = () => { + return `---\nname: ${skillConfig.name}\ndescription: ${JSON.stringify(skillConfig.description)}\n---`; +}; + +if (!fs.existsSync(sourcePath)) { + throw new Error(`Skill source not found: ${path.relative(repoRoot, sourcePath)}`); +} + +if (!fs.existsSync(docsDir)) { + throw new Error('Documentation directory not found: docs'); +} + +const source = fs.readFileSync(sourcePath, 'utf8'); +const content = rewriteLinks(resolveIncludes(source, path.dirname(sourcePath))).trim(); +const output = [ + createFrontmatter(), + '', + content, +].join('\n\n'); + +fs.rmSync(outputDir, { recursive: true, force: true }); +fs.mkdirSync(outputDir, { recursive: true }); +fs.writeFileSync(path.join(outputDir, 'SKILL.md'), `${output}\n`); +fs.cpSync(docsDir, path.join(outputDir, 'reference'), { recursive: true }); + +console.log(path.relative(repoRoot, path.join(outputDir, 'SKILL.md'))); diff --git a/scripts/check-skill.mjs b/scripts/check-skill.mjs new file mode 100644 index 0000000..18b683d --- /dev/null +++ b/scripts/check-skill.mjs @@ -0,0 +1,99 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import skillConfig from '../src-skills/slm-design/skill.config.mjs'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const skillDir = path.join(repoRoot, 'skills', skillConfig.name); +const skillPath = path.join(skillDir, 'SKILL.md'); +const referenceFiles = [ + 'README.md', + 'canons/business-factory.md', + 'canons/business-runtime-boundary.md', + 'canons/decision-process.md', + 'canons/file-atlas.md', + 'canons/index.md', + 'canons/layers.md', + 'canons/modules.md', + 'canons/monorepo.md', + 'canons/segments.md', + 'canons/validation.md', + 'examples/business-composition.md', + 'examples/business-testing.md', + 'examples/react/composition-provider.md', + 'examples/react/composition-structures.md', +]; + +const assert = (condition, message) => { + if (!condition) { + throw new Error(message); + } +}; + +const listMarkdownFiles = (directoryPath) => { + return fs.readdirSync(directoryPath, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(directoryPath, entry.name); + + if (entry.isDirectory()) { + return listMarkdownFiles(entryPath); + } + + return entry.isFile() && path.extname(entry.name) === '.md' ? [entryPath] : []; + }); +}; + +const assertLocalLinksExist = (filePath) => { + const content = fs.readFileSync(filePath, 'utf8'); + const links = [...content.matchAll(/]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)]; + + for (const [, rawTarget] of links) { + if (rawTarget.startsWith('#') || /^[a-z][a-z+.-]*:/i.test(rawTarget) || rawTarget.startsWith('//')) { + continue; + } + + const [targetPath] = rawTarget.split('#'); + const resolvedPath = path.resolve(path.dirname(filePath), targetPath); + + assert(fs.existsSync(resolvedPath), `Broken link in ${path.relative(repoRoot, filePath)}: ${rawTarget}`); + } +}; + +assert(fs.existsSync(skillPath), 'Run npm run build before checking the skill.'); + +const skillContent = fs.readFileSync(skillPath, 'utf8'); + +assert(skillContent.startsWith(`---\nname: ${skillConfig.name}\n`), 'SKILL.md must contain the skill name in frontmatter.'); +assert(skillContent.includes('description: '), 'SKILL.md must contain a description in frontmatter.'); +assert(!skillContent.includes(' + +# SLM Design + +## Процесс архитектурного решения + +Не изменяй файлы, пока не принято архитектурное решение. Название папки, существующий похожий код и удобный импорт не доказывают правильность размещения. + +### Карточка решения + +Перед реализацией определи: + +| Вопрос | Что зафиксировать | +|---|---| +| Роль изменения | 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` с приведением к полному типу. +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](./reference/canons/file-atlas.md) | +| Задача затрагивает product I/O, source hook, domain store, event, lifecycle или external errors | [Runtime-граница business](#runtime-граница-business) | +| Выполняется архитектурное ревью или финальная проверка реализации | [Архитектурная проверка](#архитектурная-проверка) | +| Неясен layer, направление import или роль `app/compositions/business/infra/ui/shared` | [Слои](./reference/canons/layers.md) | +| Нужно отличить module, component, group, nested module или спроектировать public API | [Модули](./reference/canons/modules.md) | +| Проектируется factory, Api, Deps, domain error или сборка домена | [Business-фабрика](./reference/canons/business-factory.md) | +| Неясно размещение hook/store/service/mapper/provider/type/style | [Сегменты](./reference/canons/segments.md) | +| Решается вынос из `apps/*/src` в `packages/*` | [Монорепозитории](./reference/canons/monorepo.md) | +| Нужен полный пример adapters, builder, state runtime и graph lifecycle | [Business composition](./reference/examples/business-composition.md) | +| Нужна матрица factory-level, assembly и colocated tests | [Тестирование business-модулей](./reference/examples/business-testing.md) | +| Нужен page/route provider, локальный UI store и доступ к готовому graph | [Композиция через Provider](./reference/examples/react/composition-provider.md) | +| Команда выбирает организацию groups внутри `compositions` | [Структуры compositions](./reference/examples/react/composition-structures.md) | + +Не используй карту как scaffold checklist. Наличие возможной папки не означает, что её нужно создать. + +## 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 +} +``` + +Хорошо: + +```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 +} + +export type AuthDeps = { + phoneAuth: { + requestCode: (phone: string) => Promise + resendCode: (challengeId: string) => Promise + verifyCode: (data: VerifyPhoneCodeData) => Promise + } + 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` с последующим `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-фабрике](./reference/canons/business-factory.md). Практическая сборка показана в [Business composition](./reference/examples/business-composition.md). + +## Архитектурная проверка + +Не считай задачу завершённой только потому, что код компилируется или 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` с 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-модулей](./reference/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 созданы и запущены? +- Изменение не оставило старый параллельный путь? + +Если хотя бы один ответ «нет», задача не завершена. diff --git a/skills/slm-design/reference/README.md b/skills/slm-design/reference/README.md new file mode 100644 index 0000000..d6f92b8 --- /dev/null +++ b/skills/slm-design/reference/README.md @@ -0,0 +1,12 @@ +# SLM Design + +`docs/` - файлы документации по SLM-архитектуре. + +Используй эту документацию как источник истины для задач по SLM Design: слоям, модулям, сегментам, публичному API, business factory, composition modules и монорепозиториям. + +## Структура + +- `canons/` - основные каноны SLM Design. +- `examples/` - дополнительные примеры реализации. + +Точка входа: `canons/index.md`. diff --git a/skills/slm-design/reference/canons/business-factory.md b/skills/slm-design/reference/canons/business-factory.md new file mode 100644 index 0000000..a215cc3 --- /dev/null +++ b/skills/slm-design/reference/canons/business-factory.md @@ -0,0 +1,331 @@ +--- +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`, если 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 +} +``` + +Проблема: 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 + resendCode: (challengeId: string) => Promise + verifyCode: (data: VerifyPhoneCodeData) => Promise + } + 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 + LoginButton: ComponentType + useAuth: ReturnType +} +``` + +Проблема: factory output смешивает доменную логику с UI и начинает собирать React tree. + +Хорошо: + +```ts +export type AuthApi = { + requestPhoneCode: ReturnType + resendPhoneCode: ReturnType + verifyPhoneCode: ReturnType + useAuth: ReturnType + useIsAuthenticated: ReturnType + 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(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 созданы и выполняются. diff --git a/skills/slm-design/reference/canons/business-runtime-boundary.md b/skills/slm-design/reference/canons/business-runtime-boundary.md new file mode 100644 index 0000000..69bbf2c --- /dev/null +++ b/skills/slm-design/reference/canons/business-runtime-boundary.md @@ -0,0 +1,293 @@ +--- +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 +} +``` + +Хорошо: + +```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 +} + +export type AuthDeps = { + phoneAuth: { + requestCode: (phone: string) => Promise + resendCode: (challengeId: string) => Promise + verifyCode: (data: VerifyPhoneCodeData) => Promise + } + 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` с последующим `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). diff --git a/skills/slm-design/reference/canons/decision-process.md b/skills/slm-design/reference/canons/decision-process.md new file mode 100644 index 0000000..9c6989f --- /dev/null +++ b/skills/slm-design/reference/canons/decision-process.md @@ -0,0 +1,216 @@ +--- +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` с приведением к полному типу. +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. Наличие возможной папки не означает, что её нужно создать. diff --git a/skills/slm-design/reference/canons/file-atlas.md b/skills/slm-design/reference/canons/file-atlas.md new file mode 100644 index 0000000..b325189 --- /dev/null +++ b/skills/slm-design/reference/canons/file-atlas.md @@ -0,0 +1,508 @@ +--- +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` с 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 без реального поведения. diff --git a/skills/slm-design/reference/canons/index.md b/skills/slm-design/reference/canons/index.md new file mode 100644 index 0000000..ff74cb0 --- /dev/null +++ b/skills/slm-design/reference/canons/index.md @@ -0,0 +1,148 @@ +--- +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, а внутреннюю форму композиции определяет команда. diff --git a/skills/slm-design/reference/canons/layers.md b/skills/slm-design/reference/canons/layers.md new file mode 100644 index 0000000..29e291c --- /dev/null +++ b/skills/slm-design/reference/canons/layers.md @@ -0,0 +1,285 @@ +--- +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 diff --git a/skills/slm-design/reference/canons/modules.md b/skills/slm-design/reference/canons/modules.md new file mode 100644 index 0000000..b251314 --- /dev/null +++ b/skills/slm-design/reference/canons/modules.md @@ -0,0 +1,300 @@ +--- +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/`. + +Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/skills/slm-design/reference/canons/monorepo.md b/skills/slm-design/reference/canons/monorepo.md new file mode 100644 index 0000000..5b2f6fe --- /dev/null +++ b/skills/slm-design/reference/canons/monorepo.md @@ -0,0 +1,229 @@ +--- +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/*`. diff --git a/skills/slm-design/reference/canons/segments.md b/skills/slm-design/reference/canons/segments.md new file mode 100644 index 0000000..31ca007 --- /dev/null +++ b/skills/slm-design/reference/canons/segments.md @@ -0,0 +1,222 @@ +--- +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 +``` diff --git a/skills/slm-design/reference/canons/validation.md b/skills/slm-design/reference/canons/validation.md new file mode 100644 index 0000000..3eaa218 --- /dev/null +++ b/skills/slm-design/reference/canons/validation.md @@ -0,0 +1,214 @@ +--- +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` с 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 созданы и запущены? +- Изменение не оставило старый параллельный путь? + +Если хотя бы один ответ «нет», задача не завершена. diff --git a/skills/slm-design/reference/examples/business-composition.md b/skills/slm-design/reference/examples/business-composition.md new file mode 100644 index 0000000..6fd79e1 --- /dev/null +++ b/skills/slm-design/reference/examples/business-composition.md @@ -0,0 +1,390 @@ +--- +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 + resendCode: (challengeId: string) => Promise + verifyCode: (data: VerifyPhoneCodeData) => Promise + } + 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()(() => 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 +} +``` + +```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 + userApi: ReturnType +} + +export const ProfileBusinessContext = createContext(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 ( + + {children} + + ) +} +``` + +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-модуле. diff --git a/skills/slm-design/reference/examples/business-testing.md b/skills/slm-design/reference/examples/business-testing.md new file mode 100644 index 0000000..4ddee8d --- /dev/null +++ b/skills/slm-design/reference/examples/business-testing.md @@ -0,0 +1,364 @@ +--- +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 и долгоживущих процессов. diff --git a/skills/slm-design/reference/examples/react/composition-provider.md b/skills/slm-design/reference/examples/react/composition-provider.md new file mode 100644 index 0000000..71e98d2 --- /dev/null +++ b/skills/slm-design/reference/examples/react/composition-provider.md @@ -0,0 +1,346 @@ +--- +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((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 + +type ProfilePageProviderValue = { + store: StoreApi + business: ProfileBusinessComposition +} + +export const ProfilePageContext = createContext(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 ( + + {children} + + ) +} +``` + +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 = (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 ( +
+
{title}
+
{children}
+
+ ) +} +``` + +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 + } + + if (currentProfile.error) { + return + } + + return 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 = () => ( + + + + + +) +``` + +React Router config только подключает готовый entry: + +```tsx +import { ProfileEntry } from '@/compositions/entries/profile' + +export const profileRoute = { + path: '/profile', + element: , +} +``` + +Для 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 ( + + {children} + + ) +} +``` + +```tsx +// compositions/entries/profile/profile-page.entry.tsx +'use client' + +import { ProfileScreen } from '@/compositions/screens/profile' + +export const ProfilePageEntry = () => +``` + +```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 внутри себя. diff --git a/skills/slm-design/reference/examples/react/composition-structures.md b/skills/slm-design/reference/examples/react/composition-structures.md new file mode 100644 index 0000000..b884945 --- /dev/null +++ b/skills/slm-design/reference/examples/react/composition-structures.md @@ -0,0 +1,83 @@ +--- +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 запрещены. diff --git a/src-skills/slm-design/SKILL.md b/src-skills/slm-design/SKILL.md new file mode 100644 index 0000000..18b58cd --- /dev/null +++ b/src-skills/slm-design/SKILL.md @@ -0,0 +1,7 @@ +# SLM Design + + + + + + diff --git a/src-skills/slm-design/skill.config.mjs b/src-skills/slm-design/skill.config.mjs new file mode 100644 index 0000000..e73d115 --- /dev/null +++ b/src-skills/slm-design/skill.config.mjs @@ -0,0 +1,25 @@ +export default { + name: 'slm-design', + description: 'Используй при определении архитектурной роли изменения и работе по SLM Design: выборе владельца кода, слоя, модуля, scope, public API, направления зависимостей и пути продуктовых данных. Триггеры: SLM, Scoped Layered Module Design, где разместить или перенести код, business factory, DomainApi, DomainDeps, compositions/business, dependency adapter, inline adapter в builder, прямой вызов API из page/screen/hook, Zustand/SWR/SDK внутри business, domain error, deep import, module vs component, ui vs parts, page-level provider/store, business graph, Partial, event bus, subscription cleanup, lifecycle, factory-level и assembly tests, архитектура template/scaffold, перенос между apps/*/src и packages/*. НЕ используй для форматирования уже размещённого React/TypeScript/CSS-кода, реализации REST/OpenAPI-клиента, Next.js routing/rendering или механики генерации шаблона без архитектурного выбора. В смешанной задаче сначала зафиксируй SLM-границу, затем применяй профильный skill.', + source: 'SKILL.md', + linkRewrites: [ + { from: './file-atlas.md', to: './reference/canons/file-atlas.md' }, + { from: './business-runtime-boundary.md', to: '#runtime-граница-business' }, + { from: './validation.md', to: '#архитектурная-проверка' }, + { from: './layers.md', to: './reference/canons/layers.md' }, + { from: './modules.md', to: './reference/canons/modules.md' }, + { from: './business-factory.md', to: './reference/canons/business-factory.md' }, + { from: './segments.md', to: './reference/canons/segments.md' }, + { from: './monorepo.md', to: './reference/canons/monorepo.md' }, + { + from: '../examples/react/composition-provider.md', + to: './reference/examples/react/composition-provider.md', + }, + { + from: '../examples/react/composition-structures.md', + to: './reference/examples/react/composition-structures.md', + }, + { from: '../examples/business-composition.md', to: './reference/examples/business-composition.md' }, + { from: '../examples/business-testing.md', to: './reference/examples/business-testing.md' }, + ], +};