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