chore: sync

This commit is contained in:
2026-08-10 12:37:32 +03:00
parent af155fff0a
commit 1ba664f445
15 changed files with 684 additions and 238 deletions

View File

@@ -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).

View File

@@ -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)

View File

@@ -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)

View File

@@ -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). Группа не владеет кодом и не влияет на допустимость зависимостей.
## Немодульные исключения

View File

@@ -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)

View File

@@ -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)