Alwrity today's tasks workflow implementation plan.
This commit is contained in:
433
frontend/src/services/TaskDependencyManager.ts
Normal file
433
frontend/src/services/TaskDependencyManager.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user