Files
brain/tools/card-coverage.mjs
T

49 lines
2.2 KiB
JavaScript

#!/usr/bin/env node
/**
* card-coverage — мост покрытия «узлы реестра ↔ карточки-контракты» (H, R-11/R-12).
* Чистые функции (данные инъектируются): пропущенные контракты, пустые карточки,
* нерезолвящиеся и асимметричные конфликт-рёбра. Без LLM, без диска.
*/
import { resolveNode } from './node-graph.mjs';
/** Узлы, у которых нет карточки с skill === node.slug. Возвращает id|slug. */
export function missingContracts(nodes, contracts) {
const skills = new Set((contracts || []).map((c) => c && c.skill).filter(Boolean));
return (nodes || [])
.filter((n) => n && n.slug && !skills.has(n.slug))
.map((n) => n.id || n.slug);
}
/** Пустые карточки: ни needs, ни produces (бесполезна всем потребителям). Возвращает skill. */
export function emptyCards(contracts) {
return (contracts || [])
.filter((c) => (c.needs || []).length === 0 && (c.produces || []).length === 0)
.map((c) => c.skill);
}
/** Конфликт-ссылки, не резолвящиеся в узел графа. {from, ref}. */
export function unresolvedConflicts(graph) {
const out = [];
for (const n of (graph && graph.nodes) || []) {
const refs = (n.attributes && n.attributes.conflicts_with) || [];
for (const r of refs) if (!resolveNode(graph, r)) out.push({ from: n.id || n.slug, ref: r });
}
return out;
}
/** Односторонние рёбра: A→B, но B не ссылается обратно на A. {from, to}. */
export function asymmetricConflicts(graph) {
const out = [];
for (const n of (graph && graph.nodes) || []) {
const refs = (n.attributes && n.attributes.conflicts_with) || [];
for (const r of refs) {
const t = resolveNode(graph, r);
if (!t) continue; // unresolved — отдельной функцией
const back = (t.attributes && t.attributes.conflicts_with) || [];
const resolvesBack = back.some((b) => resolveNode(graph, b) === n);
if (!resolvesBack) out.push({ from: n.id || n.slug, to: t.id || t.slug });
}
}
return out;
}