/** * Dev Intel V3 — Pipeline Glue Layer * * Orchestrates OSS tools (terraform-docs, helm-docs) + custom analysis * (graph builder, subsystem aggregator, cross-chart interactions, LLM prose). * * Data Contract: * terraform-docs → markdown per module dir → merged into docs/reference/terraform/ * helm-docs → README.md per chart dir → merged into docs/reference/helm/charts/ * graph.js → snapshot.json (nodes + edges + fileIndex) * subsystem.js → subsystems[] with dependency matrix * prose.js → LLM-enriched descriptions injected into final markdown * impact.js → change impact analysis from graph traversal */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const GraphStore = require('./graph.js'); const { buildSubsystems } = require('./subsystem.js'); const { discoverCharts, chartsToGraph } = require('./extract-helm.js'); const { detectAnomalies } = require('./prose.js'); const { queryImpact, formatImpactMarkdown } = require('./impact.js'); const { detectEntryPoints } = require('./flow.js'); const PATH_PREFIX = '/home/node/.local/bin:' + process.env.PATH; const IGNORE_DIRS = new Set([ 'node_modules', '.git', 'dist', 'build', '__pycache__', '.next', '.turbo', 'coverage', '.nyc_output', 'vendor', 'venv', '.codex', '.claude', '.cursor', '.gemini', '.kiro', '.agents', '_bmad', '_bmad-output', '.terraform', 'skills', '.ci', 'docs', '.workdir', ]); /** * Step 1: Run terraform-docs on all directories containing .tf files. * Returns: Map */ function runTerraformDocs(repoRoot) { const results = new Map(); const tfDirs = new Set(); // Find all dirs with .tf files function walk(dir) { const base = path.basename(dir); if (IGNORE_DIRS.has(base)) return; let hasTf = false; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const e of entries) { if (e.isDirectory()) walk(path.join(dir, e.name)); else if (e.name.endsWith('.tf')) hasTf = true; } } catch { return; } if (hasTf) tfDirs.add(dir); } walk(repoRoot); console.log(`terraform-docs: scanning ${tfDirs.size} directories...`); let success = 0, fail = 0; for (const dir of tfDirs) { try { const md = execSync(`terraform-docs markdown table "${dir}" 2>/dev/null`, { env: { ...process.env, PATH: PATH_PREFIX }, timeout: 15000, maxBuffer: 1024 * 1024, }).toString(); if (md.trim().length > 10) { results.set(path.relative(repoRoot, dir), md); success++; } } catch { fail++; } } console.log(`terraform-docs: ${success} generated, ${fail} skipped`); return results; } /** * Step 2: Run helm-docs on all chart directories. * Returns: Map */ function runHelmDocs(repoRoot) { const results = new Map(); const chartDirs = []; function walk(dir) { const base = path.basename(dir); if (IGNORE_DIRS.has(base)) return; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); const hasChart = entries.some(e => e.name === 'Chart.yaml'); if (hasChart) chartDirs.push(dir); for (const e of entries) { if (e.isDirectory()) walk(path.join(dir, e.name)); } } catch { return; } } walk(repoRoot); console.log(`helm-docs: scanning ${chartDirs.length} charts...`); let success = 0, fail = 0; for (const dir of chartDirs) { try { execSync(`helm-docs --chart-search-root "${dir}" --dry-run 2>/dev/null`, { env: { ...process.env, PATH: PATH_PREFIX }, timeout: 15000, maxBuffer: 1024 * 1024, }); // helm-docs writes README.md in-place with --dry-run it outputs to stdout const md = execSync(`helm-docs --chart-search-root "${dir}" --dry-run 2>/dev/null`, { env: { ...process.env, PATH: PATH_PREFIX }, timeout: 15000, maxBuffer: 1024 * 1024, }).toString(); if (md.trim().length > 10) { results.set(path.relative(repoRoot, dir), md); success++; } } catch { fail++; } } console.log(`helm-docs: ${success} generated, ${fail} skipped`); return results; } /** * Step 3: Run custom graph extraction + subsystem analysis. * Uses the existing pipeline.js batch extraction. */ function runGraphExtraction(repoRoot, snapshotPath) { console.log('Running graph extraction pipeline...'); execSync(`node pipeline.js batch "${repoRoot}" --output ./output 2>&1`, { cwd: __dirname, timeout: 300000, maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], }); return GraphStore.loadSnapshot(snapshotPath); } /** * Step 4: Assemble final docs directory. */ function assembleDocs(outDir, { tfDocs, helmDocs, graph, subs, helmCharts, impactResults, entryPoints }) { // Create directory structure const dirs = [ 'reference/terraform', 'reference/helm/charts', 'reference/subsystems', 'explanation', 'diagrams' ]; for (const d of dirs) { fs.mkdirSync(path.join(outDir, d), { recursive: true }); } // Write terraform docs for (const [dir, md] of tfDocs) { const safeName = dir.replace(/\//g, '-'); fs.writeFileSync(path.join(outDir, `reference/terraform/${safeName}.md`), `# Terraform: ${dir}\n\n${md}`); } // Write helm docs for (const [dir, md] of helmDocs) { const safeName = dir.replace(/\//g, '-'); fs.writeFileSync(path.join(outDir, `reference/helm/charts/${safeName}.md`), md); } // Write helm index with inline dependency data let helmIndex = '# Helm Charts\n\n## Quick Lookup\n\n'; helmIndex += '| Chart | Path | Dependencies | Values Keys |\n|---|---|---|---|\n'; for (const c of helmCharts) { const deps = (c.chart.dependencies || []).map(d => d.name).join(', ') || 'none'; const valKeys = Object.keys(c.values || {}).slice(0, 5).join(', '); const safeName = c.dir.replace(/\//g, '-'); helmIndex += `| ${c.chart.name} | [${c.dir}](charts/${safeName}.md) | ${deps} | ${valKeys} |\n`; } fs.writeFileSync(path.join(outDir, 'reference/helm/index.md'), helmIndex); // Write terraform index let tfIndex = '# Terraform Modules\n\n'; tfIndex += `| Directory | Generated |\n|---|---|\n`; for (const [dir] of tfDocs) { const safeName = dir.replace(/\//g, '-'); tfIndex += `| ${dir} | [docs](terraform/${safeName}.md) |\n`; } fs.writeFileSync(path.join(outDir, 'reference/terraform/index.md'), tfIndex); // Write subsystem docs for (const sub of subs.subsystems) { const anomalies = detectAnomalies(sub, subs.dependencyMatrix || {}); let content = `# Subsystem: ${sub.name}\n\n`; content += `**Kind:** ${sub.kind}\n**Files:** ${sub.files.length}\n`; content += `**Functions:** ${sub.entities.functions}, **Classes:** ${sub.entities.classes}\n\n`; if (anomalies.length > 0) { content += `## Structural Notes\n${anomalies.map(a => `- ${a}`).join('\n')}\n\n`; } const outDeps = Object.entries(subs.dependencyMatrix || {}) .filter(([k]) => k.startsWith(sub.name + '→')) .map(([k, v]) => `- → ${k.split('→')[1]}: ${v.calls} calls, ${v.imports} imports`); const inDeps = Object.entries(subs.dependencyMatrix || {}) .filter(([k]) => k.endsWith('→' + sub.name)) .map(([k, v]) => `- ← ${k.split('→')[0]}: ${v.calls} calls, ${v.imports} imports`); if (outDeps.length > 0 || inDeps.length > 0) { content += `## Dependencies\n`; if (outDeps.length) content += `### Depends On\n${outDeps.join('\n')}\n\n`; if (inDeps.length) content += `### Depended On By\n${inDeps.join('\n')}\n\n`; } if (sub.publicExports.length > 0) { content += `## Public Exports\n${sub.publicExports.map(e => `- \`${e}\``).join('\n')}\n`; } fs.writeFileSync(path.join(outDir, `reference/subsystems/${sub.name}.md`), content); } // Write architecture overview let arch = '# System Architecture\n\n'; arch += `## Summary\n- **Subsystems:** ${subs.subsystems.length}\n`; arch += `- **Helm Charts:** ${helmCharts.length}\n`; arch += `- **Terraform Modules:** ${tfDocs.size}\n`; arch += `- **Cross-Cutting:** ${subs.crossCutting.join(', ') || 'none'}\n\n`; arch += '## Subsystems\n\n| Subsystem | Kind | Files | Functions |\n|---|---|---|---|\n'; for (const s of subs.subsystems) { arch += `| ${s.name} | ${s.kind} | ${s.files.length} | ${s.entities.functions} |\n`; } fs.writeFileSync(path.join(outDir, 'reference/system-architecture.md'), arch); // Write change impact analysis if (impactResults.length > 0) { let impactContent = '# Change Impact Analysis\n\n'; for (const r of impactResults.filter(r => r.impactedCount > 0).slice(0, 15)) { impactContent += formatImpactMarkdown(r) + '\n\n---\n\n'; } fs.writeFileSync(path.join(outDir, 'explanation/change-impact.md'), impactContent); } // Write entry points if (entryPoints.length > 0) { let epContent = '# Entry Points\n\n| Entry | Kind | Reason |\n|---|---|---|\n'; for (const ep of entryPoints) { epContent += `| \`${ep.id}\` | ${ep.kind} | ${ep.reason} |\n`; } fs.writeFileSync(path.join(outDir, 'explanation/entry-points.md'), epContent); } // Count output let fileCount = 0; function countFiles(dir) { for (const e of fs.readdirSync(dir, { withFileTypes: true })) { if (e.isDirectory()) countFiles(path.join(dir, e.name)); else fileCount++; } } countFiles(outDir); return fileCount; } // Main async function main() { const repoRoot = process.argv[2] || '/home/node/.openclaw/workspace/agents/max/foxtrot/'; const outDir = process.argv[3] || './foxtrot-docs-v3'; const snapshotPath = './output/snapshot.json'; console.log('=== Dev Intel V3 Pipeline ==='); console.log(`Repo: ${repoRoot}`); console.log(`Output: ${outDir}`); // Clean output if (fs.existsSync(outDir)) { fs.rmSync(outDir, { recursive: true }); } // Step 1: terraform-docs const tfDocs = runTerraformDocs(repoRoot); // Step 2: helm-docs const helmDocs = runHelmDocs(repoRoot); // Step 3: Graph extraction (reuse existing pipeline) const graph = runGraphExtraction(repoRoot, snapshotPath); // Step 4: Helm chart discovery + merge into graph const helmIgnore = new Set([...IGNORE_DIRS]); const helmCharts = discoverCharts(repoRoot, helmIgnore); chartsToGraph(helmCharts, graph, repoRoot); // Step 5: Subsystem analysis const subs = buildSubsystems(graph, { srcDir: repoRoot }); // Step 6: Entry points const entryPoints = detectEntryPoints(graph); console.log(`Entry points: ${entryPoints.length} detected`); // Step 7: Impact analysis const impactTargets = []; for (const [id, node] of graph.nodes) { if (node.kind === 'terraform-module' || node.kind === 'terraform-resource') { impactTargets.push(id); } } for (const c of helmCharts) { if (c.interactions && c.interactions.length > 3) { const chartId = `helm:${c.dir}:${c.chart.name}`; if (graph.nodes.has(chartId)) impactTargets.push(chartId); } } const impactResults = impactTargets.slice(0, 20).map(t => queryImpact(graph, t, 5)); console.log(`Impact analysis: ${impactResults.filter(r => r.impactedCount > 0).length} nodes with dependents`); // Step 8: Assemble const fileCount = assembleDocs(outDir, { tfDocs, helmDocs, graph, subs, helmCharts, impactResults, entryPoints }); console.log(`\n=== V3 Pipeline Complete ===`); console.log(`Output: ${fileCount} files in ${outDir}`); console.log(`Terraform modules: ${tfDocs.size}`); console.log(`Helm charts: ${helmDocs.size}`); console.log(`Subsystems: ${subs.subsystems.length}`); } main().catch(err => { console.error('Pipeline failed:', err.message); process.exit(1); });