# Блок H — карточки узлов (R-11) + конфликт-рёбра (R-12) — дизайн

**Дата:** 2026-06-08 · **Эпик:** «роутер-наставник» · **Ветка:** `worktree-brainrepo`
**Статус:** дизайн на согласовании → writing-plans.
**Источник задачи:** реестр хвостов `docs/superpowers/plans/2026-06-08-router-mentor-loose-ends-registry.md`, блок **H** (R-11 + R-12) + handoff `docs/superpowers/plans/2026-06-08-router-mentor-H-cards-session-handoff.md`.

---

## 1. Цель и границы

Граф скилов роутера (Машина 3) живёт, но **пуст данными**: механика 3-A (контракты) / 3-B (граф узлов) / 3-C (охват) / 3-D (движок) построена и покрыта инвариантами, а наполнения нет — роутер выбирает узлы «вслепую» без карточек-контрактов и без явных конфликт-рёбер.

Этот блок наполняет граф **данными**:

- **R-11** — карточки-контракты для **всех 86 узлов** реестра (`docs/registry/contracts/<slug>.contract.json`). Сейчас написаны 2 seed-файла (`writing-plans`, `operations-process-doc`).
- **R-12** — явные конфликт-рёбра `attributes.conflicts_with` в `docs/registry/nodes.yaml`. Сейчас 0.
- **Acceptance** — один новый инвариант покрытия (код, TDD).

**Жёсткие границы (что НЕ делаем):**

- Механику 3-A/3-B/3-C/3-D **в основном не трогаем** — данные + инварианты (покрытие + конфликты + минимум содержания). **Исключение (решение владельца «делай все», находка G-E §10.1):** одна точечная TDD-правка `tools/skill-contract.mjs::checkContractDrift` — сделать страж дрейфа G4 инертным при пустом `source.path` (иначе ~50 external-карточек реестр считает «дрейфующими» → soft-reasoning).
- НЕ парсим free-text для конфликтов — только explicit `attributes.conflicts_with` (как и заложено в `node-graph.conflictsOf`).
- НЕ добавляем конфликт-рёбра без канона (Tooling §9 / ADR).
- НЕ трогаем нормативку (§0 cross-refs Pravila/PSR/Tooling/CLAUDE.md), `CLAUDE.md` в worktree, `.claude/settings.json`.
- commit-not-push: пуш только по слову «пуш».

---

## 2. Вселенная узлов (факт реестра)

`docs/registry/nodes.yaml` — **86 узлов**:

| status | кол-во | примеры |
|---|---|---|
| active | ~78 | phase-0/1/2/3 + off-phase инструменты |
| deferred | 6 | #44 Figma / #50 Jupyter / #54 n8n / #67 NightOwl / #82 DataForSEO / #83 Unisender |
| dormant | 1 | ruflo |
| historic | 1 | #1 PostgreSQL MCP (заменён Boost) |

Каждый узел уже несёт: `id`, `name`, `slug`, `category`, `subcategory`, `status`, `capabilities`, `triggers`, `boundaries`, `chain_membership`, `attributes`. Это **первичный материал** для карточек.

---

## 3. Зафиксированные решения (согласовано с владельцем 2026-06-08)

