From 4fad7e712ad3eea4a794581adf59b75f6c9601db Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Fri, 24 Jul 2026 22:34:49 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=9F=D0=BE=D0=BB=D0=BD=D0=BE=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BE=D1=81=D0=BC=D1=8B=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B8,=20v2=20DRAFT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BUSINESS_MODULE_REORGANIZATION.md | 543 ++++++++++++++++++ docs2/README.md | 13 + docs2/specification/architecture-model.md | 94 +++ docs2/specification/foundations.md | 39 ++ docs2/specification/index.md | 73 +++ docs2/specification/layers/app.md | 70 +++ docs2/specification/layers/compositions.md | 99 ++++ .../specification/layers/domains/business.md | 128 +++++ .../layers/domains/client-and-server.md | 93 +++ .../layers/domains/cross-domain-boundary.md | 103 ++++ .../specification/layers/domains/framework.md | 75 +++ docs2/specification/layers/domains/index.md | 91 +++ .../layers/domains/ports-and-adapters.md | 88 +++ docs2/specification/layers/index.md | 43 ++ docs2/specification/layers/infra.md | 61 ++ docs2/specification/layers/shared.md | 42 ++ docs2/specification/layers/ui.md | 48 ++ docs2/specification/modules-and-groups.md | 86 +++ docs2/specification/monorepo.md | 61 ++ docs2/specification/public-api-and-imports.md | 81 +++ docs2/specification/runtime-and-lifecycle.md | 93 +++ docs2/specification/segments.md | 75 +++ docs2/specification/state-and-data.md | 70 +++ docs2/specification/terminology.md | 112 ++++ .../specification/testing-and-conformance.md | 82 +++ 25 files changed, 2363 insertions(+) create mode 100644 BUSINESS_MODULE_REORGANIZATION.md create mode 100644 docs2/README.md create mode 100644 docs2/specification/architecture-model.md create mode 100644 docs2/specification/foundations.md create mode 100644 docs2/specification/index.md create mode 100644 docs2/specification/layers/app.md create mode 100644 docs2/specification/layers/compositions.md create mode 100644 docs2/specification/layers/domains/business.md create mode 100644 docs2/specification/layers/domains/client-and-server.md create mode 100644 docs2/specification/layers/domains/cross-domain-boundary.md create mode 100644 docs2/specification/layers/domains/framework.md create mode 100644 docs2/specification/layers/domains/index.md create mode 100644 docs2/specification/layers/domains/ports-and-adapters.md create mode 100644 docs2/specification/layers/index.md create mode 100644 docs2/specification/layers/infra.md create mode 100644 docs2/specification/layers/shared.md create mode 100644 docs2/specification/layers/ui.md create mode 100644 docs2/specification/modules-and-groups.md create mode 100644 docs2/specification/monorepo.md create mode 100644 docs2/specification/public-api-and-imports.md create mode 100644 docs2/specification/runtime-and-lifecycle.md create mode 100644 docs2/specification/segments.md create mode 100644 docs2/specification/state-and-data.md create mode 100644 docs2/specification/terminology.md create mode 100644 docs2/specification/testing-and-conformance.md diff --git a/BUSINESS_MODULE_REORGANIZATION.md b/BUSINESS_MODULE_REORGANIZATION.md new file mode 100644 index 0000000..d730a32 --- /dev/null +++ b/BUSINESS_MODULE_REORGANIZATION.md @@ -0,0 +1,543 @@ +# Реорганизация Business-модуля + +> Статус: proposal для обсуждения. Это не принятый канон и не инструкция для механической миграции. + +## Решение, которое нужно принять + +`business/{domain}` должен быть вертикальным продуктовым модулем, а не только чистым источником доменных данных. + +Один business-модуль владеет: + +- доменной моделью, правилами, сценариями и ошибками; +- доменным состоянием и его переходами; +- адаптацией внешних возможностей к потребностям домена; +- React hooks, Provider и UI, выражающими один домен; +- отдельными server/client entrypoints, когда этого требует runtime Next.js. + +React не является причиной вынести domain hooks, store или domain UI в `compositions`. Он является входным runtime-адаптером того же домена. + +`compositions` остаётся местом, где связываются несколько доменов, выбирается scope graph и собирается page/route UI. Но он не становится владельцем `useAuth`, `AuthProvider`, `LoginForm` или domain store. + +## Почему текущая модель не подходит + +Logic-only business делит один домен по технической природе файлов: + +```text +business/auth # types, factory, scenarios +compositions/business/auth # concrete runtime adapters +compositions/pages/login # provider, hooks, domain UI +``` + +В результате у `auth` нет одной физической и понятной границы. Доменное поведение, его runtime, доступ из React и UI существуют отдельно, хотя меняются совместно. + +Особенно проблемны следующие свойства. + +1. Hooks остаются частью public API domain, но их React execution model прячется за `deps`. Business формально не импортирует React, но API всё равно нельзя вызвать из Server Component, обычной функции или теста без React render context. +2. Domain state имеет трёх владельцев: модель и transitions в `business`, concrete store в integration composition, instance и lifecycle в page Provider. Невозможно коротко ответить, где находится `auth`. +3. Правило «единственный runtime export - фабрика» защищает от обхода DI, но одновременно запрещает безопасные domain hooks, Provider и компоненты. Оно ограничивает форму public API вместо утечки реализации. +4. Разделение server/client - это граница module graph и runtime, а не две разные продуктовые ответственности. Перенос client-кода в `compositions` не решает границу, а скрывает её. +5. Consumer composition вынужден знать, как React подключается к домену, хотя это часть внутренней реализации domain runtime. + +Цель реорганизации - вернуть business-модулю вертикальное владение, не потеряв полезные инварианты текущей модели: ports, domain errors, DI, минимальные API, lifecycle и отсутствие runtime-циклов. + +## Новое определение Business-модуля + +> Business-модуль - минимальная вертикальная граница продуктового домена. Он создаёт domain runtime через фабрику и предоставляет этому runtime несколько явных интерфейсов: обычный TypeScript API, React API и при необходимости server API. + +Это один модуль и один owner. Внутри него допустимы несколько зон с разными правилами зависимостей. + +```text +business/auth/ +├── kernel/ # доменная логика без React и concrete runtime +├── adapters/ # реализации исходящих ports +├── react/ # client-only React API домена +├── server/ # server-only сборка, только при необходимости +├── auth.factory.ts # создание domain runtime +├── index.ts # universal public API +├── react.ts # client-only public entrypoint +└── server.ts # server-only public entrypoint, optional +``` + +`kernel`, `adapters`, `react` и `server` - зоны ответственности внутри одного business-модуля. Они не являются новыми SLM-слоями и не должны автоматически появляться во всех доменах. + +## Зоны и зависимости + +### Kernel + +`kernel/` содержит то, что определяет домен независимо от платформы: + +- domain types и value objects; +- domain errors со стабильными `code`; +- правила, validators, normalizers и mappers; +- use cases, commands, queries и selectors; +- модель domain state, transitions и initial state; +- контракты исходящих ports. + +`kernel` не импортирует: + +- React, Next.js, browser API; +- SDK, HTTP client, storage implementation; +- Zustand, Redux, SWR, TanStack Query и другие concrete runtimes; +- `compositions`, `app` или UI. + +Именно kernel делает business runtime переносимым между server, browser и test environment. + +### Factory + +Фабрика остаётся единственным способом создать instance domain runtime. Она принимает минимальный набор ports и возвращает поведение домена. + +```ts +export type AuthPorts = { + session: { + getCurrent: () => Promise + signIn: (input: SignInInput) => Promise + signOut: () => Promise + } + tokenStorage: { + read: () => string | null + write: (token: string | null) => void + } +} + +export type AuthRuntime = { + getCurrentUser: () => Promise + signIn: (input: SignInInput) => Promise + signOut: () => Promise + getSnapshot: () => AuthState + subscribe: (listener: () => void) => () => void +} + +export const authFactory = (ports: AuthPorts): AuthRuntime => { + // Создаёт domain state, commands, queries и selectors. +} +``` + +Фабрика: + +- не создаёт HTTP client, browser storage или query client; +- не запускает I/O, subscriptions или timers при создании instance; +- не импортирует React; +- не возвращает raw store, raw context, SDK client или adapter; +- может возвращать methods, selectors, subscriptions и другие framework-neutral runtime operations. + +Фабрика не обязана быть единственным runtime export модуля. Она является единственным местом создания business runtime. + +### Adapters + +`adapters/` реализует исходящие ports kernel поверх конкретной технологии: + +```text +business/auth/adapters/ +├── auth-api.adapter.ts +├── browser-token-storage.adapter.ts +└── server-token-storage.adapter.ts +``` + +Адаптер нужен, когда concrete dependency не соответствует domain port или должен остаться внутренней деталью домена. + +```ts +export const createAuthSessionAdapter = ( + apiClient: AuthApiClient, +): AuthPorts['session'] => ({ + getCurrent: () => apiClient.auth.getCurrentSession(), + signIn: (input) => apiClient.auth.signIn({ body: input }), + signOut: () => apiClient.auth.signOut(), +}) +``` + +Назначение адаптера не в том, чтобы «упростить вызов фабрики». Он сохраняет границу: + +- kernel знает потребность домена, а не форму SDK; +- transport payload и вызов конкретного API не становятся business contract; +- browser и server могут реализовать один port по-разному; +- замена SDK, storage или event runtime не требует менять kernel. + +Адаптер: + +- импортирует port type из kernel и concrete `infra` dependency; +- преобразует domain input в transport input; +- возвращает raw result или source error в kernel для нормализации и error mapping; +- не формирует domain model, domain error или fallback; +- не экспортируется из public entrypoint. + +Адаптер не является обязательным ритуалом. Если конкретная dependency уже точно реализует domain port и не протекает через public API, её можно передать фабрике напрямую. Не нужно создавать файл-обёртку из одной строки без адаптации или runtime-границы. + +Domain-specific adapter живёт рядом с доменом, потому что только он знает, какой endpoint или storage key реализует именно `AuthPorts`. Общий технический client, transport, logger или storage primitive остаётся в `infra`. + +### React + +`react/` - client-only входной адаптер domain runtime. Он содержит: + +- закрытый Context; +- Provider, принимающий или создающий текущий `AuthRuntime`; +- access hooks, например `useAuthRuntime`; +- domain hooks, например `useAuth`, `useCurrentUser`, `usePermissions`; +- client state/query integration, если она выражает доменное состояние; +- domain-specific interactive UI, например `LoginForm` и `LogoutButton`. + +```text +business/auth/react/ +├── auth.provider.tsx +├── hooks/ +│ ├── use-auth-runtime.ts +│ ├── use-auth.ts +│ └── use-current-user.ts +├── ui/ +│ ├── login-form.tsx +│ └── logout-button.tsx +└── create-auth-browser-runtime.ts +``` + +React-код может импортировать kernel, factory и client-specific adapters. Обратный импорт запрещён: + +```text +react → factory → kernel +react → browser adapter → infra +kernel -/→ react +factory -/→ react +``` + +Provider связывает статически экспортируемый React API с конкретным instance, созданным фабрикой: + +```tsx +'use client' + +const AuthRuntimeContext = createContext(null) + +export function AuthProvider({ + runtime, + children, +}: { + runtime: AuthRuntime + children: ReactNode +}) { + return ( + + {children} + + ) +} +``` + +```ts +export function useAuthRuntime(): AuthRuntime { + const runtime = useContext(AuthRuntimeContext) + + if (!runtime) { + throw new Error('AuthProvider is missing') + } + + return runtime +} +``` + +```ts +export function useCurrentUser() { + const auth = useAuthRuntime() + + return useQuery({ + queryKey: ['auth', 'current-user'], + queryFn: auth.getCurrentUser, + }) +} +``` + +`useCurrentUser` принадлежит `auth`, хотя использует Query runtime: он выражает доменное понятие и работает только с `AuthRuntime`. Он не вызывает SDK и не выполняет domain normalization самостоятельно. + +Компоненты из `react/ui/` могут вызывать hooks своего домена и использовать универсальные компоненты из `ui`. Они не должны: + +- напрямую обращаться к SDK, storage или browser API; +- реализовывать business rule, error mapping или нормализацию; +- импортировать runtime другого домена; +- собирать UI из нескольких доменов. + +Например, `LoginForm` принадлежит `auth`; `CheckoutHeader`, объединяющий `auth`, `cart` и `orders`, принадлежит composition module. + +### Server + +`server/` - optional зона для server-only assembly домена. Она нужна только если у домена действительно есть server-specific ports или запросный scope. + +```text +business/auth/server/ +└── create-auth-server-runtime.ts +``` + +Server builder создаёт новый runtime на request scope и передаёт в фабрику server adapters: + +```ts +import 'server-only' + +export function createAuthServerRuntime( + input: AuthServerScopeInput, +): AuthRuntime { + return authFactory({ + session: createServerSessionAdapter(input), + tokenStorage: createRequestTokenStorageAdapter(input), + }) +} +``` + +`server/` не импортирует `react/`, а `react/` не импортирует `server/`. Два runtime создаются над одной factory/kernel, но имеют разные lifecycle и concrete ports. + +Создавать `server/` симметрично в каждом домене не нужно. Если server использует только universal factory с явно переданными testable ports, отдельный entrypoint не добавляется. + +## Public entrypoints + +Ограничивать public API только фабрикой не нужно. Следует разделять public entrypoints по runtime, чтобы Next.js мог построить корректные module graphs. + +```ts +// business/auth/index.ts +export { authFactory } from './auth.factory' + +export type { + AuthPorts, + AuthRuntime, + AuthState, + SignInInput, + User, +} from './kernel' +``` + +```ts +// business/auth/react.ts +'use client' + +export { AuthProvider } from './react/auth.provider' +export { useAuth, useAuthRuntime, useCurrentUser } from './react/hooks' +export { LoginForm, LogoutButton } from './react/ui' +export { createAuthBrowserRuntime } from './react/create-auth-browser-runtime' +``` + +```ts +// business/auth/server.ts +import 'server-only' + +export { createAuthServerRuntime } from './server/create-auth-server-runtime' +export type { AuthServerScopeInput } from './server/auth-server-scope-input.type' +``` + +Правила: + +- `index.ts` не импортирует и не реэкспортирует `react` или `server`; +- `react.ts` является client entrypoint и не импортирует `server`; +- `server.ts` является server entrypoint и не импортирует `react`; +- public API может содержать стабильные pure functions, factory, hooks, Provider и domain UI в соответствующем entrypoint; +- public API не раскрывает raw context, raw store, mutable singleton, persistence key, SDK client или private adapter; +- каждый runtime export имеет реального внешнего consumer. + +Это не ослабление encapsulation. Оно заменяет запрет «runtime export вообще» на полезный запрет «не раскрывай concrete mutable implementation и не смешивай runtime graphs». + +## SSR и RSC + +Разделение entrypoints существует для Next.js module graph, а не для разделения business ownership. + +```text +Server Component + → @/business/auth/server или @/business/auth + → authFactory + → server ports/adapters + +Client Component + → @/business/auth/react + → AuthProvider + hooks + UI + → browser runtime + → authFactory + → browser ports/adapters +``` + +Один `AuthRuntime` нельзя передавать из Server Component в Client Component: он содержит functions и не сериализуется через RSC. Через границу передаются только serializable domain data, например `AuthSnapshot`, `User` или initial form state. + +Server runtime: + +- создаётся на request scope; +- получает headers, cookies, request ID и abort signal только через server input/adapters; +- не становится module singleton; +- не использует browser storage или React state. + +Browser runtime: + +- создаётся на Provider или client graph scope; +- не использует request credentials другого пользователя; +- не запускает I/O, subscriptions или timers во время module import; +- получает server bootstrap data только как serializable input. + +Провайдер может принимать готовый browser runtime. Convenience API, который создаёт runtime внутри Provider, допустим только если constructor side-effect free и lifecycle явно определён. Этот выбор нужно проверить на полном примере до фиксации канона. + +## Роль Compositions после реорганизации + +`compositions` не владеет domain hooks, domain Provider, domain UI или domain adapters. Он отвечает за связи между модулями: + +- собирает граф из нескольких готовых domain runtimes; +- выбирает application, route, page или request scope; +- передаёт суженные cross-domain dependencies при создании runtime; +- монтирует domain Providers в нужной точке React tree; +- реализует UI и orchestration, объединяющие несколько доменов; +- владеет page-local presentation state. + +```tsx +const authRuntime = createAuthBrowserRuntime() +const profileRuntime = createProfileBrowserRuntime({ + auth: pickAuthForProfile(authRuntime), +}) + +return ( + + + + + +) +``` + +Это не означает, что composition реализует `AuthProvider` или `useAuth`: она только использует public React API домена и создаёт graph в scope, который реально им владеет. + +## State и lifecycle + +Нужно различать три разные ответственности. + +| Ответственность | Владелец | +|---|---| +| Модель state, transitions, selectors, commands | kernel домена | +| Concrete implementation, например Zustand или Query | `react/` или runtime-specific adapter домена | +| Количество instances и время жизни | graph owner: Provider, route, page, application или request scope | + +Concrete store не должен экспортироваться. React API отдаёт selectors, commands и hooks: + +```ts +export function useAuthState(): AuthState { + const auth = useAuthRuntime() + + return useSyncExternalStore( + auth.subscribe, + auth.getSnapshot, + auth.getSnapshot, + ) +} +``` + +Если Zustand, Redux или другой state runtime полезен только как техническая реализация, он должен быть закрыт в `react/` или adapter. Если его модель становится частью domain API, сначала нужно описать стабильный domain contract, а не экспортировать `StoreApi`. + +Любые subscription, timer, socket и event listener: + +- запускаются после mount/commit или в явно названной `start` operation; +- возвращают cleanup; +- не запускаются при import или создании factory instance; +- не делают browser runtime частью server graph. + +## Domain UI и граница с Compositions + +Возврат UI в business не означает, что любой UI становится domain UI. + +| Сущность | Владелец | +|---|---| +| `Button`, `Modal`, `Tabs`, input primitive | `ui` | +| `LoginForm`, `LogoutButton`, `AuthRequired` | `business/auth/react/ui` | +| `CartSummary`, `AddToCartButton` | соответствующий business-домен | +| Header, объединяющий Auth, Cart и Navigation | `compositions` | +| Page, route, layout и screen | `compositions` | +| Sidebar open state, active tab и page-only flow | соответствующая composition | + +Критерий для domain UI: + +> Если сущность нельзя назвать и использовать без терминов конкретного домена, она вероятно принадлежит business-модулю. + +Критерий для composition UI: + +> Если сущность связывает несколько доменов, route/page scope или формирует конкретный экран, она принадлежит composition module. + +## Что сохранить из текущей модели + +Реорганизация не должна вернуть проблемы старого permissive подхода. Сохраняются следующие инварианты. + +1. Concrete SDK, DTO и raw external errors не становятся public contract домена. +2. Домен нормализует внешние данные и наружу выдаёт только domain model и domain errors. +3. Cross-domain runtime dependencies передаются при сборке graph, сужаются до необходимого API и не образуют циклы. +4. Factory constructors и adapter constructors не делают I/O. +5. Lifecycle subscription/resource явно запускается и очищается владельцем scope. +6. Mutable internals, raw contexts, stores, adapters и clients закрыты от consumers. +7. Common UI остаётся в `ui`; domain UI не получает право быть бесконтрольным feature layer. +8. Product data не извлекается напрямую из SDK в page/screen/component, если она уже принадлежит domain scenario. + +## Что меняется относительно текущей модели + +| Текущая идея | Предложение | +|---|---| +| Business содержит только logic API | Business владеет vertical domain: kernel, runtime adapters и React API | +| React hooks - dependency wrapper business | Hooks - client input adapter домена | +| Concrete adapters в `compositions/business/{domain}` | Domain-specific adapters рядом с доменом; `infra` хранит только общие техсервисы | +| Factory - единственный runtime export | Factory - единственный creator domain runtime; public entrypoints могут экспортировать hooks, Provider и UI | +| Business не содержит UI | Domain-specific UI находится в `business/{domain}/react/ui` | +| Provider реализуется в composition | Domain Provider реализуется в business; composition выбирает место mount и graph scope | +| Каждый домен обязан иметь одинаковую server/client форму | `server/` добавляется только при реальной server-only необходимости | + +## Не цели + +- Не нужно создавать `kernel`, `adapters`, `react` и `server` в каждом модуле заранее. +- Не нужно переносить в business page/layout/route UI. +- Не нужно возвращать прямые SDK calls из hooks или компонентов. +- Не нужно создавать generic global `BusinessProvider` или service locator. +- Не нужно вводить module singleton как замену явному graph owner. +- Не нужно считать любой `useX` domain hook: многие hooks остаются page-local или universal UI hooks. +- Не нужно механически переносить все существующие `compositions/business/*` без подтверждения ownership на реальном домене. + +## Проверочный пример перед изменением канонов + +Новая модель должна быть проверена на одном полном домене, предпочтительно `auth`. + +Минимальная цепочка: + +```text +business/auth/kernel + → AuthPorts, AuthRuntime, domain errors, session transitions + +business/auth/adapters + → HTTP session adapter, browser storage adapter, request storage adapter + +business/auth/react + → AuthProvider, useAuth, useCurrentUser, LoginForm, LogoutButton + +business/auth/server (если нужен) + → request-scoped AuthRuntime + +composition + → создаёт auth runtime в нужном scope и монтирует AuthProvider +``` + +Пример должен доказать: + +1. Server Component получает current user через server или universal API без client imports. +2. Client Component получает то же доменное состояние через `useAuth` без SDK import. +3. `LoginForm` использует domain commands и domain errors без page-specific wiring. +4. Browser и server используют одну factory/kernel, но разные adapters и instances. +5. Нельзя передать runtime instance через RSC; передаётся только serializable snapshot. +6. Cross-domain dependency можно передать без runtime cycle. +7. Provider/store lifecycle не создаёт I/O при import и не оставляет subscription после unmount. +8. Public entrypoints не тянут server код в client bundle и client code в Server Component. + +## Открытые вопросы для следующего шага + +Эти вопросы нужно решить на проверочном примере, а не декларацией. + +1. Где именно создаётся browser runtime по умолчанию: в graph composition или в convenience Provider домена? Вероятно, нужны обе формы: явный низкоуровневый Provider с `runtime` и ограниченный convenience root для независимого домена. +2. Какие зависимости вправе собирать domain-specific browser builder: только собственные adapters или также API других доменов? Рекомендуемое ограничение: другие domain APIs передаются снаружи как явный input. +3. Должен ли Query cache быть частью domain runtime или только React adapter? Базовая гипотеза: cache - React implementation detail, а domain runtime предоставляет commands/queries и invalidation intent в domain language. +4. В каких случаях domain adapter остаётся в приложении, а не рядом с business? Предлагаемый критерий: если адаптер реализует app-specific integration, недоступную другим приложениям монорепозитория, его можно держать в app assembly, сохраняя тот же port. +5. Нужно ли предоставлять Server Actions в `business/{domain}/server`? Базовая гипотеза: server action - framework entry и остаётся в app/composition; domain server facade предоставляет только business operation. +6. Какой минимальный contract нужен для server hydration domain state, чтобы не дублировать запрос на первом client render? +7. Какие типы domain UI допустимо экспортировать напрямую, а какие должны оставаться private implementation Provider/flow? + +## Критерий принятия + +Модель стоит переносить в каноны, только если проверочный домен позволяет одновременно ответить «да» на все вопросы: + +- У домена один понятный owner, несмотря на server/client runtime surfaces? +- Фабрика по-прежнему является единственным creator business runtime? +- Hooks, Provider и domain UI colocated с доменом, а не вынесены ради технической чистоты? +- Server Component не тянет React/client module graph? +- Client Component не тянет server-only graph? +- Concrete implementation не протекает через public API? +- Сценарии, нормализация и errors не дублируются между server и client? +- Graph ownership и lifecycle instances по-прежнему явны? +- Новый подход проще объяснить и применить, чем текущие `business` + `compositions/business` + consumer composition? + +Если хотя бы один ответ отрицательный, сначала нужно скорректировать proposal и проверить его на реальном коде, а не добавлять новые жёсткие правила. diff --git a/docs2/README.md b/docs2/README.md new file mode 100644 index 0000000..248ab77 --- /dev/null +++ b/docs2/README.md @@ -0,0 +1,13 @@ +# SLM Design 2.0 Draft + +`docs2/` содержит черновик новой спецификации SLM Design. + +Текущая документация в `docs/` остаётся действующим источником истины до отдельного решения о принятии новой спецификации. Skill и его generated reference пока не используют `docs2/`. + +## Точка входа + +[SLM Design Specification](./specification/index.md) + +## Границы текущего этапа + +На этом этапе в `docs2/` размещается только нормативная спецификация. Учебные материалы, руководства, примеры, справочники и agent skill будут проектироваться после стабилизации правил. diff --git a/docs2/specification/architecture-model.md b/docs2/specification/architecture-model.md new file mode 100644 index 0000000..e1b4bbb --- /dev/null +++ b/docs2/specification/architecture-model.md @@ -0,0 +1,94 @@ +--- +title: Архитектурная модель +status: draft +normative: true +--- + +# Архитектурная модель + +## Структура приложения + +**SLM-ARCH-001 - ОБЯЗАН.** SLM-приложение должно разделять код по ответственности между следующими слоями: + +```text +src/ +├── app/ +├── compositions/ +├── domains/ +├── infra/ +├── ui/ +└── shared/ +``` + +Не каждый слой обязан содержать код в минимальном приложении, но роль каждого существующего модуля должна соответствовать одному владельцу. + +## Группы ответственности + +| Группа | Слои | Ответственность | +|---|---|---| +| Framework composition | `app`, `compositions` | Подключение к framework и сборка application flows | +| Product | `domains` | Продуктовые модели, сценарии и runtime surfaces | +| Technical | `infra`, `ui` | Технические capabilities и универсальный UI | +| Foundation | `shared` | Детерминированный общий фундамент | + +## Верхнеуровневое направление + +```text +app → compositions | shared +compositions → compositions | domains | infra | ui | shared +domains → infra | ui | shared согласно правилам внутренних зон +infra → infra | shared +ui → ui | shared +shared -/→ остальные SLM-слои +``` + +Схема описывает imports между SLM-слоями проекта. Framework APIs и external packages регулируются ответственностью импортирующего слоя и не показаны как SLM-слои. + +**SLM-ARCH-002 - ОБЯЗАН.** Верхнеуровневое направление зависимостей между SLM-слоями должно соблюдаться для runtime imports и type imports, кроме явно описанных исключений. + +**SLM-ARCH-003 - ЗАПРЕЩЕНО.** Нижний слой не может импортировать `app` или `compositions`. + +**SLM-ARCH-004 - ЗАПРЕЩЕНО.** `infra`, `ui` и `shared` не могут владеть product graph или выступать service locator для domain runtimes. + +## Путь данных + +```text +app + → composition + → domain runtime surface + → domain business scenario + → business-owned port + → domain adapter + → infra / SDK / storage / external source +``` + +**SLM-ARCH-005 - ОБЯЗАН.** Каждый переход в цепочке продуктовых данных должен сохранять ownership: framework связывает, domain определяет semantics, adapter интегрирует, infra предоставляет technical capability. + +## Путь UI + +```text +app route + → page/layout composition + → domain UI и composition UI + → universal UI + → shared styles/resources +``` + +Владение multi-domain и domain UI определяется правилами [SLM-CMP-006 - SLM-CMP-007](./layers/compositions.md#product-ui) и [SLM-FRM-013](./layers/domains/framework.md#domain-ui). + +## Внутренняя модель domain + +```text +domain client/server assembly + → own business factory + → own adapters + +domain framework surface + → own DomainRuntime через runtime access boundary + +composition + → создаёт несколько domain runtimes + → передаёт готовые capabilities +``` + +Внутренняя assembly одного domain и cross-domain graph разделены правилами [Client и server assembly](./layers/domains/client-and-server.md) и [Compositions](./layers/compositions.md). diff --git a/docs2/specification/foundations.md b/docs2/specification/foundations.md new file mode 100644 index 0000000..47f1afa --- /dev/null +++ b/docs2/specification/foundations.md @@ -0,0 +1,39 @@ +--- +title: Основные инварианты +status: draft +normative: true +--- + +# Основные инварианты + +SLM Design организует frontend-приложение по владельцам ответственности. Архитектурная единица определяется не типом файла, а тем, кто владеет моделью, поведением, данными, runtime и lifecycle. + +## Ответственность до размещения + +**SLM-FND-001 - ОБЯЗАН.** Перед размещением кода необходимо определить его владельца, public API, runtime-зависимости и lifecycle scope. + +**SLM-FND-002 - СЛЕДУЕТ.** Код следует размещать в минимальном scope, который полностью владеет его ответственностью. + +**SLM-FND-003 - ЗАПРЕЩЕНО.** Нельзя переносить код в общий слой или общий package только на основании предполагаемого будущего переиспользования. + +## Путь продуктовых данных + +Внешний сервис может оставаться физическим источником данных. Domain business является единственным публичным шлюзом доменной истины внутри приложения. Точные требования определены правилами [SLM-DATA-001 - SLM-DATA-003](./state-and-data.md#domain-gateway) и [SLM-BUS-017 - SLM-BUS-020](./layers/domains/business.md#normalization-и-errors). + +## Явные зависимости + +**SLM-FND-007 - ОБЯЗАН.** Runtime-возможности должны поступать владельцу поведения через явные contracts, а не через скрытые imports, service locator или global mutable state. + +**SLM-FND-008 - ЗАПРЕЩЕНО.** Type cast, barrel, alias, dynamic import или helper в `shared` не могут использоваться для обхода архитектурной границы. + +## Public API + +Межмодульное взаимодействие и deep imports регулируются [SLM-API-001 - SLM-API-005](./public-api-and-imports.md#общие-правила). + +## Scope и lifecycle + +Создание, scope, activation и cleanup runtime определены в [Runtime и lifecycle](./runtime-and-lifecycle.md). + +## Композиция доменов + +Cross-domain runtime graph регулируется [SLM-CMP-001 - SLM-CMP-005](./layers/compositions.md#cross-domain-graph) и [Cross-domain boundary](./layers/domains/cross-domain-boundary.md). diff --git a/docs2/specification/index.md b/docs2/specification/index.md new file mode 100644 index 0000000..d9a3239 --- /dev/null +++ b/docs2/specification/index.md @@ -0,0 +1,73 @@ +--- +title: SLM Design Specification +version: 0.1.0-draft +status: draft +normative: true +--- + +# SLM Design Specification + +Эта директория содержит единый нормативный корпус SLM Design 2.0. Спецификация разделена на главы, но имеет общую версию, общий статус и единый приоритет правил. + +Пока статус равен `draft`, документы описывают проектируемую архитектуру и не заменяют действующую документацию в `docs/`. + +## Нормативный язык + +| Термин | Значение | +|---|---| +| `ОБЯЗАН` | Требование необходимо выполнить для соответствия спецификации | +| `ЗАПРЕЩЕНО` | Действие является нарушением спецификации | +| `СЛЕДУЕТ` | Рекомендуемое решение; отступление требует явного обоснования | +| `МОЖЕТ` | Допустимый, но необязательный вариант | + +Правила имеют стабильные идентификаторы вида `SLM-AREA-NNN`. Точное нормативное требование принадлежит только той главе, где объявлен его rule ID. Обзорная глава может ссылаться на правило, но не должна объявлять его повторно под новым ID. + +## Приоритет + +**SLM-DOC-001 - ОБЯЗАН.** При конфликте между главами спецификации и любым ненормативным материалом приоритет имеет спецификация. + +**SLM-DOC-002 - ЗАПРЕЩЕНО.** Ненормативный документ не может вводить новое обязательное правило, исключение или архитектурную границу. + +**SLM-DOC-003 - ОБЯЗАН.** Изменение принятого архитектурного правила должно вноситься в главу, которая владеет соответствующим rule ID. + +## Главы + +### Основы + +- [Основные инварианты](./foundations.md) +- [Терминология](./terminology.md) +- [Архитектурная модель](./architecture-model.md) + +### Слои + +- [Обзор слоёв](./layers/index.md) +- [App](./layers/app.md) +- [Compositions](./layers/compositions.md) +- [Domains](./layers/domains/index.md) +- [Infra](./layers/infra.md) +- [UI](./layers/ui.md) +- [Shared](./layers/shared.md) + +### Внутренняя модель Domains + +- [Business](./layers/domains/business.md) +- [Framework surface](./layers/domains/framework.md) +- [Ports и adapters](./layers/domains/ports-and-adapters.md) +- [Client и server assembly](./layers/domains/client-and-server.md) +- [Cross-domain boundary](./layers/domains/cross-domain-boundary.md) + +### Общие правила + +- [Модули и группы](./modules-and-groups.md) +- [Сегменты](./segments.md) +- [Public API и импорты](./public-api-and-imports.md) +- [State и data](./state-and-data.md) +- [Runtime и lifecycle](./runtime-and-lifecycle.md) +- [Тестирование и соответствие](./testing-and-conformance.md) +- [Монорепозитории](./monorepo.md) + +## Область текущего draft + +Спецификация фиксирует уже согласованные границы слоёв, доменов, business-фабрик, adapters, runtime assembly и cross-domain composition. + +Точная форма React Providers, окончательная политика package extraction для domains и единая модель query cache не фиксируются сверх явно объявленных в соответствующих главах инвариантов. diff --git a/docs2/specification/layers/app.md b/docs2/specification/layers/app.md new file mode 100644 index 0000000..a9cae66 --- /dev/null +++ b/docs2/specification/layers/app.md @@ -0,0 +1,70 @@ +--- +title: Слой App +status: draft +normative: true +--- + +# Слой App + +`app` является boundary между framework routing/runtime и SLM-модулями приложения. + +## Ответственность + +`app` может содержать: + +- route files; +- framework layout/error/loading/not-found entries; +- framework metadata и route parameters; +- bootstrap imports; +- подключение global styles/assets; +- framework-required middleware и handlers. + +## Правила + +**SLM-APP-001 - ОБЯЗАН.** Route entry должен оставаться тонким adapter, нормализующим framework input и делегирующим готовому composition module. + +```text +framework route + → composition entry +``` + +**SLM-APP-002 - ЗАПРЕЩЕНО.** `app` не может владеть product page, screen, widget, domain scenario, store, domain Provider или cross-domain graph. + +**SLM-APP-003 - ЗАПРЕЩЕНО.** Route entry не должен напрямую собирать domain adapters, вызывать SDK или формировать product model. + +**SLM-APP-004 - ОБЯЗАН.** Framework-specific input должен быть считан в `app` и передан вниз в минимальной нормализованной форме. + +Механическая нормализация включает извлечение route params, headers и framework wrappers. Product validation, создание value objects и выбор domain outcome остаются в domain business. + +**SLM-APP-005 - ЗАПРЕЩЕНО.** Другие SLM-слои не могут импортировать `app`. + +**SLM-APP-006 - СЛЕДУЕТ.** Framework behavior, которому нужен product graph или product UI, следует реализовать готовым composition entry и только подключить из `app`. + +**SLM-APP-007 - МОЖЕТ.** `app` может напрямую импортировать framework APIs и static/global resources из `shared`, если framework требует подключить их в root entry. + +## Допустимая структура + +Структуру `app` определяет framework. SLM не требует превращать framework directories в SLM modules и не требует `index.ts` для route folders. + +```text +app/ +├── layout.tsx +├── error.tsx +├── not-found.tsx +├── api/ +└── products/ + └── [product]/ + └── page.tsx +``` + +## Недопустимые владельцы + +Следующие сущности не должны определяться в `app`: + +- `ProductPage`; +- `AuthProvider`; +- `createOrdersRuntime`; +- page-local store; +- domain mapper; +- reusable product component; +- concrete product adapter. diff --git a/docs2/specification/layers/compositions.md b/docs2/specification/layers/compositions.md new file mode 100644 index 0000000..d0f09e5 --- /dev/null +++ b/docs2/specification/layers/compositions.md @@ -0,0 +1,99 @@ +--- +title: Слой Compositions +status: draft +normative: true +--- + +# Слой Compositions + +`compositions` собирает application flows из готовых domain runtimes, infra capabilities, UI modules и других composition modules. + +## Ответственность + +Composition может быть: + +- page; +- route composition entry; +- layout; +- screen; +- widget; +- provider composition; +- multi-domain hook; +- non-visual graph owner. + +Структура слоя свободна и должна отражать продуктовую навигацию приложения. + +```text +compositions/ +├── pages/ +├── layouts/ +├── screens/ +├── widgets/ +└── providers/ +``` + +Эти папки являются groups, а не отдельными слоями. + +## Cross-domain graph + +**SLM-CMP-001 - ОБЯЗАН.** Runtime graph нескольких domains должен собираться в composition, которая владеет его scope. + +```ts +const auth = createAuthRuntime() +const user = createUserRuntime({ auth: auth.session }) +const orders = createOrdersRuntime({ user: user.agreements }) +``` + +**SLM-CMP-002 - ОБЯЗАН.** Composition должна создавать domain runtimes в явном ацикличном порядке. + +**SLM-CMP-003 - ОБЯЗАН.** Cross-domain dependency должна передаваться как готовая минимальная capability, а не разрешаться service locator или domain import. + +**SLM-CMP-004 - ЗАПРЕЩЕНО.** Composition не может повторять adapter wiring, если domain public assembly уже создаёт готовый runtime. + +**SLM-CMP-005 - ЗАПРЕЩЕНО.** Composition не должна импортировать private business services, domain adapters, SDK-specific domain integration или внутренний Context domain. + +**SLM-CMP-013 - ОБЯЗАН.** Composition должна использовать public client/server creator domain, если domain предоставляет runtime-specific assembly. + +**SLM-CMP-014 - МОЖЕТ.** Composition может вызвать public business factory напрямую только для полностью universal domain без concrete adapters и runtime-specific assembly. + +## Product UI + +**SLM-CMP-006 - ОБЯЗАН.** UI, объединяющий несколько domains, route/page scope или application flow, принадлежит `compositions`. + +Примеры: + +- header, объединяющий auth, cart и navigation; +- order flow, который требует auth и user agreements; +- page screen; +- route guard с navigation outcome; +- widget, использующий hooks двух domains. + +**SLM-CMP-007 - МОЖЕТ.** Composition может использовать domain UI и universal UI, передавать им props, callbacks и slots. + +## State + +**SLM-CMP-008 - ОБЯЗАН.** Page-local presentation state принадлежит минимальной composition, охватывающей всех его consumers. + +Примеры page-local state: + +- открытие sidebar; +- активная вкладка; +- route-local wizard step; +- presentation filters; +- состояние раскрытия section. + +**SLM-CMP-009 - ЗАПРЕЩЕНО.** Page store не может становиться параллельным владельцем domain model или product cache. + +## Imports + +**SLM-CMP-010 - МОЖЕТ.** Composition module может импортировать public API других composition modules, domains, infra, ui и shared. + +**SLM-CMP-011 - ЗАПРЕЩЕНО.** Runtime-циклы между composition modules запрещены. + +**SLM-CMP-012 - ОБЯЗАН.** App-specific graph type должен отражать только реально предоставленные runtimes; `Partial` с последующим приведением к полному graph запрещён. + +**SLM-CMP-015 - ОБЯЗАН.** Client и server composition entries должны иметь раздельные public entrypoints и environment markers, если composition участвует в обоих runtime graphs. + +## Scope + +Composition может владеть application, route, page, request или test scope. Выбор scope должен следовать правилам [runtime и lifecycle](../runtime-and-lifecycle.md). diff --git a/docs2/specification/layers/domains/business.md b/docs2/specification/layers/domains/business.md new file mode 100644 index 0000000..d3d045c --- /dev/null +++ b/docs2/specification/layers/domains/business.md @@ -0,0 +1,128 @@ +--- +title: Business domain +status: draft +normative: true +--- + +# Business + +`business` является framework-neutral зоной domain и единственным владельцем его продуктовой semantics. + +## Структура + +```text +domains/{group...}/{domain}/business/ +├── {domain}.factory.ts +├── index.ts +├── types/ +├── ports/ +├── services/ +├── errors/ +├── mappers/ +├── selectors/ +├── validators/ +└── lib/ +``` + +Конкретный набор внутренних segments определяется размером domain. Обязательны роль factory и public boundary, но не каждая папка из примера. + +## Factory boundary + +**SLM-BUS-001 - ОБЯЗАН.** Business должен создавать public runtime API через factory `{domain}Factory`. + +**SLM-BUS-002 - ОБЯЗАН.** Factory должна принимать все runtime capabilities через business-owned dependency contracts. + +**SLM-BUS-003 - ОБЯЗАН.** Factory должна возвращать framework-neutral DomainRuntime или logic API domain. + +**SLM-BUS-004 - ЗАПРЕЩЕНО.** Factory не может возвращать React hooks, components, Providers, layouts, route guards или framework boundaries. + +**SLM-BUS-005 - ЗАПРЕЩЕНО.** Factory constructor не может выполнять I/O, открывать socket, регистрировать subscription, запускать timer или читать hidden environment. + +## Public API + +**SLM-BUS-006 - ОБЯЗАН.** `business/index.ts` должен экспортировать единственное runtime value: factory. + +**SLM-BUS-007 - МОЖЕТ.** `business/index.ts` может экспортировать business-owned types через `export type`. + +```ts +export { authFactory } from './auth.factory' + +export type { + AuthDeps, + AuthFactory, + AuthRuntime, + AuthState, +} from './types' +``` + +**SLM-BUS-008 - ЗАПРЕЩЕНО.** Error classes, error guards, error code constants, selectors, validators, formatters, services, mappers и port implementations не экспортируются как отдельные runtime values. + +Если внешнему consumer нужна такая capability, она должна быть осмысленной частью factory runtime API, а не обходным direct export. + +## Runtime API + +DomainRuntime может предоставлять: + +- commands; +- imperative queries; +- snapshots; +- subscriptions; +- selectors через стабильные methods; +- validation operations; +- typed outcomes; +- explicit lifecycle operations. + +**SLM-BUS-009 - ОБЯЗАН.** Runtime API должен говорить на языке domain и не повторять endpoint names, SDK tree или storage schema. + +**SLM-BUS-010 - ЗАПРЕЩЕНО.** Public contract не может раскрывать generated DTO, SDK client, query-library result, concrete store API, raw Context или adapter. + +## Dependencies и ports + +**SLM-BUS-011 - ОБЯЗАН.** Business-owned dependency описывает минимальную внешнюю возможность на языке domain. + +```ts +export type AuthPhonePort = { + requestCode: (phone: string) => Promise + verifyCode: (input: VerifyPhoneCodeInput) => Promise +} +``` + +**SLM-BUS-012 - ОБЯЗАН.** Ненадёжный внешний результат должен приниматься как `unknown`, если business обязан проверить его runtime-форму. + +**SLM-BUS-013 - ЗАПРЕЩЕНО.** Business dependency не может быть generated DTO, полный SDK client, `StoreApi`, QueryClient или framework hook. + +**SLM-BUS-014 - ОБЯЗАН.** Subscription port должен предоставлять cleanup contract. + +## Imports + +Business может runtime-импортировать: + +- собственные файлы; +- детерминированный `shared`; +- pure libraries без I/O, hidden state и public type leakage. + +Business может type-only импортировать стабильный public contract другого domain, если dependency невозможно корректно описать локальным port. Локальный consumer-owned port является предпочтительным вариантом. + +**SLM-BUS-015 - ЗАПРЕЩЕНО.** Business не импортирует React, query runtime, state manager, SDK, generated operation, HTTP client, storage implementation, browser API, infra, composition или assembly. + +Cross-domain imports дополнительно регулируются [SLM-XDOM-001 и SLM-XDOM-005 - SLM-XDOM-008](./cross-domain-boundary.md). + +## Normalization и errors + +**SLM-BUS-017 - ОБЯЗАН.** External result должен быть нормализован в business-owned model до выхода из DomainRuntime. + +**SLM-BUS-018 - ОБЯЗАН.** Malformed successful response должен считаться нарушением runtime contract, а не валидным отсутствием данных. + +**SLM-BUS-019 - ОБЯЗАН.** Expected domain outcome и technical failure должны быть различимы в public contract. + +**SLM-BUS-020 - ЗАПРЕЩЕНО.** Source error, HTTP status, SDK error class, raw response и transport message не могут быть consumer contract. + +Business может выражать ожидаемые outcomes через typed result или domain error. Эта draft-версия не предписывает единственную форму обработки ожидаемых ошибок, но требует business-owned semantics и стабильных discriminants. + +## State + +**SLM-BUS-021 - ОБЯЗАН.** Business владеет domain state model, допустимыми transitions и semantics commands/selectors. + +**SLM-BUS-022 - ЗАПРЕЩЕНО.** Business не импортирует concrete store implementation. + +Framework-neutral state runtime может быть создан самой factory или предоставлен через business-owned port. Выбор не должен раскрывать concrete implementation в public API. diff --git a/docs2/specification/layers/domains/client-and-server.md b/docs2/specification/layers/domains/client-and-server.md new file mode 100644 index 0000000..fe05acc --- /dev/null +++ b/docs2/specification/layers/domains/client-and-server.md @@ -0,0 +1,93 @@ +--- +title: Client и server assembly domain +status: draft +normative: true +--- + +# Client и Server Assembly + +`client` и `server` создают готовые runtime-specific instances одного domain поверх его business factory и adapters. + +## Client assembly + +```text +domains/{group...}/{domain}/client/ +├── create-{domain}-client-runtime.ts +└── index.ts +``` + +```ts +export const createAuthClientRuntime = (): AuthRuntime => { + return authFactory({ + phoneAuth: browserPhoneAuthAdapter, + session: browserSessionAdapter, + }) +} +``` + +**SLM-ASM-001 - ОБЯЗАН.** Client assembly может импортировать только собственную business factory, собственные client adapters, собственную React surface и необходимые runtime-specific technical inputs. + +**SLM-ASM-002 - ОБЯЗАН.** Client assembly должна возвращать готовый runtime собственного domain. + +**SLM-ASM-003 - ЗАПРЕЩЕНО.** Client assembly одного domain не может импортировать creator или runtime другого domain. + +**SLM-ASM-004 - МОЖЕТ.** Client assembly может принимать готовую внешнюю capability через собственный input contract. + +```ts +createUserClientRuntime({ auth: auth.session }) +``` + +Такой input не даёт user domain права создавать AuthRuntime или импортировать его client entrypoint. + +## Server assembly + +```text +domains/{group...}/{domain}/server/ +├── create-{domain}-server-runtime.ts +└── index.ts +``` + +**SLM-ASM-005 - ОБЯЗАН.** Server assembly должна создавать новый runtime в scope, соответствующем request или другой явно выбранной server lifetime. + +**SLM-ASM-006 - ЗАПРЕЩЕНО.** Request credentials, cookies, headers и user-specific state не могут сохраняться в process-level mutable singleton. + +**SLM-ASM-007 - ОБЯЗАН.** Framework/request input используется только для создания server adapters и не протекает как raw framework object в business API. + +**SLM-ASM-008 - ОБЯЗАН.** Server entrypoint должен иметь явный server-only marker, если framework предоставляет такой механизм. + +**SLM-ASM-015 - МОЖЕТ.** Server assembly может принимать готовую внешнюю capability через собственный input contract на тех же условиях, что и client assembly. + +**SLM-ASM-016 - ОБЯЗАН.** Server assembly может импортировать только собственную business factory, собственные server adapters и необходимые server technical inputs; импорт React/client surface запрещён. + +## Constructor и activation + +Assembly определяет способ создания, но не владеет полным cross-domain graph. + +**SLM-ASM-009 - ЗАПРЕЩЕНО.** Вызов runtime creator не должен выполнять product request, открывать socket или запускать background resource. + +```text +module import + → определяет creator + +creator call + → создаёт runtime instance + +explicit start + → запускает resources +``` + +**SLM-ASM-010 - ОБЯЗАН.** Resources запускает graph owner в выбранном scope согласно [lifecycle rules](../../runtime-and-lifecycle.md). + +## Public entrypoints + +**SLM-ASM-011 - ОБЯЗАН.** Client и server assembly должны иметь разные public entrypoints. + +**SLM-ASM-012 - ЗАПРЕЩЕНО.** Общий domain barrel не может runtime-реэкспортировать одновременно client и server surfaces. + +## Server/client bridge + +Client и server runtimes являются разными instances над общей business semantics. + +**SLM-ASM-013 - ЗАПРЕЩЕНО.** DomainRuntime, functions, Context, store или query client нельзя передавать через serializable server/client boundary. + +**SLM-ASM-014 - МОЖЕТ.** Server может передать client assembly только serializable business-owned bootstrap data без secrets и mutable runtime objects. diff --git a/docs2/specification/layers/domains/cross-domain-boundary.md b/docs2/specification/layers/domains/cross-domain-boundary.md new file mode 100644 index 0000000..9da31e5 --- /dev/null +++ b/docs2/specification/layers/domains/cross-domain-boundary.md @@ -0,0 +1,103 @@ +--- +title: Cross-domain boundary +status: draft +normative: true +--- + +# Cross-domain Boundary + +Domains не образуют скрытый runtime graph внутри слоя `domains`. Граф связывается только graph owner в `compositions`. + +## Runtime imports + +**SLM-XDOM-001 - ЗАПРЕЩЕНО.** `business` domain A не импортирует runtime values domain B. + +**SLM-XDOM-002 - ЗАПРЕЩЕНО.** Framework surface domain A не импортирует hooks, Provider, Context, components или runtime domain B. + +**SLM-XDOM-003 - ЗАПРЕЩЕНО.** Adapter domain A не импортирует adapter или runtime domain B. + +**SLM-XDOM-004 - ЗАПРЕЩЕНО.** Client/server assembly domain A не импортирует runtime creator domain B. + +Запрет распространяется на direct import, barrel re-export, dynamic import, lazy import и service locator resolution. + +## Type-only contracts + +**SLM-XDOM-005 - МОЖЕТ.** Business и client/server input contracts domain могут type-only импортировать минимальный стабильный business contract другого domain. + +**SLM-XDOM-006 - СЛЕДУЕТ.** Зависимому domain следует объявлять consumer-owned port, если capability можно описать без зависимости от полного foreign API. + +```ts +export type UserAuthPort = { + getSessionSnapshot: () => SessionSnapshot + subscribeToSession: (listener: () => void) => () => void +} +``` + +Type-only import не разрешает runtime import и не переносит ownership. + +**SLM-XDOM-012 - ЗАПРЕЩЕНО.** Type dependency cycle между domains запрещён, даже если не создаёт runtime cycle. + +## Runtime capability injection + +**SLM-XDOM-007 - МОЖЕТ.** Domain runtime creator может принять готовую structurally compatible capability, созданную другим domain и переданную composition. + +```ts +const auth = createAuthClientRuntime() +const user = createUserClientRuntime({ auth: auth.session }) +``` + +User domain знает только свой input contract. Он не знает creator, Provider, adapters и scope AuthRuntime. + +**SLM-XDOM-008 - ОБЯЗАН.** Передаваемая capability должна быть минимальной и не раскрывать raw store, Context, SDK client или mutable internals foreign domain. + +**SLM-XDOM-013 - МОЖЕТ.** Structurally compatible foreign capability может реализовать consumer-owned port напрямую. Wrapper adapter создаётся только при необходимости преобразовать contracts или lifecycle. + +## React composition + +Если React-сущность использует runtime API двух domains, она принадлежит `compositions`. + +```tsx +const ProtectedOrderForm = () => { + const auth = useAuth() + const order = useOrder() + + return auth.isAuthenticated + ? + : +} +``` + +**SLM-XDOM-009 - ОБЯЗАН.** Domain UI может получать от composition только domain-local или presentation-neutral props, callbacks и slots. Foreign domain semantics остаётся во владеющей composition. + +```tsx + + + +``` + +Такое связывание выполняется в composition, а не внутри auth или orders. + +## Events + +**SLM-XDOM-010 - ЗАПРЕЩЕНО.** Domain не подписывается напрямую на event emitter другого domain через runtime import. + +Composition может передать event capability через consumer-owned port: + +```ts +const orders = createOrdersClientRuntime({ + userEvents: { + subscribeToIdentity: user.identity.subscribe, + }, +}) +``` + +## Cycles + +**SLM-XDOM-011 - ЗАПРЕЩЕНО.** Runtime dependency cycle между domains является нарушением границы и не может скрываться event bus, lazy resolution или two-way service locator. + +Ненормативное пояснение: при обнаружении цикла следует пересмотреть один из вариантов: + +- пересмотреть границы domains; +- перенести orchestration в composition; +- выделить отдельную product responsibility; +- инвертировать зависимость через consumer-owned port. diff --git a/docs2/specification/layers/domains/framework.md b/docs2/specification/layers/domains/framework.md new file mode 100644 index 0000000..b5e9b2c --- /dev/null +++ b/docs2/specification/layers/domains/framework.md @@ -0,0 +1,75 @@ +--- +title: Framework surface domain +status: draft +normative: true +--- + +# Framework Surface + +Framework surface адаптирует готовый DomainRuntime к execution model конкретного framework. В текущей структуре React surface располагается в `react/`. + +## Структура React surface + +```text +domains/{group...}/{domain}/react/ +├── context/ +├── providers/ +├── hooks/ +├── ui/ +└── index.ts +``` + +Ни один segment не обязателен без реальной потребности. + +## Runtime access + +**SLM-FRM-001 - ОБЯЗАН.** Framework surface должна работать с конкретным DomainRuntime через domain-owned runtime access boundary. + +**SLM-FRM-002 - ЗАПРЕЩЕНО.** Framework hook или component не может самостоятельно вызывать business factory, создавать adapters или разрешать runtime из global service locator. + +**SLM-FRM-003 - ОБЯЗАН.** Runtime access boundary должна получать готовый DomainRuntime извне и не создавать параллельное domain state. + +Для React типичным механизмом является private Context, связывающий статически экспортированные hooks/components с переданным runtime instance. Это пояснение не предписывает точную форму или количество Providers в текущем draft. + +## Imports + +**SLM-FRM-004 - ОБЯЗАН.** React surface может импортировать business runtime contracts только через `import type`. + +**SLM-FRM-005 - ЗАПРЕЩЕНО.** React surface не может runtime-импортировать business factory, private business services, selectors, validators, errors или constants. + +**SLM-FRM-006 - ЗАПРЕЩЕНО.** React surface не может импортировать domain adapters, SDK, product infra client или assembly. + +**SLM-FRM-007 - МОЖЕТ.** React surface может импортировать public API `ui`, `shared` и framework libraries, разрешённые её runtime profile. + +**SLM-FRM-008 - ЗАПРЕЩЕНО.** React surface одного domain не импортирует runtime surface другого domain. + +## Hooks + +**SLM-FRM-009 - ОБЯЗАН.** Domain hook должен получать product data и behavior только через текущий DomainRuntime. + +**SLM-FRM-010 - МОЖЕТ.** Hook может использовать framework query/cache runtime как private implementation поверх imperative DomainRuntime query. + +**SLM-FRM-011 - ЗАПРЕЩЕНО.** Query hook не может использовать adapter или SDK call как fetcher в обход DomainRuntime. + +**SLM-FRM-012 - ЗАПРЕЩЕНО.** Query-library types, cache keys и raw mutate API не могут становиться public business contract. + +## Domain UI + +Domain React UI может: + +- вызывать hooks своего domain; +- использовать universal UI; +- отображать domain-owned states и outcomes; +- принимать callbacks, props и slots от composition. + +**SLM-FRM-013 - ЗАПРЕЩЕНО.** Domain UI не может импортировать runtime другого domain или оркестрировать route/page flow. + +Владение React UI, использующим несколько domains, определено правилом [SLM-CMP-006](../compositions.md#product-ui). + +## Client boundary + +**SLM-FRM-015 - ОБЯЗАН.** Entry point React hooks, Context и interactive UI должен быть явно отмечен как client runtime согласно правилам используемого framework. + +**SLM-FRM-016 - ЗАПРЕЩЕНО.** Server-compatible React export не может попадать в client entrypoint только из-за нахождения рядом с client hooks или Provider. + +React не является синонимом client runtime; environment profile определяется фактическими dependencies export. diff --git a/docs2/specification/layers/domains/index.md b/docs2/specification/layers/domains/index.md new file mode 100644 index 0000000..75a6754 --- /dev/null +++ b/docs2/specification/layers/domains/index.md @@ -0,0 +1,91 @@ +--- +title: Слой Domains +status: draft +normative: true +--- + +# Слой Domains + +`domains` содержит законченные вертикальные продуктовые модули. Domain объединяет business logic, framework surfaces, concrete adapters и runtime-specific assembly одной предметной ответственности, не смешивая их внутренние направления зависимостей. + +## Domain и group + +**SLM-DOM-001 - ОБЯЗАН.** Конечный domain должен располагаться непосредственно в `domains` или внутри одной или нескольких навигационных groups. + +```text +domains/{domain} +domains/{group}/{domain} +domains/{group}/{nested-group}/{domain} +``` + +**SLM-DOM-002 - ЗАПРЕЩЕНО.** Domain group не может иметь `index.ts`, public API, state, adapters, assembly или runtime. + +```text +domains/ +├── navigation/ # domain +└── knv/ # group + ├── auth/ # domain + ├── user/ # domain + └── orders/ # domain +``` + +**SLM-DOM-003 - ОБЯЗАН.** Первой архитектурной единицей в group tree является конечная папка, владеющая самостоятельной предметной ответственностью. + +## Внутренние зоны + +Базовая форма domain: + +```text +domains/{group...}/{domain}/ +├── business/ +├── react/ +├── adapters/ +├── client/ +└── server/ +``` + +| Зона | Статус | Ответственность | +|---|---|---| +| [`business`](./business.md) | Обязательная | Domain model, factory, ports, scenarios, errors | +| [`react`](./framework.md) | Опциональная | React runtime access, hooks, Providers, domain UI | +| [`adapters`](./ports-and-adapters.md) | Опциональная | Concrete реализации business-owned ports | +| [`client`](./client-and-server.md) | Опциональная | Browser/client assembly одного domain | +| [`server`](./client-and-server.md) | Опциональная | Server/request assembly одного domain | + +**SLM-DOM-004 - ОБЯЗАН.** Каждый domain должен содержать `business` как единственный владелец product model и business semantics. + +**SLM-DOM-005 - СЛЕДУЕТ.** Опциональную зону следует добавлять только при наличии реального runtime consumer и самостоятельной ответственности. + +**SLM-DOM-006 - ЗАПРЕЩЕНО.** Нельзя создавать пустые симметричные `react`, `adapters`, `client` или `server` на будущее. + +**SLM-DOM-007 - ОБЯЗАН.** Domain zones должны соблюдать внутреннюю dependency direction, даже если физически находятся под одним владельцем. + +## Domain ownership + +Domain может владеть: + +- model и value objects; +- product scenarios; +- domain state и transitions; +- ports; +- normalization и domain errors; +- framework hooks и UI одного domain; +- concrete integrations собственных ports; +- client/server runtime assembly собственного business. + +Domain не владеет: + +- page/route/layout composition; +- UI, объединяющим несколько domains; +- cross-domain graph; +- framework route entry; +- универсальным technical service; +- product-agnostic UI primitive. + +## Product gateway + +Framework surface и runtime assembly сохраняют business runtime единственным product gateway согласно [SLM-DATA-001 - SLM-DATA-003](../../state-and-data.md#domain-gateway). + +## Cross-domain boundary + +Domain может принять готовую внешнюю capability через contract, но не импортирует runtime surface другого domain. Точные правила определены в [cross-domain boundary](./cross-domain-boundary.md). diff --git a/docs2/specification/layers/domains/ports-and-adapters.md b/docs2/specification/layers/domains/ports-and-adapters.md new file mode 100644 index 0000000..0438640 --- /dev/null +++ b/docs2/specification/layers/domains/ports-and-adapters.md @@ -0,0 +1,88 @@ +--- +title: Ports и adapters domain +status: draft +normative: true +--- + +# Ports и Adapters + +Port определяет потребность business. Adapter связывает эту потребность с concrete runtime. + +## Ownership + +```text +domain/ +├── business/ +│ └── ports/ +└── adapters/ +``` + +**SLM-ADP-001 - ОБЯЗАН.** Port должен принадлежать `business` того domain, который потребляет capability. + +**SLM-ADP-002 - ОБЯЗАН.** Concrete adapter должен принадлежать тому же domain, но находиться вне `business`. + +**SLM-ADP-003 - ЗАПРЕЩЕНО.** Infra или external SDK не могут объявлять business port от имени domain. + +## Adapter contract + +**SLM-ADP-004 - МОЖЕТ.** Adapter может выполнять только следующие integration responsibilities: + +- импортировать type-only business port и domain input types; +- импортировать public infra API, SDK или platform runtime; +- переводить domain arguments в transport arguments; +- возвращать raw/unknown source result для business normalization; +- подписываться на concrete event source через явный lifecycle contract. + +**SLM-ADP-005 - ЗАПРЕЩЕНО.** Adapter не может выполнять следующие domain/framework responsibilities: + +- создавать domain error; +- выбирать domain fallback; +- реализовывать business rule; +- объявлять domain model; +- экспортировать concrete client consumer-коду; +- вызывать framework hook; +- обращаться к другому domain runtime. + +**SLM-ADP-006 - ОБЯЗАН.** Adapter должен реализовывать ровно тот port contract, который необходим business. + +**SLM-ADP-007 - ЗАПРЕЩЕНО.** Нельзя передавать полный client, если port требует ограниченный набор capabilities. + +**SLM-ADP-008 - ЗАПРЕЩЕНО.** Adapter integration logic не должна писаться inline в composition или runtime assembly. + +## Client и server adapters + +Adapters могут быть разделены по runtime: + +```text +adapters/ +├── client/ +│ ├── browser-session.adapter.ts +│ └── websocket-orders.adapter.ts +└── server/ + ├── request-session.adapter.ts + └── server-orders-api.adapter.ts +``` + +**SLM-ADP-009 - ОБЯЗАН.** Client adapter не должен попадать в server graph, а server adapter - в client graph. + +**SLM-ADP-010 - ОБЯЗАН.** Runtime-specific adapter должен иметь явный environment marker, если framework предоставляет такой механизм. + +## Event sources + +Socket, subscription и event listener реализуют event port: + +```ts +export type OrdersEventsPort = { + subscribe: (listener: (event: unknown) => void) => () => void +} +``` + +**SLM-ADP-011 - ОБЯЗАН.** Event adapter должен возвращать cleanup и не открывать connection при module import. + +**SLM-ADP-012 - ОБЯЗАН.** Wire event проходит business normalization до изменения domain state или передачи consumer-коду. + +## Public boundary + +**SLM-ADP-013 - ЗАПРЕЩЕНО.** `adapters` не имеет внешнего public API для app, compositions или других domains. + +Adapters доступны только assembly собственного domain и собственным contract tests. diff --git a/docs2/specification/layers/index.md b/docs2/specification/layers/index.md new file mode 100644 index 0000000..6830a9d --- /dev/null +++ b/docs2/specification/layers/index.md @@ -0,0 +1,43 @@ +--- +title: Слои +status: draft +normative: true +--- + +# Слои + +Слой определяет вид ответственности, допустимые зависимости и типы modules внутри верхнеуровневой папки `src`. + +## Матрица ответственности + +| Слой | Владеет | Не владеет | +|---|---|---| +| [`app`](./app.md) | Framework routes, bootstrap, глобальные framework boundaries | Product UI, domain logic, page state, graph assembly | +| [`compositions`](./compositions.md) | Pages, layouts, screens, widgets, cross-domain graph, scope | Domain model, domain adapters, universal UI primitives | +| [`domains`](./domains/index.md) | Product model, scenarios, ports, adapters, runtime surfaces | Route/page composition и UI нескольких domains | +| [`infra`](./infra.md) | Technical services, transports, platform integrations | Product semantics и domain graph | +| [`ui`](./ui.md) | Product-agnostic UI modules | Product scenarios и data sources | +| [`shared`](./shared.md) | Детерминированные общие resources | Runtime state, I/O и product knowledge | + +## Общие правила + +**SLM-LAY-001 - ОБЯЗАН.** Модуль должен располагаться в слое, который владеет его основной ответственностью. + +**SLM-LAY-002 - ЗАПРЕЩЕНО.** Нельзя выбирать слой по техническому типу файла без определения владельца поведения и данных. + +**SLM-LAY-003 - ОБЯЗАН.** Межслойный import должен одновременно соответствовать общей dependency direction и public API импортируемого module. + +**SLM-LAY-004 - ЗАПРЕЩЕНО.** Нельзя создавать proxy module в разрешённом слое только для обхода запрещённого направления import. + +**SLM-LAY-005 - СЛЕДУЕТ.** При смешанной ответственности module следует разделить по реальным владельцам. Cross-module и cross-domain orchestration следует выполнить в `compositions`; связь business с собственными adapters выполняется assembly соответствующего domain. + +## Выбор слоя + +| Вопрос | Слой | +|---|---| +| Код существует только из-за framework route/bootstrap? | `app` | +| Код собирает page, route, несколько modules или domains? | `compositions` | +| Код выражает продуктовую модель, сценарий или domain UI? | `domains` | +| Код предоставляет техническую capability приложения? | `infra` | +| Компонент не содержит product semantics и сценария? | `ui` | +| Код детерминирован, не знает продукт и не имеет runtime state? | `shared` | diff --git a/docs2/specification/layers/infra.md b/docs2/specification/layers/infra.md new file mode 100644 index 0000000..ccffe48 --- /dev/null +++ b/docs2/specification/layers/infra.md @@ -0,0 +1,61 @@ +--- +title: Слой Infra +status: draft +normative: true +--- + +# Слой Infra + +`infra` содержит технические capabilities приложения, не определяющие продуктовую модель и сценарии. + +## Примеры modules + +```text +infra/ +├── http/ +├── backend-api/ +├── realtime/ +├── analytics/ +├── logger/ +├── app-config/ +├── storage/ +├── i18n/ +└── theme/ +``` + +## Правила + +**SLM-INF-001 - ОБЯЗАН.** Infra module должен описывать техническую capability, а не продуктовый domain. + +**SLM-INF-002 - МОЖЕТ.** Infra module может импортировать public API другого infra module и `shared`. + +**SLM-INF-003 - ЗАПРЕЩЕНО.** Infra module не может импортировать `domains`, `compositions` или `app`. + +**SLM-INF-004 - ЗАПРЕЩЕНО.** Infra не может собирать domain factory, хранить cross-domain graph или предоставлять generic product service locator. + +**SLM-INF-005 - ЗАПРЕЩЕНО.** Infra не создаёт domain errors, domain fallback и domain model из transport DTO. + +**SLM-INF-006 - МОЖЕТ.** Infra может экспортировать technical client, transport, event source, storage primitive или platform wrapper через собственный public API. + +**SLM-INF-007 - ОБЯЗАН.** Generated SDK и transport details должны оставаться внутри infra или concrete domain adapter и не становиться public contract продуктовых consumers. + +## Отличие от adapter + +Infra знает технический механизм: + +```text +HTTP client +WebSocket transport +local storage primitive +analytics SDK +``` + +Domain adapter знает, какая часть этого механизма реализует конкретный business-owned port: + +```text +AuthPhonePort +OrdersEventsPort +UserAgreementsStoragePort +``` + +Один infra module может использоваться adapters нескольких domains без знания их product semantics. diff --git a/docs2/specification/layers/shared.md b/docs2/specification/layers/shared.md new file mode 100644 index 0000000..f59ed92 --- /dev/null +++ b/docs2/specification/layers/shared.md @@ -0,0 +1,42 @@ +--- +title: Слой Shared +status: draft +normative: true +--- + +# Слой Shared + +`shared` является детерминированным фундаментом приложения и не знает о SLM-модулях верхних слоёв. + +## Допустимое содержимое + +- pure utilities; +- value predicates; +- product-agnostic types; +- styling foundation и tokens; +- static resources; +- compile-time constants без product ownership; +- deterministic formatting primitives. + +## Правила + +**SLM-SHR-001 - ОБЯЗАН.** Результат shared utility должен определяться явными аргументами и не зависеть от скрытого runtime environment. + +**SLM-SHR-002 - ЗАПРЕЩЕНО.** `shared` не может импортировать `app`, `compositions`, `domains`, `infra` или `ui`. + +**SLM-SHR-003 - ЗАПРЕЩЕНО.** `shared` не может владеть product types, domain rules, runtime state, I/O, storage access или event subscriptions. + +**SLM-SHR-004 - ЗАПРЕЩЕНО.** Нельзя переносить domain helper, DTO, adapter contract или product config в `shared` для обхода import boundary. + +**SLM-SHR-005 - СЛЕДУЕТ.** Код следует поднимать в `shared` только при подтверждённой product-agnostic semantics, а не из-за повторения нескольких строк. + +## Отличие от других слоёв + +| Код | Владелец | +|---|---| +| Domain email validator с product rules | `domains/{domain}/business` | +| Generic string trim utility | `shared` | +| Browser storage wrapper | `infra` | +| Domain storage adapter | `domains/{domain}/adapters` | +| UI spacing tokens | `shared` | +| Button consuming spacing tokens | `ui` | diff --git a/docs2/specification/layers/ui.md b/docs2/specification/layers/ui.md new file mode 100644 index 0000000..1fc66cc --- /dev/null +++ b/docs2/specification/layers/ui.md @@ -0,0 +1,48 @@ +--- +title: Слой UI +status: draft +normative: true +--- + +# Слой UI + +`ui` содержит reusable presentation modules без product scenario и domain ownership. + +## Примеры + +```text +ui/ +├── button/ +├── input/ +├── icon/ +├── modal/ +├── carousel/ +├── tabs/ +└── tooltip/ +``` + +## Правила + +**SLM-UI-001 - ОБЯЗАН.** UI module должен быть применим без знания конкретного product domain. + +**SLM-UI-002 - ЗАПРЕЩЕНО.** UI module не может импортировать `domains`, `compositions`, `app` или product-specific infra. + +**SLM-UI-003 - МОЖЕТ.** UI module может импортировать public API других UI modules и `shared`. + +**SLM-UI-004 - ЗАПРЕЩЕНО.** UI module не выбирает product data source, не вызывает domain scenario и не владеет cross-domain behavior. + +**SLM-UI-005 - МОЖЕТ.** UI module может владеть локальным interaction state, необходимым только для собственной presentation mechanics. + +**SLM-UI-006 - ОБЯЗАН.** Компонент с product semantics должен принадлежать framework surface domain или composition, а не `ui`. + +## Классификация + +| Сущность | Владелец | +|---|---| +| `Button`, `Input`, `Modal` | `ui` | +| `LoginForm` одного auth domain | domain framework surface | +| Header с auth и navigation | `compositions` | +| Generic date picker | `ui` | +| Medication schedule | domain или composition согласно используемым domains | + +Универсальность определяется отсутствием product knowledge, а не количеством текущих consumers. diff --git a/docs2/specification/modules-and-groups.md b/docs2/specification/modules-and-groups.md new file mode 100644 index 0000000..93dd8fe --- /dev/null +++ b/docs2/specification/modules-and-groups.md @@ -0,0 +1,86 @@ +--- +title: Модули и группы +status: draft +normative: true +--- + +# Модули и Группы + +## Module + +Module является минимальным самостоятельным владельцем ответственности и предоставляет public boundary внешнему коду. + +**SLM-MOD-001 - ОБЯЗАН.** Module должен иметь одну сформулированную ответственность и одного архитектурного owner. + +**SLM-MOD-002 - ОБЯЗАН.** Внешний consumer взаимодействует с module только через его public API. + +**SLM-MOD-003 - СЛЕДУЕТ.** Module следует ограничивать только теми внутренними parts и segments, которые необходимы текущей ответственности. + +Типичные modules: + +- page, layout, screen или widget в `compositions`; +- конечный domain в `domains`; +- technical service в `infra`; +- reusable UI module в `ui`. + +`app` содержит framework entries и не обязан организовываться как SLM modules. `shared` может содержать небольшие public units, но не runtime modules. + +## Group + +Group классифицирует modules и другие groups, но не владеет поведением. + +**SLM-MOD-004 - ЗАПРЕЩЕНО.** Group не может иметь `index.ts`, public API, state, runtime, dependencies или assembly. + +**SLM-MOD-005 - ЗАПРЕЩЕНО.** Внешний код не может импортировать group path. + +**SLM-MOD-006 - МОЖЕТ.** Group может содержать другие groups и конечные modules. + +```text +domains/ +└── knv/ # group + ├── auth/ # domain module + └── orders/ # domain module +``` + +```text +compositions/ +└── pages/ # group + ├── home/ # composition module + └── profile/ # composition module +``` + +## Domain zones + +**SLM-MOD-007 - ОБЯЗАН.** `business`, `react`, `adapters`, `client` и `server` внутри конечного domain являются внутренними zones одного domain, а не самостоятельными верхнеуровневыми modules. + +Zones могут иметь собственные entrypoints, но domain остаётся единым владельцем product responsibility. + +## Component + +Component является presentation unit внутри module и не считается самостоятельным архитектурным owner. + +**SLM-MOD-008 - ЗАПРЕЩЕНО.** Component не может самостоятельно выбирать product source, собирать domain runtime или оркестрировать несколько modules. + +**SLM-MOD-009 - МОЖЕТ.** Component может владеть локальной presentation mechanics и рендерить другие components, разрешённые слоем владельца. + +**SLM-MOD-010 - ОБЯЗАН.** Presentation unit с самостоятельной ответственностью, внешними архитектурными dependencies или внутренней modular structure должна оформляться как module или nested module. Сам факт локального hook/state не делает component модулем. + +## Nested module + +Самостоятельная часть родительского module может быть оформлена nested module, если имеет собственную ответственность и public boundary только внутри родителя. + +```text +compositions/pages/home/ +└── parts/ + └── hero-section/ + ├── hero-section.tsx + └── index.ts +``` + +**SLM-MOD-011 - ЗАПРЕЩЕНО.** Nested module не может использоваться для сокрытия ответственности, которой фактически владеет другой слой или domain. + +## Scope evolution + +**SLM-MOD-012 - СЛЕДУЕТ.** Код следует поднимать из локального owner в более широкий module только после появления реального совместного consumer или общей ответственности. + +**SLM-MOD-013 - ЗАПРЕЩЕНО.** Физическое повторение само по себе не доказывает общий ownership. diff --git a/docs2/specification/monorepo.md b/docs2/specification/monorepo.md new file mode 100644 index 0000000..fa8b3a6 --- /dev/null +++ b/docs2/specification/monorepo.md @@ -0,0 +1,61 @@ +--- +title: Монорепозитории +status: draft +normative: true +--- + +# Монорепозитории + +SLM применяется внутри границы каждого frontend-приложения. Workspace packages имеют собственные public boundaries и ownership. + +## Application boundary + +```text +apps/ +└── web/ + └── src/ + ├── app/ + ├── compositions/ + ├── domains/ + ├── infra/ + ├── ui/ + └── shared/ +``` + +**SLM-MONO-001 - ОБЯЗАН.** Каждое приложение должно самостоятельно определять свой product graph и application compositions. + +**SLM-MONO-002 - ЗАПРЕЩЕНО.** Workspace package не может импортировать код из `apps/*`. + +**SLM-MONO-003 - ЗАПРЕЩЕНО.** Одно приложение не может deep-import исходники другого приложения вместо общего package contract. + +## Package boundary + +**SLM-MONO-004 - ОБЯЗАН.** Package должен иметь самостоятельного owner, public exports и подтверждённую reuse/ownership semantics. + +**SLM-MONO-005 - ЗАПРЕЩЕНО.** Нельзя создавать package только для обхода layer direction или скрытия cross-domain import. + +**SLM-MONO-006 - ОБЯЗАН.** Consumers импортируют package через объявленный package export, а не через filesystem path к internal source. + +## Типичные packages + +Допустимыми кандидатами являются: + +- product-agnostic UI kit; +- technical infra client; +- deterministic shared foundation; +- schema/codegen/tooling package; +- configuration package без application graph. + +## Domains в packages + +Эта draft-версия определяет Domain как module внутри `apps/{app}/src/domains` и пока не определяет packaged domain как conforming SLM Domain. + +**SLM-MONO-007 - ОБЯЗАН.** До принятия отдельной package-модели Domain должен оставаться внутри владеющего приложения. + +**SLM-MONO-008 - ЗАПРЕЩЕНО.** Package не может называться Domain для целей этой версии Specification, если он не соответствует определённому application path и ownership. + +## Dependency direction + +**SLM-MONO-009 - ОБЯЗАН.** Package dependency graph должен оставаться ацикличным и соответствовать заявленной ответственности packages. + +**SLM-MONO-010 - ЗАПРЕЩЕНО.** Shared package не может импортировать domain, composition или app-specific infra package. diff --git a/docs2/specification/public-api-and-imports.md b/docs2/specification/public-api-and-imports.md new file mode 100644 index 0000000..ce463b9 --- /dev/null +++ b/docs2/specification/public-api-and-imports.md @@ -0,0 +1,81 @@ +--- +title: Public API и импорты +status: draft +normative: true +--- + +# Public API и Импорты + +Public API ограничивает знание consumers о внутренней структуре module и отделяет runtime profiles. + +## Общие правила + +**SLM-API-001 - ОБЯЗАН.** Межмодульный import должен использовать public entrypoint импортируемого module. + +**SLM-API-002 - ЗАПРЕЩЕНО.** Deep imports во внутренние segments, zones и files другого module запрещены. + +**SLM-API-003 - ОБЯЗАН.** Каждый runtime export должен иметь реального consumer за пределами владеющего entrypoint и стабильную ответственность. Sibling zone собственного domain и public-boundary test считаются consumers zone entrypoint. + +**SLM-API-004 - ЗАПРЕЩЕНО.** Public API не может экспортировать raw Context, mutable store, persistence key, concrete adapter, SDK client или internal service. + +**SLM-API-005 - ОБЯЗАН.** Alias или package subpath должен физически разрешаться TypeScript, tests и production build. + +## Layer matrix + +| Importer | Runtime imports | +|---|---| +| `app` | Public composition entries, shared static/global resources | +| `compositions` | Compositions, domain runtime surfaces, infra, ui, shared | +| Domain `business` | Own files, shared, pure libraries | +| Domain `react` | Own business types, ui, shared, framework libraries | +| Domain `adapters` | Own business types/ports, infra, SDK/platform runtime | +| Domain `client/server` | Own factory, own adapters, own runtime surface | +| `infra` | Infra, shared | +| `ui` | UI, shared | +| `shared` | External pure libraries only | + +Cross-domain rules дополнительно ограничены [cross-domain boundary](./layers/domains/cross-domain-boundary.md). + +## Type-only imports + +**SLM-API-006 - МОЖЕТ.** `import type` может использоваться для разрешённого contract dependency без создания runtime edge. + +**SLM-API-007 - ЗАПРЕЩЕНО.** Type-only import не разрешает перенос ownership, импорт concrete runtime type или обход layer boundary. + +**SLM-API-008 - СЛЕДУЕТ.** Cross-domain capability следует описывать consumer-owned structural port вместо зависимости от полного foreign API type. + +## Business entrypoint + +```text +domains/{domain}/business/index.ts +``` + +Точный contract business entrypoint определён правилами [SLM-BUS-006 - SLM-BUS-008](./layers/domains/business.md#public-api). + +## Runtime-specific domain entrypoints + +Domain может иметь отдельные public surfaces: + +```text +domains/{domain}/react +domains/{domain}/client +domains/{domain}/server +``` + +Разделение client/server exports и markers определено правилами [SLM-ASM-011 - SLM-ASM-014](./layers/domains/client-and-server.md#public-entrypoints). + +Точная форма re-export между `react` и `client` в этом draft не предписана. Независимо от формы должны соблюдаться environment isolation и отсутствие обхода DomainRuntime. + +## Groups и private zones + +**SLM-API-013 - ЗАПРЕЩЕНО.** Group не имеет public entrypoint. + +**SLM-API-014 - ЗАПРЕЩЕНО.** Domain `adapters` не экспортируется app, compositions или другим domains. + +**SLM-API-015 - ОБЯЗАН.** Composition public API экспортирует только entry components, точные access hooks/types и contracts, необходимые внешним composition consumers. + +## Cycles + +**SLM-API-016 - ЗАПРЕЩЕНО.** Runtime import cycle между modules запрещён независимо от того, способен ли bundler его выполнить. + +**SLM-API-017 - ЗАПРЕЩЕНО.** Barrel не должен создавать скрытый cycle между ready composition и access API её children. diff --git a/docs2/specification/runtime-and-lifecycle.md b/docs2/specification/runtime-and-lifecycle.md new file mode 100644 index 0000000..d99c78a --- /dev/null +++ b/docs2/specification/runtime-and-lifecycle.md @@ -0,0 +1,93 @@ +--- +title: Runtime и lifecycle +status: draft +normative: true +--- + +# Runtime и Lifecycle + +Lifecycle является архитектурной частью любого mutable runtime, subscription и external resource. + +## Три стадии + +```text +definition + → module объявляет creators + +creation + → creator создаёт runtime instance без внешних effects + +activation + → graph owner запускает resources и получает cleanup +``` + +**SLM-LIFE-001 - ЗАПРЕЩЕНО.** Module import не должен выполнять product I/O, открывать connection или регистрировать global listener. + +**SLM-LIFE-002 - ОБЯЗАН.** Factory и runtime creator должны быть side-effect free относительно external resources. + +**SLM-LIFE-003 - ОБЯЗАН.** Subscription, socket, timer и listener запускаются явной operation владельца scope. + +**SLM-LIFE-004 - ОБЯЗАН.** Каждый запущенный resource должен иметь cleanup или dispose contract. + +## Scope + +| Scope | Примеры владельца | +|---|---| +| Application | Root composition/provider | +| Route branch | Route layout composition | +| Page | Page composition/provider | +| Component flow | Nested composition module | +| Request | Server composition/request builder | +| Test | Test setup/wrapper | + +**SLM-LIFE-005 - ОБЯЗАН.** Graph owner должен определить количество instances и duration каждого runtime. + +**SLM-LIFE-006 - ЗАПРЕЩЕНО.** Module-level singleton не может использоваться как случайная замена application scope. + +**SLM-LIFE-007 - МОЖЕТ.** Application singleton допустим только при явном application ownership и отсутствии request-, identity- и user-specific data. + +## Graph activation + +**SLM-LIFE-008 - ОБЯЗАН.** Cross-domain graph запускается в dependency order и освобождается в обратном порядке. + +**SLM-LIFE-009 - ОБЯЗАН.** Повторный mount/unmount, включая development Strict Mode, не должен оставлять duplicate subscription или abandoned resource. + +**SLM-LIFE-010 - СЛЕДУЕТ.** `start` и cleanup следует проектировать idempotent либо явно защищать от повторного вызова. + +**SLM-LIFE-018 - ОБЯЗАН.** Если activation графа завершилась ошибкой, graph owner должен освободить уже успешно запущенную часть графа в обратном порядке. + +**SLM-LIFE-019 - ОБЯЗАН.** Ошибка cleanup должна быть наблюдаемой и не должна препятствовать попытке освободить остальные resources графа. + +## Events и sockets + +Socket является technical transport, а его product events входят в domain через business-owned event port. + +```text +socket transport + → domain adapter + → business event normalization + → state transition или invalidation intent + → framework projection +``` + +**SLM-LIFE-011 - ЗАПРЕЩЕНО.** Framework component не может подписываться на product socket напрямую. + +**SLM-LIFE-012 - ОБЯЗАН.** Invalid event и connection failure должны преобразовываться в domain state/outcome либо technical telemetry согласно их semantics; callback error нельзя терять через unobserved throw. + +**SLM-LIFE-013 - МОЖЕТ.** Один physical transport может обслуживать adapters нескольких domains, если transport остаётся domain-agnostic, а adapters получают суженные channels. + +## Revalidation events + +Event может содержать domain update или только сообщать об устаревании данных. + +**SLM-LIFE-014 - ОБЯЗАН.** Invalidation intent должен выражаться domain language и не требовать import конкретной query library в business. + +Framework surface может преобразовать domain invalidation event в private cache invalidation. + +## Server runtime + +**SLM-LIFE-015 - ОБЯЗАН.** User-specific server runtime создаётся в request scope. + +**SLM-LIFE-016 - ЗАПРЕЩЕНО.** Process singleton не может захватывать request headers, cookies, credentials, AbortSignal или user-specific cache. + +**SLM-LIFE-017 - ОБЯЗАН.** Request cancellation должна передаваться external operations через подходящий port/adapter, если runtime поддерживает cancellation. diff --git a/docs2/specification/segments.md b/docs2/specification/segments.md new file mode 100644 index 0000000..f56c320 --- /dev/null +++ b/docs2/specification/segments.md @@ -0,0 +1,75 @@ +--- +title: Сегменты +status: draft +normative: true +--- + +# Сегменты + +Segment группирует внутренние файлы module по устойчивой роли. Segment не является самостоятельным layer, module или domain. + +## Базовые segments + +| Segment | Роль | +|---|---| +| `ui/` | Presentation components текущего module | +| `parts/` | Nested modules текущего module | +| `hooks/` | Framework hooks текущей ответственности | +| `providers/` | Provider implementations текущего module | +| `stores/` | Concrete state runtime текущего owner | +| `services/` | Scenario operations и service objects | +| `mappers/` | Transformation на границе ответственности | +| `types/` | Types текущего module | +| `styles/` | Styles текущего module | +| `lib/` | Небольшие internal utilities | +| `config/` | Constants и configuration текущего module | +| `tests/` | Tests публичной границы или составного runtime | + +## Правила + +**SLM-SEG-001 - МОЖЕТ.** Module может использовать любые необходимые segments и не обязан создавать остальные. + +**SLM-SEG-002 - ЗАПРЕЩЕНО.** Нельзя создавать полный симметричный набор segments как scaffold без реального содержимого. + +**SLM-SEG-003 - ОБЯЗАН.** Файл должен размещаться в segment согласно своей фактической роли, а не только расширению или имени. + +**SLM-SEG-004 - ЗАПРЕЩЕНО.** Segment не имеет внешнего public API независимо от module owner. + +**SLM-SEG-005 - ЗАПРЕЩЕНО.** Нельзя импортировать segment другого module через deep path. + +## UI и Parts + +`ui/` содержит presentation components без самостоятельного architectural ownership. + +`parts/` содержит nested modules с собственной внутренней структурой и локальным public boundary. + +**SLM-SEG-006 - ОБЯЗАН.** Сущность с самостоятельной ответственностью, внешними архитектурными dependencies или nested modules должна размещаться в `parts`, а не маскироваться как плоский component. Локальные presentation hooks/state сами по себе не требуют `parts`. + +## Hooks + +**SLM-SEG-007 - ОБЯЗАН.** Hook принадлежит тому module, чью ответственность и runtime он выражает. + +Примеры: + +- domain hook - `domains/{domain}/react/hooks`; +- page-local hook - владеющая page composition; +- reusable technical hook - соответствующий infra module; +- product-agnostic UI hook - владеющий UI module. + +## Domain zones и segments + +**SLM-SEG-008 - ЗАПРЕЩЕНО.** Domain zones `business`, `react`, `adapters`, `client`, `server` нельзя трактовать как взаимозаменяемые generic segments. + +Внутри zone могут существовать обычные segments: + +```text +domain/ +├── business/ +│ ├── services/ +│ ├── types/ +│ └── mappers/ +└── react/ + ├── hooks/ + ├── providers/ + └── ui/ +``` diff --git a/docs2/specification/state-and-data.md b/docs2/specification/state-and-data.md new file mode 100644 index 0000000..1c3b785 --- /dev/null +++ b/docs2/specification/state-and-data.md @@ -0,0 +1,70 @@ +--- +title: State и data +status: draft +normative: true +--- + +# State и Data + +Данные и состояние должны иметь одного понятного владельца semantics, даже если runtime использует несколько caches и projections. + +## Ownership matrix + +| Вид | Владелец | +|---|---| +| Domain model и transitions | Domain business | +| Product source integration | Domain adapter | +| Framework projection доменных данных | Domain framework surface | +| Page-local presentation state | Composition | +| Component-local interaction | Владеющий component/module | +| Technical connection/cache state | Infra или runtime-specific owner | +| Request context | Server/framework scope | +| Universal UI state | Владеющий UI module | + +## Domain gateway + +**SLM-DATA-001 - ОБЯЗАН.** Consumer получает product data только через public domain runtime surface. + +**SLM-DATA-002 - ЗАПРЕЩЕНО.** Composition, UI или app не могут маппить transport DTO в параллельную domain model. + +**SLM-DATA-003 - ОБЯЗАН.** Domain business владеет normalization, validation и semantics отсутствия данных. + +## Domain state + +**SLM-DATA-004 - ОБЯЗАН.** Domain state model и допустимые transitions определяются business независимо от concrete state manager. + +**SLM-DATA-005 - ЗАПРЕЩЕНО.** Raw store API не может становиться public domain contract. + +**SLM-DATA-006 - ОБЯЗАН.** Mutable domain instance должен быть привязан к явному lifecycle scope. + +## Query cache + +Framework или technical query cache может хранить projection результата DomainRuntime query. + +**SLM-DATA-007 - ОБЯЗАН.** Fetcher продуктового query должен вызывать DomainRuntime, а не adapter или SDK напрямую. + +**SLM-DATA-008 - ЗАПРЕЩЕНО.** Query cache не может объявлять собственную domain model, error taxonomy или fallback policy. + +**SLM-DATA-009 - ОБЯЗАН.** User/session-scoped cache keys и invalidation должны изолировать данные разных identities и scopes без использования secret как публичного key contract. + +Эта draft-версия не предписывает единственное физическое место QueryClient/SWR cache. Конкретная модель должна сохранять правила gateway, lifecycle и identity isolation. + +**SLM-DATA-015 - ОБЯЗАН.** Cache instance должен иметь явного creator и scope owner в composition или runtime setup. + +**SLM-DATA-016 - ОБЯЗАН.** Shared framework cache должен передаваться domain surfaces через framework-supported runtime boundary, а не через import app-specific infra singleton. + +**SLM-DATA-017 - ОБЯЗАН.** Graph owner должен очищать или изолировать private cache при смене identity и завершении соответствующего scope. + +## Presentation state + +**SLM-DATA-010 - МОЖЕТ.** Composition или component может использовать concrete state manager для локального presentation state. + +**SLM-DATA-011 - ЗАПРЕЩЕНО.** Presentation store не должен копировать DomainRuntime state как второй source of truth. + +## Serializable boundaries + +**SLM-DATA-012 - ОБЯЗАН.** Через server/client boundary передаются только serializable business-owned data без functions, stores, clients, Context и resources. + +**SLM-DATA-013 - ЗАПРЕЩЕНО.** Secrets, access tokens и request credentials не должны включаться в client bootstrap snapshot. + +**SLM-DATA-014 - ОБЯЗАН.** Server и client initial snapshots должны быть согласованы, если framework выполняет hydration одного UI state. diff --git a/docs2/specification/terminology.md b/docs2/specification/terminology.md new file mode 100644 index 0000000..28abaf9 --- /dev/null +++ b/docs2/specification/terminology.md @@ -0,0 +1,112 @@ +--- +title: Терминология +status: draft +normative: true +--- + +# Терминология + +## Слой + +**Layer** - верхнеуровневая зона `src`, определяющая вид ответственности и допустимые направления зависимостей. + +SLM использует слои `app`, `compositions`, `domains`, `infra`, `ui` и `shared`. + +## Модуль + +**Module** - минимальный самостоятельный владелец ответственности с public boundary. Модуль может содержать код разных технических типов, если весь этот код принадлежит одной ответственности. + +## Группа + +**Group** - навигационная папка, классифицирующая модули или другие группы. Группа не является модулем, не имеет public API и не владеет runtime. + +## Домен + +**Domain** - конечный продуктовый модуль в слое `domains`, владеющий одной предметной ответственностью и всеми её runtime surfaces. + +Допустимые пути: + +```text +domains/{domain} +domains/{group...}/{domain} +``` + +## Группа доменов + +**Domain group** - группа внутри `domains`, используемая только для навигации. Например, `knv` в пути `domains/knv/auth` является группой, если не имеет собственного public API, состояния и assembly. + +## Зона домена + +**Domain zone** - внутренняя архитектурная часть domain с отдельным направлением зависимостей. Базовые зоны: `business`, `react`, `adapters`, `client`, `server`. + +Зона не является самостоятельным domain. + +## Business + +**Business** - framework-neutral зона domain, владеющая моделью, правилами, ports, сценариями, состоянием, нормализацией и domain errors. Business создаёт public logic runtime через factory. + +## Factory + +**Factory** - side-effect-free constructor, принимающий явные dependencies и возвращающий public business runtime API. + +## DomainRuntime + +**DomainRuntime** - созданный factory экземпляр доменного поведения. Он предоставляет commands, queries, snapshots, subscriptions и lifecycle operations, необходимые конкретному domain. + +Factory создаёт DomainRuntime. DomainRuntime является публичным шлюзом к данным и поведению domain. + +## Port + +**Port** - business-owned contract внешней capability, необходимой domain. Port описывается языком domain и не раскрывает concrete SDK, transport или framework runtime. + +## Adapter + +**Adapter** - concrete реализация port поверх infra, SDK, storage, platform API, framework runtime или другого внешнего механизма. + +Готовая capability одного DomainRuntime, структурно удовлетворяющая port другого domain, является cross-domain runtime dependency, а не concrete adapter автоматически. Wrapper adapter требуется только при реальном преобразовании contracts. + +## Framework surface + +**Framework surface** - API domain для конкретного UI/framework runtime. Для React он может включать runtime access boundary, hooks, Providers и domain UI. + +Framework surface не является параллельным business API и не обращается к external source в обход DomainRuntime. + +## Assembly + +**Assembly** - связывание factory с concrete adapters и runtime-specific input для создания готового runtime одного domain. + +## Composition + +**Composition** - модуль, связывающий готовые modules и domain runtimes в page, route, layout, screen, widget или другой application flow. + +## Graph owner + +**Graph owner** - composition, request setup или test setup, которое выбирает набор runtime instances, порядок их создания, lifecycle scope и cleanup. + +## Domain runtime Provider + +**Domain runtime Provider** - часть framework surface, передающая готовый runtime instance framework consumers одного domain. Она не является владельцем cross-domain graph автоматически. + +## Provider composition + +**Provider composition** - composition module, создающий или получающий несколько runtimes и монтирующий их framework boundaries в выбранном scope. + +## Segment + +**Segment** - внутренняя папка модуля, группирующая файлы по роли, например `hooks`, `services`, `types`, `styles` или `lib`. + +Domain zones не являются обычными segments. + +## Компонент + +**Component** - presentation unit внутри владеющего module. Компонент не является самостоятельным архитектурным owner и не выбирает источники данных или runtime dependencies. + +## Продуктовые данные + +**Product data** - данные, состояние и outcomes, имеющие смысл в предметной области продукта. Transport DTO, raw SDK response и browser storage schema не являются доменной моделью автоматически. + +## Runtime dependency + +**Runtime dependency** - dependency, необходимая выполняемому коду: API другого объекта, external source, store, query runtime, event source, clock, environment или platform capability. + +`import type` не создаёт runtime dependency, но может создавать статическую связанность contracts. diff --git a/docs2/specification/testing-and-conformance.md b/docs2/specification/testing-and-conformance.md new file mode 100644 index 0000000..cab8a2d --- /dev/null +++ b/docs2/specification/testing-and-conformance.md @@ -0,0 +1,82 @@ +--- +title: Тестирование и соответствие +status: draft +normative: true +--- + +# Тестирование и Соответствие + +Тесты проверяют public boundaries и runtime risks каждого owner, а не только внутренние helpers. + +## Business factory tests + +**SLM-TEST-001 - ОБЯЗАН.** Каждый public method runtime API, возвращаемого factory, должен иметь factory-level tests. + +Factory-level tests должны проверять применимые случаи: + +- happy path; +- malformed external result; +- rejected dependency; +- синхронное исключение dependency; +- domain outcome/error semantics; +- side-effect order; +- state transition; +- отсутствие constructor-time I/O; +- public API shape. + +**SLM-TEST-002 - ОБЯЗАН.** Factory-level test должен создавать runtime через public `business` entrypoint, а не deep-import factory internals. + +## Adapter tests + +**SLM-TEST-003 - ОБЯЗАН.** Adapter с mapping, transport payload, error channel или lifecycle должен иметь contract tests на применимые responsibilities. + +**SLM-TEST-004 - ЗАПРЕЩЕНО.** Adapter test не должен дублировать business scenario tests или утверждать domain fallback/error semantics. + +## Assembly tests + +**SLM-TEST-005 - ОБЯЗАН.** Client/server assembly tests должны проверять корректную передачу ports, runtime profile isolation и отсутствие I/O при creation. + +**SLM-TEST-006 - ОБЯЗАН.** Server assembly с request data должен иметь isolation test для параллельных scopes. + +## Framework tests + +**SLM-TEST-007 - ОБЯЗАН.** Framework surface tests должны проверять runtime access boundary, предсказуемую ошибку при отсутствии runtime boundary, mapping public outcomes и lifecycle integration. + +**SLM-TEST-008 - ОБЯЗАН.** Client/server import boundary должна проверяться инструментом, понимающим реальный framework module graph; DOM unit test не заменяет production build probe. + +## Composition tests + +**SLM-TEST-009 - ОБЯЗАН.** Tests cross-domain composition должны проверять topology, точный graph contract, переданные capabilities и lifecycle cleanup. + +**SLM-TEST-010 - ОБЯЗАН.** Scope с неполным набором domains не должен типизироваться как полный application graph. + +## Architecture conformance + +Repository checks должны проверять применимые ограничения: + +- направление imports; +- deep imports; +- public entrypoints; +- runtime cycles; +- client/server markers; +- forbidden cross-domain imports; +- unique rule IDs документации; +- generated artifacts, если они используются. + +**SLM-TEST-011 - ЗАПРЕЩЕНО.** Документированное правило не считается механически enforced, если repository tooling его фактически не проверяет. + +## Единица соответствия + +**SLM-TEST-014 - ОБЯЗАН.** Application соответствует Specification, если все его modules и связи выполняют применимые обязательные правила. + +**SLM-TEST-015 - ОБЯЗАН.** Изменение соответствует Specification, если новые и изменённые modules не создают новых нарушений и проходят применимые checks. + +**SLM-TEST-016 - ОБЯЗАН.** Отступление от правила `СЛЕДУЕТ` должно быть зафиксировано в архитектурном review или принятом decision с указанием причины и scope. + +**SLM-TEST-017 - ОБЯЗАН.** Manual conformance и mechanical enforcement должны различаться явно; отсутствие автоматической проверки не отменяет нормативное правило. + +## Completion gate + +**SLM-TEST-012 - ОБЯЗАН.** Изменение считается завершённым только после выполнения ближайших tests, typecheck, lint, build и architecture checks, существующих в repository. + +**SLM-TEST-013 - ОБЯЗАН.** Невыполненная проверка и остаточный риск должны быть явно указаны в результате работы.