@task-mcp/shared 1.0.22 → 1.0.24

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 (184) hide show
  1. package/dist/algorithms/critical-path.d.ts +47 -0
  2. package/dist/algorithms/critical-path.d.ts.map +1 -0
  3. package/dist/algorithms/critical-path.js +340 -0
  4. package/dist/algorithms/critical-path.js.map +1 -0
  5. package/dist/algorithms/critical-path.test.d.ts +2 -0
  6. package/dist/algorithms/critical-path.test.d.ts.map +1 -0
  7. package/dist/algorithms/critical-path.test.js +184 -0
  8. package/dist/algorithms/critical-path.test.js.map +1 -0
  9. package/dist/algorithms/dependency-integrity.d.ts +81 -0
  10. package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
  11. package/dist/algorithms/dependency-integrity.js +209 -0
  12. package/dist/algorithms/dependency-integrity.js.map +1 -0
  13. package/dist/algorithms/dependency-integrity.test.d.ts +2 -0
  14. package/dist/algorithms/dependency-integrity.test.d.ts.map +1 -0
  15. package/dist/algorithms/dependency-integrity.test.js +296 -0
  16. package/dist/algorithms/dependency-integrity.test.js.map +1 -0
  17. package/dist/algorithms/index.d.ts +5 -0
  18. package/dist/algorithms/index.d.ts.map +1 -0
  19. package/dist/algorithms/index.js +5 -0
  20. package/dist/algorithms/index.js.map +1 -0
  21. package/dist/algorithms/tech-analysis.d.ts +106 -0
  22. package/dist/algorithms/tech-analysis.d.ts.map +1 -0
  23. package/dist/algorithms/tech-analysis.js +351 -0
  24. package/dist/algorithms/tech-analysis.js.map +1 -0
  25. package/dist/algorithms/tech-analysis.test.d.ts +2 -0
  26. package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
  27. package/dist/algorithms/tech-analysis.test.js +330 -0
  28. package/dist/algorithms/tech-analysis.test.js.map +1 -0
  29. package/dist/algorithms/topological-sort.d.ts +58 -0
  30. package/dist/algorithms/topological-sort.d.ts.map +1 -0
  31. package/dist/algorithms/topological-sort.js +201 -0
  32. package/dist/algorithms/topological-sort.js.map +1 -0
  33. package/dist/algorithms/topological-sort.test.d.ts +2 -0
  34. package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
  35. package/dist/algorithms/topological-sort.test.js +154 -0
  36. package/dist/algorithms/topological-sort.test.js.map +1 -0
  37. package/dist/index.d.ts +4 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +7 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/schemas/inbox.d.ts +55 -0
  42. package/dist/schemas/inbox.d.ts.map +1 -0
  43. package/dist/schemas/inbox.js +25 -0
  44. package/dist/schemas/inbox.js.map +1 -0
  45. package/dist/schemas/index.d.ts +7 -0
  46. package/dist/schemas/index.d.ts.map +1 -0
  47. package/dist/schemas/index.js +17 -0
  48. package/dist/schemas/index.js.map +1 -0
  49. package/dist/schemas/llm-guide.d.ts +147 -0
  50. package/dist/schemas/llm-guide.d.ts.map +1 -0
  51. package/dist/schemas/llm-guide.js +72 -0
  52. package/dist/schemas/llm-guide.js.map +1 -0
  53. package/dist/schemas/project.d.ts +177 -0
  54. package/dist/schemas/project.d.ts.map +1 -0
  55. package/dist/schemas/project.js +56 -0
  56. package/dist/schemas/project.js.map +1 -0
  57. package/dist/schemas/response-format.d.ts +148 -0
  58. package/dist/schemas/response-format.d.ts.map +1 -0
  59. package/dist/schemas/response-format.js +18 -0
  60. package/dist/schemas/response-format.js.map +1 -0
  61. package/dist/schemas/response-schema.d.ts +307 -0
  62. package/dist/schemas/response-schema.d.ts.map +1 -0
  63. package/dist/schemas/response-schema.js +78 -0
  64. package/dist/schemas/response-schema.js.map +1 -0
  65. package/dist/schemas/response-schema.test.d.ts +2 -0
  66. package/dist/schemas/response-schema.test.d.ts.map +1 -0
  67. package/dist/schemas/response-schema.test.js +256 -0
  68. package/dist/schemas/response-schema.test.js.map +1 -0
  69. package/dist/schemas/state.d.ts +17 -0
  70. package/dist/schemas/state.d.ts.map +1 -0
  71. package/dist/schemas/state.js +17 -0
  72. package/dist/schemas/state.js.map +1 -0
  73. package/dist/schemas/task.d.ts +881 -0
  74. package/dist/schemas/task.d.ts.map +1 -0
  75. package/dist/schemas/task.js +177 -0
  76. package/dist/schemas/task.js.map +1 -0
  77. package/dist/schemas/view.d.ts +143 -0
  78. package/dist/schemas/view.d.ts.map +1 -0
  79. package/dist/schemas/view.js +48 -0
  80. package/dist/schemas/view.js.map +1 -0
  81. package/dist/utils/dashboard-renderer.d.ts +93 -0
  82. package/dist/utils/dashboard-renderer.d.ts.map +1 -0
  83. package/dist/utils/dashboard-renderer.js +416 -0
  84. package/dist/utils/dashboard-renderer.js.map +1 -0
  85. package/dist/utils/dashboard-renderer.test.d.ts +2 -0
  86. package/dist/utils/dashboard-renderer.test.d.ts.map +1 -0
  87. package/dist/utils/dashboard-renderer.test.js +772 -0
  88. package/dist/utils/dashboard-renderer.test.js.map +1 -0
  89. package/dist/utils/date.d.ts +94 -0
  90. package/dist/utils/date.d.ts.map +1 -0
  91. package/dist/utils/date.js +323 -0
  92. package/dist/utils/date.js.map +1 -0
  93. package/dist/utils/date.test.d.ts +2 -0
  94. package/dist/utils/date.test.d.ts.map +1 -0
  95. package/dist/utils/date.test.js +276 -0
  96. package/dist/utils/date.test.js.map +1 -0
  97. package/dist/utils/hierarchy.d.ts +102 -0
  98. package/dist/utils/hierarchy.d.ts.map +1 -0
  99. package/dist/utils/hierarchy.js +236 -0
  100. package/dist/utils/hierarchy.js.map +1 -0
  101. package/dist/utils/hierarchy.test.d.ts +2 -0
  102. package/dist/utils/hierarchy.test.d.ts.map +1 -0
  103. package/dist/utils/hierarchy.test.js +423 -0
  104. package/dist/utils/hierarchy.test.js.map +1 -0
  105. package/dist/utils/id.d.ts +60 -0
  106. package/dist/utils/id.d.ts.map +1 -0
  107. package/dist/utils/id.js +118 -0
  108. package/dist/utils/id.js.map +1 -0
  109. package/dist/utils/id.test.d.ts +2 -0
  110. package/dist/utils/id.test.d.ts.map +1 -0
  111. package/dist/utils/id.test.js +193 -0
  112. package/dist/utils/id.test.js.map +1 -0
  113. package/dist/utils/index.d.ts +12 -0
  114. package/dist/utils/index.d.ts.map +1 -0
  115. package/dist/utils/index.js +34 -0
  116. package/dist/utils/index.js.map +1 -0
  117. package/dist/utils/natural-language.d.ts +111 -0
  118. package/dist/utils/natural-language.d.ts.map +1 -0
  119. package/dist/utils/natural-language.js +297 -0
  120. package/dist/utils/natural-language.js.map +1 -0
  121. package/dist/utils/natural-language.test.d.ts +2 -0
  122. package/dist/utils/natural-language.test.d.ts.map +1 -0
  123. package/dist/utils/natural-language.test.js +197 -0
  124. package/dist/utils/natural-language.test.js.map +1 -0
  125. package/dist/utils/priority-queue.d.ts +17 -0
  126. package/dist/utils/priority-queue.d.ts.map +1 -0
  127. package/dist/utils/priority-queue.js +62 -0
  128. package/dist/utils/priority-queue.js.map +1 -0
  129. package/dist/utils/priority-queue.test.d.ts +2 -0
  130. package/dist/utils/priority-queue.test.d.ts.map +1 -0
  131. package/dist/utils/priority-queue.test.js +82 -0
  132. package/dist/utils/priority-queue.test.js.map +1 -0
  133. package/dist/utils/projection.d.ts +65 -0
  134. package/dist/utils/projection.d.ts.map +1 -0
  135. package/dist/utils/projection.js +180 -0
  136. package/dist/utils/projection.js.map +1 -0
  137. package/dist/utils/projection.test.d.ts +2 -0
  138. package/dist/utils/projection.test.d.ts.map +1 -0
  139. package/dist/utils/projection.test.js +341 -0
  140. package/dist/utils/projection.test.js.map +1 -0
  141. package/dist/utils/terminal-ui.d.ts +208 -0
  142. package/dist/utils/terminal-ui.d.ts.map +1 -0
  143. package/dist/utils/terminal-ui.js +614 -0
  144. package/dist/utils/terminal-ui.js.map +1 -0
  145. package/dist/utils/terminal-ui.test.d.ts +2 -0
  146. package/dist/utils/terminal-ui.test.d.ts.map +1 -0
  147. package/dist/utils/terminal-ui.test.js +683 -0
  148. package/dist/utils/terminal-ui.test.js.map +1 -0
  149. package/dist/utils/workspace.d.ts +102 -0
  150. package/dist/utils/workspace.d.ts.map +1 -0
  151. package/dist/utils/workspace.js +183 -0
  152. package/dist/utils/workspace.js.map +1 -0
  153. package/dist/utils/workspace.test.d.ts +2 -0
  154. package/dist/utils/workspace.test.d.ts.map +1 -0
  155. package/dist/utils/workspace.test.js +97 -0
  156. package/dist/utils/workspace.test.js.map +1 -0
  157. package/package.json +5 -1
  158. package/src/algorithms/critical-path.test.ts +227 -0
  159. package/src/algorithms/critical-path.ts +14 -34
  160. package/src/algorithms/dependency-integrity.test.ts +335 -0
  161. package/src/algorithms/dependency-integrity.ts +4 -13
  162. package/src/algorithms/tech-analysis.test.ts +405 -0
  163. package/src/algorithms/tech-analysis.ts +27 -27
  164. package/src/algorithms/topological-sort.test.ts +182 -0
  165. package/src/algorithms/topological-sort.ts +6 -10
  166. package/src/schemas/index.ts +2 -13
  167. package/src/schemas/response-format.ts +6 -6
  168. package/src/schemas/response-schema.test.ts +314 -0
  169. package/src/schemas/response-schema.ts +25 -20
  170. package/src/schemas/task.ts +4 -22
  171. package/src/utils/dashboard-renderer.test.ts +976 -0
  172. package/src/utils/dashboard-renderer.ts +27 -59
  173. package/src/utils/date.test.ts +329 -0
  174. package/src/utils/date.ts +2 -10
  175. package/src/utils/hierarchy.test.ts +488 -0
  176. package/src/utils/hierarchy.ts +4 -5
  177. package/src/utils/id.test.ts +235 -0
  178. package/src/utils/index.ts +7 -1
  179. package/src/utils/natural-language.test.ts +234 -0
  180. package/src/utils/priority-queue.test.ts +103 -0
  181. package/src/utils/projection.test.ts +430 -0
  182. package/src/utils/terminal-ui.test.ts +831 -0
  183. package/src/utils/terminal-ui.ts +53 -54
  184. package/src/utils/workspace.test.ts +125 -0
