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
This commit is contained in:
Jarvis Prime
2026-03-09 20:05:52 +00:00
parent f49a6c2dd9
commit 4f7c77b3b1
3 changed files with 124 additions and 7 deletions

View File

@@ -205,6 +205,98 @@ function buildContractXref(contracts, graph, relPathFn) {
return xref; 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) { if (require.main === module) {
const srcRoot = process.argv[2]; const srcRoot = process.argv[2];
if (!srcRoot) { if (!srcRoot) {
@@ -245,4 +337,4 @@ if (require.main === module) {
} }
} }
module.exports = { extractContracts, extractAllContracts, buildContractXref }; module.exports = { extractContracts, extractAllContracts, buildContractXref, extractHelmContracts };

View File

@@ -88,23 +88,28 @@ function generateContractDiagram(contracts) {
const lines = ['classDiagram']; const lines = ['classDiagram'];
for (const c of contracts) { 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') { if (c.type === 'Interface' || c.type === 'TypeAlias') {
lines.push(` class ${c.name} {`); lines.push(` class ${safeName} {`);
if (c.fields) { if (c.fields) {
for (const f of c.fields) { for (const f of c.fields) {
lines.push(` +${f.name}: ${f.type}`); lines.push(` +${f.name}: ${f.type}`);
} }
} }
lines.push(' }'); lines.push(' }');
lines.push(` <<${c.type}>> ${c.name}`); lines.push(` <<${c.type}>> ${safeName}`);
if (c.extends) { if (c.extends) {
for (const ext of 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') { } else if (c.type === 'Enum') {
lines.push(` class ${c.name} {`); lines.push(` class ${safeName} {`);
lines.push(' <<enumeration>>'); lines.push(' <<enumeration>>');
if (c.members) { if (c.members) {
for (const m of c.members) { for (const m of c.members) {
@@ -112,6 +117,17 @@ function generateContractDiagram(contracts) {
} }
} }
lines.push(' }'); 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(' }');
} }
} }

View File

@@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const GraphStore = require('./graph.js'); const GraphStore = require('./graph.js');
const { buildSubsystems } = require('./subsystem.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 { buildFlowIndex, traceFlow } = require('./flow.js');
const { generateDependencyDiagram, generateFlowDiagram, generateContractDiagram } = require('./diagrams.js'); const { generateDependencyDiagram, generateFlowDiagram, generateContractDiagram } = require('./diagrams.js');
const { discoverCharts, chartsToGraph, generateHelmDiagram } = require('./extract-helm.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 crossCuttingThreshold: opts.crossCuttingThreshold || 0.6
}); });
// 2. Extract Contracts (7B) // 2. Extract Contracts (7B) — TypeScript + Helm
const contractsResult = extractAllContracts(subs, srcRoot); 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\//, '')); const xref = buildContractXref(contractsResult.contracts, graph, (p) => p.replace(/^\/?src\//, ''));
// 3. Trace Flows (7C) // 3. Trace Flows (7C)