#!/usr/bin/env node
/**
 * decision-graph — НОВЫЙ граф зависимостей РЕШЕНИЙ/секций артефакта (дизайн
 * 2026-06-05 §4 #2). Отличен от графа НАВЫКОВ (coverage-machine): тут узлы =
 * секции артефакта, рёбра = «секция зависит от §X». Сменил решение в §X →
 * механически подсвечиваются зависимые секции на перепроверку («тупой наводчик
 * внимания», НЕ мыслитель). Ловит только ОБЪЯВЛЕННЫЕ зависимости. Чистый ($0).
 */

function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; }

/** Граф: узлы = id секций, рёбра {from, to} = «from зависит от to» (dependsOn).
 *  NB: НЕ валидирует — может построить рёбра на несуществующие узлы. Для
 *  безопасного входа используй buildValidatedGraph (validate→build). */
export function buildDecisionGraph(sections) {
  const list = Array.isArray(sections) ? sections : [];
  const nodes = list.map((s) => s.id);
  const edges = [];
  for (const s of list) for (const dep of (s.dependsOn || [])) edges.push({ from: s.id, to: dep });
  return { nodes, edges };
}

/** Валидатор: id непустые+уникальные; каждый dependsOn резолвится в существующую секцию. */
export function validateSections(sections) {
  const errors = [];
  const list = Array.isArray(sections) ? sections : [];
  const ids = new Set();
  list.forEach((s, i) => {
    if (!s || typeof s !== 'object' || !isNonEmptyString(s.id)) { errors.push(`section[${i}].id: непустая строка`); return; }
    if (ids.has(s.id)) errors.push(`section[${i}].id дублируется: ${s.id}`);
    ids.add(s.id);
  });
  for (const s of list) for (const dep of (s?.dependsOn || [])) {
    if (!ids.has(dep)) errors.push(`секция ${s.id}: зависимость от несуществующей §${dep}`);
  }
  return { ok: errors.length === 0, errors };
}

/**
 * Безопасный вход: сперва validateSections, при невалидном — бросает (не строит
 * молча рёбра на несуществующие узлы — аудит-footgun). Иначе — buildDecisionGraph.
 */
export function buildValidatedGraph(sections) {
  const v = validateSections(sections);
  if (!v.ok) throw new Error(`decision-graph невалиден: ${v.errors.join('; ')}`);
  return buildDecisionGraph(sections);
}

/** Прямые зависимые от sectionId — секции, чей dependsOn включает sectionId. */
export function dependentsOf(sections, sectionId) {
  return (Array.isArray(sections) ? sections : [])
    .filter((s) => (s.dependsOn || []).includes(sectionId))
    .map((s) => s.id);
}

/**
 * Транзитивное «подсвети на перепроверку»: сменил решение в changedId → ВСЕ
 * секции, что прямо или косвенно зависят от него (наведение внимания). Cycle-safe
 * (seen). Сам changedId не включается.
 */
export function impactedBy(sections, changedId) {
  const result = [];
  const seen = new Set([changedId]);
  const queue = [changedId];
  while (queue.length) {
    const cur = queue.shift();
    for (const dep of dependentsOf(sections, cur)) {
      if (!seen.has(dep)) { seen.add(dep); result.push(dep); queue.push(dep); }
    }
  }
  return result;
}