@@ -0,0 +1,227 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { criticalPathAnalysis, findParallelTasks, suggestNextTask } from "./critical-path.js";
3
+ import type { Task } from "../schemas/task.js";
4
+
5
+ // Helper to create mock tasks
6
+ function createTask(
7
+ id: string,
8
+ options: {
9
+ priority?: string;
10
+ deps?: string[];
11
+ estimate?: number;
12
+ status?: string;
13
+ contexts?: string[];
14
+ } = {}
15
+ ): Task {
16
+ const task: Task = {
17
+ id,
18
+ title: `Task ${id}`,
19
+ status: (options.status ?? "pending") as Task["status"],
20
+ priority: (options.priority ?? "medium") as Task["priority"],
21
+ workspace: "test-workspace",
22
+ createdAt: new Date().toISOString(),
23
+ updatedAt: new Date().toISOString(),
24
+ dependencies: (options.deps ?? []).map((depId) => ({
25
+ taskId: depId,
26
+ type: "blocked_by" as const,
27
+ })),
28
+ };
29
+ if (options.estimate) {
30
+ task.estimate = { expected: options.estimate, confidence: "medium" as const };
31
+ }
32
+ if (options.contexts) {
33
+ task.contexts = options.contexts;
34
+ }
35
+ return task;
36
+ }
37
+
38
+ describe("criticalPathAnalysis", () => {
39
+ test("returns empty result for empty input", () => {
40
+ const result = criticalPathAnalysis([]);
41
+ expect(result.tasks).toEqual([]);
42
+ expect(result.criticalPath).toEqual([]);
43
+ expect(result.projectDuration).toBe(0);
44
+ });
45
+
46
+ test("returns empty for all completed tasks", () => {
47
+ const tasks = [createTask("A", { status: "completed" })];
48
+ const result = criticalPathAnalysis(tasks);
49
+ expect(result.tasks).toEqual([]);
50
+ });
51
+
52
+ test("calculates single task correctly", () => {
53
+ const tasks = [createTask("A", { estimate: 60 })];
54
+ const result = criticalPathAnalysis(tasks);
55
+
56
+ expect(result.projectDuration).toBe(60);
57
+ expect(result.criticalPath.length).toBe(1);
58
+ expect(result.criticalPath[0]!.id).toBe("A");
59
+ expect(result.criticalPath[0]!.isCritical).toBe(true);
60
+ });
61
+
62
+ test("identifies critical path in linear chain", () => {
63
+ // A -> B -> C (each 30 min)
64
+ const tasks = [
65
+ createTask("A", { estimate: 30 }),
66
+ createTask("B", { estimate: 30, deps: ["A"] }),
67
+ createTask("C", { estimate: 30, deps: ["B"] }),
68
+ ];
69
+ const result = criticalPathAnalysis(tasks);
70
+
71
+ expect(result.projectDuration).toBe(90);
72
+ expect(result.criticalPath.length).toBe(3);
73
+ expect(result.criticalPath.map((t) => t.id)).toEqual(["A", "B", "C"]);
74
+ });
75
+
76
+ test("calculates slack for parallel tasks", () => {
77
+ // A (60) and B (30) both lead to C
78
+ // A is on critical path, B has 30 min slack
79
+ const tasks = [
80
+ createTask("A", { estimate: 60 }),
81
+ createTask("B", { estimate: 30 }),
82
+ createTask("C", { estimate: 30, deps: ["A", "B"] }),
83
+ ];
84
+ const result = criticalPathAnalysis(tasks);
85
+
86
+ expect(result.projectDuration).toBe(90); // A + C
87
+
88
+ const taskA = result.tasks.find((t) => t.id === "A")!;
89
+ const taskB = result.tasks.find((t) => t.id === "B")!;
90
+ const taskC = result.tasks.find((t) => t.id === "C")!;
91
+
92
+ expect(taskA.isCritical).toBe(true);
93
+ expect(taskB.slack).toBe(30); // B can start 30 min late
94
+ expect(taskC.isCritical).toBe(true);
95
+ });
96
+
97
+ test("identifies bottlenecks by dependent count", () => {
98
+ // A blocks B, C, D
99
+ const tasks = [
100
+ createTask("A", { estimate: 30 }),
101
+ createTask("B", { estimate: 30, deps: ["A"] }),
102
+ createTask("C", { estimate: 30, deps: ["A"] }),
103
+ createTask("D", { estimate: 30, deps: ["A"] }),
104
+ ];
105
+ const result = criticalPathAnalysis(tasks);
106
+
107
+ expect(result.bottlenecks.length).toBeGreaterThan(0);
108
+ expect(result.bottlenecks[0]!.id).toBe("A"); // A blocks the most tasks
109
+ });
110
+ });
111
+
112
+ describe("findParallelTasks", () => {
113
+ test("returns empty for empty input", () => {
114
+ const result = findParallelTasks([]);
115
+ expect(result).toEqual([]);
116
+ });
117
+
118
+ test("returns single group for independent tasks", () => {
119
+ const tasks = [createTask("A"), createTask("B"), createTask("C")];
120
+ const result = findParallelTasks(tasks);
121
+
122
+ expect(result.length).toBe(1);
123
+ expect(result[0]!.length).toBe(3);
124
+ });
125
+
126
+ test("excludes tasks with uncompleted dependencies", () => {
127
+ const tasks = [createTask("A"), createTask("B", { deps: ["A"] })];
128
+ const result = findParallelTasks(tasks);
129
+
130
+ // Only A is available (B is blocked)
131
+ expect(result.length).toBe(1);
132
+ expect(result[0]!.length).toBe(1);
133
+ expect(result[0]![0]!.id).toBe("A");
134
+ });
135
+
136
+ test("includes task when dependency is completed", () => {
137
+ const tasks = [
138
+ createTask("A", { status: "completed" }),
139
+ createTask("B", { deps: ["A"] }),
140
+ createTask("C"),
141
+ ];
142
+ const result = findParallelTasks(tasks);
143
+
144
+ // B and C can run in parallel
145
+ expect(result.length).toBe(1);
146
+ expect(result[0]!.map((t) => t.id).sort()).toEqual(["B", "C"]);
147
+ });
148
+
149
+ test("excludes completed tasks from result", () => {
150
+ const tasks = [createTask("A", { status: "completed" }), createTask("B")];
151
+ const result = findParallelTasks(tasks);
152
+
153
+ expect(result.length).toBe(1);
154
+ expect(result[0]!.length).toBe(1);
155
+ expect(result[0]![0]!.id).toBe("B");
156
+ });
157
+ });
158
+
159
+ describe("suggestNextTask", () => {
160
+ test("returns null for empty input", () => {
161
+ const result = suggestNextTask([]);
162
+ expect(result).toBeNull();
163
+ });
164
+
165
+ test("returns null when all tasks completed", () => {
166
+ const tasks = [createTask("A", { status: "completed" })];
167
+ const result = suggestNextTask(tasks);
168
+ expect(result).toBeNull();
169
+ });
170
+
171
+ test("prefers critical path tasks", () => {
172
+ // A is critical (longer), B has slack
173
+ const tasks = [
174
+ createTask("A", { estimate: 60, priority: "low" }),
175
+ createTask("B", { estimate: 30, priority: "high" }),
176
+ createTask("C", { estimate: 30, deps: ["A", "B"] }),
177
+ ];
178
+ const result = suggestNextTask(tasks);
179
+
180
+ // A is on critical path, should be suggested first
181
+ expect(result!.id).toBe("A");
182
+ });
183
+
184
+ test("filters by context when specified", () => {
185
+ const tasks = [
186
+ createTask("A", { priority: "critical", contexts: ["office"] }),
187
+ createTask("B", { priority: "high", contexts: ["focus"] }),
188
+ ];
189
+ const result = suggestNextTask(tasks, { contexts: ["focus"] });
190
+
191
+ expect(result!.id).toBe("B");
192
+ });
193
+
194
+ test("filters by max time when specified", () => {
195
+ const tasks = [
196
+ createTask("A", { estimate: 120, priority: "critical" }),
197
+ createTask("B", { estimate: 30, priority: "high" }),
198
+ ];
199
+ const result = suggestNextTask(tasks, { maxMinutes: 60 });
200
+
201
+ expect(result!.id).toBe("B");
202
+ });
203
+
204
+ test("skips blocked tasks", () => {
205
+ const tasks = [
206
+ createTask("A", { priority: "low" }),
207
+ createTask("B", { priority: "critical", deps: ["A"] }),
208
+ ];
209
+ const result = suggestNextTask(tasks);
210
+
211
+ // B is blocked, so A should be suggested
212
+ expect(result!.id).toBe("A");
213
+ });
214
+
215
+ test("considers tasks with more dependents", () => {
216
+ const tasks = [
217
+ createTask("A", { priority: "medium" }),
218
+ createTask("B", { priority: "medium" }),
219
+ createTask("C", { priority: "medium", deps: ["A"] }),
220
+ createTask("D", { priority: "medium", deps: ["A"] }),
221
+ ];
222
+ const result = suggestNextTask(tasks);
223
+
224
+ // A blocks more tasks (C and D), should be preferred over B
225
+ expect(result!.id).toBe("A");
226
+ });
227
+ });
@@ -1,9 +1,5 @@
1
1
  import type { Task } from "../schemas/task.js";
