@projitive/mcp 2.0.3 → 2.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.
Files changed (52) hide show
  1. package/output/package.json +8 -2
  2. package/output/source/common/artifacts.js +1 -1
  3. package/output/source/common/artifacts.test.js +11 -11
  4. package/output/source/common/errors.js +19 -19
  5. package/output/source/common/errors.test.js +59 -0
  6. package/output/source/common/files.js +30 -19
  7. package/output/source/common/files.test.js +14 -14
  8. package/output/source/common/index.js +11 -10
  9. package/output/source/common/linter.js +29 -27
  10. package/output/source/common/linter.test.js +9 -9
  11. package/output/source/common/markdown.js +3 -3
  12. package/output/source/common/markdown.test.js +15 -15
  13. package/output/source/common/response.js +91 -107
  14. package/output/source/common/response.test.js +30 -30
  15. package/output/source/common/store.js +40 -40
  16. package/output/source/common/store.test.js +72 -72
  17. package/output/source/common/tool.js +43 -0
  18. package/output/source/common/types.js +3 -3
  19. package/output/source/common/utils.js +8 -8
  20. package/output/source/common/utils.test.js +48 -0
  21. package/output/source/index.js +16 -16
  22. package/output/source/index.runtime.test.js +57 -0
  23. package/output/source/index.test.js +64 -64
  24. package/output/source/prompts/index.js +3 -3
  25. package/output/source/prompts/index.test.js +23 -0
  26. package/output/source/prompts/quickStart.js +96 -96
  27. package/output/source/prompts/quickStart.test.js +24 -0
  28. package/output/source/prompts/taskDiscovery.js +184 -184
  29. package/output/source/prompts/taskDiscovery.test.js +24 -0
  30. package/output/source/prompts/taskExecution.js +164 -148
  31. package/output/source/prompts/taskExecution.test.js +27 -0
  32. package/output/source/resources/designs.js +26 -26
  33. package/output/source/resources/designs.resources.test.js +52 -0
  34. package/output/source/resources/designs.test.js +88 -88
  35. package/output/source/resources/governance.js +19 -19
  36. package/output/source/resources/governance.test.js +35 -0
  37. package/output/source/resources/index.js +2 -2
  38. package/output/source/resources/index.test.js +18 -0
  39. package/output/source/resources/readme.js +7 -7
  40. package/output/source/resources/readme.test.js +113 -113
  41. package/output/source/resources/reports.js +10 -10
  42. package/output/source/resources/reports.test.js +83 -83
  43. package/output/source/tools/index.js +3 -3
  44. package/output/source/tools/index.test.js +23 -0
  45. package/output/source/tools/project.js +330 -377
  46. package/output/source/tools/project.test.js +308 -175
  47. package/output/source/tools/roadmap.js +236 -255
  48. package/output/source/tools/roadmap.test.js +241 -46
  49. package/output/source/tools/task.js +770 -652
  50. package/output/source/tools/task.test.js +433 -105
  51. package/output/source/types.js +28 -22
  52. package/package.json +8 -2
