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.
Files changed (58) hide show
  1. package/README.md +291 -24
  2. package/dist/commands/add.d.ts +2 -0
  3. package/dist/commands/add.js +39 -31
  4. package/dist/commands/complete.d.ts +2 -0
  5. package/dist/commands/complete.js +35 -12
  6. package/dist/commands/init.d.ts +1 -0
  7. package/dist/commands/init.js +63 -46
  8. package/dist/commands/list.d.ts +3 -0
  9. package/dist/commands/list.js +65 -62
  10. package/dist/commands/pass-test.d.ts +4 -1
  11. package/dist/commands/pass-test.js +36 -13
  12. package/dist/commands/show.d.ts +4 -1
  13. package/dist/commands/show.js +38 -59
  14. package/dist/commands/update.d.ts +2 -0
  15. package/dist/commands/update.js +54 -31
  16. package/dist/commands/validate.d.ts +4 -1
  17. package/dist/commands/validate.js +74 -32
  18. package/dist/errors/base.error.d.ts +21 -0
  19. package/dist/errors/base.error.js +35 -0
  20. package/dist/errors/circular-dependency.error.d.ts +8 -0
  21. package/dist/errors/circular-dependency.error.js +13 -0
  22. package/dist/errors/config-not-found.error.d.ts +7 -0
  23. package/dist/errors/config-not-found.error.js +12 -0
  24. package/dist/errors/index.d.ts +16 -0
  25. package/dist/errors/index.js +26 -0
  26. package/dist/errors/invalid-task.error.d.ts +7 -0
  27. package/dist/errors/invalid-task.error.js +12 -0
  28. package/dist/errors/roadmap-not-found.error.d.ts +7 -0
  29. package/dist/errors/roadmap-not-found.error.js +12 -0
  30. package/dist/errors/task-not-found.error.d.ts +7 -0
  31. package/dist/errors/task-not-found.error.js +12 -0
  32. package/dist/errors/validation.error.d.ts +16 -0
  33. package/dist/errors/validation.error.js +16 -0
  34. package/dist/repositories/config.repository.d.ts +76 -0
  35. package/dist/repositories/config.repository.js +282 -0
  36. package/dist/repositories/index.d.ts +2 -0
  37. package/dist/repositories/index.js +2 -0
  38. package/dist/repositories/roadmap.repository.d.ts +82 -0
  39. package/dist/repositories/roadmap.repository.js +201 -0
  40. package/dist/services/display.service.d.ts +182 -0
  41. package/dist/services/display.service.js +320 -0
  42. package/dist/services/error-handler.service.d.ts +114 -0
  43. package/dist/services/error-handler.service.js +169 -0
  44. package/dist/services/roadmap.service.d.ts +142 -0
  45. package/dist/services/roadmap.service.js +269 -0
  46. package/dist/services/task-dependency.service.d.ts +210 -0
  47. package/dist/services/task-dependency.service.js +371 -0
  48. package/dist/services/task-query.service.d.ts +123 -0
  49. package/dist/services/task-query.service.js +259 -0
  50. package/dist/services/task.service.d.ts +132 -0
  51. package/dist/services/task.service.js +173 -0
  52. package/dist/util/read-config.js +12 -2
  53. package/dist/util/read-roadmap.js +12 -2
  54. package/dist/util/types.d.ts +5 -0
  55. package/dist/util/update-task.js +2 -1
  56. package/dist/util/validate-task.js +6 -5
  57. package/oclif.manifest.json +114 -5
  58. 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;