docs(router-mentor): план реализации блока H (карточки + конфликт-рёбра)

Bite-sized TDD-план: Task1 G-E мех-правка, Task2 чистый чекер card-coverage,
Tasks 3.1-3.20 авторинг 86 карточек по батчам, Task4 конфликт-рёбра (§9+ADR),
Task5 живой инвариант m3e (последним → GREEN), Task6 гейт закрытия.
Исполнение инлайн (субагенты запрещены). commit-not-push.

coverage: skill:writing-plans
This commit is contained in:
Дмитрий
2026-06-08 18:07:22 +03:00
parent 48d0594a39
commit aa94dabc5b
@@ -0,0 +1,424 @@
# Блок H — карточки узлов (R-11) + конфликт-рёбра (R-12) — план реализации
> **Для исполнителя:** субагенты/Task/Workflow по решению владельца **ЗАПРЕЩЕНЫ** — исполнять **инлайн** (`superpowers:executing-plans`, батч-чекпоинты). Шаги — чекбоксы `- [ ]`.
**Goal:** Наполнить граф скилов роутера данными — 86 карточек-контрактов (per-node) + явные двусторонние конфликт-рёбра — и закрыть это инвариантами; плюс одна точечная мех-правка (G-E).
**Architecture:** Данные в `docs/registry/contracts/<slug>.contract.json` (по одной карточке на узел, `skill=slug`) + `attributes.conflicts_with` в `docs/registry/nodes.yaml`. Полнота держится новым чистым чекером `tools/card-coverage.mjs` (TDD на фикстурах) и живым инвариантом `tools/m3e-card-coverage-invariants.test.mjs` (добавляется последним). Механику 3-A/3-B/3-C/3-D не трогаем, кроме G-E-инертности стража дрейфа.
**Tech Stack:** Node ESM (`tools/*.mjs`), vitest (tools-config), YAML-реестр, JSON-контракты. Источник дизайна: [`docs/superpowers/specs/2026-06-08-router-mentor-H-cards-conflict-edges-design.md`](../specs/2026-06-08-router-mentor-H-cards-conflict-edges-design.md).
---
## Квирки исполнения (читать до старта)
- **Дерево:** `c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo`, ветка `worktree-brainrepo`. Ниже `<wt>` = этот путь.
- **vitest (через Bash, из `app/`):** `cd app && npx vitest run --config vitest.config.tools.mjs <подстрока-имени> --reporter dot`. НЕ `node …/vitest.mjs`, НЕ `npx` из корня.
- **git (через PowerShell):** по одной команде, литеральные пути, без переменных. Коммит: msg в `<wt>\.scratch\*.txt` (Write) → `git -C "<wt>" commit -F "<…>.txt"`. «Can't find lefthook in PATH» — шум.
- **enforce-tdd-gate** (только на `tools/*.mjs`): prod-правка требует (а) предшествующей правки `*.test.mjs` + (б) **наблюдаемого RED-прогона** в этом же ходе перед prod-правкой. Данные (`.json`/`.yaml`) gate не триггерят.
- **commit-not-push:** пуш только по слову «пуш» (через `superpowers:finishing-a-development-branch`).
- **Грязь дерева** (observer-jsonl, чужие планы, `.scratch/`) — НЕ коммитить; стейджить свои файлы поимённо.
- **Список файлов** — через Grep (Glob ломается на junction).
- Каждый ответ: 1-я строка «экономия:…», 2-я «coverage:…».
---
## Файловая карта
| Файл | Ответственность | Действие |
|---|---|---|
| `tools/skill-contract.mjs` | `checkContractDrift` — G4 страж дрейфа | Modify (G-E инертность) |
| `tools/skill-contract.test.mjs` | юнит-тесты схемы/стража | Modify (+G-E тест) |
| `tools/card-coverage.mjs` | **новый** чистый чекер покрытия узел↔карточка + конфликтов | Create |
| `tools/card-coverage.test.mjs` | TDD чекера на фикстурах | Create |
| `docs/registry/contracts/<slug>.contract.json` | 86 карточек (по узлу) | Create ×86 |
| `docs/registry/nodes.yaml` | `attributes.conflicts_with` рёбра | Modify (данные) |
| `tools/m3e-card-coverage-invariants.test.mjs` | **живой** инвариант на реальном реестре | Create (последним) |
---
## Task 1: G-E — страж дрейфа инертен без локального источника
**Files:**
- Modify: `tools/skill-contract.mjs` (функция `checkContractDrift`, ~стр. 104)
- Test: `tools/skill-contract.test.mjs`
- [ ] **Step 1: Написать падающий тест**`tools/skill-contract.test.mjs`, в describe про `checkContractDrift`)
```js
it('G-E: external с пустым source.path → дрейф не сторожится (инертен)', () => {
const c = { skill: 'x', kind: 'external', source: { version: '1', hash: '0'.repeat(64), path: '' } };
const r = checkContractDrift({ contract: c, currentContent: undefined });
expect(r.drifted).toBe(false);
expect(r.ok).toBe(true);
});
```
- [ ] **Step 2: Прогнать — убедиться, что падает (RED)**
Run: `cd app && npx vitest run --config vitest.config.tools.mjs skill-contract --reporter dot`
Expected: FAIL — текущий код метит `drifted:true` (stored `0…0``contractHash('')`).
- [ ] **Step 3: Минимальная правка** — вставить guard сразу после проверки `kind`:
В `tools/skill-contract.mjs`, в `checkContractDrift`, между строкой `return { ok: true, drifted: false, reason: 'own/нет внешнего источника — дрейф не сторожится' };` и строкой `const stored = contract.source?.hash;` добавить:
```js
if (!contract.source?.path)
return { ok: true, drifted: false, reason: 'external без локального source.path — дрейф не сторожится (G4 инертен)' };
```
- [ ] **Step 4: Прогнать — GREEN**
Run: `cd app && npx vitest run --config vitest.config.tools.mjs skill-contract --reporter dot`
Expected: PASS (все тесты skill-contract).
- [ ] **Step 5: Коммит** (msg в `<wt>\.scratch\t1.txt`, затем)
```
git -C "<wt>" add tools/skill-contract.mjs tools/skill-contract.test.mjs
git -C "<wt>" commit -F "<wt>\.scratch\t1.txt"
```
Текст msg: `fix(skill-contract): G-E — страж дрейфа инертен при пустом source.path`
---
## Task 2: чистый чекер покрытия `card-coverage.mjs` (TDD на фикстурах)
Четыре чистые функции: пропущенные контракты, пустые карточки, нерезолвящиеся и асимметричные конфликт-рёбра. Без диска — данные инъектируются. Это код → полноценный TDD.
**Files:**
- Create: `tools/card-coverage.mjs`
- Create: `tools/card-coverage.test.mjs`
- [ ] **Step 1: Написать падающие тесты** (`tools/card-coverage.test.mjs`)
```js
import { describe, it, expect } from 'vitest';
import { buildNodeGraph } from './node-graph.mjs';
import { missingContracts, emptyCards, unresolvedConflicts, asymmetricConflicts } from './card-coverage.mjs';
const reg = (nodes) => ({ nodes, chains: {} });
describe('card-coverage — покрытие узел↔карточка', () => {
it('missingContracts: узел без карточки (skill≠slug) попадает в список', () => {
const nodes = [{ id: '#1', slug: 'a' }, { id: '#2', slug: 'b' }];
const contracts = [{ skill: 'a' }];
expect(missingContracts(nodes, contracts)).toEqual(['#2']);
});
it('emptyCards: needs[] и produces[] пусты → карточка пустая', () => {
const contracts = [
{ skill: 'a', needs: [], produces: [] },
{ skill: 'b', needs: ['x'], produces: [] },
{ skill: 'c', needs: [], produces: ['y'] },
];
expect(emptyCards(contracts)).toEqual(['a']);
});
});
describe('card-coverage — конфликт-рёбра', () => {
it('unresolvedConflicts: ссылка в никуда ловится', () => {
const g = buildNodeGraph(reg([
{ id: '#1', slug: 'a', attributes: { conflicts_with: ['nope'] } },
]));
expect(unresolvedConflicts(g)).toEqual([{ from: '#1', ref: 'nope' }]);
});
it('asymmetricConflicts: A→B без B→A ловится', () => {
const g = buildNodeGraph(reg([
{ id: '#1', slug: 'a', attributes: { conflicts_with: ['b'] } },
{ id: '#2', slug: 'b', attributes: { conflicts_with: [] } },
]));
expect(asymmetricConflicts(g)).toEqual([{ from: '#1', to: '#2' }]);
});
it('симметричная пара A↔B → ни unresolved, ни asymmetric', () => {
const g = buildNodeGraph(reg([
{ id: '#1', slug: 'a', attributes: { conflicts_with: ['b'] } },
{ id: '#2', slug: 'b', attributes: { conflicts_with: ['a'] } },
]));
expect(unresolvedConflicts(g)).toEqual([]);
expect(asymmetricConflicts(g)).toEqual([]);
});
});
```
- [ ] **Step 2: Прогнать — RED**
Run: `cd app && npx vitest run --config vitest.config.tools.mjs card-coverage --reporter dot`
Expected: FAIL — `card-coverage.mjs` не существует.
- [ ] **Step 3: Реализация** (`tools/card-coverage.mjs`)
```js
#!/usr/bin/env node
/**
* card-coverage — мост покрытия «узлы реестра ↔ карточки-контракты» (H, R-11/R-12).
* Чистые функции (данные инъектируются): пропущенные контракты, пустые карточки,
* нерезолвящиеся и асимметричные конфликт-рёбра. Без LLM, без диска.
*/
import { resolveNode } from './node-graph.mjs';
/** Узлы, у которых нет карточки с skill === node.slug. Возвращает id|slug. */
export function missingContracts(nodes, contracts) {
const skills = new Set((contracts || []).map((c) => c && c.skill).filter(Boolean));
return (nodes || [])
.filter((n) => n && n.slug && !skills.has(n.slug))
.map((n) => n.id || n.slug);
}
/** Пустые карточки: ни needs, ни produces (бесполезна всем потребителям). Возвращает skill. */
export function emptyCards(contracts) {
return (contracts || [])
.filter((c) => (c.needs || []).length === 0 && (c.produces || []).length === 0)
.map((c) => c.skill);
}
/** Конфликт-ссылки, не резолвящиеся в узел графа. {from, ref}. */
export function unresolvedConflicts(graph) {
const out = [];
for (const n of (graph && graph.nodes) || []) {
const refs = (n.attributes && n.attributes.conflicts_with) || [];
for (const r of refs) if (!resolveNode(graph, r)) out.push({ from: n.id || n.slug, ref: r });
}
return out;
}
/** Односторонние рёбра: A→B, но B не ссылается обратно на A. {from, to}. */
export function asymmetricConflicts(graph) {
const out = [];
for (const n of (graph && graph.nodes) || []) {
const refs = (n.attributes && n.attributes.conflicts_with) || [];
for (const r of refs) {
const t = resolveNode(graph, r);
if (!t) continue; // unresolved — отдельной функцией
const back = (t.attributes && t.attributes.conflicts_with) || [];
const resolvesBack = back.some((b) => resolveNode(graph, b) === n);
if (!resolvesBack) out.push({ from: n.id || n.slug, to: t.id || t.slug });
}
}
return out;
}
```
- [ ] **Step 4: Прогнать — GREEN**
Run: `cd app && npx vitest run --config vitest.config.tools.mjs card-coverage --reporter dot`
Expected: PASS (6 тестов).
- [ ] **Step 5: Коммит** (msg в `<wt>\.scratch\t2.txt`)
```
git -C "<wt>" add tools/card-coverage.mjs tools/card-coverage.test.mjs
git -C "<wt>" commit -F "<wt>\.scratch\t2.txt"
```
Текст: `feat(card-coverage): чистый чекер покрытия узел↔карточка + конфликт-рёбра`
---
## Канон авторинга карточки (применять в каждой data-задаче 3.x)
**Файл:** `docs/registry/contracts/<slug>.contract.json`, где `<slug>` = `node.slug` из nodes.yaml.
**Форма (валидатор `validateContract`):**
```json
{
"skill": "<node.slug>",
"kind": "own | external",
"needs": ["что навык берёт на ВХОД"],
"produces": ["что выдаёт на ВЫХОД"],
"constraints": ["область/ограничения; сюда же дубль разделителя близнецов (G-A)"],
"preview-form": "none|outline|mockup|sample|dry-run|diagram",
"defaults": ["разумные дефолты-на-вето"],
"key-decisions": ["ключевые решения, что навык поднимает"],
"acceptance-criteria": ["критерий приёмки заранее"],
"inherent": [{ "need": "...", "rationale": "природа навыка, НЕ прошлый промах" }]
}
```
Для `external` добавить `"source": { "version": "<verN>", "hash": "<sha256 SKILL.md | 0×64>", "path": "<путь | пусто>" }`.
**Правила (из дизайна §3 + §10.1):**
1. `skill` = ровно `node.slug`. Источники: `nodes.yaml` (capabilities/boundaries/triggers) → SKILL.md → Tooling Прил.Н §N → CLAUDE.md §3.
2. **own/external (Р4):** own = наши project-скилы (`.claude/skills/**`) + superpowers; external = marketplace + MCP + вендоренные + ruflo.
3. **hash (Р5/G-E):** есть локальный SKILL.md в репо → реальный `sha256` + `path`; нет (MCP/marketplace) → `"0".repeat(64)` + `"path": ""`. (G-E делает страж инертным для пустого path.)
4. **G-A:** разделитель «когда я, а не близнец» — в `constraints` (дубль), и проверить, что он есть в `capabilities` узла (если нет — дописать в nodes.yaml capabilities).
5. **G-G:** не путать `needs` (вход) и `produces` (выход).
6. **G-B:** карточка не пустая — `needsproduces` непусто.
7. **Нейтральность (G1):** без проектных фактов («лендингу нужен teal»); карточка про сам навык.
8. **Зонтики (Р1):** superpowers #19 / operations #51 / marketing #74 / ruflo — одна карточка-зонтик (общие needs/produces), не разворачивать в под-скилы.
**Пример own (скил):** `docs/registry/contracts/billing-audit.contract.json`
```json
{
"skill": "billing-audit",
"kind": "own",
"needs": ["money-related diff or code under review", "billing invariants context"],
"produces": ["money-correctness audit report"],
"constraints": ["read-only audit — не правит код", "НЕ для process-modeling/process-analysis/D3-security/ru-tax-accounting"],
"preview-form": "outline",
"defaults": ["проверять bcmath-суммирование, идемпотентность, tier-резолюцию, charge_source"],
"key-decisions": ["какие инварианты денежной корректности в scope"],
"acceptance-criteria": ["каждый денежный путь проверен на потерю копеек и двойное списание"]
}
```
**Пример external (MCP, без локального файла):** `docs/registry/contracts/github-mcp.contract.json`
```json
{
"skill": "github-mcp",
"kind": "external",
"needs": ["repo/issue/PR reference"],
"produces": ["issue/PR read or write result"],
"constraints": ["внешний MCP — операции через GitHub API", "НЕ для локального git-флоу"],
"preview-form": "none",
"defaults": ["read-first перед мутацией"],
"key-decisions": ["scope операции: чтение или запись"],
"acceptance-criteria": ["операция отражена в GitHub и подтверждена ответом API"],
"source": { "version": "n/a", "hash": "0000000000000000000000000000000000000000000000000000000000000000", "path": "" }
}
```
**Валидация батча (после написания N карточек батча):**
Run: `cd app && node -e "import('../tools/skill-contract.mjs').then(async m=>{const fs=await import('node:fs');const d='../docs/registry/contracts';let bad=0;for(const f of fs.readdirSync(d).filter(x=>x.endsWith('.contract.json'))){const c=JSON.parse(fs.readFileSync(d+'/'+f,'utf8'));const v=m.validateContract(m.normalizeContract(c));if(!v.ok){bad++;console.log(f,v.errors)}}console.log(bad?('BAD '+bad):'ALL VALID')})"`
Expected: `ALL VALID` (0 ошибок формы по всем написанным карточкам).
---
## Tasks 3.13.20: авторинг карточек по батчам (данные)
Каждый батч = одна задача со структурой: **(a)** прочитать узлы батча в `nodes.yaml` (Grep по id) + их SKILL.md/Tooling §N → **(b)** написать карточки по «Канону авторинга» → **(c)** валидация батча (команда выше) = 0 ошибок → **(d)** коммит `feat(contracts): карточки <батч>` (стейджить поимённо новые `*.contract.json`). `.json` не триггерит tdd-gate.
| Задача | Батч | Узлы (id; точные slug — из nodes.yaml) |
|---|---|---|
| 3.1 | phase-0 | #2 #3 #4 #5 #6 #7 #8 #9 |
| 3.2 | phase-1 | #10 #11 #12 #13 #14 #15 #16 #17 #18 |
| 3.3 | phase-2 + FD | #19(зонтик superpowers) #20 #21 #22 #23 #24 #30 |
| 3.4 | phase-3 | #25 #26 #27 #28 #29 |
| 3.5 | UI-pool | #31 #32 #33 |
| 3.6 | debug-runtime | #34 #35 |
| 3.7 | architecture-tooling | #36 #37 #38 #43 |
| 3.8 | audit-security | #39 #40 |
| 3.9 | project-management | #41 #42 |
| 3.10 | design-tooling | #44 #45 #46 |
| 3.11 | integration-tooling | #47 |
| 3.12 | ml-ai-tooling | #48 #49 #50 |
| 3.13 | business-process | #51(зонтик) #52 #53 #54 |
| 3.14 | discovery + authoring + dev-support | #55 #56 #57 #58 #59 #60 |
| 3.15 | finance-tooling | #61 #62 #63 |
| 3.16 | backend-tooling | #64 #65 #66 #67 |
| 3.17 | infosec-tooling | #68 #69 #70 #71 #72 #73 |
| 3.18 | marketing-tooling | #74(зонтик) #75 #76 #77 #78 #79 #80 #81 #82 #83 |
| 3.19 | knowledge-graph + project-agent | #86 #84 #85 |
| 3.20 | прочее | ruflo (dormant), #1 PG MCP (historic) |
> Точные `slug` и `subcategory` каждого узла — из `nodes.yaml` (Grep `id: "#NN"` → блок узла). Seed-файлы `writing-plans`/`operations-process-doc` (под-скилы) не трогать — остаются бонусами.
**Чекбоксы (один на батч, процедура (a)-(d) выше):**
- [ ] 3.1 phase-0 · [ ] 3.2 phase-1 · [ ] 3.3 phase-2+FD · [ ] 3.4 phase-3 · [ ] 3.5 UI-pool · [ ] 3.6 debug-runtime · [ ] 3.7 architecture · [ ] 3.8 audit-security · [ ] 3.9 project-mgmt · [ ] 3.10 design · [ ] 3.11 integration · [ ] 3.12 ml-ai · [ ] 3.13 business-process · [ ] 3.14 discovery/authoring/dev-support · [ ] 3.15 finance · [ ] 3.16 backend · [ ] 3.17 infosec · [ ] 3.18 marketing · [ ] 3.19 kg/project-agent · [ ] 3.20 прочее
После 3.20 все 86 карточек написаны и валидны по форме.
---
## Task 4: конфликт-рёбра R-12 (данные)
**Files:** Modify `docs/registry/nodes.yaml` (`attributes.conflicts_with` у затронутых узлов).
- [ ] **Step 1: Собрать канон-источники.**
- Прочитать **Tooling §9** (`docs/Tooling_v8_3.md`, раздел «не ставить два инструмента на одну задачу») — Grep по `§9`/«дубл».
- Прочитать ADR-границы: `Grep "conflicts|граница|вместо|не дублир" docs/adr/*.md`.
- [ ] **Step 2: Вторичная сверка graphify (кандидаты, не источник).**
Run: `Grep "semantically_similar_to|conceptually_related_to" graphify-out/graph.json` → пары инструментов-кандидатов. Каждую пару **подтвердить** по §9/ADR; без канона — НЕ добавлять.
- [ ] **Step 3: Проставить рёбра двусторонне.** Для каждой канон-пары (A,B): добавить `B` в `A.attributes.conflicts_with` И `A` в `B.attributes.conflicts_with` (ref = slug узла). Если у узла нет `attributes.conflicts_with` — создать ключ.
- [ ] **Step 4: Быстрая проверка резолва+симметрии** (до инварианта):
Run: `cd app && node -e "Promise.all([import('../tools/registry-load.mjs'),import('../tools/node-graph.mjs'),import('../tools/card-coverage.mjs')]).then(([rl,ng,cc])=>{rl.clearCache?.();const g=ng.buildNodeGraph(rl.loadRegistry({registryPath:'../docs/registry/nodes.yaml',useCache:false}));console.log('unresolved',cc.unresolvedConflicts(g));console.log('asymmetric',cc.asymmetricConflicts(g))})"`
Expected: `unresolved []` и `asymmetric []`.
- [ ] **Step 5: Коммит** (msg `<wt>\.scratch\t4.txt`): `feat(nodes): конфликт-рёбра R-12 (Tooling §9 + ADR, двусторонние)`
```
git -C "<wt>" add docs/registry/nodes.yaml
git -C "<wt>" commit -F "<wt>\.scratch\t4.txt"
```
---
## Task 5: живой инвариант покрытия `m3e` (код, GREEN-гейт)
Добавляется **последним** — данные готовы, поэтому проходит сразу (GREEN). Связывает чистый чекер (Task 2) с реальным реестром + контрактами.
**Files:** Create `tools/m3e-card-coverage-invariants.test.mjs`
- [ ] **Step 1: Написать инвариант-тест**
```js
import { describe, it, expect } from 'vitest';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { loadRegistry as loadNodes, clearCache } from './registry-load.mjs';
import { loadRegistry as loadContracts } from './skill-contract-registry.mjs';
import { buildNodeGraph } from './node-graph.mjs';
import { missingContracts, emptyCards, unresolvedConflicts, asymmetricConflicts } from './card-coverage.mjs';
const here = dirname(fileURLToPath(import.meta.url));
const registryPath = join(here, '..', 'docs', 'registry', 'nodes.yaml');
const contractsDir = join(here, '..', 'docs', 'registry', 'contracts');
describe('Машина 3-E — покрытие реестра карточками (живой инвариант H)', () => {
it('у каждого узла nodes.yaml есть карточка с skill===slug', () => {
clearCache();
const nodes = loadNodes({ registryPath, useCache: false }).nodes;
const { contracts } = loadContracts({ dir: contractsDir });
expect(missingContracts(nodes, contracts)).toEqual([]);
});
it('нет пустых карточек (needs∪produces непусто)', () => {
const { contracts } = loadContracts({ dir: contractsDir });
expect(emptyCards(contracts)).toEqual([]);
});
it('конфликт-рёбра резолвятся и симметричны', () => {
clearCache();
const g = buildNodeGraph(loadNodes({ registryPath, useCache: false }));
expect(unresolvedConflicts(g)).toEqual([]);
expect(asymmetricConflicts(g)).toEqual([]);
});
it('реестр контрактов собирается без формальных ошибок и дублей', () => {
const { errors } = loadContracts({ dir: contractsDir });
expect(errors).toEqual([]);
});
});
```
- [ ] **Step 2: Прогнать**
Run: `cd app && npx vitest run --config vitest.config.tools.mjs m3e --reporter dot`
Expected: PASS (4 теста). Если RED — починить ДАННЫЕ (недостающая/пустая карточка, асимметричное ребро), не тест. На красный — `superpowers:systematic-debugging`.
- [ ] **Step 3: Коммит** (msg `<wt>\.scratch\t5.txt`): `test(m3e): живой инвариант покрытия реестра карточками + конфликтами`
```
git -C "<wt>" add tools/m3e-card-coverage-invariants.test.mjs
git -C "<wt>" commit -F "<wt>\.scratch\t5.txt"
```
---
## Task 6: гейт закрытия эпика H
- [ ] **Step 1: Полная регрессия tools-only** (≥ baseline, без новых RED)
Run: `cd app && npx vitest run --config vitest.config.tools.mjs --reporter dot`
Expected: всё GREEN; число тестов = baseline + новые (Task1 +1, Task2 +6, Task5 +4).
- [ ] **Step 2: Состязательная сверка данных** (инлайн-проходы по дизайну §10.1): `sharp-edges` на двусторонность/висячие рёбра (должно быть пусто per m3e) + `variant-analysis` на консистентность тона карточек между батчами + `verification-before-completion` (цифры регрессии — фактом, не на словах).
- [ ] **Step 3: Финальный коммит-сводка** (если остались мелкие правки консистентности) — msg `<wt>\.scratch\t6.txt`. **commit-not-push** — пуша НЕ делать.
---
## Self-review (соответствие спеке)
- **R-11 (86 карточек):** Tasks 3.1–3.20 покрывают все id #1–#86 + ruflo (зонтики Р1 учтены). ✓
- **R-12 (конфликты):** Task 4 (канон §9+ADR, двусторонние, graphify-сверка). ✓
- **Acceptance инвариант:** Task 5 (покрытие + минимум содержания G-B + резолв/симметрия G-H). ✓
- **G-E (мех-правка):** Task 1. ✓ · **G-A/G-G/G-B нормы:** «Канон авторинга» пп.4-6 + Task 2 emptyCards. ✓
- **Границы:** механику не трогаем кроме G-E; нормативку/CLAUDE.md/settings.json не трогаем; commit-not-push. ✓
- **G-F/G-C/G-D:** отложены осознанно (дизайн §10.1) — задач нет. ✓
## Якоря
- Дизайн: `docs/superpowers/specs/2026-06-08-router-mentor-H-cards-conflict-edges-design.md`.
- Реестр хвостов (блок H): `docs/superpowers/plans/2026-06-08-router-mentor-loose-ends-registry.md`.
- Механика: `tools/skill-contract.mjs`, `tools/skill-contract-registry.mjs`, `tools/node-graph.mjs`, `tools/coverage-machine.mjs`, `tools/router-engine.mjs`, `tools/judge-engine.mjs`.