const fs = require('fs'); const path = require('path'); const GraphStore = require('./graph.js'); const { buildSubsystems } = require('./subsystem.js'); const { extractAllContracts, buildContractXref } = require('./contracts.js'); 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. */ async function generateDocs(graph, srcRoot, outDir, opts = {}) { const entryPoints = opts.entryPoints || []; 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.'); } } // 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); const xref = buildContractXref(contractsResult.contracts, graph, (p) => p.replace(/^\/?src\//, '')); // 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); let archProse = ''; if (proseMod) { console.log('Generating architecture overview...'); archProse = await proseMod.describeArchitecture(subs.subsystems, subs.crossCutting, {}, {}); archProse = `\n${archProse.trim()}\n\n`; } const sysArchContent = `# System Architecture ${archProse} ## 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`; } 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`; } const subContent = `# Subsystem: ${sub.name} ${subProse} **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); 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}`); // 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); let flowProse = ''; if (proseMod) { console.log(`Generating prose for flow: ${fr.entryPoint}...`); flowProse = await proseMod.describeFlow(fr, {}); flowProse = `${flowProse.trim()}\n\n`; } flowsContent += `## Flow: ${fr.entryPoint}\n`; flowsContent += flowProse; 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]; const useProse = process.argv.includes('--prose'); const entryPoints = process.argv.slice(5).filter(a => a !== '--prose'); if (!snapshotPath || !srcRoot || !outDir) { console.error('Usage: node sysdoc.js [--prose] [entryPoint1] ...'); process.exit(1); } const graph = GraphStore.loadSnapshot(snapshotPath); // 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); } })(); } module.exports = { generateDocs };