feat: add semantic graph design for static code analysis

- Introduced a comprehensive design document for a Code Semantic Graph aimed at enhancing static analysis capabilities.
- Defined the architecture, core components, and implementation steps for analyzing function calls, data flow, and dependencies.
- Included detailed specifications for nodes and edges in the graph, along with database schema for storage.
- Outlined phases for implementation, technical challenges, success metrics, and application scenarios.
This commit is contained in:
catlog22
2025-12-15 09:47:18 +08:00
parent d91477ad80
commit 3ffb907a6f
17 changed files with 4557 additions and 261 deletions

View File

@@ -77,7 +77,7 @@ function getMcpServersFromFile(filePath) {
*/
function addMcpServerToMcpJson(projectPath, serverName, serverConfig) {
try {
const normalizedPath = normalizeProjectPathForConfig(projectPath);
const normalizedPath = normalizePathForFileSystem(projectPath);
const mcpJsonPath = join(normalizedPath, '.mcp.json');
// Read existing .mcp.json or create new structure
@@ -115,7 +115,7 @@ function addMcpServerToMcpJson(projectPath, serverName, serverConfig) {
*/
function removeMcpServerFromMcpJson(projectPath, serverName) {
try {
const normalizedPath = normalizeProjectPathForConfig(projectPath);
const normalizedPath = normalizePathForFileSystem(projectPath);
const mcpJsonPath = join(normalizedPath, '.mcp.json');
if (!existsSync(mcpJsonPath)) {
@@ -238,22 +238,43 @@ function getMcpConfig() {
}
/**
* Normalize project path for .claude.json (Windows backslash format)
* Normalize path to filesystem format (for accessing .mcp.json files)
* Always uses forward slashes for cross-platform compatibility
* @param {string} path
* @returns {string}
*/
function normalizeProjectPathForConfig(path) {
// Convert forward slashes to backslashes for Windows .claude.json format
let normalized = path.replace(/\//g, '\\');
// Handle /d/path format -> D:\path
if (normalized.match(/^\\[a-zA-Z]\\/)) {
function normalizePathForFileSystem(path) {
let normalized = path.replace(/\\/g, '/');
// Handle /d/path format -> D:/path
if (normalized.match(/^\/[a-zA-Z]\//)) {
normalized = normalized.charAt(1).toUpperCase() + ':' + normalized.slice(2);
}
return normalized;
}
/**
* Normalize project path to match existing format in .claude.json
* Checks both forward slash and backslash formats to find existing entry
* @param {string} path
* @param {Object} claudeConfig - Optional existing config to check format
* @returns {string}
*/
function normalizeProjectPathForConfig(path, claudeConfig = null) {
// IMPORTANT: Always normalize to forward slashes to prevent duplicate entries
// (e.g., prevents both "D:/Claude_dms3" and "D:\\Claude_dms3")
let normalizedForward = path.replace(/\\/g, '/');
// Handle /d/path format -> D:/path
if (normalizedForward.match(/^\/[a-zA-Z]\//)) {
normalizedForward = normalizedForward.charAt(1).toUpperCase() + ':' + normalizedForward.slice(2);
}
// ALWAYS return forward slash format to prevent duplicates
return normalizedForward;
}
/**
* Toggle MCP server enabled/disabled
* @param {string} projectPath
@@ -270,7 +291,7 @@ function toggleMcpServerEnabled(projectPath, serverName, enable) {
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
const config = JSON.parse(content);
const normalizedPath = normalizeProjectPathForConfig(projectPath);
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
if (!config.projects || !config.projects[normalizedPath]) {
return { error: `Project not found: ${normalizedPath}` };
@@ -332,7 +353,7 @@ function addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyC
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
const config = JSON.parse(content);
const normalizedPath = normalizeProjectPathForConfig(projectPath);
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
// Create project entry if it doesn't exist
if (!config.projects) {
@@ -387,8 +408,8 @@ function addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyC
*/
function removeMcpServerFromProject(projectPath, serverName) {
try {
const normalizedPath = normalizeProjectPathForConfig(projectPath);
const mcpJsonPath = join(normalizedPath, '.mcp.json');
const normalizedPathForFile = normalizePathForFileSystem(projectPath);
const mcpJsonPath = join(normalizedPathForFile, '.mcp.json');
let removedFromMcpJson = false;
let removedFromClaudeJson = false;
@@ -409,6 +430,9 @@ function removeMcpServerFromProject(projectPath, serverName) {
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
const config = JSON.parse(content);
// Get normalized path that matches existing config format
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
if (config.projects && config.projects[normalizedPath]) {
const projectConfig = config.projects[normalizedPath];
@@ -597,11 +621,13 @@ export async function handleMcpRoutes(ctx: RouteContext): Promise<boolean> {
// API: Copy MCP server to project
if (pathname === '/api/mcp-copy-server' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { projectPath, serverName, serverConfig } = body;
const { projectPath, serverName, serverConfig, configType } = body;
if (!projectPath || !serverName || !serverConfig) {
return { error: 'projectPath, serverName, and serverConfig are required', status: 400 };
}
return addMcpServerToProject(projectPath, serverName, serverConfig);
// configType: 'mcp' = use .mcp.json (default), 'claude' = use .claude.json
const useLegacyConfig = configType === 'claude';
return addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyConfig);
});
return true;
}

View File

@@ -733,7 +733,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
}
try {
const configPath = join(projectPath, '.claude', 'rules', 'active_memory.md');
const configPath = join(projectPath, '.claude', 'CLAUDE.md');
const configJsonPath = join(projectPath, '.claude', 'active_memory_config.json');
const enabled = existsSync(configPath);
let lastSync: string | null = null;
@@ -784,16 +784,12 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
return;
}
const rulesDir = join(projectPath, '.claude', 'rules');
const claudeDir = join(projectPath, '.claude');
const configPath = join(rulesDir, 'active_memory.md');
const configPath = join(claudeDir, 'CLAUDE.md');
const configJsonPath = join(claudeDir, 'active_memory_config.json');
if (enabled) {
// Enable: Create directories and initial file
if (!existsSync(rulesDir)) {
mkdirSync(rulesDir, { recursive: true });
}
if (!existsSync(claudeDir)) {
mkdirSync(claudeDir, { recursive: true });
}
@@ -803,8 +799,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
writeFileSync(configJsonPath, JSON.stringify(config, null, 2), 'utf-8');
}
// Create initial active_memory.md with header
const initialContent = `# Active Memory
// Create initial CLAUDE.md with header
const initialContent = `# CLAUDE.md - Project Memory
> Auto-generated understanding of frequently accessed files.
> Last updated: ${new Date().toISOString()}
@@ -867,7 +863,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
return true;
}
// API: Active Memory - Sync (analyze hot files using CLI and update active_memory.md)
// API: Active Memory - Sync (analyze hot files using CLI and update CLAUDE.md)
if (pathname === '/api/memory/active/sync' && req.method === 'POST') {
let body = '';
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
@@ -882,8 +878,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
return;
}
const rulesDir = join(projectPath, '.claude', 'rules');
const configPath = join(rulesDir, 'active_memory.md');
const claudeDir = join(projectPath, '.claude');
const configPath = join(claudeDir, 'CLAUDE.md');
// Get hot files from memory store - with fallback
let hotFiles: any[] = [];
@@ -903,8 +899,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
return isAbsolute(filePath) ? filePath : join(projectPath, filePath);
}).filter((p: string) => existsSync(p));
// Build the active memory content header
let content = `# Active Memory
// Build the CLAUDE.md content header
let content = `# CLAUDE.md - Project Memory
> Auto-generated understanding of frequently accessed files using ${tool.toUpperCase()}.
> Last updated: ${new Date().toISOString()}
@@ -942,14 +938,29 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
});
if (result.success && result.execution?.output) {
// Extract stdout from output object
cliOutput = typeof result.execution.output === 'string'
? result.execution.output
: result.execution.output.stdout || '';
// Extract stdout from output object with proper serialization
const output = result.execution.output;
if (typeof output === 'string') {
cliOutput = output;
} else if (output && typeof output === 'object') {
// Handle object output - extract stdout or serialize the object
if (output.stdout && typeof output.stdout === 'string') {
cliOutput = output.stdout;
} else if (output.stderr && typeof output.stderr === 'string') {
cliOutput = output.stderr;
} else {
// Last resort: serialize the entire object as JSON
cliOutput = JSON.stringify(output, null, 2);
}
} else {
cliOutput = '';
}
}
// Add CLI output to content
content += cliOutput + '\n\n---\n\n';
// Add CLI output to content (only if not empty)
if (cliOutput && cliOutput.trim()) {
content += cliOutput + '\n\n---\n\n';
}
} catch (cliErr) {
// Fallback to basic analysis if CLI fails
@@ -1007,8 +1018,8 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
}
// Ensure directory exists
if (!existsSync(rulesDir)) {
mkdirSync(rulesDir, { recursive: true });
if (!existsSync(claudeDir)) {
mkdirSync(claudeDir, { recursive: true });
}
// Write the file