#!/usr/bin/env node /** * graph-radar (§5.7 ДР-3) — файл-уровень проверки полноты: тронул X → граф связывает * с Y → адресован ли Y? Чистое ядро (граф инъектируется, формат project-graph B). * Сосед, который сам в touchedFiles, не флагуется (уже адресован). */ /** F-F2: нормализация пути для сравнения — backslash → '/', strip './'-префикс. */ function normPath(p) { if (typeof p !== 'string') return ''; let s = p.replace(/\\/g, '/'); while (s.startsWith('./')) s = s.slice(2); return s; } export function buildGraphRadar({ touchedFiles, graph } = {}) { const touched = (Array.isArray(touchedFiles) ? touchedFiles : []).map(normPath).filter(Boolean); const nodes = graph && Array.isArray(graph.nodes) ? graph.nodes : []; const links = graph && Array.isArray(graph.links) ? graph.links : []; const touchedSet = new Set(touched); // id → узел (для source_file соседа) const byId = new Map(nodes.map((n) => [n && n.id, n]).filter(([id]) => id)); // id тронутых узлов (сравнение по нормализованному пути — F-F2) const touchedIds = new Set(nodes.filter((n) => n && touchedSet.has(normPath(n.source_file))).map((n) => n.id)); const seen = new Set(); const neighbors = []; for (const l of links) { if (!l || typeof l !== 'object') continue; for (const [a, b] of [[l.source, l.target], [l.target, l.source]]) { if (!touchedIds.has(a)) continue; // a — тронутый конец const nb = byId.get(b); if (!nb) continue; if (touchedSet.has(normPath(nb.source_file))) continue; // сосед сам тронут → адресован const key = `${b}<-${a}`; if (seen.has(key)) continue; seen.add(key); // F-F4 (L2-пол): элемент заморожен — машинная мутация addressed бросает TypeError; // отметка «адресовано» живёт ВНЕ структуры (наставник/владелец). neighbors.push(Object.freeze({ node: b, label: nb.label ?? null, source_file: nb.source_file ?? null, viaTouched: a, addressed: false })); } } // F-F1: touchedFiles (подано) рядом с touchedNodes (найдено) — слепота радара видна: // touchedFiles>0 ∧ touchedNodes=0 ⇒ «радар слеп» (сигнал-всегда, зеркало F-C6/F-C2-3). return { neighbors, summary: { touchedFiles: touched.length, touchedNodes: touchedIds.size, unaddressed: neighbors.length } }; }