@projitive/mcp 1.0.7 → 1.1.1
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/README.md +3 -3
- package/output/package.json +4 -1
- package/output/source/{helpers/catch → common}/catch.js +6 -6
- package/output/source/{helpers/catch → common}/catch.test.js +6 -6
- package/output/source/common/confidence.js +231 -0
- package/output/source/common/confidence.test.js +205 -0
- package/output/source/common/errors.js +120 -0
- package/output/source/{helpers/files → common}/files.js +1 -1
- package/output/source/{helpers/files → common}/files.test.js +1 -1
- package/output/source/common/index.js +10 -0
- package/output/source/{helpers/linter/codes.js → common/linter.js} +13 -0
- package/output/source/{helpers/markdown → common}/markdown.test.js +1 -1
- package/output/source/{helpers/response → common}/response.test.js +1 -1
- package/output/source/common/types.js +7 -0
- package/output/source/common/utils.js +39 -0
- package/output/source/design-context.js +51 -500
- package/output/source/index.js +8 -193
- package/output/source/index.test.js +116 -0
- package/output/source/prompts/index.js +9 -0
- package/output/source/prompts/quickStart.js +94 -0
- package/output/source/prompts/taskDiscovery.js +190 -0
- package/output/source/prompts/taskExecution.js +161 -0
- package/output/source/resources/designs.js +108 -0
- package/output/source/resources/designs.test.js +154 -0
- package/output/source/resources/governance.js +40 -0
- package/output/source/resources/index.js +6 -0
- package/output/source/resources/readme.test.js +167 -0
- package/output/source/{reports.js → resources/reports.js} +5 -3
- package/output/source/resources/reports.test.js +149 -0
- package/output/source/tools/index.js +8 -0
- package/output/source/{projitive.js → tools/project.js} +6 -9
- package/output/source/tools/project.test.js +322 -0
- package/output/source/{roadmap.js → tools/roadmap.js} +4 -7
- package/output/source/tools/roadmap.test.js +103 -0
- package/output/source/{tasks.js → tools/task.js} +581 -27
- package/output/source/tools/task.test.js +473 -0
- package/output/source/types.js +67 -0
- package/package.json +4 -1
- package/output/source/designs.js +0 -38
- package/output/source/helpers/artifacts/index.js +0 -1
- package/output/source/helpers/catch/index.js +0 -1
- package/output/source/helpers/files/index.js +0 -1
- package/output/source/helpers/index.js +0 -6
- package/output/source/helpers/linter/index.js +0 -2
- package/output/source/helpers/linter/linter.js +0 -6
- package/output/source/helpers/markdown/index.js +0 -1
- package/output/source/helpers/response/index.js +0 -1
- package/output/source/projitive.test.js +0 -111
- package/output/source/roadmap.test.js +0 -11
- package/output/source/tasks.test.js +0 -152
- /package/output/source/{helpers/artifacts → common}/artifacts.js +0 -0
- /package/output/source/{helpers/artifacts → common}/artifacts.test.js +0 -0
- /package/output/source/{helpers/linter → common}/linter.test.js +0 -0
- /package/output/source/{helpers/markdown → common}/markdown.js +0 -0
- /package/output/source/{helpers/response → common}/response.js +0 -0
- /package/output/source/{readme.js → resources/readme.js} +0 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { TASKS_END, TASKS_START, collectTaskLintSuggestions, isValidTaskId, normalizeTask, parseTasksBlock, rankActionableTaskCandidates, resolveNoTaskDiscoveryGuidance, renderTaskSeedTemplate, renderTasksMarkdown, taskPriority, toTaskUpdatedAtMs, validateTransition, findTaskIdsOutsideMarkers, } from "./task.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
function buildCandidate(partial) {
|
|
7
|
+
const task = normalizeTask({
|
|
8
|
+
id: partial.id,
|
|
9
|
+
title: partial.title,
|
|
10
|
+
status: partial.status,
|
|
11
|
+
updatedAt: partial.task?.updatedAt ?? "2026-01-01T00:00:00.000Z",
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
governanceDir: partial.governanceDir ?? "/workspace/a",
|
|
15
|
+
tasksPath: partial.tasksPath ?? "/workspace/a/tasks.md",
|
|
16
|
+
task,
|
|
17
|
+
projectScore: partial.projectScore ?? 1,
|
|
18
|
+
projectLatestUpdatedAt: partial.projectLatestUpdatedAt ?? "2026-01-01T00:00:00.000Z",
|
|
19
|
+
taskUpdatedAtMs: partial.taskUpdatedAtMs ?? toTaskUpdatedAtMs(task.updatedAt),
|
|
20
|
+
taskPriority: partial.taskPriority ?? taskPriority(task.status),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
describe("tasks module", () => {
|
|
24
|
+
it("parses markdown task block and normalizes task fields", async () => {
|
|
25
|
+
const markdown = [
|
|
26
|
+
"# Tasks",
|
|
27
|
+
TASKS_START,
|
|
28
|
+
"## TASK-0001 | TODO | hello",
|
|
29
|
+
"- owner: alice",
|
|
30
|
+
"- summary: first task",
|
|
31
|
+
"- updatedAt: 2026-02-17T00:00:00.000Z",
|
|
32
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
33
|
+
"- links:",
|
|
34
|
+
" - ./designs/example.md",
|
|
35
|
+
TASKS_END,
|
|
36
|
+
].join("\n");
|
|
37
|
+
const tasks = await parseTasksBlock(markdown);
|
|
38
|
+
expect(tasks).toHaveLength(1);
|
|
39
|
+
expect(tasks[0].id).toBe("TASK-0001");
|
|
40
|
+
expect(tasks[0].status).toBe("TODO");
|
|
41
|
+
expect(tasks[0].roadmapRefs).toEqual(["ROADMAP-0001"]);
|
|
42
|
+
expect(tasks[0].links).toEqual(["./designs/example.md"]);
|
|
43
|
+
});
|
|
44
|
+
it("renders markdown containing markers", () => {
|
|
45
|
+
const task = normalizeTask({ id: "TASK-0002", title: "render", status: "IN_PROGRESS" });
|
|
46
|
+
const markdown = renderTasksMarkdown([task]);
|
|
47
|
+
expect(markdown.includes(TASKS_START)).toBe(true);
|
|
48
|
+
expect(markdown.includes(TASKS_END)).toBe(true);
|
|
49
|
+
expect(markdown.includes("## TASK-0002 | IN_PROGRESS | render")).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it("parses task with subState metadata (Spec v1.1.0)", async () => {
|
|
52
|
+
const markdown = [
|
|
53
|
+
"# Tasks",
|
|
54
|
+
TASKS_START,
|
|
55
|
+
"## TASK-0003 | IN_PROGRESS | feature with substate",
|
|
56
|
+
"- owner: bob",
|
|
57
|
+
"- summary: implementing feature",
|
|
58
|
+
"- updatedAt: 2026-02-20T00:00:00.000Z",
|
|
59
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
60
|
+
"- links:",
|
|
61
|
+
" - ./designs/feature.md",
|
|
62
|
+
"- subState:",
|
|
63
|
+
" - phase: implementation",
|
|
64
|
+
" - confidence: 0.85",
|
|
65
|
+
" - estimatedCompletion: 2026-02-25T15:00:00Z",
|
|
66
|
+
TASKS_END,
|
|
67
|
+
].join("\n");
|
|
68
|
+
const tasks = await parseTasksBlock(markdown);
|
|
69
|
+
expect(tasks).toHaveLength(1);
|
|
70
|
+
expect(tasks[0].id).toBe("TASK-0003");
|
|
71
|
+
expect(tasks[0].subState).toBeDefined();
|
|
72
|
+
expect(tasks[0].subState?.phase).toBe("implementation");
|
|
73
|
+
expect(tasks[0].subState?.confidence).toBe(0.85);
|
|
74
|
+
expect(tasks[0].subState?.estimatedCompletion).toBe("2026-02-25T15:00:00Z");
|
|
75
|
+
});
|
|
76
|
+
it("parses task with blocker metadata (Spec v1.1.0)", async () => {
|
|
77
|
+
const markdown = [
|
|
78
|
+
"# Tasks",
|
|
79
|
+
TASKS_START,
|
|
80
|
+
"## TASK-0004 | BLOCKED | waiting for api",
|
|
81
|
+
"- owner: charlie",
|
|
82
|
+
"- summary: Waiting for payment API v2.0",
|
|
83
|
+
"- updatedAt: 2026-02-20T00:00:00.000Z",
|
|
84
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
85
|
+
"- links:",
|
|
86
|
+
" - ./docs/api-waiting.md",
|
|
87
|
+
"- blocker:",
|
|
88
|
+
" - type: external_dependency",
|
|
89
|
+
" - description: Waiting for payment API v2.0",
|
|
90
|
+
" - blockingEntity: third-party/payment-provider",
|
|
91
|
+
" - unblockCondition: API v2.0 GA announced",
|
|
92
|
+
" - escalationPath: contact-pm-for-workaround",
|
|
93
|
+
TASKS_END,
|
|
94
|
+
].join("\n");
|
|
95
|
+
const tasks = await parseTasksBlock(markdown);
|
|
96
|
+
expect(tasks).toHaveLength(1);
|
|
97
|
+
expect(tasks[0].id).toBe("TASK-0004");
|
|
98
|
+
expect(tasks[0].blocker).toBeDefined();
|
|
99
|
+
expect(tasks[0].blocker?.type).toBe("external_dependency");
|
|
100
|
+
expect(tasks[0].blocker?.description).toBe("Waiting for payment API v2.0");
|
|
101
|
+
expect(tasks[0].blocker?.blockingEntity).toBe("third-party/payment-provider");
|
|
102
|
+
expect(tasks[0].blocker?.unblockCondition).toBe("API v2.0 GA announced");
|
|
103
|
+
expect(tasks[0].blocker?.escalationPath).toBe("contact-pm-for-workaround");
|
|
104
|
+
});
|
|
105
|
+
it("validates task IDs", () => {
|
|
106
|
+
expect(isValidTaskId("TASK-0001")).toBe(true);
|
|
107
|
+
expect(isValidTaskId("TASK-001")).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
it("allows and rejects expected transitions", () => {
|
|
110
|
+
expect(validateTransition("TODO", "IN_PROGRESS")).toBe(true);
|
|
111
|
+
expect(validateTransition("IN_PROGRESS", "DONE")).toBe(true);
|
|
112
|
+
expect(validateTransition("DONE", "IN_PROGRESS")).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
it("assigns priority for actionable statuses", () => {
|
|
115
|
+
expect(taskPriority("IN_PROGRESS")).toBe(2);
|
|
116
|
+
expect(taskPriority("TODO")).toBe(1);
|
|
117
|
+
expect(taskPriority("BLOCKED")).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
it("returns zero timestamp for invalid date", () => {
|
|
120
|
+
expect(toTaskUpdatedAtMs("invalid")).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
it("ranks by project score, then task priority, then recency", () => {
|
|
123
|
+
const candidates = [
|
|
124
|
+
buildCandidate({ id: "TASK-0001", title: "A", status: "TODO", projectScore: 2 }),
|
|
125
|
+
buildCandidate({ id: "TASK-0002", title: "B", status: "IN_PROGRESS", projectScore: 2 }),
|
|
126
|
+
buildCandidate({ id: "TASK-0003", title: "C", status: "IN_PROGRESS", projectScore: 3 }),
|
|
127
|
+
];
|
|
128
|
+
const ranked = rankActionableTaskCandidates(candidates);
|
|
129
|
+
expect(ranked[0].task.id).toBe("TASK-0003");
|
|
130
|
+
expect(ranked[1].task.id).toBe("TASK-0002");
|
|
131
|
+
expect(ranked[2].task.id).toBe("TASK-0001");
|
|
132
|
+
});
|
|
133
|
+
it("renders lint lines with stable code prefix", () => {
|
|
134
|
+
const task = normalizeTask({
|
|
135
|
+
id: "TASK-0001",
|
|
136
|
+
title: "lint",
|
|
137
|
+
status: "IN_PROGRESS",
|
|
138
|
+
owner: "",
|
|
139
|
+
roadmapRefs: [],
|
|
140
|
+
});
|
|
141
|
+
const lint = collectTaskLintSuggestions([task]);
|
|
142
|
+
expect(lint.some((line) => line.startsWith("- [TASK_IN_PROGRESS_OWNER_EMPTY]"))).toBe(true);
|
|
143
|
+
expect(lint.some((line) => line.startsWith("- [TASK_ROADMAP_REFS_EMPTY]"))).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
it("scopes outside-marker lint to provided task IDs", () => {
|
|
146
|
+
const tasks = [
|
|
147
|
+
normalizeTask({ id: "TASK-0001", title: "A", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
|
|
148
|
+
normalizeTask({ id: "TASK-0002", title: "B", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
|
|
149
|
+
];
|
|
150
|
+
const markdown = [
|
|
151
|
+
"# Tasks",
|
|
152
|
+
"TASK-0002 outside",
|
|
153
|
+
"TASK-0003 outside",
|
|
154
|
+
TASKS_START,
|
|
155
|
+
"## TASK-0001 | TODO | A",
|
|
156
|
+
"- owner: (none)",
|
|
157
|
+
"- summary: (none)",
|
|
158
|
+
"- updatedAt: 2026-02-18T00:00:00.000Z",
|
|
159
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
160
|
+
"- links:",
|
|
161
|
+
" - (none)",
|
|
162
|
+
"## TASK-0002 | TODO | B",
|
|
163
|
+
"- owner: (none)",
|
|
164
|
+
"- summary: (none)",
|
|
165
|
+
"- updatedAt: 2026-02-18T00:00:00.000Z",
|
|
166
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
167
|
+
"- links:",
|
|
168
|
+
" - (none)",
|
|
169
|
+
TASKS_END,
|
|
170
|
+
].join("\n");
|
|
171
|
+
const scoped = collectTaskLintSuggestions(tasks, markdown, new Set(["TASK-0001"]));
|
|
172
|
+
const scopedOutside = scoped.find((line) => line.includes("TASK IDs found outside marker block"));
|
|
173
|
+
expect(scopedOutside).toBeUndefined();
|
|
174
|
+
const all = collectTaskLintSuggestions(tasks, markdown);
|
|
175
|
+
const allOutside = all.find((line) => line.includes("TASK IDs found outside marker block"));
|
|
176
|
+
expect(allOutside).toContain("TASK-0002");
|
|
177
|
+
expect(allOutside).toContain("TASK-0003");
|
|
178
|
+
});
|
|
179
|
+
it("renders seed task template with provided roadmap ref", () => {
|
|
180
|
+
const lines = renderTaskSeedTemplate("ROADMAP-0099");
|
|
181
|
+
const markdown = lines.join("\n");
|
|
182
|
+
expect(markdown).toContain("## TASK-0001 | TODO | Define initial executable objective");
|
|
183
|
+
expect(markdown).toContain("- roadmapRefs: ROADMAP-0099");
|
|
184
|
+
expect(markdown).toContain("- links:");
|
|
185
|
+
expect(markdown).not.toContain("- hooks:");
|
|
186
|
+
});
|
|
187
|
+
it("uses default no-task guidance when hook file is absent", async () => {
|
|
188
|
+
const guidance = await resolveNoTaskDiscoveryGuidance("/path/that/does/not/exist");
|
|
189
|
+
expect(guidance.length).toBeGreaterThan(3);
|
|
190
|
+
expect(guidance.some((line) => line.includes("TODO/FIXME/HACK"))).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
it("uses hook checklist when task_no_actionable hook exists", async () => {
|
|
193
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
|
|
194
|
+
const hooksDir = path.join(dir, "hooks");
|
|
195
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
196
|
+
await fs.writeFile(path.join(hooksDir, "task_no_actionable.md"), [
|
|
197
|
+
"Objective:",
|
|
198
|
+
"- custom-item-1",
|
|
199
|
+
"- custom-item-2",
|
|
200
|
+
].join("\n"), "utf-8");
|
|
201
|
+
const guidance = await resolveNoTaskDiscoveryGuidance(dir);
|
|
202
|
+
expect(guidance).toContain("- custom-item-1");
|
|
203
|
+
expect(guidance).toContain("- custom-item-2");
|
|
204
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
205
|
+
});
|
|
206
|
+
// Additional tests for improved coverage
|
|
207
|
+
it("finds task IDs outside marker blocks", () => {
|
|
208
|
+
const markdown = [
|
|
209
|
+
"# Tasks",
|
|
210
|
+
"Reference to TASK-0001 outside",
|
|
211
|
+
"Another reference to TASK-0002",
|
|
212
|
+
TASKS_START,
|
|
213
|
+
"## TASK-0001 | TODO | A",
|
|
214
|
+
"- owner: (none)",
|
|
215
|
+
"- summary: (none)",
|
|
216
|
+
"- updatedAt: 2026-02-18T00:00:00.000Z",
|
|
217
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
218
|
+
"- links:",
|
|
219
|
+
" - (none)",
|
|
220
|
+
TASKS_END,
|
|
221
|
+
"Postscript with TASK-0003",
|
|
222
|
+
].join("\n");
|
|
223
|
+
const ids = findTaskIdsOutsideMarkers(markdown);
|
|
224
|
+
expect(ids).toContain("TASK-0001");
|
|
225
|
+
expect(ids).toContain("TASK-0002");
|
|
226
|
+
expect(ids).toContain("TASK-0003");
|
|
227
|
+
expect(ids).toHaveLength(3);
|
|
228
|
+
});
|
|
229
|
+
it("normalizes task with optional fields", () => {
|
|
230
|
+
const task = normalizeTask({
|
|
231
|
+
id: "TASK-0001",
|
|
232
|
+
title: "Test Task",
|
|
233
|
+
status: "TODO",
|
|
234
|
+
subState: {
|
|
235
|
+
phase: "discovery",
|
|
236
|
+
confidence: 0.75,
|
|
237
|
+
estimatedCompletion: "2026-03-01T00:00:00Z"
|
|
238
|
+
},
|
|
239
|
+
blocker: {
|
|
240
|
+
type: "internal_dependency",
|
|
241
|
+
description: "Waiting for team review"
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
expect(task.id).toBe("TASK-0001");
|
|
245
|
+
expect(task.title).toBe("Test Task");
|
|
246
|
+
expect(task.status).toBe("TODO");
|
|
247
|
+
expect(task.subState).toBeDefined();
|
|
248
|
+
expect(task.subState?.phase).toBe("discovery");
|
|
249
|
+
expect(task.subState?.confidence).toBe(0.75);
|
|
250
|
+
expect(task.blocker).toBeDefined();
|
|
251
|
+
expect(task.blocker?.type).toBe("internal_dependency");
|
|
252
|
+
});
|
|
253
|
+
it("renders tasks with subState and blocker metadata", () => {
|
|
254
|
+
const task1 = normalizeTask({
|
|
255
|
+
id: "TASK-0001",
|
|
256
|
+
title: "In Progress Task",
|
|
257
|
+
status: "IN_PROGRESS",
|
|
258
|
+
subState: {
|
|
259
|
+
phase: "implementation",
|
|
260
|
+
confidence: 0.85
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const task2 = normalizeTask({
|
|
264
|
+
id: "TASK-0002",
|
|
265
|
+
title: "Blocked Task",
|
|
266
|
+
status: "BLOCKED",
|
|
267
|
+
blocker: {
|
|
268
|
+
type: "external_dependency",
|
|
269
|
+
description: "Waiting for API"
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
const markdown = renderTasksMarkdown([task1, task2]);
|
|
273
|
+
expect(markdown).toContain("phase: implementation");
|
|
274
|
+
expect(markdown).toContain("confidence: 0.85");
|
|
275
|
+
expect(markdown).toContain("type: external_dependency");
|
|
276
|
+
expect(markdown).toContain("description: Waiting for API");
|
|
277
|
+
});
|
|
278
|
+
it("parses empty task block correctly", async () => {
|
|
279
|
+
const markdown = [
|
|
280
|
+
"# Tasks",
|
|
281
|
+
TASKS_START,
|
|
282
|
+
"(no tasks)",
|
|
283
|
+
TASKS_END,
|
|
284
|
+
].join("\n");
|
|
285
|
+
const tasks = await parseTasksBlock(markdown);
|
|
286
|
+
expect(tasks).toHaveLength(0);
|
|
287
|
+
});
|
|
288
|
+
it("parses task block without markers returns empty", async () => {
|
|
289
|
+
const markdown = [
|
|
290
|
+
"# Tasks",
|
|
291
|
+
"## TASK-0001 | TODO | No Markers",
|
|
292
|
+
"- owner: alice",
|
|
293
|
+
].join("\n");
|
|
294
|
+
const tasks = await parseTasksBlock(markdown);
|
|
295
|
+
expect(tasks).toHaveLength(0);
|
|
296
|
+
});
|
|
297
|
+
it("collects lint suggestions for duplicate IDs", () => {
|
|
298
|
+
const tasks = [
|
|
299
|
+
normalizeTask({ id: "TASK-0001", title: "A", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
|
|
300
|
+
normalizeTask({ id: "TASK-0001", title: "B", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
|
|
301
|
+
];
|
|
302
|
+
const lint = collectTaskLintSuggestions(tasks);
|
|
303
|
+
expect(lint.some((line) => line.includes("Duplicate task IDs"))).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
it("collects lint suggestions for DONE without links", () => {
|
|
306
|
+
const tasks = [
|
|
307
|
+
normalizeTask({ id: "TASK-0001", title: "Done Task", status: "DONE", roadmapRefs: ["ROADMAP-0001"], links: [] }),
|
|
308
|
+
];
|
|
309
|
+
const lint = collectTaskLintSuggestions(tasks);
|
|
310
|
+
expect(lint.some((line) => line.includes("DONE task(s) have no links evidence"))).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
it("collects lint suggestions for BLOCKED without summary", () => {
|
|
313
|
+
const tasks = [
|
|
314
|
+
normalizeTask({ id: "TASK-0001", title: "Blocked", status: "BLOCKED", summary: "", roadmapRefs: ["ROADMAP-0001"] }),
|
|
315
|
+
];
|
|
316
|
+
const lint = collectTaskLintSuggestions(tasks);
|
|
317
|
+
expect(lint.some((line) => line.includes("BLOCKED task(s) have empty summary"))).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
it("collects lint suggestions for invalid updatedAt", () => {
|
|
320
|
+
const tasks = [
|
|
321
|
+
normalizeTask({ id: "TASK-0001", title: "Invalid Date", status: "TODO", updatedAt: "not-a-date", roadmapRefs: ["ROADMAP-0001"] }),
|
|
322
|
+
];
|
|
323
|
+
const lint = collectTaskLintSuggestions(tasks);
|
|
324
|
+
expect(lint.some((line) => line.includes("invalid updatedAt format"))).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
it("collects lint suggestions for Spec v1.1.0 blocker validation", () => {
|
|
327
|
+
const tasks = [
|
|
328
|
+
normalizeTask({
|
|
329
|
+
id: "TASK-0001",
|
|
330
|
+
title: "Blocked Without Metadata",
|
|
331
|
+
status: "BLOCKED",
|
|
332
|
+
summary: "Blocked but no metadata",
|
|
333
|
+
roadmapRefs: ["ROADMAP-0001"]
|
|
334
|
+
}),
|
|
335
|
+
];
|
|
336
|
+
const lint = collectTaskLintSuggestions(tasks);
|
|
337
|
+
expect(lint.some((line) => line.includes("BLOCKED task(s) have no blocker metadata"))).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
it("collects lint suggestions for Spec v1.1.0 subState validation", () => {
|
|
340
|
+
const tasks = [
|
|
341
|
+
normalizeTask({
|
|
342
|
+
id: "TASK-0001",
|
|
343
|
+
title: "In Progress Without SubState",
|
|
344
|
+
status: "IN_PROGRESS",
|
|
345
|
+
owner: "ai-copilot",
|
|
346
|
+
roadmapRefs: ["ROADMAP-0001"]
|
|
347
|
+
}),
|
|
348
|
+
];
|
|
349
|
+
const lint = collectTaskLintSuggestions(tasks);
|
|
350
|
+
expect(lint.some((line) => line.includes("IN_PROGRESS task(s) have no subState metadata"))).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
it("validates all status transitions correctly", () => {
|
|
353
|
+
// TODO transitions
|
|
354
|
+
expect(validateTransition("TODO", "TODO")).toBe(true);
|
|
355
|
+
expect(validateTransition("TODO", "IN_PROGRESS")).toBe(true);
|
|
356
|
+
expect(validateTransition("TODO", "BLOCKED")).toBe(true);
|
|
357
|
+
expect(validateTransition("TODO", "DONE")).toBe(false);
|
|
358
|
+
// IN_PROGRESS transitions
|
|
359
|
+
expect(validateTransition("IN_PROGRESS", "IN_PROGRESS")).toBe(true);
|
|
360
|
+
expect(validateTransition("IN_PROGRESS", "BLOCKED")).toBe(true);
|
|
361
|
+
expect(validateTransition("IN_PROGRESS", "DONE")).toBe(true);
|
|
362
|
+
expect(validateTransition("IN_PROGRESS", "TODO")).toBe(false);
|
|
363
|
+
// BLOCKED transitions
|
|
364
|
+
expect(validateTransition("BLOCKED", "BLOCKED")).toBe(true);
|
|
365
|
+
expect(validateTransition("BLOCKED", "IN_PROGRESS")).toBe(true);
|
|
366
|
+
expect(validateTransition("BLOCKED", "TODO")).toBe(true);
|
|
367
|
+
expect(validateTransition("BLOCKED", "DONE")).toBe(false);
|
|
368
|
+
// DONE transitions
|
|
369
|
+
expect(validateTransition("DONE", "DONE")).toBe(true);
|
|
370
|
+
expect(validateTransition("DONE", "TODO")).toBe(false);
|
|
371
|
+
expect(validateTransition("DONE", "IN_PROGRESS")).toBe(false);
|
|
372
|
+
expect(validateTransition("DONE", "BLOCKED")).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
it("ranks candidates with same project score by task priority", () => {
|
|
375
|
+
const candidates = [
|
|
376
|
+
buildCandidate({ id: "TASK-0001", title: "TODO Task", status: "TODO", projectScore: 2 }),
|
|
377
|
+
buildCandidate({ id: "TASK-0002", title: "IN_PROGRESS Task", status: "IN_PROGRESS", projectScore: 2 }),
|
|
378
|
+
];
|
|
379
|
+
const ranked = rankActionableTaskCandidates(candidates);
|
|
380
|
+
expect(ranked[0].task.id).toBe("TASK-0002");
|
|
381
|
+
expect(ranked[1].task.id).toBe("TASK-0001");
|
|
382
|
+
});
|
|
383
|
+
it("ranks candidates with same project score and priority by recency", () => {
|
|
384
|
+
const older = buildCandidate({
|
|
385
|
+
id: "TASK-0001",
|
|
386
|
+
title: "Older Task",
|
|
387
|
+
status: "TODO",
|
|
388
|
+
projectScore: 2,
|
|
389
|
+
taskUpdatedAtMs: toTaskUpdatedAtMs("2026-01-01T00:00:00.000Z")
|
|
390
|
+
});
|
|
391
|
+
const newer = buildCandidate({
|
|
392
|
+
id: "TASK-0002",
|
|
393
|
+
title: "Newer Task",
|
|
394
|
+
status: "TODO",
|
|
395
|
+
projectScore: 2,
|
|
396
|
+
taskUpdatedAtMs: toTaskUpdatedAtMs("2026-02-01T00:00:00.000Z")
|
|
397
|
+
});
|
|
398
|
+
const ranked = rankActionableTaskCandidates([older, newer]);
|
|
399
|
+
expect(ranked[0].task.id).toBe("TASK-0002");
|
|
400
|
+
expect(ranked[1].task.id).toBe("TASK-0001");
|
|
401
|
+
});
|
|
402
|
+
it("renders tasks without subState or blocker when not applicable", () => {
|
|
403
|
+
const task = normalizeTask({
|
|
404
|
+
id: "TASK-0001",
|
|
405
|
+
title: "Simple TODO Task",
|
|
406
|
+
status: "TODO"
|
|
407
|
+
});
|
|
408
|
+
const markdown = renderTasksMarkdown([task]);
|
|
409
|
+
expect(markdown).not.toContain("subState:");
|
|
410
|
+
expect(markdown).not.toContain("blocker:");
|
|
411
|
+
});
|
|
412
|
+
it("parses task with invalid subState phase gracefully", async () => {
|
|
413
|
+
const markdown = [
|
|
414
|
+
"# Tasks",
|
|
415
|
+
TASKS_START,
|
|
416
|
+
"## TASK-0001 | IN_PROGRESS | Invalid Phase",
|
|
417
|
+
"- owner: test",
|
|
418
|
+
"- summary: test",
|
|
419
|
+
"- updatedAt: 2026-02-22T00:00:00.000Z",
|
|
420
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
421
|
+
"- links:",
|
|
422
|
+
" - (none)",
|
|
423
|
+
"- subState:",
|
|
424
|
+
" - phase: invalid_phase",
|
|
425
|
+
TASKS_END,
|
|
426
|
+
].join("\n");
|
|
427
|
+
const tasks = await parseTasksBlock(markdown);
|
|
428
|
+
expect(tasks).toHaveLength(1);
|
|
429
|
+
// Invalid phase should be ignored
|
|
430
|
+
expect(tasks[0].subState?.phase).toBeUndefined();
|
|
431
|
+
});
|
|
432
|
+
it("parses task with invalid confidence score gracefully", async () => {
|
|
433
|
+
const markdown = [
|
|
434
|
+
"# Tasks",
|
|
435
|
+
TASKS_START,
|
|
436
|
+
"## TASK-0001 | IN_PROGRESS | Invalid Confidence",
|
|
437
|
+
"- owner: test",
|
|
438
|
+
"- summary: test",
|
|
439
|
+
"- updatedAt: 2026-02-22T00:00:00.000Z",
|
|
440
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
441
|
+
"- links:",
|
|
442
|
+
" - (none)",
|
|
443
|
+
"- subState:",
|
|
444
|
+
" - confidence: 2.5",
|
|
445
|
+
TASKS_END,
|
|
446
|
+
].join("\n");
|
|
447
|
+
const tasks = await parseTasksBlock(markdown);
|
|
448
|
+
expect(tasks).toHaveLength(1);
|
|
449
|
+
// Invalid confidence should be ignored
|
|
450
|
+
expect(tasks[0].subState?.confidence).toBeUndefined();
|
|
451
|
+
});
|
|
452
|
+
it("parses task with invalid blocker type gracefully", async () => {
|
|
453
|
+
const markdown = [
|
|
454
|
+
"# Tasks",
|
|
455
|
+
TASKS_START,
|
|
456
|
+
"## TASK-0001 | BLOCKED | Invalid Blocker Type",
|
|
457
|
+
"- owner: test",
|
|
458
|
+
"- summary: test",
|
|
459
|
+
"- updatedAt: 2026-02-22T00:00:00.000Z",
|
|
460
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
461
|
+
"- links:",
|
|
462
|
+
" - (none)",
|
|
463
|
+
"- blocker:",
|
|
464
|
+
" - type: invalid_type",
|
|
465
|
+
" - description: test",
|
|
466
|
+
TASKS_END,
|
|
467
|
+
].join("\n");
|
|
468
|
+
const tasks = await parseTasksBlock(markdown);
|
|
469
|
+
expect(tasks).toHaveLength(1);
|
|
470
|
+
// Invalid blocker type should use default
|
|
471
|
+
expect(tasks[0].blocker?.type).toBe("external_dependency");
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projitive MCP - Core Type Definitions
|
|
3
|
+
* Spec v1.1.0 - Sub-state Metadata Support
|
|
4
|
+
*/
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Task State Machine
|
|
7
|
+
// ============================================================================
|
|
8
|
+
export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Sub-state Metadata (Spec v1.1.0)
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Phase of work within IN_PROGRESS state
|
|
14
|
+
* - discovery: Researching requirements and constraints
|
|
15
|
+
* - design: Creating architecture and implementation plan
|
|
16
|
+
* - implementation: Writing and testing code
|
|
17
|
+
* - testing: Validation and verification
|
|
18
|
+
*/
|
|
19
|
+
export const SUB_STATE_PHASES = ["discovery", "design", "implementation", "testing"];
|
|
20
|
+
/**
|
|
21
|
+
* Blocker categorization for BLOCKED state (Spec v1.1.0)
|
|
22
|
+
*/
|
|
23
|
+
export const BLOCKER_TYPES = [
|
|
24
|
+
"internal_dependency",
|
|
25
|
+
"external_dependency",
|
|
26
|
+
"resource",
|
|
27
|
+
"approval"
|
|
28
|
+
];
|
|
29
|
+
// Weight factors for confidence calculation
|
|
30
|
+
export const CONFIDENCE_WEIGHTS = {
|
|
31
|
+
contextCompleteness: 0.4,
|
|
32
|
+
similarTaskHistory: 0.3,
|
|
33
|
+
specificationClarity: 0.3,
|
|
34
|
+
};
|
|
35
|
+
// Confidence thresholds
|
|
36
|
+
export const CONFIDENCE_THRESHOLDS = {
|
|
37
|
+
autoCreate: 0.85,
|
|
38
|
+
reviewRequired: 0.6,
|
|
39
|
+
};
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Parser Constants
|
|
42
|
+
// ============================================================================
|
|
43
|
+
export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
|
|
44
|
+
export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
|
|
45
|
+
export const TASK_ID_REGEX = /^TASK-\d{4}$/;
|
|
46
|
+
export const TASK_LINT_CODES = {
|
|
47
|
+
DUPLICATE_ID: "TASK_DUPLICATE_ID",
|
|
48
|
+
IN_PROGRESS_OWNER_EMPTY: "TASK_IN_PROGRESS_OWNER_EMPTY",
|
|
49
|
+
DONE_LINKS_MISSING: "TASK_DONE_LINKS_MISSING",
|
|
50
|
+
BLOCKED_SUMMARY_EMPTY: "TASK_BLOCKED_SUMMARY_EMPTY",
|
|
51
|
+
UPDATED_AT_INVALID: "TASK_UPDATED_AT_INVALID",
|
|
52
|
+
ROADMAP_REFS_EMPTY: "TASK_ROADMAP_REFS_EMPTY",
|
|
53
|
+
OUTSIDE_MARKER: "TASK_OUTSIDE_MARKER",
|
|
54
|
+
FILTER_EMPTY: "TASK_FILTER_EMPTY",
|
|
55
|
+
LINK_TARGET_MISSING: "TASK_LINK_TARGET_MISSING",
|
|
56
|
+
HOOK_FILE_MISSING: "TASK_HOOK_FILE_MISSING",
|
|
57
|
+
CONTEXT_HOOK_HEAD_MISSING: "TASK_CONTEXT_HOOK_HEAD_MISSING",
|
|
58
|
+
CONTEXT_HOOK_FOOTER_MISSING: "TASK_CONTEXT_HOOK_FOOTER_MISSING",
|
|
59
|
+
// Spec v1.1.0 - Blocker Categorization
|
|
60
|
+
BLOCKED_WITHOUT_BLOCKER: "TASK_BLOCKED_WITHOUT_BLOCKER",
|
|
61
|
+
BLOCKER_TYPE_INVALID: "TASK_BLOCKER_TYPE_INVALID",
|
|
62
|
+
BLOCKER_DESCRIPTION_EMPTY: "TASK_BLOCKER_DESCRIPTION_EMPTY",
|
|
63
|
+
// Spec v1.1.0 - Sub-state Metadata
|
|
64
|
+
IN_PROGRESS_WITHOUT_SUBSTATE: "TASK_IN_PROGRESS_WITHOUT_SUBSTATE",
|
|
65
|
+
SUBSTATE_PHASE_INVALID: "TASK_SUBSTATE_PHASE_INVALID",
|
|
66
|
+
SUBSTATE_CONFIDENCE_INVALID: "TASK_SUBSTATE_CONFIDENCE_INVALID",
|
|
67
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projitive/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Projitive MCP Server for project and task discovery/update",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"test": "vitest run",
|
|
17
|
+
"test:coverage": "vitest run --coverage",
|
|
18
|
+
"benchmark": "vitest bench --run",
|
|
17
19
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
18
20
|
"build": "rm -rf output && tsc -p tsconfig.json",
|
|
19
21
|
"prepublishOnly": "npm run build",
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"@types/node": "^24.3.0",
|
|
33
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
31
34
|
"tsx": "^4.20.5",
|
|
32
35
|
"typescript": "^5.9.2",
|
|
33
36
|
"vitest": "^3.2.4"
|
package/output/source/designs.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { isValidRoadmapId } from "./roadmap.js";
|
|
2
|
-
import { isValidTaskId } from "./tasks.js";
|
|
3
|
-
export function parseDesignMetadata(markdown) {
|
|
4
|
-
const lines = markdown.split(/\r?\n/);
|
|
5
|
-
const metadata = {};
|
|
6
|
-
for (const line of lines) {
|
|
7
|
-
const [rawKey, ...rawValue] = line.split(":");
|
|
8
|
-
if (!rawKey || rawValue.length === 0) {
|
|
9
|
-
continue;
|
|
10
|
-
}
|
|
11
|
-
const key = rawKey.trim().toLowerCase();
|
|
12
|
-
const value = rawValue.join(":").trim();
|
|
13
|
-
if (key === "task")
|
|
14
|
-
metadata.task = value;
|
|
15
|
-
if (key === "roadmap")
|
|
16
|
-
metadata.roadmap = value;
|
|
17
|
-
if (key === "owner")
|
|
18
|
-
metadata.owner = value;
|
|
19
|
-
if (key === "status")
|
|
20
|
-
metadata.status = value;
|
|
21
|
-
if (key === "last updated")
|
|
22
|
-
metadata.lastUpdated = value;
|
|
23
|
-
}
|
|
24
|
-
return metadata;
|
|
25
|
-
}
|
|
26
|
-
export function validateDesignMetadata(metadata) {
|
|
27
|
-
const errors = [];
|
|
28
|
-
if (!metadata.task) {
|
|
29
|
-
errors.push("Missing Task metadata");
|
|
30
|
-
}
|
|
31
|
-
else if (!isValidTaskId(metadata.task)) {
|
|
32
|
-
errors.push(`Invalid Task metadata format: ${metadata.task}`);
|
|
33
|
-
}
|
|
34
|
-
if (metadata.roadmap && !isValidRoadmapId(metadata.roadmap)) {
|
|
35
|
-
errors.push(`Invalid Roadmap metadata format: ${metadata.roadmap}`);
|
|
36
|
-
}
|
|
37
|
-
return { ok: errors.length === 0, errors };
|
|
38
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./artifacts.js";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './catch.js';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './files.js';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './markdown.js';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./response.js";
|