# Router Discipline Overhaul — Этап 1: Машиночитаемый реестр узлов

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Создать `docs/registry/nodes.yaml` — машиночитаемый реестр всех 83 узлов реестра тулчейна с триггерами, границами, цепочками L1-L16. Существующие `docs/Tooling_v8_3.md` §4.X и `docs/routing-off-phase.md` начинают авто-рендериться из реестра без потери смысла (diff = 0).

**Architecture:** YAML-реестр валидируется JSON Schema. Pure-module `registry-load.mjs` отдаёт индексированный объект `{nodes, chains, indexByTrigger, indexById}`. Pure-скрипт `registry-render.mjs` пишет в Markdown auto-region между маркерами `<!-- auto:registry --> ... <!-- /auto:registry -->`. Lefthook `registry-render-check` (warn-only first week) ловит дрейф «правил YAML, не пересобрал Markdown».

**Tech Stack:** Node.js (ESM `.mjs`), `js-yaml` (parser), `ajv` (JSON Schema validator), Vitest 4.x (тесты — `npm run test:tools` уже есть), lefthook 2.1.

**Scope-нота:** В этом этапе **ничего не enforced**. Реестр пока никто не читает кроме рендера. Поведение Claude не меняется. Цель — фундамент для этапа 2 (метрики) и этапа 3 (классификатор+хук).

**Spec:** [2026-05-23-router-discipline-overhaul-design.md](../specs/2026-05-23-router-discipline-overhaul-design.md) (этап 1).

---

## Pre-flight (один раз, перед Task 1)

- [ ] **PF-1: Pre-flight sync по Pravila §15.2**

```bash
git fetch origin
git log HEAD..origin/main --oneline
```

Expected: 0 коммитов (мы на feat/supplier-group-sync-fix, актуальная база; если коммиты появились — pre-flight по 8 нормативным файлам).

- [ ] **PF-2: Установить недостающие npm-зависимости (если их нет)**

```bash
cd app && npm ls js-yaml ajv 2>&1 | head -10
```

Expected: либо обе зависимости есть, либо `(empty)`. Если empty:

```bash
cd app && npm install --save-dev js-yaml ajv ajv-formats
```

Expected: добавлено 3 пакета без warning.

- [ ] **PF-3: Проверить структуру `tools/` для нового кода**

```bash
ls tools/*.test.mjs | head -5
```

Expected: видны существующие тесты (`brain-retro-analyzer.test.mjs` и др.). Это значит Vitest уже подцепляет `tools/*.test.mjs`.

---

## Phase A — Schema + Pilot (3 узла)

### Task 1: JSON Schema для узла реестра

**Files:**

- Create: `docs/registry/schema.json`
- Create: `docs/registry/README.md` (заглушка, наполним позже)

- [ ] **Step 1: Создать schema.json**

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://liderra.local/registry-schema.json",
  "title": "Liderra Node Registry",
  "type": "object",
  "required": ["version", "nodes", "chains"],
  "properties": {
    "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
    "nodes": {
      "type": "array",
      "items": { "$ref": "#/definitions/node" }
    },
    "chains": {
      "type": "object",
      "patternProperties": {
        "^L\\d+$": { "$ref": "#/definitions/chain" }
      }
    }
  },
  "definitions": {
    "node": {
      "type": "object",
      "required": ["id", "name", "slug", "category", "status", "triggers"],
      "properties": {
        "id": { "type": "string", "pattern": "^#\\d+$" },
        "name": { "type": "string", "minLength": 3 },
        "slug": { "type": "string", "pattern": "^[a-z0-9-]+(:[a-z0-9-]+)*$" },
        "category": { "enum": ["phase-0", "phase-1", "phase-2", "phase-3", "off-phase"] },
        "subcategory": { "type": ["string", "null"] },
        "status": { "enum": ["active", "dormant", "deferred", "historic"] },
        "dormancy_reason": { "type": ["string", "null"] },
        "triggers": {
          "type": "array",
          "items": {
            "oneOf": [
              { "type": "object", "required": ["keyword"], "properties": { "keyword": { "type": "string" }, "weight": { "type": "number", "minimum": 0, "maximum": 1 } } },
              { "type": "object", "required": ["classification"], "properties": { "classification": { "type": "string" }, "weight": { "type": "number" } } },
              { "type": "object", "required": ["file_pattern"], "properties": { "file_pattern": { "type": "string" }, "weight": { "type": "number" } } }
            ]
          }
        },
        "boundaries": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "adr": { "type": "string", "pattern": "^ADR-\\d{3}$" },
              "pair": { "type": "string", "pattern": "^#\\d+$" },
              "relation": { "type": "string" },
              "role": { "type": "string" }
            }
          }
        },
        "chain_membership": {
          "type": "array",
          "items": { "type": "string", "pattern": "^L\\d+$" }
        },
        "attributes": { "type": "object" }
      }
    },
    "chain": {
      "type": "object",
      "required": ["name", "sequence"],
      "properties": {
        "name": { "type": "string" },
        "sequence": { "type": "array", "items": { "type": "string" }, "minItems": 2 },
        "triggers": { "type": "array" }
      }
    }
  }
}
```

- [ ] **Step 2: Создать заглушку `docs/registry/README.md`**

```markdown
# Node Registry

Машиночитаемый реестр узлов тулчейна Лидерры. Single source of truth для router-procedure.md и хуков enforcement'а.

**Полная документация:** TBD (закроется в Task 13).
```

- [ ] **Step 3: Валидация schema.json**

```bash
cd app && node -e "const ajv = require('ajv'); const schema = require('../docs/registry/schema.json'); const v = new ajv.default({allErrors:true}); v.compile(schema); console.log('schema valid');"
```

Expected: `schema valid` без ошибок.

- [ ] **Step 4: Commit**

```bash
git add docs/registry/schema.json docs/registry/README.md
git commit -m "feat(registry): JSON Schema для узла реестра (этап 1, task 1)

