@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
382 lines (330 loc) • 9.67 kB
text/typescript
/**
* Graph-based memory system for code intelligence
* Explores different database approaches for agent memory
*/
export interface GraphNode {
id: string;
type: 'file' | 'function' | 'class' | 'variable' | 'import' | 'test' | 'commit';
name: string;
properties: Record<string, any>;
timestamp: Date;
}
export interface GraphEdge {
from: string;
to: string;
type: 'calls' | 'imports' | 'extends' | 'implements' | 'tests' | 'modifies' | 'depends_on';
properties?: Record<string, any>;
timestamp: Date;
}
export interface MemoryQuery {
type: 'graph' | 'temporal' | 'semantic';
query: string;
timeRange?: { start: Date; end: Date };
depth?: number;
}
/**
* In-memory graph database for code relationships
* Lightweight alternative to Neo4j for local use
*/
export class CodeGraphMemory {
private nodes: Map<string, GraphNode> = new Map();
private edges: Map<string, GraphEdge[]> = new Map();
private temporalIndex: Map<number, string[]> = new Map(); // timestamp -> node IDs
/**
* Add a node to the graph
*/
addNode(node: GraphNode): void {
this.nodes.set(node.id, node);
// Add to temporal index
const timestamp = node.timestamp.getTime();
if (!this.temporalIndex.has(timestamp)) {
this.temporalIndex.set(timestamp, []);
}
this.temporalIndex.get(timestamp)!.push(node.id);
}
/**
* Add an edge between nodes
*/
addEdge(edge: GraphEdge): void {
if (!this.edges.has(edge.from)) {
this.edges.set(edge.from, []);
}
this.edges.get(edge.from)!.push(edge);
}
/**
* Find all nodes connected to a given node
*/
getConnectedNodes(nodeId: string, edgeType?: string, depth: number = 1): GraphNode[] {
const visited = new Set<string>();
const result: GraphNode[] = [];
const traverse = (id: string, currentDepth: number) => {
if (visited.has(id) || currentDepth > depth) return;
visited.add(id);
const node = this.nodes.get(id);
if (node && currentDepth > 0) {
result.push(node);
}
const edges = this.edges.get(id) || [];
for (const edge of edges) {
if (!edgeType || edge.type === edgeType) {
traverse(edge.to, currentDepth + 1);
}
}
};
traverse(nodeId, 0);
return result;
}
/**
* Find nodes modified within a time range
*/
getNodesInTimeRange(start: Date, end: Date): GraphNode[] {
const startTime = start.getTime();
const endTime = end.getTime();
const result: GraphNode[] = [];
for (const [timestamp, nodeIds] of this.temporalIndex) {
if (timestamp >= startTime && timestamp <= endTime) {
for (const nodeId of nodeIds) {
const node = this.nodes.get(nodeId);
if (node) result.push(node);
}
}
}
return result;
}
/**
* Find dependency chains
*/
findDependencyChain(fromId: string, toId: string): GraphNode[][] {
const paths: GraphNode[][] = [];
const visited = new Set<string>();
const dfs = (current: string, path: GraphNode[]) => {
if (current === toId) {
paths.push([...path]);
return;
}
if (visited.has(current)) return;
visited.add(current);
const edges = this.edges.get(current) || [];
for (const edge of edges) {
if (edge.type === 'depends_on' || edge.type === 'imports') {
const nextNode = this.nodes.get(edge.to);
if (nextNode) {
dfs(edge.to, [...path, nextNode]);
}
}
}
visited.delete(current);
};
const startNode = this.nodes.get(fromId);
if (startNode) {
dfs(fromId, [startNode]);
}
return paths;
}
/**
* Detect circular dependencies
*/
detectCycles(): string[][] {
const cycles: string[][] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
const dfs = (nodeId: string, path: string[]) => {
visited.add(nodeId);
recursionStack.add(nodeId);
const edges = this.edges.get(nodeId) || [];
for (const edge of edges) {
if (!visited.has(edge.to)) {
dfs(edge.to, [...path, edge.to]);
} else if (recursionStack.has(edge.to)) {
// Found a cycle
const cycleStart = path.indexOf(edge.to);
if (cycleStart !== -1) {
cycles.push(path.slice(cycleStart));
}
}
}
recursionStack.delete(nodeId);
};
for (const nodeId of this.nodes.keys()) {
if (!visited.has(nodeId)) {
dfs(nodeId, [nodeId]);
}
}
return cycles;
}
}
/**
* Document-based memory using embeddings and metadata
*/
export class DocumentMemory {
private documents: Map<string, {
content: string;
metadata: Record<string, any>;
embedding?: number[];
timestamp: Date;
}> = new Map();
addDocument(id: string, content: string, metadata: Record<string, any>): void {
this.documents.set(id, {
content,
metadata,
timestamp: new Date(),
});
}
searchByMetadata(query: Record<string, any>): Array<[string, any]> {
const results: Array<[string, any]> = [];
for (const [id, doc] of this.documents) {
let matches = true;
for (const [key, value] of Object.entries(query)) {
if (doc.metadata[key] !== value) {
matches = false;
break;
}
}
if (matches) {
results.push([id, doc]);
}
}
return results;
}
}
/**
* Time-series memory for tracking code evolution
*/
export class TimeSeriesMemory {
private events: Array<{
timestamp: Date;
type: string;
data: any;
tags: string[];
}> = [];
addEvent(type: string, data: any, tags: string[] = []): void {
this.events.push({
timestamp: new Date(),
type,
data,
tags,
});
}
queryByTimeRange(start: Date, end: Date, type?: string): any[] {
return this.events.filter(event => {
const inRange = event.timestamp >= start && event.timestamp <= end;
const matchesType = !type || event.type === type;
return inRange && matchesType;
});
}
getEventPatterns(windowSize: number = 3600000): Record<string, number> {
const patterns: Record<string, number> = {};
const now = Date.now();
for (const event of this.events) {
const age = now - event.timestamp.getTime();
if (age <= windowSize) {
patterns[event.type] = (patterns[event.type] || 0) + 1;
}
}
return patterns;
}
}
/**
* Unified memory interface combining all approaches
*/
export class HybridCodeMemory {
private graph: CodeGraphMemory;
private documents: DocumentMemory;
private timeSeries: TimeSeriesMemory;
constructor() {
this.graph = new CodeGraphMemory();
this.documents = new DocumentMemory();
this.timeSeries = new TimeSeriesMemory();
}
/**
* Record a code change with full context
*/
recordCodeChange(change: {
file: string;
type: 'create' | 'modify' | 'delete';
functions?: string[];
imports?: string[];
tests?: string[];
}): void {
// Add to graph
const nodeId = `file:${change.file}:${Date.now()}`;
this.graph.addNode({
id: nodeId,
type: 'file',
name: change.file,
properties: { changeType: change.type },
timestamp: new Date(),
});
// Create relationships
if (change.imports) {
for (const imp of change.imports) {
this.graph.addEdge({
from: nodeId,
to: `import:${imp}`,
type: 'imports',
timestamp: new Date(),
});
}
}
// Add to time series
this.timeSeries.addEvent('code_change', change, [change.type, change.file]);
// Add to documents
this.documents.addDocument(nodeId, JSON.stringify(change), {
file: change.file,
type: change.type,
timestamp: new Date(),
});
}
/**
* Query memory with context awareness
*/
query(query: MemoryQuery): any {
switch (query.type) {
case 'graph':
// Use graph for relationship queries
return this.graph.getConnectedNodes(query.query, undefined, query.depth);
case 'temporal':
// Use time series for time-based queries
if (query.timeRange) {
return this.timeSeries.queryByTimeRange(
query.timeRange.start,
query.timeRange.end
);
}
break;
case 'semantic':
// Use document store for content queries
return this.documents.searchByMetadata({ type: query.query });
}
}
/**
* Get coding patterns and insights
*/
getInsights(): {
recentPatterns: Record<string, number>;
dependencies: string[][];
hotspots: string[];
} {
return {
recentPatterns: this.timeSeries.getEventPatterns(),
dependencies: this.graph.detectCycles(),
hotspots: this.findHotspots(),
};
}
private findHotspots(): string[] {
const changeFrequency: Record<string, number> = {};
const recentEvents = this.timeSeries.queryByTimeRange(
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Last week
new Date()
);
for (const event of recentEvents) {
if (event.data.file) {
changeFrequency[event.data.file] = (changeFrequency[event.data.file] || 0) + 1;
}
}
return Object.entries(changeFrequency)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([file]) => file);
}
}