UNPKG

hikma-engine

Version:

Code Knowledge Graph Indexer - A sophisticated TypeScript-based indexer that transforms Git repositories into multi-dimensional knowledge stores for AI agents

540 lines (539 loc) 24 kB
"use strict"; /** * @file Result enhancement service for adding context and metadata to search results. * Provides syntax highlighting, breadcrumbs, and related information. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ResultEnhancerService = void 0; const path = __importStar(require("path")); const logger_1 = require("../../utils/logger"); const logger = (0, logger_1.getLogger)('ResultEnhancer'); /** * Service for enhancing search results with additional context and metadata. */ class ResultEnhancerService { constructor(sqliteClient) { this.sqliteClient = sqliteClient; } /** * Enhances an array of search results. */ async enhanceResults(results, options = {}, query) { const { includeSyntaxHighlighting = false, includeContext = true, includeRelatedFiles = false, contextLines = 3, } = options; logger.debug('Enhancing search results', { resultCount: results.length, options, hasQuery: !!query, }); const enhancedResults = await Promise.all(results.map(result => this.enhanceResult(result, { includeSyntaxHighlighting, includeContext, includeRelatedFiles, contextLines, }, query))); logger.debug('Results enhanced successfully', { enhancedCount: enhancedResults.length, withRelevanceScoring: !!query, }); return enhancedResults; } /** * Enhances a single search result with advanced features. */ async enhanceResult(result, options, query) { const enhanced = { ...result }; try { // Add file context if (options.includeContext) { enhanced.context = await this.addFileContext(result, options.contextLines || 3); } // Add metadata enhanced.metadata = await this.addMetadata(result); // Add advanced syntax highlighting if (options.includeSyntaxHighlighting && enhanced.context) { const codeToHighlight = result.node.properties.signature || result.node.properties.body || result.node.properties.name || ''; enhanced.context.syntaxHighlighted = this.addAdvancedSyntaxHighlighting(codeToHighlight, enhanced.metadata?.language); } // Add advanced related files with relationships if (options.includeRelatedFiles) { enhanced.context = enhanced.context || {}; const relatedInfo = await this.findAdvancedRelatedFiles(result); enhanced.context.relatedFiles = relatedInfo.relatedFiles; // Add relationship information to metadata enhanced.metadata = enhanced.metadata || {}; enhanced.metadata.fileRelationships = relatedInfo.relationships; } // Add relevance scoring explanation if (query) { const relevanceInfo = this.addRelevanceExplanation(result, query); enhanced.metadata = enhanced.metadata || {}; enhanced.metadata.relevanceScore = relevanceInfo.score; enhanced.metadata.relevanceExplanation = relevanceInfo.explanation; enhanced.metadata.relevanceFactors = relevanceInfo.factors; } // Add performance metrics enhanced.metadata = enhanced.metadata || {}; enhanced.metadata.enhancementTimestamp = new Date().toISOString(); } catch (error) { logger.warn('Failed to enhance result', { nodeId: result.node.id, error: error instanceof Error ? error.message : String(error), }); } return enhanced; } /** * Adds file context including breadcrumbs and surrounding lines. */ async addFileContext(result, contextLines) { const filePath = result.node.properties.filePath; if (!filePath) return {}; const fileName = path.basename(filePath); const breadcrumbs = this.generateBreadcrumbs(filePath); // For code nodes, try to get surrounding lines let beforeLines = []; let afterLines = []; if (result.node.type === 'CodeNode' && result.node.properties.startLine) { const contextResult = await this.getCodeContext(filePath, result.node.properties.startLine, result.node.properties.endLine, contextLines); beforeLines = contextResult.beforeLines; afterLines = contextResult.afterLines; } return { filePath, fileName, breadcrumbs, beforeLines, afterLines, }; } /** * Adds metadata about the file and node. */ async addMetadata(result) { const metadata = {}; // Get language from node properties metadata.language = result.node.properties.language; // Get file metadata if available if (result.node.properties.filePath) { try { const fileInfo = this.sqliteClient.get('SELECT language, size_kb, updated_at FROM files WHERE file_path = ?', [result.node.properties.filePath]); if (fileInfo) { metadata.language = metadata.language || fileInfo.language; metadata.fileSize = fileInfo.size_kb ? Math.round(fileInfo.size_kb * 1024) : undefined; metadata.lastModified = fileInfo.updated_at; } } catch (error) { logger.debug('Failed to get file metadata', { filePath: result.node.properties.filePath, error: error instanceof Error ? error.message : String(error), }); } } // Get author from recent commits if it's a code node if (result.node.type === 'CodeNode') { try { const commitInfo = this.sqliteClient.get(` SELECT c.author, c.date FROM commits c JOIN file_commits fc ON c.id = fc.commit_id JOIN files f ON fc.file_id = f.file_id WHERE f.file_path = ? ORDER BY c.date DESC LIMIT 1 `, [result.node.properties.filePath]); if (commitInfo) { metadata.author = commitInfo.author; if (!metadata.lastModified) { metadata.lastModified = commitInfo.date; } } } catch (error) { logger.debug('Failed to get commit metadata', { filePath: result.node.properties.filePath, error: error instanceof Error ? error.message : String(error), }); } } return metadata; } /** * Generates breadcrumb navigation for a file path. */ generateBreadcrumbs(filePath) { const parts = filePath.split(path.sep).filter(part => part.length > 0); const breadcrumbs = []; for (let i = 0; i < parts.length; i++) { breadcrumbs.push(parts.slice(0, i + 1).join(path.sep)); } return breadcrumbs; } /** * Gets code context (surrounding lines) for a code node. */ async getCodeContext(filePath, startLine, endLine, contextLines) { // This is a simplified implementation // In a real scenario, you might read the actual file content // For now, we'll return placeholder context const beforeLines = []; const afterLines = []; // Try to get context from other code nodes in the same file try { const beforeNodes = this.sqliteClient.all(` SELECT name, signature FROM code_nodes WHERE file_path = ? AND end_line < ? AND end_line >= ? ORDER BY start_line DESC LIMIT ? `, [filePath, startLine, Math.max(1, startLine - contextLines * 2), contextLines]); const afterNodes = this.sqliteClient.all(` SELECT name, signature FROM code_nodes WHERE file_path = ? AND start_line > ? AND start_line <= ? ORDER BY start_line ASC LIMIT ? `, [filePath, endLine, endLine + contextLines * 2, contextLines]); beforeLines.push(...beforeNodes.map(node => `// ${node.name}: ${node.signature}`)); afterLines.push(...afterNodes.map(node => `// ${node.name}: ${node.signature}`)); } catch (error) { logger.debug('Failed to get code context', { filePath, startLine, endLine, error: error instanceof Error ? error.message : String(error), }); } return { beforeLines, afterLines }; } /** * Finds files related to the current result. */ async findRelatedFiles(result) { const relatedFiles = []; try { if (result.node.properties.filePath) { // Find files in the same directory const directory = path.dirname(result.node.properties.filePath); const sameDirectoryFiles = this.sqliteClient.all(` SELECT file_path FROM files WHERE file_path LIKE ? AND file_path != ? LIMIT 5 `, [`${directory}%`, result.node.properties.filePath]); relatedFiles.push(...sameDirectoryFiles.map(f => f.file_path)); // Find files with similar names or extensions const fileName = path.basename(result.node.properties.filePath, path.extname(result.node.properties.filePath)); const extension = path.extname(result.node.properties.filePath); const similarFiles = this.sqliteClient.all(` SELECT file_path FROM files WHERE (file_name LIKE ? OR file_extension = ?) AND file_path != ? LIMIT 3 `, [`%${fileName}%`, extension, result.node.properties.filePath]); relatedFiles.push(...similarFiles.map(f => f.file_path)); } } catch (error) { logger.debug('Failed to find related files', { nodeId: result.node.id, error: error instanceof Error ? error.message : String(error), }); } // Remove duplicates and limit results return [...new Set(relatedFiles)].slice(0, 5); } /** * Adds relevance scoring explanation to results. */ addRelevanceExplanation(result, query) { const explanation = []; const factors = []; let totalScore = result.similarity; const queryLower = query.toLowerCase(); const name = result.node.properties.name?.toLowerCase() || ''; const signature = result.node.properties.signature?.toLowerCase() || ''; // Exact name match if (name === queryLower) { const contribution = 0.3; factors.push({ factor: 'Exact name match', weight: 0.3, contribution }); explanation.push(`Exact name match (+${(contribution * 100).toFixed(0)}%)`); } // Name contains query else if (name.includes(queryLower)) { const contribution = 0.2; factors.push({ factor: 'Name contains query', weight: 0.2, contribution }); explanation.push(`Name contains query (+${(contribution * 100).toFixed(0)}%)`); } // Signature match if (signature.includes(queryLower)) { const contribution = 0.15; factors.push({ factor: 'Signature match', weight: 0.15, contribution }); explanation.push(`Signature contains query (+${(contribution * 100).toFixed(0)}%)`); } // Node type relevance const nodeTypeWeights = { 'CodeNode': 0.9, 'FileNode': 0.8, 'CommitNode': 0.6, 'TestNode': 0.7, 'PullRequestNode': 0.5, 'FunctionNode': 0.85, }; const nodeTypeWeight = nodeTypeWeights[result.node.type] || 0.05; factors.push({ factor: `${result.node.type} relevance`, weight: nodeTypeWeight, contribution: nodeTypeWeight }); explanation.push(`${result.node.type} type (+${(nodeTypeWeight * 100).toFixed(0)}%)`); // Recent activity bonus (for commits and files) if (result.node.properties.date || result.node.properties.lastModified) { const dateStr = result.node.properties.date || result.node.properties.lastModified; if (dateStr) { const date = new Date(dateStr); const daysSinceModified = (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceModified < 30) { const contribution = Math.max(0.05, 0.1 - (daysSinceModified / 300)); factors.push({ factor: 'Recent activity', weight: 0.1, contribution }); explanation.push(`Recent activity (+${(contribution * 100).toFixed(0)}%)`); } } } // Language popularity bonus const popularLanguages = ['typescript', 'javascript', 'python', 'java', 'go']; const language = result.node.properties.language?.toLowerCase(); if (language && popularLanguages.includes(language)) { const contribution = 0.02; factors.push({ factor: 'Popular language', weight: 0.02, contribution }); explanation.push(`Popular language: ${language} (+${(contribution * 100).toFixed(0)}%)`); } return { score: totalScore, explanation, factors, }; } /** * Improves syntax highlighting with better language detection and highlighting. */ addAdvancedSyntaxHighlighting(code, language) { if (!code || !language) return code; // Enhanced syntax highlighting with more comprehensive patterns const highlightingRules = this.getAdvancedHighlightingRules(language); let highlighted = code; // Apply highlighting rules in order of precedence highlightingRules.forEach(rule => { highlighted = highlighted.replace(rule.pattern, rule.replacement); }); return highlighted; } /** * Gets advanced highlighting rules for different languages. */ getAdvancedHighlightingRules(language) { const commonRules = [ // Comments { pattern: /\/\/.*$/gm, replacement: '<span class="comment">$&</span>' }, { pattern: /\/\*[\s\S]*?\*\//g, replacement: '<span class="comment">$&</span>' }, // Strings { pattern: /"([^"\\]|\\.)*"/g, replacement: '<span class="string">$&</span>' }, { pattern: /'([^'\\]|\\.)*'/g, replacement: '<span class="string">$&</span>' }, { pattern: /`([^`\\]|\\.)*`/g, replacement: '<span class="template-string">$&</span>' }, ]; const languageSpecificRules = { typescript: [ ...commonRules, // Keywords { pattern: /\b(function|const|let|var|class|interface|type|async|await|return|if|else|for|while|try|catch|finally|import|export|from|default)\b/g, replacement: '<span class="keyword">$&</span>' }, // Types { pattern: /\b(string|number|boolean|object|any|void|never|unknown)\b/g, replacement: '<span class="type">$&</span>' }, // Decorators { pattern: /@\w+/g, replacement: '<span class="decorator">$&</span>' }, ], javascript: [ ...commonRules, // Keywords { pattern: /\b(function|const|let|var|class|async|await|return|if|else|for|while|try|catch|finally|import|export|from|default)\b/g, replacement: '<span class="keyword">$&</span>' }, ], python: [ // Comments { pattern: /#.*$/gm, replacement: '<span class="comment">$&</span>' }, // Strings { pattern: /"""[\s\S]*?"""/g, replacement: '<span class="docstring">$&</span>' }, { pattern: /"([^"\\]|\\.)*"/g, replacement: '<span class="string">$&</span>' }, { pattern: /'([^'\\]|\\.)*'/g, replacement: '<span class="string">$&</span>' }, // Keywords { pattern: /\b(def|class|if|elif|else|for|while|try|except|finally|import|from|as|return|yield|lambda|with|async|await)\b/g, replacement: '<span class="keyword">$&</span>' }, // Decorators { pattern: /@\w+/g, replacement: '<span class="decorator">$&</span>' }, ], java: [ ...commonRules, // Keywords { pattern: /\b(public|private|protected|static|final|abstract|class|interface|extends|implements|import|package|return|if|else|for|while|try|catch|finally|new|this|super)\b/g, replacement: '<span class="keyword">$&</span>' }, // Annotations { pattern: /@\w+/g, replacement: '<span class="annotation">$&</span>' }, ], go: [ ...commonRules, // Keywords { pattern: /\b(func|var|const|type|struct|interface|package|import|return|if|else|for|range|switch|case|default|go|defer|chan|select)\b/g, replacement: '<span class="keyword">$&</span>' }, ], }; return languageSpecificRules[language.toLowerCase()] || commonRules; } /** * Enhanced method to find related files with better algorithms. */ async findAdvancedRelatedFiles(result) { const relatedFiles = []; const relationships = []; try { if (result.node.properties.filePath) { const currentFile = result.node.properties.filePath; const directory = path.dirname(currentFile); const fileName = path.basename(currentFile, path.extname(currentFile)); const extension = path.extname(currentFile); // 1. Files in the same directory const sameDirectoryFiles = this.sqliteClient.all(` SELECT file_path, file_name FROM files WHERE file_path LIKE ? AND file_path != ? LIMIT 10 `, [`${directory}%`, currentFile]); sameDirectoryFiles.forEach((f) => { relatedFiles.push(f.file_path); relationships.push({ file: f.file_path, relationship: 'Same directory', strength: 0.6, }); }); // 2. Files with similar names const similarNameFiles = this.sqliteClient.all(` SELECT file_path, file_name FROM files WHERE (file_name LIKE ? OR file_name LIKE ?) AND file_path != ? LIMIT 5 `, [`%${fileName}%`, `${fileName}%`, currentFile]); similarNameFiles.forEach((f) => { if (!relatedFiles.includes(f.file_path)) { relatedFiles.push(f.file_path); relationships.push({ file: f.file_path, relationship: 'Similar name', strength: 0.7, }); } }); // 3. Files with same extension const sameExtensionFiles = this.sqliteClient.all(` SELECT file_path, file_name FROM files WHERE file_extension = ? AND file_path != ? AND file_path NOT LIKE ? ORDER BY updated_at DESC LIMIT 5 `, [extension, currentFile, `${directory}%`]); sameExtensionFiles.forEach((f) => { if (!relatedFiles.includes(f.file_path)) { relatedFiles.push(f.file_path); relationships.push({ file: f.file_path, relationship: 'Same file type', strength: 0.4, }); } }); // 4. Files that import/reference this file const referencingFiles = this.sqliteClient.all(` SELECT DISTINCT f.file_path FROM files f JOIN file_imports fi ON f.file_id = fi.file_id WHERE fi.imported_path LIKE ? AND f.file_path != ? LIMIT 5 `, [`%${fileName}%`, currentFile]); referencingFiles.forEach((f) => { if (!relatedFiles.includes(f.file_path)) { relatedFiles.push(f.file_path); relationships.push({ file: f.file_path, relationship: 'Imports this file', strength: 0.9, }); } }); // 5. Files this file imports const importedFiles = this.sqliteClient.all(` SELECT DISTINCT f.file_path FROM files f JOIN file_imports fi ON f.file_id = fi.file_id WHERE fi.file_id = (SELECT file_id FROM files WHERE file_path = ?) LIMIT 5 `, [currentFile]); importedFiles.forEach((f) => { if (!relatedFiles.includes(f.file_path)) { relatedFiles.push(f.file_path); relationships.push({ file: f.file_path, relationship: 'Imported by this file', strength: 0.8, }); } }); } } catch (error) { logger.debug('Failed to find advanced related files', { nodeId: result.node.id, error: error instanceof Error ? error.message : String(error), }); } // Sort by relationship strength relationships.sort((a, b) => b.strength - a.strength); return { relatedFiles: [...new Set(relatedFiles)].slice(0, 10), relationships: relationships.slice(0, 10), }; } } exports.ResultEnhancerService = ResultEnhancerService;