Files
dev-intel-v2/namespace.js
Jarvis Prime efb12d003b Dev Intel Pipeline v2 — multi-language semantic graph extractor
Phase 1: extract.js — tree-sitter AST parser (TS/JS/Python/Go/Java/Bash) + config parsers (YAML/HCL)
Phase 2: graph.js — in-memory directed graph store with build/query/diff CLI
Phase 3: namespace.js — cross-repo namespace registry with 3-tier resolution
Phase 4: semantic-diff.js — categorized diffs with impact scoring (0-100)
Phase 5: pipeline.js — batch extraction, incremental diffing, benchmarking

Benchmark: 4,325 files, 21,646 nodes, 133,979 edges in 67s (15ms/file)
BMad SPA reviews: all phases GO
2026-03-09 05:29:29 +00:00

292 lines
9.5 KiB
JavaScript

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 <snapshot1.json> [snapshot2.json ...] --output <registry.json>');
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 <graph-snapshot.json> <registry.json>');
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 <registry.json> <name>');
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;