Schema поддерживает: id/name/slug/category/status, триггеры трёх видов
(keyword/classification/file_pattern), границы (adr/pair), членство в
цепочках L1-L16, dormancy/deferred-статус.

README — заглушка, наполнится в Task 13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

### Task 2: Извлечь 3 пилотных узла в YAML

**Files:**

- Create: `docs/registry/nodes.yaml`

Пилотные узлы выбраны как репрезентативные: **#19 Superpowers** (phase-2, active, член L1/L8), **#18 Pest 4** (phase-1, active), **#1 PostgreSQL MCP** (phase-0, historic — заменён #10 Boost).

- [ ] **Step 1: Создать `docs/registry/nodes.yaml` с 3 узлами**

```yaml
version: "0.1.0"

nodes:
  - id: "#19"
    name: "Superpowers v5.1.0"
    slug: "superpowers"
    category: "phase-2"
    subcategory: null
    status: "active"
    dormancy_reason: null
    triggers:
      - {classification: "feature", weight: 1.0}
      - {classification: "planning", weight: 1.0}
      - {classification: "bugfix", weight: 0.8}
      - {classification: "refactor", weight: 0.8}
      - {keyword: "tdd", weight: 1.0}
      - {keyword: "brainstorm", weight: 1.0}
      - {keyword: "debug", weight: 0.8}
    boundaries:
      - {adr: "ADR-011", role: "hard-floor source"}
      - {pair: "#30", relation: "paired stack"}
    chain_membership: ["L1", "L8"]
    attributes:
      tooling_section: "§3.3 #19"
      install: "marketplace plugin, enabled in ~/.claude/settings.json"

  - id: "#18"
    name: "Pest 4"
    slug: "pest"
    category: "phase-1"
    subcategory: null
    status: "active"
    dormancy_reason: null
    triggers:
      - {classification: "bugfix", weight: 1.0}
      - {keyword: "test", weight: 1.0}
      - {keyword: "тест", weight: 1.0}
      - {file_pattern: "tests/**/*.php", weight: 1.0}
    boundaries: []
    chain_membership: ["L8"]
    attributes:
      tooling_section: "§3.2 #18"
      install: "composer require pestphp/pest --dev"

  - id: "#1"
    name: "PostgreSQL MCP"
    slug: "postgres-mcp"
    category: "phase-0"
    subcategory: null
    status: "historic"
    dormancy_reason: "Заменён #10 Laravel Boost в фазе 1 (08.05.2026)"
    triggers: []
    boundaries:
      - {pair: "#10", relation: "replaced by"}
    chain_membership: []
    attributes:
      tooling_section: "§3.1 #1 (historic)"

chains:
  L1:
    name: "Brainstorming chain"
    sequence:
      - "superpowers:brainstorming"
      - "superpowers:writing-plans"
      - "superpowers:executing-plans"
    triggers:
      - {classification: "feature"}
      - {classification: "planning"}

  L8:
    name: "TDD chain"
    sequence:
      - "superpowers:test-driven-development"
      - "#18"
    triggers:
      - {classification: "bugfix"}
      - {keyword: "tdd"}
```

- [ ] **Step 2: Прогнать через JSON Schema**

```bash
cd app && node -e "
const ajv = require('ajv').default;
const addFormats = require('ajv-formats').default;
const yaml = require('js-yaml');
const fs = require('fs');
const schema = JSON.parse(fs.readFileSync('../docs/registry/schema.json', 'utf-8'));
const data = yaml.load(fs.readFileSync('../docs/registry/nodes.yaml', 'utf-8'));
const v = new ajv({allErrors:true});
addFormats(v);
const validate = v.compile(schema);
const ok = validate(data);
if (!ok) { console.error(JSON.stringify(validate.errors, null, 2)); process.exit(1); }
console.log('nodes.yaml valid — 3 узла, 2 цепочки');
"
```

Expected: `nodes.yaml valid — 3 узла, 2 цепочки`.

- [ ] **Step 3: Commit**

```bash
git add docs/registry/nodes.yaml
git commit -m "feat(registry): 3 пилотных узла в nodes.yaml (этап 1, task 2)

#19 Superpowers (phase-2 active, L1+L8 chains)
#18 Pest 4 (phase-1 active, L8 chain)
#1 PostgreSQL MCP (phase-0 historic, replaced by #10 Boost)

YAML валидируется JSON Schema. Остальные 80 узлов — Task 8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

### Task 3: Pure module `tools/registry-load.mjs`

**Files:**

- Create: `tools/registry-load.mjs`

- [ ] **Step 1: Создать pure module**

```javascript
// tools/registry-load.mjs
// Pure module — загрузка и индексация реестра узлов.
// Никаких сетевых вызовов, никакого фс-исключения кроме чтения.

import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import yaml from 'js-yaml';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const __dirname = dirname(fileURLToPath(import.meta.url));
const DEFAULT_REGISTRY_PATH = join(__dirname, '..', 'docs', 'registry', 'nodes.yaml');
const DEFAULT_SCHEMA_PATH = join(__dirname, '..', 'docs', 'registry', 'schema.json');

let _cached = null;

