@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.
- package/dist/algorithms/critical-path.d.ts +47 -0
- package/dist/algorithms/critical-path.d.ts.map +1 -0
- package/dist/algorithms/critical-path.js +340 -0
- package/dist/algorithms/critical-path.js.map +1 -0
- package/dist/algorithms/critical-path.test.d.ts +2 -0
- package/dist/algorithms/critical-path.test.d.ts.map +1 -0
- package/dist/algorithms/critical-path.test.js +184 -0
- package/dist/algorithms/critical-path.test.js.map +1 -0
- package/dist/algorithms/dependency-integrity.d.ts +81 -0
- package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
- package/dist/algorithms/dependency-integrity.js +209 -0
- package/dist/algorithms/dependency-integrity.js.map +1 -0
- package/dist/algorithms/dependency-integrity.test.d.ts +2 -0
- package/dist/algorithms/dependency-integrity.test.d.ts.map +1 -0
- package/dist/algorithms/dependency-integrity.test.js +296 -0
- package/dist/algorithms/dependency-integrity.test.js.map +1 -0
- package/dist/algorithms/index.d.ts +5 -0
- package/dist/algorithms/index.d.ts.map +1 -0
- package/dist/algorithms/index.js +5 -0
- package/dist/algorithms/index.js.map +1 -0
- package/dist/algorithms/tech-analysis.d.ts +106 -0
- package/dist/algorithms/tech-analysis.d.ts.map +1 -0
- package/dist/algorithms/tech-analysis.js +351 -0
- package/dist/algorithms/tech-analysis.js.map +1 -0
- package/dist/algorithms/tech-analysis.test.d.ts +2 -0
- package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
- package/dist/algorithms/tech-analysis.test.js +330 -0
- package/dist/algorithms/tech-analysis.test.js.map +1 -0
- package/dist/algorithms/topological-sort.d.ts +58 -0
- package/dist/algorithms/topological-sort.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.js +201 -0
- package/dist/algorithms/topological-sort.js.map +1 -0
- package/dist/algorithms/topological-sort.test.d.ts +2 -0
- package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.test.js +154 -0
- package/dist/algorithms/topological-sort.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/inbox.d.ts +55 -0
- package/dist/schemas/inbox.d.ts.map +1 -0
- package/dist/schemas/inbox.js +25 -0
- package/dist/schemas/inbox.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +17 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/llm-guide.d.ts +147 -0
- package/dist/schemas/llm-guide.d.ts.map +1 -0
- package/dist/schemas/llm-guide.js +72 -0
- package/dist/schemas/llm-guide.js.map +1 -0
- package/dist/schemas/project.d.ts +177 -0
- package/dist/schemas/project.d.ts.map +1 -0
- package/dist/schemas/project.js +56 -0
- package/dist/schemas/project.js.map +1 -0
- package/dist/schemas/response-format.d.ts +148 -0
- package/dist/schemas/response-format.d.ts.map +1 -0
- package/dist/schemas/response-format.js +18 -0
- package/dist/schemas/response-format.js.map +1 -0
- package/dist/schemas/response-schema.d.ts +307 -0
- package/dist/schemas/response-schema.d.ts.map +1 -0
- package/dist/schemas/response-schema.js +78 -0
- package/dist/schemas/response-schema.js.map +1 -0
- package/dist/schemas/response-schema.test.d.ts +2 -0
- package/dist/schemas/response-schema.test.d.ts.map +1 -0
- package/dist/schemas/response-schema.test.js +256 -0
- package/dist/schemas/response-schema.test.js.map +1 -0
- package/dist/schemas/state.d.ts +17 -0
- package/dist/schemas/state.d.ts.map +1 -0
- package/dist/schemas/state.js +17 -0
- package/dist/schemas/state.js.map +1 -0
- package/dist/schemas/task.d.ts +881 -0
- package/dist/schemas/task.d.ts.map +1 -0
- package/dist/schemas/task.js +177 -0
- package/dist/schemas/task.js.map +1 -0
- package/dist/schemas/view.d.ts +143 -0
- package/dist/schemas/view.d.ts.map +1 -0
- package/dist/schemas/view.js +48 -0
- package/dist/schemas/view.js.map +1 -0
- package/dist/utils/dashboard-renderer.d.ts +93 -0
- package/dist/utils/dashboard-renderer.d.ts.map +1 -0
- package/dist/utils/dashboard-renderer.js +416 -0
- package/dist/utils/dashboard-renderer.js.map +1 -0
- package/dist/utils/dashboard-renderer.test.d.ts +2 -0
- package/dist/utils/dashboard-renderer.test.d.ts.map +1 -0
- package/dist/utils/dashboard-renderer.test.js +772 -0
- package/dist/utils/dashboard-renderer.test.js.map +1 -0
- package/dist/utils/date.d.ts +94 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +323 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/date.test.d.ts +2 -0
- package/dist/utils/date.test.d.ts.map +1 -0
- package/dist/utils/date.test.js +276 -0
- package/dist/utils/date.test.js.map +1 -0
- package/dist/utils/hierarchy.d.ts +102 -0
- package/dist/utils/hierarchy.d.ts.map +1 -0
- package/dist/utils/hierarchy.js +236 -0
- package/dist/utils/hierarchy.js.map +1 -0
- package/dist/utils/hierarchy.test.d.ts +2 -0
- package/dist/utils/hierarchy.test.d.ts.map +1 -0
- package/dist/utils/hierarchy.test.js +423 -0
- package/dist/utils/hierarchy.test.js.map +1 -0
- package/dist/utils/id.d.ts +60 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +118 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/id.test.d.ts +2 -0
- package/dist/utils/id.test.d.ts.map +1 -0
- package/dist/utils/id.test.js +193 -0
- package/dist/utils/id.test.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +34 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/natural-language.d.ts +111 -0
- package/dist/utils/natural-language.d.ts.map +1 -0
- package/dist/utils/natural-language.js +297 -0
- package/dist/utils/natural-language.js.map +1 -0
- package/dist/utils/natural-language.test.d.ts +2 -0
- package/dist/utils/natural-language.test.d.ts.map +1 -0
- package/dist/utils/natural-language.test.js +197 -0
- package/dist/utils/natural-language.test.js.map +1 -0
- package/dist/utils/priority-queue.d.ts +17 -0
- package/dist/utils/priority-queue.d.ts.map +1 -0
- package/dist/utils/priority-queue.js +62 -0
- package/dist/utils/priority-queue.js.map +1 -0
- package/dist/utils/priority-queue.test.d.ts +2 -0
- package/dist/utils/priority-queue.test.d.ts.map +1 -0
- package/dist/utils/priority-queue.test.js +82 -0
- package/dist/utils/priority-queue.test.js.map +1 -0
- package/dist/utils/projection.d.ts +65 -0
- package/dist/utils/projection.d.ts.map +1 -0
- package/dist/utils/projection.js +180 -0
- package/dist/utils/projection.js.map +1 -0
- package/dist/utils/projection.test.d.ts +2 -0
- package/dist/utils/projection.test.d.ts.map +1 -0
- package/dist/utils/projection.test.js +341 -0
- package/dist/utils/projection.test.js.map +1 -0
- package/dist/utils/terminal-ui.d.ts +208 -0
- package/dist/utils/terminal-ui.d.ts.map +1 -0
- package/dist/utils/terminal-ui.js +614 -0
- package/dist/utils/terminal-ui.js.map +1 -0
- package/dist/utils/terminal-ui.test.d.ts +2 -0
- package/dist/utils/terminal-ui.test.d.ts.map +1 -0
- package/dist/utils/terminal-ui.test.js +683 -0
- package/dist/utils/terminal-ui.test.js.map +1 -0
- package/dist/utils/workspace.d.ts +102 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +183 -0
- package/dist/utils/workspace.js.map +1 -0
- package/dist/utils/workspace.test.d.ts +2 -0
- package/dist/utils/workspace.test.d.ts.map +1 -0
- package/dist/utils/workspace.test.js +97 -0
- package/dist/utils/workspace.test.js.map +1 -0
- package/package.json +5 -1
- package/src/algorithms/critical-path.test.ts +227 -0
- package/src/algorithms/critical-path.ts +14 -34
- package/src/algorithms/dependency-integrity.test.ts +335 -0
- package/src/algorithms/dependency-integrity.ts +4 -13
- package/src/algorithms/tech-analysis.test.ts +405 -0
- package/src/algorithms/tech-analysis.ts +27 -27
- package/src/algorithms/topological-sort.test.ts +182 -0
- package/src/algorithms/topological-sort.ts +6 -10
- package/src/schemas/index.ts +2 -13
- package/src/schemas/response-format.ts +6 -6
- package/src/schemas/response-schema.test.ts +314 -0
- package/src/schemas/response-schema.ts +25 -20
- package/src/schemas/task.ts +4 -22
- package/src/utils/dashboard-renderer.test.ts +976 -0
- package/src/utils/dashboard-renderer.ts +27 -59
- package/src/utils/date.test.ts +329 -0
- package/src/utils/date.ts +2 -10
- package/src/utils/hierarchy.test.ts +488 -0
- package/src/utils/hierarchy.ts +4 -5
- package/src/utils/id.test.ts +235 -0
- package/src/utils/index.ts +7 -1
- package/src/utils/natural-language.test.ts +234 -0
- package/src/utils/priority-queue.test.ts +103 -0
- package/src/utils/projection.test.ts +430 -0
- package/src/utils/terminal-ui.test.ts +831 -0
- package/src/utils/terminal-ui.ts +53 -54
- 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,
|
|
9
|
-
infra: 0,
|
|
10
|
-
devops: 1,
|
|
11
|
-
backend: 2,
|
|
12
|
-
frontend: 3,
|
|
13
|
-
test: 4,
|
|
14
|
-
docs: 4,
|
|
15
|
-
refactor: 5,
|
|
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 (
|
|
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",
|
|
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;
|
|
321
|
+
low: number; // 1 to LOW_MAX
|
|
318
322
|
medium: number; // LOW_MAX+1 to MEDIUM_MAX
|
|
319
|
-
high: number;
|
|
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);
|