# Машина 3 / под-план 3-C — Машина охвата A/B/C/D — план реализации

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development / executing-plans.
> **NB сборки:** код строит контроллер по TDD (субагент не проходит TDD-гейт).

**Goal:** на контрактах 3-A построить НЕЗАВИСИМЫЙ механический верификатор полноты плана (C-14): A граф зависимостей (needs↔produces: топосорт-порядок, дыра=нужда-без-producer, цикл=флаг, декомпозиция=связные группы); B реестр нужды↔решения (дыры / сироты-скоупкрип); C чек-лист «просьбы цели → план»; D ограничения = полноправные нужды; хребет — буквальный чек-лист готовности (галочки + указатели §).

**Architecture:** чистый `tools/coverage-machine.mjs` поверх контрактов 3-A (`skill-contract` needs/produces/constraints). Только set/graph-операции, без LLM («умный не баран» — машина считает, роутер рассуждает). Сопоставление need↔produce — по нормализованной строке (lower/trim); семантическое сопоставление — НЕ здесь (это рассуждение роутера 3-D). Это рычаг E дисциплины роутера (§6.3): независимый верификатор охвата.

**Tech Stack:** Node.js ESM, vitest tools-only.

---

## ⚠️ Контекст исполнения (как 3-A/3-B)
- Git только `git -C "<worktree>"`. Тесты: `npx vitest run --root ".claude/worktrees/brainrepo/app" --config vitest.config.tools.mjs <фильтр>` через Bash, `dangerouslyDisableSandbox=true`, без `cd`.
- Тест-файлы — в `<worktree>\tools\`, ЦЕЛИКОМ через Write. TDD-гейт: Read плана прямыми слэшами + тест + Bash-RED, потом прод.

## Границы 3-C (что НЕ здесь)
- Выбор скилов рассуждением, сборка цепочки, look-ahead, L-ядро → **3-D** (машина охвата — ВЕРИФИКАТОР готового набора, не селектор).
- **[ДОПУЩЕНИЕ]** Сопоставление need↔produce — по нормализованной строке (lower/trim; равенство или подстрока для просьб). Семантическое сопоставление («spec» ≈ «требования») — рассуждение роутера (3-D), не механика. Машина охвата честно ловит точные дыры/сироты/циклы; нечёткие — задача роутера.
- **[ДОПУЩЕНИЕ]** Извлечение «просьб цели» из текста (C) — мягкий край, выполняется выше (роутер/владелец); 3-C принимает готовый массив просьб и механически сверяет покрытие.

---

## Структура файлов
**Создаём:**
- `tools/coverage-machine.mjs` — `normToken`, `effectiveNeeds` (D), `buildDependencyGraph`/`topoOrder`/`findHoles`/`decompositionGroups` (A), `coverageRegistry` (B), `requestsChecklist` (C), `readinessChecklist` (хребет).
- Тесты: `tools/coverage-machine.test.mjs` (чистый) + `tools/m3c-coverage-invariants.test.mjs` (на контрактах 3-A через skill-contract-registry).

**Порядок:** A граф+топосорт+дыры+группы (Task 1) → D effectiveNeeds + B реестр + C чек-лист просьб (Task 2) → хребет readinessChecklist + инварианты + регрессия (Task 3).

---

## Task 1: A — граф зависимостей (needs↔produces): topoOrder / findHoles / decompositionGroups

**Files:** Create `tools/coverage-machine.mjs`, `tools/coverage-machine.test.mjs`

- [ ] **Step 1: Падающий тест** — `tools/coverage-machine.test.mjs`

```js
import { describe, it, expect } from 'vitest';
import { normToken, buildDependencyGraph, topoOrder, findHoles, decompositionGroups } from './coverage-machine.mjs';

const c = (skill, needs, produces, constraints = []) => ({ skill, needs, produces, constraints });
// Y produces 'spec'; X needs 'spec' → Y перед X
const CHAIN = [c('X', ['spec'], ['code']), c('Y', [], ['spec'])];

describe('normToken', () => {
  it('lower + trim', () => { expect(normToken('  Spec ')).toBe('spec'); });
});