export function loadRegistry({ registryPath = DEFAULT_REGISTRY_PATH, schemaPath = DEFAULT_SCHEMA_PATH, useCache = true } = {}) {
  if (useCache && _cached) return _cached;

  const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
  const data = yaml.load(readFileSync(registryPath, 'utf-8'));

  const ajv = new Ajv({ allErrors: true });
  addFormats(ajv);
  const validate = ajv.compile(schema);
  if (!validate(data)) {
    throw new Error(`Registry schema violation:\n${JSON.stringify(validate.errors, null, 2)}`);
  }

  const indexById = new Map();
  for (const node of data.nodes) indexById.set(node.id, node);

  const indexByTrigger = new Map();
  for (const node of data.nodes) {
    if (node.status !== 'active') continue;
    for (const t of node.triggers || []) {
      const key = t.classification ? `cls:${t.classification}` :
                  t.keyword ? `kw:${t.keyword}` :
                  t.file_pattern ? `fp:${t.file_pattern}` : null;
      if (!key) continue;
      if (!indexByTrigger.has(key)) indexByTrigger.set(key, []);
      indexByTrigger.get(key).push({ node, weight: t.weight ?? 1.0 });
    }
  }

  const result = {
    nodes: data.nodes,
    chains: data.chains,
    version: data.version,
    indexById,
    indexByTrigger,
  };
  if (useCache) _cached = result;
  return result;
}

export function clearCache() { _cached = null; }

export function findByClassification(registry, classification) {
  return (registry.indexByTrigger.get(`cls:${classification}`) || [])
    .sort((a, b) => b.weight - a.weight);
}

export function findByKeyword(registry, keyword) {
  return (registry.indexByTrigger.get(`kw:${keyword.toLowerCase()}`) || [])
    .sort((a, b) => b.weight - a.weight);
}

export function findActiveNodes(registry) {
  return registry.nodes.filter(n => n.status === 'active');
}

export function findChainsByNode(registry, nodeIdOrSlug) {
  const chains = [];
  for (const [chainId, chain] of Object.entries(registry.chains)) {
    if (chain.sequence.some(s => s === nodeIdOrSlug)) {
      chains.push({ chainId, ...chain });
    }
  }
  return chains;
}
```

- [ ] **Step 2: Smoke-проверка вручную**

```bash
cd app && node -e "
import('../tools/registry-load.mjs').then(m => {
  const r = m.loadRegistry();
  console.log('nodes:', r.nodes.length);
  console.log('chains:', Object.keys(r.chains).length);
  console.log('triggers cls indexed:', [...r.indexByTrigger.keys()].filter(k => k.startsWith('cls:')));
  const features = m.findByClassification(r, 'feature');
  console.log('feature → ', features.map(f => f.node.id));
});
"
```

Expected:

```
nodes: 3
chains: 2
triggers cls indexed: [ 'cls:feature', 'cls:planning', 'cls:bugfix', 'cls:refactor' ]
feature →  [ '#19' ]
```

- [ ] **Step 3: Commit**

```bash
git add tools/registry-load.mjs
git commit -m "feat(registry): pure module registry-load.mjs (этап 1, task 3)

Экспортирует loadRegistry/findByClassification/findByKeyword/
findActiveNodes/findChainsByNode + clearCache для тестов.

Кэширует в module-scope (per-process); валидирует через ajv при
загрузке. Тестов нет — Task 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

### Task 4: Unit-тесты для `registry-load.mjs`

**Files:**

- Create: `tools/registry-load.test.mjs`

- [ ] **Step 1: Написать падающий тест #1 (загрузка пилотного реестра)**

```javascript
// tools/registry-load.test.mjs
import { describe, it, expect, beforeEach } from 'vitest';
import { loadRegistry, clearCache, findByClassification, findByKeyword, findActiveNodes, findChainsByNode } from './registry-load.mjs';

describe('registry-load', () => {
  beforeEach(() => clearCache());

  it('loads pilot registry with 3 nodes', () => {
    const r = loadRegistry();
    expect(r.nodes).toHaveLength(3);
    expect(r.version).toBe('0.1.0');
  });
});
```

- [ ] **Step 2: Запустить — должен FAIL пока тест не написан (или PASS если registry уже есть)**

```bash
cd app && npx vitest run ../tools/registry-load.test.mjs
```

Expected: PASS (тест простой и должен пройти на текущей структуре).

- [ ] **Step 3: Добавить ещё ≥9 тестов**

```javascript
  it('indexes by classification', () => {
    const r = loadRegistry();
    const features = findByClassification(r, 'feature');
    expect(features).toHaveLength(1);
    expect(features[0].node.id).toBe('#19');
    expect(features[0].weight).toBe(1.0);
  });

  it('returns empty array for unknown classification', () => {
    const r = loadRegistry();
    expect(findByClassification(r, 'nonexistent')).toEqual([]);
  });

  it('indexes by keyword case-insensitive', () => {
    const r = loadRegistry();
    expect(findByKeyword(r, 'tdd')).toHaveLength(1);
    expect(findByKeyword(r, 'TDD')).toHaveLength(1);
  });

  it('excludes historic/dormant from trigger index', () => {
    const r = loadRegistry();
    // #1 PostgreSQL MCP — historic, не должен попасть в индексы
    for (const entries of r.indexByTrigger.values()) {
      expect(entries.find(e => e.node.id === '#1')).toBeUndefined();
    }
  });

  it('includes historic in nodes array (full reference)', () => {
    const r = loadRegistry();
    expect(r.indexById.get('#1').status).toBe('historic');
  });

  it('findActiveNodes excludes non-active', () => {
    const r = loadRegistry();
    const active = findActiveNodes(r);
    expect(active.map(n => n.id).sort()).toEqual(['#18', '#19']);
  });

  it('findChainsByNode returns chain membership', () => {
    const r = loadRegistry();
    const superpowersChains = findChainsByNode(r, 'superpowers:test-driven-development');
    expect(superpowersChains.map(c => c.chainId)).toContain('L8');
  });

  it('caches across calls in same process', () => {
    const r1 = loadRegistry();
    const r2 = loadRegistry();
    expect(r1).toBe(r2); // same reference
  });

  it('clearCache forces reload', () => {
    const r1 = loadRegistry();
    clearCache();
    const r2 = loadRegistry();
    expect(r1).not.toBe(r2);
  });

  it('throws on schema violation', () => {
    expect(() => loadRegistry({ registryPath: '/nonexistent/path.yaml', useCache: false })).toThrow();
  });
```

