From 4f7c77b3b10e6eb347694715fbade0f34e6f225c Mon Sep 17 00:00:00 2001 From: Jarvis Prime Date: Mon, 9 Mar 2026 20:05:52 +0000 Subject: [PATCH] Phase 8b: Helm contract extraction + diagram support - extractHelmContracts() in contracts.js: values, services, workloads, deps - Merged Helm contracts into main pipeline (124 contracts on Foxtrot) - diagrams.js: generateContractDiagram now handles Helm types - Sanitized Mermaid class names for Helm contracts - 1601-line contracts index with full classDiagram --- contracts.js | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++- diagrams.js | 24 +++++++++++--- sysdoc.js | 13 ++++++-- 3 files changed, 124 insertions(+), 7 deletions(-) diff --git a/contracts.js b/contracts.js index 31561a7..e83b2a0 100644 --- a/contracts.js +++ b/contracts.js @@ -205,6 +205,98 @@ function buildContractXref(contracts, graph, relPathFn) { return xref; } +/** + * Extract contracts from Helm chart data (from extract-helm.js). + * Produces contracts in the same shape as TypeScript ones for diagram/xref compatibility. + * @param {Array} helmCharts - From discoverCharts() + * @returns {object} { contracts, bySubsystem } + */ +function extractHelmContracts(helmCharts) { + const allContracts = []; + const bySubsystem = {}; + + for (const c of helmCharts) { + const sub = c.dir.split('/')[0] || 'root'; + if (!bySubsystem[sub]) bySubsystem[sub] = []; + + // Chart itself as a contract (its values.yaml is the API surface) + if (c.values.keys.length > 0) { + const contract = { + id: `${c.dir}/values.yaml:${c.chart.name}`, + type: 'HelmValues', + name: `${c.chart.name} (values)`, + visibility: 'public', + fields: c.values.keys.map(k => ({ + name: k.name, + type: k.type || 'unknown', + })), + }; + allContracts.push(contract); + bySubsystem[sub].push(contract); + } + + // Services produced by this chart + const services = c.templates.resources.filter(r => r.kind === 'Service'); + if (services.length > 0) { + const contract = { + id: `${c.dir}/templates:${c.chart.name}-services`, + type: 'HelmServices', + name: `${c.chart.name} (services)`, + visibility: 'public', + fields: services.map(s => ({ + name: s.name === '(templated)' ? `${c.chart.name}-svc` : s.name, + type: 'Service', + })), + }; + // Add port info from interactions + const ports = c.interactions.filter(i => i.type === 'port' && i.target !== '0'); + for (const p of ports) { + contract.fields.push({ name: `port:${p.target}`, type: 'port' }); + } + allContracts.push(contract); + bySubsystem[sub].push(contract); + } + + // Deployments/StatefulSets/DaemonSets as workload contracts + const workloads = c.templates.resources.filter(r => + ['Deployment', 'StatefulSet', 'DaemonSet', 'Rollout', 'CronJob', 'Job'].includes(r.kind) + ); + if (workloads.length > 0) { + const contract = { + id: `${c.dir}/templates:${c.chart.name}-workloads`, + type: 'HelmWorkloads', + name: `${c.chart.name} (workloads)`, + visibility: 'public', + fields: workloads.map(w => ({ + name: w.name === '(templated)' ? `${c.chart.name}-${w.kind.toLowerCase()}` : w.name, + type: w.kind, + })), + }; + allContracts.push(contract); + bySubsystem[sub].push(contract); + } + + // External interactions as dependency contracts + const extInteractions = c.interactions.filter(i => i.type === 'k8s-service' || i.type === 'config-ref'); + if (extInteractions.length > 0) { + const contract = { + id: `${c.dir}/templates:${c.chart.name}-deps`, + type: 'HelmDependencies', + name: `${c.chart.name} (external deps)`, + visibility: 'public', + fields: extInteractions.map(i => ({ + name: i.target, + type: i.type, + })), + }; + allContracts.push(contract); + bySubsystem[sub].push(contract); + } + } + + return { contracts: allContracts, bySubsystem }; +} + if (require.main === module) { const srcRoot = process.argv[2]; if (!srcRoot) { @@ -245,4 +337,4 @@ if (require.main === module) { } } -module.exports = { extractContracts, extractAllContracts, buildContractXref }; +module.exports = { extractContracts, extractAllContracts, buildContractXref, extractHelmContracts }; diff --git a/diagrams.js b/diagrams.js index 2e9667d..92b978c 100644 --- a/diagrams.js +++ b/diagrams.js @@ -88,23 +88,28 @@ function generateContractDiagram(contracts) { const lines = ['classDiagram']; for (const c of contracts) { + // Sanitize name for Mermaid (no spaces, parens, special chars) + const safeName = c.name.replace(/[^a-zA-Z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, ''); + if (!safeName) continue; + if (c.type === 'Interface' || c.type === 'TypeAlias') { - lines.push(` class ${c.name} {`); + lines.push(` class ${safeName} {`); if (c.fields) { for (const f of c.fields) { lines.push(` +${f.name}: ${f.type}`); } } lines.push(' }'); - lines.push(` <<${c.type}>> ${c.name}`); + lines.push(` <<${c.type}>> ${safeName}`); if (c.extends) { for (const ext of c.extends) { - lines.push(` ${ext} <|-- ${c.name}`); + const safeExt = ext.replace(/[^a-zA-Z0-9_]/g, '_'); + lines.push(` ${safeExt} <|-- ${safeName}`); } } } else if (c.type === 'Enum') { - lines.push(` class ${c.name} {`); + lines.push(` class ${safeName} {`); lines.push(' <>'); if (c.members) { for (const m of c.members) { @@ -112,6 +117,17 @@ function generateContractDiagram(contracts) { } } lines.push(' }'); + } else if (c.type === 'HelmValues' || c.type === 'HelmServices' || c.type === 'HelmWorkloads' || c.type === 'HelmDependencies') { + lines.push(` class ${safeName} {`); + lines.push(` <<${c.type}>>`); + if (c.fields) { + for (const f of c.fields) { + const safeField = f.name.replace(/[^a-zA-Z0-9_:.-]/g, '_'); + const safeType = (f.type || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_'); + lines.push(` +${safeField}: ${safeType}`); + } + } + lines.push(' }'); } } diff --git a/sysdoc.js b/sysdoc.js index ffddd1b..cf5aa7a 100644 --- a/sysdoc.js +++ b/sysdoc.js @@ -2,7 +2,7 @@ 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 { extractAllContracts, buildContractXref, extractHelmContracts } = require('./contracts.js'); const { buildFlowIndex, traceFlow } = require('./flow.js'); const { generateDependencyDiagram, generateFlowDiagram, generateContractDiagram } = require('./diagrams.js'); const { discoverCharts, chartsToGraph, generateHelmDiagram } = require('./extract-helm.js'); @@ -54,8 +54,17 @@ async function generateDocs(graph, srcRoot, outDir, opts = {}) { crossCuttingThreshold: opts.crossCuttingThreshold || 0.6 }); - // 2. Extract Contracts (7B) + // 2. Extract Contracts (7B) — TypeScript + Helm const contractsResult = extractAllContracts(subs, srcRoot); + const helmContracts = extractHelmContracts(helmCharts); + + // Merge Helm contracts into main result + contractsResult.contracts.push(...helmContracts.contracts); + for (const [sub, contracts] of Object.entries(helmContracts.bySubsystem)) { + if (!contractsResult.bySubsystem[sub]) contractsResult.bySubsystem[sub] = []; + contractsResult.bySubsystem[sub].push(...contracts); + } + const xref = buildContractXref(contractsResult.contracts, graph, (p) => p.replace(/^\/?src\//, '')); // 3. Trace Flows (7C)