describe('buildDependencyGraph (A: needs↔produces)', () => {
  it('ребро producer→consumer via need', () => {
    const g = buildDependencyGraph(CHAIN);
    expect(g.edges).toContainEqual({ from: 'Y', to: 'X', via: 'spec' });
  });
});

describe('topoOrder (A: порядок = топосортировка, цикл = флаг)', () => {
  it('порядок: Y перед X', () => {
    const r = topoOrder(CHAIN);
    expect(r.cycle).toBe(null);
    expect(r.order.indexOf('Y')).toBeLessThan(r.order.indexOf('X'));
  });
  it('цикл помечается', () => {
    const cyc = [c('A', ['b'], ['a']), c('B', ['a'], ['b'])];
    const r = topoOrder(cyc);
    expect(r.order).toBe(null);
    expect(r.cycle.sort()).toEqual(['A', 'B']);
  });
});

describe('findHoles (A: нужда без producer; D: ограничения тоже)', () => {
  it('нужда, которую никто не производит → дыра', () => {
    const h = findHoles([c('X', ['spec'], ['code'])]);
    expect(h).toContainEqual({ need: 'spec', neededBy: 'X', kind: 'need' });
  });
  it('initialInputs закрывают нужду (не дыра)', () => {
    expect(findHoles([c('X', ['spec'], ['code'])], { initialInputs: ['spec'] })).toEqual([]);
  });
  it('ограничение без покрытия → дыра kind=constraint (D)', () => {
    const h = findHoles([c('X', [], ['code'], ['must be RLS-safe'])]);
    expect(h).toContainEqual({ need: 'must be RLS-safe', neededBy: 'X', kind: 'constraint' });
  });
  it('produced нужда не дыра', () => {
    expect(findHoles(CHAIN)).toEqual([]);
  });
});

describe('decompositionGroups (A: связные группы)', () => {
  it('связанные скилы — одна группа, несвязанный — отдельная', () => {
    const groups = decompositionGroups([...CHAIN, c('Z', [], ['unrelated'])]);
    const sizes = groups.map((g) => g.length).sort();
    expect(sizes).toEqual([1, 2]);
  });
});
```

- [ ] **Step 2: RED.**

- [ ] **Step 3: Реализация** — `tools/coverage-machine.mjs`

```js
#!/usr/bin/env node
/**
 * coverage-machine — машина охвата A/B/C/D (C-14) поверх контрактов 3-A.
 * НЕЗАВИСИМЫЙ механический верификатор полноты плана: set/graph-операции,
 * без LLM («умный не баран»). Рычаг E дисциплины роутера (§6.3).
 */
export function normToken(s) { return String(s ?? '').trim().toLowerCase(); }

/** D — эффективные нужды контракта = needs + constraints (ограничения как полноправные нужды). */
export function effectiveNeeds(contract) {
  const needs = (contract.needs || []).map((n) => ({ token: n, kind: 'need' }));
  const cons = (contract.constraints || []).map((cN) => ({ token: cN, kind: 'constraint' }));
  return [...needs, ...cons];
}

/** Множество всего, что производят контракты (нормализованные produces). */
function producedSet(contracts) {
  const s = new Set();
  for (const c of contracts) for (const p of c.produces || []) s.add(normToken(p));
  return s;
}

/** A — граф зависимостей: ребро producer→consumer via need (по needs↔produces). */
export function buildDependencyGraph(contracts) {
  const byProduce = new Map(); // normProduce → [skill]
  for (const c of contracts) for (const p of c.produces || []) {
    const k = normToken(p);
    if (!byProduce.has(k)) byProduce.set(k, []);
    byProduce.get(k).push(c.skill);
  }
  const edges = [];
  for (const c of contracts) for (const n of c.needs || []) {
    const producers = byProduce.get(normToken(n)) || [];
    for (const p of producers) if (p !== c.skill) edges.push({ from: p, to: c.skill, via: normToken(n) });
  }
  return { nodes: contracts.map((c) => c.skill), edges };
}

