vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
1,262 lines • 81.7 kB
JavaScript
import logger from '../../../logger.js';
export class OptimizedDependencyGraph {
projectId;
adjacencyList = new Map();
reverseIndex = new Map();
nodes = new Map();
edges = new Map();
topologicalOrder = [];
criticalPath = [];
parallelBatches = [];
isDirty = false;
metrics = {
totalNodes: 0,
totalEdges: 0,
maxDepth: 0,
criticalPathLength: 0,
parallelBatches: 0,
cycleCount: 0,
orphanedNodes: 0,
averageDegree: 0
};
constructor(projectId) {
this.projectId = projectId;
logger.debug({ projectId }, 'Initializing optimized dependency graph');
}
addTask(task) {
const node = {
taskId: task.id,
title: task.title,
status: task.status,
estimatedHours: task.estimatedHours,
priority: task.priority,
dependencies: [],
dependents: [],
depth: 0,
criticalPath: false
};
this.nodes.set(task.id, node);
if (!this.adjacencyList.has(task.id)) {
this.adjacencyList.set(task.id, new Set());
}
if (!this.reverseIndex.has(task.id)) {
this.reverseIndex.set(task.id, new Set());
}
this.markDirty();
logger.debug({ taskId: task.id, title: task.title }, 'Added task to dependency graph');
}
addDependency(dependentTaskId, dependencyTaskId, type = 'task', weight = 1, critical = false, description) {
if (!this.nodes.has(dependentTaskId) || !this.nodes.has(dependencyTaskId)) {
logger.warn({ dependentTaskId, dependencyTaskId }, 'Cannot add dependency: one or both tasks not found');
return false;
}
if (this.wouldCreateCycle(dependencyTaskId, dependentTaskId)) {
logger.warn({ dependentTaskId, dependencyTaskId }, 'Cannot add dependency: would create cycle');
return false;
}
const edgeId = `${dependentTaskId}->${dependencyTaskId}`;
const edge = {
from: dependentTaskId,
to: dependencyTaskId,
type,
weight,
critical,
description
};
this.adjacencyList.get(dependencyTaskId).add(dependentTaskId);
this.reverseIndex.get(dependentTaskId).add(dependencyTaskId);
this.edges.set(edgeId, edge);
const dependentNode = this.nodes.get(dependentTaskId);
const dependencyNode = this.nodes.get(dependencyTaskId);
dependentNode.dependencies.push(dependencyTaskId);
dependencyNode.dependents.push(dependentTaskId);
this.markDirty();
logger.debug({ dependentTaskId, dependencyTaskId, type }, 'Added dependency edge');
return true;
}
removeDependency(dependentTaskId, dependencyTaskId) {
const edgeId = `${dependentTaskId}->${dependencyTaskId}`;
if (!this.edges.has(edgeId)) {
return false;
}
this.adjacencyList.get(dependencyTaskId)?.delete(dependentTaskId);
this.reverseIndex.get(dependentTaskId)?.delete(dependencyTaskId);
this.edges.delete(edgeId);
const dependentNode = this.nodes.get(dependentTaskId);
const dependencyNode = this.nodes.get(dependencyTaskId);
if (dependentNode) {
dependentNode.dependencies = dependentNode.dependencies.filter(id => id !== dependencyTaskId);
}
if (dependencyNode) {
dependencyNode.dependents = dependencyNode.dependents.filter(id => id !== dependentTaskId);
}
this.markDirty();
logger.debug({ dependentTaskId, dependencyTaskId }, 'Removed dependency edge');
return true;
}
getReadyTasks() {
const readyTasks = [];
for (const [taskId, node] of this.nodes) {
if (node.status === 'pending' && this.hasNoPendingDependencies(taskId)) {
readyTasks.push(taskId);
}
}
return readyTasks;
}
detectCycles() {
const cycles = [];
const visited = new Set();
const recursionStack = new Set();
const path = [];
const dfs = (nodeId) => {
visited.add(nodeId);
recursionStack.add(nodeId);
path.push(nodeId);
const neighbors = this.adjacencyList.get(nodeId) || new Set();
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
if (dfs(neighbor)) {
return true;
}
}
else if (recursionStack.has(neighbor)) {
const cycleStart = path.indexOf(neighbor);
const cycle = path.slice(cycleStart);
cycle.push(neighbor);
cycles.push([...cycle]);
return true;
}
}
recursionStack.delete(nodeId);
path.pop();
return false;
};
for (const nodeId of this.nodes.keys()) {
if (!visited.has(nodeId)) {
dfs(nodeId);
}
}
return cycles;
}
getTopologicalOrder() {
if (!this.topologicalOrder) {
this.topologicalOrder = [];
}
if (!this.isDirty && this.topologicalOrder.length > 0) {
return [...this.topologicalOrder];
}
const cycles = this.detectCycles();
if (cycles.length > 0) {
logger.warn({ cycleCount: cycles.length }, 'Cannot create topological order - graph contains cycles');
this.topologicalOrder = [];
return [];
}
const inDegree = new Map();
const queue = [];
const result = [];
for (const nodeId of this.nodes.keys()) {
inDegree.set(nodeId, this.reverseIndex.get(nodeId)?.size || 0);
if (inDegree.get(nodeId) === 0) {
queue.push(nodeId);
}
}
while (queue.length > 0) {
const current = queue.shift();
result.push(current);
const neighbors = this.adjacencyList.get(current) || new Set();
for (const neighbor of neighbors) {
const newInDegree = inDegree.get(neighbor) - 1;
inDegree.set(neighbor, newInDegree);
if (newInDegree === 0) {
queue.push(neighbor);
}
}
}
if (result.length !== this.nodes.size) {
logger.warn({
expected: this.nodes.size,
actual: result.length
}, 'Topological sort incomplete - graph contains cycles');
return [];
}
this.topologicalOrder = result;
return [...result];
}
getCriticalPath() {
if (!this.criticalPath) {
this.criticalPath = [];
}
if (!this.isDirty && this.criticalPath.length > 0) {
return [...this.criticalPath];
}
const topOrder = this.getTopologicalOrder();
if (!topOrder || topOrder.length === 0) {
return [];
}
const distances = new Map();
const predecessors = new Map();
for (const nodeId of this.nodes.keys()) {
distances.set(nodeId, 0);
}
for (const nodeId of topOrder) {
const node = this.nodes.get(nodeId);
const currentDistance = distances.get(nodeId);
const neighbors = this.adjacencyList.get(nodeId) || new Set();
for (const neighbor of neighbors) {
const edge = this.edges.get(`${nodeId}->${neighbor}`);
const newDistance = currentDistance + (node.estimatedHours * (edge?.weight || 1));
if (newDistance > distances.get(neighbor)) {
distances.set(neighbor, newDistance);
predecessors.set(neighbor, nodeId);
}
}
}
let maxDistance = 0;
let endNode = '';
for (const [nodeId, distance] of distances) {
if (distance > maxDistance) {
maxDistance = distance;
endNode = nodeId;
}
}
const path = [];
let current = endNode;
while (current) {
path.unshift(current);
current = predecessors.get(current) || '';
}
for (const nodeId of this.nodes.keys()) {
const node = this.nodes.get(nodeId);
node.criticalPath = path.includes(nodeId);
}
this.criticalPath = path;
return [...path];
}
getParallelBatches() {
if (!this.parallelBatches) {
this.parallelBatches = [];
}
if (!this.isDirty && this.parallelBatches.length > 0) {
return [...this.parallelBatches];
}
const topOrder = this.getTopologicalOrder();
if (!topOrder || topOrder.length === 0) {
return [];
}
const batches = [];
const processed = new Set();
let batchId = 0;
while (processed.size < this.nodes.size) {
const currentBatch = [];
for (const taskId of topOrder) {
if (processed.has(taskId))
continue;
const dependencies = this.reverseIndex.get(taskId) || new Set();
const hasUnprocessedDeps = Array.from(dependencies).some(dep => !processed.has(dep));
if (!hasUnprocessedDeps) {
currentBatch.push(taskId);
}
}
if (currentBatch.length === 0) {
logger.warn('No tasks can be processed - possible cycle or error');
break;
}
const estimatedDuration = Math.max(...currentBatch.map(taskId => this.nodes.get(taskId)?.estimatedHours || 0));
const dependencies = Array.from(new Set(currentBatch.flatMap(taskId => Array.from(this.reverseIndex.get(taskId) || new Set()))));
const canStartAfter = batches.length > 0 ? [batches.length - 1] : [];
batches.push({
batchId,
taskIds: currentBatch,
estimatedDuration,
dependencies,
canStartAfter
});
currentBatch.forEach(taskId => processed.add(taskId));
batchId++;
}
this.parallelBatches = batches;
return [...batches];
}
wouldCreateCycle(fromTaskId, toTaskId) {
return this.hasPath(toTaskId, fromTaskId);
}
hasPath(from, to) {
if (from === to)
return true;
const visited = new Set();
const stack = [from];
while (stack.length > 0) {
const current = stack.pop();
if (visited.has(current))
continue;
visited.add(current);
const neighbors = this.adjacencyList.get(current) || new Set();
for (const neighbor of neighbors) {
if (neighbor === to)
return true;
if (!visited.has(neighbor)) {
stack.push(neighbor);
}
}
}
return false;
}
hasNoPendingDependencies(taskId) {
const dependencies = this.reverseIndex.get(taskId) || new Set();
for (const depId of dependencies) {
const depNode = this.nodes.get(depId);
if (depNode && depNode.status !== 'completed') {
return false;
}
}
return true;
}
markDirty() {
this.isDirty = true;
this.updateMetrics();
}
updateMetrics() {
this.metrics = {
totalNodes: this.nodes.size,
totalEdges: this.edges.size,
maxDepth: this.calculateMaxDepth(),
criticalPathLength: this.criticalPath.length,
parallelBatches: this.parallelBatches.length,
cycleCount: this.detectCycles().length,
orphanedNodes: this.getOrphanedNodes().length,
averageDegree: this.calculateAverageDegree()
};
}
calculateMaxDepth() {
let maxDepth = 0;
for (const node of this.nodes.values()) {
maxDepth = Math.max(maxDepth, node.depth);
}
return maxDepth;
}
getOrphanedNodes() {
const orphaned = [];
for (const [nodeId, node] of this.nodes) {
if (node.dependencies.length === 0 && node.dependents.length === 0) {
orphaned.push(nodeId);
}
}
return orphaned;
}
calculateAverageDegree() {
if (this.nodes.size === 0)
return 0;
let totalDegree = 0;
for (const node of this.nodes.values()) {
totalDegree += node.dependencies.length + node.dependents.length;
}
return totalDegree / this.nodes.size;
}
getMetrics() {
if (this.isDirty) {
this.updateMetrics();
}
return { ...this.metrics };
}
getNodes() {
return new Map(this.nodes);
}
getEdges() {
return new Map(this.edges);
}
clearCache() {
this.topologicalOrder = [];
this.criticalPath = [];
this.parallelBatches = [];
this.isDirty = true;
}
reset() {
this.nodes.clear();
this.edges.clear();
this.adjacencyList.clear();
this.reverseIndex.clear();
this.clearCache();
this.metrics = {
totalNodes: 0,
totalEdges: 0,
maxDepth: 0,
criticalPathLength: 0,
parallelBatches: 0,
cycleCount: 0,
orphanedNodes: 0,
averageDegree: 0
};
logger.debug({ projectId: this.projectId }, 'Dependency graph reset to empty state');
}
getSize() {
return {
nodes: this.nodes.size,
edges: this.edges.size
};
}
analyzeCriticalPath() {
const topOrder = this.getTopologicalOrder();
if (topOrder.length === 0) {
return {
paths: [],
longestPath: [],
totalDuration: 0,
resourceWeightedDuration: 0,
bottleneckTasks: []
};
}
const allPaths = this.findAllCriticalPaths();
const pathDurations = allPaths.map(path => ({
path,
duration: this.calculatePathDuration(path),
resourceWeightedDuration: this.calculateResourceWeightedDuration(path)
}));
const longestByDuration = pathDurations.reduce((max, current) => current.duration > max.duration ? current : max);
const longestByResourceWeight = pathDurations.reduce((max, current) => current.resourceWeightedDuration > max.resourceWeightedDuration ? current : max);
const taskFrequency = new Map();
allPaths.forEach(path => {
path.forEach(taskId => {
taskFrequency.set(taskId, (taskFrequency.get(taskId) || 0) + 1);
});
});
const bottleneckTasks = Array.from(taskFrequency.entries())
.filter(([_, frequency]) => frequency > 1)
.sort((a, b) => b[1] - a[1])
.map(([taskId]) => taskId);
return {
paths: allPaths,
longestPath: longestByDuration.path,
totalDuration: longestByDuration.duration,
resourceWeightedDuration: longestByResourceWeight.resourceWeightedDuration,
bottleneckTasks
};
}
analyzeDependencyImpact(taskId) {
if (!this.nodes.has(taskId)) {
throw new Error(`Task ${taskId} not found in graph`);
}
const directDependents = Array.from(this.adjacencyList.get(taskId) || new Set());
const indirectDependents = this.findIndirectDependents(taskId);
const propagationChains = this.findPropagationChains(taskId);
const impactRadius = propagationChains.length > 0 ? Math.max(...propagationChains.map(chain => chain.length - 1)) : 0;
const totalAffected = new Set([...directDependents, ...indirectDependents]).size;
const riskLevel = this.calculateRiskLevel(impactRadius, totalAffected);
return {
taskId,
directDependents,
indirectDependents,
impactRadius,
riskLevel,
propagationChain: propagationChains
};
}
detectBottlenecks() {
const bottlenecks = [];
const criticalPathAnalysis = this.analyzeCriticalPath();
for (const [taskId, node] of this.nodes) {
const analysis = this.analyzeTaskBottleneck(taskId, node, criticalPathAnalysis);
if (analysis.severity > 0.3) {
bottlenecks.push(analysis);
}
}
return bottlenecks.sort((a, b) => b.severity - a.severity);
}
optimizeResourceAllocation() {
const currentBatches = this.getParallelBatches();
const optimizedBatches = this.optimizeParallelBatches(currentBatches);
const totalTasks = this.nodes.size;
const parallelTasks = optimizedBatches.reduce((sum, batch) => sum + batch.taskIds.length, 0);
const resourceUtilization = parallelTasks / totalTasks;
const originalTime = currentBatches.reduce((sum, batch) => sum + batch.estimatedDuration, 0);
const optimizedTime = optimizedBatches.reduce((sum, batch) => sum + batch.estimatedDuration, 0);
const timeReduction = (originalTime - optimizedTime) / originalTime;
const parallelismOpportunities = this.identifyParallelismOpportunities();
return {
optimalBatches: optimizedBatches,
resourceUtilization,
timeReduction,
parallelismOpportunities
};
}
findAllCriticalPaths() {
const sources = this.findSourceNodes();
const sinks = this.findSinkNodes();
const allPaths = [];
for (const source of sources) {
for (const sink of sinks) {
const paths = this.findAllPathsBetween(source, sink);
allPaths.push(...paths);
}
}
return allPaths;
}
findSourceNodes() {
return Array.from(this.nodes.keys()).filter(nodeId => (this.reverseIndex.get(nodeId)?.size || 0) === 0);
}
findSinkNodes() {
return Array.from(this.nodes.keys()).filter(nodeId => (this.adjacencyList.get(nodeId)?.size || 0) === 0);
}
findAllPathsBetween(start, end) {
const paths = [];
const visited = new Set();
const dfs = (current, path) => {
if (current === end) {
paths.push([...path, current]);
return;
}
if (visited.has(current))
return;
visited.add(current);
const neighbors = this.adjacencyList.get(current) || new Set();
for (const neighbor of neighbors) {
dfs(neighbor, [...path, current]);
}
visited.delete(current);
};
dfs(start, []);
return paths;
}
calculatePathDuration(path) {
return path.reduce((total, taskId) => {
const node = this.nodes.get(taskId);
return total + (node?.estimatedHours || 0);
}, 0);
}
calculateResourceWeightedDuration(path) {
return path.reduce((total, taskId) => {
const node = this.nodes.get(taskId);
const baseHours = node?.estimatedHours || 0;
let weight = 1.0;
if (node?.priority === 'high')
weight *= 1.5;
if (node?.priority === 'critical')
weight *= 2.0;
return total + (baseHours * weight);
}, 0);
}
findIndirectDependents(taskId) {
const visited = new Set();
const indirectDependents = new Set();
const dfs = (current, depth) => {
if (visited.has(current) || depth > 10)
return;
visited.add(current);
const dependents = this.adjacencyList.get(current) || new Set();
for (const dependent of dependents) {
if (dependent !== taskId) {
indirectDependents.add(dependent);
dfs(dependent, depth + 1);
}
}
};
const directDependents = this.adjacencyList.get(taskId) || new Set();
for (const dependent of directDependents) {
dfs(dependent, 1);
}
for (const direct of directDependents) {
indirectDependents.delete(direct);
}
return Array.from(indirectDependents);
}
findPropagationChains(taskId) {
const chains = [];
const visited = new Set();
const dfs = (current, chain) => {
if (visited.has(current) || chain.length > 10)
return;
const dependents = this.adjacencyList.get(current) || new Set();
if (dependents.size === 0) {
chains.push([...chain, current]);
return;
}
visited.add(current);
for (const dependent of dependents) {
dfs(dependent, [...chain, current]);
}
visited.delete(current);
};
dfs(taskId, []);
return chains;
}
calculateRiskLevel(impactRadius, totalAffected) {
const riskScore = (impactRadius * 0.6) + (totalAffected * 0.4);
if (riskScore >= 8)
return 'critical';
if (riskScore >= 5)
return 'high';
if (riskScore >= 2)
return 'medium';
return 'low';
}
analyzeTaskBottleneck(taskId, node, criticalPathAnalysis) {
let severity = 0;
let bottleneckType = 'dependency';
const affectedTasks = [];
const recommendations = [];
if (criticalPathAnalysis.longestPath.includes(taskId)) {
severity += 0.4;
bottleneckType = 'critical-path';
affectedTasks.push(...criticalPathAnalysis.longestPath);
recommendations.push('Task is on critical path - prioritize completion');
}
const dependentCount = this.adjacencyList.get(taskId)?.size || 0;
if (dependentCount > 3) {
severity += 0.4;
bottleneckType = 'dependency';
affectedTasks.push(...Array.from(this.adjacencyList.get(taskId) || new Set()));
recommendations.push(`High dependency fan-out (${dependentCount} dependents) - consider breaking down task`);
}
if (node.estimatedHours > 8) {
severity += 0.2;
if (bottleneckType === 'dependency') {
}
else {
bottleneckType = 'resource';
}
recommendations.push('High time estimate - consider parallel execution or task breakdown');
}
const parallelConstraints = this.checkParallelConstraints(taskId);
if (parallelConstraints > 0) {
severity += 0.1;
if (bottleneckType === 'dependency' || bottleneckType === 'resource') {
}
else {
bottleneckType = 'parallel-constraint';
}
recommendations.push('Limited parallelization opportunities - review dependencies');
}
return {
taskId,
bottleneckType,
severity: Math.min(severity, 1.0),
affectedTasks: [...new Set(affectedTasks)],
recommendations
};
}
checkParallelConstraints(taskId) {
const dependencies = this.reverseIndex.get(taskId)?.size || 0;
const dependents = this.adjacencyList.get(taskId)?.size || 0;
return dependencies + dependents;
}
optimizeParallelBatches(currentBatches) {
const optimizedBatches = [];
for (const batch of currentBatches) {
const optimizedBatch = this.balanceBatch(batch);
optimizedBatches.push(optimizedBatch);
}
return optimizedBatches;
}
balanceBatch(batch) {
const sortedTasks = batch.taskIds.sort((a, b) => {
const nodeA = this.nodes.get(a);
const nodeB = this.nodes.get(b);
return (nodeB?.estimatedHours || 0) - (nodeA?.estimatedHours || 0);
});
const estimatedDuration = Math.max(...sortedTasks.map(taskId => this.nodes.get(taskId)?.estimatedHours || 0));
return {
...batch,
taskIds: sortedTasks,
estimatedDuration
};
}
identifyParallelismOpportunities() {
const opportunities = [];
for (const [taskId] of this.nodes) {
const canRunWith = this.findParallelizableTasks(taskId);
if (canRunWith.length > 0) {
const estimatedSavings = this.calculateParallelSavings(taskId, canRunWith);
opportunities.push({
taskId,
canRunWith,
estimatedSavings
});
}
}
return opportunities.sort((a, b) => b.estimatedSavings - a.estimatedSavings);
}
findParallelizableTasks(taskId) {
const parallelizable = [];
const taskDependencies = this.reverseIndex.get(taskId) || new Set();
const taskDependents = this.adjacencyList.get(taskId) || new Set();
for (const [otherTaskId] of this.nodes) {
if (otherTaskId === taskId)
continue;
const otherDependencies = this.reverseIndex.get(otherTaskId) || new Set();
const otherDependents = this.adjacencyList.get(otherTaskId) || new Set();
const hasDirectDependency = taskDependencies.has(otherTaskId) ||
taskDependents.has(otherTaskId) ||
otherDependencies.has(taskId) ||
otherDependents.has(taskId);
if (!hasDirectDependency) {
parallelizable.push(otherTaskId);
}
}
return parallelizable;
}
calculateParallelSavings(taskId, parallelTasks) {
const taskDuration = this.nodes.get(taskId)?.estimatedHours || 0;
const parallelDurations = parallelTasks.map(id => this.nodes.get(id)?.estimatedHours || 0);
const sequentialTime = taskDuration + parallelDurations.reduce((sum, duration) => sum + duration, 0);
const parallelTime = Math.max(taskDuration, ...parallelDurations);
return sequentialTime - parallelTime;
}
validateDependencies() {
const errors = [];
const warnings = [];
const suggestions = [];
const typeValidation = this.validateDependencyTypes();
errors.push(...typeValidation.errors);
warnings.push(...typeValidation.warnings);
const conflicts = this.detectDependencyConflicts();
conflicts.forEach(conflict => {
errors.push({
type: 'conflict',
severity: conflict.severity === 'critical' ? 'error' : 'warning',
message: conflict.description,
affectedTasks: conflict.involvedTasks,
suggestedFix: conflict.resolutionOptions[0]?.description
});
});
const autoSuggestions = this.generateDependencySuggestions();
suggestions.push(...autoSuggestions);
const redundancyCheck = this.detectRedundantDependencies();
warnings.push(...redundancyCheck);
return {
isValid: errors.filter(e => e.severity === 'error').length === 0,
errors,
warnings,
suggestions
};
}
detectDependencyConflicts() {
const conflicts = [];
const cycles = this.detectCycles();
cycles.forEach(cycle => {
conflicts.push({
conflictType: 'circular',
description: `Circular dependency detected: ${cycle.join(' -> ')}`,
involvedTasks: cycle,
involvedDependencies: this.getCycleDependencies(cycle),
severity: 'critical',
resolutionOptions: [
{
strategy: 'remove-dependency',
description: `Remove dependency between ${cycle[cycle.length - 1]} and ${cycle[0]}`,
impact: 'May require task reordering',
effort: 'low'
},
{
strategy: 'reorder-tasks',
description: 'Reorder tasks to eliminate circular dependency',
impact: 'Changes execution order',
effort: 'medium'
}
]
});
});
const typeConflicts = this.detectIncompatibleTypes();
conflicts.push(...typeConflicts);
const resourceConflicts = this.detectResourceContention();
conflicts.push(...resourceConflicts);
const timingConflicts = this.detectTimingConflicts();
conflicts.push(...timingConflicts);
return conflicts;
}
generateDependencySuggestions() {
const suggestions = [];
const missingSuggestions = this.suggestMissingDependencies();
suggestions.push(...missingSuggestions);
const typeSuggestions = this.suggestDependencyTypeImprovements();
suggestions.push(...typeSuggestions);
const parallelSuggestions = this.suggestParallelizationOpportunities();
suggestions.push(...parallelSuggestions);
return suggestions.sort((a, b) => b.confidence - a.confidence);
}
validateDependencyBeforeAdd(dependentTaskId, dependencyTaskId, type) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!this.nodes.has(dependentTaskId)) {
errors.push({
type: 'missing-task',
severity: 'error',
message: `Task ${dependentTaskId} does not exist`,
affectedTasks: [dependentTaskId],
suggestedFix: 'Create the task before adding dependencies'
});
}
if (!this.nodes.has(dependencyTaskId)) {
errors.push({
type: 'missing-task',
severity: 'error',
message: `Task ${dependencyTaskId} does not exist`,
affectedTasks: [dependencyTaskId],
suggestedFix: 'Create the task before adding dependencies'
});
}
if (dependentTaskId === dependencyTaskId) {
errors.push({
type: 'self-dependency',
severity: 'error',
message: 'A task cannot depend on itself',
affectedTasks: [dependentTaskId],
suggestedFix: 'Remove self-dependency'
});
}
if (this.wouldCreateCycle(dependencyTaskId, dependentTaskId)) {
errors.push({
type: 'cycle',
severity: 'error',
message: `Adding this dependency would create a circular dependency`,
affectedTasks: [dependentTaskId, dependencyTaskId],
suggestedFix: 'Reorder tasks or remove conflicting dependencies'
});
}
const validTypes = ['task', 'package', 'framework', 'tool', 'import', 'environment'];
if (!validTypes.includes(type)) {
errors.push({
type: 'invalid-type',
severity: 'error',
message: `Invalid dependency type: ${type}`,
affectedTasks: [dependentTaskId, dependencyTaskId],
suggestedFix: `Use one of: ${validTypes.join(', ')}`
});
}
const existingEdge = this.edges.get(`${dependentTaskId}->${dependencyTaskId}`);
if (existingEdge) {
warnings.push({
type: 'redundant',
message: `Dependency already exists between ${dependentTaskId} and ${dependencyTaskId}`,
affectedTasks: [dependentTaskId, dependencyTaskId],
recommendation: 'Consider updating the existing dependency instead'
});
}
return {
isValid: errors.length === 0,
errors,
warnings,
suggestions
};
}
validateDependencyTypes() {
const errors = [];
const warnings = [];
for (const [edgeId, edge] of this.edges) {
const validTypes = ['task', 'package', 'framework', 'tool', 'import', 'environment'];
if (!validTypes.includes(edge.type)) {
errors.push({
type: 'invalid-type',
severity: 'error',
message: `Invalid dependency type '${edge.type}' in edge ${edgeId}`,
affectedTasks: [edge.from, edge.to],
suggestedFix: `Change to one of: ${validTypes.join(', ')}`
});
}
const compatibility = this.checkTypeCompatibility(edge.from, edge.to, edge.type);
if (!compatibility.compatible) {
warnings.push({
type: 'potential-issue',
message: compatibility.reason,
affectedTasks: [edge.from, edge.to],
recommendation: compatibility.suggestion
});
}
}
return { errors, warnings };
}
checkTypeCompatibility(fromTaskId, toTaskId, type) {
const fromNode = this.nodes.get(fromTaskId);
const toNode = this.nodes.get(toTaskId);
if (!fromNode || !toNode) {
return { compatible: false, reason: 'One or both tasks not found' };
}
if (type === 'task' && fromNode.priority === 'low' && toNode.priority === 'critical') {
return {
compatible: false,
reason: `Low priority task ${fromTaskId} depending on critical task ${toTaskId} may indicate incorrect prioritization`,
suggestion: 'Review task priorities or dependency direction'
};
}
if (type === 'framework' || type === 'package') {
if (fromNode.estimatedHours > toNode.estimatedHours * 3) {
return {
compatible: false,
reason: `Large task depending on much smaller ${type} task may indicate missing breakdown`,
suggestion: 'Consider breaking down the larger task'
};
}
}
return { compatible: true, reason: 'Compatible' };
}
detectIncompatibleTypes() {
const conflicts = [];
const taskDependencies = new Map();
for (const [, edge] of this.edges) {
if (!taskDependencies.has(edge.from)) {
taskDependencies.set(edge.from, []);
}
taskDependencies.get(edge.from).push(edge.type);
}
for (const [taskId, types] of taskDependencies) {
const uniqueTypes = [...new Set(types)];
if (uniqueTypes.includes('framework') && uniqueTypes.includes('package') && uniqueTypes.length > 2) {
conflicts.push({
conflictType: 'incompatible-types',
description: `Task ${taskId} has conflicting dependency types: framework and package dependencies with others`,
involvedTasks: [taskId],
involvedDependencies: this.getTaskDependencyIds(taskId),
severity: 'medium',
resolutionOptions: [
{
strategy: 'change-type',
description: 'Consolidate dependency types or split task',
impact: 'May require task restructuring',
effort: 'medium'
}
]
});
}
}
return conflicts;
}
detectResourceContention() {
const conflicts = [];
const parallelBatches = this.getParallelBatches();
for (const batch of parallelBatches) {
if (batch.taskIds.length > 1) {
const resourceConflicts = this.checkBatchResourceConflicts(batch.taskIds);
conflicts.push(...resourceConflicts);
}
}
return conflicts;
}
checkBatchResourceConflicts(taskIds) {
const conflicts = [];
const highResourceTasks = taskIds.filter(taskId => {
const node = this.nodes.get(taskId);
return node && node.estimatedHours > 6;
});
if (highResourceTasks.length > 1) {
conflicts.push({
conflictType: 'resource-contention',
description: `Multiple high-resource tasks scheduled in parallel: ${highResourceTasks.join(', ')}`,
involvedTasks: highResourceTasks,
involvedDependencies: [],
severity: 'medium',
resolutionOptions: [
{
strategy: 'reorder-tasks',
description: 'Stagger high-resource tasks across different batches',
impact: 'May increase total project time but reduce resource pressure',
effort: 'low'
},
{
strategy: 'split-task',
description: 'Break down large tasks into smaller components',
impact: 'Increases task count but improves parallelization',
effort: 'high'
}
]
});
}
return conflicts;
}
detectTimingConflicts() {
const conflicts = [];
for (const [edgeId, edge] of this.edges) {
const fromNode = this.nodes.get(edge.from);
const toNode = this.nodes.get(edge.to);
if (fromNode && toNode) {
const timeRatio = fromNode.estimatedHours / toNode.estimatedHours;
if (timeRatio > 10) {
conflicts.push({
conflictType: 'timing-conflict',
description: `Large time disparity: ${edge.from} (${fromNode.estimatedHours}h) depends on ${edge.to} (${toNode.estimatedHours}h)`,
involvedTasks: [edge.from, edge.to],
involvedDependencies: [edgeId],
severity: 'low',
resolutionOptions: [
{
strategy: 'split-task',
description: `Consider breaking down ${edge.from} into smaller tasks`,
impact: 'Better parallelization and progress tracking',
effort: 'medium'
}
]
});
}
}
}
return conflicts;
}
getCycleDependencies(cycle) {
const dependencies = [];
for (let i = 0; i < cycle.length; i++) {
const from = cycle[i];
const to = cycle[(i + 1) % cycle.length];
const edgeId = `${from}->${to}`;
if (this.edges.has(edgeId)) {
dependencies.push(edgeId);
}
}
return dependencies;
}
getTaskDependencyIds(taskId) {
const dependencies = [];
for (const [edgeId, edge] of this.edges) {
if (edge.from === taskId || edge.to === taskId) {
dependencies.push(edgeId);
}
}
return dependencies;
}
suggestMissingDependencies() {
const suggestions = [];
for (const [taskId, node] of this.nodes) {
const potentialDeps = this.analyzePotentialDependencies(taskId, node);
suggestions.push(...potentialDeps);
}
return suggestions;
}
analyzePotentialDependencies(taskId, node) {
const suggestions = [];
const taskTitle = node.title.toLowerCase();
const existingDeps = new Set(node.dependencies);
for (const [otherTaskId, otherNode] of this.nodes) {
if (otherTaskId === taskId || existingDeps.has(otherTaskId))
continue;
const otherTitle = otherNode.title.toLowerCase();
let confidence = 0;
let reason = '';
let dependencyType = 'task';
if (taskTitle.includes('implement') && otherTitle.includes('setup')) {
confidence = 0.8;
reason = 'Implementation tasks typically depend on setup tasks';
dependencyType = 'task';
}
if (taskTitle.includes('install') && (otherTitle.includes('configure') || otherTitle.includes('setup'))) {
confidence = 0.7;
reason = 'Installation tasks often depend on configuration';
dependencyType = 'package';
}
if (taskTitle.includes('test') && otherTitle.includes('implement')) {
confidence = 0.9;
reason = 'Tests depend on implementation';
dependencyType = 'task';
}
if (taskTitle.includes('component') && otherTitle.includes('framework')) {
confidence = 0.6;
reason = 'Components typically depend on framework setup';
dependencyType = 'framework';
}
if (confidence > 0.5) {
suggestions.push({
type: 'add',
fromTaskId: taskId,
toTaskId: otherTaskId,
dependencyType,
reason,
confidence,
impact: confidence > 0.8 ? 'high' : confidence > 0.6 ? 'medium' : 'low'
});
}
}
return suggestions;
}
suggestDependencyTypeImprovements() {
const suggestions = [];
for (const [, edge] of this.edges) {
const fromNode = this.nodes.get(edge.from);
const toNode = this.nodes.get(edge.to);
if (!fromNode || !toNode)
continue;
const suggestedType = this.suggestBetterDependencyType(fromNode, toNode, edge.type);
if (suggestedType && suggestedType !== edge.type) {
suggestions.push({
type: 'modify',
fromTaskId: edge.from,
toTaskId: edge.to,
dependencyType: suggestedType,
reason: `Current type '${edge.type}' could be more specific as '${suggestedType}'`,
confidence: 0.6,
impact: 'low'
});
}
}
return suggestions;
}
suggestBetterDependencyType(fromNode, toNode, currentType) {
const fromTitle = fromNode.title.toLowerCase();
const toTitle = toNode.title.toLowerCase();
if (currentType === 'task') {
if (toTitle.includes('package') || toTitle.includes('install') || toTitle.includes('npm')) {
return 'package';
}
if (toTitle.includes('framework') || toTitle.includes('react') || toTitle.includes('vue') || toTitle.includes('angular')) {
return 'framework';
}
if (toTitle.includes('tool') || toTitle.includes('webpack') || toTitle.includes('babel') || toTitle.includes('eslint')) {
return 'tool';
}
if (toTitle.includes('import') || toTitle.includes('module') || fromTitle.includes('import')) {
return 'import';
}
if (toTitle.includes('environment') || toTitle.includes('env') || toTitle.includes('config')) {
return 'environment';
}
}
return null;
}
suggestParallelizationOpportunities() {
const suggestions = [];
for (const [, edge] of this.edges) {
if (this.couldBeParallelized(edge.from, edge.to)) {
suggestions.push({
type: 'remove',
fromTaskId: edge.from,
toTaskId: edge.to,
dependencyType: edge.type,
reason: 'Removing this dependency could enable parallel execution',
confidence: 0.4,
impact: 'medium'
});
}
}
return suggestions;
}
couldBeParallelized(taskId1, taskId2) {
const node1 = this.nodes.get(taskId1);
const node2 = this.nodes.get(taskId2);
if (!node1 || !node2)
return false;
const title1 = node1.title.toLowerCase();
const title2 = node2.title.toLowerCase();
const similarPatterns = [
['test', 'test'],
['component', 'component'],
['style', 'style'],
['util', 'util'],
['helper', 'helper']
];
for (const [pattern1, pattern2] of similarPatterns) {
if (title1.includes(pattern1) && title2.includes(pattern2)) {
return true;
}
}
return false;
}
detectRedundantDependencies() {
const warnings = [];
for (const [edgeId, edge] of this.edges) {
if (this.isTransitiveDependency(edge.from, edge.to)) {
warnings.push({
type: 'redundant',
message: `Dependency ${edgeId} may be redundant due to transitive dependencies`,
affectedTasks: [edge.from, edge.to],
recommendation: 'Consider removing this direct dependency if transitive path exists'
});
}
}
const taskPairs = new Map();
for (const [, edge] of this.edges) {
const pairKey = `${edge.from}->${edge.to}`;
if (!taskPairs.has(pairKey)) {
taskPairs.set(pairKey, []);
}
taskPairs.get(pairKey).push(edge.type);
}
for (const [pairKey, types] of taskPairs) {
if (types.length > 1) {
const [from, to] = pairKey.split('->');
warnings.push({
type: 'redundant',
message: `Multiple dependencies between ${from} and ${to}: ${types.join(', ')}`,
affectedTasks: [from, to],
recommendation: 'Consider consolidating into a single dependency with the most appropriate type'
});
}
}
return warnings;
}
isTransitiveDependency(fromTaskId, toTaskId) {
const visited = new Set();
const queue = [fromTaskId];
visited.add(fromTaskId);
while (queue.length > 0) {
const current = queue.shift();
const dependencies = this.reverseIndex.get(current) || new Set();
for (const dep of dependencies) {
if (dep === toTaskId)
continue;
if (dep === fromTaskId)
continue;
if (!visited.has(dep)) {
visited.add(dep);
queue.push(dep);
if (this.hasPath(dep, toTaskId)) {
return true;
}
}
}
}
return false;
}
serializeToJSON() {
const timestamp = new Date().toISOString();
const serializedData = this.createSerializedData('json', timestamp);
const checksum = this.calculateChecksum(serializedData);
return {
...serializedData,
checksum
};
}
serializeToYAML() {
const timestamp = new Date().toISOString();
const serializedData = this.createSerializedData('yaml', timestamp);
const checksum = this.calculateChecksum(serializedData);
return {
...serializedData,
checksum
};
}
async saveToFile(filePath, format = 'json', createBackup = true) {
try {
if (createBackup && await this.fileExists(filePath)) {
await this.createBackup(filePath);
}
const serializedGraph = format === 'json' ? this.serializeToJSON() : this.serializeToYAML();
const content = format === 'json'
? JSON.stringify(serializedGraph, null, 2)
: this.convertToYAML(serializedGraph);
await this.writeFile(filePath, content);
const size = Buffer.byteLength(content, 'utf8');
logger.info({
filePath,
format,
size,
checksum: serializedGraph.checksum
}, 'Graph saved successfully');
return {
success: true,
filePath,
format,
size,
checksum: serializedGraph.checksum,
timestamp: new Date()
};
}
catch (error) {
logger.error({ err: error, filePath, format }, 'Failed to save graph');
return {
success: false,
format,
size: 0,
checksum: '',
error: error instanceof Error ? error.message : String(error),
timestamp: new Date()
};
}
}
async loadFromFile(filePath) {
try {
logger.debug({ filePath }, 'Loading graph from file')