@task-mcp/shared 1.0.19 → 1.0.21

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 (163) hide show
  1. package/package.json +1 -6
  2. package/src/schemas/task.ts +60 -14
  3. package/src/utils/date.ts +40 -0
  4. package/src/utils/hierarchy.ts +63 -1
  5. package/src/utils/index.ts +2 -0
  6. package/src/utils/natural-language.ts +12 -0
  7. package/src/utils/projection.ts +14 -3
  8. package/dist/algorithms/critical-path.d.ts +0 -46
  9. package/dist/algorithms/critical-path.d.ts.map +0 -1
  10. package/dist/algorithms/critical-path.js +0 -320
  11. package/dist/algorithms/critical-path.js.map +0 -1
  12. package/dist/algorithms/critical-path.test.d.ts +0 -2
  13. package/dist/algorithms/critical-path.test.d.ts.map +0 -1
  14. package/dist/algorithms/critical-path.test.js +0 -194
  15. package/dist/algorithms/critical-path.test.js.map +0 -1
  16. package/dist/algorithms/dependency-integrity.d.ts +0 -81
  17. package/dist/algorithms/dependency-integrity.d.ts.map +0 -1
  18. package/dist/algorithms/dependency-integrity.js +0 -207
  19. package/dist/algorithms/dependency-integrity.js.map +0 -1
  20. package/dist/algorithms/dependency-integrity.test.d.ts +0 -2
  21. package/dist/algorithms/dependency-integrity.test.d.ts.map +0 -1
  22. package/dist/algorithms/dependency-integrity.test.js +0 -309
  23. package/dist/algorithms/dependency-integrity.test.js.map +0 -1
  24. package/dist/algorithms/index.d.ts +0 -5
  25. package/dist/algorithms/index.d.ts.map +0 -1
  26. package/dist/algorithms/index.js +0 -5
  27. package/dist/algorithms/index.js.map +0 -1
  28. package/dist/algorithms/tech-analysis.d.ts +0 -106
  29. package/dist/algorithms/tech-analysis.d.ts.map +0 -1
  30. package/dist/algorithms/tech-analysis.js +0 -344
  31. package/dist/algorithms/tech-analysis.js.map +0 -1
  32. package/dist/algorithms/tech-analysis.test.d.ts +0 -2
  33. package/dist/algorithms/tech-analysis.test.d.ts.map +0 -1
  34. package/dist/algorithms/tech-analysis.test.js +0 -338
  35. package/dist/algorithms/tech-analysis.test.js.map +0 -1
  36. package/dist/algorithms/topological-sort.d.ts +0 -41
  37. package/dist/algorithms/topological-sort.d.ts.map +0 -1
  38. package/dist/algorithms/topological-sort.js +0 -165
  39. package/dist/algorithms/topological-sort.js.map +0 -1
  40. package/dist/algorithms/topological-sort.test.d.ts +0 -2
  41. package/dist/algorithms/topological-sort.test.d.ts.map +0 -1
  42. package/dist/algorithms/topological-sort.test.js +0 -162
  43. package/dist/algorithms/topological-sort.test.js.map +0 -1
  44. package/dist/index.d.ts +0 -4
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/index.js +0 -7
  47. package/dist/index.js.map +0 -1
  48. package/dist/schemas/inbox.d.ts +0 -55
  49. package/dist/schemas/inbox.d.ts.map +0 -1
  50. package/dist/schemas/inbox.js +0 -25
  51. package/dist/schemas/inbox.js.map +0 -1
  52. package/dist/schemas/index.d.ts +0 -7
  53. package/dist/schemas/index.d.ts.map +0 -1
  54. package/dist/schemas/index.js +0 -17
  55. package/dist/schemas/index.js.map +0 -1
  56. package/dist/schemas/project.d.ts +0 -177
  57. package/dist/schemas/project.d.ts.map +0 -1
  58. package/dist/schemas/project.js +0 -56
  59. package/dist/schemas/project.js.map +0 -1
  60. package/dist/schemas/response-format.d.ts +0 -148
  61. package/dist/schemas/response-format.d.ts.map +0 -1
  62. package/dist/schemas/response-format.js +0 -18
  63. package/dist/schemas/response-format.js.map +0 -1
  64. package/dist/schemas/response-schema.d.ts +0 -307
  65. package/dist/schemas/response-schema.d.ts.map +0 -1
  66. package/dist/schemas/response-schema.js +0 -75
  67. package/dist/schemas/response-schema.js.map +0 -1
  68. package/dist/schemas/response-schema.test.d.ts +0 -2
  69. package/dist/schemas/response-schema.test.d.ts.map +0 -1
  70. package/dist/schemas/response-schema.test.js +0 -256
  71. package/dist/schemas/response-schema.test.js.map +0 -1
  72. package/dist/schemas/state.d.ts +0 -17
  73. package/dist/schemas/state.d.ts.map +0 -1
  74. package/dist/schemas/state.js +0 -17
  75. package/dist/schemas/state.js.map +0 -1
  76. package/dist/schemas/task.d.ts +0 -625
  77. package/dist/schemas/task.d.ts.map +0 -1
  78. package/dist/schemas/task.js +0 -152
  79. package/dist/schemas/task.js.map +0 -1
  80. package/dist/schemas/view.d.ts +0 -143
  81. package/dist/schemas/view.d.ts.map +0 -1
  82. package/dist/schemas/view.js +0 -48
  83. package/dist/schemas/view.js.map +0 -1
  84. package/dist/utils/dashboard-renderer.d.ts +0 -93
  85. package/dist/utils/dashboard-renderer.d.ts.map +0 -1
  86. package/dist/utils/dashboard-renderer.js +0 -424
  87. package/dist/utils/dashboard-renderer.js.map +0 -1
  88. package/dist/utils/dashboard-renderer.test.d.ts +0 -2
  89. package/dist/utils/dashboard-renderer.test.d.ts.map +0 -1
  90. package/dist/utils/dashboard-renderer.test.js +0 -774
  91. package/dist/utils/dashboard-renderer.test.js.map +0 -1
  92. package/dist/utils/date.d.ts +0 -81
  93. package/dist/utils/date.d.ts.map +0 -1
  94. package/dist/utils/date.js +0 -294
  95. package/dist/utils/date.js.map +0 -1
  96. package/dist/utils/date.test.d.ts +0 -2
  97. package/dist/utils/date.test.d.ts.map +0 -1
  98. package/dist/utils/date.test.js +0 -276
  99. package/dist/utils/date.test.js.map +0 -1
  100. package/dist/utils/hierarchy.d.ts +0 -75
  101. package/dist/utils/hierarchy.d.ts.map +0 -1
  102. package/dist/utils/hierarchy.js +0 -189
  103. package/dist/utils/hierarchy.js.map +0 -1
  104. package/dist/utils/hierarchy.test.d.ts +0 -2
  105. package/dist/utils/hierarchy.test.d.ts.map +0 -1
  106. package/dist/utils/hierarchy.test.js +0 -351
  107. package/dist/utils/hierarchy.test.js.map +0 -1
  108. package/dist/utils/id.d.ts +0 -60
  109. package/dist/utils/id.d.ts.map +0 -1
  110. package/dist/utils/id.js +0 -118
  111. package/dist/utils/id.js.map +0 -1
  112. package/dist/utils/id.test.d.ts +0 -2
  113. package/dist/utils/id.test.d.ts.map +0 -1
  114. package/dist/utils/id.test.js +0 -193
  115. package/dist/utils/id.test.js.map +0 -1
  116. package/dist/utils/index.d.ts +0 -12
  117. package/dist/utils/index.d.ts.map +0 -1
  118. package/dist/utils/index.js +0 -34
  119. package/dist/utils/index.js.map +0 -1
  120. package/dist/utils/natural-language.d.ts +0 -57
  121. package/dist/utils/natural-language.d.ts.map +0 -1
  122. package/dist/utils/natural-language.js +0 -205
  123. package/dist/utils/natural-language.js.map +0 -1
  124. package/dist/utils/natural-language.test.d.ts +0 -2
  125. package/dist/utils/natural-language.test.d.ts.map +0 -1
  126. package/dist/utils/natural-language.test.js +0 -156
  127. package/dist/utils/natural-language.test.js.map +0 -1
  128. package/dist/utils/priority-queue.d.ts +0 -17
  129. package/dist/utils/priority-queue.d.ts.map +0 -1
  130. package/dist/utils/priority-queue.js +0 -62
  131. package/dist/utils/priority-queue.js.map +0 -1
  132. package/dist/utils/projection.d.ts +0 -65
  133. package/dist/utils/projection.d.ts.map +0 -1
  134. package/dist/utils/projection.js +0 -170
  135. package/dist/utils/projection.js.map +0 -1
  136. package/dist/utils/projection.test.d.ts +0 -2
  137. package/dist/utils/projection.test.d.ts.map +0 -1
  138. package/dist/utils/projection.test.js +0 -336
  139. package/dist/utils/projection.test.js.map +0 -1
  140. package/dist/utils/terminal-ui.d.ts +0 -208
  141. package/dist/utils/terminal-ui.d.ts.map +0 -1
  142. package/dist/utils/terminal-ui.js +0 -611
  143. package/dist/utils/terminal-ui.js.map +0 -1
  144. package/dist/utils/terminal-ui.test.d.ts +0 -2
  145. package/dist/utils/terminal-ui.test.d.ts.map +0 -1
  146. package/dist/utils/terminal-ui.test.js +0 -683
  147. package/dist/utils/terminal-ui.test.js.map +0 -1
  148. package/dist/utils/workspace.d.ts +0 -100
  149. package/dist/utils/workspace.d.ts.map +0 -1
  150. package/dist/utils/workspace.js +0 -173
  151. package/dist/utils/workspace.js.map +0 -1
  152. package/src/algorithms/critical-path.test.ts +0 -241
  153. package/src/algorithms/dependency-integrity.test.ts +0 -348
  154. package/src/algorithms/tech-analysis.test.ts +0 -413
  155. package/src/algorithms/topological-sort.test.ts +0 -190
  156. package/src/schemas/response-schema.test.ts +0 -314
  157. package/src/utils/dashboard-renderer.test.ts +0 -983
  158. package/src/utils/date.test.ts +0 -329
  159. package/src/utils/hierarchy.test.ts +0 -411
  160. package/src/utils/id.test.ts +0 -235
  161. package/src/utils/natural-language.test.ts +0 -182
  162. package/src/utils/projection.test.ts +0 -425
  163. package/src/utils/terminal-ui.test.ts +0 -831
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@task-mcp/shared",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Shared utilities for task-mcp: types, algorithms, and natural language parsing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,11 +12,6 @@
12
12
  "types": "./dist/index.d.ts"
