397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
157 lines
8.5 KiB
JavaScript
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 };
|
|
}
|