#!/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 }; }