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
JavaScript
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
}
};
}
}