@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,366 +1,499 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanDepth, toProjectPath, registerProjectTools } from "./project.js";
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanRoot, resolveScanDepth, toProjectPath, registerProjectTools } from './project.js';
6
6
  const tempPaths = [];
7
7
  async function createTempDir() {
8
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-test-'));
9
9
  tempPaths.push(dir);
10
10
  return dir;
11
11
  }
12
+ function getProjectToolHandler(mockServer, toolName) {
13
+ const call = mockServer.registerTool.mock.calls.find((entry) => entry[0] === toolName);
14
+ expect(call).toBeTruthy();
15
+ return call?.[2];
16
+ }
12
17
  afterEach(async () => {
13
18
  await Promise.all(tempPaths.splice(0).map(async (dir) => {
14
19
  await fs.rm(dir, { recursive: true, force: true });
15
20
  }));
16
21
  vi.restoreAllMocks();
17
22
  });
18
- describe("projitive module", () => {
19
- describe("hasProjectMarker", () => {
20
- it("does not treat marker directory as a valid project marker", async () => {
23
+ describe('projitive module', () => {
24
+ describe('hasProjectMarker', () => {
25
+ it('does not treat marker directory as a valid project marker', async () => {
21
26
  const root = await createTempDir();
22
- const dirMarkerPath = path.join(root, ".projitive");
27
+ const dirMarkerPath = path.join(root, '.projitive');
23
28
  await fs.mkdir(dirMarkerPath, { recursive: true });
24
29
  const hasMarker = await hasProjectMarker(root);
25
30
  expect(hasMarker).toBe(false);
26
31
  });
27
- it("returns true when .projitive marker file exists", async () => {
32
+ it('returns true when .projitive marker file exists', async () => {
28
33
  const root = await createTempDir();
29
- const markerPath = path.join(root, ".projitive");
30
- await fs.writeFile(markerPath, "", "utf-8");
34
+ const markerPath = path.join(root, '.projitive');
35
+ await fs.writeFile(markerPath, '', 'utf-8');
31
36
  const hasMarker = await hasProjectMarker(root);
32
37
  expect(hasMarker).toBe(true);
33
38
  });
34
- it("returns false when .projitive marker file does not exist", async () => {
39
+ it('returns false when .projitive marker file does not exist', async () => {
35
40
  const root = await createTempDir();
36
41
  const hasMarker = await hasProjectMarker(root);
37
42
  expect(hasMarker).toBe(false);
38
43
  });
39
- it("handles fs.stat errors gracefully", async () => {
44
+ it('handles fs.stat errors gracefully', async () => {
40
45
  const root = await createTempDir();
41
- vi.spyOn(fs, "stat").mockRejectedValueOnce(new Error("Permission denied"));
46
+ vi.spyOn(fs, 'stat').mockRejectedValueOnce(new Error('Permission denied'));
42
47
  const hasMarker = await hasProjectMarker(root);
43
48
  expect(hasMarker).toBe(false);
44
49
  });
45
50
  });
46
- describe("resolveGovernanceDir", () => {
47
- it("resolves governance dir by walking upwards for .projitive", async () => {
51
+ describe('resolveGovernanceDir', () => {
52
+ it('resolves governance dir by walking upwards for .projitive', async () => {
48
53
  const root = await createTempDir();
49
- const governanceDir = path.join(root, "repo", "governance");
50
- const deepDir = path.join(governanceDir, "nested", "module");
54
+ const governanceDir = path.join(root, 'repo', 'governance');
55
+ const deepDir = path.join(governanceDir, 'nested', 'module');
51
56
  await fs.mkdir(deepDir, { recursive: true });
52
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
57
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
53
58
  const resolved = await resolveGovernanceDir(deepDir);
54
59
  expect(resolved).toBe(governanceDir);
55
60
  });
56
- it("resolves nested default governance dir when input path is project root", async () => {
61
+ it('resolves nested default governance dir when input path is project root', async () => {
57
62
  const root = await createTempDir();
58
- const projectRoot = path.join(root, "repo");
59
- const governanceDir = path.join(projectRoot, ".projitive");
63
+ const projectRoot = path.join(root, 'repo');
64
+ const governanceDir = path.join(projectRoot, '.projitive');
60
65
  await fs.mkdir(governanceDir, { recursive: true });
61
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
66
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
62
67
  const resolved = await resolveGovernanceDir(projectRoot);
63
68
  expect(resolved).toBe(governanceDir);
64
69
  });
65
- it("resolves nested custom governance dir when input path is project root", async () => {
70
+ it('resolves nested custom governance dir when input path is project root', async () => {
66
71
  const root = await createTempDir();
67
- const projectRoot = path.join(root, "repo");
68
- const governanceDir = path.join(projectRoot, "governance");
72
+ const projectRoot = path.join(root, 'repo');
73
+ const governanceDir = path.join(projectRoot, 'governance');
69
74
  await fs.mkdir(governanceDir, { recursive: true });
70
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
75
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
71
76
  const resolved = await resolveGovernanceDir(projectRoot);
72
77
  expect(resolved).toBe(governanceDir);
73
78
  });
74
- it("throws error when path not found", async () => {
79
+ it('throws error when path not found', async () => {
75
80
  const root = await createTempDir();
76
- const nonExistentPath = path.join(root, "nonexistent");
77
- await expect(resolveGovernanceDir(nonExistentPath)).rejects.toThrow("Path not found");
81
+ const nonExistentPath = path.join(root, 'nonexistent');
82
+ await expect(resolveGovernanceDir(nonExistentPath)).rejects.toThrow('Path not found');
78
83
  });
79
- it("throws error when no .projitive marker found", async () => {
84
+ it('throws error when no .projitive marker found', async () => {
80
85
  const root = await createTempDir();
81
- const deepDir = path.join(root, "a", "b", "c");
86
+ const deepDir = path.join(root, 'a', 'b', 'c');
82
87
  await fs.mkdir(deepDir, { recursive: true });
83
- await expect(resolveGovernanceDir(deepDir)).rejects.toThrow("No .projitive marker found");
88
+ await expect(resolveGovernanceDir(deepDir)).rejects.toThrow('No .projitive marker found');
84
89
  });
85
- it("prefers default .projitive directory when multiple governance roots found as children", async () => {
90
+ it('throws when multiple non-default governance roots exist under same parent', async () => {
91
+ const root = await createTempDir();
92
+ const childDir = path.join(root, 'child');
93
+ const gov1 = path.join(childDir, 'governance-a');
94
+ const gov2 = path.join(childDir, 'governance-b');
95
+ await fs.mkdir(gov1, { recursive: true });
96
+ await fs.mkdir(gov2, { recursive: true });
97
+ await fs.writeFile(path.join(gov1, '.projitive'), '', 'utf-8');
98
+ await fs.writeFile(path.join(gov2, '.projitive'), '', 'utf-8');
99
+ await expect(resolveGovernanceDir(childDir)).rejects.toThrow('Multiple governance roots found');
100
+ });
101
+ it('prefers default .projitive directory when multiple governance roots found as children', async () => {
86
102
  const root = await createTempDir();
87
- const childDir = path.join(root, "child");
88
- const governance1 = path.join(childDir, ".projitive");
89
- const governance2 = path.join(childDir, "governance");
103
+ const childDir = path.join(root, 'child');
104
+ const governance1 = path.join(childDir, '.projitive');
105
+ const governance2 = path.join(childDir, 'governance');
90
106
  await fs.mkdir(governance1, { recursive: true });
91
107
  await fs.mkdir(governance2, { recursive: true });
92
- await fs.writeFile(path.join(governance1, ".projitive"), "", "utf-8");
93
- await fs.writeFile(path.join(governance2, ".projitive"), "", "utf-8");
108
+ await fs.writeFile(path.join(governance1, '.projitive'), '', 'utf-8');
109
+ await fs.writeFile(path.join(governance2, '.projitive'), '', 'utf-8');
94
110
  const resolved = await resolveGovernanceDir(childDir);
95
111
  expect(resolved).toBe(governance1); // Should prefer default .projitive
96
112
  });
97
- it("resolves file path by using its directory", async () => {
113
+ it('resolves file path by using its directory', async () => {
98
114
  const root = await createTempDir();
99
- const governanceDir = path.join(root, ".projitive");
100
- const filePath = path.join(governanceDir, "tasks.md");
115
+ const governanceDir = path.join(root, '.projitive');
116
+ const filePath = path.join(governanceDir, 'tasks.md');
101
117
  await fs.mkdir(governanceDir, { recursive: true });
102
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
103
- await fs.writeFile(filePath, "# Tasks", "utf-8");
118
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
119
+ await fs.writeFile(filePath, '# Tasks', 'utf-8');
104
120
  const resolved = await resolveGovernanceDir(filePath);
105
121
  expect(resolved).toBe(governanceDir);
106
122
  });
107
123
  });
108
- describe("discoverProjects", () => {
109
- it("discovers projects by marker file", async () => {
124
+ describe('discoverProjects', () => {
125
+ it('discovers projects by marker file', async () => {
110
126
  const root = await createTempDir();
111
- const p1 = path.join(root, "a");
112
- const p2 = path.join(root, "b", "c");
127
+ const p1 = path.join(root, 'a');
128
+ const p2 = path.join(root, 'b', 'c');
113
129
  await fs.mkdir(p1, { recursive: true });
114
130
  await fs.mkdir(p2, { recursive: true });
115
- await fs.writeFile(path.join(p1, ".projitive"), "", "utf-8");
116
- await fs.writeFile(path.join(p2, ".projitive"), "", "utf-8");
131
+ await fs.writeFile(path.join(p1, '.projitive'), '', 'utf-8');
132
+ await fs.writeFile(path.join(p2, '.projitive'), '', 'utf-8');
117
133
  const projects = await discoverProjects(root, 4);
118
134
  expect(projects).toContain(p1);
119
135
  expect(projects).toContain(p2);
120
136
  });
121
- it("discovers nested default governance directory under project root", async () => {
137
+ it('discovers nested default governance directory under project root', async () => {
122
138
  const root = await createTempDir();
123
- const projectRoot = path.join(root, "app");
124
- const governanceDir = path.join(projectRoot, ".projitive");
139
+ const projectRoot = path.join(root, 'app');
140
+ const governanceDir = path.join(projectRoot, '.projitive');
125
141
  await fs.mkdir(governanceDir, { recursive: true });
126
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
142
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
127
143
  const projects = await discoverProjects(root, 3);
128
144
  expect(projects).toContain(governanceDir);
129
145
  });
130
- it("discovers nested custom governance directory under project root", async () => {
146
+ it('discovers nested custom governance directory under project root', async () => {
131
147
  const root = await createTempDir();
132
- const projectRoot = path.join(root, "app");
133
- const governanceDir = path.join(projectRoot, "governance");
148
+ const projectRoot = path.join(root, 'app');
149
+ const governanceDir = path.join(projectRoot, 'governance');
134
150
  await fs.mkdir(governanceDir, { recursive: true });
135
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
151
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
136
152
  const projects = await discoverProjects(root, 3);
137
153
  expect(projects).toContain(governanceDir);
138
154
  });
139
- it("respects maxDepth limit", async () => {
155
+ it('respects maxDepth limit', async () => {
140
156
  const root = await createTempDir();
141
- const shallow = path.join(root, "shallow");
142
- const deep = path.join(root, "level1", "level2", "level3", "level4", "deep");
157
+ const shallow = path.join(root, 'shallow');
158
+ const deep = path.join(root, 'level1', 'level2', 'level3', 'level4', 'deep');
143
159
  await fs.mkdir(shallow, { recursive: true });
144
160
  await fs.mkdir(deep, { recursive: true });
145
- await fs.writeFile(path.join(shallow, ".projitive"), "", "utf-8");
146
- await fs.writeFile(path.join(deep, ".projitive"), "", "utf-8");
161
+ await fs.writeFile(path.join(shallow, '.projitive'), '', 'utf-8');
162
+ await fs.writeFile(path.join(deep, '.projitive'), '', 'utf-8');
147
163
  const projects = await discoverProjects(root, 3);
148
164
  expect(projects).toContain(shallow);
149
165
  expect(projects).not.toContain(deep);
150
166
  });
151
- it("ignores common ignore directories", async () => {
167
+ it('ignores common ignore directories', async () => {
152
168
  const root = await createTempDir();
153
- const nodeModulesProject = path.join(root, "node_modules", "project");
154
- const gitProject = path.join(root, ".git", "project");
155
- const validProject = path.join(root, "valid");
169
+ const nodeModulesProject = path.join(root, 'node_modules', 'project');
170
+ const gitProject = path.join(root, '.git', 'project');
171
+ const validProject = path.join(root, 'valid');
156
172
  await fs.mkdir(nodeModulesProject, { recursive: true });
157
173
  await fs.mkdir(gitProject, { recursive: true });
158
174
  await fs.mkdir(validProject, { recursive: true });
159
- await fs.writeFile(path.join(nodeModulesProject, ".projitive"), "", "utf-8");
160
- await fs.writeFile(path.join(gitProject, ".projitive"), "", "utf-8");
161
- await fs.writeFile(path.join(validProject, ".projitive"), "", "utf-8");
175
+ await fs.writeFile(path.join(nodeModulesProject, '.projitive'), '', 'utf-8');
176
+ await fs.writeFile(path.join(gitProject, '.projitive'), '', 'utf-8');
177
+ await fs.writeFile(path.join(validProject, '.projitive'), '', 'utf-8');
162
178
  const projects = await discoverProjects(root, 3);
163
179
  expect(projects).toContain(validProject);
164
180
  expect(projects).not.toContain(nodeModulesProject);
165
181
  expect(projects).not.toContain(gitProject);
166
182
  });
167
- it("returns empty array when no projects found", async () => {
183
+ it('returns empty array when no projects found', async () => {
168
184
  const root = await createTempDir();
169
185
  const projects = await discoverProjects(root, 3);
170
186
  expect(projects).toEqual([]);
171
187
  });
172
- it("returns unique and sorted results", async () => {
188
+ it('returns unique and sorted results', async () => {
173
189
  const root = await createTempDir();
174
- const projectB = path.join(root, "b");
175
- const projectA = path.join(root, "a");
190
+ const projectB = path.join(root, 'b');
191
+ const projectA = path.join(root, 'a');
176
192
  await fs.mkdir(projectB, { recursive: true });
177
193
  await fs.mkdir(projectA, { recursive: true });
178
- await fs.writeFile(path.join(projectB, ".projitive"), "", "utf-8");
179
- await fs.writeFile(path.join(projectA, ".projitive"), "", "utf-8");
194
+ await fs.writeFile(path.join(projectB, '.projitive'), '', 'utf-8');
195
+ await fs.writeFile(path.join(projectA, '.projitive'), '', 'utf-8');
180
196
  const projects = await discoverProjects(root, 3);
181
197
  expect(projects).toEqual([projectA, projectB]);
182
198
  });
183
- it("handles fs.readdir errors gracefully", async () => {
199
+ it('handles fs.readdir errors gracefully', async () => {
184
200
  const root = await createTempDir();
185
- vi.spyOn(fs, "readdir").mockRejectedValueOnce(new Error("Permission denied"));
201
+ vi.spyOn(fs, 'readdir').mockRejectedValueOnce(new Error('Permission denied'));
186
202
  const projects = await discoverProjects(root, 3);
187
203
  expect(projects).toEqual([]);
188
204
  });
189
- it("ignores non-existent roots when scanning across multiple roots", async () => {
205
+ it('ignores non-existent roots when scanning across multiple roots', async () => {
190
206
  const validRoot = await createTempDir();
191
- const validProject = path.join(validRoot, "project-a");
192
- const missingRoot = path.join(validRoot, "__missing_root__");
207
+ const validProject = path.join(validRoot, 'project-a');
208
+ const missingRoot = path.join(validRoot, '__missing_root__');
193
209
  await fs.mkdir(validProject, { recursive: true });
194
- await fs.writeFile(path.join(validProject, ".projitive"), "", "utf-8");
210
+ await fs.writeFile(path.join(validProject, '.projitive'), '', 'utf-8');
195
211
  const projects = await discoverProjectsAcrossRoots([missingRoot, validRoot], 3);
196
212
  expect(projects).toContain(validProject);
197
213
  });
198
214
  });
199
- describe("initializeProjectStructure", () => {
200
- it("initializes governance structure under default .projitive directory", async () => {
215
+ describe('initializeProjectStructure', () => {
216
+ it('initializes governance structure under default .projitive directory', async () => {
201
217
  const root = await createTempDir();
202
218
  const initialized = await initializeProjectStructure(root);
203
- expect(initialized.governanceDir).toBe(path.join(root, ".projitive"));
219
+ expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
204
220
  const expectedPaths = [
205
- path.join(root, ".projitive", ".projitive"),
206
- path.join(root, ".projitive", "README.md"),
207
- path.join(root, ".projitive", "roadmap.md"),
208
- path.join(root, ".projitive", "tasks.md"),
209
- path.join(root, ".projitive", "templates", "README.md"),
210
- path.join(root, ".projitive", "templates", "tools", "taskNext.md"),
211
- path.join(root, ".projitive", "templates", "tools", "taskUpdate.md"),
212
- path.join(root, ".projitive", "designs"),
213
- path.join(root, ".projitive", "reports"),
214
- path.join(root, ".projitive", "templates"),
215
- path.join(root, ".projitive", "templates", "tools"),
221
+ path.join(root, '.projitive', '.projitive'),
222
+ path.join(root, '.projitive', 'README.md'),
223
+ path.join(root, '.projitive', 'roadmap.md'),
224
+ path.join(root, '.projitive', 'tasks.md'),
225
+ path.join(root, '.projitive', 'templates', 'README.md'),
226
+ path.join(root, '.projitive', 'templates', 'tools', 'taskNext.md'),
227
+ path.join(root, '.projitive', 'templates', 'tools', 'taskUpdate.md'),
228
+ path.join(root, '.projitive', 'designs'),
229
+ path.join(root, '.projitive', 'reports'),
230
+ path.join(root, '.projitive', 'templates'),
231
+ path.join(root, '.projitive', 'templates', 'tools'),
216
232
  ];
217
233
  await Promise.all(expectedPaths.map(async (targetPath) => {
218
234
  await expect(fs.access(targetPath)).resolves.toBeUndefined();
219
235
  }));
220
236
  });
221
- it("overwrites template files when force is enabled", async () => {
237
+ it('overwrites template files when force is enabled', async () => {
222
238
  const root = await createTempDir();
223
- const governanceDir = path.join(root, ".projitive");
224
- const readmePath = path.join(governanceDir, "README.md");
239
+ const governanceDir = path.join(root, '.projitive');
240
+ const readmePath = path.join(governanceDir, 'README.md');
225
241
  await initializeProjectStructure(root);
226
- await fs.writeFile(readmePath, "custom-content", "utf-8");
227
- const initialized = await initializeProjectStructure(root, ".projitive", true);
228
- const readmeContent = await fs.readFile(readmePath, "utf-8");
229
- expect(readmeContent).toContain("Projitive Governance Workspace");
230
- expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe("updated");
242
+ await fs.writeFile(readmePath, 'custom-content', 'utf-8');
243
+ const initialized = await initializeProjectStructure(root, '.projitive', true);
244
+ const readmeContent = await fs.readFile(readmePath, 'utf-8');
245
+ expect(readmeContent).toContain('Projitive Governance Workspace');
246
+ expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe('updated');
231
247
  });
232
- it("uses custom governance directory when specified", async () => {
248
+ it('uses custom governance directory when specified', async () => {
233
249
  const root = await createTempDir();
234
- const customDir = "my-governance";
250
+ const customDir = 'my-governance';
235
251
  const initialized = await initializeProjectStructure(root, customDir);
236
252
  expect(initialized.governanceDir).toBe(path.join(root, customDir));
237
253
  });
238
- it("throws error when project path not found", async () => {
254
+ it('throws error when project path not found', async () => {
239
255
  const root = await createTempDir();
240
- const nonExistentPath = path.join(root, "nonexistent");
241
- await expect(initializeProjectStructure(nonExistentPath)).rejects.toThrow("Path not found");
256
+ const nonExistentPath = path.join(root, 'nonexistent');
257
+ await expect(initializeProjectStructure(nonExistentPath)).rejects.toThrow('Path not found');
242
258
  });
243
- it("throws error when project path is not a directory", async () => {
259
+ it('throws error when project path is not a directory', async () => {
244
260
  const root = await createTempDir();
245
- const filePath = path.join(root, "file.txt");
246
- await fs.writeFile(filePath, "content", "utf-8");
247
- await expect(initializeProjectStructure(filePath)).rejects.toThrow("projectPath must be a directory");
261
+ const filePath = path.join(root, 'file.txt');
262
+ await fs.writeFile(filePath, 'content', 'utf-8');
263
+ await expect(initializeProjectStructure(filePath)).rejects.toThrow('projectPath must be a directory');
248
264
  });
249
- it("creates governance structure with default name when invalid names are provided", async () => {
265
+ it('uses default governance dir when governanceDir is omitted', async () => {
250
266
  const root = await createTempDir();
251
- // When governanceDir is invalid, it should fall back to default
252
- // Note: normalizeGovernanceDirName is not exported, so we test initialization behavior
253
267
  const initialized = await initializeProjectStructure(root);
254
- expect(initialized.governanceDir).toBe(path.join(root, ".projitive"));
268
+ expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
255
269
  });
256
- it("skips existing files when force is disabled", async () => {
270
+ it('skips existing files when force is disabled', async () => {
257
271
  const root = await createTempDir();
258
- const governanceDir = path.join(root, ".projitive");
259
- const readmePath = path.join(governanceDir, "README.md");
272
+ const governanceDir = path.join(root, '.projitive');
273
+ const readmePath = path.join(governanceDir, 'README.md');
260
274
  await initializeProjectStructure(root);
261
- await fs.writeFile(readmePath, "custom-content", "utf-8");
262
- const initialized = await initializeProjectStructure(root, ".projitive", false);
263
- const readmeContent = await fs.readFile(readmePath, "utf-8");
264
- expect(readmeContent).toBe("custom-content");
265
- expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe("skipped");
275
+ await fs.writeFile(readmePath, 'custom-content', 'utf-8');
276
+ const initialized = await initializeProjectStructure(root, '.projitive', false);
277
+ const readmeContent = await fs.readFile(readmePath, 'utf-8');
278
+ expect(readmeContent).toBe('custom-content');
279
+ expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe('skipped');
266
280
  });
267
- it("creates all required subdirectories", async () => {
281
+ it('creates all required subdirectories', async () => {
268
282
  const root = await createTempDir();
269
283
  const initialized = await initializeProjectStructure(root);
270
- expect(initialized.directories.some(d => d.path.includes("designs"))).toBe(true);
271
- expect(initialized.directories.some(d => d.path.includes("reports"))).toBe(true);
272
- expect(initialized.directories.some(d => d.path.includes("templates"))).toBe(true);
284
+ expect(initialized.directories.some(d => d.path.includes('designs'))).toBe(true);
285
+ expect(initialized.directories.some(d => d.path.includes('reports'))).toBe(true);
286
+ expect(initialized.directories.some(d => d.path.includes('templates'))).toBe(true);
287
+ });
288
+ it('throws when governanceDir is an absolute path', async () => {
289
+ const root = await createTempDir();
290
+ await expect(initializeProjectStructure(root, '/absolute/path')).rejects.toThrow('relative directory name');
291
+ });
292
+ it('throws when governanceDir contains path separators', async () => {
293
+ const root = await createTempDir();
294
+ await expect(initializeProjectStructure(root, 'path/with/slash')).rejects.toThrow('path separators');
295
+ });
296
+ it('throws when governanceDir is a dot or double-dot', async () => {
297
+ const root = await createTempDir();
298
+ await expect(initializeProjectStructure(root, '.')).rejects.toThrow('normal directory name');
299
+ await expect(initializeProjectStructure(root, '..')).rejects.toThrow('normal directory name');
273
300
  });
274
301
  });
275
- describe("utility functions", () => {
276
- describe("toProjectPath", () => {
277
- it("returns parent directory of governance dir", () => {
278
- expect(toProjectPath("/path/to/project/.projitive")).toBe("/path/to/project");
279
- expect(toProjectPath("/a/b/c")).toBe("/a/b");
302
+ describe('utility functions', () => {
303
+ describe('toProjectPath', () => {
304
+ it('returns parent directory of governance dir', () => {
305
+ expect(toProjectPath('/path/to/project/.projitive')).toBe('/path/to/project');
306
+ expect(toProjectPath('/a/b/c')).toBe('/a/b');
280
307
  });
281
308
  });
282
- describe("resolveScanRoots", () => {
283
- it("uses legacy environment variable when no multi-root env is provided", () => {
284
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
285
- expect(resolveScanRoots()).toEqual(["/test/root"]);
309
+ describe('resolveScanRoots', () => {
310
+ it('uses legacy environment variable when no multi-root env is provided', () => {
311
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
312
+ expect(resolveScanRoots()).toEqual(['/test/root']);
286
313
  vi.unstubAllEnvs();
287
314
  });
288
- it("uses input paths when provided", () => {
289
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
290
- expect(resolveScanRoots(["/custom/path", " /custom/path ", "/second/path"])).toEqual(["/custom/path", "/second/path"]);
315
+ it('uses input paths when provided', () => {
316
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
317
+ expect(resolveScanRoots(['/custom/path', ' /custom/path ', '/second/path'])).toEqual(['/custom/path', '/second/path']);
291
318
  vi.unstubAllEnvs();
292
319
  });
293
- it("uses PROJITIVE_SCAN_ROOT_PATHS with platform delimiter", () => {
294
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", ["/root/a", "/root/b", "", " /root/c "].join(path.delimiter));
295
- expect(resolveScanRoots()).toEqual(["/root/a", "/root/b", "/root/c"]);
320
+ it('uses PROJITIVE_SCAN_ROOT_PATHS with platform delimiter', () => {
321
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', ['/root/a', '/root/b', '', ' /root/c '].join(path.delimiter));
322
+ expect(resolveScanRoots()).toEqual(['/root/a', '/root/b', '/root/c']);
296
323
  vi.unstubAllEnvs();
297
324
  });
298
- it("treats JSON-like string as plain delimiter input", () => {
299
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", JSON.stringify(["/json/a", "/json/b"]));
325
+ it('treats JSON-like string as plain delimiter input', () => {
326
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', JSON.stringify(['/json/a', '/json/b']));
300
327
  expect(resolveScanRoots()).toHaveLength(1);
301
328
  vi.unstubAllEnvs();
302
329
  });
303
- it("throws error when no root environment variables are configured", () => {
330
+ it('throws error when no root environment variables are configured', () => {
304
331
  vi.unstubAllEnvs();
305
- expect(() => resolveScanRoots()).toThrow("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS");
332
+ expect(() => resolveScanRoots()).toThrow('Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS');
306
333
  });
307
334
  });
308
- describe("resolveScanDepth", () => {
309
- it("uses environment variable when no input depth", () => {
310
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
311
- vi.stubEnv("PROJITIVE_SCAN_MAX_DEPTH", "5");
335
+ describe('resolveScanDepth', () => {
336
+ it('uses environment variable when no input depth', () => {
337
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
338
+ vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '5');
312
339
  expect(resolveScanDepth()).toBe(5);
313
340
  vi.unstubAllEnvs();
314
341
  });
315
- it("uses input depth when provided", () => {
316
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
317
- vi.stubEnv("PROJITIVE_SCAN_MAX_DEPTH", "5");
342
+ it('uses input depth when provided', () => {
343
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
344
+ vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '5');
318
345
  expect(resolveScanDepth(3)).toBe(3);
319
346
  vi.unstubAllEnvs();
320
347
  });
321
- it("clamps depth to MAX_SCAN_DEPTH", () => {
322
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
323
- vi.stubEnv("PROJITIVE_SCAN_MAX_DEPTH", "10");
348
+ it('clamps depth to MAX_SCAN_DEPTH', () => {
349
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
350
+ vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '10');
324
351
  expect(resolveScanDepth()).toBe(8);
325
352
  vi.unstubAllEnvs();
326
353
  });
327
- it("throws error for invalid depth configuration", () => {
328
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
329
- vi.stubEnv("PROJITIVE_SCAN_MAX_DEPTH", "not-a-number");
330
- expect(() => resolveScanDepth()).toThrow("Invalid PROJITIVE_SCAN_MAX_DEPTH");
354
+ it('throws error for invalid depth configuration', () => {
355
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
356
+ vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', 'not-a-number');
357
+ expect(() => resolveScanDepth()).toThrow('Invalid PROJITIVE_SCAN_MAX_DEPTH');
358
+ vi.unstubAllEnvs();
359
+ });
360
+ it('throws when PROJITIVE_SCAN_MAX_DEPTH env var is missing', () => {
361
+ vi.unstubAllEnvs();
362
+ expect(() => resolveScanDepth()).toThrow('Missing required environment variable: PROJITIVE_SCAN_MAX_DEPTH');
363
+ });
364
+ });
365
+ describe('resolveScanRoot', () => {
366
+ it('returns first scan root from env', () => {
367
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
368
+ expect(resolveScanRoot()).toBe('/test/root');
369
+ vi.unstubAllEnvs();
370
+ });
371
+ it('returns normalized path when inputPath is provided', () => {
372
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/fallback');
373
+ expect(resolveScanRoot('/custom/path')).toBe('/custom/path');
331
374
  vi.unstubAllEnvs();
332
375
  });
333
376
  });
334
377
  });
335
- describe("registerProjectTools", () => {
336
- it("registers project tools without throwing", () => {
378
+ describe('registerProjectTools', () => {
379
+ it('registers project tools without throwing', () => {
337
380
  const mockServer = {
338
381
  registerTool: vi.fn(),
339
382
  };
340
383
  expect(() => registerProjectTools(mockServer)).not.toThrow();
341
384
  expect(mockServer.registerTool).toHaveBeenCalled();
342
385
  });
343
- it("projectScan lists project root paths instead of governance directories", async () => {
386
+ it('projectScan lists project root paths instead of governance directories', async () => {
344
387
  const root = await createTempDir();
345
- const projectRoot = path.join(root, "app");
346
- const governanceDir = path.join(projectRoot, ".projitive");
388
+ const projectRoot = path.join(root, 'app');
389
+ const governanceDir = path.join(projectRoot, '.projitive');
347
390
  const templateDir = await createTempDir();
348
391
  await fs.mkdir(governanceDir, { recursive: true });
349
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
350
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", root);
351
- vi.stubEnv("PROJITIVE_SCAN_MAX_DEPTH", "3");
352
- vi.stubEnv("PROJITIVE_MESSAGE_TEMPLATE_PATH", templateDir);
392
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
393
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', root);
394
+ vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '3');
395
+ vi.stubEnv('PROJITIVE_MESSAGE_TEMPLATE_PATH', templateDir);
353
396
  const mockServer = {
354
397
  registerTool: vi.fn(),
355
398
  };
356
399
  registerProjectTools(mockServer);
357
- const projectScanCall = mockServer.registerTool.mock.calls.find((call) => call[0] === "projectScan");
400
+ const projectScanCall = mockServer.registerTool.mock.calls.find((call) => call[0] === 'projectScan');
358
401
  expect(projectScanCall).toBeTruthy();
359
402
  const projectScanHandler = projectScanCall?.[2];
403
+ expect(projectScanHandler).toBeTruthy();
360
404
  const result = await projectScanHandler();
361
- const markdown = result.content[0]?.text ?? "";
405
+ const markdown = result.content[0]?.text ?? '';
362
406
  expect(markdown).toContain(`1. ${projectRoot}`);
363
407
  expect(markdown).not.toContain(`1. ${governanceDir}`);
364
408
  });
409
+ it('projectScan returns no-project guidance when scan root is empty', async () => {
410
+ const emptyRoot = await createTempDir();
411
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', emptyRoot);
412
+ vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '2');
413
+ const mockServer = { registerTool: vi.fn() };
414
+ registerProjectTools(mockServer);
415
+ const projectScan = getProjectToolHandler(mockServer, 'projectScan');
416
+ const result = await projectScan();
417
+ expect(result.isError).toBeUndefined();
418
+ expect(result.content[0].text).toContain('No governance root discovered');
419
+ });
420
+ it('projectNext ranks multiple actionable projects by score', async () => {
421
+ const scanRoot = await createTempDir();
422
+ const projectA = path.join(scanRoot, 'app-a');
423
+ const projectB = path.join(scanRoot, 'app-b');
424
+ await fs.mkdir(projectA, { recursive: true });
425
+ await fs.mkdir(projectB, { recursive: true });
426
+ await initializeProjectStructure(projectA);
427
+ await initializeProjectStructure(projectB);
428
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', scanRoot);
429
+ vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '3');
430
+ const mockServer = { registerTool: vi.fn() };
431
+ registerProjectTools(mockServer);
432
+ const projectNext = getProjectToolHandler(mockServer, 'projectNext');
433
+ const result = await projectNext({});
434
+ expect(result.isError).toBeUndefined();
435
+ expect(result.content[0].text).toContain('actionableProjects:');
436
+ });
437
+ it('projectInit handler initializes project structure', async () => {
438
+ const root = await createTempDir();
439
+ const mockServer = { registerTool: vi.fn() };
440
+ registerProjectTools(mockServer);
441
+ const projectInit = getProjectToolHandler(mockServer, 'projectInit');
442
+ const result = await projectInit({ projectPath: root });
443
+ expect(result.isError).toBeUndefined();
444
+ expect(result.content[0].text).toContain('governanceDir:');
445
+ expect(result.content[0].text).toContain('createdFiles:');
446
+ });
447
+ it('projectLocate resolves governance dir from any inner path', async () => {
448
+ const root = await createTempDir();
449
+ const projectRoot = path.join(root, 'myapp');
450
+ const governanceDir = path.join(projectRoot, '.projitive');
451
+ await fs.mkdir(governanceDir, { recursive: true });
452
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
453
+ const mockServer = { registerTool: vi.fn() };
454
+ registerProjectTools(mockServer);
455
+ const projectLocate = getProjectToolHandler(mockServer, 'projectLocate');
456
+ const result = await projectLocate({ inputPath: governanceDir });
457
+ expect(result.isError).toBeUndefined();
458
+ expect(result.content[0].text).toContain(`projectPath: ${projectRoot}`);
459
+ expect(result.content[0].text).toContain(`governanceDir: ${governanceDir}`);
460
+ });
461
+ it('projectNext ranks actionable projects by score', async () => {
462
+ const scanRoot = await createTempDir();
463
+ const projectRoot = path.join(scanRoot, 'myapp');
464
+ await fs.mkdir(projectRoot, { recursive: true });
465
+ await initializeProjectStructure(projectRoot);
466
+ vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', scanRoot);
467
+ vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '3');
468
+ const mockServer = { registerTool: vi.fn() };
469
+ registerProjectTools(mockServer);
470
+ const projectNext = getProjectToolHandler(mockServer, 'projectNext');
471
+ const result = await projectNext({});
472
+ expect(result.isError).toBeUndefined();
473
+ expect(result.content[0].text).toContain('actionableProjects:');
474
+ expect(result.content[0].text).toContain('myapp');
475
+ });
476
+ it('projectContext shows task stats and governance artifacts', async () => {
477
+ const root = await createTempDir();
478
+ await initializeProjectStructure(root);
479
+ const mockServer = { registerTool: vi.fn() };
480
+ registerProjectTools(mockServer);
481
+ const projectContext = getProjectToolHandler(mockServer, 'projectContext');
482
+ const result = await projectContext({ projectPath: root });
483
+ expect(result.isError).toBeUndefined();
484
+ expect(result.content[0].text).toContain('Task Summary');
485
+ expect(result.content[0].text).toContain('Artifacts');
486
+ });
487
+ it('syncViews materializes both tasks and roadmap markdown views', async () => {
488
+ const root = await createTempDir();
489
+ await initializeProjectStructure(root);
490
+ const mockServer = { registerTool: vi.fn() };
491
+ registerProjectTools(mockServer);
492
+ const syncViews = getProjectToolHandler(mockServer, 'syncViews');
493
+ const result = await syncViews({ projectPath: root, views: ['tasks', 'roadmap'], force: true });
494
+ expect(result.isError).toBeUndefined();
495
+ expect(result.content[0].text).toContain('tasks.md synced');
496
+ expect(result.content[0].text).toContain('roadmap.md synced');
497
+ });
365
498
  });
366
499
  });