@opichi/smartcode
Version:
Universal code intelligence MCP server - analyze any codebase with TypeScript excellence and multi-language support
227 lines • 9.46 kB
JavaScript
export class CodeKnowledgeGraph {
nodes = new Map();
relationships = [];
projectStructure = null;
constructor() { }
addNode(node) {
this.nodes.set(node.id, node);
}
addNodes(nodes) {
nodes.forEach(node => this.addNode(node));
}
addRelationship(relationship) {
this.relationships.push(relationship);
}
setProjectStructure(structure) {
this.projectStructure = structure;
}
buildRelationships() {
console.log('Building code relationships...');
// Clear existing relationships
this.relationships = [];
// Build different types of relationships
this.buildImportRelationships();
this.buildClassMethodRelationships();
this.buildCallRelationships();
this.buildArchitecturalRelationships();
console.log(`Built ${this.relationships.length} relationships`);
}
buildImportRelationships() {
const importNodes = Array.from(this.nodes.values()).filter(n => n.type === 'import');
importNodes.forEach(importNode => {
const source = importNode.metadata.source;
if (!source)
return;
// Find what this import is importing from
const targetNodes = this.findNodesByFile(source);
targetNodes.forEach(targetNode => {
if (targetNode.type === 'export' || targetNode.type === 'function' || targetNode.type === 'class') {
this.addRelationship({
from: importNode.id,
to: targetNode.id,
type: 'imports',
metadata: { source }
});
}
});
});
}
buildClassMethodRelationships() {
const classNodes = Array.from(this.nodes.values()).filter(n => n.type === 'class');
classNodes.forEach(classNode => {
// Find methods in the same file that belong to this class
const fileMethods = this.findNodesByFile(classNode.filePath)
.filter(n => n.type === 'function' &&
n.startLine > classNode.startLine &&
n.endLine < classNode.endLine);
fileMethods.forEach(method => {
this.addRelationship({
from: classNode.id,
to: method.id,
type: 'defines',
metadata: { memberType: 'method' }
});
});
});
}
buildCallRelationships() {
// This is a simplified version - in practice, you'd need more sophisticated AST analysis
const functionNodes = Array.from(this.nodes.values()).filter(n => n.type === 'function');
functionNodes.forEach(funcNode => {
// Look for function calls in the content
const content = funcNode.content;
// Find other functions that might be called
functionNodes.forEach(otherFunc => {
if (funcNode.id !== otherFunc.id && content.includes(otherFunc.name + '(')) {
this.addRelationship({
from: funcNode.id,
to: otherFunc.id,
type: 'calls',
metadata: { callType: 'function' }
});
}
});
});
}
buildArchitecturalRelationships() {
if (!this.projectStructure)
return;
// Build MVC relationships for web frameworks
if (this.projectStructure.framework === 'rails' ||
this.projectStructure.framework === 'express' ||
this.projectStructure.framework === 'django') {
this.buildMVCRelationships();
}
// Build API route relationships
this.buildRouteRelationships();
}
buildMVCRelationships() {
const models = this.findNodesByPattern('model');
const controllers = this.findNodesByPattern('controller');
const views = this.findNodesByPattern('view');
// Connect controllers to models
controllers.forEach(controller => {
models.forEach(model => {
if (this.isRelated(controller, model)) {
this.addRelationship({
from: controller.id,
to: model.id,
type: 'uses',
metadata: { pattern: 'mvc', relationship: 'controller-model' }
});
}
});
});
// Connect controllers to views
controllers.forEach(controller => {
views.forEach(view => {
if (this.isRelated(controller, view)) {
this.addRelationship({
from: controller.id,
to: view.id,
type: 'uses',
metadata: { pattern: 'mvc', relationship: 'controller-view' }
});
}
});
});
}
buildRouteRelationships() {
// Find route definitions and connect them to controllers
const routes = Array.from(this.nodes.values()).filter(n => n.content.includes('router.') ||
n.content.includes('app.get') ||
n.content.includes('app.post') ||
n.content.includes('Route::'));
const controllers = this.findNodesByPattern('controller');
routes.forEach(route => {
controllers.forEach(controller => {
if (route.content.includes(controller.name) ||
this.extractControllerFromRoute(route.content) === controller.name) {
this.addRelationship({
from: route.id,
to: controller.id,
type: 'uses',
metadata: { pattern: 'routing' }
});
}
});
});
}
findNodesByFile(filePath) {
return Array.from(this.nodes.values()).filter(n => n.filePath === filePath || n.filePath.endsWith(filePath));
}
findNodesByPattern(pattern) {
return Array.from(this.nodes.values()).filter(n => n.filePath.toLowerCase().includes(pattern) ||
n.name.toLowerCase().includes(pattern));
}
isRelated(node1, node2) {
// Simple heuristic: check if names are related
const name1 = node1.name.toLowerCase().replace(/controller|model|view/, '');
const name2 = node2.name.toLowerCase().replace(/controller|model|view/, '');
return name1.includes(name2) || name2.includes(name1) ||
this.extractEntityName(name1) === this.extractEntityName(name2);
}
extractEntityName(name) {
// Extract the core entity name (e.g., 'user' from 'UserController')
return name.replace(/(controller|model|view|service)s?$/, '').toLowerCase();
}
extractControllerFromRoute(routeContent) {
// Extract controller name from route definition
const matches = routeContent.match(/(\w+)Controller/);
return matches ? matches[1] + 'Controller' : '';
}
// Query methods
getRelatedNodes(nodeId, relationshipType) {
const relationships = this.relationships.filter(rel => (rel.from === nodeId || rel.to === nodeId) &&
(!relationshipType || rel.type === relationshipType));
const relatedNodeIds = relationships.map(rel => rel.from === nodeId ? rel.to : rel.from);
return relatedNodeIds
.map(id => this.nodes.get(id))
.filter((node) => node !== undefined);
}
getNodesByType(type) {
return Array.from(this.nodes.values()).filter(n => n.type === type);
}
findNodesByName(name) {
return Array.from(this.nodes.values()).filter(n => n.name.toLowerCase().includes(name.toLowerCase()));
}
getArchitecturalComponents() {
const components = new Map();
components.set('models', this.findNodesByPattern('model'));
components.set('controllers', this.findNodesByPattern('controller'));
components.set('services', this.findNodesByPattern('service'));
components.set('routes', this.findNodesByPattern('route'));
components.set('views', this.findNodesByPattern('view'));
components.set('components', this.findNodesByPattern('component'));
return components;
}
getImpactRadius(nodeId, maxDepth = 3) {
const impacted = new Set();
const queue = [{ id: nodeId, depth: 0 }];
const visited = new Set();
while (queue.length > 0) {
const { id, depth } = queue.shift();
if (visited.has(id) || depth > maxDepth)
continue;
visited.add(id);
impacted.add(id);
// Find all nodes that depend on this one
const dependents = this.relationships
.filter(rel => rel.to === id)
.map(rel => rel.from);
dependents.forEach(depId => {
if (!visited.has(depId)) {
queue.push({ id: depId, depth: depth + 1 });
}
});
}
return impacted;
}
exportGraph() {
return {
nodes: Array.from(this.nodes.values()),
relationships: this.relationships
};
}
}
//# sourceMappingURL=graph.js.map