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