mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
chore: Новый черновик DRAFT, удалить старые docs-v
This commit is contained in:
15
DRAFT/README.md
Normal file
15
DRAFT/README.md
Normal 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
84
DRAFT/domains/README.md
Normal 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}`.
|
||||
173
DRAFT/domains/auth-example.md
Normal file
173
DRAFT/domains/auth-example.md
Normal 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
174
DRAFT/domains/business.md
Normal 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
206
DRAFT/domains/domain.md
Normal 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 пока остаётся открытым вопросом.
|
||||
200
DRAFT/domains/factory-ports-adapters.md
Normal file
200
DRAFT/domains/factory-ports-adapters.md
Normal 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.
|
||||
109
DRAFT/domains/framework-bindings.md
Normal file
109
DRAFT/domains/framework-bindings.md
Normal 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 пока требует отдельных примеров.
|
||||
113
DRAFT/domains/open-questions.md
Normal file
113
DRAFT/domains/open-questions.md
Normal 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
141
DRAFT/domains/presets.md
Normal 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
402
DRAFT/domains/testing.md
Normal 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
33
DRAFT/index.md
Normal 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
53
DRAFT/level-1/README.md
Normal 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)
|
||||
65
DRAFT/level-1/components.md
Normal file
65
DRAFT/level-1/components.md
Normal 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, собственную границу зависимостей, область жизни или внутреннюю модульную декомпозицию, она является модулем. При локальном использовании такой модуль может размещаться как вложенный.
|
||||
49
DRAFT/level-1/dependencies.md
Normal file
49
DRAFT/level-1/dependencies.md
Normal 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
25
DRAFT/level-1/groups.md
Normal 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
85
DRAFT/level-1/layers.md
Normal 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`.
|
||||
31
DRAFT/level-1/lifecycle.md
Normal file
31
DRAFT/level-1/lifecycle.md
Normal 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
43
DRAFT/level-1/modules.md
Normal 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`.
|
||||
30
DRAFT/level-1/nested-modules.md
Normal file
30
DRAFT/level-1/nested-modules.md
Normal 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
29
DRAFT/level-1/segments.md
Normal 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. Их форму определяет стайлгайд компонентов.
|
||||
117
DRAFT/level-1/terminology.md
Normal file
117
DRAFT/level-1/terminology.md
Normal 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
|
||||
```
|
||||
|
||||
Путь и имя папки сами по себе не определяют сущность. Её определяют ответственность, владелец и публичная граница. Физическое сопоставление путей с сущностями задаётся стайлгайдом или конфигурацией проверки проекта.
|
||||
23
DRAFT/level-1/validation.md
Normal file
23
DRAFT/level-1/validation.md
Normal 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
117
DRAFT/rules/README.md
Normal 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
102
DRAFT/rules/level-1.md
Normal 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
|
||||
|
||||
> **Жизненный цикл ресурсов**
|
||||
>
|
||||
> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
|
||||
Reference in New Issue
Block a user