- [ ] **Step 4: Запустить все тесты**

```bash
cd app && npx vitest run ../tools/registry-load.test.mjs
```

Expected: ≥10 tests passed (1 + 10 = 11).

- [ ] **Step 5: Commit**

```bash
git add tools/registry-load.test.mjs
git commit -m "test(registry): 11 unit-тестов для registry-load.mjs (этап 1, task 4)

Покрытие: индексация по classification/keyword, exclude
historic/dormant из индексов, cache lifecycle, schema violation.

Все 11 GREEN на пилотном реестре из 3 узлов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

---

## Phase B — Render и интеграция

### Task 5: Pure-скрипт `tools/registry-render.mjs`

**Files:**

- Create: `tools/registry-render.mjs`

- [ ] **Step 1: Создать скрипт рендера**

```javascript
// tools/registry-render.mjs
// Pure script — рендерит секции реестра в Markdown auto-region.
// Запуск: node tools/registry-render.mjs [--check]
// --check режим: возвращает exit 1 если rendered ≠ файлу (lefthook job).

import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { loadRegistry } from './registry-load.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, '..');

const REGIONS = [
  {
    file: join(REPO_ROOT, 'docs', 'Tooling_v8_3.md'),
    marker: 'tooling-registry-summary',
    renderer: renderToolingSummary,
  },
  {
    file: join(REPO_ROOT, 'docs', 'routing-off-phase.md'),
    marker: 'routing-table',
    renderer: renderRoutingTable,
  },
];

function renderToolingSummary(registry) {
  const lines = ['<!-- This block is auto-generated from docs/registry/nodes.yaml. Do not edit by hand. -->', ''];
  lines.push(`| ID | Узел | Категория | Статус |`);
  lines.push(`|---|---|---|---|`);
  for (const node of registry.nodes) {
    lines.push(`| ${node.id} | ${node.name} | ${node.category} | ${node.status} |`);
  }
  lines.push('');
  return lines.join('\n');
}

function renderRoutingTable(registry) {
  const lines = ['<!-- This block is auto-generated from docs/registry/nodes.yaml. Do not edit by hand. -->', ''];
  const byClass = new Map();
  for (const node of registry.nodes) {
    if (node.status !== 'active') continue;
    for (const t of node.triggers) {
      if (!t.classification) continue;
      if (!byClass.has(t.classification)) byClass.set(t.classification, []);
      byClass.get(t.classification).push({ node, weight: t.weight ?? 1.0 });
    }
  }
  lines.push(`| Классификация | Рекомендуемый узел | Вес |`);
  lines.push(`|---|---|---|`);
  for (const [cls, entries] of [...byClass.entries()].sort()) {
    entries.sort((a, b) => b.weight - a.weight);
    for (const e of entries) {
      lines.push(`| \`${cls}\` | ${e.node.id} ${e.node.name} | ${e.weight} |`);
    }
  }
  lines.push('');
  return lines.join('\n');
}

export function replaceRegion(content, marker, rendered) {
  const start = `<!-- auto:${marker}:begin -->`;
  const end = `<!-- auto:${marker}:end -->`;
  const startIdx = content.indexOf(start);
  const endIdx = content.indexOf(end);
  if (startIdx === -1 || endIdx === -1) {
    throw new Error(`Markers <!-- auto:${marker}:begin/end --> not found`);
  }
  const before = content.slice(0, startIdx + start.length);
  const after = content.slice(endIdx);
  return `${before}\n${rendered}\n${after}`;
}

export function renderAll({ check = false } = {}) {
  const registry = loadRegistry();
  const results = [];
  for (const region of REGIONS) {
    const current = readFileSync(region.file, 'utf-8');
    const rendered = region.renderer(registry);
    const next = replaceRegion(current, region.marker, rendered);
    const drift = current !== next;
    if (check && drift) {
      results.push({ file: region.file, drift: true });
      continue;
    }
    if (!check && drift) {
      writeFileSync(region.file, next, 'utf-8');
    }
    results.push({ file: region.file, drift });
  }
  return results;
}

// Точка входа CLI
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`) {
  const check = process.argv.includes('--check');
  const results = renderAll({ check });
  for (const r of results) {
    console.log(`${r.drift ? '⚠️ drift' : '✅ ok  '} ${r.file}`);
  }
  if (check && results.some(r => r.drift)) {
    console.error('\nDrift detected. Run `node tools/registry-render.mjs` to regenerate.');
    process.exit(1);
  }
}
```

- [ ] **Step 2: Smoke — попробовать рендер БЕЗ маркеров (должен упасть)**

```bash
cd app && node ../tools/registry-render.mjs --check
```

Expected: упадёт с `Markers <!-- auto:tooling-registry-summary:begin/end --> not found`. Это норма — маркеры добавим в Task 6.

- [ ] **Step 3: Commit**

```bash
git add tools/registry-render.mjs
git commit -m "feat(registry): renderer YAML → Markdown auto-region (этап 1, task 5)

renderAll() режим без --check переписывает файлы; с --check
возвращает exit 1 на drift (для lefthook).

Сейчас рендерит 2 региона: Tooling summary + routing-table.
Маркеры в Markdown добавим в Task 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

### Task 6: Маркеры auto-region в Markdown

**Files:**

- Modify: `docs/Tooling_v8_3.md` (добавить маркеры вокруг места для summary-таблицы)
- Modify: `docs/routing-off-phase.md` (добавить маркеры вокруг routing-table)

- [ ] **Step 1: Найти место в `docs/Tooling_v8_3.md` для summary-таблицы**

```bash
grep -n "^## " docs/Tooling_v8_3.md | head -10
```

Expected: список заголовков. Найдём первый после `## 4` (или ближайший к началу §4) — туда добавим под-секцию `### 4.0 Краткая сводка узлов (auto-generated)`.