/** A — топосортировка (Kahn). Цикл → {order:null, cycle:[оставшиеся скилы]}. */
export function topoOrder(contracts) {
  const { nodes, edges } = buildDependencyGraph(contracts);
  const indeg = new Map(nodes.map((n) => [n, 0]));
  const adj = new Map(nodes.map((n) => [n, []]));
  for (const e of edges) { indeg.set(e.to, (indeg.get(e.to) || 0) + 1); adj.get(e.from).push(e.to); }
  const queue = nodes.filter((n) => (indeg.get(n) || 0) === 0);
  const order = [];
  while (queue.length) {
    const n = queue.shift(); order.push(n);
    for (const m of adj.get(n) || []) { indeg.set(m, indeg.get(m) - 1); if (indeg.get(m) === 0) queue.push(m); }
  }
  if (order.length !== nodes.length) {
    const cycle = nodes.filter((n) => !order.includes(n));
    return { order: null, cycle };
  }
  return { order, cycle: null };
}

/** A+D — дыры: нужда/ограничение, которую никто не производит и нет в initialInputs. */
export function findHoles(contracts, { initialInputs = [], includeConstraints = true } = {}) {
  const produced = producedSet(contracts);
  const inputs = new Set(initialInputs.map(normToken));
  const holes = [];
  for (const c of contracts) {
    const items = includeConstraints ? effectiveNeeds(c) : (c.needs || []).map((n) => ({ token: n, kind: 'need' }));
    for (const { token, kind } of items) {
      const k = normToken(token);
      if (!produced.has(k) && !inputs.has(k)) holes.push({ need: token, neededBy: c.skill, kind });
    }
  }
  return holes;
}

/** A — связные группы (неориентированные компоненты по рёбрам needs↔produces). */
export function decompositionGroups(contracts) {
  const { nodes, edges } = buildDependencyGraph(contracts);
  const parent = new Map(nodes.map((n) => [n, n]));
  const find = (x) => { while (parent.get(x) !== x) { parent.set(x, parent.get(parent.get(x))); x = parent.get(x); } return x; };
  const union = (a, b) => { parent.set(find(a), find(b)); };
  for (const e of edges) union(e.from, e.to);
  const groups = new Map();
  for (const n of nodes) { const r = find(n); if (!groups.has(r)) groups.set(r, []); groups.get(r).push(n); }
  return [...groups.values()];
}
```

- [ ] **Step 4: GREEN.**
- [ ] **Step 5: Commit** — `git -C "<worktree>" add tools/coverage-machine.mjs tools/coverage-machine.test.mjs` + commit `"feat(m3-c): coverage A — dep graph / topoOrder / holes(+D) / groups (C-14)"`

---

## Task 2: B реестр нужды↔решения (сироты) + C чек-лист просьбы→план

**Files:** Modify `tools/coverage-machine.mjs`, `tools/coverage-machine.test.mjs`

- [ ] **Step 1: Падающие тесты (добавить)**

```js
import { coverageRegistry, requestsChecklist } from './coverage-machine.mjs';

describe('coverageRegistry (B: нужды↔решения, дыры + сироты)', () => {
  it('дыра попадает в holes', () => {
    const r = coverageRegistry([c('X', ['spec'], ['code'])]);
    expect(r.holes.map((h) => h.need)).toContain('spec');
  });
  it('сирота-скоупкрип: produces никому не нужен и не покрывает просьбу', () => {
    // Y produces 'spec' (нужен X). Z produces 'extra' — никому не нужен, просьбы нет → сирота
    const r = coverageRegistry([c('X', ['spec'], ['code']), c('Y', [], ['spec']), c('Z', [], ['extra'])], { requests: ['code'] });
    expect(r.orphans.map((o) => o.skill)).toContain('Z');
    expect(r.orphans.map((o) => o.skill)).not.toContain('Y'); // Y нужен X
  });
});

describe('requestsChecklist (C: просьбы цели → план)', () => {
  it('просьба, которую кто-то производит → ok', () => {
    const list = requestsChecklist(['code'], [c('X', [], ['code'])]);
    expect(list).toEqual([{ request: 'code', coveredBy: 'X', ok: true }]);
  });
  it('непокрытая просьба → ok=false, coveredBy=null', () => {
    const list = requestsChecklist(['report'], [c('X', [], ['code'])]);
    expect(list[0]).toMatchObject({ request: 'report', coveredBy: null, ok: false });
  });
  it('покрытие по подстроке (мягкий край)', () => {
    const list = requestsChecklist(['csv'], [c('X', [], ['export to csv file'])]);
    expect(list[0].ok).toBe(true);
  });
});
```

- [ ] **Step 2: RED.**

- [ ] **Step 3: Реализация (дописать)**

```js
/** Найти контракт, чей produces покрывает запрос (равенство нормализованных ИЛИ подстрока — мягкий край C). */
function coveringSkill(contracts, request) {
  const r = normToken(request);
  for (const c of contracts) for (const p of c.produces || []) {
    const pp = normToken(p);
    if (pp === r || pp.includes(r) || r.includes(pp)) return c.skill;
  }
  return null;
}

