397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
3.6 KiB
JavaScript
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;
|
|
}
|