diff --git a/docs/superpowers/plans/2026-06-08-router-mentor-H-cards-conflict-edges.md b/docs/superpowers/plans/2026-06-08-router-mentor-H-cards-conflict-edges.md new file mode 100644 index 00000000..b3c46926 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-router-mentor-H-cards-conflict-edges.md @@ -0,0 +1,424 @@ +# Блок H — карточки узлов (R-11) + конфликт-рёбра (R-12) — план реализации + +> **Для исполнителя:** субагенты/Task/Workflow по решению владельца **ЗАПРЕЩЕНЫ** — исполнять **инлайн** (`superpowers:executing-plans`, батч-чекпоинты). Шаги — чекбоксы `- [ ]`. + +**Goal:** Наполнить граф скилов роутера данными — 86 карточек-контрактов (per-node) + явные двусторонние конфликт-рёбра — и закрыть это инвариантами; плюс одна точечная мех-правка (G-E). + +**Architecture:** Данные в `docs/registry/contracts/.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`. Ниже `` = этот путь. +- **vitest (через Bash, из `app/`):** `cd app && npx vitest run --config vitest.config.tools.mjs <подстрока-имени> --reporter dot`. НЕ `node …/vitest.mjs`, НЕ `npx` из корня. +- **git (через PowerShell):** по одной команде, литеральные пути, без переменных. Коммит: msg в `\.scratch\*.txt` (Write) → `git -C "" 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/.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 в `\.scratch\t1.txt`, затем) + +``` +git -C "" add tools/skill-contract.mjs tools/skill-contract.test.mjs +git -C "" commit -F "\.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 в `\.scratch\t2.txt`) + +``` +git -C "" add tools/card-coverage.mjs tools/card-coverage.test.mjs +git -C "" commit -F "\.scratch\t2.txt" +``` +Текст: `feat(card-coverage): чистый чекер покрытия узел↔карточка + конфликт-рёбра` + +--- + +## Канон авторинга карточки (применять в каждой data-задаче 3.x) + +**Файл:** `docs/registry/contracts/.contract.json`, где `` = `node.slug` из nodes.yaml. + +**Форма (валидатор `validateContract`):** +```json +{ + "skill": "", + "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": "", "hash": "", "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:** карточка не пустая — `needs∪produces` непусто. +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.1–3.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 `\.scratch\t4.txt`): `feat(nodes): конфликт-рёбра R-12 (Tooling §9 + ADR, двусторонние)` + ``` + git -C "" add docs/registry/nodes.yaml + git -C "" commit -F "\.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 `\.scratch\t5.txt`): `test(m3e): живой инвариант покрытия реестра карточками + конфликтами` + ``` + git -C "" add tools/m3e-card-coverage-invariants.test.mjs + git -C "" commit -F "\.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 `\.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`.