/** C — чек-лист «просьбы цели → план»: каждая просьба сверяется с produces плана. */
export function requestsChecklist(requests, contracts) {
  return (requests || []).map((req) => {
    const coveredBy = coveringSkill(contracts, req);
    return { request: req, coveredBy, ok: coveredBy !== null };
  });
}

/** B — двусторонний реестр: дыры (findHoles) + сироты (produces никому не нужен и не покрывает просьбу). */
export function coverageRegistry(contracts, { requests = [], initialInputs = [] } = {}) {
  const holes = findHoles(contracts, { initialInputs });
  // нужды всех контрактов (для проверки «кому-то нужно»)
  const allNeeds = new Set();
  for (const c of contracts) for (const n of c.needs || []) allNeeds.add(normToken(n));
  const reqTokens = (requests || []).map(normToken);
  const orphans = [];
  for (const c of contracts) {
    const produces = (c.produces || []).map(normToken);
    const neededBySomeone = produces.some((p) => [...allNeeds].some((n) => n === p));
    const coversRequest = produces.some((p) => reqTokens.some((r) => r === p || p.includes(r) || r.includes(p)));
    if (produces.length > 0 && !neededBySomeone && !coversRequest)
      orphans.push({ skill: c.skill, reason: 'produces никому не нужен и не покрывает просьбу цели (scope creep?)' });
  }
  return { holes, orphans };
}
```

- [ ] **Step 4: GREEN.**
- [ ] **Step 5: Commit** — `git -C "<worktree>" commit -am "feat(m3-c): coverage B (registry/orphans) + C (requests checklist)"`

---

## Task 3: Хребет — readinessChecklist + инварианты на контрактах 3-A + регрессия

**Files:** Modify `tools/coverage-machine.mjs`, `tools/coverage-machine.test.mjs`; Create `tools/m3c-coverage-invariants.test.mjs`

- [ ] **Step 1: Падающие тесты (добавить в coverage-machine.test.mjs)**

```js
import { readinessChecklist } from './coverage-machine.mjs';

describe('readinessChecklist (хребет — галочки + указатели §)', () => {
  it('полный план → ready=true, все галочки', () => {
    const r = readinessChecklist({ contracts: [c('Y', [], ['spec']), c('X', ['spec'], ['code'])], requests: ['code'] });
    expect(r.ready).toBe(true);
    expect(r.items.every((i) => i.ok)).toBe(true);
    expect(r.items.every((i) => typeof i.pointer === 'string')).toBe(true);
  });
  it('дыра → ready=false + пункт про дыры провален', () => {
    const r = readinessChecklist({ contracts: [c('X', ['spec'], ['code'])], requests: ['code'] });
    expect(r.ready).toBe(false);
    expect(r.items.find((i) => /дыр|hole/i.test(i.label)).ok).toBe(false);
  });
  it('цикл → пункт про циклы провален', () => {
    const r = readinessChecklist({ contracts: [c('A', ['b'], ['a']), c('B', ['a'], ['b'])], requests: [] });
    expect(r.items.find((i) => /цикл|cycle/i.test(i.label)).ok).toBe(false);
  });
});
```

- [ ] **Step 2: RED.**

- [ ] **Step 3: Реализация (дописать)**

```js
/**
 * Хребет машины охвата — буквальный чек-лист готовности плана (галочки + указатели §).
 * Объединяет A (дыры/циклы), B (сироты), C (просьбы). ready = все пункты ok.
 */
