Phase 8: Helm chart extraction with Go template support
- extract-helm.js: strips Go templates, parses Chart.yaml/values.yaml/templates - Extracts K8s resource kinds, cross-chart interactions, shared secrets, ports - generateHelmDiagram() for Mermaid interaction graphs - Integrated into sysdoc.js: Helm entities merge into main knowledge graph - Dir-based filenames to handle duplicate chart names - .gitignore for node_modules, snapshots, venv, wasm - 76 charts, 1813 entities, 1769 relationships on Foxtrot
This commit is contained in:
151
sysdoc.js
151
sysdoc.js
@@ -5,6 +5,7 @@ const { buildSubsystems } = require('./subsystem.js');
|
||||
const { extractAllContracts, buildContractXref } = require('./contracts.js');
|
||||
const { buildFlowIndex, traceFlow } = require('./flow.js');
|
||||
const { generateDependencyDiagram, generateFlowDiagram, generateContractDiagram } = require('./diagrams.js');
|
||||
const { discoverCharts, chartsToGraph, generateHelmDiagram } = require('./extract-helm.js');
|
||||
|
||||
/**
|
||||
* Phase 7D: Hierarchical Doc Generator
|
||||
@@ -25,6 +26,26 @@ async function generateDocs(graph, srcRoot, outDir, opts = {}) {
|
||||
console.warn('Prose generation requested but prose.js not available. Skipping LLM pass.');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Discover Helm Charts (Phase 8) - Do this early to feed main graph
|
||||
const helmIgnore = new Set([
|
||||
'node_modules', '.git', 'venv', '__pycache__', '.terraform',
|
||||
'_bmad', '_bmad-output', '.codex', '.claude', '.cursor', '.gemini', '.kiro', '.agents'
|
||||
]);
|
||||
const helmCharts = discoverCharts(srcRoot, helmIgnore);
|
||||
const helmGraph = chartsToGraph(helmCharts, srcRoot);
|
||||
console.log(`Helm: ${helmCharts.length} charts, ${helmGraph.entities.length} entities, ${helmGraph.relationships.length} relationships`);
|
||||
|
||||
// Merge Helm into main graph so Subsystem Aggregator sees it
|
||||
for (const e of helmGraph.entities) {
|
||||
const fakePath = e.dir ? path.join(srcRoot, e.dir, 'Chart.yaml') : path.join(srcRoot, 'Chart.yaml');
|
||||
graph.nodes.set(e.id, { ...e, type: e.type || 'Module', _file: fakePath });
|
||||
if (!graph.fileIndex.has(fakePath)) graph.fileIndex.set(fakePath, new Set());
|
||||
graph.fileIndex.get(fakePath).add(e.id);
|
||||
}
|
||||
for (const r of helmGraph.relationships) {
|
||||
graph.edges.push(r);
|
||||
}
|
||||
|
||||
// 1. Build Subsystems (7A)
|
||||
const subs = buildSubsystems(graph, {
|
||||
@@ -46,6 +67,8 @@ async function generateDocs(graph, srcRoot, outDir, opts = {}) {
|
||||
'reference/subsystems',
|
||||
'reference/contracts',
|
||||
'reference/modules',
|
||||
'reference/helm',
|
||||
'reference/helm/charts',
|
||||
'explanation',
|
||||
'tutorials',
|
||||
'how-to',
|
||||
@@ -138,6 +161,128 @@ ${sub.files.map(f => `- \`${f}\``).join('\n')}
|
||||
|
||||
fs.writeFileSync(contractDocPath, `# System Contracts\n\n\`\`\`mermaid\n${allContractsDiag}\n\`\`\`\n\n${contractProseList}`);
|
||||
|
||||
// Generate Reference: Helm Charts
|
||||
const helmIndexPath = path.join(outDir, 'reference/helm/index.md');
|
||||
let helmIndexContent = '# Helm Charts\n\n| Chart | Path | Version | Resources | Dependencies | Interactions |\n|---|---|---|---|---|---|\n';
|
||||
|
||||
// Use dir-based filenames to avoid collisions between same-named charts
|
||||
for (const c of helmCharts) {
|
||||
const safeName = c.dir.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||
const chartDocPath = path.join(outDir, `reference/helm/charts/${safeName}.md`);
|
||||
|
||||
helmIndexContent += `| [${c.chart.name}](charts/${safeName}.md) | \`${c.dir}\` | ${c.chart.version} | ${c.templates.resources.length} | ${c.chart.dependencies.length} | ${c.interactions.length} |\n`;
|
||||
|
||||
let chartContent = `# Chart: ${c.chart.name}\n\n`;
|
||||
chartContent += `**Version:** ${c.chart.version} \n`;
|
||||
chartContent += `**App Version:** ${c.chart.appVersion || 'N/A'} \n`;
|
||||
chartContent += `**Path:** \`${c.dir}\`\n\n`;
|
||||
|
||||
if (c.chart.description) {
|
||||
chartContent += `${c.chart.description}\n\n`;
|
||||
}
|
||||
|
||||
if (c.chart.dependencies.length > 0) {
|
||||
chartContent += `## Dependencies\n`;
|
||||
for (const d of c.chart.dependencies) {
|
||||
chartContent += `- **${d.name}** (${d.version})${d.condition ? ` *if ${d.condition}*` : ''}\n`;
|
||||
}
|
||||
chartContent += '\n';
|
||||
}
|
||||
|
||||
if (c.interactions.length > 0) {
|
||||
chartContent += `## Interactions (Contracts)\n`;
|
||||
for (const i of c.interactions) {
|
||||
chartContent += `- **${i.type}**: \`${i.target}\` (via \`${i.file}\`)\n`;
|
||||
}
|
||||
chartContent += '\n';
|
||||
}
|
||||
|
||||
if (c.templates.resources.length > 0) {
|
||||
chartContent += `## Resources Generated\n`;
|
||||
for (const r of c.templates.resources) {
|
||||
chartContent += `- **${r.kind}**: \`${r.name}\` (${r.file})\n`;
|
||||
}
|
||||
chartContent += '\n';
|
||||
}
|
||||
|
||||
if (c.values.keys.length > 0) {
|
||||
chartContent += `## Configuration Surface (values.yaml)\n`;
|
||||
chartContent += `| Key | Type | Default |\n|---|---|---|\n`;
|
||||
for (const k of c.values.keys) {
|
||||
let defStr = k.defaultValue !== undefined ? String(k.defaultValue).replace(/\\n/g, ' ') : (k.hasDefault ? 'yes' : 'no');
|
||||
if (defStr.includes('|')) defStr = defStr.replace(/\\|/g, '\\\\|');
|
||||
chartContent += `| \`${k.name}\` | ${k.type} | ${defStr} |\n`;
|
||||
}
|
||||
chartContent += '\n';
|
||||
}
|
||||
|
||||
fs.writeFileSync(chartDocPath, chartContent);
|
||||
}
|
||||
|
||||
// Generate Helm interaction diagram
|
||||
const helmDiag = generateHelmDiagram(helmCharts);
|
||||
fs.writeFileSync(path.join(outDir, 'diagrams/helm-interactions.mmd'), helmDiag);
|
||||
|
||||
// Shared secrets/configmaps cross-reference
|
||||
const configUsers = {};
|
||||
for (const c of helmCharts) {
|
||||
for (const i of c.interactions) {
|
||||
if (i.type === 'config-ref') {
|
||||
if (!configUsers[i.target]) configUsers[i.target] = [];
|
||||
configUsers[i.target].push(c.chart.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Port map: which charts expose which ports
|
||||
const portMap = {};
|
||||
for (const c of helmCharts) {
|
||||
for (const i of c.interactions) {
|
||||
if (i.type === 'port' && i.target !== '0') {
|
||||
if (!portMap[i.target]) portMap[i.target] = [];
|
||||
if (!portMap[i.target].includes(c.chart.name)) portMap[i.target].push(c.chart.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
helmIndexContent += `\n## Interaction Diagram\n\`\`\`mermaid\n${helmDiag}\n\`\`\`\n`;
|
||||
|
||||
// Shared config/secrets table
|
||||
const sharedConfigs = Object.entries(configUsers).filter(([, users]) => users.length > 1);
|
||||
if (sharedConfigs.length > 0) {
|
||||
helmIndexContent += `\n## Shared Secrets & ConfigMaps\n| Secret/ConfigMap | Used By |\n|---|---|\n`;
|
||||
for (const [name, users] of sharedConfigs) {
|
||||
helmIndexContent += `| \`${name}\` | ${users.join(', ')} |\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Port allocation table
|
||||
const sharedPorts = Object.entries(portMap).filter(([, users]) => users.length > 1).sort((a, b) => Number(a[0]) - Number(b[0]));
|
||||
if (sharedPorts.length > 0) {
|
||||
helmIndexContent += `\n## Port Allocation (shared)\n| Port | Charts |\n|---|---|\n`;
|
||||
for (const [port, users] of sharedPorts) {
|
||||
helmIndexContent += `| ${port} | ${users.join(', ')} |\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// K8s service references
|
||||
const svcRefs = [];
|
||||
for (const c of helmCharts) {
|
||||
for (const i of c.interactions) {
|
||||
if (i.type === 'k8s-service') {
|
||||
svcRefs.push({ from: c.chart.name, to: i.target });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (svcRefs.length > 0) {
|
||||
helmIndexContent += `\n## Service-to-Service References\n| From Chart | Calls Service |\n|---|---|\n`;
|
||||
for (const ref of svcRefs) {
|
||||
helmIndexContent += `| ${ref.from} | \`${ref.to}\` |\n`;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(helmIndexPath, helmIndexContent);
|
||||
|
||||
// Generate Explanation: Data Flows
|
||||
const flowsPath = path.join(outDir, 'explanation/data-flows.md');
|
||||
let flowsContent = '# Data Flows\n\n';
|
||||
@@ -190,7 +335,11 @@ if (require.main === module) {
|
||||
// Using an IIFE to support top-level await
|
||||
(async () => {
|
||||
try {
|
||||
const result = await generateDocs(graph, srcRoot, outDir, { entryPoints, prose: useProse });
|
||||
const result = await generateDocs(graph, srcRoot, outDir, {
|
||||
srcDir: srcRoot.endsWith('/') ? srcRoot : srcRoot + '/',
|
||||
entryPoints,
|
||||
prose: useProse
|
||||
});
|
||||
console.log(`Generated docs in ${result.outDir}`);
|
||||
console.log(`- ${result.subsystems} subsystems`);
|
||||
console.log(`- ${result.contracts} contracts`);
|
||||
|
||||
Reference in New Issue
Block a user