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