From ef9dfcff3cf6dcfcebd6c92de070265ba6bc5dfa Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Mon, 10 Aug 2026 15:41:19 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20=D0=9F=D0=B5=D1=80=D0=B5=D1=81=D0=BE?= =?UTF-8?q?=D0=B7=D0=B4=D0=B0=D1=82=D1=8C=20=D1=81=D0=BA=D0=B8=D0=BB=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/skill.yml | 2 - .opencode/agents/slm-critic.md | 36 +- .opencode/commands/slm-review.md | 4 +- README.md | 7 +- scripts/lib/skill-bundle.mjs | 111 ++- skills/slm-design/SKILL.md | 778 ++++-------------- skills/slm-design/reference/docs/README.md | 108 +++ .../reference/docs/architecture/README.md | 111 +++ .../docs/architecture/dependencies.md | 138 ++++ .../reference/docs/architecture/domains.md | 220 +++++ .../reference/docs/architecture/groups.md | 84 ++ .../reference/docs/architecture/layers.md | 112 +++ .../reference/docs/architecture/modules.md | 253 ++++++ .../reference/docs/architecture/segments.md | 106 +++ .../reference/docs/reference/terminology.md | 143 ++++ .../reference/docs/reference/validation.md | 167 ++++ .../slm-design/reference/docs/rules/README.md | 62 ++ .../reference/docs/rules/registry.md | 165 ++++ skills/slm-design/reference/draft/README.md | 15 - .../reference/draft/level-1/README.md | 53 -- .../reference/draft/level-1/components.md | 65 -- .../reference/draft/level-1/dependencies.md | 59 -- .../reference/draft/level-1/domains.md | 61 -- .../reference/draft/level-1/groups.md | 25 - .../reference/draft/level-1/layers.md | 95 --- .../reference/draft/level-1/lifecycle.md | 31 - .../reference/draft/level-1/modules.md | 43 - .../reference/draft/level-1/nested-modules.md | 30 - .../reference/draft/level-1/segments.md | 29 - .../reference/draft/level-1/terminology.md | 143 ---- .../reference/draft/level-1/validation.md | 34 - .../reference/draft/level-2/README.md | 155 ---- .../reference/draft/level-2/dependencies.md | 152 ---- .../reference/draft/level-2/domains/README.md | 31 - .../draft/level-2/domains/assemblies.md | 264 ------ .../draft/level-2/domains/auth-example.md | 204 ----- .../draft/level-2/domains/domain-api.md | 260 ------ .../draft/level-2/domains/domain-package.md | 119 --- .../level-2/domains/factory-ports-adapters.md | 235 ------ .../level-2/domains/framework-bindings.md | 211 ----- .../draft/level-2/domains/open-questions.md | 75 -- .../draft/level-2/domains/realtime.md | 217 ----- .../draft/level-2/domains/state-cache.md | 172 ---- .../draft/level-2/domains/testing.md | 189 ----- .../reference/draft/level-2/terminology.md | 233 ------ .../reference/draft/level-2/validation.md | 126 --- .../reference/draft/rules/README.md | 130 --- .../reference/draft/rules/level-1.md | 110 --- .../reference/draft/rules/level-2.md | 209 ----- src-skills/slm-design/SKILL.md | 778 ++++-------------- src-skills/slm-design/skill.config.mjs | 30 +- tests/skill-bundle.test.mjs | 161 +++- 52 files changed, 2317 insertions(+), 5034 deletions(-) create mode 100644 skills/slm-design/reference/docs/README.md create mode 100644 skills/slm-design/reference/docs/architecture/README.md create mode 100644 skills/slm-design/reference/docs/architecture/dependencies.md create mode 100644 skills/slm-design/reference/docs/architecture/domains.md create mode 100644 skills/slm-design/reference/docs/architecture/groups.md create mode 100644 skills/slm-design/reference/docs/architecture/layers.md create mode 100644 skills/slm-design/reference/docs/architecture/modules.md create mode 100644 skills/slm-design/reference/docs/architecture/segments.md create mode 100644 skills/slm-design/reference/docs/reference/terminology.md create mode 100644 skills/slm-design/reference/docs/reference/validation.md create mode 100644 skills/slm-design/reference/docs/rules/README.md create mode 100644 skills/slm-design/reference/docs/rules/registry.md delete mode 100644 skills/slm-design/reference/draft/README.md delete mode 100644 skills/slm-design/reference/draft/level-1/README.md delete mode 100644 skills/slm-design/reference/draft/level-1/components.md delete mode 100644 skills/slm-design/reference/draft/level-1/dependencies.md delete mode 100644 skills/slm-design/reference/draft/level-1/domains.md delete mode 100644 skills/slm-design/reference/draft/level-1/groups.md delete mode 100644 skills/slm-design/reference/draft/level-1/layers.md delete mode 100644 skills/slm-design/reference/draft/level-1/lifecycle.md delete mode 100644 skills/slm-design/reference/draft/level-1/modules.md delete mode 100644 skills/slm-design/reference/draft/level-1/nested-modules.md delete mode 100644 skills/slm-design/reference/draft/level-1/segments.md delete mode 100644 skills/slm-design/reference/draft/level-1/terminology.md delete mode 100644 skills/slm-design/reference/draft/level-1/validation.md delete mode 100644 skills/slm-design/reference/draft/level-2/README.md delete mode 100644 skills/slm-design/reference/draft/level-2/dependencies.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/README.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/assemblies.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/auth-example.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/domain-api.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/domain-package.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/factory-ports-adapters.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/framework-bindings.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/open-questions.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/realtime.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/state-cache.md delete mode 100644 skills/slm-design/reference/draft/level-2/domains/testing.md delete mode 100644 skills/slm-design/reference/draft/level-2/terminology.md delete mode 100644 skills/slm-design/reference/draft/level-2/validation.md delete mode 100644 skills/slm-design/reference/draft/rules/README.md delete mode 100644 skills/slm-design/reference/draft/rules/level-1.md delete mode 100644 skills/slm-design/reference/draft/rules/level-2.md diff --git a/.github/workflows/skill.yml b/.github/workflows/skill.yml index 7d58179..e01154e 100644 --- a/.github/workflows/skill.yml +++ b/.github/workflows/skill.yml @@ -5,7 +5,6 @@ on: branches: [master] paths: - '.github/workflows/skill.yml' - - 'DRAFT/**' - 'docs/**' - 'src-skills/**' - 'skills/**' @@ -21,7 +20,6 @@ on: pull_request: paths: - '.github/workflows/skill.yml' - - 'DRAFT/**' - 'docs/**' - 'src-skills/**' - 'skills/**' diff --git a/.opencode/agents/slm-critic.md b/.opencode/agents/slm-critic.md index a6c3d2d..0ae7714 100644 --- a/.opencode/agents/slm-critic.md +++ b/.opencode/agents/slm-critic.md @@ -1,5 +1,5 @@ --- -description: Проводит независимый критический аудит архитектуры SLM в DRAFT, ищет противоречия, неработающие границы и непроверяемые правила. +description: Проводит независимый критический аудит актуальной документации SLM, ищет противоречия, неработающие границы и непроверяемые правила. mode: subagent color: warning temperature: 0.1 @@ -16,17 +16,16 @@ permission: ## Область проверки -Всегда читай актуальное состояние `DRAFT` целиком, включая: +Всегда читай актуальное состояние `docs` целиком, включая: -- корневые `README.md` и `index.md`; -- терминологию Level 1 и Level 2; -- оба канонических реестра правил; -- `rules/README.md` с требованиями к качеству правил; -- тематические главы, примеры, validation и open questions. +- корневой `README.md`; +- все главы `architecture`; +- `reference/terminology.md` и `reference/validation.md`; +- `rules/README.md` и единый `rules/registry.md`. -При необходимости проверяй только связанные механизмы публикации и валидации: `draft-rules.js`, `scripts/check-site.mjs` и `site/.vitepress/config.mts`. +При необходимости проверяй только связанные механизмы публикации и валидации: `scripts/check-docs.mjs`, `scripts/check-site.mjs` и `site/.vitepress/config.mts`. -Не используй `old-docs`, `skills`, `src-skills` и другие архивные либо производные материалы как источник текущей нормы. Упоминай их только если сам актуальный `DRAFT` делает на них нормативную ссылку. +Не используй архивные и производные материалы как источник текущей нормы. Упоминай их только если актуальный `docs` делает на них нормативную ссылку. Никогда не редактируй файлы. Не запускай команды, web-поиск или других агентов. Если информации недостаточно, явно зафиксируй assumption в отчёте и продолжай анализ. @@ -34,16 +33,15 @@ permission: 1. Онтологию владения: у каждой ответственности, API, состояния, ресурса и области жизни должен быть непротиворечивый владелец. 2. Согласованность определений, правил, рекомендаций, примеров и open questions. -3. Граф зависимостей: runtime, type-only, reexports, cross-domain, client/server/shared и транзитивные npm-зависимости. -4. Composition roots: browser navigation, request scope, несколько roots, lazy loading, partial assembly, rollback, disposal и порядок запуска. -5. Реальные среды: SPA, SSR, React Server Components, hydration, server actions, workers и тестовые scopes. -6. Инкрементальное применение L1/L2: hubs, leaves, dependency-connected migration radius и смешанные формы. -7. State/cache: предметное состояние, source cache, framework projection, optimistic updates, invalidation и concurrency. -8. Контракты: consumer-owned ports, DTO boundary, structured domain errors, cancellation, clock/random/id и lifecycle capabilities. -9. Границы `domains`, `infra`, `ui`, `compositions` на частых практических примерах, включая технические сервисы с интерфейсом. -10. Стоимость модели: boilerplate, eager graph creation, рост API/runtime-фасетов и применимость на графе из 20-30 доменов. -11. Реализуемость автоматической проверки: A-правило должно однозначно проверяться без знания предметного смысла, включая allowlists, package exports и transitive environment graph. -12. Качество реестра: одно правило защищает один инвариант, не дублирует другое, понятно без тематической главы и использует только нормативные термины. +3. Граф зависимостей: обычные импорты, type-only, реэкспорты, same-layer связи, вложенные модули и сворачивание файлов к владельцам. +4. Границы `domains`, `infra`, `ui`, `compositions`, `app` и `shared`, включая немодульные исключения. +5. Доменные контракты: DTO boundary, адаптация запросов и ответов, expected failures, чужие ошибки и runtime-идентификация. +6. Публичные фасеты и реальные среды: универсальный, client, browser-only, server-only и их транзитивный executable-граф. +7. Группы, сегменты, главный файл, framework-компоненты и рекурсивная вложенность. +8. Владение состоянием и lifecycle: создание, область жизни, число экземпляров, очистка и disposal. +9. Инкрементальное внедрение и стоимость модели: радиус миграции, boilerplate, рост API и применимость на крупном модульном графе. +10. Реализуемость автоматической проверки: правило класса `A` должно однозначно проверяться без знания предметного смысла. +11. Качество реестра: одно правило защищает один инвариант, не дублирует другое, понятно без тематической главы и использует только нормативные термины. Для каждого существенного утверждения ищи хотя бы один контрпример. Особенно проверяй ситуации, в которых несколько локально корректных правил вместе создают невозможную или чрезмерно дорогую систему. diff --git a/.opencode/commands/slm-review.md b/.opencode/commands/slm-review.md index 3a10a1b..3d73e47 100644 --- a/.opencode/commands/slm-review.md +++ b/.opencode/commands/slm-review.md @@ -4,7 +4,7 @@ agent: slm-critic subtask: true --- -Проведи полный критический аудит текущего `DRAFT` по своему контракту. +Проведи полный критический аудит текущего `docs` по своему контракту. Контекст текущего прохода: @@ -14,7 +14,7 @@ $ARGUMENTS Если контекст содержит `verify`, прежние идентификаторы замечаний или описание исправлений: -- всё равно перечитай актуальный `DRAFT` целиком; +- всё равно перечитай актуальный `docs` целиком; - сначала проверь закрытие переданных замечаний; - сохрани их идентификаторы; - отдели подтверждённые исправления от оставшихся проблем и регрессий; diff --git a/README.md b/README.md index 8cb3ae7..00e786c 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ ## Структура -- `docs/` - документация SLM и единственный источник содержимого сайта. +- `docs/` - документация SLM и единственный источник содержимого сайта и reference-материалов skill. - `site/` - VitePress-рендерер: конфигурация, тема и статические ресурсы без собственной копии документации. -- `DRAFT/` - прежний рабочий материал, не используемый сайтом. - `old-docs/` - архив legacy-документации, не используемый текущим skill. - `src-skills/` - исходники agent skills. - `skills/` - собранные skills для установки через `npx skills`. @@ -23,12 +22,12 @@ npm run check:site npm run check ``` -`npm run check:docs` проверяет правила и ссылки документации. `npm run check:site` собирает VitePress из `docs/` и проверяет опубликованные страницы. Skill пока собирается отдельно из `src-skills/slm-design/` и собственных reference-материалов; не редактируй собранные файлы вручную. +`npm run check:docs` проверяет правила и ссылки документации. `npm run check:site` собирает VitePress из `docs/` и проверяет опубликованные страницы. Skill собирается из `src-skills/slm-design/`, а всё дерево `docs/` рекурсивно включается в `skills/slm-design/reference/docs/`. Собранные файлы не редактируются вручную. ## Установка После публикации репозитория: ```bash -npx skills add /slm-design-new --skill slm-design +npx skills add gromlab-ru/slm-design --skill slm-design ``` diff --git a/scripts/lib/skill-bundle.mjs b/scripts/lib/skill-bundle.mjs index b0056d2..faebb3c 100644 --- a/scripts/lib/skill-bundle.mjs +++ b/scripts/lib/skill-bundle.mjs @@ -314,6 +314,39 @@ const collectMarkdownLinks = (tokens) => { return links.filter((link) => typeof link === 'string'); }; +const collectHeadingSectionLinks = (tokens, heading) => { + const sections = []; + + for (let index = 0; index < tokens.length; index += 1) { + if ( + tokens[index].type !== 'heading_open' + || tokens[index + 1]?.type !== 'inline' + || headingText(tokens[index + 1]) !== heading + ) { + continue; + } + + const level = Number.parseInt(tokens[index].tag.slice(1), 10); + let end = tokens.length; + + for (let cursor = index + 3; cursor < tokens.length; cursor += 1) { + if ( + tokens[cursor].type === 'heading_open' + && Number.parseInt(tokens[cursor].tag.slice(1), 10) <= level + ) { + end = cursor; + break; + } + } + + sections.push(tokens.slice(index + 3, end)); + } + + assert(sections.length === 1, `SKILL.md must contain exactly one ${heading} section.`); + + return collectMarkdownLinks(sections[0]); +}; + const parseMarkdown = (content, filePath) => { const environment = {}; const tokens = markdown.parse(content, environment); @@ -323,6 +356,51 @@ const parseMarkdown = (content, filePath) => { return { links: collectMarkdownLinks(tokens), tokens }; }; +const assertReferenceMap = ({ bundle, referenceMap, skillTokens }) => { + assert( + referenceMap && typeof referenceMap === 'object' && !Array.isArray(referenceMap), + 'Skill referenceMap must be an object.', + ); + assert( + typeof referenceMap.heading === 'string' && referenceMap.heading.trim() !== '', + 'Skill referenceMap.heading must be a non-empty string.', + ); + assert( + typeof referenceMap.target === 'string' && referenceMap.target.trim() !== '', + 'Skill referenceMap.target must be a non-empty string.', + ); + + const targetRoot = normalizeBundlePath(referenceMap.target); + const targetPrefix = `${targetRoot}/`; + const expectedPaths = [...bundle.keys()].filter((filePath) => filePath.startsWith(targetPrefix)); + + assert(expectedPaths.length > 0, `Skill reference map target is empty: ${targetRoot}`); + + const mappedPaths = new Set(); + const mapLinks = collectHeadingSectionLinks(skillTokens, referenceMap.heading); + + for (const rawTarget of mapLinks) { + const target = normalizeLinkTarget(rawTarget, 'SKILL.md'); + + if (!target || target.targetPath === '') { + continue; + } + + const resolvedPath = resolveBundleLink(bundle, 'SKILL.md', target.targetPath); + + if (resolvedPath.startsWith(targetPrefix)) { + mappedPaths.add(resolvedPath); + } + } + + const missingPaths = expectedPaths.filter((filePath) => !mappedPaths.has(filePath)); + + assert( + missingPaths.length === 0, + `Skill reference map is incomplete. Missing: ${missingPaths.join(', ')}`, + ); +}; + const assertMarkdownLinks = (bundle, filePath, content) => { const { links, tokens } = parseMarkdown(content, filePath); @@ -383,27 +461,38 @@ export const validateSkillBundle = ({ bundle, config, repoRoot }) => { : []; }), ); - const requiredHeadings = [ - 'Универсальный цикл решения', - 'Алгоритмы выбора', - 'Реализация', - 'Миграция Level 1 -> Level 2', - 'Архитектурное ревью', - 'Anti-patterns', - 'Stop conditions и адресные вопросы', - 'Когда открывать references', - ]; + const requiredHeadings = config.requiredHeadings; assert(frontmatter.name === config.name, `SKILL.md name must be ${config.name}.`); assert( typeof frontmatter.description === 'string' && frontmatter.description.trim() !== '', 'SKILL.md frontmatter must contain a non-empty string description.', ); + assert( + Array.isArray(requiredHeadings) && requiredHeadings.length > 0, + 'Skill requiredHeadings must be a non-empty array.', + ); - for (const heading of requiredHeadings) { + const normalizedRequiredHeadings = requiredHeadings.map((heading) => { + assert( + typeof heading === 'string' && heading.trim() !== '', + 'Skill requiredHeadings must contain non-empty strings.', + ); + + return heading.trim(); + }); + + assert( + new Set(normalizedRequiredHeadings).size === normalizedRequiredHeadings.length, + 'Skill requiredHeadings must not contain duplicates.', + ); + + for (const heading of normalizedRequiredHeadings) { assert(headings.has(heading), `SKILL.md is missing operational heading: ${heading}`); } + assertReferenceMap({ bundle, referenceMap: config.referenceMap, skillTokens }); + for (const [filePath, entry] of bundle) { const extension = path.posix.extname(filePath).toLowerCase(); diff --git a/skills/slm-design/SKILL.md b/skills/slm-design/SKILL.md index 8448204..6ee79ea 100644 --- a/skills/slm-design/SKILL.md +++ b/skills/slm-design/SKILL.md @@ -1,666 +1,246 @@ --- name: slm-design -description: "Используй при проектировании, реализации, миграции и архитектурном ревью по SLM: когда нужно определить SLM root, ответственность, владельца, слой, модульную границу, публичный API, форму домена Level 1 или Level 2, направление зависимостей, Domain API, ports, factories, adapters, default assembly, framework state/cache, realtime, errors или lifecycle. Не используй для локального coding, debugging, форматирования, framework- или SDK-механики, если архитектурная граница уже определена и не меняется." +description: "Экспертная работа с архитектурой SLM Design: проектирование, изменение, миграция и ревью слоёв app/compositions/domains/infra/ui/shared, модулей, доменов, публичных фасетов index/client/browser/server, групп, сегментов, вложенных модулей, зависимостей, состояния и lifecycle. Триггеры: SLM, Scoped Layered Module Design, SLM root, ответственность, владелец, модульная граница, domains vs compositions, доменный контракт, DTO, глубокий импорт, модульный цикл, архитектурное ревью. НЕ применять для обычного code style или локальной правки, не затрагивающей архитектурное решение." --- # SLM Design -## Рабочий контракт +Работай как архитектор SLM, а не как генератор заранее заданного дерева каталогов. Сначала устанавливай ответственность и владельца, затем выражай решение слоями, публичными границами и зависимостями. Пути, имена, framework-роли и размер кода не заменяют смысловое решение. -Применяй SLM как способ выполнить пользовательскую задачу, а не как тему для пересказа. После чтения этого файла ты должен уметь принять типовое архитектурное решение, реализовать его в запрошенном scope и проверить результат. Открывай references только для точной формулировки правила, редкого случая или неразрешённого вопроса. +## Источники истины -Работай в таком порядке: +Весь нормативный и поясняющий материал находится в `reference/docs`. Не воспроизводи правила по памяти, если от точности формулировки зависит решение. -1. Исследуй существующий код и локальные правила проекта. -2. Определи ответственность, владельца и минимальный scope. -3. Выбери слой, архитектурную сущность и форму домена. -4. Спроектируй публичную границу, зависимости, runtime-сборку и lifecycle. -5. До редактирования проверь решение по применимым правилам. -6. Если пользователь запросил реализацию, внеси изменения до завершённого состояния. -7. Проверь импорты, exports, граф, среды, lifecycle и тесты. -8. Кратко сообщи решение, сделанные изменения, проверки, assumptions и остаточные риски. +Материалы выполняют разные нормативные роли: -Не начинай широкое перемещение кода или генерацию каркаса до шагов 1-5. Не расширяй задачу до полного аудита SLM root, если локальное изменение можно корректно выполнить в меньшем scope. +1. [`rules/registry.md`](./reference/docs/rules/registry.md) содержит единственные точные блокирующие правила. +2. [`reference/terminology.md`](./reference/docs/reference/terminology.md) задаёт нормативный смысл терминов. +3. [`architecture/layers.md`](./reference/docs/architecture/layers.md) и [`architecture/dependencies.md`](./reference/docs/architecture/dependencies.md) задают роли слоёв и матрицу направлений. +4. Остальные главы `architecture` объясняют модель и способы проектирования. +5. [`reference/validation.md`](./reference/docs/reference/validation.md) задаёт процедуру проверки и критерий завершения. +6. [`README.md`](./reference/docs/README.md) даёт обзор, мотивацию и навигацию. -## Источники и обязательность +Не превращай рекомендацию или пример в правило. При обязательном вердикте указывай существующий код из реестра. Если требование относится к локальному стайлгайду, lint-конфигурации, framework или продуктовой policy, называй его проектным ограничением, а не правилом SLM. Если реестр, определение или нормативная матрица действительно противоречат друг другу, не выбирай победителя молча: останови обязательный вывод и зафиксируй противоречие документации. -Bundled DRAFT является рабочим источником истины для этой версии skill, но остаётся черновиком архитектуры. Используй источники в следующем порядке: +## Рабочий режим -1. [`rules/level-1.md`](./reference/draft/rules/level-1.md) и [`rules/level-2.md`](./reference/draft/rules/level-2.md) - единственный источник блокирующих правил. -2. [`level-1/terminology.md`](./reference/draft/level-1/terminology.md) и [`level-2/terminology.md`](./reference/draft/level-2/terminology.md) - обязательный смысл терминов. -3. README уровней - область применения, наследование и замены правил. -4. Тематические главы - объяснения, рекомендации и варианты проектирования. -5. Примеры - иллюстрации, а не обязательный каркас. -6. `open-questions.md` - нерешённые вопросы, а не требования. +1. Определи тип задачи: проектирование, реализация, изменение существующей границы, миграция, ревью или объяснение. +2. Исследуй фактический код, импорты и локальные архитектурные соглашения. Не делай вывод о сущности только по имени каталога. +3. Открой базовую модель и только относящиеся к задаче references по [карте файлов](#карта-файлов). +4. Зафиксируй наблюдаемые факты отдельно от архитектурных выводов. +5. Для проектирования, структурного изменения или миграции составь карточку решения по [`reference/validation.md`](./reference/docs/reference/validation.md#карточка-решения). +6. Для ревью используй review-checklists и реестр; для объяснения открывай только тематические references и не требуй карточку решения. +7. Если задача предполагает изменение кода, спроектируй минимальное решение, которое оставляет одного владельца, закрытый внутренний код и ацикличный модульный граф. +8. Редактируй код только для задачи реализации, изменения или миграции; ревью, проектирование и объяснение заверши соответствующим отчётом без самовольных правок. +9. Выполни применимые проверки: для ревью - доказательства findings, для реализации - смысловую, структурную и функциональную валидацию. +10. В результате сообщи принятое решение, затронутые границы, применимые правила, выполненные проверки и оставшиеся риски. -Если тематическая глава строже реестра, не создавай из неё новое блокирующее правило. Предложи более строгую форму как рекомендацию или уточни локальную policy, если выбор влияет на API, ownership, стоимость или runtime. Если этот файл расходится с реестром или нормативной терминологией, следуй bundled DRAFT и отметь дефект skill. +Если задача локальна и не меняет ответственность, публичный API, зависимость, состояние, lifecycle или физическую модульную границу, не инициируй архитектурный рефакторинг без отдельной причины. -При review различай: +## Сбор контекста -- **Rule violation** - нарушено применимое правило с существующим кодом SLM. -- **Definition mismatch** - реализация не соответствует нормативному смыслу сущности. -- **Architectural risk** - есть доказуемый риск, но нет блокирующего правила. -- **Decision required** - DRAFT или проект оставляет значимый выбор открытым. -- **Recommendation** - улучшение, которое не является обязательным. -- **Assumption** - обратимое рабочее допущение, явно указанное в результате. +До проектирования установи: -Не придумывай коды правил. Перед ссылкой на нарушение открой соответствующий реестр и проверь точную формулировку. +- границу SLM root и локальное сопоставление путей со слоями, группами, модулями, сегментами и фасетами; +- для каждого изменяемого файла - модульного владельца либо подтверждённый статус точки входа `app`, немодульного ресурса `shared` или кода вне SLM root; +- существующие публичные фасеты и реальные внешние импорты модуля; +- потребителей изменяемого поведения и среды, в которых они выполняются; +- межмодульные связи, включая `import type` и реэкспорты; +- владельца изменяемого состояния, источник истины и область жизни ресурсов; +- для продуктовых данных - доменный контракт, контракт источника и место адаптации; +- локальные lint-правила, alias-настройки, test/build-команды и дополнительные project policies. -## Минимальная рабочая модель +Проверяй историю или соседние модули только как свидетельство принятой локальной policy. Существующий код может быть legacy и не является доказательством нормы SLM. -### SLM root и уровни +Если проект не объявляет физическое сопоставление SLM-сущностей, выведи рабочую гипотезу из структуры и конфигурации и явно обозначь её. Гипотеза подходит для проектирования и адресных вопросов, но не доказывает нарушение класса `A`. До блокирующего структурного finding подтверди mapping конфигурацией проекта или однозначно установленными модульными границами. -SLM root - граница структурной архитектуры одного приложения. Сначала найди фактический root, path aliases, локальный стайлгайд и конфигурацию архитектурной проверки. Не считай `src` root автоматически и не выводи сущность только из имени папки. +## Проектирование -Level 1 действует во всём SLM root и задаёт слои, модули, публичные API, общий dependency DAG и владение lifecycle. +Двигайся от смысла к структуре: -Level 2 применяется отдельно к выбранной предметной области и заменяет только её доменный модуль пакетной формой. Остальные домены могут постоянно оставаться на Level 1. Одна предметная область имеет ровно одну итоговую форму. +1. Сформулируй один изменяемый результат или поведение без названий файлов, папок, библиотек и паттернов. +2. Определи, является ли поведение доменным сценарием. +3. Найди существующего владельца или обоснуй новую самостоятельную ответственность. +4. Зафиксируй, что владелец делает сам и какие готовые возможности получает от других модулей. +5. Назови реальных внешних потребителей. +6. Выбери слой по роли ответственности. +7. Спроектируй минимальный публичный API и только необходимые фасеты сред выполнения. +8. Построй impact map межмодульных рёбер и проверь публичные пути, матрицу слоёв и ацикличность. +9. Назначь владельца состоянию и каждому lifecycle-ресурсу. +10. Только после этого выбери папку модуля, главный файл, сегменты, группы или вложенные модули. -### Слои - -| Исходный слой | Может зависеть от | -|---|---| -| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` | -| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` | -| `domains` | `domains`, `infra`, `ui`, `shared` | -| `infra` | `infra`, `shared` | -| `ui` | `ui`, `shared` | -| `shared` | `shared` | - -Матрица не требует проходить через каждый промежуточный слой. Разрешённый импорт не переносит владение ответственностью. - -| Слой | Помещай сюда | -|---|---| -| `app` | Framework entry points: запуск, routes, преобразование внешнего input и подключение готовых API | -| `compositions` | Pages, layouts, screens, widgets, route outcomes и multi-domain UI | -| `domains` | Предметные модели, правила, сценарии и продуктовое состояние | -| `infra` | Универсальные технические capabilities без собственной предметной модели | -| `ui` | Универсальные UI-модули без зависимости от продуктовой композиции | -| `shared` | Детерминированный product-agnostic фундамент без I/O, mutable state и lifecycle | - -### Архитектурные сущности - -| Признак | Сущность | -|---|---| -| Самостоятельная ответственность со своим API, dependencies, state или lifecycle | Module | -| Только навигационно классифицирует modules и Groups, а в `domains` также domain packages | Group | -| Организует внутренности одного module | Segment | -| Framework UI entity, реализующая часть ответственности родителя | Component | -| Самостоятельный module, скрытый внутри parent module | Nested module | -| Framework bootstrap или route entry | Немодульная единица `app` | -| Малый deterministic product-agnostic файл без внутренней границы | Shared resource | - -Module является узлом dependency graph, размещается в отдельной папке и имеет единый логический публичный API. Group, segment и component не владеют API, состоянием или lifecycle. Наличие локального `index.ts`, нескольких файлов, hook, data access или lifecycle-кода само по себе не превращает component или segment в module: всё это принадлежит ближайшему module-owner. - -Nested module имеет собственную ответственность, API и узел графа, но внешний код получает его exports только через публичный API parent module. - -Navigation Group непосредственно в `domains` может содержать доменные модули Level 1, доменные пакеты Level 2 и другие navigation Groups. Пакет при этом не становится модулем или Group. - -### Пакетная форма Level 2 - -Минимальная структура доменного пакета: +Для каждого спорного вывода используй цепочку: ```text -domains// -├── metadata # optional, declarative only -├── api/ # required SLM module -├── assemblies/ # required non-empty Group -│ └── default/ # required baseline production assembly -├── adapters/ # when factories have dependency ports -└── react|vue|... # when domain-specific bindings exist +Факт в коде -> ближайший владелец -> архитектурный смысл -> решение -> reference или код правила ``` -Доменный пакет является policy boundary, но не module, Group, public API или graph node. В его корне нет executable files, state, lifecycle, barrel или реэкспортов. Исполняемыми владельцами являются модули внутри пакета. +### Выбор структурной сущности -`api` является единственным семантическим шлюзом пакета. Его публичный API состоит из фасетов: +Открой [`architecture/modules.md`](./reference/docs/architecture/modules.md), [`architecture/segments.md`](./reference/docs/architecture/segments.md) и при необходимости [`architecture/groups.md`](./reference/docs/architecture/groups.md). По таблицам и критериям этих глав последовательно установи: -| Путь | Содержимое | -|---|---| -| `api` | Только consumer-facing public types: Domain API, models, outcomes, errors | -| `api/ports` | Implementer-facing types при наличии dependency ports | -| `api/factory` | Только именованные runtime factories, по одной на Domain API | -| `api/runtime` | Только реально нужные внешним consumers детерминированные runtime values/functions | +1. Продолжает ли код существующий результат или вводит отдельно формулируемую ответственность. +2. Достаточны ли колокация или сегмент, либо нужен новый владелец. +3. Является ли новый владелец внутренней подответственностью родителя или общим модулем для внешних потребителей. +4. Нужна ли только навигационная группа без реализации и API. -Другой публичный путь внутрь `api` является deep import. `api/ports` и `api/runtime` не создавай без реальной границы или consumer. - -Роли Level 2: - -- `api` определяет Domain API, public models, validation, outcomes, dependency ports и expected domain errors, но не framework state/cache. -- Adapter module реализует связанные dependency ports поверх SDK, storage, platform API, transport или другого provider runtime. -- `assemblies/default` выбирает штатные adapters, вызывает factories и возвращает baseline graph готовых API; дополнительные assemblies представляют отличающиеся production contexts. -- Framework binding module получает готовые Domain API и владеет domain-specific state, cache, hydration и framework integration. -- Composition, `app`, request handler или test setup вызывает assemblies в ацикличном порядке и владеет общим scope graph. - -## Универсальный цикл решения - -### 1. Discover - -Перед решением найди только релевантный контекст: - -- локальные инструкции и стайлгайд; -- SLM root и mapping путей на слои и модули; -- существующие public entry points и package exports; -- внешних consumers затрагиваемой границы; -- runtime- и type-only imports, реэкспорты и aliases; -- state, I/O, SDK, framework runtime и источники недетерминизма; -- места создания graph и instances; -- subscriptions, timers, requests, connections и cleanup; -- тесты и команды проверки затрагиваемых owners. -- architecture mapping, assembly contexts, environment declarations и API-safe allowlists, если проект их использует. - -Считай type-only import и reexport архитектурным ребром. Для runtime-графа дополнительно ищи arguments factories, callbacks, registries, event buses, service locators и singletons: фактическая зависимость может не иметь прямого runtime import. - -### 2. Classify - -Сформулируй краткую внутреннюю карточку: - -```text -Task outcome: -Responsibility: -Owner: -Layer: -Entity: -Domain form: -Public consumers: -Runtime dependencies: -Environment: -State and lifecycle: -Change scope: -``` - -Не обязан показывать карточку пользователю, если решение однозначно. Если одно из ключевых полей неизвестно и влияет на границу, сначала исследуй код, затем задай один конкретный вопрос. - -### 3. Design boundary - -Определи: - -- один owner каждой самостоятельной ответственности; -- минимальный публичный контракт для реальных consumers; -- разрешённые static edges; -- runtime injection и место сборки graph; -- владельцев domain state, technical cache и framework projection; -- безопасную форму expected errors; -- environment entry points и их transitive reachability; -- scope, multiplicity и cleanup каждого lifecycle resource; -- тестовую границу каждого изменяемого owner. - -### 4. Validate before edits - -До изменения файлов ответь: - -- Соответствует ли ответственность роли слоя? -- Является ли выбранная сущность настоящим owner, а не удобной папкой? -- Есть ли у domain одна форма? -- Импортируется ли каждый чужой module через public API? -- Разрешены ли layer и cross-domain edges? -- Остаётся ли graph ацикличным? -- Совместим ли transitive graph с environment entry point? -- Есть ли owner, scope, multiplicity и cleanup у ресурсов? -- Не расширяет ли решение scope на dependency-connected owners, и согласовано ли это расширение? - -### 5. Act and verify - -Если пользователь просит код, не останавливайся на рекомендации. Реализуй согласованную границу, обнови consumers и tests, удали obsolete paths и проверь завершённое состояние. Если пользователь просит только анализ, план или review, не редактируй код. - -## Алгоритмы выбора - -### Ответственность и владелец - -1. Опиши ответственность одним предложением без имени папки, файла или библиотеки. -2. Назови одну причину её изменения. -3. Найди данные, behavior и state, которые изменяются вместе с ней. -4. Найди внешних consumers. -5. Проверь, нужны ли ей собственные API, dependencies, state или lifecycle. -6. Если самостоятельность доказана, назначь ровно один module-owner. -7. Если ответственность нельзя сформулировать или у неё конкурирующие owners, остановись до структурных изменений. - -Место выполнения не переносит владение. Provider, hook, controller, route и component могут запускать чужую ответственность, не становясь её owner. +Зафиксируй решение о владельце до выбора пути. Количество файлов, props, Context, Provider, store, hook или lifecycle-код сами по себе не выбирают структурную сущность. ### Выбор слоя -```text -Только framework bootstrap, route entry или external input adaptation? - -> app +Открой таблицу ролей и границу доменов/композиций в [`architecture/layers.md`](./reference/docs/architecture/layers.md). Сопоставь одно предложение об ответственности с нормативной ролью слоя. Отдельно проверь немодульные исключения `app` и `shared`, прямой доступ композиции к HTTP, SDK или storage и координацию нескольких доменов. Технический механизм и разрешённое направление импорта не доказывают правильность владельца. -Page/layout/screen/widget, route outcome или multi-domain UI? - -> compositions +### Проектирование домена -Domain model, scenario, validation, transition или product state? - -> domains +Для любого создаваемого или изменяемого доменного сценария открой [`architecture/domains.md`](./reference/docs/architecture/domains.md) до проектирования интеграции. -Production implementation technical dependency конкретного Level 2 domain? - -> adapter module внутри package этого domain +Следуй порядку из раздела [«Порядок создания домена»](./reference/docs/architecture/domains.md#порядок-создания-домена). Результатом проектирования должны стать четыре явных артефакта: предметный контракт, карта ожидаемых исходов и defects, план адаптации source boundary и список реально нужных публичных runtime-capabilities. -Самостоятельный универсальный technical service без domain model? - -> infra +Не начинай контракт с endpoint, SDK, DTO или формы ответа. Публично экспортируй guard, parser, schema, constructor или другой механизм runtime-идентификации доменной ошибки только для доказанного потребителя и подходящей среды. Внутреннюю валидацию недоверенных данных и адаптацию источника оценивай отдельно: им не нужен внешний потребитель. -Product-independent reusable UI? - -> ui +### Проектирование API и фасетов -Deterministic, product-agnostic, без I/O/state/lifecycle? - -> shared +Открой разделы о публичном API и фасетах в [`architecture/modules.md`](./reference/docs/architecture/modules.md#публичный-api), затем проверь executable-граф по [`reference/validation.md`](./reference/docs/reference/validation.md#проверка-фасетов). -Иначе -> уточни ответственность, не выбирай папку по аналогии. -``` +Составь consumer/environment map: какая capability нужна какому внешнему потребителю и в какой среде. По ней выбери минимально подходящие фасеты, затем проверь весь транзитивный executable-граф. Не открывай внутренние механизмы про запас и отдельно проверь browser-only и server-only пути. -Domain-specific framework integration над готовым API может принадлежать Framework Group пакета Level 2. Зависимость от React/Vue сама по себе не переносит domain behavior в `compositions` или `app`. +### Проектирование зависимостей -### Выбор сущности +Для каждого нового или изменённого импорта открой [`architecture/dependencies.md`](./reference/docs/architecture/dependencies.md). + +1. Классифицируй обе стороны как модуль, точку входа `app`, немодульный ресурс `shared` или код вне текущего SLM root. +2. Если оба файла принадлежат одному модулю, считай связь внутренней реализацией. +3. Если оба файла принадлежат разным модулям, проверь фасет, направление слоёв и добавь ребро в свёрнутый граф; вложенный модуль является отдельным узлом. +4. Для точки входа `app` или немодульного ресурса `shared` сначала повторно проверь право исходной единицы оставаться немодульной после изменения. Если критерии исключения сохранены, применяй относящиеся к ней правила слоя и публичной границы цели; иначе спроектируй модульного владельца. +5. Внешний package или код за пределами SLM root не становится узлом внутреннего модульного графа. Проверь его влияние на ответственность и среду исходной архитектурной единицы, которой может быть модуль, точка входа `app` или ресурс `shared`, а также на project policy. +6. Проверь весь свёрнутый граф на цикл, а не только пути между конкретными файлами. +7. Отдельно проверь смысл связи: формально разрешённый импорт не должен скрывать неверное владение. + +## Реализация изменений + +После принятия решения: + +- изменяй самую узкую достаточную область и сохраняй принятые соглашения проекта; +- создавай модульную папку только для уже обоснованного владельца; +- добавляй обязательную публичную точку входа и специализированные фасеты только по фактической потребности; +- оставляй детали реализации закрытыми и размещай их по правилам корня, сегментов и компонентных единиц; +- не создавай группы, сегменты, вложенные модули, guards, factories или runtime schemas про запас; +- перенос доменного поведения выполняй вместе с его контрактом, состоянием, UI и интерпретацией ошибок, не оставляя второго владельца; +- при изменении источника сохраняй доменный контракт, пока продуктовый смысл не требует отдельного изменения; +- переключай потребителей на публичный API согласованно с переносом, затем удаляй ставшие недоступными глубокие пути; +- обновляй тесты на контракт и поведение владельца, а интеграционную адаптацию проверяй отдельно от доменных сценариев; +- не исправляй структурный симптом новым barrel или реэкспортом, если проблема находится в ответственности или положении владельца. + +Если реализация обнаружила новый продуктовый смысл, внешнего потребителя или lifecycle, которого не было в карточке решения, останови механическое редактирование и пересмотри архитектурное решение. + +## Архитектурное ревью + +Перед вердиктом открой [`rules/registry.md`](./reference/docs/rules/registry.md), [`reference/validation.md`](./reference/docs/reference/validation.md) и тематическую главу. Проверяй отдельно: + +- смысл: ответственность, единственного владельца, слой, доменный контракт, состояние и lifecycle; +- структуру: модульные корни, фасеты, глубокие импорты, вложенные модули, внутреннюю глубину и свёрнутый граф; +- поведение изменения: не появился ли новый публичный контракт, источник истины или скрытая междоменная координация. + +Оформляй подтверждённое замечание так: ```text -Есть самостоятельный owner/API/dependencies/state/lifecycle? - Да -> module. - Нет -> часть текущего owner. - -Самостоятельный module должен оставаться внутренней границей parent, -а внешний код получать его exports только через parent API? - Да -> nested module. - -Папка в `domains` только классифицирует domain modules, -domain packages и navigation Groups? - Да -> navigation Group слоя `domains`. - -Другая папка только классифицирует modules/Groups? - Да -> Group. - -Папка только организует содержимое одного module? - Да -> segment. - -Framework UI entity не имеет самостоятельной ответственности? - Да -> component parent module. +[Серьёзность] SLM-<код> () +Доказательства: path:line, другие рёбра или отсутствующий обязательный артефакт. +Факт: что наблюдается в коде. +Нарушение: почему факт противоречит точной формулировке правила. +Исправление: какая ответственность, граница или связь должна измениться. ``` -Не создавай module только из-за размера, повторного использования внутреннего helper или желания получить отдельную папку. Не оставляй самостоятельную ответственность component-ом или segment-ом только ради меньшего diff. +Правила ревью: -### Выбор формы домена +- findings идут первыми и сортируются по риску; +- один finding описывает один нарушенный инвариант; +- код правила берётся только из реестра, без выдуманных номеров; +- серьёзность следует принятой шкале проекта; если её нет, используй `high`, `medium`, `low` только как оценку влияния, а не как часть SLM; +- `A`/`R` обозначает способ окончательной проверки, а не серьёзность; +- класс `A` подтверждается структурным фактом при доказанном path mapping, класс `R` требует смыслового обоснования; +- для цикла покажи замкнутую последовательность модулей и location каждого ребра; для отсутствующего фасета или файла назови ожидаемый путь и доказательство модульной границы; +- сигнал вроде `fetch`, Provider, большого файла или локального `index.ts` не является нарушением без проверки владельца; +- рекомендация и project policy маркируются отдельно и не выдаются за блокирующее правило; +- если продуктового контекста недостаточно, формулируй адресный вопрос или риск, а не категоричный finding; +- при отсутствии findings сообщи это явно и перечисли только непроверенные области или ограничения проверки. -По умолчанию используй доменный модуль Level 1. Level 1 не требует factory, ports, adapters, assemblies или разделения по техническим ролям. +Не ограничивай ревью изменёнными строками, если новая связь меняет публичный API, транзитивную среду фасета или модульный цикл. -Рассматривай Level 2, когда конкретному домену действительно нужны: +## Миграция -- несколько независимо собираемых Domain API; -- собственные public models и stable errors поверх provider contracts; -- baseline `assemblies/default` и дополнительные production contexts; -- несколько production technical integrations; -- HTTP, storage или realtime behind consumer-owned ports; -- строгие environment boundaries; -- самостоятельные domain-specific framework modules. +Мигрируй небольшими связными срезами, каждый из которых оставляет понятного владельца и рабочий публичный контракт: -Не выбирай Level 2 из-за количества файлов, одного SDK, одного hook, желания унифицировать дерево или гипотетической будущей интеграции. Зафиксируй, какую реальную потребность окупает дополнительная стоимость package, facets, assembly и adapters. +1. Инвентаризируй фактические ответственности, внешних потребителей и текущие межмодульные рёбра выбранного участка. +2. Составь целевую карточку решения, не начиная с желаемого дерева папок. +3. Объяви целевой публичный контракт; для домена до интеграции также зафиксируй предметный контракт, ожидаемые исходы и границу defects. +4. Создай или скорректируй границу владельца; для домена добавь внутреннюю адаптацию источников и ошибок. +5. Перенеси поведение, состояние, доменный UI и lifecycle целиком, не создавая параллельного владельца. +6. Переключи потребителей на фасеты и удаляй глубокие импорты. +7. Пересчитай свёрнутый граф, проверь среды фасетов и очистку ресурсов. +8. Удали legacy-путь после перехода всех реальных потребителей. +9. Повтори процесс для следующего независимого среза. -### Публичная граница +Не используй `compositions` как временного владельца нового доменного сценария. Если промежуточное состояние ещё нарушает правило, не называй его завершённой SLM-миграцией и явно фиксируй ограничение. -1. Перечисли реальных внешних consumers. -2. Для каждого запиши минимально необходимый contract. -3. Удали exports, которым нет consumer. -4. Не включай mutable internals, concrete clients, stores, contexts, adapter implementations или lifecycle internals в Domain API, модуль `api` или parent module. -5. Для обычного module оставь одну логическую external entry point. -6. Для модуля `api` используй только `api`, `api/factory`, optional `api/ports` и optional `api/runtime`. -7. Удали deep imports и обнови package exports/aliases при необходимости. -8. Не открывай nested module напрямую за пределы parent boundary. +## Проверка результата -Каждый adapter остаётся обычным SLM-модулем и предоставляет собственный минимальный public API, через который assembly получает production implementation. Запрещён не public API adapter-модуля, а его реэкспорт через `api`, корень пакета, Domain API или другой несвязанный owner. +Перед завершением открой полный [критерий завершения](./reference/docs/reference/validation.md#критерий-завершения) и проверь только применимые пункты. Сохрани доказательства по смысловым решениям, доменной границе, структуре, средам выполнения и проектным test/lint/build-командам, не копируя checklist в отчёт. -Для Level 2 разделяй Domain API по устойчивым различиям consumers, dependencies или environments, а не по внутренним техническим папкам. Каждый публичный scenario принадлежит ровно одному Domain API; каждому API соответствует одна factory. Именованный graph assembly не является новым Domain API. +Успешная сборка не заменяет смысловую проверку. Если автоматического SLM lint нет, выполни структурную проверку вручную и перечисли проверенные модули, фасеты и рёбра. Не утверждай прохождение проверки, которую фактически не запускал или не мог выполнить. -### Проверка зависимости +## Stop conditions -Для каждого нового или изменённого edge: +Сначала ищи ответ в коде, конфигурации и references. Задавай пользователю адресный вопрос только когда решение зависит от отсутствующего продуктового или эксплуатационного факта: -1. Определи source owner и target owner. -2. Определи их слои и формы доменов. -3. Если owners различаются, импортируй target только через public API. -4. Проверь матрицу слоёв. -5. Если edge пересекает Level 2 package boundary, примени более строгую cross-domain модель. -6. Проверь transitive environment compatibility. -7. Добавь edge в общий module DAG и runtime graph и проверь цикл, включая callbacks и mixed L1/L2 construction. +- результат поведения нельзя однозначно сформулировать; +- подходят несколько владельцев, а предметная граница не следует из кода; +- неизвестно, является ли координация техническим связыванием или новым доменным сценарием; +- ожидаемые неуспешные исходы и граница programming defect не определены продуктом; +- неизвестны реальные внешние потребители или требуемая среда выполнения; +- неизвестны область жизни, число экземпляров или момент очистки ресурса; +- локальное сопоставление путей с SLM-сущностями нельзя подтвердить, а задача требует блокирующего структурного вердикта. -Между двумя доменными модулями Level 1 допустим обычный runtime-import публичного API при соблюдении layer matrix и DAG. Не навязывай им runtime injection Level 2. - -Если хотя бы одна сторона является пакетом Level 2, статически допустимы три формы: - -```ts -import type { LevelOneDomainApi } from '.../level-one-domain' -import type { LevelTwoDomainApi } from '.../level-two-domain/api' -import { deterministicValue } from '.../level-two-domain/api/runtime' -``` - -Готовый runtime API, связь с которым пересекает Level 2 package boundary, создаёт внешний graph owner и передаёт assembly зависимого пакета Level 2 либо явной construction point/public callback модуля Level 1. Через такую границу не импортируй чужие `api/ports`, factory, assembly, adapter, API singleton, framework state, hook, context, Provider, component или внутренний путь `api`. - -Не скрывай cross-domain dependency локальным structural interface, callback, global registry или event bus. Установи владельца контракта и отрази фактический runtime edge в graph, иначе можно пропустить цикл. - -### Runtime capabilities - -| Capability | Размещение в Level 2 | -|---|---| -| SDK, HTTP/GraphQL source, storage, platform API | Adapter | -| Concrete state/query runtime для materialized domain values | Framework binding или composition | -| Clock, timer, random, ID, environment | Dependency port с production implementation в adapter | -| Готовый API другого домена | Cross-domain dependency, передаваемая graph owner | -| Provider, hook или query projection готового Domain API | Framework binding module | -| Page-local или multi-domain UI state | Владеющий composition module | -| Универсальный technical service | `infra` module | - -Adapter переводит provider arguments, records и expected failures в consumer-owned port. Он не объявляет Domain API scenario, domain fallback, transition или public domain error. Production implementation port не прячь inline в assembly или composition. - -`assemblies/default` выбирает штатные adapters, вызывает factories и возвращает baseline graph API. Дополнительная assembly представляет реально отличающийся production context. Они не добавляют scenarios, методы API или собственные domain errors. Framework binding получает готовые API и не вызывает factory/assembly и не выбирает adapter. - -Даже если готовый `infra` API структурно совпадает с technical dependency, текущие правила Level 2 требуют production implementation в adapter-модуле домена. Сделай его public API минимальным и не добавляй фиктивные преобразования, но не обходи обязательную adapter boundary прямой передачей `infra` capability в factory. - -Перед новым external import в `api`: - -1. Определи реально resolved package entry и resolver conditions нужных environments. -2. Проверь transitive runtime graph, side effects, I/O, mutable state и runtime capabilities. -3. Убедись, что package соответствует API-safe критериям, и обнови project allowlist/declaration. -4. Если доказательства нет, вынеси capability в factory dependency и реализуй production binding через adapter. - -### State и cache - -```text -Domain models, validation, transitions, commands, scenario outcomes - -> api authority - -Transport/source cache - -> adapter - -Framework/query projection готового Domain API - -> framework binding - -State только текущей UI composition - -> composition owner -``` - -Raw DTO, query-library result и mutable client не являются Domain API. Private source cache внутри adapter может хранить provider records, если DTO и library types не выходят в Domain API. Binding может владеть framework metadata и local UI state, но domain payload projection использует только public values, outcomes и events, произведённые или проверенные `api`, и не создаёт параллельную предметную модель. - -При optimistic или concurrent mutations не придумывай универсальный rollback. Предметные ordering, versioning, rebase/rollback и reconciliation определяет операция Domain API либо deterministic `api/runtime`; иначе binding invalidates projection и получает authoritative snapshot через API. - -### Errors - -- Каждый expected failure публичного scenario, включая собственный domain rejection, представлен именованным readonly error type текущего домена со stable code. -- Expected provider failure проходит через adapter и closed port failure, после чего текущий `api` преобразует его в собственный domain error. -- Expected foreign-domain outcome или error поступает через готовый публичный API другого домена и преобразуется текущим `api` напрямую, без автоматического local port. -- Source object, SDK class, message, status, payload и `cause` не входят в public domain contract. -- Type errors экспортируются через `api`; необходимые runtime codes и guards - только через реально нужный `api/runtime`. -- Не выбирай exception или discriminated `Result` как правило SLM: сохрани project policy и архитектурное владение. -- Не маскируй programming defect под expected domain outcome. Если меняется публичный failure channel, выясни политику unexpected failures, cancellation и serialization. - -### Realtime - -Realtime transport остаётся внутри adapter. Domain API публикует только проверенные events, outcomes, statuses и stable errors. Для command-response protocol установи correlation scope, ACK semantics, timeout, cancellation и `OUTCOME_UNKNOWN`; без correlation не обещай индивидуальный result. - -Для каждой subscription установи ordering, duplicate delivery, reconnect, gap detection, resync, shared connection ownership и момент, после которого cleanup гарантирует отсутствие callbacks. Framework binding materializes events через API-owned transition либо invalidates cache и повторно запрашивает snapshot. - -### Lifecycle и environment - -Для каждого request, subscription, listener, timer, observer, connection или другого долгоживущего ресурса зафиксируй: - -```text -Owner: -Created or started by: -Scope: -Multiplicity: -Environment: -Owned or borrowed: -Cleanup: -``` - -Factory не запускает долгоживущую работу. Явная операция, запускающая resource, предоставляет cleanup. У каждого resource один owner: adapter-owned resource экспортирует handle для aggregate cleanup, assembly-owned resource передаётся adapter как borrowed capability. Assembly немедленно регистрирует cleanup каждого owned resource и полученный adapter lifecycle handle; при любом obligation возвращает идемпотентный aggregate cleanup. - -Спроектируй failure path assembly. Если следующий шаг завершился ошибкой до возврата graph, assembly выполняет все зарегистрированные cleanup obligations в обратном dependency order. После awaited cleanup callbacks запрещены. Покрой partial acquisition, adapter handles, repeated disposal и cleanup errors тестами. - -Environment определяется transitive import graph, а не именем файла или tree shaking. Для RSC, Server Actions, workers, edge runtime и conditional exports установи executable edges, framework references и runtime capabilities. Для SSR-enabled Client Component отдельно проверь server prerender graph, browser hydration graph и framework-deferred browser effects. - -## Рабочие процедуры - -### Проектирование - -1. Ограничь scope пользовательской задачей. -2. Найди SLM root, project mapping и существующие owners. -3. Построй карту consumers и текущих public paths. -4. Определи ответственность, layer, entity и domain form. -5. Спроектируй target boundaries и минимальные public contracts. -6. Классифицируй technical и cross-domain dependencies. -7. Определи graph owner, environments, state, errors и lifecycle. -8. Проверь правила и stop conditions. -9. Выдай решение, target structure, dependencies и порядок реализации. - -Не предлагай файловое дерево до определения owners и boundaries. Имена файлов и segments следуют локальному стайлгайду, а не задаются SLM. - -### Реализация - -1. Зафиксируй принятое решение и change scope. -2. Изменяй код в dependency order: contracts и behavior раньше adapters и assembly, providers/consumers после готовых API. -3. Для Level 1 не создавай отсутствующие роли Level 2. -4. Для Level 2 сначала реализуй consumer types, `api/ports` при наличии dependency ports, errors, operations и `api/factory`. -5. Затем реализуй production adapters, обязательную `assemblies/default`, дополнительные assemblies и framework bindings. -6. В graph owner вызывай assembly builders пакетов Level 2 и явные construction points/public callbacks модулей Level 1, передавая им готовые cross-domain API. -7. Переведи всех затронутых consumers на public paths. -8. Обнови architecture mapping, assembly contexts, package exports, environment declarations и API-safe allowlists, затронутые новой границей. -9. Удали obsolete exports, deep imports и старые boundaries в согласованном scope. -10. Добавь tests рядом с owners, включая adapter contract tests, realtime guarantees и cleanup failure paths assemblies. -11. Запусти доступные structural, type, unit, integration и architecture checks и убедись, что новые пути входят в анализ. - -Не оставляй заведомо промежуточную смешанную границу как завершённый результат. Backward compatibility добавляй только для реального внешнего consumer, persisted contract или явно согласованной phased migration. - -### Миграция Level 1 -> Level 2 - -1. Выбери ровно один domain module и докажи потребность Level 2. -2. Найди все consumers, exports, state, I/O, framework integration и lifecycle resources. -3. Вычисли dependency-connected migration radius до редактирования. -4. Спроектируй Domain API по scenarios и consumers, а не по текущим technical segments. -5. Перенеси public models, validation, transitions, outcomes и errors под authority `api`. -6. Объяви consumer-owned ports, closed port failures и по одной factory на Domain API. -7. Оформи production implementations ports как adapter modules и добавь contract tests. -8. Создай обязательную `assemblies/default` для baseline production context и дополнительные assemblies только при реальном отличии graph. -9. Перенеси domain-specific state, cache, hydration и framework responsibilities в Framework Group. -10. Оставь pages, routes и multi-domain UI в `compositions`. -11. Переключи external consumers и graph roots. -12. Обнови declarations формы домена, модулей, facets, environments и public entry points в project architecture mapping. -13. Удали прежний root API и старую форму домена. -14. Проверь, что каждый завершённый этап оставляет одну форму затронутой domain responsibility. - -Временное физическое сосуществование старой и новой структуры допустимо только внутри незавершённого изменения. Не объявляй его conforming state. Не мигрируй несвязанные соседние домены, но включи в migration radius dependency-connected consumer, если его API нужно рефакторить или перевести на Level 2 для явной runtime injection. Такое расширение scope сначала согласуй. Если атомарный cutover невозможен, согласуй ограниченную compatibility strategy и срок её удаления. - -### Архитектурное ревью - -1. Определи review scope, SLM root и формы затронутых доменов. -2. Построй фактическую карту owners, public boundaries, imports и runtime injection. -3. Проверь structural правила класса `A` по наблюдаемым evidence. -4. Отдельно проверь смысловые правила класса `R`; отсутствие lint error не доказывает их соблюдение. -5. Проверь transitive `api` closure, resolved external package entries и API-safe declarations. -6. Проверь importer matrix `api/ports`, `api/factory`, concrete adapters и assemblies. -7. Проверь environment graph, resolver conditions и framework reference edges. -8. Проверь state/cache/error/realtime/lifecycle ownership, включая cleanup частично созданной assembly. -9. Проверь, что tests находятся у правильных owners и не дублируют весь behavior на каждом уровне. -10. Сначала сообщи findings по severity, затем краткий verdict и остаточные gaps. - -Каждый finding содержит: - -```text -Location: -Kind: -Rule or definition: -Evidence: -Impact: -Minimal remediation: -Required tests: -Confidence: -``` - -Не называй рекомендацию нарушением. Не подтверждай полное SLM conformance, если не исследовал весь нужный graph или не знаешь project mapping. - -### Тестирование по владельцам - -| Ответственность | Основная test boundary | -|---|---| -| Domain scenarios, validation, models, outcomes и expected errors | `api` через соответствующую factory | -| Deterministic runtime/guards | `api` | -| Port mapping и provider behavior | Adapter module | -| Graph composition, adapter selection, environment, success cleanup и partial-failure cleanup | Assembly module | -| Provider, hook, form или query projection | Framework binding module | -| Multi-domain graph и lifecycle | Composition, `app` или другой graph owner | - -Не повторяй полный Domain API scenario suite в adapter, assembly и framework tests. Проверяй в каждой границе только принадлежащий ей behavior и integration contract. - -## Anti-patterns - -### Ownership и структура - -- Выбирать слой или сущность по имени существующей папки. -- Размещать domain model или scenario в `infra`/`shared`. -- Оставлять page, route policy или multi-domain responsibility внутри домена. -- Делать Group, segment или component скрытым owner. -- Создавать общий module или Level 2 package на будущее. -- Требовать от component быть stateless: локальные data/lifecycle details допустимы, пока ответственность принадлежит parent module. - -### Public boundaries - -- Deep imports во внутренности module или `api`. -- Root barrel доменного пакета или Group. -- Reexport adapter implementation через `api`, package root или Domain API вместо public API самого adapter-модуля. -- Reexport client и server entry points через общий barrel. -- Создавать `api/ports` без dependency port или `api/runtime` без внешнего consumer. - -### Domain API и runtime - -- Импортировать SDK, storage, framework, state/query manager, platform API или hidden nondeterminism в `api`. -- Обходить boundary через helper, `shared` или type alias. -- Публиковать raw DTO или library-specific cache/store types в Domain API. -- Экспортировать port contracts через consumer-facing `api` вместо `api/ports`. -- Позволять adapter определять domain fallback, transition или error semantics. -- Прятать production adapter inline в assembly/composition. -- Позволять factory выбирать environment или assembly. - -### Assembly, framework и cross-domain - -- Добавлять scenario или API method в assembly. -- Вызывать factory/assembly из framework binding. -- Импортировать `api/factory` или concrete adapter из production graph owner в обход assembly. -- При пересечении Level 2 package boundary импортировать framework state, hooks или components другого домена. -- При пересечении Level 2 package boundary импортировать чужие ports, factory, assembly, adapter или API singleton. -- Прятать runtime dependency в service locator, mutable registry или event bus. -- Передавать production `infra` capability напрямую в Level 2 factory в обход обязательного adapter-модуля. -- Считать `assemblies/default` изоморфной только из-за имени или runtime branch. - -### State и lifecycle - -- Делать cache параллельной domain model. -- Строить optimistic domain value из raw form/DTO без API validation. -- Использовать file-level singleton без доказанного application scope. -- Запускать скрытую subscription/timer при создании API. -- Оставлять resource без scope или cleanup. -- Возвращать пустой `dispose` только для одинаковой формы assemblies. -- Вызывать callbacks после завершившегося cleanup. -- Повторять realtime command без idempotency guarantee после `OUTCOME_UNKNOWN`. - -### Процесс - -- Выбирать Level 2 по размеру каталога. -- Генерировать полный package scaffold без потребности. -- Мигрировать несвязанные соседние домены ради локального изменения. -- Копировать пример как нормативное дерево. -- Перечислять коды правил вместо анализа фактического graph и runtime. -- Задавать пользователю все открытые вопросы независимо от задачи. - -## Stop conditions и адресные вопросы - -Остановись до изменения публичной или runtime-границы, если: - -- ответственность или owner не определены; -- одна ответственность имеет конкурирующих owners; -- неизвестны consumers изменяемого API; -- одна domain responsibility окажется в двух формах; -- planned edge создаёт цикл; -- environment compatibility нельзя установить; -- resource scope, multiplicity или cleanup неизвестны; -- изменение требует незапрошенной широкой миграции; -- локальные инструкции противоречат выбранной SLM boundary; -- корректность зависит от открытой semantics cancellation, concurrency, hydration или disposal; -- задача требует правил монорепозитория, versioning или нескольких SLM roots, которых текущий DRAFT не задаёт. - -Задавай вопрос только при наличии trigger: - -| Trigger | Что выяснить | -|---|---| -| L1 -> L2 или удаление старого API | Полный migration radius, атомарный cutover или compatibility strategy | -| Новая technical dependency | Ownership contract, timeout/retry/idempotency/order/subscription semantics | -| Abort или cancellable operation | Кто владеет cancellation и как она связана с cleanup/outcome | -| Публичные errors, RPC, server action | Expected failure, cancellation, unexpected defect и serialization policy | -| Store, persistence или external events | Projection owner, API validation, hydration, resync и authoritative source | -| Optimistic/concurrent mutations | Ordering, versioning, rollback/rebase и authoritative refresh | -| Assembly, lazy graph или новый root | Scope, multiplicity, owned/borrowed resources и disposal | -| SSR, hydration, RSC | Serialization boundary, validation/reset и executable/reference edges | -| Worker, edge, conditional exports | Реальные capabilities и resolver conditions | -| Готовый `infra` API совпадает с port | Какой минимальный public API adapter-модуля свяжет capability без фиктивной domain semantics | - -Можно продолжить с явным assumption только когда решение обратимо, не меняет owner/public API, не ослабляет environment boundary и не скрывает lifecycle. - -## Проверочные списки - -### До изменения файлов - -- [ ] Найден SLM root и path mapping. -- [ ] Найдены architecture declarations, assembly contexts, environment/API-safe allowlists проекта. -- [ ] Прочитаны локальные инструкции. -- [ ] Сформулирована responsibility. -- [ ] Назначен один owner. -- [ ] Выбраны layer и entity. -- [ ] Для domain выбрана одна form. -- [ ] Найдены реальные consumers. -- [ ] Спроектирован минимальный public API. -- [ ] Классифицированы static и runtime dependencies. -- [ ] Проверены layer, cross-domain и environment edges. -- [ ] Для resources определены scope и cleanup. -- [ ] Нет активного stop condition. - -### После реализации - -- [ ] Каждый module имеет отдельную boundary и public API. -- [ ] Нет deep imports и package/Group barrels. -- [ ] Layer matrix соблюдена. -- [ ] Общий module graph ацикличен. -- [ ] `api` import closure environment-neutral и technical-runtime-free. -- [ ] Runtime APIs, пересекающие Level 2 package boundary, передаются аргументами; L1 -> L1 использует public API. -- [ ] Новые external imports `api` доказанно API-safe и объявлены в allowlist. -- [ ] Client/server graphs не содержат несовместимый executable code. -- [ ] Обязательные и фактически существующие optional facets `api` имеют допустимое содержимое и consumers. -- [ ] Production implementations ports принадлежат нужным adapters. -- [ ] `assemblies/default` представляет объявленный baseline context и не имеет import side effects. -- [ ] Assemblies возвращают точный graph и выполняют все owned и adapter-provided cleanup obligations после успеха и partial failure. -- [ ] Framework bindings получают готовые APIs. -- [ ] Все expected scenario failures имеют собственный stable domain error; technical и foreign errors не протекают наружу. -- [ ] Framework projection не подменяет authority `api`. -- [ ] Realtime ports определяют correlation, ordering, resync, outcome uncertainty и cleanup. -- [ ] Architecture mapping, exports, facets и environment declarations соответствуют новым путям. -- [ ] Tests проверяют behavior соответствующих owners. -- [ ] После migration удалена старая form/boundary. +В вопросе укажи наблюдаемый факт, архитектурное последствие и конкретные варианты выбора. Не проси пользователя решать то, что можно установить поиском по репозиторию. ## Формат результата -Не печатай полную внутреннюю карточку и все checklists без необходимости. Пользователю нужен результат задачи. +Для проектирования, реализации или миграции сообщай: -| Режим | Обязательный результат | -|---|---| -| Design | Decision, owner/layer/form, boundaries, public APIs, dependencies, lifecycle, implementation order, assumptions | -| Implementation | Использованное решение, изменённые boundaries/files, API/import changes, tests/checks, отклонения и риски | -| Migration | Source/target forms, consumer map, phases, cutover, удаление старой boundary и completion gate | -| Review | Findings с evidence, verdict, remediation order, unresolved decisions и непроверенный scope | +- ответственность, владельца и выбранный слой; +- изменённые публичные границы и межмодульные связи; +- существенные решения по доменному контракту, состоянию и lifecycle; +- какие references и правила повлияли на решение; +- выполненные проверки и непроверенные риски. -Для однозначной локальной реализации достаточно кратко объяснить архитектурное решение и выполнить работу. Для дорогого, публично несовместимого или неоднозначного решения сначала покажи варианты и запроси выбор. +Для чистого проектирования вместо списка изменённых файлов дай целевую physical form, consumer/environment map, impact map и нерешённые продуктовые факты. -## Когда открывать references +Для объяснения отделяй определения и правила SLM от рекомендаций и project policy. Для ревью используй формат findings из раздела выше. -| Ситуация | Reference | -|---|---| -| Нужна точная формулировка правила | [`rules/level-1.md`](./reference/draft/rules/level-1.md), [`rules/level-2.md`](./reference/draft/rules/level-2.md) | -| Неясен смысл сущности | [`level-1/terminology.md`](./reference/draft/level-1/terminology.md), [`level-2/terminology.md`](./reference/draft/level-2/terminology.md) | -| Сложный Level 1 module/dependency/lifecycle case | [`level-1/`](./reference/draft/level-1/README.md) | -| Package, Domain API, ports, factory или adapters | [`level-2/domains/`](./reference/draft/level-2/domains/README.md) | -| Cross-domain или environment edge | [`level-2/dependencies.md`](./reference/draft/level-2/dependencies.md) | -| State, cache, SSR или hydration | [`state-cache.md`](./reference/draft/level-2/domains/state-cache.md), [`open-questions.md`](./reference/draft/level-2/domains/open-questions.md) | -| Assembly lifecycle и cleanup | [`assemblies.md`](./reference/draft/level-2/domains/assemblies.md) | -| Realtime messages и subscriptions | [`realtime.md`](./reference/draft/level-2/domains/realtime.md) | -| Full architecture review | [`level-1/validation.md`](./reference/draft/level-1/validation.md), [`level-2/validation.md`](./reference/draft/level-2/validation.md) | -| L1 -> L2 migration example | [`auth-example.md`](./reference/draft/level-2/domains/auth-example.md) | +## Карта файлов -Будущие project examples открывай только после архитектурной классификации. Используй их как evidence конкретной реализации для похожего stack/environment, но не копируй naming, дерево или дополнительные роли без потребности. Example никогда не переопределяет rule или terminology. +References являются частью собранного skill. Карта покрывает весь комплект материалов; открывай минимальный набор для текущей задачи, но перед блокирующим вердиктом всегда сверяй точную формулировку с реестром. + +| Файл | Что содержит | Когда открывать | +|---|---|---| +| [`reference/docs/README.md`](./reference/docs/README.md) | Обзор SLM, мотивация, область вопросов и стартовая навигация | Первое знакомство, объяснение подхода, выбор начального участка внедрения | +| [`reference/docs/architecture/README.md`](./reference/docs/architecture/README.md) | Базовая модель владения, структурное дерево, порядок проектирования и область применения | В начале проектирования, миграции или широкого ревью | +| [`reference/docs/architecture/layers.md`](./reference/docs/architecture/layers.md) | Роли шести слоёв, граница `domains`/`compositions`, немодульные исключения | Выбор или проверка слоя, страницы, доменного UI, `app`, `infra`, `shared` | +| [`reference/docs/architecture/modules.md`](./reference/docs/architecture/modules.md) | Ответственность модуля, ближайший владелец, API, фасеты, корень, компоненты, вложенность, состояние и lifecycle | Создание и изменение модуля, проектирование экспортов и внутренней структуры | +| [`reference/docs/architecture/domains.md`](./reference/docs/architecture/domains.md) | Доменный контракт, source boundary, адаптация, ошибки, runtime-идентификация и порядок создания домена | Любой доменный сценарий, продуктовые данные, DTO, SDK, storage или error contract | +| [`reference/docs/architecture/dependencies.md`](./reference/docs/architecture/dependencies.md) | Свёрнутый модульный граф, публичные пути, матрица слоёв, same-layer связи и циклы | Добавление импорта, реэкспорта, фасета, анализ deep import или цикла | +| [`reference/docs/architecture/groups.md`](./reference/docs/architecture/groups.md) | Навигационные группы и их отличие от модулей и сегментов | Группировка модулей, каталоги `pages`/`layouts`/`widgets`, group barrel | +| [`reference/docs/architecture/segments.md`](./reference/docs/architecture/segments.md) | Внутренняя организация владельца, колокация, компонентные единицы и переход к вложенному модулю | Размещение внутреннего файла, рост модуля, спор о `components`/`hooks`/`services` | +| [`reference/docs/reference/terminology.md`](./reference/docs/reference/terminology.md) | Нормативные определения всех сущностей и границ SLM | Спор о термине, классификация сущности, точное толкование правила | +| [`reference/docs/reference/validation.md`](./reference/docs/reference/validation.md) | Карточка решения, review-checklists, автоматические проверки, фасеты и критерий завершения | До структурного изменения, на ревью и перед завершением любой архитектурной задачи | +| [`reference/docs/rules/README.md`](./reference/docs/rules/README.md) | Разница между определением, правилом, рекомендацией и примером; классы `A`/`R` | Оформление вердикта, проектирование lint-проверки, оценка нормативной силы утверждения | +| [`reference/docs/rules/registry.md`](./reference/docs/rules/registry.md) | Единственный реестр точных блокирующих требований и стабильных кодов | Любой finding, заявление о нарушении или обязательном соответствии | + +### Маршруты чтения + +- Новый или изменяемый модуль: `architecture/README.md` -> `architecture/modules.md` -> нужная глава о слое или домене -> `architecture/dependencies.md` -> `reference/validation.md`. +- Доменный сценарий: `architecture/domains.md` -> `architecture/layers.md` -> `architecture/dependencies.md` -> `reference/validation.md`. +- Размещение внутреннего кода: `architecture/modules.md` -> `architecture/segments.md`; `architecture/groups.md` добавляется только для внешней навигации модулей. +- Фасеты и runtime boundaries: `architecture/modules.md` -> раздел проверки фасетов в `reference/validation.md` -> environment-правила в `rules/registry.md`. +- Архитектурное ревью: `reference/validation.md` -> `rules/registry.md` -> тематические главы по каждому найденному риску. +- Терминологический спор: `reference/terminology.md` -> тематическая глава -> `rules/registry.md`, если требуется обязательный вердикт. diff --git a/skills/slm-design/reference/docs/README.md b/skills/slm-design/reference/docs/README.md new file mode 100644 index 0000000..cc2f7cb --- /dev/null +++ b/skills/slm-design/reference/docs/README.md @@ -0,0 +1,108 @@ +--- +layout: home +title: Архитектура фронтенд-приложений +description: SLM Design помогает командам сохранять понятную структуру и предсказуемо развивать фронтенд-приложения по мере роста продукта. + +hero: + name: SLM Design + text: Архитектура фронтенд-приложений + tagline: Практичная модель для растущих команд и продуктов. Меньше споров о структуре, безопаснее изменения и понятнее код. + image: + src: /logo.svg + alt: SLM Design + actions: + - theme: brand + text: Узнать, как это работает + link: /architecture/ + - theme: alt + text: Посмотреть правила + link: /rules/registry + +features: + - title: Один язык для всей команды + details: Разработчики одинаково понимают границы, ответственность и место нового кода. Архитектурные решения перестают зависеть от личных предпочтений. + - title: Предсказуемые изменения + details: Локальная правка остаётся локальной. Команда может развивать внутреннюю реализацию, не переписывая половину приложения. + - title: Рост без хаоса + details: Структура усложняется только вместе с продуктом, а не из-за количества файлов, компонентов или выбранных библиотек. + - title: Архитектура видна в репозитории + details: Правила выражены кодом и структурой проекта, поэтому документация не расходится с реальным приложением. + - title: Независимость от стека + details: SLM не требует конкретного фреймворка, state manager или способа работы с данными и не ограничивает внутреннюю реализацию. + - title: Постепенное внедрение + details: Начните с одного спорного участка и расширяйте модель по мере необходимости, без полной перестройки приложения. +--- + +## Папки перестают быть архитектурой, когда продукт начинает расти + +На старте почти любая структура выглядит понятной. Затем появляются десятки компонентов, общие hooks, Providers, stores, глубокие импорты и модули, которые знают друг о друге слишком много. Папки остаются на месте, но границы ответственности исчезают. + +SLM возвращает архитектуре наблюдаемый смысл: + +> **Модуль владеет ответственностью. Весь код внутри ближайшей модульной границы реализует её.** + +Это правило одинаково работает для страницы, доменного сценария, UI-библиотеки, инфраструктурного сервиса и небольшого внутреннего модуля. + +## Что меняется для команды + +| Когда границ нет | С SLM Design | +|---|---| +| Решение о размещении кода принимается по похожей папке | Сначала определяется ответственность и её владелец | +| Компоненты и services становятся скрытыми архитектурными центрами | Любой внутренний механизм остаётся реализацией ближайшего модуля | +| Потребители импортируют удобный внутренний файл | Чужой модуль доступен только через публичный фасет | +| Циклы обнаруживаются во время большого рефакторинга | Модульный граф можно проверять lint-инструментами | +| Новые уровни создаются из-за размера каталога | Вложенный модуль появляется только для самостоятельной подответственности | + +Результат: меньше случайной связанности, меньше споров о папках и предсказуемый радиус каждого изменения. + +## Не ещё один шаблон директорий + +SLM не диктует, какие библиотеки, state managers или framework-механизмы использовать. Внутри модуля могут находиться компоненты, Providers, Guards, hooks, stores, services, utilities и сторонние SDK. + +Архитектура отвечает на другие вопросы: + +1. За какой результат отвечает этот код? +2. Какой модуль владеет его контрактом и состоянием? +3. Что действительно нужно внешним потребителям? +4. Какие зависимости допустимы и не создают ли они цикл? +5. Где заканчивается область жизни ресурсов? +6. Какой доменный контракт и какие ошибки определены до подключения источника данных? + +Файловая структура появляется после ответов, а не заменяет их. + +## От одного файла до дерева владельцев + +Модуль может начинаться с одного главного файла и расти без смены архитектурной сущности: + +```text +checkout/ +├── index.ts # Публичный контракт +├── checkout.tsx # Главная реализация +├── components/ # Внутренний код checkout +├── hooks/ +├── services/ +└── modules/ + └── form-session/ # Самостоятельная подответственность + ├── index.ts + ├── form-session.provider.tsx + └── hooks/ +``` + +Размер, количество файлов и framework-роли не создают владельца. Только отдельно сформулированная ответственность получает модульную границу. + +## Правила, которые можно проверить + +SLM разделяет смысловые решения и структурные инварианты: + +- ответственность, владелец и минимальный публичный контракт проверяются на архитектурном ревью; +- направления между слоями, глубокие импорты, публичные фасеты и модульные циклы проверяются автоматически; +- каждый rule имеет стабильный код и одно место нормативной формулировки; +- framework-компоненты не образуют бесконечную файловую рекурсию, а новые уровни появляются только через вложенные модули. + +Вы получаете не абстрактный набор рекомендаций, а модель, которую можно обсуждать одинаковыми терминами, видеть в репозитории и постепенно автоматизировать. + +## Начните с одной ответственности + +Не нужно переписывать приложение целиком. Выберите один спорный участок, сформулируйте его ответственность, назначьте владельца и закройте внутреннюю реализацию публичным API. Этого достаточно, чтобы увидеть разницу между папкой и архитектурной границей. + +[Спроектировать первый модуль](./architecture/modules.md) · [Спроектировать домен](./architecture/domains.md) · [Разобрать зависимости](./architecture/dependencies.md) · [Проверить существующую структуру](./reference/validation.md) diff --git a/skills/slm-design/reference/docs/architecture/README.md b/skills/slm-design/reference/docs/architecture/README.md new file mode 100644 index 0000000..27b03e3 --- /dev/null +++ b/skills/slm-design/reference/docs/architecture/README.md @@ -0,0 +1,111 @@ +# Архитектура SLM + +SLM описывает владение ответственностями внутри одного фронтенд-приложения. Слой определяет роль кода, группа классифицирует модули, модуль владеет ответственностью, домен специализирует модуль для предметной ответственности, а сегмент организует реализацию владельца. + +## Владение как основа + +**Ответственность** — результат или поведение приложения, за которое отвечает один модуль-владелец. Она становится самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни, а не только внутренняя роль в работе другого модуля. Props, импорты, локальное состояние и lifecycle-код сами по себе этого не доказывают. + +У каждой самостоятельной ответственности есть ровно один владелец. В SLM владельцем является модуль. Он определяет: + +- какие возможности доступны внешним потребителям; +- от каких других модулей зависит ответственность; +- кому принадлежат данные и изменяемое состояние; +- когда создаются и уничтожаются долгоживущие ресурсы; +- как устроена внутренняя реализация. + +Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Место выполнения или вид кода не меняют владельца. + +Доменный сценарий всегда получает владельца в слое `domains`. Его бизнес-правила, продуктовое состояние, операции с предметными данными, доменный UI и обслуживающие framework-механизмы остаются внутри доменной границы. Модуль `compositions` использует и компонует готовый публичный API домена, но не реализует сценарий вместо него. Если подходящего доменного модуля ещё нет, его отсутствие не является основанием временно разместить сценарий в композиции. Эта граница закреплена правилом [`SLM-DOMAIN-R022`](../rules/registry.md#slm-domain-r022) и подробно описана в разделе [Домены](./domains.md). + +## Структурная модель + +```text +SLM root +└── слой + ├── модуль + │ ├── сегмент + │ └── вложенный модуль + │ ├── сегмент + │ └── вложенный модуль + └── группа + ├── модуль + └── группа + └── модуль +``` + +| Сущность | Назначение | Владеет ответственностью | +|---|---|---| +| Слой | Классифицирует код по архитектурной роли | Нет | +| Группа | Навигационно классифицирует модули внутри слоя | Нет | +| Модуль | Владеет одной самостоятельной ответственностью | Да | +| Домен | Специализирует модуль для предметной ответственности, контракта и ошибок | Да, как модуль | +| Сегмент | Организует внутренности одного модуля | Нет | + +Домен не добавляет уровень в структурное дерево: в слое `domains` он занимает место обычного модуля и отличается дополнительными инвариантами. + +Вложенный модуль является обычным модулем, размещённым внутри родительского. Он владеет отдельно сформулированной подответственностью и создаёт следующий рекурсивный структурный уровень. + +Слой и группа находятся снаружи модульной границы. Сегмент находится внутри неё. Framework-компоненты, Providers, Guards, hooks, stores, services и другой внутренний код не являются архитектурными сущностями SLM и принадлежат ближайшему модулю. + +## Внутренняя реализация + +Модуль может содержать любой код, относящийся к его ответственности. SLM ограничивает не набор framework-механизмов, а владение и наблюдаемую структуру: + +- помимо опционального главного framework-файла в корне, остальные компонентные единицы располагаются на одном внутреннем уровне относительно модуля; +- каталог компонентной единицы не содержит другие компонентные единицы или вложенные модули; +- рекурсивная структурная вложенность создаётся только вложенными модулями; +- помимо публичных фасетов, в корне находится не более одного главного implementation- или assembly-файла; +- если главный файл нельзя определить уверенно, реализация размещается в сегментах. + +Ограничение глубины framework-компонентов относится к файловой организации, а не к runtime-дереву фреймворка. + +## Порядок проектирования + +Архитектурное решение принимается от смысла к структуре: + +1. Описать результат, который должен получить пользователь или приложение. +2. Назначить модуль, который отвечает за этот результат. +3. Определить, что модуль делает сам, а что получает от других модулей. +4. Определить, кто использует результат работы модуля. +5. Выбрать [слой](./layers.md) по роли ответственности. +6. Спроектировать публичный API и допустимые [зависимости](./dependencies.md). +7. При необходимости классифицировать модули [группами](./groups.md), организовать внутренний код [сегментами](./segments.md) и выбрать физические пути. + +Для домена после определения сценариев сначала проектируются [доменный контракт и ожидаемые неуспешные исходы](./domains.md#порядок-создания-домена), и только затем выбираются источники данных и механизм их адаптации. + +Например, Header сам определяет расположение шапки, отображение навигации и состояние мобильного меню. Текущего пользователя он получает от модуля `auth`, а `Button` и `Avatar` — от модулей `ui`. Если удалить Header, авторизация и UI-компоненты останутся нужны приложению, поэтому Header использует их, но не владеет ими. Header может сообщить через `infra` о показе собственной раскладки, но получение пользователя и события сценария авторизации остаются ответственностью `auth`. + +Если ответственность или владелец не определены, файловая структура не может исправить архитектурную неопределённость. + +## Логическая и физическая границы + +Модуль не определяется наличием папки, `index.ts`, framework-компонента или нескольких файлов. Его определяет самостоятельная ответственность и владение её контрактом, зависимостями, состоянием и жизненным циклом. + +После принятия решения модуль получает отдельную папку и публичные точки входа. Это обязательное физическое представление модульной границы, необходимое потребителям и автоматическим проверкам. + +Верны обе формулировки: + +- самостоятельная ответственность требует модульной границы; +- отдельная папка сама по себе не доказывает наличие модуля. + +Пути сопоставляются со слоями, группами, модулями, вложенными модулями и сегментами в стайлгайде или конфигурации конкретного проекта. Имя пути не меняет нормативный смысл сущности. + +## Область применения + +SLM применяется внутри **SLM root** — границы структурной архитектуры одного приложения. Это может быть `src/` или другая область, установленная проектом. + +Архитектура определяет: + +- роли слоёв и допустимые межслойные направления; +- владельцев самостоятельных ответственностей; +- публичные границы модулей; +- общий ацикличный граф модулей; +- назначение групп и сегментов; +- внутреннюю глубину framework-компонентных единиц; +- владение состоянием и жизненным циклом ресурсов; +- независимость доменных контрактов и ошибок от внешних источников. + +SLM не задаёт обязательный поток данных, конкретный framework, фиксированные имена сегментов, правила монорепозиториев или полный файловый стайлгайд. + +Нормативный смысл терминов находится в [терминологии](../reference/terminology.md). Точные блокирующие требования объявлены только в [реестре правил](../rules/registry.md). diff --git a/skills/slm-design/reference/docs/architecture/dependencies.md b/skills/slm-design/reference/docs/architecture/dependencies.md new file mode 100644 index 0000000..6da68fd --- /dev/null +++ b/skills/slm-design/reference/docs/architecture/dependencies.md @@ -0,0 +1,138 @@ +# Зависимости + +Зависимость связывает архитектурных владельцев. Исходный файл создаёт ребро от своего ближайшего модуля к ближайшему модулю импортируемого файла. + +## Модульный граф + +Узлами архитектурного графа являются модули, включая вложенные. Группы, сегменты, 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`, предметный сценарий остаётся ответственностью доменного модуля, а техническая возможность — ответственностью инфраструктурного. + +## Допустимость связи и владение + +Матрица отвечает только на вопрос, может ли один слой зависеть от другого. Она не разрешает исходному модулю реализовывать ответственность, которая по своей роли принадлежит целевому или другому слою. + +`compositions` может зависеть от `domains` и `infra`, но использует эти направления по-разному: + +- доменный сценарий доступен композиции только как готовый публичный API доменного модуля; +- инфраструктурный API может обслуживать собственную техническую потребность композиции, например тему, локализацию или доставку метрики показа страницы; +- инфраструктурный HTTP-клиент, SDK или storage не используются композицией для реализации продуктовой операции, загрузки предметных данных или определения доменного исхода. + +```ts +// Допустимо: композиция использует готовый доменный UI. +import { OrdersList } from '@/domains/orders/client' + +// Допустимо: техническая возможность обслуживает саму композицию. +import { useTheme } from '@/infra/theme/client' + +// Направление импорта допустимо, но ответственность выбрана неверно. +import { http } from '@/infra/http' + +await http.get('/orders') +``` + +В последнем примере запрос получает предметные данные и участвует в доменном сценарии. Его смысл, параметры, продуктовые исходы и вызов принадлежат доменному модулю, который открывает композиции готовый API. + +Разрешённая зависимость `domains` от `infra` также не объединяет их контракты. Домен может использовать HTTP-транспорт, SDK или storage через публичный API инфраструктурного модуля, но DTO и ошибки источника остаются только во внутреннем интеграционном коде домена. До попадания в правила, состояние, доменный UI или публичный результат данные адаптируются к доменному контракту, а ошибка источника интерпретируется в терминах текущего сценария. Полные требования описаны в разделе [Домены](./domains.md#граница-внешних-данных). + +Аналогично, технической доставкой метрик владеет `infra`, но смысл события определяется модулем-владельцем наблюдаемого поведения. Разрешённый вызов telemetry API из композиции не позволяет ей объявлять события доменного сценария от своего имени. + +Композиция может размещать и связывать несколько доменных API. Если эта связь задаёт обязательный порядок, продуктовые условия, общий предметный результат или политику ошибок между доменами, она является отдельным доменным сценарием, а не внутренней логикой композиции. + +## Зависимости внутри слоя + +Модули одного слоя могут зависеть друг от друга в любом направлении при одновременном выполнении двух условий: + +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-DOMAIN-R022`](../rules/registry.md#slm-domain-r022) +- [`SLM-DOMAIN-R023`](../rules/registry.md#slm-domain-r023) +- [`SLM-DOMAIN-R024`](../rules/registry.md#slm-domain-r024) +- [`SLM-DOMAIN-R025`](../rules/registry.md#slm-domain-r025) +- [`SLM-DOMAIN-R026`](../rules/registry.md#slm-domain-r026) +- [`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/skills/slm-design/reference/docs/architecture/domains.md b/skills/slm-design/reference/docs/architecture/domains.md new file mode 100644 index 0000000..d9b0d38 --- /dev/null +++ b/skills/slm-design/reference/docs/architecture/domains.md @@ -0,0 +1,220 @@ +# Домены + +Домен является специализированным SLM-модулем слоя `domains`. Он владеет одной связной предметной ответственностью, её сценариями, публичным контрактом, ошибками, состоянием, доменным UI и интеграцией с источниками данных. + +Домен не создаёт новый структурный уровень над модулями. Он остаётся модулем-владельцем, отдельным узлом графа зависимостей и подчиняется всем общим [правилам модулей](./modules.md). Дополнительные правила домена защищают независимость предметной модели от внешних сервисов и технических контрактов. + +## Место в структурной модели + +```text +SLM root +└── domains # Слой + └── orders # Домен, специализированный модуль + ├── index.ts # Публичный контракт + ├── client.ts # Доменный UI при необходимости + ├── ... # Внутренняя реализация + └── modules/ # Вложенные модули при необходимости +``` + +Как обычный модуль, домен: + +- имеет одну физическую модульную границу; +- предоставляет единый логический публичный API через фасеты; +- владеет состоянием и жизненным циклом своей ответственности; +- использует другие модули только через их публичные API; +- может содержать сегменты и вложенные модули; +- участвует в общем ацикличном модульном графе. + +Корень домена не является package-контейнером или группой модулей. Сегменты, функции преобразования, компоненты и source-specific код остаются внутренней реализацией ближайшего доменного модуля и не получают самостоятельного API только из-за технической роли. + +## Что принадлежит домену + +Домен полностью определяет предметный смысл ответственности: + +- принимаемые команды, параметры и значения; +- возвращаемые модели и результаты; +- бизнес-правила, переходы и допустимые состояния; +- ожидаемые неуспешные исходы сценариев; +- смысл операций с продуктовыми данными; +- адаптацию внешних данных к доменному контракту; +- интерпретацию ошибок источника; +- состояние, доменный UI и framework-механизмы сценариев. + +Техническая возможность сохраняет собственного владельца. Например, `infra` может владеть HTTP-транспортом, SDK runtime или storage, но домен определяет, зачем выполняется операция, какие данные она принимает и возвращает и какой предметный результат получает потребитель. + +## Доменный контракт + +Доменный контракт описывает ответственность в терминах продукта, а не источника данных. Он включает публичные входы, модели, результаты, события, доступные потребителям формы состояния и ожидаемые неуспешные исходы. + +Контракт объявляется самим доменом. Даже если форма внешнего DTO временно совпадает с нужной моделью, домен создаёт собственную форму. Совпадение полей не передаёт источнику владение предметным контрактом. + +```ts +// Один из возможных способов объявить доменный контракт. +export type Order = Readonly<{ + id: OrderId + state: OrderState + total: Money +}> + +export type GetOrderInput = Readonly<{ + orderId: OrderId +}> +``` + +Недопустимо строить публичный контракт из типов источника: + +```ts +// DTO источника стал доменной моделью. +export type Order = OrdersApiDto + +// Внешний вызов стал публичным сценарием домена. +export const getOrder = ordersSdk.getOrder + +// Форма результата выводится из SDK. +export type GetOrderResult = Awaited> +``` + +Источник может измениться, не меняя доменный контракт. Если новая форма источника не позволяет выполнить уже объявленный сценарий, меняется интеграция или принимается отдельное продуктовое решение, но контракт не подгоняется автоматически под DTO. + +## Граница внешних данных + +DTO, request types, response types и ошибки источника допускаются только во внутреннем интеграционном коде домена. До использования в правилах, состоянии, доменном UI или публичном результате внешнее значение адаптируется к доменному контракту. + +Адаптация принадлежит домену, потому что только он определяет целевой предметный смысл. Она может быть реализована mapper-функцией, adapter-объектом, parser-ом или другим внутренним механизмом. SLM ограничивает результат пересечения границы, а не имя файла, функции или выбранный паттерн. Общий технический клиент при этом может принадлежать `infra`. + +```ts +// Mapper является одним из возможных механизмов адаптации. +type OrderDto = Awaited> + +const mapOrderDto = (dto: OrderDto): Order => ({ + id: dto.order_id, + state: mapOrderState(dto.status), + total: mapMoney(dto.total), +}) +``` + +Входящие и исходящие направления симметричны: + +- response источника адаптируется к доменной модели; +- доменная команда адаптируется к контракту запроса источника; +- source-specific enum, nullable semantics и служебные поля не становятся частью доменной модели автоматически; +- невалидный ответ источника получает смысл, определённый доменом; +- DTO не сохраняется как продуктовое состояние и не передаётся доменному UI. + +Внутренний механизм может быть близок к identity-преобразованию, но публичная граница остаётся независимой. Запрещены прямой реэкспорт, type alias, `Pick`, `Omit`, `ReturnType` или другое выведение публичной доменной модели из source type. + +## Доменные ошибки + +Домен самостоятельно определяет, какие неуспешные исходы его сценариев являются ожидаемыми и какой публичный контракт получают потребители. Ошибка источника не становится доменной ошибкой только потому, что была получена во время выполнения сценария. + +SLM не устанавливает способ представления или передачи доменных ошибок. Проект может использовать exception, `Result`, discriminated union, отдельные типы сценариев или другую форму. Архитектурным инвариантом остаётся владелец смысла: потребитель зависит только от контракта текущего домена. + +### Декларация и реализация + +Доменная декларация определяет допустимые неуспешные исходы до реализации сценария и подключения источника. Реализация домена конструирует и возвращает только объявленные исходы, а интеграционный код преобразует ошибки источника в уже существующий доменный контракт. + +Новый ожидаемый исход сначала добавляется в декларацию домена и только затем используется реализацией. Реализация, mapper, adapter или framework-механизм не объявляют собственные ошибки параллельно доменному контракту. + +Декларация и реализация являются ролями внутри одного доменного модуля, а не новыми структурными сущностями, обязательными сегментами или именами файлов. + +Публичный контракт ожидаемой ошибки не включает чужую ошибку в исходной форме: + +- тип или экземпляр ошибки SDK; +- код и message внешнего сервиса; +- HTTP status или другой транспортный status источника; +- raw response payload; +- `cause`, stack trace или другие source-specific диагностические данные. + +Домен может объявить собственные идентификаторы, данные для обработки и представления ошибки. Их форма, общий каталог, casing, имена полей и группировка по сценариям являются проектной policy, а не правилами SLM. Если проект выбирает машинные коды или единый union, соответствующие соглашения закрепляются в style guide и могут проверяться отдельным lint-правилом. + +В примерах этой документации доменные коды ошибок записываются в `SCREAMING_SNAKE_CASE` по соглашению команды. Это соглашение определяет оформление примеров, но не является архитектурным требованием SLM. + +```ts +// Публичная декларация домена Orders. +// Форма является project policy, а не обязательной формой SLM. +export type OrdersError = + | Readonly<{ + code: 'ORDER_NOT_FOUND' + }> + | Readonly<{ + code: 'ORDER_CANNOT_BE_CANCELLED' + payload: Readonly<{ + currentState: OrderState + }> + }> +``` + +```ts +// Внутренняя реализация использует декларацию домена. +const createOrderNotFoundError = (): OrdersError => ({ + code: 'ORDER_NOT_FOUND', +}) +``` + +### Runtime-идентификация + +SLM не требует универсального constructor, base class, marker, guard или parser для доменных ошибок. Домен предоставляет runtime-механизм идентификации только тогда, когда он необходим реальному потребителю и совместим с его средой выполнения. + +| Условия использования | Возможный механизм | +|---|---| +| Типизированный результат внутри одного TypeScript-графа | Discriminated result без дополнительного guard | +| Ошибка поступает как `unknown` через `catch` | Domain guard или class с `instanceof` | +| Ошибка пересекает JSON, SSR, RSC, worker или другую serialization boundary | Сериализуемый discriminant и runtime parser | +| Значение приходит из недоверенной среды | Schema validation | +| Ошибка не покидает доменную реализацию | Публичный runtime-механизм не нужен | + +Constructor или factory обычно остаётся внутренней частью реализации: внешние потребители распознают и обрабатывают доменные ошибки, но не создают их. Если потребителю действительно требуется runtime-идентификация, домен может открыть минимальную capability, например `isOrdersError` или `parseOrdersError`, через подходящий публичный фасет. + +Механизм идентификации не переносит владение ошибкой. Общий product-agnostic marker или guard может принадлежать `shared`, но перечень ожидаемых исходов и domain-specific проверка остаются контрактом соответствующего домена. + +## Преобразование ошибок источника + +Домен интерпретирует ошибку источника в контексте текущего сценария. Одинаковый HTTP status может означать отсутствие предметного объекта, конфликт состояния, ошибку доступа или технический сбой, поэтому транспортный признак не передаётся потребителю как готовый доменный исход. + +Ошибка источника, влияющая на публичный результат, преобразуется в собственный ожидаемый исход домена либо в неожиданный дефект согласно общей политике приложения. Raw ошибка может использоваться для внутренней диагностики и телеметрии, но не становится публичным контрактом домена. + +Если домен использует API другого домена, он также не возвращает чужой error contract от своего имени. Неуспешный исход зависимого домена интерпретируется в терминах текущего сценария. + +## Порядок создания домена + +Интеграция с источником начинается только после определения предметной границы: + +1. Сформулировать ответственность и сценарии домена. +2. Объявить доменный контракт входов, моделей и результатов. +3. Определить ожидаемые неуспешные исходы и их публичный контракт. +4. Определить границу между ожидаемым исходом и programming defect. +5. Только после этого определить внешние источники и технические зависимости. +6. Реализовать адаптацию запросов, ответов и ошибок выбранным внутренним механизмом. +7. Проверить сценарии и адаптацию источников независимо друг от друга. +8. Убедиться, что публичный API транзитивно не содержит типов источника. + +Если контракт или семантика ошибок ещё не определены, запрос к реальному источнику не считается допустимым временным началом домена. Сначала создаётся предметная граница, затем к ней адаптируется источник. + +## Проверка границы + +При ревью домена проверяется: + +- можно ли описать публичный контракт без упоминания API, endpoint, SDK или DTO; +- объявлены ли модели, результаты и ожидаемые неуспешные исходы самим доменом; +- использует ли реализация только исходы, объявленные доменной декларацией; +- не навязывает ли контракт источника форму доменной модели; +- адаптируются ли внешние значения до использования в правилах, состоянии и доменном UI; +- отсутствуют ли source types в публичных фасетах, состоянии и доменном UI; +- интерпретируются ли ошибки источника и зависимых доменов в терминах текущего сценария; +- не протекают ли наружу чужие error types, codes, messages, transport statuses, raw payload или cause; +- не маскируется ли programming defect под ожидаемый доменный исход; +- нужен ли реальным потребителям runtime-механизм идентификации и совместим ли он с их средой; +- не экспортирует ли домен constructor, guard, parser или schema без реального потребителя; +- не объявлена ли выбранная форма error contract универсальным требованием SLM. + +## Связанные правила + +- [`SLM-DOMAIN-R022`](../rules/registry.md#slm-domain-r022) +- [`SLM-DOMAIN-R023`](../rules/registry.md#slm-domain-r023) +- [`SLM-DOMAIN-R024`](../rules/registry.md#slm-domain-r024) +- [`SLM-DOMAIN-R025`](../rules/registry.md#slm-domain-r025) +- [`SLM-DOMAIN-R026`](../rules/registry.md#slm-domain-r026) +- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004) +- [`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) diff --git a/skills/slm-design/reference/docs/architecture/groups.md b/skills/slm-design/reference/docs/architecture/groups.md new file mode 100644 index 0000000..2d3847b --- /dev/null +++ b/skills/slm-design/reference/docs/architecture/groups.md @@ -0,0 +1,84 @@ +# Группы + +Группа является необязательным навигационным классификатором модулей внутри одного слоя. Она помогает ориентироваться в дереве владельцев, но не реализует ответственность и не создаёт архитектурную границу. + +## Место в модели + +Модуль может находиться непосредственно в слое или внутри одной или нескольких групп: + +```text +слой +├── модуль +└── группа + ├── модуль + └── группа + └── модуль +``` + +Группа создаётся, когда плоский список модулей перестаёт быть понятным. Названия и глубину групп определяет проект. + +```text +compositions/ +├── pages/ # Группа +│ ├── catalog/ # Модуль +│ └── profile/ # Модуль +├── layouts/ # Группа +│ └── main/ # Модуль +└── widgets/ # Группа + └── dashboard/ # Модуль, компонующий несколько доменных API +``` + +Названия `pages`, `layouts` и `widgets` показывают один из вариантов навигации и не создают дополнительные слои или обязательные роли. + +Модули `catalog`, `profile` и `dashboard` в примере отвечают только за представление и связывание готовых публичных API. Сценарии каталога, профиля и других предметных областей остаются в соответствующих модулях `domains`. + +## Ограничения группы + +Группа: + +- содержит только модули и вложенные группы; +- не владеет файлами реализации; +- не имеет состояния или жизненного цикла; +- не предоставляет публичный API; +- не является узлом графа зависимостей; +- не импортируется внешним кодом; +- не реэкспортирует содержащиеся в ней модули. + +Barrel-файл, открывающий несколько модулей группы как единый контракт, превращает каталог в новую модульную границу. Если такой контракт действительно нужен, для него определяется ответственность и создаётся обычный модуль. + +## Группы и зависимости + +Принадлежность модулей одной или разным группам не влияет на допустимость импорта. Модули одного слоя могут зависеть друг от друга через публичные API, если общий граф остаётся ацикличным. + +Код импортирует конкретный модуль: + +```ts +import { Dashboard } from '@/compositions/widgets/dashboard' +``` + +Группа не становится промежуточной точкой доступа: + +```ts +// Недопустимый API группы +import { Dashboard } from '@/compositions/widgets' +``` + +Полные правила графа находятся в разделе [Зависимости](./dependencies.md). + +## Группа и сегмент + +Группа организует несколько модулей внутри слоя. [Сегмент](./segments.md) организует код внутри одного модуля. + +| Группа | Сегмент | +|---|---| +| Находится снаружи модульной границы | Находится внутри модульной границы | +| Содержит модули и группы | Содержит внутреннюю реализацию владельца | +| Не принадлежит одному модулю | Всегда принадлежит ближайшему модулю | +| Не содержит файлы реализации | Существует для организации файлов реализации | + +## Связанные правила + +- [`SLM-DOMAIN-R022`](../rules/registry.md#slm-domain-r022) +- [`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/skills/slm-design/reference/docs/architecture/layers.md b/skills/slm-design/reference/docs/architecture/layers.md new file mode 100644 index 0000000..9f7c1ca --- /dev/null +++ b/skills/slm-design/reference/docs/architecture/layers.md @@ -0,0 +1,112 @@ +# Слои + +Слой классифицирует код по архитектурной роли. Слой не владеет ответственностью: владельцем остаётся модуль, размещённый в этом слое. + +Слой выбирается после определения ответственности. Похожее имя папки или наличие зависимости от конкретной библиотеки не являются основанием для выбора слоя. + +## Роли слоёв + +SLM определяет шесть ролей: + +| Слой | Роль | +|---|---| +| `app` | Связь приложения с фреймворком: запуск, маршруты, преобразование внешних входных данных и подключение готовых публичных API | +| `compositions` | Представление и связывание готовых публичных API в страницы, макеты, экраны, виджеты и другие продуктовые композиции | +| `domains` | Полная реализация предметных ответственностей и сценариев, включая их модели, правила, состояние, операции с продуктовыми данными и доменный UI | +| `infra` | Технические сервисы и возможности приложения без собственной предметной модели | +| `ui` | Универсальные интерфейсные модули без зависимости от конкретной продуктовой композиции | +| `shared` | Детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода | + +Отсутствующая роль не требует пустой папки. Проект создаёт слой только тогда, когда в нём появляется соответствующая ответственность. + +### App + +`app` содержит точки связи с фреймворком: запуск, файлы маршрутов и преобразование внешних входных данных. Они подключают готовые публичные API других слоёв, но не присваивают их ответственность. + +Точки входа `app` являются специальным немодульным исключением. Самостоятельная продуктовая ответственность, даже если она представлена страницей, макетом или Provider, реализуется в подходящем модуле и только подключается из `app`. + +### Compositions + +`compositions` содержит владельцев представления продуктовых композиций: страниц, макетов, экранов, виджетов, результатов маршрутов и интерфейса, объединяющего несколько готовых модульных возможностей. Композиция размещает и связывает публичные API доменных, инфраструктурных и UI-модулей, но не присваивает их ответственность. + +Модуль композиции может владеть структурой страницы, расположением частей интерфейса и состоянием, смысл которого существует только внутри этой композиции. Он не определяет, не реализует, не расширяет и не замещает доменный сценарий. Количество потребителей и использование сценария только на одной странице не меняют эту границу. + +Конкретная организация слоя определяется продуктом и фреймворком. Названия `pages`, `layouts`, `screens` и `widgets` могут использоваться как [группы](./groups.md), но не являются дополнительными слоями и не задают направление импортов. + +### Domains + +`domains` содержит модули-владельцы полных предметных ответственностей и доменных сценариев. Доменный модуль владеет не только моделями и бизнес-правилами, но и продуктовым состоянием, смыслом операций с продуктовыми данными, предметными исходами, доменным UI и framework-механизмами, которые обслуживают сценарий. + +Домен является вертикальным владельцем ответственности, а не только каталогом независимой от интерфейса бизнес-логики. Внутри него могут находиться компоненты, Providers, hooks, stores, services и другой код, если он реализует принадлежащий домену результат. Универсальные визуальные элементы домен получает из `ui`, а технические возможности без предметной модели — из `infra`. + +Домен является специализированным SLM-модулем: он сохраняет обычную модульную форму и получает дополнительные требования к предметному контракту, адаптации источников и ошибкам. Полная модель описана в разделе [Домены](./domains.md). + +### Infra + +`infra` содержит технические возможности приложения: аналитику, локализацию, тему, телеметрию, интеграции с платформой и другие сервисы без собственной предметной модели. + +Технический способ выполнения предметного сценария не переносит владение сценарием из `domains` в `infra`. + +### UI + +`ui` содержит универсальные интерфейсные модули, которые не знают о конкретной странице, маршруте или продуктовой композиции. + +### Shared + +`shared` содержит детерминированный фундамент, не зависящий от продукта и не имеющий ввода-вывода, изменяемого состояния или жизненного цикла. + +В `shared` могут находиться обычные модули и небольшие немодульные ресурсы: чистые функции, общие типы, стили, декларативная конфигурация и статические файлы. + +## Граница доменов и композиций + +Граница определяется смыслом поведения, а не местом его вызова или отображения: + +| Код определяет | Слой-владелец | +|---|---| +| Продуктовую операцию, правило, переход, предметный исход или состояние | `domains` | +| Получение или изменение продуктовых данных, параметры операции и смысл её ошибок | `domains` | +| Форму, список, карточку, Guard или другой UI, выраженный в терминах одного домена | `domains` | +| Расположение и связывание готовых публичных API на странице или экране | `compositions` | +| Состояние панели, секции или раскладки, имеющее смысл только в одной композиции | `compositions` | +| Универсальный визуальный элемент без предметного смысла | `ui` | +| HTTP-транспорт, тему, локализацию или техническую доставку телеметрии | `infra` | + +Если для нового доменного сценария ещё нет подходящего владельца, сначала выбирается существующий или создаётся новый модуль в `domains`. Реализация сценария в `compositions` с намерением перенести её позже не является допустимым промежуточным архитектурным решением. + +Композиция может показать рядом несколько доменных возможностей и вызвать их готовые публичные операции. Если координация определяет продуктовый порядок действий, условия, общий предметный результат, политику ошибок или компенсацию между доменами, такая координация сама является доменным сценарием и требует владельца в `domains`. + +Разрешённая зависимость `compositions` от `infra` сохраняется. Композиция может использовать тему, локализацию, доставку метрик и другие технические возможности для собственной ответственности. Эта связь не разрешает получать или изменять продуктовые данные через HTTP-клиент, SDK или storage непосредственно из композиции: техническим механизмом владеет `infra`, а смысл такой операции и её продуктовый результат принадлежат домену. + +Смысл события метрики принадлежит владельцу наблюдаемого поведения. Домен определяет событие доменного сценария, композиция — событие показа или взаимодействия со своей раскладкой, `app` — событие запуска или маршрутизации, а `infra` отвечает за техническую доставку телеметрии. + +## Направление зависимостей + +Слой ограничивает только межслойное направление. Модули одного слоя могут зависеть друг от друга через публичные API, если общий граф остаётся ацикличным. + +Нормативная матрица, правила same-layer импортов и требования к lint-проверке находятся в разделе [Зависимости](./dependencies.md). + +## Группировка + +Модули могут находиться непосредственно в слое или объединяться в необязательные навигационные [группы](./groups.md). Группа не владеет кодом и не влияет на допустимость зависимостей. + +## Немодульные исключения + +Внутри SLM root код по умолчанию принадлежит модулю. Исключения ограничены двумя случаями: + +- точка входа `app` непосредственно связывает приложение с фреймворком; +- ресурс `shared` является небольшой самостоятельной детерминированной единицей без внутренней границы. + +Если ресурсу `shared` нужны несколько файлов реализации, собственные архитектурные зависимости, изменяемое состояние, ввод-вывод или жизненный цикл, ему требуется модуль-владелец. + +## Связанные правила + +- [`SLM-LAYER-R001`](../rules/registry.md#slm-layer-r001) +- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002) +- [`SLM-LAYER-R003`](../rules/registry.md#slm-layer-r003) +- [`SLM-DOMAIN-R022`](../rules/registry.md#slm-domain-r022) +- [`SLM-DOMAIN-R023`](../rules/registry.md#slm-domain-r023) +- [`SLM-DOMAIN-R024`](../rules/registry.md#slm-domain-r024) +- [`SLM-DOMAIN-R025`](../rules/registry.md#slm-domain-r025) +- [`SLM-DOMAIN-R026`](../rules/registry.md#slm-domain-r026) +- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007) +- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011) diff --git a/skills/slm-design/reference/docs/architecture/modules.md b/skills/slm-design/reference/docs/architecture/modules.md new file mode 100644 index 0000000..40bb059 --- /dev/null +++ b/skills/slm-design/reference/docs/architecture/modules.md @@ -0,0 +1,253 @@ +# Модули + +Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или механизме выполняется код. + +## Ответственность и владелец + +Перед созданием модуля ответственность формулируется без упоминания желаемой папки, файла или библиотеки. + +Самостоятельность ответственности определяется вопросами: + +- какой один результат или поведение она обеспечивает; +- что модуль должен делать сам для получения этого результата; +- какие возможности ему нужны от других модулей; +- нужен ли внешним потребителям собственный контракт; +- требуют ли зависимости отдельного архитектурного владения; +- владеет ли она смыслом данных или изменяемого состояния; +- нужна ли ей собственная область жизни; +- можно ли назвать её независимо от внутренней реализации. + +Props, импорты, Context, локальное состояние, lifecycle-код или количество файлов сами по себе не доказывают самостоятельность ответственности. Если ответственность самостоятельна, она получает ровно один модуль-владелец. + +Для доменного сценария выбор владельца дополнительно ограничен ролью слоя: такой сценарий принадлежит модулю `domains`. Этот модуль является [доменом](./domains.md) и дополнительно владеет предметным контрактом, ожидаемыми неуспешными исходами и адаптацией источников. Модуль `compositions` может владеть представлением страницы или экрана и использовать готовый доменный API, но не становится владельцем сценария из-за места вызова, единственного потребителя или отсутствия уже созданного доменного модуля. Подробная граница описана в разделе [Слои](./layers.md#граница-доменов-и-композиций). + +Размер не определяет модуль. Модуль может состоять из одного файла реализации, а большая папка может оставаться частью другого владельца. + +## Ближайшая граница + +Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Вид кода не меняет владельца: 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 +``` + +Родитель и вложенный модуль не владеют одной ответственностью одновременно. Родитель определяет общий результат, а вложенный модуль — отдельно сформулированную связную часть этого результата. + +## Граница владения + +Модуль определяет: + +- публичные возможности ответственности; +- допустимые внешние зависимости; +- модели и правила, принадлежащие ответственности; +- состояние и источник истины; +- создание и очистку долгоживущих ресурсов; +- устройство внутренней реализации. + +Внутренний файл может технически выполнять часть или всю эту работу. Архитектурное владение остаётся у модуля и не переносится в компонент, Provider, store или другой механизм реализации. + +## Публичный API + +У модуля один логический публичный API. Он является контрактом между владельцем и внешними потребителями, а не перечнем всех внутренних файлов. + +Публичный API: + +- открывает только возможности, необходимые реальным внешним потребителям; +- скрывает детали реализации и изменяемые внутренние механизмы; +- не раскрывает внутренние сегменты; +- представлен объявленными публичными фасетами; +- является единственным способом доступа к модулю извне. + +Внутри своей границы модуль может обращаться к собственным файлам напрямую. Требование публичного API действует при пересечении модульной границы. + +### Фасеты + +Фасет — публичная точка входа, открывающая часть единого API для определённой среды выполнения. + +| Фасет | Назначение | +|---|---| +| `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением | +| `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере | +| `browser` | Browser-only и динамически подключаемый клиентский код; фасет всегда импортируется динамически с отключённым SSR | +| `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения | + +`index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде. + +Ограничение среды распространяется на все исполняемые импорты и реэкспорты фасета, включая транзитивные. Имя файла, директива `use client`, tree shaking или проверка `typeof window` сами по себе не доказывают совместимость. + +Один исполняемый экспорт размещается в минимально подходящем фасете и не дублируется между ними. Универсальный фасет не реэкспортирует специализированные фасеты. + +```text +auth/ +├── index.ts # Обязательный универсальный фасет +├── client.ts # При необходимости +├── browser.ts # При необходимости +├── server.ts # При необходимости +└── ... # Внутренняя реализация +``` + +Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль. + +## Зависимости + +Модули одного слоя могут зависеть друг от друга. При пересечении любой модульной границы код использует публичный фасет целевого модуля, а общий граф модулей должен оставаться ацикличным. + +```ts +// Допустимо +import { Button } from '@/ui/button' + +// Недопустимый глубокий импорт +import { Button } from '@/ui/button/button' +``` + +Направления между слоями, same-layer импорты, роль групп и требования к lint-проверке описаны в разделе [Зависимости](./dependencies.md). + +## Корень модуля + +Корень модуля не используется как плоский каталог реализации. В нём находятся: + +- объявленные публичные фасеты; +- не более одного опционального главного implementation- или assembly-файла. + +Главный файл непосредственно реализует или собирает ответственность модуля и однозначно соответствует ей по имени и роли. Возможные примеры: + +```text +header/header.tsx +footer/footer.tsx +auth-guard/auth-guard.provider.tsx +``` + +Архитектурным владельцем остаётся модуль, а главный файл является основной реализацией или точкой её сборки. Если проблемно доказать, что файл является главным, его не выносят в корень. Вся остальная реализация размещается в [сегментах](./segments.md). + +Главный 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, собственные зависимости и область жизни и подчиняется всем правилам модулей. + +```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. + +Для каждого долгоживущего ресурса модуль-владелец определяет: + +- место создания; +- момент запуска; +- область жизни; +- допустимое число экземпляров; +- способ остановки, отмены или освобождения. + +Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить framework-компонент или сам фреймворк, но ответственность за корректную границу остаётся у модуля. + +Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем. + +## Связанные правила + +- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004) +- [`SLM-MODULE-A014`](../rules/registry.md#slm-module-a014) +- [`SLM-DOMAIN-R022`](../rules/registry.md#slm-domain-r022) +- [`SLM-DOMAIN-R023`](../rules/registry.md#slm-domain-r023) +- [`SLM-DOMAIN-R024`](../rules/registry.md#slm-domain-r024) +- [`SLM-DOMAIN-R025`](../rules/registry.md#slm-domain-r025) +- [`SLM-DOMAIN-R026`](../rules/registry.md#slm-domain-r026) +- [`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-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) +- [`SLM-ENVIRONMENT-R017`](../rules/registry.md#slm-environment-r017) +- [`SLM-ENVIRONMENT-R018`](../rules/registry.md#slm-environment-r018) +- [`SLM-ENVIRONMENT-R019`](../rules/registry.md#slm-environment-r019) diff --git a/skills/slm-design/reference/docs/architecture/segments.md b/skills/slm-design/reference/docs/architecture/segments.md new file mode 100644 index 0000000..40ea484 --- /dev/null +++ b/skills/slm-design/reference/docs/architecture/segments.md @@ -0,0 +1,106 @@ +# Сегменты + +Сегмент организует внутреннее содержимое одного модуля по назначению. Он помогает ориентироваться в реализации владельца, но не создаёт новую ответственность или архитектурную границу. + +## Место в модели + +Сегмент появляется только внутри уже определённого модуля: + +```text +Слой → [Группа*] → Модуль → [Сегмент*] +``` + +Группа классифицирует модули внутри слоя. Сегмент классифицирует код одного модуля. Ни группа, ни сегмент не являются владельцами. + +Все файлы, framework-компоненты, состояние, зависимости и lifecycle-код сегмента принадлежат ближайшему модулю. Исключением является только вложенный модуль, который начинает собственную границу владения. + +## Назначение + +Сегмент используется, когда группировка внутренних файлов по назначению упрощает навигацию. Набор и названия сегментов определяет проект. + +```text +profile/ +├── index.ts # Публичный фасет +├── profile.tsx # Главная реализация +├── components/ # Возможный сегмент +├── hooks/ # Возможный сегмент +├── services/ # Возможный сегмент +├── stores/ # Возможный сегмент +├── types/ # Возможный сегмент +└── styles/ # Возможный сегмент +``` + +Ни один сегмент не создаётся заранее. Модуль может обойтись без сегментов, если помимо публичных фасетов содержит только один главный implementation- или assembly-файл, однозначно выражающий его ответственность. Любая остальная реализация размещается в подходящих сегментах. Если главный файл нельзя определить уверенно, вся реализация остаётся в сегментах. + +Сегмент: + +- не имеет самостоятельной ответственности; +- не предоставляет публичный API; +- не владеет состоянием или жизненным циклом; +- не является узлом графа зависимостей; +- не импортируется внешним кодом как отдельная архитектурная сущность. + +Локальный `index.ts` может использоваться во внутренней единице сегмента. Он не превращает эту единицу или сегмент в модульную границу. + +## Framework-компоненты + +Framework-компоненты являются обычным внутренним кодом модуля. Они могут выполнять визуальные и невизуальные роли, включая Provider, Guard или Error Boundary, если используемый фреймворк считает соответствующую сущность компонентом. + +Помимо опционального главного framework-файла в корне, остальные компонентные единицы размещаются на одном внутреннем уровне относительно модуля. Каталог такой единицы может содержать локальные `styles`, `types`, `hooks`, `tests` и внутренний `index.ts`, но не содержит другие компонентные единицы или вложенные модули. + +```text +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 и границу зависимостей. + +```text +landing/ # Родительский модуль +└── modules/ # Сегмент + └── hero/ # Вложенный модуль + ├── index.ts # Публичный фасет вложенного модуля + ├── hero.tsx # Главная реализация hero + └── modules/ # Допустимая модульная рекурсия + └── media/ + └── index.ts +``` + +Компонентный каталог не содержит `components` или `modules`. Рекурсивная структурная вложенность допускается только через вложенные модули. Имена `components` и `modules` являются примерами локального стайлгайда, а не обязательными соглашениями SLM. + +## Выбор размещения + +| Ситуация | Решение | +|---|---| +| Код относится к существующему владельцу и группируется по назначению | Сегмент | +| Вспомогательный код нужен только одной компонентной единице | Колоцировать в её локальном каталоге | +| Выделена отдельная framework-компонентная единица | Разместить на общем внутреннем уровне модуля | +| Код нужен нескольким внутренним единицам модуля | Поднять в ближайший общий сегмент | +| Появилась самостоятельная связная подответственность | Создать вложенный модуль | +| Файл однозначно является главной реализацией или сборкой ответственности | Допустимо разместить в корне модуля | +| Файл не является главным или его роль неоднозначна | Разместить в подходящем сегменте | + +Размер каталога и количество файлов не определяют модульную границу. Её создаёт только самостоятельная ответственность и назначение нового владельца. + +## Связанные правила + +- [`SLM-SEGMENT-R008`](../rules/registry.md#slm-segment-r008) +- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004) +- [`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/skills/slm-design/reference/docs/reference/terminology.md b/skills/slm-design/reference/docs/reference/terminology.md new file mode 100644 index 0000000..8434e51 --- /dev/null +++ b/skills/slm-design/reference/docs/reference/terminology.md @@ -0,0 +1,143 @@ +# Терминология SLM + +Этот документ задаёт нормативный смысл терминов. Определения используются при толковании архитектуры и правил, но сами по себе не являются отдельными правилами. + +## Владение + +### SLM root + +Граница структурной архитектуры одного приложения. Внутри неё определяются владельцы ответственностей, слои, модули и их зависимости. + +### Ответственность + +Результат или поведение приложения, за которое отвечает один модуль-владелец. Ответственность является самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни, а не только внутренняя роль в работе другого модуля. Наличие у framework-сущности props, импортов, локального состояния или lifecycle-кода само по себе не создаёт самостоятельную ответственность. + +### Доменный сценарий + +Продуктово значимое поведение, сформулированное в предметных терминах и приводящее к предметному результату. Его владелец определяет модели, правила, переходы, продуктовое состояние, смысл операций с продуктовыми данными, допустимые исходы и доменный UI. Количество потребителей, текущая страница и технический механизм выполнения не меняют принадлежность сценария. + +Техническая возможность, которую сценарий получает через публичный API другого модуля, сохраняет собственного владельца. Например, `infra` может владеть HTTP-транспортом или доставкой телеметрии, но смысл продуктовой операции и доменного события остаётся у доменного сценария. + +### Доменный UI + +UI-код, чьи данные, действия, состояния или исходы выражены в терминах одного домена и представляют либо запускают его сценарий. Доменный UI является частью реализации доменной ответственности даже тогда, когда используется только одной страницей. Универсальные визуальные элементы принадлежат `ui`, а размещение и связывание готовых доменных API в страницу или экран принадлежит `compositions`. + +### Владелец + +Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннюю реализацию. Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Место выполнения кода или вид framework-сущности не переносит владение. + +## Структурные сущности + +### Слой + +Архитектурная роль кода внутри SLM root. Слой классифицирует владельцев по назначению и ограничивает допустимые направления зависимостей. Нормативные роли и матрица определены в разделе [Слои](../architecture/layers.md). + +### Группа + +Необязательный навигационный классификатор модулей внутри одного слоя или другой группы. Группа не является владельцем, публичным API или границей зависимостей. + +### Модуль + +Минимальная самостоятельная архитектурная единица SLM. Модуль владеет одной связной ответственностью, имеет публичный API и физически размещается в отдельной папке. + +### Домен + +Специализированный модуль слоя `domains`, владеющий одной связной предметной ответственностью и её сценариями. Домен самостоятельно определяет публичный доменный контракт, ожидаемые неуспешные исходы, продуктовое состояние, доменный UI и адаптацию внешних данных и ошибок. Он остаётся обычным узлом модульного графа, подчиняется всем правилам модулей и не создаёт дополнительный контейнерный уровень. + +### Сегмент + +Необязательная внутренняя часть одного модуля, группирующая его содержимое по назначению. Сегмент не является владельцем, публичным API или границей зависимостей. + +### Вложенный модуль + +Обычный модуль, физически размещённый внутри родительского модуля. Он владеет отдельно сформулированной связной частью ответственности родителя, имеет публичный API и собственную границу зависимостей. Родитель владеет общим результатом, а вложенный модуль — выделенной подответственностью; одна и та же ответственность не получает двух владельцев. + +Для кода за пределами родительской границы вложенный модуль остаётся внутренней реализацией родителя. Внутри вложенного модуля снова действуют все правила обычного модуля, поэтому рекурсивная структурная вложенность создаётся только модульными границами. + +## Публичная граница + +### Публичный API + +Единый логический контракт внешнего доступа к модулю. Он скрывает внутреннюю реализацию и физически представлен обязательным фасетом `index` и только необходимыми фасетами `client`, `browser` и `server`. + +### Доменный контракт + +Принадлежащая домену предметная форма его публичного API: принимаемые значения, возвращаемые модели и результаты, события, доступные потребителям состояния и ожидаемые неуспешные исходы. Доменный контракт определяется смыслом сценариев и не выводится из DTO, схемы, SDK или типов источника данных. + +### Фасет + +Объявленная публичная точка входа модуля, открывающая часть его единого API для определённой среды выполнения. Импорт фасета не является глубоким импортом; любой другой внешний путь внутрь модуля остаётся внутренним. + +### Глубокий импорт + +Импорт или реэкспорт внутреннего пути чужого модуля, который не объявлен его публичным фасетом. + +## Граница источника данных + +### Контракт источника + +Техническая форма обмена с внешним сервисом, SDK, storage или другим источником данных. К ней относятся request и response DTO, source-specific enum, nullable semantics, статусы, payload и типы ошибок. Контракт источника не является доменным контрактом даже при полном структурном совпадении. + +### DTO + +Значение или тип контракта источника, предназначенный для передачи данных через техническую границу. DTO допускается во внутреннем интеграционном коде домена, но не используется как публичная модель, продуктовое состояние или значение доменного UI. + +### Mapper + +Один из возможных внутренних механизмов адаптации источника: функция или связный набор функций, преобразующий контракт источника в доменный контракт либо доменное значение в контракт запроса. Адаптация принадлежит доменному владельцу, но SLM не требует использовать mapper как конкретный паттерн, имя или файловую единицу. + +## Доменные ошибки + +### Доменная ошибка + +Ожидаемый неуспешный исход доменного сценария, смысл и публичный контракт которого определены текущим доменом. Способ представления и передачи такого исхода, включая exception, `Result`, union или другую форму, SLM не устанавливает. + +Доменная ошибка не является технической ошибкой источника. Чужой тип ошибки, source code, message, transport status, raw payload, `cause` и диагностические данные не входят в доменный контракт в исходной форме. + +Реализация домена создаёт только исходы, объявленные доменным контрактом. Интеграционный или framework-код использует эту декларацию и не становится отдельным владельцем ошибок. + +### Runtime-идентификация доменной ошибки + +Публичная capability, позволяющая реальному потребителю отличить доменную ошибку от другого runtime-значения. Она может быть реализована constructor-ом, marker-ом, guard-ом, parser-ом, schema или иным способом, подходящим среде потребителя. SLM не требует такую capability для каждого домена и не устанавливает её форму. + +### Неожиданный дефект + +Неуспешное выполнение, которое не объявлено ожидаемым исходом доменного сценария и обрабатывается согласно общей политике приложения. Способ доставки и диагностики дефекта SLM не устанавливает, но техническая ошибка чужого источника не становится частью публичного API домена в исходной форме. + +## Зависимости + +### Зависимость + +Направленная статическая связь между архитектурными границами внутри одного SLM root. Обычный импорт, импорт типа (`import type`) и реэкспорт одинаково создают архитектурную зависимость. + +Зависимость внутреннего файла или сегмента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей. + +### Нормативная матрица слоёв + +Отношение допустимой зависимости между слоями одного SLM root. Матрица определяет доступные целевые роли, но не требует проходить через каждый промежуточный слой. + +## Жизненный цикл + +### Область жизни + +Период, в течение которого принадлежащие модулю состояние или долгоживущий ресурс должны оставаться активными. + +### Ресурс жизненного цикла + +Ресурс, работа которого продолжается после первоначального вызова и требует остановки, отмены, отписки или освобождения. Например, подписка, обработчик событий, таймер, наблюдатель, запрос или соединение. + +### Очистка + +Гарантированное прекращение работы ресурса не позже завершения его области жизни. Автоматическая очистка фреймворка считается очисткой владельца, если модуль устанавливает и контролирует соответствующую границу. + +## Немодульные единицы + +### Точка входа фреймворка + +Специальная немодульная единица слоя `app`, которая запускает приложение, объявляет точку маршрута, преобразует внешние входные данные или подключает готовые публичные API. + +### Ресурс shared + +Небольшая детерминированная единица слоя `shared`, не зависящая от продукта и не скрывающая отдельного внутреннего устройства. У неё нет изменяемого состояния, ввода-вывода, области жизни или собственного публичного API. + +Путь и имя сами по себе не определяют ни одну из перечисленных сущностей. Физическое сопоставление задаётся стайлгайдом или конфигурацией проверки после определения ответственности и владельца. diff --git a/skills/slm-design/reference/docs/reference/validation.md b/skills/slm-design/reference/docs/reference/validation.md new file mode 100644 index 0000000..cff4a7e --- /dev/null +++ b/skills/slm-design/reference/docs/reference/validation.md @@ -0,0 +1,167 @@ +# Проверка архитектуры + +Проверка SLM подтверждает две разные стороны решения: + +- смысловая проверка устанавливает ответственность, владельца и корректность границ; +- структурная проверка подтверждает, что решение правильно выражено путями, публичными фасетами и зависимостями. + +Успешная сборка или корректно отображаемый интерфейс не доказывают архитектурную корректность. + +## Карточка решения + +Перед изменением структуры нужно ответить: + +| Вопрос | Что зафиксировать | +|---|---| +| Ответственность | Какой результат или поведение изменяется как единое целое | +| Доменный сценарий | Какой модуль `domains` владеет предметным результатом и через какой API его используют внешние потребители, включая композиции | +| Доменный контракт | Какие входы, модели и результаты определены предметным смыслом независимо от источника данных | +| Доменные ошибки | Какие ожидаемые неуспешные исходы определяет домен и где проходит граница с programming defects | +| Граница источника | Какие внешние контракты получает домен и как они адаптируются к предметному смыслу | +| Владелец | Какой модуль определяет контракт и внутреннюю реализацию | +| Слой | Какой архитектурной роли соответствует ответственность | +| Потребители | Кому действительно нужен публичный API | +| Зависимости | Какие другие владельцы и возможности необходимы | +| Состояние | Кто определяет смысл и допустимые изменения данных | +| Жизненный цикл | Кто создаёт ресурсы, какова их область жизни и очистка | +| Физическая форма | Какими путями и фасетами представлено принятое решение | + +Если ответственность или владелец не определены, проверка путей откладывается: одинаковая файловая структура может представлять разные архитектурные решения. + +## Архитектурное ревью + +На ревью проверяется смысл кода, который нельзя надёжно вывести из файловой системы: + +- одна ли связная ответственность находится внутри модульной границы; +- есть ли у каждой самостоятельной ответственности ровно один ближайший владелец; +- принадлежит ли каждый доменный сценарий модулю слоя `domains`; +- находятся ли бизнес-правила, продуктовое состояние, смысл операций с предметными данными, предметные исходы и доменный UI у владельца сценария; +- только ли использует и компонует модуль `compositions` готовые публичные API доменов, не определяя и не дополняя их сценарии; +- не размещён ли сценарий временно в композиции только потому, что подходящий доменный модуль ещё не создан; +- не скрывает ли связывание нескольких доменных API новый порядок, условие, общий предметный результат или политику ошибок; +- обслуживает ли прямое использование `infra` собственную техническую потребность композиции, а не продуктовую операцию или доступ к предметным данным; +- соответствует ли ответственность роли выбранного слоя; +- владеет ли вложенный модуль отдельно сформулированной подответственностью; +- не стали ли группа или сегмент скрытыми владельцами; +- реализует ли внутренний код ответственность ближайшего модуля; +- не принимаются ли props, Context, локальное состояние или lifecycle-код за достаточное основание для новой модульной границы; +- является ли главный файл в корне однозначной реализацией или сборкой ответственности; +- не лежат ли прочие файлы реализации в корне вместо подходящих сегментов; +- нужен ли каждый экспорт реальному внешнему потребителю; +- не раскрывает ли публичный API изменяемые внутренние механизмы; +- определены ли владелец, область жизни, число экземпляров и очистка каждого ресурса. + +Окончательные смысловые требования имеют класс `R` в [реестре правил](../rules/registry.md). + +Вызовы `fetch`, HTTP-клиента, SDK, query client или storage внутри `compositions` являются сигналами для проверки, но не самостоятельным доказательством нарушения. Ревью устанавливает, обслуживает ли вызов техническую ответственность самой композиции или реализует доменный сценарий в обход его владельца. + +## Проверка домена + +Для каждого создаваемого или изменяемого домена дополнительно проверяется: + +- остаётся ли домен одним специализированным модулем и узлом графа, а не неявным контейнером нескольких владельцев; +- объявлены ли входы, модели и результаты самим доменом до подключения источника; +- можно ли описать доменный контракт без упоминания endpoint, SDK, DTO или схемы внешнего сервиса; +- не выведен ли публичный тип через alias, наследование, `Pick`, `Omit`, `ReturnType` или другой source type; +- определены ли ожидаемые неуспешные исходы самим доменом; +- создаёт ли реализация только исходы, объявленные доменным контрактом; +- не определяют ли mapper, adapter или framework-код независимые ошибки параллельно декларации домена; +- зависит ли потребитель только от error contract текущего домена независимо от выбранной формы его представления; +- не выдаётся ли project policy о code, payload, union или casing за универсальное правило SLM; +- отсутствуют ли в публичной ошибке чужие error type, source code, message, transport status, raw payload и `cause`; +- адаптируются ли request и response источника выбранным внутренним механизмом; +- попадают ли в правила, состояние и доменный UI только значения доменного контракта; +- интерпретируется ли каждая ошибка источника и зависимого домена в терминах текущего сценария; +- нужен ли runtime-механизм идентификации реальным потребителям и совместим ли он с их средой выполнения; +- не экспортируется ли constructor, guard, parser или schema без доказанной потребности; +- не замаскирована ли programming defect под ожидаемую доменную ошибку. + +Прямой импорт source type во внутренний код адаптации сам по себе допустим. Нарушением является его достижимость из публичного фасета, использование как доменной модели или состояния либо передача потребителю без преобразования. + +Интеграционный код ревьюится только после определения доменного контракта и семантики ожидаемых ошибок. Запрос к реальному источнику не считается допустимой временной реализацией домена, если предметная граница ещё не объявлена. + +## Автоматическая проверка + +Проект сопоставляет физические пути с SLM root, слоями, группами, модулями, вложенными модулями, сегментами, фасетами, точками входа `app` и ресурсами `shared`. Сопоставление задаётся локальной конфигурацией и не меняет смысл сущностей. + +Наличие `index.ts` само по себе не объявляет модуль. Проверка отличает объявленные корни модулей и их публичные фасеты от локальных точек входа и других внутренних единиц. + +Автоматически проверяются: + +- допустимое направление импортов по матрице слоёв; +- отдельная папка каждого модуля; +- доступ к чужому модулю только через объявленные фасеты; +- отсутствие циклов в свёрнутом модульном графе; +- отсутствие прямого внешнего доступа к вложенным модулям; +- допустимый транзитивный граф исполняемых импортов каждого фасета среды выполнения; +- динамическое подключение `browser`-фасета с отключённым SSR. + +Каждое правило класса `A` должно полностью блокировать проверку при нарушении. SLM не требует конкретного lint-инструмента. + +## Проверка зависимостей + +Для каждого внешнего импорта определяется: + +1. Ближайший модуль-владелец исходного файла. +2. Ближайший модуль-владелец целевого файла. +3. Слои исходного и целевого владельцев. +4. Публичный фасет, через который выполнен импорт. +5. Отсутствие цикла после добавления межмодульного ребра. + +Импорты типов (`import type`) и реэкспорты проверяются как архитектурные связи. Импорты файлов одного модуля сворачиваются и не создают межмодульного ребра. Вложенный модуль считается отдельным узлом. + +File-level проверка не заменяет модульную: два модуля могут зависеть друг от друга через разные файлы без замкнутого пути между конкретными файлами. Полный алгоритм описан в разделе [Зависимости](../architecture/dependencies.md#запрет-циклов). + +## Проверка внутренней структуры + +Ревью или дополнительный project lint подтверждают: + +- помимо опционального главного framework-файла, остальные компонентные единицы размещены на одном внутреннем уровне модуля; +- их локальные каталоги не содержат другие компонентные единицы или вложенные модули; +- runtime-дерево компонентов не используется как файловая иерархия; +- рекурсивная структурная вложенность проходит только через вложенные модули; +- в корне модуля находятся только фасеты и опциональный главный implementation- или assembly-файл; +- остальная реализация организована сегментами. + +## Проверка фасетов + +Совместимость фасета определяется всем достижимым исполняемым кодом, а не только его собственным файлом. + +Проверка подтверждает: + +- `index` не достигает `client`, `browser` или `server`; +- `client` не достигает `browser` или `server`; +- `browser` и `server` не достигают друг друга; +- `browser` экспортирует только browser-only или предназначенный для динамического подключения клиентский код; +- `browser` доступен только через поддерживаемую динамическую границу без SSR; +- специализированный фасет существует ради реального потребителя; +- один исполняемый экспорт не дублируется между фасетами. + +Импорт типа остаётся архитектурной зависимостью, но не добавляет исполняемый код в среду фасета. + +## Критерий завершения + +Изменение соответствует SLM, когда одновременно выполнены условия: + +- ответственность и единственный ближайший владелец определены; +- каждый доменный сценарий целиком принадлежит модулю `domains` и используется композициями только через его публичный API; +- отсутствие готового доменного модуля не привело к временной реализации сценария в `compositions`; +- междоменная координация с собственным продуктовым результатом получила доменного владельца; +- каждый домен остаётся специализированным модулем и самостоятельно объявляет предметный контракт; +- публичный API домена не содержит DTO, source types или чужие error contracts; +- все внешние значения адаптированы к доменному контракту до использования в правилах, состоянии или доменном UI; +- ожидаемые неуспешные исходы определены текущим доменом независимо от способа их представления; +- реализация и интеграции используют только исходы, объявленные доменным контрактом; +- ошибки источников и зависимых доменов не пересекают публичную границу в исходной форме; +- runtime-идентификация предоставлена только при наличии реального потребителя и совместима с его средой; +- роль слоя соответствует ответственности; +- публичный API минимален и используется всеми внешними потребителями; +- зависимости разрешены и не образуют модульных циклов; +- группа и сегменты не подменяют модульную границу; +- вложенные модули владеют отдельными подответственностями; +- внутренний код реализует ответственность ближайшего модуля; +- framework-компоненты имеют одноуровневую файловую организацию с единственным допустимым исключением для главного файла в корне; +- корень и сегменты соответствуют своим назначениям; +- состояние и ресурсы имеют владельца и корректную область жизни; +- физическая структура однозначно выражает принятое решение; +- применимые автоматические проверки и архитектурное ревью пройдены. diff --git a/skills/slm-design/reference/docs/rules/README.md b/skills/slm-design/reference/docs/rules/README.md new file mode 100644 index 0000000..29b8dec --- /dev/null +++ b/skills/slm-design/reference/docs/rules/README.md @@ -0,0 +1,62 @@ +# Правила SLM + +Правило SLM задаёт один блокирующий архитектурный инвариант. Точные формулировки правил находятся только в [едином реестре](./registry.md); архитектурные главы объясняют модель и ссылаются на соответствующие коды. + +## Виды утверждений + +- **Определение** задаёт нормативный смысл термина. +- **Правило** задаёт блокирующее требование. +- **Рекомендация** помогает принять решение, но не является обязательной. +- **Пример** показывает один из вариантов реализации и не задаёт каркас проекта. + +Определения собраны в [терминологии](../reference/terminology.md). Определение может быть обязательным для толкования правил, но не получает отдельный код. + +## Код правила + +```text +SLM-{group}-{class}{number} +``` + +| Часть | Значение | +|---|---| +| `SLM` | Принадлежность архитектуре SLM | +| `group` | Предмет правила | +| `class` | Способ окончательной проверки: `A` или `R` | +| `number` | Глобально уникальный трёхзначный номер | + +Пример: [`SLM-MODULE-A004`](./registry.md#slm-module-a004). + +## Способ проверки + +### Автоматические правила (`A`) + +Всё требование можно однозначно проверить программно по структуре проекта, публичным путям и графу импортов. Нарушение блокирует автоматическую проверку. + +### Правила для ревью (`R`) + +Для окончательного решения требуется понимание ответственности, владельца, потребителей или области жизни. Инструмент может найти подозрительный код, но не заменяет архитектурное решение. + +## Разделы правил + +| Код | Предмет | +|---|---| +| `LAYER` | Роль слоя и направление зависимостей | +| `DOMAIN` | Владение доменными сценариями, контрактами, внешними данными и ошибками | +| `MODULE` | Ответственность, владение, публичная граница и внутренняя структура модуля | +| `DEPENDENCY` | Граф зависимостей модулей | +| `GROUP` | Навигационная группировка модулей | +| `SEGMENT` | Внутренняя организация модуля | +| `NESTED_MODULE` | Доступ к вложенному модулю | +| `LIFECYCLE` | Владение долгоживущими ресурсами | +| `ENVIRONMENT` | Совместимость публичных фасетов со средами выполнения | + +Раздел правила не создаёт одноимённую главу или дополнительный уровень архитектуры. Например, `GROUP` классифицирует правило о группировке, а сама группа остаётся необязательной частью слоя. + +## Требования к реестру + +- Одно правило защищает один инвариант. +- Точная формулировка не повторяется в тематических документах. +- Название кратко обозначает предмет, а описание полностью формулирует требование. +- Рекомендации, обоснования и примеры не входят в формулировку правила. +- Один инвариант не получает отдельные автоматическую и ручную копии. +- Номер правила не обозначает важность и не переиспользуется после удаления. diff --git a/skills/slm-design/reference/docs/rules/registry.md b/skills/slm-design/reference/docs/rules/registry.md new file mode 100644 index 0000000..bc99858 --- /dev/null +++ b/skills/slm-design/reference/docs/rules/registry.md @@ -0,0 +1,165 @@ +# Реестр правил SLM + +Здесь собраны правила SLM. Это единственное место, где они формулируются; тематические документы объясняют архитектуру и ссылаются на коды. + +## Размещение кода по слоям + +### SLM-LAYER-R001 + +> **Назначение слоёв** +> +> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода. + +### SLM-LAYER-A002 + +> **Направление зависимостей** +> +> Внутри одного SLM root код каждого слоя может зависеть только от кода целевых слоёв, разрешённых для него нормативной матрицей слоёв. + +### SLM-LAYER-R003 + +> **Граница слоя `app`** +> +> В `app` размещаются только точки входа фреймворка для запуска, маршрутов, преобразования входных данных и подключения публичных API модулей разрешённых слоёв или ресурсов `shared`; ответственности этих модулей остаются за пределами `app`. + +## Домены + +### SLM-DOMAIN-R022 + +> **Владение доменным сценарием** +> +> Каждый доменный сценарий имеет ровно один модуль-владелец в слое `domains`. Код, который придаёт сценарию предметный смысл или определяет его продуктовый результат, включая модели, правила, переходы, продуктовое состояние, смысл операций с продуктовыми данными, предметные исходы, доменный UI и обслуживающие сценарий framework-механизмы, принадлежит этому модулю. Модули других слоёв, включая `compositions`, могут только использовать и компоновать сценарий через публичный API доменного модуля; отсутствие подходящего доменного модуля не разрешает временную или постоянную реализацию сценария вне `domains`. + +### SLM-DOMAIN-R023 + +> **Владение доменным контрактом** +> +> Домен самостоятельно определяет публичный контракт своих сценариев на основе предметного смысла. Контракт источника данных не определяет форму доменного контракта и не становится его частью. + +### SLM-DOMAIN-R024 + +> **Граница внешних данных** +> +> Данные и типы внешнего источника не пересекают публичную границу домена и не используются как доменные модели, состояние или результаты. Домен адаптирует внешние данные к собственному контракту до их использования в предметной реализации. + +### SLM-DOMAIN-R025 + +> **Владение доменными ошибками** +> +> Домен объявляет публичный контракт ожидаемых неуспешных исходов своих сценариев. Реализация домена и интеграции с источниками используют этот контракт и не определяют независимые ошибки или новые исходы вне доменной декларации. Потребители зависят только от доменного контракта, а способ представления и передачи ошибок SLM не устанавливает. + +### SLM-DOMAIN-R026 + +> **Изоляция чужих ошибок** +> +> Ошибка внешнего источника или другого модуля не пересекает публичную границу домена в исходной форме. Домен преобразует её в собственный ожидаемый исход либо в неожиданный дефект согласно общей политике приложения. + +## Границы модулей + +### SLM-MODULE-A004 + +> **Публичный API модуля** +> +> Каждый модуль предоставляет единый логический публичный API через обязательный корневой фасет `index` и, при необходимости, фасеты `client`, `browser` и `server`; код за пределами модуля импортирует его содержимое только через эти фасеты. + +### SLM-MODULE-A014 + +> **Папка модуля** +> +> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки. + +### SLM-MODULE-R006 + +> **Ответственность модуля** +> +> Одна модульная граница содержит только код, который модуль выполняет сам для обеспечения одного результата или поведения; код, отвечающий за другой результат, принадлежит другой модульной границе. + +### SLM-MODULE-R011 + +> **Владелец ответственности** +> +> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одной ближайшей модульной границе; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`. + +### SLM-MODULE-R012 + +> **Состав публичного API** +> +> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми. + +### SLM-MODULE-R020 + +> **Глубина framework-компонентов** +> +> Помимо опционального главного framework-файла в корне модуля, остальные framework-компоненты, в том числе выполняющие роли Provider, Guard или Error Boundary, размещаются на одном внутреннем уровне относительно модуля; каталог такой единицы может содержать локальный вспомогательный код, но не содержит другие компонентные единицы или вложенные модули. + +### SLM-MODULE-R021 + +> **Семантика корня модуля** +> +> Помимо объявленных публичных фасетов, в корне модуля допускается только один опциональный главный implementation- или assembly-файл, который однозначно отражает, непосредственно реализует или собирает ответственность модуля; вся остальная реализация размещается в сегментах. + +## Зависимости между модулями + +### SLM-DEPENDENCY-A005 + +> **Циклические зависимости** +> +> Зависимости между модулями внутри одного SLM root, включая вложенные модули, не образуют циклов. + +## Назначение групп + +### SLM-GROUP-R007 + +> **Назначение группы** +> +> Группа содержит только модули и другие группы, не владеет файлами реализации, состоянием, жизненным циклом или публичным API и не импортируется внешним кодом. + +## Назначение сегментов + +### SLM-SEGMENT-R008 + +> **Граница сегмента** +> +> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или границы зависимостей. + +## Границы вложенных модулей + +### SLM-NESTED_MODULE-A010 + +> **Доступ к вложенному модулю** +> +> Код за пределами родительского модуля не импортирует вложенный модуль напрямую и получает его экспорты только через публичный API родителя. + +## Жизненный цикл + +### SLM-LIFECYCLE-R013 + +> **Жизненный цикл ресурсов** +> +> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни. + +## Границы сред выполнения + +### SLM-ENVIRONMENT-R016 + +> **Универсальный фасет** +> +> Корневой фасет `index` экспортирует только публичный код, совместимый как с серверным рендерингом, включая RSC, так и с клиентским выполнением, и не импортирует или реэкспортирует код фасетов `client`, `browser` или `server` прямо либо транзитивно. + +### SLM-ENVIRONMENT-R017 + +> **Клиентский фасет** +> +> Фасет `client` экспортирует только клиентский код, который не может выполняться как RSC, и не импортирует или реэкспортирует код фасетов `browser` или `server` прямо либо транзитивно. + +### SLM-ENVIRONMENT-R018 + +> **Браузерный фасет** +> +> Фасет `browser` экспортирует browser-only код и клиентский код, предназначенный для динамического подключения без SSR; потребители всегда импортируют его динамически с отключённым SSR. + +### SLM-ENVIRONMENT-R019 + +> **Серверный фасет** +> +> Фасет `server` экспортирует только server-only код и не импортируется или реэкспортируется фасетами `index`, `client` или `browser` прямо либо транзитивно. diff --git a/skills/slm-design/reference/draft/README.md b/skills/slm-design/reference/draft/README.md deleted file mode 100644 index 375af6e..0000000 --- a/skills/slm-design/reference/draft/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Черновики SLM - -> Материалы в `DRAFT` являются рабочими черновиками и не задают нормативную спецификацию SLM. - -## Материалы - -- [Первый уровень](./level-1/README.md) - слои, доменные модули, публичные API и зависимости. -- [Второй уровень](./level-2/README.md) - доменные пакеты, именованные Domain API, assemblies и Framework Groups. -- [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки. - -## Соглашение - -Черновики могут содержать определения, правила, рекомендации, примеры и открытые вопросы. - -Нормативные определения задаются терминологией соответствующего уровня. Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки. diff --git a/skills/slm-design/reference/draft/level-1/README.md b/skills/slm-design/reference/draft/level-1/README.md deleted file mode 100644 index 527c90b..0000000 --- a/skills/slm-design/reference/draft/level-1/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# SLM Level 1 - -> Статус: рабочий черновик. Документы в этой папке не являются спецификацией. - -Level 1 задаёт полную структурную основу SLM: слои, модули, доменные модули, публичные границы, граф зависимостей и владение жизненным циклом. - -## Место в уровнях SLM - -| Уровень | Назначение | -|---|---| -| Level 1 | Слои, доменные модули, зависимости, структурные сущности и жизненный цикл ресурсов | -| Level 2 | Опциональная пакетная форма отдельных доменов, именованные API, assemblies и явные границы сред выполнения | - -Переход отдельного домена на Level 2 может требовать рефакторинга, но базовые понятия Level 1 сохраняются. Остальные домены того же SLM root могут оставаться модулями Level 1. - -## Область Level 1 - -Level 1 описывает слои, доменные модули, группы, сегменты, компоненты, публичный API, граф зависимостей и владение жизненным циклом ресурсов. - -Level 1 не задаёт обязательную внутреннюю форму доменного модуля, фабрики, порты, адаптеры, assemblies, обязательный поток данных, монорепозитории, соглашения об именовании и файловый стайлгайд. - -Появление нескольких сред выполнения, нескольких независимо собираемых API или необходимости разделить бизнес-логику и технические сборки является сигналом перевести конкретный домен на [Level 2](../level-2/). - -## Виды утверждений - -- **Определение** нормативно задаёт смысл архитектурного термина, но не получает код правила. -- **Правило** задаёт блокирующий архитектурный инвариант и объявляется в каноническом реестре. -- **Рекомендация** помогает принять решение, но не делает архитектуру невалидной. -- **Пример** иллюстрирует модель и не задаёт обязательную структуру. - -Нормативные определения находятся в [терминологии](./terminology.md). Канонический набор правил: [правила SLM Level 1](../rules/level-1.md). Формат и требования к ним: [Правила SLM](../rules/). - -Остальные документы Level 1 объясняют и иллюстрируют модель, но не владеют точными формулировками определений и правил. - -## Основная идея - -Модуль является основной архитектурной единицей SLM. Слой задаёт роль и направление зависимостей. Доменный модуль представляет одну предметную область. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю. - -Level 1 требует отдельную папку и единый публичный API модуля. Имена этих элементов, внутренняя файловая форма модулей, сегментов и компонентов определяются стайлгайдом проекта. - -## Карта черновика - -- [Терминология](./terminology.md) -- [Слои](./layers.md) -- [Доменные модули](./domains.md) -- [Зависимости](./dependencies.md) -- [Модули](./modules.md) -- [Группы](./groups.md) -- [Сегменты](./segments.md) -- [Компоненты](./components.md) -- [Вложенные модули](./nested-modules.md) -- [Жизненный цикл](./lifecycle.md) -- [Проверка](./validation.md) diff --git a/skills/slm-design/reference/draft/level-1/components.md b/skills/slm-design/reference/draft/level-1/components.md deleted file mode 100644 index 94a4d56..0000000 --- a/skills/slm-design/reference/draft/level-1/components.md +++ /dev/null @@ -1,65 +0,0 @@ -# Компоненты Level 1 - -> Пояснение нормативной модели компонентов Level 1. - -Компонент является строительным элементом интерфейса, а не самостоятельной архитектурной единицей. - -## Связанные правила - -- [`SLM-L1-COMPONENT-R009`](../rules/level-1.md#slm-l1-component-r009) -- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) -- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) -- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) -- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013) - -## Файловая форма - -Файловую форму компонента определяет стайлгайд. Компонент может быть одним файлом фреймворка или каталогом со вспомогательными файлами. - -```text -landing/ -└── ui/ - └── hero.tsx -``` - -```text -landing/ -└── ui/ - └── hero/ - ├── hero.tsx - ├── styles/ - │ └── hero.module.css - └── types/ - └── hero-props.type.ts -``` - -Наличие каталога, типов, стилей или локального `index.ts` не превращает компонент в модуль. - -## Реализация - -Компонент может отображать входные данные, вызывать переданные обработчики, условно строить интерфейс и хранить локальное состояние представления. - -Level 1 не вводит отдельного запрета на импорты, выполняемые кодом компонента. Каждый такой импорт считается зависимостью родительского модуля и должен соблюдать направление слоёв, публичные API и запрет циклов. - -Доступ к данным, состояние, контекст или код жизненного цикла внутри компонента сами по себе не создают новую архитектурную границу. Их источники, зависимости и область жизни определяет родительский модуль. - -Провайдер может технически реализовывать контекст и жизненный цикл фреймворка, но владельцем состояния и ресурсов остаётся родительский модуль. - -Файл в `app` может технически быть компонентом React или Vue. Архитектурно он является точкой входа фреймворка, а не компонентом SLM. - -## Компонент и модуль - -| Признак | Компонент | Модуль | -|---|---|---| -| Самостоятельная ответственность | Нет | Да | -| Собственный публичный API | Нет | Да | -| Собственная граница зависимостей | Нет | Да | -| Вспомогательные файлы | Может иметь | Может иметь | -| Сегменты и вложенные модули | Нет | Может иметь | - -Модуль может состоять всего из одного корневого компонента. Различие определяется владением, а не количеством файлов. - -## Когда нужен вложенный модуль - -Если часть интерфейса получает самостоятельную ответственность, публичный API, собственную границу зависимостей, область жизни или внутреннюю модульную декомпозицию, она является модулем. При локальном использовании такой модуль может размещаться как вложенный. diff --git a/skills/slm-design/reference/draft/level-1/dependencies.md b/skills/slm-design/reference/draft/level-1/dependencies.md deleted file mode 100644 index 5a4f213..0000000 --- a/skills/slm-design/reference/draft/level-1/dependencies.md +++ /dev/null @@ -1,59 +0,0 @@ -# Зависимости Level 1 - -> Пояснение нормативной модели зависимостей Level 1. - -Матрица слоёв задаёт допустимые связи, а модули образуют граф зависимостей. - -## Что считается зависимостью - -- Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость. -- Импорт между файлами одного модуля не пересекает модульную границу и не создаёт отдельный узел графа. -- Импорт любого внутреннего файла, сегмента или компонента считается зависимостью ближайшего модуля-владельца. -- Вложенный модуль является обычным самостоятельным узлом графа. -- Группы, сегменты и компоненты не являются самостоятельными узлами графа. - -Точка входа фреймворка не является модулем. Её импорты участвуют в проверке направления слоёв, но не образуют исходящий узел графа модулей. - -Ресурс `shared` также не является модулем. Его прямой импорт участвует в проверке направления слоёв, но не нарушает требование о публичном API модуля. - -## Допустимые связи - -- Модуль может импортировать модули своего слоя и слоёв, разрешённых нормативной матрицей. -- Модули одного слоя могут импортировать друг друга. -- Промежуточный слой не является обязательным посредником. -- `infra` и `ui` не импортируют друг друга; их связывает владелец из `domains`, `compositions` или `app`. - -Доменный модуль Level 1 может импортировать публичный API другого доменного модуля. Runtime- и type-only импорты одинаково создают ребро графа, поэтому общий граф обязан оставаться ацикличным. - -```ts -// domains/orders -import type { Product } from '@/domains/catalog' -``` - -Level 1 не требует отдельного механизма междоменной инъекции. Более строгая модель cross-domain зависимостей задаётся Level 2. - -Матрица слоёв определена в [Слоях](./layers.md). - -## Связанные правила - -- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) -- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) -- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010) - -## Публичный API - -```ts -// Допустимо -import { Button } from '@/ui/button' - -// Недопустимо -import { Button } from '@/ui/button/button' -``` - -## Циклы - -```text -ui/modal → ui/button → ui/icon -ui/icon -/→ ui/modal -``` diff --git a/skills/slm-design/reference/draft/level-1/domains.md b/skills/slm-design/reference/draft/level-1/domains.md deleted file mode 100644 index 8b97e6e..0000000 --- a/skills/slm-design/reference/draft/level-1/domains.md +++ /dev/null @@ -1,61 +0,0 @@ -# Доменные модули Level 1 - -> Пояснение базовой модели предметных областей без обязательной внутренней архитектуры. - -## Связанные правила - -- [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) -- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) -- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) - -## Один домен, один модуль - -Связная предметная область получает один доменный модуль. Level 1 не требует выделять Domain API, ports, adapters, assemblies или framework bindings в самостоятельные соседние модули. - -```text -domains/auth/ -├── hooks/ -├── services/ -├── stores/ -├── types/ -├── ui/ -└── index.ts -``` - -Показанные каталоги являются возможными сегментами, а не обязательным каркасом. Доменный модуль может содержать предметные типы, сценарии, состояние, framework-код, локальные адаптеры, компоненты и вложенные модули. - -## Публичный API - -Внешний код использует домен через обычный публичный API модуля: - -```ts -import { signOut, useSession } from '@/domains/auth' -``` - -Глубокий импорт во внутренний сегмент нарушает модульную границу: - -```ts -import { useSession } from '@/domains/auth/hooks/use-session' -``` - -## Groups - -При большом количестве доменных модулей слой `domains` может содержать обычные навигационные Groups: - -```text -domains/ -├── shop/ # Group -│ ├── catalog/ # Доменный модуль -│ └── orders/ # Доменный модуль -└── cabinet/ # Group - └── profile/ # Доменный модуль -``` - -Group не имеет `index.ts`, реализации, состояния или публичного API. Она не создаёт dependency boundary и не реэкспортирует содержащиеся в ней домены. - -## Переход на Level 2 - -Доменный модуль переводится в доменный пакет Level 2, когда ему нужны устойчивые API, несколько фабрик, разные среды выполнения или независимые SLM-модули сборок и framework-интеграции. - -Такой переход изменяет только форму выбранного домена: корневой доменный модуль исчезает, а его публичные модели и операции переходят обязательному модулю `api` внутри доменного пакета. Другие предметные области SLM root не обязаны переходить вместе с ним, но входящие imports и production composition roots выбранного домена обновляются. diff --git a/skills/slm-design/reference/draft/level-1/groups.md b/skills/slm-design/reference/draft/level-1/groups.md deleted file mode 100644 index 62059f8..0000000 --- a/skills/slm-design/reference/draft/level-1/groups.md +++ /dev/null @@ -1,25 +0,0 @@ -# Группы Level 1 - -> Пояснение нормативной модели групп Level 1. - -Группа помогает ориентироваться в большом количестве модулей. Она классифицирует структуру, но ничего не реализует и не образует узел графа зависимостей. - -## Связанное правило - -- [`SLM-L1-GROUP-R007`](../rules/level-1.md#slm-l1-group-r007) -- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) - -## Пример - -```text -compositions/ -├── pages/ # Группа -│ ├── landing/ # Модуль -│ └── contacts/ # Модуль -└── layouts/ # Группа - └── main/ # Модуль -``` - -`pages` и `layouts` являются возможной группировкой проекта, а не обязательной структурой Level 1. - -Рекомендуется создавать группу только при реальной навигационной потребности. Если папка начинает владеть файлами реализации, состоянием, жизненным циклом или публичным API, она является модулем и должна получить модульную границу. diff --git a/skills/slm-design/reference/draft/level-1/layers.md b/skills/slm-design/reference/draft/level-1/layers.md deleted file mode 100644 index b7583d4..0000000 --- a/skills/slm-design/reference/draft/level-1/layers.md +++ /dev/null @@ -1,95 +0,0 @@ -# Слои Level 1 - -> Пояснение нормативной модели слоёв Level 1. - -## Базовая структура - -```text -src/ -├── app/ -├── compositions/ -├── domains/ -├── infra/ -├── ui/ -└── shared/ -``` - -`src/` здесь является примером SLM root. Фактическую границу определяет устройство приложения. - -Отсутствующая в проекте роль не требует пустой папки. Слой является доступной архитектурной ролью, а не обязательным элементом каркаса. - -## Роли слоёв - -### App - -`app` связывает приложение с фреймворком: запускает его, объявляет маршруты, преобразует входные данные и подключает публичные API модулей разрешённых слоёв или ресурсы `shared`. Файлы `app` являются точками входа фреймворка, а не модулями SLM. - -Точка входа может напрямую использовать `compositions`, `domains`, `infra`, `ui` или `shared`, если зависимость разрешена матрицей слоёв. Такое использование не переносит ответственность импортируемого модуля в `app`. - -### Compositions - -`compositions` содержит продуктовый интерфейс: страницы, макеты, экраны, виджеты, точки входа и другие композиционные модули. Внутреннюю группировку слоя определяет проект. - -### Domains - -`domains` содержит предметные области приложения: их модели, правила, сценарии и продуктовое состояние. Каждая самостоятельная предметная область представлена [доменным модулем](./domains.md). - -Доменная логика не принадлежит устройству одной страницы или маршрута. Конкретный visual outcome и сборка нескольких доменов остаются в `compositions`. - -### Infra - -`infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной доменной модели. - -### UI - -`ui` содержит универсальные модули интерфейса, которые не зависят от конкретной страницы или продуктовой композиции. - -### Shared - -`shared` содержит независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода. - -В `shared` допускаются обычные модули и специальные немодульные ресурсы: небольшие чистые утилиты, общие типы, стили, конфигурация и статические файлы. Ресурс не имеет самостоятельной ответственности, публичного API или жизненного цикла и может импортироваться напрямую по пути, установленному стайлгайдом. - -Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль. Каталог ресурсов не реэкспортирует модули и не используется для обхода их публичных API. - -## Матрица зависимостей - -```text -app - | -compositions - | -domains - / \ -infra ui - \ / -shared -``` - -Код слоя может импортировать модули своего слоя и слоёв, разрешённых строкой матрицы. Разрешённая зависимость может пропускать промежуточные роли. - -| Слой | Может импортировать | -|---|---| -| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` | -| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` | -| `domains` | `domains`, `infra`, `ui`, `shared` | -| `infra` | `infra`, `shared` | -| `ui` | `ui`, `shared` | -| `shared` | `shared` | - -`infra` и `ui` не импортируют друг друга. Универсальный UI получает локализованный текст, тему, callbacks аналитики и другие возможности через входной контракт либо связывается с ними в `domains` или `compositions`. Если UI-модулю необходимо напрямую знать конкретный технический сервис приложения, такой код не является универсальным UI. - -Импорты внутри слоя, публичный API и циклы описаны отдельно в [Зависимостях](./dependencies.md). - -## Связанные правила - -- [`SLM-L1-LAYER-R001`](../rules/level-1.md#slm-l1-layer-r001) -- [`SLM-L1-LAYER-A002`](../rules/level-1.md#slm-l1-layer-a002) -- [`SLM-L1-LAYER-R003`](../rules/level-1.md#slm-l1-layer-r003) -- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) - -## Граница Level 1 - -Разрешённый импорт не переносит владение. Например, доменный модуль может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев. - -Если код определяет продуктовую модель, правило или сценарий, он принадлежит `domains`, а не `shared` или `infra`. Строгая внутренняя форма домена появляется только на [Level 2](../level-2/). diff --git a/skills/slm-design/reference/draft/level-1/lifecycle.md b/skills/slm-design/reference/draft/level-1/lifecycle.md deleted file mode 100644 index 31a5d51..0000000 --- a/skills/slm-design/reference/draft/level-1/lifecycle.md +++ /dev/null @@ -1,31 +0,0 @@ -# Жизненный цикл Level 1 - -> Пояснение нормативной модели владения ресурсами Level 1. - -Жизненный цикл является частью ответственности модуля. Файл, компонент, провайдер или точка входа фреймворка могут технически создавать и останавливать ресурс, но архитектурным владельцем остаётся модуль. - -## Связанные правила - -- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) -- [`SLM-L1-LIFECYCLE-R013`](../rules/level-1.md#slm-l1-lifecycle-r013) - -## Граница ресурса - -Для ресурса определяются: - -- модуль-владелец; -- место создания; -- момент начала работы; -- область жизни; -- допустимое число экземпляров; -- способ остановки и очистки. - -Ресурс начинает работу не раньше начала своей области жизни и не остаётся активным после её завершения. Подписки, слушатели, таймеры, наблюдатели, запросы и соединения рассматриваются одинаково, если требуют явного завершения или отмены. - -## Реализация - -Очистку может выполнять сам модуль, компонент, провайдер или фреймворк. Способ реализации не меняет владельца и не переносит ответственность в технический файл. - -Одиночный экземпляр на всё приложение допустим только тогда, когда модуль действительно владеет областью жизни приложения или процесса. Размещение экземпляра на уровне файла само по себе этого не доказывает. - -Точка входа `app` может запускать или подключать ресурс через публичный API импортируемого модуля, но не становится его владельцем. diff --git a/skills/slm-design/reference/draft/level-1/modules.md b/skills/slm-design/reference/draft/level-1/modules.md deleted file mode 100644 index f5d2d4a..0000000 --- a/skills/slm-design/reference/draft/level-1/modules.md +++ /dev/null @@ -1,43 +0,0 @@ -# Модули Level 1 - -> Пояснение нормативной модели модулей Level 1. - -Модуль является основной архитектурной единицей SLM. Он размещается в отдельной папке, но может состоять только из публичной точки входа и одного файла реализации. - -## Связанные правила - -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) -- [`SLM-L1-MODULE-A014`](../rules/level-1.md#slm-l1-module-a014) -- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) -- [`SLM-L1-MODULE-R011`](../rules/level-1.md#slm-l1-module-r011) -- [`SLM-L1-MODULE-R012`](../rules/level-1.md#slm-l1-module-r012) - -## Владение - -Каждая самостоятельная ответственность имеет одного модуля-владельца. Модуль определяет её публичный API, зависимости, состояние, область жизни и внутреннее устройство независимо от того, в каком файле выполняется конкретный код. - -Точки входа `app` и нормативные ресурсы `shared` являются единственными немодульными исключениями. Остальной код внутри SLM root либо принадлежит существующему модулю, либо образует новый модуль. - -## Публичный API - -Модуль предоставляет один логический публичный API. Конкретное имя точки входа и механизм экспорта определяет стайлгайд проекта. - -Внешний код использует модуль только через публичный API. Сам API открывает только контракт, необходимый реальным внешним потребителям; внутренние механизмы, изменяемое состояние и детали жизненного цикла остаются закрытыми. - -## Внутреннее устройство - -Модуль может содержать корневые файлы, сегменты, компоненты и [вложенные модули](./nested-modules.md). Внутри своей границы он может использовать относительные импорты и не обязан обращаться к собственному публичному API; точную форму внутренних импортов определяет стайлгайд. - -SLM не требует полного каркаса или обязательного каталога сегментов. - -## Визуальный модуль - -Визуальный модуль обычно имеет корневой компонент, который экспортируется через публичный API. - -```text -button/ -├── button.tsx -└── index.ts -``` - -Корневой компонент остаётся компонентом, а владельцем ответственности является модуль `button`. diff --git a/skills/slm-design/reference/draft/level-1/nested-modules.md b/skills/slm-design/reference/draft/level-1/nested-modules.md deleted file mode 100644 index ef0706a..0000000 --- a/skills/slm-design/reference/draft/level-1/nested-modules.md +++ /dev/null @@ -1,30 +0,0 @@ -# Вложенные модули Level 1 - -> Пояснение нормативной модели вложенных модулей Level 1. - -Вложенный модуль является обычным модулем, размещённым внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей. - -## Связанные правила - -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) -- [`SLM-L1-MODULE-R006`](../rules/level-1.md#slm-l1-module-r006) -- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) -- [`SLM-L1-NESTED_MODULE-A010`](../rules/level-1.md#slm-l1-nested_module-a010) - -## Пример - -```text -landing/ -├── landing.page.tsx -├── parts/ -│ └── hero/ -│ ├── hero.tsx -│ └── index.ts -└── index.ts -``` - -`parts/` здесь является примером сегмента, а не обязательным именем. - -Код родительского модуля использует вложенный модуль через его собственный публичный API. Код за пределами родительского модуля получает доступ только через публичный API родителя. - -Если вложенный модуль становится нужен за пределами родителя, рекомендуется перенести его в минимальную общую область без изменения внутренней формы. Доступ через API родителя при этом остаётся допустимым и сам по себе не требует переноса. diff --git a/skills/slm-design/reference/draft/level-1/segments.md b/skills/slm-design/reference/draft/level-1/segments.md deleted file mode 100644 index 0ab790a..0000000 --- a/skills/slm-design/reference/draft/level-1/segments.md +++ /dev/null @@ -1,29 +0,0 @@ -# Сегменты Level 1 - -> Пояснение нормативной модели сегментов Level 1. - -Сегмент организует внутреннее содержимое модуля. Level 1 определяет роль сегмента, но не задаёт обязательный список имён. - -## Связанное правило - -- [`SLM-L1-SEGMENT-R008`](../rules/level-1.md#slm-l1-segment-r008) -- [`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) - -## Файловая форма - -Названия, набор и содержимое сегментов определяет стайлгайд проекта. Сегмент может группировать файлы, компоненты или вложенные модули. - -Файлы и компоненты сегмента принадлежат родительскому модулю. Вложенный модуль внутри сегмента сохраняет собственные ответственность, публичный API и узел графа зависимостей. - -## Пример - -```text -landing/ # Модуль -└── ui/ # Сегмент модуля - └── hero/ # Каталог компонента - ├── hero.tsx - ├── styles/ # Вспомогательный каталог компонента - └── types/ # Вспомогательный каталог компонента -``` - -`styles/` и `types/` внутри каталога компонента не обязаны считаться сегментами SLM. Их форму определяет стайлгайд компонентов. diff --git a/skills/slm-design/reference/draft/level-1/terminology.md b/skills/slm-design/reference/draft/level-1/terminology.md deleted file mode 100644 index 21585c9..0000000 --- a/skills/slm-design/reference/draft/level-1/terminology.md +++ /dev/null @@ -1,143 +0,0 @@ -# Терминология Level 1 - -> Нормативные определения рабочего черновика. Этот раздел не объявляет правила. - -Определения Level 1 задают обязательный смысл архитектурных терминов и используются при толковании всех правил. Код получают только блокирующие требования, а не сами определения. - -## Базовые понятия - -### SLM root - -Граница структурной архитектуры одного приложения. Внутри неё определяются слои, модули и граф зависимостей Level 1. Монорепозиторий, пакеты и отношения между несколькими SLM root находятся за пределами Level 1. - -### Ответственность - -Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственные публичный API, зависимости, состояние или область жизни. - -### Владелец - -Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннее устройство. Место выполнения кода не переносит владение. - -### Публичный API - -Единая логическая точка внешнего доступа к модулю. Публичный API скрывает внутреннее устройство; конкретное имя файла и механизм экспорта определяет стайлгайд проекта. - -### Зависимость - -Статическая связь внутри одного SLM root, которую импорт или реэкспорт создаёт между архитектурными границами. Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость. - -Зависимость любого внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу и становится отдельным узлом графа зависимостей. - -### Область жизни - -Период, в течение которого принадлежащие модулю состояние или долгоживущий ресурс должны оставаться активными. - -### Ресурс жизненного цикла - -Ресурс, работа которого продолжается после первоначального вызова и требует остановки, отмены, отписки или освобождения. Например, подписка, слушатель событий, таймер, наблюдатель, запрос или соединение. - -### Очистка - -Гарантированное прекращение работы ресурса не позже завершения его области жизни. Автоматическая очистка фреймворка считается очисткой владельца, если модуль устанавливает и контролирует соответствующую границу. - -## Структурные сущности - -### Нормативная матрица слоёв - -Отношение допустимой зависимости между слоями одного SLM root. Матрица определяет, код каких слоёв может импортировать исходный слой; она не обязана образовывать линейный порядок. - -Для Level 1 нормативно отношение `app → compositions → domains → { infra, ui } → shared`. `infra` и `ui` являются независимыми ветвями: они не импортируют друг друга. Промежуточный слой не является обязательным посредником. - -### Слой - -Одна из шести верхнеуровневых ролей внутри SLM root: - -| Слой | Роль | -|---|---| -| `app` | Связь приложения с фреймворком: запуск, маршруты и преобразование входных данных | -| `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции | -| `domains` | Предметные области, их модели, правила, сценарии и продуктовое состояние | -| `infra` | Технические сервисы и возможности приложения | -| `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции | -| `shared` | Независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода | - -Полная матрица допустимых зависимостей: - -| Исходный слой | Допустимые целевые слои | -|---|---| -| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` | -| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` | -| `domains` | `domains`, `infra`, `ui`, `shared` | -| `infra` | `infra`, `shared` | -| `ui` | `ui`, `shared` | -| `shared` | `shared` | - -### Модуль - -Минимальная самостоятельная архитектурная единица Level 1. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API. - -### Доменная ответственность - -Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Количество экранов, endpoint-ов, hooks или файлов само по себе не определяет её границу. - -### Доменный модуль - -Обычный SLM-модуль слоя `domains`, представляющий одну доменную ответственность. Он подчиняется всем правилам модулей, предоставляет единый публичный API и является узлом графа зависимостей. - -Доменный модуль может содержать сегменты, компоненты и вложенные модули. Level 1 не требует разделять его внутреннее содержимое по техническим ролям. - -### Группа - -Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или узла графа зависимостей. - -### Сегмент - -Внутренняя часть одного модуля, которая группирует его содержимое по назначению. Сегмент не является самостоятельным владельцем, публичным API или узлом графа зависимостей. - -### Компонент - -Сущность фреймворка, которая реализует часть интерфейса родительского модуля. Компонент не образует собственного владельца, публичного API или узла графа зависимостей. - -Импорты, состояние, доступ к данным и код жизненного цикла компонента принадлежат родительскому модулю. Их наличие само по себе не создаёт новый модуль; решающим признаком является самостоятельная ответственность. - -### Вложенный модуль - -Обычный модуль, размещённый внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и узел графа зависимостей и подчиняется всем общим правилам модулей. - -Публичный API вложенного модуля доступен коду родительской границы. Для кода за пределами родительского модуля вложенный модуль остаётся внутренней реализацией родителя. - -### Точка входа фреймворка - -Специальная немодульная единица слоя `app`, которая непосредственно связывает приложение с фреймворком. Её импорты участвуют в проверке направления слоёв, но сама точка входа не является узлом графа модулей. - -### Ресурс shared - -Специальная немодульная единица слоя `shared`: небольшая детерминированная утилита, общий тип, стиль, конфигурация или статический ресурс без продуктового знания, изменяемого состояния, ввода-вывода, области жизни и собственного публичного API. - -Ресурс `shared` может импортироваться напрямую по пути, установленному стайлгайдом, и не является узлом графа модулей. Доступный по этому пути файл является всей единицей и не скрывает отдельное внутреннее устройство. - -Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль. - -## Структурная модель - -```text -SLM root -├── app -│ └── точка входа фреймворка -├── compositions | domains | infra | ui -│ ├── группа -│ │ └── модуль -│ └── модуль -│ ├── корневые файлы -│ ├── сегмент -│ │ ├── файлы -│ │ ├── компоненты -│ │ └── вложенные модули -│ └── вложенный модуль -└── shared - ├── группа - ├── модуль - └── ресурс shared -``` - -Путь и имя папки сами по себе не определяют сущность. Её определяют ответственность, владелец и публичная граница. Физическое сопоставление путей с сущностями задаётся стайлгайдом или конфигурацией проверки проекта. diff --git a/skills/slm-design/reference/draft/level-1/validation.md b/skills/slm-design/reference/draft/level-1/validation.md deleted file mode 100644 index a61715d..0000000 --- a/skills/slm-design/reference/draft/level-1/validation.md +++ /dev/null @@ -1,34 +0,0 @@ -# Проверка Level 1 - -> Граница автоматической проверки и архитектурного ревью Level 1. - -## Автоматическая проверка - -Проект, заявляющий соответствие Level 1, сопоставляет физические пути с SLM root, слоями, доменными модулями, остальными модулями, Groups, вложенными модулями, публичными точками входа, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей. - -Каждое правило класса `A` должно быть реализовано проверкой проекта и блокировать её при нарушении. Level 1 не навязывает конкретный инструмент. - -Скрипт `draft-rules.js` проверяет только целостность документов: формат и уникальность кодов, ссылки и наличие тематических упоминаний. Он не проверяет архитектуру приложения. - -Актуальный список правил скрипт получает из [канонического реестра](../rules/level-1.md). - -## Архитектурное ревью - -Правила класса `R` проверяются вручную. Статический анализ может обнаружить подозрительный код, но не способен окончательно определить: - -- ответственность и её владельца; -- связность предметной области доменного модуля; -- соответствие кода роли слоя; -- необходимость экспортов публичного API; -- область жизни ресурса и достаточность очистки; -- наличие самостоятельной границы у компонента, группы или сегмента. - -## Проверка доменных модулей - -На ревью определяется: - -- представляет ли доменный модуль одну связную предметную область; -- не разделена ли одна область на соседние модули без самостоятельных владельцев; -- не объединены ли в одном модуле несвязанные предметные области; -- остаются ли страницы, маршруты и multi-domain UI в `compositions`; -- остаются ли самостоятельные технические сервисы без предметной модели в `infra`. diff --git a/skills/slm-design/reference/draft/level-2/README.md b/skills/slm-design/reference/draft/level-2/README.md deleted file mode 100644 index 1d29496..0000000 --- a/skills/slm-design/reference/draft/level-2/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# SLM Level 2 - -> Статус: рабочий черновик. Документы в этой папке не являются спецификацией. - -Level 2 предназначен для отдельных предметных областей, которым нужен устойчивый Domain API поверх нескольких внешних источников, сред выполнения или самостоятельных framework-модулей. Он сохраняет структурную базу Level 1, но заменяет выбранный доменный модуль доменным пакетом с обязательными `api`, production adapters и штатной сборкой `assemblies/default`. - -## Наследование Level 1 - -Доменный пакет Level 2 соблюдает определения и правила Level 1, кроме явно заменённых положений: - -| Положение Level 1 | Статус в Level 2 | -|---|---| -| Матрица `app → compositions → domains → { infra, ui } → shared` | Сохраняется | -| Модуль, Group, сегмент, компонент, публичный API и статический граф зависимостей | Сохраняют смысл | -| Доменный модуль и [`SLM-L1-DOMAIN-R015`](../rules/level-1.md#slm-l1-domain-r015) | Заменяются только для предметной области в пакетной форме | -| Единый публичный API модуля `api` | Представлен обязательными consumer type и factory-фасетами, implementer-фасетом ports при необходимости и необязательным runtime-фасетом | -| Навигационная Group слоя `domains` | Может одновременно содержать доменные модули и пакеты | -| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups | - -Канонический набор требований образуют [правила Level 1](../rules/level-1.md) и [правила Level 2](../rules/level-2.md). - -## Основная идея - -Для прикладного consumer предметная область существует как Domain API: - -```text -framework / composition - │ - ▼ - Domain API - │ - ▼ -dependency ports - ▲ - │ -production adapters - │ - ▼ -SDK / backend / storage / realtime -``` - -Модуль `api` владеет публичными моделями, validation, операциями, outcomes и стабильными ошибками. Он объявляет consumer-owned ports и получает их реализации через фабрику. Adapter знает конкретный provider, assembly выбирает adapters, а framework binding получает готовый API и организует state, cache, reactivity и hydration средствами своего framework. - -Приложение не обращается к предметному внешнему источнику в обход Domain API. Это не запрещает самостоятельные технические сервисы `infra`, universal UI или framework-only SDK для получения opaque input; запрет относится к данным и операциям конкретного домена. - -## Когда выбирать Level 2 - -Level 2 оправдан, когда предметной области нужны: - -- собственная модель, отличающаяся от backend DTO; -- стабильные ошибки независимо от SDK и транспорта; -- несколько production sources или providers; -- HTTP, storage, realtime или platform integrations за одной предметной границей; -- разные baseline и специальные assemblies; -- строгие client/server/RSC/worker boundaries; -- самостоятельные domain-specific framework bindings; -- изолированные tests Domain API через fake ports и contract tests adapters. - -Level 2 выбирается для конкретной предметной области. Один SLM root может корректно содержать простые доменные модули Level 1 и доменные пакеты Level 2. Размер каталога, один endpoint или один hook сами по себе не требуют перехода. - -## Цена Level 2 - -Пакетная форма намеренно дороже простого доменного модуля. Она добавляет фасеты `api`, dependency ports, production adapters, обязательную штатную assembly, mapping внешних records и failures, а также отдельные test boundaries. - -Эта цена окупается, когда Domain API действительно изолирует приложение от внешней модели, ошибок, provider и runtime. Если фабрика только переименовывает один метод SDK и возвращает тот же DTO и error, домену обычно достаточно Level 1. - -Импорт assembly не создаёт граф и не запускает side effects. Composition root вызывает только assemblies dependency-connected доменов, нужных текущему route, request, worker или application scope; глобальная eager-сборка всех `default` не является требованием Level 2. - -## Базовая форма - -```text -src/domains/ -├── catalog/ # Доменный модуль Level 1 -└── auth/ # Доменный пакет Level 2 - ├── README.md # Необязательная metadata - ├── api/ # Обязательный SLM-модуль - │ ├── index.ts # Только consumer-facing public types - │ ├── factory.ts # Public factories - │ ├── ports.ts # При наличии dependency ports - │ └── runtime.ts # Необязательный deterministic runtime - ├── adapters/ # При наличии dependency ports - │ ├── identity-rest/ # SLM-модуль - │ └── identity-realtime/ # SLM-модуль - ├── assemblies/ # Обязательная Group - │ ├── default/ # Обязательная штатная assembly - │ └── administration/ # Дополнительная assembly - └── react/ # Необязательная Framework Group - ├── session/ # SLM-модуль - └── queries/ # SLM-модуль -``` - -Корень пакета не является модулем и не имеет `index.ts`. Groups также не имеют агрегирующих API. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своей публичной границей. - -## Публичные границы - -```ts -import type { - AuthError, - AuthSession, - AuthSessionApi, -} from '@/domains/auth/api' - -import type { - AuthIdentityPort, - AuthIdentityPortFailure, -} from '@/domains/auth/api/ports' - -import { createAuthSessionApi } from '@/domains/auth/api/factory' -import { isAuthError } from '@/domains/auth/api/runtime' - -import { createAuth } from '@/domains/auth/assemblies/default' -import { AuthSessionProvider } from '@/domains/auth/react/session' -``` - -Обычный прикладной consumer импортирует типы `api`, при необходимости deterministic `api/runtime`, готовую production-сборку и framework bindings. Фасет `api/ports` предназначен для adapters, assemblies и tests. Фасет `api/factory` в production импортируют только assemblies своего домена. - -Общие импорты `@/domains/auth`, `@/domains/auth/adapters`, `@/domains/auth/assemblies` и `@/domains/auth/react` запрещены: пакет и Groups не имеют публичного API. - -## Штатная assembly - -Каждый пакет содержит `assemblies/default`. Она создаёт канонический production-граф одного baseline capability context, объявленного проектом. - -`default` может быть browser-only в React + Vite или действительно изоморфной в Next.js. Имя не является доказательством совместимости: проверяется executable import-граф для заявленных resolver conditions. Если RSC, administration, worker или realtime session требуют другого набора API, dependencies, trust или lifecycle, появляется дополнительная именованная assembly. - -## State и framework - -Domain API не является framework store. TanStack Query, SWR, Zustand, Redux, Pinia, Signals и аналогичные runtimes находятся в framework bindings или compositions. Они могут владеть framework metadata и UI-state, но их domain payload состоит только из public values, outcomes и events Domain API. - -Server и client создают разные API instances и caches. Через RSC boundary передаются сериализуемые public values или hydration payload, но не фабрики, API objects, ports или mutable clients. - -## Realtime - -Realtime transport остаётся внутри adapter. Domain API может предоставлять command methods и subscriptions, но публикует только проверенные events, outcomes и stable domain errors. Correlation, acknowledgement, ordering, reconnect, duplicate delivery, resync, outcome uncertainty и cleanup задаются контрактом realtime port и не выводятся из поведения конкретного WebSocket SDK. - -## Совместное применение форм - -Простой доменный модуль и доменный пакет могут постоянно сосуществовать в одном SLM root, но одна предметная область имеет только одну форму. При связи, пересекающей пакет Level 2, доменный код использует type-only публичный контракт либо deterministic `api/runtime`; готовые API передаются runtime-аргументами assemblies пакетов Level 2 либо явным construction points модулей Level 1. - -Переход одного домена изменяет его входящие dependency edges и composition roots, но не требует переводить несвязанные соседние домены на Level 2. - -## Карта черновика - -- [Терминология](./terminology.md) -- [Доменный пакет](./domains/domain-package.md) -- [Модуль api и Domain API](./domains/domain-api.md) -- [Фабрики, ports и adapters](./domains/factory-ports-adapters.md) -- [Assemblies и default](./domains/assemblies.md) -- [Состояние и кэш](./domains/state-cache.md) -- [Framework Groups и модули](./domains/framework-bindings.md) -- [Realtime](./domains/realtime.md) -- [Зависимости](./dependencies.md) -- [Тестирование](./domains/testing.md) -- [Проверка](./validation.md) -- [Переход auth](./domains/auth-example.md) -- [Открытые вопросы](./domains/open-questions.md) diff --git a/skills/slm-design/reference/draft/level-2/dependencies.md b/skills/slm-design/reference/draft/level-2/dependencies.md deleted file mode 100644 index 97a479a..0000000 --- a/skills/slm-design/reference/draft/level-2/dependencies.md +++ /dev/null @@ -1,152 +0,0 @@ -# Зависимости Level 2 - -> Уточнение статического import-графа и runtime injection graph внутри и между доменными границами. - -## Связанные правила - -- [`SLM-L2-API-A007`](../rules/level-2.md#slm-l2-api-a007) -- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012) -- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013) -- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026) -- [`SLM-L2-API-A019`](../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021) -- [`SLM-L2-API-A022`](../rules/level-2.md#slm-l2-api-a022) -- [`SLM-L2-PORT-R027`](../rules/level-2.md#slm-l2-port-r027) -- [`SLM-L2-ASSEMBLY-R030`](../rules/level-2.md#slm-l2-assembly-r030) -- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005) - -## Статическая матрица внутри пакета - -| Исходный модуль | Допустимые зависимости | -|---|---| -| `api` | Собственные файлы, объявленные environment-neutral ресурсы `shared`, API-safe packages, type-only Domain API и `api/runtime` других доменов | -| Adapter module | `api/ports` своего домена, `infra`, concrete provider runtime, `shared` | -| Assembly | `api`, `api/ports`, `api/factory`, при необходимости `api/runtime` своего домена, публичные adapters своего домена, type-only Domain API других доменов, `shared` | -| Framework binding module | `api` и `api/runtime` своего домена, публичные framework modules своего домена, framework/state/query runtime, `ui`, `shared` | -| Graph owner | Assemblies и framework modules входящих в граф доменов, а также разрешённые матрицей `infra`, `ui` и `shared` | - -Модуль `api` не достигает adapters, assemblies, framework modules, product SDK, storage, state/query manager, DOM, Node.js API или других environment-specific capabilities. Проверяется весь транзитивный executable и type graph его фасетов. - -Adapter импортирует contract только через `api/ports`. Он не импортирует factory и consumer-facing runtime, потому что не создаёт API и не выбирает публичный domain outcome. - -Assembly импортирует только adapters собственного домена. Production graph owner не импортирует concrete adapters или `api/factory`: он вызывает готовые assembly builders. - -## Публичные фасеты - -```text -api - → import type прикладных contracts - -api/ports - → import type adapters, assemblies и tests - -api/factory - → runtime import assemblies и API tests - -api/runtime - → runtime import реальных consumers -``` - -Символьная type-проверка ports может быть строже обычного path allowlist. Проект объявляет, какие files и modules считаются adapters, assemblies и test boundaries. - -## Междоменные статические импорты - -Если связь пересекает границу пакета Level 2, разрешены: - -```ts -import type { - AuthSessionApi, -} from '@/domains/auth/api' - -import { - isAuthError, -} from '@/domains/auth/api/runtime' -``` - -Для доменного модуля Level 1 используется type-only импорт его обычного публичного API. - -Запрещено импортировать из другого домена: - -- `api/factory`; -- `api/ports`; -- готовый API singleton; -- assembly; -- adapter; -- framework state, hook, context, Provider или component; -- любой внутренний путь `api`. - -Runtime-импорт `api/runtime` остаётся статическим ребром общего DAG. Если он создаёт цикл, границы доменов или владелец pure-функции пересматриваются. - -## Runtime-инъекция cross-domain API - -Готовый API другого домена передаётся assembly аргументом: - -```text -createAuth() - → AuthSessionApi - → createUser({ auth }) - → UserProfileApi -``` - -User assembly передаёт `auth` своей factory. Она не импортирует runtime instance Auth. - -Cross-domain API не превращается автоматически в local port. Bridge port нужен только при реальном translation contract. Structural copy чужого API скрывает owner и затрудняет обнаружение runtime-цикла. - -## Runtime dependency graph - -Статический DAG импортов не показывает все runtime edges, передаваемые аргументами. Architecture mapping объявляет либо review явно восстанавливает: - -- assembly inputs; -- создаваемые Domain API; -- public APIs и construction points доменных модулей Level 1; -- передаваемые factories dependencies; -- callbacks и late-bound capabilities, пересекающие Level 2 boundary; -- scope и multiplicity; -- cleanup order. - -Graph owner создаёт независимые APIs раньше зависимых и освобождает их в обратном порядке. Цикл `A API → B API → A API` запрещён, даже если одна сторона является модулем Level 1, а callback, lazy holder или local structural type сохраняет статически ацикличный import graph. - -Lazy provider или registry не является автоматическим исключением. Для него требуется отдельный readiness, lifecycle и failure contract, а сам runtime edge остаётся частью graph review. - -## Совместное применение Level 1 и Level 2 - -Один SLM root может постоянно содержать обе формы. Между двумя доменными модулями Level 1 продолжают действовать обычные правила Level 1. - -Если хотя бы одна сторона является пакетом Level 2, runtime API создаёт внешний graph owner и передаёт assembly зависимого пакета Level 2 либо явной construction point/public callback зависимого модуля Level 1. Если у модуля Level 1 такой точки нет и связь невозможна без global singleton или обратного импорта, модуль рефакторится либо переводится на Level 2. - -Переход формы остаётся локальным для предметной ответственности, но change radius включает все входящие imports и composition roots выбранного домена. - -## Framework state - -Framework binding использует framework API только своего доменного пакета: - -```ts -// Допустимо внутри domains/auth/react/queries -import { - useAuthApi, -} from '@/domains/auth/react/session' -``` - -```ts -// Недопустимо внутри domains/user/react/profile -import { - useAuthApi, -} from '@/domains/auth/react/session' -``` - -Во втором случае composition читает projections обоих доменов и передаёт values или callbacks через публичные props. Если User Domain API зависит от Auth, связь выполняется assemblies на runtime graph level. - -## Границы сред и RSC - -Каждая declared client, server, edge, worker или shared entry point проверяется под реальными resolver conditions. Название `assemblies/default` не объявляет environment compatibility. - -Tree shaking и runtime condition не доказывают изоляцию. Server-only adapter не достигается из client entry, даже если ветка считается неиспользуемой. - -Checker различает: - -- executable import edge; -- type-only import edge; -- framework reference edge; -- dynamic import с объявленным target capability set. - -Server Component выполняется в server scope. Ссылка на Client Component и invocation Server Action анализируются как framework references, а не как обычное совместное выполнение. Для SSR-enabled Client Component отдельно проверяются server prerender graph, browser hydration graph и объявленные framework-deferred browser edges. Необъявленный или неанализируемый dynamic import запрещается либо явно allowlist-ится project policy. diff --git a/skills/slm-design/reference/draft/level-2/domains/README.md b/skills/slm-design/reference/draft/level-2/domains/README.md deleted file mode 100644 index 9c74aab..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Доменные пакеты Level 2 - -Доменный пакет заменяет один выбранный доменный модуль Level 1. Он объединяет SLM-модули одной предметной области вокруг контролируемого Domain API, но сам не является модулем, Group или публичным API. - -```text -domains/auth/ -├── api/ # Обязательный модуль -├── adapters/ # При наличии dependency ports -├── assemblies/ -│ └── default/ # Обязательная штатная assembly -└── react/ - ├── session/ - └── queries/ -``` - -Другие предметные области того же SLM root могут оставаться доменными модулями Level 1. - -## Основные границы - -- [Доменный пакет](./domain-package.md) определяет предметную и структурную policy boundary. -- [Domain API](./domain-api.md) является единственным семантическим шлюзом к данным и операциям домена. -- [Фабрики, ports и adapters](./factory-ports-adapters.md) изолируют SDK, backend, storage, runtime capabilities и provider failures. -- [Assemblies](./assemblies.md) содержат обязательную штатную сборку `default` и дополнительные production-контексты. -- [Состояние и кэш](./state-cache.md) принадлежат framework bindings или compositions: они могут хранить framework metadata и UI-state, но materialize domain payload только из значений Domain API. -- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка. -- [Realtime](./realtime.md) задаёт messages, subscriptions, correlation, resync, errors и cleanup. -- [Тестирование](./testing.md) проверяет Domain API через фабрики, adapters через port contracts и assemblies через production wiring. -- [Переход auth](./auth-example.md) показывает локальный переход с Level 1 без big-bang всего root. -- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей. - -Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md). diff --git a/skills/slm-design/reference/draft/level-2/domains/assemblies.md b/skills/slm-design/reference/draft/level-2/domains/assemblies.md deleted file mode 100644 index fe0bc38..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/assemblies.md +++ /dev/null @@ -1,264 +0,0 @@ -# Assemblies и production-граф - -> Пояснение обязательной штатной сборки, дополнительных контекстов, environment compatibility и lifecycle. - -## Связанные правила - -- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008) -- [`SLM-L2-ASSEMBLY-R011`](../../rules/level-2.md#slm-l2-assembly-r011) -- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) -- [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013) -- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020) -- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021) -- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022) -- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023) -- [`SLM-L2-ASSEMBLY-R030`](../../rules/level-2.md#slm-l2-assembly-r030) -- [`SLM-L2-ASSEMBLY-R031`](../../rules/level-2.md#slm-l2-assembly-r031) - -## Назначение - -Assembly является SLM-модулем Group `assemblies`. Она выбирает production adapters своего домена, вызывает фабрики `api` и возвращает готовый именованный граф Domain API для одного объявленного production-контекста. - -```text -api/factory + adapters + cross-domain APIs - → assembly - → named Domain API graph -``` - -Assembly не добавляет предметные методы, модели, transitions или ошибки. Она также не владеет framework state: готовый API передаётся framework binding или composition. - -Импорт assembly не запускает side effects. Граф появляется только после вызова builder. - -## Обязательная default assembly - -Каждый пакет содержит модуль `assemblies/default`: - -```text -auth/assemblies/ -├── default/ -│ └── index.ts -└── administration/ - └── index.ts -``` - -`default` является штатной production-сборкой домена для одного baseline capability set, объявленного проектом. Она может быть browser-only, server-only, worker-compatible или действительно isomorphic. Имя не сообщает environment compatibility. - -Пример metadata: - -```yaml -assemblies: - default: - capabilities: [fetch, web-crypto] - conditions: [browser, import] - administration: - capabilities: [node, server-secrets] - conditions: [node, import] -``` - -Формат metadata не нормирован, но checker должен получать capability set и resolver conditions из явного project mapping, а не угадывать их по имени `default`. - -## Дополнительные assemblies - -Дополнительная assembly появляется, когда отличается реальная production-граница: - -- набор Domain API; -- dependencies или providers; -- trust boundary; -- environment capabilities; -- scope или lifecycle; -- способ аутентификации; -- realtime guarantees. - -Хорошие имена описывают контекст: `administration`, `realtime-session`, `worker`, `rsc`. Имя `rsc` оправдано только при отличающемся RSC wiring; само наличие Server Component не требует отдельной assembly. - -Не создаётся assembly-заглушка с методами, бросающими `NOT_SUPPORTED`. Контекст возвращает только реально доступные API. - -## Штатный граф - -```ts -import type { - AuthSessionApi, -} from '@/domains/auth/api' - -import { - createAuthSessionApi, -} from '@/domains/auth/api/factory' - -import { - createAuthRestAdapter, -} from '@/domains/auth/adapters/identity-rest' - -export type AuthGraph = Readonly<{ - session: AuthSessionApi -}> - -export const createAuth = (): AuthGraph => { - const session = createAuthSessionApi({ - identity: createAuthRestAdapter(), - }) - - return { session } -} -``` - -Обычный graph owner импортирует только production builder: - -```ts -import { - createAuth, -} from '@/domains/auth/assemblies/default' - -const auth = createAuth() -``` - -Factory и concrete adapter остаются construction details assembly. Тесты API и adapters импортируют соответствующие границы напрямую. - -## React + Vite и Next.js - -В React + Vite `default` часто использует browser adapters: - -```text -assemblies/default - → browser REST adapter - → browser WebSocket adapter -``` - -В Next.js та же `default` может считаться isomorphic только при совместимом executable graph под всеми заявленными conditions. Runtime branch не делает импорт безопасным: - -```ts -// Недостаточное доказательство изоморфности. -if (typeof window === 'undefined') { - return createServerAdapter() -} - -return createBrowserAdapter() -``` - -Если server и client требуют разных concrete dependencies, используются разные assemblies или framework-specific resolver entries, проверяемые отдельно. - -## RSC boundary - -RSC не переносит API instance с сервера в браузер: - -```text -Server Component - → request-scoped server assembly - → server Domain API instance - → public serializable value - → Client Component boundary - → separate client assembly - → separate client Domain API instance -``` - -Server Component исполняется в server scope. Его импорт Client Component является framework reference, а не обычным executable edge RSC graph. При включённом SSR или prerender сам Client Component дополнительно исполняется в отдельном server render graph, а затем в browser hydration graph; обе фазы проверяются, а browser-only effects объявляются как framework-deferred edges. Server Action создаёт и очищает собственный request graph на каждый вызов. - -Через boundary не передаются functions, API objects, ports, adapters, mutable cache clients или request secrets. - -## Cross-domain input - -Assembly зависимого домена принимает готовый API аргументом: - -```ts -import type { - AuthSessionApi, -} from '@/domains/auth/api' - -export type CreateUserInput = Readonly<{ - auth: Pick -}> - -export const createUser = ({ - auth, -}: CreateUserInput): UserGraph => { - const profile = createUserProfileApi({ - auth, - profile: createUserProfileRestAdapter(), - }) - - return { profile } -} -``` - -Graph owner выполняет runtime-связь: - -```ts -const auth = createAuth() -const user = createUser({ - auth: auth.session, -}) -``` - -User assembly делает только type-only импорт Auth API. Она не импортирует Auth factory, adapter или assembly. Общий runtime dependency graph остаётся ацикличным. - -## Dependency-connected graph - -Наличие `assemblies/default` у каждого Level 2 package не требует eager-сборки всех доменов: - -```text -route A - → auth/default - → user/default - -route B - → catalog/default -``` - -Graph owner вызывает только builders, необходимые текущему scope. Module-level вызов `createAuth()` и global registry готовых APIs нарушают явное владение scope. - -## Lifecycle - -Factory не запускает запрос, socket, subscription или timer во время создания API. Явная операция, которая позже запускает ресурс, возвращает cleanup: - -```ts -const subscription = await chat.subscribe(observer) - -try { - await runScope() -} finally { - await subscription.close() -} -``` - -Если assembly создаёт owned resource или получает lifecycle handle adapter-owned resource, результат предоставляет aggregate cleanup: - -```ts -export type ChatAssembly = Readonly<{ - apis: ChatGraph - dispose: () => Promise -}> -``` - -Cleanup является идемпотентным. После завершившегося cleanup resource не вызывает callbacks. - -У каждого resource ровно один owner. Adapter, который сам создаёт connection или source cache, остаётся владельцем и экспортирует lifecycle handle; assembly только включает этот handle в aggregate cleanup. Если connection создаёт assembly, adapter получает borrowed capability и не закрывает её самостоятельно. - -## Частичная ошибка сборки - -Assembly регистрирует cleanup сразу после создания каждого owned resource и сразу после получения adapter lifecycle handle. Если следующий шаг завершается ошибкой, все зарегистрированные obligations выполняются до передачи ошибки caller-у: - -```ts -export const createChat = async (): Promise => { - const cleanups: Array<() => Promise> = [] - - try { - const connection = await createRealtimeConnection() - cleanups.push(connection.close) - - const history = createHistoryAdapter(connection) - const messages = createMessagesApi({ history }) - - return { - apis: { messages }, - dispose: createIdempotentReverseCleanup(cleanups), - } - } catch (error) { - await runReverseCleanup(cleanups) - throw error - } -} -``` - -Реализация helper не нормирована. Нормативны достижимость cleanup на failure path, обратный dependency order и отсутствие callbacks после завершения disposal. - -Assembly без cleanup obligations возвращает только API graph и не добавляет пустой `dispose` для симметрии. Наличие adapter-owned resource с переданным handle уже является cleanup obligation, даже если assembly не считается его владельцем. diff --git a/skills/slm-design/reference/draft/level-2/domains/auth-example.md b/skills/slm-design/reference/draft/level-2/domains/auth-example.md deleted file mode 100644 index ceb6e8e..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/auth-example.md +++ /dev/null @@ -1,204 +0,0 @@ -# Переход домена auth с Level 1 - -> Проверочный пример локального перехода от доменного модуля к пакету с Domain API, ports, adapters и default assembly. - -## Связанные правила - -- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) -- [`SLM-L2-API-R005`](../../rules/level-2.md#slm-l2-api-r005) -- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020) -- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021) -- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022) -- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026) -- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027) -- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028) - -## Исходная форма Level 1 - -```text -domains/ -├── auth/ # Доменный модуль -│ ├── hooks/ -│ ├── services/ -│ ├── stores/ -│ ├── ui/ -│ └── index.ts # Общий API модуля -└── catalog/ # Независимый доменный модуль - └── index.ts -``` - -Level 1 разрешает external calls, framework hooks, state и Auth scenarios внутри одной module boundary. - -## Целевая форма Auth - -```text -domains/ -├── auth/ # Доменный пакет Level 2 -│ ├── README.md -│ ├── api/ # Один SLM-модуль -│ │ ├── errors/ -│ │ ├── factories/ -│ │ ├── models/ -│ │ ├── operations/ -│ │ ├── ports/ -│ │ ├── index.ts # Consumer-facing types -│ │ ├── ports.ts # Implementer-facing types -│ │ ├── factory.ts # Domain API factories -│ │ └── runtime.ts # Guards и public pure runtime -│ ├── adapters/ # Group -│ │ ├── identity-rest/ # SLM-модуль -│ │ ├── identity-realtime/ # SLM-модуль -│ │ └── request-session/ # SLM-модуль -│ ├── assemblies/ # Обязательная Group -│ │ ├── default/ # Штатный Auth graph -│ │ └── administration/ # Специальный trusted graph -│ └── react/ # Framework Group -│ ├── session/ # Provider готового API -│ ├── queries/ # Query/cache projection -│ └── login-form/ # Переиспользуемый domain UI -└── catalog/ # По-прежнему модуль Level 1 - └── index.ts -``` - -Корневой `domains/auth/index.ts` удаляется. `catalog` и остальные домены не меняют форму только из-за перехода Auth. - -## Перенос ответственности - -| Исходная часть | Владелец Level 2 | Публичный путь | -|---|---|---| -| Session operations и public models | `auth/api` | `auth/api` | -| Port contracts и failures | `auth/api` | `auth/api/ports` | -| Runtime factories | `auth/api` | `auth/api/factory` | -| Error guards и public pure-функции | `auth/api` | `auth/api/runtime` | -| REST provider mapping | `auth/adapters/identity-rest` | Adapter API для assembly | -| Realtime protocol и correlation | `auth/adapters/identity-realtime` | Adapter API для assembly | -| Request cookies mapping | `auth/adapters/request-session` | Adapter API для assembly | -| Штатный production graph | `auth/assemblies/default` | `auth/assemblies/default` | -| Trusted administration graph | `auth/assemblies/administration` | `auth/assemblies/administration` | -| Provider и session hooks | `auth/react/session` | `auth/react/session` | -| Query/cache/hydration | `auth/react/queries` | `auth/react/queries` | -| Переиспользуемая форма | `auth/react/login-form` | `auth/react/login-form` | -| Страница, текст и redirect | `compositions` | API конкретной composition | - -## Domain API и port - -```ts -export type AuthSessionApi = { - getSession: () => Promise - requestPhoneOtp: ( - command: RequestPhoneOtpCommand, - ) => Promise - verifyPhoneOtp: ( - command: VerifyPhoneOtpCommand, - ) => Promise -} -``` - -```ts -export type AuthIdentityPort = { - requestPhoneOtp: ( - command: AuthIdentityPortCommand, - ) => Promise - verifyPhoneOtp: ( - command: VerifyIdentityPortCommand, - ) => Promise -} -``` - -REST adapter реализует этот port поверх generated client. API проверяет records и преобразует port failures в `AuthError`. - -## Штатная сборка - -```ts -import { - createAuthSessionApi, -} from '@/domains/auth/api/factory' - -import { - createIdentityRestAdapter, -} from '@/domains/auth/adapters/identity-rest' - -export const createAuth = (): AuthGraph => ({ - session: createAuthSessionApi({ - identity: createIdentityRestAdapter(), - }), -}) -``` - -Обычный production consumer использует: - -```ts -import { - createAuth, -} from '@/domains/auth/assemblies/default' -``` - -Он не импортирует factory или adapter напрямую. - -## Framework state - -Старый `auth/stores` не переносится в `api`. React query/store projection принадлежит `auth/react/queries`: - -```ts -export const useAuthSessionQuery = () => { - const api = useAuthApi() - - return useQuery({ - queryKey: ['auth', 'session'], - queryFn: api.getSession, - }) -} -``` - -При Vue или другом framework та же модель и errors Domain API материализуются его собственными средствами. - -## Realtime - -`identity-realtime` скрывает socket protocol, operation IDs, acknowledgements и reconnect. Domain API возвращает обычный command outcome и публикует проверенные Auth events. - -Если disconnect произошёл до acknowledgement, API не утверждает ложный отказ и может вернуть `AUTH_OPERATION_OUTCOME_UNKNOWN`. После gap binding получает `RESYNC_REQUIRED` и повторно вызывает `getSession()`. - -## RSC - -Server Component создаёт request-scoped Auth graph и передаёт Client Component только сериализуемый `AuthSession` или hydration payload. Client Component создаёт отдельный client graph; при SSR его render должен быть совместим с server prerender, а browser-only capabilities остаются в deferred effects. - -`assemblies/default` используется в обоих местах только если её executable graph действительно совместим со всеми declared conditions. Иначе появляется отдельная assembly, например `auth/assemblies/rsc`. - -## Cross-domain graph - -Если User package зависит от Auth, он импортирует только type contract: - -```ts -import type { - AuthSessionApi, -} from '@/domains/auth/api' -``` - -User assembly принимает готовый API: - -```ts -const auth = createAuth() -const user = createUser({ - auth: auth.session, -}) -``` - -User не импортирует Auth factory, port, adapter, assembly или React hooks. Если User остаётся модулем Level 1, его public API должен иметь явную точку передачи нужного Auth behavior. - -## Порядок перехода - -1. Зафиксировать consumers, external sources, state, errors и lifecycle исходного Auth module. -2. Объявить consumer-facing Domain API и public models. -3. Объявить dependency ports, records и closed failures. -4. Реализовать factory и проверить Domain API через fake ports. -5. Оформить каждую production implementation модулем `adapters/*` и добавить contract tests. -6. Создать `assemblies/default` для штатного production context. -7. Добавить специальные assemblies только для реально отличающихся graphs. -8. Перенести framework state, cache и hydration в modules Group `react`. -9. Перенести страницы, redirects и multi-domain UI в `compositions`. -10. Перевести внешние imports на разрешённые public paths. -11. Обновить dependency-connected graph owners и cross-domain inputs. -12. Удалить старый root `index.ts` Auth и объявить package checker-у. - -Завершённость перехода определяется одной формой Auth и отсутствием обходных imports. Наличие других доменных модулей Level 1 не является миграционным долгом. diff --git a/skills/slm-design/reference/draft/level-2/domains/domain-api.md b/skills/slm-design/reference/draft/level-2/domains/domain-api.md deleted file mode 100644 index 3823656..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/domain-api.md +++ /dev/null @@ -1,260 +0,0 @@ -# Модуль api и Domain API - -> Пояснение семантического шлюза домена, его публичных фасетов, моделей, операций и ошибок. - -## Связанные правила - -- [`SLM-L2-API-R005`](../../rules/level-2.md#slm-l2-api-r005) -- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006) -- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-a007) -- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008) -- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009) -- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010) -- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018) -- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022) -- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024) -- [`SLM-L2-API-R025`](../../rules/level-2.md#slm-l2-api-r025) -- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027) -- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028) - -## Роль - -`api` является обязательным SLM-модулем доменного пакета. Для прикладного consumer предметная область доступна только через объявленные им Domain API, public models, outcomes и errors. - -Модуль `api` владеет: - -- именованными Domain API; -- публичными командами, запросами и подписками; -- public domain models; -- validation внешних и port values; -- семантикой outcomes и expected errors; -- dependency ports и port failures; -- одной фабрикой для каждого Domain API; -- необходимыми consumers deterministic guards и pure-функциями. - -Модуль не владеет framework store, query cache, hydration runtime, SDK, transport client или production adapter. Он может координировать одну операцию и замыкать переданные ports, но не хранит скрытую mutable projection данных приложения между вызовами. - -## Domain API как шлюз - -```text -consumer command - → Domain API - → dependency port - → adapter - → provider - -provider record/failure - → adapter mapping - → port record/failure - → Domain API validation and semantics - → public model/outcome/error - → consumer -``` - -Framework hook, store или composition не импортирует concrete SDK и не читает предметный внешний источник напрямую. Это позволяет менять endpoint, provider и transport, сохраняя публичный контракт, пока не изменилась продуктовая семантика. - -Domain API не обязан скрывать реальное предметное изменение. Если backend изменил правило, которое влияет на публичный outcome приложения, контракт домена пересматривается явно. - -## Публичные фасеты - -Один логический публичный API модуля `api` разделён по аудиториям. - -### Consumer types - -Корневой `api/index.ts` экспортирует только типы, необходимые прикладным consumers: - -```ts -export type { - AuthError, - AuthErrorCode, - AuthSession, - AuthSessionApi, - RequestPhoneOtpCommand, - VerifyPhoneOtpCommand, -} from './types' -``` - -```ts -import type { - AuthSession, - AuthSessionApi, -} from '@/domains/auth/api' -``` - -Port contracts, factory dependencies, provider records и technical failures не входят в consumer-facing barrel. - -### Implementer types - -`api/ports.ts` существует только при наличии dependency ports и экспортирует implementer-facing contracts: - -```ts -export type { - AuthIdentityPort, - AuthIdentityPortFailure, - AuthIdentityRecord, - AuthSessionApiDependencies, -} from './ports' -``` - -```ts -import type { - AuthIdentityPort, -} from '@/domains/auth/api/ports' -``` - -Этим фасетом пользуются adapters своего домена, assemblies и tests. Прикладной consumer не строит поведение по port records или failures. - -### Factory entry - -`api/factory.ts` экспортирует только именованные runtime-фабрики: - -```ts -export { - createAuthAdministrationApi, - createAuthSessionApi, -} from './factories' -``` - -```ts -import { - createAuthSessionApi, -} from '@/domains/auth/api/factory' -``` - -В production этот фасет импортируют только assemblies текущего домена. API-тесты используют его с fake ports. - -### Runtime entry - -Необязательный `api/runtime.ts` экспортирует только публичный детерминированный runtime: - -```ts -export { - AUTH_ERROR_CODES, - isAuthError, - projectSessionEvent, -} from './runtime' -``` - -Здесь допустимы error codes и guards, validators, value constructors, pure transitions, reconciliation functions и immutable-константы. Фасет не содержит фабрики, API instances, ports, I/O, subscriptions, mutable state или environment-specific код. - -Если runtime-потребителей нет, файл не создаётся. Другие внешние пути внутри `api` являются deep imports. - -## Stateless runtime boundary - -Domain API управляет смыслом данных, а не способом их materialization. Query и command возвращают public values или outcomes, которые framework binding может сохранить в TanStack Query, Zustand, Pinia или другом runtime: - -```ts -export type AuthSessionApi = { - getSession: () => Promise - requestPhoneOtp: ( - command: RequestPhoneOtpCommand, - ) => Promise - verifyPhoneOtp: ( - command: VerifyPhoneOtpCommand, - ) => Promise - signOut: () => Promise -} -``` - -API не экспортирует `getState`, mutable store, QueryClient или framework subscription. Operation-local correlation, cancellation и validation допустимы; canonical cache приложения остаётся у framework consumer. - -Если клиентский workflow имеет предметное состояние, framework хранит readonly value, а API определяет переход: - -```ts -const nextCheckout = checkoutApi.applyCommand( - currentCheckout, - command, -) -``` - -Или consumer использует pure-функцию `api/runtime`. Framework не применяет предметный merge самостоятельно. - -## Несколько Domain API - -```ts -export type AuthSessionApi = { - getSession: () => Promise - signIn: (command: SignInCommand) => Promise - signOut: () => Promise -} - -export type AuthAdministrationApi = { - revokeUserSessions: ( - command: RevokeUserSessionsCommand, - ) => Promise -} -``` - -`AuthSessionApi` и `AuthAdministrationApi` могут иметь разные ports, trust boundaries и assemblies. Один публичный сценарий принадлежит ровно одному API. - -Разделение не используется только ради файловой декомпозиции. Если APIs не могут быть созданы независимо из-за общей atomicity, состояния или lifecycle, они объединяются либо получают один явно созданный shared capability через assembly. - -Assembly возвращает именованный граф готовых контрактов: - -```ts -export type AuthGraph = Readonly<{ - session: AuthSessionApi -}> -``` - -Такой граф сообщает доступный набор API, но не является новым предметным API. - -## Errors и failure algebra - -Ожидаемая публичная ошибка имеет устойчивую readonly сериализуемую форму: - -```ts -export type AuthErrorCode = - | 'AUTH_IDENTITY_INVALID' - | 'AUTH_RATE_LIMITED' - | 'AUTH_SERVICE_UNAVAILABLE' - -export type AuthError = Readonly<{ - code: AuthErrorCode -}> -``` - -Внешний failure проходит две границы: - -```text -provider error - → adapter - → closed port failure - → api - → stable domain error -``` - -Например, adapter переводит HTTP `429`, SDK class или socket error frame в `AuthIdentityPortFailure` с типом `RATE_LIMITED`. Domain API решает, что публичная операция завершается `AUTH_RATE_LIMITED`. - -Port failure не содержит raw provider object в публично доступной форме. Domain error не включает status, SDK class, source message, payload или `cause`. Диагностические данные остаются в observability-механизме adapter или infra. - -Cancellation и `OUTCOME_UNKNOWN` не объединяются с обычным failure, если приложение должно различать их. Ошибка программирования и нарушенный внутренний инвариант не маскируются под expected domain error. - -Выбор exception или discriminated `Result` остаётся policy проекта. Архитектурная цепочка provider failure → port failure → domain error не зависит от канала передачи. - -## Недетерминизм - -Clock, timer, random, ID generator и environment передаются как dependency ports: - -```ts -export type AuthRuntimePort = { - now: () => number - createId: () => string -} -``` - -Модуль `api` не читает `Date.now`, `Math.random`, env или platform globals напрямую, если они влияют на результат операции. Это сохраняет детерминированность API-тестов и явную environment boundary. - -## Потребители фасетов - -| Потребитель | `api` | `api/ports` | `api/factory` | `api/runtime` | -|---|---|---|---|---| -| Adapter своего домена | Нет | Type-only | Нет | Нет | -| Assembly своего домена | Type-only | Type-only | Да | При необходимости | -| Framework binding своего домена | Type-only | Нет | Нет | При необходимости | -| `composition` или `app` | Type-only | Нет | Нет | При необходимости | -| Код другого домена | Type-only | Нет | Нет | При необходимости | -| API-тест | Type-only | Type-only | Да | По тестируемой границе | - -Прикладной production graph создаётся assemblies. `app`, compositions и framework bindings не импортируют factory или concrete adapters. diff --git a/skills/slm-design/reference/draft/level-2/domains/domain-package.md b/skills/slm-design/reference/draft/level-2/domains/domain-package.md deleted file mode 100644 index 95e043d..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/domain-package.md +++ /dev/null @@ -1,119 +0,0 @@ -# Граница доменного пакета - -> Пояснение контейнерной сущности Level 2 и её совместного использования с доменными модулями Level 1. - -## Связанные правила - -- [`SLM-L2-DOMAIN-R002`](../../rules/level-2.md#slm-l2-domain-r002) -- [`SLM-L2-DOMAIN-A003`](../../rules/level-2.md#slm-l2-domain-a003) -- [`SLM-L2-GROUP-R004`](../../rules/level-2.md#slm-l2-group-r004) -- [`SLM-L2-API-R005`](../../rules/level-2.md#slm-l2-api-r005) -- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026) -- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020) -- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021) - -## Предметная граница - -Доменный пакет представляет одну связную предметную область и объединяет только принадлежащие ей SLM-модули. Пакет `auth` может содержать Domain API авторизации, production adapters её providers, assemblies и React bindings, но не страницу профиля, общий database client или multi-domain navigation policy. - -Пакет не владеет исполняемой ответственностью. Domain API, adapters, production graph, framework projection и lifecycle принадлежат конкретным модулям внутри него. - -Level 2 применяется к пакету, а не ко всему SLM root. Например, `auth` может быть пакетом Level 2, пока `catalog` и `news` остаются доменными модулями Level 1. - -## Корень пакета - -```text -domains/auth/ -├── README.md -├── api/ -├── adapters/ -├── assemblies/ -└── react/ -``` - -В корне разрешены: - -- документация; -- ownership metadata; -- декларативный manifest архитектурной проверки; -- объявления environment capability sets; -- обязательный модуль `api`; -- обязательная непустая Group `assemblies` с модулем `default`; -- непустая Group `adapters`, если хотя бы одна фабрика имеет dependency port; -- Framework Groups при наличии соответствующих модулей. - -В корне запрещены: - -- `index.ts` или другой агрегирующий executable entry point; -- runtime-файлы и side effects; -- изменяемое состояние и lifecycle resources; -- реэкспорт API внутренних модулей; -- page-specific компоненты или сборка нескольких доменов. - -Metadata содержит только статические данные. Проверяющий инструмент может читать её декларативно, но она не исполняется и не становится скрытым API пакета. - -## Policy boundary - -Доменный пакет не является узлом import-графа. Его техническая граница состоит из объявленного проверке набора дочерних модулей и правил, применяемых ко всем связям через эту границу. - -Отсутствие root barrel намеренно: - -- client, server, RSC и worker entry points не агрегируются в один импорт; -- каждый модуль сохраняет отдельные ответственность и environment boundary; -- concrete adapters не становятся частью Domain API; -- Groups не превращаются в скрытые modules; -- versioning publishable package остаётся за пределами Level 2. - -## Модули и Groups - -```text -auth/ -├── api/ # SLM-модуль -│ ├── index.ts # Consumer-facing types -│ ├── ports.ts # Implementer-facing types -│ ├── factory.ts # Runtime factories -│ └── runtime.ts # Необязательный deterministic runtime -├── adapters/ # Group при наличии ports -│ ├── identity-rest/ # SLM-модуль -│ └── identity-realtime/ # SLM-модуль -├── assemblies/ # Обязательная Group -│ ├── default/ # Обязательный SLM-модуль -│ └── administration/ # Дополнительный SLM-модуль -└── react/ # Framework Group - ├── session/ # SLM-модуль - └── queries/ # SLM-модуль -``` - -Groups не имеют `index.ts`. Публичными путями являются `auth/api`, `auth/api/ports`, `auth/api/factory`, опциональный `auth/api/runtime`, `auth/adapters/identity-rest`, `auth/assemblies/default` и `auth/react/session`, но не `auth`, `auth/adapters`, `auth/assemblies` или `auth/react`. - -## Навигационные Groups - -Слой `domains` может содержать Groups с обеими формами домена: - -```text -domains/ -└── commerce/ # Навигационная Group - ├── catalog/ # Доменный модуль Level 1 - └── orders/ # Доменный пакет Level 2 -``` - -Такая Group отличается от Group внутри пакета допустимым составом: она содержит доменные модули, доменные пакеты и другие navigation Groups, а не внутренние модули пакета. - -Одна предметная область не представляется одновременно модулем и пакетом. Переход завершается удалением старой границы выбранного домена, но требует обновить все его входящие imports и production composition roots. - -## Границы соседних слоёв - -| Ответственность | Владелец | -|---|---| -| Публичные модели, Domain API, validation и domain errors | `api` | -| Контракт external capability | `api/ports` | -| Production-реализация dependency port | Adapter внутри пакета | -| Штатный production-граф | `assemblies/default` | -| Специальный production-граф | Дополнительная assembly | -| Domain-specific framework state, cache и bindings | Модуль внутри `react`, `vue` и аналогичной Group | -| Универсальный технический сервис | `infra` | -| Страница, маршрут, redirect, продуктовый текст | `compositions` | -| UI, объединяющий несколько доменов | `compositions` | - -Зависимость от React, WebSocket или SDK сама по себе не определяет владельца. Решающими остаются предметная ответственность, направление dependency inversion и публичная граница. diff --git a/skills/slm-design/reference/draft/level-2/domains/factory-ports-adapters.md b/skills/slm-design/reference/draft/level-2/domains/factory-ports-adapters.md deleted file mode 100644 index 1194b10..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/factory-ports-adapters.md +++ /dev/null @@ -1,235 +0,0 @@ -# Фабрики, ports и adapters - -> Пояснение dependency inversion между Domain API и внешними runtime-возможностями. - -## Связанные правила - -- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-a007) -- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008) -- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010) -- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) -- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018) -- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021) -- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022) -- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024) -- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027) - -## Одна фабрика на Domain API - -```text -явные ports + cross-domain APIs + factory → один Domain API -``` - -Модуль `api` предоставляет одну именованную фабрику для каждого объявленного Domain API: - -```ts -import type { - AuthSessionApi, -} from '@/domains/auth/api' - -import type { - AuthIdentityPort, - AuthRuntimePort, -} from '@/domains/auth/api/ports' - -export type AuthSessionApiDependencies = Readonly<{ - identity: AuthIdentityPort - runtime: AuthRuntimePort -}> - -export type AuthSessionApiFactory = ( - dependencies: AuthSessionApiDependencies, -) => AuthSessionApi -``` - -```ts -import { - createAuthSessionApi, -} from '@/domains/auth/api/factory' -``` - -Фабрика не выбирает environment, provider, adapter или assembly. Она не открывает connection, не запускает subscription и не создаёт framework state. Разные Domain API могут иметь разные dependency sets и собираться независимо. - -## Consumer-owned ports - -Port описывает capability с позиции модуля `api`, а не повторяет конкретный provider: - -```ts -export type AuthIdentityRecord = Readonly<{ - expiresAt: number - subject: string -}> - -export type AuthIdentityPortFailure = - | Readonly<{ type: 'FORBIDDEN' }> - | Readonly<{ type: 'RATE_LIMITED' }> - | Readonly<{ type: 'UNAVAILABLE' }> - -export type AuthIdentityPortResult = - | Readonly<{ - ok: true - value: AuthIdentityRecord - }> - | Readonly<{ - ok: false - failure: AuthIdentityPortFailure - }> - -export type AuthIdentityPort = { - signIn: ( - command: AuthIdentityPortCommand, - ) => Promise -} -``` - -Port не экспортирует generated DTO, SDK error class, HTTP status или concrete client. `AuthIdentityRecord` не становится `AuthSession`: модуль `api` проверяет record и создаёт публичную модель. - -Не каждый port обязан использовать `Result`. Exception, callback или async iterable допустимы при project policy, если expected failures, cancellation, outcome uncertainty и cleanup остаются типизированными и проверяемыми. - -## Гранулярность ports - -Port соответствует связной capability, а не каждому endpoint и не всему SDK: - -```text -AuthIdentityPort - ├── requestCode - ├── verifyCode - └── revokeSession -``` - -Допустимо разделить capability, если операции имеют разные trust boundaries, lifecycle или providers. Запрещено создавать десятки pass-through ports только ради зеркала transport operations. - -Clock, timer, random, ID generator и environment также являются ports, если влияют на результат Domain API. Materialized framework state и query cache ports не являются: они принадлежат framework binding. - -## Failure algebra - -Expected failure проходит две явные стадии: - -```text -provider-specific failure - → adapter mapping - → closed port failure - → api mapping - → stable domain error or outcome -``` - -Port failure должен сохранять различия, которые нужны Domain API. Если adapter сводит `FORBIDDEN`, `CONFLICT` и `UNAVAILABLE` к `unknown`, API не может выбрать корректную публичную семантику. Если adapter передаёт HTTP status или SDK error, concrete provider протекает внутрь API. - -Unexpected exception не обязана превращаться в expected failure. Cancellation объявляется отдельно от failure, если caller управляет ею. Disconnect или timeout после отправки неидемпотентной команды может означать `OUTCOME_UNKNOWN`, а не доказанный отказ. - -## Adapter module - -Adapter соединяет port с concrete provider: - -```text -api-owned port ← adapter → SDK / REST / storage / platform / realtime -``` - -```ts -import type { - AuthIdentityPort, -} from '@/domains/auth/api/ports' - -export const createAuthRestAdapter = ( - client: IdentityClient, -): AuthIdentityPort => ({ - async signIn(command) { - try { - const response = await client.signIn({ - login: command.identifier, - password: command.secret, - }) - - return { - ok: true, - value: { - expiresAt: response.expires_at, - subject: response.user_id, - }, - } - } catch (error) { - return mapIdentityProviderFailure(error) - } - }, -}) -``` - -Adapter преобразует protocol arguments, records и expected failures, но не решает, какой `AuthError` получит приложение, не добавляет предметный fallback и не объявляет метод Domain API. - -## Размещение adapters - -Каждая связная production-реализация является отдельным SLM-модулем Group `adapters`: - -```text -auth/adapters/ -├── identity-rest/ -│ └── index.ts -├── identity-realtime/ -│ └── index.ts -└── session-cookie/ - └── index.ts -``` - -Один adapter-модуль может реализовать несколько тесно связанных ports одного provider. Group `adapters` не имеет `index.ts` и не реэкспортирует дочерние модули. - -Production adapter запрещено определять: - -- внутри `api`; -- закрытым сегментом assembly; -- inline-функцией в `app` или composition; -- частью framework binding; -- mutable registry или service locator. - -Concrete adapters в production импортируют только assemblies своего домена. Adapter tests импортируют соответствующий module напрямую. - -## Универсальный infra service - -Adapter может использовать публичный API `infra`, если concrete technical service является универсальным для приложения: - -```text -auth adapter - → infra/http-client - → external identity provider -``` - -Совпадение сигнатур `infra` API и port не переносит ownership port в `infra`. Adapter остаётся явной границей provider mapping, failures и environment. Он может быть тонким, но не добавляет фиктивные преобразования ради объёма кода. - -## Cross-domain API dependency - -Готовый API другого домена не является technical port: - -```ts -import type { - AuthSessionApi, -} from '@/domains/auth/api' - -export type UserProfileApiDependencies = Readonly<{ - auth: Pick - profile: UserProfilePort -}> -``` - -Graph owner создаёт Auth раньше User и передаёт `auth.session` в User assembly. User не объявляет structural copy чужого API и не создаёт bridge adapter без реального преобразования контракта. - -Если expected Auth failure становится публичным outcome User, User API преобразует его в собственную `UserError`. При exception-модели он может использовать публичный guard из `auth/api/runtime`. - -## Framework-only SDK - -Некоторые SDK доступны только как framework Provider, hook или component, например CAPTCHA или payment element. Framework binding может получить opaque token или operation input через такой SDK и передать его команде Domain API: - -```text -framework SDK - → opaque token - → Domain API command - → port - → provider adapter -``` - -Binding не вызывает предметную provider operation напрямую, SDK type не входит в public Domain API, а generic technical UI при необходимости разделяется между `infra`, `ui` и composition. - -## Tests и fake ports - -Локальные fake implementations в API-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри test boundary и позволяют детерминированно задавать records, failures, cancellation и realtime события. - -Adapter contract tests отдельно доказывают, что concrete provider действительно реализует port. API-тест с идеальным fake не заменяет эту проверку. diff --git a/skills/slm-design/reference/draft/level-2/domains/framework-bindings.md b/skills/slm-design/reference/draft/level-2/domains/framework-bindings.md deleted file mode 100644 index 5022ff0..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/framework-bindings.md +++ /dev/null @@ -1,211 +0,0 @@ -# Framework Groups и модули - -> Пояснение domain-specific framework-кода, materialized state и RSC boundaries на примере React. - -## Связанные правила - -- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006) -- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) -- [`SLM-L2-FRAMEWORK-R014`](../../rules/level-2.md#slm-l2-framework-r014) -- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015) -- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022) -- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028) - -## Framework Group - -Папка для domain-specific React binding modules называется `react`: - -```text -domains/auth/react/ # Framework Group -├── session/ # SLM-модуль -│ ├── hooks/ -│ ├── providers/ -│ └── index.ts -├── queries/ # SLM-модуль -│ └── index.ts -└── login-form/ # SLM-модуль - ├── components/ - └── index.ts -``` - -`react` является Group, а не модулем. У неё нет `index.ts`, реализации, state, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`. - -Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. - -## Framework binding module - -Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании готового Domain API своего домена с конкретным framework. - -Framework binding может: - -- передавать готовый API через Provider и context; -- предоставлять domain-specific hooks; -- хранить framework projection в query cache или store; -- отображать public models, outcomes и domain errors; -- реализовывать SSR prefetch и client hydration; -- реализовывать переиспользуемую domain-specific форму или guard; -- связывать framework lifecycle с явной realtime subscription. - -Он не вызывает `api/factory` или assembly, не выбирает adapters, не импортирует SDK предметного external source и не определяет новые предметные операции. - -Framework binding импортирует consumer types и deterministic runtime через разные фасеты: - -```ts -import type { - AuthError, - AuthSessionApi, -} from '@/domains/auth/api' - -import { - isAuthError, -} from '@/domains/auth/api/runtime' -``` - -Импорты `api/ports`, `api/factory` и `adapters/*` запрещены. - -## Готовый API - -`auth/react/session` может владеть Provider для уже созданного `AuthSessionApi`: - -```tsx -'use client' - -type AuthSessionProviderProps = PropsWithChildren<{ - api: AuthSessionApi -}> - -export const AuthSessionProvider = ({ - api, - children, -}: AuthSessionProviderProps) => { - return ( - - {children} - - ) -} -``` - -Публичный путь модуля: - -```ts -import { - AuthSessionProvider, - useAuthApi, -} from '@/domains/auth/react/session' -``` - -Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API. - -## Query и store projection - -`auth/react/queries` может использовать TanStack Query, SWR, Zustand или другой React runtime поверх готового API: - -```ts -export const useAuthSessionQuery = () => { - const api = useAuthApi() - - return useQuery({ - queryKey: ['auth', 'session'], - queryFn: api.getSession, - }) -} -``` - -Query keys, stale time, pending status и hydration принадлежат binding. Значения и ошибки поступают через Domain API. Framework types не становятся частью `AuthSessionApi`. - -Framework projection не импортируется другим доменом. Cross-domain UI собирается в `compositions`. - -## Realtime binding - -Binding может запускать subscription готового API в framework lifecycle: - -```text -component/provider scope - → Domain API subscribe - → verified domain events - → query invalidation or API-owned projection - → cleanup on scope end -``` - -Binding не импортирует WebSocket client и не разбирает frames. После cleanup он не принимает late callbacks. Если reconnect создаёт gap, binding обрабатывает публичный `RESYNC_REQUIRED` outcome и повторно загружает snapshot через Domain API. - -## Domain-specific UI - -`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с Auth API, public models и errors своего домена. Она может использовать публичный API соседнего `auth/react/session`, если статический граф остаётся ацикличным. - -Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Domain API может вернуть `AUTH_REQUIRED`, но переход на `/login` выбирает composition. - -## Framework-only SDK - -SDK, доступный только через Provider, hook или component, может использоваться binding для получения opaque operation input: - -```text -CAPTCHA React component - → opaque token - → AuthApi command -``` - -Binding не использует SDK для самостоятельной предметной операции, не превращает SDK response в public domain model и не экспортирует SDK type через Domain API. Если SDK предоставляет reusable technical UI без предметной модели, его generic integration может принадлежать `infra` и `ui`, а composition связывает её с доменом. - -## Запрет cross-domain framework imports - -Framework binding module не импортирует hooks, contexts, Providers, stores или components другого домена: - -```ts -// Недопустимо: domains/user/react/profile -import { - useAuthSessionQuery, -} from '@/domains/auth/react/queries' -``` - -Cross-domain UI собирается в `compositions`: - -```tsx -const session = useAuthSessionQuery() - -return ( - -) -``` - -Если User Domain API постоянно нуждается в Auth, готовый `AuthSessionApi` передаётся User assembly при сборке runtime-графа. User framework binding работает уже со своим API. - -## SSR, RSC и client boundary - -Server prefetch и client hooks могут принадлежать разным modules Framework Group с совместимыми entry points. Они не разделяют API instance или mutable cache: - -```text -server binding - → server API instance - → prefetch - → hydration payload - -client binding - → client API instance - → hydrate - → rendering -``` - -Server Component не передаёт API object в Client Component. Client reference и Server Action reference объявляются checker-у отдельно от executable imports. Если Client Component участвует в SSR или prerender, его server render graph проверяется отдельно от browser hydration graph; browser-only capability используется только через объявленную framework-deferred boundary. - -## Публичные API - -```ts -import { - AuthSessionProvider, -} from '@/domains/auth/react/session' - -import { - useAuthSessionQuery, -} from '@/domains/auth/react/queries' - -import { - LoginForm, -} from '@/domains/auth/react/login-form' -``` - -Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена. diff --git a/skills/slm-design/reference/draft/level-2/domains/open-questions.md b/skills/slm-design/reference/draft/level-2/domains/open-questions.md deleted file mode 100644 index 4d93f5f..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/open-questions.md +++ /dev/null @@ -1,75 +0,0 @@ -# Открытые вопросы Level 2 - -> Эти вопросы не являются правилами и не отменяют зафиксированные границы. - -## Зафиксированные решения - -- Level 1 является общей базой, а Level 2 применяется отдельно к выбранным предметным областям. -- Доменный модуль Level 1 и доменный пакет Level 2 могут постоянно сосуществовать в одном SLM root. -- Одна предметная область имеет только одну форму. -- Корень package содержит только metadata, модуль `api` и допустимые Groups и не имеет executable API. -- Модуль `api` является единственным семантическим шлюзом данных и операций домена. -- Публичные фасеты разделяют consumer types, implementer ports, factories и optional deterministic runtime. -- Каждый Domain API имеет одну factory; production factories импортируют только assemblies своего домена. -- Dependency ports принадлежат `api`, а production adapters являются отдельными modules Group `adapters`. -- Provider errors проходят через closed port failures и преобразуются в stable domain errors. -- Каждый package содержит `assemblies/default` для одного baseline production context. -- Имя `default` не определяет environment или isomorphic compatibility. -- Дополнительная assembly появляется только для отличающегося graph, dependencies, trust, capabilities или lifecycle. -- Framework bindings владеют state, cache, reactivity и hydration и не обращаются к предметному external source в обход Domain API. -- Server и client используют разные API instances и caches; через RSC boundary проходят только serializable values. -- Realtime transport скрыт adapter, а messages и subscriptions доступны через Domain API. -- Realtime port объявляет correlation, ACK, ordering, duplicate, reconnect, resync, cancellation и cleanup semantics. -- Assembly rollback выполняет cleanup собственных resources и полученных adapter lifecycle handles; successful aggregate cleanup идемпотентен и прекращает callbacks. -- Cross-domain Domain API является отдельной runtime dependency, а не автоматически local port. -- Runtime assembly graph остаётся ацикличным. - -## Канал ошибок - -Нужно выбрать project-wide recommendation между exceptions и discriminated `Result`, определить форму cancellation и unexpected failures, а также сериализацию domain errors через RPC и Server Actions. - -Архитектурная цепочка provider failure → port failure → domain error от выбора канала не зависит. - -## Port semantics - -Нужно определить минимальный machine-readable способ объявлять behavioral guarantees ports: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup. - -Не все ports требуют все поля, но существенная для корректности semantics не должна существовать только в комментарии adapter implementation. - -## Environment metadata - -Нужно выбрать формат для capability sets, resolver conditions, executable edges, framework reference edges, dynamic imports и API-safe package declarations. - -Особенно требуется проверить Next.js RSC, Server Actions, edge runtime, workers и conditional exports внешних packages. - -## Runtime dependency graph - -Нужно выбрать machine-readable формат assembly inputs и создаваемых API, чтобы автоматически обнаруживать runtime cycles, скрытые static structural ports и неверный cleanup order. - -До появления формата runtime graph остаётся обязательной review boundary. - -## Lifecycle - -Гарантии rollback, reverse cleanup, idempotence и отсутствия callbacks после disposal зафиксированы. Ещё нужно определить aggregate cleanup errors, retry failed cleanup, request abort, deadline disposal и поведение API после завершения scope. - -## Hydration payload - -Нужно выбрать рекомендации по versioning, schema validation, stale persisted cache, partial hydration и защите request-specific или sensitive values. - -Hydration payload остаётся framework-owned и не может содержать API instance или mutable client. - -## Multiple APIs и shared capabilities - -Нужно проверить рекомендуемую форму для нескольких Domain API, которые используют один shared connection, transaction coordinator или framework-neutral operation context, не перенося предметную семантику в adapter или assembly. - -Если independent factories не сохраняют atomicity, APIs должны объединяться; точный критерий требует дополнительных примеров. - -## Framework-only SDK - -Нужно проверить React/Vue SDK, которые предоставляют capability только через Provider, hook или component: payment elements, CAPTCHA, maps и identity widgets. - -Зафиксировано, что binding может передать Domain API только opaque operation input и не выполняет предметную provider operation напрямую. Требуются проверочные примеры для `infra` + `ui` + composition. - -## Масштаб production graph - -Нужно проверить lazy и route-scoped сборку на SLM root с десятками Level 2 packages. Импорт assemblies остаётся side-effect-free, а graph owner создаёт только dependency-connected часть graph; конкретный registry или lazy-loading mechanism пока не нормирован. diff --git a/skills/slm-design/reference/draft/level-2/domains/realtime.md b/skills/slm-design/reference/draft/level-2/domains/realtime.md deleted file mode 100644 index 7ba2bbf..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/realtime.md +++ /dev/null @@ -1,217 +0,0 @@ -# Realtime messages и subscriptions - -> Пояснение Domain API поверх WebSocket, SSE, GraphQL subscriptions и provider SDK. - -## Связанные правила - -- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006) -- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009) -- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010) -- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021) -- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023) -- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027) -- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028) -- [`SLM-L2-REALTIME-R029`](../../rules/level-2.md#slm-l2-realtime-r029) - -## Граница транспорта - -Realtime transport находится внутри adapter: - -```text -WebSocket / SSE / GraphQL / SDK - → adapter - → realtime port - → Domain API - → domain event/outcome/error - → framework projection -``` - -Domain API не экспортирует `WebSocket`, `MessageEvent`, raw frames, SDK subscription, provider error или transport close code. Port также не должен быть generic socket API с `send(frame)` и `onMessage(frame)`: он описывает capability, необходимую конкретному домену. - -## Realtime-команда - -Публичная команда может выглядеть как обычный Promise независимо от транспорта: - -```ts -export type ChatApi = { - sendMessage: ( - command: SendMessageCommand, - ) => Promise -} -``` - -Port возвращает типизированный technical outcome: - -```ts -export type SendMessagePortFailure = - | Readonly<{ type: 'FORBIDDEN' }> - | Readonly<{ type: 'RATE_LIMITED' }> - | Readonly<{ type: 'UNAVAILABLE' }> - | Readonly<{ type: 'OUTCOME_UNKNOWN' }> - -export type ChatRealtimePort = { - sendMessage: ( - command: SendMessagePortCommand, - ) => Promise> -} -``` - -Domain API преобразует port result в `ChatMessage` или собственную `ChatError`. Для прикладного consumer transport остаётся незаметным. - -## Correlation - -`socket.send()` подтверждает только локальную отправку frame. Чтобы завершить `sendMessage()` результатом server command, protocol должен сопоставить command и acknowledgement: - -```text -Domain API command - → adapter assigns operationId - → transport frame - → server ACK or ERROR with operationId - → adapter settles pending port operation - → Domain API maps outcome -``` - -Adapter владеет protocol registry pending operations и не бросает error из async `onmessage`, который невозможно поймать вокруг исходного `send`. Он завершает соответствующую Promise или другой объявленный operation channel. - -Correlation contract фиксирует: - -- источник и scope уникальности operation ID; -- момент, когда команда считается принятой или выполненной; -- поведение при duplicate и late acknowledgement; -- timeout и cancellation; -- очистку pending operation при disconnect; -- связь command outcome с последующими domain events. - -Если provider не возвращает correlation metadata, API не обещает индивидуальный результат. Такая операция является fire-and-forget, а поздний отказ публикуется отдельным domain event либо доступна только общая ошибка transport scope. - -## Outcome uncertainty и idempotency - -Disconnect после отправки и до acknowledgement не доказывает, что command не выполнена: - -```text -frame sent - → connection lost - → server may have committed command - → acknowledgement unknown -``` - -Port возвращает `OUTCOME_UNKNOWN`, если это различие нужно Domain API. Автоматический retry безопасен только при provider guarantee или idempotency key. Domain API не преобразует неопределённый outcome в ложное `MESSAGE_NOT_SENT`. - -## Subscription - -Публичная subscription предоставляет проверенные events и явный cleanup: - -```ts -export type ChatEvent = - | Readonly<{ - type: 'MESSAGE_CREATED' - message: ChatMessage - revision: number - }> - | Readonly<{ - type: 'MESSAGE_REMOVED' - messageId: string - revision: number - }> - -export type ChatSubscription = Readonly<{ - close: () => Promise -}> - -export type ChatObserver = Readonly<{ - onEvent: (event: ChatEvent) => void - onError: (error: ChatRealtimeError) => void - onStatus: (status: ChatRealtimeStatus) => void -}> - -export type ChatApi = { - subscribe: ( - observer: ChatObserver, - ) => Promise -} -``` - -Callback, async iterable или другой project-wide channel допустимы. Обязательны типизированные domain events/errors, определённый lifecycle и cleanup. - -## Stable errors и statuses - -Начальная ошибка подключения может завершить `subscribe()` domain error. Ошибка после успешного запуска приходит через stream channel. - -Не каждый transport failure становится domain error. Adapter может восстановить соединение и опубликовать только устойчивый status: - -```ts -export type ChatRealtimeStatus = - | Readonly<{ type: 'CONNECTED' }> - | Readonly<{ type: 'RECONNECTING' }> - | Readonly<{ type: 'RESYNC_REQUIRED' }> - | Readonly<{ type: 'CLOSED' }> -``` - -Публичные errors описывают реакции приложения, например `CHAT_REALTIME_UNAVAILABLE`, `CHAT_FORBIDDEN` или `CHAT_SESSION_EXPIRED`. Close codes, provider messages и SDK classes остаются внутри adapter. - -Caller-initiated close не является domain error. - -## Ordering, duplicates и resync - -Realtime port явно объявляет: - -- гарантируется ли порядок событий; -- возможна ли at-least-once delivery; -- кто устраняет duplicates; -- содержит ли event revision или sequence; -- как обнаруживается gap после reconnect; -- откуда загружается authoritative snapshot. - -Если adapter не может доказать непрерывность, Domain API публикует `RESYNC_REQUIRED`. Framework binding invalidates projection и получает snapshot через query Domain API. - -Binding не применяет raw delta к публичной модели. Если безопасный merge содержит предметную семантику, его выполняет операция Domain API или pure-функция `api/runtime`. - -## Shared connection - -Один adapter может multiplex несколько ports и subscriptions через физическое соединение. Connection имеет явные owner, scope, multiplicity и cleanup: - -```text -assembly-owned connection - ├── chat messages port - ├── presence port - └── notification port -``` - -Cleanup отдельной subscription снимает её lease. Cleanup assembly закрывает shared connection после завершения всех принадлежащих графу operations. После awaited cleanup новые callbacks запрещены. - -Если создание connection завершилось успешно, а следующий шаг assembly упал, connection закрывается на rollback path до возврата ошибки. - -## Framework materialization - -Framework binding выбирает техническую реакцию на domain event: - -```text -MESSAGE_CREATED - → update query cache verified full model - -RESYNC_REQUIRED - → invalidate query - → fetch snapshot through Domain API -``` - -Zustand, QueryClient, Pinia или другой store не импортирует socket adapter и не интерпретирует protocol frame. Он хранит только public values, events, statuses и errors Domain API. - -## SSR, RSC и workers - -Browser assembly может включать realtime adapter, а request/RSC assembly — только query API. Отсутствующий realtime API не заменяется throwing stub. - -Server process или worker получает отдельную assembly и scope, если ему действительно нужна долгоживущая subscription. Server Component не открывает connection, которая переживает request, без отдельного owner вне request scope. - -## Тестовые границы - -API-тест с fake realtime port проверяет mapping records, failures, stable errors и public events. Adapter contract test проверяет protocol frames, correlation, timeout, disconnect, duplicate acknowledgement, reconnect, resync и cleanup. Framework test проверяет materialization и invalidation. Assembly test проверяет shared connection, rollback и отсутствие callbacks после disposal. - -Контрольные случаи: - -- acknowledgement приходит после timeout; -- duplicate acknowledgement приходит после reconnect; -- event приходит раньше command acknowledgement; -- disconnect происходит после send и до ACK; -- unsubscribe завершается во время pending callback; -- adapter получает malformed payload; -- следующий resource assembly падает после открытия connection. diff --git a/skills/slm-design/reference/draft/level-2/domains/state-cache.md b/skills/slm-design/reference/draft/level-2/domains/state-cache.md deleted file mode 100644 index 3b44381..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/state-cache.md +++ /dev/null @@ -1,172 +0,0 @@ -# Состояние, cache и hydration - -> Пояснение границы между семантической властью Domain API и framework-owned materialization. - -## Связанные правила - -- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006) -- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-a007) -- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012) -- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015) -- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018) -- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028) - -## Основная граница - -Модуль `api` определяет форму и семантику доменных значений, но не выбирает способ их хранения и реактивной доставки. TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX, Pinia, Signals и RxJS остаются в framework bindings или compositions. - -```text -Domain API - → public model/outcome/event - → framework projection - → rendering -``` - -Concrete state/query runtime не импортируется модулем `api`, не является dependency port фабрики и не входит в публичный Domain API. - -## Виды materialization - -### Source cache - -Технический cache внешнего provider внутри adapter. Он может отвечать за transport deduplication, connection state, provider retry и хранение port records. - -Source cache не публикует raw DTO, query keys, mutable client или library result через Domain API. Если adapter создаёт timers, subscriptions или connection, он остаётся единственным владельцем и экспортирует lifecycle handle, который assembly только агрегирует. Если resource создаёт assembly, adapter использует его как borrowed capability и не закрывает самостоятельно. - -### Framework projection - -State или cache, который framework binding строит из готового Domain API: - -```ts -export const useAuthSession = () => { - const api = useAuthApi() - - return useQuery({ - queryKey: ['auth', 'session'], - queryFn: api.getSession, - }) -} -``` - -Query key, stale time, pending/retry status, Suspense, rendering stale data и техническая invalidation принадлежат binding. `AuthSession` и `AuthError` принадлежат `api`. - -### Composition state - -Состояние конкретной страницы или multi-domain flow принадлежит composition: выбранная вкладка, открытый modal, draft формы, route transition и координация нескольких API. - -Если draft приобретает самостоятельную доменную семантику, Domain API предоставляет validation или transition, но framework по-прежнему хранит возвращаемое readonly value. - -## Domain API не является store - -Публичный Domain API не экспортирует: - -- mutable store; -- `getState` и `setState` framework runtime; -- QueryClient; -- Zustand `StoreApi`; -- framework hook; -- глобальный singleton данных; -- универсальный state port. - -API methods возвращают значения и outcomes. Framework consumer решает, как долго их хранить и когда повторно запросить. - -Это не означает, что framework определяет предметные transitions. Он материализует только то, что произвёл или проверил API. - -## Invalidation и retry - -| Политика | Обычный владелец | -|---|---| -| Query key, stale time, deduplication, background refetch | Framework binding | -| Transport retry безопасного запроса | Adapter | -| Rendering stale data, Suspense, polling UI | Framework binding или composition | -| Запрет повторной предметной команды | Domain API | -| Cooldown, лимит попыток, допустимый transition | Domain API | -| Freshness, влияющая на корректность сценария | Domain API через operation contract | - -После успешной команды binding может технически invalidировать известные query keys. Если выбор invalidation выражает предметную семантику, Domain API возвращает устойчивый outcome/event, а binding только отображает его на framework cache. - -## Optimistic updates - -Framework binding не конструирует произвольную публичную модель из form input, raw DTO или текущего cache. Optimistic projection допустима, когда предполагаемое значение: - -- возвращено командой Domain API; -- создано отдельной операцией Domain API; -- создано или проверено pure-функцией `api/runtime`. - -```ts -import { - projectProfileUpdate, -} from '@/domains/user/api/runtime' - -const optimisticProfile = projectProfileUpdate( - currentProfile, - command, -) - -queryClient.setQueryData(profileKey, optimisticProfile) -``` - -`projectProfileUpdate` владеет предметным transition, а `setQueryData` остаётся framework operation. - -## Concurrent mutations и realtime - -При нескольких optimistic commands и realtime events binding не выбирает самостоятельно ordering, versioning, rollback или rebase. Domain API возвращает correlation/version metadata либо предоставляет deterministic reconciliation: - -```ts -const nextProjection = reconcileProfile({ - current, - event, - pendingCommands, -}) -``` - -Если API не объявляет безопасный merge, binding invalidates cache и получает authoritative snapshot через Domain API. Это предпочтительнее скрытого применения неполного delta. - -## Persistence - -Framework cache может технически сохраняться между reloads, но persisted value не становится источником предметной истины. После восстановления значение: - -- используется как stale projection до revalidation; -- либо проверяется публичным validator `api/runtime`; -- либо отбрасывается и загружается через Domain API. - -Если storage является самостоятельным предметным внешним источником, доступ к нему оформляется dependency port и adapter. Автоматический framework middleware не обходит API validation и transitions. - -## SSR и hydration - -Server и client имеют разные API instances и caches: - -```text -server request - → request assembly - → server Domain API - → server framework cache - → serializable hydration payload - -browser - → client assembly - → client Domain API - → hydrated client cache -``` - -Hydration payload принадлежит framework binding и содержит только public domain values и framework metadata. API object, functions, ports, adapters, mutable clients и request secrets не сериализуются. - -Server cache создаётся на каждый request и не хранится в module singleton. Client cache создаётся на согласованный application или route scope. - -## RSC и Server Actions - -Server Component вызывает server Domain API и передаёт Client Component только сериализуемые values или hydration payload. Client Component создаёт или получает отдельный client API instance; при SSR его render отдельно проверяется в server prerender graph до browser hydration. - -Server Action создаёт request-scoped production graph на каждый вызов, выполняет Domain API command и гарантированно выполняет все cleanup obligations графа. Client invocation Server Action является framework reference edge, а не передачей server API в browser. - -## Проверка на ревью - -Для каждого state/query runtime определяется: - -- является ли он source cache, framework projection или composition state; -- откуда поступают public domain values; -- кто определяет validation и transition; -- где находятся library-specific types и keys; -- как invalidation связана с Domain API outcomes; -- как обрабатываются optimistic concurrency и realtime events; -- что сериализуется при SSR/RSC; -- соответствует ли cache scope области жизни API graph. diff --git a/skills/slm-design/reference/draft/level-2/domains/testing.md b/skills/slm-design/reference/draft/level-2/domains/testing.md deleted file mode 100644 index 4356bc8..0000000 --- a/skills/slm-design/reference/draft/level-2/domains/testing.md +++ /dev/null @@ -1,189 +0,0 @@ -# Тестирование доменного пакета - -> Проверка Domain API, port contracts, production wiring и framework projections Level 2. - -## Связанные правила - -- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016) -- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020) -- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021) -- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022) -- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023) -- [`SLM-L2-API-R024`](../../rules/level-2.md#slm-l2-api-r024) -- [`SLM-L2-PORT-R027`](../../rules/level-2.md#slm-l2-port-r027) -- [`SLM-L2-REALTIME-R029`](../../rules/level-2.md#slm-l2-realtime-r029) -- [`SLM-L2-ASSEMBLY-R030`](../../rules/level-2.md#slm-l2-assembly-r030) - -## Размещение - -Тест находится рядом с module-owner проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`. - -| Проверяемая граница | Владелец теста | -|---|---| -| Domain API operations, models, outcomes и errors | `api` через factory | -| Deterministic runtime и guards | `api` | -| Реализация dependency port | Adapter | -| Default и специальный production graph | Assembly | -| Provider, hook, query/store integration и hydration | Framework binding | -| Cross-domain graph | Graph owner | - -## Domain API через фабрику - -Каждый публичный сценарий проверяется через фабрику владеющего им Domain API с управляемыми fake ports: - -```ts -import type { - AuthSessionApi, -} from '@/domains/auth/api' - -import type { - AuthIdentityPort, -} from '@/domains/auth/api/ports' - -import { - createAuthSessionApi, -} from '@/domains/auth/api/factory' - -const identity: AuthIdentityPort = createIdentityPortFake({ - signIn: { - ok: true, - value: { - expiresAt: 1_700_000_000_000, - subject: 'user-1', - }, - }, -}) - -const api: AuthSessionApi = createAuthSessionApi({ - identity, - runtime: { - createId: () => 'id-1', - now: () => 1_700_000_000_000, - }, -}) -``` - -API suite проверяет: - -- public models и outcomes; -- validation commands и port records; -- mapping каждого expected port failure; -- отсутствие raw provider details в domain errors; -- cancellation и outcome uncertainty при наличии; -- pure transitions и reconciliation; -- каждый API отдельно при нескольких factories. - -API-тест не использует React, production assembly, реальный SDK, backend, system clock или module singleton. - -## Adapter contract test - -Adapter test доказывает, что concrete provider реализует port: - -- правильно преобразует arguments; -- валидно читает provider record; -- возвращает port record, а не raw DTO; -- различает закрытые port failures; -- не создаёт public domain error; -- соблюдает cancellation, timeout и lifecycle contract; -- использует заявленный environment capability set. - -Fake port в API-тесте не заменяет adapter contract test. Идеальный fake может соответствовать типу, пока реальный endpoint или SDK уже изменился. - -## Default assembly - -Тест `assemblies/default` проверяет: - -- вызов только нужных API factories; -- выбор штатных adapter modules; -- точный именованный состав graph; -- объявленный baseline capability set; -- отсутствие module-import side effects; -- передачу cross-domain API аргументом; -- отсутствие factory/adapter leakage наружу; -- aggregate cleanup, если assembly создаёт owned resource или получает adapter lifecycle handle. - -Каждая дополнительная assembly тестирует отличие своего production context, а не повторяет полный API suite. - -## Partial construction и cleanup - -Assembly test моделирует ошибку после регистрации каждого cleanup obligation, включая adapter-owned handle: - -```text -resource A created -resource B creation failed - → cleanup A awaited - → original failure propagated -``` - -Проверяются reverse dependency order, idempotent repeated disposal, попытка очистить все resources и отсутствие callbacks после завершившегося cleanup. - -Assembly без cleanup obligations не тестирует пустой `dispose`, потому что не обязана его предоставлять. Если adapter передал lifecycle handle, obligation существует независимо от resource ownership. - -## Framework binding - -Framework test получает fake готового Domain API и проверяет собственную responsibility: - -- Provider и hook; -- query keys, stale policy и invalidation; -- store projection; -- optimistic update через API-owned function; -- hydration payload; -- public domain errors; -- subscription cleanup; -- отсутствие direct SDK/external source access. - -Framework test не повторяет validation и failure mapping всех API operations. - -## Realtime - -API realtime test с fake port проверяет public events, stable errors, acknowledgement semantics и `OUTCOME_UNKNOWN` mapping. - -Adapter realtime contract test проверяет: - -- command correlation; -- duplicate и late acknowledgement; -- disconnect до ACK; -- ordering и sequence gaps; -- reconnect и resync; -- malformed frames; -- cancellation и unsubscribe; -- отсутствие callbacks после cleanup. - -Assembly test отдельно проверяет shared connection, multiplexing, rollback и graph-level disposal. Framework test проверяет только materialization events и invalidation. - -## SSR, RSC и Server Actions - -Environment tests подтверждают: - -- request-scoped API и cache не разделяются между users; -- API instance не входит в hydration payload; -- Client Component создаёт отдельный client graph; -- SSR-enabled Client Component проверяется в server prerender и browser hydration graphs; -- browser-only effect не выполняется во время server render; -- Server Action создаёт и очищает graph на каждый вызов; -- framework reference edge не превращается в executable client/server leak; -- `default` проверяется под всеми объявленными resolver conditions. - -## Cross-domain graph - -Graph owner test создаёт assemblies и construction points модулей Level 1 в dependency order и проверяет runtime inputs и callbacks. Отдельно проверяется невозможность mixed L1/L2 циклической сборки и reverse cleanup order. - -Не достаточно проверить только статический import DAG: runtime dependencies, передаваемые arguments, должны быть представлены architecture mapping или review evidence. - -## Автоматические структурные проверки - -Import и export checks подтверждают: - -- отсутствие root API пакета и Groups; -- обязательные `api`, `api/factory` и `assemblies/default`; -- `api/ports` только при наличии declared ports; -- допустимые exports каждого фасета; -- importer matrix factories, ports и concrete adapters; -- отсутствие deep imports; -- отсутствие SDK, framework и state/query runtime в graph `api`; -- отсутствие запрещённых cross-domain imports; -- environment compatibility под configured conditions; -- отсутствие статических cycles. - -Runtime tests не заменяют import-graph checks и architecture review. diff --git a/skills/slm-design/reference/draft/level-2/terminology.md b/skills/slm-design/reference/draft/level-2/terminology.md deleted file mode 100644 index c4b3089..0000000 --- a/skills/slm-design/reference/draft/level-2/terminology.md +++ /dev/null @@ -1,233 +0,0 @@ -# Терминология Level 2 - -> Нормативные определения рабочего черновика. Этот раздел не объявляет правила. - -Level 2 наследует терминологию и матрицу слоёв Level 1. Он применяется отдельно к предметным областям, которым нужна пакетная форма с контролируемым Domain API, dependency ports, production adapters, штатной assembly и самостоятельными framework bindings. - -## Формы домена - -### Форма домена - -Структурное представление одной доменной ответственности. На Level 1 предметная область представлена доменным модулем. Для предметной области, использующей Level 2, этот модуль заменяется доменным пакетом. Одна предметная область не использует обе формы одновременно. - -### Доменный пакет - -Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла статического графа зависимостей. - -Корень пакета может содержать только декларативную metadata, обязательный модуль `api` и допустимые Groups. Metadata хранит статические данные о пакете, владении, environment capability sets и конфигурации проверки и не содержит кода, выполняемого приложением. - -Доменный пакет является policy boundary: проверка использует объявленную принадлежность модулей пакету для контроля структуры, внешних источников и междоменных связей. Публичными dependency boundaries кода остаются модули внутри пакета, а не пакет целиком. - -### Навигационная Group слоя `domains` - -Group, размещённая непосредственно в слое `domains` или другой такой Group. Она классифицирует доменные модули Level 1, доменные пакеты Level 2 и другие навигационные Groups, но не содержит исполняемый код и не образует dependency boundary. - -### Модуль доменного пакета - -Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: Domain API, assembly, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел статического графа зависимостей. - -Модуль внутри Group пакета не является вложенным модулем, потому что его ближайшая внешняя граница не является модулем. - -## Доменный API - -### Модуль `api` - -Обязательный SLM-модуль `api`, который является семантическим шлюзом предметной области для приложения. Он объявляет публичные модели, один или несколько именованных Domain API, соответствующие фабрики, dependency ports, ожидаемые доменные ошибки и необходимый внешним потребителям детерминированный runtime. - -Модуль `api` определяет смысл данных и операций, но не является framework store или query cache. Он не импортирует SDK, transport client, storage implementation, framework, state/query manager или platform I/O. Его экземпляры замыкают переданные ports и могут координировать отдельную операцию, но не служат скрытым изменяемым источником данных приложения между вызовами. - -Термин **публичный API модуля `api`** обозначает фасеты SLM-модуля. Термин **Domain API** обозначает именованный runtime-контракт предметных операций. Эти понятия не взаимозаменяемы. - -### Domain API - -Именованный публичный runtime-контракт связного набора предметных команд, запросов или подписок внутри одного домена. Потребитель вызывает Domain API и получает только публичные модели, outcomes и ошибки предметной области, не зная provider, endpoint, SDK или transport protocol. - -Модуль `api` может объявить несколько Domain API, если они независимо собираются, имеют разные ports, trust boundaries или реальные consumers. Каждый публичный сценарий принадлежит ровно одному Domain API. APIs с общим неразделимым состоянием, atomicity или lifecycle образуют один контракт либо получают один явно созданный shared capability через assembly. - -### Публичная доменная модель - -Readonly-форма данных, которую Domain API принимает или возвращает внешнему потребителю. Публичная доменная модель принадлежит модулю `api`, не является backend DTO, cache record или framework view model и экспортируется только при наличии реального consumer. - -Внутренняя модель модуля `api`, port record и framework view model могут иметь другую форму и не становятся публичными только из-за принадлежности тому же домену. - -### Семантическая власть Domain API - -Право определять публичную доменную модель, validation внешних значений, допустимые предметные transitions, семантику операций, outcomes и ожидаемых ошибок. Adapter, assembly или framework binding может транспортировать, хранить, кэшировать и отображать значения, но не становится независимым источником этих решений. - -### Публичные фасеты `api` - -Объявленные entry points одного логического публичного API модуля `api`: - -| Путь | Статус | Содержимое | -|---|---|---| -| `api` | Обязательный | Только consumer-facing types: Domain API, public models, commands, outcomes и domain errors | -| `api/factory` | Обязательный | Только именованные runtime-фабрики Domain API | -| `api/ports` | При наличии dependency ports | Только implementer-facing types: ports, port records, port failures и factory dependency types | -| `api/runtime` | Необязательный | Только публичные детерминированные runtime-значения и функции | - -Фасет `api/runtime` может публиковать устойчивые error codes и guards, validators, value constructors, pure transitions, reconciliation functions, предметные константы и чистые projections, если они нужны реальным внешним потребителям. Он не содержит фабрики, изменяемое состояние, ввод-вывод, подписки, сценарии с runtime-зависимостями или environment-specific код. - -Фасеты не являются сегментами, вложенными модулями или самостоятельными узлами графа. Любой другой внешний путь внутрь `api` является deep import. - -### API-safe внешний пакет - -Внешняя библиотека, допустимая в import-графе модуля `api`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, query runtime, framework и техническая интеграция не становятся API-safe только из-за совместимости с несколькими средами. - -### Фабрика Domain API - -Публичная функция фасета `api/factory`, которая получает явные dependency ports и cross-domain API dependencies и создаёт экземпляр одного объявленного Domain API. Каждому Domain API соответствует ровно одна публичная фабрика. - -Фабрика не выбирает concrete adapter, assembly, environment или framework, не создаёт framework state и не запускает запрос, socket, subscription, timer или другую долгоживущую работу во время создания API. - -## Ports, adapters и ошибки - -### Dependency port - -Consumer-owned контракт runtime-возможности, которая нужна модулю `api` и требует production-реализации. Port определяет минимальные операции, success values, закрытые expected failures и существенные behavioral guarantees со стороны потребителя capability, а не копирует API конкретного provider. - -К ports относятся источники данных, external command gateways, storage, platform capabilities, clock, timer, random, ID generator и realtime event sources. Framework state/query manager, materialized cache и готовый API другого домена не являются dependency ports. - -Port и связанные implementer-facing types принадлежат модулю `api` и публикуются через type-only фасет `api/ports`. Они не содержат SDK classes, generated DTO, HTTP status, `WebSocket`, framework hooks или другие concrete provider types. - -### Port record - -Технически нейтральная форма значения на границе port, достаточная модулю `api` для validation и преобразования в публичную доменную модель. Port record принадлежит implementer-facing контракту и не является публичной моделью приложения или raw provider DTO. - -### Port failure - -Закрытый implementer-facing набор ожидаемых сбоев dependency port, достаточный модулю `api` для выбора собственного outcome или domain error. Adapter преобразует provider-specific failure в port failure; модуль `api` преобразует port failure в публичную семантику. - -Cancellation и неопределённый результат операции объявляются отдельно, если потребитель способен различать их. Unexpected programming failure не маскируется под expected port failure. - -### Доменная ошибка - -Безопасная публичная форма ожидаемого сбоя операции Domain API. Модуль `api` объявляет устойчивый readonly сериализуемый тип с кодом; при необходимости runtime-коды и guards публикуются через `api/runtime`. - -Ошибки provider, SDK, транспорта, storage, adapter или другого домена не являются доменными ошибками текущего API. Публичная ошибка не содержит исходные `message`, status, payload, class, stack или `cause`. Способ передачи ошибки, например exception или discriminated `Result`, не изменяет её владельца. - -### Cross-domain API dependency - -Готовый публичный API доменного модуля Level 1 или Domain API пакета Level 2, необходимый операции текущего Domain API. Это отдельный вид runtime-зависимости, а не dependency port и не adapter. Graph owner создаёт независимый API раньше зависимого и передаёт готовое значение assembly, которая передаёт его фабрике. - -Локальный bridge port вводится только при реальном переводе чужого контракта, а не автоматически для каждого междоменного ребра. - -### Adapter - -SLM-модуль в Group `adapters`, который реализует один или несколько связанных dependency ports поверх SDK, generated client, storage, transport, platform API, данных запроса или технического сервиса. Одна связная production-реализация принадлежит одному adapter-модулю и не размещается внутри assembly, framework binding или composition. - -Adapter знает concrete provider и переводит его arguments, records и expected failures в контракт port. Он не объявляет операции Domain API, публичные доменные модели, предметные fallbacks или domain errors. - -### Source cache - -Технический cache внешнего источника внутри adapter: transport deduplication, connection state, provider retry или хранение port records. Source cache не является публичной доменной моделью и не передаёт наружу library-specific keys, clients или result types. Adapter является владельцем созданного им cache и экспортирует lifecycle handle; assembly может агрегировать этот cleanup, не становясь вторым владельцем. Если resource создаёт assembly, adapter получает его как borrowed capability. - -## Assemblies и runtime-граф - -### Assembly - -SLM-модуль в Group `assemblies`, который создаёт явный именованный граф одного или нескольких Domain API пакета для одного объявленного production-контекста. Assembly выбирает публичные adapter-модули своего домена, вызывает фабрики и может принимать готовые API других доменов аргументами. - -Assembly не добавляет предметные операции, модели или ошибки. Импорт assembly не создаёт API и не запускает side effects; граф появляется только при явном вызове её builder. - -### Default assembly - -Обязательный модуль `assemblies/default`, который создаёт штатный production-граф домена для одного baseline capability set, объявленного проектом. Имя `default` означает каноническую сборку проекта, но не означает browser-, server-, shared- или isomorphic-совместимость. - -Для React + Vite `default` может быть browser-only. Для Next.js она может быть действительно изоморфной, только если каждый executable import совместим со всеми заявленными resolver conditions. Отличающийся набор API, dependencies, trust, runtime capabilities или lifecycle получает отдельную именованную assembly, например `rsc`, `administration` или `realtime-session`. - -### Дополнительная assembly - -Assembly, отличная от `default` и представляющая реальный дополнительный production-контекст. Имя может отражать environment только тогда, когда environment действительно определяет wiring; наличие RSC, server action или worker само по себе не требует отдельной assembly при неизменном совместимом графе. - -### Graph owner - -Composition root уровня `app`, `composition`, request handler, worker entry или test setup, который вызывает assemblies в ацикличном порядке, передаёт готовые cross-domain API зависимым assemblies и владеет областью жизни совокупного графа. - -Graph owner импортирует production builders assemblies, но не `api/factory` или concrete adapters. Он создаёт только dependency-connected часть графа, необходимую текущему application, route, request, worker или test scope. - -### Ресурс assembly - -Ресурс жизненного цикла, владельцем которого является assembly и который она обязана создать для возвращаемого графа. Adapter-owned resource сохраняет adapter owner и передаёт assembly только lifecycle handle для aggregate cleanup; borrowed resource не закрывается получателем. - -Assembly немедленно регистрирует каждое cleanup obligation: cleanup собственного resource и полученный adapter lifecycle handle. При частичной ошибке все зарегистрированные obligations выполняются в обратном порядке. Успешный результат с хотя бы одним obligation предоставляет идемпотентный aggregate async cleanup, после завершения которого resources не вызывают callbacks. Только graph без cleanup obligations не возвращает пустой `dispose`. - -## State, cache и framework - -### Framework projection - -Материализованное состояние или cache, которое framework binding строит из public models, outcomes и events Domain API для rendering, revalidation, optimistic UI и координации интерфейса. Concrete runtime может быть TanStack Query, SWR, Apollo, Zustand, Redux, Pinia, Signals или механизм конкретного framework. - -Framework projection принадлежит binding или composition, а не модулю `api`. Она может хранить значения и технические статусы, но не определяет параллельную предметную модель. Предметный optimistic merge, ordering, rollback, reconciliation или transition производится операцией Domain API либо детерминированной функцией `api/runtime`. - -### Hydration payload - -Сериализуемая framework-owned форма переноса projection между server и client scopes. Payload содержит только разрешённые публичные доменные значения и framework metadata и не содержит API instances, functions, mutable cache clients, ports, adapters или request secrets. - -Server и client создают отдельные API instances и framework caches. RSC передаёт через client boundary только сериализуемые значения или hydration payload; Server Action создаёт собственный request-scoped граф на каждый вызов. - -### Framework Group - -Group доменного пакета, названная по конкретному фреймворку: `react`, `vue` и аналогично. Она содержит framework binding modules, не имеет собственного `index.ts`, реализации, состояния, жизненного цикла или публичного API. - -### Framework binding module - -SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией готового Domain API с конкретным framework. Он может владеть Provider, hooks, query policy, framework projection, hydration и переиспользуемым domain-specific UI. - -Framework binding module получает готовый Domain API, не вызывает его фабрику или assembly и не выбирает adapters. Он не импортирует framework state, hooks или components другого домена и не обращается к предметному external source в обход Domain API. - -Framework-only SDK допустим внутри binding только для получения opaque operation input, например token от CAPTCHA или payment element; предметная операция всё равно выполняется через Domain API, а SDK type не пересекает его публичную границу. - -## Realtime - -### Realtime port - -Dependency port для двусторонних сообщений или подписок поверх WebSocket, SSE, GraphQL subscription, provider SDK или другого push-транспорта. Realtime port описывает предметно необходимую capability и проверяемые guarantees, но не публикует transport frames или concrete client. - -### Realtime-команда - -Операция Domain API, отправляемая через realtime port и имеющая объявленный момент подтверждения. Если приложение должно получить индивидуальный outcome, protocol adapter сопоставляет command, acknowledgement и failure посредством correlation metadata. - -Разрыв соединения после отправки и до подтверждения может означать неопределённый outcome. Без idempotency key или provider guarantee такой исход не объявляется безопасным failure или автоматически повторяемой командой. - -### Realtime subscription - -Явная операция Domain API, которая публикует только проверенные domain events, statuses и errors и предоставляет cleanup. Realtime port определяет ordering, duplicate delivery, reconnect, gap detection, resync, cancellation и момент, после которого завершившийся cleanup гарантирует отсутствие новых callbacks. - -Shared physical connection принадлежит adapter или assembly с явными scope, multiplicity и cleanup. Framework binding решает, как материализовать domain events: обновить projection, применить API-owned transition либо invalidировать cache и повторно запросить snapshot через Domain API. - -## Environment - -### Environment capability set - -Явно объявленный набор runtime-возможностей, доступных конкретной точке входа или assembly: DOM, cookies, filesystem, worker API, edge API, framework server runtime и аналогично. Название папки не определяет capability set. - -Совместимость проверяется по executable import-графу для каждого поддерживаемого набора resolver conditions и framework execution phase, включая server prerender и browser hydration. Runtime branching и tree shaking не доказывают изоляцию несовместимых импортов. - -### Framework reference edge - -Связь, которую framework преобразует в ссылку на другой executable graph вместо обычного runtime-вызова, например ссылка Server Component на Client Component или client invocation Server Action. Такая связь объявляется конфигурации проверки и анализируется отдельно от executable и type-only edges, но не отменяет проверку всех сред, в которых target graph исполняется самостоятельно. - -RSC не является универсальной третьей средой рядом с browser и server. Server Component выполняется в server scope. Client Component участвует в browser hydration и, при включённом SSR или prerender, также исполняется в отдельном server render graph; framework-deferred browser effects проверяются отдельно. Между RSC и client graph проходит serialization/reference boundary. - -## Структурная модель - -```text -SLM root -└── domains - ├── доменный модуль Level 1 - └── доменный пакет Level 2 - ├── metadata - ├── модуль api - │ ├── api - │ ├── api/factory - │ ├── api/ports при наличии ports - │ └── api/runtime при наличии consumers - ├── обязательная Group assemblies - │ ├── модуль default - │ └── дополнительные assembly-модули - ├── Group adapters при наличии ports - │ └── adapter-модуль - └── Framework Group react - ├── модуль session - └── модуль queries -``` diff --git a/skills/slm-design/reference/draft/level-2/validation.md b/skills/slm-design/reference/draft/level-2/validation.md deleted file mode 100644 index 4742023..0000000 --- a/skills/slm-design/reference/draft/level-2/validation.md +++ /dev/null @@ -1,126 +0,0 @@ -# Проверка Level 2 - -> Граница автоматической проверки, architecture review и contract tests Level 2. - -## Конфигурация проекта - -Конфигурация проверки сопоставляет физические пути с: - -- доменными модулями Level 1 и пакетами Level 2; -- metadata, SLM-модулями и Groups; -- фасетами `api`; -- dependency ports и adapter modules; -- assemblies и их baseline/special contexts; -- public entry points; -- executable, type-only, framework reference и deferred edges; -- environment capability sets, resolver conditions и framework execution phases; -- API-safe external packages; -- runtime assembly inputs и создаваемыми API, если проект автоматизирует runtime DAG. - -Формат такой конфигурации пока не выбран. Проверка анализирует объявленные boundaries и resolved graphs, а не угадывает сущность только по имени папки. - -## Автоматическая проверка - -Каждое правило класса `A` реализуется блокирующей проверкой проекта. Автоматическая проверка обнаруживает: - -- одновременное объявление одной предметной области module и package; -- executable file, root `index.ts`, state или reexport в корне package; -- отсутствие `api` или несколько модулей `api`; -- отсутствие `api` либо `api/factory`; -- недопустимый type/runtime export kind фасетов `api`, `api/ports`, `api/factory` и `api/runtime`; -- другой public path или deep import внутри `api`; -- отсутствие Group `assemblies` или модуля `assemblies/default`; -- прямой дочерний элемент `assemblies` или `adapters` без module boundary; -- нарушение importer matrix ports, factories и concrete adapters; -- достижимость adapter, assembly, framework, SDK, storage, state/query runtime или environment-specific code из `api`; -- запрещённый cross-domain import; -- type-only import не из public facet владельца; -- import framework state, hooks, contexts или components другого домена; -- несовместимую executable reachability под каждым configured resolver condition set; -- runtime- или type-only cycles статического module graph. - -Проверка external package reachability использует project allowlist API-safe packages. Решение о том, соответствует ли package критериям API-safe, принимается на review; автоматизация проверяет объявленный label и фактически resolved entries. - -Неанализируемые dynamic imports запрещаются или явно allowlist-ятся project policy с target capability set. - -## Architecture review - -На review определяется: - -- представляет ли package одну связную предметную область; -- является ли `api` единственным семантическим шлюзом домена; -- соответствуют ли exports `api` реальным consumer contracts, `api/ports` implementer contracts, а `api/factory` объявленным Domain API factories; -- отличаются ли public models от raw provider DTO там, где это необходимо; -- принадлежат ли operations ровно одному Domain API; -- оправдано ли разделение нескольких Domain API независимой сборкой, trust или consumers; -- описывают ли ports consumer-owned capabilities, а не endpoints конкретного SDK; -- достаточна ли closed failure algebra для выбора domain outcomes; -- преобразуются ли provider и foreign-domain failures в собственные errors; -- является ли каждая production implementation отдельным adapter module; -- не выполняют ли framework bindings предметные external operations в обход API; -- не создаёт ли framework projection параллельную модель; -- определены ли optimistic ordering, versioning и reconciliation модулем `api`; -- представляет ли `default` один реальный baseline capability context; -- оправданы ли дополнительные assemblies реальным отличием graph; -- остаётся ли runtime assembly graph ацикличным; -- создаётся ли только dependency-connected часть production graph; -- полностью ли определены lifecycle и cleanup failure paths; -- соответствует ли каждый API-safe package ограничениям; -- остаются ли Groups без implementation и aggregate API. - -## Environment review - -Для каждого public entry point рассматриваются реальные executable imports под заявленными conditions. Отдельно проверяются: - -- RSC server execution; -- Client Component references; -- server prerender graph Client Components при включённом SSR; -- browser hydration graph Client Components; -- framework-deferred browser effects; -- Server Action references; -- browser, Node.js, edge и worker capabilities; -- conditional exports external packages; -- dynamic imports; -- serialization boundaries. - -Название `default`, `rsc`, `server` или `client` не является доказательством совместимости. Tree shaking и runtime branching также не являются доказательством. - -## Realtime review - -Для каждого realtime port фиксируются: - -- correlation scope и ACK semantics; -- ordering и duplicate policy; -- disconnect, timeout и `OUTCOME_UNKNOWN`; -- idempotency и retry; -- reconnect, gap detection и resync; -- cancellation; -- shared connection owner; -- cleanup и запрет callbacks после disposal. - -Без этих guarantees adapter нельзя считать проверяемой реализацией port. - -## Testing - -Domain API проверяется через factory с fake ports. Adapter проверяется contract tests concrete provider. Assembly проверяет production wiring, capabilities, partial construction и cleanup. Framework binding проверяет projection, hydration и lifecycle с fake API. - -Import-graph checks не заменяются runtime tests, а API fake не заменяет adapter contract test. - -## Смешанный SLM root - -Наличие доменных модулей Level 1 рядом с пакетами Level 2 является завершённым допустимым состоянием. Проверка применяет правила формы отдельно к каждой предметной области и правила Level 2 ко всем статическим связям, пересекающим package boundary. - -Переход одного домена завершается, когда его старая module boundary удалена и checker видит только package. Другие домены не входят в критерий формы, но dependency-connected consumers и graph owners входят в change radius. - -## Связанные правила - -- [`SLM-L2-DOMAIN-A003`](../rules/level-2.md#slm-l2-domain-a003) -- [`SLM-L2-API-A007`](../rules/level-2.md#slm-l2-api-a007) -- [`SLM-L2-DEPENDENCY-A012`](../rules/level-2.md#slm-l2-dependency-a012) -- [`SLM-L2-ENVIRONMENT-A013`](../rules/level-2.md#slm-l2-environment-a013) -- [`SLM-L2-API-A019`](../rules/level-2.md#slm-l2-api-a019) -- [`SLM-L2-ASSEMBLY-A020`](../rules/level-2.md#slm-l2-assembly-a020) -- [`SLM-L2-API-A022`](../rules/level-2.md#slm-l2-api-a022) -- [`SLM-L2-DOMAIN-A026`](../rules/level-2.md#slm-l2-domain-a026) - -Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения. diff --git a/skills/slm-design/reference/draft/rules/README.md b/skills/slm-design/reference/draft/rules/README.md deleted file mode 100644 index c56d532..0000000 --- a/skills/slm-design/reference/draft/rules/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Правила SLM - -> Статус: системный черновик. Не является нормативной спецификацией. - -Эта директория является единственным местом объявления правил SLM. Остальные черновики объясняют архитектуру и ссылаются на канонические коды, но не повторяют формулировки правил. - -## Что считается правилом - -Правило задаёт один блокирующий архитектурный инвариант. - -Определения, рекомендации, разрешения, примеры и открытые вопросы не получают код правила. - -Нормативные определения объявляются в терминологии соответствующего уровня. Они обязательны для толкования правил, но нормативность определения сама по себе не превращает его в правило. - -## Код правила - -```text -SLM-L{level}-{group}-{class}{number} -``` - -| Часть | Значение | -|---|---| -| `SLM` | Принадлежность архитектуре SLM | -| `L{level}` | Уровень архитектуры | -| `group` | Раздел правил | -| `class` | Способ проверки: `A` или `R` | -| `number` | Трёхзначный номер внутри уровня | - -## Способы проверки - -### `A`: автоматическая проверка - -Всё правило можно однозначно проверить программно без понимания предметного смысла кода. Нарушение такого правила должно блокировать автоматическую проверку. - -### `R`: проверка на ревью - -Для окончательного решения требуется понимание ответственности, владения или смысла зависимости. Линтер может проверять отдельные признаки, но не заменяет решение на ревью. - -Одно правило не разделяется на автоматическую и ручную копии только из-за разных способов проверки. Если существенная часть инварианта требует смыслового решения, всё правило получает класс `R`. - -## Разделы правил - -| Код | Раздел | -|---|---| -| `LAYER` | Слои | -| `DEPENDENCY` | Зависимости | -| `MODULE` | Модули | -| `GROUP` | Группы | -| `SEGMENT` | Сегменты | -| `COMPONENT` | Компоненты | -| `NESTED_MODULE` | Вложенные модули | -| `LIFECYCLE` | Жизненный цикл | -| `DOMAIN` | Домены | -| `API` | Доменный API | -| `FACTORY` | Фабрики Domain API | -| `ERROR` | Ошибки домена | -| `PORT` | Dependency ports | -| `ADAPTER` | Адаптеры | -| `ASSEMBLY` | Сборка API и жизненный цикл | -| `ENVIRONMENT` | Границы сред выполнения | -| `FRAMEWORK` | Модули фреймворков | -| `STATE` | Материализация состояния | -| `REALTIME` | Realtime-взаимодействие | -| `TEST` | Тестирование | - -Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования. - -## Формат записи - -```md -### SLM-L1-MODULE-A004 - -> **Публичный API модуля** -> -> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API. -``` - -Код является заголовком третьего уровня и автоматически получает адрес для ссылки `#slm-l1-module-a004`. - -Название и описание входят в одну цитату. Название выделяется жирным и служит кратким именем правила. Описание полностью формулирует требование и занимает одну физическую строку. - -Ссылка из тематического черновика: - -```md -[`SLM-L1-MODULE-A004`](../rules/level-1.md#slm-l1-module-a004) -``` - -## Как формулировать правила - -1. Правило понятно без чтения тематической главы и опирается только на нормативные термины своего уровня. -2. Правило защищает один архитектурный инвариант. -3. Один инвариант получает один код независимо от числа участников и способов проверки. -4. Название является кратким и устойчивым именем правила. -5. Название обозначает предмет правила, а описание полностью формулирует требование. -6. Описание объясняет допустимую границу и то, что считается нарушением. -7. Описание раскрывает названный инвариант и не вводит второе независимое требование. -8. Описание использует нормативные определения и не пересказывает их без необходимости. -9. Название и описание используют человеческий язык и только необходимые архитектурные термины. -10. Обоснование, подробности, примеры и исключения размещаются в тематическом черновике, а не в описании. -11. Правило не создаётся отдельно с позиции владельца и потребителя, если обе формулировки защищают одну границу. -12. Перед добавлением правила реестр проверяется на дубли и противоречия. -13. Код присваивается после проверки правила на примерах и контрпримерах. - -## Нумерация - -1. Номер уникален внутри уровня независимо от раздела и способа проверки. -2. Номер не обозначает важность или порядок выполнения. -3. Удалённый номер не переиспользуется для другого правила. -4. При изменении способа проверки номер сохраняется, но меняется полный код. - -## Проверка качества - -Перед принятием правила нужно ответить «да»: - -- Понятно, о чём правило? -- Название кратко и однозначно называет правило? -- Понятно, что оно требует? -- Понятно, что является нарушением? -- Нельзя ли объединить его с существующим правилом? -- Не содержит ли оно рекомендацию или разрешение? -- Соответствует ли класс способу окончательной проверки? - -## Проверка документов - -Корневой скрипт `draft-rules.js` читает объявления только из этой директории, проверяет формат и уникальность кодов, валидирует ссылки из остальных черновиков и выводит правила разделами «Автоматические» и «Для ревью». Скрипт проверяет документы, а не архитектуру приложения. - -## Наборы правил - -- [Первый уровень](./level-1.md) -- [Второй уровень](./level-2.md) diff --git a/skills/slm-design/reference/draft/rules/level-1.md b/skills/slm-design/reference/draft/rules/level-1.md deleted file mode 100644 index 29669d7..0000000 --- a/skills/slm-design/reference/draft/rules/level-1.md +++ /dev/null @@ -1,110 +0,0 @@ -# Правила SLM первого уровня -Здесь собраны правила первого уровня. Это единственное место, где они формулируются; тематические черновики объясняют их и ссылаются на коды. - -## Размещение кода по слоям - -### SLM-L1-LAYER-R001 - -> **Назначение слоёв** -> -> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода. - -### SLM-L1-LAYER-A002 - -> **Направление зависимостей** -> -> Внутри одного SLM root код каждого слоя может зависеть только от кода целевых слоёв, разрешённых для него нормативной матрицей слоёв. - -### SLM-L1-LAYER-R003 - -> **Граница слоя `app`** -> -> В `app` размещаются только точки входа фреймворка для запуска, маршрутов, преобразования входных данных и подключения публичных API модулей разрешённых слоёв или ресурсов `shared`; ответственности этих модулей остаются за пределами `app`. - -## Границы модулей - -### SLM-L1-MODULE-A004 - -> **Публичный API модуля** -> -> Каждый модуль предоставляет единый публичный API; код за пределами модуля импортирует его содержимое только через этот API. - -### SLM-L1-MODULE-A014 - -> **Папка модуля** -> -> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки. - -### SLM-L1-MODULE-R006 - -> **Ответственность модуля** -> -> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях. - -### SLM-L1-MODULE-R011 - -> **Владелец ответственности** -> -> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`. - -### SLM-L1-MODULE-R012 - -> **Состав публичного API** -> -> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми. - -## Зависимости между модулями - -### SLM-L1-DEPENDENCY-A005 - -> **Циклические зависимости** -> -> Граф зависимостей модулей внутри одного SLM root, включая вложенные модули, не содержит циклов. - -## Назначение групп - -### SLM-L1-GROUP-R007 - -> **Назначение группы** -> -> Группа содержит только модули и другие группы, не владеет файлами реализации, состоянием, жизненным циклом или публичным API и не импортируется внешним кодом. - -## Назначение сегментов - -### SLM-L1-SEGMENT-R008 - -> **Граница сегмента** -> -> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или узла графа зависимостей. - -## Ответственность компонентов - -### SLM-L1-COMPONENT-R009 - -> **Ответственность компонента** -> -> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу. - -## Границы вложенных модулей - -### SLM-L1-NESTED_MODULE-A010 - -> **Доступ к вложенному модулю** -> -> Код за пределами родительского модуля не импортирует вложенный модуль напрямую и получает его экспорты только через публичный API родителя. - -## Жизненный цикл - -### SLM-L1-LIFECYCLE-R013 - -> **Жизненный цикл ресурсов** -> -> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни. - -## Граница доменных модулей - -### SLM-L1-DOMAIN-R015 - -> **Доменный модуль** -> -> Каждая самостоятельная предметная область слоя `domains` представлена ровно одним доменным модулем; принадлежащие ей модели, правила, сценарии и продуктовое состояние размещаются внутри его границы, включая границы вложенных модулей. diff --git a/skills/slm-design/reference/draft/rules/level-2.md b/skills/slm-design/reference/draft/rules/level-2.md deleted file mode 100644 index 4bf3c50..0000000 --- a/skills/slm-design/reference/draft/rules/level-2.md +++ /dev/null @@ -1,209 +0,0 @@ -# Правила SLM второго уровня - -Доменный пакет Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. Для предметной области в пакетной форме [`SLM-L1-DOMAIN-R015`](./level-1.md#slm-l1-domain-r015) заменяется правилами доменного пакета; остальные предметные области того же SLM root могут сохранять форму доменного модуля Level 1. [`SLM-L1-GROUP-R007`](./level-1.md#slm-l1-group-r007) сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Для модуля `api` правило [`SLM-L1-MODULE-A004`](./level-1.md#slm-l1-module-a004) уточняется `SLM-L2-API-A019`: объявленные фасеты вместе образуют один логический публичный API и не считаются deep imports. Ранее использовавшиеся номера `SLM-L2-DOMAIN-R001` и `SLM-L2-MIGRATION-A017` не переиспользуются. - -## Граница доменного пакета - -### SLM-L2-DOMAIN-R002 - -> **Предметная граница пакета** -> -> Каждая предметная область, использующая пакетную форму Level 2, представлена ровно одним доменным пакетом; все его модули относятся только к этой предметной области. - -### SLM-L2-DOMAIN-A003 - -> **Корень доменного пакета** -> -> Корень доменного пакета содержит только декларативную metadata, модуль `api` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов. - -### SLM-L2-GROUP-R004 - -> **Навигационная Group доменов** -> -> Group, расположенная непосредственно в слое `domains` или другой такой Group, содержит только доменные модули Level 1, доменные пакеты Level 2 и навигационные Groups и не владеет реализацией, состоянием, жизненным циклом или публичным API. - -## Доменный API - -### SLM-L2-API-R005 - -> **Модуль api** -> -> Каждый доменный пакет содержит ровно один модуль `api`, который объявляет один или несколько именованных Domain API, их публичные модели, результаты, ошибки, dependency ports и фабрики. - -### SLM-L2-API-R006 - -> **Семантическая власть Domain API** -> -> Доступные приложению доменные данные, модели, validation, семантика команд и запросов, результаты и ожидаемые ошибки производятся или проверяются модулем `api`; adapters, assemblies и framework bindings не определяют параллельную предметную модель или переход. - -### SLM-L2-API-A007 - -> **Импортная замкнутость api** -> -> Все runtime- и type-only импорты модуля `api`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как API-safe. - -### SLM-L2-FACTORY-R008 - -> **Фабрики Domain API** -> -> Каждому публичному Domain API соответствует ровно одна именованная фабрика фасета `api/factory`; фабрика получает явные ports и cross-domain API, создаёт только этот Domain API, не выбирает adapter или assembly и не запускает скрытые ресурсы жизненного цикла. - -## Ошибки домена - -### SLM-L2-ERROR-R009 - -> **Публичный контракт ошибок** -> -> Каждый ожидаемый сбой публичной операции Domain API представлен именованным readonly сериализуемым типом собственной доменной ошибки с устойчивым кодом; тип экспортируется через `api`, а необходимые внешним потребителям runtime-коды и guards только через `api/runtime`. - -### SLM-L2-ERROR-R010 - -> **Изоляция исходных ошибок** -> -> Сбой provider, adapter, SDK, транспорта, storage или другого домена, доступный приложению через Domain API, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause. - -## Assemblies и зависимости - -### SLM-L2-ASSEMBLY-R011 - -> **Роль assembly** -> -> Каждая assembly является SLM-модулем одного объявленного production-контекста, выбирает adapter-модули, вызывает одну или несколько фабрик своего `api` и возвращает явный именованный граф готовых Domain API, не добавляя предметные операции, модели или ошибки. - -### SLM-L2-DEPENDENCY-A012 - -> **Междоменные импорты Level 2** -> -> Статическая связь между разными доменными границами, хотя бы одна из которых является пакетом Level 2, допускает только type-only импорт публичного API доменного модуля Level 1 или фасета `api` пакета Level 2 либо runtime-импорт `api/runtime` пакета Level 2; остальные публичные и внутренние пути другого домена не импортируются. - -### SLM-L2-ENVIRONMENT-A013 - -> **Совместимость среды выполнения** -> -> Для каждой объявленной точки входа, поддерживаемого набора resolver conditions и framework execution phase её достижимый executable import-граф не содержит несовместимых runtime capabilities; type-only связи, framework reference и deferred edges проверяются отдельно и не считаются обычным выполнением. - -## Framework Groups и тестирование - -### SLM-L2-FRAMEWORK-R014 - -> **Framework Group домена** -> -> Framework binding modules доменного пакета размещаются в Group, названной по фреймворку, и каждый прямой дочерний элемент этой Group является framework binding module. - -### SLM-L2-FRAMEWORK-R015 - -> **Framework binding module** -> -> Модуль внутри Framework Group владеет одной domain-specific framework-ответственностью, получает готовые Domain API, материализует их значения средствами фреймворка и не вызывает фабрики, не выбирает adapters, не обращается к предметному внешнему источнику в обход Domain API и не владеет страницей, маршрутом или multi-domain композицией. - -### SLM-L2-TEST-R016 - -> **Проверка владельцев Level 2** -> -> Каждый публичный сценарий проверяется через фабрику владеющего им Domain API, каждый adapter — по контракту реализуемого port, а основные тесты assembly и framework binding module проверяют только собственные публичные границы и не повторяют полный набор сценариев Domain API. - -## Совместное применение форм - -### SLM-L2-DOMAIN-A026 - -> **Однозначная форма домена** -> -> Каждая предметная область объявлена ровно в одной форме: как доменный модуль Level 1 либо как доменный пакет Level 2; один SLM root может одновременно содержать разные предметные области обеих форм. - -## Внешние библиотеки api - -### SLM-L2-API-R018 - -> **API-safe внешний пакет** -> -> Внешний пакет объявляется API-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state/query manager, framework или другой технической интеграцией. - -## Публичные фасеты api - -### SLM-L2-API-A019 - -> **Публичные фасеты api** -> -> Публичный API модуля `api` имеет обязательные entry points `api` только с type exports и `api/factory` только с runtime exports, может иметь `api/ports` только при наличии объявленного dependency port и только с type exports и `api/runtime` только с runtime exports и не имеет других публичных путей или deep imports. - -## Обязательная штатная сборка - -### SLM-L2-ASSEMBLY-A020 - -> **Обязательная assembly default** -> -> Корень каждого доменного пакета содержит ровно одну непустую Group `assemblies` с ровно одним прямым модулем `default`; каждый другой прямой дочерний элемент Group также является объявленной границей assembly-модуля. - -### SLM-L2-ADAPTER-R021 - -> **Модули production adapters** -> -> Если хотя бы одна фабрика имеет dependency port, корень доменного пакета содержит непустую Group `adapters`, а каждая связная production-реализация одного или нескольких ports принадлежит ровно одному adapter-модулю этой Group и не определяется в другом месте production-графа. - -### SLM-L2-API-A022 - -> **Потребители фасетов и сборочных модулей** -> -> Фасет `api` импортируется извне только через `import type`, `api/ports` импортируют только adapters своего домена, assemblies и тесты, `api/factory` и concrete adapters в production импортируют только assemblies своего домена, а `api/runtime` не импортируют adapters и используют только через объявленный публичный путь с соблюдением правил слоёв и междоменных зависимостей. - -## Жизненный цикл assembly - -### SLM-L2-ASSEMBLY-R023 - -> **Транзакционный lifecycle assembly** -> -> Assembly не запускает скрытую долгоживущую работу; cleanup каждого созданного ею ресурса и каждого полученного adapter lifecycle handle немедленно регистрируется, при частичной ошибке выполняется в обратном порядке, а успешный результат с cleanup obligations предоставляет идемпотентный aggregate cleanup, после завершения которого resources не вызывают callbacks. - -## Недетерминизм api - -### SLM-L2-API-R024 - -> **Явные источники недетерминизма** -> -> Операция Domain API получает текущее время, timer, random, ID generator, environment и другие источники недетерминизма только через явные ports фабрики и не читает их из скрытого runtime-окружения. - -## Публичный runtime api - -### SLM-L2-API-R025 - -> **Детерминированный runtime api** -> -> Фасет `api/runtime` экспортирует только необходимые реальным внешним потребителям детерминированные значения и функции без ввода-вывода, изменяемого состояния, runtime capability или environment-specific поведения. - -## Dependency ports - -### SLM-L2-PORT-R027 - -> **Consumer-owned port** -> -> Каждый dependency port принадлежит модулю `api`, описывает минимальную необходимую ему capability и закрытый набор ожидаемых port failures без concrete provider, SDK, framework или transport types; adapter реализует этот контракт, но не определяет его семантику. - -## Материализация состояния - -### SLM-L2-STATE-R028 - -> **Framework-owned materialization** -> -> Framework binding или composition может владеть framework metadata и собственным UI-state, но материализует доменный payload только из values, outcomes и events, произведённых или проверенных Domain API, и применяет предметный optimistic merge, reconciliation или transition только через операцию либо детерминированный runtime модуля `api`. - -## Realtime - -### SLM-L2-REALTIME-R029 - -> **Проверяемый realtime-контракт** -> -> Каждый realtime port явно определяет correlation, момент подтверждения команды, ordering, duplicate, reconnect, resync, cancellation и cleanup semantics; adapter скрывает transport protocol, а Domain API публикует только проверенные события, outcomes и собственные стабильные ошибки. - -## Runtime-граф assemblies - -### SLM-L2-ASSEMBLY-R030 - -> **Ацикличная runtime-сборка** -> -> Runtime-граф публичных API доменных модулей Level 1 и Domain API пакетов Level 2, включая assembly inputs, factory dependencies и передаваемые callbacks, не содержит циклов, а graph owner создаёт независимые API раньше зависимых и очищает их в обратном порядке. - -### SLM-L2-ASSEMBLY-R031 - -> **Контекст default assembly** -> -> `assemblies/default` представляет один объявленный штатный production-набор API, dependencies, runtime capabilities и lifecycle; имя `default` само по себе не означает browser-, server- или isomorphic-совместимость, а отличающийся контекст получает отдельную именованную assembly. diff --git a/src-skills/slm-design/SKILL.md b/src-skills/slm-design/SKILL.md index 8448204..6ee79ea 100644 --- a/src-skills/slm-design/SKILL.md +++ b/src-skills/slm-design/SKILL.md @@ -1,666 +1,246 @@ --- name: slm-design -description: "Используй при проектировании, реализации, миграции и архитектурном ревью по SLM: когда нужно определить SLM root, ответственность, владельца, слой, модульную границу, публичный API, форму домена Level 1 или Level 2, направление зависимостей, Domain API, ports, factories, adapters, default assembly, framework state/cache, realtime, errors или lifecycle. Не используй для локального coding, debugging, форматирования, framework- или SDK-механики, если архитектурная граница уже определена и не меняется." +description: "Экспертная работа с архитектурой SLM Design: проектирование, изменение, миграция и ревью слоёв app/compositions/domains/infra/ui/shared, модулей, доменов, публичных фасетов index/client/browser/server, групп, сегментов, вложенных модулей, зависимостей, состояния и lifecycle. Триггеры: SLM, Scoped Layered Module Design, SLM root, ответственность, владелец, модульная граница, domains vs compositions, доменный контракт, DTO, глубокий импорт, модульный цикл, архитектурное ревью. НЕ применять для обычного code style или локальной правки, не затрагивающей архитектурное решение." --- # SLM Design -## Рабочий контракт +Работай как архитектор SLM, а не как генератор заранее заданного дерева каталогов. Сначала устанавливай ответственность и владельца, затем выражай решение слоями, публичными границами и зависимостями. Пути, имена, framework-роли и размер кода не заменяют смысловое решение. -Применяй SLM как способ выполнить пользовательскую задачу, а не как тему для пересказа. После чтения этого файла ты должен уметь принять типовое архитектурное решение, реализовать его в запрошенном scope и проверить результат. Открывай references только для точной формулировки правила, редкого случая или неразрешённого вопроса. +## Источники истины -Работай в таком порядке: +Весь нормативный и поясняющий материал находится в `reference/docs`. Не воспроизводи правила по памяти, если от точности формулировки зависит решение. -1. Исследуй существующий код и локальные правила проекта. -2. Определи ответственность, владельца и минимальный scope. -3. Выбери слой, архитектурную сущность и форму домена. -4. Спроектируй публичную границу, зависимости, runtime-сборку и lifecycle. -5. До редактирования проверь решение по применимым правилам. -6. Если пользователь запросил реализацию, внеси изменения до завершённого состояния. -7. Проверь импорты, exports, граф, среды, lifecycle и тесты. -8. Кратко сообщи решение, сделанные изменения, проверки, assumptions и остаточные риски. +Материалы выполняют разные нормативные роли: -Не начинай широкое перемещение кода или генерацию каркаса до шагов 1-5. Не расширяй задачу до полного аудита SLM root, если локальное изменение можно корректно выполнить в меньшем scope. +1. [`rules/registry.md`](./reference/docs/rules/registry.md) содержит единственные точные блокирующие правила. +2. [`reference/terminology.md`](./reference/docs/reference/terminology.md) задаёт нормативный смысл терминов. +3. [`architecture/layers.md`](./reference/docs/architecture/layers.md) и [`architecture/dependencies.md`](./reference/docs/architecture/dependencies.md) задают роли слоёв и матрицу направлений. +4. Остальные главы `architecture` объясняют модель и способы проектирования. +5. [`reference/validation.md`](./reference/docs/reference/validation.md) задаёт процедуру проверки и критерий завершения. +6. [`README.md`](./reference/docs/README.md) даёт обзор, мотивацию и навигацию. -## Источники и обязательность +Не превращай рекомендацию или пример в правило. При обязательном вердикте указывай существующий код из реестра. Если требование относится к локальному стайлгайду, lint-конфигурации, framework или продуктовой policy, называй его проектным ограничением, а не правилом SLM. Если реестр, определение или нормативная матрица действительно противоречат друг другу, не выбирай победителя молча: останови обязательный вывод и зафиксируй противоречие документации. -Bundled DRAFT является рабочим источником истины для этой версии skill, но остаётся черновиком архитектуры. Используй источники в следующем порядке: +## Рабочий режим -1. [`rules/level-1.md`](./reference/draft/rules/level-1.md) и [`rules/level-2.md`](./reference/draft/rules/level-2.md) - единственный источник блокирующих правил. -2. [`level-1/terminology.md`](./reference/draft/level-1/terminology.md) и [`level-2/terminology.md`](./reference/draft/level-2/terminology.md) - обязательный смысл терминов. -3. README уровней - область применения, наследование и замены правил. -4. Тематические главы - объяснения, рекомендации и варианты проектирования. -5. Примеры - иллюстрации, а не обязательный каркас. -6. `open-questions.md` - нерешённые вопросы, а не требования. +1. Определи тип задачи: проектирование, реализация, изменение существующей границы, миграция, ревью или объяснение. +2. Исследуй фактический код, импорты и локальные архитектурные соглашения. Не делай вывод о сущности только по имени каталога. +3. Открой базовую модель и только относящиеся к задаче references по [карте файлов](#карта-файлов). +4. Зафиксируй наблюдаемые факты отдельно от архитектурных выводов. +5. Для проектирования, структурного изменения или миграции составь карточку решения по [`reference/validation.md`](./reference/docs/reference/validation.md#карточка-решения). +6. Для ревью используй review-checklists и реестр; для объяснения открывай только тематические references и не требуй карточку решения. +7. Если задача предполагает изменение кода, спроектируй минимальное решение, которое оставляет одного владельца, закрытый внутренний код и ацикличный модульный граф. +8. Редактируй код только для задачи реализации, изменения или миграции; ревью, проектирование и объяснение заверши соответствующим отчётом без самовольных правок. +9. Выполни применимые проверки: для ревью - доказательства findings, для реализации - смысловую, структурную и функциональную валидацию. +10. В результате сообщи принятое решение, затронутые границы, применимые правила, выполненные проверки и оставшиеся риски. -Если тематическая глава строже реестра, не создавай из неё новое блокирующее правило. Предложи более строгую форму как рекомендацию или уточни локальную policy, если выбор влияет на API, ownership, стоимость или runtime. Если этот файл расходится с реестром или нормативной терминологией, следуй bundled DRAFT и отметь дефект skill. +Если задача локальна и не меняет ответственность, публичный API, зависимость, состояние, lifecycle или физическую модульную границу, не инициируй архитектурный рефакторинг без отдельной причины. -При review различай: +## Сбор контекста -- **Rule violation** - нарушено применимое правило с существующим кодом SLM. -- **Definition mismatch** - реализация не соответствует нормативному смыслу сущности. -- **Architectural risk** - есть доказуемый риск, но нет блокирующего правила. -- **Decision required** - DRAFT или проект оставляет значимый выбор открытым. -- **Recommendation** - улучшение, которое не является обязательным. -- **Assumption** - обратимое рабочее допущение, явно указанное в результате. +До проектирования установи: -Не придумывай коды правил. Перед ссылкой на нарушение открой соответствующий реестр и проверь точную формулировку. +- границу SLM root и локальное сопоставление путей со слоями, группами, модулями, сегментами и фасетами; +- для каждого изменяемого файла - модульного владельца либо подтверждённый статус точки входа `app`, немодульного ресурса `shared` или кода вне SLM root; +- существующие публичные фасеты и реальные внешние импорты модуля; +- потребителей изменяемого поведения и среды, в которых они выполняются; +- межмодульные связи, включая `import type` и реэкспорты; +- владельца изменяемого состояния, источник истины и область жизни ресурсов; +- для продуктовых данных - доменный контракт, контракт источника и место адаптации; +- локальные lint-правила, alias-настройки, test/build-команды и дополнительные project policies. -## Минимальная рабочая модель +Проверяй историю или соседние модули только как свидетельство принятой локальной policy. Существующий код может быть legacy и не является доказательством нормы SLM. -### SLM root и уровни +Если проект не объявляет физическое сопоставление SLM-сущностей, выведи рабочую гипотезу из структуры и конфигурации и явно обозначь её. Гипотеза подходит для проектирования и адресных вопросов, но не доказывает нарушение класса `A`. До блокирующего структурного finding подтверди mapping конфигурацией проекта или однозначно установленными модульными границами. -SLM root - граница структурной архитектуры одного приложения. Сначала найди фактический root, path aliases, локальный стайлгайд и конфигурацию архитектурной проверки. Не считай `src` root автоматически и не выводи сущность только из имени папки. +## Проектирование -Level 1 действует во всём SLM root и задаёт слои, модули, публичные API, общий dependency DAG и владение lifecycle. +Двигайся от смысла к структуре: -Level 2 применяется отдельно к выбранной предметной области и заменяет только её доменный модуль пакетной формой. Остальные домены могут постоянно оставаться на Level 1. Одна предметная область имеет ровно одну итоговую форму. +1. Сформулируй один изменяемый результат или поведение без названий файлов, папок, библиотек и паттернов. +2. Определи, является ли поведение доменным сценарием. +3. Найди существующего владельца или обоснуй новую самостоятельную ответственность. +4. Зафиксируй, что владелец делает сам и какие готовые возможности получает от других модулей. +5. Назови реальных внешних потребителей. +6. Выбери слой по роли ответственности. +7. Спроектируй минимальный публичный API и только необходимые фасеты сред выполнения. +8. Построй impact map межмодульных рёбер и проверь публичные пути, матрицу слоёв и ацикличность. +9. Назначь владельца состоянию и каждому lifecycle-ресурсу. +10. Только после этого выбери папку модуля, главный файл, сегменты, группы или вложенные модули. -### Слои - -| Исходный слой | Может зависеть от | -|---|---| -| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` | -| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` | -| `domains` | `domains`, `infra`, `ui`, `shared` | -| `infra` | `infra`, `shared` | -| `ui` | `ui`, `shared` | -| `shared` | `shared` | - -Матрица не требует проходить через каждый промежуточный слой. Разрешённый импорт не переносит владение ответственностью. - -| Слой | Помещай сюда | -|---|---| -| `app` | Framework entry points: запуск, routes, преобразование внешнего input и подключение готовых API | -| `compositions` | Pages, layouts, screens, widgets, route outcomes и multi-domain UI | -| `domains` | Предметные модели, правила, сценарии и продуктовое состояние | -| `infra` | Универсальные технические capabilities без собственной предметной модели | -| `ui` | Универсальные UI-модули без зависимости от продуктовой композиции | -| `shared` | Детерминированный product-agnostic фундамент без I/O, mutable state и lifecycle | - -### Архитектурные сущности - -| Признак | Сущность | -|---|---| -| Самостоятельная ответственность со своим API, dependencies, state или lifecycle | Module | -| Только навигационно классифицирует modules и Groups, а в `domains` также domain packages | Group | -| Организует внутренности одного module | Segment | -| Framework UI entity, реализующая часть ответственности родителя | Component | -| Самостоятельный module, скрытый внутри parent module | Nested module | -| Framework bootstrap или route entry | Немодульная единица `app` | -| Малый deterministic product-agnostic файл без внутренней границы | Shared resource | - -Module является узлом dependency graph, размещается в отдельной папке и имеет единый логический публичный API. Group, segment и component не владеют API, состоянием или lifecycle. Наличие локального `index.ts`, нескольких файлов, hook, data access или lifecycle-кода само по себе не превращает component или segment в module: всё это принадлежит ближайшему module-owner. - -Nested module имеет собственную ответственность, API и узел графа, но внешний код получает его exports только через публичный API parent module. - -Navigation Group непосредственно в `domains` может содержать доменные модули Level 1, доменные пакеты Level 2 и другие navigation Groups. Пакет при этом не становится модулем или Group. - -### Пакетная форма Level 2 - -Минимальная структура доменного пакета: +Для каждого спорного вывода используй цепочку: ```text -domains// -├── metadata # optional, declarative only -├── api/ # required SLM module -├── assemblies/ # required non-empty Group -│ └── default/ # required baseline production assembly -├── adapters/ # when factories have dependency ports -└── react|vue|... # when domain-specific bindings exist +Факт в коде -> ближайший владелец -> архитектурный смысл -> решение -> reference или код правила ``` -Доменный пакет является policy boundary, но не module, Group, public API или graph node. В его корне нет executable files, state, lifecycle, barrel или реэкспортов. Исполняемыми владельцами являются модули внутри пакета. +### Выбор структурной сущности -`api` является единственным семантическим шлюзом пакета. Его публичный API состоит из фасетов: +Открой [`architecture/modules.md`](./reference/docs/architecture/modules.md), [`architecture/segments.md`](./reference/docs/architecture/segments.md) и при необходимости [`architecture/groups.md`](./reference/docs/architecture/groups.md). По таблицам и критериям этих глав последовательно установи: -| Путь | Содержимое | -|---|---| -| `api` | Только consumer-facing public types: Domain API, models, outcomes, errors | -| `api/ports` | Implementer-facing types при наличии dependency ports | -| `api/factory` | Только именованные runtime factories, по одной на Domain API | -| `api/runtime` | Только реально нужные внешним consumers детерминированные runtime values/functions | +1. Продолжает ли код существующий результат или вводит отдельно формулируемую ответственность. +2. Достаточны ли колокация или сегмент, либо нужен новый владелец. +3. Является ли новый владелец внутренней подответственностью родителя или общим модулем для внешних потребителей. +4. Нужна ли только навигационная группа без реализации и API. -Другой публичный путь внутрь `api` является deep import. `api/ports` и `api/runtime` не создавай без реальной границы или consumer. - -Роли Level 2: - -- `api` определяет Domain API, public models, validation, outcomes, dependency ports и expected domain errors, но не framework state/cache. -- Adapter module реализует связанные dependency ports поверх SDK, storage, platform API, transport или другого provider runtime. -- `assemblies/default` выбирает штатные adapters, вызывает factories и возвращает baseline graph готовых API; дополнительные assemblies представляют отличающиеся production contexts. -- Framework binding module получает готовые Domain API и владеет domain-specific state, cache, hydration и framework integration. -- Composition, `app`, request handler или test setup вызывает assemblies в ацикличном порядке и владеет общим scope graph. - -## Универсальный цикл решения - -### 1. Discover - -Перед решением найди только релевантный контекст: - -- локальные инструкции и стайлгайд; -- SLM root и mapping путей на слои и модули; -- существующие public entry points и package exports; -- внешних consumers затрагиваемой границы; -- runtime- и type-only imports, реэкспорты и aliases; -- state, I/O, SDK, framework runtime и источники недетерминизма; -- места создания graph и instances; -- subscriptions, timers, requests, connections и cleanup; -- тесты и команды проверки затрагиваемых owners. -- architecture mapping, assembly contexts, environment declarations и API-safe allowlists, если проект их использует. - -Считай type-only import и reexport архитектурным ребром. Для runtime-графа дополнительно ищи arguments factories, callbacks, registries, event buses, service locators и singletons: фактическая зависимость может не иметь прямого runtime import. - -### 2. Classify - -Сформулируй краткую внутреннюю карточку: - -```text -Task outcome: -Responsibility: -Owner: -Layer: -Entity: -Domain form: -Public consumers: -Runtime dependencies: -Environment: -State and lifecycle: -Change scope: -``` - -Не обязан показывать карточку пользователю, если решение однозначно. Если одно из ключевых полей неизвестно и влияет на границу, сначала исследуй код, затем задай один конкретный вопрос. - -### 3. Design boundary - -Определи: - -- один owner каждой самостоятельной ответственности; -- минимальный публичный контракт для реальных consumers; -- разрешённые static edges; -- runtime injection и место сборки graph; -- владельцев domain state, technical cache и framework projection; -- безопасную форму expected errors; -- environment entry points и их transitive reachability; -- scope, multiplicity и cleanup каждого lifecycle resource; -- тестовую границу каждого изменяемого owner. - -### 4. Validate before edits - -До изменения файлов ответь: - -- Соответствует ли ответственность роли слоя? -- Является ли выбранная сущность настоящим owner, а не удобной папкой? -- Есть ли у domain одна форма? -- Импортируется ли каждый чужой module через public API? -- Разрешены ли layer и cross-domain edges? -- Остаётся ли graph ацикличным? -- Совместим ли transitive graph с environment entry point? -- Есть ли owner, scope, multiplicity и cleanup у ресурсов? -- Не расширяет ли решение scope на dependency-connected owners, и согласовано ли это расширение? - -### 5. Act and verify - -Если пользователь просит код, не останавливайся на рекомендации. Реализуй согласованную границу, обнови consumers и tests, удали obsolete paths и проверь завершённое состояние. Если пользователь просит только анализ, план или review, не редактируй код. - -## Алгоритмы выбора - -### Ответственность и владелец - -1. Опиши ответственность одним предложением без имени папки, файла или библиотеки. -2. Назови одну причину её изменения. -3. Найди данные, behavior и state, которые изменяются вместе с ней. -4. Найди внешних consumers. -5. Проверь, нужны ли ей собственные API, dependencies, state или lifecycle. -6. Если самостоятельность доказана, назначь ровно один module-owner. -7. Если ответственность нельзя сформулировать или у неё конкурирующие owners, остановись до структурных изменений. - -Место выполнения не переносит владение. Provider, hook, controller, route и component могут запускать чужую ответственность, не становясь её owner. +Зафиксируй решение о владельце до выбора пути. Количество файлов, props, Context, Provider, store, hook или lifecycle-код сами по себе не выбирают структурную сущность. ### Выбор слоя -```text -Только framework bootstrap, route entry или external input adaptation? - -> app +Открой таблицу ролей и границу доменов/композиций в [`architecture/layers.md`](./reference/docs/architecture/layers.md). Сопоставь одно предложение об ответственности с нормативной ролью слоя. Отдельно проверь немодульные исключения `app` и `shared`, прямой доступ композиции к HTTP, SDK или storage и координацию нескольких доменов. Технический механизм и разрешённое направление импорта не доказывают правильность владельца. -Page/layout/screen/widget, route outcome или multi-domain UI? - -> compositions +### Проектирование домена -Domain model, scenario, validation, transition или product state? - -> domains +Для любого создаваемого или изменяемого доменного сценария открой [`architecture/domains.md`](./reference/docs/architecture/domains.md) до проектирования интеграции. -Production implementation technical dependency конкретного Level 2 domain? - -> adapter module внутри package этого domain +Следуй порядку из раздела [«Порядок создания домена»](./reference/docs/architecture/domains.md#порядок-создания-домена). Результатом проектирования должны стать четыре явных артефакта: предметный контракт, карта ожидаемых исходов и defects, план адаптации source boundary и список реально нужных публичных runtime-capabilities. -Самостоятельный универсальный technical service без domain model? - -> infra +Не начинай контракт с endpoint, SDK, DTO или формы ответа. Публично экспортируй guard, parser, schema, constructor или другой механизм runtime-идентификации доменной ошибки только для доказанного потребителя и подходящей среды. Внутреннюю валидацию недоверенных данных и адаптацию источника оценивай отдельно: им не нужен внешний потребитель. -Product-independent reusable UI? - -> ui +### Проектирование API и фасетов -Deterministic, product-agnostic, без I/O/state/lifecycle? - -> shared +Открой разделы о публичном API и фасетах в [`architecture/modules.md`](./reference/docs/architecture/modules.md#публичный-api), затем проверь executable-граф по [`reference/validation.md`](./reference/docs/reference/validation.md#проверка-фасетов). -Иначе -> уточни ответственность, не выбирай папку по аналогии. -``` +Составь consumer/environment map: какая capability нужна какому внешнему потребителю и в какой среде. По ней выбери минимально подходящие фасеты, затем проверь весь транзитивный executable-граф. Не открывай внутренние механизмы про запас и отдельно проверь browser-only и server-only пути. -Domain-specific framework integration над готовым API может принадлежать Framework Group пакета Level 2. Зависимость от React/Vue сама по себе не переносит domain behavior в `compositions` или `app`. +### Проектирование зависимостей -### Выбор сущности +Для каждого нового или изменённого импорта открой [`architecture/dependencies.md`](./reference/docs/architecture/dependencies.md). + +1. Классифицируй обе стороны как модуль, точку входа `app`, немодульный ресурс `shared` или код вне текущего SLM root. +2. Если оба файла принадлежат одному модулю, считай связь внутренней реализацией. +3. Если оба файла принадлежат разным модулям, проверь фасет, направление слоёв и добавь ребро в свёрнутый граф; вложенный модуль является отдельным узлом. +4. Для точки входа `app` или немодульного ресурса `shared` сначала повторно проверь право исходной единицы оставаться немодульной после изменения. Если критерии исключения сохранены, применяй относящиеся к ней правила слоя и публичной границы цели; иначе спроектируй модульного владельца. +5. Внешний package или код за пределами SLM root не становится узлом внутреннего модульного графа. Проверь его влияние на ответственность и среду исходной архитектурной единицы, которой может быть модуль, точка входа `app` или ресурс `shared`, а также на project policy. +6. Проверь весь свёрнутый граф на цикл, а не только пути между конкретными файлами. +7. Отдельно проверь смысл связи: формально разрешённый импорт не должен скрывать неверное владение. + +## Реализация изменений + +После принятия решения: + +- изменяй самую узкую достаточную область и сохраняй принятые соглашения проекта; +- создавай модульную папку только для уже обоснованного владельца; +- добавляй обязательную публичную точку входа и специализированные фасеты только по фактической потребности; +- оставляй детали реализации закрытыми и размещай их по правилам корня, сегментов и компонентных единиц; +- не создавай группы, сегменты, вложенные модули, guards, factories или runtime schemas про запас; +- перенос доменного поведения выполняй вместе с его контрактом, состоянием, UI и интерпретацией ошибок, не оставляя второго владельца; +- при изменении источника сохраняй доменный контракт, пока продуктовый смысл не требует отдельного изменения; +- переключай потребителей на публичный API согласованно с переносом, затем удаляй ставшие недоступными глубокие пути; +- обновляй тесты на контракт и поведение владельца, а интеграционную адаптацию проверяй отдельно от доменных сценариев; +- не исправляй структурный симптом новым barrel или реэкспортом, если проблема находится в ответственности или положении владельца. + +Если реализация обнаружила новый продуктовый смысл, внешнего потребителя или lifecycle, которого не было в карточке решения, останови механическое редактирование и пересмотри архитектурное решение. + +## Архитектурное ревью + +Перед вердиктом открой [`rules/registry.md`](./reference/docs/rules/registry.md), [`reference/validation.md`](./reference/docs/reference/validation.md) и тематическую главу. Проверяй отдельно: + +- смысл: ответственность, единственного владельца, слой, доменный контракт, состояние и lifecycle; +- структуру: модульные корни, фасеты, глубокие импорты, вложенные модули, внутреннюю глубину и свёрнутый граф; +- поведение изменения: не появился ли новый публичный контракт, источник истины или скрытая междоменная координация. + +Оформляй подтверждённое замечание так: ```text -Есть самостоятельный owner/API/dependencies/state/lifecycle? - Да -> module. - Нет -> часть текущего owner. - -Самостоятельный module должен оставаться внутренней границей parent, -а внешний код получать его exports только через parent API? - Да -> nested module. - -Папка в `domains` только классифицирует domain modules, -domain packages и navigation Groups? - Да -> navigation Group слоя `domains`. - -Другая папка только классифицирует modules/Groups? - Да -> Group. - -Папка только организует содержимое одного module? - Да -> segment. - -Framework UI entity не имеет самостоятельной ответственности? - Да -> component parent module. +[Серьёзность] SLM-<код> () +Доказательства: path:line, другие рёбра или отсутствующий обязательный артефакт. +Факт: что наблюдается в коде. +Нарушение: почему факт противоречит точной формулировке правила. +Исправление: какая ответственность, граница или связь должна измениться. ``` -Не создавай module только из-за размера, повторного использования внутреннего helper или желания получить отдельную папку. Не оставляй самостоятельную ответственность component-ом или segment-ом только ради меньшего diff. +Правила ревью: -### Выбор формы домена +- findings идут первыми и сортируются по риску; +- один finding описывает один нарушенный инвариант; +- код правила берётся только из реестра, без выдуманных номеров; +- серьёзность следует принятой шкале проекта; если её нет, используй `high`, `medium`, `low` только как оценку влияния, а не как часть SLM; +- `A`/`R` обозначает способ окончательной проверки, а не серьёзность; +- класс `A` подтверждается структурным фактом при доказанном path mapping, класс `R` требует смыслового обоснования; +- для цикла покажи замкнутую последовательность модулей и location каждого ребра; для отсутствующего фасета или файла назови ожидаемый путь и доказательство модульной границы; +- сигнал вроде `fetch`, Provider, большого файла или локального `index.ts` не является нарушением без проверки владельца; +- рекомендация и project policy маркируются отдельно и не выдаются за блокирующее правило; +- если продуктового контекста недостаточно, формулируй адресный вопрос или риск, а не категоричный finding; +- при отсутствии findings сообщи это явно и перечисли только непроверенные области или ограничения проверки. -По умолчанию используй доменный модуль Level 1. Level 1 не требует factory, ports, adapters, assemblies или разделения по техническим ролям. +Не ограничивай ревью изменёнными строками, если новая связь меняет публичный API, транзитивную среду фасета или модульный цикл. -Рассматривай Level 2, когда конкретному домену действительно нужны: +## Миграция -- несколько независимо собираемых Domain API; -- собственные public models и stable errors поверх provider contracts; -- baseline `assemblies/default` и дополнительные production contexts; -- несколько production technical integrations; -- HTTP, storage или realtime behind consumer-owned ports; -- строгие environment boundaries; -- самостоятельные domain-specific framework modules. +Мигрируй небольшими связными срезами, каждый из которых оставляет понятного владельца и рабочий публичный контракт: -Не выбирай Level 2 из-за количества файлов, одного SDK, одного hook, желания унифицировать дерево или гипотетической будущей интеграции. Зафиксируй, какую реальную потребность окупает дополнительная стоимость package, facets, assembly и adapters. +1. Инвентаризируй фактические ответственности, внешних потребителей и текущие межмодульные рёбра выбранного участка. +2. Составь целевую карточку решения, не начиная с желаемого дерева папок. +3. Объяви целевой публичный контракт; для домена до интеграции также зафиксируй предметный контракт, ожидаемые исходы и границу defects. +4. Создай или скорректируй границу владельца; для домена добавь внутреннюю адаптацию источников и ошибок. +5. Перенеси поведение, состояние, доменный UI и lifecycle целиком, не создавая параллельного владельца. +6. Переключи потребителей на фасеты и удаляй глубокие импорты. +7. Пересчитай свёрнутый граф, проверь среды фасетов и очистку ресурсов. +8. Удали legacy-путь после перехода всех реальных потребителей. +9. Повтори процесс для следующего независимого среза. -### Публичная граница +Не используй `compositions` как временного владельца нового доменного сценария. Если промежуточное состояние ещё нарушает правило, не называй его завершённой SLM-миграцией и явно фиксируй ограничение. -1. Перечисли реальных внешних consumers. -2. Для каждого запиши минимально необходимый contract. -3. Удали exports, которым нет consumer. -4. Не включай mutable internals, concrete clients, stores, contexts, adapter implementations или lifecycle internals в Domain API, модуль `api` или parent module. -5. Для обычного module оставь одну логическую external entry point. -6. Для модуля `api` используй только `api`, `api/factory`, optional `api/ports` и optional `api/runtime`. -7. Удали deep imports и обнови package exports/aliases при необходимости. -8. Не открывай nested module напрямую за пределы parent boundary. +## Проверка результата -Каждый adapter остаётся обычным SLM-модулем и предоставляет собственный минимальный public API, через который assembly получает production implementation. Запрещён не public API adapter-модуля, а его реэкспорт через `api`, корень пакета, Domain API или другой несвязанный owner. +Перед завершением открой полный [критерий завершения](./reference/docs/reference/validation.md#критерий-завершения) и проверь только применимые пункты. Сохрани доказательства по смысловым решениям, доменной границе, структуре, средам выполнения и проектным test/lint/build-командам, не копируя checklist в отчёт. -Для Level 2 разделяй Domain API по устойчивым различиям consumers, dependencies или environments, а не по внутренним техническим папкам. Каждый публичный scenario принадлежит ровно одному Domain API; каждому API соответствует одна factory. Именованный graph assembly не является новым Domain API. +Успешная сборка не заменяет смысловую проверку. Если автоматического SLM lint нет, выполни структурную проверку вручную и перечисли проверенные модули, фасеты и рёбра. Не утверждай прохождение проверки, которую фактически не запускал или не мог выполнить. -### Проверка зависимости +## Stop conditions -Для каждого нового или изменённого edge: +Сначала ищи ответ в коде, конфигурации и references. Задавай пользователю адресный вопрос только когда решение зависит от отсутствующего продуктового или эксплуатационного факта: -1. Определи source owner и target owner. -2. Определи их слои и формы доменов. -3. Если owners различаются, импортируй target только через public API. -4. Проверь матрицу слоёв. -5. Если edge пересекает Level 2 package boundary, примени более строгую cross-domain модель. -6. Проверь transitive environment compatibility. -7. Добавь edge в общий module DAG и runtime graph и проверь цикл, включая callbacks и mixed L1/L2 construction. +- результат поведения нельзя однозначно сформулировать; +- подходят несколько владельцев, а предметная граница не следует из кода; +- неизвестно, является ли координация техническим связыванием или новым доменным сценарием; +- ожидаемые неуспешные исходы и граница programming defect не определены продуктом; +- неизвестны реальные внешние потребители или требуемая среда выполнения; +- неизвестны область жизни, число экземпляров или момент очистки ресурса; +- локальное сопоставление путей с SLM-сущностями нельзя подтвердить, а задача требует блокирующего структурного вердикта. -Между двумя доменными модулями Level 1 допустим обычный runtime-import публичного API при соблюдении layer matrix и DAG. Не навязывай им runtime injection Level 2. - -Если хотя бы одна сторона является пакетом Level 2, статически допустимы три формы: - -```ts -import type { LevelOneDomainApi } from '.../level-one-domain' -import type { LevelTwoDomainApi } from '.../level-two-domain/api' -import { deterministicValue } from '.../level-two-domain/api/runtime' -``` - -Готовый runtime API, связь с которым пересекает Level 2 package boundary, создаёт внешний graph owner и передаёт assembly зависимого пакета Level 2 либо явной construction point/public callback модуля Level 1. Через такую границу не импортируй чужие `api/ports`, factory, assembly, adapter, API singleton, framework state, hook, context, Provider, component или внутренний путь `api`. - -Не скрывай cross-domain dependency локальным structural interface, callback, global registry или event bus. Установи владельца контракта и отрази фактический runtime edge в graph, иначе можно пропустить цикл. - -### Runtime capabilities - -| Capability | Размещение в Level 2 | -|---|---| -| SDK, HTTP/GraphQL source, storage, platform API | Adapter | -| Concrete state/query runtime для materialized domain values | Framework binding или composition | -| Clock, timer, random, ID, environment | Dependency port с production implementation в adapter | -| Готовый API другого домена | Cross-domain dependency, передаваемая graph owner | -| Provider, hook или query projection готового Domain API | Framework binding module | -| Page-local или multi-domain UI state | Владеющий composition module | -| Универсальный technical service | `infra` module | - -Adapter переводит provider arguments, records и expected failures в consumer-owned port. Он не объявляет Domain API scenario, domain fallback, transition или public domain error. Production implementation port не прячь inline в assembly или composition. - -`assemblies/default` выбирает штатные adapters, вызывает factories и возвращает baseline graph API. Дополнительная assembly представляет реально отличающийся production context. Они не добавляют scenarios, методы API или собственные domain errors. Framework binding получает готовые API и не вызывает factory/assembly и не выбирает adapter. - -Даже если готовый `infra` API структурно совпадает с technical dependency, текущие правила Level 2 требуют production implementation в adapter-модуле домена. Сделай его public API минимальным и не добавляй фиктивные преобразования, но не обходи обязательную adapter boundary прямой передачей `infra` capability в factory. - -Перед новым external import в `api`: - -1. Определи реально resolved package entry и resolver conditions нужных environments. -2. Проверь transitive runtime graph, side effects, I/O, mutable state и runtime capabilities. -3. Убедись, что package соответствует API-safe критериям, и обнови project allowlist/declaration. -4. Если доказательства нет, вынеси capability в factory dependency и реализуй production binding через adapter. - -### State и cache - -```text -Domain models, validation, transitions, commands, scenario outcomes - -> api authority - -Transport/source cache - -> adapter - -Framework/query projection готового Domain API - -> framework binding - -State только текущей UI composition - -> composition owner -``` - -Raw DTO, query-library result и mutable client не являются Domain API. Private source cache внутри adapter может хранить provider records, если DTO и library types не выходят в Domain API. Binding может владеть framework metadata и local UI state, но domain payload projection использует только public values, outcomes и events, произведённые или проверенные `api`, и не создаёт параллельную предметную модель. - -При optimistic или concurrent mutations не придумывай универсальный rollback. Предметные ordering, versioning, rebase/rollback и reconciliation определяет операция Domain API либо deterministic `api/runtime`; иначе binding invalidates projection и получает authoritative snapshot через API. - -### Errors - -- Каждый expected failure публичного scenario, включая собственный domain rejection, представлен именованным readonly error type текущего домена со stable code. -- Expected provider failure проходит через adapter и closed port failure, после чего текущий `api` преобразует его в собственный domain error. -- Expected foreign-domain outcome или error поступает через готовый публичный API другого домена и преобразуется текущим `api` напрямую, без автоматического local port. -- Source object, SDK class, message, status, payload и `cause` не входят в public domain contract. -- Type errors экспортируются через `api`; необходимые runtime codes и guards - только через реально нужный `api/runtime`. -- Не выбирай exception или discriminated `Result` как правило SLM: сохрани project policy и архитектурное владение. -- Не маскируй programming defect под expected domain outcome. Если меняется публичный failure channel, выясни политику unexpected failures, cancellation и serialization. - -### Realtime - -Realtime transport остаётся внутри adapter. Domain API публикует только проверенные events, outcomes, statuses и stable errors. Для command-response protocol установи correlation scope, ACK semantics, timeout, cancellation и `OUTCOME_UNKNOWN`; без correlation не обещай индивидуальный result. - -Для каждой subscription установи ordering, duplicate delivery, reconnect, gap detection, resync, shared connection ownership и момент, после которого cleanup гарантирует отсутствие callbacks. Framework binding materializes events через API-owned transition либо invalidates cache и повторно запрашивает snapshot. - -### Lifecycle и environment - -Для каждого request, subscription, listener, timer, observer, connection или другого долгоживущего ресурса зафиксируй: - -```text -Owner: -Created or started by: -Scope: -Multiplicity: -Environment: -Owned or borrowed: -Cleanup: -``` - -Factory не запускает долгоживущую работу. Явная операция, запускающая resource, предоставляет cleanup. У каждого resource один owner: adapter-owned resource экспортирует handle для aggregate cleanup, assembly-owned resource передаётся adapter как borrowed capability. Assembly немедленно регистрирует cleanup каждого owned resource и полученный adapter lifecycle handle; при любом obligation возвращает идемпотентный aggregate cleanup. - -Спроектируй failure path assembly. Если следующий шаг завершился ошибкой до возврата graph, assembly выполняет все зарегистрированные cleanup obligations в обратном dependency order. После awaited cleanup callbacks запрещены. Покрой partial acquisition, adapter handles, repeated disposal и cleanup errors тестами. - -Environment определяется transitive import graph, а не именем файла или tree shaking. Для RSC, Server Actions, workers, edge runtime и conditional exports установи executable edges, framework references и runtime capabilities. Для SSR-enabled Client Component отдельно проверь server prerender graph, browser hydration graph и framework-deferred browser effects. - -## Рабочие процедуры - -### Проектирование - -1. Ограничь scope пользовательской задачей. -2. Найди SLM root, project mapping и существующие owners. -3. Построй карту consumers и текущих public paths. -4. Определи ответственность, layer, entity и domain form. -5. Спроектируй target boundaries и минимальные public contracts. -6. Классифицируй technical и cross-domain dependencies. -7. Определи graph owner, environments, state, errors и lifecycle. -8. Проверь правила и stop conditions. -9. Выдай решение, target structure, dependencies и порядок реализации. - -Не предлагай файловое дерево до определения owners и boundaries. Имена файлов и segments следуют локальному стайлгайду, а не задаются SLM. - -### Реализация - -1. Зафиксируй принятое решение и change scope. -2. Изменяй код в dependency order: contracts и behavior раньше adapters и assembly, providers/consumers после готовых API. -3. Для Level 1 не создавай отсутствующие роли Level 2. -4. Для Level 2 сначала реализуй consumer types, `api/ports` при наличии dependency ports, errors, operations и `api/factory`. -5. Затем реализуй production adapters, обязательную `assemblies/default`, дополнительные assemblies и framework bindings. -6. В graph owner вызывай assembly builders пакетов Level 2 и явные construction points/public callbacks модулей Level 1, передавая им готовые cross-domain API. -7. Переведи всех затронутых consumers на public paths. -8. Обнови architecture mapping, assembly contexts, package exports, environment declarations и API-safe allowlists, затронутые новой границей. -9. Удали obsolete exports, deep imports и старые boundaries в согласованном scope. -10. Добавь tests рядом с owners, включая adapter contract tests, realtime guarantees и cleanup failure paths assemblies. -11. Запусти доступные structural, type, unit, integration и architecture checks и убедись, что новые пути входят в анализ. - -Не оставляй заведомо промежуточную смешанную границу как завершённый результат. Backward compatibility добавляй только для реального внешнего consumer, persisted contract или явно согласованной phased migration. - -### Миграция Level 1 -> Level 2 - -1. Выбери ровно один domain module и докажи потребность Level 2. -2. Найди все consumers, exports, state, I/O, framework integration и lifecycle resources. -3. Вычисли dependency-connected migration radius до редактирования. -4. Спроектируй Domain API по scenarios и consumers, а не по текущим technical segments. -5. Перенеси public models, validation, transitions, outcomes и errors под authority `api`. -6. Объяви consumer-owned ports, closed port failures и по одной factory на Domain API. -7. Оформи production implementations ports как adapter modules и добавь contract tests. -8. Создай обязательную `assemblies/default` для baseline production context и дополнительные assemblies только при реальном отличии graph. -9. Перенеси domain-specific state, cache, hydration и framework responsibilities в Framework Group. -10. Оставь pages, routes и multi-domain UI в `compositions`. -11. Переключи external consumers и graph roots. -12. Обнови declarations формы домена, модулей, facets, environments и public entry points в project architecture mapping. -13. Удали прежний root API и старую форму домена. -14. Проверь, что каждый завершённый этап оставляет одну форму затронутой domain responsibility. - -Временное физическое сосуществование старой и новой структуры допустимо только внутри незавершённого изменения. Не объявляй его conforming state. Не мигрируй несвязанные соседние домены, но включи в migration radius dependency-connected consumer, если его API нужно рефакторить или перевести на Level 2 для явной runtime injection. Такое расширение scope сначала согласуй. Если атомарный cutover невозможен, согласуй ограниченную compatibility strategy и срок её удаления. - -### Архитектурное ревью - -1. Определи review scope, SLM root и формы затронутых доменов. -2. Построй фактическую карту owners, public boundaries, imports и runtime injection. -3. Проверь structural правила класса `A` по наблюдаемым evidence. -4. Отдельно проверь смысловые правила класса `R`; отсутствие lint error не доказывает их соблюдение. -5. Проверь transitive `api` closure, resolved external package entries и API-safe declarations. -6. Проверь importer matrix `api/ports`, `api/factory`, concrete adapters и assemblies. -7. Проверь environment graph, resolver conditions и framework reference edges. -8. Проверь state/cache/error/realtime/lifecycle ownership, включая cleanup частично созданной assembly. -9. Проверь, что tests находятся у правильных owners и не дублируют весь behavior на каждом уровне. -10. Сначала сообщи findings по severity, затем краткий verdict и остаточные gaps. - -Каждый finding содержит: - -```text -Location: -Kind: -Rule or definition: -Evidence: -Impact: -Minimal remediation: -Required tests: -Confidence: -``` - -Не называй рекомендацию нарушением. Не подтверждай полное SLM conformance, если не исследовал весь нужный graph или не знаешь project mapping. - -### Тестирование по владельцам - -| Ответственность | Основная test boundary | -|---|---| -| Domain scenarios, validation, models, outcomes и expected errors | `api` через соответствующую factory | -| Deterministic runtime/guards | `api` | -| Port mapping и provider behavior | Adapter module | -| Graph composition, adapter selection, environment, success cleanup и partial-failure cleanup | Assembly module | -| Provider, hook, form или query projection | Framework binding module | -| Multi-domain graph и lifecycle | Composition, `app` или другой graph owner | - -Не повторяй полный Domain API scenario suite в adapter, assembly и framework tests. Проверяй в каждой границе только принадлежащий ей behavior и integration contract. - -## Anti-patterns - -### Ownership и структура - -- Выбирать слой или сущность по имени существующей папки. -- Размещать domain model или scenario в `infra`/`shared`. -- Оставлять page, route policy или multi-domain responsibility внутри домена. -- Делать Group, segment или component скрытым owner. -- Создавать общий module или Level 2 package на будущее. -- Требовать от component быть stateless: локальные data/lifecycle details допустимы, пока ответственность принадлежит parent module. - -### Public boundaries - -- Deep imports во внутренности module или `api`. -- Root barrel доменного пакета или Group. -- Reexport adapter implementation через `api`, package root или Domain API вместо public API самого adapter-модуля. -- Reexport client и server entry points через общий barrel. -- Создавать `api/ports` без dependency port или `api/runtime` без внешнего consumer. - -### Domain API и runtime - -- Импортировать SDK, storage, framework, state/query manager, platform API или hidden nondeterminism в `api`. -- Обходить boundary через helper, `shared` или type alias. -- Публиковать raw DTO или library-specific cache/store types в Domain API. -- Экспортировать port contracts через consumer-facing `api` вместо `api/ports`. -- Позволять adapter определять domain fallback, transition или error semantics. -- Прятать production adapter inline в assembly/composition. -- Позволять factory выбирать environment или assembly. - -### Assembly, framework и cross-domain - -- Добавлять scenario или API method в assembly. -- Вызывать factory/assembly из framework binding. -- Импортировать `api/factory` или concrete adapter из production graph owner в обход assembly. -- При пересечении Level 2 package boundary импортировать framework state, hooks или components другого домена. -- При пересечении Level 2 package boundary импортировать чужие ports, factory, assembly, adapter или API singleton. -- Прятать runtime dependency в service locator, mutable registry или event bus. -- Передавать production `infra` capability напрямую в Level 2 factory в обход обязательного adapter-модуля. -- Считать `assemblies/default` изоморфной только из-за имени или runtime branch. - -### State и lifecycle - -- Делать cache параллельной domain model. -- Строить optimistic domain value из raw form/DTO без API validation. -- Использовать file-level singleton без доказанного application scope. -- Запускать скрытую subscription/timer при создании API. -- Оставлять resource без scope или cleanup. -- Возвращать пустой `dispose` только для одинаковой формы assemblies. -- Вызывать callbacks после завершившегося cleanup. -- Повторять realtime command без idempotency guarantee после `OUTCOME_UNKNOWN`. - -### Процесс - -- Выбирать Level 2 по размеру каталога. -- Генерировать полный package scaffold без потребности. -- Мигрировать несвязанные соседние домены ради локального изменения. -- Копировать пример как нормативное дерево. -- Перечислять коды правил вместо анализа фактического graph и runtime. -- Задавать пользователю все открытые вопросы независимо от задачи. - -## Stop conditions и адресные вопросы - -Остановись до изменения публичной или runtime-границы, если: - -- ответственность или owner не определены; -- одна ответственность имеет конкурирующих owners; -- неизвестны consumers изменяемого API; -- одна domain responsibility окажется в двух формах; -- planned edge создаёт цикл; -- environment compatibility нельзя установить; -- resource scope, multiplicity или cleanup неизвестны; -- изменение требует незапрошенной широкой миграции; -- локальные инструкции противоречат выбранной SLM boundary; -- корректность зависит от открытой semantics cancellation, concurrency, hydration или disposal; -- задача требует правил монорепозитория, versioning или нескольких SLM roots, которых текущий DRAFT не задаёт. - -Задавай вопрос только при наличии trigger: - -| Trigger | Что выяснить | -|---|---| -| L1 -> L2 или удаление старого API | Полный migration radius, атомарный cutover или compatibility strategy | -| Новая technical dependency | Ownership contract, timeout/retry/idempotency/order/subscription semantics | -| Abort или cancellable operation | Кто владеет cancellation и как она связана с cleanup/outcome | -| Публичные errors, RPC, server action | Expected failure, cancellation, unexpected defect и serialization policy | -| Store, persistence или external events | Projection owner, API validation, hydration, resync и authoritative source | -| Optimistic/concurrent mutations | Ordering, versioning, rollback/rebase и authoritative refresh | -| Assembly, lazy graph или новый root | Scope, multiplicity, owned/borrowed resources и disposal | -| SSR, hydration, RSC | Serialization boundary, validation/reset и executable/reference edges | -| Worker, edge, conditional exports | Реальные capabilities и resolver conditions | -| Готовый `infra` API совпадает с port | Какой минимальный public API adapter-модуля свяжет capability без фиктивной domain semantics | - -Можно продолжить с явным assumption только когда решение обратимо, не меняет owner/public API, не ослабляет environment boundary и не скрывает lifecycle. - -## Проверочные списки - -### До изменения файлов - -- [ ] Найден SLM root и path mapping. -- [ ] Найдены architecture declarations, assembly contexts, environment/API-safe allowlists проекта. -- [ ] Прочитаны локальные инструкции. -- [ ] Сформулирована responsibility. -- [ ] Назначен один owner. -- [ ] Выбраны layer и entity. -- [ ] Для domain выбрана одна form. -- [ ] Найдены реальные consumers. -- [ ] Спроектирован минимальный public API. -- [ ] Классифицированы static и runtime dependencies. -- [ ] Проверены layer, cross-domain и environment edges. -- [ ] Для resources определены scope и cleanup. -- [ ] Нет активного stop condition. - -### После реализации - -- [ ] Каждый module имеет отдельную boundary и public API. -- [ ] Нет deep imports и package/Group barrels. -- [ ] Layer matrix соблюдена. -- [ ] Общий module graph ацикличен. -- [ ] `api` import closure environment-neutral и technical-runtime-free. -- [ ] Runtime APIs, пересекающие Level 2 package boundary, передаются аргументами; L1 -> L1 использует public API. -- [ ] Новые external imports `api` доказанно API-safe и объявлены в allowlist. -- [ ] Client/server graphs не содержат несовместимый executable code. -- [ ] Обязательные и фактически существующие optional facets `api` имеют допустимое содержимое и consumers. -- [ ] Production implementations ports принадлежат нужным adapters. -- [ ] `assemblies/default` представляет объявленный baseline context и не имеет import side effects. -- [ ] Assemblies возвращают точный graph и выполняют все owned и adapter-provided cleanup obligations после успеха и partial failure. -- [ ] Framework bindings получают готовые APIs. -- [ ] Все expected scenario failures имеют собственный stable domain error; technical и foreign errors не протекают наружу. -- [ ] Framework projection не подменяет authority `api`. -- [ ] Realtime ports определяют correlation, ordering, resync, outcome uncertainty и cleanup. -- [ ] Architecture mapping, exports, facets и environment declarations соответствуют новым путям. -- [ ] Tests проверяют behavior соответствующих owners. -- [ ] После migration удалена старая form/boundary. +В вопросе укажи наблюдаемый факт, архитектурное последствие и конкретные варианты выбора. Не проси пользователя решать то, что можно установить поиском по репозиторию. ## Формат результата -Не печатай полную внутреннюю карточку и все checklists без необходимости. Пользователю нужен результат задачи. +Для проектирования, реализации или миграции сообщай: -| Режим | Обязательный результат | -|---|---| -| Design | Decision, owner/layer/form, boundaries, public APIs, dependencies, lifecycle, implementation order, assumptions | -| Implementation | Использованное решение, изменённые boundaries/files, API/import changes, tests/checks, отклонения и риски | -| Migration | Source/target forms, consumer map, phases, cutover, удаление старой boundary и completion gate | -| Review | Findings с evidence, verdict, remediation order, unresolved decisions и непроверенный scope | +- ответственность, владельца и выбранный слой; +- изменённые публичные границы и межмодульные связи; +- существенные решения по доменному контракту, состоянию и lifecycle; +- какие references и правила повлияли на решение; +- выполненные проверки и непроверенные риски. -Для однозначной локальной реализации достаточно кратко объяснить архитектурное решение и выполнить работу. Для дорогого, публично несовместимого или неоднозначного решения сначала покажи варианты и запроси выбор. +Для чистого проектирования вместо списка изменённых файлов дай целевую physical form, consumer/environment map, impact map и нерешённые продуктовые факты. -## Когда открывать references +Для объяснения отделяй определения и правила SLM от рекомендаций и project policy. Для ревью используй формат findings из раздела выше. -| Ситуация | Reference | -|---|---| -| Нужна точная формулировка правила | [`rules/level-1.md`](./reference/draft/rules/level-1.md), [`rules/level-2.md`](./reference/draft/rules/level-2.md) | -| Неясен смысл сущности | [`level-1/terminology.md`](./reference/draft/level-1/terminology.md), [`level-2/terminology.md`](./reference/draft/level-2/terminology.md) | -| Сложный Level 1 module/dependency/lifecycle case | [`level-1/`](./reference/draft/level-1/README.md) | -| Package, Domain API, ports, factory или adapters | [`level-2/domains/`](./reference/draft/level-2/domains/README.md) | -| Cross-domain или environment edge | [`level-2/dependencies.md`](./reference/draft/level-2/dependencies.md) | -| State, cache, SSR или hydration | [`state-cache.md`](./reference/draft/level-2/domains/state-cache.md), [`open-questions.md`](./reference/draft/level-2/domains/open-questions.md) | -| Assembly lifecycle и cleanup | [`assemblies.md`](./reference/draft/level-2/domains/assemblies.md) | -| Realtime messages и subscriptions | [`realtime.md`](./reference/draft/level-2/domains/realtime.md) | -| Full architecture review | [`level-1/validation.md`](./reference/draft/level-1/validation.md), [`level-2/validation.md`](./reference/draft/level-2/validation.md) | -| L1 -> L2 migration example | [`auth-example.md`](./reference/draft/level-2/domains/auth-example.md) | +## Карта файлов -Будущие project examples открывай только после архитектурной классификации. Используй их как evidence конкретной реализации для похожего stack/environment, но не копируй naming, дерево или дополнительные роли без потребности. Example никогда не переопределяет rule или terminology. +References являются частью собранного skill. Карта покрывает весь комплект материалов; открывай минимальный набор для текущей задачи, но перед блокирующим вердиктом всегда сверяй точную формулировку с реестром. + +| Файл | Что содержит | Когда открывать | +|---|---|---| +| [`reference/docs/README.md`](./reference/docs/README.md) | Обзор SLM, мотивация, область вопросов и стартовая навигация | Первое знакомство, объяснение подхода, выбор начального участка внедрения | +| [`reference/docs/architecture/README.md`](./reference/docs/architecture/README.md) | Базовая модель владения, структурное дерево, порядок проектирования и область применения | В начале проектирования, миграции или широкого ревью | +| [`reference/docs/architecture/layers.md`](./reference/docs/architecture/layers.md) | Роли шести слоёв, граница `domains`/`compositions`, немодульные исключения | Выбор или проверка слоя, страницы, доменного UI, `app`, `infra`, `shared` | +| [`reference/docs/architecture/modules.md`](./reference/docs/architecture/modules.md) | Ответственность модуля, ближайший владелец, API, фасеты, корень, компоненты, вложенность, состояние и lifecycle | Создание и изменение модуля, проектирование экспортов и внутренней структуры | +| [`reference/docs/architecture/domains.md`](./reference/docs/architecture/domains.md) | Доменный контракт, source boundary, адаптация, ошибки, runtime-идентификация и порядок создания домена | Любой доменный сценарий, продуктовые данные, DTO, SDK, storage или error contract | +| [`reference/docs/architecture/dependencies.md`](./reference/docs/architecture/dependencies.md) | Свёрнутый модульный граф, публичные пути, матрица слоёв, same-layer связи и циклы | Добавление импорта, реэкспорта, фасета, анализ deep import или цикла | +| [`reference/docs/architecture/groups.md`](./reference/docs/architecture/groups.md) | Навигационные группы и их отличие от модулей и сегментов | Группировка модулей, каталоги `pages`/`layouts`/`widgets`, group barrel | +| [`reference/docs/architecture/segments.md`](./reference/docs/architecture/segments.md) | Внутренняя организация владельца, колокация, компонентные единицы и переход к вложенному модулю | Размещение внутреннего файла, рост модуля, спор о `components`/`hooks`/`services` | +| [`reference/docs/reference/terminology.md`](./reference/docs/reference/terminology.md) | Нормативные определения всех сущностей и границ SLM | Спор о термине, классификация сущности, точное толкование правила | +| [`reference/docs/reference/validation.md`](./reference/docs/reference/validation.md) | Карточка решения, review-checklists, автоматические проверки, фасеты и критерий завершения | До структурного изменения, на ревью и перед завершением любой архитектурной задачи | +| [`reference/docs/rules/README.md`](./reference/docs/rules/README.md) | Разница между определением, правилом, рекомендацией и примером; классы `A`/`R` | Оформление вердикта, проектирование lint-проверки, оценка нормативной силы утверждения | +| [`reference/docs/rules/registry.md`](./reference/docs/rules/registry.md) | Единственный реестр точных блокирующих требований и стабильных кодов | Любой finding, заявление о нарушении или обязательном соответствии | + +### Маршруты чтения + +- Новый или изменяемый модуль: `architecture/README.md` -> `architecture/modules.md` -> нужная глава о слое или домене -> `architecture/dependencies.md` -> `reference/validation.md`. +- Доменный сценарий: `architecture/domains.md` -> `architecture/layers.md` -> `architecture/dependencies.md` -> `reference/validation.md`. +- Размещение внутреннего кода: `architecture/modules.md` -> `architecture/segments.md`; `architecture/groups.md` добавляется только для внешней навигации модулей. +- Фасеты и runtime boundaries: `architecture/modules.md` -> раздел проверки фасетов в `reference/validation.md` -> environment-правила в `rules/registry.md`. +- Архитектурное ревью: `reference/validation.md` -> `rules/registry.md` -> тематические главы по каждому найденному риску. +- Терминологический спор: `reference/terminology.md` -> тематическая глава -> `rules/registry.md`, если требуется обязательный вердикт. diff --git a/src-skills/slm-design/skill.config.mjs b/src-skills/slm-design/skill.config.mjs index dd812e6..d1e4b27 100644 --- a/src-skills/slm-design/skill.config.mjs +++ b/src-skills/slm-design/skill.config.mjs @@ -1,12 +1,34 @@ export default { name: 'slm-design', source: 'SKILL.md', + requiredHeadings: [ + 'Источники истины', + 'Рабочий режим', + 'Сбор контекста', + 'Проектирование', + 'Реализация изменений', + 'Архитектурное ревью', + 'Миграция', + 'Проверка результата', + 'Stop conditions', + 'Карта файлов', + ], references: [ { - source: 'DRAFT', - target: 'reference/draft', - include: ['README.md', 'rules', 'level-1', 'level-2'], + source: 'docs', + target: 'reference/docs', + include: ['.'], }, ], - legacyMarkers: ['old-docs', 'reference/canons', 'reference/slm-design'], + referenceMap: { + heading: 'Карта файлов', + target: 'reference/docs', + }, + legacyMarkers: [ + 'DRAFT/', + 'reference/draft', + 'old-docs', + 'reference/canons', + 'reference/slm-design', + ], }; diff --git a/tests/skill-bundle.test.mjs b/tests/skill-bundle.test.mjs index 6c1baa2..c2a3fce 100644 --- a/tests/skill-bundle.test.mjs +++ b/tests/skill-bundle.test.mjs @@ -3,6 +3,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; +import { fileURLToPath } from 'node:url'; import { createSkillBundle, listBundleFiles, @@ -11,6 +12,7 @@ import { writeSkillBundle, } from '../scripts/lib/skill-bundle.mjs'; import { slugifyHeading } from '../scripts/lib/slugify-heading.mjs'; +import skillConfig from '../src-skills/slm-design/skill.config.mjs'; const requiredSkill = (extra = '') => [ '---', @@ -20,25 +22,23 @@ const requiredSkill = (extra = '') => [ '', '# SLM Design', '', - '## Универсальный цикл решения', - '## Алгоритмы выбора', - '### Реализация', - '### Миграция Level 1 -> Level 2', - '### Архитектурное ревью', - '## Anti-patterns', - '## Stop conditions и адресные вопросы', - '## Когда открывать references', + ...skillConfig.requiredHeadings.map((heading) => `## ${heading}`), + '[Reference](./reference/docs/README.md)', extra, '', ].join('\n'); const entry = (content) => ({ content: Buffer.from(content), sourcePath: 'fixture' }); -const config = { name: 'slm-design', references: [], legacyMarkers: ['old-docs'] }; +const config = { + ...skillConfig, + references: [], +}; +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); test('validates parsed Markdown links and ignores code examples', () => { const extra = [ - '[Target](<./reference/draft/a(b).md#title>)', - '[Encoded](./reference/draft/foo%23bar.md)', + '[Target](<./reference/docs/a(b).md#title>)', + '[Encoded](./reference/docs/foo%23bar.md)', '', '`[inline][missing]`', '\\[escaped][missing]', @@ -50,8 +50,9 @@ test('validates parsed Markdown links and ignores code examples', () => { ].join('\n'); const bundle = new Map([ ['SKILL.md', entry(requiredSkill(extra))], - ['reference/draft/a(b).md', entry('Title\n=====\n')], - ['reference/draft/foo#bar.md', entry('# Encoded\n')], + ['reference/docs/README.md', entry('# Documentation\n')], + ['reference/docs/a(b).md', entry('Title\n=====\n')], + ['reference/docs/foo#bar.md', entry('# Encoded\n')], ]); assert.doesNotThrow(() => validateSkillBundle({ bundle, config, repoRoot: '/workspace/repo' })); @@ -60,6 +61,7 @@ test('validates parsed Markdown links and ignores code examples', () => { test('rejects undefined reference-style links', () => { const bundle = new Map([ ['SKILL.md', entry(requiredSkill('[broken][missing-reference]'))], + ['reference/docs/README.md', entry('# Documentation\n')], ]); assert.throws( @@ -78,6 +80,7 @@ test('rejects non-portable or unsafe links', () => { ]) { const bundle = new Map([ ['SKILL.md', entry(requiredSkill(`[broken](${target})`))], + ['reference/docs/README.md', entry('# Documentation\n')], ]); assert.throws( @@ -88,16 +91,19 @@ test('rejects non-portable or unsafe links', () => { }); test('uses the same heading slugs as the documentation site', () => { - assert.equal(slugifyHeading('Миграция Level 1 -> Level 2'), 'миграция-level-1-level-2'); + assert.equal(slugifyHeading('Архитектурное ревью'), 'архитектурное-ревью'); assert.equal(slugifyHeading('`app`'), 'layer-app'); }); test('requires operational sections to be real headings', () => { const fencedSkill = requiredSkill().replace( - '## Универсальный цикл решения', - '```md\n## Универсальный цикл решения\n```', + '## Рабочий режим', + '```md\n## Рабочий режим\n```', ); - const bundle = new Map([['SKILL.md', entry(fencedSkill)]]); + const bundle = new Map([ + ['SKILL.md', entry(fencedSkill)], + ['reference/docs/README.md', entry('# Documentation\n')], + ]); assert.throws( () => validateSkillBundle({ bundle, config, repoRoot: '/workspace/repo' }), @@ -105,6 +111,80 @@ test('requires operational sections to be real headings', () => { ); }); +test('requires a non-empty unique operational heading list', () => { + const bundle = new Map([ + ['SKILL.md', entry(requiredSkill())], + ['reference/docs/README.md', entry('# Documentation\n')], + ]); + + for (const requiredHeadings of [undefined, [], ['Рабочий режим', 'Рабочий режим']]) { + assert.throws( + () => validateSkillBundle({ + bundle, + config: { ...config, requiredHeadings }, + repoRoot: '/workspace/repo', + }), + /requiredHeadings/, + ); + } +}); + +test('rejects an incomplete reference map', () => { + const bundle = new Map([ + ['SKILL.md', entry(requiredSkill('[Mapped](./reference/docs/mapped.md)'))], + ['reference/docs/README.md', entry('# Documentation\n')], + ['reference/docs/mapped.md', entry('# Mapped\n')], + ['reference/docs/missing.md', entry('# Missing\n')], + ]); + const mapConfig = { + ...config, + referenceMap: { + heading: 'Карта файлов', + target: 'reference/docs', + }, + }; + + assert.throws( + () => validateSkillBundle({ bundle, config: mapConfig, repoRoot: '/workspace/repo' }), + /reference map is incomplete.*reference\/docs\/missing\.md/, + ); +}); + +test('requires reference map configuration', () => { + const bundle = new Map([ + ['SKILL.md', entry(requiredSkill())], + ['reference/docs/README.md', entry('# Documentation\n')], + ]); + + assert.throws( + () => validateSkillBundle({ + bundle, + config: { ...config, referenceMap: undefined }, + repoRoot: '/workspace/repo', + }), + /referenceMap must be an object/, + ); +}); + +test('rejects configured legacy reference markers', () => { + const bundle = new Map([ + ['SKILL.md', entry(requiredSkill('[Legacy](https://example.com/DRAFT/rules.md)'))], + ['reference/docs/README.md', entry('# Documentation\n')], + ]); + + assert.throws( + () => validateSkillBundle({ bundle, config, repoRoot: '/workspace/repo' }), + /legacy marker: DRAFT\//, + ); +}); + +test('validates the production skill and complete reference map', () => { + const bundle = createSkillBundle({ config: skillConfig, repoRoot }); + + assert.doesNotThrow(() => validateSkillBundle({ bundle, config: skillConfig, repoRoot })); + assert(bundle.has('reference/docs/rules/registry.md')); +}); + test('rejects traversal through the skill name', () => { assert.throws( () => resolveSkillPaths({ config: { name: '../source' }, repoRoot: '/workspace/repo' }), @@ -153,12 +233,55 @@ test('rejects symbolic links in source and generated trees', () => { } }); +test('recursively bundles an entire reference root deterministically', () => { + const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'slm-skill-recursive-')); + const sourceDir = path.join(fixtureRoot, 'src-skills', 'slm-design'); + const docsDir = path.join(fixtureRoot, 'docs'); + const architectureDir = path.join(docsDir, 'architecture'); + const recursiveConfig = { + name: 'slm-design', + source: 'SKILL.md', + references: [ + { + source: 'docs', + target: 'reference/docs', + include: ['.'], + }, + ], + }; + + fs.mkdirSync(sourceDir, { recursive: true }); + fs.mkdirSync(architectureDir, { recursive: true }); + fs.writeFileSync(path.join(sourceDir, 'SKILL.md'), requiredSkill()); + fs.writeFileSync(path.join(docsDir, 'README.md'), '# Documentation\n'); + fs.writeFileSync(path.join(architectureDir, 'modules.md'), '# Modules\n'); + + try { + const firstBundle = createSkillBundle({ config: recursiveConfig, repoRoot: fixtureRoot }); + const secondBundle = createSkillBundle({ config: recursiveConfig, repoRoot: fixtureRoot }); + const expectedPaths = [ + 'SKILL.md', + 'reference/docs/README.md', + 'reference/docs/architecture/modules.md', + ]; + + assert.deepEqual([...firstBundle.keys()], expectedPaths); + assert.deepEqual([...secondBundle.keys()], expectedPaths); + assert.deepEqual( + [...firstBundle.values()].map((item) => item.content.toString('utf8')), + [...secondBundle.values()].map((item) => item.content.toString('utf8')), + ); + } finally { + fs.rmSync(fixtureRoot, { recursive: true, force: true }); + } +}); + test('atomically replaces generated output and remains deterministic', () => { const outputParent = fs.mkdtempSync(path.join(os.tmpdir(), 'slm-skill-output-')); const outputDir = path.join(outputParent, 'slm-design'); const bundle = new Map([ ['SKILL.md', entry(requiredSkill())], - ['reference/draft/README.md', entry('# Draft\n')], + ['reference/docs/README.md', entry('# Documentation\n')], ]); fs.mkdirSync(outputDir); @@ -171,7 +294,7 @@ test('atomically replaces generated output and remains deterministic', () => { const files = listBundleFiles(outputDir) .map((filePath) => path.relative(outputDir, filePath).replaceAll(path.sep, '/')); - assert.deepEqual(files, ['SKILL.md', 'reference/draft/README.md']); + assert.deepEqual(files, ['SKILL.md', 'reference/docs/README.md']); assert.equal(fs.readFileSync(path.join(outputDir, 'SKILL.md'), 'utf8'), requiredSkill()); } finally { fs.rmSync(outputParent, { recursive: true, force: true });