UNPKG

devcontext

Version:

DevContext is a cutting-edge Model Context Protocol (MCP) server designed to provide developers with continuous, project-centric context awareness.

599 lines (521 loc) 21 kB
/** * ContextPrioritizerLogic.js * * Logic for prioritizing and scoring context snippets based on * query relevance, focus area alignment, recency, and relationships. */ import { nonVectorRelevanceScore } from "./SmartSearchServiceLogic.js"; import { getRelatedEntities } from "./RelationshipContextManagerLogic.js"; import { executeQuery } from "../db.js"; import { CONTEXT_DECAY_RATE } from "../config.js"; /** * @typedef {Object} CodeEntity * @property {string} entity_id - Unique identifier for the code entity * @property {string} file_path - Path to the file containing the entity * @property {string} entity_type - Type of code entity (e.g., 'file', 'function', 'class') * @property {string} name - Name of the code entity * @property {string} [parent_entity_id] - ID of the parent entity (if any) * @property {string} [content_hash] - Hash of the entity content * @property {string} [raw_content] - Raw content of the entity * @property {number} [start_line] - Start line of the entity within the file * @property {number} [end_line] - End line of the entity within the file * @property {string} [language] - Programming language of the entity * @property {Date} [created_at] - Creation timestamp * @property {Date} [last_modified_at] - Last modification timestamp * @property {Date} [last_accessed_at] - Last access timestamp * @property {number} [importance_score] - Predefined importance score */ /** * @typedef {Object} ContextSnippet * @property {CodeEntity} entity - The code entity * @property {number} baseRelevance - Base relevance score from initial search */ /** * @typedef {Object} FocusArea * @property {string} focus_id - Unique identifier for the focus area * @property {string} focus_type - Type of focus (e.g., 'file', 'function', 'task') * @property {string} identifier - Human-readable identifier * @property {string} description - Description of the focus area * @property {string[]} related_entity_ids - IDs of entities related to this focus * @property {string[]} keywords - Keywords associated with this focus area */ /** * @typedef {Object} RecencyInfo * @property {Date} lastAccessedThreshold - Threshold for considering entities as recently accessed * @property {Date} [lastModifiedThreshold] - Threshold for considering entities as recently modified * @property {number} [recencyBoostFactor] - Factor to boost score for recent entities (default: 1.25) */ /** * Score a context snippet based on multiple relevance factors * * @param {ContextSnippet} snippet - The context snippet to score * @param {string[]} queryKeywords - Keywords from the current query * @param {FocusArea} currentFocus - Current focus area * @param {RecencyInfo} recencyData - Information about recency thresholds * @returns {number} Final relevance score */ export async function scoreContextSnippet( snippet, queryKeywords, currentFocus, recencyData ) { try { // Define weights for different scoring factors const weights = { baseRelevance: 0.2, // 20% weight for initial relevance queryRelevance: 0.25, // 25% weight for query matching focusAlignment: 0.25, // 25% weight for focus area alignment (reduced from 30%) recency: 0.15, // 15% weight for recency entityType: 0.05, // 5% weight for entity type priority relationshipProximity: 0.1, // 10% weight for relationship proximity (increased from 5%) }; // 1. Query Relevance const queryRelevanceScore = nonVectorRelevanceScore( snippet.entity, queryKeywords, currentFocus.keywords ); // 2. Focus Alignment const focusAlignmentScore = calculateFocusAlignmentScore( snippet.entity, currentFocus ); // 3. Recency const recencyScore = calculateRecencyScore(snippet.entity, recencyData); // 4. Entity Type Priority const entityTypePriorityScore = calculateEntityTypePriorityScore( snippet.entity, currentFocus.focus_type ); // 5. Relationship Proximity (async) const relationshipProximityScore = await calculateRelationshipProximityScore( snippet.entity, currentFocus.related_entity_ids ); // Combine all factors into a weighted score const finalScore = snippet.baseRelevance * weights.baseRelevance + queryRelevanceScore * weights.queryRelevance + focusAlignmentScore * weights.focusAlignment + recencyScore * weights.recency + entityTypePriorityScore * weights.entityType + relationshipProximityScore * weights.relationshipProximity; // Ensure score is between 0 and 1 return Math.max(0, Math.min(1, finalScore)); } catch (error) { console.error("Error scoring context snippet:", error); // Fall back to base relevance in case of error return snippet.baseRelevance; } } /** * Calculate focus alignment score based on the relationship between * the entity and the current focus area * * @param {CodeEntity} entity - The code entity * @param {FocusArea} focus - Current focus area * @returns {number} Focus alignment score between 0 and 1 */ function calculateFocusAlignmentScore(entity, focus) { // Highest score if the entity is directly in the focus area's related entities if (focus.related_entity_ids.includes(entity.entity_id)) { return 1.0; } // Check parent relationship - high score if parent is in focus if ( entity.parent_entity_id && focus.related_entity_ids.includes(entity.parent_entity_id) ) { return 0.9; } // Check if the entity is from the same file as the focus const focusEntityPaths = focus.related_entity_ids.map((id) => { // This is a simplified approach - in practice, you would look up the entity // path from the database or another data structure return id.split(":")[0]; // Assuming ID format includes file path }); if ( entity.file_path && focusEntityPaths.some((path) => entity.file_path.startsWith(path)) ) { return 0.7; } // Check keyword overlap if (focus.keywords && focus.keywords.length > 0) { const entityText = [entity.name || "", entity.raw_content || ""] .join(" ") .toLowerCase(); const matchingKeywords = focus.keywords.filter((keyword) => entityText.includes(keyword.toLowerCase()) ); if (matchingKeywords.length > 0) { return 0.5 * (matchingKeywords.length / focus.keywords.length); } } // Minimal focus alignment return 0.1; } /** * Calculate recency score based on when the entity was last accessed or modified * * @param {CodeEntity} entity - The code entity * @param {RecencyInfo} recencyData - Information about recency thresholds * @returns {number} Recency score between 0 and 1 */ function calculateRecencyScore(entity, recencyData) { const { lastAccessedThreshold, lastModifiedThreshold, recencyBoostFactor = 1.25, } = recencyData; let recencyScore = 0.5; // Default medium score // Check if the entity has been accessed recently if (entity.last_accessed_at) { const lastAccessed = new Date(entity.last_accessed_at); if (lastAccessed >= lastAccessedThreshold) { recencyScore = 0.8; // High score for recently accessed entities } } // Check if the entity has been modified recently (this takes precedence) if (entity.last_modified_at && lastModifiedThreshold) { const lastModified = new Date(entity.last_modified_at); if (lastModified >= lastModifiedThreshold) { recencyScore = 1.0; // Maximum score for recently modified entities } } // Apply recency decay based on time since last access/modification if (entity.last_accessed_at || entity.last_modified_at) { const lastTimepoint = entity.last_modified_at || entity.last_accessed_at; const lastTime = new Date(lastTimepoint); const now = new Date(); const daysSince = (now - lastTime) / (1000 * 60 * 60 * 24); // Exponential decay: score = baseScore * e^(-daysSince/60) // This gives a decay to ~37% of original value after 60 days const decayFactor = Math.exp(-daysSince / 60); recencyScore *= decayFactor; } return recencyScore; } /** * Calculate entity type priority score based on entity type and current focus * * @param {CodeEntity} entity - The code entity * @param {string} focusType - Type of the current focus * @returns {number} Entity type priority score between 0 and 1 */ function calculateEntityTypePriorityScore(entity, focusType) { // Base priorities for different entity types const typePriorities = { function: 0.9, class: 0.9, method: 0.85, file: 0.8, variable: 0.7, comment: 0.5, default: 0.6, }; // Get base priority for this entity type const entityType = (entity.entity_type || "").toLowerCase(); let typePriority = typePriorities[entityType] || typePriorities.default; // Boost priority if the entity type matches the focus type if (entityType === focusType.toLowerCase()) { typePriority = Math.min(1.0, typePriority * 1.2); } // Additional context-based adjustments could be added here // For example, if working on a bug fix, error handling code might get a boost return typePriority; } /** * Calculate relationship proximity score based on relationship to focus entities * * @param {CodeEntity} entity - The code entity * @param {string[]} focusEntityIds - IDs of entities in the current focus * @returns {number} Relationship proximity score between 0 and 1 */ async function calculateRelationshipProximityScore(entity, focusEntityIds) { // Return default score if no entity ID or no focus entities if (!entity.entity_id || !focusEntityIds || focusEntityIds.length === 0) { return 0.5; // Default score if there are no focus entities } try { // Import necessary modules const { getRelationships, findCodePaths } = await import( "./RelationshipContextManagerLogic.js" ); const LRUCache = (await import("../utils/lru-cache.js")).default; // Create or get the relationship cache (static cache shared across function calls) if (!calculateRelationshipProximityScore.cache) { calculateRelationshipProximityScore.cache = new LRUCache(100); // Cache up to 100 relationship lookups } const cache = calculateRelationshipProximityScore.cache; // Check if we have this relationship calculation cached const cacheKey = `${entity.entity_id}:${focusEntityIds.join(",")}`; const cachedScore = cache.get(cacheKey); if (cachedScore !== null) { return cachedScore; } // Define weights for different relationship types const relationshipTypeWeights = { calls: 1.0, // Direct function calls are very relevant extends: 0.9, // Class inheritance is highly relevant implements: 0.9, // Interface implementation is highly relevant imports: 0.8, // Import relationship is fairly relevant references: 0.7, // References relationship is somewhat relevant depends_on: 0.7, // Dependencies are somewhat relevant contains: 0.6, // Containment is moderately relevant references_variable: 0.5, // Variable references are less relevant default: 0.5, // Default weight for other types }; // Collect metrics to calculate the final score let totalScore = 0; let relationshipCount = 0; let hasDirectRelationship = false; let hasSecondDegreeRelationship = false; // Check for direct (1st-degree) relationships const firstDegreeRelationships = await getRelationships( entity.entity_id, "both", // Get both incoming and outgoing relationships [] // All relationship types ); if (firstDegreeRelationships.length === 0) { // Store in cache and return slightly below default if no relationships exist const score = 0.4; cache.put(cacheKey, score); return score; } // Process direct relationships with focus entities const directRelationshipsWithFocus = firstDegreeRelationships.filter( (rel) => { const otherEntityId = rel.source_entity_id === entity.entity_id ? rel.target_entity_id : rel.source_entity_id; return focusEntityIds.includes(otherEntityId); } ); if (directRelationshipsWithFocus.length > 0) { hasDirectRelationship = true; // Calculate score based on relationship types and weights for (const rel of directRelationshipsWithFocus) { const relType = rel.relationship_type; const weight = relationshipTypeWeights[relType] || relationshipTypeWeights.default; // Direction matters - outgoing relationships (entity calls/uses focus) are slightly more relevant const directionMultiplier = rel.source_entity_id === entity.entity_id ? 1.0 : 0.9; totalScore += weight * directionMultiplier; relationshipCount++; } } // Check for 2nd-degree relationships (only if we don't have strong direct relationships) // This is more expensive, so we limit it if ( !hasDirectRelationship || (hasDirectRelationship && directRelationshipsWithFocus.length < 2) ) { // Get all entities related to our entity (1st degree connections) const connectedEntityIds = firstDegreeRelationships.map((rel) => rel.source_entity_id === entity.entity_id ? rel.target_entity_id : rel.source_entity_id ); // Check if any focus entity is connected to any of our 1st degree connections // We're limiting this to 5 entities to avoid expensive queries const focusEntitiesToCheck = focusEntityIds.slice(0, 5); const secondDegreeConnectionPromises = []; // For each focus entity, check if it has connections to any of our 1st degree connections for (const focusEntityId of focusEntitiesToCheck) { // Skip focus entities that already have direct connections if ( directRelationshipsWithFocus.some( (rel) => rel.source_entity_id === focusEntityId || rel.target_entity_id === focusEntityId ) ) { continue; } const promise = getRelationships(focusEntityId, "both", []).then( (focusRelationships) => { const secondDegreeConnections = focusRelationships.filter((rel) => { const otherEntityId = rel.source_entity_id === focusEntityId ? rel.target_entity_id : rel.source_entity_id; return connectedEntityIds.includes(otherEntityId); }); if (secondDegreeConnections.length > 0) { hasSecondDegreeRelationship = true; // Second-degree relationships are less valuable, so we apply a discount for (const rel of secondDegreeConnections) { const relType = rel.relationship_type; const weight = relationshipTypeWeights[relType] || relationshipTypeWeights.default; // Second-degree connections are worth less totalScore += weight * 0.5; relationshipCount++; } } } ); secondDegreeConnectionPromises.push(promise); } // Wait for all second-degree connection checks to complete await Promise.all(secondDegreeConnectionPromises); } // For exceptional cases, try to find paths between the entity and important focus entities // This is expensive, so we only do it for a limited number of focus entities and when we don't have many direct relationships if ( (!hasDirectRelationship && !hasSecondDegreeRelationship) || relationshipCount < 2 ) { // Only consider the first 2 focus entities for this expensive operation const importantFocusEntities = focusEntityIds.slice(0, 2); for (const focusEntityId of importantFocusEntities) { // Try to find paths up to 3 hops away (this can be expensive) try { // Look for important relationship types for (const relType of ["calls", "extends", "implements", "imports"]) { const paths = await findCodePaths( entity.entity_id, focusEntityId, relType ); if (paths.length > 0) { hasSecondDegreeRelationship = true; // For each path, calculate a score based on path length for (const path of paths) { const pathLength = path.length; if (pathLength <= 4) { // Only consider relatively short paths const pathScore = relationshipTypeWeights[relType] * (1 / pathLength); totalScore += pathScore; relationshipCount++; } } // If we found paths, no need to check other relationship types break; } } } catch (error) { console.error( `Error finding code paths for entity ${entity.entity_id}:`, error ); // Continue processing other focus entities } } } // Calculate final score let finalScore; if (relationshipCount === 0) { // No relationships found, return below default finalScore = 0.45; } else { // Normalize the score let normalizedScore = totalScore / relationshipCount; // Apply bonuses for direct and indirect relationships if (hasDirectRelationship) { normalizedScore *= 1.2; // 20% boost for direct relationships } if (hasSecondDegreeRelationship) { normalizedScore *= 1.1; // 10% boost for second-degree relationships } // Ensure score is between 0 and 1 finalScore = Math.min(1.0, normalizedScore); } // Cache the result cache.put(cacheKey, finalScore); return finalScore; } catch (error) { console.error("Error calculating relationship proximity:", error); return 0.5; // Default score in case of error } } /** * Prioritize context snippets based on relevance to query and current focus * * @param {ContextSnippet[]} contexts - Array of context snippets to prioritize * @param {string[]} queryKeywords - Keywords from the current query * @param {FocusArea} currentFocus - Current focus area * @param {number} maxResults - Maximum number of results to return * @returns {Promise<ContextSnippet[]>} Prioritized context snippets */ export async function prioritizeContexts( contexts, queryKeywords, currentFocus, maxResults ) { // Create recencyData with default thresholds const recencyData = { lastAccessedThreshold: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago lastModifiedThreshold: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago recencyBoostFactor: 1.25, }; // Score each context snippet const scoredContexts = []; for (const snippet of contexts) { try { // Score the snippet const finalScore = await scoreContextSnippet( snippet, queryKeywords, currentFocus, recencyData ); // Add the score to the snippet object scoredContexts.push({ ...snippet, finalScore, }); } catch (error) { console.error(`Error scoring context snippet: ${error.message}`); // Include the snippet with its base relevance as fallback scoredContexts.push({ ...snippet, finalScore: snippet.baseRelevance || 0, }); } } // Sort contexts by finalScore in descending order scoredContexts.sort((a, b) => b.finalScore - a.finalScore); // Return top maxResults return scoredContexts.slice(0, maxResults); } /** * Apply decay to importance scores of all entities that haven't been * accessed recently to reflect diminishing relevance over time * * @returns {Promise<void>} */ export async function applyDecayToAll() { try { // Define the minimum threshold to prevent scores from becoming too small const MIN_IMPORTANCE_THRESHOLD = 0.1; // Define the access threshold (entities not accessed in the last 30 days) const accessThreshold = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // Construct and execute the SQL query to apply decay const query = ` UPDATE code_entities SET importance_score = importance_score * ? WHERE last_accessed_at < ? AND importance_score > ? `; const params = [ CONTEXT_DECAY_RATE, accessThreshold.toISOString(), MIN_IMPORTANCE_THRESHOLD, ]; // Execute the query const result = await executeQuery(query, params); console.log(`Applied decay to ${result.changes || 0} entities`); } catch (error) { console.error("Error applying decay to importance scores:", error); throw error; } }