Files
ALwrity/frontend/src/services/TaskDependencyManager.ts

434 lines
11 KiB
TypeScript

import {
TodayTask,
DailyWorkflow,
WorkflowError
} from '../types/workflow';
interface DependencyGraph {
[taskId: string]: {
dependencies: string[];
dependents: string[];
status: 'ready' | 'blocked' | 'completed' | 'skipped';
};
}
interface DependencyValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
readyTasks: string[];
blockedTasks: string[];
}
class TaskDependencyManager {
private dependencyGraph: DependencyGraph = {};
/**
* Build dependency graph from workflow tasks
*/
buildDependencyGraph(workflow: DailyWorkflow): DependencyGraph {
this.dependencyGraph = {};
// Initialize all tasks in the graph
workflow.tasks.forEach(task => {
this.dependencyGraph[task.id] = {
dependencies: task.dependencies || [],
dependents: [],
status: this.getTaskStatus(task, workflow)
};
});
// Build dependent relationships
Object.keys(this.dependencyGraph).forEach(taskId => {
const task = this.dependencyGraph[taskId];
task.dependencies.forEach(depId => {
if (this.dependencyGraph[depId]) {
this.dependencyGraph[depId].dependents.push(taskId);
}
});
});
return this.dependencyGraph;
}
/**
* Validate dependency graph for issues
*/
validateDependencyGraph(workflow: DailyWorkflow): DependencyValidationResult {
const result: DependencyValidationResult = {
isValid: true,
errors: [],
warnings: [],
readyTasks: [],
blockedTasks: []
};
this.buildDependencyGraph(workflow);
// Check for circular dependencies
const circularDeps = this.detectCircularDependencies();
if (circularDeps.length > 0) {
result.isValid = false;
result.errors.push(`Circular dependencies detected: ${circularDeps.join(', ')}`);
}
// Check for missing dependencies
const missingDeps = this.detectMissingDependencies(workflow);
if (missingDeps.length > 0) {
result.isValid = false;
result.errors.push(`Missing dependencies: ${missingDeps.join(', ')}`);
}
// Check for orphaned tasks (tasks with no dependencies or dependents)
const orphanedTasks = this.detectOrphanedTasks();
if (orphanedTasks.length > 0) {
result.warnings.push(`Orphaned tasks detected: ${orphanedTasks.join(', ')}`);
}
// Categorize tasks by readiness
Object.keys(this.dependencyGraph).forEach(taskId => {
const task = this.dependencyGraph[taskId];
if (task.status === 'ready') {
result.readyTasks.push(taskId);
} else if (task.status === 'blocked') {
result.blockedTasks.push(taskId);
}
});
return result;
}
/**
* Get tasks that are ready to be executed
*/
getReadyTasks(workflow: DailyWorkflow): TodayTask[] {
this.buildDependencyGraph(workflow);
return workflow.tasks.filter(task => {
const graphTask = this.dependencyGraph[task.id];
return graphTask && graphTask.status === 'ready' && task.status === 'pending';
});
}
/**
* Get tasks that are blocked by dependencies
*/
getBlockedTasks(workflow: DailyWorkflow): TodayTask[] {
this.buildDependencyGraph(workflow);
return workflow.tasks.filter(task => {
const graphTask = this.dependencyGraph[task.id];
return graphTask && graphTask.status === 'blocked';
});
}
/**
* Get tasks that depend on a specific task
*/
getDependentTasks(taskId: string): string[] {
const task = this.dependencyGraph[taskId];
return task ? [...task.dependents] : [];
}
/**
* Get tasks that a specific task depends on
*/
getDependencyTasks(taskId: string): string[] {
const task = this.dependencyGraph[taskId];
return task ? [...task.dependencies] : [];
}
/**
* Check if a task can be executed (all dependencies satisfied)
*/
canExecuteTask(taskId: string, workflow: DailyWorkflow): boolean {
this.buildDependencyGraph(workflow);
const task = this.dependencyGraph[taskId];
if (!task) {
return false;
}
return task.status === 'ready';
}
/**
* Get the optimal execution order for tasks
*/
getOptimalExecutionOrder(workflow: DailyWorkflow): TodayTask[] {
this.buildDependencyGraph(workflow);
const visited = new Set<string>();
const visiting = new Set<string>();
const executionOrder: TodayTask[] = [];
const visit = (taskId: string) => {
if (visiting.has(taskId)) {
throw new WorkflowError({
code: 'CIRCULAR_DEPENDENCY',
message: `Circular dependency detected involving task ${taskId}`,
timestamp: new Date(),
recoverable: false
});
}
if (visited.has(taskId)) {
return;
}
visiting.add(taskId);
const task = this.dependencyGraph[taskId];
if (task) {
// Visit dependencies first
task.dependencies.forEach(depId => {
visit(depId);
});
}
visiting.delete(taskId);
visited.add(taskId);
// Add task to execution order
const workflowTask = workflow.tasks.find(t => t.id === taskId);
if (workflowTask) {
executionOrder.push(workflowTask);
}
};
// Visit all tasks
workflow.tasks.forEach(task => {
if (!visited.has(task.id)) {
visit(task.id);
}
});
return executionOrder;
}
/**
* Update task status in dependency graph
*/
updateTaskStatus(taskId: string, status: 'completed' | 'skipped' | 'in_progress'): void {
if (this.dependencyGraph[taskId]) {
// Update status of the task
this.dependencyGraph[taskId].status = status === 'in_progress' ? 'ready' : status;
// Update status of dependent tasks
this.updateDependentTasksStatus(taskId);
}
}
/**
* Get dependency chain for a task (all tasks that must be completed first)
*/
getDependencyChain(taskId: string): string[] {
const chain: string[] = [];
const visited = new Set<string>();
const buildChain = (currentTaskId: string) => {
if (visited.has(currentTaskId)) {
return;
}
visited.add(currentTaskId);
const task = this.dependencyGraph[currentTaskId];
if (task) {
task.dependencies.forEach(depId => {
buildChain(depId);
if (!chain.includes(depId)) {
chain.push(depId);
}
});
}
};
buildChain(taskId);
return chain;
}
/**
* Get impact of completing a task (what tasks become available)
*/
getCompletionImpact(taskId: string): string[] {
const impact: string[] = [];
const task = this.dependencyGraph[taskId];
if (task) {
task.dependents.forEach(dependentId => {
const dependent = this.dependencyGraph[dependentId];
if (dependent && dependent.status === 'blocked') {
// Check if all dependencies are now satisfied
const allDepsSatisfied = dependent.dependencies.every(depId => {
const depTask = this.dependencyGraph[depId];
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
});
if (allDepsSatisfied) {
impact.push(dependentId);
}
}
});
}
return impact;
}
/**
* Detect circular dependencies in the graph
*/
private detectCircularDependencies(): string[] {
const visited = new Set<string>();
const visiting = new Set<string>();
const circular: string[] = [];
const visit = (taskId: string, path: string[] = []) => {
if (visiting.has(taskId)) {
const cycleStart = path.indexOf(taskId);
if (cycleStart !== -1) {
circular.push(...path.slice(cycleStart), taskId);
}
return;
}
if (visited.has(taskId)) {
return;
}
visiting.add(taskId);
const task = this.dependencyGraph[taskId];
if (task) {
task.dependencies.forEach(depId => {
visit(depId, [...path, taskId]);
});
}
visiting.delete(taskId);
visited.add(taskId);
};
Object.keys(this.dependencyGraph).forEach(taskId => {
if (!visited.has(taskId)) {
visit(taskId);
}
});
return [...new Set(circular)];
}
/**
* Detect missing dependencies
*/
private detectMissingDependencies(workflow: DailyWorkflow): string[] {
const missing: string[] = [];
const taskIds = new Set(workflow.tasks.map(t => t.id));
Object.keys(this.dependencyGraph).forEach(taskId => {
const task = this.dependencyGraph[taskId];
task.dependencies.forEach(depId => {
if (!taskIds.has(depId)) {
missing.push(`${taskId} -> ${depId}`);
}
});
});
return missing;
}
/**
* Detect orphaned tasks
*/
private detectOrphanedTasks(): string[] {
const orphaned: string[] = [];
Object.keys(this.dependencyGraph).forEach(taskId => {
const task = this.dependencyGraph[taskId];
if (task.dependencies.length === 0 && task.dependents.length === 0) {
orphaned.push(taskId);
}
});
return orphaned;
}
/**
* Update dependent tasks status when a dependency is completed
*/
private updateDependentTasksStatus(completedTaskId: string): void {
const task = this.dependencyGraph[completedTaskId];
if (!task) {
return;
}
task.dependents.forEach(dependentId => {
const dependent = this.dependencyGraph[dependentId];
if (dependent && dependent.status === 'blocked') {
// Check if all dependencies are now satisfied
const allDepsSatisfied = dependent.dependencies.every(depId => {
const depTask = this.dependencyGraph[depId];
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
});
if (allDepsSatisfied) {
dependent.status = 'ready';
}
}
});
}
/**
* Get task status based on dependencies
*/
private getTaskStatus(task: TodayTask, workflow: DailyWorkflow): 'ready' | 'blocked' | 'completed' | 'skipped' {
if (task.status === 'completed' || task.status === 'skipped') {
return task.status;
}
if (!task.dependencies || task.dependencies.length === 0) {
return 'ready';
}
// Check if all dependencies are satisfied
const allDepsSatisfied = task.dependencies.every(depId => {
const depTask = workflow.tasks.find(t => t.id === depId);
return depTask && (depTask.status === 'completed' || depTask.status === 'skipped');
});
return allDepsSatisfied ? 'ready' : 'blocked';
}
/**
* Get dependency graph visualization data
*/
getDependencyGraphData(): {
nodes: Array<{ id: string; label: string; status: string }>;
edges: Array<{ from: string; to: string; type: string }>;
} {
const nodes = Object.keys(this.dependencyGraph).map(taskId => ({
id: taskId,
label: taskId,
status: this.dependencyGraph[taskId].status
}));
const edges: Array<{ from: string; to: string; type: string }> = [];
Object.keys(this.dependencyGraph).forEach(taskId => {
const task = this.dependencyGraph[taskId];
task.dependencies.forEach(depId => {
edges.push({
from: depId,
to: taskId,
type: 'dependency'
});
});
});
return { nodes, edges };
}
}
// Export singleton instance
export const taskDependencyManager = new TaskDependencyManager();
export default TaskDependencyManager;