2026-03-09 14:42:15 +00:00
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const GraphStore = require('./graph.js');
|
|
|
|
|
const { buildSubsystems } = require('./subsystem.js');
|
2026-03-09 18:44:19 +00:00
|
|
|
const { extractAllContracts, buildContractXref } = require('./contracts.js');
|
2026-03-09 14:42:15 +00:00
|
|
|
const { buildFlowIndex, traceFlow } = require('./flow.js');
|
|
|
|
|
const { generateDependencyDiagram, generateFlowDiagram, generateContractDiagram } = require('./diagrams.js');
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Phase 7D: Hierarchical Doc Generator
|
|
|
|
|
* Orchestrates 7A, 7B, 7C, and 7E to generate a Divio-structured documentation site.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-09 18:44:19 +00:00
|
|
|
async function generateDocs(graph, srcRoot, outDir, opts = {}) {
|
2026-03-09 14:42:15 +00:00
|
|
|
const entryPoints = opts.entryPoints || [];
|
2026-03-09 18:44:19 +00:00
|
|
|
const useProse = opts.prose === true;
|
|
|
|
|
|
|
|
|
|
// Optional LLM module for prose enrichment
|
|
|
|
|
let proseMod = null;
|
|
|
|
|
if (useProse) {
|
|
|
|
|
try {
|
|
|
|
|
proseMod = require('./prose.js');
|
|
|
|
|
console.log('Prose generation enabled (LLM pass active)');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn('Prose generation requested but prose.js not available. Skipping LLM pass.');
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-09 14:42:15 +00:00
|
|
|
|
|
|
|
|
// 1. Build Subsystems (7A)
|
|
|
|
|
const subs = buildSubsystems(graph, {
|
|
|
|
|
srcDir: opts.srcDir || '/src/',
|
|
|
|
|
minTraffic: opts.minTraffic || 3,
|
|
|
|
|
crossCuttingThreshold: opts.crossCuttingThreshold || 0.6
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. Extract Contracts (7B)
|
|
|
|
|
const contractsResult = extractAllContracts(subs, srcRoot);
|
2026-03-09 18:44:19 +00:00
|
|
|
const xref = buildContractXref(contractsResult.contracts, graph, (p) => p.replace(/^\/?src\//, ''));
|
2026-03-09 14:42:15 +00:00
|
|
|
|
|
|
|
|
// 3. Trace Flows (7C)
|
|
|
|
|
const flowIndex = buildFlowIndex(graph, subs);
|
|
|
|
|
const flowResults = entryPoints.map(ep => traceFlow(ep, flowIndex));
|
|
|
|
|
|
|
|
|
|
// Initialize output directory structure (Divio)
|
|
|
|
|
const dirs = [
|
|
|
|
|
'reference/subsystems',
|
|
|
|
|
'reference/contracts',
|
|
|
|
|
'reference/modules',
|
|
|
|
|
'explanation',
|
|
|
|
|
'tutorials',
|
|
|
|
|
'how-to',
|
|
|
|
|
'diagrams'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const d of dirs) {
|
|
|
|
|
fs.mkdirSync(path.join(outDir, d), { recursive: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate Reference: System Architecture
|
|
|
|
|
const sysArchPath = path.join(outDir, 'reference/system-architecture.md');
|
|
|
|
|
const depDiag = generateDependencyDiagram(subs);
|
|
|
|
|
const depDiagPath = 'diagrams/system-deps.mmd';
|
|
|
|
|
fs.writeFileSync(path.join(outDir, depDiagPath), depDiag);
|
|
|
|
|
|
2026-03-09 18:44:19 +00:00
|
|
|
let archProse = '';
|
|
|
|
|
if (proseMod) {
|
|
|
|
|
console.log('Generating architecture overview...');
|
|
|
|
|
archProse = await proseMod.describeArchitecture(subs.subsystems, subs.crossCutting, {}, {});
|
|
|
|
|
archProse = `\n${archProse.trim()}\n\n`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 14:42:15 +00:00
|
|
|
const sysArchContent = `# System Architecture
|
2026-03-09 18:44:19 +00:00
|
|
|
${archProse}
|
2026-03-09 14:42:15 +00:00
|
|
|
## Subsystems
|
|
|
|
|
${subs.subsystems.map(s => `- **${s.name}** (${s.kind}): ${s.entities.modules} modules, ${s.entities.functions} functions`).join('\n')}
|
|
|
|
|
|
|
|
|
|
## Cross-Cutting Concerns
|
|
|
|
|
${subs.crossCutting.map(c => `- **${c}**`).join('\n')}
|
|
|
|
|
|
|
|
|
|
## Dependency Map
|
|
|
|
|
\`\`\`mermaid
|
|
|
|
|
${depDiag}
|
|
|
|
|
\`\`\`
|
|
|
|
|
`;
|
|
|
|
|
fs.writeFileSync(sysArchPath, sysArchContent);
|
|
|
|
|
|
|
|
|
|
// Generate Reference: Subsystem Docs
|
|
|
|
|
for (const sub of subs.subsystems) {
|
|
|
|
|
const subDocPath = path.join(outDir, `reference/subsystems/${sub.name}.md`);
|
|
|
|
|
const subContracts = contractsResult.bySubsystem[sub.name] || [];
|
|
|
|
|
|
|
|
|
|
let contractSection = '';
|
|
|
|
|
if (subContracts.length > 0) {
|
|
|
|
|
const contractDiag = generateContractDiagram(subContracts);
|
|
|
|
|
fs.writeFileSync(path.join(outDir, `diagrams/${sub.name}-contracts.mmd`), contractDiag);
|
|
|
|
|
contractSection = `\n## Contracts\n\`\`\`mermaid\n${contractDiag}\n\`\`\`\n`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 18:44:19 +00:00
|
|
|
let subProse = '';
|
|
|
|
|
if (proseMod) {
|
|
|
|
|
console.log(`Generating prose for subsystem: ${sub.name}...`);
|
|
|
|
|
subProse = await proseMod.describeSubsystem(sub, subs.dependencyMatrix, {});
|
|
|
|
|
subProse = `\n${subProse.trim()}\n\n`;
|
|
|
|
|
}
|
2026-03-09 14:42:15 +00:00
|
|
|
|
2026-03-09 18:44:19 +00:00
|
|
|
const subContent = `# Subsystem: ${sub.name}
|
|
|
|
|
${subProse}
|
2026-03-09 14:42:15 +00:00
|
|
|
**Kind:** ${sub.kind}
|
|
|
|
|
**Files:** ${sub.files.length}
|
|
|
|
|
|
|
|
|
|
## Public Exports
|
|
|
|
|
${sub.publicExports.length > 0 ? sub.publicExports.map(e => `- \`${e}\``).join('\n') : '*None*'}
|
|
|
|
|
${contractSection}
|
|
|
|
|
## Files
|
|
|
|
|
${sub.files.map(f => `- \`${f}\``).join('\n')}
|
|
|
|
|
`;
|
|
|
|
|
fs.writeFileSync(subDocPath, subContent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate Reference: Contracts
|
|
|
|
|
const contractDocPath = path.join(outDir, 'reference/contracts/index.md');
|
|
|
|
|
const allContractsDiag = generateContractDiagram(contractsResult.contracts);
|
|
|
|
|
fs.writeFileSync(path.join(outDir, 'diagrams/all-contracts.mmd'), allContractsDiag);
|
2026-03-09 18:44:19 +00:00
|
|
|
|
|
|
|
|
let contractProseList = '';
|
|
|
|
|
if (proseMod && contractsResult.contracts.length > 0) {
|
|
|
|
|
console.log(`Generating prose for ${contractsResult.contracts.length} contracts...`);
|
|
|
|
|
// Batch processing to avoid overloading the API
|
|
|
|
|
const batchSize = 10;
|
|
|
|
|
const contractDocs = [];
|
|
|
|
|
for (let i = 0; i < contractsResult.contracts.length; i += batchSize) {
|
|
|
|
|
const batch = contractsResult.contracts.slice(i, i + batchSize);
|
|
|
|
|
const docs = await Promise.all(batch.map(c => proseMod.describeContract(c, xref, {})));
|
|
|
|
|
contractDocs.push(...docs);
|
|
|
|
|
}
|
|
|
|
|
contractProseList = contractsResult.contracts.map((c, i) => `### ${c.name}\n${contractDocs[i].trim()}\n`).join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(contractDocPath, `# System Contracts\n\n\`\`\`mermaid\n${allContractsDiag}\n\`\`\`\n\n${contractProseList}`);
|
2026-03-09 14:42:15 +00:00
|
|
|
|
|
|
|
|
// Generate Explanation: Data Flows
|
|
|
|
|
const flowsPath = path.join(outDir, 'explanation/data-flows.md');
|
|
|
|
|
let flowsContent = '# Data Flows\n\n';
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < flowResults.length; i++) {
|
|
|
|
|
const fr = flowResults[i];
|
|
|
|
|
if (fr.error) {
|
|
|
|
|
flowsContent += `## Flow: ${fr.entryPoint}\n**Error:** ${fr.error}\n\n`;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const flowDiag = generateFlowDiagram(fr);
|
|
|
|
|
const diagName = `flow-${i}.mmd`;
|
|
|
|
|
fs.writeFileSync(path.join(outDir, `diagrams/${diagName}`), flowDiag);
|
|
|
|
|
|
2026-03-09 18:44:19 +00:00
|
|
|
let flowProse = '';
|
|
|
|
|
if (proseMod) {
|
|
|
|
|
console.log(`Generating prose for flow: ${fr.entryPoint}...`);
|
|
|
|
|
flowProse = await proseMod.describeFlow(fr, {});
|
|
|
|
|
flowProse = `${flowProse.trim()}\n\n`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 14:42:15 +00:00
|
|
|
flowsContent += `## Flow: ${fr.entryPoint}\n`;
|
2026-03-09 18:44:19 +00:00
|
|
|
flowsContent += flowProse;
|
2026-03-09 14:42:15 +00:00
|
|
|
flowsContent += `**Subsystem Sequence:** ${fr.subsystemSequence.join(' → ')}\n\n`;
|
|
|
|
|
flowsContent += `\`\`\`mermaid\n${flowDiag}\n\`\`\`\n\n`;
|
|
|
|
|
}
|
|
|
|
|
fs.writeFileSync(flowsPath, flowsContent);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
subsystems: subs.subsystems.length,
|
|
|
|
|
contracts: contractsResult.contracts.length,
|
|
|
|
|
flows: flowResults.length,
|
|
|
|
|
outDir
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (require.main === module) {
|
|
|
|
|
const snapshotPath = process.argv[2];
|
|
|
|
|
const srcRoot = process.argv[3];
|
|
|
|
|
const outDir = process.argv[4];
|
2026-03-09 18:44:19 +00:00
|
|
|
const useProse = process.argv.includes('--prose');
|
|
|
|
|
const entryPoints = process.argv.slice(5).filter(a => a !== '--prose');
|
2026-03-09 14:42:15 +00:00
|
|
|
|
|
|
|
|
if (!snapshotPath || !srcRoot || !outDir) {
|
2026-03-09 18:44:19 +00:00
|
|
|
console.error('Usage: node sysdoc.js <snapshot.json> <srcRoot> <outDir> [--prose] [entryPoint1] ...');
|
2026-03-09 14:42:15 +00:00
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const graph = GraphStore.loadSnapshot(snapshotPath);
|
2026-03-09 18:44:19 +00:00
|
|
|
// Using an IIFE to support top-level await
|
|
|
|
|
(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await generateDocs(graph, srcRoot, outDir, { entryPoints, prose: useProse });
|
|
|
|
|
console.log(`Generated docs in ${result.outDir}`);
|
|
|
|
|
console.log(`- ${result.subsystems} subsystems`);
|
|
|
|
|
console.log(`- ${result.contracts} contracts`);
|
|
|
|
|
console.log(`- ${result.flows} flows`);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error generating docs:', err);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
})();
|
2026-03-09 14:42:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { generateDocs };
|