diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index ed23dcc..904ab7a 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -9,6 +9,7 @@ on:
- 'site/**'
- 'draft-rules.js'
- 'scripts/check-site.mjs'
+ - 'scripts/lib/slugify-heading.mjs'
- 'package.json'
- 'package-lock.json'
pull_request:
@@ -18,6 +19,7 @@ on:
- 'site/**'
- 'draft-rules.js'
- 'scripts/check-site.mjs'
+ - 'scripts/lib/slugify-heading.mjs'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
diff --git a/.github/workflows/skill.yml b/.github/workflows/skill.yml
new file mode 100644
index 0000000..976ceb8
--- /dev/null
+++ b/.github/workflows/skill.yml
@@ -0,0 +1,60 @@
+name: Skill
+
+on:
+ push:
+ branches: [master]
+ paths:
+ - '.github/workflows/skill.yml'
+ - 'DRAFT/**'
+ - 'src-skills/**'
+ - 'skills/**'
+ - 'scripts/build-skill.mjs'
+ - 'scripts/check-skill.mjs'
+ - 'scripts/lib/skill-bundle.mjs'
+ - 'scripts/lib/slugify-heading.mjs'
+ - 'tests/skill-bundle.test.mjs'
+ - 'site/.vitepress/config.mts'
+ - 'draft-rules.js'
+ - 'package.json'
+ - 'package-lock.json'
+ pull_request:
+ paths:
+ - '.github/workflows/skill.yml'
+ - 'DRAFT/**'
+ - 'src-skills/**'
+ - 'skills/**'
+ - 'scripts/build-skill.mjs'
+ - 'scripts/check-skill.mjs'
+ - 'scripts/lib/skill-bundle.mjs'
+ - 'scripts/lib/slugify-heading.mjs'
+ - 'tests/skill-bundle.test.mjs'
+ - 'site/.vitepress/config.mts'
+ - 'draft-rules.js'
+ - 'package.json'
+ - 'package-lock.json'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Check rules and skill
+ run: npm run test:skill && npm run check:draft-rules && npm run check:skill
+
+ - name: Rebuild skill deterministically
+ run: mv skills/slm-design "$RUNNER_TEMP/slm-design-expected" && npm run build:skill && diff -qr "$RUNNER_TEMP/slm-design-expected" skills/slm-design && git diff --exit-code -- skills/slm-design
diff --git a/.opencode/agents/slm-critic.md b/.opencode/agents/slm-critic.md
new file mode 100644
index 0000000..a6c3d2d
--- /dev/null
+++ b/.opencode/agents/slm-critic.md
@@ -0,0 +1,120 @@
+---
+description: Проводит независимый критический аудит архитектуры SLM в DRAFT, ищет противоречия, неработающие границы и непроверяемые правила.
+mode: subagent
+color: warning
+temperature: 0.1
+steps: 40
+permission:
+ "*": deny
+ read: allow
+ glob: allow
+ grep: allow
+ list: allow
+---
+
+Ты независимый архитектурный критик SLM Design. Твоя задача не соглашаться с авторами и не быть недовольным любой ценой, а находить доказуемые противоречия, критические пробелы, неработающие границы, неоправданную стоимость и правила, которые нельзя проверить заявленным способом.
+
+## Область проверки
+
+Всегда читай актуальное состояние `DRAFT` целиком, включая:
+
+- корневые `README.md` и `index.md`;
+- терминологию Level 1 и Level 2;
+- оба канонических реестра правил;
+- `rules/README.md` с требованиями к качеству правил;
+- тематические главы, примеры, validation и open questions.
+
+При необходимости проверяй только связанные механизмы публикации и валидации: `draft-rules.js`, `scripts/check-site.mjs` и `site/.vitepress/config.mts`.
+
+Не используй `old-docs`, `skills`, `src-skills` и другие архивные либо производные материалы как источник текущей нормы. Упоминай их только если сам актуальный `DRAFT` делает на них нормативную ссылку.
+
+Никогда не редактируй файлы. Не запускай команды, web-поиск или других агентов. Если информации недостаточно, явно зафиксируй assumption в отчёте и продолжай анализ.
+
+## Что проверять
+
+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. Качество реестра: одно правило защищает один инвариант, не дублирует другое, понятно без тематической главы и использует только нормативные термины.
+
+Для каждого существенного утверждения ищи хотя бы один контрпример. Особенно проверяй ситуации, в которых несколько локально корректных правил вместе создают невозможную или чрезмерно дорогую систему.
+
+## Дисциплина критики
+
+- Отличай внутреннее противоречие от альтернативного архитектурного предпочтения.
+- Не объявляй отсутствие дополнительного удобства дефектом, если распространённый сценарий уже имеет ясное и пропорциональное решение.
+- Не требуй нового слоя, сущности или правила без конкретного контрпримера.
+- Предлагай минимальную коррекцию, сохраняющую сильные стороны модели.
+- Не считай явно описанный tradeoff ошибкой только потому, что выбрал бы иначе.
+- На повторном проходе сохраняй идентификаторы прежних замечаний, если они переданы во входном контексте.
+- Новое замечание после повторного прохода должно содержать новый контрпример, ранее пропущенную зависимость или регрессию. Не двигай критерии приёмки без такого обоснования.
+- Не повторяй закрытое замечание; помести его в раздел подтверждённых исправлений.
+
+## Уровни серьёзности
+
+### BLOCKER
+
+Нормативное противоречие, невозможная реализация, нарушение жизненного цикла или среды, либо распространённый сценарий, для которого модель не оставляет корректного пути.
+
+### MAJOR
+
+Существенный архитектурный риск, неприемлемая стоимость распространённого сценария, ложное обещание инкрементальности или правило, чья заявленная проверяемость практически недостижима.
+
+### MINOR
+
+Локальная неоднозначность, неточный пример, неполная рекомендация или терминологическая проблема, которая не ломает основной сценарий.
+
+### DECISION
+
+Осознанный tradeoff или развилка, которую нельзя разрешить только техническим анализом. DECISION не является дефектом, пока выбор и его цена явно зафиксированы.
+
+## Формат каждого замечания
+
+Используй устойчивый тематический идентификатор, например `CRIT-COMPOSITION-001` или `CRIT-ERROR-001`.
+
+```text
+### CRIT-TOPIC-001 [BLOCKER|MAJOR|MINOR|DECISION] Краткое название
+Место: file:line или код правила
+Инвариант: что обещает модель
+Контрпример: минимальный реалистичный сценарий
+Проблема: почему текущая модель его не закрывает
+Минимальная коррекция: наименьшее достаточное изменение
+Затрагивает: определения, правила и главы, которые нужно синхронизировать
+```
+
+Не объединяй независимые проблемы в одно замечание.
+
+## Итоговый отчёт
+
+Сначала перечисли findings по убыванию серьёзности. Затем выдай:
+
+```text
+Verdict: REJECT | CONDITIONAL ACCEPT | ACCEPT
+Blockers: N
+Major: N
+Minor: N
+Decisions: N
+```
+
+Правила verdict:
+
+- `REJECT`, если есть хотя бы один BLOCKER;
+- `CONDITIONAL ACCEPT`, если BLOCKER отсутствуют, но есть MAJOR;
+- `ACCEPT`, если BLOCKER и MAJOR отсутствуют;
+- MINOR и DECISION не блокируют ACCEPT, если tradeoffs названы явно.
+
+После verdict добавь разделы:
+
+1. `Подтверждённые исправления прошлого прохода`, если переданы прежние findings.
+2. `Проверенные риски без замечаний`, чтобы не поднимать их повторно без регрессии.
+3. `Условия следующей приёмки` с конечным проверяемым списком.
+
+Если BLOCKER и MAJOR не найдены, скажи это прямо. Не создавай замечания только для наполнения отчёта.
diff --git a/.opencode/commands/slm-review.md b/.opencode/commands/slm-review.md
new file mode 100644
index 0000000..3a10a1b
--- /dev/null
+++ b/.opencode/commands/slm-review.md
@@ -0,0 +1,23 @@
+---
+description: Запускает полный независимый аудит актуальной архитектуры SLM и повторную проверку прежних замечаний.
+agent: slm-critic
+subtask: true
+---
+
+Проведи полный критический аудит текущего `DRAFT` по своему контракту.
+
+Контекст текущего прохода:
+
+$ARGUMENTS
+
+Если контекст пуст или содержит `baseline`, выполни независимый baseline review всей документации.
+
+Если контекст содержит `verify`, прежние идентификаторы замечаний или описание исправлений:
+
+- всё равно перечитай актуальный `DRAFT` целиком;
+- сначала проверь закрытие переданных замечаний;
+- сохрани их идентификаторы;
+- отдели подтверждённые исправления от оставшихся проблем и регрессий;
+- добавляй новый finding только при наличии нового контрпримера, ранее пропущенной зависимости или регрессии.
+
+Не редактируй файлы и не предлагай выполнить изменения самостоятельно. Верни только структурированный отчёт с findings, verdict и конечными условиями следующей приёмки.
diff --git a/.opencode/skills/rest-client/SKILL.md b/.opencode/skills/rest-client/SKILL.md
new file mode 100644
index 0000000..109d9ea
--- /dev/null
+++ b/.opencode/skills/rest-client/SKILL.md
@@ -0,0 +1,537 @@
+---
+name: rest-client
+description: "Используй при создании, изменении или ревью REST API клиента и infra REST-модуля. Триггеры: REST API, REST client, API client, backend API, external API, OpenAPI, Swagger, ручной клиент без OpenAPI, @gromlab/api-codegen, @gromlab/api-codegen@5.1.0, src/infra/*-rest-api, *-rest-api-sdk, REST SDK, npm SDK, client.ts, rest-api.ts, operations-tree.ts, *HttpClient, *RestApi, generated/, operations/, operations/*, data-contracts, hooks/, types/, errors/, HttpClient, ApiRequestClient, RequestParams, ContentType, createApiClient, operationsTree, full client, minimal client, SDK exports, onRequest, onResponse, onError, JWT, refresh token, ApiError, useGet*, get*Key, useSWR, SWRConfiguration, DTO, API error, public API REST-модуля. НЕ используй для любого fetch вне REST-клиента проекта, route-level data fetching Next.js, SLM-архитектуры без REST-модуля, code style, SVG sprites или генерации шаблонов."
+---
+
+
+
+# REST Client
+
+## Работа с REST-клиентом
+
+### Базовые Правила
+
+- REST-клиент сервиса оформляй как отдельный `infra/` module с именем `{name}-rest-api`: `pet-store-rest-api`, `billing-rest-api`, `maps-rest-api`.
+- Генерация внутри клиента живёт в `{name}-rest-api/generated`. Generated-клиент, выносимый в npm-пакет или пакет монорепозитория, называется `{name}-rest-api-sdk`.
+- Внешний код импортирует клиент, типы, enum и GET-хуки только через корневой `index.ts` REST-модуля.
+- `client.ts` экспортирует только настроенный транспортный `*HttpClient`: `petStoreHttpClient`, `billingHttpClient`, `mapsHttpClient`.
+- `rest-api.ts` экспортирует полный bound API-клиент `*RestApi`, собранный через `createApiClient(*HttpClient, operationsTree)`.
+- Не размещай в `client.ts` DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логику.
+- GET-хуки не импортируют полный `*RestApi`; они импортируют точечную operation и вызывают её через общий `*HttpClient`.
+- `hooks/index.ts` содержит `'use client'`; отдельные `use-get-*.hook.ts` остаются обычными файлами без директивы.
+- Если OpenAPI нет, ручные operations пиши через `@gromlab/api-codegen@5.1.0+` с тем же контрактом, что у generated SDK: `operation(http, params/body, requestParams?)`.
+- Не создавай собственные `fetch`-классы, `methods/*Methods(client)` и ручные `get/post` wrappers для нового ручного клиента.
+- Не правь файлы в `generated/` руками. Расширения типов, DTO и именованные response-типы держи в `types/` REST-модуля.
+- UI/components не импортируют SDK-пакет, `generated/`, `operations/`, `operationsTree` и транспорт напрямую.
+- SDK operations напрямую импортируются только внутри `infra/*-rest-api` и boundary-файлов минимальных clients в `compositions` или `features`.
+- Для прямого REST-вызова в server code, submit-функции или сервисе используй именованный API-объект клиента из публичного API REST-модуля или минимальный composition client.
+- Для GET-данных в Client Component сначала используй готовый `useGet*` hook REST-модуля.
+- GET-хуки являются прозрачными SWR-обёртками над GET operation и живут в `hooks/` этого REST-модуля.
+- Не выноси SWR keys, fetcher, DTO mapping, API errors и транспортные детали в UI-компоненты.
+- Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` или `hasPets`, это уже не REST-клиент, а `business/`.
+
+### Full Client vs Minimal Client
+
+- Full client живёт только в `infra/{name}-rest-api/rest-api.ts`.
+- Full client собирается из всего generated дерева: `createApiClient(nameHttpClient, operationsTree)`.
+- Full client экспортируется через `infra/{name}-rest-api/index.ts` как `nameRestApi`.
+- Minimal client допустим только в feature/composition boundary-файле, где нужен ограниченный набор операций для конкретного бизнес-сценария.
+- Minimal client собирается из selected operations: `createApiClient(nameHttpClient, { ...только нужные операции... })`.
+- GET-hook не использует bound client вообще: он вызывает `operation(nameHttpClient, params, requestParams?)`.
+- `operationsTree` не импортируется в GET-хуки и minimal clients.
+- Для ручного клиента `operationsTree` пишется вручную в `infra/{name}-rest-api/operations-tree.ts`.
+
+### Рабочий Алгоритм
+
+1. Найди REST-модуль сервиса в `infra/` и работай через его публичный API.
+2. Проверь корневой `index.ts`: внешний код не должен импортировать из `generated/`, `hooks/`, `types/` или `errors/` напрямую.
+3. Если нужен прямой REST-вызов в server code, submit-функции или сервисе, используй полный `*RestApi` из `infra/{name}-rest-api`.
+4. Если данные нужны в Client Component и запрос является GET, сначала ищи готовый `useGet*` hook.
+5. Если GET-хука нет, добавь его рядом с клиентом в `hooks/` по контракту раздела `GET-хуки REST-клиента`.
+6. Если feature/composition нужен компактный клиент с небольшим набором операций, создай minimal client в boundary-файле этой composition.
+7. Если нужно изменить тип ответа или дополнить generated-тип, не меняй generated-файл; добавь тип или расширение в `types/`.
+8. Если REST-клиента ещё нет или нужно подключить новый внешний API, открой конкретный локальный setup-материал ниже.
+9. После изменений проверь публичные экспорты, отсутствие SDK imports в UI/components и то, что SWR-механика не утекла в компоненты.
+
+### Создание И Настройка Клиента
+
+Создание нового REST-клиента - редкий сценарий. Не открывай setup-материалы, если задача сводится к использованию существующего клиента, GET-хука или публичного API.
+
+- [Настройка REST-клиента](./reference/canons/setup.md) - состав REST-клиента, структура модуля и базовая настройка.
+- [Автогенерация из OpenAPI](./reference/canons/auto.md) - генерация split-клиента через `@gromlab/api-codegen`.
+- [Кастомизация HTTP-клиента](./reference/canons/http-client.md) - опции и хуки `HttpClient`: авторизация, refresh token, транспорт.
+- [SDK-пакет REST-клиента](./reference/canons/sdk.md) - вынос generated-клиента в npm-пакет или пакет монорепозитория `{name}-rest-api-sdk`.
+- [Ручное создание](./reference/canons/manual.md) - ручной REST-клиент, если OpenAPI нет или он неполный.
+
+### Включённые Разделы
+
+- [Использование REST-клиента](#использование-rest-клиента) - прямой вызов готового клиента.
+- [GET-хуки REST-клиента](#get-хуки-rest-клиента) - контракт `useGet*`, key-функций и SWR-обёрток.
+
+## Использование REST-клиента
+
+Как выбрать правильную точку вызова REST API.
+
+### Прямой вызов полного API
+
+Для server code, submit-функции, adapter или сервиса импортируйте полный API-клиент из публичного API REST-модуля.
+
+```ts
+import { petStoreRestApi } from 'infra/pet-store-rest-api'
+
+export const getPet = async (petId: number) => {
+ return petStoreRestApi.pet.getPetById({ petId })
+}
+```
+
+Внешний код не импортирует SDK-пакет, `generated/`, `operations/`, `operationsTree`, `client.ts` или `rest-api.ts` напрямую.
+
+### Minimal Client В Composition Boundary
+
+Если business composition нужен компактный клиент из нескольких операций, собирайте его в boundary-файле этой composition.
+
+```ts
+// src/compositions/business/pet-store/orders/pet-store-rest-api.ts
+import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
+import { createOrder } from '@company/pet-store-rest-api-sdk/operations/create-order'
+import { getOrder } from '@company/pet-store-rest-api-sdk/operations/get-order'
+import { petStoreHttpClient } from 'infra/pet-store-rest-api'
+
+export const petStoreOrdersRestApi = createApiClient(petStoreHttpClient, {
+ orders: {
+ create: createOrder,
+ get: getOrder,
+ },
+})
+```
+
+Minimal client не экспортируется из общего `infra/{name}-rest-api`. Он принадлежит конкретному feature/composition boundary и содержит только операции этого сценария.
+
+### GET В Client Components
+
+Client Components используют только готовые `useGet*` hooks REST-модуля.
+
+```tsx
+import { useGetPetDetail } from 'infra/pet-store-rest-api'
+
+export const PetCard = ({ petId }: { petId: number }) => {
+ const { data: pet } = useGetPetDetail({ petId })
+
+ return
{pet?.name}
+}
+```
+
+Не вызывайте `useSWR`, SDK operation или полный `*RestApi` прямо в UI-компоненте.
+
+## GET-хуки REST-клиента
+
+Прозрачные SWR-обёртки над GET operations REST-клиента.
+
+### Зачем нужны
+
+GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую.
+
+### Где лежат
+
+GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
+
+```text
+src/infra/
+└── pet-store-rest-api/
+ ├── client.ts
+ ├── rest-api.ts
+ ├── generated/
+ ├── hooks/
+ │ ├── lib/
+ │ │ └── create-query-string.ts
+ │ ├── use-get-pet-list.hook.ts
+ │ ├── use-get-pet-detail.hook.ts
+ │ └── index.ts
+ ├── types/
+ └── index.ts
+```
+
+### Контракт
+
+- Один GET-хук = одна GET operation.
+- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
+- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
+- Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration`.
+- Для GET operation без параметров хук принимает только `config?: SWRConfiguration`.
+- Key-функция принимает те же `params`, что и хук.
+- Key-функция возвращает `null`, если обязательные параметры не готовы.
+- Проверка готовности запроса живёт в key-функции, а не в теле хука.
+- Хук вызывает `useSWR` один раз и безусловно.
+- Fetcher вызывает точечную operation через общий `*HttpClient`: `operation(nameHttpClient, params, requestParams?)`.
+- Fetcher не проверяет `null`, не бросает ошибку и не вызывает operation с `null`.
+- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
+- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
+- Хук не объединяет несколько запросов.
+- Хук не маппит DTO в доменную модель.
+- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
+- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
+- Хук не импортирует полный `*RestApi`, `createApiClient` или `operationsTree`.
+- `hooks/index.ts` содержит `'use client'`; отдельные `use-get-*.hook.ts` не содержат эту директиву.
+
+### Формат SWR-ключа
+
+SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией.
+
+Формат ключа:
+
+```ts
+['pet-store-rest-api', '/pet/10'] as const
+```
+
+- Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`.
+- Второй элемент — endpoint запроса: path и query string.
+- Key-функция возвращает `null`, когда запрос нельзя выполнять.
+- Key-функция нужна и GET-хуку, и `SWRConfig fallback`.
+- Не используйте произвольные части вроде `['pet-store-rest-api', 'pet', 'detail', params]`.
+- Не используйте только строку endpoint без имени сервиса.
+
+Примеры ключей:
+
+```ts
+export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
+ if (!params?.petId) {
+ return null
+ }
+
+ return ['pet-store-rest-api', `/pet/${params.petId}`] as const
+}
+```
+
+```ts
+export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
+ if (!params?.status) {
+ return null
+ }
+
+ return ['pet-store-rest-api', `/pet/findByStatus?status=${params.status}`] as const
+}
+```
+
+```ts
+export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => {
+ if (!params?.tags.length) {
+ return null
+ }
+
+ return ['pet-store-rest-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const
+}
+```
+
+Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно.
+
+### Query String Для Key
+
+Если key зависит от query-параметров, собирайте query string отдельной маленькой функцией или общим helper внутри `hooks/lib/`.
+
+```ts
+// src/infra/pet-store-rest-api/hooks/lib/create-query-string.ts
+type QueryValue = boolean | number | string | null | undefined
+
+export const createQueryString = (query: Record): string => {
+ const searchParams = new URLSearchParams()
+
+ Object.entries(query).forEach(([key, value]) => {
+ if (value === null || value === undefined || value === '') {
+ return
+ }
+
+ searchParams.set(key, String(value))
+ })
+
+ const search = searchParams.toString()
+
+ return search ? `?${search}` : ''
+}
+```
+
+Key должен отражать фактический URL запроса: path плюс query string. Не кладите весь `params` object в SWR key.
+
+### Пример списка
+
+```ts
+// src/infra/pet-store-rest-api/hooks/use-get-pet-list.hook.ts
+import { findPetsByStatus } from '../generated/operations/find-pets-by-status'
+import type { SWRConfiguration } from 'swr'
+import useSWR from 'swr'
+import { petStoreHttpClient } from '../client'
+import { createQueryString } from './lib/create-query-string'
+import type { FindPetsByStatusParams, Pet } from '../generated'
+
+const getPetListQuery = (params: FindPetsByStatusParams): string => {
+ return createQueryString({ status: params.status })
+}
+
+export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
+ if (!params?.status) {
+ return null
+ }
+
+ return ['pet-store-rest-api', `/pet/findByStatus${getPetListQuery(params)}`] as const
+}
+
+/**
+ * Получает список питомцев по статусу.
+ */
+export const useGetPetList = (
+ params?: FindPetsByStatusParams | null,
+ config?: SWRConfiguration,
+) => {
+ const key = getPetListKey(params)
+ const fetcher = () => findPetsByStatus(
+ petStoreHttpClient,
+ params as FindPetsByStatusParams,
+ )
+
+ return useSWR(key, fetcher, config)
+}
+```
+
+`params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher.
+
+### Пример detail-запроса
+
+```ts
+// src/infra/pet-store-rest-api/hooks/use-get-pet-detail.hook.ts
+import { getPetById } from '../generated/operations/get-pet-by-id'
+import type { SWRConfiguration } from 'swr'
+import useSWR from 'swr'
+import { petStoreHttpClient } from '../client'
+import type { GetPetByIdParams, Pet } from '../generated'
+
+export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
+ if (!params?.petId) {
+ return null
+ }
+
+ return ['pet-store-rest-api', `/pet/${params.petId}`] as const
+}
+
+/**
+ * Получает детальную карточку питомца с кешированием результата.
+ */
+export const useGetPetDetail = (
+ params?: GetPetByIdParams | null,
+ config?: SWRConfiguration,
+) => {
+ const key = getPetDetailKey(params)
+ const fetcher = () => getPetById(petStoreHttpClient, params as GetPetByIdParams)
+
+ return useSWR(key, fetcher, config)
+}
+```
+
+### Пример без параметров
+
+```ts
+// src/infra/pet-store-rest-api/hooks/use-get-store-inventory.hook.ts
+import { getStoreInventory } from '../generated/operations/get-store-inventory'
+import type { SWRConfiguration } from 'swr'
+import useSWR from 'swr'
+import { petStoreHttpClient } from '../client'
+import type { StoreInventory } from '../types'
+
+export const getStoreInventoryKey = () => {
+ return ['pet-store-rest-api', '/store/inventory'] as const
+}
+
+/**
+ * Получает инвентарь магазина.
+ */
+export const useGetStoreInventory = (
+ config?: SWRConfiguration,
+) => {
+ return useSWR(
+ getStoreInventoryKey(),
+ () => getStoreInventory(petStoreHttpClient),
+ config,
+ )
+}
+```
+
+Если generated operation возвращает безымянный тип вроде `Record`, а тип нужен наружу, вынесите его в `types/`.
+
+### Пример С Request Params
+
+Если operation зависит от разовых headers или дополнительных query-параметров, соберите `requestParams` внутри fetcher. Key-функция должна учитывать параметры, которые меняют результат запроса.
+
+```ts
+// src/infra/cms-rest-api/hooks/use-get-post-detail.hook.ts
+import { postsDetail } from '@company/cms-rest-api-sdk/operations/posts-detail'
+import type { SWRConfiguration } from 'swr'
+import useSWR from 'swr'
+import { cmsHttpClient } from '../client'
+import { createQueryString } from './lib/create-query-string'
+import type {
+ PostDetail,
+ PostsDetailParams,
+ RequestParams,
+} from '@company/cms-rest-api-sdk'
+
+export type GetPostDetailParams = PostsDetailParams & {
+ app?: string
+}
+
+const getPostDetailPath = (params: GetPostDetailParams): string => {
+ return `/v1/posts/${params.slug}${createQueryString({
+ app: params.app,
+ status: params.status,
+ })}`
+}
+
+export const getPostDetailKey = (params?: GetPostDetailParams | null) => {
+ if (!params?.slug) {
+ return null
+ }
+
+ return ['cms-rest-api', getPostDetailPath(params)] as const
+}
+
+export const useGetPostDetail = (
+ params?: GetPostDetailParams | null,
+ config?: SWRConfiguration,
+) => {
+ const key = getPostDetailKey(params)
+ const fetcher = () => {
+ const { app, ...postParams } = params as GetPostDetailParams
+ const requestParams: RequestParams = {
+ headers: app ? { 'x-app': app } : undefined,
+ }
+
+ return postsDetail(cmsHttpClient, postParams, requestParams)
+ }
+
+ return useSWR(key, fetcher, config)
+}
+```
+
+### Отложенный запрос
+
+GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
+
+```ts
+const key = getPetDetailKey(params)
+```
+
+Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа.
+
+Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`.
+
+### Экспорт
+
+```ts
+// src/infra/pet-store-rest-api/hooks/index.ts
+'use client'
+
+export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
+export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
+export {
+ getStoreInventoryKey,
+ useGetStoreInventory,
+} from './use-get-store-inventory.hook'
+```
+
+```ts
+// src/infra/pet-store-rest-api/index.ts
+export { petStoreHttpClient } from './client'
+export { petStoreRestApi } from './rest-api'
+export type {
+ FindPetsByStatusParams,
+ GetPetByIdParams,
+ Pet,
+} from './generated'
+export * from './hooks'
+export type { StoreInventory } from './types'
+```
+
+Наружу импортируют только из `infra/pet-store-rest-api`, не из `generated/` и не из `hooks/` напрямую.
+
+### Где заканчивается infra
+
+```ts
+// Хорошо: infra, прозрачный GET-хук
+const { data: pets } = useGetPetList({ status: 'available' })
+```
+
+```ts
+// Хорошо: business, доменная интерпретация
+export const useAvailablePets = () => {
+ const query = useGetPetList({ status: 'available' })
+
+ return {
+ ...query,
+ hasPets: Boolean(query.data?.length),
+ }
+}
+```
+
+`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
+
+### Что запрещено
+
+```ts
+// Плохо — useSWR в компоненте
+const { data } = useSWR(
+ ['pet-store-rest-api', '/pet/findByStatus?status=available'],
+ () => findPetsByStatus(petStoreHttpClient, { status: 'available' }),
+)
+
+// Плохо — проверка готовности размазана по хуку
+export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
+ const key = params?.petId ? getPetDetailKey(params) : null
+ const fetcher = () => {
+ if (!params?.petId) {
+ throw new Error('Pet id is required')
+ }
+
+ return getPetById(petStoreHttpClient, params)
+ }
+
+ return useSWR(key, fetcher)
+}
+
+// Плохо — условный вызов useSWR нарушает rules of hooks
+export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
+ const key = getPetDetailKey(params)
+
+ if (key === null) {
+ return useSWR(null, null)
+ }
+
+ return useSWR(key, () => getPetById(petStoreHttpClient, params))
+}
+
+// Плохо — GET-хук импортирует полный bound client вместо точечной operation
+export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
+ const key = getPetDetailKey(params)
+
+ return useSWR(
+ key,
+ () => petStoreRestApi.pet.getPetById(params as GetPetByIdParams),
+ )
+}
+
+// Плохо — несколько GET внутри infra-хука
+export const usePetDashboard = () => {
+ const available = useGetPetList({ status: 'available' })
+ const sold = useGetPetList({ status: 'sold' })
+
+ return { available, sold }
+}
+
+// Плохо — бизнес-флаг внутри GET-хука REST-клиента
+export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
+ const query = useSWR(...)
+
+ return {
+ ...query,
+ hasPets: Boolean(query.data?.length),
+ }
+}
+```
+
+Потребление таких хуков на уровне route-level data fetching относится к `nextjs-style-guide`.
diff --git a/.opencode/skills/rest-client/agents/openai.yaml b/.opencode/skills/rest-client/agents/openai.yaml
new file mode 100644
index 0000000..99303dd
--- /dev/null
+++ b/.opencode/skills/rest-client/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "REST Client"
+ short_description: "Generated и manual REST-клиенты проекта"
+ default_prompt: "Use $rest-client to generate, update, or review REST clients and their integration points in the monorepo."
diff --git a/.opencode/skills/rest-client/reference/canons/auto.md b/.opencode/skills/rest-client/reference/canons/auto.md
new file mode 100644
index 0000000..7a8cf7a
--- /dev/null
+++ b/.opencode/skills/rest-client/reference/canons/auto.md
@@ -0,0 +1,342 @@
+---
+title: Автогенерация REST-клиента
+description: Генерация split REST-клиента из OpenAPI-спецификации.
+keywords: [rest, openapi, api-codegen, автогенерация, generated, split, operations, npx]
+---
+
+# Автогенерация REST-клиента
+
+Генерация REST-клиента из OpenAPI-спецификации.
+
+## Когда использовать
+
+Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт split-клиент: HTTP-клиент, типы и отдельную operation-функцию на каждый endpoint. Разработчик вручную добавляет транспорт в `client.ts`, полный API в `rest-api.ts` и GET-хуки.
+
+По умолчанию генерация идёт внутрь infra-модуля приложения в `{name}-rest-api/generated` — этот сценарий описан ниже. Если клиент нужен нескольким приложениям, generated-код выносится в отдельный пакет `{name}-rest-api-sdk`: [SDK-пакет REST-клиента](./sdk.md).
+
+## Пример API
+
+В примерах используется Swagger Petstore:
+
+```text
+https://petstore3.swagger.io/api/v3/openapi.json
+```
+
+Имена модуля:
+
+```text
+src/infra/pet-store-rest-api/
+petStoreRestApi
+```
+
+## Скрипт генерации
+
+`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию.
+
+```json
+{
+ "scripts": {
+ "codegen:pet-store-rest-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infra/pet-store-rest-api/generated"
+ }
+}
+```
+
+Параметры:
+
+- `-i` — путь к OpenAPI-спецификации: URL или локальный файл.
+- `-o` — директория для сгенерированного split-клиента.
+
+По умолчанию генератор работает в режиме `split`. Legacy-режим `--mode single -n <имя>` генерирует один монолитный файл и используется только в старых проектах, которые уже завязаны на монолитный generated-клиент. В новых проектах single-режим не используется.
+
+Генератор не создаёт SWR-хуки. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = одна GET operation, без бизнес-логики и композиции.
+
+## Генерация
+
+```bash
+npm run codegen:pet-store-rest-api
+```
+
+Ожидаемый результат:
+
+```text
+src/infra/pet-store-rest-api/generated/
+├── create-api-client.ts
+├── data-contracts.ts
+├── http-client.ts
+├── index.ts
+├── operations-tree.ts
+└── operations/
+ ├── index.ts
+ ├── get-pet-by-id.ts
+ └── find-pets-by-status.ts
+```
+
+Основные части:
+
+- `http-client.ts` — fetch-based `HttpClient`: `baseUrl`, заголовки, авторизация и хуки транспорта.
+- `data-contracts.ts` — TypeScript-типы из OpenAPI schemas, включая `*Params`-типы операций.
+- `operations/*.ts` — отдельная typed operation-функция на каждый endpoint.
+- `operations-tree.ts` — дерево всех операций для сборки полного клиента.
+- `create-api-client.ts` — `createApiClient`, который привязывает дерево операций к `HttpClient`.
+- `index.ts` — входная точка generated-кода, реэкспортирует всё перечисленное.
+
+Файлы в `generated/` не правятся руками и коммитятся в репозиторий.
+
+Enum-значения в split-режиме генерируются как union-типы строковых литералов. Отдельных runtime enum в `generated/` нет: в коде используются строковые литералы вроде `'available'`, а тип проверяет их допустимость.
+
+## Проверка операций
+
+После генерации откройте `generated/operations-tree.ts` и проверьте фактические имена operation-функций и структуру дерева.
+
+Для Petstore нужны GET-операции вида:
+
+```ts
+import { findPetsByStatus } from './generated/operations/find-pets-by-status'
+import { getPetById } from './generated/operations/get-pet-by-id'
+```
+
+Точечная operation вызывается через настроенный `HttpClient`:
+
+```ts
+getPetById(petStoreHttpClient, { petId: 10 })
+findPetsByStatus(petStoreHttpClient, { status: 'available' })
+```
+
+После сборки полного клиента в `rest-api.ts` те же операции доступны как bound API:
+
+```ts
+petStoreRestApi.pet.findPetsByStatus({ status: 'available' })
+petStoreRestApi.pet.getPetById({ petId: 10 })
+```
+
+Имена операций и группировка дерева зависят от `operationId` и тегов OpenAPI-схемы. В рабочих задачах всегда сверяйтесь с `generated/operations-tree.ts` и `generated/data-contracts.ts`.
+
+## Источник Imports
+
+Если generated-код лежит внутри infra-модуля, импортируйте operation из локального `generated/`:
+
+```ts
+import { getPetById } from '../generated/operations/get-pet-by-id'
+```
+
+Если generated-код вынесен в SDK-пакет, импортируйте operation через subpath export пакета:
+
+```ts
+import { getPetById } from '@company/pet-store-rest-api-sdk/operations/get-pet-by-id'
+```
+
+Не импортируйте весь `operations` namespace ради одной операции.
+
+## Алгоритм для агента
+
+После генерации агент должен действовать по шагам:
+
+1. Открыть `generated/operations-tree.ts` и найти фактические имена нужных operation-функций.
+2. Для каждой нужной операции найти тип параметров и тип ответа в `generated/data-contracts.ts`.
+3. Создать или обновить `client.ts`: настроить и экспортировать только `*HttpClient`.
+4. Создать или обновить `rest-api.ts`: собрать полный `*RestApi` через `createApiClient(*HttpClient, operationsTree)`.
+5. Создать GET-хуки только для реально нужных GET-операций, не для всех операций API на всякий случай.
+6. В каждом GET-хуке импортировать точечную operation из `operations/`.
+7. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`.
+8. В key-функции вернуть `null`, если обязательные параметры не готовы.
+9. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration`.
+10. В fetcher вызвать operation через общий `*HttpClient`: `operation(nameHttpClient, params as GeneratedParams, requestParams?)`.
+11. Экспортировать хук и key-функцию из `hooks/index.ts`; если это первый hook, добавить в `hooks/index.ts` директиву `'use client'`.
+12. Экспортировать наружу только нужные generated-типы, DTO, `*HttpClient`, `*RestApi` и `hooks` через корневой `index.ts`.
+
+Что агент не должен делать:
+
+- Не править файлы в `generated/` руками.
+- Не импортировать операции и `HttpClient` из `generated/` или SDK в UI/components.
+- Не импортировать `operationsTree` или полный `*RestApi` в GET-хуки.
+- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE.
+- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук.
+- Не создавать словари enum-маппинга внутри GET-хука.
+- Не объявлять DTO и response-типы в файле хука.
+- Не вызывать `useSWR` условно.
+- Не добавлять `throw` в fetcher для неготовых params.
+
+## `client.ts`
+
+`client.ts` содержит только настроенный транспортный `HttpClient`.
+
+```ts
+// src/infra/pet-store-rest-api/client.ts
+import { HttpClient } from './generated'
+
+export const petStoreHttpClient = new HttpClient({
+ baseUrl: 'https://example.com/api',
+})
+```
+
+`client.ts` не содержит расширения типов, `declare module`, `Extended`-типы, GET-хуки, `operationsTree`, `createApiClient` и бизнес-логику.
+
+Авторизация, refresh token, логирование и другие настройки транспорта задаются опциями и хуками `HttpClient` в этом же файле: [Кастомизация HTTP-клиента](./http-client.md).
+
+## `rest-api.ts`
+
+`rest-api.ts` собирает полный bound API-клиент из всего generated дерева операций.
+
+```ts
+// src/infra/pet-store-rest-api/rest-api.ts
+import { createApiClient, operationsTree } from './generated'
+import { petStoreHttpClient } from './client'
+
+export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
+```
+
+Импорт `operationsTree` означает полный клиент: в bound API доступны все операции API.
+
+Если generated-код вынесен в SDK-пакет, используйте subpath exports:
+
+```ts
+// src/infra/pet-store-rest-api/rest-api.ts
+import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
+import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
+import { petStoreHttpClient } from './client'
+
+export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
+```
+
+## GET-хуки
+
+GET-хуки пишутся вручную после проверки generated-операций.
+
+Пример для операции `getPetById`:
+
+```ts
+// src/infra/pet-store-rest-api/hooks/use-get-pet-detail.hook.ts
+import { getPetById } from '../generated/operations/get-pet-by-id'
+import type { SWRConfiguration } from 'swr'
+import useSWR from 'swr'
+import { petStoreHttpClient } from '../client'
+import type { GetPetByIdParams, Pet } from '../generated'
+
+export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
+ if (!params?.petId) {
+ return null
+ }
+
+ return ['pet-store-rest-api', `/pet/${params.petId}`] as const
+}
+
+/**
+ * Получает детальную карточку питомца с кешированием результата.
+ */
+export const useGetPetDetail = (
+ params?: GetPetByIdParams | null,
+ config?: SWRConfiguration,
+) => {
+ const key = getPetDetailKey(params)
+ const fetcher = () => getPetById(petStoreHttpClient, params as GetPetByIdParams)
+
+ return useSWR(key, fetcher, config)
+}
+```
+
+Типы импортируются как `import type` из `./generated`: generated `index.ts` реэкспортирует все типы из `data-contracts.ts`.
+
+Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
+
+## Расширение сгенерированных типов
+
+Файлы в `generated/` не правятся руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
+
+```text
+src/infra/biocad-less-rest-api/
+├── generated/
+│ ├── data-contracts.ts
+│ ├── operations/
+│ └── index.ts
+├── types/
+│ ├── term.ts
+│ └── index.ts
+├── client.ts
+├── rest-api.ts
+└── index.ts
+```
+
+Пример расширения generated-типа:
+
+```ts
+// src/infra/biocad-less-rest-api/types/term.ts
+import type { TermRecordItem } from '../generated/data-contracts'
+
+declare module '../generated/data-contracts' {
+ interface TermRecordItem {
+ media?: {
+ file?: string
+ title?: string
+ url?: string
+ }
+ }
+}
+
+export type TermRecordItemExtended = Omit<
+ TermRecordItem,
+ 'categories' | 'tags' | 'fields'
+> & {
+ categories?: Array<{
+ _id?: string
+ id?: string
+ slug?: string
+ name?: string
+ }>
+ tags?: Array<{
+ _id?: string
+ id?: string
+ slug?: string
+ name?: string
+ }>
+ fields?: Record
+}
+```
+
+```ts
+// src/infra/biocad-less-rest-api/types/index.ts
+export type { TermRecordItemExtended } from './term'
+```
+
+`declare module` нацеливается на `../generated/data-contracts` — именно там живут generated-интерфейсы. Он используется для добавления отсутствующих полей. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файлы.
+
+## Публичный API
+
+```ts
+// src/infra/pet-store-rest-api/index.ts
+export { petStoreHttpClient } from './client'
+export { petStoreRestApi } from './rest-api'
+export type {
+ FindPetsByStatusParams,
+ GetPetByIdParams,
+ Pet,
+} from './generated'
+export * from './hooks'
+```
+
+Наружу импортируют только из `infra/pet-store-rest-api`, не из `generated/`.
+
+Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
+
+```ts
+// src/infra/biocad-less-rest-api/index.ts
+export type { TermRecordItemExtended } from './types'
+```
+
+## Регенерация
+
+При изменении OpenAPI-схемы:
+
+```bash
+npm run codegen:pet-store-rest-api
+```
+
+Что меняется:
+
+- Папка `generated/` — перезаписывается генератором целиком.
+- `client.ts`, `rest-api.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически.
+
+Если после регенерации поменялись имена операций, сигнатуры или типы, это исправляется в ручном коде модуля: `rest-api.ts`, GET-хуки, minimal clients и реэкспорты.
+
+## Следующий шаг
+
+После генерации настройте транспорт в `client.ts` по разделу [Кастомизация HTTP-клиента](./http-client.md), соберите полный API в `rest-api.ts`, затем проверьте [использование REST-клиента](../../SKILL.md#использование-rest-клиента) или добавьте [GET-хук REST-клиента](../../SKILL.md#get-хуки-rest-клиента) для Client Components.
diff --git a/.opencode/skills/rest-client/reference/canons/http-client.md b/.opencode/skills/rest-client/reference/canons/http-client.md
new file mode 100644
index 0000000..eb4d91d
--- /dev/null
+++ b/.opencode/skills/rest-client/reference/canons/http-client.md
@@ -0,0 +1,168 @@
+---
+title: Кастомизация HTTP-клиента
+description: Настройка транспорта REST-клиента через опции и хуки HttpClient.
+keywords: [rest, http, транспорт, авторизация, jwt, refresh token, onRequest, onError]
+---
+
+# Кастомизация HTTP-клиента
+
+Настройка транспорта REST-клиента через опции и хуки `HttpClient`.
+
+## Где живёт кастомизация
+
+Вся настройка транспорта — `baseUrl`, заголовки, авторизация, retry — задаётся в `client.ts` REST-модуля при создании `HttpClient`.
+
+Не размещайте авторизацию, обработку 401 и логирование в компонентах, GET-хуках или обёртках над операциями: у транспорта одна точка настройки.
+
+В generated/SDK-сценарии `HttpClient` импортируется из generated-кода или SDK-пакета. В ручном сценарии без OpenAPI `HttpClient` импортируется из runtime-зависимости `@gromlab/api-codegen`.
+
+```ts
+import { HttpClient } from '@gromlab/api-codegen'
+```
+
+## Опции HttpClient
+
+`HttpClient` принимает плоский конфиг: стандартные `fetch`-опции задаются вместе с хуками клиента.
+
+| Опция | Назначение |
+| --- | --- |
+| `baseUrl` | Базовый URL API. |
+| `headers` | Заголовки по умолчанию для всех запросов. |
+| `credentials` | Политика отправки cookies: `omit`, `same-origin`, `include`. |
+| `timeout` | Таймаут запроса в миллисекундах, работает через `AbortSignal`. |
+| `customFetch` | Замена стандартного `fetch`: тесты, SSR, custom transport. |
+| `paramsSerializer` | Кастомная сериализация query params в URL. |
+| `responseParser` | Кастомный парсинг response body. |
+| `onRequest` | Request-хук перед вызовом `fetch`. |
+| `onResponse` | Response-хук после успешного HTTP-ответа. |
+| `onError` | Error-хук для HTTP-ошибок, network errors и ошибок парсинга. |
+
+Полный список опций — в README `@gromlab/api-codegen`.
+
+## Контракт хуков
+
+- `onRequest(params, context)` вызывается перед `fetch` и возвращает изменённые `params`.
+- `onResponse(response, context)` вызывается после успешного ответа и возвращает `response`.
+- `onError(error, context)` вызывается для HTTP-ошибок, network errors и ошибок парсинга.
+- `context` содержит `url`, `request`, `retryCount` и `retry()` — повтор текущего запроса.
+- `onError` должен либо бросить ошибку, либо вернуть fallback-значение, либо вернуть результат `context.retry()`. Если вернуть `undefined`, ошибка будет считаться обработанной, а вызывающий код получит `undefined` вместо исключения.
+- Для защищённых endpoints generated operation передаёт `secure: true`, поэтому авторизацию можно добавлять только там, где она нужна.
+
+## JWT-авторизация
+
+Токен добавляется в `onRequest` только для защищённых запросов и не перезаписывает явно переданный `Authorization`.
+
+```ts
+// src/infra/pet-store-rest-api/client.ts
+export const petStoreHttpClient = new HttpClient({
+ baseUrl: 'https://example.com/api',
+ onRequest: (params) => {
+ const token = localStorage.getItem('access_token')
+
+ if (!params.secure || !token) {
+ return params
+ }
+
+ const headers = new Headers(params.headers)
+
+ if (!headers.has('Authorization')) {
+ headers.set('Authorization', `Bearer ${token}`)
+ }
+
+ return {
+ ...params,
+ headers,
+ }
+ },
+})
+```
+
+## Refresh token
+
+Обновление токена и повтор запроса выполняются в `onError` через `context.retry()`. `context.retryCount` защищает от бесконечного цикла повторов.
+
+```ts
+// src/infra/pet-store-rest-api/client.ts
+import { ApiError, HttpClient } from './generated'
+
+export const petStoreHttpClient = new HttpClient({
+ baseUrl: 'https://example.com/api',
+ onError: async (error, context) => {
+ if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
+ await refreshToken()
+ return context.retry()
+ }
+
+ throw error
+ },
+})
+```
+
+`ApiError` экспортируется из `generated/` и содержит `status`, `statusText`, `response`, `data` и исходный `request`.
+
+## Логирование
+
+```ts
+const petStoreHttpClient = new HttpClient({
+ baseUrl: 'https://example.com/api',
+ onResponse: (response, context) => {
+ console.log(context.request.method, context.url, response.status)
+ return response
+ },
+})
+```
+
+## Сериализация query params
+
+```ts
+const petStoreHttpClient = new HttpClient({
+ baseUrl: 'https://example.com/api',
+ paramsSerializer: (query) => {
+ const params = new URLSearchParams()
+
+ Object.entries(query).forEach(([key, value]) => {
+ if (Array.isArray(value)) {
+ params.set(key, value.join(','))
+ return
+ }
+
+ if (value !== undefined) {
+ params.set(key, String(value))
+ }
+ })
+
+ return params.toString()
+ },
+})
+```
+
+## Параметры одного вызова
+
+Разовые настройки запроса не относятся к `HttpClient`. Они передаются последним аргументом operation:
+
+```ts
+import { getPetById } from './generated/operations/get-pet-by-id'
+import { petStoreHttpClient } from './client'
+
+await getPetById(
+ petStoreHttpClient,
+ { petId },
+ {
+ headers: {
+ 'X-Request-Id': requestId,
+ },
+ },
+)
+```
+
+## Правила
+
+- Кастомизация транспорта живёт только в `client.ts` REST-модуля.
+- Авторизация добавляется в `onRequest` с учётом `params.secure` и без перезаписи явного `Authorization`.
+- `onError` либо бросает ошибку, либо возвращает fallback или `context.retry()`; молчаливый `return` запрещён.
+- Повторы запроса ограничиваются проверкой `context.retryCount`.
+- Бизнес-реакции на ошибки — тосты, редиректы, UI-состояние — не размещаются в хуках `HttpClient`.
+
+## Следующий шаг
+
+После настройки транспорта проверьте [использование REST-клиента](../../SKILL.md#использование-rest-клиента) или добавьте [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
diff --git a/.opencode/skills/rest-client/reference/canons/manual.md b/.opencode/skills/rest-client/reference/canons/manual.md
new file mode 100644
index 0000000..ffba3f0
--- /dev/null
+++ b/.opencode/skills/rest-client/reference/canons/manual.md
@@ -0,0 +1,305 @@
+---
+title: Ручное создание REST-клиента
+description: Создание generated-style REST-клиента вручную, когда OpenAPI нет или он неполный.
+keywords: [rest, ручной клиент, api-codegen, operation, ApiRequestClient, RequestParams, ContentType]
+---
+
+# Ручное создание REST-клиента
+
+Ручной клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
+
+Ручной режим не является отдельной архитектурой. Это тот же generated-style клиент, где operation-функции написаны вручную вместо генерации из OpenAPI.
+
+## Зависимость
+
+Для ручного клиента установите `@gromlab/api-codegen@5.1.0+` как runtime-зависимость проекта.
+
+```bash
+bun add @gromlab/api-codegen
+```
+
+Не используйте `npx @gromlab/api-codegen` для ручного режима: `npx` нужен для генерации SDK из OpenAPI, а ручной клиент импортирует runtime API пакета напрямую.
+
+## Что нужно создать
+
+```text
+src/infra/
+└── pet-project-rest-api/
+ ├── client.ts
+ ├── rest-api.ts
+ ├── operations-tree.ts
+ ├── operations/
+ │ ├── posts-create.ts
+ │ ├── posts-detail.ts
+ │ ├── posts-list.ts
+ │ └── index.ts
+ ├── hooks/
+ │ ├── lib/
+ │ │ └── create-query-string.ts
+ │ ├── use-get-post-list.hook.ts
+ │ └── index.ts
+ ├── types/
+ │ ├── post.ts
+ │ └── index.ts
+ └── index.ts
+```
+
+| Файл | Роль |
+|------|------|
+| `client.ts` | Настройка и экспорт `*HttpClient` |
+| `operations/` | Ручные operation-функции в стиле generated SDK |
+| `operations-tree.ts` | Ручное дерево операций для `createApiClient` |
+| `rest-api.ts` | Полный bound API через `createApiClient` |
+| `types/` | DTO запросов, ответов и именованные response-типы |
+| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components |
+| `index.ts` | Публичный API REST-модуля |
+
+## Типы API
+
+DTO запросов и ответов живут в `types/`. `client.ts` и operation-файлы не объявляют доменные типы.
+
+```ts
+// src/infra/pet-project-rest-api/types/post.ts
+export type PostDto = {
+ id: string
+ slug: string
+ title: string
+}
+
+export type PostListQueryDto = {
+ limit?: number
+ category?: string
+}
+
+export type CreatePostPayload = {
+ title: string
+}
+```
+
+```ts
+// src/infra/pet-project-rest-api/types/index.ts
+export type { CreatePostPayload, PostDto, PostListQueryDto } from './post'
+```
+
+Если данным нужен доменный смысл или маппинг DTO, делайте это выше, в `business/`, а не в REST-клиенте.
+
+## Operation-Функции
+
+Каждая ручная operation повторяет контракт generated operation:
+
+- первым аргументом принимает `http: ApiRequestClient`;
+- принимает typed params/body как последующие аргументы;
+- последним аргументом принимает `requestParams: RequestParams = {}`;
+- внутри вызывает только `http.request(...)`;
+- не использует прямой `fetch`;
+- не знает про React, SWR, UI и бизнес-логику.
+
+```ts
+// src/infra/pet-project-rest-api/operations/posts-list.ts
+import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
+import type { PostDto, PostListQueryDto } from '../types'
+
+export const postsList = (
+ http: ApiRequestClient,
+ query: PostListQueryDto = {},
+ requestParams: RequestParams = {},
+) =>
+ http.request({
+ path: '/posts',
+ method: 'GET',
+ query,
+ format: 'json',
+ ...requestParams,
+ })
+```
+
+```ts
+// src/infra/pet-project-rest-api/operations/posts-detail.ts
+import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
+import type { PostDto } from '../types'
+
+export type PostsDetailParams = {
+ slug: string
+}
+
+export const postsDetail = (
+ http: ApiRequestClient,
+ { slug }: PostsDetailParams,
+ requestParams: RequestParams = {},
+) =>
+ http.request({
+ path: `/posts/${slug}`,
+ method: 'GET',
+ format: 'json',
+ ...requestParams,
+ })
+```
+
+```ts
+// src/infra/pet-project-rest-api/operations/posts-create.ts
+import { ContentType } from '@gromlab/api-codegen'
+import type { ApiRequestClient, RequestParams } from '@gromlab/api-codegen'
+import type { CreatePostPayload, PostDto } from '../types'
+
+export const postsCreate = (
+ http: ApiRequestClient,
+ body: CreatePostPayload,
+ requestParams: RequestParams = {},
+) =>
+ http.request({
+ path: '/posts',
+ method: 'POST',
+ body,
+ type: ContentType.Json,
+ format: 'json',
+ secure: true,
+ ...requestParams,
+ })
+```
+
+```ts
+// src/infra/pet-project-rest-api/operations/index.ts
+export { postsCreate } from './posts-create'
+export { postsDetail } from './posts-detail'
+export { postsList } from './posts-list'
+export type { PostsDetailParams } from './posts-detail'
+```
+
+## Транспорт
+
+`client.ts` настраивает и экспортирует только `*HttpClient`.
+
+```ts
+// src/infra/pet-project-rest-api/client.ts
+import { HttpClient } from '@gromlab/api-codegen'
+
+export const petProjectHttpClient = new HttpClient({
+ baseUrl: 'https://example.com/api',
+})
+```
+
+Авторизация, refresh token, логирование и другие настройки транспорта задаются опциями и хуками `HttpClient` в этом же файле: [Кастомизация HTTP-клиента](./http-client.md).
+
+## Operations Tree
+
+`operations-tree.ts` вручную собирает дерево операций. Держите структуру такой, какой вы ожидаете видеть после будущей автогенерации.
+
+```ts
+// src/infra/pet-project-rest-api/operations-tree.ts
+import { postsCreate } from './operations/posts-create'
+import { postsDetail } from './operations/posts-detail'
+import { postsList } from './operations/posts-list'
+
+export const operationsTree = {
+ posts: {
+ create: postsCreate,
+ detail: postsDetail,
+ list: postsList,
+ },
+} as const
+```
+
+## Полный API-Клиент
+
+`rest-api.ts` собирает полный bound API через тот же `createApiClient`, что используется в generated SDK.
+
+```ts
+// src/infra/pet-project-rest-api/rest-api.ts
+import { createApiClient } from '@gromlab/api-codegen'
+import { petProjectHttpClient } from './client'
+import { operationsTree } from './operations-tree'
+
+export const petProjectRestApi = createApiClient(
+ petProjectHttpClient,
+ operationsTree,
+)
+```
+
+После binding внешний вызов совпадает с generated-клиентом:
+
+```ts
+await petProjectRestApi.posts.create({ title: 'Новый пост' })
+await petProjectRestApi.posts.detail({ slug: 'hello' })
+```
+
+## GET-Хуки
+
+GET-хуки ручного клиента пишутся так же, как hooks для generated operations: импортируют точечную operation и вызывают её через общий `*HttpClient`.
+
+```ts
+// src/infra/pet-project-rest-api/hooks/use-get-post-list.hook.ts
+import type { SWRConfiguration } from 'swr'
+import useSWR from 'swr'
+import { petProjectHttpClient } from '../client'
+import { postsList } from '../operations/posts-list'
+import type { PostDto, PostListQueryDto } from '../types'
+
+export const getPostListKey = (params: PostListQueryDto = {}) => {
+ const searchParams = new URLSearchParams()
+
+ if (params.limit !== undefined) {
+ searchParams.set('limit', String(params.limit))
+ }
+
+ if (params.category) {
+ searchParams.set('category', params.category)
+ }
+
+ const search = searchParams.toString()
+
+ return ['pet-project-rest-api', `/posts${search ? `?${search}` : ''}`] as const
+}
+
+export const useGetPostList = (
+ params: PostListQueryDto = {},
+ config?: SWRConfiguration,
+) => {
+ const key = getPostListKey(params)
+ const fetcher = () => postsList(petProjectHttpClient, params)
+
+ return useSWR(key, fetcher, config)
+}
+```
+
+```ts
+// src/infra/pet-project-rest-api/hooks/index.ts
+'use client'
+
+export { getPostListKey, useGetPostList } from './use-get-post-list.hook'
+```
+
+## Публичный API
+
+```ts
+// src/infra/pet-project-rest-api/index.ts
+export { petProjectHttpClient } from './client'
+export { petProjectRestApi } from './rest-api'
+export * from './hooks'
+export type { PostsDetailParams } from './operations'
+export type { CreatePostPayload, PostDto, PostListQueryDto } from './types'
+```
+
+Внешний код импортирует только из `infra/pet-project-rest-api`, не из внутренних файлов модуля.
+
+## Миграция На OpenAPI
+
+Ручной клиент проектируйте так, чтобы его можно было заменить автогенерацией без смены API потребителей.
+
+- Называйте operation-функции и дерево близко к будущим `operationId` и tags OpenAPI.
+- Держите сигнатуры operation в стиле generated SDK: `operation(http, params/body, requestParams?)`.
+- Не создавайте собственный класс клиента и методные фабрики.
+- Если позже появится OpenAPI, сгенерируйте SDK и замените ручные operations на generated operations с минимальными правками `rest-api.ts`, GET-хуков и re-exports.
+
+## Правила
+
+- Ручной клиент использует `@gromlab/api-codegen@5.1.0+` как runtime dependency.
+- `client.ts` экспортирует только `*HttpClient`.
+- Ручные запросы живут в `operations/` и пишутся как generated-style operations.
+- `operations-tree.ts` вручную собирает дерево для `createApiClient`.
+- `rest-api.ts` экспортирует полный `*RestApi` через `createApiClient(*HttpClient, operationsTree)`.
+- GET-хуки вызывают точечные operations через `*HttpClient`, не полный `*RestApi`.
+- Не используйте прямой `fetch`, custom `RestApiClient` class, `methods/*Methods(client)` и ручные `get/post` wrappers.
+- DTO запросов и ответов живут в `types/`.
+- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
+
+Следующий шаг: [Использование REST-клиента](../../SKILL.md#использование-rest-клиента), [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента) или выбор route-level data fetching по `nextjs-style-guide`.
diff --git a/.opencode/skills/rest-client/reference/canons/sdk.md b/.opencode/skills/rest-client/reference/canons/sdk.md
new file mode 100644
index 0000000..87a465a
--- /dev/null
+++ b/.opencode/skills/rest-client/reference/canons/sdk.md
@@ -0,0 +1,166 @@
+---
+title: SDK-пакет REST-клиента
+description: Вынос generated REST-клиента в npm-пакет или пакет монорепозитория.
+keywords: [rest, sdk, npm, монорепозиторий, api-codegen, generated, пакет]
+---
+
+# SDK-пакет REST-клиента
+
+Вынос generated REST-клиента в npm-пакет или пакет монорепозитория.
+
+## Когда выносить
+
+SDK-пакет нужен, когда один и тот же API используется несколькими приложениями: в монорепозитории или через публикацию в npm registry.
+
+Если API нужен одному приложению, SDK-пакет не создаётся — генерация идёт классически внутрь infra-модуля в `{name}-rest-api/generated` по разделу [Автогенерация из OpenAPI](./auto.md).
+
+## Нейминг
+
+SDK-пакет называется `{name}-rest-api-sdk`.
+
+```text
+pet-store-rest-api-sdk
+@company/pet-store-rest-api-sdk
+```
+
+Имя SDK-пакета образуется от имени infra-модуля: приложение с модулем `infra/pet-store-rest-api` потребляет пакет `pet-store-rest-api-sdk`.
+
+## Что содержит SDK
+
+SDK-пакет содержит только generated-код и `package.json` exports для точечных импортов. Это транспорт-нейтральная библиотека: она не знает про приложение, авторизацию и SWR конкретного проекта.
+
+В SDK не размещаются:
+
+- `client.ts` и `rest-api.ts` — настройка транспорта и bound API живут в приложении;
+- GET-хуки — SWR-обёртки живут в infra-модуле приложения;
+- бизнес-логика и DTO-маппинг.
+
+## Структура пакета
+
+```text
+packages/pet-store-rest-api-sdk/
+├── package.json
+└── src/
+ └── generated/
+```
+
+Скрипт генерации внутри пакета выводит split-клиент в `src/generated`:
+
+```json
+{
+ "scripts": {
+ "codegen": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/generated"
+ }
+}
+```
+
+`package.json` обязан открыть subpath exports для generated частей:
+
+```json
+{
+ "name": "@company/pet-store-rest-api-sdk",
+ "type": "module",
+ "exports": {
+ ".": "./src/generated/index.ts",
+ "./create-api-client": "./src/generated/create-api-client.ts",
+ "./data-contracts": "./src/generated/data-contracts.ts",
+ "./http-client": "./src/generated/http-client.ts",
+ "./operations": "./src/generated/operations/index.ts",
+ "./operations/*": "./src/generated/operations/*.ts",
+ "./operations-tree": "./src/generated/operations-tree.ts"
+ }
+}
+```
+
+Файлы в `src/generated/` не правятся руками и коммитятся в репозиторий пакета.
+
+Корневой `package.json` монорепозитория добавляет удобный script для запуска codegen через workspace filter:
+
+```json
+{
+ "scripts": {
+ "codegen:pet-store-rest-api-sdk": "dotenv -- pnpm --filter @company/pet-store-rest-api-sdk run codegen"
+ }
+}
+```
+
+## Потребление в приложении
+
+Приложение оформляет REST-модуль как обычно: `src/infra/{name}-rest-api/` с `client.ts`, `rest-api.ts`, `hooks/`, `types/` и корневым `index.ts`. Меняется только источник generated-кода: вместо локальной папки `generated/` импортируется SDK-пакет.
+
+```ts
+// src/infra/pet-store-rest-api/client.ts
+import { HttpClient } from '@company/pet-store-rest-api-sdk'
+
+export const petStoreHttpClient = new HttpClient({
+ baseUrl: 'https://example.com/api',
+})
+```
+
+```ts
+// src/infra/pet-store-rest-api/rest-api.ts
+import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
+import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
+import { petStoreHttpClient } from './client'
+
+export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
+```
+
+Типы в хуках и `types/` импортируются из пакета вместо `../generated`:
+
+```ts
+import type { GetPetByIdParams, Pet } from '@company/pet-store-rest-api-sdk'
+```
+
+GET-хуки импортируют точечные operations из SDK subpath:
+
+```ts
+import { getPetById } from '@company/pet-store-rest-api-sdk/operations/get-pet-by-id'
+```
+
+Остальной контракт модуля не меняется:
+
+- настройка транспорта — [Кастомизация HTTP-клиента](./http-client.md);
+- GET-хуки — [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента);
+- внешний код импортирует только из `infra/pet-store-rest-api`, не из SDK-пакета напрямую.
+
+## Minimal Client В Composition
+
+SDK operations можно импортировать напрямую в boundary-файлах feature/composition, если там собирается минимальный клиент для конкретного бизнес-сценария.
+
+```ts
+// src/compositions/business/pet-store/orders/pet-store-rest-api.ts
+import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
+import { createOrder } from '@company/pet-store-rest-api-sdk/operations/create-order'
+import { getOrder } from '@company/pet-store-rest-api-sdk/operations/get-order'
+import { petStoreHttpClient } from 'infra/pet-store-rest-api'
+
+export const petStoreOrdersRestApi = createApiClient(petStoreHttpClient, {
+ orders: {
+ create: createOrder,
+ get: getOrder,
+ },
+})
+```
+
+Это исключение действует только для boundary-файлов сборки клиента. UI/components, pages и произвольные helpers не импортируют SDK напрямую.
+
+## Регенерация
+
+При изменении OpenAPI-схемы перегенерируется `src/generated` внутри SDK-пакета:
+
+```bash
+npm run codegen
+```
+
+Приложения получают обновление через новую версию пакета. Если поменялись имена операций или типы, правки в приложении локализованы в `rest-api.ts`, GET-хуках, minimal clients и реэкспортах.
+
+## Правила
+
+- SDK-пакет называется `{name}-rest-api-sdk` и содержит только generated-код.
+- SDK-пакет обязан открыть subpath exports: `.`, `./operations/*`, `./create-api-client`, `./operations-tree`, `./http-client`, `./data-contracts`.
+- Генерация внутри пакета идёт в `src/generated`, файлы не правятся руками.
+- `client.ts`, `rest-api.ts`, авторизация и GET-хуки живут в infra-модуле приложения, не в SDK.
+- Приложение импортирует SDK внутри своего `infra/{name}-rest-api` модуля.
+- Boundary-файл feature/composition может импортировать SDK operations напрямую только для сборки minimal client.
+- UI/components не импортируют SDK напрямую.
diff --git a/.opencode/skills/rest-client/reference/canons/setup.md b/.opencode/skills/rest-client/reference/canons/setup.md
new file mode 100644
index 0000000..7556878
--- /dev/null
+++ b/.opencode/skills/rest-client/reference/canons/setup.md
@@ -0,0 +1,135 @@
+---
+title: Настройка REST-клиента
+description: Подготовка REST-клиента сервиса к использованию.
+keywords: [rest, клиент, infra, operation, openapi, get-хуки, swr]
+---
+
+# Настройка REST-клиента
+
+Подготовка REST-клиента сервиса к использованию.
+
+## Что настраиваем
+
+REST-клиент — это infra-модуль, через который проект работает с внешним REST API.
+
+На этапе настройки нужно подготовить транспортный HTTP-клиент, полный API-клиент и GET-хуки для клиентских компонентов.
+
+## Нейминг
+
+- infra-модуль REST-клиента называется `{name}-rest-api`: `src/infra/pet-store-rest-api/`.
+- Генерация внутри клиента — классический вариант — живёт в `{name}-rest-api/generated`.
+- Если generated-клиент выносится в npm-пакет или пакет монорепозитория, пакет называется `{name}-rest-api-sdk`: [SDK-пакет REST-клиента](./sdk.md).
+- Производные имена образуются от имени модуля: транспорт `petStoreHttpClient`, полный API-клиент `petStoreRestApi`, SWR-ключ `['pet-store-rest-api', ...]`.
+
+## Из чего состоит клиент
+
+REST-клиент состоит из четырёх основных частей:
+
+1. **Транспорт** — ручной `client.ts` с настроенным `*HttpClient`.
+2. **Полный API-клиент** — `rest-api.ts` с `createApiClient(*HttpClient, operationsTree)`.
+3. **Операции** — operation-функции, сгенерированные из OpenAPI, поставляемые SDK-пакетом или написанные вручную через `@gromlab/api-codegen`.
+4. **GET-хуки** — SWR-обёртки для GET-запросов.
+
+Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису.
+
+## Транспорт
+
+`client.ts` — ручной слой, который настраивает транспорт: `HttpClient`, заголовки, авторизацию и обработку ошибок.
+
+Авторизация, refresh token и другие транспортные сценарии настраиваются хуками `HttpClient` — `onRequest`, `onResponse`, `onError`: [Кастомизация HTTP-клиента](./http-client.md).
+
+Даже если операции генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта.
+
+`client.ts` экспортирует только настроенный `*HttpClient`. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки, `operationsTree`, `createApiClient` и бизнес-логика.
+
+```ts
+// src/infra/pet-store-rest-api/client.ts
+import { HttpClient } from '@company/pet-store-rest-api-sdk'
+
+export const petStoreHttpClient = new HttpClient({
+ baseUrl: 'https://example.com/api',
+})
+```
+
+## Полный API-клиент
+
+`rest-api.ts` собирает полный bound API-клиент из всего generated дерева операций.
+
+```ts
+// src/infra/pet-store-rest-api/rest-api.ts
+import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
+import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
+import { petStoreHttpClient } from './client'
+
+export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
+```
+
+Импортируйте `operationsTree` только в `rest-api.ts`. Для GET-хуков и minimal clients используйте точечные operation imports.
+
+## Операции
+
+Операции описывают конкретные запросы к API.
+
+Они появляются одним из трёх способов:
+
+- генерируются из OpenAPI в `generated/` как отдельные operation-функции;
+- поставляются SDK-пакетом `{name}-rest-api-sdk`;
+- создаются вручную в `operations/` через `@gromlab/api-codegen`, если OpenAPI нет или он неполный.
+
+Подробности:
+
+- [Автогенерация из OpenAPI](./auto.md)
+- [Ручное создание](./manual.md)
+
+## GET-хуки
+
+Для GET-запросов добавляются GET-хуки REST-клиента.
+
+Это прозрачные SWR-обёртки над generated GET operations. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
+
+GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
+
+Каждый GET-хук имеет экспортируемую key-функцию. SWR-ключ всегда имеет формат `[serviceName, endpoint]`: например `['pet-store-rest-api', '/pet/10']`.
+
+Хук принимает generated-параметры операции и SWR-настройки: `params?: GetPetByIdParams | null`, `config?: SWRConfiguration`.
+
+`hooks/index.ts` содержит `'use client'` и экспортирует все GET-хуки. Сами `use-get-*.hook.ts` остаются обычными файлами без директивы.
+
+Подробности:
+
+- [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента)
+
+## Структура модуля
+
+```text
+src/infra/{name}-rest-api/
+├── client.ts # настройка и экспорт *HttpClient
+├── rest-api.ts # полный *RestApi через operationsTree
+├── generated/ или operations/ # локальный split-клиент или ручные operations
+├── operations-tree.ts # ручное дерево операций, если нет generated/operations-tree.ts
+├── hooks/ # GET-хуки REST-клиента
+│ ├── lib/
+│ │ └── create-query-string.ts
+│ ├── use-get-*.hook.ts
+│ └── index.ts # 'use client' и публичные экспорты hooks
+├── types/ # DTO, именованные response-типы и расширения типов
+├── errors/ # ошибки API, если нужны
+└── index.ts # публичный API
+```
+
+`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
+
+Если generated-код вынесен в `{name}-rest-api-sdk`, локальной папки `generated/` внутри infra-модуля может не быть: `client.ts`, `rest-api.ts` и GET-хуки импортируют generated части из SDK subpath exports.
+
+Если OpenAPI нет, не создавайте самописный `fetch`-класс и `methods/`. Ручной клиент пишется тем же API, что generated-клиент: `HttpClient`, `ApiRequestClient`, `RequestParams`, operation-функции и `createApiClient` из `@gromlab/api-codegen`.
+
+Если generated operation возвращает безымянный тип вроде `Record`, а этот тип нужен снаружи, вынесите его в `types/`. Не объявляйте DTO внутри `hooks/use-get-*.hook.ts`.
+
+## Что делаем дальше
+
+1. Создайте операции клиента: [Автогенерация из OpenAPI](./auto.md), SDK-пакет или [Ручное создание](./manual.md).
+2. Если клиент нужен нескольким приложениям, вынесите generated-код в пакет: [SDK-пакет REST-клиента](./sdk.md).
+3. Настройте транспорт — авторизацию, хуки, таймауты: [Кастомизация HTTP-клиента](./http-client.md).
+4. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
+5. Проверьте прямые вызовы клиента: [Использование REST-клиента](../../SKILL.md#использование-rest-клиента).
+6. После настройки клиента выбирайте стратегию route-level data fetching по `nextjs-style-guide`.
diff --git a/.opencode/skills/slm-design/SKILL.md b/.opencode/skills/slm-design/SKILL.md
new file mode 100644
index 0000000..629ef3d
--- /dev/null
+++ b/.opencode/skills/slm-design/SKILL.md
@@ -0,0 +1,612 @@
+---
+name: slm-design
+description: "Используй при проектировании, реализации, миграции и архитектурном ревью по SLM: когда нужно определить SLM root, ответственность, владельца, слой, модульную границу, публичный API, форму домена Level 1 или Level 2, направление зависимостей, Domain API, business factory, adapter, assembly, framework binding, state/cache/error/lifecycle ownership или исправить deep import, цикл и environment leak. Не используй для локального coding, debugging, форматирования, framework- или SDK-механики, если архитектурная граница уже определена и не меняется."
+---
+
+# SLM Design
+
+## Рабочий контракт
+
+Применяй SLM как способ выполнить пользовательскую задачу, а не как тему для пересказа. После чтения этого файла ты должен уметь принять типовое архитектурное решение, реализовать его в запрошенном scope и проверить результат. Открывай references только для точной формулировки правила, редкого случая или неразрешённого вопроса.
+
+Работай в таком порядке:
+
+1. Исследуй существующий код и локальные правила проекта.
+2. Определи ответственность, владельца и минимальный scope.
+3. Выбери слой, архитектурную сущность и форму домена.
+4. Спроектируй публичную границу, зависимости, runtime-сборку и lifecycle.
+5. До редактирования проверь решение по применимым правилам.
+6. Если пользователь запросил реализацию, внеси изменения до завершённого состояния.
+7. Проверь импорты, exports, граф, среды, lifecycle и тесты.
+8. Кратко сообщи решение, сделанные изменения, проверки, assumptions и остаточные риски.
+
+Не начинай широкое перемещение кода или генерацию каркаса до шагов 1-5. Не расширяй задачу до полного аудита SLM root, если локальное изменение можно корректно выполнить в меньшем scope.
+
+## Источники и обязательность
+
+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` - нерешённые вопросы, а не требования.
+
+Если тематическая глава строже реестра, не создавай из неё новое блокирующее правило. Предложи более строгую форму как рекомендацию или уточни локальную policy, если выбор влияет на API, ownership, стоимость или runtime. Если этот файл расходится с реестром или нормативной терминологией, следуй bundled DRAFT и отметь дефект skill.
+
+При review различай:
+
+- **Rule violation** - нарушено применимое правило с существующим кодом SLM.
+- **Definition mismatch** - реализация не соответствует нормативному смыслу сущности.
+- **Architectural risk** - есть доказуемый риск, но нет блокирующего правила.
+- **Decision required** - DRAFT или проект оставляет значимый выбор открытым.
+- **Recommendation** - улучшение, которое не является обязательным.
+- **Assumption** - обратимое рабочее допущение, явно указанное в результате.
+
+Не придумывай коды правил. Перед ссылкой на нарушение открой соответствующий реестр и проверь точную формулировку.
+
+## Минимальная рабочая модель
+
+### SLM root и уровни
+
+SLM root - граница структурной архитектуры одного приложения. Сначала найди фактический root, path aliases, локальный стайлгайд и конфигурацию архитектурной проверки. Не считай `src` root автоматически и не выводи сущность только из имени папки.
+
+Level 1 действует во всём SLM root и задаёт слои, модули, публичные API, общий dependency DAG и владение lifecycle.
+
+Level 2 применяется отдельно к выбранной предметной области и заменяет только её доменный модуль пакетной формой. Остальные домены могут постоянно оставаться на Level 1. Одна предметная область имеет ровно одну итоговую форму.
+
+### Слои
+
+| Исходный слой | Может зависеть от |
+|---|---|
+| `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 | 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.
+
+### Пакетная форма Level 2
+
+Минимальная структура доменного пакета:
+
+```text
+domains//
+├── metadata # optional, declarative only
+├── business/ # required SLM module
+├── assemblies/ # required non-empty Group
+├── adapters/ # when factories have technical dependencies
+└── react|vue|... # when domain-specific bindings exist
+```
+
+Доменный пакет является policy boundary, но не module, Group, public API или graph node. В его корне нет executable files, state, lifecycle, barrel или реэкспортов. Исполняемыми владельцами являются модули внутри пакета.
+
+`business` является единственным предметным владельцем пакета. Его публичный API состоит из фасетов:
+
+| Путь | Содержимое |
+|---|---|
+| `business` | Только public types: Domain API, dependencies, factory types, error types |
+| `business/factory` | Только именованные runtime factories, по одной на Domain API |
+| `business/runtime` | Только реально нужные внешним consumers детерминированные runtime values/functions |
+
+Другой публичный путь внутрь `business` является deep import. `business/runtime` не создавай для симметрии.
+
+Роли Level 2:
+
+- `business` определяет Domain API, модели, validation, transitions, scenario results, dependency contracts и expected domain errors.
+- Adapter module реализует связанные technical dependencies поверх SDK, storage, platform API, state/query runtime или другого technical runtime.
+- Assembly module выбирает adapters, вызывает factories и возвращает именованный graph готовых API для одного execution context.
+- Framework binding module получает готовые Domain API и владеет одной domain-specific интеграцией с framework.
+- Composition, `app`, request handler или test setup собирает междоменный graph в ацикличном порядке и владеет его общим scope.
+
+## Универсальный цикл решения
+
+### 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.
+
+Считай 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 у ресурсов?
+- Не требует ли решение незапрошенной миграции соседних 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.
+
+### Выбор слоя
+
+```text
+Только framework bootstrap, route entry или external input adaptation?
+ -> app
+
+Page/layout/screen/widget, route outcome или multi-domain UI?
+ -> compositions
+
+Domain model, scenario, validation, transition или product state?
+ -> domains
+
+Technical capability без собственной domain model?
+ -> infra
+
+Product-independent reusable UI?
+ -> ui
+
+Deterministic, product-agnostic, без I/O/state/lifecycle?
+ -> shared
+
+Иначе -> уточни ответственность, не выбирай папку по аналогии.
+```
+
+Domain-specific framework integration над готовым API может принадлежать Framework Group пакета Level 2. Зависимость от React/Vue сама по себе не переносит domain behavior в `compositions` или `app`.
+
+### Выбор сущности
+
+```text
+Есть самостоятельный owner/API/dependencies/state/lifecycle?
+ Да -> module.
+ Нет -> часть текущего owner.
+
+Module нужен только внутри одного parent module?
+ Да -> nested module.
+
+Папка только классифицирует modules/Groups?
+ Да -> Group.
+
+Папка только организует содержимое одного module?
+ Да -> segment.
+
+Framework UI entity не имеет самостоятельной ответственности?
+ Да -> component parent module.
+```
+
+Не создавай module только из-за размера, повторного использования внутреннего helper или желания получить отдельную папку. Не оставляй самостоятельную ответственность component-ом или segment-ом только ради меньшего diff.
+
+### Выбор формы домена
+
+По умолчанию используй доменный модуль Level 1. Level 1 не требует factory, ports, adapters, assemblies или разделения по техническим ролям.
+
+Рассматривай Level 2, когда конкретному домену действительно нужны:
+
+- несколько независимо собираемых Domain API;
+- разные browser/server/request assemblies;
+- несколько production technical integrations;
+- строгие environment boundaries;
+- самостоятельные domain-specific framework modules.
+
+Не выбирай Level 2 из-за количества файлов, одного SDK, одного hook, желания унифицировать дерево или гипотетической будущей интеграции. Зафиксируй, какую реальную потребность окупает дополнительная стоимость package, facets, assembly и adapters.
+
+### Публичная граница
+
+1. Перечисли реальных внешних consumers.
+2. Для каждого запиши минимально необходимый contract.
+3. Удали exports, которым нет consumer.
+4. Не экспортируй mutable internals, concrete clients, stores, contexts, adapters или lifecycle implementation.
+5. Для обычного module оставь одну логическую external entry point.
+6. Для `business` используй только объявленные facets.
+7. Удали deep imports и обнови package exports/aliases при необходимости.
+8. Не открывай nested module напрямую за пределы parent boundary.
+
+Для Level 2 разделяй Domain API по устойчивым различиям consumers, dependencies или environments, а не по внутренним техническим папкам. Каждый публичный scenario принадлежит ровно одному Domain API; каждому API соответствует одна factory. Именованный graph assembly не является новым Domain API.
+
+### Проверка зависимости
+
+Для каждого нового или изменённого edge:
+
+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 и проверь цикл.
+
+При пересечении границы Level 2 статически допустимы:
+
+```ts
+import type { OtherDomainApi } from '.../other/business'
+import { deterministicValue } from '.../other/business/runtime'
+```
+
+Готовый API другого домена создаёт внешний graph owner и передаёт assembly или factory аргументом. Не импортируй из другого домена его factory, assembly, adapter, API singleton, framework state, hook, context, Provider, component или внутренний путь `business`.
+
+Не скрывай 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 для business dependency | Adapter |
+| Clock, timer, random, ID, environment | Явная factory dependency с 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 переводит technical arguments/results и реализует dependency contract. Он не объявляет Domain API scenario, domain fallback, transition или public domain error. Production implementation technical dependency не прячь inline в assembly или composition.
+
+Assembly выбирает public adapters своего домена, вызывает factories и возвращает точный именованный graph API. Она не добавляет scenarios, методы API или собственные domain errors. Framework binding получает готовые API и не вызывает factory/assembly и не выбирает adapter.
+
+### State и cache
+
+```text
+Domain facts, validation, transitions, commands, scenario outcomes
+ -> business 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. Technical и framework cache могут хранить и проецировать только значения, произведённые или проверенные `business`, и не создают параллельную предметную модель.
+
+При optimistic или concurrent mutations не придумывай универсальный rollback. Сначала установи owner политики ordering, versioning, rebase/rollback и authoritative refresh.
+
+### Errors
+
+- Expected technical или foreign-domain failure, доступный через текущий Domain API, преобразуется текущим `business` в собственный readonly domain error со stable code.
+- Source object, SDK class, message, status, payload и `cause` не входят в public domain contract.
+- Type errors экспортируются через `business`; необходимые runtime codes и guards - только через реально нужный `business/runtime`.
+- Не выбирай exception или discriminated `Result` как правило SLM: сохрани project policy и архитектурное владение.
+- Не маскируй programming defect под expected domain outcome. Если меняется публичный failure channel, выясни политику unexpected failures, cancellation и serialization.
+
+### Lifecycle и environment
+
+Для каждого request, subscription, listener, timer, observer, connection или другого долгоживущего ресурса зафиксируй:
+
+```text
+Owner:
+Created or started by:
+Scope:
+Multiplicity:
+Environment:
+Owned or borrowed:
+Cleanup:
+```
+
+Factory или assembly не должна запускать неучтённую долгоживущую работу. Явная операция, запускающая ресурс, предоставляет cleanup. Если assembly обязана создать resource для graph, её публичный result предоставляет cleanup handle; graph owner вызывает его не позже конца scope. Assembly без собственного ресурса не возвращает пустой `dispose` для симметрии.
+
+Environment определяется transitive import graph, а не именем файла или tree shaking. Для RSC, server actions, workers, edge runtime и conditional exports сначала установи реальные executable edges, framework reference edges и runtime capabilities; не объявляй environment safety только по метке `client`/`server`.
+
+## Рабочие процедуры
+
+### Проектирование
+
+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 сначала реализуй types, errors, behavior и factories `business`.
+5. Затем реализуй production adapters, assemblies и framework bindings, которые реально нужны задаче.
+6. Собери междоменный graph в composition, `app`, request handler или test setup.
+7. Переведи всех затронутых consumers на public paths.
+8. Удали obsolete exports, deep imports и старые boundaries в согласованном scope.
+9. Добавь tests рядом с owners.
+10. Запусти доступные 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. Перенеси модели, validation, transitions, outcomes и errors под authority `business`.
+6. Объяви явные factory dependencies и по одной factory на API.
+7. Оформи production technical implementations как adapter modules.
+8. Создай минимум одну assembly для реального execution context.
+9. Перенеси domain-specific framework responsibilities в Framework Group.
+10. Оставь pages, routes и multi-domain UI в `compositions`.
+11. Переключи external consumers и graph roots.
+12. Удали прежний root API и старую форму домена.
+13. Проверь, что итог содержит одну форму и не требует миграции соседних доменов.
+
+Временное физическое сосуществование старой и новой структуры допустимо только внутри незавершённого изменения. Не объявляй его conforming state. Если атомарный 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 `business` closure и environment graph.
+6. Проверь state/cache/error/lifecycle ownership.
+7. Проверь, что tests находятся у правильных owners и не дублируют весь behavior на каждом уровне.
+8. Сначала сообщи 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, state и expected errors | `business` через соответствующую factory |
+| Deterministic runtime/guards | `business` |
+| Technical mapping и provider behavior | Adapter module |
+| Graph composition, adapter selection, environment и cleanup | Assembly module |
+| Provider, hook, form или query projection | Framework binding module |
+| Multi-domain graph и lifecycle | Composition, `app` или другой graph owner |
+
+Не повторяй полный business 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 или `business`.
+- Root barrel доменного пакета или Group.
+- Export mutable store, context, client, adapter или singleton.
+- Reexport client и server entry points через общий barrel.
+- Создавать `business/runtime` без внешнего consumer.
+
+### Business и runtime
+
+- Импортировать SDK, storage, framework, state/query manager, platform API или hidden nondeterminism в `business`.
+- Обходить boundary через helper, `shared` или type alias.
+- Публиковать raw DTO или library-specific cache/store types в Domain API.
+- Позволять 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.
+- Импортировать framework state, hooks или components другого домена.
+- Импортировать чужую factory, assembly, adapter или API singleton.
+- Прятать runtime dependency в service locator, mutable registry или event bus.
+- Создавать pass-through adapter автоматически без проверки project policy и реальной boundary value.
+
+### State и lifecycle
+
+- Делать cache параллельной domain model.
+- Строить optimistic domain value из raw form/DTO без business validation.
+- Использовать file-level singleton без доказанного application scope.
+- Запускать скрытую subscription/timer при создании API.
+- Оставлять resource без scope или cleanup.
+- Возвращать пустой `dispose` только для одинаковой формы assemblies.
+
+### Процесс
+
+- Выбирать 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 | Initial state, transitions, reset, persistence и owner |
+| 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 | Нужен ли domain adapter или допустима прямая передача capability |
+
+Можно продолжить с явным assumption только когда решение обратимо, не меняет owner/public API, не ослабляет environment boundary и не скрывает lifecycle.
+
+## Проверочные списки
+
+### До изменения файлов
+
+- [ ] Найден SLM root и path mapping.
+- [ ] Прочитаны локальные инструкции.
+- [ ] Сформулирована 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 ацикличен.
+- [ ] `business` import closure environment-neutral и technical-runtime-free.
+- [ ] Cross-domain runtime APIs передаются аргументами.
+- [ ] Client/server graphs не содержат несовместимый executable code.
+- [ ] Facets `business` имеют допустимое содержимое и consumers.
+- [ ] Production technical dependencies принадлежат нужным adapters.
+- [ ] Assembly возвращает точный graph и cleanup, если владеет resource.
+- [ ] Framework bindings получают готовые APIs.
+- [ ] Technical и foreign errors не протекают наружу.
+- [ ] Cache не подменяет business authority.
+- [ ] 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 |
+
+Для однозначной локальной реализации достаточно кратко объяснить архитектурное решение и выполнить работу. Для дорогого, публично несовместимого или неоднозначного решения сначала покажи варианты и запроси выбор.
+
+## Когда открывать references
+
+| Ситуация | 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, business, 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) |
+| 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.
diff --git a/.opencode/skills/slm-design/reference/draft/README.md b/.opencode/skills/slm-design/reference/draft/README.md
new file mode 100644
index 0000000..375af6e
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/README.md
@@ -0,0 +1,15 @@
+# Черновики SLM
+
+> Материалы в `DRAFT` являются рабочими черновиками и не задают нормативную спецификацию SLM.
+
+## Материалы
+
+- [Первый уровень](./level-1/README.md) - слои, доменные модули, публичные API и зависимости.
+- [Второй уровень](./level-2/README.md) - доменные пакеты, именованные Domain API, assemblies и Framework Groups.
+- [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки.
+
+## Соглашение
+
+Черновики могут содержать определения, правила, рекомендации, примеры и открытые вопросы.
+
+Нормативные определения задаются терминологией соответствующего уровня. Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки.
diff --git a/.opencode/skills/slm-design/reference/draft/level-1/README.md b/.opencode/skills/slm-design/reference/draft/level-1/README.md
new file mode 100644
index 0000000..527c90b
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/README.md
@@ -0,0 +1,53 @@
+# 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/.opencode/skills/slm-design/reference/draft/level-1/components.md b/.opencode/skills/slm-design/reference/draft/level-1/components.md
new file mode 100644
index 0000000..94a4d56
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/components.md
@@ -0,0 +1,65 @@
+# Компоненты 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/.opencode/skills/slm-design/reference/draft/level-1/dependencies.md b/.opencode/skills/slm-design/reference/draft/level-1/dependencies.md
new file mode 100644
index 0000000..5a4f213
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/dependencies.md
@@ -0,0 +1,59 @@
+# Зависимости 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/.opencode/skills/slm-design/reference/draft/level-1/domains.md b/.opencode/skills/slm-design/reference/draft/level-1/domains.md
new file mode 100644
index 0000000..bf4bedc
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/domains.md
@@ -0,0 +1,61 @@
+# Доменные модули 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 не требует выделять business, 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-интеграции.
+
+Такой переход изменяет только форму выбранного домена: корневой доменный модуль исчезает, а его предметная ответственность переходит обязательному модулю `business` внутри доменного пакета. Другие предметные области SLM root не обязаны переходить вместе с ним.
diff --git a/.opencode/skills/slm-design/reference/draft/level-1/groups.md b/.opencode/skills/slm-design/reference/draft/level-1/groups.md
new file mode 100644
index 0000000..62059f8
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/groups.md
@@ -0,0 +1,25 @@
+# Группы 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/.opencode/skills/slm-design/reference/draft/level-1/layers.md b/.opencode/skills/slm-design/reference/draft/level-1/layers.md
new file mode 100644
index 0000000..b7583d4
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/layers.md
@@ -0,0 +1,95 @@
+# Слои 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/.opencode/skills/slm-design/reference/draft/level-1/lifecycle.md b/.opencode/skills/slm-design/reference/draft/level-1/lifecycle.md
new file mode 100644
index 0000000..31a5d51
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/lifecycle.md
@@ -0,0 +1,31 @@
+# Жизненный цикл 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/.opencode/skills/slm-design/reference/draft/level-1/modules.md b/.opencode/skills/slm-design/reference/draft/level-1/modules.md
new file mode 100644
index 0000000..f5d2d4a
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/modules.md
@@ -0,0 +1,43 @@
+# Модули 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/.opencode/skills/slm-design/reference/draft/level-1/nested-modules.md b/.opencode/skills/slm-design/reference/draft/level-1/nested-modules.md
new file mode 100644
index 0000000..ef0706a
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/nested-modules.md
@@ -0,0 +1,30 @@
+# Вложенные модули 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/.opencode/skills/slm-design/reference/draft/level-1/segments.md b/.opencode/skills/slm-design/reference/draft/level-1/segments.md
new file mode 100644
index 0000000..0ab790a
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/segments.md
@@ -0,0 +1,29 @@
+# Сегменты 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/.opencode/skills/slm-design/reference/draft/level-1/terminology.md b/.opencode/skills/slm-design/reference/draft/level-1/terminology.md
new file mode 100644
index 0000000..21585c9
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/terminology.md
@@ -0,0 +1,143 @@
+# Терминология 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/.opencode/skills/slm-design/reference/draft/level-1/validation.md b/.opencode/skills/slm-design/reference/draft/level-1/validation.md
new file mode 100644
index 0000000..a61715d
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-1/validation.md
@@ -0,0 +1,34 @@
+# Проверка 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/.opencode/skills/slm-design/reference/draft/level-2/README.md b/.opencode/skills/slm-design/reference/draft/level-2/README.md
new file mode 100644
index 0000000..7c88cfc
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/README.md
@@ -0,0 +1,102 @@
+# SLM Level 2
+
+> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
+
+Level 2 предназначен для отдельных предметных областей с устойчивыми API, несколькими способами сборки или самостоятельными framework-модулями. Он сохраняет структурную базу Level 1, но заменяет выбранный доменный модуль доменным пакетом с явными владельцами ролей.
+
+## Наследование 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 модуля `business` | Представлен обязательными type-only и factory-фасетами и необязательным runtime-фасетом |
+| Навигационная Group слоя `domains` | Может одновременно содержать доменные модули и пакеты |
+| Group внутри доменного пакета | Содержит обычные SLM-модули и Groups |
+
+Канонический набор требований образуют [правила Level 1](../rules/level-1.md) и [правила Level 2](../rules/level-2.md).
+
+## Когда выбирать Level 2
+
+Level 2 оправдан, когда одной предметной области нужны несколько независимо собираемых Domain API, разные browser/server assemblies, несколько технических интеграций или самостоятельные domain-specific модули React, Vue либо другого фреймворка.
+
+Level 2 выбирается для конкретной предметной области. Один SLM root может корректно содержать простые доменные модули Level 1 и доменные пакеты Level 2. Размер каталога сам по себе не требует перехода.
+
+## Цена Level 2
+
+Пакетная форма намеренно дороже простого доменного модуля. Она добавляет отдельные business-фасеты, минимум одну assembly, явную runtime-инъекцию cross-domain API и adapter-модули для production capabilities. Concrete state/query runtime, SDK и недетерминизм не импортируются напрямую в `business`.
+
+Эта цена оправдана независимой сборкой API, несколькими средами, строгой environment boundary или самостоятельными framework bindings. Если домену не нужны такие свойства, он остаётся модулем Level 1.
+
+## Базовая форма
+
+```text
+src/domains/
+├── catalog/ # Доменный модуль Level 1
+└── auth/ # Доменный пакет Level 2
+ ├── README.md # Необязательная metadata
+ ├── business/ # Обязательный SLM-модуль
+ │ ├── index.ts # Только public types
+ │ ├── factory.ts # Public factories entry
+ │ └── runtime.ts # Необязательный deterministic runtime
+ ├── assemblies/ # Обязательная непустая Group
+ │ ├── browser/ # SLM-модуль
+ │ └── request/ # SLM-модуль
+ ├── adapters/ # При наличии technical dependencies
+ │ └── identity-provider/ # SLM-модуль
+ └── react/ # Необязательная framework Group
+ ├── session/ # SLM-модуль
+ └── login-form/ # SLM-модуль
+```
+
+Корень пакета не является модулем и не имеет `index.ts`. Каждый исполняемый владелец внутри пакета остаётся обычным SLM-модулем со своим публичным API.
+
+## Публичные границы
+
+`business` может объявить несколько API и соответствующих фабрик. Assembly создаёт только нужный для своего контекста именованный граф:
+
+```ts
+import type {
+ AuthAdministrationApi,
+ AuthSessionApi,
+} from '@/domains/auth/business'
+
+import {
+ authAdministrationFactory,
+ authSessionFactory,
+} from '@/domains/auth/business/factory'
+
+import {
+ AUTH_ERROR_CODES,
+ isAuthError,
+} from '@/domains/auth/business/runtime'
+
+import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
+import { AuthSessionProvider } from '@/domains/auth/react/session'
+```
+
+`business/runtime` существует только при наличии реальных внешних потребителей детерминированных runtime-экспортов. Общие импорты `@/domains/auth` и `@/domains/auth/react` запрещены: пакет и Groups не имеют агрегирующих API.
+
+## Совместное применение форм
+
+Простой доменный модуль и доменный пакет могут постоянно сосуществовать в одном SLM root, но одна предметная область имеет только одну форму. При связи, пересекающей границу пакета Level 2, доменный код использует type-only публичный контракт либо детерминированный `business/runtime`; готовые API передаются runtime-аргументами в месте сборки графа.
+
+Если доменному модулю Level 1 нужна runtime-инъекция, которой его текущий публичный API не поддерживает, этот потребитель рефакторится или сам переводится в пакетную форму. Level 2 не создаёт скрытый механизм внедрения в старый модуль.
+
+## Карта черновика
+
+- [Терминология](./terminology.md)
+- [Доменный пакет](./domains/domain-package.md)
+- [Модуль business](./domains/business.md)
+- [Фабрики, зависимости и adapters](./domains/factory-ports-adapters.md)
+- [Assemblies и среды выполнения](./domains/assemblies.md)
+- [Состояние и кэш](./domains/state-cache.md)
+- [Framework Groups и модули](./domains/framework-bindings.md)
+- [Зависимости](./dependencies.md)
+- [Тестирование](./domains/testing.md)
+- [Проверка](./validation.md)
+- [Переход auth](./domains/auth-example.md)
+- [Открытые вопросы](./domains/open-questions.md)
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/dependencies.md b/.opencode/skills/slm-design/reference/draft/level-2/dependencies.md
new file mode 100644
index 0000000..1d74235
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/dependencies.md
@@ -0,0 +1,109 @@
+# Зависимости Level 2
+
+> Уточнение графа зависимостей внутри и между доменными границами.
+
+## Связанные правила
+
+- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-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-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-a019)
+- [`SLM-L2-ADAPTER-R021`](../rules/level-2.md#slm-l2-adapter-r021)
+- [`SLM-L2-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
+- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
+
+## Матрица внутри пакета
+
+| Исходный модуль | Допустимые зависимости |
+|---|---|
+| `business` | Собственные файлы, объявленные environment-neutral ресурсы `shared`, business-safe packages, type-only контракты и `business/runtime` других доменов |
+| Adapter module | Type-only barrel собственного `business`, `infra`, concrete technical runtime, `shared` |
+| Assembly | Type-only barrel, `factory` и при необходимости `runtime` собственного `business`, публичные adapters своего домена, type-only API других доменов, `shared` |
+| Framework binding module | Type-only barrel и `runtime` собственного `business`, публичные framework-модули своего домена, framework/state/query runtime, `ui`, `shared` |
+| Место сборки графа | Assemblies либо `business/factory` и adapter-модули, `business/runtime`, framework-модули входящих в граф доменов, а также разрешённые матрицей `infra`, `ui` и `shared` |
+
+`business` не достигает adapters, assemblies, framework-модулей, product SDK, storage, state/query manager, API браузера или Node.js. Проверяется весь транзитивный import-граф публичных фасетов.
+
+Adapter module импортирует из собственного `business` только типы технических зависимостей. Он не импортирует `business/factory`, потому что не собирает API. Публичный deterministic runtime обычно также не нужен adapter: внешний результат возвращается business для проверки и error mapping.
+
+Assembly не содержит inline adapters. Она импортирует production implementations через публичные API конкретных модулей `adapters/*`.
+
+## Междоменные импорты
+
+При связи, пересекающей границу пакета Level 2, разрешены два вида статического импорта из другого домена:
+
+```ts
+import type { AuthSessionApi } from '@/domains/auth/business'
+import { isAuthError } from '@/domains/auth/business/runtime'
+```
+
+Первый импорт предоставляет type-only контракт для runtime-инъекции. Второй предоставляет только публичную детерминированную функцию или значение. Оба импорта являются рёбрами общего DAG.
+
+Запрещено импортировать из другого домена:
+
+- `business/factory`;
+- готовый API instance или singleton;
+- assembly;
+- adapter;
+- framework state, hook, context, Provider или component;
+- любой внутренний путь `business`.
+
+Такая модель действует и между двумя пакетами Level 2, и между модулем Level 1 и пакетом Level 2. Между двумя простыми доменными модулями продолжают действовать правила Level 1.
+
+## Детерминированный runtime
+
+`business/runtime` закрывает случай общей продуктовой pure-функции, которую нельзя переносить в `shared` из-за знания о предметной области:
+
+```ts
+import {
+ normalizeAuthIdentifier,
+} from '@/domains/auth/business/runtime'
+```
+
+Функция остаётся собственностью Auth, а зависимый домен получает явное runtime-ребро к её публичному контракту. Если связь создаёт цикл, границы доменов или владелец общего понятия пересматриваются.
+
+Перенос в `shared` допустим только после устранения продуктового знания, а не как обход cross-domain правила.
+
+## Runtime-инъекция API
+
+Готовый API другого домена передаётся assembly или одноразовому месту сборки аргументом. Код зависимого домена не импортирует его runtime-фабрику или сборку:
+
+```text
+createAuthForRequest()
+ → AuthSessionApi
+ → createUserForRequest({ auth })
+ → UserProfileApi
+```
+
+Место сборки графа создаёт независимые домены раньше зависимых. Если сбой Auth становится результатом публичного сценария User, наружу выходит только собственная ошибка User. При exception-модели User может распознать ожидаемый Auth error через публичный guard из `auth/business/runtime`.
+
+## Совместное применение Level 1 и Level 2
+
+Один SLM root может постоянно содержать обе формы. Каждая предметная область объявляется либо модулем, либо пакетом, поэтому checker однозначно определяет применимые правила.
+
+Runtime-взаимодействие с пакетом Level 2 собирается за пределами доменного кода. Если существующий модуль Level 1 должен принимать готовый API, его собственный публичный контракт явно предоставляет такую точку инъекции. Если это невозможно без скрытого singleton или обратного импорта, зависимый модуль рефакторится либо переводится на Level 2.
+
+Runtime-значение из модуля Level 1, необходимое пакету Level 2, также передаётся внешним graph owner как API, callback или другой явно типизированный аргумент. Код пакета не импортирует executable API модуля Level 1 напрямую.
+
+## Framework-состояние
+
+Framework binding module использует framework API только своего доменного пакета:
+
+```ts
+// Допустимо внутри domains/auth/react/login-form
+import { useAuthSession } from '@/domains/auth/react/session'
+
+// Недопустимо внутри domains/user/react/profile
+import { useAuthSession } from '@/domains/auth/react/session'
+```
+
+Во втором случае composition читает состояния обоих доменов и передаёт необходимые данные или callbacks через публичные свойства компонентов.
+
+State/query cache внутри framework binding не создаёт исключение для cross-domain imports. Он работает поверх переданного API своего домена.
+
+## Границы сред
+
+Каждый client-, server- или shared-entry point имеет совместимый транзитивный import-граф. Серверная assembly или adapter не реэкспортируется через `business`, Framework Group или клиентскую assembly.
+
+Tree shaking не является доказательством изоляции. Проверка выполняется до удаления неиспользуемого кода.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/README.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/README.md
new file mode 100644
index 0000000..0a037b0
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/README.md
@@ -0,0 +1,29 @@
+# Доменные пакеты Level 2
+
+Доменный пакет заменяет один выбранный доменный модуль Level 1. Он объединяет SLM-модули одной предметной области, но сам не является модулем, Group или публичным API.
+
+```text
+domains/auth/
+├── business/
+├── assemblies/ # Обязательная Group
+├── adapters/ # При наличии technical dependencies
+└── react/
+ ├── session/
+ └── login-form/
+```
+
+Другие предметные области того же SLM root могут оставаться доменными модулями Level 1.
+
+## Основные границы
+
+- [Доменный пакет](./domain-package.md) определяет предметную и структурную policy boundary.
+- [Business](./business.md) владеет Domain API и разделяет public types, factories и необязательный deterministic runtime.
+- [Фабрики, зависимости и adapters](./factory-ports-adapters.md) изолируют runtime-capabilities, включая state/query runtime и недетерминизм.
+- [Assemblies](./assemblies.md) обязательны и собирают именованный граф нужных API для реального контекста.
+- [Состояние и кэш](./state-cache.md) разделяет предметную власть business и технические проекции.
+- [Framework Groups](./framework-bindings.md) содержат domain-specific SLM-модули фреймворка.
+- [Тестирование](./testing.md) проверяет каждого владельца через его публичную границу.
+- [Переход auth](./auth-example.md) показывает локальный переход с Level 1 без big-bang всего root.
+- [Открытые вопросы](./open-questions.md) отделяют принятые решения от ещё не нормированных деталей.
+
+Точные блокирующие требования объявлены только в [реестре правил Level 2](../../rules/level-2.md).
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/assemblies.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/assemblies.md
new file mode 100644
index 0000000..1fcb033
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/assemblies.md
@@ -0,0 +1,170 @@
+# Assemblies и среды выполнения
+
+> Пояснение повторяемой сборки именованного графа Domain API.
+
+## Связанные правила
+
+- [`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-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-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-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
+- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
+
+## Назначение
+
+Assembly является SLM-модулем в Group `assemblies`. Она создаёт явный граф одного или нескольких Domain API пакета для конкретного повторяемого контекста выполнения. Каждый доменный пакет содержит минимум одну assembly.
+
+```text
+business/factory
+├── assemblies/browser → { session: AuthSessionApi }
+├── assemblies/request → { session, administration }
+└── assemblies/server-action → { administration }
+```
+
+Архитектура не требует `base` или изоморфную assembly и не ограничивает их максимальное количество. Обязательная assembly соответствует реальному поддерживаемому контексту, а не существует только для заполнения структуры.
+
+Место сборки графа в `composition` может вызвать фабрики напрямую для одноразовой конфигурации. Такая сборка не отменяет обязательную assembly пакета и при наличии технических зависимостей использует публичные adapter-модули, а не inline implementations.
+
+## Именованный граф API
+
+Browser assembly импортирует только фабрики и adapters нужных ей API:
+
+```ts
+import type { AuthSessionApi } from '@/domains/auth/business'
+import { authSessionFactory } from '@/domains/auth/business/factory'
+import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
+import { createBrowserSessionAdapter } from '@/domains/auth/adapters/browser-session'
+
+export type AuthBrowserGraph = Readonly<{
+ session: AuthSessionApi
+}>
+
+export const createBrowserAuth = (): AuthBrowserGraph => {
+ const session = authSessionFactory({
+ phone: createPhoneHttpAdapter(),
+ session: createBrowserSessionAdapter(),
+ })
+
+ return { session }
+}
+```
+
+Request assembly может собрать дополнительный API, которого нет в браузере:
+
+```ts
+import type {
+ AuthAdministrationApi,
+ AuthSessionApi,
+} from '@/domains/auth/business'
+
+import {
+ authAdministrationFactory,
+ authSessionFactory,
+} from '@/domains/auth/business/factory'
+
+export type AuthRequestGraph = Readonly<{
+ administration: AuthAdministrationApi
+ session: AuthSessionApi
+}>
+```
+
+Assembly не добавляет методы к этим контрактам и не создаёт общий `AuthApi`. Именованный объект только сообщает, какие независимые API доступны в контексте.
+
+## Cross-domain input
+
+Assembly зависимого домена принимает готовый API аргументом. Cross-domain API не является adapter и не размещается в Group `adapters`:
+
+```ts
+import type { AuthSessionApi } from '@/domains/auth/business'
+import type { UserProfileApi } from '@/domains/user/business'
+import { userProfileFactory } from '@/domains/user/business/factory'
+import { createUserProfileAdapter } from '@/domains/user/adapters/profile'
+
+export type CreateUserForRequestInput = {
+ auth: Pick
+ request: UserRequestInput
+}
+
+export type UserRequestGraph = Readonly<{
+ profile: UserProfileApi
+}>
+
+export const createUserForRequest = ({
+ auth,
+ request,
+}: CreateUserForRequestInput): UserRequestGraph => {
+ const profile = userProfileFactory({
+ auth,
+ profile: createUserProfileAdapter(request),
+ })
+
+ return { profile }
+}
+```
+
+Assembly делает только type-only импорт `AuthSessionApi`. Runtime-фабрику, assembly или instance Auth она не импортирует.
+
+Место сборки графа выполняет runtime-связь:
+
+```ts
+const auth = createAuthForRequest(authInput)
+const user = createUserForRequest({
+ auth: auth.session,
+ request: userInput,
+})
+```
+
+## Environment entry points
+
+Server assembly имеет отдельный публичный entry point и marker выбранного framework или bundler:
+
+```ts
+import 'server-only'
+
+export { createAuthForRequest } from './create-auth-for-request'
+```
+
+Server entry point не реэкспортируется через `business`, Framework Group, browser assembly или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость.
+
+## Lifecycle
+
+Factory не запускает запрос, subscription, timer или другую скрытую долгоживущую работу во время создания API. Assembly может активировать технический ресурс только с явной передачей cleanup своему caller. Операция Domain API, которая запускает ресурс позже, сама возвращает cleanup:
+
+```ts
+const stop = auth.session.startInvalidationTracking()
+
+try {
+ // Scope использует API.
+} finally {
+ await stop()
+}
+```
+
+Если assembly сама создаёт ресурс, принадлежащий всему возвращённому графу, результат дополнительно предоставляет cleanup handle:
+
+```ts
+export type AuthRequestAssembly = Readonly<{
+ apis: AuthRequestGraph
+ dispose: () => Promise
+}>
+```
+
+```ts
+const auth = createAuthForRequest(input)
+
+try {
+ return await handleRequest(auth.apis)
+} finally {
+ await auth.dispose()
+}
+```
+
+Assembly без собственного ресурса не обязана возвращать пустой `dispose`. Graph owner вызывает каждый реально предоставленный cleanup не позже завершения application, route, request или test scope.
+
+Одноразовое место сборки, которое вызывает фабрики напрямую, подчиняется той же границе: оно не запускает скрытый ресурс в constructor и сохраняет cleanup любого созданного adapter-ресурса до завершения своего scope.
+
+Рекомендуется делать `dispose` идемпотентным, освобождать частично созданные ресурсы при ошибке assembly и закрывать зависимые ресурсы раньше их зависимостей. Детальная политика rollback и поведения API после cleanup остаётся открытым вопросом.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/auth-example.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/auth-example.md
new file mode 100644
index 0000000..bdf4c13
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/auth-example.md
@@ -0,0 +1,139 @@
+# Переход домена auth с Level 1
+
+> Проверочный пример локального перехода от доменного модуля к доменному пакету.
+
+## Связанные правила
+
+- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
+- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
+- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-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-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
+
+## Исходная форма Level 1
+
+```text
+domains/
+├── auth/ # Доменный модуль
+│ ├── hooks/
+│ ├── services/
+│ ├── stores/
+│ ├── ui/
+│ └── index.ts # Общий API модуля
+└── catalog/ # Независимый доменный модуль
+ └── index.ts
+```
+
+Level 1 разрешает business-сценариям, framework hooks, state adapter и локальной сборке Auth находиться внутри одного модуля.
+
+## Целевая форма Auth
+
+```text
+domains/
+├── auth/ # Доменный пакет Level 2
+│ ├── README.md
+│ ├── business/ # Один SLM-модуль
+│ │ ├── errors/
+│ │ ├── factories/
+│ │ ├── services/
+│ │ ├── types/
+│ │ ├── index.ts # Только public types нескольких API
+│ │ ├── factory.ts # Public factories entry
+│ │ └── runtime.ts # Error codes, guards, public pure runtime
+│ ├── adapters/ # Group
+│ │ ├── phone-http/ # SLM-модуль
+│ │ ├── browser-session/ # SLM-модуль
+│ │ └── request-session/ # SLM-модуль
+│ ├── assemblies/ # Обязательная Group
+│ │ ├── browser/ # Только AuthSessionApi
+│ │ └── request/ # Session + Administration API
+│ └── react/ # Framework Group
+│ ├── session/ # SLM-модуль
+│ └── login-form/ # SLM-модуль
+└── catalog/ # По-прежнему модуль Level 1
+ └── index.ts
+```
+
+Корневой `domains/auth/index.ts` удаляется. Потребители переходят на публичные API конкретных модулей. `catalog` и остальные домены не меняют форму только из-за перехода Auth.
+
+## Перенос ответственности
+
+| Исходная часть | Владелец Level 2 | Публичный путь |
+|---|---|---|
+| Session-сценарии и public types | `auth/business` | `auth/business` |
+| Administration-сценарии и public types | `auth/business` | `auth/business` |
+| Runtime-фабрики API | `auth/business` | `auth/business/factory` |
+| Коды, guards и public pure-функции | `auth/business` | `auth/business/runtime` |
+| Browser storage и HTTP adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
+| Cookies, request data и server adapters | Соответствующий adapter-модуль | `auth/adapters/*` |
+| Browser graph | `auth/assemblies/browser` | `auth/assemblies/browser` |
+| Request graph | `auth/assemblies/request` | `auth/assemblies/request` |
+| Provider и session hooks | `auth/react/session` | `auth/react/session` |
+| Переиспользуемая форма | `auth/react/login-form` | `auth/react/login-form` |
+| Страница, текст и redirect | `compositions` | API конкретной composition |
+
+## Новые импорты
+
+```ts
+import type {
+ AuthAdministrationApi,
+ AuthError,
+ AuthErrorCode,
+ AuthSessionApi,
+} from '@/domains/auth/business'
+
+import {
+ authAdministrationFactory,
+ authSessionFactory,
+} from '@/domains/auth/business/factory'
+
+import {
+ AUTH_ERROR_CODES,
+ isAuthError,
+} from '@/domains/auth/business/runtime'
+
+import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
+import { createBrowserAuth } from '@/domains/auth/assemblies/browser'
+import { AuthSessionProvider } from '@/domains/auth/react/session'
+import { LoginForm } from '@/domains/auth/react/login-form'
+```
+
+## Cross-domain граф
+
+Если User package зависит от Auth, он получает только type-only API contract и при необходимости deterministic runtime:
+
+```ts
+import type { AuthSessionApi } from '@/domains/auth/business'
+import { isAuthError } from '@/domains/auth/business/runtime'
+
+export type UserDeps = {
+ auth: Pick
+}
+```
+
+Место сборки создаёт instances:
+
+```ts
+const auth = createBrowserAuth()
+const user = createBrowserUser({ auth: auth.session })
+```
+
+User не импортирует Auth factory или assembly, а его React-модули не импортируют `useAuthSession` или Auth components.
+
+Если User остаётся модулем Level 1, runtime-связь также выполняется снаружи доменных границ. Для этого его собственный API должен иметь явную точку передачи нужного Auth behavior; иначе именно User требуется рефакторинг или переход, но несвязанные домены не затрагиваются.
+
+## Порядок перехода
+
+1. Выбрать один доменный модуль и зафиксировать его внешние consumers.
+2. Объявить `business` с type-only и factory entry points.
+3. Разделить сценарии на независимо собираемые Domain API без дублирования методов.
+4. Добавить `business/runtime`, только если внешним consumers нужны codes, guards или pure-функции.
+5. Оформить каждую связную production implementation модулем `adapters/*`.
+6. Создать минимум одну assembly и перенести туда повторяемый выбор API и adapters.
+7. Разделить React-ответственности на модули внутри Group `react`.
+8. Перенести страницы, redirects и multi-domain UI в `compositions`.
+9. Перевести внешние импорты на разрешённые public paths.
+10. Удалить старый root `index.ts` Auth и объявить пакетную форму checker-у.
+
+Завершённость перехода определяется только границей Auth. Наличие других доменных модулей Level 1 не является миграционным долгом.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/business.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/business.md
new file mode 100644
index 0000000..b524bcb
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/business.md
@@ -0,0 +1,218 @@
+# Модуль business
+
+> Пояснение предметного владельца, нескольких Domain API и публичного deterministic runtime.
+
+## Связанные правила
+
+- [`SLM-L2-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
+- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
+- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
+- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
+- [`SLM-L2-ERROR-R009`](../../rules/level-2.md#slm-l2-error-r009)
+- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
+- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
+- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
+- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
+- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
+- [`SLM-L2-BUSINESS-R025`](../../rules/level-2.md#slm-l2-business-r025)
+
+## Роль
+
+`business` является обязательным SLM-модулем доменного пакета. Он владеет:
+
+- публичными предметными сценариями;
+- одним или несколькими именованными Domain API;
+- одной публичной фабрикой для каждого API;
+- типами явных зависимостей фабрик;
+- предметными типами и детерминированными правилами;
+- контрактами ожидаемых доменных ошибок;
+- публичным представлением доменных данных и состояния.
+
+Несколько API остаются частью одного модуля, пока относятся к одной связной предметной области. Они нужны не для копирования use cases, а для независимой сборки разных наборов сценариев, зависимостей и сред.
+
+## Публичные фасеты
+
+Один логический API модуля `business` имеет два обязательных и один необязательный entry point.
+
+### Type-only barrel
+
+Корневой `business/index.ts` экспортирует только типы:
+
+```ts
+export type {
+ AuthAdministrationApi,
+ AuthAdministrationDeps,
+ AuthAdministrationFactory,
+ AuthError,
+ AuthErrorCode,
+ AuthSessionApi,
+ AuthSessionDeps,
+ AuthSessionFactory,
+ AuthState,
+} from './types'
+```
+
+Потребитель использует этот путь только через `import type`:
+
+```ts
+import type {
+ AuthSessionApi,
+ AuthState,
+} from '@/domains/auth/business'
+```
+
+### Factory entry
+
+`business/factory.ts` экспортирует только именованные runtime-фабрики:
+
+```ts
+export { authAdministrationFactory } from './factories/auth-administration.factory'
+export { authSessionFactory } from './factories/auth-session.factory'
+```
+
+```ts
+import {
+ authAdministrationFactory,
+ authSessionFactory,
+} from '@/domains/auth/business/factory'
+```
+
+Каждому API соответствует одна фабрика. Фасет не экспортирует готовые instances, adapters или environment-specific assembly.
+
+### Runtime entry
+
+Необязательный `business/runtime.ts` экспортирует только публичные детерминированные значения и функции:
+
+```ts
+export {
+ AUTH_ERROR_CODES,
+ isAuthError,
+} from './errors/auth-error'
+
+export { normalizeAuthIdentifier } from './lib/normalize-auth-identifier'
+```
+
+Здесь допустимы error codes и guards, validators, value constructors, чистые продуктовые функции и immutable-константы. Все public types по-прежнему импортируются из корневого type-only barrel.
+
+`business/runtime` не содержит:
+
+- фабрики и готовые API instances;
+- I/O или изменяемое состояние;
+- state/query runtime;
+- чтение clock, random, environment или platform API;
+- сценарии, которым нужны runtime-зависимости.
+
+Если внешним потребителям не нужен детерминированный runtime, файл `runtime.ts` не создаётся. Другие внешние пути внутри `business` являются deep imports.
+
+## Несколько Domain API
+
+```ts
+export type AuthSessionApi = {
+ getCurrentSession: () => Promise
+ getSnapshot: () => AuthState
+ requestPhoneOtp: (phone: string) => Promise
+ startInvalidationTracking: () => () => Promise
+ verifyPhoneOtp: (code: string) => Promise
+}
+
+export type AuthAdministrationApi = {
+ revokeUserSessions: (userId: string) => Promise
+}
+```
+
+`AuthSessionApi` может собираться в browser и request contexts, а `AuthAdministrationApi` только в доверенной server assembly. Browser assembly не получает метод-заглушку и не импортирует adapters административного API.
+
+Общий `business/factory` является публичным фасетом, а не гарантией отдельного business chunk для каждой фабрики. Разделение API устраняет обязательное создание лишних adapters и instances. Если самой business-логике нужны независимо поставляемые bundle boundaries, требуется отдельный build/package mechanism за пределами текущего Level 2, а не искусственное разделение предметной области.
+
+Один публичный сценарий принадлежит ровно одному API. Если два API постоянно требуют одинаковых методов, состояния и lifecycle, их граница пересматривается вместо дублирования.
+
+Assembly может вернуть именованный граф нескольких API:
+
+```ts
+export type AuthBrowserGraph = Readonly<{
+ session: AuthSessionApi
+}>
+
+export type AuthRequestGraph = Readonly<{
+ administration: AuthAdministrationApi
+ session: AuthSessionApi
+}>
+```
+
+Такой граф является составом готовых контрактов для контекста, а не новым предметным API.
+
+## Предметная власть и состояние
+
+Business API остаются единственной границей, определяющей предметную модель, validation, переходы и семантику результатов. Это не означает, что только `business` физически хранит байты.
+
+Adapter или framework binding может использовать Zustand, TanStack Query, SWR, Apollo либо другой runtime для хранения и доставки значений. Такое хранилище является технической реализацией или проекцией, если:
+
+- значения получены или проверены business API либо `business/runtime`;
+- предметные переходы выполняются через business API;
+- внешний DTO не становится публичной моделью напрямую;
+- optimistic value создаётся или проверяется предметным владельцем;
+- библиотечные cache/store types не становятся Domain API.
+
+Подробная граница описана в [Состоянии и кэше](./state-cache.md).
+
+## Потребители фасетов
+
+| Потребитель | `business` | `business/factory` | `business/runtime` |
+|---|---|---|---|
+| Adapter своего домена | Type-only | Нет | Обычно нет |
+| Assembly своего домена | Type-only | Да | При необходимости |
+| Framework binding своего домена | Type-only | Нет | Да, если нужен public runtime |
+| `composition` или `app` | Type-only | Для одноразовой сборки | Да |
+| Код другого домена при связи с Level 2 | Type-only | Нет | Да |
+| Тест | Type-only | По границе тестируемого API | По границе тестируемого владельца |
+
+Runtime-импорт `business/runtime` другого домена остаётся архитектурным ребром и участвует в общей проверке циклов.
+
+## Контракт ошибок
+
+Ожидаемая ошибка имеет устойчивую безопасную readonly-форму:
+
+```ts
+export type AuthErrorCode =
+ | 'AUTH_PHONE_INVALID'
+ | 'AUTH_OTP_REQUEST_FAILED'
+ | 'AUTH_OTP_CODE_INVALID'
+
+export type AuthError = Readonly<{
+ code: AuthErrorCode
+}>
+```
+
+Если runtime-потребителям нужны constants или guard, они публикуются через `business/runtime`:
+
+```ts
+export const AUTH_ERROR_CODES = {
+ PHONE_INVALID: 'AUTH_PHONE_INVALID',
+ OTP_REQUEST_FAILED: 'AUTH_OTP_REQUEST_FAILED',
+ OTP_CODE_INVALID: 'AUTH_OTP_CODE_INVALID',
+} as const
+
+export const isAuthError = (value: unknown): value is AuthError => {
+ return isSafeCodedError(value, Object.values(AUTH_ERROR_CODES))
+}
+```
+
+Guard не является обязательной частью каждого домена. При discriminated `Result` типизированному потребителю может быть достаточно error union; при exception, RPC или неизвестной runtime-границе guard часто нужен. Выбор `throw` или `Result` не меняет набор обязательных фасетов.
+
+## Изоляция технических и чужих ошибок
+
+Ошибки SDK, HTTP, database, storage и adapters не пересекают Domain API в форме, доступной приложению. `business` преобразует обрабатываемый технический сбой в собственный код:
+
+```text
+SDK error
+ → adapter failure
+ → business mapping
+ → AuthErrorCode
+ → приложение
+```
+
+Публичная форма не включает исходные `message`, `status`, `payload`, `cause`, SDK class или сам объект ошибки. Диагностические данные остаются во внутреннем observability-механизме.
+
+То же относится к cross-domain вызову. Если User API использует Auth API, сбой, который становится результатом публичного сценария User, представлен собственным `UserError`. При exception-модели зависимый business может импортировать `isAuthError` и error codes из `auth/business/runtime`, после чего преобразовать ожидаемую ошибку в собственный контракт.
+
+Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/domain-package.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/domain-package.md
new file mode 100644
index 0000000..b0d1959
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/domain-package.md
@@ -0,0 +1,114 @@
+# Граница доменного пакета
+
+> Пояснение контейнерной сущности 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-BUSINESS-R005`](../../rules/level-2.md#slm-l2-business-r005)
+- [`SLM-L2-DOMAIN-A026`](../../rules/level-2.md#slm-l2-domain-a026)
+- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-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` может содержать business-сценарии авторизации, её browser/server assemblies и React-модули, но не страницу профиля или общий database client.
+
+Пакет не владеет исполняемой ответственностью. Конкретными API, состоянием, зависимостями и lifecycle владеют модули внутри него.
+
+Level 2 применяется к пакету, а не ко всему SLM root. Например, `auth` может быть пакетом Level 2, пока `catalog` и `news` остаются доменными модулями Level 1.
+
+## Корень пакета
+
+```text
+domains/auth/
+├── README.md
+├── business/
+├── assemblies/
+├── adapters/
+└── react/
+```
+
+В корне разрешены:
+
+- документация;
+- ownership metadata;
+- декларативный manifest или декларативная конфигурация архитектурной проверки;
+- обязательный модуль `business`;
+- обязательная непустая Group `assemblies`;
+- непустая Group `adapters`, если хотя бы одна фабрика имеет технические зависимости;
+- Framework Groups при наличии соответствующих модулей.
+
+В корне запрещены:
+
+- `index.ts` или другой агрегирующий executable entry point;
+- runtime-файлы и side effects;
+- изменяемое состояние и ресурсы lifecycle;
+- реэкспорт API внутренних модулей;
+- page-specific компоненты или сборка нескольких доменов.
+
+Metadata содержит только статические данные. Проверяющий инструмент может читать её декларативно, но она не исполняется и не становится скрытым API пакета.
+
+## Policy boundary
+
+Доменный пакет не является узлом import-графа. Его техническая граница состоит из объявленного проверке набора дочерних модулей и правил, применяемых ко всем связям через эту границу.
+
+Отсутствие root barrel намеренно:
+
+- client- и server-entry points не агрегируются в один импорт;
+- каждый модуль сохраняет отдельную ответственность и environment boundary;
+- переименование публичного модуля является изменением его собственного контракта, а не скрывается пакетом;
+- versioning целого publishable package остаётся за пределами Level 2.
+
+## Модули и Groups
+
+`business` размещается непосредственно в пакете. Assemblies находятся в обязательной Group `assemblies`. Production adapters являются самостоятельными модулями Group `adapters`. Framework Group называется по фреймворку: `react`, `vue` и аналогично.
+
+```text
+auth/
+├── business/ # SLM-модуль
+│ ├── index.ts # Только public types
+│ ├── factory.ts # Public factories entry
+│ └── runtime.ts # Необязательный deterministic runtime
+├── adapters/ # Group при наличии technical dependencies
+│ └── phone-http/ # SLM-модуль
+├── assemblies/ # Обязательная Group
+│ └── browser/ # SLM-модуль
+└── react/ # Framework Group
+ ├── session/ # SLM-модуль
+ └── login-form/ # SLM-модуль
+```
+
+Groups не имеют `index.ts`. Поэтому публичными путями являются `auth/business`, `auth/business/factory`, опциональный `auth/business/runtime`, `auth/adapters/phone-http`, `auth/assemblies/browser` и `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, а не внутренние модули пакета.
+
+Одна предметная область не представляется одновременно модулем и пакетом. Переход завершается удалением старой границы именно этого домена, а не переводом всех соседних областей.
+
+## Границы соседних слоёв
+
+| Ответственность | Владелец |
+|---|---|
+| Предметные сценарии, Domain API, доменные ошибки | `business` |
+| Техническая реализация зависимости одного домена | Adapter внутри пакета |
+| Сборка API для именованного контекста | Assembly внутри пакета |
+| Универсальный технический сервис | `infra` |
+| Domain-specific framework API | Модуль внутри `react`, `vue` и аналогичной Group |
+| Страница, маршрут, redirect, продуктовый текст | `compositions` |
+| UI, объединяющий несколько доменов | `compositions` |
+
+Зависимость от React сама по себе не доказывает принадлежность пакету. Решающим остаётся владелец ответственности.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/factory-ports-adapters.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/factory-ports-adapters.md
new file mode 100644
index 0000000..5da3f9e
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/factory-ports-adapters.md
@@ -0,0 +1,158 @@
+# Фабрики, зависимости и adapters
+
+> Пояснение границы между `business` и технической средой.
+
+## Связанные правила
+
+- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
+- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
+- [`SLM-L2-ERROR-R010`](../../rules/level-2.md#slm-l2-error-r010)
+- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
+- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
+- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
+- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
+- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
+- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
+
+## Одна фабрика на API
+
+```text
+явные зависимости + business factory → один Domain API
+```
+
+Модуль `business` предоставляет одну именованную фабрику для каждого объявленного API. Фабрики экспортируются через общий runtime-фасет `business/factory`:
+
+```ts
+import type {
+ AuthAdministrationApi,
+ AuthAdministrationDeps,
+ AuthSessionApi,
+ AuthSessionDeps,
+} from '@/domains/auth/business'
+
+export type AuthSessionFactory = (
+ deps: AuthSessionDeps,
+) => AuthSessionApi
+
+export type AuthAdministrationFactory = (
+ deps: AuthAdministrationDeps,
+) => AuthAdministrationApi
+```
+
+```ts
+import {
+ authAdministrationFactory,
+ authSessionFactory,
+} from '@/domains/auth/business/factory'
+```
+
+Фабрика не выбирает environment, assembly или конкретную production-реализацию зависимости. Разные API могут иметь разные наборы deps и собираться независимо.
+
+## Технические зависимости
+
+Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы.
+
+```ts
+export type AuthPhoneDependency = {
+ requestCode: (phone: string) => Promise
+ verifyCode: (code: string) => Promise
+}
+```
+
+`unknown` допустим на границе непроверенного внешнего результата. `business` проверяет его до превращения в предметные данные или состояние.
+
+Техническими зависимостями также являются:
+
+- concrete state/query runtime;
+- subscription и event source;
+- browser, Node.js и framework capabilities;
+- request data и abort signal;
+- текущее время и timer;
+- random и ID generator;
+- environment и runtime configuration provider.
+
+```ts
+export type VerificationDeps = {
+ clock: { now: () => number }
+ ids: { create: () => string }
+ timer: { delay: (ms: number) => Promise }
+}
+```
+
+`Date.now()`, `new Date()` без аргумента, `Math.random()`, `crypto.randomUUID()`, скрытый env и глобальный timer не читаются напрямую business-кодом, если влияют на поведение сценария. Детерминированная арифметика над переданным timestamp остаётся business-safe.
+
+Cancellation также описывается business-owned контрактом. Concrete `AbortSignal` или другой platform type остаётся внутри adapter, пока не принят отдельный общий публичный контракт.
+
+Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics.
+
+## Cross-domain API dependency
+
+Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом:
+
+```ts
+import type { AuthSessionApi } from '@/domains/auth/business'
+
+export type UserDeps = {
+ auth: Pick
+}
+```
+
+Runtime-значение место сборки графа передаёт через assembly либо напрямую зависимой business-фабрике. `user/business` не импортирует executable factory, assembly или instance Auth.
+
+Если exception-модели нужен runtime guard чужой ожидаемой ошибки, зависимый business может импортировать его из детерминированного `auth/business/runtime`. Этот импорт остаётся обычным ребром DAG.
+
+## Adapter module
+
+Adapter module соединяет явную техническую зависимость фабрики с конкретной системой:
+
+```text
+business dependency ← adapter → SDK / query runtime / platform / request data
+```
+
+Adapter преобразует аргументы и технический результат к контракту зависимости. Он не определяет публичный метод Domain API, предметный fallback или код доменной ошибки.
+
+Ожидаемый исходный сбой возвращается `business`, который выбирает собственный error code. Поэтому приложение не строит поведение по HTTP status, SDK error class или storage exception.
+
+Adapter может использовать TanStack Query, Apollo, Zustand или другой state/query runtime, если реализует business-owned dependency. Library types, query keys и mutable clients не протекают в public types `business`.
+
+## Размещение adapters
+
+Каждая связная production-реализация является отдельным SLM-модулем в Group `adapters`:
+
+```text
+auth/adapters/
+├── phone-http/
+│ └── index.ts
+├── browser-session/
+│ └── index.ts
+└── browser-runtime/
+ └── index.ts
+```
+
+Один adapter-модуль может реализовать несколько тесно связанных зависимостей одного technical provider. Например, `browser-runtime` может предоставить clock, timer и ID generator. Правило не требует отдельного модуля для каждого метода deps.
+
+Adapter module имеет собственные ответственность, публичный API, environment label и тестовую границу. Group `adapters` не имеет `index.ts` и не реэкспортирует дочерние модули.
+
+Production adapter запрещено определять:
+
+- закрытым сегментом assembly;
+- inline-функцией в `composition` или `app`;
+- частью framework binding module;
+- скрытой реализацией внутри `business`.
+
+Assembly и одноразовое место сборки импортируют конкретные adapter-модули через их публичные API:
+
+```ts
+import { authSessionFactory } from '@/domains/auth/business/factory'
+import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
+import { createBrowserRuntimeAdapter } from '@/domains/auth/adapters/browser-runtime'
+
+const session = authSessionFactory({
+ phone: createPhoneHttpAdapter(),
+ runtime: createBrowserRuntimeAdapter(),
+})
+```
+
+Если ни одна фабрика не имеет технических зависимостей, Group `adapters` не обязательна. Cross-domain API dependency не считается adapter и передаётся отдельно.
+
+Локальные fake implementations в business-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/framework-bindings.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/framework-bindings.md
new file mode 100644
index 0000000..42912fc
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/framework-bindings.md
@@ -0,0 +1,156 @@
+# Framework Groups и модули
+
+> Пояснение domain-specific framework-кода на примере React.
+
+## Связанные правила
+
+- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-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-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
+- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
+
+## 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`, состояния, реализации, lifecycle или агрегирующего API. Если пакет поддерживает Vue, рядом появляется отдельная Group `vue`.
+
+Каждый прямой дочерний каталог является обычным SLM-модулем со своей ответственностью, публичным API и узлом графа. Он не является вложенным модулем, потому что родительская граница `react` является Group.
+
+## Framework binding module
+
+Модуль принадлежит Framework Group, если его самостоятельная ответственность состоит в связывании одного или нескольких готовых Domain API своего домена с конкретным framework. Сам факт зависимости от framework недостаточен: framework-specific assembly остаётся в `assemblies`, а page-specific модуль остаётся в `compositions`.
+
+Framework binding module может:
+
+- передавать готовые API через Provider и context;
+- предоставлять domain-specific hooks;
+- отображать состояние и безопасные ошибки домена;
+- использовать framework-compatible state/query runtime;
+- реализовывать переиспользуемую domain-specific форму или guard;
+- связывать framework lifecycle с явными операциями Domain API.
+
+Он не вызывает business-фабрику или assembly, не выбирает adapters и не создаёт новые предметные сценарии.
+
+Framework binding импортирует типы и deterministic runtime через разные фасеты:
+
+```ts
+import type {
+ AuthError,
+ AuthSessionApi,
+} from '@/domains/auth/business'
+
+import {
+ AUTH_ERROR_CODES,
+ isAuthError,
+} from '@/domains/auth/business/runtime'
+```
+
+Импорт `business/factory` запрещён: готовые API передаются модулю извне.
+
+## Модуль session
+
+`auth/react/session` может владеть Provider и hooks доступа к уже созданному `AuthSessionApi`:
+
+```tsx
+'use client'
+
+type AuthSessionProviderProps = PropsWithChildren<{
+ api: AuthSessionApi
+}>
+
+export const AuthSessionProvider = ({
+ api,
+ children,
+}: AuthSessionProviderProps) => {
+ return (
+
+ {children}
+
+ )
+}
+```
+
+Публичный путь модуля:
+
+```ts
+import {
+ AuthSessionProvider,
+ useAuthSession,
+} from '@/domains/auth/react/session'
+```
+
+Импорт `@/domains/auth/react` запрещён, потому что Group не имеет API.
+
+## State/query runtime
+
+`auth/react/queries` может использовать TanStack Query, SWR или другой React runtime поверх готового `AuthSessionApi`. Query cache остаётся framework projection, пока данные и transitions проходят через API, а optimistic values создаются или проверяются business-владельцем.
+
+```ts
+export const useAuthSessionQuery = () => {
+ const api = useAuthSession()
+
+ return useQuery({
+ queryKey: ['auth', 'session'],
+ queryFn: api.getCurrentSession,
+ })
+}
+```
+
+Server prefetch и client hook могут быть разными модулями Framework Group с совместимыми environment markers. Общая query policy выносится в отдельный framework-модуль при наличии нескольких потребителей, а не дублируется в compositions.
+
+Подробности описаны в [Состоянии и кэше](./state-cache.md).
+
+## Модуль login-form
+
+`auth/react/login-form` может владеть переиспользуемой формой, если она работает только с Auth API, состоянием и ошибками своего домена. Она может использовать публичный API соседнего `auth/react/session`, если зависимость остаётся ацикличной.
+
+Конкретная страница, продуктовый текст, layout, redirect и выбор маршрута принадлежат `compositions`. Domain-owned guard может решить, разрешено ли действие, но политика перехода на `/login` остаётся у route composition.
+
+## Запрет cross-domain framework imports
+
+Framework binding module не импортирует hooks, contexts, Providers, components или framework state другого домена:
+
+```ts
+// Недопустимо: domains/user/react/profile
+import { useAuthSession } from '@/domains/auth/react/session'
+```
+
+Cross-domain UI собирается в `compositions`:
+
+```tsx
+const session = useAuthSession()
+
+return (
+
+)
+```
+
+Передача через props является границей композиции, но не требует prop drilling внутри домена: каждый пакет может использовать собственный Provider и context. Если User business постоянно нуждается в Auth, готовый `AuthSessionApi` передаётся User factory при сборке графа, а User framework module работает уже со своим API.
+
+## Публичные API
+
+```ts
+import { AuthSessionProvider } from '@/domains/auth/react/session'
+import { LoginForm } from '@/domains/auth/react/login-form'
+```
+
+Framework Group не реэкспортирует дочерние модули. Это сохраняет независимые ответственности и не превращает `react` в скрытый корневой модуль домена.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/open-questions.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/open-questions.md
new file mode 100644
index 0000000..ba92c91
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/open-questions.md
@@ -0,0 +1,52 @@
+# Открытые вопросы Level 2
+
+> Эти вопросы не являются правилами и не отменяют зафиксированные границы.
+
+## Зафиксированные решения
+
+- Level 1 является общей базой, а Level 2 применяется отдельно к выбранным предметным областям.
+- Доменный модуль Level 1 и доменный пакет Level 2 могут постоянно сосуществовать в одном SLM root.
+- Одна предметная область имеет только одну форму.
+- Корень пакета содержит только metadata, модуль `business` и допустимые Groups и не имеет executable API.
+- `business` может объявлять несколько независимо собираемых Domain API и одну фабрику для каждого API.
+- Публичный API `business` имеет обязательные type-only `business` и runtime `business/factory`, а также необязательный deterministic `business/runtime`.
+- Приложение получает предметную модель, transitions и результаты через Domain API; technical и framework cache могут хранить и проецировать эти значения.
+- Ожидаемые ошибки adapters и других доменов не пересекают API текущего домена в исходной форме.
+- Каждый доменный пакет содержит минимум одну assembly; универсальная изоморфная assembly не обязательна.
+- Assembly может создавать именованный граф нескольких API и не добавляет к ним сценарии.
+- При наличии технических зависимостей Group `adapters` обязательна, а каждая связная production implementation принадлежит adapter-модулю.
+- Runtime cross-domain imports через пакетную границу разрешены только из `business/runtime`; type-only public contracts разрешены и входят в DAG.
+- Framework Group называется по фреймворку и содержит самостоятельные SLM-модули.
+- Cross-domain framework state, hooks, contexts и components не импортируются.
+- Clock, timer, random, ID generator и environment являются явными dependencies business.
+- Assembly без собственного lifecycle-ресурса не обязана возвращать пустой `dispose`.
+
+## Владение состоянием
+
+Нужно подробнее определить создание initial state, применение transitions, persistence и внешний event input. Нормативным уже является то, что предметную модель и переходы определяет `business`, а concrete state manager реализует business-owned port либо framework projection.
+
+Отдельно требуется проверить SSR snapshot, hydration, concurrent rendering, reset и поведение после завершения request scope.
+
+## Передача ошибок
+
+Нужно выбрать общую рекомендацию exception или discriminated `Result`, определить cancellation и unexpected failures, а также сериализацию ошибок через RPC и server actions.
+
+Структура публичных фасетов от этого решения больше не зависит: type errors публикуются через `business`, а необходимые runtime codes и guards через опциональный `business/runtime`.
+
+## Технические порты
+
+Нужно определить, является ли consumer-owned port обязательной формой каждой технической зависимости и какие behavioral guarantees он описывает: timeout, retry, cancellation, idempotency, ordering, concurrency и subscription cleanup.
+
+Cross-domain API dependency уже зафиксирована как отдельный вид зависимости и не зависит от этого решения.
+
+## Lifecycle сборки
+
+Уже зафиксировано, что явная операция, запускающая ресурс, возвращает cleanup, а assembly с собственным ресурсом предоставляет cleanup handle результата. Ещё нужно определить async cleanup, rollback частичной сборки, repeated disposal, request abort и поведение API после завершения scope.
+
+## Cache hydration
+
+Нужно проверить единый способ разделять framework-neutral cache policy, client hooks, server prefetch и serialization boundary без утечки query-library types в Domain API.
+
+## Автоматическая проверка
+
+Нужно выбрать machine-readable формат для форм домена, metadata, модулей, Groups, entry points, environment labels и запрещённых транзитивных импортов.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/state-cache.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/state-cache.md
new file mode 100644
index 0000000..3c4992c
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/state-cache.md
@@ -0,0 +1,114 @@
+# Состояние и кэш
+
+> Пояснение границы между предметной властью business и техническими state/query runtimes.
+
+## Связанные правила
+
+- [`SLM-L2-BUSINESS-R006`](../../rules/level-2.md#slm-l2-business-r006)
+- [`SLM-L2-BUSINESS-A007`](../../rules/level-2.md#slm-l2-business-a007)
+- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
+- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
+- [`SLM-L2-BUSINESS-R018`](../../rules/level-2.md#slm-l2-business-r018)
+
+## Библиотеки не запрещены
+
+Запрет state/query manager в import-графе `business` не запрещает TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX или RxJS в доменном пакете. Он запрещает concrete runtime становиться предметным контрактом.
+
+Такая библиотека может находиться:
+
+- в adapter-модуле, если реализует техническую зависимость business-фабрики;
+- в framework binding module, если доставляет готовый Domain API конкретному framework;
+- в composition, если состояние принадлежит только её UI scope и не подменяет доменную модель.
+
+## Три вида состояния
+
+### Предметное состояние
+
+Модель фактов и переходов домена. `business` определяет initial value, validation, допустимые команды, transitions и selectors. Concrete store может быть передан фабрике как business-owned port:
+
+```ts
+export type AuthStateDependency = {
+ create: (initial: AuthState) => {
+ get: () => AuthState
+ set: (state: AuthState) => void
+ subscribe: (listener: () => void) => () => void
+ }
+}
+```
+
+Adapter поверх Zustand или другого manager реализует этот контракт, но не выбирает initial state и не добавляет переходы.
+
+### Technical source cache
+
+Кэш SDK, HTTP, GraphQL или storage-вызовов. Adapter может использовать QueryClient, Apollo cache или иной runtime для deduplication, transport retry и хранения внешних результатов. Business по-прежнему проверяет и преобразует результат до публикации предметной модели.
+
+Query keys и библиотечные result types остаются внутри adapter. Domain API не экспортирует `UseQueryResult`, `ApolloError`, raw DTO или mutable QueryClient.
+
+Если technical cache создаёт timers, subscriptions или другой lifecycle-ресурс для всего графа, владеющая им assembly передаёт cleanup graph owner. Cache без такого ресурса не требует искусственного `dispose`.
+
+### Framework projection cache
+
+Кэш, который framework binding строит поверх вызовов готового Domain API для rendering, Suspense, revalidation или UI coordination.
+
+```ts
+const useProfile = () => {
+ const api = useUserApi()
+
+ return useQuery({
+ queryKey: ['user', 'profile'],
+ queryFn: api.getProfile,
+ })
+}
+```
+
+Такой cache не является параллельным предметным источником, пока его значения происходят из Domain API и все предметные изменения выполняются через API.
+
+## Invalidation и retry
+
+Не каждая cache policy является бизнес-правилом.
+
+| Политика | Обычный владелец |
+|---|---|
+| Query key, stale time, deduplication, background refetch | Adapter или framework binding |
+| Rendering stale data, Suspense, polling UI | Framework binding или composition |
+| Transport retry безопасного запроса | Adapter |
+| Запрет повторной предметной команды | `business` |
+| Cooldown, лимит попыток, допустимый transition | `business` |
+| Freshness, влияющая на корректность сценария | `business` через явный контракт |
+
+Если после команды требуется invalidation, framework binding может связать успешный результат API с техническими keys. Если выбор invalidation выражает предметную семантику, business возвращает устойчивый outcome/event либо публикует чистое правило через `business/runtime`; binding только отображает его на библиотечные операции.
+
+## Optimistic updates
+
+Framework binding не конструирует произвольную предметную модель из input формы или transport DTO. Optimistic update допустим, когда предполагаемое значение:
+
+- возвращено командой Domain API как безопасная projection;
+- создано отдельным pure-методом Domain API;
+- создано или проверено публичной функцией `business/runtime`.
+
+```ts
+const optimisticProfile = projectProfileUpdate(currentProfile, command)
+
+queryClient.setQueryData(profileKey, optimisticProfile)
+```
+
+Здесь `projectProfileUpdate` принадлежит `business/runtime`, а `setQueryData` остаётся технической операцией framework binding.
+
+Запись raw form data или DTO напрямую в публичный product cache обходит предметного владельца и нарушает границу.
+
+## Browser, SSR и RSC
+
+Client hook, server prefetch и hydrate/dehydrate могут принадлежать разным framework binding modules с совместимыми environment entry points. Общая framework-specific cache policy выносится в отдельный модуль той же Framework Group, если у неё несколько реальных потребителей.
+
+`compositions` вызывает публичный server binding для prefetch и публичный client binding для rendering. Она не повторяет query keys и mapping доменных результатов.
+
+## Проверка на ревью
+
+Для каждого state/query runtime определяется:
+
+- является ли он adapter, framework projection или локальным UI state;
+- откуда поступают значения;
+- кто определяет transition и optimistic projection;
+- где находятся library-specific types и keys;
+- как invalidation соотносится с результатами Domain API;
+- соответствует ли cache lifecycle области жизни API и framework scope.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/domains/testing.md b/.opencode/skills/slm-design/reference/draft/level-2/domains/testing.md
new file mode 100644
index 0000000..3f84cee
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/domains/testing.md
@@ -0,0 +1,91 @@
+# Тестирование доменного пакета
+
+> Проверка владельцев и публичных границ Level 2.
+
+## Связанные правила
+
+- [`SLM-L2-TEST-R016`](../../rules/level-2.md#slm-l2-test-r016)
+- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-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-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
+- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
+- [`SLM-L2-BUSINESS-R024`](../../rules/level-2.md#slm-l2-business-r024)
+
+## Размещение
+
+Тест находится рядом с модулем-владельцем проверяемой ответственности. У доменного пакета нет общей корневой папки `tests`.
+
+| Проверяемая граница | Владелец теста |
+|---|---|
+| Сценарии, Domain API, данные и ошибки | `business` |
+| Публичная pure-функция или guard | `business/runtime` внутри тестов модуля business |
+| Техническое преобразование | Adapter |
+| Выбор API, dependencies и environment boundary | Assembly |
+| Provider, hook, query integration, form или guard | Соответствующий framework binding module |
+| Граф нескольких доменов | Модуль `composition`, точка входа `app` или другое место сборки |
+
+## Business через фабрику
+
+Каждый публичный сценарий проверяется через фабрику владеющего им API с управляемыми dependencies:
+
+```ts
+import type { AuthSessionApi } from '@/domains/auth/business'
+import { authSessionFactory } from '@/domains/auth/business/factory'
+import {
+ AUTH_ERROR_CODES,
+ isAuthError,
+} from '@/domains/auth/business/runtime'
+
+const api: AuthSessionApi = authSessionFactory(createAuthSessionTestDeps({
+ clock: { now: () => 1_700_000_000_000 },
+ phone: { requestCode: async () => ({ ok: true }) },
+}))
+
+await api.requestPhoneOtp('+79991112233')
+```
+
+Набор проверяет успешные и ожидаемые ошибочные результаты, validation, преобразование внешних данных и публичные изменения состояния. Способ assertion для ошибки зависит от `throw` или `Result`, но наружу проверяется только собственный AuthErrorCode.
+
+Business-тест не использует React, реальный SDK, database, production assembly или системные clock/random. Локальная fake implementation допустима в тесте и не становится adapter-модулем, потому что не входит в production graph.
+
+Если `business` объявляет несколько API, каждый тестируется через свою фабрику. Общая внутренняя pure-логика не требует повторять одинаковые cases на уровне всех API.
+
+## Остальные модули
+
+Тест adapter-модуля проверяет технический вызов, аргументы, преобразование результата и передачу исходного сбоя business-слою. Он не повторяет mapping в доменные ошибки.
+
+Тест обязательной assembly проверяет:
+
+- вызов только нужных business-фабрик;
+- точный именованный состав возвращённого графа;
+- выбор публичных adapter-модулей;
+- отсутствие несовместимого environment-кода;
+- передачу cross-domain API аргументом, а не импортом;
+- cleanup handle, если assembly создаёт ресурс жизненного цикла.
+
+Assembly без собственного lifecycle-ресурса не тестирует пустой `dispose`, потому что не обязана его предоставлять.
+
+Framework-тест импортирует только конкретный модуль, например `auth/react/session`, передаёт тестовый `AuthSessionApi` и проверяет Provider, hook или component. Query binding дополнительно проверяет keys, invalidation и отсутствие raw DTO в публичном результате, но не повторяет полный набор business-сценариев.
+
+## Автоматические структурные проверки
+
+Проверка файлов, exports и import-графа подтверждает:
+
+- отсутствие root API доменного пакета и Framework Groups;
+- обязательные `business` и `business/factory`, type-only exports в корневом barrel и допустимый опциональный `business/runtime`;
+- соблюдение матрицы потребителей фасетов business;
+- наличие непосредственно в корне пакета непустой Group `assemblies` с объявленными модульными границами;
+- отсутствие запрещённых runtime cross-domain imports;
+- отсутствие type-only импортов из чужих assemblies, adapters и framework-модулей;
+- отсутствие cross-domain framework hooks, contexts и components;
+- отсутствие server-only достижимости из client modules;
+- отсутствие runtime- и type-only циклов.
+
+## Архитектурное ревью
+
+На ревью проверяется, что `business/factory` экспортирует только объявленные фабрики, а `business/runtime` при наличии содержит только публичный deterministic runtime. Для каждой технической зависимости рассматриваются все production implementations: каждая связная implementation должна принадлежать одному модулю Group `adapters`, но один такой модуль может реализовать несколько тесно связанных capabilities одного provider.
+
+Отдельно проверяются прямые обращения business к `Date.now`, `Math.random`, `crypto.randomUUID`, timers, env и другим скрытым runtime-capabilities.
+
+Runtime-тест не заменяет автоматическую проверку или архитектурное ревью.
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/terminology.md b/.opencode/skills/slm-design/reference/draft/level-2/terminology.md
new file mode 100644
index 0000000..14de1e5
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/terminology.md
@@ -0,0 +1,147 @@
+# Терминология Level 2
+
+> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
+
+Level 2 наследует терминологию и матрицу слоёв Level 1. Он применяется отдельно к предметным областям, которым нужна пакетная форма, и не требует переводить остальные доменные модули того же SLM root.
+
+## Формы домена
+
+### Форма домена
+
+Структурное представление одной доменной ответственности. На Level 1 предметная область представлена доменным модулем. Для предметной области, использующей Level 2, этот модуль заменяется доменным пакетом. Одна предметная область не использует обе формы одновременно.
+
+### Доменный пакет
+
+Самостоятельная контейнерная предметная граница слоя `domains`, представляющая одну доменную ответственность. Доменный пакет не является модулем или Group. Он объединяет SLM-модули и Groups одной предметной области, но не имеет собственного исполняемого кода, состояния, жизненного цикла, публичного API или узла графа зависимостей.
+
+Корень пакета может содержать только декларативную metadata, обязательный модуль `business` и допустимые Groups. Metadata хранит статические данные о пакете, владении либо конфигурации проверки и не содержит кода, выполняемого приложением.
+
+Доменный пакет является policy boundary: проверка использует объявленную принадлежность модулей пакету для контроля структуры и междоменных связей. Публичными dependency boundaries кода остаются модули внутри пакета, а не пакет целиком.
+
+### Навигационная Group слоя `domains`
+
+Group, размещённая непосредственно в слое `domains` или другой такой Group. Она классифицирует доменные модули Level 1, доменные пакеты Level 2 и другие навигационные Groups, но не содержит исполняемый код и не образует dependency boundary.
+
+### Модуль доменного пакета
+
+Обычный SLM-модуль внутри доменного пакета. Его ответственность относится к одной роли пакета: business, assembly, adapter или framework binding. Каждый такой модуль имеет отдельную папку, публичный API и узел графа зависимостей.
+
+Модуль внутри Group пакета не является вложенным модулем, потому что его ближайшая внешняя граница не является модулем.
+
+## Business
+
+### Модуль business
+
+Обязательный SLM-модуль `business`, который определяет публичные предметные сценарии, один или несколько именованных Domain API, соответствующие им фабрики, типы зависимостей и публичные контракты ожидаемых ошибок.
+
+`business` является предметным владельцем данных, моделей, переходов и результатов сценариев домена. Он не зависит от конкретного фреймворка, среды или технической реализации.
+
+### Публичные фасеты business
+
+Объявленные entry points одного логического публичного API модуля `business`:
+
+| Путь | Статус | Содержимое |
+|---|---|---|
+| `business` | Обязательный | Только public types, включая Domain API, зависимости, factory types и error types |
+| `business/factory` | Обязательный | Только именованные runtime-фабрики Domain API |
+| `business/runtime` | Необязательный | Только публичные детерминированные runtime-значения и функции |
+
+Фасет `business/runtime` может публиковать устойчивые error codes и guards, validators, value constructors, предметные константы и чистые функции, если они нужны реальным внешним потребителям. Он не содержит фабрики, изменяемое состояние, ввод-вывод, сценарии с runtime-зависимостями или environment-specific код.
+
+Фасеты не являются сегментами, вложенными модулями или самостоятельными узлами графа. Любой другой внешний путь внутрь `business` является deep import.
+
+### Business-safe внешний пакет
+
+Внешняя библиотека, допустимая в import-графе `business`: детерминированная, environment-neutral, не выполняющая ввод-вывод и не владеющая изменяемым состоянием или runtime capability. SDK, generated client, storage, state manager, query runtime, framework и техническая интеграция не становятся business-safe только из-за совместимости с несколькими средами.
+
+### Domain API
+
+Именованный публичный runtime-контракт связного набора предметных сценариев внутри одного домена. Модуль `business` может объявить несколько Domain API, если они независимо собираются, имеют разные технические зависимости или нужны разным средам и потребителям.
+
+Разделение на API не создаёт новые доменные пакеты и не разрешает дублировать один сценарий в нескольких контрактах. Каждый публичный сценарий принадлежит ровно одному Domain API.
+
+### Фабрика business
+
+Публичная функция фасета `business/factory`, которая получает явные зависимости и создаёт экземпляр одного объявленного Domain API. Каждому Domain API соответствует ровно одна публичная фабрика. Фабрика не выбирает assembly или среду выполнения.
+
+### Предметная власть business
+
+Право определять публичную доменную модель, допустимые переходы, валидацию внешних значений и семантику результатов. Adapter, assembly или framework binding может хранить, кэшировать и проецировать значения Domain API, но не становится независимым источником предметной модели или переходов.
+
+### Доменная ошибка
+
+Безопасная публичная форма ожидаемого сбоя предметного сценария. Модуль `business` объявляет устойчивый readonly-тип с кодом; при необходимости runtime-коды и guards публикуются через `business/runtime`. Ошибки SDK, транспорта, storage, adapter или другого домена не являются доменными ошибками текущего API.
+
+Способ передачи ошибки, например exception или discriminated `Result`, не изменяет владение и публичную безопасность ошибки.
+
+## Техническая сборка
+
+### Техническая зависимость
+
+Явная runtime-возможность, необходимая business-фабрике и требующая production-реализации. К техническим зависимостям относятся источники данных, SDK, storage, API платформы, state/query runtime, технические сервисы, данные request scope, clock, timer, random, ID generator и другие источники ввода-вывода, состояния или недетерминизма.
+
+Type-only cross-domain API dependency, неизменяемая конфигурация и аргумент отдельного предметного сценария не являются техническими зависимостями.
+
+### Adapter
+
+SLM-модуль в Group `adapters`, который реализует одну или несколько связанных технических зависимостей business-фабрик поверх SDK, storage, state/query runtime, API платформы, данных запроса или технического сервиса. Одна связная production-реализация принадлежит одному adapter-модулю и не размещается внутри assembly или composition.
+
+Group `adapters` обязательна и непуста, если хотя бы одна business-фабрика имеет техническую зависимость. Фабрики без технических зависимостей не требуют создания этой Group.
+
+Точная обязательная поведенческая форма технических портов пока не определена и остаётся открытым вопросом Level 2.
+
+### Assembly
+
+SLM-модуль в Group `assemblies`, который создаёт явный именованный граф одного или нескольких Domain API пакета для одного реального контекста выполнения: браузера, запроса, server action или другого окружения. При наличии технических зависимостей assembly выбирает их adapter-модули и передаёт фабрикам готовые runtime-зависимости.
+
+Assembly может принимать готовые API других доменов аргументами. Она не добавляет предметные сценарии, методы API или собственные доменные ошибки.
+
+Каждый доменный пакет содержит минимум одну assembly. Архитектура не ограничивает их максимальное количество и не требует универсальной изоморфной assembly.
+
+### Ресурс assembly
+
+Ресурс жизненного цикла, который assembly сама создаёт для возвращаемого графа API. Создание фабрики или assembly не запускает скрытую долгоживущую работу. Если технический ресурс необходимо активировать при сборке, публичный результат assembly предоставляет cleanup handle; если ресурс запускается явной операцией API, эта операция предоставляет cleanup.
+
+Assembly без собственного ресурса жизненного цикла не обязана возвращать пустой `dispose`.
+
+## Framework binding
+
+### Framework Group
+
+Group доменного пакета, названная по конкретному фреймворку: `react`, `vue` и аналогично. Она содержит framework binding modules, не имеет собственного `index.ts`, реализации, состояния, жизненного цикла или публичного API.
+
+### Framework binding module
+
+SLM-модуль внутри Framework Group, ответственность которого ограничена domain-specific интеграцией с фреймворком. Например, `react/session` может владеть Provider и hooks сессии, а `react/login-form` - переиспользуемой формой авторизации.
+
+Framework binding module получает готовые Domain API, не вызывает фабрику или assembly и не импортирует framework-состояние, hooks или компоненты другого домена. Он может использовать совместимые с его ролью state/query libraries и технически кэшировать результаты Domain API, не создавая новую предметную модель.
+
+## Сборка графа
+
+### Место сборки графа
+
+Код модуля `composition`, точки входа `app`, request handler или test setup, который создаёт assemblies либо напрямую вызывает фабрики в ацикличном порядке и передаёт уже созданные API зависимым сборкам.
+
+Место сборки владеет областью жизни совокупного графа и вызывает предоставленные cleanup handles не позже её завершения. Это координационное владение не переносит к нему предметные или технические ответственности внутренних модулей.
+
+### Граница среды выполнения
+
+Граница между import-графами, предназначенными для клиента, сервера или обеих сред. Она определяется транзитивной достижимостью импортов, а не только именем папки или tree shaking.
+
+## Структурная модель
+
+```text
+SLM root
+└── domains
+ ├── доменный модуль Level 1
+ └── доменный пакет Level 2
+ ├── metadata
+ ├── модуль business
+ ├── обязательная Group assemblies
+ │ └── assembly-модуль
+ ├── Group adapters при наличии технических зависимостей
+ │ └── adapter-модуль
+ └── Framework Group react
+ ├── модуль session
+ └── модуль login-form
+```
diff --git a/.opencode/skills/slm-design/reference/draft/level-2/validation.md b/.opencode/skills/slm-design/reference/draft/level-2/validation.md
new file mode 100644
index 0000000..a01f05c
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/level-2/validation.md
@@ -0,0 +1,81 @@
+# Проверка Level 2
+
+> Граница автоматической проверки, архитектурного ревью и тестирования Level 2.
+
+## Конфигурация проекта
+
+Конфигурация проверки сопоставляет физические пути с доменными модулями Level 1, доменными пакетами Level 2, metadata, SLM-модулями, Groups, фасетами `business`, техническими зависимостями, adapter-модулями, assemblies, публичными точками входа, метками сред выполнения и allowlist внешних business-safe пакетов.
+
+Формат такой конфигурации пока не выбран. Независимо от формата проверка анализирует объявленные границы и import-граф, а не угадывает сущность только по имени папки.
+
+## Автоматическая проверка
+
+Автоматическая проверка блокирует:
+
+- одновременное объявление одной предметной области доменным модулем и пакетом;
+- исполняемый файл, root `index.ts`, состояние или реэкспорт в корне доменного пакета;
+- отсутствие `business` или несколько модулей `business` в одном пакете;
+- отсутствие `business` либо `business/factory`, runtime export из корневого barrel, export не-фабрики из `business/factory`, type export из `business/runtime`, другой публичный путь либо deep import внутри `business`;
+- импорт фасета `business` потребителем, которому этот фасет не разрешён;
+- отсутствие непосредственно в корне пакета непустой Group `assemblies` или наличие в ней прямого дочернего элемента без объявленной модульной границы;
+- deep imports во внутренние части модулей;
+- runtime- или type-only достижимость framework-, adapter-, assembly-, infra- или environment-specific кода из `business`;
+- запрещённый runtime-импорт через границу пакета Level 2;
+- type-only импорт не из публичной точки входа владельца;
+- импорт framework state, hooks, contexts или components другого домена;
+- достижимость server-only кода из client-entry point и обратное несовместимое направление;
+- runtime- или type-only циклы в графе модулей.
+
+Статический анализ может отдельно находить прямые вызовы `Date.now`, `Math.random`, `crypto.randomUUID`, timers и чтение env внутри `business`. Окончательное решение о скрытом недетерминизме остаётся за ревью.
+
+## Архитектурное ревью
+
+На ревью определяется:
+
+- представляет ли пакет одну связную предметную область;
+- принадлежит ли каждая соседняя область ровно одной форме независимо от форм других доменов;
+- принадлежат ли публичные сценарии ровно одному из именованных Domain API;
+- оправдано ли разделение API разными consumers, dependencies или assemblies, а не техническим дроблением;
+- остаются ли модель, validation и transitions под предметной властью `business`;
+- не создаёт ли state/query cache параллельную продуктовую модель или raw DTO boundary;
+- соответствует ли каждой фабрике ровно один API и остаётся ли она environment-neutral;
+- содержит ли `business/runtime` только реально публичные deterministic values и functions;
+- преобразует ли business ожидаемые technical и cross-domain сбои в собственные ошибки;
+- является ли каждая связная production-реализация технических dependencies отдельным модулем Group `adapters`;
+- представляет ли каждая assembly один реальный контекст выполнения и возвращает ли точный именованный граф;
+- не запускают ли фабрики и assemblies скрытую долгоживущую работу при создании графа;
+- предоставляет ли assembly cleanup только для действительно созданного ею lifecycle-ресурса и вызывает ли graph owner этот cleanup;
+- принадлежит ли каждый framework binding module домену, а не странице или multi-domain сценарию;
+- соответствует ли каждый объявленный business-safe внешний пакет ограничениям детерминированной библиотеки без runtime capability;
+- остаются ли Framework Groups и другие Groups без реализации и агрегирующего API.
+
+## Тестирование
+
+Business-сценарии проверяются через соответствующие фабрики с управляемыми test fakes, включая fake clock/random/id при необходимости. Adapter module проверяет technical transformation. Assembly проверяет состав графа, выбор adapters, environment boundary и условный cleanup. Framework binding module проверяет собственный Provider, hook, cache integration или component без повторения полного набора business-сценариев.
+
+Import-graph checks не заменяются runtime-тестами.
+
+## Смешанный SLM root
+
+Наличие доменных модулей Level 1 рядом с пакетами Level 2 является завершённым допустимым состоянием. Проверка применяет правила формы отдельно к каждой предметной области и правила Level 2 ко всем статическим связям, пересекающим пакетную границу.
+
+Переход одного домена завершается, когда его старая модульная граница удалена и checker видит только пакет. Другие домены не входят в критерий завершения.
+
+## Связанные правила
+
+- [`SLM-L2-DOMAIN-A003`](../rules/level-2.md#slm-l2-domain-a003)
+- [`SLM-L2-BUSINESS-A007`](../rules/level-2.md#slm-l2-business-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-BUSINESS-R018`](../rules/level-2.md#slm-l2-business-r018)
+- [`SLM-L2-BUSINESS-A019`](../rules/level-2.md#slm-l2-business-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-BUSINESS-A022`](../rules/level-2.md#slm-l2-business-a022)
+- [`SLM-L2-ASSEMBLY-R023`](../rules/level-2.md#slm-l2-assembly-r023)
+- [`SLM-L2-BUSINESS-R024`](../rules/level-2.md#slm-l2-business-r024)
+- [`SLM-L2-BUSINESS-R025`](../rules/level-2.md#slm-l2-business-r025)
+- [`SLM-L1-DEPENDENCY-A005`](../rules/level-1.md#slm-l1-dependency-a005)
+
+Скрипт `draft-rules.js` проверяет целостность реестров и ссылок документации, но не архитектуру приложения.
diff --git a/.opencode/skills/slm-design/reference/draft/rules/README.md b/.opencode/skills/slm-design/reference/draft/rules/README.md
new file mode 100644
index 0000000..ba6ef58
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/rules/README.md
@@ -0,0 +1,128 @@
+# Правила 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` | Домены |
+| `BUSINESS` | Контракты бизнес-логики |
+| `FACTORY` | Фабрики бизнес-логики |
+| `ERROR` | Ошибки домена |
+| `PORT` | Порты бизнес-логики |
+| `ADAPTER` | Адаптеры |
+| `ASSEMBLY` | Сборка API и жизненный цикл |
+| `ENVIRONMENT` | Границы сред выполнения |
+| `FRAMEWORK` | Модули фреймворков |
+| `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/.opencode/skills/slm-design/reference/draft/rules/level-1.md b/.opencode/skills/slm-design/reference/draft/rules/level-1.md
new file mode 100644
index 0000000..29669d7
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/rules/level-1.md
@@ -0,0 +1,110 @@
+# Правила 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/.opencode/skills/slm-design/reference/draft/rules/level-2.md b/.opencode/skills/slm-design/reference/draft/rules/level-2.md
new file mode 100644
index 0000000..e986bd5
--- /dev/null
+++ b/.opencode/skills/slm-design/reference/draft/rules/level-2.md
@@ -0,0 +1,171 @@
+# Правила SLM второго уровня
+
+Доменный пакет Level 2 соблюдает правила Level 1 и дополнительные правила этого реестра. Для предметной области в пакетной форме `SLM-L1-DOMAIN-R015` заменяется правилами доменного пакета; остальные предметные области того же SLM root могут сохранять форму доменного модуля Level 1. `SLM-L1-GROUP-R007` сохраняется для Groups внутри пакета и вне слоя `domains`, а для навигационных Groups слоя `domains` заменяется `SLM-L2-GROUP-R004`. Для модуля `business` правило `SLM-L1-MODULE-A004` уточняется правилом `SLM-L2-BUSINESS-A019`: объявленные фасеты вместе образуют один логический публичный API и не считаются deep imports. Ранее использовавшиеся номера `SLM-L2-DOMAIN-R001` и `SLM-L2-MIGRATION-A017` не переиспользуются после изменения модели уровней и перехода к постоянному совместному применению форм.
+
+## Граница доменного пакета
+
+### SLM-L2-DOMAIN-R002
+
+> **Предметная граница пакета**
+>
+> Каждая предметная область, использующая пакетную форму Level 2, представлена ровно одним доменным пакетом; все его модули относятся только к этой предметной области.
+
+### SLM-L2-DOMAIN-A003
+
+> **Корень доменного пакета**
+>
+> Корень доменного пакета содержит только декларативную metadata, модуль `business` и допустимые Groups и не содержит других прямых модулей, исполняемых файлов, изменяемого состояния, ресурсов жизненного цикла, публичного API или реэкспортов.
+
+### SLM-L2-GROUP-R004
+
+> **Навигационная Group доменов**
+>
+> Group, расположенная непосредственно в слое `domains` или другой такой Group, содержит только доменные модули Level 1, доменные пакеты Level 2 и навигационные Groups и не владеет реализацией, состоянием, жизненным циклом или публичным API.
+
+## Business и Domain API
+
+### SLM-L2-BUSINESS-R005
+
+> **Модуль business**
+>
+> Каждый доменный пакет содержит ровно один модуль `business`, который владеет публичными предметными сценариями и объявляет один или несколько именованных Domain API этой предметной области.
+
+### SLM-L2-BUSINESS-R006
+
+> **Предметная власть business**
+>
+> Adapters, assemblies и framework binding modules транспортируют, кэшируют или проецируют доменные данные только в форме, произведённой или проверенной публичным API либо детерминированным runtime модуля `business`, и не определяют независимую предметную модель, переход или результат сценария.
+
+### SLM-L2-BUSINESS-A007
+
+> **Импортная замкнутость business**
+>
+> Все runtime- и type-only импорты `business`, кроме междоменных зависимостей по `SLM-L2-DEPENDENCY-A012`, ведут только к файлам этого модуля, объявленным environment-neutral ресурсам `shared` и внешним пакетам, объявленным как business-safe.
+
+### SLM-L2-FACTORY-R008
+
+> **Фабрики Domain API**
+>
+> Каждому публичному Domain API соответствует ровно одна именованная фабрика фасета `business/factory`; фабрика получает явные runtime-зависимости, создаёт только этот API и не выбирает assembly или среду выполнения.
+
+## Ошибки домена
+
+### SLM-L2-ERROR-R009
+
+> **Публичный контракт ошибок**
+>
+> Каждый ожидаемый сбой публичного предметного сценария представлен именованным readonly-типом собственной доменной ошибки с устойчивым кодом; тип экспортируется через type-only barrel, а необходимые внешним потребителям runtime-коды и guards только через `business/runtime`.
+
+### SLM-L2-ERROR-R010
+
+> **Изоляция исходных ошибок**
+>
+> Сбой adapter, SDK, транспорта, storage или другого домена, доступный приложению через Domain API, представлен только безопасной ошибкой собственного доменного контракта и не раскрывает исходный объект, тип, message, status, payload или cause.
+
+## Assemblies и зависимости
+
+### SLM-L2-ASSEMBLY-R011
+
+> **Роль assembly**
+>
+> Каждая assembly является SLM-модулем одного именованного контекста выполнения, выбирает необходимые adapter-модули, вызывает одну или несколько business-фабрик и возвращает явный именованный граф их API, не добавляя предметные сценарии, методы API или собственные доменные ошибки.
+
+### SLM-L2-DEPENDENCY-A012
+
+> **Междоменные импорты Level 2**
+>
+> Статическая связь между разными доменными границами, хотя бы одна из которых является пакетом Level 2, допускает только type-only импорт публичного API доменного модуля Level 1 или корневого `business` пакета Level 2 либо runtime-импорт `business/runtime` пакета Level 2; все такие связи входят в общий ацикличный граф, а остальные runtime-экспорты другого домена не импортируются.
+
+### SLM-L2-ENVIRONMENT-A013
+
+> **Совместимость среды выполнения**
+>
+> Публичная точка входа, обозначенная как client-, server- или shared-entry point, не импортирует и не реэкспортирует код несовместимой среды выполнения во всём достижимом графе импортов.
+
+## 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 и не выполняет business-сборку, не выбирает adapters и не владеет страницей, маршрутом или multi-domain композицией.
+
+### SLM-L2-TEST-R016
+
+> **Проверка владельцев Level 2**
+>
+> Каждый публичный предметный сценарий проверяется через фабрику владеющего им Domain API, а основные тесты adapter, assembly и framework binding module проверяют только собственные публичные границы и не повторяют полный набор предметных сценариев.
+
+## Совместное применение форм
+
+### SLM-L2-DOMAIN-A026
+
+> **Однозначная форма домена**
+>
+> Каждая предметная область объявлена ровно в одной форме: как доменный модуль Level 1 либо как доменный пакет Level 2; один SLM root может одновременно содержать разные предметные области обеих форм.
+
+## Внешние библиотеки business
+
+### SLM-L2-BUSINESS-R018
+
+> **Business-safe внешний пакет**
+>
+> Внешний пакет объявляется business-safe только если он детерминирован, не выполняет ввод-вывод, не владеет изменяемым состоянием или runtime capability и не является SDK, generated client, storage, state/query manager, framework или другой технической интеграцией.
+
+## Публичные фасеты business
+
+### SLM-L2-BUSINESS-A019
+
+> **Публичные фасеты business**
+>
+> Публичный API `business` имеет обязательные entry points `business` только с type exports и `business/factory` только с именованными runtime-фабриками, может иметь `business/runtime` только с runtime exports и не имеет других публичных путей или deep imports.
+
+## Обязательные роли сборки
+
+### SLM-L2-ASSEMBLY-A020
+
+> **Обязательная Group assemblies**
+>
+> Корень каждого доменного пакета содержит ровно одну непустую Group `assemblies`, каждый прямой дочерний элемент которой является объявленной границей SLM-модуля.
+
+### SLM-L2-ADAPTER-R021
+
+> **Модули production adapters**
+>
+> Если хотя бы одна business-фабрика имеет техническую зависимость, корень доменного пакета содержит непустую Group `adapters`, а каждая связная production-реализация одной или нескольких таких зависимостей принадлежит ровно одному adapter-модулю этой Group и не определяется в другом месте production-графа.
+
+### SLM-L2-BUSINESS-A022
+
+> **Потребители фасетов business**
+>
+> Корневой `business` импортируется извне только через `import type`; `business/factory` импортируют только assemblies своего домена, `app`, `compositions` и тесты, а `business/runtime` импортируется только через объявленный публичный путь с соблюдением правил слоёв и междоменных зависимостей.
+
+## Жизненный цикл assembly
+
+### SLM-L2-ASSEMBLY-R023
+
+> **Cleanup ресурса assembly**
+>
+> Создание Domain API фабрикой или assembly не запускает скрытый ресурс жизненного цикла; ресурс запускается явной операцией с cleanup, а технический ресурс, который assembly обязана создать для графа, сопровождается публичным cleanup handle, вызываемым не позже завершения области жизни графа.
+
+## Недетерминизм business
+
+### SLM-L2-BUSINESS-R024
+
+> **Явные источники недетерминизма**
+>
+> Business-сценарий получает текущее время, timer, random, ID generator, environment и другие источники недетерминизма только через явные зависимости фабрики и не читает их из скрытого runtime-окружения.
+
+## Публичный runtime business
+
+### SLM-L2-BUSINESS-R025
+
+> **Детерминированный runtime business**
+>
+> Фасет `business/runtime` экспортирует только необходимые реальным внешним потребителям детерминированные значения и функции без ввода-вывода, изменяемого состояния, runtime capability или environment-specific поведения.
diff --git a/.opencode/skills/style-guide/SKILL.md b/.opencode/skills/style-guide/SKILL.md
new file mode 100644
index 0000000..8fff4f6
--- /dev/null
+++ b/.opencode/skills/style-guide/SKILL.md
@@ -0,0 +1,1226 @@
+---
+name: style-guide
+description: "Используй при создании, изменении, форматировании или ревью frontend-кода для соблюдения code style и style guide. Триггеры: JS, TS, JSX, TSX, HTML, CSS, .js, .ts, .jsx, .tsx, .module.css, React-компонент, props, стили, CSS Modules, JSDoc, документация, именование, импорты/экспорты, типизация, отформатировать по гайду, поправить кодстайл. НЕ используй для архитектуры SLM, выбора слоя/модуля/public API, генерации .templates, Next.js routing/data fetching/rendering, настройки PostCSS/tooling, backend-кода и вопросов без правки frontend-кода."
+---
+
+
+
+# Style guide
+
+## База style guide
+
+Применяй этот канон ко всем файлам перед языковыми правилами. Если правило языка уточняет базовое правило, используй языковое правило.
+
+### Порядок применения
+
+- Сначала проверь formatter, linter, `editorconfig`, `tsconfig` и локальный стиль затронутых файлов.
+- Если formatter или linter задаёт формат, следуй ему даже при отличии от этого канона.
+- Не меняй formatter, linter или `tsconfig` в задаче на обычную правку кода.
+- Меняй стиль только в затронутом коде. Не форматируй весь проект без отдельной задачи.
+- Сохраняй локальную консистентность, если она не конфликтует с автоматическими проверками.
+- Не переименовывай публичные сущности без отдельной задачи на переименование.
+
+### Базовый формат
+
+- Используй 2 пробела. Не используй табы.
+- Ориентируйся на 120 символов в строке, если проект не задаёт другой `lineWidth`.
+- Не переноси читаемую строку механически только ради лимита.
+- Переноси выражение на новые строки, когда строка становится плохо читаемой.
+- Не переноси строку внутри строкового литерала без необходимости.
+
+### Именование
+
+Следуй этим форматам, если проект не задаёт другой локальный стандарт.
+
+| Сущность | Формат |
+| --- | --- |
+| Папки и файлы | `kebab-case` |
+| Переменные и функции | `camelCase` |
+| Классы, типы, React-компоненты | `PascalCase` |
+| Константы верхнего уровня | `SCREAMING_SNAKE_CASE` |
+| Хуки | `useSomething` |
+| CSS-классы в CSS Modules | `camelCase` |
+| Enum-ключи | `SCREAMING_SNAKE_CASE` |
+
+### Файлы
+
+- Суффикс нужен, чтобы быстро определить роль или тип открытого файла без контекста дерева папок.
+- Суффикс пишется в единственном числе.
+- Формат ролевого файла: `name..ts` или `name..tsx`.
+- Список ниже - примеры типовых суффиксов, а не закрытый перечень.
+- Если роли файла нет в списке, добавь понятный суффикс по тому же принципу и сохраняй локальную консистентность проекта.
+- Не переименовывай публичные файлы без отдельной задачи на переименование.
+
+Типовые суффиксы:
+
+- `use-name.hook.ts` - файл хука, функция именуется `useName`.
+- `.store.ts` - store.
+- `.service.ts` - сервис.
+- `.type.ts` - типы и интерфейсы.
+- `.interface.ts` - интерфейсы, если проект отделяет их от типов.
+- `.enum.ts` - enum.
+- `.dto.ts` - внешние DTO.
+- `.schema.ts` - схемы валидации.
+- `.constant.ts` - константы.
+- `.config.ts` - конфигурация.
+- `.util.ts` - утилиты.
+- `.helper.ts` - вспомогательные функции.
+- `.lib.ts` - библиотечный код.
+- `.test.ts` или `.test.tsx` - тесты.
+- `.mock.ts` - моки.
+
+### Общие правила комментариев
+
+- Комментарий объясняет назначение, сценарий применения, ограничение или неочевидную причину.
+- Не добавляй комментарий, который дословно пересказывает код.
+- Однострочный комментарий отделяй пробелом после маркера.
+- Завершай комментарий точкой, если это фраза или предложение.
+- Заголовок комментария пиши по правилам русского языка: с заглавной только первая буква, кроме имён собственных и аббревиатур.
+- Если комментарий состоит из заголовка и тела, отделяй заголовок от тела одной пустой строкой.
+- Логические части внутри многострочного комментария отделяй одной пустой строкой.
+- Удаляй устаревший комментарий при изменении поведения кода.
+
+
+### Чеклист базы
+
+- Formatter, linter, `editorconfig`, `tsconfig` и локальный стиль файла соблюдены.
+- Diff не содержит форматирования незатронутого кода.
+- Отступы, длина строк и переносы не создают альтернативный стиль.
+- Имена соответствуют типу сущности и локальному стандарту проекта.
+- Комментарии объясняют причину или назначение, а не пересказывают код.
+
+## JS/TS
+
+Применяй этот раздел к `.js`, `.ts` и JS/TS-частям `.jsx` и `.tsx`. Для React-компонентов дополнительно применяй раздел `React и TSX`, для разметки - раздел `HTML, JSX и TSX`.
+
+### Строки
+
+- Для обычных строк используй одинарные кавычки.
+- Строки в обратных кавычках используй только для интерполяции `${...}` или многострочного текста.
+
+```ts
+const label = 'Сохранить';
+const title = `Привет, ${name}`;
+```
+
+### Импорты и экспорты
+
+- В именованных импортах ставь пробелы внутри фигурных скобок: `import { User } from './user'`.
+- Используй `import type`, если импорт нужен только на уровне типов.
+- Для собственного кода предпочитай именованные экспорты.
+- `default export` используй только когда это требует framework, tooling или устоявшийся локальный стандарт.
+- `default import` допустим для сторонних библиотек, CSS Modules и framework API.
+- Избегай namespace imports вида `import * as api`, если библиотека или локальный стандарт этого не требует.
+- Не импортируй глубже публичного API модуля, если проектная архитектура это запрещает.
+- Не меняй направление импортов ради удобства. Для архитектурных ограничений используй `slm-design`.
+- Не меняй порядок, путь или тип импортов только ради форматирования, кроме автоформатирования существующим formatter/linter.
+- Не создавай новый barrel или public API без понимания архитектурной границы.
+- Экспорт типов оформляй через `export type`, если экспортируется только тип.
+
+```ts
+import { createUser } from './create-user';
+import type { User } from './user.type';
+
+export { UserCard } from './user-card';
+export type { UserCardProps } from './types/user-card-props.type';
+```
+
+### Объекты, массивы, коллекции и вызовы
+
+- Короткие объекты, массивы и вызовы оставляй в одну строку, если они читаются.
+- В многострочном объекте размещай каждое свойство на новой строке.
+- В многострочном массиве размещай каждый элемент на новой строке.
+- В длинном вызове функции размещай каждый аргумент или смысловую группу на новой строке.
+- В однострочных объектах и массивах ставь пробелы после запятых.
+- Массивы называй во множественном числе.
+- Списки идентификаторов называй с суффиксом `Ids`.
+- Словари и мапы называй с суффиксом `ById`, `Map` или `Dict`.
+
+```ts
+const roles = ['admin', 'editor', 'viewer']
+const options = { id: 1, name: 'User' }
+const userIds = ['u1', 'u2']
+const usersById = {} as Record
+
+const config = createRequestConfig(
+ endpoint,
+ {
+ headers: {
+ 'X-Request-Id': requestId,
+ 'X-User-Id': userId
+ },
+ params: {
+ page,
+ pageSize,
+ sort: 'createdAt'
+ }
+ },
+ timeoutMs
+)
+```
+
+### Точки с запятой и trailing comma
+
+- По умолчанию не ставь точку с запятой в конце инструкций.
+- По умолчанию не ставь trailing comma после последнего свойства объекта, элемента массива, аргумента или параметра.
+- Если formatter, linter или локальный стиль файла задаёт другое поведение, следуй ему.
+- Не делай отдельную правку только ради массового добавления или удаления точек с запятой и trailing comma вне затронутого кода.
+
+### Early return
+
+- Используй ранние возвраты для упрощения чтения.
+- Избегай `else` после `return`.
+
+```ts
+const getName = (user?: { name: string }) => {
+ if (!user) {
+ return 'Гость';
+ }
+
+ return user.name;
+};
+```
+
+### Булевые значения
+
+- Булевые значения начинай с `is`, `has`, `can` или `should`.
+- Не используй нейтральные имена вроде `ready`, `access`, `submit`, если по имени неясно, что это boolean.
+
+```ts
+const isReady = true;
+const hasAccess = false;
+const canSubmit = true;
+const shouldRedirect = false;
+```
+
+### События и callback
+
+- Обработчики внутри кода называй через `handle*`.
+- Callback-параметры и callback-свойства называй через `on*`.
+
+```ts
+const handleSubmit = () => {
+ // ...
+};
+
+type FormOptions = {
+ onSubmit: () => void;
+};
+```
+
+### TypeScript
+
+- Указывай типы для параметров функций и компонентов.
+- Для публичных функций указывай возвращаемый тип.
+- Не полагайся на неявный вывод типов для публичных API и важных контрактов.
+- Предпочитай `type` для описания сущностей и `interface` для расширяемых контрактов.
+- Избегай `any` и `unknown` без необходимости.
+- Не используй `// @ts-ignore`, кроме крайних случаев с явным комментарием причины.
+- Если нужно временно подавить ошибку TypeScript, предпочитай `// @ts-expect-error` с объяснением причины.
+
+```ts
+/**
+ * Форматирует цену с символом валюты.
+ */
+export const formatPrice = (value: number): string => {
+ return `${value} ₽`;
+};
+```
+
+### Type vs interface
+
+- Используй `type` для props, DTO, view-model, unions, mapped types и композиции типов.
+- Используй `interface`, когда контракт должен расширяться через `extends` или declaration merging.
+- Не смешивай `type` и `interface` в одной области без причины.
+
+```ts
+export type UserCardProps = {
+ user: User;
+ onSelect: (userId: string) => void;
+};
+
+export interface StorageAdapter {
+ get(key: string): Promise;
+ set(key: string, value: string): Promise;
+}
+```
+
+### Any и unknown
+
+- `any` используй только как временную заглушку или при работе с внешним API, который невозможно типизировать сразу.
+- Каждый `any` должен иметь понятную причину и план замены, если это не неизбежная граница интеграции.
+- `unknown` используй на границах с внешними данными и обязательно сужай перед использованием.
+
+```ts
+const parseName = (value: unknown): string => {
+ if (typeof value === 'string') {
+ return value;
+ }
+
+ return '';
+};
+```
+
+Плохо:
+
+```ts
+const parseName = (value: any) => value.trim();
+```
+
+### Predicates и narrowing
+
+- Для runtime-проверок базовых значений используй `shared/lib/value-predicates`.
+- Если в проекте нет `shared/lib/value-predicates`, создай библиотеку перед использованием predicates. Состав утилит, сигнатуры и поведение бери из `reference/value-predicates` внутри skill.
+- Импортируй predicates из публичного API библиотеки, а не из внутренних файлов.
+- `shared/lib/value-predicates` содержит только базовые проверки nullish, primitives, strings, arrays, objects и literal unions.
+- Доменные guards вроде `isOrder`, `isCity`, `isUserError` размещай рядом с владельцем данных: business-модулем, mapper/source или infra-адаптером.
+- Не размещай доменные guards в `shared/lib/value-predicates`.
+
+Минимальная структура библиотеки:
+
+```text
+shared/lib/value-predicates/
+├── index.ts
+└── value-predicates.ts
+```
+
+```ts
+import { hasOwn, isArrayOf, isNonEmptyArray, isRecord, isString } from 'shared/lib/value-predicates'
+```
+
+Predicates применяй, когда нужно проверить:
+
+- значение определено или отсутствует;
+- значение является `string`, `number` или `boolean`;
+- строка является непустой;
+- массив пустой или непустой;
+- `unknown`-значение является массивом элементов нужной формы;
+- `unknown`-значение является объектом-записью;
+- значение входит в список допустимых литералов.
+
+Для определения пустого или непустого списка не используй прямые проверки длины массива:
+
+```ts
+items?.length
+items.length > 0
+items.length !== 0
+items.length === 0
+!items.length
+```
+
+Используй predicates:
+
+```ts
+isEmptyArray(items)
+isNonEmptyArray(items)
+```
+
+Для `unknown`-данных и внешних API-ответов не используй `Array.isArray(value)` как проверку формы элементов. Используй `isArrayOf` с item guard:
+
+```ts
+if (isArrayOf(value, isOrder)) {
+ // value: Order[]
+}
+```
+
+Для object-like `unknown` используй `isRecord` и `hasOwn` перед чтением полей:
+
+```ts
+const isOrder = (value: unknown): value is Order => {
+ return isRecord(value) && hasOwn(value, 'id') && isString(value.id)
+}
+```
+
+Для literal union используй `isOneOf`:
+
+```ts
+const statuses = ['draft', 'published'] as const
+
+if (isOneOf(value, statuses)) {
+ // value: 'draft' | 'published'
+}
+```
+
+Прямое обращение к `.length` допустимо, когда нужен именно числовой размер:
+
+```ts
+const count = items.length
+
+if (password.length < 8) {
+ return 'Минимум 8 символов'
+}
+
+for (let index = 0; index < items.length; index += 1) {
+ // ...
+}
+```
+
+Прямой `map` допустим, если массив заранее нормализован:
+
+```ts
+const items = data ?? []
+
+return items.map((item) => mapItem(item))
+```
+
+### Type assertions
+
+- Не используй `as` для обхода TypeScript без проверки данных.
+- Допускай `as const` для литеральных конфигураций и enum-like объектов.
+- Type assertion на границе внешних данных должен сопровождаться проверкой, схемой валидации или явной причиной.
+
+```ts
+const sortDirections = ['asc', 'desc'] as const;
+
+export type SortDirection = (typeof sortDirections)[number];
+```
+
+### Публичный API
+
+- Если файл является public API, экспортируй только то, что разрешено наружу.
+- Не экспортируй внутренние helpers, временные типы и implementation details.
+- Если задача требует изменить public API слоя или модуля, сначала применяй `slm-design`.
+
+### Документирование JS/TS
+
+#### Базовые правила
+
+- Документируй точку объявления именованной JS/TS-сущности.
+- JSDoc ставь непосредственно перед объявлением функции, класса, типа, интерфейса, enum или документируемой константы.
+- Если функция объявлена через `const name = () => {}`, комментарий ставь перед `const`.
+- Документируй назначение сущности, а не синтаксис её объявления.
+- Объясняй, зачем нужна сущность, какой сценарий она закрывает или какое ограничение важно знать.
+- Не документируй использование сущности, вызовы функций и строки внутри тела кода.
+- Не дублируй TypeScript-сигнатуру.
+- Не описывай параметры, возвращаемые значения и props через `@param`, `@returns` или `@type`, если они уже видны из типов.
+- Если назначение очевидно из имени и типа, всё равно добавь короткий JSDoc без пересказа сигнатуры.
+- React-компоненты документируй по правилам раздела `React и TSX`.
+
+#### Базовый шаблон многострочного комментария
+
+- Для документирования JS/TS-сущностей используй многострочный JSDoc-комментарий.
+
+```ts
+/**
+ * <Что делает сущность в 1 строке>.
+ *
+ * <Опционально: описание сложной механики или важных нюансов>.
+ */
+```
+
+#### Функции
+
+- Каждая именованная функция должна иметь многострочный JSDoc-комментарий.
+- Под правило попадают `function name()`, `const name = () => {}`, `const name = function () {}` и именованные обработчики вроде `const handleSubmit = () => {}`.
+- Комментарий функции описывает действие, результат, побочный эффект или важное ограничение.
+
+```ts
+/**
+ * Рекурсивно собирает дерево категорий из плоского списка.
+ *
+ * Группирует элементы по parentId, начиная с корневых категорий.
+ * Категории без родителя попадают в корень дерева.
+ */
+export const buildCategoryTree = (categories: Category[]): CategoryTree[] => {
+ // ...
+}
+```
+
+#### Классы
+
+- Каждый класс должен иметь многострочный JSDoc-комментарий.
+- Комментарий класса описывает роль класса и границу ответственности.
+- Не перечисляй методы класса, если их назначение понятно из public API.
+
+```ts
+/**
+ * Клиент для работы с заказами через REST API.
+ *
+ * Инкапсулирует маршруты, сериализацию параметров и обработку ответа.
+ */
+export class OrdersClient {
+ // ...
+}
+```
+
+#### Типы и интерфейсы
+
+- Каждый `type` и `interface` должен иметь многострочный JSDoc-комментарий.
+- Комментарий типа или интерфейса описывает контракт: модель, DTO, props, config, состояние или внешний формат.
+- Каждое поле `type` или `interface` должно иметь JSDoc-комментарий `/** ... */` непосредственно над полем.
+- Комментарий поля описывает смысл значения, доменное ограничение, формат или сценарий использования.
+- Не объединяй одним комментарием несколько полей.
+- Не отделяй поля пустой строкой только из-за JSDoc-комментария.
+- Пустую строку между полями добавляй только для смыслового разделения групп полей.
+- Если поле кажется очевидным, всё равно добавь короткое описание без пересказа имени.
+
+```ts
+/**
+ * Фильтры списка заказов.
+ */
+export type OrderFilters = {
+ /** Идентификатор пользователя-владельца заказов. */
+ userId?: string
+ /** Статус заказа для фильтрации выдачи. */
+ status?: OrderStatus
+}
+```
+
+#### Константы
+
+- Документируй константу только если она является публичным контрактом, доменным ограничением, magic value или переиспользуемой конфигурацией.
+- Обычные локальные константы не документируй.
+
+```ts
+/**
+ * Максимальное количество заказов на одной странице выдачи.
+ *
+ * Значение синхронизировано с ограничением backend API.
+ */
+export const MAX_ORDERS_PAGE_SIZE = 100
+```
+
+#### Enum
+
+- Каждый `enum` должен иметь многострочный JSDoc-комментарий.
+- Комментарий enum описывает назначение набора значений.
+- Каждое значение enum должно иметь JSDoc-комментарий `/** ... */` непосредственно над значением.
+- Комментарий значения enum описывает смысл значения или состояние, которое оно обозначает.
+- Не отделяй значения enum пустой строкой только из-за JSDoc-комментария.
+- Не оставляй значение enum без комментария, даже если оно кажется очевидным.
+
+```ts
+/**
+ * Состояние оплаты заказа.
+ */
+export enum PaymentStatus {
+ /** Оплата создана, но ещё не подтверждена провайдером. */
+ PENDING = 'pending',
+ /** Оплата успешно подтверждена провайдером. */
+ PAID = 'paid',
+ /** Оплата отклонена или завершилась ошибкой. */
+ FAILED = 'failed'
+}
+```
+
+#### Что не документировать
+
+- Не документируй обычные переменные и inline callback внутри вызовов вроде `useEffect`, `map`, `filter`, `reduce`, `forEach`, `then` или обработчиков API.
+- Если inline callback требует пояснения, вынеси его в именованную функцию и задокументируй объявление.
+
+#### Плохо
+
+```ts
+/**
+ * @param value - число.
+ * @returns строка с ценой.
+ */
+export const formatPrice = (value: number): string => {
+ return `${value} ₽`
+}
+```
+
+### Чеклист JS/TS
+
+- Строки, объекты, массивы, коллекции, вызовы, semicolon и trailing comma соответствуют дефолту или formatter/linter.
+- Early return упрощает чтение и не меняет поведение.
+- Булевые значения и callback названы по JS/TS-соглашениям.
+- Параметры функций типизированы; публичные функции имеют возвращаемый тип.
+- `any`, `unknown`, `as` и suppress-комментарии имеют явную причину.
+- Если `shared/lib/value-predicates` отсутствует, библиотека создана по `reference/value-predicates` перед использованием predicates.
+- Проверки пустых и непустых массивов используют `shared/lib/value-predicates`, а не прямые `.length`-условия.
+- `unknown` массивы проверяются через `isArrayOf(value, itemGuard)`, если важен тип элементов.
+- Object-like `unknown` проверяется через `isRecord` и `hasOwn` перед чтением полей.
+- Доменные guards размещены рядом с владельцем данных, а не в `shared/lib/value-predicates`.
+- Импорты типов оформлены через `import type`, а экспорты типов через `export type`.
+- Public API не раскрывает implementation details.
+- JS/TS-функции, классы, типы, интерфейсы, поля типов, enum, значения enum и константы-контракты документированы по правилам своего подраздела.
+
+## React и TSX
+
+Этот раздел описывает прикладную форму React UI-сущности после того, как архитектура уже определила её место: компонент внутри `ui/` или UI-модуль с корневым `.tsx`. Архитектурные границы компонентов, модулей, сегментов и public API определяет `slm-design`. Форму JSX-разметки дополнительно проверяй по разделу `HTML, JSX и TSX`.
+
+### Базовая React UI-сущность
+
+- Используй эту форму для базового React-компонента или UI-модуля с корневым компонентом.
+- Держи `.tsx`, props-типы, CSS Module и локальный `index.ts` в отдельных файлах.
+- Не объявляй props-типы внутри `.tsx`.
+- Не размещай CSS рядом с TSX-кодом: стили живут в `styles/{name}.module.css`.
+- Корневой CSS-класс всегда называется `.root`.
+- Компонент обязан иметь JSDoc-комментарий по React-шаблону.
+- Всё, что не является React-компонентом, документируй по правилам `Документирование JS/TS`.
+
+### Структура файлов
+
+- Базовая React UI-сущность живёт в собственной папке.
+- Файлы структуры обязательны для новой сущности: `.tsx`, `types/`, `styles/`, `index.ts`.
+- Имя файла props-типа строится как `{name}-props.type.ts`.
+- CSS Module строится как `{name}.module.css`.
+- `index.ts` экспортирует компонент и public props-тип.
+
+```text
+user-status/
+├── styles/
+│ └── user-status.module.css
+├── types/
+│ └── user-status-props.type.ts
+├── user-status.tsx
+└── index.ts
+```
+
+### Типизация props
+
+- Props-типы выноси в `types/{name}-props.type.ts`.
+- Собственные параметры компонента называй `{Name}Params`.
+- Атрибуты корневого HTML-элемента называй `RootAttrs`.
+- Итоговый тип props называй `{Name}Props`.
+- `RootAttrs` типизируй через `ComponentPropsWithoutRef<'tag'>`.
+- Исключай из `RootAttrs` атрибуты, которыми компонент управляет сам: например `children`, если компонент рендерит собственный контент.
+- Если собственный параметр конфликтует с HTML-атрибутом, исключай этот атрибут через `Omit`.
+- Документируй `Params`, `RootAttrs`, `Props` и каждое поле props по правилам `JS/TS`.
+
+```ts
+import type { ComponentPropsWithoutRef } from 'react'
+
+/**
+ * Собственные параметры UserStatus.
+ */
+export type UserStatusParams = {
+ /** Текст статуса пользователя. */
+ label: string
+ /** Доступен ли пользователь сейчас. */
+ isOnline: boolean
+}
+
+/**
+ * Атрибуты корневого span без children.
+ */
+type RootAttrs = Omit, 'children'>
+
+/**
+ * Props UserStatus.
+ */
+export type UserStatusProps = RootAttrs & UserStatusParams
+```
+
+### Реализация TSX
+
+- В `.tsx` держи сам компонент, импорт props-типа, импорт CSS Module и импорт функции склейки классов.
+- Функцию склейки классов импортируй как `cl`: `import cl from 'clsx'`.
+- Компонент объявляй через `const` и именованный экспорт, если framework или локальный стандарт не требует другой формы.
+- Не используй `React.FC` по умолчанию.
+- Параметр компонента называй `props` и типизируй через `{Name}Props`.
+- Возвращаемый тип компонента не указывай: TypeScript корректно выводит JSX-результат, а явный `ReactElement` сужает допустимые варианты возврата.
+- Деструктурируй props внутри тела компонента, а не в сигнатуре.
+- Из props обязательно выделяй `className` и `...rootAttrs`, если корневой элемент принимает HTML-атрибуты.
+- Прокидывай `rootAttrs` на корневой DOM-элемент.
+- Корневой элемент обязан получить `styles.root` первым CSS-классом.
+- Внешний `className` добавляй последним аргументом в `cl(...)`.
+- Не создавай вложенный компонент внутри render без причины.
+- Не дублируй условия в JSX, если их можно выразить через заранее подготовленную переменную с понятным именем.
+- Для сложной разметки сначала упрощай данные и условия, потом JSX.
+
+```tsx
+import cl from 'clsx'
+import type { UserStatusProps } from './types/user-status-props.type'
+import styles from './styles/user-status.module.css'
+
+/**
+ * Статус пользователя в карточке профиля.
+ *
+ * Используется для:
+ * - отображения текущей доступности пользователя
+ * - визуального выделения онлайн- и офлайн-состояний
+ */
+export const UserStatus = (props: UserStatusProps) => {
+ const { label, isOnline, className, ...rootAttrs } = props
+
+ return (
+
+ {label}
+
+ )
+}
+```
+
+### Props и events
+
+- Callback props называй через `on*`: `onSubmit`, `onChange`, `onClose`.
+- Внутренние обработчики называй через `handle*`: `handleSubmit`, `handleChange`, `handleClose`.
+- Не прокидывай событие DOM наружу, если внешний код должен знать только бизнес-смысл действия.
+- Boolean props называй через `is*`, `has*`, `can*` или `should*`.
+- Named handlers документируй как функции по правилам `JS/TS`.
+- Inline callback в JSX props не документируй.
+
+### Hooks
+
+- Соблюдай Rules of Hooks: вызывай hooks только на верхнем уровне компонента или другого hook.
+- Не используй `useEffect` для вычисления данных, которые можно получить из props/state во время render.
+- Выноси повторяемую stateful-логику в custom hook только когда она реально переиспользуется или упрощает компонент.
+- Custom hook называй через `use*`.
+- Вызовы `useEffect`, `useMemo`, `useCallback` и других hooks не документируй.
+- Inline callback внутри hooks не документируй.
+- Если логика hook требует комментария, вынеси её в именованную функцию или custom hook и документируй объявление по `JS/TS`.
+- Custom hook документируй как именованную функцию по `JS/TS`.
+
+### Стили
+
+- CSS Module размещай в `styles/{name}.module.css`.
+- Корневой CSS-класс всегда называй `.root`.
+- `.root` описывает реальный корневой DOM-элемент компонента.
+- Дополнительные классы описывай рядом с `.root` и подключай через `styles.*`.
+- Не подменяй `.root` внешним `className`: внешний класс только дополняет `styles.root`.
+
+```css
+.root {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--color-text-muted);
+}
+```
+
+### Локальный экспорт
+
+- В `index.ts` экспортируй компонент и public props-тип.
+- Компонент экспортируй обычным named export.
+- Props экспортируй через `export type`.
+- Не экспортируй `Params` и `RootAttrs` наружу без необходимости.
+
+```ts
+export { UserStatus } from './user-status'
+export type { UserStatusProps } from './types/user-status-props.type'
+```
+
+### Документирование компонентов
+
+- Каждый React-компонент должен иметь многострочный JSDoc-комментарий.
+- Комментарий ставь непосредственно перед объявлением компонента.
+- Компонент документируй по React-шаблону, а не по шаблону функции из `JS/TS`.
+- Компонент описывает назначение и сценарии применения.
+- Комментарий должен помогать понять, когда использовать компонент, без чтения реализации.
+- В `Используется для` указывай только реальные сценарии применения.
+- Добавляй минимум один сценарий; верхнего ограничения по количеству сценариев нет.
+- Не добавляй сценарии ради количества.
+- Не описывай реализацию вида `рендерит div с className`, если это видно из кода.
+- Всё, что не является React-компонентом, документируй по правилам `Документирование JS/TS`.
+- Props-типы, типы, enum, классы, функции, константы-контракты и named handlers подчиняются правилам `JS/TS`.
+- Вызовы `useEffect`, `useMemo`, `useCallback` и других hooks не документируй.
+- Inline callback внутри hooks, `map`, `filter`, event props и других вызовов не документируй.
+- Если inline callback требует пояснения, вынеси его в именованную функцию и задокументируй объявление по `JS/TS`.
+
+Шаблон:
+
+```tsx
+/**
+ * <Назначение компонента в 1 строке>.
+ *
+ * Используется для:
+ * - <сценарий 1>
+ * - <сценарий 2, если есть>
+ */
+```
+
+Плохо:
+
+```tsx
+/**
+ * Рендерит span с className и label.
+ */
+export const UserStatus = (props: UserStatusProps) => {
+ // ...
+}
+```
+
+### Чеклист React/TSX
+
+- Базовая React UI-сущность имеет `.tsx`, `types/`, `styles/` и `index.ts`.
+- Props вынесены в `types/{name}-props.type.ts` и собраны из `{Name}Params`, `RootAttrs`, `{Name}Props`.
+- Props-типы и поля props документированы по правилам `JS/TS`.
+- Компонент объявлен через `const`, принимает `props` и деструктурирует их внутри тела.
+- Компонент не использует `React.FC` по умолчанию и не указывает возвращаемый тип без необходимости.
+- Корневой DOM-элемент получает `{...rootAttrs}` и `className={cl(styles.root, ..., className)}`.
+- CSS Module лежит в `styles/{name}.module.css`, корневой класс называется `.root`.
+- `index.ts` экспортирует компонент и props-тип.
+- Каждый React-компонент имеет комментарий по React-шаблону с назначением и сценариями применения.
+- Callback props названы через `on*`, внутренние обработчики - через `handle*`.
+- Named handlers и custom hooks документированы по правилам `JS/TS`; вызовы hooks и inline callbacks не документируются.
+- Hooks вызваны только на верхнем уровне компонента или custom hook.
+
+## HTML, JSX и TSX
+
+Применяй этот раздел к HTML-разметке и JSX/TSX-деревьям. Для React-компонентов дополнительно применяй раздел `React и TSX`, для JS/TS-выражений внутри JSX/TSX - раздел `JS/TS`.
+
+### Атрибуты и props
+
+- HTML-теги и HTML-атрибуты пиши в нижнем регистре.
+- Строковые значения HTML/JSX/TSX-атрибутов пиши в двойных кавычках.
+- Динамические значения передавай через `{...}`.
+- Статические boolean-атрибуты записывай без `={true}`.
+- Динамические boolean-атрибуты записывай через выражение: `disabled={isSaving}`.
+- Не добавляй пустые, дублирующиеся или неиспользуемые атрибуты.
+- Props компонентов именуй по правилам `React и TSX`.
+
+```tsx
+
+```
+
+### Формат элемента
+
+- Короткий элемент с небольшим числом атрибутов или props можно оставлять в одну строку.
+- Если атрибутов или props много, размещай каждый на отдельной строке.
+- Закрывающую скобку многострочного элемента размещай на отдельной строке.
+- Если элемент содержит вложенные элементы, размещай открывающий и закрывающий теги на отдельных строках.
+- Не смешивай в одном участке разметки разные стили записи props без причины.
+
+```tsx
+
+
+
+```
+
+### Условия и значения в разметке
+
+- Не усложняй JSX/TSX условиями, которые можно подготовить в переменной до `return`.
+- Не размещай составные runtime-условия непосредственно в JSX/TSX.
+- Не используй тернарные выражения внутри JSX/TSX-разметки.
+- Подготовь label, локальные данные, boolean-флаги, списки и ветки отображения до JSX.
+- В JSX используй простые predicate gates или заранее подготовленные флаги.
+- Если для JSX-условий нужны predicates, но в проекте нет `shared/lib/value-predicates`, сначала создай библиотеку по `reference/value-predicates`.
+- Если ветвление большое, вынеси его в переменную, named function или отдельный компонент по правилам `React и TSX`.
+- Render-переменные вида `profileContent`, `ordersContent`, `emptyStateContent` используй только когда ветка большая, повторяется или содержит несколько состояний.
+- Если условие простое, не создавай render-переменную без необходимости.
+- Не дублируй длинные пути к данным внутри JSX: вынеси значение в локальную переменную до `return`.
+- Inline callback в JSX props не документируй; если ему нужно пояснение, вынеси в named handler.
+
+```tsx
+const submitLabel = isSaving ? 'Сохраняем' : 'Сохранить'
+const profileUser = currentUser.error ? null : currentUser.data
+const ordersData = orders.data
+const shouldShowEmptyState = isEmptyArray(ordersData) && !orders.isLoading
+
+return (
+ <>
+
+
+ {isDefined(profileUser) && (
+
+ )}
+
+ {isNonEmptyArray(ordersData) && ordersData.map((order) => (
+
+ ))}
+
+ {shouldShowEmptyState && (
+
+ )}
+ >
+)
+```
+
+Плохо:
+
+```tsx
+return (
+ <>
+
+
+ {data && !currentUser.error && (
+
+ )}
+
+ {orders.data?.length !== 0 && !orders.isLoading && orders.data?.map((order) => (
+
+ ))}
+ >
+)
+```
+
+Для conditional rendering списков не используй прямые проверки длины массива:
+
+```tsx
+items?.length
+items.length > 0
+items.length !== 0
+items.length === 0
+!items.length
+items && items.map(...)
+```
+
+Для уже типизированных массивов используй `isEmptyArray` и `isNonEmptyArray`:
+
+```tsx
+const ordersData = orders.data
+
+{isNonEmptyArray(ordersData) && (
+
+)}
+
+{isEmptyArray(ordersData) && !orders.isLoading && (
+
+)}
+```
+
+Для `unknown` или внешних API-данных используй `isArrayOf` с item guard:
+
+```tsx
+const ordersData = response.data
+
+{isArrayOf(ordersData, isOrder) && (
+
+)}
+```
+
+Перед проверкой выноси список в локальную переменную. Не дублируй длинный путь к данным внутри JSX:
+
+```tsx
+const itemsData = query.data
+
+{isNonEmptyArray(itemsData) && itemsData.map((item) => (
+
+))}
+```
+
+Render-переменные используй только для крупных или повторяющихся веток:
+
+```tsx
+const ordersContent = isNonEmptyArray(ordersData) ? (
+
+) : null
+
+return (
+ <>
+ {ordersContent}
+ >
+)
+```
+
+Для подготовленных boolean-значений используй смысловые префиксы:
+
+- `is*` - состояние или классификация: `isLoading`, `isProfileReady`.
+- `has*` - наличие значения или содержимого: `hasOrders`, `hasError`.
+- `can*` - возможность действия: `canSubmit`, `canEditProfile`.
+- `should*` - UI-решение: `shouldShowEmptyState`, `shouldDisableSubmit`.
+
+Для локальных данных используй имена с суффиксами `Data`, `Items`, `List`:
+
+```tsx
+const ordersData = orders.data
+const cityItems = citySuggestions.data
+```
+
+### Wrapper-элементы и семантика
+
+- Не добавляй wrapper-элемент только ради форматирования.
+- Не добавляй wrapper, если он меняет DOM-структуру, CSS-поведение или доступность.
+- Не меняй HTML-семантику или ARIA в задаче на чистое форматирование.
+- Если задача требует выбрать семантический тег, ARIA или поведение формы, решай это как отдельную semantic/accessibility-задачу, а не как style-only правку.
+
+```tsx
+return (
+
+
{title}
+ {children}
+
+)
+```
+
+### Комментарии
+
+- В HTML/JSX/TSX-разметке по умолчанию обходись без комментариев.
+- Комментируй только хаки, внешние ограничения и неочевидные причины.
+- JSX/TSX-комментарий оформляй только однострочно: `{/* Комментарий. */}`.
+- HTML-комментарий оформляй только однострочно: ``.
+- Не используй многострочные комментарии в HTML/JSX/TSX-разметке.
+- Не оставляй комментарий в разметке, если можно дать блоку, компоненту или переменной понятное имя.
+- Не комментируй очевидную структуру, семантику или назначение элемента.
+
+### Чеклист HTML/JSX/TSX
+
+- Атрибуты и props оформлены единообразно с локальным стилем файла.
+- Строковые атрибуты используют двойные кавычки.
+- Boolean-атрибуты записаны без лишнего `={true}`.
+- Многострочный элемент читается без горизонтального скролла.
+- В JSX/TSX-разметке нет тернарных выражений.
+- JSX использует подготовленные локальные данные, `is*`, `has*`, `can*`, `should*` или простые predicate gates вместо составных runtime-условий в разметке.
+- Conditional rendering списков использует `isEmptyArray`, `isNonEmptyArray` или `isArrayOf`, а не прямые `.length`-условия в JSX.
+- Длинные пути к данным не дублируются внутри JSX: список выносится в локальную переменную до `return`.
+- Render-переменные не создаются для простых условий без необходимости.
+- Нет пустых, дублирующихся или случайных wrapper-элементов.
+- Комментарии в HTML/JSX/TSX используются только для хаков, внешних ограничений и неочевидных причин.
+
+## CSS
+
+Этот канон описывает, как писать CSS без привязки к frontend-фреймворку, препроцессору или PostCSS-настройке. Он не описывает установку tooling, подключение плагинов и диагностику сборки.
+
+### Базовый подход
+
+- По умолчанию стили пишутся модульно: локальный UI-стиль живёт в CSS Module владельца.
+- CSS Module принадлежит конкретному компоненту или модулю.
+- Не импортируй CSS Module одного компонента или модуля в другой.
+- Не выноси локальные классы компонента в global styles.
+- Глобальные стили используй только для проектных основ: tokens, media, reset, typography, themes.
+- Если стиль должен переиспользоваться, выноси переиспользуемую UI-сущность или token, а не общий CSS Module.
+
+### CSS Modules
+
+- Корневой класс CSS Module всегда называй `.root`.
+- Обычные классы внутри CSS Module называй в `camelCase`.
+- Не смешивай `camelCase`, BEM и `kebab-case` в одном CSS Module.
+- Внешний `className` должен дополнять `.root`, а не заменять его.
+- Модификаторы оформляй отдельными короткими классами с `_`: `._active`, `._disabled`, `._open`.
+
+```css
+.root {
+ display: flex;
+}
+
+.title {
+ color: var(--color-text);
+}
+
+.button {
+ opacity: 1;
+
+ &._disabled {
+ opacity: 0.5;
+ }
+}
+```
+
+### Форматирование
+
+- Используй 2 пробела. Не используй табы.
+- В каждом CSS-правиле размещай одно свойство на строку.
+- Между CSS-правилами верхнего уровня оставляй одну пустую строку.
+- Перед каждым вложенным блоком оставляй одну пустую строку.
+- Если проект не задаёт порядок, группируй свойства так: позиционирование, блочная модель, оформление, текст, прочее.
+- Не меняй порядок свойств во всём файле без отдельной задачи на форматирование.
+
+```css
+.root {
+ position: relative;
+ display: flex;
+ width: 100%;
+ padding: var(--space-4);
+ border-radius: var(--radius-2);
+ background-color: var(--color-bg);
+ color: var(--color-text);
+}
+```
+
+### Вложенность
+
+- Не вкладывай селекторы друг в друга без необходимости.
+- Разрешённая вложенность: `@media`, псевдоклассы, псевдоэлементы, modifier-классы.
+- Не вкладывай элементы компонента внутрь `.root`; заводи отдельный класс.
+- Не пиши каскад вида `.root .title`, если можно использовать локальный класс `.title`.
+- Каждый вложенный блок отделяй пустой строкой от свойств и соседних вложенных блоков.
+
+```css
+.root {
+ display: flex;
+ color: var(--color-text);
+
+ &:hover {
+ color: var(--color-primary);
+ }
+
+ &::before {
+ content: '';
+ }
+
+ &._active {
+ opacity: 1;
+ }
+
+ @media (--md) {
+ display: grid;
+ }
+}
+
+.title {
+ color: var(--color-text);
+}
+```
+
+Плохо:
+
+```css
+.root {
+ display: flex;
+
+ .title {
+ color: var(--color-text);
+ }
+}
+```
+
+### Модификаторы
+
+- Модификатор оформляй отдельным коротким классом с `_`: `._active`, `._disabled`, `._open`.
+- Модификатор описывай через вложенность внутри базового класса: `&._active`.
+- Не кодируй состояние через BEM-цепочки вроде `.button_active`.
+- Не создавай отдельный modifier-файл.
+- Не используй модификатор для самостоятельной сущности: если стиль можно назвать отдельным элементом, заведи отдельный класс.
+
+```css
+.root {
+ opacity: 1;
+
+ &._disabled {
+ pointer-events: none;
+ opacity: 0.5;
+ }
+}
+```
+
+### Media queries
+
+- Mobile First обязателен: базовое состояние описывает минимальный viewport без media query.
+- Стили для больших viewport добавляй поверх базового состояния через `@media (--*)`.
+- Запрещено писать desktop-first: сначала desktop-стили, потом откатывать их вниз через `max-width`.
+- Запрещено игнорировать Mobile First ради удобства конкретного блока.
+- По умолчанию используй custom media: `@media (--md)`, `@media (--lg)`, `@media (--h-md)`.
+- Локальный `@media (min-width: ...)` допустим только как исключение для уникального порога, который нужен одному конкретному компоненту и не подходит для общей media-шкалы.
+- Локальный `@media (min-width: ...)` не отменяет Mobile First: это тоже расширение вверх от базового состояния.
+- У локального breakpoint обязательно оставляй короткий комментарий с причиной исключения.
+- Если breakpoint совпадает с общей шкалой или может понадобиться повторно, используй существующий custom media или добавь новый в `media.css`.
+- `@media` пиши внутри селектора, который изменяется.
+- Не пиши `@media` верхнего уровня с набором селекторов внутри.
+- Если шкалы media не хватает, расширяй `media.css` как проектный стандарт, а не добавляй локальный breakpoint.
+
+```css
+.root {
+ display: grid;
+ grid-template-columns: 1fr;
+
+ @media (--md) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ /* Уникальный порог, где карточки этого блока помещаются в 3 колонки. */
+ @media (min-width: 68.75rem) {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+```
+
+Плохо:
+
+```css
+@media (--md) {
+ .root {
+ display: flex;
+ }
+
+ .title {
+ font-size: 20px;
+ }
+}
+
+.root {
+ @media (min-width: 48rem) {
+ display: flex;
+ }
+}
+
+.card {
+ display: flex;
+
+ /* Плохо: desktop-first откат вниз вместо Mobile First расширения вверх. */
+ @media (max-width: 47.9375rem) {
+ display: block;
+ }
+}
+```
+
+### media.css
+
+- Все custom media объявляй в отдельном файле `media.css`.
+- `media.css` содержит только `@custom-media`.
+- Не объявляй custom media внутри CSS Modules.
+- Не дублируй breakpoints локально в компонентах.
+- Имена ширины пиши короткой шкалой `--xs` ... `--3xl`.
+- Имена высоты пиши с префиксом `--h-`.
+- Значения ширины пиши через `min-width`, кроме `--xs` как диапазона меньше `--sm`.
+- Значения высоты пиши через `min-height`.
+- Используй `rem` для значений media; пиксельный эквивалент указывай комментарием.
+
+Базовый список media:
+
+```css
+/* Ширина: Mobile First, кроме --xs. */
+@custom-media --xs (max-width: 35.9375rem); /* до 575px */
+@custom-media --sm (min-width: 36rem); /* 576px */
+@custom-media --md (min-width: 48rem); /* 768px */
+@custom-media --lg (min-width: 62rem); /* 992px */
+@custom-media --xl (min-width: 75rem); /* 1200px */
+@custom-media --2xl (min-width: 88rem); /* 1408px */
+@custom-media --3xl (min-width: 120rem); /* 1920px */
+
+/* Высота. */
+@custom-media --h-xs (min-height: 41.6875rem); /* 667px */
+@custom-media --h-sm (min-height: 43.875rem); /* 702px */
+@custom-media --h-md (min-height: 50.625rem); /* 810px */
+@custom-media --h-lg (min-height: 56.25rem); /* 900px */
+@custom-media --h-xl (min-height: 62.5rem); /* 1000px */
+@custom-media --h-2xl (min-height: 68.75rem); /* 1100px */
+@custom-media --h-3xl (min-height: 75rem); /* 1200px */
+```
+
+### Tokens и значения
+
+- Цвета, радиусы, spacing и повторяемые смысловые значения используй через CSS variables.
+- Локальное одноразовое значение можно оставить в CSS Module.
+- Не выноси значение в token только ради форматирования.
+- Не меняй token-систему в задаче на локальную CSS-правку.
+- Не дублируй смысловое значение в нескольких CSS Modules, если оно уже является token.
+
+```css
+.root {
+ padding: var(--space-4);
+ border-radius: var(--radius-2);
+ background-color: var(--color-bg);
+}
+```
+
+### Комментарии
+
+- CSS-комментарий оформляй через `/* Комментарий. */`.
+- Комментируй только неочевидные ограничения, hacks и причины.
+- Не комментируй очевидные CSS-свойства.
+- Удаляй устаревший комментарий при изменении поведения стиля.
+
+### Чеклист CSS
+
+- Локальный UI-стиль написан через CSS Module владельца.
+- Корневой класс CSS Module называется `.root`.
+- Классы CSS Module названы в `camelCase`, модификаторы - через `._modifier`.
+- Одно CSS-свойство размещено на строку.
+- Между правилами верхнего уровня и перед вложенными блоками есть пустая строка.
+- Вложенность используется только для `@media`, псевдоклассов, псевдоэлементов и модификаторов.
+- Mobile First соблюдён: базовое состояние написано без media query, расширения идут вверх через `@media (--*)`.
+- Desktop-first и откаты вниз через `max-width` не используются.
+- Breakpoints из общей шкалы используются через `@media (--*)`; локальные `min-width` есть только как обоснованные одноразовые исключения с комментарием причины.
+- Все custom media объявлены в `media.css`.
+- `media.css` содержит только `@custom-media`.
+- Повторяемые смысловые значения оформлены через CSS variables.
+- CSS-комментарии объясняют неочевидную причину, а не пересказывают свойства.
diff --git a/.opencode/skills/style-guide/agents/openai.yaml b/.opencode/skills/style-guide/agents/openai.yaml
new file mode 100644
index 0000000..301477e
--- /dev/null
+++ b/.opencode/skills/style-guide/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "Style Guide"
+ short_description: "Frontend code style, naming and local consistency"
+ default_prompt: "Use $style-guide to format or review frontend code according to the project style guide and local code conventions."
diff --git a/.opencode/skills/style-guide/reference/value-predicates/README.md b/.opencode/skills/style-guide/reference/value-predicates/README.md
new file mode 100644
index 0000000..23219cd
--- /dev/null
+++ b/.opencode/skills/style-guide/reference/value-predicates/README.md
@@ -0,0 +1,154 @@
+# Value Predicates
+
+`value-predicates` — небольшая библиотека runtime-предикатов для безопасной работы с `unknown`, `null`, массивами и объектами.
+
+Файл содержит два типа утилит:
+
+- Type guards: возвращают `value is T` и сужают тип в TypeScript.
+- Boolean-предикаты: возвращают `boolean` и используются для читаемых условий без обязательного narrowing.
+
+Проверки доменных DTO и API-ответов не размещаются здесь. Они должны жить рядом с владельцем данных: business-модулем, mapper/source или infra-адаптером.
+
+## Группы
+
+- `Nullish`: `isDefined`, `isNotDefined`.
+- `Primitives`: `isString`, `isNumber`, `isBoolean`.
+- `Strings`: `isNonEmptyString`.
+- `Arrays`: `isArray`, `isArrayOf`, `isEmptyArray`, `isNonEmptyArray`.
+- `Objects`: `isRecord`, `hasOwn`.
+- `Combinators`: `isOneOf`.
+
+## Правило для TSX
+
+В conditional rendering не пишем голые проверки длины массива:
+
+```tsx
+items?.length
+items.length > 0
+items.length !== 0
+items.length === 0
+!items.length
+items && items.map(...)
+```
+
+Для списков используем `isEmptyArray`, `isNonEmptyArray`, а для `unknown`-данных — `isArrayOf`.
+
+Голый `.length` допустим, когда нужен именно числовой размер для текста, расчётов или атрибутов.
+
+## Уже типизированный массив
+
+```tsx
+const ordersData = orders.data
+
+{isNonEmptyArray(ordersData) && (
+
+)}
+```
+
+В этом сценарии `ordersData` уже имеет тип вроде `Order[] | null | undefined`, поэтому достаточно проверить, что массив существует и не пуст.
+
+## Empty state
+
+```tsx
+const ordersData = orders.data
+const shouldShowEmptyState = isEmptyArray(ordersData) && !orders.isLoading
+
+{shouldShowEmptyState && (
+
+)}
+```
+
+`isEmptyArray` считает `null` и `undefined` пустым списком. Это удобно для UI-состояний, где отсутствие данных и пустой список показывают один empty state.
+
+## Map в JSX
+
+```tsx
+const itemsData = items
+
+{isNonEmptyArray(itemsData) && itemsData.map((item) => (
+
+))}
+```
+
+Сначала сужаем локальную переменную, потом используем её в `map`. Не дублируем путь к данным внутри JSX.
+
+## Unknown/API данные
+
+```tsx
+type Order = {
+ id: string
+ title: string
+}
+
+const isOrder = (value: unknown): value is Order => {
+ return (
+ isRecord(value) &&
+ hasOwn(value, 'id') &&
+ isString(value.id) &&
+ hasOwn(value, 'title') &&
+ isString(value.title)
+ )
+}
+
+const ordersData = response.data
+
+{isArrayOf(ordersData, isOrder) && (
+
+)}
+```
+
+`isArrayOf` нужен на границе с недоверенными данными: он проверяет не только массив, но и каждый элемент через item guard.
+
+Если нужно одновременно проверить форму элементов и непустой массив:
+
+```tsx
+const canRenderOrders = isArrayOf(ordersData, isOrder) && isNonEmptyArray(ordersData)
+
+{canRenderOrders && (
+
+)}
+```
+
+Если такой паттерн повторится много раз, можно добавить отдельный `isNonEmptyArrayOf`, но заранее его не вводим.
+
+## Object fields
+
+```ts
+if (isRecord(value) && hasOwn(value, 'code') && isString(value.code)) {
+ // value.code: string
+}
+```
+
+`isRecord` проверяет только базовую форму объекта: не `null` и не массив. Конкретные поля всегда проверяются отдельно.
+
+## Literal unions
+
+```ts
+const statuses = ['draft', 'published'] as const
+
+if (isOneOf(value, statuses)) {
+ // value: 'draft' | 'published'
+}
+```
+
+`isOneOf` удобен для runtime-проверки union-типов, собранных из `as const` массивов.
+
+## Нормализованный массив
+
+Если массив заранее нормализован, прямой `map` допустим:
+
+```tsx
+const ordersData = orders.data ?? []
+
+return ordersData.map((order) => (
+
+))
+```
+
+Для conditional rendering empty state всё равно используем predicate:
+
+```tsx
+{isEmptyArray(ordersData) && (
+
+)}
+```
diff --git a/.opencode/skills/style-guide/reference/value-predicates/index.ts b/.opencode/skills/style-guide/reference/value-predicates/index.ts
new file mode 100644
index 0000000..1b84539
--- /dev/null
+++ b/.opencode/skills/style-guide/reference/value-predicates/index.ts
@@ -0,0 +1 @@
+export * from './value-predicates'
diff --git a/.opencode/skills/style-guide/reference/value-predicates/value-predicates.ts b/.opencode/skills/style-guide/reference/value-predicates/value-predicates.ts
new file mode 100644
index 0000000..fa95751
--- /dev/null
+++ b/.opencode/skills/style-guide/reference/value-predicates/value-predicates.ts
@@ -0,0 +1,169 @@
+/**
+ * Value predicates для проверки runtime-значений.
+ *
+ * Type guards в этом файле сужают unknown-данные только до базовых типов.
+ * Boolean-предикаты используются для читаемых условий и не обязаны сужать тип.
+ * Проверки доменных DTO и API-ответов размещаются рядом с владельцем данных.
+ */
+
+/* --- Nullish --- */
+
+/**
+ * Исключает только null и undefined из типа значения.
+ *
+ * `0`, `false` и пустая строка не считаются отсутствующими.
+ * Удобно для `.filter(isDefined)`.
+ *
+ * @example
+ * ```ts
+ * const values = [0, null, false, undefined, '']
+ * const definedValues = values.filter(isDefined)
+ * // definedValues: Array<0 | false | ''>
+ * ```
+ */
+export const isDefined = (value: T | null | undefined): value is T => {
+ return value != null
+}
+
+/**
+ * Проверяет, что значение отсутствует как null или undefined.
+ */
+export const isNotDefined = (value: T | null | undefined): value is null | undefined => {
+ return value == null
+}
+
+/* --- Primitives --- */
+
+/**
+ * Сужает unknown-значение до string.
+ */
+export const isString = (value: unknown): value is string => {
+ return typeof value === 'string'
+}
+
+/**
+ * Сужает unknown-значение до конечного number.
+ *
+ * NaN и Infinity не проходят проверку.
+ */
+export const isNumber = (value: unknown): value is number => {
+ return typeof value === 'number' && Number.isFinite(value)
+}
+
+/**
+ * Сужает unknown-значение до boolean.
+ */
+export const isBoolean = (value: unknown): value is boolean => {
+ return typeof value === 'boolean'
+}
+
+/* --- Strings --- */
+
+/**
+ * Проверяет, что значение является строкой с непустым содержимым.
+ *
+ * Пробельная строка считается пустой.
+ */
+export const isNonEmptyString = (value: unknown): value is string => {
+ return typeof value === 'string' && value.trim().length > 0
+}
+
+/* --- Arrays --- */
+
+/**
+ * Проверяет, что значение является массивом.
+ *
+ * Не проверяет тип элементов. Для проверки элементов используйте `isArrayOf`.
+ */
+export const isArray = (value: unknown): value is unknown[] => {
+ return Array.isArray(value)
+}
+
+/**
+ * Проверяет массив и каждый его элемент через переданный предикат элемента.
+ *
+ * Используется на границах с unknown-данными, когда нужно получить `T[]`.
+ *
+ * @example
+ * ```ts
+ * if (isArrayOf(value, isString)) {
+ * // value: string[]
+ * }
+ * ```
+ */
+export const isArrayOf = (value: unknown, isItem: (item: unknown) => item is T): value is T[] => {
+ return Array.isArray(value) && value.every(isItem)
+}
+
+/**
+ * Проверяет, что массив отсутствует или не содержит элементов.
+ *
+ * Null и undefined считаются пустым списком для UI-условий.
+ */
+export const isEmptyArray = (value: readonly unknown[] | null | undefined): boolean => {
+ return !Array.isArray(value) || value.length === 0
+}
+
+/**
+ * Проверяет, что массив существует и содержит хотя бы один элемент.
+ *
+ * Сужает тип до non-empty tuple, чтобы TypeScript знал,
+ * что обращение к первому элементу безопасно.
+ *
+ * @example
+ * ```ts
+ * if (isNonEmptyArray(items)) {
+ * const firstItem = items[0]
+ * }
+ * ```
+ */
+export const isNonEmptyArray = (value: readonly T[] | null | undefined): value is readonly [T, ...T[]] => {
+ return Array.isArray(value) && value.length > 0
+}
+
+/* --- Objects --- */
+
+/**
+ * Проверяет, что значение является объектом-записью.
+ *
+ * Исключает null и массивы, но не проверяет конкретную форму объекта.
+ */
+export const isRecord = (value: unknown): value is Record => {
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+/**
+ * Проверяет наличие собственного свойства объекта.
+ *
+ * Используйте вместе с `isRecord` перед чтением unknown-свойств.
+ *
+ * @example
+ * ```ts
+ * if (isRecord(value) && hasOwn(value, 'code') && isString(value.code)) {
+ * // value.code: string
+ * }
+ * ```
+ */
+export const hasOwn = (value: object, key: K): value is Record => {
+ return Object.prototype.hasOwnProperty.call(value, key)
+}
+
+/* --- Combinators --- */
+
+/**
+ * Проверяет, что значение входит в список допустимых литералов.
+ *
+ * Удобно для runtime-проверки union-типов из `as const` массивов.
+ *
+ * @example
+ * ```ts
+ * const statuses = ['draft', 'published'] as const
+ *
+ * if (isOneOf(value, statuses)) {
+ * // value: 'draft' | 'published'
+ * }
+ * ```
+ */
+export const isOneOf = (value: unknown, values: T): value is T[number] => {
+ return values.some((item) => item === value)
+}
diff --git a/.opencode/skills/svg-sprites-ru/SKILL.md b/.opencode/skills/svg-sprites-ru/SKILL.md
new file mode 100644
index 0000000..4fe09e5
--- /dev/null
+++ b/.opencode/skills/svg-sprites-ru/SKILL.md
@@ -0,0 +1,359 @@
+---
+name: svg-sprites-ru
+description: "Используй только при настройке, изменении или диагностике @gromlab/svg-sprites. Триггеры: @gromlab/svg-sprites, svg-sprite.config.json, defineSpriteConfig, generateSprite, standalone@server, source: remote, ServerSvgInput, exact modes для standalone, React, Next.js, Vue, Nuxt, Svelte, Angular, Astro, Solid, Preact, Qwik, Lit или Alpine.js, SpriteConfig.input, --input, SpriteViewer и --icon-color-N. НЕ используй для самописных SVG-спрайтов, inline SVG, favicon, растровых изображений, icon fonts или выбора библиотеки иконок."
+---
+
+
+
+# @gromlab/svg-sprites
+
+## Что делает пакет
+
+`@gromlab/svg-sprites` — CLI-генератор SVG-спрайтов для пользовательских SVG-файлов. Пакет не содержит собственного набора иконок: он собирает SVG проекта во внешний sprite asset и создаёт типизированный нативный компонент для выбранного exact framework/bundler mode.
+
+Пакет рассчитан на несколько независимых спрайтов в одном проекте. Каждый явно выбранный config-файл или config-less каталог описывает один спрайт и получает собственные:
+
+- SVG asset;
+- mode-specific manifest data;
+- для всех modes, кроме bare `standalone`, — типы имён и production entry `.svg-sprite/index.js`;
+- для framework modes — изолированный нативный компонент и declarations;
+- для `standalone@vite`/`standalone@webpack` — нативный Web Component с явной функцией регистрации;
+- для bare `standalone` — deployment-neutral JSON manifest без публичного URL.
+- для `standalone@server` — content-addressed server release с двумя compile profiles и integrity manifest.
+
+Количество и расположение каталогов определяет проект. Например, `name: 'file-manager'` создаёт `FileManagerIcon`, `FileManagerIconName` и `fileManagerIconNames`, а другой каталог с `name: 'navigation'` создаст отдельный `NavigationIcon`. Это примеры API отдельных спрайтов, а не фиксированные экспорты пакета.
+
+Generated production runtime и declarations не импортируют `@gromlab/svg-sprites`. Генерация через `npx --yes @gromlab/svg-sprites ` не добавляет package в проект. Устанавливай его как development dependency только для Viewer, package-типов config или программного API.
+
+Любой consumer exact mode может использовать `source: 'remote'` с одним local path
+или HTTP(S) URL manifest, созданного `standalone@server`. До запуска adapter генератор
+скачивает и проверяет нужный profile, после чего создаётся обычный локальный API и
+asset; в runtime браузер не зависит от server manifest.
+
+## Выбор режима
+
+Выбери ровно один поддерживаемый mode key:
+
+| Проект | Mode key |
+|---|---|
+| Static HTML / собственная публикация | `standalone` |
+| Standalone + Vite | `standalone@vite` |
+| Standalone + Webpack 5 | `standalone@webpack` |
+| Server или CI release | `standalone@server` |
+| React + Vite | `react@vite` |
+| React + Webpack 5 | `react@webpack` |
+| Vue + Vite | `vue@vite` |
+| Vue + Webpack | `vue@webpack` |
+| Nuxt + Vite | `nuxt@vite` |
+| Nuxt + Webpack | `nuxt@webpack` |
+| Svelte + Vite | `svelte@vite` |
+| Svelte + Webpack | `svelte@webpack` |
+| SvelteKit + Vite | `sveltekit@vite` |
+| Angular application builder | `angular@application` |
+| Angular + Webpack | `angular@webpack` |
+| Astro + Vite | `astro@vite` |
+| Solid + Vite | `solid@vite` |
+| Solid + Webpack | `solid@webpack` |
+| SolidStart + Vite | `solid-start@vite` |
+| Preact + Vite | `preact@vite` |
+| Preact + Webpack | `preact@webpack` |
+| Qwik + Vite | `qwik@vite` |
+| Lit + Vite | `lit@vite` |
+| Lit + Webpack | `lit@webpack` |
+| Alpine.js + Vite | `alpine@vite` |
+| Alpine.js + Webpack | `alpine@webpack` |
+| Next.js App Router + Turbopack | `next@app/turbopack` |
+| Next.js App Router + Webpack 5 | `next@app/webpack` |
+| Next.js Pages Router + Turbopack | `next@pages/turbopack` |
+| Next.js Pages Router + Webpack 5 | `next@pages/webpack` |
+
+Mode задаётся в config, CLI или программном API. Порядок применения: `defaults → config → CLI/API overrides`. После объединения mode обязателен.
+
+`name` необязателен. Если он не задан, генератор преобразует имя каталога sprite-модуля в kebab-case; для каталогов `svg-sprite` и `svg-sprites` используется имя родительского каталога. Явное `name` должно уже быть записано в kebab-case и начинаться с латинской буквы.
+
+CLI принимает ровно один путь. Путь к файлу `.ts`, `.js` или `.json` загружает именно этот конфиг независимо от имени. Путь к каталогу включает config-less генерацию, и настройки передаются флагами CLI.
+
+```json
+{
+ "scripts": {
+ "sprite:": "npx --yes @gromlab/svg-sprites ",
+ "sprite::cli": "npx --yes @gromlab/svg-sprites --mode "
+ }
+}
+```
+
+Генерация через `npx` не добавляет package в проект. Не придумывай сокращённые или generic mode keys и не используй удалённый `legacy`: выбери один полный key из таблицы. Bare `standalone` выбирай только когда приложение само публикует SVG, а `standalone@server` — только для централизованного release, используемого во время генерации consumers. Для нескольких спрайтов создай отдельную команду для каждого config-файла или каталога.
+
+## Инспекция проекта
+
+До изменений установи фактический контракт проекта:
+
+1. Прочитай корневой `package.json`, lock-файл и workspace-конфигурацию; определи framework, bundler и существующие команды.
+2. Найди config-файлы, команды `svg-sprites` и импорты generated-компонентов. Имя конфига произвольное; ориентируйся на переданный CLI путь и поля объекта.
+3. Определи framework, router при его наличии и фактический bundler по scripts и конфигу. Для Next.js отдельно определи App/Pages Router и сборщик реальных `dev`/`build` команд.
+4. Проверь существующие `predev`, `prebuild`, `pretypecheck` и агрегирующие scripts. Не перезаписывай их.
+5. Для нового спрайта выбери целевой каталог, не навязывая конкретный слой или архитектуру приложения.
+6. Проверь TypeScript и alias-настройки. Для package subpath exports нужен TypeScript 5+ с `moduleResolution: 'bundler'`, `'node16'` или `'nodenext'`.
+
+Для обычного local consumer все input-пути считаются относительно каталога, содержащего явно переданный config-файл; в config-less режиме — относительно переданного каталога. Проверяй local `input` по этому контракту:
+
+- `input?: string | string[]` по умолчанию равен `./icons`;
+- каждая строка задаёт папку, точный SVG-файл или glob;
+- папка сканируется плоско; вложенные файлы включаются только явным recursive glob, например `./icons/**/*.svg`;
+- массив объединяет positive-источники, а элемент с префиксом `!` исключает свои совпадения из общего набора;
+- каждый positive-источник должен разрешаться хотя бы в один SVG, поэтому отсутствующая или пустая папка, glob без совпадений, отсутствующий файл или точный путь не к SVG являются ошибкой;
+- разрешённые файлы дедуплицируются и детерминированно сортируются;
+- разные файлы с одинаковым basename конфликтуют, даже если получены из разных источников.
+
+До применения этих правил выбери нужную ветку:
+
+- `standalone@server` может объединять local strings и HTTP(S) descriptors `{ name, url, sha256? }`; `name` задаёт публичное имя иконки, а необязательный `sha256` проверяет скачанные байты;
+- `source: 'remote'` требует ровно одну строку с local path или HTTP(S) URL manifest и не принимает source globs или descriptors;
+- remote consumer config содержит только `mode`, `source` и `input`; name, description, transforms и generated notice приходят из проверенного server manifest.
+
+Не копируй общий SVG в несколько папок: добавь его точный путь или подходящий glob в `input` каждого нужного спрайта. Используй `**/*.svg` только для намеренного рекурсивного включения.
+
+## Настройка интеграции
+
+Не воспроизводи настройку mode по памяти. После инспекции проекта выбери один exact mode и открой соответствующий файл из `references/docs/ru/guides/`. Используй guide как базовый рабочий контракт, затем адаптируй его к существующей структуре проекта.
+
+Работай в таком порядке:
+
+1. Определи каталог исходных SVG и каталог одного sprite-модуля. Один config создаёт один независимый спрайт; для нескольких наборов нужны отдельные config-файлы и уникальные `name`.
+2. Сверь framework, router и bundler с exact mode. Для Next.js проверяй реальные `dev` и `build` scripts, а не только наличие `next.config.*`.
+3. Предпочитай JSON-конфиг, если проекту не нужны package-типы config. TypeScript-конфиг также загружается через CLI, но установка package нужна, когда он импортирует `defineSpriteConfig` или типы.
+4. Разрешай все `input` относительно каталога config-файла. Не меняй структуру SVG без необходимости: используй путь к папке, точный файл, glob или массив этих источников.
+5. Добавь sprite-команду с явным путём к config. Сохрани существующие `dev`, `build`, `typecheck` и lifecycle hooks; встрой генерацию до первого процесса, импортирующего `.svg-sprite`.
+6. Не запускай одну генерацию дважды через одновременный `predev` и `npm run sprites && ...`. Для нескольких спрайтов создай отдельные команды и один агрегирующий script.
+7. Если приложение импортирует каталог sprite-модуля, создай пользовательский `index.ts` рядом с `.svg-sprite`; не помещай пользовательские файлы внутрь generated-каталога.
+8. Выполни первую генерацию до typecheck или запуска приложения, затем проверь mode-specific output и фактический импорт компонента.
+
+Для централизованного release открой `references/docs/ru/guides/standalone-server.md`.
+Генерируй и публикуй весь каталог `.svg-sprite` атомарно. В каждом consumer сохрани
+его собственный exact framework mode, укажи `source: 'remote'` и направь `input` на
+manifest. Не копируй server files во framework output и не загружай manifest из
+runtime приложения.
+
+Не добавляй Viewer автоматически. Подключай его только по запросу пользователя или когда нужна визуальная проверка набора, цветов либо сложных SVG. Способ изоляции Viewer от production бери из exact guide: frameworks, bundlers и routers используют разные границы.
+
+Не копируй snippets между exact modes даже при похожем API. Различаются asset URL, generated-файлы, CSS handling, router boundary и способ подключения debug-инструментов.
+
+## Контракт generated-каталога
+
+Например, после генерации React/Next-каталог имеет следующий вид:
+
+```text
+svg-sprite/
+├── icons/ # пользовательские исходники
+├── svg-sprite.config.json # рекомендуемое имя конфига
+├── index.ts # необязательный пользовательский barrel
+├── .gitignore # управляет генератор
+└── .svg-sprite/
+ ├── index.js
+ ├── index.d.ts
+ ├── icon-data.js
+ ├── icon-data.d.ts
+ ├── sprite.svg
+ ├── svg-sprite.manifest.js
+ ├── svg-sprite.manifest.d.ts
+ └── react/
+ ├── react-component.js
+ ├── react-component.d.ts
+ └── react-component.module.css
+```
+
+Standalone не создаёт `react/`. Bare `standalone` генерирует `sprite.svg` и `svg-sprite.manifest.json`; `standalone@vite`/`standalone@webpack` дополнительно генерируют `index.*`, `icon-data.*` и resolved manifest. Их `index.*` также содержит нативный generated Web Component; bare `standalone` не получает JS runtime и не создаёт `.gitignore`.
+
+`standalone@server` генерирует `sprite..svg`,
+`sprite-root-viewbox..svg` и `svg-sprite.manifest.json`. У него нет
+consumer facade, browser runtime, Viewer entry или `.gitignore`. Manifest хранит
+relative URL обоих profiles, полные SHA-256, размеры в байтах, metadata иконок и
+настройки transforms.
+
+Редактируй исходные SVG, config-файл и пользовательский `index.ts`. Не изменяй вручную содержимое `.svg-sprite`: повторная генерация его перезапишет. Во всех modes, кроме bare `standalone`, generated `.gitignore` также находится под управлением генератора. Для импорта из корня sprite-модуля создай barrel:
+
+```ts
+export * from './.svg-sprite/index.js'
+```
+
+Генератор полностью владеет каталогом `.svg-sprite` и заменяет его при каждом запуске. Никогда не помещай туда пользовательские файлы. Генератор также владеет `.gitignore`, когда выбранный mode его создаёт. Bare `standalone` сохраняет пользовательский `.gitignore`, но удаляет управляемый `.gitignore`, оставшийся после другого mode. Generated-пути не должны содержать symlink.
+
+Каждый exact-mode adapter владеет facade, framework-каталогом, runtime нативного компонента, declarations, manifest source, styles и asset URL. React/Next используют `react/`; остальные framework modes используют собственный generated-контракт из соответствующего guide. Standalone bundler modes экспортируют Web Component helpers и типы, а bare `standalone` не создаёт facade. Manifest declarations объявляют типы локально и не импортируют generator package.
+
+В bundler modes спрайт остаётся отдельным asset, а SVG path-данные не встраиваются в JavaScript. Content hash зависит от настроек сборщика. Bare `standalone` создаёт файл с фиксированным именем, а приложение само определяет его публичное имя и версионирование:
+
+- Vite-based adapters используют mode-owned static asset import, сохраняющий sprite внешним;
+- `standalone@vite` использует тот же Vite asset-механизм и экспортирует href helper и нативный Web Component без React;
+- `standalone@webpack` использует Webpack Asset Modules и экспортирует такой же mode-local Web Component без React;
+- Webpack-based adapters и все Next modes используют adapter-owned механизм внешнего asset, обычно `new URL(..., import.meta.url).href`;
+- кастомный Webpack SVG loader не должен перехватывать generated `sprite.svg`;
+- в Next mode generated-компонент не содержит `'use client'` и работает в Server Components, SSR и SSG; не добавляй клиентскую границу только ради иконки;
+- команда сборки Next и mode key должны совпадать: Turbopack с `.../turbopack`, Webpack с `.../webpack`.
+- remote consumers всё равно публикуются через локальный asset pipeline своего adapter; не сохраняй и не собирай URL server profile в generated application code.
+
+Для bundler modes не перемещай generated sprite в `public` и не переписывай URL вручную. Для bare `standalone` не перемещай managed original: приложение может явно копировать его в deploy output и само отвечает за публичный URL и очистку копии. При смене mode перегенерируй спрайт с новым полным key.
+
+## Использование, доступность и цвета
+
+Имя компонента зависит от `name` конкретного спрайта. В `standalone@vite` и `standalone@webpack` значение `name: 'file-manager'` создаёт tag `` и функцию `defineFileManagerIconElement()`:
+
+```ts
+import { defineFileManagerIconElement } from './svg-sprite'
+
+defineFileManagerIconElement()
+```
+
+```html
+
+```
+
+Нативный элемент не имеет runtime-зависимостей, сам выбирает generated ID и `viewBox`, получает URL через bundler и рендерит `