Files
brain/tools/decision-graph.mjs

73 lines
3.6 KiB
JavaScript

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