UNPKG

devcontext

Version:

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

489 lines (433 loc) 14.8 kB
/** * RelationshipContextManagerLogic.js * * Provides functions for managing relationships between code entities. */ import { v4 as uuidv4 } from "uuid"; import { executeQuery } from "../db.js"; /** * Adds a relationship between two code entities * * @param {string} sourceEntityId - ID of the source entity * @param {string} targetEntityId - ID of the target entity * @param {string} relationshipType - Type of relationship (e.g., 'calls', 'imports', 'extends') * @param {number} weight - Weight of the relationship (default: 1.0) * @param {object} metadata - Additional metadata about the relationship * @returns {Promise<void>} */ export async function addRelationship( sourceEntityId, targetEntityId, relationshipType, weight = 1.0, metadata = {} ) { // Validate required parameters if (!sourceEntityId || !targetEntityId || !relationshipType) { throw new Error( "Source entity ID, target entity ID, and relationship type are required" ); } // Generate a new UUID for the relationship const relationshipId = uuidv4(); // Convert metadata object to JSON string const metadataJson = JSON.stringify(metadata); try { // Insert the relationship into the database const query = ` INSERT INTO code_relationships ( relationship_id, source_entity_id, target_entity_id, relationship_type, weight, metadata ) VALUES (?, ?, ?, ?, ?, ?) `; await executeQuery(query, [ relationshipId, sourceEntityId, targetEntityId, relationshipType, weight, metadataJson, ]); } catch (error) { // Check if error is due to unique constraint violation if (error.message && error.message.includes("UNIQUE constraint failed")) { // If duplicate, we'll update the existing relationship const updateQuery = ` UPDATE code_relationships SET weight = ?, metadata = ? WHERE source_entity_id = ? AND target_entity_id = ? AND relationship_type = ? `; await executeQuery(updateQuery, [ weight, metadataJson, sourceEntityId, targetEntityId, relationshipType, ]); } else { // For other errors, rethrow console.error( `Error adding relationship between ${sourceEntityId} and ${targetEntityId}:`, error ); throw error; } } } /** * Relationship type definition matching code_relationships table structure * @typedef {Object} Relationship * @property {string} relationship_id - Unique identifier for the relationship * @property {string} source_entity_id - ID of the source entity * @property {string} target_entity_id - ID of the target entity * @property {string} relationship_type - Type of relationship * @property {number} weight - Weight of the relationship * @property {Object} metadata - Additional metadata about the relationship */ /** * Gets relationships for a specific entity * * @param {string} entityId - ID of the entity to get relationships for * @param {string} direction - Direction of relationships to get ('outgoing', 'incoming', or 'both') * @param {string[]} types - Types of relationships to filter by (empty array for all types) * @returns {Promise<Relationship[]>} Array of relationship objects */ export async function getRelationships( entityId, direction = "outgoing", types = [] ) { // Validate required parameters if (!entityId) { throw new Error("Entity ID is required"); } // Validate direction parameter if (!["outgoing", "incoming", "both"].includes(direction)) { throw new Error("Direction must be 'outgoing', 'incoming', or 'both'"); } // Build the base query let query = ` SELECT relationship_id, source_entity_id, target_entity_id, relationship_type, weight, metadata FROM code_relationships WHERE `; const queryParams = []; // Add direction-specific conditions if (direction === "outgoing") { query += "source_entity_id = ?"; queryParams.push(entityId); } else if (direction === "incoming") { query += "target_entity_id = ?"; queryParams.push(entityId); } else { // direction === "both" query += "(source_entity_id = ? OR target_entity_id = ?)"; queryParams.push(entityId, entityId); } // Add relationship type filter if provided if (types.length > 0) { // Create placeholders for the IN clause const typePlaceholders = types.map(() => "?").join(", "); query += ` AND relationship_type IN (${typePlaceholders})`; queryParams.push(...types); } try { // Execute the query const relationships = await executeQuery(query, queryParams); // Process metadata for each relationship return relationships.map((relationship) => ({ ...relationship, // Parse metadata JSON string to object, default to empty object if null or invalid metadata: relationship.metadata ? JSON.parse(relationship.metadata) : {}, })); } catch (error) { console.error(`Error getting relationships for entity ${entityId}:`, error); throw error; } } /** * GraphSnippet type definition for call graph data * @typedef {Object} GraphSnippet * @property {Array<{id: string, name: string, type: string}>} nodes - Entities in the graph * @property {Array<{source: string, target: string, type: string}>} edges - Relationships between entities */ /** * Builds a call graph snippet starting from a function entity * * @param {string} functionEntityId - ID of the function entity to start from * @param {number} depth - Maximum depth of the call graph (default: 2) * @returns {Promise<GraphSnippet>} Call graph snippet with nodes and edges */ export async function buildCallGraphSnippet(functionEntityId, depth = 2) { // Validate required parameters if (!functionEntityId) { throw new Error("Function entity ID is required"); } // Validate depth if (depth < 1) { throw new Error("Depth must be at least 1"); } try { // Use a recursive Common Table Expression (CTE) to get function calls up to specified depth const outgoingCallsQuery = ` WITH RECURSIVE call_graph AS ( -- Base case: start with the source function SELECT cr.source_entity_id, cr.target_entity_id, cr.relationship_type, 0 AS depth FROM code_relationships cr WHERE cr.source_entity_id = ? AND cr.relationship_type = 'calls' UNION ALL -- Recursive case: find further calls up to max depth SELECT cr.source_entity_id, cr.target_entity_id, cr.relationship_type, cg.depth + 1 AS depth FROM code_relationships cr JOIN call_graph cg ON cr.source_entity_id = cg.target_entity_id WHERE cr.relationship_type = 'calls' AND cg.depth < ? ) SELECT DISTINCT source_entity_id, target_entity_id, relationship_type, depth FROM call_graph ORDER BY depth `; const outgoingCalls = await executeQuery(outgoingCallsQuery, [ functionEntityId, depth - 1, ]); // Get incoming calls (functions that call our target function) const incomingCallsQuery = ` SELECT cr.source_entity_id, cr.target_entity_id, cr.relationship_type, 0 AS depth FROM code_relationships cr WHERE cr.target_entity_id = ? AND cr.relationship_type = 'calls' `; const incomingCalls = await executeQuery(incomingCallsQuery, [ functionEntityId, ]); // Combine outgoing and incoming calls const allCalls = [...outgoingCalls, ...incomingCalls]; // Extract all unique entity IDs involved const entityIds = new Set(); entityIds.add(functionEntityId); // Add the root function allCalls.forEach((call) => { entityIds.add(call.source_entity_id); entityIds.add(call.target_entity_id); }); // Get entity details for all involved entities const entityIdsArray = Array.from(entityIds); const placeholders = entityIdsArray.map(() => "?").join(","); const entitiesQuery = ` SELECT id, name, type FROM code_entities WHERE id IN (${placeholders}) `; const entities = await executeQuery(entitiesQuery, entityIdsArray); // Build the graph nodes const nodes = entities.map((entity) => ({ id: entity.id, name: entity.name, type: entity.type, })); // Build the graph edges const edges = allCalls.map((call) => ({ source: call.source_entity_id, target: call.target_entity_id, type: call.relationship_type, })); // Return the call graph snippet return { nodes, edges, }; } catch (error) { console.error( `Error building call graph for function ${functionEntityId}:`, error ); throw error; } } /** * Path type definition for code paths * @typedef {string[]} Path - An array of entity IDs representing a path */ /** * Finds all paths between two entities with a specific relationship type * * @param {string} startEntityId - ID of the starting entity * @param {string} endEntityId - ID of the ending entity * @param {string} relationshipType - Type of relationship to follow * @returns {Promise<Path[]>} Array of paths (each path is an array of entity IDs) */ export async function findCodePaths( startEntityId, endEntityId, relationshipType ) { // Validate required parameters if (!startEntityId || !endEntityId || !relationshipType) { throw new Error( "Start entity ID, end entity ID, and relationship type are required" ); } try { // Use a recursive CTE to find all paths const query = ` WITH RECURSIVE paths(path, current_id, visited) AS ( -- Base case: start with the starting entity SELECT startEntityId || '', -- Initialize path with just the start entity startEntityId, startEntityId -- Initialize visited set with start entity FROM (SELECT ? AS startEntityId) UNION ALL -- Recursive case: extend paths that haven't reached the end entity SELECT paths.path || ',' || cr.target_entity_id, -- Append target to path cr.target_entity_id, -- New current entity is the target paths.visited || ',' || cr.target_entity_id -- Update visited set FROM code_relationships cr JOIN paths ON cr.source_entity_id = paths.current_id WHERE cr.relationship_type = ? AND cr.target_entity_id != paths.startEntityId -- Avoid immediate cycles back to start AND paths.visited NOT LIKE '%,' || cr.target_entity_id || ',%' -- Check for cycles AND paths.visited NOT LIKE cr.target_entity_id || ',%' -- Check for cycles at start AND paths.visited NOT LIKE '%,' || cr.target_entity_id -- Check for cycles at end ) -- Select paths that end at the target entity SELECT path FROM paths WHERE current_id = ? `; const results = await executeQuery(query, [ startEntityId, relationshipType, endEntityId, ]); // Process the results into an array of paths return results.map((row) => { // Split the path string into an array of entity IDs return row.path.split(","); }); } catch (error) { console.error( `Error finding paths between ${startEntityId} and ${endEntityId}:`, error ); // SQLite might not fully support the recursive CTE with the cycle detection as written // If we get an error, let's use a more basic approach that has limited depth try { // Fallback to a simpler implementation with finite depth const maxDepth = 10; // Reasonable limit to prevent excessive path lengths const fallbackQuery = ` WITH RECURSIVE paths(path, current_id, depth) AS ( -- Base case: start with the starting entity SELECT ? AS path, ? AS current_id, 0 AS depth UNION ALL -- Recursive case: extend paths that haven't reached the end entity SELECT paths.path || ',' || cr.target_entity_id, cr.target_entity_id, paths.depth + 1 FROM code_relationships cr JOIN paths ON cr.source_entity_id = paths.current_id WHERE cr.relationship_type = ? AND paths.depth < ? AND paths.path NOT LIKE '%' || cr.target_entity_id || '%' -- Simple cycle check ) -- Select paths that end at the target entity SELECT path FROM paths WHERE current_id = ? `; const fallbackResults = await executeQuery(fallbackQuery, [ startEntityId, startEntityId, relationshipType, maxDepth, endEntityId, ]); // Process the fallback results return fallbackResults.map((row) => { return row.path.split(","); }); } catch (fallbackError) { console.error( "Fallback path finding approach also failed:", fallbackError ); // If all else fails, return an empty array return []; } } } /** * Gets entities related to a given entity * * @param {string} entityId - ID of the entity to get related entities for * @param {string[]} [relationshipTypes=[]] - Types of relationships to filter by (empty array for all types) * @param {number} [maxResults=20] - Maximum number of results to return * @returns {Promise<string[]>} Array of related entity IDs */ export async function getRelatedEntities( entityId, relationshipTypes = [], maxResults = 20 ) { // Validate required parameters if (!entityId) { throw new Error("Entity ID is required"); } try { // Get both incoming and outgoing relationships const relationships = await getRelationships( entityId, "both", relationshipTypes ); // Extract unique entity IDs from relationships const relatedEntityIds = new Set(); for (const relationship of relationships) { if (relationship.source_entity_id === entityId) { relatedEntityIds.add(relationship.target_entity_id); } else { relatedEntityIds.add(relationship.source_entity_id); } // Stop if we've reached the maximum number of results if (relatedEntityIds.size >= maxResults) { break; } } return Array.from(relatedEntityIds); } catch (error) { console.error(`Error getting related entities for ${entityId}:`, error); return []; } }