diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 0dd9b4893..0fffaf733 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -11,16 +11,18 @@ export type ImportMap = Map>; export const createImportMap = (): ImportMap => new Map(); -// Helper: Resolve relative paths (e.g. "../utils" -> "src/lib/utils.ts") +// Helper: Resolve import paths (relative and absolute/package-style) const resolveImportPath = ( currentFile: string, importPath: string, - allFiles: Set + allFiles: Set, + allFileList: string[], + resolveCache: Map ): string | null => { - // 1. Handle non-relative imports (libraries like 'react') - if (!importPath.startsWith('.')) return null; // We skip node_modules for now + const cacheKey = `${currentFile}::${importPath}`; + if (resolveCache.has(cacheKey)) return resolveCache.get(cacheKey) ?? null; - // 2. Resolve '..' and '.' + // 1. Resolve '..' and '.' for relative imports const currentDir = currentFile.split('/').slice(0, -1); const parts = importPath.split('/'); @@ -35,7 +37,7 @@ const resolveImportPath = ( const basePath = currentDir.join('/'); - // 3. Try extensions for all supported languages + // 2. Try extensions for all supported languages const extensions = [ '', // TypeScript/JavaScript @@ -54,11 +56,56 @@ const resolveImportPath = ( '.rs', '/mod.rs' ]; - for (const ext of extensions) { - const candidate = basePath + ext; - if (allFiles.has(candidate)) return candidate; + if (importPath.startsWith('.')) { + for (const ext of extensions) { + const candidate = basePath + ext; + if (allFiles.has(candidate)) { + resolveCache.set(cacheKey, candidate); + return candidate; + } + } + resolveCache.set(cacheKey, null); + return null; } + // 3. Handle absolute/package imports (Java, Go, Python, etc.) + if (importPath.endsWith('.*')) { + resolveCache.set(cacheKey, null); + return null; + } + + const pathLike = importPath.includes('/') + ? importPath + : importPath.replace(/\./g, '/'); + const pathParts = pathLike.split('/').filter(Boolean); + + // Normalize all file paths to forward slashes for matching + const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/')); + + for (let i = 0; i < pathParts.length; i++) { + const suffix = pathParts.slice(i).join('/'); + for (const ext of extensions) { + const suffixWithExt = suffix + ext; + // Require path separator before match to avoid false positives like "View.java" matching "RootView.java" + const suffixPattern = '/' + suffixWithExt; + const matchIdx = normalizedFileList.findIndex(filePath => + filePath.endsWith(suffixPattern) || filePath.toLowerCase().endsWith(suffixPattern.toLowerCase()) + ); + if (matchIdx !== -1) { + const match = allFileList[matchIdx]; + if (import.meta.env.DEV) { + console.log(`📦 Import resolved: ${importPath} → ${match}`); + } + resolveCache.set(cacheKey, match); + return match; + } + } + } + + if (import.meta.env.DEV && pathParts.length > 0) { + console.log(`⚠️ Import unresolved: ${importPath} (tried suffixes from ${pathParts.join('/')})`); + } + resolveCache.set(cacheKey, null); return null; }; @@ -72,6 +119,12 @@ export const processImports = async ( // Create a Set of all file paths for fast lookup during resolution const allFilePaths = new Set(files.map(f => f.path)); const parser = await loadParser(); + const resolveCache = new Map(); + const allFileList = files.map(f => f.path); + + // Track import statistics + let totalImportsFound = 0; + let totalImportsResolved = 0; for (let i = 0; i < files.length; i++) { const file = files[i]; @@ -102,6 +155,13 @@ export const processImports = async ( try { query = parser.getLanguage().query(queryStr); matches = query.matches(tree.rootNode); + + if (import.meta.env.DEV && language === 'java') { + const importMatches = matches.filter(m => m.captures.some(c => c.name === 'import')); + if (importMatches.length > 0) { + console.log(`📋 Java file ${file.path}: ${importMatches.length} import matches found`); + } + } } catch (queryError: any) { // Detailed debug logging for query failures console.group(`🔴 Query Error: ${file.path}`); @@ -123,13 +183,29 @@ export const processImports = async ( if (captureMap['import']) { const sourceNode = captureMap['import.source']; - if (!sourceNode) return; + if (!sourceNode) { + if (import.meta.env.DEV) { + console.log(`⚠️ Import captured but no source node in ${file.path}`); + } + return; + } // Clean path (remove quotes) const rawImportPath = sourceNode.text.replace(/['"]/g, ''); + totalImportsFound++; + + if (import.meta.env.DEV && language === 'java') { + console.log(`🔍 Java import found in ${file.path}: ${rawImportPath}`); + } // Resolve to actual file in the system - const resolvedPath = resolveImportPath(file.path, rawImportPath, allFilePaths); + const resolvedPath = resolveImportPath( + file.path, + rawImportPath, + allFilePaths, + allFileList, + resolveCache + ); if (resolvedPath) { // A. Update Graph (File -> IMPORTS -> File) @@ -137,6 +213,8 @@ export const processImports = async ( const targetId = generateId('File', resolvedPath); const relId = generateId('IMPORTS', `${file.path}->${resolvedPath}`); + totalImportsResolved++; + graph.addRelationship({ id: relId, sourceId, @@ -161,6 +239,10 @@ export const processImports = async ( tree.delete(); } } + + if (import.meta.env.DEV) { + console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`); + } }; diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index acdfb4f0c..8c276b312 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -123,6 +123,16 @@ export const runPipelineFromFiles = async ( stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount }, }); }); + + // Debug: Count IMPORTS relationships + if (import.meta.env.DEV) { + const importsCount = graph.relationships.filter(r => r.type === 'IMPORTS').length; + console.log(`📊 Pipeline: After import phase, graph has ${importsCount} IMPORTS relationships (total: ${graph.relationshipCount})`); + if (importsCount > 0) { + const sample = graph.relationships.filter(r => r.type === 'IMPORTS').slice(0, 3); + sample.forEach(r => console.log(` Sample IMPORTS: ${r.sourceId} → ${r.targetId}`)); + } + } // Phase 5: Calls (82-98%) diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 464ed1f86..a931b4a40 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -160,8 +160,8 @@ export const JAVA_QUERIES = ` (method_declaration name: (identifier) @name) @definition.method (constructor_declaration name: (identifier) @name) @definition.constructor -; Imports -(import_declaration (scoped_identifier) @import.source) @import +; Imports - capture any import declaration child as source +(import_declaration (_) @import.source) @import ; Calls (method_invocation name: (identifier) @call.name) @call diff --git a/gitnexus/src/core/llm/tools.ts b/gitnexus/src/core/llm/tools.ts index ca75c7f63..872914417 100644 --- a/gitnexus/src/core/llm/tools.ts +++ b/gitnexus/src/core/llm/tools.ts @@ -926,12 +926,40 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`, return `Could not find "${target}" in the codebase. Try using the search tool first to find the exact name.`; } - // Use the first match - const targetNode = targetResults[0]; + // Handle multiple matches - require disambiguation + const allPaths = targetResults.map((r: any) => Array.isArray(r) ? r[2] : r.filePath).filter(Boolean); + + // If multiple matches and target doesn't look like a specific path, ask for clarification + if (targetResults.length > 1 && !target.includes('/')) { + return `⚠️ AMBIGUOUS TARGET: Multiple files named "${target}" found:\n\n${allPaths.map((p: string, i: number) => `${i + 1}. ${p}`).join('\n')}\n\nPlease specify which file you mean by using a more specific path, e.g.:\n- impact("${allPaths[0].split('/').slice(-3).join('/')}")\n- impact("${allPaths[1]?.split('/').slice(-3).join('/') || allPaths[0]}")`; + } + + // If target contains a path, try to find matching file + let targetNode = targetResults[0]; + if (target.includes('/') && targetResults.length > 1) { + const exactMatch = targetResults.find((r: any) => { + const path = Array.isArray(r) ? r[2] : r.filePath; + return path && path.toLowerCase().includes(target.toLowerCase()); + }); + if (exactMatch) { + targetNode = exactMatch; + } else { + // Still ambiguous even with path + return `⚠️ AMBIGUOUS TARGET: Could not uniquely match "${target}". Found:\n\n${allPaths.map((p: string, i: number) => `${i + 1}. ${p}`).join('\n')}\n\nPlease use a more specific path.`; + } + } + const targetId = Array.isArray(targetNode) ? targetNode[0] : targetNode.id; const targetType = Array.isArray(targetNode) ? targetNode[1] : targetNode.nodeType; const targetFilePath = Array.isArray(targetNode) ? targetNode[2] : targetNode.filePath; + if (import.meta.env.DEV) { + console.log(`🎯 Impact: Found target "${target}" → id=${targetId}, type=${targetType}, filePath=${targetFilePath}`); + } + + // No more multipleMatchWarning needed - we either disambiguated or returned early + const multipleMatchWarning = ''; + // For File targets, find what calls code INSIDE the file (by filePath) // For code elements (Function, Class, etc.), use the direct id const isFileTarget = targetType === 'File'; @@ -1016,7 +1044,18 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`, r.reason AS reason LIMIT 300 `; - depthQueries.push(executeQuery(d1Query).catch(err => { + if (import.meta.env.DEV) { + console.log(`🔍 Impact d=1 query:\n${d1Query}`); + } + depthQueries.push(executeQuery(d1Query).then(results => { + if (import.meta.env.DEV) { + console.log(`📊 Impact d=1 results: ${results.length} rows`); + if (results.length > 0) { + console.log(' Sample:', results.slice(0, 3)); + } + } + return results; + }).catch(err => { if (import.meta.env.DEV) console.warn('Impact d=1 query failed:', err); return []; })); @@ -1174,7 +1213,38 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`, const totalAffected = allNodeIds.length; if (totalAffected === 0) { - return `No ${direction} dependencies found for "${target}" (types: ${activeRelTypes.join(', ')}). This code appears to be ${direction === 'upstream' ? 'unused (not called by anything)' : 'self-contained (no outgoing dependencies)'}.`; + if (isFileTarget) { + const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const targetFileName = (targetFilePath || target).split('/').pop() || target; + const baseName = targetFileName.replace(/\.[^/.]+$/, ''); + const refRegex = new RegExp(`\\b${escapeRegex(baseName)}\\b`, 'g'); + const hints: Array<{ file: string; line: number; content: string }> = []; + const hintLimit = 15; + + for (const [filePath, content] of fileContents.entries()) { + if (filePath === targetFilePath) continue; + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (refRegex.test(lines[i])) { + hints.push({ + file: filePath, + line: i + 1, + content: lines[i].trim().slice(0, 150), + }); + if (hints.length >= hintLimit) break; + } + refRegex.lastIndex = 0; + } + if (hints.length >= hintLimit) break; + } + + if (hints.length > 0) { + const formatted = hints.map(h => `${h.file}:${h.line}: ${h.content}`).join('\n'); + return `No ${direction} dependencies found for "${target}" (types: ${activeRelTypes.join(', ')}), but textual references were detected (graph may be incomplete):\n\n${formatted}${multipleMatchWarning}`; + } + } + + return `No ${direction} dependencies found for "${target}" (types: ${activeRelTypes.join(', ')}). This code appears to be ${direction === 'upstream' ? 'unused (not called by anything)' : 'self-contained (no outgoing dependencies)'}.${multipleMatchWarning}`; } const depth1 = byDepth.get(1) || []; @@ -1361,6 +1431,9 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`, // Compact footer lines.push(`✅ GRAPH ANALYSIS COMPLETE (trusted)`); lines.push(`⚠️ Optional: grep("${target}") for dynamic patterns`); + if (multipleMatchWarning) { + lines.push(multipleMatchWarning); + } lines.push(``); return lines.join('\n');