mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(graph-explorer): implement interactive code relationship visualization with Cytoscape.js
- Added main render function to initialize the graph explorer view. - Implemented data loading functions for graph nodes, edges, and search process data. - Created UI layout with tabs for graph view and search process view. - Developed filtering options for nodes and edges with corresponding UI elements. - Integrated Cytoscape.js for graph visualization, including node and edge styling. - Added functionality for node selection and details display. - Implemented impact analysis feature with modal display for results. - Included utility functions for refreshing data and managing UI states.
This commit is contained in:
166
ccw/src/core/routes/graph-routes.md
Normal file
166
ccw/src/core/routes/graph-routes.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Graph Routes API Documentation
|
||||
|
||||
## Overview
|
||||
The Graph Routes module provides REST API endpoints for querying and visualizing code structure data from CodexLens indices. It exposes symbols and their relationships as graph nodes and edges.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /api/graph/nodes
|
||||
Query all symbols from the CodexLens SQLite database and return them as graph nodes.
|
||||
|
||||
**Query Parameters:**
|
||||
- `path` (optional): Project path. Defaults to server's initial path.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "src/file.ts:functionName:10",
|
||||
"name": "functionName",
|
||||
"type": "FUNCTION",
|
||||
"file": "src/file.ts",
|
||||
"line": 10,
|
||||
"docstring": "function_type",
|
||||
"tokenCount": 45
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Node Types:**
|
||||
- `FUNCTION`: Functions and procedures
|
||||
- `CLASS`: Classes, interfaces, and type definitions
|
||||
- `METHOD`: Class methods
|
||||
- `VARIABLE`: Variables and constants
|
||||
- `MODULE`: Module-level definitions
|
||||
|
||||
---
|
||||
|
||||
### GET /api/graph/edges
|
||||
Query all code relationships from the CodexLens SQLite database and return them as graph edges.
|
||||
|
||||
**Query Parameters:**
|
||||
- `path` (optional): Project path. Defaults to server's initial path.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"edges": [
|
||||
{
|
||||
"source": "src/file.ts:caller:10",
|
||||
"target": "module.callee",
|
||||
"type": "CALLS",
|
||||
"sourceLine": 10,
|
||||
"sourceFile": "src/file.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Edge Types:**
|
||||
- `CALLS`: Function/method invocations
|
||||
- `IMPORTS`: Import/require relationships
|
||||
- `INHERITS`: Class inheritance
|
||||
|
||||
---
|
||||
|
||||
### GET /api/graph/impact
|
||||
Get impact analysis for a symbol - determines what code is affected if this symbol changes.
|
||||
|
||||
**Query Parameters:**
|
||||
- `path` (optional): Project path. Defaults to server's initial path.
|
||||
- `symbol` (required): Symbol ID in format `file:name:line`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"directDependents": [
|
||||
"src/other.ts:caller:20",
|
||||
"src/another.ts:user:35"
|
||||
],
|
||||
"affectedFiles": [
|
||||
"src/other.ts",
|
||||
"src/another.ts"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### PathMapper
|
||||
Maps source code paths to CodexLens index database paths following the storage structure:
|
||||
- Windows: `D:\path\to\project` → `~/.codexlens/indexes/D/path/to/project/_index.db`
|
||||
- Unix: `/home/user/project` → `~/.codexlens/indexes/home/user/project/_index.db`
|
||||
|
||||
### Database Schema
|
||||
Queries two main tables:
|
||||
1. **symbols** - Code symbol definitions
|
||||
- `id`, `file_id`, `name`, `kind`, `start_line`, `end_line`, `token_count`, `symbol_type`
|
||||
2. **code_relationships** - Inter-symbol dependencies
|
||||
- `id`, `source_symbol_id`, `target_qualified_name`, `relationship_type`, `source_line`, `target_file`
|
||||
|
||||
### Error Handling
|
||||
- Returns empty arrays (`[]`) if index database doesn't exist
|
||||
- Returns 500 with error message on database query failures
|
||||
- Returns 400 if required parameters are missing
|
||||
|
||||
### Type Mappings
|
||||
|
||||
**Symbol Kinds → Node Types:**
|
||||
```typescript
|
||||
{
|
||||
'function' → 'FUNCTION',
|
||||
'class' → 'CLASS',
|
||||
'method' → 'METHOD',
|
||||
'variable' → 'VARIABLE',
|
||||
'module' → 'MODULE',
|
||||
'interface' → 'CLASS',
|
||||
'type' → 'CLASS'
|
||||
}
|
||||
```
|
||||
|
||||
**Relationship Types → Edge Types:**
|
||||
```typescript
|
||||
{
|
||||
'call' → 'CALLS',
|
||||
'import' → 'IMPORTS',
|
||||
'inherits' → 'INHERITS',
|
||||
'uses' → 'CALLS'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Fetch All Nodes
|
||||
```bash
|
||||
curl "http://localhost:3000/api/graph/nodes?path=/path/to/project"
|
||||
```
|
||||
|
||||
### Fetch All Edges
|
||||
```bash
|
||||
curl "http://localhost:3000/api/graph/edges?path=/path/to/project"
|
||||
```
|
||||
|
||||
### Analyze Impact
|
||||
```bash
|
||||
curl "http://localhost:3000/api/graph/impact?path=/path/to/project&symbol=src/file.ts:functionName:10"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with CodexLens
|
||||
|
||||
This module requires:
|
||||
1. CodexLens to be installed and initialized (`/api/codexlens/init`)
|
||||
2. Project to be indexed (creates `_index.db` files)
|
||||
3. better-sqlite3 package for direct database access
|
||||
|
||||
## Dependencies
|
||||
- `better-sqlite3`: Direct SQLite database access
|
||||
- `codex-lens`: Python package providing the indexing infrastructure
|
||||
- `path`, `fs`, `os`: Node.js built-in modules for file system operations
|
||||
315
ccw/src/core/routes/graph-routes.ts
Normal file
315
ccw/src/core/routes/graph-routes.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Graph Routes Module
|
||||
* Handles graph visualization API endpoints for codex-lens data
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { executeCodexLens } from '../../tools/codex-lens.js';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
initialPath: string;
|
||||
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
||||
broadcastToClients: (data: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* PathMapper utility class (simplified from codex-lens Python implementation)
|
||||
* Maps source paths to index database paths
|
||||
*/
|
||||
class PathMapper {
|
||||
private indexRoot: string;
|
||||
|
||||
constructor(indexRoot?: string) {
|
||||
this.indexRoot = indexRoot || join(homedir(), '.codexlens', 'indexes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path to cross-platform storage format
|
||||
* Windows: D:\path\to\dir → D/path/to/dir
|
||||
* Unix: /home/user/path → home/user/path
|
||||
*/
|
||||
normalizePath(sourcePath: string): string {
|
||||
const resolved = sourcePath.replace(/\\/g, '/');
|
||||
|
||||
// Handle Windows paths with drive letters
|
||||
if (process.platform === 'win32' && /^[A-Za-z]:/.test(resolved)) {
|
||||
const drive = resolved[0]; // D
|
||||
const rest = resolved.slice(2); // /path/to/dir
|
||||
return `${drive}${rest}`.replace(/^\//, '');
|
||||
}
|
||||
|
||||
// Handle Unix paths - remove leading slash
|
||||
return resolved.replace(/^\//, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert source path to index database path
|
||||
*/
|
||||
sourceToIndexDb(sourcePath: string): string {
|
||||
const normalized = this.normalizePath(sourcePath);
|
||||
return join(this.indexRoot, normalized, '_index.db');
|
||||
}
|
||||
}
|
||||
|
||||
interface GraphNode {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
file: string;
|
||||
line: number;
|
||||
docstring?: string;
|
||||
tokenCount?: number;
|
||||
}
|
||||
|
||||
interface GraphEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
sourceLine: number;
|
||||
sourceFile: string;
|
||||
}
|
||||
|
||||
interface ImpactAnalysis {
|
||||
directDependents: string[];
|
||||
affectedFiles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map codex-lens symbol kinds to graph node types
|
||||
*/
|
||||
function mapSymbolKind(kind: string): string {
|
||||
const kindMap: Record<string, string> = {
|
||||
'function': 'FUNCTION',
|
||||
'class': 'CLASS',
|
||||
'method': 'METHOD',
|
||||
'variable': 'VARIABLE',
|
||||
'module': 'MODULE',
|
||||
'interface': 'CLASS', // TypeScript interfaces as CLASS
|
||||
'type': 'CLASS', // Type aliases as CLASS
|
||||
};
|
||||
return kindMap[kind.toLowerCase()] || 'VARIABLE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Map codex-lens relationship types to graph edge types
|
||||
*/
|
||||
function mapRelationType(relType: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
'call': 'CALLS',
|
||||
'import': 'IMPORTS',
|
||||
'inherits': 'INHERITS',
|
||||
'uses': 'CALLS', // Fallback uses → CALLS
|
||||
};
|
||||
return typeMap[relType.toLowerCase()] || 'CALLS';
|
||||
}
|
||||
|
||||
/**
|
||||
* Query symbols from codex-lens database
|
||||
*/
|
||||
async function querySymbols(projectPath: string): Promise<GraphNode[]> {
|
||||
const mapper = new PathMapper();
|
||||
const dbPath = mapper.sourceToIndexDb(projectPath);
|
||||
|
||||
if (!existsSync(dbPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const db = Database(dbPath, { readonly: true });
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
s.kind,
|
||||
s.start_line,
|
||||
s.token_count,
|
||||
s.symbol_type,
|
||||
f.path as file
|
||||
FROM symbols s
|
||||
JOIN files f ON s.file_id = f.id
|
||||
ORDER BY f.path, s.start_line
|
||||
`).all();
|
||||
|
||||
db.close();
|
||||
|
||||
return rows.map((row: any) => ({
|
||||
id: `${row.file}:${row.name}:${row.start_line}`,
|
||||
name: row.name,
|
||||
type: mapSymbolKind(row.kind),
|
||||
file: row.file,
|
||||
line: row.start_line,
|
||||
docstring: row.symbol_type || undefined,
|
||||
tokenCount: row.token_count || undefined,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(`[Graph] Failed to query symbols: ${err.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query code relationships from codex-lens database
|
||||
*/
|
||||
async function queryRelationships(projectPath: string): Promise<GraphEdge[]> {
|
||||
const mapper = new PathMapper();
|
||||
const dbPath = mapper.sourceToIndexDb(projectPath);
|
||||
|
||||
if (!existsSync(dbPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const db = Database(dbPath, { readonly: true });
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT
|
||||
s.name as source_name,
|
||||
s.start_line as source_line,
|
||||
f.path as source_file,
|
||||
r.target_qualified_name,
|
||||
r.relationship_type,
|
||||
r.target_file
|
||||
FROM code_relationships r
|
||||
JOIN symbols s ON r.source_symbol_id = s.id
|
||||
JOIN files f ON s.file_id = f.id
|
||||
ORDER BY f.path, s.start_line
|
||||
`).all();
|
||||
|
||||
db.close();
|
||||
|
||||
return rows.map((row: any) => ({
|
||||
source: `${row.source_file}:${row.source_name}:${row.source_line}`,
|
||||
target: row.target_qualified_name,
|
||||
type: mapRelationType(row.relationship_type),
|
||||
sourceLine: row.source_line,
|
||||
sourceFile: row.source_file,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(`[Graph] Failed to query relationships: ${err.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform impact analysis for a symbol
|
||||
* Find all symbols that depend on this symbol (direct and transitive)
|
||||
*/
|
||||
async function analyzeImpact(projectPath: string, symbolId: string): Promise<ImpactAnalysis> {
|
||||
const mapper = new PathMapper();
|
||||
const dbPath = mapper.sourceToIndexDb(projectPath);
|
||||
|
||||
if (!existsSync(dbPath)) {
|
||||
return { directDependents: [], affectedFiles: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const db = Database(dbPath, { readonly: true });
|
||||
|
||||
// Parse symbolId to extract symbol name
|
||||
const parts = symbolId.split(':');
|
||||
const symbolName = parts.length >= 2 ? parts[1] : symbolId;
|
||||
|
||||
// Find all symbols that reference this symbol
|
||||
const rows = db.prepare(`
|
||||
SELECT DISTINCT
|
||||
s.name as dependent_name,
|
||||
f.path as dependent_file,
|
||||
s.start_line as dependent_line
|
||||
FROM code_relationships r
|
||||
JOIN symbols s ON r.source_symbol_id = s.id
|
||||
JOIN files f ON s.file_id = f.id
|
||||
WHERE r.target_qualified_name LIKE ?
|
||||
`).all(`%${symbolName}%`);
|
||||
|
||||
db.close();
|
||||
|
||||
const directDependents = rows.map((row: any) =>
|
||||
`${row.dependent_file}:${row.dependent_name}:${row.dependent_line}`
|
||||
);
|
||||
|
||||
const affectedFiles = [...new Set(rows.map((row: any) => row.dependent_file))];
|
||||
|
||||
return {
|
||||
directDependents,
|
||||
affectedFiles,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`[Graph] Failed to analyze impact: ${err.message}`);
|
||||
return { directDependents: [], affectedFiles: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Graph routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath } = ctx;
|
||||
|
||||
// API: Graph Nodes - Get all symbols as graph nodes
|
||||
if (pathname === '/api/graph/nodes') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const nodes = await querySymbols(projectPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ nodes }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message, nodes: [] }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Graph Edges - Get all relationships as graph edges
|
||||
if (pathname === '/api/graph/edges') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const edges = await queryRelationships(projectPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ edges }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message, edges: [] }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Impact Analysis - Get impact analysis for a symbol
|
||||
if (pathname === '/api/graph/impact') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const symbolId = url.searchParams.get('symbol');
|
||||
|
||||
if (!symbolId) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'symbol parameter is required' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const impact = await analyzeImpact(projectPath, symbolId);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(impact));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
error: err.message,
|
||||
directDependents: [],
|
||||
affectedFiles: []
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { handleMemoryRoutes } from './routes/memory-routes.js';
|
||||
import { handleMcpRoutes } from './routes/mcp-routes.js';
|
||||
import { handleHooksRoutes } from './routes/hooks-routes.js';
|
||||
import { handleCodexLensRoutes } from './routes/codexlens-routes.js';
|
||||
import { handleGraphRoutes } from './routes/graph-routes.js';
|
||||
import { handleSystemRoutes } from './routes/system-routes.js';
|
||||
import { handleFilesRoutes } from './routes/files-routes.js';
|
||||
import { handleSkillsRoutes } from './routes/skills-routes.js';
|
||||
@@ -270,6 +271,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
if (await handleCodexLensRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Graph routes (/api/graph/*)
|
||||
if (pathname.startsWith('/api/graph/')) {
|
||||
if (await handleGraphRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// CCW routes (/api/ccw/*)
|
||||
if (pathname.startsWith('/api/ccw/')) {
|
||||
if (await handleCcwRoutes(routeContext)) return;
|
||||
|
||||
Reference in New Issue
Block a user