@@ -1,75 +1,270 @@
1
- import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import os from "node:os";
5
- import { isValidRoadmapId, collectRoadmapLintSuggestions, loadRoadmapDocument, renderRoadmapMarkdown, registerRoadmapTools } from "./roadmap.js";
6
- describe("roadmap module", () => {
1
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { replaceRoadmapsInStore } from '../common/store.js';
6
+ import { isValidRoadmapId, collectRoadmapLintSuggestions, loadRoadmapDocument, renderRoadmapMarkdown, registerRoadmapTools } from './roadmap.js';
7
+ async function createGovernanceWorkspace() {
8
+ const projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-roadmap-workspace-'));
9
+ const governanceDir = path.join(projectRoot, '.projitive');
10
+ const dbPath = path.join(governanceDir, '.projitive');
11
+ await fs.mkdir(governanceDir, { recursive: true });
12
+ await fs.writeFile(dbPath, '', 'utf-8');
13
+ return { projectRoot, governanceDir, dbPath };
14
+ }
15
+ function getRoadmapToolHandler(mockServer, toolName) {
16
+ const call = mockServer.registerTool.mock.calls.find((entry) => entry[0] === toolName);
17
+ expect(call).toBeTruthy();
18
+ return call?.[2];
19
+ }
20
+ describe('roadmap module', () => {
7
21
  let tempDir;
8
22
  beforeAll(async () => {
9
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-roadmap-test-"));
23
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-roadmap-test-'));
10
24
  });
11
25
  afterAll(async () => {
12
26
  await fs.rm(tempDir, { recursive: true, force: true });
13
27
  });
14
- describe("isValidRoadmapId", () => {
15
- it("should validate correct roadmap IDs", () => {
16
- expect(isValidRoadmapId("ROADMAP-0001")).toBe(true);
17
- expect(isValidRoadmapId("ROADMAP-1234")).toBe(true);
28
+ describe('isValidRoadmapId', () => {
29
+ it('should validate correct roadmap IDs', () => {
30
+ expect(isValidRoadmapId('ROADMAP-0001')).toBe(true);
31
+ expect(isValidRoadmapId('ROADMAP-1')).toBe(true);
32
+ expect(isValidRoadmapId('ROADMAP-1234')).toBe(true);
33
+ expect(isValidRoadmapId('ROADMAP-12345')).toBe(true);
18
34
  });
19
- it("should reject invalid roadmap IDs", () => {
20
- expect(isValidRoadmapId("roadmap-0001")).toBe(false);
21
- expect(isValidRoadmapId("TASK-0001")).toBe(false);
22
- expect(isValidRoadmapId("invalid")).toBe(false);
35
+ it('should reject invalid roadmap IDs', () => {
36
+ expect(isValidRoadmapId('roadmap-0001')).toBe(false);
37
+ expect(isValidRoadmapId('TASK-0001')).toBe(false);
38
+ expect(isValidRoadmapId('invalid')).toBe(false);
23
39
  });
24
40
  });
25
- describe("collectRoadmapLintSuggestions", () => {
26
- it("should return lint suggestion for empty roadmap IDs", () => {
41
+ describe('collectRoadmapLintSuggestions', () => {
42
+ it('should return lint suggestion for empty roadmap IDs', () => {
27
43
  const suggestions = collectRoadmapLintSuggestions([], []);
28
- expect(suggestions.some(s => s.includes("IDS_EMPTY"))).toBe(true);
44
+ expect(suggestions.some(s => s.includes('IDS_EMPTY'))).toBe(true);
29
45
  });
30
- it("should return lint suggestion for empty tasks", () => {
31
- const suggestions = collectRoadmapLintSuggestions(["ROADMAP-0001"], []);
32
- expect(suggestions.some(s => s.includes("TASKS_EMPTY"))).toBe(true);
46
+ it('should return lint suggestion for empty tasks', () => {
47
+ const suggestions = collectRoadmapLintSuggestions(['ROADMAP-0001'], []);
48
+ expect(suggestions.some(s => s.includes('TASKS_EMPTY'))).toBe(true);
33
49
  });
34
- it("should return lint suggestion for tasks without roadmap refs", () => {
50
+ it('should return lint suggestion for tasks without roadmap refs', () => {
35
51
  const tasks = [{
36
- id: "TASK-0001",
37
- title: "Test Task",
38
- status: "TODO",
39
- owner: "ai-copilot",
40
- summary: "Test",
41
- updatedAt: "2026-01-01T00:00:00.000Z",
52
+ id: 'TASK-0001',
53
+ title: 'Test Task',
54
+ status: 'TODO',
55
+ owner: 'ai-copilot',
56
+ summary: 'Test',
57
+ updatedAt: '2026-01-01T00:00:00.000Z',
42
58
  links: [],
43
59
  roadmapRefs: [],
44
60
  }];
45
- const suggestions = collectRoadmapLintSuggestions(["ROADMAP-0001"], tasks);
46
- expect(suggestions.some(s => s.includes("TASK_REFS_EMPTY"))).toBe(true);
61
+ const suggestions = collectRoadmapLintSuggestions(['ROADMAP-0001'], tasks);
62
+ expect(suggestions.some(s => s.includes('TASK_REFS_EMPTY'))).toBe(true);
47
63
  });
48
- it("loads from governance store and rewrites roadmap markdown view", async () => {
49
- const governanceDir = path.join(tempDir, ".projitive-db");
64
+ it('collects lint suggestion for tasks with unknown roadmap refs', () => {
65
+ const tasks = [{
66
+ id: 'TASK-0001',
67
+ title: 'Orphaned Task',
68
+ status: 'TODO',
69
+ owner: 'ai-copilot',
70
+ summary: '',
71
+ updatedAt: '2026-01-01T00:00:00.000Z',
72
+ links: [],
73
+ roadmapRefs: ['ROADMAP-9999'],
74
+ }];
75
+ const suggestions = collectRoadmapLintSuggestions(['ROADMAP-0001'], tasks);
76
+ expect(suggestions.some(s => s.includes('UNKNOWN_REFS'))).toBe(true);
77
+ });
78
+ it('loads from governance store and rewrites roadmap markdown view', async () => {
79
+ const governanceDir = path.join(tempDir, '.projitive-db');
50
80
  await fs.mkdir(governanceDir, { recursive: true });
51
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
81
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
52
82
  const doc = await loadRoadmapDocument(governanceDir);
53
- expect(doc.roadmapPath.endsWith(".projitive")).toBe(true);
54
- expect(doc.markdownPath.endsWith("roadmap.md")).toBe(true);
55
- const markdown = await fs.readFile(path.join(governanceDir, "roadmap.md"), "utf-8");
56
- expect(markdown).toContain("generated from .projitive governance store");
83
+ expect(doc.roadmapPath.endsWith('.projitive')).toBe(true);
84
+ expect(doc.markdownPath.endsWith('roadmap.md')).toBe(true);
85
+ const markdown = await fs.readFile(path.join(governanceDir, 'roadmap.md'), 'utf-8');
86
+ expect(markdown).toContain('generated from .projitive governance store');
87
+ expect(markdown).toContain('Author: yinxulai');
88
+ expect(markdown).toContain('Repository: https://github.com/yinxulai/projitive');
89
+ expect(markdown).toContain('Do not edit this file manually.');
57
90
  });
58
- it("renders milestones in newest-first order", () => {
91
+ it('renders milestones in newest-first order', () => {
59
92
  const markdown = renderRoadmapMarkdown([
60
- { id: "ROADMAP-0001", title: "Older", status: "active", updatedAt: "2026-01-01T00:00:00.000Z" },
61
- { id: "ROADMAP-0002", title: "Newer", status: "done", updatedAt: "2026-02-01T00:00:00.000Z" },
93
+ { id: 'ROADMAP-0001', title: 'Older', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
94
+ { id: 'ROADMAP-0002', title: 'Newer', status: 'done', updatedAt: '2026-02-01T00:00:00.000Z' },
62
95
  ]);
63
- expect(markdown.indexOf("ROADMAP-0002")).toBeLessThan(markdown.indexOf("ROADMAP-0001"));
64
- expect(markdown).toContain("[x] ROADMAP-0002");
96
+ expect(markdown.indexOf('ROADMAP-0002')).toBeLessThan(markdown.indexOf('ROADMAP-0001'));
97
+ expect(markdown).toContain('[x] ROADMAP-0002');
65
98
  });
66
- it("registers roadmapCreate tool", () => {
99
+ it('registers roadmapCreate tool', () => {
67
100
  const mockServer = {
68
- registerTool: (..._args) => undefined,
101
+ registerTool: (...args) => {
102
+ void args;
103
+ return undefined;
104
+ },
69
105
  };
70
- const spy = vi.spyOn(mockServer, "registerTool");
106
+ const spy = vi.spyOn(mockServer, 'registerTool');
107
+ registerRoadmapTools(mockServer);
108
+ expect(spy.mock.calls.some((call) => call[0] === 'roadmapCreate')).toBe(true);
109
+ });
110
+ it('sorts milestones with same timestamp by ID descending', () => {
111
+ const markdown = renderRoadmapMarkdown([
112
+ { id: 'ROADMAP-0001', title: 'First', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
113
+ { id: 'ROADMAP-0003', title: 'Third', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
114
+ { id: 'ROADMAP-0002', title: 'Second', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
115
+ ]);
116
+ expect(markdown.indexOf('ROADMAP-0003')).toBeLessThan(markdown.indexOf('ROADMAP-0002'));
117
+ expect(markdown.indexOf('ROADMAP-0002')).toBeLessThan(markdown.indexOf('ROADMAP-0001'));
118
+ });
119
+ it('skips view write on second load with no changes', async () => {
120
+ const { projectRoot, governanceDir } = await createGovernanceWorkspace();
121
+ await loadRoadmapDocument(governanceDir);
122
+ const doc = await loadRoadmapDocument(governanceDir);
123
+ expect(doc.milestones).toHaveLength(0);
124
+ await fs.rm(projectRoot, { recursive: true, force: true });
125
+ });
126
+ });
127
+ describe('roadmapList tool handler', () => {
128
+ it('lists roadmap IDs and linked task count', async () => {
129
+ const { projectRoot, dbPath } = await createGovernanceWorkspace();
130
+ await replaceRoadmapsInStore(dbPath, [
131
+ { id: 'ROADMAP-0001', title: 'First', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
132
+ { id: 'ROADMAP-0002', title: 'Second', status: 'done', updatedAt: '2026-02-01T00:00:00.000Z' },
133
+ ]);
134
+ const mockServer = { registerTool: vi.fn() };
135
+ registerRoadmapTools(mockServer);
136
+ const roadmapList = getRoadmapToolHandler(mockServer, 'roadmapList');
137
+ const result = await roadmapList({ projectPath: projectRoot });
138
+ expect(result.isError).toBeUndefined();
139
+ expect(result.content[0].text).toContain('roadmapCount: 2');
140
+ expect(result.content[0].text).toContain('ROADMAP-0001');
141
+ await fs.rm(projectRoot, { recursive: true, force: true });
142
+ });
143
+ it('returns no nextCall when roadmap store is empty', async () => {
144
+ const { projectRoot } = await createGovernanceWorkspace();
145
+ const mockServer = { registerTool: vi.fn() };
146
+ registerRoadmapTools(mockServer);
147
+ const roadmapList = getRoadmapToolHandler(mockServer, 'roadmapList');
148
+ const result = await roadmapList({ projectPath: projectRoot });
149
+ expect(result.isError).toBeUndefined();
150
+ expect(result.content[0].text).toContain('roadmapCount: 0');
151
+ await fs.rm(projectRoot, { recursive: true, force: true });
152
+ });
153
+ });
154
+ describe('roadmapContext tool handler', () => {
155
+ it('returns error for invalid roadmap ID format', async () => {
156
+ const { projectRoot } = await createGovernanceWorkspace();
157
+ const mockServer = { registerTool: vi.fn() };
158
+ registerRoadmapTools(mockServer);
159
+ const roadmapContext = getRoadmapToolHandler(mockServer, 'roadmapContext');
160
+ const result = await roadmapContext({ projectPath: projectRoot, roadmapId: 'INVALID-ID' });
161
+ expect(result.isError).toBe(true);
162
+ expect(result.content[0].text).toContain('Invalid roadmap ID format');
163
+ await fs.rm(projectRoot, { recursive: true, force: true });
164
+ });
165
+ it('returns context with related tasks and reference locations', async () => {
166
+ const { projectRoot, dbPath } = await createGovernanceWorkspace();
167
+ await replaceRoadmapsInStore(dbPath, [
168
+ { id: 'ROADMAP-0001', title: 'Bootstrap', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
169
+ ]);
170
+ const mockServer = { registerTool: vi.fn() };
171
+ registerRoadmapTools(mockServer);
172
+ const roadmapContext = getRoadmapToolHandler(mockServer, 'roadmapContext');
173
+ const result = await roadmapContext({ projectPath: projectRoot, roadmapId: 'ROADMAP-0001' });
174
+ expect(result.isError).toBeUndefined();
175
+ expect(result.content[0].text).toContain('ROADMAP-0001');
176
+ expect(result.content[0].text).toContain('relatedTasks: 0');
177
+ expect(result.content[0].text).toContain('roadmapView:');
178
+ await fs.rm(projectRoot, { recursive: true, force: true });
179
+ });
180
+ });
181
+ describe('roadmapCreate tool handler', () => {
182
+ it('returns error for invalid roadmap ID format', async () => {
183
+ const { projectRoot } = await createGovernanceWorkspace();
184
+ const mockServer = { registerTool: vi.fn() };
185
+ registerRoadmapTools(mockServer);
186
+ const roadmapCreate = getRoadmapToolHandler(mockServer, 'roadmapCreate');
187
+ const result = await roadmapCreate({ projectPath: projectRoot, roadmapId: 'BAD-FORMAT', title: 'Test' });
188
+ expect(result.isError).toBe(true);
189
+ expect(result.content[0].text).toContain('Invalid roadmap ID format');
190
+ await fs.rm(projectRoot, { recursive: true, force: true });
191
+ });
192
+ it('returns error when roadmap ID already exists', async () => {
193
+ const { projectRoot, dbPath } = await createGovernanceWorkspace();
194
+ await replaceRoadmapsInStore(dbPath, [
195
+ { id: 'ROADMAP-0001', title: 'Existing', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
196
+ ]);
197
+ const mockServer = { registerTool: vi.fn() };
198
+ registerRoadmapTools(mockServer);
199
+ const roadmapCreate = getRoadmapToolHandler(mockServer, 'roadmapCreate');
200
+ const result = await roadmapCreate({ projectPath: projectRoot, roadmapId: 'ROADMAP-0001', title: 'Duplicate' });
201
+ expect(result.isError).toBe(true);
202
+ expect(result.content[0].text).toContain('already exists');
203
+ await fs.rm(projectRoot, { recursive: true, force: true });
204
+ });
205
+ it('auto-generates next roadmap ID', async () => {
206
+ const { projectRoot, dbPath } = await createGovernanceWorkspace();
207
+ await replaceRoadmapsInStore(dbPath, [
208
+ { id: 'ROADMAP-0005', title: 'Existing', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
209
+ ]);
210
+ const mockServer = { registerTool: vi.fn() };
211
+ registerRoadmapTools(mockServer);
212
+ const roadmapCreate = getRoadmapToolHandler(mockServer, 'roadmapCreate');
213
+ const result = await roadmapCreate({ projectPath: projectRoot, title: 'New Milestone' });
214
+ expect(result.isError).toBeUndefined();
215
+ expect(result.content[0].text).toContain('ROADMAP-0006');
216
+ await fs.rm(projectRoot, { recursive: true, force: true });
217
+ });
218
+ it('creates milestone with explicit ID, status done, and time', async () => {
219
+ const { projectRoot } = await createGovernanceWorkspace();
220
+ const mockServer = { registerTool: vi.fn() };
221
+ registerRoadmapTools(mockServer);
222
+ const roadmapCreate = getRoadmapToolHandler(mockServer, 'roadmapCreate');
223
+ const result = await roadmapCreate({ projectPath: projectRoot, roadmapId: 'ROADMAP-0001', title: 'Planned', status: 'done', time: '2026-Q2' });
224
+ expect(result.isError).toBeUndefined();
225
+ expect(result.content[0].text).toContain('ROADMAP-0001');
226
+ expect(result.content[0].text).toContain('done');
227
+ await fs.rm(projectRoot, { recursive: true, force: true });
228
+ });
229
+ });
230
+ describe('roadmapUpdate tool handler', () => {
231
+ it('returns error for invalid roadmap ID format', async () => {
232
+ const { projectRoot } = await createGovernanceWorkspace();
233
+ const mockServer = { registerTool: vi.fn() };
234
+ registerRoadmapTools(mockServer);
235
+ const roadmapUpdate = getRoadmapToolHandler(mockServer, 'roadmapUpdate');
236
+ const result = await roadmapUpdate({ projectPath: projectRoot, roadmapId: 'NOT-VALID', updates: { title: 'New' } });
237
+ expect(result.isError).toBe(true);
238
+ expect(result.content[0].text).toContain('Invalid roadmap ID format');
239
+ await fs.rm(projectRoot, { recursive: true, force: true });
240
+ });
241
+ it('returns error when roadmap milestone not found', async () => {
242
+ const { projectRoot } = await createGovernanceWorkspace();
243
+ const mockServer = { registerTool: vi.fn() };
244
+ registerRoadmapTools(mockServer);
245
+ const roadmapUpdate = getRoadmapToolHandler(mockServer, 'roadmapUpdate');
246
+ const result = await roadmapUpdate({ projectPath: projectRoot, roadmapId: 'ROADMAP-0099', updates: { title: 'Does not exist' } });
247
+ expect(result.isError).toBe(true);
248
+ expect(result.content[0].text).toContain('not found');
249
+ await fs.rm(projectRoot, { recursive: true, force: true });
250
+ });
251
+ it('updates milestone title, status, and time', async () => {
252
+ const { projectRoot, dbPath } = await createGovernanceWorkspace();
253
+ await replaceRoadmapsInStore(dbPath, [
254
+ { id: 'ROADMAP-0001', title: 'Original', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
255
+ ]);
256
+ const mockServer = { registerTool: vi.fn() };
71
257
  registerRoadmapTools(mockServer);
72
- expect(spy.mock.calls.some((call) => call[0] === "roadmapCreate")).toBe(true);
258
+ const roadmapUpdate = getRoadmapToolHandler(mockServer, 'roadmapUpdate');
259
+ const result = await roadmapUpdate({
260
+ projectPath: projectRoot,
261
+ roadmapId: 'ROADMAP-0001',
262
+ updates: { title: 'Updated', status: 'done', time: '2026-Q3' },
263
+ });
264
+ expect(result.isError).toBeUndefined();
265
+ expect(result.content[0].text).toContain('ROADMAP-0001');
266
+ expect(result.content[0].text).toContain('done');
267
+ await fs.rm(projectRoot, { recursive: true, force: true });
73
268
  });
74
269
  });
75
270
  });