mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
feat: переработать уровни SLM
This commit is contained in:
26
DRAFT/level-2/domains/README.md
Normal file
26
DRAFT/level-2/domains/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Доменные пакеты Level 2
|
||||
|
||||
Доменный пакет заменяет корневой доменный модуль Level 1. Он объединяет SLM-модули одной предметной области, но сам не является модулем, Group или публичным API.
|
||||
|
||||
```text
|
||||
domains/auth/
|
||||
├── business/
|
||||
├── presets/
|
||||
├── adapters/
|
||||
└── react/
|
||||
├── session/
|
||||
└── login-form/
|
||||
```
|
||||
|
||||
## Основные границы
|
||||
|
||||
- [Доменный пакет](./domain-package.md) определяет предметную и структурную границу.
|
||||
- [Business](./business.md) владеет `DomainApi`, фабрикой и доменными ошибками.
|
||||
- [Фабрика, зависимости и adapters](./factory-ports-adapters.md) отделяют business от технической реализации.
|
||||
- [Presets](./presets.md) собирают один API для нужных окружений.
|
||||
- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
|
||||
- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу.
|
||||
- [Миграция auth](./auth-example.md) показывает переход с Level 1.
|
||||
- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей.
|
||||
|
||||
Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md).
|
||||
101
DRAFT/level-2/domains/auth-example.md
Normal file
101
DRAFT/level-2/domains/auth-example.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Миграция домена auth с Level 1
|
||||
|
||||
> Проверочный пример перехода от доменного модуля к доменному пакету.
|
||||
|
||||
## Связанное правило
|
||||
|
||||
- [`SLM-L2-MIGRATION-A017`](../../rules/level-2.md#slm-l2-migration-a017)
|
||||
|
||||
## Исходная форма Level 1
|
||||
|
||||
```text
|
||||
domains/auth/ # Доменный модуль
|
||||
├── hooks/
|
||||
├── services/
|
||||
├── stores/
|
||||
├── ui/
|
||||
└── index.ts # Общий API модуля
|
||||
```
|
||||
|
||||
Level 1 разрешает business-сценариям, framework hooks, state adapter и локальной сборке находиться внутри одного модуля.
|
||||
|
||||
## Целевая форма Level 2
|
||||
|
||||
```text
|
||||
domains/auth/ # Доменный пакет
|
||||
├── README.md
|
||||
├── business/ # SLM-модуль
|
||||
│ ├── errors/
|
||||
│ ├── lib/
|
||||
│ ├── services/
|
||||
│ ├── types/
|
||||
│ └── index.ts
|
||||
├── presets/ # Group
|
||||
│ ├── browser/ # SLM-модуль
|
||||
│ │ ├── adapters/
|
||||
│ │ └── index.ts
|
||||
│ └── request/ # SLM-модуль
|
||||
│ └── index.ts
|
||||
└── react/ # Framework Group
|
||||
├── session/ # SLM-модуль
|
||||
│ └── index.ts
|
||||
└── login-form/ # SLM-модуль
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Корневой `domains/auth/index.ts` удаляется. Потребители переходят на публичные API конкретных модулей.
|
||||
|
||||
## Перенос ответственности
|
||||
|
||||
| Исходная часть | Владелец Level 2 |
|
||||
|---|---|
|
||||
| Сценарии, предметные типы, единый API | `auth/business` |
|
||||
| Коды, тип и guard ошибок | `auth/business` |
|
||||
| Browser storage и HTTP adapters | `auth/presets/browser` |
|
||||
| Cookies, request data и server adapters | `auth/presets/request` |
|
||||
| Provider и session hooks | `auth/react/session` |
|
||||
| Переиспользуемая форма | `auth/react/login-form` |
|
||||
| Страница, текст и redirect | `compositions` |
|
||||
|
||||
## Новые импорты
|
||||
|
||||
```ts
|
||||
import { authFactory, isAuthError } from '@/domains/auth/business'
|
||||
import { createBrowserAuth } from '@/domains/auth/presets/browser'
|
||||
import { AuthSessionProvider } from '@/domains/auth/react/session'
|
||||
import { LoginForm } from '@/domains/auth/react/login-form'
|
||||
```
|
||||
|
||||
## Cross-domain граф
|
||||
|
||||
Если User зависит от Auth, оба связанных домена сначала переводятся в пакеты. Затем User получает только type-only контракт:
|
||||
|
||||
```ts
|
||||
import type { AuthApi } from '@/domains/auth/business'
|
||||
|
||||
export type UserDeps = {
|
||||
auth: Pick<AuthApi, 'getSession'>
|
||||
}
|
||||
```
|
||||
|
||||
Место сборки графа создаёт экземпляры:
|
||||
|
||||
```ts
|
||||
const authApi = createBrowserAuth()
|
||||
const userApi = createBrowserUser({ authApi })
|
||||
```
|
||||
|
||||
User не импортирует runtime-код Auth, а его React-модули не импортируют `useAuthSession` или Auth components.
|
||||
|
||||
## Порядок перехода
|
||||
|
||||
1. Определить dependency-connected набор доменов, который нужно мигрировать вместе.
|
||||
2. Выделить `business` и одну фабрику без environment-specific import-графа.
|
||||
3. Зафиксировать `DomainApi`, error codes, error type и runtime guard.
|
||||
4. Перенести browser/server wiring в нужные presets и adapters.
|
||||
5. Разделить React-ответственности на модули внутри Group `react`.
|
||||
6. Перенести страницы, redirects и multi-domain UI в `compositions`.
|
||||
7. Перевести внешние импорты на module-specific paths.
|
||||
8. Удалить старый root `index.ts` и проверить import-граф.
|
||||
|
||||
Простые доменные модули могут оставаться в SLM root только как временное миграционное состояние. Они не создают прямые runtime- или type-only зависимости с уже переведёнными пакетами.
|
||||
122
DRAFT/level-2/domains/business.md
Normal file
122
DRAFT/level-2/domains/business.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Модуль business
|
||||
|
||||
> Пояснение единственного runtime-источника доменных данных и результатов.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
|
||||
- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
|
||||
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
|
||||
- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009)
|
||||
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
|
||||
- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
|
||||
|
||||
## Роль
|
||||
|
||||
`business` является обязательным SLM-модулем доменного пакета. Он владеет:
|
||||
|
||||
- публичными предметными сценариями;
|
||||
- единым контрактом `DomainApi`;
|
||||
- одной публичной фабрикой;
|
||||
- типом явных зависимостей фабрики;
|
||||
- предметными типами и детерминированными правилами;
|
||||
- кодами, типом и runtime guard доменных ошибок;
|
||||
- публичным представлением доменных данных и состояния.
|
||||
|
||||
Приложение получает runtime-данные, состояние и результаты домена только через экземпляр `DomainApi`. Adapter, preset или framework binding module не открывает параллельный источник доменных данных.
|
||||
|
||||
## Публичный API модуля
|
||||
|
||||
```ts
|
||||
export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error'
|
||||
export { authFactory } from './auth.factory'
|
||||
|
||||
export type {
|
||||
AuthApi,
|
||||
AuthDeps,
|
||||
AuthError,
|
||||
AuthErrorCode,
|
||||
AuthFactory,
|
||||
AuthState,
|
||||
} from './types'
|
||||
```
|
||||
|
||||
Фабрика, error contract и типы экспортируются для presets, adapters, framework-модулей и мест сборки графа. Предметные validators, normalizers, внутренние преобразователи исходных ошибок, constructors, mutable store и технические DTO остаются закрытыми и используются публичными сценариями `DomainApi`.
|
||||
|
||||
## Один DomainApi
|
||||
|
||||
```ts
|
||||
export type AuthApi = {
|
||||
getSnapshot: () => AuthState
|
||||
requestPhoneOtp: (phone: string) => Promise<void>
|
||||
verifyPhoneOtp: (code: string) => Promise<void>
|
||||
}
|
||||
|
||||
export type AuthFactory = (deps: AuthDeps) => AuthApi
|
||||
```
|
||||
|
||||
Все presets вызывают одну `authFactory` и создают `AuthApi` этого контракта. Preset может использовать другую техническую реализацию, но не добавляет метод и не меняет семантику сценария.
|
||||
|
||||
Точная модель хранения, initial state, подписки и SSR snapshot пока остаётся открытым вопросом. Нормативной уже является публичная граница: приложение наблюдает доменное состояние через `DomainApi`, а не напрямую через adapter или framework store.
|
||||
|
||||
## Обязательный контракт ошибок
|
||||
|
||||
Каждый `business` объявляет устойчивые коды, безопасную readonly-форму и runtime guard:
|
||||
|
||||
```ts
|
||||
export const AUTH_ERROR_CODES = {
|
||||
PHONE_INVALID: 'AUTH_PHONE_INVALID',
|
||||
OTP_REQUEST_FAILED: 'AUTH_OTP_REQUEST_FAILED',
|
||||
OTP_CODE_INVALID: 'AUTH_OTP_CODE_INVALID',
|
||||
} as const
|
||||
|
||||
export type AuthErrorCode =
|
||||
typeof AUTH_ERROR_CODES[keyof typeof AUTH_ERROR_CODES]
|
||||
|
||||
export type AuthError = Readonly<{
|
||||
code: AuthErrorCode
|
||||
}>
|
||||
|
||||
const authErrorCodes = new Set<string>(Object.values(AUTH_ERROR_CODES))
|
||||
|
||||
export const isAuthError = (value: unknown): value is AuthError => {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(value)
|
||||
const keys = Reflect.ownKeys(value)
|
||||
|
||||
if (
|
||||
(prototype !== Object.prototype && prototype !== null)
|
||||
|| keys.length !== 1
|
||||
|| keys[0] !== 'code'
|
||||
|| !('code' in value)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return typeof value.code === 'string' && authErrorCodes.has(value.code)
|
||||
}
|
||||
```
|
||||
|
||||
Способ передачи ошибки, exception или discriminated `Result`, пока не закреплён. В обоих вариантах публичный сценарий сообщает ожидаемый сбой только через собственный `AuthError`.
|
||||
|
||||
## Изоляция технических ошибок
|
||||
|
||||
Ошибки SDK, HTTP, database, storage и adapters не пересекают `DomainApi` в форме, доступной приложению. `business` преобразует обрабатываемый технический сбой в собственный код:
|
||||
|
||||
```text
|
||||
SDK error
|
||||
→ adapter failure
|
||||
→ business mapping
|
||||
→ AuthErrorCode
|
||||
→ приложение
|
||||
```
|
||||
|
||||
Публичная форма не включает исходные `message`, `status`, `payload`, `cause`, SDK class или сам объект ошибки. Диагностические данные могут сохраняться только во внутреннем механизме observability, контракт которого будет определён отдельно.
|
||||
|
||||
То же относится к cross-domain вызову. Если `UserApi` использует `AuthApi`, сбой, который становится результатом публичного сценария User, представлен собственным `UserError`, а не `AuthError`.
|
||||
|
||||
Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки. Способ отличить их от ожидаемых cross-domain ошибок при exception-модели и их диагностическая политика остаются открытыми вопросами; исходная ошибка при этом не добавляется в публичную форму DomainError.
|
||||
87
DRAFT/level-2/domains/domain-package.md
Normal file
87
DRAFT/level-2/domains/domain-package.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Граница доменного пакета
|
||||
|
||||
> Пояснение новой контейнерной сущности Level 2.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-DOMAIN-R002`](../../rules/level-2.md#slm-l2-domain-r002)
|
||||
- [`SLM-L2-DOMAIN-A003`](../../rules/level-2.md#slm-l2-domain-a003)
|
||||
- [`SLM-L2-GROUP-R004`](../../rules/level-2.md#slm-l2-group-r004)
|
||||
- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
|
||||
|
||||
## Предметная граница
|
||||
|
||||
Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать business-сценарии авторизации, её browser/server presets и React-модули, но не страницу профиля или общий database client.
|
||||
|
||||
Пакет не владеет исполняемой ответственностью. Конкретными API, состоянием, зависимостями и lifecycle владеют модули внутри него.
|
||||
|
||||
## Корень пакета
|
||||
|
||||
```text
|
||||
domains/auth/
|
||||
├── README.md
|
||||
├── business/
|
||||
├── presets/
|
||||
├── adapters/
|
||||
└── react/
|
||||
```
|
||||
|
||||
В корне разрешены:
|
||||
|
||||
- документация;
|
||||
- ownership metadata;
|
||||
- декларативный manifest или декларативная конфигурация архитектурной проверки;
|
||||
- обязательный модуль `business`;
|
||||
- Groups допустимых ролей.
|
||||
|
||||
В корне запрещены:
|
||||
|
||||
- `index.ts` или другой агрегирующий executable entry point;
|
||||
- runtime-файлы и side effects;
|
||||
- изменяемое состояние и ресурсы lifecycle;
|
||||
- реэкспорт API внутренних модулей;
|
||||
- page-specific компоненты или сборка нескольких доменов.
|
||||
|
||||
Metadata содержит только статические данные, не исполняется приложением, build tooling или проверяющим инструментом и не становится скрытым API пакета.
|
||||
|
||||
## Модули и Groups
|
||||
|
||||
`business` размещается непосредственно в пакете. Presets и самостоятельные adapters размещаются в Groups `presets` и `adapters`. Framework Group называется по фреймворку: `react`, `vue` и аналогично.
|
||||
|
||||
```text
|
||||
auth/
|
||||
├── business/ # SLM-модуль
|
||||
├── presets/ # Group
|
||||
│ └── browser/ # SLM-модуль
|
||||
└── react/ # Framework Group
|
||||
├── session/ # SLM-модуль
|
||||
└── login-form/ # SLM-модуль
|
||||
```
|
||||
|
||||
Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/presets/browser`, `auth/react/session`, но не `auth`, `auth/presets` или `auth/react`.
|
||||
|
||||
## Навигационные Groups
|
||||
|
||||
Слой `domains` может содержать навигационные Groups с пакетами:
|
||||
|
||||
```text
|
||||
domains/
|
||||
└── commerce/ # Навигационная Group
|
||||
├── catalog/ # Доменный пакет
|
||||
└── orders/ # Доменный пакет
|
||||
```
|
||||
|
||||
Такая Group отличается от Group внутри пакета только допустимым составом: она содержит доменные пакеты и другие navigation Groups, а не модули.
|
||||
|
||||
## Границы соседних слоёв
|
||||
|
||||
| Ответственность | Владелец |
|
||||
|---|---|
|
||||
| Предметные сценарии, `DomainApi`, доменные ошибки | `business` |
|
||||
| Техническая реализация зависимости одного домена | Adapter внутри пакета |
|
||||
| Универсальный технический сервис | `infra` |
|
||||
| Domain-specific framework API | Модуль внутри `react`, `vue` и аналогичной Group |
|
||||
| Страница, маршрут, redirect, продуктовый текст | `compositions` |
|
||||
| UI, объединяющий несколько доменов | `compositions` |
|
||||
|
||||
Зависимость от React сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.
|
||||
86
DRAFT/level-2/domains/factory-ports-adapters.md
Normal file
86
DRAFT/level-2/domains/factory-ports-adapters.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Фабрика, зависимости и adapters
|
||||
|
||||
> Пояснение границы между `business` и технической средой.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
|
||||
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
|
||||
- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
|
||||
## Одна фабрика
|
||||
|
||||
```text
|
||||
явные зависимости + business factory → DomainApi
|
||||
```
|
||||
|
||||
Модуль `business` предоставляет одну публичную фабрику. Она получает все runtime-возможности явными аргументами и создаёт API одного контракта независимо от выбранного preset.
|
||||
|
||||
```ts
|
||||
export type AuthFactory = (deps: AuthDeps) => AuthApi
|
||||
```
|
||||
|
||||
Рекомендуется сохранять сам вызов фабрики чистым: он создаёт объекты и closures, но не читает скрытое окружение и не выбирает конкретную техническую реализацию. Детальный lifecycle ресурсов будет нормирован позже.
|
||||
|
||||
## Технические зависимости
|
||||
|
||||
Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы.
|
||||
|
||||
```ts
|
||||
export type AuthPhoneDependency = {
|
||||
requestCode: (phone: string) => Promise<unknown>
|
||||
verifyCode: (code: string) => Promise<unknown>
|
||||
}
|
||||
```
|
||||
|
||||
`unknown` допустим на границе непроверенного внешнего результата. `business` проверяет его до превращения в предметные данные или состояние.
|
||||
|
||||
Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics.
|
||||
|
||||
## Cross-domain API dependency
|
||||
|
||||
Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом:
|
||||
|
||||
```ts
|
||||
import type { AuthApi } from '@/domains/auth/business'
|
||||
|
||||
export type UserDeps = {
|
||||
auth: Pick<AuthApi, 'getSession'>
|
||||
}
|
||||
```
|
||||
|
||||
Runtime-значение передаёт место сборки графа через preset. `user/business` не импортирует executable API, factory или preset Auth.
|
||||
|
||||
## Adapter
|
||||
|
||||
Adapter соединяет явную зависимость фабрики с технической системой:
|
||||
|
||||
```text
|
||||
business dependency ← adapter → SDK / storage / platform / request data
|
||||
```
|
||||
|
||||
Adapter преобразует аргументы и технический результат к контракту зависимости. Он не определяет публичный метод `DomainApi`, предметный fallback или код доменной ошибки.
|
||||
|
||||
Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение никогда не строит поведение по HTTP status, SDK error class или storage exception.
|
||||
|
||||
## Размещение adapter
|
||||
|
||||
Одноразовый adapter остаётся закрытым сегментом preset-модуля:
|
||||
|
||||
```text
|
||||
auth/presets/browser/
|
||||
├── adapters/
|
||||
│ └── phone.adapter.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Adapter становится самостоятельным SLM-модулем, когда нужен нескольким presets или имеет отдельную integration responsibility:
|
||||
|
||||
```text
|
||||
auth/adapters/
|
||||
└── identity-provider/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Самостоятельный adapter сохраняет минимальный публичный API и не становится альтернативным источником доменных данных для приложения.
|
||||
115
DRAFT/level-2/domains/framework-bindings.md
Normal file
115
DRAFT/level-2/domains/framework-bindings.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Framework Groups и модули
|
||||
|
||||
> Пояснение domain-specific framework-кода на примере React.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-FRAMEWORK-R014`](../../rules/level-2.md#slm-l2-framework-r014)
|
||||
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
|
||||
|
||||
## Framework Group
|
||||
|
||||
Папка для domain-specific React binding modules называется `react`:
|
||||
|
||||
```text
|
||||
domains/auth/react/ # Framework Group
|
||||
├── session/ # SLM-модуль
|
||||
│ ├── hooks/
|
||||
│ ├── providers/
|
||||
│ └── index.ts
|
||||
└── login-form/ # SLM-модуль
|
||||
├── components/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`react` является Group, а не модулем. У неё нет `index.ts`, состояния, реализации, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
|
||||
|
||||
Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. Он не является вложенным модулем, потому что родительская граница `react` является Group.
|
||||
|
||||
## Framework binding module
|
||||
|
||||
Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании готового `DomainApi` одного домена с конкретным framework. Сам факт зависимости от framework недостаточен: framework-specific preset остаётся в `presets`, а page-specific модуль остаётся в `compositions`.
|
||||
|
||||
Framework binding module может:
|
||||
|
||||
- передавать готовый `DomainApi` через Provider и context;
|
||||
- предоставлять domain-specific hooks;
|
||||
- отображать состояние и безопасные ошибки домена;
|
||||
- реализовывать переиспользуемую domain-specific форму или guard;
|
||||
- связывать framework lifecycle с публичным API домена.
|
||||
|
||||
Он не вызывает business-фабрику или preset, не выбирает adapters и не создаёт новые предметные сценарии.
|
||||
|
||||
## Модуль session
|
||||
|
||||
`auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthApi`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
type AuthSessionProviderProps = PropsWithChildren<{
|
||||
api: AuthApi
|
||||
}>
|
||||
|
||||
export const AuthSessionProvider = ({
|
||||
api,
|
||||
children,
|
||||
}: AuthSessionProviderProps) => {
|
||||
return (
|
||||
<AuthSessionContext.Provider value={api}>
|
||||
{children}
|
||||
</AuthSessionContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Публичный путь модуля:
|
||||
|
||||
```ts
|
||||
import {
|
||||
AuthSessionProvider,
|
||||
useAuthSession,
|
||||
} from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API.
|
||||
|
||||
## Модуль login-form
|
||||
|
||||
`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с `AuthApi`, AuthState и AuthError. Она может использовать публичный API соседнего `auth/react/session`, если зависимость остаётся ацикличной.
|
||||
|
||||
Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Поэтому domain-owned `AuthGuard` может решить, разрешён ли доступ, но политика перехода на `/login` остаётся у route composition.
|
||||
|
||||
## Запрет cross-domain framework imports
|
||||
|
||||
Framework binding module не импортирует hooks, contexts, Providers, components или framework state другого доменного пакета:
|
||||
|
||||
```ts
|
||||
// Недопустимо: domains/user/react/profile
|
||||
import { useAuthSession } from '@/domains/auth/react/session'
|
||||
```
|
||||
|
||||
Cross-domain UI собирается в `compositions`:
|
||||
|
||||
```tsx
|
||||
const session = useAuthSession()
|
||||
|
||||
return (
|
||||
<UserProfile
|
||||
userId={session.userId}
|
||||
canEdit={session.isAuthenticated}
|
||||
/>
|
||||
)
|
||||
```
|
||||
|
||||
Передача через props является границей композиции, но не требует prop drilling внутри домена: каждый пакет может использовать собственный Provider и context. Если User business постоянно нуждается в Auth, готовый `AuthApi` передаётся User factory при сборке графа, а User framework module работает уже со своим `UserApi`.
|
||||
|
||||
## Публичные API
|
||||
|
||||
```ts
|
||||
import { AuthSessionProvider } from '@/domains/auth/react/session'
|
||||
import { LoginForm } from '@/domains/auth/react/login-form'
|
||||
```
|
||||
|
||||
Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена.
|
||||
43
DRAFT/level-2/domains/open-questions.md
Normal file
43
DRAFT/level-2/domains/open-questions.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Открытые вопросы Level 2
|
||||
|
||||
> Эти вопросы не являются правилами и не отменяют зафиксированные границы.
|
||||
|
||||
## Зафиксированные решения
|
||||
|
||||
- Level 1 включает слой `domains` и простые доменные модули.
|
||||
- Level 2 заменяет доменный модуль доменным пакетом.
|
||||
- Корень пакета содержит только metadata, модули и Groups и не имеет executable API.
|
||||
- `business` предоставляет одну фабрику и один `DomainApi`.
|
||||
- Приложение получает доменные данные, состояние и результаты только через `DomainApi`.
|
||||
- Каждый `business` экспортирует коды, тип и runtime guard доменных ошибок.
|
||||
- Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена.
|
||||
- Количество presets определяется реальными окружениями; универсальный preset не обязателен.
|
||||
- Runtime cross-domain imports запрещены; type-only business contracts разрешены и входят в DAG.
|
||||
- Framework Group называется по фреймворку и содержит самостоятельные SLM-модули.
|
||||
- Cross-domain framework state, hooks, contexts и components не импортируются.
|
||||
|
||||
## Владение состоянием
|
||||
|
||||
Нужно определить создание initial state, применение transitions, persistence и внешний event input. Нормативным уже является только то, что приложение читает состояние через `DomainApi`, но точная граница между business state machine и storage adapter пока не выбрана.
|
||||
|
||||
Отдельно требуется проверить SSR snapshot, hydration, concurrent rendering, reset и поведение после завершения request scope.
|
||||
|
||||
## Передача ошибок
|
||||
|
||||
Нужно выбрать общую политику exception или discriminated `Result`, определить cancellation и unexpected failures, а также сериализацию ошибок через RPC и server actions.
|
||||
|
||||
Для exception-модели отдельно нужно определить, как зависимый business отличает ожидаемую ошибку другого домена без runtime-импорта его guard: через переданный discriminator, wrapper места сборки или другой явный контракт.
|
||||
|
||||
## Технические порты
|
||||
|
||||
Нужно определить, является ли consumer-owned port обязательной формой каждой технической зависимости и какие behavioral guarantees он обязан описывать: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
|
||||
|
||||
Cross-domain `Pick<OtherDomainApi>` уже зафиксирован как отдельный вид API dependency и не зависит от этого решения.
|
||||
|
||||
## Lifecycle сборки
|
||||
|
||||
Нужно определить контракты `start` и `dispose`, async cleanup, rollback частичного старта, repeated disposal, request abort и поведение API после завершения scope. До решения применяется только общее владение lifecycle Level 1.
|
||||
|
||||
## Автоматическая проверка
|
||||
|
||||
Нужно выбрать machine-readable формат для domain packages, metadata, модулей, Groups, entry points, environment labels и запрещённых транзитивных импортов.
|
||||
101
DRAFT/level-2/domains/presets.md
Normal file
101
DRAFT/level-2/domains/presets.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Presets и среды выполнения
|
||||
|
||||
> Пояснение повторяемых сборок одного `DomainApi`.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
|
||||
- [`SLM-L2-PRESET-R011`](../../rules/level-2.md#slm-l2-preset-r011)
|
||||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||||
- [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013)
|
||||
|
||||
## Назначение
|
||||
|
||||
Preset является SLM-модулем в Group `presets`. Он создаёт API одной business-фабрики для конкретного повторяемого контекста выполнения.
|
||||
|
||||
```text
|
||||
authFactory
|
||||
├── presets/browser → AuthApi в браузере
|
||||
├── presets/request → AuthApi одного server request
|
||||
└── presets/server-action → AuthApi server action
|
||||
```
|
||||
|
||||
Архитектура не требует обязательный `base` или изоморфный preset и не ограничивает количество presets. Проект создаёт только те сборки, которые нужны его реальным средам и областям использования.
|
||||
|
||||
Если фабрика используется в одном месте и отдельная повторяемая конфигурация не возникает, место сборки графа может вызвать её напрямую.
|
||||
|
||||
## Один контракт API
|
||||
|
||||
Каждый preset выбирает технические реализации, но вызывает одну и ту же фабрику и возвращает один контракт `DomainApi`:
|
||||
|
||||
```ts
|
||||
export const createBrowserAuth = (): AuthApi => {
|
||||
return authFactory({
|
||||
phone: createHttpPhoneAdapter(),
|
||||
session: createBrowserSessionAdapter(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
export const createAuthForRequest = (
|
||||
input: AuthRequestInput,
|
||||
): AuthApi => {
|
||||
return authFactory({
|
||||
phone: createServerPhoneAdapter(input),
|
||||
session: createRequestSessionAdapter(input),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Server preset может обращаться к database напрямую через adapter, а browser preset реализует тот же сценарий через HTTP или RPC. Preset не добавляет server-only метод к `AuthApi` и не меняет доменные ошибки.
|
||||
|
||||
Если полный `DomainApi` невозможно корректно создать в некоторой среде, пакет просто не предоставляет preset для этой среды. Метод, намеренно падающий только потому, что среда не поддерживается, не считается реализацией контракта.
|
||||
|
||||
## Cross-domain input
|
||||
|
||||
Preset зависимого домена принимает готовый API аргументом:
|
||||
|
||||
```ts
|
||||
import type { AuthApi } from '@/domains/auth/business'
|
||||
|
||||
export type CreateUserForRequestInput = {
|
||||
authApi: Pick<AuthApi, 'getSession'>
|
||||
request: UserRequestInput
|
||||
}
|
||||
|
||||
export const createUserForRequest = ({
|
||||
authApi,
|
||||
request,
|
||||
}: CreateUserForRequestInput): UserApi => {
|
||||
return userFactory({
|
||||
auth: authApi,
|
||||
profile: createUserProfileAdapter(request),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Preset делает только type-only импорт `AuthApi`. Runtime-фабрику, preset или instance Auth он не импортирует.
|
||||
|
||||
Место сборки графа выполняет сборку:
|
||||
|
||||
```ts
|
||||
const authApi = createAuthForRequest(authInput)
|
||||
const userApi = createUserForRequest({ authApi, request: userInput })
|
||||
```
|
||||
|
||||
## Environment entry points
|
||||
|
||||
Server preset имеет отдельный публичный entry point и marker выбранного framework или bundler:
|
||||
|
||||
```ts
|
||||
import 'server-only'
|
||||
|
||||
export { createAuthForRequest } from './create-auth-for-request'
|
||||
```
|
||||
|
||||
Server entry point не реэкспортируется через `business`, Framework Group, browser preset или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Preset может создавать ресурсы, которым потребуется запуск или cleanup, но точная форма `start`, `dispose`, rollback и request abort пока не нормирована. До принятия решения действует общее правило владения lifecycle Level 1.
|
||||
56
DRAFT/level-2/domains/testing.md
Normal file
56
DRAFT/level-2/domains/testing.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Тестирование доменного пакета
|
||||
|
||||
> Проверка владельцев и публичных границ Level 2.
|
||||
|
||||
## Связанное правило
|
||||
|
||||
- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016)
|
||||
|
||||
## Размещение
|
||||
|
||||
Тест находится рядом с модулем-владельцем проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`.
|
||||
|
||||
| Проверяемая граница | Владелец теста |
|
||||
|---|---|
|
||||
| Предметные сценарии, `DomainApi`, данные и ошибки | `business` |
|
||||
| Техническое преобразование | Adapter |
|
||||
| Выбор зависимостей и environment boundary | Preset |
|
||||
| Provider, hook, form или guard | Соответствующий framework binding module |
|
||||
| Граф нескольких доменов | Модуль `composition`, точка входа `app` или другое место сборки |
|
||||
|
||||
## Business через фабрику
|
||||
|
||||
Каждый публичный предметный сценарий проверяется через единственную фабрику с управляемыми зависимостями:
|
||||
|
||||
```ts
|
||||
const api = authFactory(createAuthTestDeps({
|
||||
requestCode: async () => ({ ok: true }),
|
||||
}))
|
||||
|
||||
await api.requestPhoneOtp('+79991112233')
|
||||
```
|
||||
|
||||
Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от будущего решения `throw` или `Result`, но наружу всегда проверяется только AuthErrorCode.
|
||||
|
||||
Business-тест не использует React, реальный SDK, database или production preset.
|
||||
|
||||
## Остальные модули
|
||||
|
||||
Adapter-тест проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки.
|
||||
|
||||
Preset-тест проверяет выбранные реализации, вызов одной фабрики, контракт возвращённого `DomainApi` и отсутствие несовместимого environment-кода.
|
||||
|
||||
Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthApi` и проверяет Provider, hook или component. `login-form` не повторяет полный набор business-сценариев.
|
||||
|
||||
## Архитектурные проверки
|
||||
|
||||
Отдельная import-graph проверка подтверждает:
|
||||
|
||||
- отсутствие root API доменного пакета и Framework Groups;
|
||||
- отсутствие runtime cross-domain imports;
|
||||
- отсутствие type-only импортов из чужих presets, adapters и framework-модулей;
|
||||
- отсутствие cross-domain framework hooks, contexts и components;
|
||||
- отсутствие server-only достижимости из client modules;
|
||||
- отсутствие runtime- и type-only циклов.
|
||||
|
||||
Runtime-тест не заменяет эти проверки.
|
||||
Reference in New Issue
Block a user