| # | Решение | Значение |
|---|---|---|
| Р1 | **Гранулярность** | **per-node**: 86 карточек, по одной на узел; `contract.skill` = `node.slug`. Зонтичные узлы (superpowers #19, operations #51, marketing #74, ruflo) — **одна карточка-зонтик** на узел (общий контракт), не разворачиваются в под-скилы. |
| Р2 | **Scope** | **Все 86** полными карточками, включая 6 deferred + ruflo dormant + #1 historic. |
| Р3 | **Seed-файлы** | 2 готовых файла под-скилов (`writing-plans`, `operations-process-doc`) — **под-скилы, не узлы**; остаются как безвредные бонус-контракты. В инвариант покрытия не входят (инвариант — про узлы). |
| Р4 | **own/external** | `own` = наши self-authored project-скилы (`.claude/skills/**`, писали мы) + superpowers (по прецеденту seed `writing-plans`). `external` = marketplace-плагины + MCP-серверы + вендоренные чужие скилы + ruflo. `external` несёт `source{version,hash,path}`. |
| Р5 | **hash external** | Настоящий `sha256(SKILL.md)` + `path` — где есть локальный SKILL.md в репо (вендоренные/свои скилы). Где локального файла нет (MCP-серверы, marketplace-плагины) — нулевой плейсхолдер `0…0` + `path=""` (G4 инертен, как в seed `operations-process-doc`). |
| Р6 | **Конфликт-рёбра** | Источник — **только** Tooling §9 (запрещённые дубли) + ADR-границы (006/007/.../017). Двусторонние (A↔B оба несут ссылку). graphify — вторичная сверка кандидатов, не источник. |
| Р7 | **Acceptance** | Новый инвариант покрытия (TDD): каждый узел реестра имеет валидный контракт; конфликт-рёбра резолвятся и симметричны. |
| Р8 | **Батчинг** | По `category`/`subcategory` (фазы + off-phase подкатегории). |

---

## 4. Компонент 1 — карточки-контракты (данные)

**Где:** `docs/registry/contracts/<slug>.contract.json` (имя файла = slug узла; механика `loadRegistry` читает любой `*.contract.json`, маппинг узел↔контракт — по полю `skill`).

**Форма** (схема `tools/skill-contract.mjs`, валидатор `validateContract`):

| Поле | Тип | Содержание |
|---|---|---|
| `skill` | string | = `node.slug` (ключ связи узел↔контракт; роутер/`dispatchContract` ищут по нему) |
| `kind` | `own`\|`external` | по правилу Р4 |
| `needs` | string[] | что навыку нужно на входе (рёбра графа 3-C) |
| `produces` | string[] | что навык выдаёт (рёбра графа 3-C) |
| `constraints` | string[] | ограничения/область («только пишет план», «READ-ONLY», «marketplace skill — outputs doc only») |
| `preview-form` | enum | `none`\|`outline`\|`mockup`\|`sample`\|`dry-run`\|`diagram` — форма дешёвого образца (L1), по природе узла |
| `defaults` | string[] | разумные дефолты-на-вето (L3) |
| `key-decisions` | string[] | ключевые решения, которые навык поднимает (look-ahead роутера) |
| `acceptance-criteria` | string[] | критерий приёмки навыка заранее (L7) |
| `inherent` | `{need,rationale}[]` | «природные» нужды навыка (опционально). `rationale` — про **природу** навыка, НЕ про прошлый промах (гард R4: маркеры «забыл/прошлый раз/…» отклоняются валидатором) |
| `source` | `{version,hash,path}` | **только** для `external` (Р5) |

**Источники контента (по приоритету):**

1. `nodes.yaml` — `capabilities` / `boundaries` / `triggers` / `subcategory` (первичный, узлы уже описаны).
2. SKILL.md (для скилов) — реальное описание навыка.
3. Tooling Прил.Н §4.NN (роль/границы узла в каноне).
4. CLAUDE.md §3 (категория/роль).

**Нейтральность (G1, `checkContractNeutrality`).** Карточка — про **сам инструмент**, без проектных фактов («лендингу нужен teal» → хардкод). Проектные термины инъецируются страж-проверкой; авторим карточки портативно.

**Зонтичные узлы (Р1).** superpowers #19 / operations #51 / marketing #74 / ruflo — карточка описывает **узел как целое** (общие needs/produces зонтика), а не разворачивается в под-скилы. Seed-файлы `writing-plans`/`operations-process-doc` (под-скилы) остаются параллельно как бонусы.

**Авторские нормы (из аудита достаточности §10.1):**

- **G-A (разведение близнецов).** Разделитель «когда я, а не сосед-близнец» писать в `capabilities` узла (его читает каталог роутера, `router-engine.mjs:100`) **и** дублировать в `constraints` карточки (её читает судья). Только `constraints` недостаточно — роутер look-ahead их не видит (`router-engine.mjs:103`).
- **G-G (не путать вход/выход).** `needs` = что навык берёт на ВХОД; `produces` = что выдаёт на ВЫХОД. Перепутать местами молча инвертирует граф охвата (`coverage-machine.mjs:43-56`) — сверять при авторинге каждой карточки.
- **G-B (не пустые).** Карточка обязана нести содержание (см. инвариант §6 п.3) — пустые `needs/produces` запрещены.

---

## 5. Компонент 2 — конфликт-рёбра (данные)

**Где:** `nodes.yaml` → `<node>.attributes.conflicts_with: [<ref>, …]` (ref = id/slug/name, резолвится `resolveNode`). Читается `node-graph.conflictsOf` (free-text не парсит — только explicit).

**Источник (Р6) — только канон:**

- **Tooling §9** — явный список «два инструмента на одну задачу» (запрещённые дубли).
- **ADR-границы** — зафиксированные разграничения (ADR-006 icon-path / 007 ml / … / 017 graphify и пр.).

**Двусторонность.** Если A конфликтует с B — ссылка ставится **в обоих** узлах (`A.conflicts_with ⊇ [B]` И `B.conflicts_with ⊇ [A]`). Симметрию и отсутствие висячих ссылок проверяет инвариант (Компонент 3) + проход `sharp-edges`.

**graphify — вторичная сверка (не источник).** `graphify-out/graph.json` Grep-доступен (бинарь не на PATH, но JSON читается напрямую). Используется так: Grep на `semantically_similar_to`/`conceptually_related_to` между инструментами → **кандидаты** в конфликты → каждый кандидат **проверяется по §9/ADR**; без канона ребро не добавляется.

---

## 6. Компонент 3 — инвариант покрытия (код, TDD)

Новый тест-инвариант на **реальном** реестре (стиль m3a/m3b/m3c — `tools/m3*-invariants.test.mjs`):

1. **Покрытие контрактами:** для каждого узла `nodes.yaml` существует контракт-файл, чей `skill === node.slug`, и он проходит `validateContract` (`ok:true`). Дубли `skill` отлавливаются (как в `buildRegistry`).
2. **Целостность конфликт-рёбер (G-H):** каждый ref в любом `attributes.conflicts_with` резолвится (`resolveNode ≠ null`) **и** симметричен (`A→B ⟹ B→A`). Закрывает молчаливый дроп опечатки (`node-graph.mjs:85` `filter(Boolean)`) и одностороннее ребро.
3. **Минимум содержания (G-B):** карточка не пуста — как минимум `produces` непусто (или `needs∪produces` непусто; для скилов — `acceptance-criteria` непусто). Форма-валидатор пропускает пустые массивы (`isStringArray([])===true`, `skill-contract.mjs:28`), поэтому полноту содержания держит **отдельная** проверка инварианта, не `validateContract`.
4. **Страж дрейфа инертен без источника (G-E):** после правки `checkContractDrift` external-карточка с `source.path===""` (нулевой плейсхолдер Р5) **не** помечается `drifted`; тест фиксирует, что `buildRegistry().driftFlags` для таких карточек пуст. (Это и есть точечная мех-правка из §1; TDD: RED — текущий seed `operations-process-doc` даёт drift → GREEN после инертности.)

Цикл TDD: тест добавляется → **RED** (карточек/рёбер нет) → авторинг данных → **GREEN**. Поскольку инвариант — это `tools/*.test.mjs` (не prod-`tools/*.mjs`), enforce-tdd-gate не блокирует (он только на prod-`tools/*.mjs`).

**Заметка по 86 vs active.** Раз Р2 = все 86 получают карточки, инвариант покрытия проверяет **все узлы**, а не только active (сильнее и согласован с Р2).

---

## 7. Поток данных

```
nodes.yaml (86 узлов)
   │  авторинг по узлам (capabilities/SKILL.md/Tooling §N/CLAUDE.md §3)
   ▼
docs/registry/contracts/<slug>.contract.json (86)
   │  loadRegistry → buildRegistry (валидация + дрейф G4 для external)
   ▼
роутер look-ahead (buildRouterPrompt.contracts) / dispatchContract(skill)
   связь: node.slug == contract.skill

nodes.yaml.attributes.conflicts_with  → buildNodeGraph → conflictsOf(ref)
инвариант покрытия связывает оба слоя.
```

---

## 8. Батчи (≈18–20) и порядок

Батч = `category` / `subcategory` (Р8). Внутри батча узлы однородны → консистентный тон карточек + удобная variant-analysis.

- **Фазовые:** phase-0 · phase-1 · phase-2 (+Frontend Design) · phase-3.
- **Off-phase подкатегории:** architecture-tooling · audit-security · design-tooling · integration-tooling · ml-ai-tooling · business-process · discovery-tooling · authoring-tooling · dev-support · project-management · finance-tooling · backend-tooling · infosec-tooling · marketing-tooling · knowledge-graph-tooling · debug-runtime · UI-pool (UPM/21st/claude-md-management) · project-agent (#84/#85).
- **Прочее:** ruflo (dormant) + #1 historic.
- **Отдельные батчи:** (а) конфликт-рёбра R-12 — **после** всех карточек; (б) инвариант покрытия — отдельный **код-батч** (TDD).

**Цикл per-батч:** `audit-context-building` → авторинг карточек → `validateContract` 0 ошибок + инварианты GREEN (`test-driven-development`; `systematic-debugging` на красный) → `verification-before-completion` → `variant-analysis` (консистентность полей/тона между карточками батча).

**Гейт закрытия эпика:** `audit-context-building` → `sharp-edges` (двусторонняя согласованность конфликт-рёбер, нет висячих ref) → `variant-analysis` → `regression` (tools-only ≥ baseline) → `verification-before-completion` → **commit (не push)**.

---

## 9. Тестирование (сводка)

| Уровень | Что | Где |
|---|---|---|
| Форма контракта | `validateContract.ok` на каждом новом файле | стиль m3a |
| Охват реестра | новый инвариант покрытия (§6 п.1) | новый `tools/m3*-…test.mjs` |
| Граф конфликтов | резолв + симметрия (§6 п.2) | расширение m3b / новый |
| Регрессия | vitest tools-only ≥ baseline | `cd app && npx vitest run --config vitest.config.tools.mjs … --reporter dot` |

---

## 10. Открытые мелочи (в реализацию, не блокеры)

- Точный список конфликт-рёбер собирается в батче R-12 (Grep Tooling §9 + ADR + вторичная сверка graphify graph.json).
- Для каждого external-узла — определить наличие локального SKILL.md (Р5): есть → реальный hash+path; нет → нулевой плейсхолдер.
- Имя файла seed `operations-process-doc.contract.json` — под-скил, остаётся; узловой контракт operations пишется как `operations.contract.json` (skill=`operations`).

---

## 10.1. Находки аудита достаточности (2026-06-08, вложены в этот план)

Аудит «достаточно ли паспорта для роутера и судьи» (цепочка `audit-context-building` → `variant-analysis` → `sharp-edges` → verification). **Вердикт: схема паспорта (9 полей + `inherent`) достаточна — новое поле не нужно.** Судья получает карточку целиком (`judge-engine.mjs:56`), охват 3-C читает needs/produces/constraints/inherent, роутер — needs/key-decisions/acceptance-criteria. Риски — авторские/инвариантные + одна точечная мех-правка:

| # | Сев. | Суть | Лечение | Где в плане |
|---|---|---|---|---|
| G-E | ВЫС | Нулевой hash external → `checkContractDrift` метит `drifted` → `dispatchContract` soft-reasoning у ~50 узлов (`skill-contract.mjs:104-113`, `:47`) | G4 инертен при `path==""` (TDD-правка) | §1 искл. + §6 п.4 |
| G-A | СР | Роутер слеп к `constraints`/`boundaries` — не разводит близнецов (`router-engine.mjs:100,103`) | разделитель в `capabilities`+`constraints` | §4 норма |
| G-B | СР | Пустая карточка валидна, прошла бы инвариант (`skill-contract.mjs:28`) | инвариант «минимум содержания» | §6 п.3 |
| G-G | СР | needs/produces один тип `string[]`, путаются молча (`coverage-machine.mjs:43-56`) | авторская дисциплина вход/выход | §4 норма |
| G-H | СР | Опечатка/асимметрия конфликт-ребра молчит (`node-graph.mjs:85`) | инвариант резолв+симметрия | §6 п.2 |
| G-F | НИЗ | `kind`-mislabel выключает страж (`skill-contract.mjs:105`) | опц. эвристика — **отложено** | — |
| G-C | НИЗ | у судьи нет явного «зачем навык» (выводит из produces+acceptance) | опц. назначение в `produces` — **отложено** | — |
| G-D | НИЗ | два слоя хеша: card-guard `expectedHash` ≠ `source.hash` | owner-wiring, вне H | — |

## 11. Якоря

- Реестр хвостов: `docs/superpowers/plans/2026-06-08-router-mentor-loose-ends-registry.md` (блок H).
- Handoff: `docs/superpowers/plans/2026-06-08-router-mentor-H-cards-session-handoff.md`.
- Механика: `tools/skill-contract.mjs`, `tools/skill-contract-registry.mjs`, `tools/node-graph.mjs`, `tools/router-engine.mjs`, инварианты `tools/m3a..m3d-*-invariants.test.mjs`.
- Образцы: `docs/registry/contracts/writing-plans.contract.json` (own), `…/operations-process-doc.contract.json` (external).