2
- import {
3
- topologicalSort,
4
- findDependents,
5
- priorityToNumber,
6
- } from "./topological-sort.js";
2
+ import { topologicalSort, priorityToNumber } from "./topological-sort.js";
7
3
 
8
4
  /**
9
5
  * Task with computed CPM (Critical Path Method) values
@@ -43,9 +39,7 @@ function getTaskDuration(task: Task): number {
43
39
  * Get blocked_by dependencies for a task
44
40
  */
45
41
  function getBlockedByDeps(task: Task): string[] {
46
- return (task.dependencies ?? [])
47
- .filter((d) => d.type === "blocked_by")
48
- .map((d) => d.taskId);
42
+ return (task.dependencies ?? []).filter((d) => d.type === "blocked_by").map((d) => d.taskId);
49
43
  }
50
44
 
51
45
  /**
@@ -110,7 +104,10 @@ function calculateProjectDuration(taskMap: Map<string, CPMTask>): number {
110
104
  * This is O(n * d) where d is average dependencies, done once upfront
111
105
  * Allows O(1) successor lookup instead of O(n) per task
112
106
  */
113
- function buildSuccessorIndex(sortedTasks: Task[], taskMap: Map<string, CPMTask>): Map<string, CPMTask[]> {
107
+ function buildSuccessorIndex(
108
+ sortedTasks: Task[],
109
+ taskMap: Map<string, CPMTask>
110
+ ): Map<string, CPMTask[]> {
114
111
  const successorIndex = new Map<string, CPMTask[]>();
115
112
 
116
113
  // Initialize empty arrays for all tasks
@@ -244,9 +241,7 @@ function extractCriticalPath(sortedTasks: Task[], taskMap: Map<string, CPMTask>)
244
241
  * Find top bottlenecks (critical tasks blocking the most downstream work)
245
242
  */
246
243
  function findBottlenecks(criticalPath: CPMTask[], limit = 5): CPMTask[] {
247
- return [...criticalPath]
248
- .sort((a, b) => b.dependentCount - a.dependentCount)
249
- .slice(0, limit);
244
+ return [...criticalPath].sort((a, b) => b.dependentCount - a.dependentCount).slice(0, limit);
250
245
  }
251
246
 
252
247
  /**
@@ -262,9 +257,7 @@ function findBottlenecks(criticalPath: CPMTask[], limit = 5): CPMTask[] {
262
257
  */
263
258
  export function criticalPathAnalysis(tasks: Task[]): CPMResult {
264
259
  // Filter to only pending/in_progress tasks
265
- const activeTasks = tasks.filter(
266
- (t) => t.status === "pending" || t.status === "in_progress"
267
- );
260
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
268
261
 
269
262
  if (activeTasks.length === 0) {
270
263
  return {
@@ -318,16 +311,12 @@ export function criticalPathAnalysis(tasks: Task[]): CPMResult {
318
311
  * Optimized to O(n + e) where n = number of tasks, e = number of dependencies
319
312
  */
320
313
  export function findParallelTasks(tasks: Task[]): Task[][] {
321
- const activeTasks = tasks.filter(
322
- (t) => t.status === "pending" || t.status === "in_progress"
323
- );
314
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
324
315
 
325
316
  if (activeTasks.length === 0) return [];
326
317
 
327
318
  // Find tasks with no uncompleted dependencies
328
- const completedIds = new Set(
329
- tasks.filter((t) => t.status === "completed").map((t) => t.id)
330
- );
319
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
331
320
 
332
321
  const available = activeTasks.filter((task) => {
333
322
  const deps = getBlockedByDeps(task);
@@ -376,9 +365,7 @@ export function findParallelTasks(tasks: Task[]): Task[][] {
376
365
  if (processed.has(other.id)) continue;
377
366
 
378
367
  // O(1) check: tasks are independent if neither depends on the other
379
- const independent =
380
- !conflicting.has(other.id) &&
381
- !dependsOn.get(other.id)?.has(task.id);
368
+ const independent = !conflicting.has(other.id) && !dependsOn.get(other.id)?.has(task.id);
382
369
 
383
370
  if (independent) {
384
371
  group.push(other);
@@ -402,9 +389,7 @@ export function suggestNextTask(
402
389
  maxMinutes?: number;
403
390
  } = {}
404
391
  ): Task | null {
405
- const activeTasks = tasks.filter(
406
- (t) => t.status === "pending" || t.status === "in_progress"
407
- );
392
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
408
393
 
409
394
  if (activeTasks.length === 0) return null;
410
395
 
@@ -412,9 +397,7 @@ export function suggestNextTask(
412
397
  const cpm = criticalPathAnalysis(tasks);
413
398
 
414
399
  // Filter by availability (all dependencies completed)
415
- const completedIds = new Set(
416
- tasks.filter((t) => t.status === "completed").map((t) => t.id)
417
- );
400
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
418
401
 
419
402
  let candidates = cpm.tasks.filter((task) => {
420
403
  const deps = getBlockedByDeps(task);
@@ -434,9 +417,7 @@ export function suggestNextTask(
434
417
 
435
418
  // Filter by time if specified
436
419
  if (options.maxMinutes) {
437
- const timeFiltered = candidates.filter(
438
- (t) => getTaskDuration(t) <= options.maxMinutes!
439
- );
420
+ const timeFiltered = candidates.filter((t) => getTaskDuration(t) <= options.maxMinutes!);
440
421
  if (timeFiltered.length > 0) {
441
422
  candidates = timeFiltered;
442
423
  }
@@ -459,4 +440,3 @@ export function suggestNextTask(
459
440
  scored.sort((a, b) => b.score - a.score);
460
441
  return scored[0]?.task ?? null;
461
442
  }
462
-
@@ -0,0 +1,335 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ DependencyErrorCode,
4
+ validateDependency,
5
+ findInvalidDependencies,
6
+ findSelfDependencies,
7
+ detectCircularDependencies,
8
+ checkDependencyIntegrity,
9
+ } from "./dependency-integrity.js";
10
+ import type { Task } from "../schemas/task.js";
11
+
12
+ // Helper to create mock tasks
13
+ function createTask(
14
+ id: string,
15
+ title: string = `Task ${id}`,
16
+ deps: Array<{ taskId: string; type: "blocked_by" | "blocks" | "related" }> = []
17
+ ): Task {
18
+ return {
19
+ id,
20
+ title,
21
+ status: "pending",
22
+ priority: "medium",
23
+ workspace: "test-workspace",
24
+ createdAt: new Date().toISOString(),
25
+ updatedAt: new Date().toISOString(),
26
+ dependencies: deps,
27
+ };
28
+ }
29
+
30
+ describe("validateDependency", () => {
31
+ test("returns valid for a valid dependency", () => {
32
+ const tasks = [createTask("A"), createTask("B")];
33
+ const result = validateDependency({
34
+ taskId: "B",
35
+ blockedBy: "A",
36
+ tasks,
37
+ });
38
+ expect(result.valid).toBe(true);
39
+ expect(result.errorCode).toBeUndefined();
40
+ });
41
+
42
+ test("rejects self-dependency", () => {
43
+ const tasks = [createTask("A")];
44
+ const result = validateDependency({
45
+ taskId: "A",
46
+ blockedBy: "A",
47
+ tasks,
48
+ });
49
+ expect(result.valid).toBe(false);
50
+ expect(result.errorCode).toBe(DependencyErrorCode.SELF_DEPENDENCY);
51
+ expect(result.errorMessage).toContain("cannot depend on itself");
52
+ expect(result.suggestion).toBeDefined();
53
+ });
54
+
55
+ test("rejects when task not found", () => {
56
+ const tasks = [createTask("A")];
57
+ const result = validateDependency({
58
+ taskId: "B",
59
+ blockedBy: "A",
60
+ tasks,
61
+ });
62
+ expect(result.valid).toBe(false);
63
+ expect(result.errorCode).toBe(DependencyErrorCode.TASK_NOT_FOUND);
64
+ expect(result.errorMessage).toContain("Task not found");
65
+ });
66
+
67
+ test("rejects when blocker not found", () => {
68
+ const tasks = [createTask("A")];
69
+ const result = validateDependency({
70
+ taskId: "A",
71
+ blockedBy: "B",
72
+ tasks,
73
+ });
74
+ expect(result.valid).toBe(false);
75
+ expect(result.errorCode).toBe(DependencyErrorCode.BLOCKER_NOT_FOUND);
76
+ expect(result.errorMessage).toContain("Blocking task not found");
77
+ });
78
+
79
+ test("rejects duplicate dependency", () => {
80
+ const tasks = [
81
+ createTask("A"),
82
+ createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
83
+ ];
84
+ const result = validateDependency({
85
+ taskId: "B",
86
+ blockedBy: "A",
87
+ tasks,
88
+ });
89
+ expect(result.valid).toBe(false);
90
+ expect(result.errorCode).toBe(DependencyErrorCode.DUPLICATE_DEPENDENCY);
91
+ expect(result.errorMessage).toContain("already blocked by");
92
+ });
93
+
94
+ test("allows duplicate when checkDuplicates is false", () => {
95
+ const tasks = [
96
+ createTask("A"),
97
+ createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
98
+ ];
99
+ const result = validateDependency({
100
+ taskId: "B",
101
+ blockedBy: "A",
102
+ tasks,
103
+ checkDuplicates: false,
104
+ });
105
+ expect(result.valid).toBe(true);
106
+ });
107
+
108
+ test("rejects circular dependency", () => {
109
+ const tasks = [
110
+ createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
111
+ createTask("B"),
112
+ ];
113
+ // B is blocking A, so A blocking B would create a cycle
114
+ const result = validateDependency({
115
+ taskId: "B",
116
+ blockedBy: "A",
117
+ tasks,
118
+ });
119
+ expect(result.valid).toBe(false);
120
+ expect(result.errorCode).toBe(DependencyErrorCode.CIRCULAR_DEPENDENCY);
121
+ expect(result.errorMessage).toContain("cycle");
122
+ expect(result.suggestion).toContain("depends on");
123
+ });
124
+
125
+ test("rejects indirect circular dependency", () => {
126
+ const tasks = [
127
+ createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
128
+ createTask("B", "Task B", [{ taskId: "C", type: "blocked_by" }]),
129
+ createTask("C"),
130
+ ];
131
+ // A <- B <- C, adding C <- A would create cycle
132
+ const result = validateDependency({
133
+ taskId: "C",
134
+ blockedBy: "A",
135
+ tasks,
136
+ });
137
+ expect(result.valid).toBe(false);
138
+ expect(result.errorCode).toBe(DependencyErrorCode.CIRCULAR_DEPENDENCY);
139
+ });
140
+ });
141
+
142
+ describe("findInvalidDependencies", () => {
143
+ test("returns empty array when all dependencies are valid", () => {
144
+ const tasks = [
145
+ createTask("A"),
146
+ createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
147
+ ];
148
+ const result = findInvalidDependencies(tasks);
149
+ expect(result).toEqual([]);
150
+ });
151
+
152
+ test("finds orphaned dependency references", () => {
153
+ const tasks = [createTask("A", "Task A", [{ taskId: "non-existent", type: "blocked_by" }])];
154
+ const result = findInvalidDependencies(tasks);
155
+ expect(result.length).toBe(1);
156
+ expect(result[0]!.taskId).toBe("A");
157
+ expect(result[0]!.taskTitle).toBe("Task A");
158
+ expect(result[0]!.invalidDependencyId).toBe("non-existent");
159
+ expect(result[0]!.type).toBe("blocked_by");
160
+ });
161
+
162
+ test("finds multiple invalid references across tasks", () => {
163
+ const tasks = [
164
+ createTask("A", "Task A", [{ taskId: "deleted-1", type: "blocked_by" }]),
165
+ createTask("B", "Task B", [
166
+ { taskId: "A", type: "blocked_by" },
167
+ { taskId: "deleted-2", type: "blocks" },
168
+ ]),
169
+ ];
170
+ const result = findInvalidDependencies(tasks);
171
+ expect(result.length).toBe(2);
172
+ expect(result.map((r) => r.invalidDependencyId).sort()).toEqual(["deleted-1", "deleted-2"]);
173
+ });
174
+
175
+ test("returns empty array for tasks without dependencies", () => {
176
+ const tasks = [createTask("A"), createTask("B")];
177
+ const result = findInvalidDependencies(tasks);
178
+ expect(result).toEqual([]);
179
+ });
180
+ });
181
+
182
+ describe("findSelfDependencies", () => {
183
+ test("returns empty array when no self-dependencies exist", () => {
184
+ const tasks = [
185
+ createTask("A"),
186
+ createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
187
+ ];
188
+ const result = findSelfDependencies(tasks);
189
+ expect(result).toEqual([]);
190
+ });
191
+
192
+ test("finds task with self-dependency", () => {
193
+ const tasks = [createTask("A", "Task A", [{ taskId: "A", type: "blocked_by" }])];
194
+ const result = findSelfDependencies(tasks);
195
+ expect(result).toEqual(["A"]);
196
+ });
197
+
198
+ test("finds multiple self-dependencies", () => {
199
+ const tasks = [
200
+ createTask("A", "Task A", [{ taskId: "A", type: "blocked_by" }]),
201
+ createTask("B"),
202
+ createTask("C", "Task C", [{ taskId: "C", type: "blocks" }]),
203
+ ];
204
+ const result = findSelfDependencies(tasks);
205
+ expect(result.sort()).toEqual(["A", "C"]);
206
+ });
207
+ });
208
+
209
+ describe("detectCircularDependencies", () => {
210
+ test("returns empty array when no cycles exist", () => {
211
+ const tasks = [
212
+ createTask("A"),
213
+ createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
214
+ createTask("C", "Task C", [{ taskId: "B", type: "blocked_by" }]),
215
+ ];
216
+ const result = detectCircularDependencies(tasks);
217
+ expect(result).toEqual([]);
218
+ });
219
+
220
+ test("detects direct circular dependency (A -> B -> A)", () => {
221
+ const tasks = [
222
+ createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
223
+ createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
224
+ ];
225
+ const result = detectCircularDependencies(tasks);
226
+ expect(result.length).toBe(1);
227
+ expect(result[0]!.sort()).toEqual(["A", "B"]);
228
+ });
229
+
230
+ test("detects longer cycle (A -> B -> C -> A)", () => {
231
+ const tasks = [
232
+ createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
233
+ createTask("B", "Task B", [{ taskId: "C", type: "blocked_by" }]),
234
+ createTask("C", "Task C", [{ taskId: "A", type: "blocked_by" }]),
235
+ ];
236
+ const result = detectCircularDependencies(tasks);
237
+ expect(result.length).toBe(1);
238
+ expect(result[0]!.sort()).toEqual(["A", "B", "C"]);
239
+ });
240
+
241
+ test("ignores non-blocking dependency types for cycle detection", () => {
242
+ const tasks = [
243
+ createTask("A", "Task A", [{ taskId: "B", type: "related" }]),
244
+ createTask("B", "Task B", [{ taskId: "A", type: "related" }]),
245
+ ];
246
+ const result = detectCircularDependencies(tasks);
247
+ expect(result).toEqual([]);
248
+ });
249
+
250
+ test("handles tasks with missing dependency targets", () => {
251
+ const tasks = [createTask("A", "Task A", [{ taskId: "non-existent", type: "blocked_by" }])];
252
+ const result = detectCircularDependencies(tasks);
253
+ expect(result).toEqual([]);
254
+ });
255
+ });
256
+
257
+ describe("checkDependencyIntegrity", () => {
258
+ test("returns valid for healthy task set", () => {
259
+ const tasks = [
260
+ createTask("A"),
261
+ createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
262
+ createTask("C", "Task C", [{ taskId: "B", type: "blocked_by" }]),
263
+ ];
264
+ const result = checkDependencyIntegrity(tasks);
265
+ expect(result.valid).toBe(true);
266
+ expect(result.totalTasks).toBe(3);
267
+ expect(result.totalDependencies).toBe(2);
268
+ expect(result.issues.selfDependencies).toEqual([]);
269
+ expect(result.issues.invalidReferences).toEqual([]);
270
+ expect(result.issues.circularDependencies).toEqual([]);
271
+ expect(result.summary).toContain("All 2 dependencies");
272
+ expect(result.summary).toContain("valid");
273
+ });
274
+
275
+ test("detects self-dependency issues", () => {
276
+ const tasks = [createTask("A", "Task A", [{ taskId: "A", type: "blocked_by" }])];
277
+ const result = checkDependencyIntegrity(tasks);
278
+ expect(result.valid).toBe(false);
279
+ expect(result.issues.selfDependencies).toEqual(["A"]);
280
+ expect(result.summary).toContain("self-dependencies");
281
+ });
282
+
283
+ test("detects invalid reference issues", () => {
284
+ const tasks = [createTask("A", "Task A", [{ taskId: "deleted", type: "blocked_by" }])];
285
+ const result = checkDependencyIntegrity(tasks);
286
+ expect(result.valid).toBe(false);
287
+ expect(result.issues.invalidReferences.length).toBe(1);
288
+ expect(result.summary).toContain("invalid references");
289
+ });
290
+
291
+ test("detects circular dependency issues", () => {
292
+ const tasks = [
293
+ createTask("A", "Task A", [{ taskId: "B", type: "blocked_by" }]),
294
+ createTask("B", "Task B", [{ taskId: "A", type: "blocked_by" }]),
295
+ ];
296
+ const result = checkDependencyIntegrity(tasks);
297
+ expect(result.valid).toBe(false);
298
+ expect(result.issues.circularDependencies.length).toBe(1);
299
+ expect(result.summary).toContain("circular dependency");
300
+ });
301
+
302
+ test("reports multiple issue types", () => {
303
+ const tasks = [
304
+ createTask("A", "Task A", [
305
+ { taskId: "A", type: "blocked_by" }, // self-dependency
306
+ { taskId: "deleted", type: "blocked_by" }, // invalid reference
307
+ ]),
308
+ createTask("B", "Task B", [{ taskId: "C", type: "blocked_by" }]),
309
+ createTask("C", "Task C", [{ taskId: "B", type: "blocked_by" }]), // circular
310
+ ];
311
+ const result = checkDependencyIntegrity(tasks);
312
+ expect(result.valid).toBe(false);
313
+ expect(result.issues.selfDependencies.length).toBeGreaterThan(0);
314
+ expect(result.issues.invalidReferences.length).toBeGreaterThan(0);
315
+ expect(result.issues.circularDependencies.length).toBeGreaterThan(0);
316
+ expect(result.summary).toContain("self-dependencies");
317
+ expect(result.summary).toContain("invalid references");
318
+ expect(result.summary).toContain("circular dependency");
319
+ });
320
+
321
+ test("handles empty task array", () => {
322
+ const result = checkDependencyIntegrity([]);
323
+ expect(result.valid).toBe(true);
324
+ expect(result.totalTasks).toBe(0);
325
+ expect(result.totalDependencies).toBe(0);
326
+ });
327
+
328
+ test("handles tasks with no dependencies", () => {
329
+ const tasks = [createTask("A"), createTask("B"), createTask("C")];
330
+ const result = checkDependencyIntegrity(tasks);
331
+ expect(result.valid).toBe(true);
332
+ expect(result.totalTasks).toBe(3);
333
+ expect(result.totalDependencies).toBe(0);
334
+ });
335
+ });