diff --git a/docs/README.md b/docs/README.md index 25c59c1..0cb49d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,75 +1,107 @@ --- layout: home -title: SLM Design -description: Архитектура фронтенд-приложений с явным владением ответственностями +title: Архитектура фронтенд-приложений +description: SLM Design помогает командам сохранять понятную структуру и предсказуемо развивать фронтенд-приложения по мере роста продукта. hero: name: SLM Design - text: Архитектура владения ответственностями - tagline: Сначала определяется ответственность и её владелец. Слои, группы, модули и сегменты выражают уже принятое архитектурное решение. + text: Архитектура фронтенд-приложений + tagline: Практичная модель для растущих команд и продуктов. Меньше споров о структуре, безопаснее изменения и понятнее код. image: src: /logo.svg alt: SLM Design actions: - theme: brand - text: Изучить архитектуру + text: Узнать, как это работает link: /architecture/ - theme: alt - text: Открыть правила + text: Посмотреть правила link: /rules/registry features: - - title: Ответственность раньше структуры - details: Модуль появляется из самостоятельной ответственности, а не из размера каталога, количества файлов или выбранного фреймворка. - - title: Явная структурная модель - details: Слой определяет роль, группа классифицирует модули, модуль владеет ответственностью, сегмент организует реализацию. - - title: Проверяемые границы - details: Публичные API, направление зависимостей и правила жизненного цикла делают архитектурное решение наблюдаемым и проверяемым. + - title: Один язык для всей команды + details: Разработчики одинаково понимают границы, ответственность и место нового кода. Архитектурные решения перестают зависеть от личных предпочтений. + - title: Предсказуемые изменения + details: Локальная правка остаётся локальной. Команда может развивать внутреннюю реализацию, не переписывая половину приложения. + - title: Рост без хаоса + details: Структура усложняется только вместе с продуктом, а не из-за количества файлов, компонентов или выбранных библиотек. + - title: Архитектура видна в репозитории + details: Правила выражены кодом и структурой проекта, поэтому документация не расходится с реальным приложением. + - title: Независимость от стека + details: SLM не требует конкретного фреймворка, state manager или способа работы с данными и не ограничивает внутреннюю реализацию. + - title: Постепенное внедрение + details: Начните с одного спорного участка и расширяйте модель по мере необходимости, без полной перестройки приложения. --- -SLM Design (Scoped Layered Module Design) — структурная архитектура фронтенд-приложений, основанная на явном владении ответственностями. +## Папки перестают быть архитектурой, когда продукт начинает расти -Архитектурное решение начинается не с папки или имени файла. Сначала определяется ответственность, затем её владелец, роль владельца в приложении и только после этого физическое размещение кода. +На старте почти любая структура выглядит понятной. Затем появляются десятки компонентов, общие hooks, Providers, stores, глубокие импорты и модули, которые знают друг о друге слишком много. Папки остаются на месте, но границы ответственности исчезают. -## Основа +SLM возвращает архитектуре наблюдаемый смысл: -SLM использует четыре структурных понятия: +> **Модуль владеет ответственностью. Весь код внутри ближайшей модульной границы реализует её.** -1. **Слой** классифицирует код по архитектурной роли и ограничивает направление зависимостей. -2. **Группа** помогает классифицировать модули внутри слоя, но ничего не реализует и ничем не владеет. -3. **Модуль** владеет самостоятельной ответственностью, её публичным API, зависимостями, состоянием и жизненным циклом. -4. **Сегмент** организует внутреннее содержимое одного модуля и не создаёт нового владельца. +Это правило одинаково работает для страницы, доменного сценария, UI-библиотеки, инфраструктурного сервиса и небольшого внутреннего модуля. + +## Что меняется для команды + +| Когда границ нет | С SLM Design | +|---|---| +| Решение о размещении кода принимается по похожей папке | Сначала определяется ответственность и её владелец | +| Компоненты и services становятся скрытыми архитектурными центрами | Любой внутренний механизм остаётся реализацией ближайшего модуля | +| Потребители импортируют удобный внутренний файл | Чужой модуль доступен только через публичный фасет | +| Циклы обнаруживаются во время большого рефакторинга | Модульный граф можно проверять lint-инструментами | +| Новые уровни создаются из-за размера каталога | Вложенный модуль появляется только для самостоятельной подответственности | + +Результат: меньше случайной связанности, меньше споров о папках и предсказуемый радиус каждого изменения. + +## Не ещё один шаблон директорий + +SLM не диктует, какие библиотеки, state managers или framework-механизмы использовать. Внутри модуля могут находиться компоненты, Providers, Guards, hooks, stores, services, utilities и сторонние SDK. + +Архитектура отвечает на другие вопросы: + +1. За какой результат отвечает этот код? +2. Какой модуль владеет его контрактом и состоянием? +3. Что действительно нужно внешним потребителям? +4. Какие зависимости допустимы и не создают ли они цикл? +5. Где заканчивается область жизни ресурсов? + +Файловая структура появляется после ответов, а не заменяет их. + +## От одного файла до дерева владельцев + +Модуль может начинаться с одного главного файла и расти без смены архитектурной сущности: ```text -Слой → [Группа*] → Модуль → [Сегмент*] +checkout/ +├── index.ts # Публичный контракт +├── checkout.tsx # Главная реализация +├── components/ # Внутренний код checkout +├── hooks/ +├── services/ +└── modules/ + └── form-session/ # Самостоятельная подответственность + ├── index.ts + ├── form-session.provider.tsx + └── hooks/ ``` -Знак `*` означает, что элементов может не быть или их может быть несколько. Группы могут быть вложены друг в друга внутри одного слоя. Сегменты всегда остаются внутри одного модуля. +Размер, количество файлов и framework-роли не создают владельца. Только отдельно сформулированная ответственность получает модульную границу. -## Документация +## Правила, которые можно проверить -### Архитектура +SLM разделяет смысловые решения и структурные инварианты: -- [Обзор архитектуры](./architecture/) -- [Слои](./architecture/layers.md) -- [Модули](./architecture/modules.md) -- [Сегменты](./architecture/segments.md) +- ответственность, владелец и минимальный публичный контракт проверяются на архитектурном ревью; +- направления между слоями, глубокие импорты, публичные фасеты и модульные циклы проверяются автоматически; +- каждый rule имеет стабильный код и одно место нормативной формулировки; +- framework-компоненты не образуют бесконечную файловую рекурсию, а новые уровни появляются только через вложенные модули. -### Правила +Вы получаете не абстрактный набор рекомендаций, а модель, которую можно обсуждать одинаковыми терминами, видеть в репозитории и постепенно автоматизировать. -- [Как устроены правила](./rules/) -- [Реестр правил](./rules/registry.md) +## Начните с одной ответственности -### Справочные материалы +Не нужно переписывать приложение целиком. Выберите один спорный участок, сформулируйте его ответственность, назначьте владельца и закройте внутреннюю реализацию публичным API. Этого достаточно, чтобы увидеть разницу между папкой и архитектурной границей. -- [Терминология](./reference/terminology.md) -- [Проверка архитектуры](./reference/validation.md) - -## Порядок принятия решения - -1. Сформулировать ответственность без упоминания папок, файлов и библиотек. -2. Назначить одного владельца ответственности. -3. Выбрать слой по роли владельца. -4. Определить публичный контракт, зависимости, состояние и жизненный цикл. -5. Организовать реализацию сегментами, если это упрощает навигацию. -6. Представить принятое решение папками, файлами и публичными точками входа. +[Спроектировать первый модуль](./architecture/modules.md) · [Разобрать зависимости](./architecture/dependencies.md) · [Проверить существующую структуру](./reference/validation.md) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index f646e9f..4657574 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -4,17 +4,17 @@ SLM описывает владение ответственностями вн ## Владение как основа -**Ответственность** — связная часть приложения с одной причиной изменяться. Она становится самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни. +**Ответственность** — результат или поведение приложения, за которое отвечает один модуль-владелец. Она становится самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни, а не только внутренняя роль в работе другого модуля. Props, импорты, локальное состояние и lifecycle-код сами по себе этого не доказывают. У каждой самостоятельной ответственности есть ровно один владелец. В SLM владельцем является модуль. Он определяет: - какие возможности доступны внешним потребителям; -- от каких других возможностей зависит ответственность; +- от каких других модулей зависит ответственность; - кому принадлежат данные и изменяемое состояние; - когда создаются и уничтожаются долгоживущие ресурсы; - как устроена внутренняя реализация. -Место выполнения кода не меняет владельца. Компонент, провайдер, маршрут или точка запуска могут технически вызывать код ответственности, но не получают владение ею автоматически. +Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Место выполнения или вид кода не меняют владельца. ## Структурная модель @@ -22,41 +22,58 @@ SLM описывает владение ответственностями вн SLM root └── слой ├── модуль - │ └── сегмент + │ ├── сегмент + │ └── вложенный модуль + │ ├── сегмент + │ └── вложенный модуль └── группа ├── модуль └── группа └── модуль - └── сегмент ``` | Сущность | Назначение | Владеет ответственностью | |---|---|---| | Слой | Классифицирует код по архитектурной роли | Нет | -| Группа | Классифицирует модули внутри слоя | Нет | -| Модуль | Реализует одну самостоятельную ответственность | Да | +| Группа | Навигационно классифицирует модули внутри слоя | Нет | +| Модуль | Владеет одной самостоятельной ответственностью | Да | | Сегмент | Организует внутренности одного модуля | Нет | -Слой и группа находятся снаружи модульной границы. Сегмент находится внутри неё. Компоненты, хуки, сервисы, хранилища и другие детали реализации принадлежат ближайшему модулю-владельцу, если сами не образуют вложенный модуль. +Вложенный модуль является обычным модулем, размещённым внутри родительского. Он владеет отдельно сформулированной подответственностью и создаёт следующий рекурсивный структурный уровень. + +Слой и группа находятся снаружи модульной границы. Сегмент находится внутри неё. Framework-компоненты, Providers, Guards, hooks, stores, services и другой внутренний код не являются архитектурными сущностями SLM и принадлежат ближайшему модулю. + +## Внутренняя реализация + +Модуль может содержать любой код, относящийся к его ответственности. SLM ограничивает не набор framework-механизмов, а владение и наблюдаемую структуру: + +- помимо опционального главного framework-файла в корне, остальные компонентные единицы располагаются на одном внутреннем уровне относительно модуля; +- каталог компонентной единицы не содержит другие компонентные единицы или вложенные модули; +- рекурсивная структурная вложенность создаётся только вложенными модулями; +- помимо публичных фасетов, в корне находится не более одного главного implementation- или assembly-файла; +- если главный файл нельзя определить уверенно, реализация размещается в сегментах. + +Ограничение глубины framework-компонентов относится к файловой организации, а не к runtime-дереву фреймворка. ## Порядок проектирования Архитектурное решение принимается от смысла к структуре: -1. Описать результат или поведение, за которое должен отвечать код. -2. Определить одну причину изменения этой ответственности. -3. Найти связанные данные, поведение, состояние и жизненный цикл. -4. Назначить модуль владельцем и определить его внешних потребителей. +1. Описать результат, который должен получить пользователь или приложение. +2. Назначить модуль, который отвечает за этот результат. +3. Определить, что модуль делает сам, а что получает от других модулей. +4. Определить, кто использует результат работы модуля. 5. Выбрать [слой](./layers.md) по роли ответственности. -6. Спроектировать публичный API и допустимые зависимости [модуля](./modules.md). -7. При необходимости организовать реализацию [сегментами](./segments.md). -8. Только после этого выбрать физические пути и имена файлов. +6. Спроектировать публичный API и допустимые [зависимости](./dependencies.md). +7. При необходимости классифицировать модули [группами](./groups.md), организовать внутренний код [сегментами](./segments.md) и выбрать физические пути. + +Например, Header сам определяет расположение шапки, отображение навигации и состояние мобильного меню. Текущего пользователя он получает от модуля `auth`, а `Button` и `Avatar` — от модулей `ui`. Если удалить Header, авторизация и UI-компоненты останутся нужны приложению, поэтому Header использует их, но не владеет ими. Если ответственность или владелец не определены, файловая структура не может исправить архитектурную неопределённость. ## Логическая и физическая границы -Модуль не определяется наличием папки, `index.ts` или нескольких файлов. Его определяет самостоятельная ответственность и владение её контрактом, зависимостями, состоянием и жизненным циклом. +Модуль не определяется наличием папки, `index.ts`, framework-компонента или нескольких файлов. Его определяет самостоятельная ответственность и владение её контрактом, зависимостями, состоянием и жизненным циклом. После принятия решения модуль получает отдельную папку и публичные точки входа. Это обязательное физическое представление модульной границы, необходимое потребителям и автоматическим проверкам. @@ -65,7 +82,7 @@ SLM root - самостоятельная ответственность требует модульной границы; - отдельная папка сама по себе не доказывает наличие модуля. -Пути сопоставляются со слоями, группами, модулями и сегментами в стайлгайде или конфигурации конкретного проекта. Имя пути не меняет нормативный смысл сущности. +Пути сопоставляются со слоями, группами, модулями, вложенными модулями и сегментами в стайлгайде или конфигурации конкретного проекта. Имя пути не меняет нормативный смысл сущности. ## Область применения @@ -73,12 +90,14 @@ SLM применяется внутри **SLM root** — границы стру Архитектура определяет: -- роли слоёв и допустимые направления зависимостей; +- роли слоёв и допустимые межслойные направления; - владельцев самостоятельных ответственностей; - публичные границы модулей; +- общий ацикличный граф модулей; - назначение групп и сегментов; +- внутреннюю глубину framework-компонентных единиц; - владение состоянием и жизненным циклом ресурсов. -SLM не задаёт обязательный поток данных, полный файловый стайлгайд, фиксированный набор сегментов, правила монорепозиториев или обязательную внутреннюю форму каждого модуля. +SLM не задаёт обязательный поток данных, конкретный framework, фиксированные имена сегментов, правила монорепозиториев или полный файловый стайлгайд. Нормативный смысл терминов находится в [терминологии](../reference/terminology.md). Точные блокирующие требования объявлены только в [реестре правил](../rules/registry.md). diff --git a/docs/architecture/dependencies.md b/docs/architecture/dependencies.md new file mode 100644 index 0000000..ab0baa0 --- /dev/null +++ b/docs/architecture/dependencies.md @@ -0,0 +1,102 @@ +# Зависимости + +Зависимость связывает архитектурных владельцев. Исходный файл создаёт ребро от своего ближайшего модуля к ближайшему модулю импортируемого файла. + +## Модульный граф + +Узлами архитектурного графа являются модули, включая вложенные. Группы, сегменты, framework-компоненты, hooks, stores и другие файлы реализации отдельных узлов не создают. + +Для каждой связи определяются: + +1. Ближайший модуль-владелец исходного файла. +2. Ближайший модуль-владелец целевого файла. +3. Слои исходного и целевого владельцев. +4. Публичный фасет, через который пересечена граница. + +Обычный импорт, `import type` и реэкспорт одинаково создают архитектурное ребро. Связь файлов внутри одного модуля остаётся внутренней реализацией и не создаёт межмодульную зависимость. + +Вложенный модуль начинает новый узел. Импорт из родительского модуля во вложенный или обратно проверяется как обычная межмодульная связь. + +## Публичная граница + +При пересечении модульной границы используется только объявленный публичный фасет целевого модуля: + +```ts +// Допустимо +import { Button } from '@/ui/button' + +// Недопустимый глубокий импорт +import { Button } from '@/ui/button/button' +``` + +Разрешённое направление слоя или отсутствие цикла не делает глубокий импорт допустимым. + +## Направление между слоями + +Матрица определяет, от каких слоёв может зависеть исходный слой: + +| Исходный слой | Допустимые целевые слои | +|---|---| +| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` | +| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` | +| `domains` | `domains`, `infra`, `ui`, `shared` | +| `infra` | `infra`, `ui`, `shared` | +| `ui` | `ui`, `shared` | +| `shared` | `shared` | + +Разрешённая зависимость может пропускать промежуточные слои. Например, `compositions` может напрямую использовать модуль `ui`, не создавая посредника в `domains` или `infra`. + +Разрешённое направление не переносит владение. Если модуль `domains` использует `infra`, предметный сценарий остаётся ответственностью доменного модуля, а техническая возможность — ответственностью инфраструктурного. + +## Зависимости внутри слоя + +Модули одного слоя могут зависеть друг от друга в любом направлении при одновременном выполнении двух условий: + +1. Целевой модуль используется только через публичный API. +2. Общий модульный граф остаётся ацикличным. + +Принадлежность модулей одной или разным [группам](./groups.md) не влияет на разрешение связи. Группа не имеет API и не является промежуточным узлом импорта. + +SLM не задаёт отдельные same-layer матрицы для `pages`, `layouts`, `widgets`, доменов, инфраструктуры, UI или shared. Если проекту нужна более строгая локальная политика, она является дополнительным проектным ограничением, а не общим правилом SLM. + +## Запрет циклов + +Общий граф модулей внутри одного SLM root остаётся ацикличным. Запрет действует для модулей одного слоя, разных разрешённых слоёв и вложенных модулей. + +Проверки только файлового графа недостаточно. Например: + +```text +module-a/file-1.ts → module-b/file-1.ts +module-b/file-2.ts → module-a/file-2.ts +``` + +Между конкретными файлами может не существовать замкнутого пути, но после сопоставления файлов владельцам возникает архитектурный цикл: + +```text +module-a ↔ module-b +``` + +Lint-проверка модульных циклов должна: + +1. Сопоставить каждый файл ближайшему модулю-владельцу. +2. Свернуть внутренние импорты файлов одного модуля. +3. Добавить межмодульные рёбра для импортов типов, исполняемого кода и реэкспортов. +4. Считать каждый вложенный модуль отдельным узлом. +5. Блокировать любое сильносвязное множество из нескольких модулей. + +Стандартная file-level проверка циклов может использоваться дополнительно, но не заменяет проверку модульного графа. + +## Проверка связи + +Для каждого нового или изменённого импорта проверяются три условия: + +1. Направление разрешено матрицей слоёв. +2. Целевая модульная граница пересечена через публичный фасет. +3. После добавления ребра модульный граф остаётся ацикличным. + +## Связанные правила + +- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002) +- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004) +- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005) +- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010) diff --git a/docs/architecture/groups.md b/docs/architecture/groups.md new file mode 100644 index 0000000..1fe626a --- /dev/null +++ b/docs/architecture/groups.md @@ -0,0 +1,81 @@ +# Группы + +Группа является необязательным навигационным классификатором модулей внутри одного слоя. Она помогает ориентироваться в дереве владельцев, но не реализует ответственность и не создаёт архитектурную границу. + +## Место в модели + +Модуль может находиться непосредственно в слое или внутри одной или нескольких групп: + +```text +слой +├── модуль +└── группа + ├── модуль + └── группа + └── модуль +``` + +Группа создаётся, когда плоский список модулей перестаёт быть понятным. Названия и глубину групп определяет проект. + +```text +compositions/ +├── pages/ # Группа +│ ├── catalog/ # Модуль +│ └── profile/ # Модуль +├── layouts/ # Группа +│ └── main/ # Модуль +└── widgets/ # Группа + └── cart-summary/ # Модуль +``` + +Названия `pages`, `layouts` и `widgets` показывают один из вариантов навигации и не создают дополнительные слои или обязательные роли. + +## Ограничения группы + +Группа: + +- содержит только модули и вложенные группы; +- не владеет файлами реализации; +- не имеет состояния или жизненного цикла; +- не предоставляет публичный API; +- не является узлом графа зависимостей; +- не импортируется внешним кодом; +- не реэкспортирует содержащиеся в ней модули. + +Barrel-файл, открывающий несколько модулей группы как единый контракт, превращает каталог в новую модульную границу. Если такой контракт действительно нужен, для него определяется ответственность и создаётся обычный модуль. + +## Группы и зависимости + +Принадлежность модулей одной или разным группам не влияет на допустимость импорта. Модули одного слоя могут зависеть друг от друга через публичные API, если общий граф остаётся ацикличным. + +Код импортирует конкретный модуль: + +```ts +import { CartSummary } from '@/compositions/widgets/cart-summary' +``` + +Группа не становится промежуточной точкой доступа: + +```ts +// Недопустимый API группы +import { CartSummary } from '@/compositions/widgets' +``` + +Полные правила графа находятся в разделе [Зависимости](./dependencies.md). + +## Группа и сегмент + +Группа организует несколько модулей внутри слоя. [Сегмент](./segments.md) организует код внутри одного модуля. + +| Группа | Сегмент | +|---|---| +| Находится снаружи модульной границы | Находится внутри модульной границы | +| Содержит модули и группы | Содержит внутреннюю реализацию владельца | +| Не принадлежит одному модулю | Всегда принадлежит ближайшему модулю | +| Не содержит файлы реализации | Существует для организации файлов реализации | + +## Связанные правила + +- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007) +- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011) +- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005) diff --git a/docs/architecture/layers.md b/docs/architecture/layers.md index 7335839..44993c4 100644 --- a/docs/architecture/layers.md +++ b/docs/architecture/layers.md @@ -1,6 +1,6 @@ # Слои -Слой классифицирует код по архитектурной роли и задаёт допустимые направления зависимостей. Слой не владеет ответственностью: владельцем остаётся модуль, размещённый в этом слое. +Слой классифицирует код по архитектурной роли. Слой не владеет ответственностью: владельцем остаётся модуль, размещённый в этом слое. Слой выбирается после определения ответственности. Похожее имя папки или наличие зависимости от конкретной библиотеки не являются основанием для выбора слоя. @@ -23,13 +23,13 @@ SLM определяет шесть ролей: `app` содержит точки связи с фреймворком: запуск, файлы маршрутов и преобразование внешних входных данных. Они подключают готовые публичные API других слоёв, но не присваивают их ответственность. -Точки входа `app` являются специальным немодульным исключением. Страница, макет, провайдер или другой самостоятельный продуктовый владелец реализуется в подходящем модуле и только подключается из `app`. +Точки входа `app` являются специальным немодульным исключением. Самостоятельная продуктовая ответственность, даже если она представлена страницей, макетом или Provider, реализуется в подходящем модуле и только подключается из `app`. ### Compositions `compositions` содержит владельцев продуктовых композиций: страниц, макетов, экранов, виджетов, результатов маршрутов и интерфейса, объединяющего несколько ответственностей. -Конкретная организация слоя определяется продуктом и фреймворком. Названия `pages`, `layouts`, `screens` и `widgets` могут использоваться как группы, но не являются дополнительными слоями. +Конкретная организация слоя определяется продуктом и фреймворком. Названия `pages`, `layouts`, `screens` и `widgets` могут использоваться как [группы](./groups.md), но не являются дополнительными слоями и не задают направление импортов. ### Domains @@ -55,52 +55,13 @@ SLM определяет шесть ролей: ## Направление зависимостей -Матрица определяет, от каких слоёв может зависеть исходный слой: +Слой ограничивает только межслойное направление. Модули одного слоя могут зависеть друг от друга через публичные API, если общий граф остаётся ацикличным. -| Исходный слой | Допустимые целевые слои | -|---|---| -| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` | -| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` | -| `domains` | `domains`, `infra`, `ui`, `shared` | -| `infra` | `infra`, `ui`, `shared` | -| `ui` | `ui`, `shared` | -| `shared` | `shared` | +Нормативная матрица, правила same-layer импортов и требования к lint-проверке находятся в разделе [Зависимости](./dependencies.md). -Разрешённая зависимость может пропускать промежуточные слои. Например, `compositions` может напрямую использовать модуль `ui`, не создавая посредника в `domains` или `infra`. +## Группировка -Обычный импорт, импорт типа (`import type`) и реэкспорт одинаково участвуют в архитектурном графе. Для связи между модулями дополнительно действуют их [публичные границы и запрет циклов](./modules.md#зависимости-между-модулями). - -Разрешённое направление не переносит владение. Если модуль `domains` использует `infra`, предметный сценарий остаётся ответственностью доменного модуля, а техническая возможность — ответственностью инфраструктурного. - -`infra` может использовать `ui`, когда технической возможности нужно собственное визуальное представление: CAPTCHA, uploader, карта или инструмент разработчика. `ui` не использует `infra`; необходимые технические возможности универсальный UI получает через входной контракт. - -## Группировка модулей - -Группа классифицирует модули внутри одного слоя или другой группы. Она нужна, когда плоский список модулей перестаёт быть понятным. - -```text -compositions/ -├── pages/ # Группа -│ ├── catalog/ # Модуль -│ └── profile/ # Модуль -├── layouts/ # Группа -│ └── main/ # Модуль -└── widgets/ # Группа - └── cart-summary/ # Модуль -``` - -Группа: - -- содержит только модули и вложенные группы; -- не владеет ответственностью или реализацией; -- не имеет состояния и жизненного цикла; -- не предоставляет публичный API; -- не является узлом графа зависимостей; -- не реэкспортирует содержащиеся в ней модули. - -Модуль может находиться непосредственно в слое. Группа вводится только ради реальной классификации, а её названия и глубину определяет проект. - -Группа организует несколько владельцев внутри слоя. [Сегмент](./segments.md) организует код внутри одного владельца. +Модули могут находиться непосредственно в слое или объединяться в необязательные навигационные [группы](./groups.md). Группа не владеет кодом и не влияет на допустимость зависимостей. ## Немодульные исключения diff --git a/docs/architecture/modules.md b/docs/architecture/modules.md index 8dd3723..868c42d 100644 --- a/docs/architecture/modules.md +++ b/docs/architecture/modules.md @@ -1,6 +1,6 @@ # Модули -Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или сущности фреймворка выполняется код. +Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или механизме выполняется код. ## Ответственность и владелец @@ -8,17 +8,37 @@ Самостоятельность ответственности определяется вопросами: -- есть ли у неё отдельная причина изменяться; +- какой один результат или поведение она обеспечивает; +- что модуль должен делать сам для получения этого результата; +- какие возможности ему нужны от других модулей; - нужен ли внешним потребителям собственный контракт; -- есть ли у неё архитектурные зависимости; -- владеет ли она данными или изменяемым состоянием; +- требуют ли зависимости отдельного архитектурного владения; +- владеет ли она смыслом данных или изменяемого состояния; - нужна ли ей собственная область жизни; - можно ли назвать её независимо от внутренней реализации. -Если ответственность самостоятельна, она получает ровно один модуль-владелец. Если несколько частей изменяются по несвязанным причинам, им нужны разные владельцы. +Props, импорты, Context, локальное состояние, lifecycle-код или количество файлов сами по себе не доказывают самостоятельность ответственности. Если ответственность самостоятельна, она получает ровно один модуль-владелец. Размер не определяет модуль. Модуль может состоять из одного файла реализации, а большая папка может оставаться частью другого владельца. +## Ближайшая граница + +Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Вид кода не меняет владельца: framework-компонент, Provider, Guard, hook, store, service или utility остаются внутренней реализацией ближайшего модуля. + +Вложенный модуль начинает новую границу. Его содержимое реализует выделенную подответственность, а сам вложенный модуль как единица участвует в реализации общего результата родителя: + +```text +checkout/ # Владеет ответственностью checkout +├── checkout.tsx # Реализует checkout +├── components/ # Реализуют checkout +└── modules/ + └── form-session/ # Владеет подответственностью form session + ├── form-session.provider.tsx + └── hooks/ # Реализуют form session +``` + +Родитель и вложенный модуль не владеют одной ответственностью одновременно. Родитель определяет общий результат, а вложенный модуль — отдельно сформулированную связную часть этого результата. + ## Граница владения Модуль определяет: @@ -30,7 +50,7 @@ - создание и очистку долгоживущих ресурсов; - устройство внутренней реализации. -Компонент, хук, провайдер, хранилище, сервис или маршрут могут выполнять часть этой работы технически. Владение остаётся у ближайшего модуля, пока часть кода не получает самостоятельную ответственность и собственную модульную границу. +Внутренний файл может технически выполнять часть или всю эту работу. Архитектурное владение остаётся у модуля и не переносится в компонент, Provider, store или другой механизм реализации. ## Публичный API @@ -54,7 +74,7 @@ |---|---| | `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением | | `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере | -| `browser` | Код только для браузера, подключаемый динамически через границу с отключённым SSR | +| `browser` | Browser-only и динамически подключаемый клиентский код; фасет всегда импортируется динамически с отключённым SSR | | `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения | `index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде. @@ -74,11 +94,9 @@ auth/ Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль. -## Зависимости между модулями +## Зависимости -Зависимость связывает владельца исходного кода с владельцем импортируемого кода. Импорт внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей. - -При пересечении модульной границы код использует только публичный фасет целевого модуля: +Модули одного слоя могут зависеть друг от друга. При пересечении любой модульной границы код использует публичный фасет целевого модуля, а общий граф модулей должен оставаться ацикличным. ```ts // Допустимо @@ -88,56 +106,115 @@ import { Button } from '@/ui/button' import { Button } from '@/ui/button/button' ``` -Для каждой связи выполняются три условия: +Направления между слоями, same-layer импорты, роль групп и требования к lint-проверке описаны в разделе [Зависимости](./dependencies.md). -1. Направление разрешено [матрицей слоёв](./layers.md#направление-зависимостей). -2. Целевой модуль используется только через публичный API. -3. Общий граф модулей остаётся ацикличным. +## Корень модуля -Модули одного слоя могут зависеть друг от друга, если соблюдены публичные границы и не возникает цикл. SLM не требует обязательного посредника или отдельного механизма dependency injection для любой межмодульной связи. +Корень модуля не используется как плоский каталог реализации. В нём находятся: -## Компоненты +- объявленные публичные фасеты; +- не более одного опционального главного implementation- или assembly-файла. -Компонент является сущностью фреймворка, реализующей часть интерфейса родительского модуля. Он не образует самостоятельного владельца только из-за наличия отдельного файла, каталога, props, локального состояния, доступа к данным или кода жизненного цикла. - -Зависимости, состояние и ресурсы компонента относятся к родительскому модулю. Если UI-часть получает самостоятельную ответственность, публичный API, собственные зависимости или область жизни, ей требуется модульная граница. - -Модуль может состоять из одного корневого компонента. В этом случае компонент реализует интерфейс, а модуль остаётся владельцем ответственности. - -Компонент может быть оформлен отдельным каталогом и иметь локальный `index.ts`: +Главный файл непосредственно реализует или собирает ответственность модуля и однозначно соответствует ей по имени и роли. Возможные примеры: ```text -button-submit/ -├── button-submit.tsx -├── styles/ -│ └── button-submit.module.css -├── types/ -│ └── button-submit.types.ts -└── index.ts +header/header.tsx +footer/footer.tsx +auth-guard/auth-guard.provider.tsx ``` -Такой `index.ts` является внутренней точкой входа компонента. Он упрощает импорты внутри модуля, но не создаёт SLM public API, нового владельца или узел графа зависимостей. +Архитектурным владельцем остаётся модуль, а главный файл является основной реализацией или точкой её сборки. Если проблемно доказать, что файл является главным, его не выносят в корень. Вся остальная реализация размещается в [сегментах](./segments.md). -| `index.ts` компонента | Публичный фасет модуля | -|---|---| -| Действует внутри ближайшего модуля | Действует при пересечении модульной границы | -| Экспортирует локальную реализацию и типы | Экспортирует контракт владельца | -| Не создаёт архитектурную границу | Представляет архитектурную границу | -| Не делает компонент модулем | Принадлежит уже определённому модулю | +Главный framework-файл не обязан экспортироваться через `index`. Модуль открывает его через минимально подходящий фасет среды выполнения: -Если компонент входит в публичный контракт владельца, корневой фасет модуля явно реэкспортирует его локальную точку входа. Внешний код по-прежнему импортирует модуль, а не внутренний путь компонента. +```text +theme/ +├── index.ts # Универсальные публичные типы +├── client.ts # Экспортирует ThemeProvider и useTheme +├── theme.provider.tsx # Главная framework-реализация +├── context/ +│ └── theme-context.ts +├── hooks/ +│ └── use-theme.ts +├── types/ +└── styles/ +``` + +`ThemeProvider` может хранить состояние, синхронизироваться с платформой, предоставлять Context и очищать ресурсы при размонтировании. Он технически реализует ответственность, но её смысл, контракт, зависимости и область жизни определяет модуль `theme`. + +## Framework-компоненты + +SLM не вводит собственный вид компонента. Модуль может содержать любые framework-компоненты: визуальные элементы, Providers, Guards, Error Boundaries и другие сущности, которые используемый фреймворк считает компонентами. + +Помимо опционального главного framework-файла в корне, остальные framework-компоненты организуются на одном внутреннем уровне относительно модуля. Они могут быть одиночными файлами или каталогами с локальными стилями, типами, hooks, тестами и внутренним `index.ts`, но каталог компонентной единицы не содержит другие компонентные единицы или вложенные модули. + +```text +main-layout/ # Модуль +├── index.ts # Публичный фасет +├── main-layout.tsx # Главная реализация +├── components/ # Сегмент +│ ├── header/ +│ │ ├── index.ts # Локальная точка входа +│ │ ├── header.tsx +│ │ ├── styles/ +│ │ └── types/ +│ ├── navigation-item.tsx +│ └── footer.tsx +└── providers/ # Сегмент + └── layout-state/ + ├── layout-state.provider.tsx + ├── hooks/ + └── types/ +``` + +`Header` может рендерить `NavigationItem`, но их файловые области остаются соседними относительно `main-layout`. Ограничение относится к организации файлов, а не к runtime-дереву фреймворка. + +Локальный `index.ts` компонентной единицы упрощает импорты внутри модуля, но не создаёт публичный API, владельца или узел графа зависимостей. Если компонент нужен внешнему потребителю, фасет модуля явно реэкспортирует его, а внешний код по-прежнему импортирует модуль. + +Названия `components`, `providers`, `styles`, `types` и `hooks` являются примерами локального стайлгайда, а не обязательными путями SLM. ## Вложенные модули -Вложенный модуль — самостоятельный владелец, физически размещённый внутри родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется общим правилам модулей. +Вложенный модуль — обычный модуль, физически размещённый внутри родительского. Он владеет отдельно сформулированной связной подответственностью, имеет публичный API, собственные зависимости и область жизни и подчиняется всем правилам модулей. + +```text +checkout/ # Родительский модуль +├── index.ts +├── checkout.tsx # Главная реализация checkout +├── components/ +│ ├── order-summary.tsx +│ └── submit-order.tsx +└── modules/ + └── form-session/ # Вложенный модуль + ├── index.ts # Универсальные публичные типы + ├── client.ts # Экспортирует Provider и hook + ├── form-session.provider.tsx # Главная реализация form session + ├── hooks/ + │ └── use-form-session.ts + └── types/ +``` + +Framework-компонент не превращается в архитектурную сущность. Если окружающему его коду требуется самостоятельная ответственность, вокруг кода создаётся вложенный модуль, а компонент остаётся его обычной framework-реализацией. Код родительского модуля использует вложенный модуль через его публичный API. Код за пределами родительской границы не импортирует вложенный модуль напрямую и получает необходимые возможности через API родителя. -Если вложенный модуль становится нужен нескольким внешним владельцам, его можно перенести в минимальную общую область. Сам факт повторного использования внутри родителя переноса не требует. +Вложенный модуль может рекурсивно содержать другие вложенные модули. Именно модульная граница, а не каталог компонента, создаёт каждый следующий структурный уровень. Если вложенный модуль становится нужен нескольким внешним владельцам, его переносят в минимальную общую область. + +## Колокация и рост + +Код создаётся в самой узкой допустимой области внутри своего архитектурного владельца: + +1. Вспомогательный код, нужный одной компонентной единице, колоцируется в её файле или каталоге. +2. Каждая выделенная framework-компонентная единица размещается на общем внутреннем уровне модуля, даже если используется только одним другим компонентом. +3. Код, нужный нескольким внутренним единицам, поднимается в их ближайший общий сегмент. +4. При появлении самостоятельной подответственности вокруг кода создаётся вложенный модуль. +5. Вложенный модуль переносится в общую область, когда прямой доступ к нему требуется внешним владельцам. + +Расширение числа потребителей меняет колокацию внутри владельца. Появление самостоятельной ответственности меняет модульный граф. ## Состояние и жизненный цикл -Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в файл хранилища, провайдер или контекст фреймворка. +Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в store, Provider или Context. Для каждого долгоживущего ресурса модуль-владелец определяет: @@ -147,18 +224,10 @@ button-submit/ - допустимое число экземпляров; - способ остановки, отмены или освобождения. -Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить компонент, провайдер или фреймворк, но ответственность за корректную границу остаётся у модуля. +Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить framework-компонент или сам фреймворк, но ответственность за корректную границу остаётся у модуля. Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем. -## Внутренняя организация - -После определения ответственности и публичной границы модуль может организовать реализацию [сегментами](./segments.md), компонентами и вложенными модулями. SLM не требует полного каркаса и не задаёт обязательный набор внутренних папок. - -Каждый модуль физически размещается в отдельной папке. Это делает логическую границу наблюдаемой для потребителей и автоматических проверок, но не заменяет решение о владельце. - -Наличие `index.ts` не используется как единственный признак модуля. Модульную границу определяют ответственность, владелец, публичные потребители и сопоставление путей, принятое проектом. - ## Связанные правила - [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004) @@ -166,8 +235,9 @@ button-submit/ - [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006) - [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011) - [`SLM-MODULE-R012`](../rules/registry.md#slm-module-r012) +- [`SLM-MODULE-R020`](../rules/registry.md#slm-module-r020) +- [`SLM-MODULE-R021`](../rules/registry.md#slm-module-r021) - [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005) -- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009) - [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010) - [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013) - [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016) diff --git a/docs/architecture/segments.md b/docs/architecture/segments.md index 635cc5f..40ea484 100644 --- a/docs/architecture/segments.md +++ b/docs/architecture/segments.md @@ -10,28 +10,27 @@ Слой → [Группа*] → Модуль → [Сегмент*] ``` -Группа классифицирует несколько модулей внутри слоя. Сегмент классифицирует код одного модуля. Ни группа, ни сегмент не являются владельцами. +Группа классифицирует модули внутри слоя. Сегмент классифицирует код одного модуля. Ни группа, ни сегмент не являются владельцами. -Все файлы, компоненты, состояние, зависимости и код жизненного цикла сегмента принадлежат родительскому модулю. Исключением является только вложенный модуль, который начинает собственную границу владения. +Все файлы, framework-компоненты, состояние, зависимости и lifecycle-код сегмента принадлежат ближайшему модулю. Исключением является только вложенный модуль, который начинает собственную границу владения. ## Назначение Сегмент используется, когда группировка внутренних файлов по назначению упрощает навигацию. Набор и названия сегментов определяет проект. -Возможная структура: - ```text profile/ -├── index.ts -├── profile.tsx -├── hooks/ # Возможный сегмент -├── services/ # Возможный сегмент -├── stores/ # Возможный сегмент -├── types/ # Возможный сегмент -└── ui/ # Возможный сегмент +├── index.ts # Публичный фасет +├── profile.tsx # Главная реализация +├── components/ # Возможный сегмент +├── hooks/ # Возможный сегмент +├── services/ # Возможный сегмент +├── stores/ # Возможный сегмент +├── types/ # Возможный сегмент +└── styles/ # Возможный сегмент ``` -Ни один из показанных сегментов не обязателен. Маленький модуль может хранить реализацию в корне без дополнительных каталогов. +Ни один сегмент не создаётся заранее. Модуль может обойтись без сегментов, если помимо публичных фасетов содержит только один главный implementation- или assembly-файл, однозначно выражающий его ответственность. Любая остальная реализация размещается в подходящих сегментах. Если главный файл нельзя определить уверенно, вся реализация остаётся в сегментах. Сегмент: @@ -41,52 +40,67 @@ profile/ - не является узлом графа зависимостей; - не импортируется внешним кодом как отдельная архитектурная сущность. -Локальный `index.ts` может использоваться во внутренней единице сегмента, например в каталоге компонента. Он не превращает эту единицу или сегмент в модульную границу. +Локальный `index.ts` может использоваться во внутренней единице сегмента. Он не превращает эту единицу или сегмент в модульную границу. -## Компоненты и вложенные модули +## Framework-компоненты -Сегмент может содержать компоненты и вспомогательные файлы родительского модуля. Компонент вправе иметь локальные `styles/`, `types/`, `tests/` и внутренний `index.ts`; всё это остаётся реализацией ближайшего модуля. +Framework-компоненты являются обычным внутренним кодом модуля. Они могут выполнять визуальные и невизуальные роли, включая Provider, Guard или Error Boundary, если используемый фреймворк считает соответствующую сущность компонентом. + +Помимо опционального главного framework-файла в корне, остальные компонентные единицы размещаются на одном внутреннем уровне относительно модуля. Каталог такой единицы может содержать локальные `styles`, `types`, `hooks`, `tests` и внутренний `index.ts`, но не содержит другие компонентные единицы или вложенные модули. ```text -header/ # Модуль -└── components/ # Сегмент - └── button-submit/ # Компонент - ├── button-submit.tsx - ├── styles/ - │ └── button-submit.module.css - ├── types/ - │ └── button-submit.types.ts - └── index.ts # Внутренняя точка входа +header/ # Модуль +├── index.ts # Публичный фасет +├── header.tsx # Главная реализация +└── components/ # Сегмент + ├── button-submit/ + │ ├── index.ts # Локальная точка входа + │ ├── button-submit.tsx + │ ├── styles/ + │ ├── types/ + │ └── hooks/ + └── icon.tsx # Соседняя компонентная единица ``` +`ButtonSubmit` может рендерить `Icon`, но их файловые области не вкладываются друг в друга. Ограничение относится к файловой структуре, а не к runtime-дереву. + Внешний код не импортирует `components/button-submit`. Если компонент нужен снаружи, модуль-владелец реэкспортирует его через собственный публичный фасет. -Сегмент также может содержать вложенные модули. В отличие от остальных файлов сегмента, каждый вложенный модуль сам владеет отдельной ответственностью и имеет публичный API и границу зависимостей. +## Вложенные модули + +Сегмент может содержать вложенные модули. В отличие от остальных файлов сегмента, каждый вложенный модуль владеет отдельно сформулированной подответственностью и имеет публичный API и границу зависимостей. ```text landing/ # Родительский модуль -└── parts/ # Сегмент +└── modules/ # Сегмент └── hero/ # Вложенный модуль - ├── hero.tsx - └── index.ts + ├── index.ts # Публичный фасет вложенного модуля + ├── hero.tsx # Главная реализация hero + └── modules/ # Допустимая модульная рекурсия + └── media/ + └── index.ts ``` -Имя `parts` является примером, а не обязательным соглашением SLM. +Компонентный каталог не содержит `components` или `modules`. Рекурсивная структурная вложенность допускается только через вложенные модули. Имена `components` и `modules` являются примерами локального стайлгайда, а не обязательными соглашениями SLM. -## Выбор границы +## Выбор размещения | Ситуация | Решение | |---|---| -| Код относится к существующему владельцу и группируется только по назначению | Сегмент | -| Части нужна собственная ответственность, API, зависимости, состояние или область жизни | Модуль | -| Несколько модулей слоя нужно классифицировать для навигации | Группа | -| Нескольким файлам не нужна отдельная группировка | Оставить в корне модуля | +| Код относится к существующему владельцу и группируется по назначению | Сегмент | +| Вспомогательный код нужен только одной компонентной единице | Колоцировать в её локальном каталоге | +| Выделена отдельная framework-компонентная единица | Разместить на общем внутреннем уровне модуля | +| Код нужен нескольким внутренним единицам модуля | Поднять в ближайший общий сегмент | +| Появилась самостоятельная связная подответственность | Создать вложенный модуль | +| Файл однозначно является главной реализацией или сборкой ответственности | Допустимо разместить в корне модуля | +| Файл не является главным или его роль неоднозначна | Разместить в подходящем сегменте | -Размер каталога и количество файлов не определяют выбор между сегментом и модулем. +Размер каталога и количество файлов не определяют модульную границу. Её создаёт только самостоятельная ответственность и назначение нового владельца. ## Связанные правила - [`SLM-SEGMENT-R008`](../rules/registry.md#slm-segment-r008) - [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004) -- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009) +- [`SLM-MODULE-R020`](../rules/registry.md#slm-module-r020) +- [`SLM-MODULE-R021`](../rules/registry.md#slm-module-r021) - [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010) diff --git a/docs/reference/terminology.md b/docs/reference/terminology.md index d4472cc..7f9f53c 100644 --- a/docs/reference/terminology.md +++ b/docs/reference/terminology.md @@ -10,11 +10,11 @@ ### Ответственность -Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственный публичный API, зависимости, состояние или область жизни. +Результат или поведение приложения, за которое отвечает один модуль-владелец. Ответственность является самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни, а не только внутренняя роль в работе другого модуля. Наличие у framework-сущности props, импортов, локального состояния или lifecycle-кода само по себе не создаёт самостоятельную ответственность. ### Владелец -Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннюю реализацию. Место выполнения кода не переносит владение. +Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннюю реализацию. Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Место выполнения кода или вид framework-сущности не переносит владение. ## Структурные сущности @@ -34,13 +34,11 @@ Необязательная внутренняя часть одного модуля, группирующая его содержимое по назначению. Сегмент не является владельцем, публичным API или границей зависимостей. -### Компонент - -Сущность фреймворка, реализующая часть интерфейса родительского модуля. Зависимости, состояние и жизненный цикл компонента принадлежат этому модулю и сами по себе не создают нового владельца. Компонент может иметь внутренний `index.ts`, который не является публичным фасетом SLM. - ### Вложенный модуль -Обычный модуль, физически размещённый внутри родительского модуля. Он сам владеет отдельной ответственностью и имеет публичный API и границу зависимостей, но остаётся внутренней реализацией родителя для внешнего кода. +Обычный модуль, физически размещённый внутри родительского модуля. Он владеет отдельно сформулированной связной частью ответственности родителя, имеет публичный API и собственную границу зависимостей. Родитель владеет общим результатом, а вложенный модуль — выделенной подответственностью; одна и та же ответственность не получает двух владельцев. + +Для кода за пределами родительской границы вложенный модуль остаётся внутренней реализацией родителя. Внутри вложенного модуля снова действуют все правила обычного модуля, поэтому рекурсивная структурная вложенность создаётся только модульными границами. ## Публичная граница @@ -62,7 +60,7 @@ Направленная статическая связь между архитектурными границами внутри одного SLM root. Обычный импорт, импорт типа (`import type`) и реэкспорт одинаково создают архитектурную зависимость. -Зависимость внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей. +Зависимость внутреннего файла или сегмента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей. ### Нормативная матрица слоёв diff --git a/docs/reference/validation.md b/docs/reference/validation.md index 6af6ce0..a06e258 100644 --- a/docs/reference/validation.md +++ b/docs/reference/validation.md @@ -28,14 +28,18 @@ На ревью проверяется смысл кода, который нельзя надёжно вывести из файловой системы: -- одна ли связная ответственность находится внутри модуля; -- есть ли у каждой самостоятельной ответственности ровно один владелец; +- одна ли связная ответственность находится внутри модульной границы; +- есть ли у каждой самостоятельной ответственности ровно один ближайший владелец; - соответствует ли ответственность роли выбранного слоя; -- не стали ли группа, сегмент или компонент скрытыми владельцами; +- владеет ли вложенный модуль отдельно сформулированной подответственностью; +- не стали ли группа или сегмент скрытыми владельцами; +- реализует ли внутренний код ответственность ближайшего модуля; +- не принимаются ли props, Context, локальное состояние или lifecycle-код за достаточное основание для новой модульной границы; +- является ли главный файл в корне однозначной реализацией или сборкой ответственности; +- не лежат ли прочие файлы реализации в корне вместо подходящих сегментов; - нужен ли каждый экспорт реальному внешнему потребителю; - не раскрывает ли публичный API изменяемые внутренние механизмы; -- определены ли владелец, область жизни, число экземпляров и очистка каждого ресурса; -- не переносится ли владение из-за места вызова, провайдера фреймворка или точки маршрута. +- определены ли владелец, область жизни, число экземпляров и очистка каждого ресурса. Окончательные смысловые требования имеют класс `R` в [реестре правил](../rules/registry.md). @@ -43,14 +47,14 @@ Проект сопоставляет физические пути с SLM root, слоями, группами, модулями, вложенными модулями, сегментами, фасетами, точками входа `app` и ресурсами `shared`. Сопоставление задаётся локальной конфигурацией и не меняет смысл сущностей. -Наличие `index.ts` само по себе не объявляет модуль. Проверка отличает объявленные корни модулей и их публичные фасеты от локальных точек входа компонентов и других внутренних единиц. +Наличие `index.ts` само по себе не объявляет модуль. Проверка отличает объявленные корни модулей и их публичные фасеты от локальных точек входа и других внутренних единиц. Автоматически проверяются: - допустимое направление импортов по матрице слоёв; - отдельная папка каждого модуля; - доступ к чужому модулю только через объявленные фасеты; -- отсутствие циклов между модулями; +- отсутствие циклов в свёрнутом модульном графе; - отсутствие прямого внешнего доступа к вложенным модулям; - допустимый транзитивный граф исполняемых импортов каждого фасета среды выполнения; - динамическое подключение `browser`-фасета с отключённым SSR. @@ -61,13 +65,26 @@ Для каждого внешнего импорта определяется: -1. Модуль-владелец исходного файла. -2. Модуль-владелец целевого файла. +1. Ближайший модуль-владелец исходного файла. +2. Ближайший модуль-владелец целевого файла. 3. Слои исходного и целевого владельцев. 4. Публичный фасет, через который выполнен импорт. -5. Отсутствие цикла после добавления связи. +5. Отсутствие цикла после добавления межмодульного ребра. -Импорты типов (`import type`) и реэкспорты проверяются как архитектурные связи. Относительные импорты внутри одного модуля не пересекают модульную границу. +Импорты типов (`import type`) и реэкспорты проверяются как архитектурные связи. Импорты файлов одного модуля сворачиваются и не создают межмодульного ребра. Вложенный модуль считается отдельным узлом. + +File-level проверка не заменяет модульную: два модуля могут зависеть друг от друга через разные файлы без замкнутого пути между конкретными файлами. Полный алгоритм описан в разделе [Зависимости](../architecture/dependencies.md#запрет-циклов). + +## Проверка внутренней структуры + +Ревью или дополнительный project lint подтверждают: + +- помимо опционального главного framework-файла, остальные компонентные единицы размещены на одном внутреннем уровне модуля; +- их локальные каталоги не содержат другие компонентные единицы или вложенные модули; +- runtime-дерево компонентов не используется как файловая иерархия; +- рекурсивная структурная вложенность проходит только через вложенные модули; +- в корне модуля находятся только фасеты и опциональный главный implementation- или assembly-файл; +- остальная реализация организована сегментами. ## Проверка фасетов @@ -78,6 +95,7 @@ - `index` не достигает `client`, `browser` или `server`; - `client` не достигает `browser` или `server`; - `browser` и `server` не достигают друг друга; +- `browser` экспортирует только browser-only или предназначенный для динамического подключения клиентский код; - `browser` доступен только через поддерживаемую динамическую границу без SSR; - специализированный фасет существует ради реального потребителя; - один исполняемый экспорт не дублируется между фасетами. @@ -88,11 +106,15 @@ Изменение соответствует SLM, когда одновременно выполнены условия: -- ответственность и единственный владелец определены; +- ответственность и единственный ближайший владелец определены; - роль слоя соответствует ответственности; - публичный API минимален и используется всеми внешними потребителями; -- зависимости разрешены и не образуют циклов; +- зависимости разрешены и не образуют модульных циклов; - группа и сегменты не подменяют модульную границу; +- вложенные модули владеют отдельными подответственностями; +- внутренний код реализует ответственность ближайшего модуля; +- framework-компоненты имеют одноуровневую файловую организацию с единственным допустимым исключением для главного файла в корне; +- корень и сегменты соответствуют своим назначениям; - состояние и ресурсы имеют владельца и корректную область жизни; - физическая структура однозначно выражает принятое решение; - применимые автоматические проверки и архитектурное ревью пройдены. diff --git a/docs/rules/README.md b/docs/rules/README.md index 9886f7a..6339114 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -41,11 +41,10 @@ SLM-{group}-{class}{number} | Код | Предмет | |---|---| | `LAYER` | Роль слоя и направление зависимостей | -| `MODULE` | Ответственность, владение и публичная граница модуля | +| `MODULE` | Ответственность, владение, публичная граница и внутренняя структура модуля | | `DEPENDENCY` | Граф зависимостей модулей | | `GROUP` | Навигационная группировка модулей | | `SEGMENT` | Внутренняя организация модуля | -| `COMPONENT` | Принадлежность компонента модулю | | `NESTED_MODULE` | Доступ к вложенному модулю | | `LIFECYCLE` | Владение долгоживущими ресурсами | | `ENVIRONMENT` | Совместимость публичных фасетов со средами выполнения | diff --git a/docs/rules/registry.md b/docs/rules/registry.md index 1a22983..0aeec1c 100644 --- a/docs/rules/registry.md +++ b/docs/rules/registry.md @@ -40,13 +40,13 @@ > **Ответственность модуля** > -> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях. +> Одна модульная граница содержит только код, который модуль выполняет сам для обеспечения одного результата или поведения; код, отвечающий за другой результат, принадлежит другой модульной границе. ### SLM-MODULE-R011 > **Владелец ответственности** > -> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`. +> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одной ближайшей модульной границе; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`. ### SLM-MODULE-R012 @@ -54,6 +54,18 @@ > > Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми. +### SLM-MODULE-R020 + +> **Глубина framework-компонентов** +> +> Помимо опционального главного framework-файла в корне модуля, остальные framework-компоненты, в том числе выполняющие роли Provider, Guard или Error Boundary, размещаются на одном внутреннем уровне относительно модуля; каталог такой единицы может содержать локальный вспомогательный код, но не содержит другие компонентные единицы или вложенные модули. + +### SLM-MODULE-R021 + +> **Семантика корня модуля** +> +> Помимо объявленных публичных фасетов, в корне модуля допускается только один опциональный главный implementation- или assembly-файл, который однозначно отражает, непосредственно реализует или собирает ответственность модуля; вся остальная реализация размещается в сегментах. + ## Зависимости между модулями ### SLM-DEPENDENCY-A005 @@ -78,14 +90,6 @@ > > Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или границы зависимостей. -## Ответственность компонентов - -### SLM-COMPONENT-R009 - -> **Ответственность компонента** -> -> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу. - ## Границы вложенных модулей ### SLM-NESTED_MODULE-A010 @@ -120,7 +124,7 @@ > **Браузерный фасет** > -> Фасет `browser` экспортирует только browser-only код, а потребители импортируют его только динамически с отключённым SSR. +> Фасет `browser` экспортирует browser-only код и клиентский код, предназначенный для динамического подключения без SSR; потребители всегда импортируют его динамически с отключённым SSR. ### SLM-ENVIRONMENT-R019 diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs index cbc40ee..a89d2e0 100644 --- a/scripts/check-site.mjs +++ b/scripts/check-site.mjs @@ -13,6 +13,8 @@ const ruleRegistries = [ const expectedPages = [ '404.html', 'index.html', + 'architecture/dependencies.html', + 'architecture/groups.html', 'architecture/index.html', 'architecture/layers.html', 'architecture/modules.html', @@ -138,7 +140,7 @@ if (!notFoundHtml.includes('Такой страницы нет')) { } const homeHtml = htmlByPage.get('index.html') -if (!homeHtml.includes('Архитектура владения ответственностями')) { +if (!homeHtml.includes('Архитектура фронтенд-приложений')) { throw new Error('Home page does not render the documentation-owned hero') } @@ -163,8 +165,6 @@ for (const forbiddenRoute of [ '/ru/', '/specification/', '/architecture/domains', - '/architecture/dependencies', - '/architecture/groups', '/architecture/components', '/architecture/nested-modules', '/architecture/lifecycle', diff --git a/site/.vitepress/config.mts b/site/.vitepress/config.mts index d8bdebd..e127c31 100644 --- a/site/.vitepress/config.mts +++ b/site/.vitepress/config.mts @@ -10,9 +10,11 @@ const documentationSidebar = [ text: 'Архитектурная модель', items: [ { text: 'Владение и структура', link: '/architecture/' }, - { text: 'Слои и группы', link: '/architecture/layers' }, + { text: 'Слои', link: '/architecture/layers' }, + { text: 'Группы', link: '/architecture/groups' }, { text: 'Модули и границы', link: '/architecture/modules' }, { text: 'Сегменты', link: '/architecture/segments' }, + { text: 'Зависимости', link: '/architecture/dependencies' }, ], }, { diff --git a/site/.vitepress/theme/style.css b/site/.vitepress/theme/style.css index 2faafe9..29e0ced 100644 --- a/site/.vitepress/theme/style.css +++ b/site/.vitepress/theme/style.css @@ -49,10 +49,16 @@ html { letter-spacing: -0.045em; } +.VPHero .text { + max-width: 820px; + font-size: clamp(2.6rem, 5vw, 4.4rem); + line-height: 1.02; +} + .VPHero .tagline { - max-width: 660px; - font-size: 19px; - line-height: 1.65; + max-width: 760px; + font-size: 20px; + line-height: 1.6; } .VPFeature { @@ -61,6 +67,113 @@ html { backdrop-filter: blur(8px); } +.VPFeature .title { + font-size: 17px; + letter-spacing: -0.02em; +} + +.VPFeature .details { + line-height: 1.65; +} + +.VPContent.is-home .vp-doc { + max-width: 1120px; + padding-bottom: 112px; +} + +.VPContent.is-home .vp-doc > h2 { + max-width: 860px; + margin-top: 96px; + padding-top: 0; + border-top: 0; + font-size: clamp(2rem, 4vw, 3.25rem); + line-height: 1.08; + letter-spacing: -0.045em; +} + +.VPContent.is-home .vp-doc > h2:first-child { + margin-top: 48px; +} + +.VPContent.is-home .vp-doc > p { + max-width: 820px; + font-size: 17px; + line-height: 1.78; +} + +.VPContent.is-home .vp-doc > blockquote { + max-width: 920px; + margin: 40px 0; + padding: 28px 32px; + border: 1px solid color-mix(in srgb, var(--vp-c-brand-2) 36%, var(--vp-c-divider)); + border-left: 4px solid var(--vp-c-brand-2); + border-radius: 0 14px 14px 0; + background: color-mix(in srgb, var(--vp-c-brand-soft) 62%, var(--vp-c-bg-soft)); + color: var(--vp-c-text-1); + font-size: clamp(1.25rem, 2.5vw, 1.75rem); + line-height: 1.45; + letter-spacing: -0.025em; +} + +.VPContent.is-home .vp-doc > blockquote p { + margin: 0; +} + +.VPContent.is-home .vp-doc table { + display: table; + width: 100%; + margin: 36px 0; + border-collapse: separate; + border-spacing: 0; + overflow: hidden; + border: 1px solid var(--vp-c-divider); + border-radius: 14px; +} + +.VPContent.is-home .vp-doc th, +.VPContent.is-home .vp-doc td { + width: 50%; + padding: 18px 20px; + border: 0; + border-bottom: 1px solid var(--vp-c-divider); + vertical-align: top; +} + +.VPContent.is-home .vp-doc th { + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-1); + font-size: 14px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.VPContent.is-home .vp-doc tr:last-child td { + border-bottom: 0; +} + +.VPContent.is-home .vp-doc th + th, +.VPContent.is-home .vp-doc td + td { + border-left: 1px solid var(--vp-c-divider); +} + +.VPContent.is-home .vp-doc ol, +.VPContent.is-home .vp-doc ul { + max-width: 860px; +} + +.VPContent.is-home .vp-doc li { + margin: 10px 0; + line-height: 1.7; +} + +.VPContent.is-home .vp-doc div[class*='language-'] { + max-width: 920px; + margin: 36px 0; + border: 1px solid var(--vp-c-divider); + border-radius: 14px; + box-shadow: 0 24px 64px rgba(41, 37, 36, 0.08); +} + .vp-doc h1, .vp-doc h2, .vp-doc h3 { @@ -128,10 +241,37 @@ html { } @media (max-width: 640px) { + .VPHero .text { + font-size: clamp(2.15rem, 11vw, 3rem); + line-height: 1.06; + } + .VPHero .tagline { font-size: 16px; } + .VPContent.is-home .vp-doc { + padding-bottom: 72px; + } + + .VPContent.is-home .vp-doc > h2 { + margin-top: 72px; + } + + .VPContent.is-home .vp-doc > blockquote { + margin-inline: 0; + padding: 22px 20px; + } + + .VPContent.is-home .vp-doc table { + font-size: 14px; + } + + .VPContent.is-home .vp-doc th, + .VPContent.is-home .vp-doc td { + padding: 14px 12px; + } + .vp-doc h3[id^='slm-'] + blockquote { margin-inline: -8px; padding: 42px 14px 14px; diff --git a/site/README.md b/site/README.md index 8e15d3a..a190c9f 100644 --- a/site/README.md +++ b/site/README.md @@ -8,9 +8,11 @@ - `/` - главная страница; - `/architecture/` - владение и структурная модель; -- `/architecture/layers` - слои и группы; +- `/architecture/layers` - слои; +- `/architecture/groups` - группы модулей; - `/architecture/modules` - модули и публичные границы; - `/architecture/segments` - внутренняя организация модулей; +- `/architecture/dependencies` - направления импортов и модульный граф; - `/rules/` - устройство правил; - `/rules/registry` - единый реестр правил; - `/reference/terminology` - нормативные определения;