@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
@@ -35,9 +35,7 @@ export interface ValidateDependencyOptions {
35
35
  /**
36
36
  * Validates if a dependency can be added between two tasks
37
37
  */
38
- export function validateDependency(
39
- options: ValidateDependencyOptions
40
- ): DependencyValidationResult {
38
+ export function validateDependency(options: ValidateDependencyOptions): DependencyValidationResult {
41
39
  const { taskId, blockedBy, tasks, checkDuplicates = true } = options;
42
40
 
43
41
  // 1. Self-dependency check
@@ -117,9 +115,7 @@ export interface InvalidDependencyReference {
117
115
  /**
118
116
  * Find all invalid dependency references (orphaned references)
119
117
  */
120
- export function findInvalidDependencies(
121
- tasks: Task[]
122
- ): InvalidDependencyReference[] {
118
+ export function findInvalidDependencies(tasks: Task[]): InvalidDependencyReference[] {
123
119
  const taskIds = new Set(tasks.map((t) => t.id));
124
120
  const invalid: InvalidDependencyReference[] = [];
125
121
 
@@ -242,17 +238,12 @@ export interface DependencyIntegrityReport {
242
238
  /**
243
239
  * Check overall dependency integrity of a project
244
240
  */
245
- export function checkDependencyIntegrity(
246
- tasks: Task[]
247
- ): DependencyIntegrityReport {
241
+ export function checkDependencyIntegrity(tasks: Task[]): DependencyIntegrityReport {
248
242
  const selfDeps = findSelfDependencies(tasks);
249
243
  const invalidRefs = findInvalidDependencies(tasks);
250
244
  const cycles = detectCircularDependencies(tasks);
251
245
 
252
- const totalDependencies = tasks.reduce(
253
- (sum, t) => sum + (t.dependencies?.length ?? 0),
254
- 0
255
- );
246
+ const totalDependencies = tasks.reduce((sum, t) => sum + (t.dependencies?.length ?? 0), 0);
256
247
 
257
248
  const issueCount = selfDeps.length + invalidRefs.length + cycles.length;
258
249
  const valid = issueCount === 0;
@@ -0,0 +1,405 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ suggestSafeOrder,
4
+ findBreakingChanges,
5
+ findHighRiskTasks,
6
+ groupByTechArea,
7
+ getComplexitySummary,
8
+ getTechStackSummary,
9
+ suggestSubtaskCount,
10
+ } from "./tech-analysis.js";
11
+ import type { Task, TechArea, RiskLevel } from "../schemas/task.js";
12
+
13
+ // Helper to create mock tasks with tech stack analysis
14
+ function createTask(
15
+ id: string,
16
+ options: {
17
+ priority?: "critical" | "high" | "medium" | "low";
18
+ status?: "pending" | "in_progress" | "completed" | "blocked" | "cancelled";
19
+ areas?: TechArea[];
20
+ riskLevel?: RiskLevel;
21
+ hasBreakingChange?: boolean;
22
+ complexityScore?: number;
23
+ } = {}
24
+ ): Task {
25
+ const task: Task = {
26
+ id,
27
+ title: `Task ${id}`,
28
+ status: options.status ?? "pending",
29
+ priority: options.priority ?? "medium",
30
+ workspace: "test-workspace",
31
+ createdAt: new Date().toISOString(),
32
+ updatedAt: new Date().toISOString(),
33
+ };
34
+
35
+ if (options.areas || options.riskLevel !== undefined || options.hasBreakingChange !== undefined) {
36
+ task.techStack = {};
37
+ if (options.areas) {
38
+ task.techStack.areas = options.areas;
39
+ }
40
+ if (options.riskLevel !== undefined) {
41
+ task.techStack.riskLevel = options.riskLevel;
42
+ }
43
+ if (options.hasBreakingChange !== undefined) {
44
+ task.techStack.hasBreakingChange = options.hasBreakingChange;
45
+ }
46
+ }
47
+
48
+ if (options.complexityScore !== undefined) {
49
+ task.complexity = {
50
+ score: options.complexityScore,
51
+ };
52
+ }
53
+
54
+ return task;
55
+ }
56
+
57
+ describe("suggestSafeOrder", () => {
58
+ test("returns empty result for empty input", () => {
59
+ const result = suggestSafeOrder([]);
60
+ expect(result.orderedTasks).toEqual([]);
61
+ expect(result.phases).toEqual([]);
62
+ expect(result.summary.totalTasks).toBe(0);
63
+ });
64
+
65
+ test("returns empty for all completed tasks", () => {
66
+ const tasks = [createTask("A", { status: "completed" })];
67
+ const result = suggestSafeOrder(tasks);
68
+ expect(result.orderedTasks).toEqual([]);
69
+ });
70
+
71
+ test("orders by tech level (schema before backend)", () => {
72
+ const tasks = [
73
+ createTask("backend-task", { areas: ["backend"] }),
74
+ createTask("schema-task", { areas: ["schema"] }),
75
+ ];
76
+ const result = suggestSafeOrder(tasks);
77
+
78
+ expect(result.orderedTasks[0]!.id).toBe("schema-task");
79
+ expect(result.orderedTasks[1]!.id).toBe("backend-task");
80
+ });
81
+
82
+ test("orders by tech level (full chain)", () => {
83
+ const tasks = [
84
+ createTask("frontend", { areas: ["frontend"] }),
85
+ createTask("backend", { areas: ["backend"] }),
86
+ createTask("schema", { areas: ["schema"] }),
87
+ createTask("test", { areas: ["test"] }),
88
+ createTask("devops", { areas: ["devops"] }),
89
+ ];
90
+ const result = suggestSafeOrder(tasks);
91
+
92
+ const order = result.orderedTasks.map((t) => t.id);
93
+ expect(order).toEqual(["schema", "devops", "backend", "frontend", "test"]);
94
+ });
95
+
96
+ test("orders by risk level within same tech level", () => {
97
+ const tasks = [
98
+ createTask("high-risk", { areas: ["backend"], riskLevel: "high" }),
99
+ createTask("low-risk", { areas: ["backend"], riskLevel: "low" }),
100
+ createTask("medium-risk", { areas: ["backend"], riskLevel: "medium" }),
101
+ ];
102
+ const result = suggestSafeOrder(tasks);
103
+
104
+ const order = result.orderedTasks.map((t) => t.id);
105
+ expect(order).toEqual(["low-risk", "medium-risk", "high-risk"]);
106
+ });
107
+
108
+ test("puts breaking changes last within same tech/risk level", () => {
109
+ const tasks = [
110
+ createTask("breaking", { areas: ["backend"], riskLevel: "low", hasBreakingChange: true }),
111
+ createTask("safe", { areas: ["backend"], riskLevel: "low", hasBreakingChange: false }),
112
+ ];
113
+ const result = suggestSafeOrder(tasks);
114
+
115
+ expect(result.orderedTasks[0]!.id).toBe("safe");
116
+ expect(result.orderedTasks[1]!.id).toBe("breaking");
117
+ });
118
+
119
+ test("uses priority as tiebreaker", () => {
120
+ const tasks = [
121
+ createTask("low-priority", { areas: ["backend"], priority: "low" }),
122
+ createTask("high-priority", { areas: ["backend"], priority: "high" }),
123
+ ];
124
+ const result = suggestSafeOrder(tasks);
125
+
126
+ expect(result.orderedTasks[0]!.id).toBe("high-priority");
127
+ expect(result.orderedTasks[1]!.id).toBe("low-priority");
128
+ });
129
+
130
+ test("groups tasks into phases by tech level", () => {
131
+ const tasks = [
132
+ createTask("schema1", { areas: ["schema"] }),
133
+ createTask("schema2", { areas: ["schema"] }),
134
+ createTask("backend1", { areas: ["backend"] }),
135
+ ];
136
+ const result = suggestSafeOrder(tasks);
137
+
138
+ expect(result.phases.length).toBe(2);
139
+ expect(result.phases[0]!.primaryArea).toBe("schema");
140
+ expect(result.phases[0]!.tasks.length).toBe(2);
141
+ expect(result.phases[1]!.primaryArea).toBe("backend");
142
+ expect(result.phases[1]!.tasks.length).toBe(1);
143
+ });
144
+
145
+ test("calculates summary statistics correctly", () => {
146
+ const tasks = [
147
+ createTask("t1", { areas: ["backend"], hasBreakingChange: true }),
148
+ createTask("t2", { areas: ["backend"], riskLevel: "high" }),
149
+ createTask("t3", { areas: ["backend"], riskLevel: "critical" }),
150
+ createTask("t4", { areas: ["backend"] }),
151
+ ];
152
+ const result = suggestSafeOrder(tasks);
153
+
154
+ expect(result.summary.totalTasks).toBe(4);
155
+ expect(result.summary.breakingChanges).toBe(1);
156
+ expect(result.summary.highRiskCount).toBe(2);
157
+ });
158
+ });
159
+
160
+ describe("findBreakingChanges", () => {
161
+ test("returns empty for no breaking changes", () => {
162
+ const tasks = [
163
+ createTask("t1", { areas: ["backend"], hasBreakingChange: false }),
164
+ createTask("t2", { areas: ["backend"] }),
165
+ ];
166
+ const result = findBreakingChanges(tasks);
167
+ expect(result).toEqual([]);
168
+ });
169
+
170
+ test("finds tasks with breaking changes", () => {
171
+ const tasks = [
172
+ createTask("t1", { areas: ["backend"], hasBreakingChange: true }),
173
+ createTask("t2", { areas: ["backend"], hasBreakingChange: false }),
174
+ createTask("t3", { areas: ["schema"], hasBreakingChange: true }),
175
+ ];
176
+ const result = findBreakingChanges(tasks);
177
+ expect(result.length).toBe(2);
178
+ expect(result.map((t) => t.id).sort()).toEqual(["t1", "t3"]);
179
+ });
180
+
181
+ test("excludes completed tasks", () => {
182
+ const tasks = [
183
+ createTask("t1", { status: "completed", areas: ["backend"], hasBreakingChange: true }),
184
+ createTask("t2", { status: "pending", areas: ["backend"], hasBreakingChange: true }),
185
+ ];
186
+ const result = findBreakingChanges(tasks);
187
+ expect(result.length).toBe(1);
188
+ expect(result[0]!.id).toBe("t2");
189
+ });
190
+ });
191
+
192
+ describe("findHighRiskTasks", () => {
193
+ test("returns empty for no high-risk tasks", () => {
194
+ const tasks = [
195
+ createTask("t1", { areas: ["backend"], riskLevel: "low" }),
196
+ createTask("t2", { areas: ["backend"], riskLevel: "medium" }),
197
+ ];
198
+ const result = findHighRiskTasks(tasks);
199
+ expect(result).toEqual([]);
200
+ });
201
+
202
+ test("finds high and critical risk tasks", () => {
203
+ const tasks = [
204
+ createTask("t1", { areas: ["backend"], riskLevel: "low" }),
205
+ createTask("t2", { areas: ["backend"], riskLevel: "high" }),
206
+ createTask("t3", { areas: ["backend"], riskLevel: "critical" }),
207
+ ];
208
+ const result = findHighRiskTasks(tasks);
209
+ expect(result.length).toBe(2);
210
+ expect(result.map((t) => t.id).sort()).toEqual(["t2", "t3"]);
211
+ });
212
+ });
213
+
214
+ describe("groupByTechArea", () => {
215
+ test("returns all areas with empty arrays for empty input", () => {
216
+ const result = groupByTechArea([]);
217
+ expect(result.get("schema")).toEqual([]);
218
+ expect(result.get("backend")).toEqual([]);
219
+ expect(result.get("frontend")).toEqual([]);
220
+ });
221
+
222
+ test("groups tasks by their tech areas", () => {
223
+ const tasks = [
224
+ createTask("t1", { areas: ["backend"] }),
225
+ createTask("t2", { areas: ["backend"] }),
226
+ createTask("t3", { areas: ["frontend"] }),
227
+ ];
228
+ const result = groupByTechArea(tasks);
229
+
230
+ expect(result.get("backend")!.length).toBe(2);
231
+ expect(result.get("frontend")!.length).toBe(1);
232
+ expect(result.get("schema")!.length).toBe(0);
233
+ });
234
+
235
+ test("task appears in multiple groups if it spans multiple areas", () => {
236
+ const tasks = [createTask("fullstack", { areas: ["backend", "frontend"] })];
237
+ const result = groupByTechArea(tasks);
238
+
239
+ expect(result.get("backend")!.length).toBe(1);
240
+ expect(result.get("frontend")!.length).toBe(1);
241
+ expect(result.get("backend")![0]!.id).toBe("fullstack");
242
+ });
243
+
244
+ test("defaults to backend if no area specified", () => {
245
+ const tasks = [createTask("t1", {})];
246
+ const result = groupByTechArea(tasks);
247
+ expect(result.get("backend")!.length).toBe(1);
248
+ });
249
+
250
+ test("excludes completed tasks", () => {
251
+ const tasks = [
252
+ createTask("t1", { status: "completed", areas: ["backend"] }),
253
+ createTask("t2", { status: "pending", areas: ["backend"] }),
254
+ ];
255
+ const result = groupByTechArea(tasks);
256
+ expect(result.get("backend")!.length).toBe(1);
257
+ });
258
+ });
259
+
260
+ describe("getComplexitySummary", () => {
261
+ test("returns empty summary for no tasks", () => {
262
+ const result = getComplexitySummary([]);
263
+ expect(result.distribution).toEqual({ low: 0, medium: 0, high: 0 });
264
+ expect(result.needsBreakdown).toEqual([]);
265
+ expect(result.averageScore).toBe(0);
266
+ expect(result.unanalyzed).toEqual([]);
267
+ });
268
+
269
+ test("calculates distribution correctly", () => {
270
+ const tasks = [
271
+ createTask("t1", { complexityScore: 2 }), // low
272
+ createTask("t2", { complexityScore: 3 }), // low
273
+ createTask("t3", { complexityScore: 5 }), // medium
274
+ createTask("t4", { complexityScore: 8 }), // high
275
+ ];
276
+ const result = getComplexitySummary(tasks);
277
+
278
+ expect(result.distribution.low).toBe(2);
279
+ expect(result.distribution.medium).toBe(1);
280
+ expect(result.distribution.high).toBe(1);
281
+ });
282
+
283
+ test("identifies tasks needing breakdown (score >= 7)", () => {
284
+ const tasks = [
285
+ createTask("t1", { complexityScore: 6 }),
286
+ createTask("t2", { complexityScore: 7 }),
287
+ createTask("t3", { complexityScore: 9 }),
288
+ ];
289
+ const result = getComplexitySummary(tasks);
290
+
291
+ expect(result.needsBreakdown.length).toBe(2);
292
+ expect(result.needsBreakdown.map((t) => t.id).sort()).toEqual(["t2", "t3"]);
293
+ });
294
+
295
+ test("calculates average score correctly", () => {
296
+ const tasks = [
297
+ createTask("t1", { complexityScore: 2 }),
298
+ createTask("t2", { complexityScore: 6 }),
299
+ createTask("t3", { complexityScore: 10 }),
300
+ ];
301
+ const result = getComplexitySummary(tasks);
302
+
303
+ expect(result.averageScore).toBe(6); // (2+6+10)/3
304
+ });
305
+
306
+ test("identifies unanalyzed tasks", () => {
307
+ const tasks = [createTask("analyzed", { complexityScore: 5 }), createTask("not-analyzed", {})];
308
+ const result = getComplexitySummary(tasks);
309
+
310
+ expect(result.unanalyzed.length).toBe(1);
311
+ expect(result.unanalyzed[0]!.id).toBe("not-analyzed");
312
+ });
313
+
314
+ test("excludes completed tasks", () => {
315
+ const tasks = [
316
+ createTask("t1", { status: "completed", complexityScore: 10 }),
317
+ createTask("t2", { status: "pending", complexityScore: 5 }),
318
+ ];
319
+ const result = getComplexitySummary(tasks);
320
+
321
+ expect(result.distribution.high).toBe(0);
322
+ expect(result.distribution.medium).toBe(1);
323
+ });
324
+ });
325
+
326
+ describe("getTechStackSummary", () => {
327
+ test("returns empty summary for no tasks", () => {
328
+ const result = getTechStackSummary([]);
329
+ expect(result.areaCounts.schema).toBe(0);
330
+ expect(result.breakingChanges).toEqual([]);
331
+ expect(result.unanalyzed).toEqual([]);
332
+ });
333
+
334
+ test("counts tasks by area correctly", () => {
335
+ const tasks = [
336
+ createTask("t1", { areas: ["backend"] }),
337
+ createTask("t2", { areas: ["backend", "frontend"] }),
338
+ createTask("t3", { areas: ["schema"] }),
339
+ ];
340
+ const result = getTechStackSummary(tasks);
341
+
342
+ expect(result.areaCounts.backend).toBe(2);
343
+ expect(result.areaCounts.frontend).toBe(1);
344
+ expect(result.areaCounts.schema).toBe(1);
345
+ });
346
+
347
+ test("tracks risk distribution", () => {
348
+ const tasks = [
349
+ createTask("t1", { areas: ["backend"], riskLevel: "low" }),
350
+ createTask("t2", { areas: ["backend"], riskLevel: "medium" }),
351
+ createTask("t3", { areas: ["backend"], riskLevel: "high" }),
352
+ ];
353
+ const result = getTechStackSummary(tasks);
354
+
355
+ expect(result.riskDistribution.low).toBe(1);
356
+ expect(result.riskDistribution.medium).toBe(1);
357
+ expect(result.riskDistribution.high).toBe(1);
358
+ });
359
+
360
+ test("identifies breaking changes", () => {
361
+ const tasks = [
362
+ createTask("t1", { areas: ["backend"], hasBreakingChange: true }),
363
+ createTask("t2", { areas: ["backend"], hasBreakingChange: false }),
364
+ ];
365
+ const result = getTechStackSummary(tasks);
366
+
367
+ expect(result.breakingChanges.length).toBe(1);
368
+ expect(result.breakingChanges[0]!.id).toBe("t1");
369
+ });
370
+
371
+ test("identifies unanalyzed tasks", () => {
372
+ const tasks = [createTask("analyzed", { areas: ["backend"] }), createTask("not-analyzed", {})];
373
+ const result = getTechStackSummary(tasks);
374
+
375
+ expect(result.unanalyzed.length).toBe(1);
376
+ expect(result.unanalyzed[0]!.id).toBe("not-analyzed");
377
+ });
378
+ });
379
+
380
+ describe("suggestSubtaskCount", () => {
381
+ test("returns 0 for very low complexity (1-2)", () => {
382
+ expect(suggestSubtaskCount(1)).toBe(0);
383
+ expect(suggestSubtaskCount(2)).toBe(0);
384
+ });
385
+
386
+ test("returns 2 for low complexity (3-4)", () => {
387
+ expect(suggestSubtaskCount(3)).toBe(2);
388
+ expect(suggestSubtaskCount(4)).toBe(2);
389
+ });
390
+
391
+ test("returns 3-4 for medium complexity (5-6)", () => {
392
+ expect(suggestSubtaskCount(5)).toBe(4);
393
+ expect(suggestSubtaskCount(6)).toBe(4);
394
+ });
395
+
396
+ test("returns 5-6 for high complexity (7-8)", () => {
397
+ expect(suggestSubtaskCount(7)).toBe(6);
398
+ expect(suggestSubtaskCount(8)).toBe(6);
399
+ });
400
+
401
+ test("returns 7-10 for very high complexity (9-10)", () => {
402
+ expect(suggestSubtaskCount(9)).toBe(9);
403
+ expect(suggestSubtaskCount(10)).toBe(10);
404
+ });
405
+ });
@@ -5,14 +5,14 @@ import type { Task, TechArea, RiskLevel } from "../schemas/task.js";
5
5
  * Based on dependency flow: DB changes → Infrastructure → Backend → Frontend → Tests
6
6
  */
7
7
  const TECH_ORDER: Record<TechArea, number> = {
8
- schema: 0, // DB/schema changes first
9
- infra: 0, // Infrastructure setup
10
- devops: 1, // CI/CD pipelines
11
- backend: 2, // API/server
12
- frontend: 3, // UI
13
- test: 4, // Tests
14
- docs: 4, // Documentation
15
- refactor: 5, // Refactoring last
8
+ schema: 0, // DB/schema changes first
9
+ infra: 0, // Infrastructure setup
10
+ devops: 1, // CI/CD pipelines
11
+ backend: 2, // API/server
12
+ frontend: 3, // UI
13
+ test: 4, // Tests
14
+ docs: 4, // Documentation
15
+ refactor: 5, // Refactoring last
16
16
  };
17
17
 
18
18
  /**
@@ -143,9 +143,7 @@ function hasBreakingChange(task: Task): boolean {
143
143
  */
144
144
  export function suggestSafeOrder(tasks: Task[]): SafeOrderResult {
145
145
  // Filter to active tasks only
146
- const activeTasks = tasks.filter(
147
- (t) => t.status === "pending" || t.status === "in_progress"
148
- );
146
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
149
147
 
150
148
  if (activeTasks.length === 0) {
151
149
  return {
@@ -175,7 +173,10 @@ export function suggestSafeOrder(tasks: Task[]): SafeOrderResult {
175
173
  if (aBreaking !== bBreaking) return aBreaking - bBreaking;
176
174
 
177
175
  // 4. Priority as tiebreaker (higher priority first)
178
- return (PRIORITY_ORDER[a.priority] ?? DEFAULT_PRIORITY_ORDER) - (PRIORITY_ORDER[b.priority] ?? DEFAULT_PRIORITY_ORDER);
176
+ return (
177
+ (PRIORITY_ORDER[a.priority] ?? DEFAULT_PRIORITY_ORDER) -
178
+ (PRIORITY_ORDER[b.priority] ?? DEFAULT_PRIORITY_ORDER)
179
+ );
179
180
  });
180
181
 
181
182
  // Group into phases by tech level
@@ -258,9 +259,7 @@ function getPrimaryArea(task: Task): TechArea | "mixed" {
258
259
  */
259
260
  export function findBreakingChanges(tasks: Task[]): Task[] {
260
261
  return tasks.filter(
261
- (t) =>
262
- (t.status === "pending" || t.status === "in_progress") &&
263
- hasBreakingChange(t)
262
+ (t) => (t.status === "pending" || t.status === "in_progress") && hasBreakingChange(t)
264
263
  );
265
264
  }
266
265
 
@@ -284,16 +283,21 @@ export function groupByTechArea(tasks: Task[]): Map<TechArea, Task[]> {
284
283
 
285
284
  // Initialize all groups
286
285
  const allAreas: TechArea[] = [
287
- "schema", "infra", "devops", "backend", "frontend", "test", "docs", "refactor"
286
+ "schema",
287
+ "infra",
288
+ "devops",
289
+ "backend",
290
+ "frontend",
291
+ "test",
292
+ "docs",
293
+ "refactor",
288
294
  ];
289
295
  for (const area of allAreas) {
290
296
  groups.set(area, []);
291
297
  }
292
298
 
293
299
  // Group active tasks
294
- const activeTasks = tasks.filter(
295
- (t) => t.status === "pending" || t.status === "in_progress"
296
- );
300
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
297
301
 
298
302
  for (const task of activeTasks) {
299
303
  const areas = task.techStack?.areas ?? [];
@@ -314,9 +318,9 @@ export function groupByTechArea(tasks: Task[]): Map<TechArea, Task[]> {
314
318
  * Complexity distribution by level
315
319
  */
316
320
  export interface ComplexityDistribution {
317
- low: number; // 1 to LOW_MAX
321
+ low: number; // 1 to LOW_MAX
318
322
  medium: number; // LOW_MAX+1 to MEDIUM_MAX
319
- high: number; // MEDIUM_MAX+1 to 10
323
+ high: number; // MEDIUM_MAX+1 to 10
320
324
  }
321
325
 
322
326
  /**
@@ -337,9 +341,7 @@ export interface ComplexitySummary {
337
341
  * Analyze complexity distribution across tasks
338
342
  */
339
343
  export function getComplexitySummary(tasks: Task[]): ComplexitySummary {
340
- const activeTasks = tasks.filter(
341
- (t) => t.status === "pending" || t.status === "in_progress"
342
- );
344
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
343
345
 
344
346
  const analyzed = activeTasks.filter((t) => t.complexity?.score !== undefined);
345
347
  const unanalyzed = activeTasks.filter((t) => t.complexity?.score === undefined);
@@ -394,9 +396,7 @@ export interface TechStackSummary {
394
396
  * Analyze tech stack distribution across tasks
395
397
  */
396
398
  export function getTechStackSummary(tasks: Task[]): TechStackSummary {
397
- const activeTasks = tasks.filter(
398
- (t) => t.status === "pending" || t.status === "in_progress"
399
- );
399
+ const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
400
400
 
401
401
  const analyzed = activeTasks.filter((t) => t.techStack?.areas !== undefined);
402
402
  const unanalyzed = activeTasks.filter((t) => t.techStack?.areas === undefined);