145 lines
4.4 KiB
JavaScript
145 lines
4.4 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Phase 7E: Diagram Generator
|
|
* Auto-generates Mermaid diagrams from graph analysis outputs.
|
|
*/
|
|
|
|
/**
|
|
* Generate a subsystem dependency diagram from the dependency matrix.
|
|
* @param {object} subsystemMap - Result from buildSubsystems
|
|
* @returns {string} Mermaid graph TD source
|
|
*/
|
|
function generateDependencyDiagram(subsystemMap) {
|
|
const lines = ['graph TD'];
|
|
const { subsystems, crossCutting, dependencyMatrix } = subsystemMap;
|
|
|
|
// Define nodes
|
|
for (const sub of subsystems) {
|
|
const label = sub.name.charAt(0).toUpperCase() + sub.name.slice(1);
|
|
if (sub.kind === 'cross-cutting') {
|
|
lines.push(` ${sub.name}["${label} (shared)"]:::shared`);
|
|
} else {
|
|
lines.push(` ${sub.name}["${label}"]`);
|
|
}
|
|
}
|
|
|
|
// Define edges (skip cross-cutting → cross-cutting to reduce noise)
|
|
for (const [key, val] of Object.entries(dependencyMatrix)) {
|
|
const [from, to] = key.split('→');
|
|
const weight = val.calls + val.imports;
|
|
if (weight === 0) continue;
|
|
|
|
// Skip edges between two cross-cutting subsystems
|
|
if (crossCutting.includes(from) && crossCutting.includes(to)) continue;
|
|
|
|
const label = weight > 5 ? `|${weight}|` : '';
|
|
lines.push(` ${from} -->${label} ${to}`);
|
|
}
|
|
|
|
// Style
|
|
lines.push(' classDef shared fill:#f9f,stroke:#333,stroke-dasharray: 5 5');
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Generate a sequence diagram from a flow trace.
|
|
* @param {object} flowResult - Result from traceFlow
|
|
* @returns {string} Mermaid sequenceDiagram source
|
|
*/
|
|
function generateFlowDiagram(flowResult) {
|
|
const lines = ['sequenceDiagram'];
|
|
|
|
// Participants in order
|
|
for (const sub of flowResult.subsystemSequence) {
|
|
const label = sub.charAt(0).toUpperCase() + sub.slice(1);
|
|
lines.push(` participant ${sub} as ${label}`);
|
|
}
|
|
|
|
// Messages from flow steps
|
|
let prevSub = null;
|
|
for (const step of flowResult.flow) {
|
|
if (step.crossedVia && prevSub && prevSub !== step.subsystem) {
|
|
const funcName = step.entity.includes(':') ? step.entity.split(':')[1] : step.entity.split('/').pop();
|
|
lines.push(` ${prevSub}->>${step.subsystem}: ${funcName}()`);
|
|
}
|
|
prevSub = step.subsystem;
|
|
}
|
|
|
|
// Note cycles
|
|
for (const cycle of flowResult.cyclesDetected) {
|
|
const sub = null; // We'd need subsystem lookup, just note it
|
|
const atFunc = cycle.at.includes(':') ? cycle.at.split(':')[1] : cycle.at;
|
|
const backFunc = cycle.backEdgeTo.includes(':') ? cycle.backEdgeTo.split(':')[1] : cycle.backEdgeTo;
|
|
lines.push(` Note right of ${flowResult.subsystemSequence[flowResult.subsystemSequence.length - 1]}: Cycle: ${atFunc} ↔ ${backFunc}`);
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Generate a contract/class diagram from extracted contracts.
|
|
* @param {Array} contracts - Array of contract objects with fields
|
|
* @returns {string} Mermaid classDiagram source
|
|
*/
|
|
function generateContractDiagram(contracts) {
|
|
const lines = ['classDiagram'];
|
|
|
|
for (const c of contracts) {
|
|
if (c.type === 'Interface' || c.type === 'TypeAlias') {
|
|
lines.push(` class ${c.name} {`);
|
|
if (c.fields) {
|
|
for (const f of c.fields) {
|
|
lines.push(` +${f.name}: ${f.type}`);
|
|
}
|
|
}
|
|
lines.push(' }');
|
|
lines.push(` <<${c.type}>> ${c.name}`);
|
|
|
|
if (c.extends) {
|
|
for (const ext of c.extends) {
|
|
lines.push(` ${ext} <|-- ${c.name}`);
|
|
}
|
|
}
|
|
} else if (c.type === 'Enum') {
|
|
lines.push(` class ${c.name} {`);
|
|
lines.push(' <<enumeration>>');
|
|
if (c.members) {
|
|
for (const m of c.members) {
|
|
lines.push(` ${m}`);
|
|
}
|
|
}
|
|
lines.push(' }');
|
|
}
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const cmd = process.argv[2];
|
|
const inputPath = process.argv[3];
|
|
|
|
if (!cmd || !inputPath) {
|
|
console.error('Usage: node diagrams.js <deps|flow|contracts> <input.json>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const data = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
|
|
|
if (cmd === 'deps') {
|
|
console.log(generateDependencyDiagram(data));
|
|
} else if (cmd === 'flow') {
|
|
console.log(generateFlowDiagram(data));
|
|
} else if (cmd === 'contracts') {
|
|
console.log(generateContractDiagram(data.contracts || data));
|
|
} else {
|
|
console.error('Unknown command:', cmd);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
module.exports = { generateDependencyDiagram, generateFlowDiagram, generateContractDiagram };
|