chore: Новый черновик DRAFT, удалить старые docs-v

This commit is contained in:
2026-07-30 09:19:59 +03:00
parent 6f6e4896af
commit 590fb63ca7
122 changed files with 29145 additions and 3253 deletions

15
DRAFT/README.md Normal file
View File

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

84
DRAFT/domains/README.md Normal file
View File

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

View File

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

174
DRAFT/domains/business.md Normal file
View File

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

206
DRAFT/domains/domain.md Normal file
View File

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

View File

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

View File

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

View File

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

141
DRAFT/domains/presets.md Normal file
View File

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

402
DRAFT/domains/testing.md Normal file
View File

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

33
DRAFT/index.md Normal file
View File

@@ -0,0 +1,33 @@
---
layout: home
title: SLM Level 1
hero:
name: SLM Level 1
text: Базовая архитектура фронтенд-приложений
tagline: Слои, модули, зависимости, публичные границы и жизненный цикл без отдельной доменной архитектуры.
image:
src: /logo.svg
alt: SLM Design
actions:
- theme: brand
text: Читать Level 1
link: /level-1/
- theme: alt
text: Открыть правила
link: /rules/level-1
features:
- title: Пять слоёв
details: Линейный порядок app, compositions, infra, ui и shared задаёт роли кода и допустимое направление зависимостей.
- title: Модульные границы
details: Ответственность имеет одного владельца, а внешний код использует модуль только через его публичный API.
- title: 14 правил
details: Пять правил проверяются автоматически, девять требуют архитектурного ревью.
---
## Что опубликовано
Сайт содержит только рабочий черновик SLM Level 1 и его канонический реестр правил. Доменные уровни, монорепозитории и дополнительные режимы архитектуры пока не входят в опубликованную документацию.
Определения Level 1 нормативны внутри черновика. Точные формулировки блокирующих требований находятся только в [реестре правил](/rules/level-1).

53
DRAFT/level-1/README.md Normal file
View File

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

View File

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

View File

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

25
DRAFT/level-1/groups.md Normal file
View File

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

85
DRAFT/level-1/layers.md Normal file
View File

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

View File

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

43
DRAFT/level-1/modules.md Normal file
View File

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

View File

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

29
DRAFT/level-1/segments.md Normal file
View File

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

View File

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

View File

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

117
DRAFT/rules/README.md Normal file
View File

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

102
DRAFT/rules/level-1.md Normal file
View File

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