@task-mcp/shared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/algorithms/critical-path.d.ts +46 -0
- package/dist/algorithms/critical-path.d.ts.map +1 -0
- package/dist/algorithms/critical-path.js +308 -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 +194 -0
- package/dist/algorithms/critical-path.test.js.map +1 -0
- package/dist/algorithms/index.d.ts +3 -0
- package/dist/algorithms/index.d.ts.map +1 -0
- package/dist/algorithms/index.js +3 -0
- package/dist/algorithms/index.js.map +1 -0
- package/dist/algorithms/topological-sort.d.ts +41 -0
- package/dist/algorithms/topological-sort.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.js +168 -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 +162 -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/index.d.ts +4 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/project.d.ts +55 -0
- package/dist/schemas/project.d.ts.map +1 -0
- package/dist/schemas/project.js +48 -0
- package/dist/schemas/project.js.map +1 -0
- package/dist/schemas/task.d.ts +124 -0
- package/dist/schemas/task.d.ts.map +1 -0
- package/dist/schemas/task.js +89 -0
- package/dist/schemas/task.js.map +1 -0
- package/dist/schemas/view.d.ts +44 -0
- package/dist/schemas/view.d.ts.map +1 -0
- package/dist/schemas/view.js +33 -0
- package/dist/schemas/view.js.map +1 -0
- package/dist/utils/date.d.ts +25 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +103 -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 +138 -0
- package/dist/utils/date.test.js.map +1 -0
- package/dist/utils/id.d.ts +27 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/natural-language.d.ts +12 -0
- package/dist/utils/natural-language.d.ts.map +1 -0
- package/dist/utils/natural-language.js +112 -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 +132 -0
- package/dist/utils/natural-language.test.js.map +1 -0
- package/package.json +46 -0
- package/src/algorithms/critical-path.test.ts +241 -0
- package/src/algorithms/critical-path.ts +413 -0
- package/src/algorithms/index.ts +17 -0
- package/src/algorithms/topological-sort.test.ts +190 -0
- package/src/algorithms/topological-sort.ts +204 -0
- package/src/index.ts +8 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/project.ts +62 -0
- package/src/schemas/task.ts +116 -0
- package/src/schemas/view.ts +46 -0
- package/src/utils/date.test.ts +160 -0
- package/src/utils/date.ts +119 -0
- package/src/utils/id.ts +45 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/natural-language.test.ts +154 -0
- package/src/utils/natural-language.ts +125 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { parseRelativeDate } from "./date.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse natural language task input
|
|
4
|
+
*
|
|
5
|
+
* Examples:
|
|
6
|
+
* - "Review PR tomorrow #dev !high"
|
|
7
|
+
* - "내일까지 보고서 작성 #업무 !높음 @집중"
|
|
8
|
+
* - "Fix bug by Friday #backend !critical"
|
|
9
|
+
* - "Write tests every Monday #testing"
|
|
10
|
+
*/
|
|
11
|
+
export function parseTaskInput(input) {
|
|
12
|
+
let remaining = input.trim();
|
|
13
|
+
const result = { title: "" };
|
|
14
|
+
// Extract priority (!high, !critical, !medium, !low, !높음, !보통, !낮음)
|
|
15
|
+
const priorityMatch = remaining.match(/!([\p{L}\p{N}_]+)/gu);
|
|
16
|
+
if (priorityMatch) {
|
|
17
|
+
for (const match of priorityMatch) {
|
|
18
|
+
const priority = parsePriority(match.slice(1));
|
|
19
|
+
if (priority) {
|
|
20
|
+
result.priority = priority;
|
|
21
|
+
remaining = remaining.replace(match, "").trim();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Extract contexts (@focus, @review, @집중)
|
|
27
|
+
const contextMatches = remaining.match(/@([\p{L}\p{N}_]+)/gu);
|
|
28
|
+
if (contextMatches) {
|
|
29
|
+
result.contexts = contextMatches.map((m) => m.slice(1));
|
|
30
|
+
for (const match of contextMatches) {
|
|
31
|
+
remaining = remaining.replace(match, "").trim();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Extract tags (#dev, #backend, #개발)
|
|
35
|
+
const tagMatches = remaining.match(/#([\p{L}\p{N}_]+)/gu);
|
|
36
|
+
if (tagMatches) {
|
|
37
|
+
result.tags = tagMatches.map((m) => m.slice(1));
|
|
38
|
+
for (const match of tagMatches) {
|
|
39
|
+
remaining = remaining.replace(match, "").trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Extract due date patterns
|
|
43
|
+
// "by Friday", "by tomorrow", "until next week"
|
|
44
|
+
const byMatch = remaining.match(/\b(by|until|before)\s+(\w+(\s+\w+)?)/i);
|
|
45
|
+
if (byMatch) {
|
|
46
|
+
const dateStr = byMatch[2];
|
|
47
|
+
const date = parseRelativeDate(dateStr);
|
|
48
|
+
if (date) {
|
|
49
|
+
result.dueDate = date.toISOString();
|
|
50
|
+
remaining = remaining.replace(byMatch[0], "").trim();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Korean date patterns: "내일까지", "금요일까지"
|
|
54
|
+
const koreanDueMatch = remaining.match(/(\S+)까지/);
|
|
55
|
+
if (koreanDueMatch && !result.dueDate) {
|
|
56
|
+
const dateStr = koreanDueMatch[1];
|
|
57
|
+
const date = parseRelativeDate(dateStr);
|
|
58
|
+
if (date) {
|
|
59
|
+
result.dueDate = date.toISOString();
|
|
60
|
+
remaining = remaining.replace(koreanDueMatch[0], "").trim();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// "tomorrow", "today" at the end
|
|
64
|
+
const dateWords = ["tomorrow", "today", "내일", "오늘", "모레"];
|
|
65
|
+
for (const word of dateWords) {
|
|
66
|
+
if (remaining.toLowerCase().endsWith(word) && !result.dueDate) {
|
|
67
|
+
const date = parseRelativeDate(word);
|
|
68
|
+
if (date) {
|
|
69
|
+
result.dueDate = date.toISOString();
|
|
70
|
+
remaining = remaining.slice(0, -word.length).trim();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Extract time estimate (30m, 2h, 1h30m)
|
|
75
|
+
// Pattern matches: "2h30m", "2h", "30m" (but not empty string)
|
|
76
|
+
const timeMatch = remaining.match(/\b(\d+h\d+m|\d+h|\d+m)\b/);
|
|
77
|
+
if (timeMatch) {
|
|
78
|
+
let minutes = 0;
|
|
79
|
+
const hoursMatch = timeMatch[0].match(/(\d+)h/);
|
|
80
|
+
const minsMatch = timeMatch[0].match(/(\d+)m/);
|
|
81
|
+
if (hoursMatch) {
|
|
82
|
+
minutes += parseInt(hoursMatch[1], 10) * 60;
|
|
83
|
+
}
|
|
84
|
+
if (minsMatch) {
|
|
85
|
+
minutes += parseInt(minsMatch[1], 10);
|
|
86
|
+
}
|
|
87
|
+
if (minutes > 0) {
|
|
88
|
+
result.estimate = { expected: minutes, confidence: "medium" };
|
|
89
|
+
remaining = remaining.replace(timeMatch[0], "").trim();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Clean up extra spaces
|
|
93
|
+
result.title = remaining.replace(/\s+/g, " ").trim();
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
function parsePriority(input) {
|
|
97
|
+
const lower = input.toLowerCase();
|
|
98
|
+
const priorityMap = {
|
|
99
|
+
critical: "critical",
|
|
100
|
+
highest: "critical",
|
|
101
|
+
긴급: "critical",
|
|
102
|
+
high: "high",
|
|
103
|
+
높음: "high",
|
|
104
|
+
medium: "medium",
|
|
105
|
+
normal: "medium",
|
|
106
|
+
보통: "medium",
|
|
107
|
+
low: "low",
|
|
108
|
+
낮음: "low",
|
|
109
|
+
};
|
|
110
|
+
return priorityMap[lower] ?? null;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=natural-language.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"natural-language.js","sourceRoot":"","sources":["../../src/utils/natural-language.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"natural-language.test.d.ts","sourceRoot":"","sources":["../../src/utils/natural-language.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { parseTaskInput } from "./natural-language.js";
|
|
3
|
+
describe("parseTaskInput", () => {
|
|
4
|
+
describe("priority parsing", () => {
|
|
5
|
+
test("parses !critical priority", () => {
|
|
6
|
+
const result = parseTaskInput("Fix bug !critical");
|
|
7
|
+
expect(result.priority).toBe("critical");
|
|
8
|
+
expect(result.title).toBe("Fix bug");
|
|
9
|
+
});
|
|
10
|
+
test("parses !high priority", () => {
|
|
11
|
+
const result = parseTaskInput("Review PR !high");
|
|
12
|
+
expect(result.priority).toBe("high");
|
|
13
|
+
});
|
|
14
|
+
test("parses !medium priority", () => {
|
|
15
|
+
const result = parseTaskInput("Write docs !medium");
|
|
16
|
+
expect(result.priority).toBe("medium");
|
|
17
|
+
});
|
|
18
|
+
test("parses !low priority", () => {
|
|
19
|
+
const result = parseTaskInput("Clean up !low");
|
|
20
|
+
expect(result.priority).toBe("low");
|
|
21
|
+
});
|
|
22
|
+
test("parses Korean priority !높음", () => {
|
|
23
|
+
const result = parseTaskInput("버그 수정 !높음");
|
|
24
|
+
expect(result.priority).toBe("high");
|
|
25
|
+
expect(result.title).toBe("버그 수정");
|
|
26
|
+
});
|
|
27
|
+
test("parses Korean priority !긴급", () => {
|
|
28
|
+
const result = parseTaskInput("장애 대응 !긴급");
|
|
29
|
+
expect(result.priority).toBe("critical");
|
|
30
|
+
expect(result.title).toBe("장애 대응");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("tag parsing", () => {
|
|
34
|
+
test("parses single tag", () => {
|
|
35
|
+
const result = parseTaskInput("Fix auth #backend");
|
|
36
|
+
expect(result.tags).toEqual(["backend"]);
|
|
37
|
+
expect(result.title).toBe("Fix auth");
|
|
38
|
+
});
|
|
39
|
+
test("parses multiple tags", () => {
|
|
40
|
+
const result = parseTaskInput("Update API #backend #api #v2");
|
|
41
|
+
expect(result.tags).toEqual(["backend", "api", "v2"]);
|
|
42
|
+
});
|
|
43
|
+
test("parses Korean tags", () => {
|
|
44
|
+
const result = parseTaskInput("보고서 작성 #업무 #문서");
|
|
45
|
+
expect(result.tags).toEqual(["업무", "문서"]);
|
|
46
|
+
expect(result.title).toBe("보고서 작성");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("context parsing", () => {
|
|
50
|
+
test("parses single context", () => {
|
|
51
|
+
const result = parseTaskInput("Deep work task @focus");
|
|
52
|
+
expect(result.contexts).toEqual(["focus"]);
|
|
53
|
+
expect(result.title).toBe("Deep work task");
|
|
54
|
+
});
|
|
55
|
+
test("parses multiple contexts", () => {
|
|
56
|
+
const result = parseTaskInput("Review code @focus @office");
|
|
57
|
+
expect(result.contexts).toEqual(["focus", "office"]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("due date parsing", () => {
|
|
61
|
+
test("parses 'by tomorrow'", () => {
|
|
62
|
+
const result = parseTaskInput("Submit report by tomorrow");
|
|
63
|
+
expect(result.dueDate).toBeDefined();
|
|
64
|
+
expect(result.title).toBe("Submit report");
|
|
65
|
+
});
|
|
66
|
+
test("parses 'until Friday'", () => {
|
|
67
|
+
const result = parseTaskInput("Complete feature until Friday");
|
|
68
|
+
expect(result.dueDate).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
test("parses Korean '내일까지'", () => {
|
|
71
|
+
const result = parseTaskInput("보고서 제출 내일까지");
|
|
72
|
+
expect(result.dueDate).toBeDefined();
|
|
73
|
+
expect(result.title).toBe("보고서 제출");
|
|
74
|
+
});
|
|
75
|
+
test("parses standalone 'tomorrow' at end", () => {
|
|
76
|
+
const result = parseTaskInput("Submit PR tomorrow");
|
|
77
|
+
expect(result.dueDate).toBeDefined();
|
|
78
|
+
expect(result.title).toBe("Submit PR");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("time estimate parsing", () => {
|
|
82
|
+
test("parses hours only", () => {
|
|
83
|
+
const result = parseTaskInput("Write tests 2h");
|
|
84
|
+
expect(result.estimate?.expected).toBe(120);
|
|
85
|
+
});
|
|
86
|
+
test("parses minutes only", () => {
|
|
87
|
+
const result = parseTaskInput("Quick fix 30m");
|
|
88
|
+
expect(result.estimate?.expected).toBe(30);
|
|
89
|
+
});
|
|
90
|
+
test("parses combined hours and minutes", () => {
|
|
91
|
+
const result = parseTaskInput("Big refactor 1h30m");
|
|
92
|
+
expect(result.estimate?.expected).toBe(90);
|
|
93
|
+
});
|
|
94
|
+
test("removes time estimate from title", () => {
|
|
95
|
+
const result = parseTaskInput("Task 2h30m done");
|
|
96
|
+
expect(result.estimate?.expected).toBe(150);
|
|
97
|
+
expect(result.title).toBe("Task done");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("combined parsing", () => {
|
|
101
|
+
test("parses full English input", () => {
|
|
102
|
+
const result = parseTaskInput("Review PR #dev !high @focus");
|
|
103
|
+
expect(result.title).toBe("Review PR");
|
|
104
|
+
expect(result.tags).toEqual(["dev"]);
|
|
105
|
+
expect(result.priority).toBe("high");
|
|
106
|
+
expect(result.contexts).toEqual(["focus"]);
|
|
107
|
+
});
|
|
108
|
+
test("parses input with due date", () => {
|
|
109
|
+
const result = parseTaskInput("Submit report by tomorrow #work");
|
|
110
|
+
expect(result.title).toBe("Submit report");
|
|
111
|
+
expect(result.tags).toEqual(["work"]);
|
|
112
|
+
expect(result.dueDate).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
test("parses full Korean input", () => {
|
|
115
|
+
const result = parseTaskInput("보고서 작성 내일까지 #업무 !높음 @집중");
|
|
116
|
+
expect(result.title).toBe("보고서 작성");
|
|
117
|
+
expect(result.tags).toEqual(["업무"]);
|
|
118
|
+
expect(result.priority).toBe("high");
|
|
119
|
+
expect(result.contexts).toEqual(["집중"]);
|
|
120
|
+
expect(result.dueDate).toBeDefined();
|
|
121
|
+
});
|
|
122
|
+
test("returns clean title with no metadata", () => {
|
|
123
|
+
const result = parseTaskInput("Simple task");
|
|
124
|
+
expect(result.title).toBe("Simple task");
|
|
125
|
+
expect(result.tags).toBeUndefined();
|
|
126
|
+
expect(result.priority).toBeUndefined();
|
|
127
|
+
expect(result.contexts).toBeUndefined();
|
|
128
|
+
expect(result.dueDate).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=natural-language.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"natural-language.test.js","sourceRoot":"","sources":["../../src/utils/natural-language.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@task-mcp/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared utilities for task-mcp: types, algorithms, and natural language parsing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.ts",
|
|
11
|
+
"types": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"dev": "tsc --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "bun test"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"task-mcp",
|
|
27
|
+
"shared",
|
|
28
|
+
"critical-path",
|
|
29
|
+
"topological-sort",
|
|
30
|
+
"natural-language-parsing"
|
|
31
|
+
],
|
|
32
|
+
"author": "addsalt1t",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/addsalt1t/task-mcp.git",
|
|
37
|
+
"directory": "packages/shared"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/addsalt1t/task-mcp#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/addsalt1t/task-mcp/issues"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"arktype": "2.0.0-rc.31"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
criticalPathAnalysis,
|
|
4
|
+
findParallelTasks,
|
|
5
|
+
suggestNextTask,
|
|
6
|
+
} from "./critical-path.js";
|
|
7
|
+
import type { Task } from "../schemas/task.js";
|
|
8
|
+
|
|
9
|
+
// Helper to create mock tasks
|
|
10
|
+
function createTask(
|
|
11
|
+
id: string,
|
|
12
|
+
options: {
|
|
13
|
+
priority?: string;
|
|
14
|
+
deps?: string[];
|
|
15
|
+
estimate?: number;
|
|
16
|
+
status?: string;
|
|
17
|
+
contexts?: string[];
|
|
18
|
+
} = {}
|
|
19
|
+
): Task {
|
|
20
|
+
const task: Task = {
|
|
21
|
+
id,
|
|
22
|
+
title: `Task ${id}`,
|
|
23
|
+
status: (options.status ?? "pending") as Task["status"],
|
|
24
|
+
priority: (options.priority ?? "medium") as Task["priority"],
|
|
25
|
+
projectId: "test-project",
|
|
26
|
+
createdAt: new Date().toISOString(),
|
|
27
|
+
updatedAt: new Date().toISOString(),
|
|
28
|
+
dependencies: (options.deps ?? []).map((depId) => ({
|
|
29
|
+
taskId: depId,
|
|
30
|
+
type: "blocked_by" as const,
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
33
|
+
if (options.estimate) {
|
|
34
|
+
task.estimate = { expected: options.estimate, confidence: "medium" as const };
|
|
35
|
+
}
|
|
36
|
+
if (options.contexts) {
|
|
37
|
+
task.contexts = options.contexts;
|
|
38
|
+
}
|
|
39
|
+
return task;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("criticalPathAnalysis", () => {
|
|
43
|
+
test("returns empty result for empty input", () => {
|
|
44
|
+
const result = criticalPathAnalysis([]);
|
|
45
|
+
expect(result.tasks).toEqual([]);
|
|
46
|
+
expect(result.criticalPath).toEqual([]);
|
|
47
|
+
expect(result.projectDuration).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns empty for all completed tasks", () => {
|
|
51
|
+
const tasks = [createTask("A", { status: "completed" })];
|
|
52
|
+
const result = criticalPathAnalysis(tasks);
|
|
53
|
+
expect(result.tasks).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("calculates single task correctly", () => {
|
|
57
|
+
const tasks = [createTask("A", { estimate: 60 })];
|
|
58
|
+
const result = criticalPathAnalysis(tasks);
|
|
59
|
+
|
|
60
|
+
expect(result.projectDuration).toBe(60);
|
|
61
|
+
expect(result.criticalPath.length).toBe(1);
|
|
62
|
+
expect(result.criticalPath[0]!.id).toBe("A");
|
|
63
|
+
expect(result.criticalPath[0]!.isCritical).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("identifies critical path in linear chain", () => {
|
|
67
|
+
// A -> B -> C (each 30 min)
|
|
68
|
+
const tasks = [
|
|
69
|
+
createTask("A", { estimate: 30 }),
|
|
70
|
+
createTask("B", { estimate: 30, deps: ["A"] }),
|
|
71
|
+
createTask("C", { estimate: 30, deps: ["B"] }),
|
|
72
|
+
];
|
|
73
|
+
const result = criticalPathAnalysis(tasks);
|
|
74
|
+
|
|
75
|
+
expect(result.projectDuration).toBe(90);
|
|
76
|
+
expect(result.criticalPath.length).toBe(3);
|
|
77
|
+
expect(result.criticalPath.map((t) => t.id)).toEqual(["A", "B", "C"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("calculates slack for parallel tasks", () => {
|
|
81
|
+
// A (60) and B (30) both lead to C
|
|
82
|
+
// A is on critical path, B has 30 min slack
|
|
83
|
+
const tasks = [
|
|
84
|
+
createTask("A", { estimate: 60 }),
|
|
85
|
+
createTask("B", { estimate: 30 }),
|
|
86
|
+
createTask("C", { estimate: 30, deps: ["A", "B"] }),
|
|
87
|
+
];
|
|
88
|
+
const result = criticalPathAnalysis(tasks);
|
|
89
|
+
|
|
90
|
+
expect(result.projectDuration).toBe(90); // A + C
|
|
91
|
+
|
|
92
|
+
const taskA = result.tasks.find((t) => t.id === "A")!;
|
|
93
|
+
const taskB = result.tasks.find((t) => t.id === "B")!;
|
|
94
|
+
const taskC = result.tasks.find((t) => t.id === "C")!;
|
|
95
|
+
|
|
96
|
+
expect(taskA.isCritical).toBe(true);
|
|
97
|
+
expect(taskB.slack).toBe(30); // B can start 30 min late
|
|
98
|
+
expect(taskC.isCritical).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("identifies bottlenecks by dependent count", () => {
|
|
102
|
+
// A blocks B, C, D
|
|
103
|
+
const tasks = [
|
|
104
|
+
createTask("A", { estimate: 30 }),
|
|
105
|
+
createTask("B", { estimate: 30, deps: ["A"] }),
|
|
106
|
+
createTask("C", { estimate: 30, deps: ["A"] }),
|
|
107
|
+
createTask("D", { estimate: 30, deps: ["A"] }),
|
|
108
|
+
];
|
|
109
|
+
const result = criticalPathAnalysis(tasks);
|
|
110
|
+
|
|
111
|
+
expect(result.bottlenecks.length).toBeGreaterThan(0);
|
|
112
|
+
expect(result.bottlenecks[0]!.id).toBe("A"); // A blocks the most tasks
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("findParallelTasks", () => {
|
|
117
|
+
test("returns empty for empty input", () => {
|
|
118
|
+
const result = findParallelTasks([]);
|
|
119
|
+
expect(result).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("returns single group for independent tasks", () => {
|
|
123
|
+
const tasks = [
|
|
124
|
+
createTask("A"),
|
|
125
|
+
createTask("B"),
|
|
126
|
+
createTask("C"),
|
|
127
|
+
];
|
|
128
|
+
const result = findParallelTasks(tasks);
|
|
129
|
+
|
|
130
|
+
expect(result.length).toBe(1);
|
|
131
|
+
expect(result[0]!.length).toBe(3);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("excludes tasks with uncompleted dependencies", () => {
|
|
135
|
+
const tasks = [
|
|
136
|
+
createTask("A"),
|
|
137
|
+
createTask("B", { deps: ["A"] }),
|
|
138
|
+
];
|
|
139
|
+
const result = findParallelTasks(tasks);
|
|
140
|
+
|
|
141
|
+
// Only A is available (B is blocked)
|
|
142
|
+
expect(result.length).toBe(1);
|
|
143
|
+
expect(result[0]!.length).toBe(1);
|
|
144
|
+
expect(result[0]![0]!.id).toBe("A");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("includes task when dependency is completed", () => {
|
|
148
|
+
const tasks = [
|
|
149
|
+
createTask("A", { status: "completed" }),
|
|
150
|
+
createTask("B", { deps: ["A"] }),
|
|
151
|
+
createTask("C"),
|
|
152
|
+
];
|
|
153
|
+
const result = findParallelTasks(tasks);
|
|
154
|
+
|
|
155
|
+
// B and C can run in parallel
|
|
156
|
+
expect(result.length).toBe(1);
|
|
157
|
+
expect(result[0]!.map((t) => t.id).sort()).toEqual(["B", "C"]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("excludes completed tasks from result", () => {
|
|
161
|
+
const tasks = [
|
|
162
|
+
createTask("A", { status: "completed" }),
|
|
163
|
+
createTask("B"),
|
|
164
|
+
];
|
|
165
|
+
const result = findParallelTasks(tasks);
|
|
166
|
+
|
|
167
|
+
expect(result.length).toBe(1);
|
|
168
|
+
expect(result[0]!.length).toBe(1);
|
|
169
|
+
expect(result[0]![0]!.id).toBe("B");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("suggestNextTask", () => {
|
|
174
|
+
test("returns null for empty input", () => {
|
|
175
|
+
const result = suggestNextTask([]);
|
|
176
|
+
expect(result).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("returns null when all tasks completed", () => {
|
|
180
|
+
const tasks = [createTask("A", { status: "completed" })];
|
|
181
|
+
const result = suggestNextTask(tasks);
|
|
182
|
+
expect(result).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("prefers critical path tasks", () => {
|
|
186
|
+
// A is critical (longer), B has slack
|
|
187
|
+
const tasks = [
|
|
188
|
+
createTask("A", { estimate: 60, priority: "low" }),
|
|
189
|
+
createTask("B", { estimate: 30, priority: "high" }),
|
|
190
|
+
createTask("C", { estimate: 30, deps: ["A", "B"] }),
|
|
191
|
+
];
|
|
192
|
+
const result = suggestNextTask(tasks);
|
|
193
|
+
|
|
194
|
+
// A is on critical path, should be suggested first
|
|
195
|
+
expect(result!.id).toBe("A");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("filters by context when specified", () => {
|
|
199
|
+
const tasks = [
|
|
200
|
+
createTask("A", { priority: "critical", contexts: ["office"] }),
|
|
201
|
+
createTask("B", { priority: "high", contexts: ["focus"] }),
|
|
202
|
+
];
|
|
203
|
+
const result = suggestNextTask(tasks, { contexts: ["focus"] });
|
|
204
|
+
|
|
205
|
+
expect(result!.id).toBe("B");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("filters by max time when specified", () => {
|
|
209
|
+
const tasks = [
|
|
210
|
+
createTask("A", { estimate: 120, priority: "critical" }),
|
|
211
|
+
createTask("B", { estimate: 30, priority: "high" }),
|
|
212
|
+
];
|
|
213
|
+
const result = suggestNextTask(tasks, { maxMinutes: 60 });
|
|
214
|
+
|
|
215
|
+
expect(result!.id).toBe("B");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("skips blocked tasks", () => {
|
|
219
|
+
const tasks = [
|
|
220
|
+
createTask("A", { priority: "low" }),
|
|
221
|
+
createTask("B", { priority: "critical", deps: ["A"] }),
|
|
222
|
+
];
|
|
223
|
+
const result = suggestNextTask(tasks);
|
|
224
|
+
|
|
225
|
+
// B is blocked, so A should be suggested
|
|
226
|
+
expect(result!.id).toBe("A");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("considers tasks with more dependents", () => {
|
|
230
|
+
const tasks = [
|
|
231
|
+
createTask("A", { priority: "medium" }),
|
|
232
|
+
createTask("B", { priority: "medium" }),
|
|
233
|
+
createTask("C", { priority: "medium", deps: ["A"] }),
|
|
234
|
+
createTask("D", { priority: "medium", deps: ["A"] }),
|
|
235
|
+
];
|
|
236
|
+
const result = suggestNextTask(tasks);
|
|
237
|
+
|
|
238
|
+
// A blocks more tasks (C and D), should be preferred over B
|
|
239
|
+
expect(result!.id).toBe("A");
|
|
240
|
+
});
|
|
241
|
+
});
|