- [ ] **Step 2: Добавить маркеры под новой подсекцией**

Найти строку с `## 4. Реестр инструментов` (или эквивалент в текущем файле). После заголовка вставить:

```markdown
### 4.0 Краткая сводка узлов (auto-generated)

<!-- auto:tooling-registry-summary:begin -->
<!-- auto:tooling-registry-summary:end -->
```

- [ ] **Step 3: Добавить маркеры в `docs/routing-off-phase.md`**

Найти секцию routing-таблицы (грепом по `| Классификация |` или похожим), обернуть существующую таблицу маркерами:

```markdown
<!-- auto:routing-table:begin -->
<!-- auto:routing-table:end -->
```

Если таблицы пока нет — просто вставить пустой блок перед `## 4 Chains` (или эквивалент).

- [ ] **Step 4: Запустить рендер (без --check)**

```bash
cd app && node ../tools/registry-render.mjs
```

Expected:

```
✅ ok   .../docs/Tooling_v8_3.md  (или ⚠️ drift в первый раз, потом ✅)
✅ ok   .../docs/routing-off-phase.md
```

- [ ] **Step 5: Проверить что регионы заполнились**

```bash
grep -A 20 "auto:tooling-registry-summary:begin" docs/Tooling_v8_3.md | head -25
```

