claude-code-graph
Version:
Claude Code with live structural graphs for large codebases
289 lines (254 loc) • 9.02 kB
JavaScript
/**
* TodoGraphBridge - Intercepts and analyzes todo interactions to map them to graph nodes
*/
import { readFile, writeFile } from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';
export class TodoGraphBridge {
constructor(rootPath = '.') {
this.rootPath = rootPath;
this.graphPath = path.join(rootPath, '.graph');
this.todoLogPath = path.join(this.graphPath, 'todo-log.json');
this.todoMappingPath = path.join(this.graphPath, 'todo-mapping.json');
this.superGraphPath = path.join(this.graphPath, 'supergraph.json');
// Initialize todo tracking
this.todoLog = [];
this.todoMappings = new Map();
this.loadExistingData();
}
async loadExistingData() {
try {
if (existsSync(this.todoLogPath)) {
const data = await readFile(this.todoLogPath, 'utf8');
this.todoLog = JSON.parse(data);
}
if (existsSync(this.todoMappingPath)) {
const data = await readFile(this.todoMappingPath, 'utf8');
const mappings = JSON.parse(data);
this.todoMappings = new Map(Object.entries(mappings));
}
} catch (error) {
console.warn('Failed to load existing todo data:', error.message);
}
}
/**
* Intercept and analyze a todo interaction
*/
async interceptTodoCall(callType, todos, timestamp = new Date().toISOString()) {
const logEntry = {
timestamp,
callType, // 'TodoRead' or 'TodoWrite'
todos: Array.isArray(todos) ? todos : [],
mappings: {}
};
// Analyze each todo for file/graph references
if (Array.isArray(todos)) {
for (const todo of todos) {
const analysis = await this.analyzeTodoContent(todo);
if (analysis.graphReferences.length > 0) {
logEntry.mappings[todo.id] = analysis;
}
}
}
this.todoLog.push(logEntry);
await this.persistData();
console.log(`📝 Todo Bridge: Captured ${callType} with ${Object.keys(logEntry.mappings).length} graph mappings`);
return logEntry;
}
/**
* Analyze todo content for file paths, cluster references, and graph nodes
*/
async analyzeTodoContent(todo) {
const content = todo.content || '';
const analysis = {
todoId: todo.id,
status: todo.status,
priority: todo.priority,
graphReferences: [],
fileReferences: [],
clusterReferences: [],
confidence: 0
};
// Extract file paths with improved patterns and deduplication
const fileSet = new Set();
// Pattern 1: Explicit file extensions
const fileExtPattern = /(?:^|\s)([a-zA-Z0-9_\-\/\.]+\.[a-zA-Z]{2,4})(?=\s|$|:|,|;)/g;
let match;
while ((match = fileExtPattern.exec(content)) !== null) {
const filePath = match[1];
if (filePath.length > 3 && !filePath.startsWith('.') && !filePath.endsWith('.')) {
fileSet.add(filePath);
analysis.confidence += 0.4;
}
}
// Pattern 2: Directory paths (only if they look like code paths)
const dirPattern = /(?:^|\s)((?:src|lib|tests?|bin|tools?|graph)\/[a-zA-Z0-9_\-\/]+)(?=\s|$)/g;
while ((match = dirPattern.exec(content)) !== null) {
const dirPath = match[1];
if (dirPath.length > 4) {
fileSet.add(dirPath);
analysis.confidence += 0.3;
}
}
// Pattern 3: Relative paths with ./
const relativePattern = /(?:\.\/)([a-zA-Z0-9_\-\/\.]+)/g;
while ((match = relativePattern.exec(content)) !== null) {
const relPath = `./${match[1]}`;
if (relPath.length > 3) {
fileSet.add(relPath);
analysis.confidence += 0.3;
}
}
// Pattern 4: Context keywords + file paths
const contextPattern = /(?:in|file|from|update|fix|modify)\s+([a-zA-Z0-9_\-\/\.]+(?:\.[a-zA-Z]{2,4})?)/gi;
while ((match = contextPattern.exec(content)) !== null) {
const contextFile = match[1];
if (contextFile.length > 3 && (contextFile.includes('/') || contextFile.includes('.'))) {
fileSet.add(contextFile);
analysis.confidence += 0.5;
}
}
analysis.fileReferences = Array.from(fileSet);
// Extract cluster references (c0, c1, etc.) - more precise
const clusterPattern = /\bcluster\s+c(\d+)\b|\bc(\d+)\s+cluster\b|\bc(\d+)\b/gi;
const clusterSet = new Set();
while ((match = clusterPattern.exec(content)) !== null) {
const clusterId = `c${match[1] || match[2] || match[3]}`;
clusterSet.add(clusterId);
analysis.confidence += 0.4;
}
analysis.clusterReferences = Array.from(clusterSet);
// Map to actual graph nodes
await this.mapToGraphNodes(analysis);
return analysis;
}
/**
* Map file/cluster references to actual graph nodes
*/
async mapToGraphNodes(analysis) {
try {
// Load super-graph for cluster mapping
if (existsSync(this.superGraphPath)) {
const superGraph = JSON.parse(await readFile(this.superGraphPath, 'utf8'));
// Map cluster references
for (const clusterId of analysis.clusterReferences) {
if (superGraph.clusters[clusterId]) {
analysis.graphReferences.push({
type: 'cluster',
id: clusterId,
files: superGraph.clusters[clusterId].fileList || [],
confidence: 0.8
});
}
}
// Map file references to clusters
for (const filePath of analysis.fileReferences) {
const normalizedPath = this.normalizePath(filePath);
// Find which cluster contains this file
for (const [clusterId, cluster] of Object.entries(superGraph.clusters)) {
if (cluster.fileList && cluster.fileList.some(f =>
this.normalizePath(f).includes(normalizedPath) ||
normalizedPath.includes(this.normalizePath(f))
)) {
analysis.graphReferences.push({
type: 'file_in_cluster',
clusterId,
filePath: normalizedPath,
confidence: 0.6
});
break;
}
}
}
}
} catch (error) {
console.warn('Failed to map to graph nodes:', error.message);
}
}
/**
* Normalize file paths for better matching
*/
normalizePath(filePath) {
return filePath
.replace(/^\.\//, '')
.replace(/^src\//, '')
.replace(/\\/g, '/')
.toLowerCase();
}
/**
* Get todo-graph mappings for a specific todo or all todos
*/
getTodoMappings(todoId = null) {
if (todoId) {
const relevant = this.todoLog
.filter(entry => entry.mappings[todoId])
.map(entry => entry.mappings[todoId]);
return relevant.length > 0 ? relevant[relevant.length - 1] : null;
}
// Return all current mappings
const currentMappings = {};
for (const entry of this.todoLog.slice(-10)) { // Last 10 entries
Object.assign(currentMappings, entry.mappings);
}
return currentMappings;
}
/**
* Get todos affecting a specific cluster or file
*/
getTodosForGraphNode(nodeId) {
const affectingTodos = [];
for (const entry of this.todoLog) {
for (const [todoId, mapping] of Object.entries(entry.mappings)) {
for (const ref of mapping.graphReferences) {
if (ref.id === nodeId || ref.clusterId === nodeId ||
(ref.filePath && ref.filePath.includes(nodeId))) {
affectingTodos.push({
todoId,
mapping,
timestamp: entry.timestamp,
status: mapping.status
});
}
}
}
}
return affectingTodos;
}
/**
* Persist todo data to disk
*/
async persistData() {
try {
await writeFile(this.todoLogPath, JSON.stringify(this.todoLog, null, 2));
const mappingsObj = Object.fromEntries(this.todoMappings);
await writeFile(this.todoMappingPath, JSON.stringify(mappingsObj, null, 2));
} catch (error) {
console.warn('Failed to persist todo data:', error.message);
}
}
/**
* Generate a report of todo-graph relationships
*/
generateReport() {
const mappings = this.getTodoMappings();
const clusters = {};
const files = {};
for (const [todoId, mapping] of Object.entries(mappings)) {
for (const ref of mapping.graphReferences) {
if (ref.type === 'cluster') {
if (!clusters[ref.id]) clusters[ref.id] = [];
clusters[ref.id].push({ todoId, status: mapping.status, priority: mapping.priority });
} else if (ref.type === 'file_in_cluster') {
if (!files[ref.filePath]) files[ref.filePath] = [];
files[ref.filePath].push({ todoId, status: mapping.status, priority: mapping.priority });
}
}
}
return {
clusterTodos: clusters,
fileTodos: files,
totalMappings: Object.keys(mappings).length,
timestamp: new Date().toISOString()
};
}
}