From b34abb37b420a160f4fbb7324bc54d5e488b2c86 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Mon, 10 Aug 2026 19:41:48 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D1=82=D0=B0=D1=80=D1=8B=D0=B5=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- old-docs/README.md | 12 - old-docs/canons/business-factory.md | 331 ------------ old-docs/canons/business-runtime-boundary.md | 293 ---------- old-docs/canons/decision-process.md | 216 -------- old-docs/canons/file-atlas.md | 508 ------------------ old-docs/canons/index.md | 148 ----- old-docs/canons/layers.md | 285 ---------- old-docs/canons/modules.md | 300 ----------- old-docs/canons/monorepo.md | 229 -------- old-docs/canons/segments.md | 222 -------- old-docs/canons/validation.md | 214 -------- old-docs/examples/business-composition.md | 390 -------------- old-docs/examples/business-testing.md | 364 ------------- .../examples/react/composition-provider.md | 346 ------------ .../examples/react/composition-structures.md | 83 --- 15 files changed, 3941 deletions(-) delete mode 100644 old-docs/README.md delete mode 100644 old-docs/canons/business-factory.md delete mode 100644 old-docs/canons/business-runtime-boundary.md delete mode 100644 old-docs/canons/decision-process.md delete mode 100644 old-docs/canons/file-atlas.md delete mode 100644 old-docs/canons/index.md delete mode 100644 old-docs/canons/layers.md delete mode 100644 old-docs/canons/modules.md delete mode 100644 old-docs/canons/monorepo.md delete mode 100644 old-docs/canons/segments.md delete mode 100644 old-docs/canons/validation.md delete mode 100644 old-docs/examples/business-composition.md delete mode 100644 old-docs/examples/business-testing.md delete mode 100644 old-docs/examples/react/composition-provider.md delete mode 100644 old-docs/examples/react/composition-structures.md diff --git a/old-docs/README.md b/old-docs/README.md deleted file mode 100644 index 0128dae..0000000 --- a/old-docs/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# SLM Design - -Этот каталог содержит действующую legacy-документацию по SLM-архитектуре. - -Используй эту документацию как источник истины для задач по SLM Design: слоям, модулям, сегментам, публичному API, business factory, composition modules и монорепозиториям. - -## Структура - -- `canons/` - основные каноны SLM Design. -- `examples/` - дополнительные примеры реализации. - -Точка входа: `canons/index.md`. diff --git a/old-docs/canons/business-factory.md b/old-docs/canons/business-factory.md deleted file mode 100644 index a215cc3..0000000 --- a/old-docs/canons/business-factory.md +++ /dev/null @@ -1,331 +0,0 @@ ---- -title: Business-фабрика -description: Контракт business-модуля, runtime-зависимости, logic API, доменные ошибки и место сборки фабрик ---- - -# Business-фабрика - -Раздел фиксирует архитектурный контракт business-модуля: фабрика описывает доменный logic API приложения и не знает о конкретном backend, SDK, storage, state/query runtime, browser API или React tree. - -## Главный принцип - -Business-модуль описывает домен приложения, а не форму backend API, SDK, storage, external hook, state manager или внешнего сервиса. - -Фабрика должна отвечать на вопрос: какой API нужен приложению для работы с доменом. Она не должна отвечать на вопрос: какие методы есть у конкретного REST-клиента, SDK или browser API. - -## Когда создавать business-модуль - -Полный контракт business-модуля — фабрика, `deps`, доменные типы, доменные ошибки, adapters, сборка в `compositions/business/{domain}`, обязательные factory/assembly tests и colocated tests для внутренней runtime-safe логики. Он применяется к любому модулю, созданному в `business`. - -Создавай business-модуль, когда выполняется хотя бы одно условие: - -- сценарии нужны нескольким страницам, composition modules или другим доменам; -- у логики есть собственная доменная модель и доменные ошибки, которые нельзя выразить типами внешнего API; -- доменный сценарий зависит от внешних runtime-capabilities (backend, SDK, product storage, source hook, domain store, event, browser API), которые нужно изолировать за `deps`; -- домен нужно тестировать независимо от UI и конкретного backend. - -Не создавай business-модуль заранее, если логика нужна одной странице, не имеет доменной модели, product I/O и domain state. Presentation-only store или browser interaction, принадлежащие UI scope, сами по себе не создают business-домен. Такая page-local orchestration живёт в соответствующем composition module. - -Наличие product I/O отменяет page-local исключение. Даже если источник нужен одной странице, consumer composition не обращается к нему напрямую: объяви business-контракт, dependency adapter и domain errors. - -«Облегчённого» business-модуля не существует: если модуль создан в `business/`, контракт применяется целиком. Подъём вызревшей логики из composition module в business-модуль — обычный рефакторинг: объяви доменные типы и `deps`, перенеси сценарии в services и hooks, собери фабрику и покрой её factory-level тестами. - -## Обязательные правила - -- Фабрика лежит в корне business-модуля: `business/{domain}/{domain}.factory.ts`. -- Фабрика принимает runtime-зависимости только через `deps`. -- Фабрика возвращает только logic API домена: hooks, selectors, command/query methods, scenario services. -- Фабрика не возвращает React-компоненты, layouts, guards, boundaries, providers или page-level wrappers. -- Business-модуль не содержит React-компоненты. UI-решения домена размещаются в `compositions`; полностью универсальные UI-контролы размещаются в `ui`. -- Business-модуль не импортирует реальные SDK, generated operations, HTTP-клиенты, storage, env, browser API или composition-сборку. -- Business-модуль не импортирует React state/effect runtime, SWR/query runtime, Zustand/Redux/MobX store runtime или event bus implementation. -- Source/query hooks, domain state stores, subscriptions, clock, random и technical services передаются через business-owned `deps`. -- Public contract business-модуля использует собственные доменные типы, а не DTO внешнего API. -- `index.ts` business-модуля экспортирует только фабрику и type-only экспорты. Исключений нет. -- Runtime-сборка business-фабрики выполняется вне `business`, обычно в `compositions/business/{domain}`. -- Для каждой runtime-capability создаётся явный dependency adapter. Adapter не пишется inline внутри builder. -- Из public API business выходят только собственные domain errors со стабильным `code`. -- Factory-level и assembly tests обязательны и входят в критерий завершения модуля. - -## Проектирование фабрики - -Проектирование начинается со сценариев домена, а не с API-клиента. - -Порядок работы: - -1. Опиши публичные сценарии, которые нужны приложению. -2. Опиши доменные типы, которыми должен оперировать UI и другие business-модули. -3. Проведи inventory всех runtime-capabilities: data sources, hooks, stores, events, browser APIs, env, clock и cross-domain APIs. -4. Опиши минимальные внешние возможности в `{Domain}Deps`. -5. Назови внешние возможности бизнес-языком, а не языком backend endpoint'ов и библиотек. -6. Опиши domain error codes для каждого публичного сценария. -7. Собери фабрику из внутренних services, hook wrappers, mappers и helpers поверх переданных deps. -8. Верни наружу только `{Domain}Api`. -9. Реализуй каждую dependency отдельным adapter в `compositions/business/{domain}`. - -Правильные вопросы: - -- Не `какой endpoint дернуть?`, а `какой бизнес-сценарий выполняем?`. -- Не `какой DTO пришёл?`, а `какую доменную модель отдаём наружу?`. -- Не `как называется метод SDK?`, а `какая внешняя возможность нужна домену?`. -- Не `как устроен backend сейчас?`, а `какой стабильный контракт нужен приложению?`. - -## Контракт зависимостей - -`{Domain}Deps` описывает не технические инструменты, а возможности, которые нужны домену. - -Правила: - -- имя зависимости отражает бизнес-возможность: `phoneAuth`, `session`, `profile`, `agreements`, `notifications`; -- методы внутри зависимости называют сценарное действие без повторения endpoint names: `phoneAuth.requestCode`, `phoneAuth.verifyCode`, `profile.getCurrentUser`, `agreements.saveUserAgreements`; -- параметры используют доменные типы business-модуля; -- результат внешней границы принимается как `unknown`, если данные требуют runtime-нормализации; -- пустой успешный ответ описывается как `Promise`, если 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/old-docs/canons/business-runtime-boundary.md b/old-docs/canons/business-runtime-boundary.md deleted file mode 100644 index 69bbf2c..0000000 --- a/old-docs/canons/business-runtime-boundary.md +++ /dev/null @@ -1,293 +0,0 @@ ---- -title: Runtime-граница business -description: Строгая изоляция business-фабрики от источников данных, state/query runtime, infra, browser API и внешних ошибок ---- - -# Runtime-граница business - -## Главный инвариант - -Business-модуль выполняет доменную композицию только над: - -- собственными типами и детерминированной логикой; -- capabilities, переданными фабрике через `{Domain}Deps`. - -Business не вызывает runtime-возможность, если она не была передана фабрике. Это относится не только к данным и `infra`, но и к hooks, stores, subscriptions, browser API и другим concrete runtime-механизмам. - -Фабрика отвечает на вопрос «какой стабильный доменный API нужен приложению», а не «какими библиотеками и источниками он реализован». - -## Что разрешено внутри business - -Business может напрямую использовать: - -- собственные domain types; -- собственные services, mappers, normalizers, validators и type guards; -- собственные domain errors; -- детерминированные вычисления без I/O, runtime-state и скрытого окружения; -- чистые библиотеки вроде schema validators, decimal/date utilities, если результат определяется только явными аргументами и типы библиотеки не становятся public contract; -- type-only контракты других business API для cross-domain dependencies. - -## Что передаётся через deps - -Через `{Domain}Deps` передавай любую runtime-capability: - -| Capability | Примеры concrete implementation | -|---|---| -| Product source | REST SDK, GraphQL client, CMS, storage | -| Source/query hook | SWR, TanStack Query, Apollo hook | -| Domain state runtime | Zustand, Redux, MobX, RxJS store | -| Technical service | logger, telemetry, notifications, i18n engine | -| Platform API | `window`, navigation, clipboard, geolocation | -| Lifecycle event | unauthorized event, socket event, subscription | -| Environment | env/config provider, feature runtime configuration | -| Nondeterminism | clock, timer, random, ID generator | -| Cross-domain behavior | ограниченный API другой business-фабрики | - -Не импортируй concrete implementation в business даже в том случае, если библиотека используется только в одном внутреннем файле. - -## Запрещённые imports - -Production-код `business/**` не импортирует напрямую ни runtime values, ни types из concrete runtimes: - -- `infra`, `compositions`, `app`; -- SDK, generated operations и HTTP clients; -- storage implementation; -- React state/effect APIs; -- SWR, TanStack Query, Apollo и другие query runtimes; -- Zustand, Redux, MobX, RxJS stores; -- browser и framework runtime APIs; -- event bus implementation; -- env и process-specific configuration. - -Запрет нельзя обойти через `import type`, alias, barrel, type cast или helper в `shared`. Type-only разрешены собственные contracts, детерминированный `shared`, чистые libraries и суженные public API других business-доменов. - -## Доменный шлюз данных - -Business API является единственной продуктовой границей для потребительского кода. - -```text -page / layout / screen / widget - → {Domain}Api - → business scenario - → {Domain}Deps - → private dependency adapter - → infra client / SDK / storage / external source -``` - -Внешний сервис остаётся физическим источником данных. Business является единственным источником доменной истины: он определяет модель, сценарий, нормализацию, fallback и ошибки. - -Обычный consumer composition не создаёт параллельный продуктовый контракт поверх DTO или client. Единственная зона concrete product integration внутри `compositions` — `compositions/business/{domain}`. - -## Business-owned deps - -`{Domain}Deps` принадлежит business-модулю и описывает необходимые возможности доменным языком. - -Требования: - -- группируй методы по capability, а не по имени SDK/client; -- принимай доменные аргументы; -- принимай внешние результаты как `unknown`, если нужна runtime-проверка; -- описывай собственную минимальную форму dependency hook/result; -- описывай собственный state port, а не `StoreApi` конкретной библиотеки; -- возвращай cleanup из subscription capability; -- не передавай mapper, normalizer или domain error через deps; -- не используй generated DTO как доменную модель; -- не передавай целый client, если домену нужны два конкретных действия. - -Плохо: - -```ts -import type { StoreApi } from 'zustand' -import type { AdminApiClient } from '@vendor/admin-sdk' - -export type AuthDeps = { - api: AdminApiClient - store: StoreApi -} -``` - -Хорошо: - -```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/old-docs/canons/decision-process.md b/old-docs/canons/decision-process.md deleted file mode 100644 index 9c6989f..0000000 --- a/old-docs/canons/decision-process.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -title: Процесс архитектурного решения -description: Обязательный порядок классификации задачи, выбора владельца, слоя, scope и стратегии изменений ---- - -# Процесс архитектурного решения - -Не изменяй файлы, пока не принято архитектурное решение. Название папки, существующий похожий код и удобный импорт не доказывают правильность размещения. - -## Карточка решения - -Перед реализацией определи: - -| Вопрос | Что зафиксировать | -|---|---| -| Роль изменения | Framework wiring, продуктовый сценарий, интеграция, композиция интерфейса, технический сервис, UI или чистый фундамент | -| Владелец | Домен, route/page scope, composition module, infra-модуль, UI-модуль или локальный consumer | -| Данные | Продуктовые данные, техническое состояние, framework input, локальное UI-state или отсутствуют | -| Runtime-возможности | Источники данных, hooks, stores, SDK, browser API, events, clock, random, env и другие внешние capabilities | -| Место | Приложение или package, слой, модуль, вложенный модуль и сегмент | -| Публичная граница | Что действительно нужно экспортировать и кто будет consumer | -| Путь данных | От consumer до business API, dependency adapter и конкретного источника | -| Lifecycle | Кто создаёт instance, сколько instances допустимо и кто выполняет cleanup | -| Стратегия | Локальная правка, новый модуль, новый business-контракт, adapter, перенос или исправление public API | -| Проверки | Typecheck, тесты, import graph, public API, lifecycle и архитектурные инварианты | - -Карточку не обязательно выводить пользователю, если решение очевидно. Но агент обязан уметь обосновать каждый пункт до изменения файлов. - -## Сбор контекста - -Перед выбором места: - -1. Прочитай локальные инструкции приложения или package. -2. Найди фактическую границу SLM: `src/`, `apps/{app}/src` или другой локальный root. -3. Проверь существующие слои, группы и соседние модули. Не создавай новую параллельную структуру без необходимости. -4. Проверь aliases, package exports и реальную разрешимость импортов. -5. Найди текущих consumers, public API и runtime import graph изменяемой ответственности. -6. Проверь существующие templates или generators после архитектурного выбора. Шаблон не принимает решение за SLM. -7. Отдельно найди product I/O, hooks, stores, subscriptions, browser API и другие runtime-возможности. -8. Проверь, нет ли уже business-домена, которому принадлежит сценарий. - -Не считай неиспользуемый provider, пустой context, тип будущего graph или ссылку на несуществующий домен готовой архитектурой. Решение должно быть достижимо из runtime entry point и иметь реальных consumers. - -## Выбор роли - -Классифицируй ответственность в следующем порядке. - -### Framework wiring - -Если код существует только из-за фреймворка, размести его в `app`: - -- route-файл; -- bootstrap; -- framework error entry; -- подключение глобальных ресурсов; -- тонкое подключение готового composition module. - -`app` не реализует продуктовую композицию, business graph, store, provider или экран. - -### Продуктовый сценарий - -Если код определяет пользовательский сценарий, доменную модель, продуктовый state, бизнес-правило, нормализацию внешних данных, error mapping или доменный переход после ошибки, владелец находится в `business/{domain}`. - -Визуальная реакция на готовый domain error принадлежит consumer composition: сообщение, error screen, redirect, retry control и UI fallback выбираются по стабильному доменному `code`. - -Любой новый внешний источник продуктовых данных требует business-контракта. Колокация внешних вызовов в page/screen/widget services не является допустимым упрощением. - -### Интеграция business-домена - -Если код реализует `{Domain}Deps` через SDK, HTTP, storage, browser API, state/query runtime, event bus или другой concrete runtime, размести его в `compositions/business/{domain}`. - -Это интеграционный composition module, а не business-домен и не обычная page/screen/widget composition. - -### Продуктовая композиция - -Если код собирает route/page/layout/screen/widget, управляет UI-state, provider scope или lifecycle готового business graph, размести его в соответствующем composition module. - -Потребительский composition module получает продуктовые данные только через `{Domain}Api`. Он не импортирует product SDK, generated operations, product storage adapter или конкретный источник. - -### Технический сервис - -Если код предоставляет техническую возможность без продуктовой модели и сценариев, размести его в `infra`: - -- HTTP client; -- SDK wrapper; -- logger; -- theme engine; -- i18n engine; -- telemetry transport; -- технический realtime client. - -Composition может использовать технический infra-сервис напрямую, если сервис не становится обходным путём к продуктовым данным. Если capability нужна business, она всё равно передаётся через business-owned `deps` и adapter. - -### Универсальный UI - -Если сущность отображает интерфейс, не знает продуктовый сценарий и применима независимо от конкретной composition, размести её в `ui`. - -### Чистый фундамент - -Если код детерминирован, не имеет runtime-state, не знает продукт и переиспользуется несколькими владельцами, рассмотри `shared`. По умолчанию оставляй код рядом с первым владельцем. - -## Выбор scope - -Выбирай минимальный scope, который полностью владеет ответственностью: - -1. Нужен одному component/module и не имеет самостоятельной ответственности: оставь внутри владельца. -2. Нужен как самостоятельная часть одного module: создай nested module в `parts/`. -3. Нужен нескольким частям одной page/route ветки: подними в общий composition scope этой ветки. -4. Нужен нескольким composition modules и остаётся продуктовой композицией: создай отдельный composition module. -5. Является доменным сценарием или product data boundary: создай или расширь `business/{domain}`. -6. Является техническим сервисом: создай или расширь `infra/{service}`. -7. Является универсальным UI: создай или расширь `ui/{module}`. -8. Выноси в package только `ui`, `infra` или `shared` код с реальным вторым consumer либо явно зафиксированным межприложенческим ownership/reuse-контрактом. - -Не поднимай код выше ради короткого импорта. Не создавай `shared`, общий provider, generic business context или package «на будущее». - -## Component, module и group - -Применяй решение последовательно: - -1. Только отображает готовые props и не владеет зависимостями: component в `ui/` родительского module. -2. Владеет сценарием, данными, state, dependency, lifecycle или внутренней декомпозицией: самостоятельный module. -3. Самостоятельный module, локальный для владельца: nested module в `parts/`. -4. Папка только классифицирует конечные modules: group без `index.ts`, state и runtime logic. -5. `ui/`, `parts/`, `hooks/`, `types/`, `services/` и другие служебные папки внутри module: segments, а не modules. - -Если component начинает получать данные, выбирать источник, вызывать сценарный hook или управлять процессом, не добавляй логику в component. Измени архитектурную форму сущности. - -## Выбор стратегии - -### Новый продуктовый сценарий - -1. Найди домен-владелец. -2. Спроектируй `{Domain}Api`, доменные типы и доменные ошибки. -3. Опиши минимальные runtime-capabilities в `{Domain}Deps`. -4. Реализуй детерминированную доменную логику. -5. Создай отдельные adapters в `compositions/business/{domain}`. -6. Собери фабрику чистым builder. -7. Подключи API во владельце lifecycle graph. -8. Используй API из потребительских compositions. -9. Добавь factory-level и assembly tests. - -### Прямой product I/O вне business boundary - -Не расширяй существующее нарушение. - -1. Определи сценарий и домен. -2. Перенеси контракт данных в business-owned `Deps`. -3. Перенеси нормализацию, fallback и error mapping в business. -4. Оставь concrete source call в dependency adapter. -5. Замени прямой вызов на `{Domain}Api`. -6. Закрой adapter и source details из public API. - -### Новый store или dependency hook - -Сначала определи, является state локальным UI-state или доменным state. - -- State является локальным UI-state, если сбрасывается вместе с UI scope, управляет только представлением и не хранит продуктовый факт или product data cache. Такой state может принадлежать composition module и использовать выбранный state manager внутри владельца. -- State является доменным, если выражает продуктовый факт, инвариант, доступен через business API или участвует в бизнес-сценарии. -- Доменный state принадлежит business-контракту. Фабрика получает state adapter factory через `deps`, выбирает initial domain state и создаёт concrete port через adapter. -- Source/query hook реализуется adapter-ом; business вызывает только dependency hook и возвращает собственный доменный hook/result. - -### Сборка graph - -1. Собери каждый домен отдельным `compositions/business/{domain}` builder. -2. Определи DAG cross-domain зависимостей. -3. Выбери один явный lifecycle scope: application-lifetime composition, route, page, request или test. Слой `app` только подключает application composition. -4. Создавай graph у владельца scope, а не в случайном screen/widget или на module scope без обоснования. -5. Передавай consumers точный graph type. Не используй `Partial` с приведением к полному типу. -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/old-docs/canons/file-atlas.md b/old-docs/canons/file-atlas.md deleted file mode 100644 index b325189..0000000 --- a/old-docs/canons/file-atlas.md +++ /dev/null @@ -1,508 +0,0 @@ ---- -title: Атлас файлов SLM -description: Карта слоёв, типов modules, root files, segments, public API, tests и запрещённых файловых сочетаний ---- - -# Атлас файлов SLM - -Используй атлас после того, как определены роль изменения и владелец ответственности. Не выбирай архитектуру по желаемому имени файла. - -Атлас исчерпывает стандартные архитектурные роли SLM, но не является закрытым списком framework-файлов. Команда может добавить локальный segment или suffix, если его ответственность не дублирует существующую, не нарушает направление зависимостей и закреплена в локальных инструкциях. - -## Единицы структуры - -| Единица | Что означает | Имеет public API | Владеет runtime | -|---|---|---|---| -| Layer | Верхнеуровневая область ответственности внутри SLM root | Нет общего требования | Зависит от layer | -| Group | Навигационная папка для modules или других groups | Нет, `index.ts` запрещён | Нет | -| Module | Самостоятельный владелец одной ответственности | Да | Может | -| Segment | Папка внутри module по назначению файлов | Нет отдельного внешнего API | Только как часть владельца | -| Component | Презентационная часть родительского module в `ui/` | Локальный `index.ts` допустим | Нет архитектурного runtime | -| Root file | Главный entry/contract конкретного module | Экспортируется module `index.ts` | Зависит от типа module | - -Сначала определи module, затем root file, затем необходимые segments. Не создавай все папки из атласа заранее. - -## Карта SLM root - -```text -src/ -├── app/ # framework wiring -├── compositions/ # product tree, graph owners и business integrations -├── business/ # доменные контракты и сценарии -├── infra/ # технические runtime-сервисы -├── ui/ # универсальные UI modules -└── shared/ # детерминированный фундамент -``` - -В monorepo вместо `src/` границей приложения обычно является `apps/{app}/src/`. Packages находятся выше SLM root и не являются дополнительными слоями. - -## Root files modules - -| Pattern | Роль | Где допустим | -|---|---|---| -| `{name}.page.tsx` | Готовая page composition | `compositions` | -| `{name}.layout.tsx` | Product layout composition | `compositions` | -| `{name}.screen.tsx` | Уникальный screen leaf/branch | `compositions` | -| `{name}.widget.tsx` | Самостоятельный composition block | `compositions` | -| `{name}.route.tsx` | Route composition и route lifecycle | `compositions` | -| `{name}.entry.tsx` | Готовая точка подключения product tree | `compositions` | -| `{scope}-business-composition.ts` | Non-visual сборка business graph/scope | `compositions` | -| `{name}.ts` | Другой non-visual root, названный по ответственности | `compositions`, nested modules | -| `{name}.tsx` | Root UI/module component без специальной роли | `compositions`, `ui`, nested modules | -| `{domainName}.factory.ts` | Единственный runtime entry business-домена | `business/{domainPath}` | -| `create-{domainName}-business.ts` | Builder одной business-фабрики | `compositions/business/{domainPath}` | -| `{name}.client.ts` | Технический client | `infra/{service}` | -| `{name}.service.ts` | Технический root service, если service и есть module entry | `infra/{service}` | -| `index.ts` | Public API конечного module | В module; запрещён у group | - -Root suffix не определяет owner автоматически. Например, `profile.store.ts` остаётся domain store или page UI store в зависимости от смысла state. - -## Layer App - -`app` содержит только файлы, требуемые framework/runtime entry. - -Возможные файлы: - -| Файл или pattern | Назначение | -|---|---| -| `main.tsx`, `bootstrap.tsx` | Запуск приложения | -| `app.tsx` | Тонкое подключение application entry/providers | -| `app-router.tsx`, `router.tsx` | Framework route registry | -| `page.tsx`, `layout.tsx`, `route.ts`, `error.tsx`, `not-found.tsx` | Framework-convention files | -| `middleware.ts` | Framework middleware boundary | -| framework metadata/config files | Только framework contract | - -Правила: - -- framework file импортирует готовый entry/route/page composition через public API; -- product tree собирается в `compositions`, не в `app`; -- business graph, domain store, screen, widget и product provider в `app` запрещены; -- у `app` нет SLM modules и общего `index.ts`; -- framework-specific server/client правила определяет профильный framework skill. - -Минимальный пример: - -```text -src/app/ -├── app-router.tsx -├── app.tsx -└── main.tsx - -src/compositions/entries/profile/ -├── profile.entry.tsx -└── index.ts -``` - -## Consumer composition module - -Consumer composition собирает product UI и использует готовые `{Domain}Api`. - -```text -compositions/{group}/{name}/ -├── {name}.{page|layout|screen|widget|route|entry}.tsx # visual entry, если нужен -├── {name}-business-composition.ts # business graph, если module владеет им -├── {name}.ts # другой non-visual entry, если нужен -├── ui/ # presentation components -├── parts/ # nested modules -├── providers/ # provider владельца scope -├── guards/ # route/UI guards над готовым DomainApi -├── hooks/ # access/orchestration hooks -├── stores/ # только локальный UI-state -├── services/ # orchestration готовых API -├── mappers/ # domain result -> ViewModel -├── types/ # module-owned types -├── errors/ # только composition/UI errors, не domain errors -├── lib/ # локальные deterministic helpers -├── config/ # module configuration -├── styles/ # styles module -├── tests/ # scope/integration tests при необходимости -└── index.ts # public API -``` - -Не все segments обязательны. Создавай только используемые. - -Composition module не обязан иметь `.tsx`: request/application business graph использует `{scope}-business-composition.ts`, а другая non-visual orchestration может иметь root `.ts`, названный по ответственности, и public `index.ts`. - -Guard принадлежит composition scope, использует готовый `{Domain}Api` и выбирает route/UI outcome. Guard не получает product data напрямую, не создаёт business graph и не импортирует source runtime. - -Consumer composition не содержит: - -- product SDK/client/generated operation; -- product storage adapter; -- source/query adapter домена; -- domain store implementation; -- domain mapper/error; -- business factory implementation. - -Product data поступает только через `{Domain}Api`. `stores/` хранит presentation-only state: sidebar, tab, local step, transient form UI. Product entities и domain state туда не копируются. - -### Page/route graph owner - -Если composition владеет business graph и lifecycle, возможна структура: - -```text -compositions/routes/profile/ -├── profile.route.tsx -├── profile-business-composition.ts -├── providers/ -│ ├── profile-business.provider.tsx -│ └── profile-business.context.ts -├── hooks/ -│ └── use-profile-business.hook.ts -├── types/ -│ └── profile-business.type.ts -├── tests/ -│ └── profile-business-lifecycle.test.tsx -└── index.ts -``` - -Правила graph owner: - -- graph type перечисляет только реально доступные API; -- `Partial` с 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/old-docs/canons/index.md b/old-docs/canons/index.md deleted file mode 100644 index ff74cb0..0000000 --- a/old-docs/canons/index.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: SLM Design -description: Назначение архитектуры, ключевые принципы и карта разделов документации ---- - -# Основы SLM Design - -Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. - -## Рабочий алгоритм - -1. Заполни архитектурную карточку из [процесса принятия решения](./decision-process.md): роль, владелец, данные, runtime-capabilities, место, public API, lifecycle и проверки. -2. Сначала выбери слой ответственности, затем module scope, затем segments и только после этого конкретные файлы. -3. Для product data и domain state примени [runtime-границу business](./business-runtime-boundary.md). -4. Выбирай минимальное корректное место и не создавай общий provider, store, package или business-контракт «на будущее». -5. После реализации пройди [архитектурную проверку](./validation.md). Задача не завершена без обязательных business и assembly tests. - -## Дополнительные примеры - -Каноны ниже достаточны для базового архитектурного решения. Если нужен подробный пример реализации, открой конкретный файл: - -- [Композиция через Provider](../examples/react/composition-provider.md) — page-level provider, store и business composition. -- [Структуры compositions](../examples/react/composition-structures.md) — допустимые структуры слоя `compositions`. -- [Business composition](../examples/business-composition.md) — runtime-сборка business-фабрик в `compositions/business/{domain}`. -- [Тестирование business-модулей](../examples/business-testing.md) — factory-level тесты и тесты сборки. - -## Разделы спецификации - -Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: - -- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. -- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. -- [Атлас файлов SLM](./file-atlas.md) — root files, segments, структуры всех типов modules, public API и tests. -- [Business-фабрика](./business-factory.md) — контракт business-модуля, logic API, deps, доменные ошибки и сборка фабрик. -- [Runtime-граница business](./business-runtime-boundary.md) — capabilities фабрики, adapters, hooks, stores и безусловные domain errors. -- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. -- [Монорепозитории](./monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business/compositions. -- [Архитектурная проверка](./validation.md) — блокирующие gates создания, рефакторинга и ревью. - -Рекомендуемый порядок чтения: процесс решения → runtime-граница business → архитектурная проверка → атлас или подробности только нужной ветки. - -## Преимущества - -### Единый слой композиции - -Страницы, маршруты и крупные продуктовые части интерфейса собираются в `compositions`. Слой не навязывает жёсткую структуру: команда может использовать `pages/layouts/screens/widgets` или другую организацию под свой фреймворк и продукт. - -### Вертикальная организация домена - -Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы, hooks, services и mappers живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: доменная логика локализована. - -### Dependency Injection без фреймворков - -Runtime-зависимости business-модуля реализуются через фабрики — модуль декларирует что ему нужно, а composition-сборка предоставляет зависимости. Домены изолированы от SDK, storage и backend-клиентов без DI-контейнеров и шин событий. - -### Разделение ответственности без перегрузки слоёв - -Композиция приложения (`compositions/`), сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — разные слои с разной природой. Ни один слой не превращается в свалку разнородного кода. - -### Графовая композиция там, где она нужна - -Внутри `compositions` допускается граф импортов через публичный API. Это позволяет page-level store, provider или сборку business-фабрик использовать одновременно в layout, screen и widget, не перенося продуктовый runtime-state в `infra` или `shared`. - -### Горизонтальная инкапсуляция - -Вложенные модули (`parts/`) и публичные API позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. - -### Колокация по умолчанию - -Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. - -### Масштабирование через группировку - -При росте проекта слои не теряют структуру — модули группируются по естественным признакам: композиции по страницам и маршрутам, бизнес-домены по субдоменам, UI-компоненты по уровню абстракции. - -### Адаптация к монорепозиториям - -SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. `compositions` и бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы. - -## Происхождение - -SLM Design вырос на основе: - -- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей -- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое -- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию -- **Colocation Principle** — код живёт рядом с местом использования - -## Пример структуры проекта - -```text -src/ -├── app/ -│ -├── compositions/ -│ ├── business/ -│ │ ├── auth/ -│ │ └── user/ -│ ├── pages/ -│ │ ├── home/ -│ │ ├── profile/ -│ │ └── product-detail/ -│ ├── layouts/ -│ │ ├── main/ -│ │ └── dashboard/ -│ ├── screens/ -│ │ ├── home/ -│ │ └── profile/ -│ └── widgets/ -│ ├── page-heading/ -│ └── promo-banner/ -│ -├── business/ -│ ├── auth/ -│ ├── catalog/ -│ ├── orders/ -│ └── chat/ -│ -├── infra/ -│ ├── theme/ -│ ├── i18n/ -│ ├── backend-api/ -│ └── logger/ -│ -├── ui/ -│ ├── button/ -│ ├── input/ -│ ├── modal/ -│ ├── toast/ -│ └── dropdown/ -│ -└── shared/ - ├── lib/ - ├── types/ - └── styles/ -``` - -## Принципы - -- **Композиция — отдельный слой.** Страницы, маршруты и крупные продуктовые части интерфейса собираются в `compositions`. -- **Структура композиции свободна.** Команда сама выбирает организацию внутри `compositions`; базовая рекомендация — `pages/layouts/screens/widgets`. -- **Домен — единое целое.** Доменная модель, сценарии, типы, services и mappers живут в одном business-модуле. Concrete data/state/query hooks передаются фабрике через adapters. -- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. -- **Зависимости однонаправлены за пределами compositions.** `app` подключает `compositions`; `compositions` связывает `business`, `infra`, `ui` и `shared`; `business` вызывает concrete runtime-возможности только через переданные фабрике `deps`. -- **Product data проходит через business.** Page, layout, screen и widget не обращаются к product source напрямую. -- **Ошибки принадлежат домену.** Из business API выходят только собственные domain errors со стабильным `code`. -- **Внутри compositions допустим граф.** Composition modules могут импортировать друг друга через public API. -- **Архитектура — каркас, не клетка.** Правила фиксируют границы ответственности и public API, а внутреннюю форму композиции определяет команда. diff --git a/old-docs/canons/layers.md b/old-docs/canons/layers.md deleted file mode 100644 index 29e291c..0000000 --- a/old-docs/canons/layers.md +++ /dev/null @@ -1,285 +0,0 @@ ---- -title: Слои -description: Иерархия слоёв от app до shared, правила зависимостей и зона ответственности каждого слоя ---- - -# Слои - -Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. - -## Определение - -**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** - -## Группы слоёв - -Слои делятся на три группы: - -| Группа | Слои | Описание | -|--------|------|----------| -| Композиция | `app`, `compositions` | Подключают приложение к фреймворку и собирают страницы, маршруты и крупные продуктовые части интерфейса | -| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | -| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | - -## Направление зависимостей - -Любой импорт между модулями — только через публичный API. - -```text -app → compositions -compositions → business | infra | ui | shared -business → shared -infra → infra | shared -ui → ui | shared -shared -/→ SLM-слои -``` - -- `app` подключает приложение к фреймворку и импортирует готовые composition modules -- `compositions` импортирует `business`, `infra`, `ui`, `shared` -- `business` импортирует только собственные файлы, детерминированный `shared`, чистые библиотеки и type-only контракты других business-модулей -- `infra` импортирует `infra` и `shared` -- `ui` импортирует `ui` и `shared` -- `shared` не импортирует другие SLM-слои -- `business`, `infra`, `ui`, `shared` не импортируют `compositions` -- Внутри `compositions` направление импортов между composition modules не фиксируется, но импорты разрешены только через публичный API и не должны создавать runtime-циклы -- Модули `business` вызывают любую runtime-capability только через `deps` фабрики; source/query hooks, stores, events и platform APIs также являются dependencies -- `import type` не разрешает переносить ownership: business не импортирует generated DTO, SDK/client/store/query types, а infra не объявляет product graph через type-only business imports -- Pure deterministic third-party libraries допустимы внутри business, если не имеют I/O/state/hidden environment и не протекают в public contract - -## Слой App - -Точка входа приложения. Отвечает за запуск, роутинг и подключение composition modules к фреймворку. - -В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации. - -### Требования - -- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация -- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов -- Провайдеры, guards, layouts, screens и страницы — только подключает готовые из `compositions`, не реализует -- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы -- Никем не импортируется - -## Слой Compositions - -`compositions/` — слой сборки страниц, маршрутов и крупных продуктовых частей интерфейса. - -На этом слое собираются page, layout, screen, widget и другие composition modules. Они связываются между собой и с нижними слоями: `business`, `infra`, `ui`, `shared`. - -SLM не фиксирует жёсткую структуру внутри `compositions`. Команда выбирает организацию под фреймворк, роутинг, CMS и продуктовую задачу. - -Базовая рекомендация: - -```text -src/compositions/ -├── business/ -├── pages/ -├── layouts/ -├── screens/ -└── widgets/ -``` - -`business`, `pages`, `layouts`, `screens` и `widgets` внутри `compositions` не являются отдельными SLM-слоями. Это группы composition modules. - -`compositions/business/{domain}` используется для runtime-сборки business-фабрик с реальными зависимостями приложения. Это не business-слой, а composition module, который адаптирует `infra`, SDK, storage и browser API к `deps` business-фабрики. - -Внутри `compositions` различай три роли: - -| Роль | Ответственность | -|---|---| -| Integration module | `compositions/business/{domain}` реализует adapters и собирает одну business-фабрику | -| Graph owner | Page/route/provider/request scope собирает готовые business API и управляет lifecycle | -| Consumer composition | Page/layout/screen/widget вызывает `{Domain}Api` и не знает concrete product source | - -Consumer composition получает продуктовые данные только через business API. Прямые SDK/HTTP/generated/storage вызовы разрешены только dependency adapters интеграционного модуля домена. - -Технический infra-сервис без product data можно использовать в composition напрямую. Если такой сервис нужен business, он всё равно описывается business-owned capability и передаётся фабрике через adapter. - -Если business-домены сгруппированы, `compositions/business` повторяет ту же группировку: `business/app/auth` соответствует `compositions/business/app/auth`, `business/cms/content` соответствует `compositions/business/cms/content`. Это не default-структура, а способ сохранить навигацию в крупных проектах. - -Composition module может содержать обычные сегменты SLM: `ui/`, `parts/`, `hooks/`, `stores/`, `services/`, `mappers/`, `types/`, `styles/`, `lib/`, `config/`, `providers/`. - -Page-level store, provider, guard или business composition размещаются внутри page composition module, если они нужны всей странице. - -```text -compositions/pages/profile/ -├── profile.page.tsx -├── profile-business-composition.ts -├── providers/ -├── hooks/ -├── stores/ -├── types/ -└── index.ts -``` - -Layout, screen и widget могут получать через public API page composition локальный UI-state и готовые `{Domain}Api`. Product data не копируется в page store как параллельный источник истины. - -```ts -import { useProfilePageStore } from '@/compositions/pages/profile' -``` - -Внутри `compositions` направление импортов между composition modules не фиксируется. Допустим граф, но все импорты идут только через public API. - -```ts -// Хорошо -import { useProfilePageStore } from '@/compositions/pages/profile' - -// Плохо -import { useProfilePageStore } from '@/compositions/pages/profile/hooks/use-profile-page-store.hook' -``` - -### Требования - -- `compositions` содержит composition modules страниц, маршрутов и крупных продуктовых частей интерфейса -- Структура внутри `compositions` выбирается командой -- Базовая рекомендация: `business/`, `pages/`, `layouts/`, `screens/`, `widgets/` -- `business`, `pages`, `layouts`, `screens`, `widgets` внутри `compositions` являются группами composition modules -- `compositions/business/{domain}` собирает конкретную business-фабрику с runtime-зависимостями; при группировке используется зеркальный путь `compositions/business/{group}/{domain}` -- Concrete product sources, source/query hooks, domain stores, browser APIs и events подключаются только adapters интеграционного module -- Page/layout/screen/widget используют product data только через `{Domain}Api` -- Builder одной фабрики явно создаёт или получает scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters factory; integration logic не пишется inline -- Providers, stores, guards и business composition размещаются внутри того composition module, которому они принадлежат -- Внутри `compositions` импорты между composition modules разрешены в любую сторону, но только через public API -- Runtime-циклы между composition modules запрещены -- Deep imports внутрь composition modules запрещены -- `business`, `infra`, `ui` и `shared` не импортируют `compositions` - -## Слой Business - -Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, hooks, services, mappers и доменной логикой. - -Слой входит в группу «Ядро». Импортирует собственные файлы, детерминированный `shared/`, чистые библиотеки и type-only контракты других business-модулей. Каждый бизнес-модуль создаёт публичный API фабрики в корне. Любые runtime-capabilities передаются через аргументы фабрики. - -Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель и dependency contracts, `hooks/` и `services/` — wrappers над переданными capabilities, `mappers/` — доменная нормализация, `lib/` — детерминированные доменные утилиты. - -Business-модуль не содержит React-компоненты, layouts, guards, providers и page-level wrappers. Визуальный fallback, route boundary и привязка logic API к React tree размещаются в `compositions`; error mapping и допустимый доменный fallback остаются в business. - -```text -src/business/ -├── auth/ -├── catalog/ -├── orders/ -├── checkout/ -└── chat/ -``` - -Когда количество доменов затрудняет навигацию — можно ввести группировку по крупным предметным областям. Группа — папка для организации, не модуль и не public API. - -```text -src/business/ -├── app/ -│ ├── auth/ -│ ├── profile/ -│ └── orders/ -└── cms/ - ├── content/ - ├── media/ - └── navigation/ -``` - -`app` и `cms` здесь являются группами доменов. Модулями остаются конечные папки: `auth`, `profile`, `content`, `media`, `navigation`. - -### Требования - -- Один модуль = один бизнес-домен -- Business-домены можно группировать при необходимости; группа не является модулем и не содержит `index.ts` -- Циклические зависимости между доменами запрещены -- Публичный API фабрики — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты, без исключений -- Фабрика возвращает только logic API: hooks, selectors, command/query methods, scenario services -- Business-модуль не содержит React-компоненты и не возвращает компоненты из фабрики -- Любые runtime-capabilities — только через `deps` фабрики: product sources, hooks, stores, events, technical services, env и browser APIs -- Business не импортирует React/SWR/query/store runtime; concrete hooks и stores реализуются adapters -- Реальные SDK, API-клиенты, storage, state/query runtime, env и browser API подключаются в `compositions/business/{domain}` или зеркальном пути при группировке -- Business нормализует внешние результаты и подставляет только собственные domain errors -- Доменные типы (`User`, `Product`) живут здесь, не в `shared/` - -## Слой infra - -Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. - -Слой входит в группу «Ядро». Импортирует `infra/` и `shared/`. - -Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). - -```text -src/infra/ -├── theme/ -├── i18n/ -├── backend-api/ -├── maps-api/ -├── logger/ -├── feature-flags/ -└── realtime/ -``` - -### Требования - -- Один модуль = один техсервис -- Импортирует `infra/` и `shared/` -- Не содержит продуктовые composition modules конкретных страниц или маршрутов - -## Слой UI - -UI-кит без бизнес-логики: button, carousel, toast, modal. - -Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`. - -Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. - -```text -src/ui/ -├── button/ -├── input/ -├── icon/ -├── carousel/ -├── modal/ -├── toast/ -├── dropdown/ -├── tabs/ -└── tooltip/ -``` - -Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах. - -```text -src/ui/ -├── primitives/ -│ ├── button/ -│ ├── input/ -│ ├── icon/ -│ └── badge/ -└── composites/ - ├── carousel/ - ├── modal/ - ├── dropdown/ - ├── tabs/ - └── tooltip/ -``` - -### Требования - -- Не содержит бизнес-логику -- Импортирует только `ui/` и `shared/` - -## Слой Shared - -Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. - -Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. - -Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). - -Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. - -```text -src/shared/ -├── lib/ -├── types/ -├── styles/ -└── sprites/ -``` - -### Требования - -- Не имеет runtime-состояния -- Не знает о продуктовых composition modules diff --git a/old-docs/canons/modules.md b/old-docs/canons/modules.md deleted file mode 100644 index b251314..0000000 --- a/old-docs/canons/modules.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: Модули -description: Структура модуля, типы (композиционный, UI, бизнес, инфра), публичный API, отличие модуля от компонента ---- - -# Модули - -Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. - -## Определение - -**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.** - -Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно. - -Модуль не обязан быть UI-блоком. Это может быть page composition, layout composition, screen composition, widget composition, бизнес-домен, инфраструктурный сервис или UI-kit сущность. - -Главная граница модуля — не папка, а ответственность. - -## Компонент - -**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.** - -Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля. - -> Компонент отображает. Модуль организует. - -Компонент не может: - -- Импортировать код проекта за пределами родительского модуля. Единственное исключение — компоненты слоя `ui`, если правила слоёв разрешают родительскому модулю импортировать `ui`. -- Владеть архитектурными зависимостями. -- Содержать вложенные компоненты: папка компонента включает только `{name}.tsx`, `index.ts`, `styles/`, `types/`. -- Содержать вложенные модули. -- Делать внешние запросы. -- Самостоятельно получать данные. -- Выбирать источник данных. -- Композировать данные. -- Вызывать сценарные хуки. -- Оркестрировать сценарий. -- Композировать модули. -- Решать, как устроен процесс. -- Содержать бизнес-логику. -- Содержать сценарную логику. - -Компонент может рендерить другие компоненты: соседние компоненты из `ui/` своего модуля, компоненты слоя `ui` и элементы, переданные через props. Запрет «содержать» относится к структуре папки, а не к JSX-разметке: декомпозиция компонентов остаётся плоской внутри `ui/` родительского модуля. - -Если компоненту требуется что-то из запрещённого списка, он перестаёт быть компонентом и должен быть оформлен как модуль. - -```text -auth/ -├── ui/ -│ └── logout-button/ -│ ├── logout-button.tsx -│ ├── styles/ -│ │ └── logout-button.module.css -│ ├── types/ -│ │ └── logout-button-props.type.ts -│ └── index.ts -└── index.ts -``` - -## Что считается модулем - -Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу. - -## Группы модулей - -Слой может содержать не только модули, но и группы модулей. По умолчанию модуль лежит прямо в слое; группу вводят только когда модулей много и нужна явная классификация. - -Группа — навигационная папка внутри слоя или другой группы. Она классифицирует модули по типу, предметной области, продуктовой зоне или runtime-назначению, но сама не является модулем. - -Жёсткие правила группы: - -- группа не имеет public API; -- группа не содержит `index.ts`; -- группа не импортируется внешним кодом; -- группа не владеет логикой, состоянием, deps или runtime-сборкой; -- группа может содержать другие группы и конечные модули. - -Модулем считается конечная папка с самостоятельной ответственностью и публичной границей. - -```text -src/business/ -├── app/ # группа -│ ├── auth/ # business-модуль -│ └── profile/ # business-модуль -└── cms/ # группа - ├── content/ # business-модуль - └── media/ # business-модуль -``` - -Примеры модулей: - -- `compositions/pages/home/` — модуль page composition. -- `compositions/layouts/main/` — модуль layout composition. -- `compositions/screens/profile/` — модуль screen composition. -- `compositions/widgets/page-heading/` — модуль widget composition. -- `business/auth/` — модуль бизнес-домена. -- `infra/theme/` — модуль инфраструктурного сервиса. -- `ui/button/` — модуль UI-kit сущности. -- `compositions/pages/home/parts/hero-section/` — вложенный модуль page composition. - -Не считаются модулями: - -- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/`, `providers/` — это сегменты. -- `compositions/pages/`, `business/app/`, `business/cms/` — это группы, если в них нет `index.ts`. -- `compositions/pages/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента. - -## Типы модулей - -Тип модуля определяет обязательный корневой файл и стартовую структуру. - -### Композиционный модуль - -Композиционный модуль — модуль внутри `compositions`, который участвует в сборке страниц, маршрутов и крупных продуктовых частей интерфейса. - -Он может быть page, layout, screen, widget, block, entry-point, CMS-entry, route segment или другим типом композиции, выбранным командой. - -```text -compositions/pages/profile/ -├── profile.page.tsx -├── profile-business-composition.ts -├── providers/ -├── hooks/ -├── stores/ -├── parts/ -├── types/ -└── index.ts -``` - -Композиционный модуль может импортировать другие composition modules через public API. Это отличие слоя `compositions`: внутри него допускается графовая композиция. - -При этом deep imports запрещены. - -```ts -// Хорошо -import { useProfilePageStore } from '@/compositions/pages/profile' - -// Плохо -import { useProfilePageStore } from '@/compositions/pages/profile/hooks/use-profile-page-store.hook' -``` - -### UI-модуль - -Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне: - -```text -button/ -├── button.tsx -└── index.ts -``` - -`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу. - -### Бизнес-модуль - -Бизнес-модуль — модуль, который строится вокруг публичного API фабрики. - -Business-модуль содержит доменную логику, типы, hooks, services, mappers и helpers. Он не содержит React-компоненты и не возвращает компоненты из фабрики. - -Бизнес-модуль обязан иметь фабрику в корне: - -```text -auth/ -├── auth.factory.ts -├── index.ts -└── types/ -``` - -Фабрика возвращает публичный API модуля для использования в runtime. - -### Инфраструктурный модуль - -Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции. - -Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса. - -```text -theme/ -├── index.ts -├── config/ -├── hooks/ -├── styles/ -└── ui/ -``` - -```text -backend-api/ -├── backend-api.client.ts -├── config/ -├── types/ -└── index.ts -``` - -## Структура - -Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности. - -```text -{module-name}/ -├── {module-name}.factory.ts # фабрика (для business-модулей) -├── {module-name}.tsx # корневой файл модуля (опционален) -├── ui/ # компоненты модуля, кроме business-модулей -├── parts/ # вложенные модули -├── providers/ # провайдеры модуля -├── hooks/ # хуки -├── stores/ # сторы состояния -├── services/ # сценарии и операции модуля -├── mappers/ # трансформация на границе ответственности -├── types/ # типы -├── styles/ # стили -├── lib/ # утилиты модуля -├── config/ # константы и конфигурация -└── index.ts # публичный API -``` - -Подробное описание сегментов — в разделе [Сегменты](./segments.md). - -## Публичный API - -Внешний код импортирует модуль только через публичный API. - -```ts -// Хорошо -import { customerFactory } from '@/business/customer' -import type { Customer } from '@/business/customer' -``` - -```ts -// Плохо -import { validateToken } from '@/business/auth/lib/tokens' -``` - -`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. - -Внутренние сегменты модуля остаются деталями реализации. - -Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Это жёсткое правило без исключений. - -```ts -// business/customer/index.ts -export { customerFactory } from './customer.factory' - -export type { Customer } from './types/customer.type' -export type { CustomerApi } from './types/customer-api.type' -export type { CustomerDeps } from './types/customer-deps.type' -export type { CustomerFactory } from './types/customer-factory.type' -``` - -Composition module экспортирует через `index.ts` только безопасный контракт, который нужен другим composition modules или `app`: page/layout/screen/widget, provider, hooks доступа, типы. Внутренние stores, context objects и функции создания состояния не экспортируются без необходимости. - -Stateful module по умолчанию не экспортирует raw context, `StoreApi`, mutable singleton, persistence key или concrete adapter. Экспортируй domain/technical commands, selectors и access hooks. Каждый mutable export требует реального внешнего consumer и отдельного обоснования. - -Если layout, screen или widget импортируют hooks из page composition, не смешивайте в одном public API готовую page composition и hooks для дочерних модулей: это может создать runtime-цикл. - -```ts -// compositions/pages/profile/index.ts -export { ProfilePageProvider } from './providers/profile-page.provider' -export { useProfilePageStore } from './hooks/use-profile-page-store.hook' -export { useProfileBusinessComposition } from './hooks/use-profile-business-composition.hook' - -export type { ProfilePageState } from './types/profile-page-state.type' -``` - -## Фабрика - -Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный logic API фабрики. - -Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. - -Фабрика не возвращает React-компоненты, layouts, guards, boundaries, providers или page-level wrappers. Business-модуль не содержит React-компоненты: UI-решения домена размещаются в `compositions`, а полностью универсальные UI-компоненты — в `ui`. - -Модуль без runtime-capabilities экспортирует фабрику без аргументов. Модуль с зависимостями экспортирует фабрику, принимающую `deps`: API других доменов и внешние возможности, описанные бизнес-языком. Source/query hooks, state stores, subscriptions, browser API, clock и другие runtime-механизмы также считаются dependencies. Типы всегда экспортируются напрямую через `export type`, но `import type` не разрешает протащить в business generated DTO, SDK/store/query contracts. - -Runtime-сборка фабрики с реальными SDK, storage, infra-клиентами, source hooks, stores, events и browser API происходит в `compositions/business/{domain}` через отдельные adapters. Builder явно создаёт или получает scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters factory. Конечный граф business API собирается в месте, которое владеет lifecycle: page composition, route composition, application-lifetime composition provider, request scope или test setup. - -### Примеры - -Подробные правила фабрики см. в [Business-фабрика](./business-factory.md). - -Пример runtime-сборки business-фабрик см. в [Business composition](../examples/business-composition.md). - -Пример page-level Provider в React см. в [Композиция через Provider](../examples/react/composition-provider.md). - -Примеры разных структур слоя `compositions` см. в [Структуры compositions](../examples/react/composition-structures.md). - -## Жизненный цикл - -Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. - -- Нужен одной странице, route branch или крупной продуктовой части интерфейса → внутри соответствующего composition module. -- Нужен нескольким частям одной страницы → внутри page composition или другого общего composition scope. -- Нужен нескольким страницам или маршрутам → отдельный composition module внутри `compositions`. -- Абстрактный UI без бизнес-логики → `ui/`. -- Сценарий, product data contract, domain state, тип или доменная логика → `business/{domain}/`. -- Concrete implementation business dependency → adapter в `compositions/business/{domain}/`. -- Технический сервис → `infra/`. -- Общая чистая утилита → `shared/`. - -Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/old-docs/canons/monorepo.md b/old-docs/canons/monorepo.md deleted file mode 100644 index 5b2f6fe..0000000 --- a/old-docs/canons/monorepo.md +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: Монорепозитории -description: Правила применения SLM Design для frontend-проектов, находящихся в монорепозитории ---- - -# Монорепозитории - -Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов. - -## Определение - -**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.** - -## Базовая структура - -Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`. - -```text -repo/ -├── apps/ -│ ├── web/ -│ │ └── src/ -│ │ ├── app/ -│ │ ├── compositions/ -│ │ ├── business/ -│ │ ├── infra/ -│ │ ├── ui/ -│ │ └── shared/ -│ └── admin/ -│ └── src/ -│ └── ... -└── packages/ - ├── ui/ - │ ├── button/ # самостоятельный пакет UI-модуля - │ ├── input/ # самостоятельный пакет UI-модуля - │ └── modal/ # самостоятельный пакет UI-модуля - ├── infra/ - │ ├── theme/ # самостоятельный пакет infra-модуля - │ ├── backend-api/ # самостоятельный пакет infra-модуля - │ └── logger/ # самостоятельный пакет infra-модуля - └── shared/ # единый shared-пакет - ├── package.json - └── src/ - ├── lib/ # переиспользуемые утилиты - ├── helpers/ # переиспользуемые helpers - └── index.ts -``` - -`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои. - -## Группировка frontend-пакетов - -Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`. - -```text -packages/ui/* # пакеты UI-модулей -packages/infra/* # пакеты infra-модулей -packages/shared # единый shared-пакет -``` - -Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM. - -## Пакет и модуль - -Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации. - -В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению. - -```text -packages/ui/button/ -packages/ui/modal/ -packages/infra/theme/ -packages/infra/backend-api/ -packages/shared/ -``` - -## Что остаётся в приложении - -Слои `app`, `compositions` и `business` остаются внутри конкретного приложения. - -```text -apps/web/src/app/ -apps/web/src/compositions/ -apps/web/src/business/ -``` - -`app` привязан к фреймворку и entry points приложения. - -`compositions` привязан к страницам, маршрутам и крупным продуктовым частям интерфейса конкретного приложения. Этот слой не выносится в `packages/*`, потому что отражает продуктовую сборку приложения. - -`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой. - -## Что можно выносить - -В пакеты выносится только код из `ui`, `infra` и `shared`, который уже нужен двум фронтенд-приложениям либо имеет явно зафиксированный межприложенческий ownership/reuse-контракт. - -| Группа | Что выносить | Пример | -|--------|--------------|--------| -| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` | -| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` | -| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` | - -По умолчанию начинай внутри приложения. Пакет можно создать до второго consumer только при явном архитектурном решении, известном consumer и стабильном межприложенческом контракте. Абстрактная «общая природа» сама по себе недостаточна. - -## UI-пакеты - -В `packages/ui/*` размещаются переиспользуемые UI-модули. - -```text -packages/ui/button/ -├── package.json -└── src/ - ├── button.tsx - ├── styles/ - ├── types/ - └── index.ts -``` - -UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц. - -## Infra-пакеты - -В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули. - -```text -packages/infra/backend-api/ -├── package.json -└── src/ - ├── clients/ - ├── config/ - ├── types/ - └── index.ts -``` - -Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок. - -## Shared-пакет - -`packages/shared` является единым пакетом. - -```text -packages/shared/ -├── package.json -└── src/ - ├── lib/ - ├── helpers/ - └── index.ts -``` - -В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте. - -Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся. - -## Имена пакетов и импорты - -Путь импорта задаётся `name` в `package.json`, а не расположением директории. - -```json -{ - "name": "@repo/theme" -} -``` - -```text -packages/infra/theme/package.json -``` - -```ts -import { ThemeProvider } from '@repo/theme' -``` - -Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены. - -```ts -// Хорошо -import { Button } from '@repo/button' - -// Плохо -import { Button } from '@repo/button/src/button' -``` - -## Зависимости - -На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений. - -```text -apps → packages -packages -/→ apps -``` - -Внутри приложения продолжает действовать обычное направление зависимостей SLM: `app` подключает `compositions`, `compositions` связывает `business`, `infra`, `ui` и `shared`, а `business` вызывает concrete runtime-capabilities только через переданные фабрике `deps`. - -Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях. - -## Когда не выносить - -Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API. - -Фактическое использование в одном приложении допустимо для package только при зафиксированном втором consumer или внешнем ownership/reuse-контракте. Иначе сохрани локальную колокацию. - -```text -# Плохо -apps/web/src/compositions/pages/home/parts/promo-section/ -packages/ui/promo-section/ -``` - -Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным composition module. - -## Конфигурационные пакеты - -Конфигурационные пакеты не относятся к SLM-архитектуре. - -Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`. - -## Правила - -- SLM применяется внутри каждого `apps/{app}/src`. -- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`. -- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями. -- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. -- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. -- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers. -- Модуль можно размещать в package при реальном втором consumer или явном межприложенческом ownership/reuse-контракте. -- `app`, `compositions` и `business` не выносятся в пакеты. -- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`. -- Пакеты не импортируют приложения. -- Межпакетные импорты идут только через публичный API. -- Deep imports внутрь пакетов запрещены. -- Локальная колокация важнее преждевременного выноса в `packages/*`. diff --git a/old-docs/canons/segments.md b/old-docs/canons/segments.md deleted file mode 100644 index 31ca007..0000000 --- a/old-docs/canons/segments.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Сегменты -description: Сегменты внутри модуля (ui/, parts/, hooks/ и др.), назначение и правила размещения файлов ---- - -# Сегменты - -Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. - -## Определение - -**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** - -## Обзор - -| Сегмент | Содержимое | -|---------|------------| -| `ui/` | Презентационные компоненты родительского модуля | -| `parts/` | Вложенные модули со своими сегментами | -| `providers/` | Провайдеры модуля | -| `hooks/` | React-хуки | -| `stores/` | Сторы состояния | -| `services/` | Сценарии и операции владельца module | -| `mappers/` | Трансформация данных между форматами | -| `types/` | TypeScript-типы и интерфейсы | -| `styles/` | Стили | -| `lib/` | Утилиты и хелперы модуля | -| `config/` | Константы и конфигурация | - -Сегменты не являются обязательными. Например, `providers/` нужен только модулю, который владеет провайдерами. Если provider, store или guard относится к конкретной странице или маршруту, он размещается внутри соответствующего composition module, а не в `infra` или `shared`. - -Business-модули не используют `ui/` для React-компонентов. Доменный logic API живёт в factory/services/hook wrappers/mappers. React tree, guards, layouts и visual fallbacks размещаются в consumer compositions; concrete domain source hooks/stores — в adapters `compositions/business/{domain}`. - -## Сегмент ui/ - -Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля. - -Компонент в `ui/`: - -- Находится в собственной папке. -- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`. -- Не содержит вложенные компоненты и модули — папка компонента остаётся плоской. -- Может рендерить соседние компоненты из `ui/` своего модуля и компоненты слоя `ui`, если правила слоёв разрешают родительскому модулю импортировать `ui`. -- Не импортирует другой код проекта за пределами родительского модуля. -- Не делает внешние запросы. -- Не вызывает сценарные хуки. -- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. -- Не содержит бизнес-логику или сценарную логику. - -Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент). - -Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. - -```text -user/ -├── ui/ -│ ├── user-avatar/ -│ │ ├── user-avatar.tsx -│ │ ├── styles/ -│ │ │ └── user-avatar.module.css -│ │ ├── types/ -│ │ │ └── user-avatar-props.type.ts -│ │ └── index.ts -│ └── user-status/ -│ ├── user-status.tsx -│ └── index.ts -├── types/ -├── hooks/ -├── user.tsx -└── index.ts -``` - -Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`. - -## Сегмент parts/ - -Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются. - -```text -compositions/pages/home/ -├── parts/ -│ ├── hero-section/ -│ │ ├── hero-section.tsx -│ │ ├── styles/ -│ │ ├── parts/ -│ │ │ └── top-banner/ -│ │ │ ├── top-banner.tsx -│ │ │ └── index.ts -│ │ └── index.ts -│ └── features-section/ -│ ├── features-section.tsx -│ ├── hooks/ -│ └── index.ts -├── home.page.tsx -└── index.ts -``` - -Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности. - -Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке. - -Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше. - -## Сегмент providers/ - -Провайдеры модуля: React Context providers, провайдеры scope-состояния, провайдеры композиции фабрик или другие обёртки, которые принадлежат модулю. - -```text -providers/ -├── profile-page.provider.tsx -└── profile-business-composition.provider.tsx -``` - -Provider размещается в том модуле, который владеет соответствующим состоянием или композицией. Page-level provider живёт в page composition module; application-level provider, завязанный на фреймворк, подключается в `app`, но реализуется в нижнем подходящем слое. - -## Сегмент hooks/ - -React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты. - -```text -hooks/ -├── use-auth.hook.ts -├── use-session.hook.ts -└── use-permissions.hook.ts -``` - -В business-модуле `hooks/` содержит только wrappers, созданные поверх dependency hooks, переданных фабрике. Business не импортирует React state/effect APIs, SWR, TanStack Query, Apollo или другой hook runtime напрямую. - -Concrete source hook реализуется adapter-ом в `compositions/business/{domain}` и возвращает business-owned result type. Business wrapper нормализует данные и заменяет source error собственной domain error. - -## Сегмент stores/ - -Сторы состояния composition/infra/UI module. Конкретная реализация зависит от выбранного state manager (Zustand, MobX, Redux и т.д.). - -```text -stores/ -├── auth.store.ts -└── session.store.ts -``` - -Если состояние нужно всей странице, concrete store живёт в page composition module. Если состояние относится к бизнес-домену, business владеет state model, transitions и state port. Concrete Zustand/Redux/MobX adapter factory реализуется в `compositions/business/{domain}`, передаётся через `deps`, принимает initial domain state от business-фабрики и возвращает concrete port. - -Для каждого store определи creator, scope, количество instances и cleanup. Module singleton допустим только для явно доказанного application/process lifetime. - -## Сегмент services/ - -Сценарии и операции module. Содержимое зависит от слоя и владельца. - -```text -services/ -├── auth.service.ts -└── token.service.ts -``` - -Правила по слоям: - -- `business/services` реализует доменные сценарии только поверх `deps` фабрики; -- `compositions/business/{domain}/adapters` реализует concrete product/runtime dependencies, а не `services/` обычной composition; -- composition `services/` может оркестрировать готовые business API и технические infra-сервисы, но не обращаться к product source напрямую; -- `infra/services` реализует технический сервис или transport; -- `ui` и `shared` не выполняют product I/O. - -Business service не импортирует SDK, generated API, HTTP-клиент, storage, env, browser API, React/SWR/query runtime или concrete store напрямую. - -## Сегмент mappers/ - -Функции трансформации данных на границе ответственности module. - -```text -mappers/ -├── map-user.ts -├── map-product.ts -└── map-order-to-dto.ts -``` - -В business-модулях mappers защищают public contract от ненадёжной runtime-границы: преобразуют `unknown` в доменную модель, отклоняют невалидные структуры и не импортируют concrete DTO SDK. - -Dependency adapter может преобразовать доменные аргументы в transport payload, но не создаёт доменную модель из ответа, domain error или business fallback. Domain-to-ViewModel mapping принадлежит потребительской composition, если описывает только представление. - -## Сегмент types/ - -TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов. - -```text -types/ -├── user.type.ts -└── session.type.ts -``` - -В business-модулях `types/` содержит собственные доменные типы, `{Domain}Api`, `{Domain}Deps`, `{Domain}Factory`, dependency hook/state result types и доменные error codes. Generated DTO, SDK-типы, `StoreApi`, query-library types и типы HTTP-клиента не входят в контракт business-модуля. - -## Сегмент styles/ - -Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.). - -```text -styles/ -├── auth.module.css -└── login-form.module.css -``` - -## Сегмент lib/ - -Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов. - -```text -lib/ -├── validate-email.ts -└── format-phone.ts -``` - -Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`. - -## Сегмент config/ - -Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения. - -```text -config/ -├── routes.ts -└── constants.ts -``` diff --git a/old-docs/canons/validation.md b/old-docs/canons/validation.md deleted file mode 100644 index 3eaa218..0000000 --- a/old-docs/canons/validation.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -title: Архитектурная проверка -description: Блокирующие проверки SLM перед завершением создания, рефакторинга и архитектурного ревью ---- - -# Архитектурная проверка - -Не считай задачу завершённой только потому, что код компилируется или UI отображается. Проверь архитектурное решение, runtime-цепочку и обязательные тестовые границы. - -## Проверка владельца - -- У каждой ответственности один явный владелец. -- Product state и сценарии принадлежат business-домену. -- Page/route UI-state принадлежит соответствующему composition module. -- Concrete technical service принадлежит infra-модулю. -- Concrete business dependency adapter принадлежит `compositions/business/{domain}`. -- Provider находится у владельца scope, а не в generic infra-модуле. -- Код не поднят в общий слой или package без реального consumer. - -## Проверка runtime-цепочки - -Для каждого `{Domain}Api` проследи цепочку сборки: - -```text -graph owner - → per-domain builder - → dependency adapters + business factory - → {Domain}Api - → provider/entry -``` - -И отдельно цепочку вызова: - -```text -consumer - → {Domain}Api - → business scenario - → {Domain}Deps - → dependency adapter - → source/runtime -``` - -Если отсутствует хотя бы одно необходимое звено, домен не подключён. Тип будущего API, пустой provider или неиспользуемый builder не заменяет runtime-сборку. - -Для каждого route/page entry проверь, что он достигает готового composition module. `app` не должен реализовывать screen/layout/product wiring самостоятельно. - -## Проверка business imports - -В production-коде `business/**` не должно быть прямых runtime или type-only imports из concrete runtimes: - -- `infra`, `compositions`, `app`; -- SDK/generated clients; -- SWR/TanStack Query/Apollo; -- Zustand/Redux/MobX/RxJS store runtime; -- React state/effect runtime; -- storage/browser API; -- env/event bus/clock/random implementations. - -Разрешены собственные файлы, type-only business contracts, `shared` без runtime-capabilities и чистые детерминированные библиотеки. - -Проверь не только import paths, но и re-export/barrel/helper, который может скрывать запрещённую зависимость. - -## Проверка deps - -- Каждая runtime-capability присутствует в `{Domain}Deps`. -- Контракт принадлежит business и назван доменным языком. -- В `Deps` нет client/SDK/generated operation/StoreApi/query-library types. -- Dependency hook возвращает business-owned source result. -- State dependency использует business-owned state port. -- External result имеет `unknown`, если требует runtime validation. -- Subscription возвращает cleanup. -- Cross-domain API сужен до реально используемых методов. - -## Проверка adapters - -- Для каждой runtime-capability есть явный adapter. -- Adapter находится в `compositions/business/{domain}`. -- Builder не содержит inline integration logic. -- Adapter не формирует domain error. -- Adapter не выполняет domain normalization. -- Adapter не выбирает business fallback. -- Private adapters отсутствуют в public `index.ts`. -- Concrete transport imports не протекают в обычные consumer compositions. - -## Проверка product data - -- Page/layout/screen/widget получает product data через `{Domain}Api`. -- Нет прямого вызова SDK/client/generated operation из потребительской composition. -- Нет product storage access в UI/component/composition service. -- DTO не используется как domain/view contract без business normalization. -- Один источник не имеет параллельного прямого и business-пути. - -## Проверка ошибок - -Для каждого public runtime operation проверь: - -- rejected dependency; -- synchronous throw dependency; -- `undefined`/`null`/empty body; -- объект неправильной формы; -- source hook error; -- invalid state/storage value; -- неизвестную runtime-ошибку. - -Во всех случаях наружу выходит только domain error со стабильным `code`. Source error сохраняется в `cause`, но не становится consumer contract. Technical failure и malformed response нельзя превращать в fallback; fallback разрешён только для валидного доменного исхода. - -Не оставляй формулировку «domain error, если контракт это обещает». Business public contract всегда обещает только domain errors. - -## Проверка state и hooks - -- Business владеет domain state model, но не concrete state manager. -- Zustand/Redux/MobX store создаётся adapter-ом, не factory. -- SWR/Query hook создаётся adapter-ом, не business-модулем. -- Dependency hook является non-throwing/non-Suspense и передаёт technical error через business-owned result. -- Business wrapper вызывает dependency hook и возвращает собственный result type. -- Business wrapper преобразует error result и ошибки callbacks в domain errors. -- Store/query library types отсутствуют в public API и `Deps`. -- Определены creator, scope, количество instances и cleanup. -- Module singleton используется только при явно доказанном application/process lifetime. -- Provider не создаёт ложное впечатление владения instance, созданным на module scope. - -## Проверка graph - -- Per-domain builders собирают только свои фабрики. -- Graph owner назван и соответствует lifecycle. -- Домены создаются в топологическом порядке. -- Runtime-циклы отсутствуют. -- Один и тот же graph не копируется по нескольким providers без обоснования scope. -- Screen/widget не собирает graph самостоятельно. -- Provider value имеет точный тип. -- Нет `Partial` с 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/old-docs/examples/business-composition.md b/old-docs/examples/business-composition.md deleted file mode 100644 index 6fd79e1..0000000 --- a/old-docs/examples/business-composition.md +++ /dev/null @@ -1,390 +0,0 @@ ---- -title: Business composition -description: Пример runtime-сборки business-фабрик в compositions/business ---- - -# Business composition - -`compositions/business/{domain}` — composition module, который собирает конкретную business-фабрику с реальными runtime-зависимостями приложения. - -Этот модуль не является бизнес-доменом. Он находится на слое `compositions`, потому что связывает `business`, `infra`, SDK, storage, browser API и другие внешние runtime-источники. - -Это единственная integration-зона concrete product dependencies. Page, layout, screen и widget не импортируют SDK/client/storage напрямую и получают product data только через готовый `{Domain}Api`. - -## Структура - -```text -src/compositions/business/ -├── auth/ -│ ├── create-auth-business.ts -│ ├── create-auth-business.test.ts -│ ├── adapters/ -│ │ ├── phone-auth.adapter.ts -│ │ ├── session.adapter.ts -│ │ ├── auth-session-events.adapter.ts -│ │ └── zustand-auth-state.adapter.ts -│ └── index.ts -├── user/ -│ ├── create-user-business.ts -│ ├── create-user-business.test.ts -│ ├── adapters/ -│ │ ├── user-profile.adapter.ts -│ │ └── user-storage.adapter.ts -│ ├── types/ -│ │ └── create-user-business-deps.type.ts -│ └── index.ts -└── content/ - ├── create-content-business.ts - ├── create-content-business.test.ts - ├── adapters/ - │ └── content-api.adapter.ts - └── index.ts -``` - -Если business-домены сгруппированы, `compositions/business` повторяет тот же относительный путь. Например: `business/app/auth` соответствует `compositions/business/app/auth`, `business/cms/content` соответствует `compositions/business/cms/content`. - -Сегменты добавляются только по необходимости, но каждая concrete business dependency всегда оформляется отдельным файлом в `adapters/`. Не оставляй короткий adapter inline внутри builder. - -## Ответственность - -`compositions/business/{domain}` отвечает за adapter composition: - -- создаёт или получает внешние клиенты из `infra`; -- отдельными adapters адаптирует SDK, API, storage, source/query hooks, state managers, events и browser API к `deps` business-модуля; -- вызывает business-фабрику; -- принимает API других business-модулей, если текущий домен зависит от них; -- экспортирует готовый `{Domain}Api` через builder-функцию; -- тестирует сборку и корректность адаптеров. - -`compositions/business/{domain}` не должен содержать доменную логику. Если код описывает бизнес-правило, маппинг доменной модели, доменную ошибку или сценарий, он должен жить в соответствующем `business`-модуле. - -`compositions/business/{domain}` не должен содержать React-компоненты, layouts, guards, providers и page-level wrappers. Применение logic API в React tree выполняется в обычных composition modules страниц, layouts, screens или widgets. - -Builder не реализует dependencies inline. Он явно создаёт scoped runtime instances без I/O, создаёт adapters поверх них и передаёт adapters фабрике. - -## Business-контракт - -Business-модуль объявляет dependency contract. - -```ts -// business/auth/types/auth-deps.type.ts -import type { AuthState } from './auth-state.type' -import type { VerifyPhoneCodeData } from './verify-phone-code-data.type' - -export type AuthDeps = { - phoneAuth: { - requestCode: (phone: string) => Promise - 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/old-docs/examples/business-testing.md b/old-docs/examples/business-testing.md deleted file mode 100644 index 4ddee8d..0000000 --- a/old-docs/examples/business-testing.md +++ /dev/null @@ -1,364 +0,0 @@ ---- -title: Тестирование business-модулей -description: Factory-level и colocated unit-тесты для business-фабрик SLM ---- - -# Тестирование business-модулей - -Business-модуль тестируется как доменный контракт приложения. Главный контракт business-модуля — фабрика и API, который она возвращает. - -Factory-level тесты обязательны для каждого business-модуля. Assembly tests обязательны для `compositions/business/{domain}`. Внутренние colocated unit-тесты добавляются для runtime-safe логики и не заменяют проверку public API фабрики. - -## Уровни тестов - -Полное изменение домена проверяется на трёх границах: - -1. Factory-level тесты. -2. Assembly tests dependency adapters и builder. -3. Colocated unit-тесты внутренней runtime-safe логики. - -Factory-level тесты отвечают на вопрос: работает ли домен снаружи через публичный API фабрики. - -Assembly tests отвечают на вопрос: правильно ли concrete runtime реализует `Deps` и передан фабрике. - -Colocated unit-тесты отвечают на вопрос: надёжна ли внутренняя runtime-safe механика, на которой держится публичный контракт. - -## Factory-level тесты - -Размещение: - -```text -business/{domain}/tests/{domain}-factory/ -``` - -Пример: - -```text -business/user/tests/user-factory/ -├── public-api.test.tsx -├── use-current-user.test.tsx -├── update-current-user-profile.test.ts -├── get-stored-user-agreements.test.ts -└── testing/ - └── create-user-deps.mock.ts -``` - -Factory-level тесты импортируют модуль только через public API. - -```ts -import { userFactory } from '@/business/user' -``` - -Factory-level тесты не импортируют: - -- `services/*`; -- `hooks/*`; -- `mappers/*`; -- `lib/*`; -- `errors/*`; -- любые deep imports business-модуля. - -## Что покрывать на factory-level - -Каждый runtime-метод, который возвращает фабрика, должен иметь factory-level тесты. - -Обязательно проверяются: - -- полный публичный runtime API фабрики; -- happy path каждого метода; -- edge cases публичного контракта; -- корректные ответы DI-зависимостей; -- пустые ответы DI-зависимостей; -- невалидные ответы DI-зависимостей; -- rejected promise от dependency; -- синхронное исключение dependency; -- преобразование внешних ошибок в доменные ошибки; -- преобразование ошибок source hooks, stores и subscriptions в доменные ошибки; -- стабильный доменный `code` для каждой ошибки public contract; -- сохранение исходной ошибки в `cause`; -- отсутствие зависимости public contract от `status`, `message`, `response` и других внешних полей ошибки; -- порядок side effects; -- отсутствие следующих side effects после ошибки; -- hooks, если фабрика возвращает hooks. - -Пример: - -```ts -const deps = createUserDepsMock({ - profile: { - useCurrent: createCurrentUserSourceHookMock({ data: sourceUser }), - }, -}) -const userApi = userFactory(deps) - -await userApi.updateCurrentUserProfile(data) - -const result = renderHook(() => userApi.useCurrentUser()) -``` - -Тест проверяет поведение `userApi`, а не внутреннее устройство `createUpdateCurrentUserProfile`. - -## Public API тест - -У каждого business-модуля должен быть тест, который фиксирует публичный runtime API фабрики. - -```ts -it('returns stable user business API', () => { - const userApi = userFactory(createUserDepsMock()) - - expect(Object.keys(userApi).sort()).toEqual([ - 'getStoredUserAgreements', - 'updateCurrentUserProfile', - 'useCurrentUser', - ]) -}) -``` - -Такой тест не заменяет сценарные тесты методов. Он только фиксирует форму API и защищает от случайного удаления или переименования методов. - -## DI-границы - -Любая dependency фабрики считается ненадёжной runtime-границей. - -Для каждой dependency нужно проверить минимум: - -- корректный успешный ответ; -- `undefined`; -- `null`; -- пустой объект; -- объект неправильной формы; -- rejected promise; -- синхронный throw обычного method/callback/state/lifecycle dependency; -- повторные вызовы; -- смену результата dependency hook или domain state. - -Если dependency является callback'ом, проверяется порядок вызовов и payload. - -Если dependency работает с storage, проверяются битые, устаревшие и отсутствующие данные. - -## Hooks через фабрику - -Hooks, которые возвращает фабрика, тестируются через factory API. - -Проверяй: - -- hook не делает запрос без готовых входных данных; -- dependency hook получает ожидаемые доменные аргументы; -- hook корректно обрабатывает смену dependency result; -- `data` имеет доменную модель; -- `error` имеет доменный контракт; -- loading/refresh state соответствует собственному API модуля; -- невалидный dependency response не попадает наружу как валидная доменная модель. - -```ts -const useCurrent = createCurrentUserSourceHookMock({ data: sourceUser }) -const deps = createUserDepsMock({ profile: { useCurrent } }) -const userApi = userFactory(deps) - -const { result } = renderHook(() => userApi.useCurrentUser()) -``` - -Не тестируй hook business-модуля как отдельную публичную сущность, если он не является public API фабрики. - -SWR/Query cache keys, provider wrapper и library-specific revalidation тестируются в assembly tests dependency adapter, а не в business factory tests. - -## Command-сценарии - -Для command-методов вроде `save`, `update`, `change`, `request`, `verify` проверяются: - -- payload передаётся во внешнюю dependency в ожидаемой форме; -- входной payload не мутируется; -- пустой успешный ответ считается успехом, если body не нужен; -- ошибка dependency превращается в доменную ошибку; -- потребитель может принять решение по доменному `code`; -- side effects выполняются в правильном порядке; -- при ошибке одного шага следующие side effects не выполняются; -- повторный вызов не использует устаревшее состояние, если это важно для сценария. - -Если сценарий использует несколько зависимостей, тест должен явно фиксировать порядок. - -## Colocated unit-тесты - -Colocated unit-тесты размещаются рядом с файлом, который владеет runtime-логикой. - -```text -business/{domain}/ -├── errors/ -│ ├── {domain}-business.error.ts -│ └── {domain}-business.error.test.ts -├── lib/ -│ ├── normalize-{entity}.ts -│ └── normalize-{entity}.test.ts -├── mappers/ -│ ├── map-{entity}.ts -│ └── map-{entity}.test.ts -├── services/ -│ ├── update-{entity}.service.ts -│ └── update-{entity}.service.test.ts -└── hooks/ - ├── use-{scenario}.hook.ts - └── use-{scenario}.hook.test.tsx -``` - -Colocated unit-тесты нужны для: - -- mappers; -- normalizers; -- type guards; -- runtime-safe helpers; -- domain errors; -- сложных services; -- hook wrappers со сложной нормализацией domain result/error; -- storage parsers; -- fallback-логики. - -Colocated unit-тесты не нужны для: - -- type-only файлов; -- `index.ts` без runtime-логики; -- простых re-export файлов; -- статических config-файлов без branching; -- типов, которые проверяются typecheck'ом. - -## Почему colocated тесты не заменяют factory-level - -Colocated тест может доказать, что mapper работает правильно, но он не доказывает, что фабрика использует этот mapper в публичном сценарии. - -Colocated тест может доказать, что service обрабатывает ошибку, но он не доказывает, что service реально попал в public API фабрики. - -Factory-level тест проверяет интеграцию внутренних частей business-модуля как чёрный ящик. - -Правило: - -- сначала покрывай public API фабрики; -- затем усиливай покрытие colocated тестами там, где есть runtime-safe логика. - -## Маппинг и runtime safety - -Если business-модуль получает данные с любой dependency boundary, тестируй не только happy path. Boundary включает методы, source hooks, stores, events и browser capabilities. - -Проверяй: - -- отсутствующие обязательные поля; -- nullable-поля; -- поля неправильного runtime-типа; -- пустые строки; -- пробельные строки; -- странные идентификаторы; -- пустые массивы; -- не-массив вместо массива; -- частично валидные объекты; -- дефолтные значения; -- domain error для malformed response, если модель не может быть безопасно построена. - -Fallback допустим только для валидного доменного исхода, например корректно представленного отсутствия данных. Rejection, synchronous throw, source error и malformed response всегда дают domain error. - -## Доменные ошибки - -Business-модуль никогда не отдаёт наружу сырые ошибки SDK, HTTP-клиента, source hook, store, storage или browser API. Public contract всегда содержит только собственные domain errors. - -Factory-level тесты должны доказывать, что потребитель может работать только с доменным `code` и не знает форму внешней ошибки. - -Проверяй: - -- `error.name`; -- стабильный `error.code`; -- сохранение `cause`; -- отсутствие утечки DTO/HTTP-specific деталей в public contract; -- rejected promise от разных dependencies маппится в ожидаемый доменный код; -- синхронный throw dependency маппится в ожидаемый доменный код; -- невалидный успешный ответ превращается в доменный код ошибки; -- разные технические ошибки дают один код, если для потребителя это один бизнес-сценарий; -- разные пользовательские сценарии дают разные коды, если UI должен реагировать по-разному; -- safe fallback только для валидного доменного исхода, явно представленного dependency contract. - -UI и i18n должны ориентироваться на `code`, а не на `message` внешней ошибки. - -Пример factory-level проверки: - -```ts -it('throws domain error code when phone code verification fails', async () => { - const externalError = new Error('Request failed with status code 500') - const authApi = authFactory({ - phoneAuth: { - requestCode: vi.fn(), - resendCode: vi.fn(), - verifyCode: vi.fn().mockRejectedValue(externalError), - }, - session, - sessionEvents: createAuthSessionEventsMock(), - state: createAuthStateAdapterMock(), - }) - - await expect(authApi.verifyPhoneCode(data)).rejects.toMatchObject({ - name: 'AuthBusinessError', - code: 'AUTH_PHONE_CODE_VERIFY_FAILED', - cause: externalError, - }) -}) -``` - -Не проверяй в потребительских сценариях `externalError.message`, HTTP status или тип ошибки SDK как ожидаемое поведение business API. - -## Тестирование compositions/business - -Тесты `compositions/business/{domain}` проверяют сборку, а не бизнес-поведение. - -Проверяй: - -- builder вызывает нужную business-фабрику; -- adapter вызывает SDK operation с ожидаемым payload; -- storage/browser adapter соответствует dependency contract; -- API другого домена передаётся в нужном виде; -- builder deps содержат только API других собранных business-фабрик; -- builder/client/adapter constructors не выполняют I/O, storage/env reads или subscriptions во время создания API; -- state/query runtime находится в adapter и не импортируется business-модулем; -- dependency hook работает без Suspense/throw-on-error и возвращает technical error через result; -- adapter пробрасывает source error без создания domain error; -- lifecycle subscription возвращает и вызывает cleanup; -- public API composition-модуля не экспортирует внутренние adapters. - -Не проверяй здесь domain errors, fallback'и и маппинг доменной модели. Это ответственность factory-level и colocated тестов в `business/{domain}`. - -## Что не тестировать unit-тестами business-модуля - -Unit-тесты business-модуля не проверяют: - -- реальные REST-запросы; -- generated-клиенты; -- настоящий backend; -- Next.js routing; -- визуальную вёрстку; -- интеграцию с production storage; -- реальные внешние сервисы; -- e2e-поток целого приложения. - -Эти проверки относятся к `infra`, `compositions`, integration или e2e уровням. - -## Архитектурные импорты - -Проверяй production import graph business-модуля. В `business/**` не должно быть runtime или type-only imports из concrete runtimes: - -- SDK/client/infra; -- SWR/TanStack Query/Apollo; -- Zustand/Redux/MobX; -- React state/effect APIs; -- storage/browser/event implementations. - -Factory-level test обязан импортировать фабрику через public API business-модуля. Deep import `../../{domain}.factory` не доказывает корректность public boundary. - -## Чеклист - -- Каждый runtime-метод фабрики имеет factory-level тесты. -- Factory-level тесты импортируют модуль только через public API. -- Public API фабрики зафиксирован отдельным тестом. -- DI-зависимости проверены на корректные, пустые, невалидные и ошибочные ответы. -- Hooks тестируются через API, который вернула фабрика. -- Command-сценарии проверяют порядок side effects. -- После ошибки не выполняются лишние side effects. -- Runtime-safe mappers, normalizers и guards покрыты colocated unit-тестами. -- Type-only файлы не покрываются бессмысленными unit-тестами. -- Доменные ошибки имеют стабильный `code`, сохраняют `cause` и не раскрывают внешнюю ошибку как public contract. -- Тесты `compositions/business/{domain}` проверяют сборку, а не бизнес-логику. -- Business production imports не содержат concrete state/query/source runtime. -- Тесты не требуют backend, network, env и долгоживущих процессов. diff --git a/old-docs/examples/react/composition-provider.md b/old-docs/examples/react/composition-provider.md deleted file mode 100644 index 71e98d2..0000000 --- a/old-docs/examples/react/composition-provider.md +++ /dev/null @@ -1,346 +0,0 @@ ---- -title: Композиция через Provider -description: Пример page-level Provider для composition modules в React-проекте ---- - -# Композиция через Provider - -Раздел показывает, как page composition может владеть provider, store и business composition, которые нужны layout, screen и другим composition modules. - -## Идея - -Page composition хранит состояние и композицию бизнес-доменов на уровне страницы. Layout и screen не импортируют друг друга: они получают доступ к page-level данным через публичный API page composition. - -В примере `ProfilePageState` — только локальное UI-state страницы. Это не domain state и не product data cache. Доменное состояние описывается business-модулем, а concrete store/query hook передаётся его фабрике через adapter в `compositions/business/{domain}`. - -В примере page composition владеет scope-контрактом страницы, но не экспортирует готовый `ProfilePage`, потому что layout и screen импортируют hooks из `pages/profile`. Дерево страницы собирается в отдельном entry-point composition module, который слой `app` только подключает. - -## Принципы - -1. **Владение.** Page-level store, provider и business composition принадлежат page composition module. -2. **Обычные сегменты.** Provider, hooks, stores и types лежат в обычных сегментах модуля: `providers/`, `hooks/`, `stores/`, `types/`. -3. **Публичный контракт.** Page composition экспортирует только безопасные hooks, provider и типы, которые нужны другим composition modules. -4. **Сборка снаружи business.** Business-модули не используют page-level providers. Page composition вызывает builders из `compositions/business/{domain}` и владеет lifecycle готового графа. -5. **Без deep imports.** Layout и screen импортируют hooks только из public API page composition. - -## Структура модулей - -```text -compositions/pages/profile/ -├── profile-business-composition.ts -├── providers/ -│ └── profile-page.provider.tsx -├── hooks/ -│ ├── use-profile-page-store.hook.ts -│ └── use-profile-business-composition.hook.ts -├── stores/ -│ └── profile-page.store.ts -├── types/ -│ └── profile-page-state.type.ts -└── index.ts - -compositions/layouts/profile-main/ -├── profile-main.layout.tsx -└── index.ts - -compositions/screens/profile/ -├── profile.screen.tsx -├── ui/ -│ ├── profile-error/ -│ ├── profile-summary/ -│ └── profile-summary-skeleton/ -└── index.ts -``` - -## Тип состояния страницы - -Файл: `compositions/pages/profile/types/profile-page-state.type.ts`. - -```ts -export type ProfilePageState = { - title: string - isSidebarOpen: boolean - setSidebarOpen: (value: boolean) => void -} -``` - -## Store страницы - -Файл: `compositions/pages/profile/stores/profile-page.store.ts`. - -```ts -import { createStore } from 'zustand/vanilla' -import type { ProfilePageState } from '../types/profile-page-state.type' - -export const createProfilePageStore = () => - createStore((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/old-docs/examples/react/composition-structures.md b/old-docs/examples/react/composition-structures.md deleted file mode 100644 index b884945..0000000 --- a/old-docs/examples/react/composition-structures.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Структуры compositions -description: Примеры организации слоя compositions под разные способы сборки React-приложения ---- - -# Структуры compositions - -Раздел показывает, что SLM не фиксирует жёсткую структуру внутри `compositions`. Команда выбирает организацию под фреймворк, роутинг, CMS и продуктовую задачу. - -## Базовая рекомендация - -Подходит для большинства приложений, где есть явные страницы, layouts, screens и переиспользуемые композиционные блоки. - -```text -src/compositions/ -├── business/ -│ ├── auth/ -│ └── user/ -├── pages/ -│ ├── home/ -│ └── profile/ -├── layouts/ -│ ├── main/ -│ └── dashboard/ -├── screens/ -│ ├── home/ -│ └── profile/ -└── widgets/ - ├── page-heading/ - └── promo-banner/ -``` - -`business`, `pages`, `layouts`, `screens` и `widgets` здесь не являются отдельными SLM-слоями. Это группы composition modules внутри одного слоя `compositions`. - -`compositions/business/{domain}` используется для runtime-сборки business-фабрик. Он не заменяет `business/{domain}` и не содержит доменную логику. - -Только эта группа integration modules знает одновременно business dependency contract и concrete product runtime. Остальные composition modules являются graph owners или consumers готовых business API. - -## Entry-points и blocks - -Подходит для проектов, где точка сборки не всегда является страницей: CMS registry, embedded UI, route entries, feature entries. - -```text -src/compositions/ -├── entry-points/ -│ ├── cms-profile/ -│ └── embedded-checkout/ -├── pages/ -│ └── profile/ -├── layouts/ -│ └── profile-main/ -├── screens/ -│ └── profile/ -└── blocks/ - ├── profile-summary/ - └── recommended-products/ -``` - -## Группировка вокруг продукта - -Подходит, когда удобнее держать все части одной крупной области рядом. - -```text -src/compositions/ -└── profile/ - ├── page/ - ├── layout/ - ├── screen/ - └── blocks/ -``` - -## Главное правило - -Любая структура допустима, если соблюдаются границы слоя: - -- `app` подключает готовые composition modules к фреймворку. -- `compositions` может импортировать `business`, `infra`, `ui`, `shared`. -- `compositions/business/{domain}` отдельными adapters собирает конкретную business-фабрику с runtime-зависимостями. -- Page/layout/screen/widget получают product data только через `{Domain}Api`. -- Graph owner импортирует builders, но не raw product SDK/client/event для досборки домена. -- `business`, `infra`, `ui`, `shared` не импортируют `compositions`. -- Импорты между composition modules идут только через public API. -- Deep imports внутрь composition modules запрещены.