export function readinessChecklist({ contracts = [], requests = [], initialInputs = [] }) {
  const { holes, orphans } = coverageRegistry(contracts, { requests, initialInputs });
  const topo = topoOrder(contracts);
  const reqList = requestsChecklist(requests, contracts);
  const items = [
    { label: 'Все нужды/ограничения покрыты (нет дыр)', ok: holes.length === 0, pointer: '§A findHoles', detail: holes },
    { label: 'Нет циклов зависимостей', ok: topo.cycle === null, pointer: '§A topoOrder', detail: topo.cycle },
    { label: 'Нет сирот-скоупкрипа', ok: orphans.length === 0, pointer: '§B coverageRegistry', detail: orphans },
    { label: 'Все просьбы цели покрыты', ok: reqList.every((r) => r.ok), pointer: '§C requestsChecklist', detail: reqList.filter((r) => !r.ok) },
  ];
  return { ready: items.every((i) => i.ok), items };
}
```

- [ ] **Step 4: GREEN** (coverage-machine).

- [ ] **Step 5: Инвариант на контрактах 3-A** — `tools/m3c-coverage-invariants.test.mjs`

```js
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { buildRegistry } from './skill-contract-registry.mjs';
import { readinessChecklist, findHoles } from './coverage-machine.mjs';

const here = dirname(fileURLToPath(import.meta.url));
const cdir = join(here, '..', 'docs', 'registry', 'contracts');

describe('Машина 3-C — охват на реальных контрактах 3-A', () => {
  it('контракты 3-A загружаются и прогоняются через машину охвата', () => {
    const wp = JSON.parse(readFileSync(join(cdir, 'writing-plans.contract.json'), 'utf8'));
    const pd = JSON.parse(readFileSync(join(cdir, 'operations-process-doc.contract.json'), 'utf8'));
    const { contracts } = buildRegistry([{ contract: wp }, { contract: pd, currentContent: '' }]);
    // машина охвата принимает контракты 3-A без ошибок и даёт детерминированный чек-лист
    const r = readinessChecklist({ contracts, requests: ['implementation-plan'] });
    expect(Array.isArray(r.items)).toBe(true);
    expect(r.items).toHaveLength(4);
    // writing-plans produces 'implementation-plan...' → просьба покрыта по подстроке
    expect(r.items.find((i) => /просьб/i.test(i.label)).ok).toBe(true);
  });
  it('findHoles на контракте с непокрытой нуждой ловит дыру', () => {
    const wp = JSON.parse(readFileSync(join(cdir, 'writing-plans.contract.json'), 'utf8'));
    const holes = findHoles([wp]);
    // writing-plans needs 'spec or requirements' — никто в одиночном наборе не производит → дыра
    expect(holes.length).toBeGreaterThan(0);
  });
});
```

- [ ] **Step 6: GREEN** (m3c invariants).
- [ ] **Step 7: Полная регрессия** — `npx vitest run --root ... --config ...` без фильтра → всё зелёное.
- [ ] **Step 8: Commit** — `git -C "<worktree>" add tools/coverage-machine.mjs tools/coverage-machine.test.mjs tools/m3c-coverage-invariants.test.mjs docs/superpowers/plans/2026-06-04-router-mentor-3c-coverage-machine.md` + commit `"feat(m3-c): readinessChecklist backbone + invariants on 3-A contracts + plan"`

---

## Self-Review (против канона §2 3-C + C-14)
- **A граф зависимостей (needs↔produces; декомпозиция=группы, порядок=топосорт, дыра=нужда-без-producer, цикл=флаг)** → buildDependencyGraph/topoOrder/findHoles/decompositionGroups. ✅
- **B двусторонний реестр нужды↔решения (дыра / сирота-скоупкрип)** → coverageRegistry (holes + orphans). ✅
- **C чек-лист «просьбы цели → план» (мягкий край на извлечении просьб)** → requestsChecklist (просьбы принимаются готовым массивом — извлечение выше; сопоставление мех. по подстроке). ✅
- **D ограничения как полноправные нужды** → effectiveNeeds + findHoles includeConstraints (дыра kind='constraint'). ✅
- **Хребет — буквальный чек-лист готовности (галочки + указатели §)** → readinessChecklist (4 пункта, pointer на §). ✅
- **На контрактах 3-A, без LLM, рычаг E верификатора (§6.3)** → инварианты на реальных контрактах; чистые set/graph-операции. ✅
- **Граница с 3-D** (селектор/рассуждение) — машина охвата только ВЕРИФИЦИРУЕТ. ✅