13
13
  }
14
14
  },
15
- "files": [
16
- "src",
17
- "dist",
18
- "README.md"
19
- ],
20
15
  "scripts": {
21
16
  "build": "tsc",
22
17
  "dev": "tsc --watch",
@@ -26,22 +26,68 @@ export const Dependency = z.object({
26
26
  });
27
27
  export type Dependency = z.infer<typeof Dependency>;
28
28
 
29
- // Time estimation
30
- export const TimeEstimate = z.object({
31
- optimistic: z.number().optional(), // minutes
32
- expected: z.number().optional(), // minutes
33
- pessimistic: z.number().optional(), // minutes
34
- confidence: z.enum(["low", "medium", "high"]).optional(),
35
- });
29
+ // Time estimation (values in minutes, max 10080 = 1 week)
30
+ export const TimeEstimate = z
31
+ .object({
32
+ optimistic: z.number().min(0).max(10080).optional(), // minutes
33
+ expected: z.number().min(0).max(10080).optional(), // minutes
34
+ pessimistic: z.number().min(0).max(10080).optional(), // minutes
35
+ confidence: z.enum(["low", "medium", "high"]).optional(),
36
+ })
37
+ .refine(
38
+ (data) => {
39
+ const { optimistic, expected, pessimistic } = data;
40
+ if (
41
+ optimistic !== undefined &&
42
+ expected !== undefined &&
43
+ optimistic > expected
44
+ ) {
45
+ return false;
46
+ }
47
+ if (
48
+ expected !== undefined &&
49
+ pessimistic !== undefined &&
50
+ expected > pessimistic
51
+ ) {
52
+ return false;
53
+ }
54
+ if (
55
+ optimistic !== undefined &&
56
+ pessimistic !== undefined &&
57
+ optimistic > pessimistic
58
+ ) {
59
+ return false;
60
+ }
61
+ return true;
62
+ },
63
+ { message: "Time estimates must satisfy: optimistic <= expected <= pessimistic" }
64
+ );
36
65
  export type TimeEstimate = z.infer<typeof TimeEstimate>;
37
66
 
38
- // Recurrence pattern
39
- export const Recurrence = z.object({
40
- pattern: z.enum(["daily", "weekly", "monthly", "after_completion"]),
41
- interval: z.number().optional(), // every N days/weeks/months
42
- daysOfWeek: z.array(z.number()).optional(), // 0-6 for weekly
43
- endDate: z.string().optional(),
44
- });
67
+ // Recurrence pattern (discriminated union for type-safe pattern handling)
68
+ export const Recurrence = z.discriminatedUnion("pattern", [
69
+ z.object({
70
+ pattern: z.literal("daily"),
71
+ interval: z.number().min(1).optional(), // every N days
72
+ endDate: z.string().optional(),
73
+ }),
74
+ z.object({
75
+ pattern: z.literal("weekly"),
76
+ interval: z.number().min(1).optional(), // every N weeks
77
+ daysOfWeek: z.array(z.number().min(0).max(6)).optional(), // 0-6 for weekly
78
+ endDate: z.string().optional(),
79
+ }),
80
+ z.object({
81
+ pattern: z.literal("monthly"),
82
+ interval: z.number().min(1).optional(), // every N months
83
+ endDate: z.string().optional(),
84
+ }),
85
+ z.object({
86
+ pattern: z.literal("after_completion"),
87
+ interval: z.number().min(1), // Required: days after completion
88
+ endDate: z.string().optional(),
89
+ }),
90
+ ]);
45
91
  export type Recurrence = z.infer<typeof Recurrence>;
46
92
 
47
93
  // Complexity factors that contribute to task difficulty
package/src/utils/date.ts CHANGED
@@ -28,6 +28,15 @@ export type DateParseResult =
28
28
  | { success: true; date: Date }
29
29
  | { success: false; error: string; reason: DateParseErrorReason };
30
30
 
31
+ /**
32
+ * Result type for isWithinDaysSafe operation
33
+ */
34
+ export interface IsWithinDaysResult {
35
+ success: boolean;
36
+ withinDays?: boolean;
37
+ error?: string;
38
+ }
39
+
31
40
  /**
32
41
  * Get current ISO timestamp
33
42
  */
@@ -351,3 +360,34 @@ export function isWithinDays(date: Date | string, days: number): boolean {
351
360
  future.setDate(future.getDate() + days);
352
361
  return d >= today && d <= future;
353
362
  }
363
+
364
+ /**
365
+ * Safe version of isWithinDays that returns a Result type
366
+ * Allows distinguishing between errors and actual out-of-range results
367
+ */
368
+ export function isWithinDaysSafe(date: Date | string, days: number): IsWithinDaysResult {
369
+ if (typeof days !== "number" || !Number.isFinite(days) || days < 0) {
370
+ return { success: false, error: `Invalid days parameter: ${days}` };
371
+ }
372
+
373
+ let targetDate: Date;
374
+ if (typeof date === "string") {
375
+ targetDate = new Date(date);
376
+ if (isNaN(targetDate.getTime())) {
377
+ return { success: false, error: `Invalid date string: ${date}` };
378
+ }
379
+ } else if (date instanceof Date) {
380
+ if (isNaN(date.getTime())) {
381
+ return { success: false, error: "Invalid Date object" };
382
+ }
383
+ targetDate = date;
384
+ } else {
385
+ return { success: false, error: `Invalid date type: ${typeof date}` };
386
+ }
387
+
388
+ const now = new Date();
389
+ const diffMs = targetDate.getTime() - now.getTime();
390
+ const diffDays = diffMs / (1000 * 60 * 60 * 24);
391
+
392
+ return { success: true, withinDays: diffDays >= 0 && diffDays <= days };
393
+ }
@@ -34,7 +34,8 @@ export function getTaskLevel(tasks: Task[], taskId: string): number {
34
34
  while (currentTask.parentId) {
35
35
  const parent = taskMap.get(currentTask.parentId);
36
36
  if (!parent) {
37
- break; // Parent not found, stop traversal
37
+ console.warn(`[task-mcp] Orphaned parent reference: task "${currentTask.id}" references non-existent parent "${currentTask.parentId}"`);
38
+ break;
38
39
  }
39
40
  level++;
40
41
  currentTask = parent;
@@ -239,3 +240,64 @@ export function buildTaskTree(
239
240
  const rootTasks = tasks.filter((t) => !t.parentId);
240
241
  return rootTasks.map((task) => buildNode(task, 0, new Set()));
241
242
  }
243
+
244
+ /**
245
+ * Result of hierarchy validation.
246
+ */
247
+ export interface HierarchyValidationResult {
248
+ valid: boolean;
249
+ orphanedReferences: { taskId: string; parentId: string }[];
250
+ circularReferences: string[];
251
+ maxDepthExceeded: { taskId: string; depth: number }[];
252
+ }
253
+
254
+ /**
255
+ * Validates the hierarchy integrity of a task list.
256
+ *
257
+ * Checks for:
258
+ * - Orphaned parent references (task references non-existent parent)
259
+ * - Circular references (task is its own ancestor)
260
+ * - Tasks exceeding maximum hierarchy depth
261
+ *
262
+ * @param tasks - Array of all tasks to validate
263
+ * @returns Validation result with details of any issues found
264
+ */
265
+ export function validateHierarchy(tasks: Task[]): HierarchyValidationResult {
266
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
267
+ const result: HierarchyValidationResult = {
268
+ valid: true,
269
+ orphanedReferences: [],
270
+ circularReferences: [],
271
+ maxDepthExceeded: [],
272
+ };
273
+
274
+ for (const task of tasks) {
275
+ // Check for orphaned parent references
276
+ if (task.parentId && !taskMap.has(task.parentId)) {
277
+ result.orphanedReferences.push({ taskId: task.id, parentId: task.parentId });
278
+ result.valid = false;
279
+ }
280
+
281
+ // Check for circular references
282
+ const visited = new Set<string>();
283
+ let current: Task | undefined = task;
284
+ while (current?.parentId) {
285
+ if (visited.has(current.id)) {
286
+ result.circularReferences.push(task.id);
287
+ result.valid = false;
288
+ break;
289
+ }
290
+ visited.add(current.id);
291
+ current = taskMap.get(current.parentId);
292
+ }
293
+
294
+ // Check for max depth exceeded
295
+ const level = getTaskLevel(tasks, task.id);
296
+ if (level > MAX_HIERARCHY_DEPTH) {
297
+ result.maxDepthExceeded.push({ taskId: task.id, depth: level });
298
+ result.valid = false;
299
+ }
300
+ }
301
+
302
+ return result;
303
+ }
@@ -24,10 +24,12 @@ export {
24
24
  isToday,
25
25
  isPastDue,
26
26
  isWithinDays,
27
+ isWithinDaysSafe,
27
28
  isValidDate,
28
29
  DateParseError,
29
30
  type DateParseErrorReason,
30
31
  type DateParseResult,
32
+ type IsWithinDaysResult,
31
33
  } from "./date.js";
32
34
  export { parseTaskInput, parseInboxInput, parseInput, type ParseTarget, type ParsedInput } from "./natural-language.js";
33
35
  export {
@@ -110,6 +110,12 @@ export function parseInboxInput(input: string): InboxCreateInput {
110
110
  // Clean up and set content
111
111
  result.content = remaining.replace(/\s+/g, " ").trim();
112
112
 
113
+ if (!result.content) {
114
+ throw new InputValidationError(
115
+ "Content cannot be empty after parsing. Input contained only metadata (tags)."
116
+ );
117
+ }
118
+
113
119
  return result;
114
120
  }
115
121
 
@@ -223,6 +229,12 @@ export function parseTaskInput(input: string): TaskCreateInput {
223
229
  // Clean up extra spaces
224
230
  result.title = remaining.replace(/\s+/g, " ").trim();
225
231
 
232
+ if (!result.title) {
233
+ throw new InputValidationError(
234
+ "Task title cannot be empty after parsing. Input contained only metadata."
235
+ );
236
+ }
237
+
226
238
  return result;
227
239
  }
228
240
 
@@ -16,6 +16,14 @@ import type {
16
16
  PaginatedResponse,
17
17
  } from "../schemas/response-format.js";
18
18
 
19
+ /**
20
+ * Exhaustiveness check helper for switch statements.
21
+ * TypeScript will error if a case is not handled.
22
+ */
23
+ function assertNever(value: never): never {
24
+ throw new Error(`Unexpected value: ${value}`);
25
+ }
26
+
19
27
  /**
20
28
  * Project a single task to the specified format
21
29
  */
@@ -42,8 +50,9 @@ export function projectTask(task: Task, format: ResponseFormat): TaskSummary | T
42
50
  };
43
51
 
44
52
  case "detailed":
45
- default:
46
53
  return task;
54
+ default:
55
+ return assertNever(format);
47
56
  }
48
57
  }
49
58
 
@@ -106,8 +115,9 @@ export function projectInboxItem(
106
115
  };
107
116
 
108
117
  case "detailed":
109
- default:
110
118
  return item;
119
+ default:
120
+ return assertNever(format);
111
121
  }
