Files
brain/tools/coverage-machine.mjs

157 lines
8.5 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(); }
/**
* Покрытие токенов (F-1, аудит M1-M4): точное равенство ИЛИ множество слов одного
* ⊆ множеству слов другого (границы слов = не-буквы/цифры). Закрывает дыру «мягкого
* края C», где двусторонняя подстрока считала produces «a» покрывающим запрос
* «audit-rls-policy» (ложное «всё покрыто»). Подстрока без границы слова больше не
* покрывает; «csv» ⊆ «csv-export-deals» (по словам) — да.
*/
export function tokensCover(a, b) {
const na = normToken(a), nb = normToken(b);
if (!na || !nb) return false;
if (na === nb) return true;
const words = (s) => new Set(s.split(/[^\p{L}\p{N}]+/u).filter(Boolean));
const wa = words(na), wb = words(nb);
if (wa.size === 0 || wb.size === 0) return false;
const subset = (x, y) => [...x].every((t) => y.has(t));
return subset(wa, wb) || subset(wb, wa);
}
/** D — эффективные нужды контракта = needs + constraints + inherent (#1 присущее по природе). */
export function effectiveNeeds(contract) {
const needs = (contract.needs || []).map((n) => ({ token: n, kind: 'need' }));
const cons = (contract.constraints || []).map((cN) => ({ token: cN, kind: 'constraint' }));
const inh = (contract.inherent || []).map((it) => ({ token: it.need, kind: 'inherent' }));
return [...needs, ...cons, ...inh];
}
/** Множество всего, что производят контракты (нормализованные 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);
if (!r) return null; // F3: пустой/пробельный запрос не «покрывается» (иначе ''.includes('') = ложное всё-покрыто)
for (const c of contracts) for (const p of c.produces || []) {
const pp = normToken(p);
if (!pp) continue;
if (tokensCover(pp, r)) return c.skill; // F-1: по словам, не по подстроке
}
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).filter(Boolean); // F3: пустые токены не покрывают
const orphans = [];
for (const c of contracts) {
const produces = (c.produces || []).map(normToken);
const neededBySomeone = produces.some((p) => p && allNeeds.has(p));
const coversRequest = produces.some((p) => p && reqTokens.some((r) => tokensCover(p, r))); // F-1: по словам, не по подстроке
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 };
}