UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

541 lines (540 loc) 23.3 kB
import { getDependencyOperations } from '../core/operations/dependency-operations.js'; import { getTaskOperations } from '../core/operations/task-operations.js'; import logger from '../../../logger.js'; const DEFAULT_ORDERING_CONFIG = { checkLogicalOrdering: true, checkTypeOrdering: true, checkPriorityConflicts: true, checkEpicOrdering: true, maxDependencyDepth: 10, maxChainLength: 20 }; export class DependencyValidator { config; constructor(config = {}) { this.config = { ...DEFAULT_ORDERING_CONFIG, ...config }; } async validateProjectDependencies(projectId) { const startTime = Date.now(); try { logger.info({ projectId }, 'Starting comprehensive dependency validation'); const taskOps = getTaskOperations(); const dependencyOps = getDependencyOperations(); const tasksResult = await taskOps.listTasks({ projectId }); if (!tasksResult.success) { throw new Error(`Failed to get tasks for project: ${tasksResult.error}`); } const tasks = tasksResult.data || []; const dependencies = []; for (const task of tasks) { const taskDepsResult = await dependencyOps.getDependenciesForTask(task.id); if (taskDepsResult.success && taskDepsResult.data) { dependencies.push(...taskDepsResult.data); } } const errors = []; const warnings = []; const suggestions = []; const circularDependencies = await this.detectCircularDependencies(tasks, dependencies); circularDependencies.forEach(cycle => { errors.push({ type: 'circular_dependency', severity: cycle.severity, message: cycle.description, affectedTasks: cycle.cycle, suggestedFix: cycle.resolutionOptions[0]?.description || 'Remove one dependency from the cycle', autoFixable: cycle.resolutionOptions.some(opt => opt.complexity === 'low') }); }); if (this.config.checkLogicalOrdering) { const orderingIssues = await this.validateLogicalOrdering(tasks, dependencies); errors.push(...orderingIssues.errors); warnings.push(...orderingIssues.warnings); suggestions.push(...orderingIssues.suggestions); } if (this.config.checkTypeOrdering) { const typeOrderingIssues = await this.validateTaskTypeOrdering(tasks, dependencies); warnings.push(...typeOrderingIssues.warnings); suggestions.push(...typeOrderingIssues.suggestions); } if (this.config.checkPriorityConflicts) { const priorityIssues = await this.validatePriorityOrdering(tasks, dependencies); warnings.push(...priorityIssues.warnings); suggestions.push(...priorityIssues.suggestions); } const depthIssues = await this.validateDependencyDepth(tasks, dependencies); warnings.push(...depthIssues.warnings); suggestions.push(...depthIssues.suggestions); let executionOrder = []; if (circularDependencies.length === 0) { executionOrder = await this.calculateExecutionOrder(tasks, dependencies); } const validationTime = Date.now() - startTime; logger.info({ projectId, isValid: errors.length === 0, errorsFound: errors.length, warningsFound: warnings.length, suggestionsFound: suggestions.length, circularDependencies: circularDependencies.length, validationTime }, 'Dependency validation completed'); return { isValid: errors.length === 0, errors, warnings, suggestions, circularDependencies, executionOrder, metadata: { validatedAt: new Date(), validationTime, tasksValidated: tasks.length, dependenciesValidated: dependencies.length } }; } catch (error) { const validationTime = Date.now() - startTime; logger.error({ err: error, projectId, validationTime }, 'Dependency validation failed'); return { isValid: false, errors: [{ type: 'logical_error', severity: 'critical', message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`, affectedTasks: [], suggestedFix: 'Check project data integrity and try again', autoFixable: false }], warnings: [], suggestions: [], circularDependencies: [], executionOrder: [], metadata: { validatedAt: new Date(), validationTime, tasksValidated: 0, dependenciesValidated: 0 } }; } } async validateDependencyBeforeCreation(fromTaskId, toTaskId, projectId) { const startTime = Date.now(); try { logger.debug({ fromTaskId, toTaskId, projectId }, 'Validating dependency before creation'); const errors = []; const warnings = []; const suggestions = []; const taskOps = getTaskOperations(); const fromTaskResult = await taskOps.getTask(fromTaskId); const toTaskResult = await taskOps.getTask(toTaskId); if (!fromTaskResult.success || !toTaskResult.success) { errors.push({ type: 'missing_task', severity: 'critical', message: 'One or both tasks do not exist', affectedTasks: [fromTaskId, toTaskId], suggestedFix: 'Ensure both tasks exist before creating dependency', autoFixable: false }); return this.createValidationResult(errors, warnings, suggestions, [], [], startTime, 0, 0); } const fromTask = fromTaskResult.data; const toTask = toTaskResult.data; if (fromTaskId === toTaskId) { errors.push({ type: 'invalid_dependency', severity: 'high', message: 'A task cannot depend on itself', affectedTasks: [fromTaskId], suggestedFix: 'Remove self-dependency', autoFixable: true }); } const wouldCreateCycle = await this.wouldCreateCircularDependency(fromTaskId, toTaskId, projectId); if (wouldCreateCycle.wouldCreate) { errors.push({ type: 'circular_dependency', severity: 'critical', message: `Adding this dependency would create a circular dependency: ${wouldCreateCycle.cyclePath.join(' → ')}`, affectedTasks: wouldCreateCycle.cyclePath, suggestedFix: 'Reorder tasks or remove conflicting dependencies', autoFixable: false }); } const logicalIssues = await this.validateTaskPairLogic(fromTask, toTask); warnings.push(...logicalIssues.warnings); suggestions.push(...logicalIssues.suggestions); const validationTime = Date.now() - startTime; return { isValid: errors.length === 0, errors, warnings, suggestions, circularDependencies: wouldCreateCycle.wouldCreate ? [{ cycle: wouldCreateCycle.cyclePath, severity: 'critical', description: `Circular dependency would be created: ${wouldCreateCycle.cyclePath.join(' → ')}`, resolutionOptions: [{ type: 'remove_dependency', description: 'Do not create this dependency', affectedDependencies: [], complexity: 'low' }] }] : [], executionOrder: [], metadata: { validatedAt: new Date(), validationTime, tasksValidated: 2, dependenciesValidated: 1 } }; } catch (error) { logger.error({ err: error, fromTaskId, toTaskId, projectId }, 'Single dependency validation failed'); return this.createValidationResult([{ type: 'logical_error', severity: 'critical', message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`, affectedTasks: [fromTaskId, toTaskId], suggestedFix: 'Check task data and try again', autoFixable: false }], [], [], [], [], startTime, 0, 0); } } async detectCircularDependencies(tasks, dependencies) { const cycles = []; const visited = new Set(); const recursionStack = new Set(); const adjacencyList = new Map(); tasks.forEach(task => adjacencyList.set(task.id, [])); dependencies.forEach(dep => { const dependents = adjacencyList.get(dep.fromTaskId) || []; dependents.push(dep.toTaskId); adjacencyList.set(dep.fromTaskId, dependents); }); const dfs = (taskId, path) => { if (recursionStack.has(taskId)) { const cycleStart = path.indexOf(taskId); const cycle = path.slice(cycleStart).concat([taskId]); cycles.push({ cycle, severity: this.determineCycleSeverity(cycle, tasks), description: `Circular dependency detected: ${cycle.join(' → ')}`, resolutionOptions: this.generateCycleResolutionOptions(cycle, dependencies) }); return true; } if (visited.has(taskId)) { return false; } visited.add(taskId); recursionStack.add(taskId); path.push(taskId); const dependents = adjacencyList.get(taskId) || []; for (const dependent of dependents) { if (dfs(dependent, [...path])) { } } recursionStack.delete(taskId); return false; }; for (const task of tasks) { if (!visited.has(task.id)) { dfs(task.id, []); } } return cycles; } async wouldCreateCircularDependency(fromTaskId, toTaskId, projectId) { try { const dependencyOps = getDependencyOperations(); const visited = new Set(); const path = []; const dfs = async (currentTaskId) => { if (currentTaskId === fromTaskId) { path.push(currentTaskId); return true; } if (visited.has(currentTaskId)) { return false; } visited.add(currentTaskId); path.push(currentTaskId); const depsResult = await dependencyOps.getDependenciesForTask(currentTaskId); if (depsResult.success && depsResult.data) { for (const dep of depsResult.data) { if (await dfs(dep.toTaskId)) { return true; } } } path.pop(); return false; }; const wouldCreate = await dfs(toTaskId); return { wouldCreate, cyclePath: wouldCreate ? [fromTaskId, ...path] : [] }; } catch (error) { logger.warn({ err: error, fromTaskId, toTaskId, projectId }, 'Failed to check for circular dependency'); return { wouldCreate: false, cyclePath: [] }; } } async validateLogicalOrdering(tasks, dependencies) { const errors = []; const warnings = []; const suggestions = []; for (const dep of dependencies) { const fromTask = tasks.find(t => t.id === dep.fromTaskId); const toTask = tasks.find(t => t.id === dep.toTaskId); if (!fromTask || !toTask) continue; const priorityOrder = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 }; const fromPriority = priorityOrder[fromTask.priority] || 0; const toPriority = priorityOrder[toTask.priority] || 0; if (fromPriority < toPriority) { warnings.push({ type: 'potential_issue', message: `Lower priority task "${fromTask.title}" blocks higher priority task "${toTask.title}"`, affectedTasks: [fromTask.id, toTask.id], recommendation: 'Consider adjusting task priorities or dependency relationships', impact: 'medium' }); } if (fromTask.estimatedHours > toTask.estimatedHours * 3) { suggestions.push({ type: 'optimization', description: `Large task "${fromTask.title}" (${fromTask.estimatedHours}h) blocks smaller task "${toTask.title}" (${toTask.estimatedHours}h)`, affectedTasks: [fromTask.id, toTask.id], estimatedBenefit: 'Better parallelization and faster completion', implementationComplexity: 'medium' }); } } return { errors, warnings, suggestions }; } async validateTaskTypeOrdering(tasks, dependencies) { const warnings = []; const suggestions = []; const typeOrder = { 'research': 1, 'development': 2, 'testing': 3, 'review': 4, 'deployment': 5, 'documentation': 6 }; for (const dep of dependencies) { const fromTask = tasks.find(t => t.id === dep.fromTaskId); const toTask = tasks.find(t => t.id === dep.toTaskId); if (!fromTask || !toTask) continue; const fromOrder = typeOrder[fromTask.type] || 4; const toOrder = typeOrder[toTask.type] || 4; if (fromOrder > toOrder) { warnings.push({ type: 'best_practice', message: `${toTask.type} task "${toTask.title}" depends on ${fromTask.type} task "${fromTask.title}" which typically comes later`, affectedTasks: [fromTask.id, toTask.id], recommendation: 'Review if this dependency order makes logical sense', impact: 'low' }); } } return { warnings, suggestions }; } async validatePriorityOrdering(tasks, dependencies) { const warnings = []; const suggestions = []; const priorityOrder = { 'critical': 4, 'high': 3, 'medium': 2, 'low': 1 }; for (const dep of dependencies) { const fromTask = tasks.find(t => t.id === dep.fromTaskId); const toTask = tasks.find(t => t.id === dep.toTaskId); if (!fromTask || !toTask) continue; const fromPriority = priorityOrder[fromTask.priority] || 0; const toPriority = priorityOrder[toTask.priority] || 0; if (fromPriority < toPriority - 1) { suggestions.push({ type: 'reordering', description: `Consider increasing priority of "${fromTask.title}" or decreasing priority of "${toTask.title}"`, affectedTasks: [fromTask.id, toTask.id], estimatedBenefit: 'Better task prioritization and resource allocation', implementationComplexity: 'low' }); } } return { warnings, suggestions }; } async validateDependencyDepth(tasks, dependencies) { const warnings = []; const suggestions = []; const adjacencyList = new Map(); tasks.forEach(task => adjacencyList.set(task.id, [])); dependencies.forEach(dep => { const dependents = adjacencyList.get(dep.fromTaskId) || []; dependents.push(dep.toTaskId); adjacencyList.set(dep.fromTaskId, dependents); }); const calculateDepth = (taskId, visited = new Set()) => { if (visited.has(taskId)) return 0; visited.add(taskId); const dependents = adjacencyList.get(taskId) || []; if (dependents.length === 0) return 1; const maxDepth = Math.max(...dependents.map(dep => calculateDepth(dep, new Set(visited)))); return maxDepth + 1; }; for (const task of tasks) { const depth = calculateDepth(task.id); if (depth > this.config.maxDependencyDepth) { warnings.push({ type: 'performance', message: `Task "${task.title}" has dependency depth of ${depth}, exceeding recommended maximum of ${this.config.maxDependencyDepth}`, affectedTasks: [task.id], recommendation: 'Consider breaking down long dependency chains', impact: 'medium' }); } } return { warnings, suggestions }; } async calculateExecutionOrder(tasks, dependencies) { const inDegree = new Map(); const adjacencyList = new Map(); tasks.forEach(task => { inDegree.set(task.id, 0); adjacencyList.set(task.id, []); }); dependencies.forEach(dep => { adjacencyList.get(dep.fromTaskId)?.push(dep.toTaskId); inDegree.set(dep.toTaskId, (inDegree.get(dep.toTaskId) || 0) + 1); }); const queue = []; const result = []; for (const [taskId, degree] of inDegree) { if (degree === 0) { queue.push(taskId); } } while (queue.length > 0) { const taskId = queue.shift(); result.push(taskId); const dependents = adjacencyList.get(taskId) || []; for (const dependent of dependents) { const newDegree = (inDegree.get(dependent) || 0) - 1; inDegree.set(dependent, newDegree); if (newDegree === 0) { queue.push(dependent); } } } return result; } async validateTaskPairLogic(fromTask, toTask) { const warnings = []; const suggestions = []; if (fromTask.epicId && toTask.epicId && fromTask.epicId !== toTask.epicId) { warnings.push({ type: 'potential_issue', message: `Cross-epic dependency: "${fromTask.title}" (${fromTask.epicId}) depends on "${toTask.title}" (${toTask.epicId})`, affectedTasks: [fromTask.id, toTask.id], recommendation: 'Consider if this cross-epic dependency is necessary', impact: 'low' }); } const fromFiles = new Set(fromTask.filePaths); const toFiles = new Set(toTask.filePaths); const commonFiles = [...fromFiles].filter(file => toFiles.has(file)); if (commonFiles.length > 0) { suggestions.push({ type: 'optimization', description: `Tasks share common files: ${commonFiles.join(', ')}`, affectedTasks: [fromTask.id, toTask.id], estimatedBenefit: 'Consider merging tasks or ensuring proper file coordination', implementationComplexity: 'medium' }); } return { warnings, suggestions }; } determineCycleSeverity(cycle, tasks) { const cycleTasks = tasks.filter(task => cycle.includes(task.id)); if (cycleTasks.some(task => task.priority === 'critical')) { return 'critical'; } if (cycle.length > 4 || cycleTasks.some(task => task.priority === 'high')) { return 'high'; } return 'medium'; } generateCycleResolutionOptions(cycle, dependencies) { const options = []; const cycleDeps = dependencies.filter(dep => cycle.includes(dep.fromTaskId) && cycle.includes(dep.toTaskId)); if (cycleDeps.length > 0) { const weakestDep = cycleDeps.find(dep => dep.type === 'suggests') || cycleDeps[0]; options.push({ type: 'remove_dependency', description: `Remove dependency from ${weakestDep.fromTaskId} to ${weakestDep.toTaskId}`, affectedDependencies: [weakestDep.id], complexity: 'low' }); } options.push({ type: 'reorder_tasks', description: 'Reorder tasks to break the circular dependency', affectedDependencies: cycleDeps.map(dep => dep.id), complexity: 'medium' }); if (cycle.length <= 3) { options.push({ type: 'split_task', description: 'Split one of the tasks to break the dependency cycle', affectedDependencies: [], complexity: 'high' }); } return options; } createValidationResult(errors, warnings, suggestions, circularDependencies, executionOrder, startTime, tasksValidated, dependenciesValidated) { const validationTime = Date.now() - startTime; return { isValid: errors.length === 0, errors, warnings, suggestions, circularDependencies, executionOrder, metadata: { validatedAt: new Date(), validationTime, tasksValidated, dependenciesValidated } }; } }