112
122
  }
113
123
 
@@ -226,8 +236,9 @@ export function sortTasks(
226
236
  }
227
237
 
228
238
  case "updatedAt":
229
- default:
230
239
  return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
240
+ default:
241
+ return assertNever(secondarySort);
231
242
  }
232
243
  });
233
244
  }
@@ -1,46 +0,0 @@
1
- import type { Task } from "../schemas/task.js";
2
- /**
3
- * Task with computed CPM (Critical Path Method) values
4
- */
5
- export interface CPMTask extends Task {
6
- earliestStart: number;
7
- earliestFinish: number;
8
- latestStart: number;
9
- latestFinish: number;
10
- slack: number;
11
- isCritical: boolean;
12
- dependentCount: number;
13
- }
14
- /**
15
- * Result of critical path analysis
16
- */
17
- export interface CPMResult {
18
- tasks: CPMTask[];
19
- criticalPath: CPMTask[];
20
- projectDuration: number;
21
- bottlenecks: CPMTask[];
22
- }
23
- /**
24
- * Perform Critical Path Method analysis
25
- *
26
- * CPM calculates:
27
- * - Earliest Start (ES): Earliest a task can start
28
- * - Earliest Finish (EF): ES + duration
29
- * - Latest Finish (LF): Latest a task can finish without delaying project
30
- * - Latest Start (LS): LF - duration
31
- * - Slack: LS - ES (or LF - EF)
32
- * - Critical Path: Tasks with slack = 0
33
- */
34
- export declare function criticalPathAnalysis(tasks: Task[]): CPMResult;
35
- /**
36
- * Find tasks that can be executed in parallel (no dependencies between them)
37
- */
38
- export declare function findParallelTasks(tasks: Task[]): Task[][];
39
- /**
40
- * Suggest the next best task to work on
41
- */
42
- export declare function suggestNextTask(tasks: Task[], options?: {
43
- contexts?: string[];
44
- maxMinutes?: number;
45
- }): Task | null;
46
- //# sourceMappingURL=critical-path.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"critical-path.d.ts","sourceRoot":"","sources":["../../src/algorithms/critical-path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAO/C;;GAEG;AACH,MAAM,WAAW,OAAQ,SAAQ,IAAI;IAEnC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,YAAY,EAAE,OAAO,EAAE,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,OAAO,EAAE,CAAC;CACxB;AA8ND;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,SAAS,CAmD7D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,EAAE,CAiDzD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CAChB,GACL,IAAI,GAAG,IAAI,CAyDb"}
@@ -1,320 +0,0 @@
1
- import { topologicalSort, priorityToNumber, } from "./topological-sort.js";
2
- /** Default task duration if no estimate provided */
3
- const DEFAULT_DURATION = 30;
4
- /**
5
- * Get task duration with fallback to default
6
- */
7
- function getTaskDuration(task) {
8
- return task.estimate?.expected ?? DEFAULT_DURATION;
9
- }
10
- /**
11
- * Get blocked_by dependencies for a task
12
- */
13
- function getBlockedByDeps(task) {
14
- return (task.dependencies ?? [])
15
- .filter((d) => d.type === "blocked_by")
16
- .map((d) => d.taskId);
17
- }
18
- /**
19
- * Initialize CPM tasks with default values
20
- */
21
- function initializeCPMTasks(sortedTasks) {
22
- const taskMap = new Map();
23
- for (const task of sortedTasks) {
24
- const duration = getTaskDuration(task);
25
- taskMap.set(task.id, {
26
- ...task,
27
- earliestStart: 0,
28
- earliestFinish: duration,
29
- latestStart: Infinity,
30
- latestFinish: Infinity,
31
- slack: 0,
32
- isCritical: false,
33
- dependentCount: 0,
34
- });
35
- }
36
- return taskMap;
37
- }
38
- /**
39
- * Forward pass: Calculate earliest start/finish times
40
- * ES = max(EF of all predecessors)
41
- * EF = ES + duration
42
- */
43
- function forwardPass(sortedTasks, taskMap) {
44
- for (const task of sortedTasks) {
45
- const cpmTask = taskMap.get(task.id);
46
- if (!cpmTask)
47
- continue;
48
- const duration = getTaskDuration(task);
49
- const deps = getBlockedByDeps(task);
50
- // Find maximum EF among predecessors
51
- let maxPredecessorEF = 0;
52
- for (const depId of deps) {
53
- const dep = taskMap.get(depId);
54
- if (dep) {
55
- maxPredecessorEF = Math.max(maxPredecessorEF, dep.earliestFinish);
56
- }
57
- }
58
- cpmTask.earliestStart = maxPredecessorEF;
59
- cpmTask.earliestFinish = cpmTask.earliestStart + duration;
60
- }
61
- }
62
- /**
63
- * Calculate project duration (maximum earliest finish)
64
- */
65
- function calculateProjectDuration(taskMap) {
66
- return Math.max(...Array.from(taskMap.values()).map((t) => t.earliestFinish));
67
- }
68
- /**
69
- * Build successor index: maps taskId -> list of tasks that depend on it
70
- * This is O(n * d) where d is average dependencies, done once upfront
71
- * Allows O(1) successor lookup instead of O(n) per task
72
- */
73
- function buildSuccessorIndex(sortedTasks, taskMap) {
74
- const successorIndex = new Map();
75
- // Initialize empty arrays for all tasks
76
- for (const task of sortedTasks) {
77
- successorIndex.set(task.id, []);
78
- }
79
- // Build the index: if task A is blocked_by task B, then A is a successor of B
80
- for (const task of sortedTasks) {
81
- const deps = getBlockedByDeps(task);
82
- const cpmTask = taskMap.get(task.id);
83
- if (!cpmTask)
84
- continue;
85
- for (const depId of deps) {
86
- const successors = successorIndex.get(depId);
87
- if (successors) {
88
- successors.push(cpmTask);
89
- }
90
- }
91
- }
92
- return successorIndex;
93
- }
94
- /**
95
- * Find tasks that have successors (are dependencies of other tasks)
96
- */
97
- function findTasksWithSuccessors(successorIndex) {
98
- const tasksWithSuccessors = new Set();
99
- for (const [taskId, successors] of successorIndex) {
100
- if (successors.length > 0) {
101
- tasksWithSuccessors.add(taskId);
102
- }
103
- }
104
- return tasksWithSuccessors;
105
- }
106
- /**
107
- * Backward pass: Calculate latest start/finish times
108
- * LF = min(LS of all successors) or projectDuration for end tasks
109
- * LS = LF - duration
110
- */
111
- function backwardPass(sortedTasks, taskMap, successorIndex, projectDuration) {
112
- const tasksWithSuccessors = findTasksWithSuccessors(successorIndex);
113
- // Initialize end tasks (tasks with no successors)
114
- for (const task of taskMap.values()) {
115
- if (!tasksWithSuccessors.has(task.id)) {
116
- const duration = getTaskDuration(task);
117
- task.latestFinish = projectDuration;
118
- task.latestStart = task.latestFinish - duration;
119
- }
120
- }
121
- // Process in reverse topological order
122
- const reverseSorted = [...sortedTasks].reverse();
123
- for (const task of reverseSorted) {
124
- const cpmTask = taskMap.get(task.id);
125
- if (!cpmTask)
126
- continue;
127
- const duration = getTaskDuration(task);
128
- const successors = successorIndex.get(task.id) ?? [];
129
- if (successors.length > 0) {
130
- // LF = min(LS of all successors)
131
- cpmTask.latestFinish = Math.min(...successors.map((s) => s.latestStart));
132
- cpmTask.latestStart = cpmTask.latestFinish - duration;
133
- }
134
- }
135
- }
136
- /**
137
- * Calculate slack and mark critical tasks
138
- * Slack = LS - ES (or LF - EF)
139
- * Critical = slack ≈ 0
140
- */
141
- function calculateSlackAndCritical(taskMap) {
142
- const FLOAT_TOLERANCE = 0.001;
143
- for (const task of taskMap.values()) {
144
- task.slack = task.latestStart - task.earliestStart;
145
- task.isCritical = Math.abs(task.slack) < FLOAT_TOLERANCE;
146
- }
147
- }
148
- /**
149
- * Count dependents for bottleneck detection using successor index
150
- * Uses dynamic programming: dependentCount = direct successors + their dependentCounts
151
- * O(n) instead of O(n³) with findDependents
152
- */
153
- function countDependents(sortedTasks, taskMap, successorIndex) {
154
- // Process in reverse topological order so we compute children before parents
155
- const reverseSorted = [...sortedTasks].reverse();
156
- for (const task of reverseSorted) {
157
- const cpmTask = taskMap.get(task.id);
158
- if (!cpmTask)
159
- continue;
160
- const successors = successorIndex.get(task.id) ?? [];
161
- // dependentCount = number of direct successors + sum of their dependentCounts
162
- let count = successors.length;
163
- for (const successor of successors) {
164
- count += successor.dependentCount;
165
- }
166
- cpmTask.dependentCount = count;
167
- }
168
- }
169
- /**
170
- * Extract critical path tasks in topological order
171
- */
172
- function extractCriticalPath(sortedTasks, taskMap) {
173
- return sortedTasks
174
- .map((t) => taskMap.get(t.id))
175
- .filter((t) => t !== undefined && t.isCritical);
176
- }
177
- /**
178
- * Find top bottlenecks (critical tasks blocking the most downstream work)
179
- */
180
- function findBottlenecks(criticalPath, limit = 5) {
181
- return [...criticalPath]
182
- .sort((a, b) => b.dependentCount - a.dependentCount)
183
- .slice(0, limit);
184
- }
185
- /**
186
- * Perform Critical Path Method analysis
187
- *
188
- * CPM calculates:
189
- * - Earliest Start (ES): Earliest a task can start
190
- * - Earliest Finish (EF): ES + duration
191
- * - Latest Finish (LF): Latest a task can finish without delaying project
192
- * - Latest Start (LS): LF - duration
193
- * - Slack: LS - ES (or LF - EF)
194
- * - Critical Path: Tasks with slack = 0
195
- */
196
- export function criticalPathAnalysis(tasks) {
197
- // Filter to only pending/in_progress tasks
198
- const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
199
- if (activeTasks.length === 0) {
200
- return {
201
- tasks: [],
202
- criticalPath: [],
203
- projectDuration: 0,
204
- bottlenecks: [],
205
- };
206
- }
207
- // Sort topologically
208
- const sortedTasks = topologicalSort(activeTasks);
209
- // Initialize CPM tasks
210
- const taskMap = initializeCPMTasks(sortedTasks);
211
- // Build successor index once (O(n*d)) for O(1) lookups
212
- const successorIndex = buildSuccessorIndex(sortedTasks, taskMap);
213
- // Forward pass: Calculate earliest start/finish
214
- forwardPass(sortedTasks, taskMap);
215
- // Calculate project duration
216
- const projectDuration = calculateProjectDuration(taskMap);
217
- // Backward pass: Calculate latest start/finish (uses successorIndex)
218
- backwardPass(sortedTasks, taskMap, successorIndex, projectDuration);
219
- // Calculate slack and mark critical tasks
220
- calculateSlackAndCritical(taskMap);
221
- // Count dependents for bottleneck detection (uses successorIndex, O(n))
222
- countDependents(sortedTasks, taskMap, successorIndex);
223
- // Extract critical path
224
- const criticalPath = extractCriticalPath(sortedTasks, taskMap);
225
- // Find bottlenecks
226
- const bottlenecks = findBottlenecks(criticalPath);
227
- return {
228
- tasks: sortedTasks.map((t) => taskMap.get(t.id)),
229
- criticalPath,
230
- projectDuration,
231
- bottlenecks,
232
- };
233
- }
234
- /**
235
- * Find tasks that can be executed in parallel (no dependencies between them)
236
- */
237
- export function findParallelTasks(tasks) {
238
- const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
239
- if (activeTasks.length === 0)
240
- return [];
241
- // Find tasks with no uncompleted dependencies
242
- const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
243
- const available = activeTasks.filter((task) => {
244
- const deps = getBlockedByDeps(task);
245
- return deps.every((depId) => completedIds.has(depId));
246
- });
247
- if (available.length <= 1)
248
- return [available];
249
- // Group tasks that don't depend on each other
250
- const groups = [];
251
- const processed = new Set();
252
- for (const task of available) {
253
- if (processed.has(task.id))
254
- continue;
255
- const group = [task];
256
- processed.add(task.id);
257
- for (const other of available) {
258
- if (processed.has(other.id))
259
- continue;
260
- // Check if these tasks are independent
261
- const taskDeps = (task.dependencies ?? []).map((d) => d.taskId);
262
- const otherDeps = (other.dependencies ?? []).map((d) => d.taskId);
263
- const independent = !taskDeps.includes(other.id) && !otherDeps.includes(task.id);
264
- if (independent) {
265
- group.push(other);
266
- processed.add(other.id);
267
- }
268
- }
269
- groups.push(group);
270
- }
271
- return groups;
272
- }
273
- /**
274
- * Suggest the next best task to work on
275
- */
276
- export function suggestNextTask(tasks, options = {}) {
277
- const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
278
- if (activeTasks.length === 0)
279
- return null;
280
- // Get CPM analysis
281
- const cpm = criticalPathAnalysis(tasks);
282
- // Filter by availability (all dependencies completed)
283
- const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
284
- let candidates = cpm.tasks.filter((task) => {
285
- const deps = getBlockedByDeps(task);
286
- return deps.every((depId) => completedIds.has(depId));
287
- });
288
- // Filter by context if specified
289
- if (options.contexts?.length) {
290
- const contextSet = new Set(options.contexts);
291
- const contextFiltered = candidates.filter((t) => (t.contexts ?? []).some((c) => contextSet.has(c)));
292
- if (contextFiltered.length > 0) {
293
- candidates = contextFiltered;
294
- }
295
- }
296
- // Filter by time if specified
297
- if (options.maxMinutes) {
298
- const timeFiltered = candidates.filter((t) => getTaskDuration(t) <= options.maxMinutes);
299
- if (timeFiltered.length > 0) {
300
- candidates = timeFiltered;
301
- }
302
- }
303
- if (candidates.length === 0)
304
- return null;
305
- // Score and rank candidates
306
- // Priority: Critical path > High priority > Most dependents > Shortest duration
307
- const scored = candidates.map((task) => {
308
- let score = 0;
309
- if (task.isCritical)
310
- score += 1000;
311
- score += task.dependentCount * 100;
312
- score += priorityToNumber(task.priority) * 10;
313
- // Prefer shorter tasks (quick wins)
314
- score += Math.max(0, 100 - getTaskDuration(task));
315
- return { task, score };
316
- });
317
- scored.sort((a, b) => b.score - a.score);
318
- return scored[0]?.task ?? null;
319
- }
320
- //# sourceMappingURL=critical-path.js.map