@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
290 lines • 9.02 kB
JavaScript
/**
* Graph-based memory system for code intelligence
* Explores different database approaches for agent memory
*/
/**
* In-memory graph database for code relationships
* Lightweight alternative to Neo4j for local use
*/
export class CodeGraphMemory {
nodes = new Map();
edges = new Map();
temporalIndex = new Map(); // timestamp -> node IDs
/**
* Add a node to the graph
*/
addNode(node) {
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) {
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, edgeType, depth = 1) {
const visited = new Set();
const result = [];
const traverse = (id, currentDepth) => {
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, end) {
const startTime = start.getTime();
const endTime = end.getTime();
const result = [];
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, toId) {
const paths = [];
const visited = new Set();
const dfs = (current, path) => {
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() {
const cycles = [];
const visited = new Set();
const recursionStack = new Set();
const dfs = (nodeId, path) => {
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 {
documents = new Map();
addDocument(id, content, metadata) {
this.documents.set(id, {
content,
metadata,
timestamp: new Date(),
});
}
searchByMetadata(query) {
const results = [];
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 {
events = [];
addEvent(type, data, tags = []) {
this.events.push({
timestamp: new Date(),
type,
data,
tags,
});
}
queryByTimeRange(start, end, type) {
return this.events.filter(event => {
const inRange = event.timestamp >= start && event.timestamp <= end;
const matchesType = !type || event.type === type;
return inRange && matchesType;
});
}
getEventPatterns(windowSize = 3600000) {
const patterns = {};
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 {
graph;
documents;
timeSeries;
constructor() {
this.graph = new CodeGraphMemory();
this.documents = new DocumentMemory();
this.timeSeries = new TimeSeriesMemory();
}
/**
* Record a code change with full context
*/
recordCodeChange(change) {
// 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) {
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() {
return {
recentPatterns: this.timeSeries.getEventPatterns(),
dependencies: this.graph.detectCycles(),
hotspots: this.findHotspots(),
};
}
findHotspots() {
const changeFrequency = {};
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);
}
}
//# sourceMappingURL=graph-memory.js.map