From bd479b346faaca715ec78748e15ed55c93c706b6 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Thu, 30 Jul 2026 10:56:48 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20level-2=20=D0=B4=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 20 ++-- DRAFT/README.md | 3 +- DRAFT/index.md | 31 ++++--- DRAFT/level-1/README.md | 6 +- DRAFT/level-1/layers.md | 2 +- DRAFT/level-1/terminology.md | 6 ++ DRAFT/level-2/README.md | 49 ++++++++++ DRAFT/level-2/dependencies.md | 50 ++++++++++ DRAFT/level-2/domains.md | 117 ++++++++++++++++++++++++ DRAFT/level-2/layers.md | 74 +++++++++++++++ DRAFT/level-2/terminology.md | 54 +++++++++++ DRAFT/level-2/validation.md | 48 ++++++++++ DRAFT/level-3/domains/README.md | 2 + DRAFT/level-3/domains/open-questions.md | 13 +-- DRAFT/rules/README.md | 2 + DRAFT/rules/level-1.md | 2 +- DRAFT/rules/level-2.md | 11 +++ README.md | 2 +- scripts/check-site.mjs | 46 +++++++--- site/.vitepress/config.mts | 33 ++++--- site/.vitepress/theme/style.css | 18 ++-- site/README.md | 4 +- 22 files changed, 516 insertions(+), 77 deletions(-) create mode 100644 DRAFT/level-2/README.md create mode 100644 DRAFT/level-2/dependencies.md create mode 100644 DRAFT/level-2/domains.md create mode 100644 DRAFT/level-2/layers.md create mode 100644 DRAFT/level-2/terminology.md create mode 100644 DRAFT/level-2/validation.md create mode 100644 DRAFT/rules/level-2.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 34327d5..ed23dcc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,17 +5,19 @@ on: branches: [master] paths: - '.github/workflows/docs.yml' - - 'docs/**' + - 'DRAFT/**' - 'site/**' - - 'scripts/check-docs.mjs' + - 'draft-rules.js' + - 'scripts/check-site.mjs' - 'package.json' - 'package-lock.json' pull_request: paths: - '.github/workflows/docs.yml' - - 'docs/**' + - 'DRAFT/**' - 'site/**' - - 'scripts/check-docs.mjs' + - 'draft-rules.js' + - 'scripts/check-site.mjs' - 'package.json' - 'package-lock.json' workflow_dispatch: @@ -45,14 +47,8 @@ jobs: - name: Install dependencies run: npm ci - - name: Check specification - run: npm run check:docs - - - name: Build documentation - run: npm run docs:build - - - name: Check documentation search - run: npm run check:docs-search + - name: Check documentation + run: npm run check:site - name: Configure Pages uses: actions/configure-pages@v5 diff --git a/DRAFT/README.md b/DRAFT/README.md index c869c3d..ab6bff2 100644 --- a/DRAFT/README.md +++ b/DRAFT/README.md @@ -5,7 +5,8 @@ ## Материалы - [Первый уровень](./level-1/README.md) - базовые слои, модули и зависимости. -- [Домены](./level-3/domains/README.md) - исследование доменов и строгих границ выполнения. +- [Второй уровень](./level-2/README.md) - доменный слой и доменные модули. +- [Третий уровень](./level-3/domains/README.md) - исследование строгой внутренней архитектуры доменов. - [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки. ## Соглашение diff --git a/DRAFT/index.md b/DRAFT/index.md index 55f5a05..cae2330 100644 --- a/DRAFT/index.md +++ b/DRAFT/index.md @@ -1,11 +1,11 @@ --- layout: home -title: SLM Level 1 +title: SLM Design hero: - name: SLM Level 1 - text: Базовая архитектура фронтенд-приложений - tagline: Слои, модули, зависимости, публичные границы и жизненный цикл без отдельной доменной архитектуры. + name: SLM Design + text: Последовательная архитектура фронтенд-приложений + tagline: Начните со слоёв и модулей, затем добавьте доменные границы без преждевременной сложности строгой runtime-архитектуры. image: src: /logo.svg alt: SLM Design @@ -14,20 +14,23 @@ hero: text: Читать Level 1 link: /level-1/ - theme: alt - text: Открыть правила - link: /rules/level-1 + text: Читать Level 2 + link: /level-2/ + - theme: alt + text: Реестр правил + link: /rules/ features: - - title: Пять слоёв - details: Линейный порядок app, compositions, infra, ui и shared задаёт роли кода и допустимое направление зависимостей. - - title: Модульные границы - details: Ответственность имеет одного владельца, а внешний код использует модуль только через его публичный API. - - title: 14 правил - details: Пять правил проверяются автоматически, девять требуют архитектурного ревью. + - title: Level 1 · Архитектурная база + details: Слои, модули, публичные API, зависимости, вложенные границы и жизненный цикл ресурсов. + - title: Level 2 · Доменные модули + details: Новый слой domains локализует модели, правила, сценарии и продуктовое состояние без обязательной внутренней структуры домена. + - title: Канонические правила + details: Точные блокирующие требования отделены от определений, рекомендаций и примеров и собраны по уровням. --- ## Что опубликовано -Сайт содержит только рабочий черновик SLM Level 1 и его канонический реестр правил. Доменные уровни, монорепозитории и дополнительные режимы архитектуры пока не входят в опубликованную документацию. +Сайт содержит рабочие черновики SLM Levels 1-2 и их канонические реестры правил. Исследования Level 3, монорепозитории и дополнительные режимы архитектуры пока не входят в опубликованную документацию. -Определения Level 1 нормативны внутри черновика. Точные формулировки блокирующих требований находятся только в [реестре правил](/rules/level-1). +Определения выбранного уровня нормативны внутри черновика. Точные формулировки блокирующих требований находятся только в [реестре правил](/rules/). diff --git a/DRAFT/level-1/README.md b/DRAFT/level-1/README.md index 41365dd..d76df6d 100644 --- a/DRAFT/level-1/README.md +++ b/DRAFT/level-1/README.md @@ -9,8 +9,8 @@ Level 1 задаёт основу SLM для лёгких проектов, ко | Уровень | Назначение | |---|---| | Level 1 | Слои, зависимости, структурные сущности и жизненный цикл ресурсов | -| Level 2 | Слой `domains` для модулей с доменной логикой | -| Level 3 | Строгие правила доменов для крупных и критичных проектов | +| Level 2 | Слой `domains` и доменные модули без строгой внутренней формы | +| Level 3 | Строгие роли и runtime-границы внутри доменов | Повышение уровня может требовать рефакторинга, но базовые понятия Level 1 сохраняются. @@ -20,7 +20,7 @@ Level 1 описывает слои, модули, группы, сегмент Level 1 не описывает домены, фабрики, порты, адаптеры, обязательный поток данных, монорепозитории, соглашения об именовании и файловый стайлгайд. -Появление самостоятельной доменной логики является сигналом рассмотреть Level 2. +Появление самостоятельной доменной логики является сигналом рассмотреть [Level 2](../level-2/). ## Виды утверждений diff --git a/DRAFT/level-1/layers.md b/DRAFT/level-1/layers.md index 234a520..3685948 100644 --- a/DRAFT/level-1/layers.md +++ b/DRAFT/level-1/layers.md @@ -82,4 +82,4 @@ shared Разрешённый импорт не переносит владение. Например, `infra` может использовать `ui`, но продуктовый интерфейс по-прежнему принадлежит `compositions`. -Самостоятельная доменная модель или сценарий являются сигналом рассмотреть Level 2, а не расширять ответственность `shared` или `infra`. +Самостоятельная доменная модель или сценарий являются сигналом рассмотреть [Level 2](../level-2/), а не расширять ответственность `shared` или `infra`. diff --git a/DRAFT/level-1/terminology.md b/DRAFT/level-1/terminology.md index d96a563..892a22f 100644 --- a/DRAFT/level-1/terminology.md +++ b/DRAFT/level-1/terminology.md @@ -42,6 +42,12 @@ ## Структурные сущности +### Нормативный порядок слоёв + +Полный линейный порядок слоёв выбранного уровня SLM. Он определяет, какой слой является нижним для проверки зависимостей. + +Для Level 1 нормативным является порядок `app → compositions → infra → ui → shared`. Более высокий уровень может добавить новые роли и задаёт собственный полный порядок, сохраняя направление от верхних слоёв к нижним. + ### Слой Одна из пяти верхнеуровневых ролей внутри SLM root: diff --git a/DRAFT/level-2/README.md b/DRAFT/level-2/README.md new file mode 100644 index 0000000..aa4de26 --- /dev/null +++ b/DRAFT/level-2/README.md @@ -0,0 +1,49 @@ +# SLM Level 2 + +> Статус: рабочий черновик. Документы в этой папке не являются спецификацией. + +Level 2 расширяет архитектурную базу Level 1 слоем `domains`. Он предназначен для проектов, в которых появились самостоятельные предметные области, но ещё не требуется строгая runtime-архитектура доменов. + +## Наследование Level 1 + +Проект Level 2 соблюдает все определения и правила Level 1, если терминология Level 2 не задаёт расширение для нового слоя. Модуль, Group, сегмент, компонент, вложенный модуль, публичный API, граф зависимостей и владение жизненным циклом сохраняют смысл Level 1. + +Канонический набор требований образуют два реестра: + +- [правила Level 1](../rules/level-1.md); +- [дополнительные правила Level 2](../rules/level-2.md). + +## Место в уровнях SLM + +| Уровень | Назначение | +|---|---| +| Level 1 | Слои, зависимости, структурные сущности и жизненный цикл ресурсов | +| Level 2 | Доменный слой и доменные модули без строгой внутренней формы | +| Level 3 | Строгая внутренняя архитектура и runtime-границы доменов | + +## Основная идея + +Домен Level 2 является обычным SLM-модулем слоя `domains`. Он владеет одной связной предметной областью и может содержать всю необходимую ей реализацию, сегменты, компоненты и вложенные модули. + +По умолчанию доменные модули размещаются непосредственно в слое. При большом количестве доменов слой также может содержать обычные навигационные Groups Level 1. + +```text +src/domains/ +├── auth/ # Доменный модуль +├── catalog/ # Доменный модуль +└── orders/ # Доменный модуль +``` + +## Область Level 2 + +Level 2 описывает роль слоя `domains`, границу доменного модуля, опциональную группировку и зависимости с участием нового слоя. + +Level 2 не задаёт обязательные внутренние роли, каталоги или способ сборки доменного модуля. Строгая внутренняя архитектура относится к Level 3. + +## Карта черновика + +- [Терминология](./terminology.md) +- [Слои](./layers.md) +- [Домены](./domains.md) +- [Зависимости](./dependencies.md) +- [Проверка](./validation.md) diff --git a/DRAFT/level-2/dependencies.md b/DRAFT/level-2/dependencies.md new file mode 100644 index 0000000..6133234 --- /dev/null +++ b/DRAFT/level-2/dependencies.md @@ -0,0 +1,50 @@ +# Зависимости Level 2 + +> Расширение графа зависимостей Level 1 слоем `domains`. + +Level 2 не вводит отдельный вид зависимости. Доменные модули являются обычными узлами графа модулей, а Groups не участвуют в графе. + +## Направление + +Модуль слоя `domains` может импортировать: + +- публичные API других доменных модулей; +- публичные API модулей `infra`, `ui` и `shared`; +- нормативные ресурсы `shared`. + +`infra`, `ui` и `shared` не импортируют `domains`. `compositions` и `app` могут использовать публичные API доменных модулей как код нижнего слоя. + +## Междоменные зависимости + +Импорт между доменными модулями разрешён независимо от их Group: + +```ts +// domains/orders +import type { Product } from '@/domains/catalog' +``` + +Он создаёт обычное ребро графа модулей: + +```text +orders → catalog +``` + +Обратная runtime- или type-only зависимость, создающая цикл, запрещена общим правилом Level 1. + +## Groups и зависимости + +Путь опциональной Group участвует в адресе модуля, но сама Group не является импортируемой сущностью. Размещение в разных Groups не запрещает импорт и не задаёт его направление. + +Если двум бизнес-приложениям нужна гарантированная архитектурная изоляция, одной Group недостаточно: такая граница требует отдельных SLM roots или правил более высокого проектного уровня. + +## Вложенные модули + +Вложенный модуль домена остаётся внутренним для родительской границы. Другой домен не импортирует его напрямую и получает необходимые экспорты через API корневого доменного модуля. + +## Связанные правила + +- [`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-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) +- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010) diff --git a/DRAFT/level-2/domains.md b/DRAFT/level-2/domains.md new file mode 100644 index 0000000..684d3f7 --- /dev/null +++ b/DRAFT/level-2/domains.md @@ -0,0 +1,117 @@ +# Домены Level 2 + +> Пояснение модели доменных модулей без строгой внутренней архитектуры Level 3. + +Домен Level 2 является обычным SLM-модулем. Новый уровень добавляет доменную роль и место в порядке слоёв, но не вводит отдельную структурную сущность поверх модуля. + +## Связанные правила + +- [`SLM-L2-DOMAIN-R001`](../rules/level-2.md#slm-l2-domain-r001) +- [`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) +- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) +- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010) +- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013) + +## Один домен, один модуль + +Связная предметная область получает один корневой доменный модуль. Вся логика авторизации может находиться внутри `auth` без обязательного выделения `session`, `phone-login` или каждого сценария в соседний доменный модуль. + +```text +domains/auth/ +├── hooks/ +├── services/ +│ ├── session.service.ts +│ └── phone-login.service.ts +├── stores/ +├── types/ +├── ui/ +└── index.ts +``` + +Названия и набор сегментов определяет стайлгайд проекта. Level 2 не требует показанный каркас. + +## Внутренняя свобода + +Доменный модуль может содержать всё, что нужно его ответственности: + +- доменные типы, модели, правила и сценарии; +- продуктовое состояние и управление его жизненным циклом; +- framework hooks и domain-specific компоненты; +- вызовы переданных или импортированных технических сервисов; +- локальные adapters, mappers и интеграционный код; +- сегменты и вложенные модули. + +Если техническая реализация становится самостоятельным сервисом без доменной семантики, она переносится в `infra` по общим правилам назначения слоёв. + +## Когда нужен вложенный модуль + +Часть домена становится вложенным модулем только при появлении самостоятельной ответственности, публичного API внутри родительской границы, собственных зависимостей или области жизни. + +```text +domains/auth/ +├── parts/ +│ ├── auth-form/ +│ │ ├── auth-form.tsx +│ │ └── index.ts +│ └── registration-form/ +│ ├── registration-form.tsx +│ └── index.ts +├── auth.ts +└── index.ts +``` + +Внешний код по-прежнему получает `auth-form` и `registration-form` только через публичный API `auth`. Само наличие нескольких файлов или отдельного пользовательского сценария не требует вложенного модуля. + +## Опциональная группировка + +Если количество доменов затрудняет навигацию, Groups внутри `domains` могут классифицировать их по принадлежности к разным бизнес-приложениям или продуктовым областям. + +```text +domains/ +├── shop/ # Group +│ ├── auth/ # Доменный модуль Shop Auth +│ ├── catalog/ # Доменный модуль +│ └── orders/ # Доменный модуль +└── cabinet/ # Group + ├── auth/ # Отдельный доменный модуль Cabinet Auth + ├── profile/ # Доменный модуль + └── documents/ # Доменный модуль +``` + +`shop` и `cabinet` не имеют `index.ts`, состояния, реализации или публичного API. Они могут содержать только доменные модули и другие Groups. + +Одинаковое имя модуля в разных Groups допустимо, если это разные владельцы и разные предметные области. Если авторизация действительно общая, ей нужен один общий модуль-владелец, а не две копии. + +Group не создаёт dependency boundary. Импорт между модулями разных Groups проверяется так же, как любой импорт внутри слоя `domains`. + +## Публичный API + +Внешний код использует домен через обычный публичный API модуля: + +```ts +import { signOut, useSession } from '@/domains/auth' +``` + +Deep import остаётся нарушением: + +```ts +import { useSession } from '@/domains/auth/hooks/use-session' +``` + +Group не предоставляет агрегирующий API и не реэкспортирует содержащиеся в ней домены. + +## Граница с другими слоями + +| Ответственность | Владелец | +|---|---| +| Доменная модель, правило, сценарий или продуктовое состояние | Доменный модуль | +| Страница, маршрут, экран и конкретный visual outcome | Модуль `compositions` | +| Самостоятельный технический сервис без предметной модели | Модуль `infra` | +| Универсальный интерфейс без продуктовой семантики | Модуль `ui` | +| Независимая детерминированная утилита | `shared` или локальный владелец | + +Число потребителей не является единственным критерием. Самостоятельная доменная ответственность может принадлежать `domains`, даже если сегодня используется одной композицией. diff --git a/DRAFT/level-2/layers.md b/DRAFT/level-2/layers.md new file mode 100644 index 0000000..b7a72bf --- /dev/null +++ b/DRAFT/level-2/layers.md @@ -0,0 +1,74 @@ +# Слои Level 2 + +> Пояснение нормативной модели слоёв Level 2. + +Level 2 добавляет `domains` между продуктовой композицией и техническими сервисами приложения. + +## Базовая структура + +```text +src/ +├── app/ +├── compositions/ +├── domains/ +├── infra/ +├── ui/ +└── shared/ +``` + +Отсутствующая роль не требует пустой папки. Проект без самостоятельной доменной ответственности может оставаться на Level 1. + +## Роли слоёв + +| Слой | Роль | +|---|---| +| `app` | Запуск, маршруты, преобразование входных данных и подключение готовых публичных API | +| `compositions` | Страницы, макеты, экраны, виджеты и конкретные продуктовые композиции | +| `domains` | Предметные области, их модели, правила, сценарии и продуктовое состояние | +| `infra` | Технические сервисы и возможности приложения без самостоятельной предметной модели | +| `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции | +| `shared` | Независимый детерминированный фундамент без знания о продукте, состояния и ввода-вывода | + +## Порядок слоёв + +```text +app + ↓ +compositions + ↓ +domains + ↓ +infra + ↓ +ui + ↓ +shared +``` + +Код слоя может импортировать модули своего или любого нижнего слоя. Промежуточные слои можно пропускать. + +| Слой | Может импортировать другие слои | +|---|---| +| `app` | `compositions`, `domains`, `infra`, `ui`, `shared` | +| `compositions` | `domains`, `infra`, `ui`, `shared` | +| `domains` | `infra`, `ui`, `shared` | +| `infra` | `ui`, `shared` | +| `ui` | `shared` | +| `shared` | Нет | + +Импорты между модулями одного слоя разрешены. Поэтому один доменный модуль может зависеть от публичного API другого доменного модуля, если общий граф остаётся ацикличным. + +## Границы ролей + +`compositions` определяет устройство конкретной страницы, маршрута или визуального результата. `domains` определяет повторяемую предметную семантику, которая не принадлежит одной композиции. + +`infra` предоставляет техническую возможность. Если код определяет продуктовую модель, правило или сценарий поверх этой возможности, владельцем такого кода является доменный модуль. + +Разрешённый импорт не переносит владение. Доменный модуль может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев. + +## Связанные правила + +- [`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-L2-DOMAIN-R001`](../rules/level-2.md#slm-l2-domain-r001) diff --git a/DRAFT/level-2/terminology.md b/DRAFT/level-2/terminology.md new file mode 100644 index 0000000..ddcfdbc --- /dev/null +++ b/DRAFT/level-2/terminology.md @@ -0,0 +1,54 @@ +# Терминология Level 2 + +> Нормативные определения рабочего черновика. Этот раздел не объявляет правила. + +Level 2 наследует терминологию Level 1 и добавляет определения, необходимые слою `domains`. Структурные сущности Level 1 не меняют смысл. + +## Нормативный порядок слоёв + +Для Level 2 нормативным является полный порядок: + +```text +app → compositions → domains → infra → ui → shared +``` + +Нижним считается любой слой справа от исходного. Промежуточный слой не является обязательным посредником. + +## Слой `domains` + +Слой предметных областей приложения. Он содержит доменные модули и Groups, которые классифицируют эти модули. + +Код слоя выражает продуктовые понятия, правила, сценарии или продуктовое состояние, которые не принадлежат устройству одной конкретной страницы, маршрута или визуальной композиции. + +## Доменная ответственность + +Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Наличие каждого из этих элементов не является обязательным. + +Количество экранов, endpoint-ов, хуков или файлов само по себе не определяет границу доменной ответственности. + +## Доменный модуль + +Обычный SLM-модуль слоя `domains`, представляющий одну доменную ответственность. Его ближайшей внешней структурной границей является слой `domains` или Group этого слоя, а не другой модуль. + +Доменный модуль подчиняется всем правилам модулей Level 1: имеет отдельную папку, одного владельца, единый публичный API, собственный узел графа зависимостей и определённый жизненный цикл ресурсов. + +Доменный модуль может содержать корневые файлы, сегменты, компоненты и вложенные модули. Вложенный модуль внутри него остаётся обычным вложенным модулем и не становится самостоятельным доменным модулем. + +## Group слоя `domains` + +Обычная Group Level 1, которая классифицирует доменные модули по принадлежности к бизнес-приложению, продуктовой области или другому понятному проекту признаку. + +Такая Group не является доменом, владельцем ответственности или узлом графа зависимостей. Она не задаёт отдельного направления импортов и не изолирует содержащиеся в ней модули от других Groups. + +## Структурная модель + +```text +SLM root +└── domains + ├── доменный модуль + │ ├── сегменты + │ └── вложенные модули + └── доменный модуль +``` + +Путь помогает определить структурную границу, но не доказывает корректность предметной декомпозиции. Решение о том, является ли ответственность самостоятельным доменом, требует понимания продукта. diff --git a/DRAFT/level-2/validation.md b/DRAFT/level-2/validation.md new file mode 100644 index 0000000..cdcc754 --- /dev/null +++ b/DRAFT/level-2/validation.md @@ -0,0 +1,48 @@ +# Проверка Level 2 + +> Проверка расширенной модели слоёв и доменных границ. + +Проект Level 2 выполняет все автоматические проверки и архитектурное ревью Level 1, используя нормативный порядок из шести слоёв. + +## Сопоставление структуры + +Конфигурация проверки проекта дополнительно определяет: + +- физический путь слоя `domains`; +- доменные модули непосредственно в слое и внутри Groups; +- Groups слоя `domains`; +- вложенные модули внутри доменных модулей. + +Сопоставление путей не определяет предметный смысл. Оно позволяет проверить направление импортов, публичные API, циклы и доступ к вложенным модулям. + +## Автоматическая проверка + +Новых автоматических правил Level 2 не требуется. Структурные инварианты обеспечивают правила Level 1: + +- порядок `app → compositions → domains → infra → ui → shared`; +- импорт модулей только через публичные API; +- отсутствие циклов в графе модулей; +- отсутствие прямого доступа к вложенным модулям извне родителя; +- отсутствие реализации и API у Groups. + +## Архитектурное ревью + +Дополнительное правило Level 2 проверяется на ревью. Нужно определить: + +- представляет ли доменный модуль одну связную предметную область; +- не разделена ли одна область на соседние доменные модули без самостоятельных владельцев; +- не объединены ли в одном модуле несвязанные предметные области; +- принадлежат ли модели, правила, сценарии и продуктовое состояние правильному домену; +- остаётся ли Group только навигационной классификацией; +- не размещены ли page-specific composition или самостоятельный технический сервис в `domains`. + +## Связанные правила + +- [`SLM-L2-DOMAIN-R001`](../rules/level-2.md#slm-l2-domain-r001) +- [`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-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) +- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) +- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) + +Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не определяет предметные границы приложения. diff --git a/DRAFT/level-3/domains/README.md b/DRAFT/level-3/domains/README.md index 5428384..8b430c4 100644 --- a/DRAFT/level-3/domains/README.md +++ b/DRAFT/level-3/domains/README.md @@ -4,6 +4,8 @@ Эта папка фиксирует текущую гипотезу о новой сущности `Domain`, business-модуле внутри неё, framework-neutral factory, ports, adapters, presets и framework bindings. +Level 3 развивает доменный модуль Level 2 в строгую доменную границу с несколькими модулями разных ролей. Такой переход может потребовать рефакторинга, но сохраняет предметного владельца и базовые модульные правила. + Идентификаторы вида `DOM-N001` и `FAC-N001` являются стабильными якорями заметок. Они нужны для обсуждения и последующего переноса решений в спецификацию, но не являются идентификаторами нормативных правил. ## Основная формула diff --git a/DRAFT/level-3/domains/open-questions.md b/DRAFT/level-3/domains/open-questions.md index 640d3a9..6856f33 100644 --- a/DRAFT/level-3/domains/open-questions.md +++ b/DRAFT/level-3/domains/open-questions.md @@ -83,19 +83,16 @@ Framework dependency сама по себе не доказывает Domain own ### OPEN-N010: На каком уровне появляется Domain -Нужно встроить Domain в монотонную шкалу архитектурных уровней. Более высокий уровень должен добавлять требования и не отменять правила предыдущего. +Статус: предварительно закрыт в пользу трёх уровней. Более высокий уровень добавляет требования и может потребовать структурного рефакторинга без изменения предметного владельца. -Предварительный вариант: +Текущая шкала: ```text -Level 1: modules -Level 2: layers -Level 3: domains -Level 4: runtime-safe factories, ports, presets и verification +Level 1: базовые слои и модули +Level 2: доменные модули без строгой внутренней формы +Level 3: business, factories, ports, adapters, presets и verification внутри Domain ``` -Точная классификация будет выполнена после извлечения атомарных правил из legacy-документации и этих заметок. - ## Проверяемость ### OPEN-N011: Architecture lint diff --git a/DRAFT/rules/README.md b/DRAFT/rules/README.md index d8d4a07..b998129 100644 --- a/DRAFT/rules/README.md +++ b/DRAFT/rules/README.md @@ -50,6 +50,7 @@ SLM-L{level}-{group}-{class}{number} | `COMPONENT` | Компоненты | | `NESTED_MODULE` | Вложенные модули | | `LIFECYCLE` | Жизненный цикл | +| `DOMAIN` | Домены | Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования. @@ -115,3 +116,4 @@ SLM-L{level}-{group}-{class}{number} ## Наборы правил - [Первый уровень](./level-1.md) +- [Второй уровень](./level-2.md) diff --git a/DRAFT/rules/level-1.md b/DRAFT/rules/level-1.md index 397f17d..26f43b7 100644 --- a/DRAFT/rules/level-1.md +++ b/DRAFT/rules/level-1.md @@ -13,7 +13,7 @@ > **Направление зависимостей** > -> Внутри одного SLM root код каждого слоя может зависеть только от кода этого же или любого нижнего слоя в порядке `app → compositions → infra → ui → shared`. +> Внутри одного SLM root код каждого слоя может зависеть только от кода этого же или любого нижнего слоя в нормативном порядке слоёв выбранного уровня SLM. ### SLM-L1-LAYER-R003 diff --git a/DRAFT/rules/level-2.md b/DRAFT/rules/level-2.md new file mode 100644 index 0000000..46e4877 --- /dev/null +++ b/DRAFT/rules/level-2.md @@ -0,0 +1,11 @@ +# Правила SLM второго уровня + +Проект Level 2 соблюдает все правила Level 1 и дополнительные правила этого реестра. + +## Граница доменов + +### SLM-L2-DOMAIN-R001 + +> **Граница домена** +> +> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие этой области доменные модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей. diff --git a/README.md b/README.md index ce2181a..27967ca 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Структура -- `DRAFT/` - рабочая документация Level 1 и источник содержимого сайта. +- `DRAFT/` - рабочая документация Levels 1-2, исследования Level 3 и источник содержимого сайта. - `site/` - VitePress-конфигурация, тема и статические ресурсы. - `docs/` и `docs-v3/` - архивные версии документации, не используемые сайтом. - `old-docs/` - действующая legacy-документация для текущего skill. diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs index adc21de..1a208dc 100644 --- a/scripts/check-site.mjs +++ b/scripts/check-site.mjs @@ -4,9 +4,12 @@ import { fileURLToPath, pathToFileURL } from 'node:url' const repositoryRoot = fileURLToPath(new URL('../', import.meta.url)) const distRoot = path.join(repositoryRoot, 'site', '.vitepress', 'dist') -const rulesSource = path.join(repositoryRoot, 'DRAFT', 'rules', 'level-1.md') const siteOrigin = 'https://site.test' const siteBase = '/slm-design/' +const ruleRegistries = [ + { source: path.join(repositoryRoot, 'DRAFT', 'rules', 'level-1.md'), route: 'rules/level-1' }, + { source: path.join(repositoryRoot, 'DRAFT', 'rules', 'level-2.md'), route: 'rules/level-2' }, +] const expectedPages = [ '404.html', @@ -22,8 +25,15 @@ const expectedPages = [ 'level-1/nested-modules.html', 'level-1/lifecycle.html', 'level-1/validation.html', + 'level-2/index.html', + 'level-2/terminology.html', + 'level-2/layers.html', + 'level-2/domains.html', + 'level-2/dependencies.html', + 'level-2/validation.html', 'rules/index.html', 'rules/level-1.html', + 'rules/level-2.html', ].sort() async function collectHtmlFiles(directory, prefix = '') { @@ -100,10 +110,6 @@ for (const [relativePath, html] of htmlByPage) { } } -const rulesMarkdown = await readFile(rulesSource, 'utf8') -const ruleIds = [...rulesMarkdown.matchAll(/^### (SLM-L1-[A-Z_]+-[AR]\d{3})$/gm)] - .map((match) => match[1]) -const rulesHtml = htmlByPage.get('rules/level-1.html') const searchChunksDirectory = path.join(distRoot, 'assets', 'chunks') const searchChunks = (await readdir(searchChunksDirectory)) .filter((file) => file.startsWith('@localSearchIndex') && file.endsWith('.js')) @@ -115,18 +121,28 @@ if (searchChunks.length !== 1) { const searchModuleUrl = `${pathToFileURL(path.join(searchChunksDirectory, searchChunks[0])).href}?t=${Date.now()}` const searchData = JSON.parse((await import(searchModuleUrl)).default) const searchUrls = new Set(Object.values(searchData.documentIds)) +let ruleCount = 0 -for (const ruleId of ruleIds) { - const anchor = ruleId.toLowerCase() - const expectedUrl = `${siteBase}rules/level-1#${anchor}` +for (const registry of ruleRegistries) { + const rulesMarkdown = await readFile(registry.source, 'utf8') + const ruleIds = [...rulesMarkdown.matchAll(/^### (SLM-L\d+-[A-Z_]+-[AR]\d{3})$/gm)] + .map((match) => match[1]) + const rulesHtml = htmlByPage.get(`${registry.route}.html`) - if (!rulesHtml.includes(`id="${anchor}"`)) { - throw new Error(`Published registry does not contain anchor ${anchor}`) + for (const ruleId of ruleIds) { + const anchor = ruleId.toLowerCase() + const expectedUrl = `${siteBase}${registry.route}#${anchor}` + + if (!rulesHtml.includes(`id="${anchor}"`)) { + throw new Error(`Published registry does not contain anchor ${anchor}`) + } + + if (!searchUrls.has(expectedUrl)) { + throw new Error(`Local search index does not contain canonical record ${expectedUrl}`) + } } - if (!searchUrls.has(expectedUrl)) { - throw new Error(`Local search index does not contain canonical record ${expectedUrl}`) - } + ruleCount += ruleIds.length } const notFoundHtml = htmlByPage.get('404.html') @@ -135,10 +151,10 @@ if (!notFoundHtml.includes('Страница не найдена')) { } const sitemap = await readFile(path.join(distRoot, 'sitemap.xml'), 'utf8') -for (const forbiddenRoute of ['/ru/', '/domains/', '/specification/']) { +for (const forbiddenRoute of ['/ru/', '/domains/', '/level-3/', '/specification/']) { if (sitemap.includes(forbiddenRoute)) { throw new Error(`Sitemap contains archival or excluded route ${forbiddenRoute}`) } } -console.log(`Site check passed: ${actualPages.length - 1} pages and ${ruleIds.length} searchable rules.`) +console.log(`Site check passed: ${actualPages.length - 1} pages and ${ruleCount} searchable rules.`) diff --git a/site/.vitepress/config.mts b/site/.vitepress/config.mts index 515bd98..75b1fbb 100644 --- a/site/.vitepress/config.mts +++ b/site/.vitepress/config.mts @@ -14,12 +14,12 @@ function slugifyHeading(value: string) { .replace(/[^\p{L}\p{N}_-]/gu, '') .replace(/-+/g, '-') - return ['app', 'compositions', 'infra', 'ui', 'shared'].includes(slug) + return ['app', 'compositions', 'domains', 'infra', 'ui', 'shared'].includes(slug) ? `layer-${slug}` : slug } -const levelOneSidebar = [ +const documentationSidebar = [ { text: 'SLM Level 1', items: [ @@ -36,24 +36,37 @@ const levelOneSidebar = [ { text: 'Проверка', link: '/level-1/validation' }, ], }, + { + text: 'SLM Level 2', + items: [ + { text: 'Обзор', link: '/level-2/' }, + { text: 'Терминология', link: '/level-2/terminology' }, + { text: 'Слои', link: '/level-2/layers' }, + { text: 'Домены', link: '/level-2/domains' }, + { text: 'Зависимости', link: '/level-2/dependencies' }, + { text: 'Проверка', link: '/level-2/validation' }, + ], + }, { text: 'Правила', items: [ { text: 'Как устроены правила', link: '/rules/' }, { text: 'Реестр Level 1', link: '/rules/level-1' }, + { text: 'Реестр Level 2', link: '/rules/level-2' }, ], }, ] export default defineConfig({ srcDir: '../DRAFT', - srcExclude: ['README.md', 'domains/**'], + srcExclude: ['README.md', 'level-3/**'], rewrites: { 'level-1/README.md': 'level-1/index.md', + 'level-2/README.md': 'level-2/index.md', 'rules/README.md': 'rules/index.md', }, title: 'SLM Design', - description: 'Базовая архитектура фронтенд-приложений SLM Level 1', + description: 'Последовательная архитектура фронтенд-приложений SLM', lang: 'ru-RU', base: '/slm-design/', cleanUrls: true, @@ -78,12 +91,10 @@ export default defineConfig({ siteTitle: 'SLM Design', nav: [ { text: 'Level 1', link: '/level-1/' }, - { text: 'Правила', link: '/rules/level-1' }, + { text: 'Level 2', link: '/level-2/' }, + { text: 'Правила', link: '/rules/' }, ], - sidebar: { - '/level-1/': levelOneSidebar, - '/rules/': levelOneSidebar, - }, + sidebar: documentationSidebar, socialLinks: [{ icon: 'github', link: repositoryUrl }], search: { provider: 'local', @@ -113,7 +124,7 @@ export default defineConfig({ notFound: { code: '404', title: 'Страница не найдена', - quote: 'Запрошенная страница отсутствует в документации Level 1.', + quote: 'Запрошенная страница отсутствует в опубликованной документации SLM.', linkLabel: 'Перейти на главную', linkText: 'Вернуться к документации', }, @@ -136,7 +147,7 @@ export default defineConfig({ returnToTopLabel: 'Наверх', skipToContentLabel: 'Перейти к содержанию', footer: { - message: 'SLM Level 1', + message: 'SLM Levels 1-2', copyright: 'Рабочий черновик архитектуры.', }, }, diff --git a/site/.vitepress/theme/style.css b/site/.vitepress/theme/style.css index 04f8c42..2004d68 100644 --- a/site/.vitepress/theme/style.css +++ b/site/.vitepress/theme/style.css @@ -71,7 +71,7 @@ html { font-size: clamp(2rem, 4vw, 2.65rem); } -.vp-doc h3[id^='slm-l1-'] { +.vp-doc h3[id^='slm-l'] { margin-top: 34px; scroll-margin-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height) + 24px); color: var(--vp-c-brand-1); @@ -81,7 +81,7 @@ html { letter-spacing: -0.02em; } -.vp-doc h3[id^='slm-l1-'] + blockquote { +.vp-doc h3[id^='slm-l'] + blockquote { position: relative; margin: 10px 0 24px; padding: 42px 20px 18px; @@ -94,7 +94,7 @@ html { line-height: 1.7; } -.vp-doc h3[id^='slm-l1-'] + blockquote::before { +.vp-doc h3[id^='slm-l'] + blockquote::before { position: absolute; top: 14px; left: 20px; @@ -106,23 +106,23 @@ html { text-transform: uppercase; } -.vp-doc h3[id^='slm-l1-'][id*='-a'] + blockquote::before { +.vp-doc h3[id^='slm-l'][id*='-a'] + blockquote::before { content: 'Автоматическая проверка'; } -.vp-doc h3[id^='slm-l1-'][id*='-r'] + blockquote::before { +.vp-doc h3[id^='slm-l'][id*='-r'] + blockquote::before { content: 'Проверка на ревью'; } -.vp-doc h3[id^='slm-l1-'] + blockquote > p { +.vp-doc h3[id^='slm-l'] + blockquote > p { margin: 0; } -.vp-doc h3[id^='slm-l1-'] + blockquote > p + p { +.vp-doc h3[id^='slm-l'] + blockquote > p + p { margin-top: 8px; } -.vp-doc h3[id^='slm-l1-'] + blockquote strong:first-child { +.vp-doc h3[id^='slm-l'] + blockquote strong:first-child { color: var(--vp-c-text-1); font-weight: 700; } @@ -132,7 +132,7 @@ html { font-size: 16px; } - .vp-doc h3[id^='slm-l1-'] + blockquote { + .vp-doc h3[id^='slm-l'] + blockquote { margin-inline: -8px; padding: 42px 14px 14px; } diff --git a/site/README.md b/site/README.md index 8899f7a..5d58291 100644 --- a/site/README.md +++ b/site/README.md @@ -2,14 +2,16 @@ `site/` содержит конфигурацию, тему и статические ресурсы VitePress. -Источником опубликованной документации служит [`DRAFT`](../DRAFT/index.md). Сайт включает только Level 1 и его правила; каталог `DRAFT/domains` исключён из маршрутов и поиска. +Источником опубликованной документации служит [`DRAFT`](../DRAFT/index.md). Сайт включает Levels 1-2 и их правила; исследовательский каталог `DRAFT/level-3` исключён из маршрутов и поиска. ## Маршруты - `/` - главная страница; - `/level-1/` - документация Level 1; +- `/level-2/` - документация Level 2; - `/rules/` - устройство правил; - `/rules/level-1` - канонический реестр Level 1. +- `/rules/level-2` - канонический реестр Level 2. ## Локальный запуск