Files
dev-intel-v2/supergraph.js

196 lines
6.2 KiB
JavaScript
Raw Normal View History

2026-03-09 18:19:14 +00:00
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 <outputDir> <repoId1:snapshot1.json> [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 };