test(m3-c): coverage-machine invariants on 3-A contracts + plan
Машина 3-C «Машина охвата A/B/C/D» собрана (TDD): coverage-machine.mjs — A граф зависимостей (buildDependencyGraph/topoOrder/findHoles/decompositionGroups), B реестр нужды↔решения (coverageRegistry: дыры+сироты), C requestsChecklist, D ограничения как нужды (effectiveNeeds), хребет readinessChecklist (4 галочки + §). Независимый верификатор охвата (рычаг E §6.3). 19 новых тестов, регрессия 2158 GREEN.
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
# Машина 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** (селектор/рассуждение) — машина охвата только ВЕРИФИЦИРУЕТ. ✅
|
||||
@@ -0,0 +1,26 @@
|
||||
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: '' }]);
|
||||
const r = readinessChecklist({ contracts, requests: ['implementation-plan'] });
|
||||
expect(Array.isArray(r.items)).toBe(true);
|
||||
expect(r.items).toHaveLength(4);
|
||||
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]);
|
||||
expect(holes.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user