Files
portal/tools/coverage-machine.mjs
T

136 lines
7.0 KiB
JavaScript

#!/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();
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()];
}
/** Найти контракт, чей 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.has(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 };
}
/**
* Хребет машины охвата — буквальный чек-лист готовности плана (галочки + указатели §).
* Объединяет 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 };
}