Expected: видна таблица с 3 строками (#19/#18/#1).

- [ ] **Step 6: Запустить --check (должен пройти)**

```bash
cd app && node ../tools/registry-render.mjs --check
```

Expected: `✅ ok` для обоих файлов, exit 0.

- [ ] **Step 7: Commit**

```bash
git add docs/Tooling_v8_3.md docs/routing-off-phase.md
git commit -m "docs(registry): маркеры auto-region в Tooling+routing-off-phase (этап 1, task 6)

§4.0 Tooling — краткая сводка узлов (auto-generated из nodes.yaml).
routing-off-phase — routing-таблица (auto-generated).

После Task 8 (все 83 узла) таблицы наполнятся; сейчас — 3 пилотных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

### Task 7: Snapshot-тесты для renderer'а

**Files:**

- Create: `tools/registry-render.test.mjs`

- [ ] **Step 1: Написать тесты**

```javascript
// tools/registry-render.test.mjs
import { describe, it, expect } from 'vitest';
import { replaceRegion } from './registry-render.mjs';

describe('registry-render', () => {
  it('replaces auto-region between markers', () => {
    const input = `Header\n<!-- auto:foo:begin -->\nold content\n<!-- auto:foo:end -->\nFooter`;
    const out = replaceRegion(input, 'foo', 'NEW CONTENT');
    expect(out).toContain('NEW CONTENT');
    expect(out).not.toContain('old content');
    expect(out).toContain('Header');
    expect(out).toContain('Footer');
  });

  it('preserves end marker exactly', () => {
    const input = `<!-- auto:foo:begin -->\nold\n<!-- auto:foo:end -->`;
    const out = replaceRegion(input, 'foo', 'new');
    expect(out).toMatch(/<!-- auto:foo:end -->$/);
  });

  it('throws if markers missing', () => {
    expect(() => replaceRegion('no markers here', 'foo', 'x')).toThrow(/Markers.*not found/);
  });

  it('throws if only begin marker present', () => {
    expect(() => replaceRegion('<!-- auto:foo:begin -->\nstuff', 'foo', 'x')).toThrow();
  });

  it('handles multi-line rendered content', () => {
    const input = `<!-- auto:foo:begin -->\n<!-- auto:foo:end -->`;
    const out = replaceRegion(input, 'foo', 'line1\nline2\nline3');
    expect(out).toMatch(/line1\nline2\nline3/);
  });
});
```

- [ ] **Step 2: Запустить тесты**

```bash
cd app && npx vitest run ../tools/registry-render.test.mjs
```

Expected: 5 tests passed.

- [ ] **Step 3: Commit**

```bash
git add tools/registry-render.test.mjs
git commit -m "test(registry): 5 unit-тестов для replaceRegion (этап 1, task 7)

Покрытие: replacement, preservation границ, ошибки на пропавших маркерах,
multi-line content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

---

## Phase C — Полный перенос 83 узлов

### Task 8: Извлечь все 83 узла в `nodes.yaml`

**Files:**

- Modify: `docs/registry/nodes.yaml` (расширить с 3 до 83 узлов)

**Подход:** Парсим `docs/Tooling_v8_3.md` Прил. Н §4.X. Каждый узел там оформлен 9-attribute блоком. Парсинг **ручной с верификацией** (не subagent — высокий риск пропустить нюансы). Группами по 10-15 узлов с промежуточными коммитами.

- [ ] **Step 1: Извлечь оглавление узлов Tooling**

```bash
grep -E "^### §4\.[0-9]+" docs/Tooling_v8_3.md | head -100
```

Expected: видим все §4.1 .. §4.58 (примерно). Это список наших источников.

- [ ] **Step 2: Прочитать §4.1 .. §4.10 целиком (phase-0/1)**

Прочитать Tooling §4.1 — §4.10 одной операцией Read. Для каждого узла извлечь: id (`#N`), name, slug (если есть), category, status, dormancy_reason, triggers (из блока «когда использовать»), boundaries (из ADR ссылок), chain_membership (из routing-off-phase.md L1-L16 секций).

- [ ] **Step 3: Добавить узлы #2..#17 в nodes.yaml**

Дописать в `docs/registry/nodes.yaml` массив `nodes:`. Пример формы:

```yaml
  - id: "#10"
    name: "Laravel Boost"
    slug: "boost"
    category: "phase-1"
    subcategory: null
    status: "active"
    dormancy_reason: null
    triggers:
      - {keyword: "boost", weight: 1.0}
      - {file_pattern: "app/**/*.php", weight: 0.3}
    boundaries: []
    chain_membership: []
    attributes:
      tooling_section: "§4.NN"
      install: "composer require laravel/boost --dev"
```

(И так далее для каждого узла. Реальные значения triggers/boundaries — из Tooling.)

- [ ] **Step 4: Прогнать schema валидацию**

```bash
cd app && node -e "
const m = await import('../tools/registry-load.mjs');
m.clearCache();
const r = m.loadRegistry({useCache: false});
console.log('nodes:', r.nodes.length);
"
```

Expected: `nodes: 17` (или сколько уже добавили — рост виден).

- [ ] **Step 5: Commit прогресс**

```bash
git add docs/registry/nodes.yaml
git commit -m "feat(registry): +узлы #2..#17 (phase-0/1, task 8a)

17 узлов в реестре. Triggers/boundaries извлечены из Tooling §4.1..§4.10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

- [ ] **Step 6: Повторить шаги 2-5 для узлов #20..#35 (phase-2 + off-phase ранние)**

Аналогично. Commit `feat(registry): +узлы #20..#35 (task 8b)`.

- [ ] **Step 7: Повторить для #36..#55 (off-phase средние)**

Commit `feat(registry): +узлы #36..#55 (task 8c)`.

- [ ] **Step 8: Повторить для #56..#83 (off-phase поздние)**

Commit `feat(registry): +узлы #56..#83 (task 8d)`.

- [ ] **Step 9: Финальная валидация**

```bash
cd app && node -e "
const m = await import('../tools/registry-load.mjs');
m.clearCache();
const r = m.loadRegistry({useCache: false});
console.log('total nodes:', r.nodes.length);
console.log('active:', r.nodes.filter(n => n.status === 'active').length);
console.log('dormant:', r.nodes.filter(n => n.status === 'dormant').length);
console.log('deferred:', r.nodes.filter(n => n.status === 'deferred').length);
console.log('historic:', r.nodes.filter(n => n.status === 'historic').length);
console.log('chains:', Object.keys(r.chains).length);
"
```

Expected: total = 83, sum по статусам = 83, chains = 16 (L1..L16).

### Task 9: Добавить L1-L16 chains в реестр

**Files:**

- Modify: `docs/registry/nodes.yaml` (расширить `chains:` секцию)

- [ ] **Step 1: Прочитать `docs/routing-off-phase.md` §4 chains**

```bash
grep -n "^### L[0-9]" docs/routing-off-phase.md
```

- [ ] **Step 2: Для каждой L1-L16 chain добавить в nodes.yaml `chains:`**

Каждая chain — `{name, sequence (массив slug/id), triggers}`. Пример L8:

```yaml
  L8:
    name: "TDD chain"
    sequence:
      - "superpowers:test-driven-development"
      - "#18"
      - "superpowers:verification-before-completion"
    triggers:
      - {classification: "bugfix"}
      - {keyword: "tdd"}
```

- [ ] **Step 3: Валидация — 16 chains**

```bash
cd app && node -e "
const m = await import('../tools/registry-load.mjs');
m.clearCache();
const r = m.loadRegistry({useCache: false});
console.log('chains:', Object.keys(r.chains).sort());
"
```

Expected: `['L1','L10','L11','L12','L13','L14','L15','L16','L2','L3','L4','L5','L6','L7','L8','L9']`.

- [ ] **Step 4: Commit**

```bash
git add docs/registry/nodes.yaml
git commit -m "feat(registry): +L1-L16 canonical chains (этап 1, task 9)

16 цепочек из routing-off-phase.md §4 в machine-readable виде.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

### Task 10: Diff-check — rendered Markdown совместим с original

**Files:**

- Modify: `docs/Tooling_v8_3.md` (regenerate summary)
- Modify: `docs/routing-off-phase.md` (regenerate routing table)

- [ ] **Step 1: Регенерировать оба auto-region**

```bash
cd app && node ../tools/registry-render.mjs
```

Expected: `✅ ok` для обоих файлов (или `⚠️ drift` + перезапись, если ранее были 3 узла — теперь 83).

- [ ] **Step 2: Проверить git diff**

```bash
git diff --stat docs/Tooling_v8_3.md docs/routing-off-phase.md
```

Expected: оба файла изменены, но **ТОЛЬКО внутри auto-region маркеров**. Если диф вышел за маркеры — баг рендера.

- [ ] **Step 3: Проверить содержимое регионов**

```bash
sed -n '/auto:tooling-registry-summary:begin/,/auto:tooling-registry-summary:end/p' docs/Tooling_v8_3.md | wc -l
```

Expected: ~85 строк (83 узла + header + footer).

- [ ] **Step 4: Прогнать --check (должен пройти)**

```bash
cd app && node ../tools/registry-render.mjs --check
```

Expected: `✅ ok` оба, exit 0.

- [ ] **Step 5: Commit**

```bash
git add docs/Tooling_v8_3.md docs/routing-off-phase.md
git commit -m "docs(registry): re-render auto-region на полном реестре 83 узлов (этап 1, task 10)

Tooling §4.0 — сводная таблица 83 узлов из реестра.
routing-off-phase — routing-table по всем классификациям.

Diff только внутри auto-region маркеров — проверено.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

---

## Phase D — Закрытие этапа

### Task 11: Lefthook job `registry-render-check`

**Files:**

- Modify: `lefthook.yml` (+1 pre-commit job, warn-only первую неделю)

- [ ] **Step 1: Прочитать текущий lefthook.yml**

```bash
grep -n "^  [a-z]" lefthook.yml | head -20
```

Expected: видны существующие jobs (markdownlint, cspell, lychee, gitleaks, …).

- [ ] **Step 2: Добавить новый job**

В секции `pre-commit:` блока `commands:` (или эквивалент):

```yaml
  registry-render-check:
    glob: "docs/registry/nodes.yaml,docs/Tooling_v8_3.md,docs/routing-off-phase.md"
    run: |
      node tools/registry-render.mjs --check || {
        echo "[registry] WARN: rendered ≠ файл. Запусти 'node tools/registry-render.mjs' и закоммить."
        exit 0  # warn-only первую неделю, потом убрать "exit 0"
      }
```

- [ ] **Step 3: Создать намеренный drift — проверить что warn срабатывает**

```bash
# Изменить nodes.yaml (например, добавить пробел в строке)
echo " " >> docs/registry/nodes.yaml
git add docs/registry/nodes.yaml
git commit -m "test: проверка лефтхук-warn (откатим)"
# Должен увидеть WARN в выводе, но коммит проходит
git reset HEAD~1
git checkout docs/registry/nodes.yaml
```

Expected: видим WARN-сообщение, коммит проходит (warn-only).

- [ ] **Step 4: Commit lefthook config**

```bash
git add lefthook.yml
git commit -m "feat(lefthook): registry-render-check pre-commit warn-only (этап 1, task 11)

Триггерится на изменения nodes.yaml/Tooling/routing-off-phase.
Warn-only первую неделю (exit 0); после стабилизации — убрать exit 0
и сделать blocking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

### Task 12: Полноценный `docs/registry/README.md`

**Files:**

- Modify: `docs/registry/README.md` (заменить заглушку из Task 1)

- [ ] **Step 1: Написать полный README**

```markdown
# Node Registry

Машиночитаемый реестр узлов тулчейна Лидерры — single source of truth для router-procedure.md и хуков enforcement'а (этапы 2-3 router discipline overhaul).

## Файлы

- **`nodes.yaml`** — реестр 83 узлов + 16 цепочек L1-L16. Источник истины.
- **`schema.json`** — JSON Schema валидация `nodes.yaml` при загрузке.
- **`README.md`** — этот файл.

## Как читать узел

```yaml
- id: "#19"                # уникальный идентификатор из Tooling Прил. Н
  name: "Superpowers v5.1.0"
  slug: "superpowers"       # каноническое имя для invocation
  category: "phase-2"       # phase-0/1/2/3 или off-phase
  status: "active"          # active | dormant | deferred | historic
  triggers:                 # как роутер выбирает этот узел
    - {classification: "feature", weight: 1.0}
    - {keyword: "tdd", weight: 1.0}
  boundaries:               # связи с другими узлами (ADR-ссылки, paired stack)
    - {adr: "ADR-011", role: "hard-floor source"}
    - {pair: "#30", relation: "paired stack"}
  chain_membership: ["L1", "L8"]   # в каких L-цепочках участвует
```

## Как добавить новый узел

1. Получить новый `#N` из Tooling Прил. Н §0 (канон счётчика).
2. Открыть `nodes.yaml`, добавить блок в массив `nodes:`.
3. Триггеры — что должен сказать заказчик/какой класс задач включает узел.
4. Границы — какие ADR разделяют узел от соседей.
5. Прогнать `node tools/registry-render.mjs` — должно перерендерить Markdown.
6. Запустить тесты: `cd app && npx vitest run ../tools/registry-load.test.mjs`.

## Auto-render

`tools/registry-render.mjs` пишет в auto-region маркеры:

- `<!-- auto:tooling-registry-summary:begin -->` в `docs/Tooling_v8_3.md`
- `<!-- auto:routing-table:begin -->` в `docs/routing-off-phase.md`

**Не правьте содержимое между маркерами вручную** — оно перезатрётся.

## Lefthook job

`registry-render-check` (pre-commit, warn-only first week) — ловит дрейф «правил YAML, не пересобрал Markdown». Если видишь WARN — запусти `node tools/registry-render.mjs && git add -u && git commit --amend --no-edit`.

## Связано

- Spec: [`docs/superpowers/specs/2026-05-23-router-discipline-overhaul-design.md`](../specs/2026-05-23-router-discipline-overhaul-design.md)
- Этап 1 plan: [`docs/superpowers/plans/2026-05-23-router-overhaul-stage-1-registry.md`](2026-05-23-router-overhaul-stage-1-registry.md)
- Pravila §15.2 — pre-flight sync для нормативных файлов (Tooling §4.X входит в список).
- ADR-011 — brain governance.

```

- [ ] **Step 2: Commit**

```bash
git add docs/registry/README.md
git commit -m "docs(registry): полный README (этап 1, task 12)

Описывает структуру узла, процесс добавления, auto-render, lefthook job,
cross-refs на spec/plan/Pravila/ADR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

### Task 13: Финальная регрессия + STATUS.md обновление

**Files:**

- Verify: вся ветка, regression run.
- Modify: `docs/observer/STATUS.md` — добавить блок «Активные многоэтапные проекты» (continuity механизм из spec).

- [ ] **Step 1: Quick regression**

```bash
cd app && npx vitest run ../tools/registry-load.test.mjs ../tools/registry-render.test.mjs
```

Expected: все тесты GREEN (≥16 total).

- [ ] **Step 2: Markdownlint + cspell + lychee**

```bash
npm run lint:md && npm run spell && npm run links
```

Expected: 0 errors на новых файлах (`docs/registry/*`, `tools/registry-*.mjs`).

- [ ] **Step 3: Добавить блок «Активные проекты» в STATUS.md generator**

Найти `tools/status-md-generator.mjs`, добавить функцию `renderActiveProjects()` которая читает `memory/project_router_overhaul.md` (создадим в Step 4) и рендерит блок:

```markdown
## Активные многоэтапные проекты

- **Router discipline overhaul** ([spec](../specs/2026-05-23-router-discipline-overhaul-design.md))
  - Этап 1 ✅ закрыт YYYY-MM-DD (коммиты abc..def)
  - Этап 2 ⏸ ждёт «продолжаем» от заказчика
  - Следующий: `docs/superpowers/plans/2026-XX-XX-router-overhaul-stage-2.md`
```

- [ ] **Step 4: Создать memory-файл continuity**

`memory/project_router_overhaul.md`:

```markdown
---
name: project-router-overhaul
description: Router discipline overhaul — multi-stage project, current stage tracker
metadata:
  type: project
---

# Router discipline overhaul

**Spec:** [docs/superpowers/specs/2026-05-23-router-discipline-overhaul-design.md](../docs/superpowers/specs/2026-05-23-router-discipline-overhaul-design.md)

## Stages

- **Этап 1 (Справочник):** ✅ DONE YYYY-MM-DD (commits abc..def, PR #N).
- **Этап 2 (Измерения):** ⏸ ждёт «продолжаем» от заказчика. Plan: TBD.
- **Этап 3 (Принуждение):** не начат.
- **Этап 4 (Уборка правил):** не начат.

## Last action

YYYY-MM-DD — этап 1 закрыт. Жду команды «продолжаем».
```

Добавить ссылку в `memory/MEMORY.md` index.

- [ ] **Step 5: Commit + PR**

```bash
git add docs/observer/STATUS.md tools/status-md-generator.mjs memory/project_router_overhaul.md memory/MEMORY.md
git commit -m "feat(continuity): STATUS.md \"Активные проекты\" + memory tracker (этап 1, task 13)

STATUS.md generator теперь рендерит блок активных проектов из memory.
Memory-файл project_router_overhaul.md — single source состояния
многоэтапного проекта.

Этап 1 закрыт.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"

git push origin feat/supplier-group-sync-fix
```

- [ ] **Step 6: Создать PR в main**

```bash
gh pr create --title "feat(registry): машиночитаемый реестр узлов (router overhaul этап 1)" --body "$(cat <<'EOF'
## Summary
- Создан `docs/registry/nodes.yaml` — 83 узла + 16 цепочек L1-L16, валидируется JSON Schema.
- `tools/registry-load.mjs` + 11 unit-тестов GREEN (индексация по триггерам, lifecycle, schema violation).
- `tools/registry-render.mjs` + 5 snapshot-тестов GREEN — рендерит auto-region в Tooling §4.0 и routing-off-phase routing-table.
- Lefthook `registry-render-check` (pre-commit, warn-only первую неделю) ловит дрейф.
- Continuity: STATUS.md «Активные проекты» + memory tracker `project_router_overhaul.md`.

**Поведение Claude не меняется.** Это фундамент для этапа 2 (метрики) и этапа 3 (классификатор+хук) — см. [spec](docs/superpowers/specs/2026-05-23-router-discipline-overhaul-design.md).

## Test plan
- [x] Vitest: registry-load + registry-render — 16 tests GREEN
- [x] JSON Schema валидация nodes.yaml — passing
- [x] node tools/registry-render.mjs --check — exit 0
- [x] markdownlint/cspell/lychee — 0 errors
- [x] git diff на Tooling+routing-off-phase — только внутри auto-region маркеров

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```

- [ ] **Step 7: Спросить заказчика про этап 2**

```
Этап 1 закрыт. Метрики:
- 83 узла в реестре, 16 цепочек.
- 16 unit-тестов GREEN.
- Auto-render Tooling §4.0 + routing-off-phase работает.
- Lefthook gate (warn-only) поставлен.
- PR #N в main.

Поведение Claude не изменилось — реестр пока никем не enforce'ится.

Запускаем этап 2 (измерения + парсер fix) или пауза?
```

---

## Self-Review

**1. Spec coverage:**

- ✅ `docs/registry/nodes.yaml` — Task 2 (пилотные 3) + Task 8 (полные 83) + Task 9 (chains).
- ✅ `docs/registry/schema.json` — Task 1.
- ✅ `tools/registry-render.mjs` — Task 5.
- ✅ `tools/registry-load.mjs` + ≥10 тестов — Task 3, 4.
- ✅ Lefthook job — Task 11.
- ✅ `docs/registry/README.md` — Task 1 заглушка + Task 12 полный.
- ✅ Diff = 0 совместимость render'а с originalом — Task 10.
- ✅ Continuity механизм (STATUS.md + memory) — Task 13 step 3-4.
- ✅ Pre-flight Pravila §15.2 — PF-1 + Definition of done.

**2. Placeholder scan:**

- ⚠️ Task 8 «Прочитать §4.1..§4.10 целиком» — это валидный шаг, не placeholder (содержит конкретный grep команды). Реальное содержание узлов заполняется при исполнении из источника. ОК.
- ⚠️ Task 13 Step 3 — «Найти `tools/status-md-generator.mjs`, добавить функцию» — это нормальная инструкция в существующий код (генератор уже есть согласно §3.6 CLAUDE.md). Конкретный код функции в плане есть. ОК.
- ✅ Нет TBD/TODO/«implement later».

**3. Type consistency:**

- ✅ `loadRegistry()` возвращает один и тот же shape во всех тасках.
- ✅ `findByClassification` сигнатура в Task 3 и Task 4 совпадает.
- ✅ Маркеры `<!-- auto:foo:begin/end -->` имеют единый формат во всех тасках.
- ✅ Версии `nodes.yaml` начинаются с 0.1.0 (пилот) и явно не меняются в плане (рост содержимого, не схемы).

Готово.

---

## Execution Handoff

Plan complete and saved to `docs/superpowers/plans/2026-05-23-router-overhaul-stage-1-registry.md`. Two execution options:

**1. Subagent-Driven (recommended)** — я диспатчу fresh субагент per task, ревью между задачами, быстрая итерация. Для Task 8 (перенос 83 узлов) — может быть несколько суб-задач параллельно (по 15-20 узлов каждая).

**2. Inline Execution** — выполняю в этой сессии через executing-plans, batch с checkpoints для ревью.

**Какой подход выбираешь?**
