Files
brain/tools/graph-radar.mjs
T

45 lines
2.7 KiB
JavaScript
Raw Normal View History

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