const fs = require('fs'); const path = require('path'); const GraphStore = require('./graph.js'); const NamespaceRegistry = require('./namespace.js'); /** * Phase 7F: Supergraph — Multi-Repo Merge * Merges multiple repo graph snapshots into a unified super-graph, * resolving cross-repo references via the namespace registry. */ /** * Merge multiple repo snapshots into a single super-graph. * Prefixes all node IDs and edge source/targets with repoId to avoid collisions. * Resolves cross-repo CALLS/IMPORTS via namespace registry. * * @param {Array<{repoId: string, snapshotPath: string}>} repos * @param {object} opts - { overridesPath } * @returns {{ graph: GraphStore, registry: NamespaceRegistry, crossRepoEdges: Array, stats: object }} */ function buildSupergraph(repos, opts = {}) { const snapshots = []; const graphs = []; // Load all snapshots for (const repo of repos) { const graph = GraphStore.loadSnapshot(repo.snapshotPath); snapshots.push({ repoId: repo.repoId, snapshot: graph }); graphs.push({ repoId: repo.repoId, graph }); } // Build namespace registry const registry = NamespaceRegistry.build(snapshots); if (opts.overridesPath) { registry.loadOverrides(opts.overridesPath); } // Create merged graph const merged = new GraphStore(); const crossRepoEdges = []; const stats = { repos: repos.length, totalNodes: 0, totalEdges: 0, crossRepoEdges: 0, resolvedCalls: 0, unresolvedCalls: 0, }; // 1. Merge all nodes with repo-prefixed IDs for (const { repoId, graph } of graphs) { for (const [id, node] of graph.nodes) { const prefixedId = `${repoId}::${id}`; merged.nodes.set(prefixedId, { ...node, _repo: repoId, _originalId: id, }); stats.totalNodes++; } // Merge file index for (const [file, ids] of graph.fileIndex) { const prefixedFile = `${repoId}::${file}`; const prefixedIds = (Array.isArray(ids) ? ids : [...ids]).map(id => `${repoId}::${id}`); merged.fileIndex.set(prefixedFile, prefixedIds); } } // 2. Merge all edges, resolving cross-repo references for (const { repoId, graph } of graphs) { for (const edge of graph.edges) { const prefixedSource = `${repoId}::${edge.source}`; // Check if target is internal if (graph.nodes.has(edge.target) || edge.target.startsWith('dep:')) { const prefixedTarget = edge.target.startsWith('dep:') ? `${repoId}::${edge.target}` : `${repoId}::${edge.target}`; merged.edges.push({ type: edge.type, source: prefixedSource, target: prefixedTarget, _repo: repoId, }); stats.totalEdges++; continue; } // Try to resolve via namespace registry if (edge.type === 'CALLS' || edge.type === 'IMPORTS') { const resolution = registry.resolve(edge.target, repoId); if (resolution) { const resolvedTarget = `${resolution.resolvedTo.repoId}::${resolution.resolvedTo.entityId}`; const crossEdge = { type: edge.type, source: prefixedSource, target: resolvedTarget, _repo: repoId, _crossRepo: true, _tier: resolution.tier, _confidence: resolution.confidence, _targetRepo: resolution.resolvedTo.repoId, }; merged.edges.push(crossEdge); crossRepoEdges.push(crossEdge); stats.totalEdges++; stats.crossRepoEdges++; stats.resolvedCalls++; } else { // Unresolved — keep as-is with repo prefix merged.edges.push({ type: edge.type, source: prefixedSource, target: `${repoId}::${edge.target}`, _repo: repoId, _unresolved: true, }); stats.totalEdges++; stats.unresolvedCalls++; } } else { // Non-CALLS/IMPORTS edges (CONTAINS, IMPLEMENTS) merged.edges.push({ type: edge.type, source: prefixedSource, target: `${repoId}::${edge.target}`, _repo: repoId, }); stats.totalEdges++; } } } return { graph: merged, registry, crossRepoEdges, stats }; } /** * Save the supergraph snapshot to disk. */ function saveSupergraph(result, outputDir) { fs.mkdirSync(outputDir, { recursive: true }); // Save merged graph const graphPath = path.join(outputDir, 'supergraph.json'); const graphData = { nodes: Object.fromEntries(result.graph.nodes), edges: result.graph.edges, fileIndex: Object.fromEntries(result.graph.fileIndex), }; fs.writeFileSync(graphPath, JSON.stringify(graphData, null, 2)); // Save registry const regPath = path.join(outputDir, 'registry.json'); fs.writeFileSync(regPath, JSON.stringify(result.registry.toJSON(), null, 2)); // Save cross-repo edges summary const xrepoPath = path.join(outputDir, 'cross-repo-edges.json'); fs.writeFileSync(xrepoPath, JSON.stringify(result.crossRepoEdges, null, 2)); // Save stats const statsPath = path.join(outputDir, 'stats.json'); fs.writeFileSync(statsPath, JSON.stringify(result.stats, null, 2)); return { graphPath, regPath, xrepoPath, statsPath }; } if (require.main === module) { const args = process.argv.slice(2); if (args.length < 2) { console.error('Usage: node supergraph.js [repoId2:snapshot2.json ...]'); process.exit(1); } const outputDir = args[0]; const repos = args.slice(1).map(arg => { const [repoId, snapshotPath] = arg.split(':'); return { repoId, snapshotPath }; }); console.log(`Merging ${repos.length} repos...`); const result = buildSupergraph(repos); const paths = saveSupergraph(result, outputDir); console.log(`Supergraph built:`); console.log(` Repos: ${result.stats.repos}`); console.log(` Nodes: ${result.stats.totalNodes}`); console.log(` Edges: ${result.stats.totalEdges}`); console.log(` Cross-repo edges: ${result.stats.crossRepoEdges}`); console.log(` Resolved: ${result.stats.resolvedCalls}, Unresolved: ${result.stats.unresolvedCalls}`); console.log(` Saved to: ${outputDir}`); } module.exports = { buildSupergraph, saveSupergraph };