const fs = require('fs'); const path = require('path'); const GraphStore = require('./graph.js'); /** * Developer Intelligence Pipeline v2 - Cross-Repo Namespace Registry * Resolves cross-repo references using 3-tier matching. * No external dependencies. */ const SCRIPT_DIR = __dirname; /** * Classify an entity into an artifact type for infrastructure-level matching. * Supports: rest-api, grpc-service, helm-chart, terraform-resource, config, code-module */ function classifyArtifact(entity) { if (entity.type === 'Config') { if (entity.kind === 'terraform' || entity.kind === 'hcl-block') return 'terraform-resource'; if (entity.kind === 'yaml-config' || entity.kind === 'yaml-key') return 'config'; return 'config'; } if (entity.type === 'Class' && entity.kind === 'interface') return 'interface'; if (entity.type === 'Class') return 'class'; if (entity.type === 'Function') return 'code-module'; if (entity.type === 'Module') return 'code-module'; return 'code-module'; } class NamespaceRegistry { constructor() { this.byShortName = new Map(); // shortName -> [{repoId, entityId, type, kind}] this.byEntityId = new Map(); // entityId -> {repoId, shortName} this.overrides = new Map(); // localName -> {repoId, entityId} } /** * Build registry from multiple graph snapshots. * Collects public entities and indexes them for cross-repo resolution. * @param {Array<{repoId: string, snapshot: GraphStore}>} repos * @returns {NamespaceRegistry} */ static build(repos) { const reg = new NamespaceRegistry(); for (const { repoId, snapshot } of repos) { for (const [id, entity] of snapshot.nodes.entries()) { if (entity.visibility !== 'public') continue; if (entity.type === 'Dependency') continue; const shortName = entity.name; const entry = { repoId, entityId: id, type: entity.type, kind: entity.kind, // Artifact classification for infrastructure matching artifact: classifyArtifact(entity), }; // byShortName if (!reg.byShortName.has(shortName)) { reg.byShortName.set(shortName, []); } reg.byShortName.get(shortName).push(entry); // byEntityId (prefix with repoId for cross-repo uniqueness) reg.byEntityId.set(`${repoId}:${id}`, { repoId, shortName, artifact: entry.artifact }); } } return reg; } /** * Load overrides from a JSON file. * @param {string} overridePath */ loadOverrides(overridePath) { if (!fs.existsSync(overridePath)) return; const data = JSON.parse(fs.readFileSync(overridePath, 'utf8')); for (const [localName, target] of Object.entries(data)) { const colonIdx = target.indexOf(':'); if (colonIdx > 0) { this.overrides.set(localName, { repoId: target.slice(0, colonIdx), entityId: target.slice(colonIdx + 1), }); } } } /** * Resolve a name using 3-tier matching. * @param {string} name - The unresolved target name * @param {string} [sourceRepoId] - The repo making the call (excluded from results) * @returns {{resolvedTo: {repoId, entityId}, tier: number, confidence: number} | null} */ resolve(name, sourceRepoId) { // Override always wins if (this.overrides.has(name)) { const target = this.overrides.get(name); return { resolvedTo: target, tier: 0, confidence: 1.0 }; } // Tier 1: Exact entity ID match for (const [key, val] of this.byEntityId.entries()) { const entityId = key.slice(key.indexOf(':') + 1); if (entityId === name && val.repoId !== sourceRepoId) { return { resolvedTo: { repoId: val.repoId, entityId }, tier: 1, confidence: 1.0 }; } } // Tier 2: Normalized match (strip extensions, normalize paths) const normalized = name.replace(/\.(ts|js|tsx|jsx|py|java|go|sh)$/, '').replace(/\\/g, '/'); for (const [key, val] of this.byEntityId.entries()) { const entityId = key.slice(key.indexOf(':') + 1); const normId = entityId.replace(/\.(ts|js|tsx|jsx|py|java|go|sh)/, '').replace(/\\/g, '/'); if (normId === normalized && val.repoId !== sourceRepoId) { return { resolvedTo: { repoId: val.repoId, entityId }, tier: 2, confidence: 0.9 }; } } // Tier 3: Name-only match const matches = (this.byShortName.get(name) || []).filter(e => e.repoId !== sourceRepoId); if (matches.length === 1) { return { resolvedTo: { repoId: matches[0].repoId, entityId: matches[0].entityId }, tier: 3, confidence: 0.7 }; } if (matches.length > 1) { // Ambiguous — return first match with lower confidence return { resolvedTo: { repoId: matches[0].repoId, entityId: matches[0].entityId }, tier: 3, confidence: 0.5 }; } return null; } /** * Resolve all unresolved CALLS edges in a graph. * @param {GraphStore} graph * @param {NamespaceRegistry} registry * @param {string} sourceRepoId * @returns {Array<{source, target, resolvedTo, tier, confidence}>} */ static resolveExternalCalls(graph, registry, sourceRepoId) { const results = []; for (const edge of graph.edges) { if (edge.type !== 'CALLS') continue; // If target exists as a node, it's internal — skip if (graph.nodes.has(edge.target)) continue; const resolution = registry.resolve(edge.target, sourceRepoId); if (resolution) { results.push({ source: edge.source, target: edge.target, resolvedTo: resolution.resolvedTo, tier: resolution.tier, confidence: resolution.confidence, }); } } return results; } /** * Serialize registry to JSON. */ toJSON() { return { byShortName: Object.fromEntries(this.byShortName), byEntityId: Object.fromEntries(this.byEntityId), overrides: Object.fromEntries(this.overrides), }; } /** * Deserialize registry from JSON. */ static fromJSON(data) { const reg = new NamespaceRegistry(); for (const [k, v] of Object.entries(data.byShortName || {})) { reg.byShortName.set(k, v); } for (const [k, v] of Object.entries(data.byEntityId || {})) { reg.byEntityId.set(k, v); } for (const [k, v] of Object.entries(data.overrides || {})) { reg.overrides.set(k, v); } return reg; } /** * Lookup a name in the registry. */ lookup(name) { const exact = this.byShortName.get(name) || []; // Also check entity IDs containing the name const byId = []; for (const [key, val] of this.byEntityId.entries()) { const entityId = key.slice(key.indexOf(':') + 1); if (entityId.includes(name)) { byId.push({ ...val, entityId }); } } return { byName: exact, byId }; } } // --- CLI --- if (require.main === module) { const args = process.argv.slice(2); const command = args[0]; if (command === 'build') { const outputIdx = args.indexOf('--output'); const outputPath = outputIdx >= 0 ? args[outputIdx + 1] : null; const snapshotPaths = args.slice(1).filter((_, i) => { const argIdx = i + 1; return argIdx !== outputIdx && argIdx !== outputIdx + 1; }); if (snapshotPaths.length === 0 || !outputPath) { console.error('Usage: node namespace.js build [snapshot2.json ...] --output '); process.exit(1); } const repos = snapshotPaths.map((p, i) => { const snapshot = GraphStore.loadSnapshot(p); const repoId = path.basename(p, '.json'); return { repoId, snapshot }; }); const registry = NamespaceRegistry.build(repos); // Load overrides if present const overridePath = path.join(path.dirname(outputPath), 'namespace-overrides.json'); registry.loadOverrides(overridePath); fs.writeFileSync(outputPath, JSON.stringify(registry.toJSON(), null, 2), 'utf8'); console.log(`Registry built: ${registry.byShortName.size} names, ${registry.byEntityId.size} entities from ${repos.length} repos. Saved to ${outputPath}`); } else if (command === 'resolve') { const graphPath = args[1]; const registryPath = args[2]; if (!graphPath || !registryPath) { console.error('Usage: node namespace.js resolve '); process.exit(1); } const graph = GraphStore.loadSnapshot(graphPath); const regData = JSON.parse(fs.readFileSync(registryPath, 'utf8')); const registry = NamespaceRegistry.fromJSON(regData); const sourceRepoId = path.basename(graphPath, '.json'); const results = NamespaceRegistry.resolveExternalCalls(graph, registry, sourceRepoId); if (results.length === 0) { console.log('No external calls resolved.'); } else { console.log(`Resolved ${results.length} external call(s):`); for (const r of results) { console.log(` ${r.source} -> ${r.target} => ${r.resolvedTo.repoId}:${r.resolvedTo.entityId} (tier ${r.tier}, confidence ${r.confidence})`); } } } else if (command === 'lookup') { const registryPath = args[1]; const name = args[2]; if (!registryPath || !name) { console.error('Usage: node namespace.js lookup '); process.exit(1); } const regData = JSON.parse(fs.readFileSync(registryPath, 'utf8')); const registry = NamespaceRegistry.fromJSON(regData); const result = registry.lookup(name); console.log(JSON.stringify(result, null, 2)); } else { console.error('Unknown command. Available: build, resolve, lookup'); process.exit(1); } } module.exports = NamespaceRegistry;