project-roadmap-tracking 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +291 -24
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +39 -31
- package/dist/commands/complete.d.ts +2 -0
- package/dist/commands/complete.js +35 -12
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +63 -46
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +65 -62
- package/dist/commands/pass-test.d.ts +4 -1
- package/dist/commands/pass-test.js +36 -13
- package/dist/commands/show.d.ts +4 -1
- package/dist/commands/show.js +38 -59
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +54 -31
- package/dist/commands/validate.d.ts +4 -1
- package/dist/commands/validate.js +74 -32
- package/dist/errors/base.error.d.ts +21 -0
- package/dist/errors/base.error.js +35 -0
- package/dist/errors/circular-dependency.error.d.ts +8 -0
- package/dist/errors/circular-dependency.error.js +13 -0
- package/dist/errors/config-not-found.error.d.ts +7 -0
- package/dist/errors/config-not-found.error.js +12 -0
- package/dist/errors/index.d.ts +16 -0
- package/dist/errors/index.js +26 -0
- package/dist/errors/invalid-task.error.d.ts +7 -0
- package/dist/errors/invalid-task.error.js +12 -0
- package/dist/errors/roadmap-not-found.error.d.ts +7 -0
- package/dist/errors/roadmap-not-found.error.js +12 -0
- package/dist/errors/task-not-found.error.d.ts +7 -0
- package/dist/errors/task-not-found.error.js +12 -0
- package/dist/errors/validation.error.d.ts +16 -0
- package/dist/errors/validation.error.js +16 -0
- package/dist/repositories/config.repository.d.ts +76 -0
- package/dist/repositories/config.repository.js +282 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.js +2 -0
- package/dist/repositories/roadmap.repository.d.ts +82 -0
- package/dist/repositories/roadmap.repository.js +201 -0
- package/dist/services/display.service.d.ts +182 -0
- package/dist/services/display.service.js +320 -0
- package/dist/services/error-handler.service.d.ts +114 -0
- package/dist/services/error-handler.service.js +169 -0
- package/dist/services/roadmap.service.d.ts +142 -0
- package/dist/services/roadmap.service.js +269 -0
- package/dist/services/task-dependency.service.d.ts +210 -0
- package/dist/services/task-dependency.service.js +371 -0
- package/dist/services/task-query.service.d.ts +123 -0
- package/dist/services/task-query.service.js +259 -0
- package/dist/services/task.service.d.ts +132 -0
- package/dist/services/task.service.js +173 -0
- package/dist/util/read-config.js +12 -2
- package/dist/util/read-roadmap.js +12 -2
- package/dist/util/types.d.ts +5 -0
- package/dist/util/update-task.js +2 -1
- package/dist/util/validate-task.js +6 -5
- package/oclif.manifest.json +114 -5
- package/package.json +19 -3
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Roadmap, Task, TaskID } from '../util/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a dependency graph as an adjacency list.
|
|
4
|
+
* Maps task IDs to arrays of task IDs they depend on or block.
|
|
5
|
+
*/
|
|
6
|
+
export interface DependencyGraph {
|
|
7
|
+
/** Map of task ID to array of task IDs it blocks */
|
|
8
|
+
blocks: Map<TaskID, TaskID[]>;
|
|
9
|
+
/** Map of task ID to array of task IDs it depends on */
|
|
10
|
+
dependsOn: Map<TaskID, TaskID[]>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Represents a circular dependency cycle
|
|
14
|
+
*/
|
|
15
|
+
export interface CircularDependency {
|
|
16
|
+
/** The cycle path as an array of task IDs (e.g., ['A', 'B', 'C', 'A']) */
|
|
17
|
+
cycle: TaskID[];
|
|
18
|
+
/** Human-readable description of the cycle */
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validation error for dependencies
|
|
23
|
+
*/
|
|
24
|
+
export interface DependencyValidationError {
|
|
25
|
+
/** Error message */
|
|
26
|
+
message: string;
|
|
27
|
+
/** Related task IDs (e.g., the invalid reference) */
|
|
28
|
+
relatedTaskIds?: TaskID[];
|
|
29
|
+
/** The task ID where the error occurred */
|
|
30
|
+
taskId: TaskID;
|
|
31
|
+
/** The type of error */
|
|
32
|
+
type: 'circular' | 'invalid-reference' | 'missing-task';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* TaskDependencyService provides operations for managing and validating task dependencies.
|
|
36
|
+
* Includes dependency graph construction, circular dependency detection, and validation.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import taskDependencyService from './services/task-dependency.service.js';
|
|
41
|
+
*
|
|
42
|
+
* // Build dependency graph
|
|
43
|
+
* const graph = taskDependencyService.buildGraph(roadmap.tasks);
|
|
44
|
+
*
|
|
45
|
+
* // Detect circular dependencies
|
|
46
|
+
* const circular = taskDependencyService.detectCircular(roadmap.tasks);
|
|
47
|
+
* if (circular) {
|
|
48
|
+
* console.log('Found circular dependency:', circular.message);
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* // Validate all dependencies
|
|
52
|
+
* const errors = taskDependencyService.validateDependencies(roadmap);
|
|
53
|
+
* if (errors.length > 0) {
|
|
54
|
+
* console.log('Validation errors:', errors);
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare class TaskDependencyService {
|
|
59
|
+
/**
|
|
60
|
+
* Builds a dependency graph from an array of tasks.
|
|
61
|
+
* Creates adjacency lists for both depends-on and blocks relationships.
|
|
62
|
+
*
|
|
63
|
+
* @param tasks - The tasks to build the graph from
|
|
64
|
+
* @returns A dependency graph with both depends-on and blocks relationships
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const graph = taskDependencyService.buildGraph(roadmap.tasks);
|
|
69
|
+
* console.log('Task B-001 depends on:', graph.dependsOn.get('B-001'));
|
|
70
|
+
* console.log('Task F-001 blocks:', graph.blocks.get('F-001'));
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
buildGraph(tasks: Task[]): DependencyGraph;
|
|
74
|
+
/**
|
|
75
|
+
* Detects circular dependencies in tasks using depth-first search.
|
|
76
|
+
* Returns the first circular dependency found, or null if none exist.
|
|
77
|
+
*
|
|
78
|
+
* Uses three-color DFS algorithm:
|
|
79
|
+
* - White (unvisited): not yet explored
|
|
80
|
+
* - Gray (visiting): currently in the DFS path (back edge = cycle)
|
|
81
|
+
* - Black (visited): completely explored
|
|
82
|
+
*
|
|
83
|
+
* @param tasks - The tasks to check for circular dependencies
|
|
84
|
+
* @returns A CircularDependency object if a cycle is found, null otherwise
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const circular = taskDependencyService.detectCircular(roadmap.tasks);
|
|
89
|
+
* if (circular) {
|
|
90
|
+
* console.log('Circular dependency detected:', circular.message);
|
|
91
|
+
* // circular.cycle = ['A', 'B', 'C', 'A']
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
detectCircular(tasks: Task[]): CircularDependency | null;
|
|
96
|
+
/**
|
|
97
|
+
* Gets all tasks that are blocked by the specified task.
|
|
98
|
+
* Returns tasks where this task appears in their depends-on array.
|
|
99
|
+
*
|
|
100
|
+
* @param task - The task to find blocked tasks for
|
|
101
|
+
* @param allTasks - All tasks in the roadmap
|
|
102
|
+
* @returns Array of tasks that are blocked by this task
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const blockedTasks = taskDependencyService.getBlockedTasks(task, roadmap.tasks);
|
|
107
|
+
* console.log(`${task.id} blocks ${blockedTasks.length} tasks`);
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
getBlockedTasks(task: Task, allTasks: Task[]): Task[];
|
|
111
|
+
/**
|
|
112
|
+
* Gets all tasks that this task depends on.
|
|
113
|
+
* Returns tasks listed in this task's depends-on array.
|
|
114
|
+
*
|
|
115
|
+
* @param task - The task to find dependencies for
|
|
116
|
+
* @param allTasks - All tasks in the roadmap
|
|
117
|
+
* @returns Array of tasks that this task depends on
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const dependencies = taskDependencyService.getDependsOnTasks(task, roadmap.tasks);
|
|
122
|
+
* console.log(`${task.id} depends on ${dependencies.length} tasks`);
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
getDependsOnTasks(task: Task, allTasks: Task[]): Task[];
|
|
126
|
+
/**
|
|
127
|
+
* Sorts tasks in topological order (dependencies first).
|
|
128
|
+
* Uses Kahn's algorithm or DFS-based topological sort.
|
|
129
|
+
* Throws error if circular dependency exists.
|
|
130
|
+
*
|
|
131
|
+
* This is useful for executing tasks in the correct dependency order.
|
|
132
|
+
*
|
|
133
|
+
* @param tasks - The tasks to sort
|
|
134
|
+
* @returns Tasks sorted in dependency order
|
|
135
|
+
* @throws Error if circular dependency is detected
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* try {
|
|
140
|
+
* const sorted = taskDependencyService.topologicalSort(roadmap.tasks);
|
|
141
|
+
* console.log('Tasks in execution order:', sorted.map(t => t.id));
|
|
142
|
+
* } catch (error) {
|
|
143
|
+
* console.error('Cannot sort: circular dependency exists');
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
topologicalSort(tasks: Task[]): Task[];
|
|
148
|
+
/**
|
|
149
|
+
* Validates all dependencies in a roadmap.
|
|
150
|
+
* Checks for:
|
|
151
|
+
* - Invalid task ID references (tasks that don't exist)
|
|
152
|
+
* - Circular dependencies
|
|
153
|
+
* - Missing tasks in depends-on or blocks arrays
|
|
154
|
+
*
|
|
155
|
+
* @param roadmap - The roadmap to validate
|
|
156
|
+
* @returns Array of validation errors (empty if valid)
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* const errors = taskDependencyService.validateDependencies(roadmap);
|
|
161
|
+
* if (errors.length > 0) {
|
|
162
|
+
* for (const error of errors) {
|
|
163
|
+
* console.log(`${error.taskId}: ${error.message}`);
|
|
164
|
+
* }
|
|
165
|
+
* }
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
validateDependencies(roadmap: Roadmap): DependencyValidationError[];
|
|
169
|
+
/**
|
|
170
|
+
* Builds a unified dependency graph combining both depends-on and blocks relationships.
|
|
171
|
+
* For depends-on: adds direct edges (task → dependency)
|
|
172
|
+
* For blocks: adds reverse edges (if A blocks B, then B → A)
|
|
173
|
+
*
|
|
174
|
+
* @param tasks - The tasks to build the unified graph from
|
|
175
|
+
* @returns Adjacency list representation of the unified graph
|
|
176
|
+
*/
|
|
177
|
+
private buildUnifiedGraph;
|
|
178
|
+
/**
|
|
179
|
+
* Extracts the cycle from a DFS path when a back edge is detected.
|
|
180
|
+
* The path contains all nodes from root to the repeated node.
|
|
181
|
+
*
|
|
182
|
+
* @param path - The DFS path containing a cycle
|
|
183
|
+
* @returns The cycle as an array of task IDs
|
|
184
|
+
*/
|
|
185
|
+
private extractCycle;
|
|
186
|
+
/**
|
|
187
|
+
* Recursive DFS helper to detect cycles in the dependency graph.
|
|
188
|
+
* Uses visited set (black nodes) and path array (gray nodes).
|
|
189
|
+
*
|
|
190
|
+
* @param taskId - Current task ID being explored
|
|
191
|
+
* @param graph - The unified dependency graph
|
|
192
|
+
* @param visited - Set of fully explored nodes (black)
|
|
193
|
+
* @param path - Current DFS path (gray nodes)
|
|
194
|
+
* @returns true if cycle detected, false otherwise
|
|
195
|
+
*/
|
|
196
|
+
private hasCycle;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Singleton instance of TaskDependencyService for convenience.
|
|
200
|
+
* Import this to use the service without creating a new instance.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* import taskDependencyService from './services/task-dependency.service.js';
|
|
205
|
+
*
|
|
206
|
+
* const graph = taskDependencyService.buildGraph(tasks);
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
declare const taskDependencyService: TaskDependencyService;
|
|
210
|
+
export default taskDependencyService;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskDependencyService provides operations for managing and validating task dependencies.
|
|
3
|
+
* Includes dependency graph construction, circular dependency detection, and validation.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import taskDependencyService from './services/task-dependency.service.js';
|
|
8
|
+
*
|
|
9
|
+
* // Build dependency graph
|
|
10
|
+
* const graph = taskDependencyService.buildGraph(roadmap.tasks);
|
|
11
|
+
*
|
|
12
|
+
* // Detect circular dependencies
|
|
13
|
+
* const circular = taskDependencyService.detectCircular(roadmap.tasks);
|
|
14
|
+
* if (circular) {
|
|
15
|
+
* console.log('Found circular dependency:', circular.message);
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* // Validate all dependencies
|
|
19
|
+
* const errors = taskDependencyService.validateDependencies(roadmap);
|
|
20
|
+
* if (errors.length > 0) {
|
|
21
|
+
* console.log('Validation errors:', errors);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class TaskDependencyService {
|
|
26
|
+
/**
|
|
27
|
+
* Builds a dependency graph from an array of tasks.
|
|
28
|
+
* Creates adjacency lists for both depends-on and blocks relationships.
|
|
29
|
+
*
|
|
30
|
+
* @param tasks - The tasks to build the graph from
|
|
31
|
+
* @returns A dependency graph with both depends-on and blocks relationships
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const graph = taskDependencyService.buildGraph(roadmap.tasks);
|
|
36
|
+
* console.log('Task B-001 depends on:', graph.dependsOn.get('B-001'));
|
|
37
|
+
* console.log('Task F-001 blocks:', graph.blocks.get('F-001'));
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
buildGraph(tasks) {
|
|
41
|
+
const dependsOn = new Map();
|
|
42
|
+
const blocks = new Map();
|
|
43
|
+
// Initialize maps for all tasks
|
|
44
|
+
for (const task of tasks) {
|
|
45
|
+
dependsOn.set(task.id, task['depends-on'] || []);
|
|
46
|
+
blocks.set(task.id, task.blocks || []);
|
|
47
|
+
}
|
|
48
|
+
return { blocks, dependsOn };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Detects circular dependencies in tasks using depth-first search.
|
|
52
|
+
* Returns the first circular dependency found, or null if none exist.
|
|
53
|
+
*
|
|
54
|
+
* Uses three-color DFS algorithm:
|
|
55
|
+
* - White (unvisited): not yet explored
|
|
56
|
+
* - Gray (visiting): currently in the DFS path (back edge = cycle)
|
|
57
|
+
* - Black (visited): completely explored
|
|
58
|
+
*
|
|
59
|
+
* @param tasks - The tasks to check for circular dependencies
|
|
60
|
+
* @returns A CircularDependency object if a cycle is found, null otherwise
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const circular = taskDependencyService.detectCircular(roadmap.tasks);
|
|
65
|
+
* if (circular) {
|
|
66
|
+
* console.log('Circular dependency detected:', circular.message);
|
|
67
|
+
* // circular.cycle = ['A', 'B', 'C', 'A']
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
detectCircular(tasks) {
|
|
72
|
+
const graph = this.buildUnifiedGraph(tasks);
|
|
73
|
+
const visited = new Set();
|
|
74
|
+
for (const task of tasks) {
|
|
75
|
+
const path = [];
|
|
76
|
+
if (this.hasCycle(task.id, graph, visited, path)) {
|
|
77
|
+
const cycle = this.extractCycle(path);
|
|
78
|
+
const message = `Circular dependency detected: ${cycle.join(' -> ')}`;
|
|
79
|
+
return { cycle, message };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets all tasks that are blocked by the specified task.
|
|
86
|
+
* Returns tasks where this task appears in their depends-on array.
|
|
87
|
+
*
|
|
88
|
+
* @param task - The task to find blocked tasks for
|
|
89
|
+
* @param allTasks - All tasks in the roadmap
|
|
90
|
+
* @returns Array of tasks that are blocked by this task
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const blockedTasks = taskDependencyService.getBlockedTasks(task, roadmap.tasks);
|
|
95
|
+
* console.log(`${task.id} blocks ${blockedTasks.length} tasks`);
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
getBlockedTasks(task, allTasks) {
|
|
99
|
+
// Find all tasks that list this task in their depends-on array
|
|
100
|
+
return allTasks.filter((t) => t['depends-on'].includes(task.id));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Gets all tasks that this task depends on.
|
|
104
|
+
* Returns tasks listed in this task's depends-on array.
|
|
105
|
+
*
|
|
106
|
+
* @param task - The task to find dependencies for
|
|
107
|
+
* @param allTasks - All tasks in the roadmap
|
|
108
|
+
* @returns Array of tasks that this task depends on
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const dependencies = taskDependencyService.getDependsOnTasks(task, roadmap.tasks);
|
|
113
|
+
* console.log(`${task.id} depends on ${dependencies.length} tasks`);
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
getDependsOnTasks(task, allTasks) {
|
|
117
|
+
// Find all tasks whose IDs are in this task's depends-on array
|
|
118
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
119
|
+
return task['depends-on'].map((id) => taskMap.get(id)).filter((t) => t !== undefined);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Sorts tasks in topological order (dependencies first).
|
|
123
|
+
* Uses Kahn's algorithm or DFS-based topological sort.
|
|
124
|
+
* Throws error if circular dependency exists.
|
|
125
|
+
*
|
|
126
|
+
* This is useful for executing tasks in the correct dependency order.
|
|
127
|
+
*
|
|
128
|
+
* @param tasks - The tasks to sort
|
|
129
|
+
* @returns Tasks sorted in dependency order
|
|
130
|
+
* @throws Error if circular dependency is detected
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* try {
|
|
135
|
+
* const sorted = taskDependencyService.topologicalSort(roadmap.tasks);
|
|
136
|
+
* console.log('Tasks in execution order:', sorted.map(t => t.id));
|
|
137
|
+
* } catch (error) {
|
|
138
|
+
* console.error('Cannot sort: circular dependency exists');
|
|
139
|
+
* }
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
topologicalSort(tasks) {
|
|
143
|
+
// First check for circular dependencies
|
|
144
|
+
const circular = this.detectCircular(tasks);
|
|
145
|
+
if (circular) {
|
|
146
|
+
throw new Error(circular.message);
|
|
147
|
+
}
|
|
148
|
+
// Build task map for quick lookups
|
|
149
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
150
|
+
// Calculate in-degree for each task (number of dependencies this task has)
|
|
151
|
+
const inDegree = new Map();
|
|
152
|
+
for (const task of tasks) {
|
|
153
|
+
inDegree.set(task.id, task['depends-on'].length);
|
|
154
|
+
}
|
|
155
|
+
// Queue tasks with no dependencies (in-degree = 0)
|
|
156
|
+
// These tasks can be executed first since they don't depend on anything
|
|
157
|
+
const queue = [];
|
|
158
|
+
for (const task of tasks) {
|
|
159
|
+
if (inDegree.get(task.id) === 0) {
|
|
160
|
+
queue.push(task.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Process queue using Kahn's algorithm
|
|
164
|
+
const sorted = [];
|
|
165
|
+
while (queue.length > 0) {
|
|
166
|
+
const taskId = queue.shift();
|
|
167
|
+
const task = taskMap.get(taskId);
|
|
168
|
+
sorted.push(task);
|
|
169
|
+
// For each task that depends on this completed task, reduce its in-degree
|
|
170
|
+
for (const otherTask of tasks) {
|
|
171
|
+
if (otherTask['depends-on'].includes(taskId)) {
|
|
172
|
+
const newInDegree = (inDegree.get(otherTask.id) || 0) - 1;
|
|
173
|
+
inDegree.set(otherTask.id, newInDegree);
|
|
174
|
+
if (newInDegree === 0) {
|
|
175
|
+
queue.push(otherTask.id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return sorted;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Validates all dependencies in a roadmap.
|
|
184
|
+
* Checks for:
|
|
185
|
+
* - Invalid task ID references (tasks that don't exist)
|
|
186
|
+
* - Circular dependencies
|
|
187
|
+
* - Missing tasks in depends-on or blocks arrays
|
|
188
|
+
*
|
|
189
|
+
* @param roadmap - The roadmap to validate
|
|
190
|
+
* @returns Array of validation errors (empty if valid)
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* const errors = taskDependencyService.validateDependencies(roadmap);
|
|
195
|
+
* if (errors.length > 0) {
|
|
196
|
+
* for (const error of errors) {
|
|
197
|
+
* console.log(`${error.taskId}: ${error.message}`);
|
|
198
|
+
* }
|
|
199
|
+
* }
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
validateDependencies(roadmap) {
|
|
203
|
+
const errors = [];
|
|
204
|
+
// Step 1: Build valid task ID set for O(1) lookups
|
|
205
|
+
const validTaskIds = new Set(roadmap.tasks.map((task) => task.id));
|
|
206
|
+
// Step 2: Validate task references (check both depends-on and blocks arrays)
|
|
207
|
+
for (const task of roadmap.tasks) {
|
|
208
|
+
// Validate depends-on references
|
|
209
|
+
if (task['depends-on']) {
|
|
210
|
+
for (const depId of task['depends-on']) {
|
|
211
|
+
if (!validTaskIds.has(depId)) {
|
|
212
|
+
errors.push({
|
|
213
|
+
message: `Task ${task.id} depends on non-existent task ${depId}`,
|
|
214
|
+
relatedTaskIds: [depId],
|
|
215
|
+
taskId: task.id,
|
|
216
|
+
type: 'missing-task',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Validate blocks references
|
|
222
|
+
if (task.blocks) {
|
|
223
|
+
for (const blockId of task.blocks) {
|
|
224
|
+
if (!validTaskIds.has(blockId)) {
|
|
225
|
+
errors.push({
|
|
226
|
+
message: `Task ${task.id} blocks non-existent task ${blockId}`,
|
|
227
|
+
relatedTaskIds: [blockId],
|
|
228
|
+
taskId: task.id,
|
|
229
|
+
type: 'missing-task',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Step 3: Detect circular dependencies
|
|
236
|
+
const circular = this.detectCircular(roadmap.tasks);
|
|
237
|
+
if (circular) {
|
|
238
|
+
errors.push({
|
|
239
|
+
message: circular.message,
|
|
240
|
+
relatedTaskIds: circular.cycle,
|
|
241
|
+
taskId: circular.cycle[0],
|
|
242
|
+
type: 'circular',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Step 4: (Optional) Check for bidirectional consistency between depends-on and blocks
|
|
246
|
+
// This step is commented out to avoid overly strict validation.
|
|
247
|
+
// Uncomment if bidirectional consistency is required.
|
|
248
|
+
const taskMap = new Map(roadmap.tasks.map((t) => [t.id, t]));
|
|
249
|
+
const dependsOnMap = new Map();
|
|
250
|
+
for (const task of roadmap.tasks) {
|
|
251
|
+
dependsOnMap.set(task.id, new Set(task['depends-on'] || []));
|
|
252
|
+
}
|
|
253
|
+
for (const task of roadmap.tasks) {
|
|
254
|
+
if (task.blocks) {
|
|
255
|
+
for (const blockedId of task.blocks) {
|
|
256
|
+
const blockedTask = taskMap.get(blockedId);
|
|
257
|
+
if (blockedTask) {
|
|
258
|
+
const blockedDependsOn = dependsOnMap.get(blockedId);
|
|
259
|
+
// eslint-disable-next-line max-depth
|
|
260
|
+
if (blockedDependsOn && !blockedDependsOn.has(task.id)) {
|
|
261
|
+
// Inconsistency found: task blocks blockedId, but blockedId does not depend on task
|
|
262
|
+
errors.push({
|
|
263
|
+
message: `Inconsistency: Task ${task.id} blocks ${blockedId}, but ${blockedId} does not depend on ${task.id}`,
|
|
264
|
+
relatedTaskIds: [blockedId],
|
|
265
|
+
taskId: task.id,
|
|
266
|
+
type: 'invalid-reference',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/*
|
|
274
|
+
Note: Bidirectional consistency checking (blocks <-> depends-on symmetry)
|
|
275
|
+
was considered but removed as it was too strict for practical use cases.
|
|
276
|
+
The blocks and depends-on relationships can be used independently without
|
|
277
|
+
requiring full symmetry.
|
|
278
|
+
*/
|
|
279
|
+
return errors;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Builds a unified dependency graph combining both depends-on and blocks relationships.
|
|
283
|
+
* For depends-on: adds direct edges (task → dependency)
|
|
284
|
+
* For blocks: adds reverse edges (if A blocks B, then B → A)
|
|
285
|
+
*
|
|
286
|
+
* @param tasks - The tasks to build the unified graph from
|
|
287
|
+
* @returns Adjacency list representation of the unified graph
|
|
288
|
+
*/
|
|
289
|
+
buildUnifiedGraph(tasks) {
|
|
290
|
+
const graph = new Map();
|
|
291
|
+
// Initialize empty adjacency lists for all tasks
|
|
292
|
+
for (const task of tasks) {
|
|
293
|
+
graph.set(task.id, []);
|
|
294
|
+
}
|
|
295
|
+
// Add edges from both depends-on and blocks relationships
|
|
296
|
+
for (const task of tasks) {
|
|
297
|
+
const edges = graph.get(task.id) || [];
|
|
298
|
+
// Add depends-on edges (task → dependency)
|
|
299
|
+
for (const dependency of task['depends-on'] || []) {
|
|
300
|
+
edges.push(dependency);
|
|
301
|
+
}
|
|
302
|
+
// Add blocks edges in reverse (if A blocks B, then B → A)
|
|
303
|
+
for (const blockedTask of task.blocks || []) {
|
|
304
|
+
const blockedEdges = graph.get(blockedTask) || [];
|
|
305
|
+
blockedEdges.push(task.id);
|
|
306
|
+
graph.set(blockedTask, blockedEdges);
|
|
307
|
+
}
|
|
308
|
+
graph.set(task.id, edges);
|
|
309
|
+
}
|
|
310
|
+
return graph;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Extracts the cycle from a DFS path when a back edge is detected.
|
|
314
|
+
* The path contains all nodes from root to the repeated node.
|
|
315
|
+
*
|
|
316
|
+
* @param path - The DFS path containing a cycle
|
|
317
|
+
* @returns The cycle as an array of task IDs
|
|
318
|
+
*/
|
|
319
|
+
extractCycle(path) {
|
|
320
|
+
const repeatedNode = path.at(-1);
|
|
321
|
+
const firstOccurrence = path.indexOf(repeatedNode);
|
|
322
|
+
return path.slice(firstOccurrence);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Recursive DFS helper to detect cycles in the dependency graph.
|
|
326
|
+
* Uses visited set (black nodes) and path array (gray nodes).
|
|
327
|
+
*
|
|
328
|
+
* @param taskId - Current task ID being explored
|
|
329
|
+
* @param graph - The unified dependency graph
|
|
330
|
+
* @param visited - Set of fully explored nodes (black)
|
|
331
|
+
* @param path - Current DFS path (gray nodes)
|
|
332
|
+
* @returns true if cycle detected, false otherwise
|
|
333
|
+
*/
|
|
334
|
+
hasCycle(taskId, graph, visited, path) {
|
|
335
|
+
// Back edge detected - node already in current path
|
|
336
|
+
if (path.includes(taskId)) {
|
|
337
|
+
path.push(taskId); // Add repeated node to complete the cycle
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
// Already fully explored this node
|
|
341
|
+
if (visited.has(taskId)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
// Mark as visiting (gray node)
|
|
345
|
+
visited.add(taskId);
|
|
346
|
+
path.push(taskId);
|
|
347
|
+
// Explore all neighbors
|
|
348
|
+
const neighbors = graph.get(taskId) || [];
|
|
349
|
+
for (const neighbor of neighbors) {
|
|
350
|
+
if (this.hasCycle(neighbor, graph, visited, path)) {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Backtrack - remove from path (node becomes black)
|
|
355
|
+
path.pop();
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Singleton instance of TaskDependencyService for convenience.
|
|
361
|
+
* Import this to use the service without creating a new instance.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```typescript
|
|
365
|
+
* import taskDependencyService from './services/task-dependency.service.js';
|
|
366
|
+
*
|
|
367
|
+
* const graph = taskDependencyService.buildGraph(tasks);
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
const taskDependencyService = new TaskDependencyService();
|
|
371
|
+
export default taskDependencyService;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { PRIORITY, STATUS, Task, TASK_TYPE } from '../util/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sort order for sorting operations
|
|
4
|
+
*/
|
|
5
|
+
export declare enum SortOrder {
|
|
6
|
+
Ascending = "asc",
|
|
7
|
+
Descending = "desc"
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fields that can be used for sorting tasks
|
|
11
|
+
*/
|
|
12
|
+
export type SortField = 'createdAt' | 'dueDate' | 'effort' | 'priority' | 'status' | 'title' | 'type' | 'updatedAt';
|
|
13
|
+
/**
|
|
14
|
+
* Criteria for filtering tasks
|
|
15
|
+
*/
|
|
16
|
+
export interface FilterCriteria {
|
|
17
|
+
/** Filter by assigned user */
|
|
18
|
+
assignedTo?: null | string;
|
|
19
|
+
/** Filter by whether task blocks others */
|
|
20
|
+
hasBlocks?: boolean;
|
|
21
|
+
/** Filter by whether task has dependencies */
|
|
22
|
+
hasDependencies?: boolean;
|
|
23
|
+
/** Filter by priority level */
|
|
24
|
+
priority?: PRIORITY;
|
|
25
|
+
/** Filter by status (single status or array of statuses) */
|
|
26
|
+
status?: STATUS | STATUS[];
|
|
27
|
+
/** Filter by tags (tasks must have all specified tags) */
|
|
28
|
+
tags?: Array<string>;
|
|
29
|
+
/** Filter by type */
|
|
30
|
+
type?: TASK_TYPE;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* TaskQueryService provides operations for querying, filtering, and sorting tasks.
|
|
34
|
+
* All operations are pure functions that do not mutate the input arrays.
|
|
35
|
+
*/
|
|
36
|
+
export declare class TaskQueryService {
|
|
37
|
+
/**
|
|
38
|
+
* Filters tasks based on the provided criteria.
|
|
39
|
+
* Returns tasks that match ALL specified criteria (AND logic).
|
|
40
|
+
*
|
|
41
|
+
* @param tasks - The tasks to filter
|
|
42
|
+
* @param criteria - The filter criteria to apply
|
|
43
|
+
* @returns A new array of tasks matching the criteria
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const highPriorityTasks = taskQueryService.filter(tasks, {
|
|
48
|
+
* priority: PRIORITY.High,
|
|
49
|
+
* status: STATUS.InProgress
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
filter(tasks: Array<Task>, criteria: FilterCriteria): Array<Task>;
|
|
54
|
+
/**
|
|
55
|
+
* Gets all tasks with a specific status.
|
|
56
|
+
* This is a convenience method that uses the filter method.
|
|
57
|
+
*
|
|
58
|
+
* @param tasks - The tasks to search
|
|
59
|
+
* @param status - The status to filter by
|
|
60
|
+
* @returns A new array of tasks with the specified status
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const completedTasks = taskQueryService.getByStatus(tasks, STATUS.Completed);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
getByStatus(tasks: Array<Task>, status: STATUS): Array<Task>;
|
|
68
|
+
/**
|
|
69
|
+
* Gets all tasks of a specific type.
|
|
70
|
+
* This is a convenience method that uses the filter method.
|
|
71
|
+
*
|
|
72
|
+
* @param tasks - The tasks to search
|
|
73
|
+
* @param type - The type to filter by
|
|
74
|
+
* @returns A new array of tasks with the specified type
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const featureTasks = taskQueryService.getByType(tasks, TASK_TYPE.Feature);
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
getByType(tasks: Array<Task>, type: TASK_TYPE): Array<Task>;
|
|
82
|
+
/**
|
|
83
|
+
* Searches for tasks matching a query string in title or details.
|
|
84
|
+
* The search is case-insensitive.
|
|
85
|
+
*
|
|
86
|
+
* @param tasks - The tasks to search
|
|
87
|
+
* @param query - The search query string
|
|
88
|
+
* @returns A new array of tasks matching the query
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const loginTasks = taskQueryService.search(tasks, 'login');
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
search(tasks: Array<Task>, query: string): Array<Task>;
|
|
96
|
+
/**
|
|
97
|
+
* Sorts tasks by the specified field and order.
|
|
98
|
+
* Returns a new sorted array without mutating the original.
|
|
99
|
+
*
|
|
100
|
+
* @param tasks - The tasks to sort
|
|
101
|
+
* @param field - The field to sort by
|
|
102
|
+
* @param order - The sort order (ascending or descending)
|
|
103
|
+
* @returns A new sorted array of tasks
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* const sorted = taskQueryService.sort(tasks, 'priority', SortOrder.Descending);
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
sort(tasks: Array<Task>, field: SortField, order?: SortOrder): Array<Task>;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Default export instance of TaskQueryService for convenience.
|
|
114
|
+
* Can be imported and used directly without instantiation.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* import taskQueryService from './services/task-query.service.js';
|
|
119
|
+
* const filtered = taskQueryService.filter(tasks, { status: STATUS.InProgress });
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare const _default: TaskQueryService;
|
|
123
|
+
export default _default;
|