@projitive/mcp 2.0.3 → 2.0.4

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 (40) 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/files.js +11 -11
  6. package/output/source/common/files.test.js +14 -14
  7. package/output/source/common/index.js +10 -10
  8. package/output/source/common/linter.js +27 -27
  9. package/output/source/common/linter.test.js +9 -9
  10. package/output/source/common/markdown.js +3 -3
  11. package/output/source/common/markdown.test.js +15 -15
  12. package/output/source/common/response.js +74 -74
  13. package/output/source/common/response.test.js +30 -30
  14. package/output/source/common/store.js +40 -40
  15. package/output/source/common/store.test.js +72 -72
  16. package/output/source/common/types.js +3 -3
  17. package/output/source/common/utils.js +8 -8
  18. package/output/source/index.js +16 -16
  19. package/output/source/index.test.js +64 -64
  20. package/output/source/prompts/index.js +3 -3
  21. package/output/source/prompts/quickStart.js +96 -96
  22. package/output/source/prompts/taskDiscovery.js +184 -184
  23. package/output/source/prompts/taskExecution.js +148 -148
  24. package/output/source/resources/designs.js +26 -26
  25. package/output/source/resources/designs.test.js +88 -88
  26. package/output/source/resources/governance.js +19 -19
  27. package/output/source/resources/index.js +2 -2
  28. package/output/source/resources/readme.js +7 -7
  29. package/output/source/resources/readme.test.js +113 -113
  30. package/output/source/resources/reports.js +10 -10
  31. package/output/source/resources/reports.test.js +83 -83
  32. package/output/source/tools/index.js +3 -3
  33. package/output/source/tools/project.js +191 -191
  34. package/output/source/tools/project.test.js +174 -173
  35. package/output/source/tools/roadmap.js +110 -95
  36. package/output/source/tools/roadmap.test.js +54 -46
  37. package/output/source/tools/task.js +305 -277
  38. package/output/source/tools/task.test.js +117 -110
  39. package/output/source/types.js +22 -22
  40. package/package.json +8 -2
@@ -1,32 +1,31 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { z } from "zod";
4
- import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, ROADMAP_LINT_CODES, renderLintSuggestions, findTextReferences, ensureStore, loadRoadmapsFromStore, upsertRoadmapInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from "../common/index.js";
5
- import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
6
- import { resolveGovernanceDir, toProjectPath } from "./project.js";
7
- import { loadTasks } from "./task.js";
8
- export const ROADMAP_ID_REGEX = /^ROADMAP-\d{4}$/;
9
- export const ROADMAP_MARKDOWN_FILE = "roadmap.md";
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, ROADMAP_LINT_CODES, renderLintSuggestions, findTextReferences, ensureStore, loadRoadmapsFromStore, upsertRoadmapInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from '../common/index.js';
5
+ import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from '../common/index.js';
6
+ import { resolveGovernanceDir, toProjectPath } from './project.js';
7
+ import { loadTasks, loadTasksDocument } from './task.js';
8
+ export const ROADMAP_ID_REGEX = /^ROADMAP-(\d+)$/;
9
+ export const ROADMAP_MARKDOWN_FILE = 'roadmap.md';
10
10
  function nowIso() {
11
11
  return new Date().toISOString();
12
12
  }
13
13
  function nextRoadmapId(milestones) {
14
14
  const maxSuffix = milestones
15
15
  .map((item) => toRoadmapIdNumericSuffix(item.id))
16
- .filter((value) => Number.isFinite(value) && value >= 0)
16
+ .filter((value) => value > 0)
17
17
  .reduce((max, value) => Math.max(max, value), 0);
18
18
  const next = maxSuffix + 1;
19
- if (next > 9999) {
20
- throw new Error("ROADMAP ID overflow: maximum supported ID is ROADMAP-9999");
21
- }
22
- return `ROADMAP-${String(next).padStart(4, "0")}`;
19
+ const minWidth = Math.max(4, String(next).length);
20
+ return `ROADMAP-${String(next).padStart(minWidth, '0')}`;
23
21
  }
24
22
  function toRoadmapIdNumericSuffix(roadmapId) {
25
- const match = roadmapId.match(/^(?:ROADMAP-)(\d{4})$/);
23
+ const match = roadmapId.match(ROADMAP_ID_REGEX);
26
24
  if (!match) {
27
25
  return -1;
28
26
  }
29
- return Number.parseInt(match[1], 10);
27
+ const suffix = Number.parseInt(match[1], 10);
28
+ return Number.isFinite(suffix) ? suffix : -1;
30
29
  }
31
30
  function sortMilestonesNewestFirst(milestones) {
32
31
  return [...milestones].sort((a, b) => {
@@ -45,9 +44,9 @@ function normalizeMilestone(raw) {
45
44
  return {
46
45
  id: String(raw.id),
47
46
  title: String(raw.title),
48
- status: raw.status === "done" ? "done" : "active",
49
- time: typeof raw.time === "string" && raw.time.trim().length > 0 ? raw.time.trim() : undefined,
50
- updatedAt: typeof raw.updatedAt === "string" && Number.isFinite(new Date(raw.updatedAt).getTime()) ? raw.updatedAt : nowIso(),
47
+ status: raw.status === 'done' ? 'done' : 'active',
48
+ time: typeof raw.time === 'string' && raw.time.trim().length > 0 ? raw.time.trim() : undefined,
49
+ updatedAt: typeof raw.updatedAt === 'string' && Number.isFinite(new Date(raw.updatedAt).getTime()) ? raw.updatedAt : nowIso(),
51
50
  };
52
51
  }
53
52
  function normalizeAndSortMilestones(milestones) {
@@ -57,29 +56,33 @@ function normalizeAndSortMilestones(milestones) {
57
56
  }
58
57
  export function renderRoadmapMarkdown(milestones) {
59
58
  const lines = sortMilestonesNewestFirst(milestones).map((item) => {
60
- const checkbox = item.status === "done" ? "x" : " ";
61
- const timeText = item.time ? ` (time: ${item.time})` : "";
59
+ const checkbox = item.status === 'done' ? 'x' : ' ';
60
+ const timeText = item.time ? ` (time: ${item.time})` : '';
62
61
  return `- [${checkbox}] ${item.id}: ${item.title}${timeText}`;
63
62
  });
64
63
  return [
65
- "# Roadmap",
66
- "",
67
- "This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.",
68
- "",
69
- "## Active Milestones",
70
- ...(lines.length > 0 ? lines : ["- (no milestones)"]),
71
- "",
72
- ].join("\n");
64
+ '# Roadmap',
65
+ '',
66
+ 'Projitive is an AI-first project governance framework for tasks, roadmaps, reports, and designs.',
67
+ 'Author: yinxulai',
68
+ 'Repository: https://github.com/yinxulai/projitive',
69
+ 'Do not edit this file manually. This file is automatically generated by Projitive.',
70
+ 'This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.',
71
+ '',
72
+ '## Active Milestones',
73
+ ...(lines.length > 0 ? lines : ['- (no milestones)']),
74
+ '',
75
+ ].join('\n');
73
76
  }
74
77
  function resolveRoadmapArtifactPaths(governanceDir) {
75
78
  return {
76
- roadmapPath: path.join(governanceDir, ".projitive"),
79
+ roadmapPath: path.join(governanceDir, '.projitive'),
77
80
  markdownPath: path.join(governanceDir, ROADMAP_MARKDOWN_FILE),
78
81
  };
79
82
  }
80
83
  async function syncRoadmapMarkdownView(roadmapPath, markdownPath, markdown, force = false) {
81
- const sourceVersion = await getStoreVersion(roadmapPath, "roadmaps");
82
- const viewState = await getMarkdownViewState(roadmapPath, "roadmaps_markdown");
84
+ const sourceVersion = await getStoreVersion(roadmapPath, 'roadmaps');
85
+ const viewState = await getMarkdownViewState(roadmapPath, 'roadmaps_markdown');
83
86
  const markdownExists = await fs.access(markdownPath).then(() => true).catch(() => false);
84
87
  const shouldWrite = force
85
88
  || !markdownExists
@@ -88,8 +91,8 @@ async function syncRoadmapMarkdownView(roadmapPath, markdownPath, markdown, forc
88
91
  if (!shouldWrite) {
89
92
  return;
90
93
  }
91
- await fs.writeFile(markdownPath, markdown, "utf-8");
92
- await markMarkdownViewBuilt(roadmapPath, "roadmaps_markdown", sourceVersion);
94
+ await fs.writeFile(markdownPath, markdown, 'utf-8');
95
+ await markMarkdownViewBuilt(roadmapPath, 'roadmaps_markdown', sourceVersion);
93
96
  }
94
97
  export async function loadRoadmapDocument(inputPath) {
95
98
  return loadRoadmapDocumentWithOptions(inputPath, false);
@@ -118,15 +121,15 @@ function collectRoadmapLintSuggestionItems(roadmapIds, tasks) {
118
121
  if (roadmapIds.length === 0) {
119
122
  suggestions.push({
120
123
  code: ROADMAP_LINT_CODES.IDS_EMPTY,
121
- message: "No roadmap IDs found in .projitive roadmap table.",
122
- fixHint: "Add at least one ROADMAP-xxxx milestone.",
124
+ message: 'No roadmap IDs found in .projitive roadmap table.',
125
+ fixHint: 'Add at least one ROADMAP-xxxx milestone.',
123
126
  });
124
127
  }
125
128
  if (tasks.length === 0) {
126
129
  suggestions.push({
127
130
  code: ROADMAP_LINT_CODES.TASKS_EMPTY,
128
- message: "No tasks found in .projitive task table.",
129
- fixHint: "Add task cards and bind roadmapRefs for traceability.",
131
+ message: 'No tasks found in .projitive task table.',
132
+ fixHint: 'Add task cards and bind roadmapRefs for traceability.',
130
133
  });
131
134
  return suggestions;
132
135
  }
@@ -136,15 +139,15 @@ function collectRoadmapLintSuggestionItems(roadmapIds, tasks) {
136
139
  suggestions.push({
137
140
  code: ROADMAP_LINT_CODES.TASK_REFS_EMPTY,
138
141
  message: `${unboundTasks.length} task(s) have empty roadmapRefs.`,
139
- fixHint: "Bind ROADMAP-xxxx where applicable.",
142
+ fixHint: 'Bind ROADMAP-xxxx where applicable.',
140
143
  });
141
144
  }
142
145
  const unknownRefs = Array.from(new Set(tasks.flatMap((task) => task.roadmapRefs).filter((id) => !roadmapSet.has(id))));
143
146
  if (unknownRefs.length > 0) {
144
147
  suggestions.push({
145
148
  code: ROADMAP_LINT_CODES.UNKNOWN_REFS,
146
- message: `Unknown roadmapRefs detected: ${unknownRefs.join(", ")}.`,
147
- fixHint: "Add missing roadmap IDs or fix task references.",
149
+ message: `Unknown roadmapRefs detected: ${unknownRefs.join(', ')}.`,
150
+ fixHint: 'Add missing roadmap IDs or fix task references.',
148
151
  });
149
152
  }
150
153
  const noLinkedRoadmaps = roadmapIds.filter((id) => !tasks.some((task) => task.roadmapRefs.includes(id)));
@@ -152,7 +155,7 @@ function collectRoadmapLintSuggestionItems(roadmapIds, tasks) {
152
155
  suggestions.push({
153
156
  code: ROADMAP_LINT_CODES.ZERO_LINKED_TASKS,
154
157
  message: `${noLinkedRoadmaps.length} roadmap ID(s) have zero linked tasks.`,
155
- fixHint: `Consider binding tasks to: ${noLinkedRoadmaps.slice(0, 3).join(", ")}${noLinkedRoadmaps.length > 3 ? ", ..." : ""}.`,
158
+ fixHint: `Consider binding tasks to: ${noLinkedRoadmaps.slice(0, 3).join(', ')}${noLinkedRoadmaps.length > 3 ? ', ...' : ''}.`,
156
159
  });
157
160
  }
158
161
  return suggestions;
@@ -161,48 +164,51 @@ export function collectRoadmapLintSuggestions(roadmapIds, tasks) {
161
164
  return renderLintSuggestions(collectRoadmapLintSuggestionItems(roadmapIds, tasks));
162
165
  }
163
166
  export function isValidRoadmapId(id) {
164
- return ROADMAP_ID_REGEX.test(id);
167
+ return toRoadmapIdNumericSuffix(id) > 0;
165
168
  }
166
169
  export function registerRoadmapTools(server) {
167
- server.registerTool("roadmapList", {
168
- title: "Roadmap List",
169
- description: "List roadmap IDs and task linkage for planning or traceability",
170
+ server.registerTool('roadmapList', {
171
+ title: 'Roadmap List',
172
+ description: 'List roadmap IDs and task linkage for planning or traceability',
170
173
  inputSchema: {
171
174
  projectPath: z.string(),
172
175
  },
173
176
  }, async ({ projectPath }) => {
174
177
  const governanceDir = await resolveGovernanceDir(projectPath);
175
178
  const normalizedProjectPath = toProjectPath(governanceDir);
176
- const roadmapIds = await loadRoadmapIds(governanceDir);
177
- const { tasks } = await loadTasks(governanceDir);
179
+ const { milestones, markdownPath: roadmapViewPath } = await loadRoadmapDocument(governanceDir);
180
+ const roadmapIds = milestones.map((item) => item.id);
181
+ const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
178
182
  const lintSuggestions = collectRoadmapLintSuggestions(roadmapIds, tasks);
179
183
  const markdown = renderToolResponseMarkdown({
180
- toolName: "roadmapList",
184
+ toolName: 'roadmapList',
181
185
  sections: [
182
186
  summarySection([
183
187
  `- projectPath: ${normalizedProjectPath}`,
184
188
  `- governanceDir: ${governanceDir}`,
189
+ `- tasksView: ${tasksViewPath}`,
190
+ `- roadmapView: ${roadmapViewPath}`,
185
191
  `- roadmapCount: ${roadmapIds.length}`,
186
192
  ]),
187
193
  evidenceSection([
188
- "- roadmaps:",
194
+ '- roadmaps:',
189
195
  ...roadmapIds.map((id) => {
190
196
  const linkedTasks = tasks.filter((task) => task.roadmapRefs.includes(id));
191
197
  return `- ${id} | linkedTasks=${linkedTasks.length}`;
192
198
  }),
193
199
  ]),
194
- guidanceSection(["- Pick one roadmap ID and call `roadmapContext`."]),
200
+ guidanceSection(['- Pick one roadmap ID and call `roadmapContext`.']),
195
201
  lintSection(lintSuggestions),
196
202
  nextCallSection(roadmapIds[0]
197
- ? `roadmapContext(projectPath=\"${toProjectPath(governanceDir)}\", roadmapId=\"${roadmapIds[0]}\")`
203
+ ? `roadmapContext(projectPath="${toProjectPath(governanceDir)}", roadmapId="${roadmapIds[0]}")`
198
204
  : undefined),
199
205
  ],
200
206
  });
201
207
  return asText(markdown);
202
208
  });
203
- server.registerTool("roadmapContext", {
204
- title: "Roadmap Context",
205
- description: "Inspect one roadmap with linked tasks and reference locations",
209
+ server.registerTool('roadmapContext', {
210
+ title: 'Roadmap Context',
211
+ description: 'Inspect one roadmap with linked tasks and reference locations',
206
212
  inputSchema: {
207
213
  projectPath: z.string(),
208
214
  roadmapId: z.string(),
@@ -210,16 +216,17 @@ export function registerRoadmapTools(server) {
210
216
  }, async ({ projectPath, roadmapId }) => {
211
217
  if (!isValidRoadmapId(roadmapId)) {
212
218
  return {
213
- ...asText(renderErrorMarkdown("roadmapContext", `Invalid roadmap ID format: ${roadmapId}`, ["expected format: ROADMAP-0001", "retry with a valid roadmap ID"], `roadmapContext(projectPath=\"${projectPath}\", roadmapId=\"ROADMAP-0001\")`)),
219
+ ...asText(renderErrorMarkdown('roadmapContext', `Invalid roadmap ID format: ${roadmapId}`, ['expected format: ROADMAP-1 or ROADMAP-0001', 'retry with a valid roadmap ID'], `roadmapContext(projectPath="${projectPath}", roadmapId="ROADMAP-0001")`)),
214
220
  isError: true,
215
221
  };
216
222
  }
217
223
  const governanceDir = await resolveGovernanceDir(projectPath);
218
224
  const normalizedProjectPath = toProjectPath(governanceDir);
225
+ const { markdownPath: roadmapViewPath } = resolveRoadmapArtifactPaths(governanceDir);
219
226
  const artifacts = await discoverGovernanceArtifacts(governanceDir);
220
227
  const fileCandidates = candidateFilesFromArtifacts(artifacts);
221
228
  const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, roadmapId)))).flat();
222
- const { tasks } = await loadTasks(governanceDir);
229
+ const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
223
230
  const relatedTasks = tasks.filter((task) => task.roadmapRefs.includes(roadmapId));
224
231
  const roadmapIds = await loadRoadmapIds(governanceDir);
225
232
  const lintSuggestionItems = collectRoadmapLintSuggestionItems(roadmapIds, tasks);
@@ -227,70 +234,73 @@ export function registerRoadmapTools(server) {
227
234
  lintSuggestionItems.push({
228
235
  code: ROADMAP_LINT_CODES.CONTEXT_RELATED_TASKS_EMPTY,
229
236
  message: `relatedTasks=0 for ${roadmapId}.`,
230
- fixHint: "Batch bind task roadmapRefs to improve execution traceability.",
237
+ fixHint: 'Batch bind task roadmapRefs to improve execution traceability.',
231
238
  });
232
239
  }
233
240
  const lintSuggestions = renderLintSuggestions(lintSuggestionItems);
234
241
  const markdown = renderToolResponseMarkdown({
235
- toolName: "roadmapContext",
242
+ toolName: 'roadmapContext',
236
243
  sections: [
237
244
  summarySection([
238
245
  `- projectPath: ${normalizedProjectPath}`,
239
246
  `- governanceDir: ${governanceDir}`,
247
+ `- tasksView: ${tasksViewPath}`,
248
+ `- roadmapView: ${roadmapViewPath}`,
240
249
  `- roadmapId: ${roadmapId}`,
241
250
  `- relatedTasks: ${relatedTasks.length}`,
242
251
  `- references: ${referenceLocations.length}`,
243
252
  ]),
244
253
  evidenceSection([
245
- "### Related Tasks",
254
+ '### Related Tasks',
246
255
  ...relatedTasks.map((task) => `- ${task.id} | ${task.status} | ${task.title}`),
247
- "",
248
- "### Reference Locations",
256
+ '',
257
+ '### Reference Locations',
249
258
  ...referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`),
250
259
  ]),
251
260
  guidanceSection([
252
- "- Read roadmap references first, then related tasks.",
253
- "- Keep ROADMAP/TASK IDs unchanged while updating markdown files.",
254
- "- Re-run `roadmapContext` after edits to confirm references remain consistent.",
261
+ '- Read roadmap references first, then related tasks.',
262
+ '- Keep ROADMAP/TASK IDs unchanged while updating markdown files.',
263
+ '- Re-run `roadmapContext` after edits to confirm references remain consistent.',
255
264
  ]),
256
265
  lintSection(lintSuggestions),
257
- nextCallSection(`roadmapContext(projectPath=\"${toProjectPath(governanceDir)}\", roadmapId=\"${roadmapId}\")`),
266
+ nextCallSection(`roadmapContext(projectPath="${toProjectPath(governanceDir)}", roadmapId="${roadmapId}")`),
258
267
  ],
259
268
  });
260
269
  return asText(markdown);
261
270
  });
262
- server.registerTool("roadmapCreate", {
263
- title: "Roadmap Create",
264
- description: "Create one roadmap milestone in governance store",
271
+ server.registerTool('roadmapCreate', {
272
+ title: 'Roadmap Create',
273
+ description: 'Create one roadmap milestone in governance store',
265
274
  inputSchema: {
266
275
  projectPath: z.string(),
267
276
  roadmapId: z.string().optional(),
268
277
  title: z.string(),
269
- status: z.enum(["active", "done"]).optional(),
278
+ status: z.enum(['active', 'done']).optional(),
270
279
  time: z.string().optional(),
271
280
  },
272
281
  }, async ({ projectPath, roadmapId, title, status, time }) => {
273
282
  if (roadmapId && !isValidRoadmapId(roadmapId)) {
274
283
  return {
275
- ...asText(renderErrorMarkdown("roadmapCreate", `Invalid roadmap ID format: ${roadmapId}`, ["expected format: ROADMAP-0001", "omit roadmapId to auto-generate next ID"], `roadmapCreate(projectPath=\"${projectPath}\", title=\"Define milestone\", time=\"2026-Q2\")`)),
284
+ ...asText(renderErrorMarkdown('roadmapCreate', `Invalid roadmap ID format: ${roadmapId}`, ['expected format: ROADMAP-1 or ROADMAP-0001', 'omit roadmapId to auto-generate next ID'], `roadmapCreate(projectPath="${projectPath}", title="Define milestone", time="2026-Q2")`)),
276
285
  isError: true,
277
286
  };
278
287
  }
279
288
  const governanceDir = await resolveGovernanceDir(projectPath);
280
289
  const normalizedProjectPath = toProjectPath(governanceDir);
281
290
  const doc = await loadRoadmapDocument(governanceDir);
291
+ const { markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
282
292
  const finalRoadmapId = roadmapId ?? nextRoadmapId(doc.milestones);
283
293
  const duplicated = doc.milestones.some((item) => item.id === finalRoadmapId);
284
294
  if (duplicated) {
285
295
  return {
286
- ...asText(renderErrorMarkdown("roadmapCreate", `Roadmap milestone already exists: ${finalRoadmapId}`, ["roadmap IDs must be unique", "use roadmapUpdate for existing milestone"], `roadmapUpdate(projectPath=\"${normalizedProjectPath}\", roadmapId=\"${finalRoadmapId}\", updates={...})`)),
296
+ ...asText(renderErrorMarkdown('roadmapCreate', `Roadmap milestone already exists: ${finalRoadmapId}`, ['roadmap IDs must be unique', 'use roadmapUpdate for existing milestone'], `roadmapUpdate(projectPath="${normalizedProjectPath}", roadmapId="${finalRoadmapId}", updates={...})`)),
287
297
  isError: true,
288
298
  };
289
299
  }
290
300
  const created = normalizeMilestone({
291
301
  id: finalRoadmapId,
292
302
  title,
293
- status: status ?? "active",
303
+ status: status ?? 'active',
294
304
  time,
295
305
  updatedAt: nowIso(),
296
306
  });
@@ -299,58 +309,61 @@ export function registerRoadmapTools(server) {
299
309
  const { tasks } = await loadTasks(governanceDir);
300
310
  const lintSuggestions = collectRoadmapLintSuggestions(refreshed.milestones.map((item) => item.id), tasks);
301
311
  const markdown = renderToolResponseMarkdown({
302
- toolName: "roadmapCreate",
312
+ toolName: 'roadmapCreate',
303
313
  sections: [
304
314
  summarySection([
305
315
  `- projectPath: ${normalizedProjectPath}`,
306
316
  `- governanceDir: ${governanceDir}`,
317
+ `- tasksView: ${tasksViewPath}`,
318
+ `- roadmapView: ${refreshed.markdownPath}`,
307
319
  `- roadmapId: ${created.id}`,
308
320
  `- status: ${created.status}`,
309
321
  `- updatedAt: ${created.updatedAt}`,
310
322
  ]),
311
323
  evidenceSection([
312
- "### Created Milestone",
313
- `- ${created.id} | ${created.status} | ${created.title}${created.time ? ` | time=${created.time}` : ""}`,
314
- "",
315
- "### Roadmap Count",
324
+ '### Created Milestone',
325
+ `- ${created.id} | ${created.status} | ${created.title}${created.time ? ` | time=${created.time}` : ''}`,
326
+ '',
327
+ '### Roadmap Count',
316
328
  `- total: ${refreshed.milestones.length}`,
317
329
  ]),
318
330
  guidanceSection([
319
- "Milestone created successfully and roadmap.md has been synced.",
320
- "Re-run roadmapContext to verify linked task traceability.",
331
+ 'Milestone created successfully and roadmap.md has been synced.',
332
+ 'Re-run roadmapContext to verify linked task traceability.',
321
333
  ]),
322
334
  lintSection(lintSuggestions),
323
- nextCallSection(`roadmapContext(projectPath=\"${normalizedProjectPath}\", roadmapId=\"${created.id}\")`),
335
+ nextCallSection(`roadmapContext(projectPath="${normalizedProjectPath}", roadmapId="${created.id}")`),
324
336
  ],
325
337
  });
326
338
  return asText(markdown);
327
339
  });
328
- server.registerTool("roadmapUpdate", {
329
- title: "Roadmap Update",
330
- description: "Update one roadmap milestone fields incrementally in governance store",
340
+ server.registerTool('roadmapUpdate', {
341
+ title: 'Roadmap Update',
342
+ description: 'Update one roadmap milestone fields incrementally in governance store',
331
343
  inputSchema: {
332
344
  projectPath: z.string(),
333
345
  roadmapId: z.string(),
334
346
  updates: z.object({
335
347
  title: z.string().optional(),
336
- status: z.enum(["active", "done"]).optional(),
348
+ status: z.enum(['active', 'done']).optional(),
337
349
  time: z.string().optional(),
338
350
  }),
339
351
  },
340
352
  }, async ({ projectPath, roadmapId, updates }) => {
341
353
  if (!isValidRoadmapId(roadmapId)) {
342
354
  return {
343
- ...asText(renderErrorMarkdown("roadmapUpdate", `Invalid roadmap ID format: ${roadmapId}`, ["expected format: ROADMAP-0001", "retry with a valid roadmap ID"], `roadmapUpdate(projectPath="${projectPath}", roadmapId="ROADMAP-0001", updates={...})`)),
355
+ ...asText(renderErrorMarkdown('roadmapUpdate', `Invalid roadmap ID format: ${roadmapId}`, ['expected format: ROADMAP-1 or ROADMAP-0001', 'retry with a valid roadmap ID'], `roadmapUpdate(projectPath="${projectPath}", roadmapId="ROADMAP-0001", updates={...})`)),
344
356
  isError: true,
345
357
  };
346
358
  }
347
359
  const governanceDir = await resolveGovernanceDir(projectPath);
348
360
  const normalizedProjectPath = toProjectPath(governanceDir);
349
361
  const doc = await loadRoadmapDocument(governanceDir);
362
+ const { markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
350
363
  const existing = doc.milestones.find((item) => item.id === roadmapId);
351
364
  if (!existing) {
352
365
  return {
353
- ...asText(renderErrorMarkdown("roadmapUpdate", `Roadmap milestone not found: ${roadmapId}`, ["run roadmapList to discover existing roadmap IDs", "retry with an existing roadmap ID"], `roadmapList(projectPath="${toProjectPath(governanceDir)}")`)),
366
+ ...asText(renderErrorMarkdown('roadmapUpdate', `Roadmap milestone not found: ${roadmapId}`, ['run roadmapList to discover existing roadmap IDs', 'retry with an existing roadmap ID'], `roadmapList(projectPath="${toProjectPath(governanceDir)}")`)),
354
367
  isError: true,
355
368
  };
356
369
  }
@@ -364,26 +377,28 @@ export function registerRoadmapTools(server) {
364
377
  await upsertRoadmapInStore(doc.roadmapPath, updated);
365
378
  const refreshed = await loadRoadmapDocumentWithOptions(governanceDir, true);
366
379
  const markdown = renderToolResponseMarkdown({
367
- toolName: "roadmapUpdate",
380
+ toolName: 'roadmapUpdate',
368
381
  sections: [
369
382
  summarySection([
370
383
  `- projectPath: ${normalizedProjectPath}`,
371
384
  `- governanceDir: ${governanceDir}`,
385
+ `- tasksView: ${tasksViewPath}`,
386
+ `- roadmapView: ${refreshed.markdownPath}`,
372
387
  `- roadmapId: ${roadmapId}`,
373
388
  `- newStatus: ${updated.status}`,
374
389
  `- updatedAt: ${updated.updatedAt}`,
375
390
  ]),
376
391
  evidenceSection([
377
- "### Updated Milestone",
378
- `- ${updated.id} | ${updated.status} | ${updated.title}${updated.time ? ` | time=${updated.time}` : ""}`,
379
- "",
380
- `### Roadmap Count`,
392
+ '### Updated Milestone',
393
+ `- ${updated.id} | ${updated.status} | ${updated.title}${updated.time ? ` | time=${updated.time}` : ''}`,
394
+ '',
395
+ '### Roadmap Count',
381
396
  `- total: ${refreshed.milestones.length}`,
382
397
  ]),
383
398
  guidanceSection([
384
- "Milestone updated successfully and roadmap.md has been synced.",
385
- "Re-run roadmapContext to verify linked task traceability.",
386
- ".projitive governance store is source of truth; roadmap.md is a generated view and may be overwritten.",
399
+ 'Milestone updated successfully and roadmap.md has been synced.',
400
+ 'Re-run roadmapContext to verify linked task traceability.',
401
+ '.projitive governance store is source of truth; roadmap.md is a generated view and may be overwritten.',
387
402
  ]),
388
403
  lintSection([]),
389
404
  nextCallSection(`roadmapContext(projectPath="${toProjectPath(governanceDir)}", roadmapId="${roadmapId}")`),
@@ -1,75 +1,83 @@
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 { isValidRoadmapId, collectRoadmapLintSuggestions, loadRoadmapDocument, renderRoadmapMarkdown, registerRoadmapTools } from './roadmap.js';
6
+ describe('roadmap module', () => {
7
7
  let tempDir;
8
8
  beforeAll(async () => {
9
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-roadmap-test-"));
9
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-roadmap-test-'));
10
10
  });
11
11
  afterAll(async () => {
12
12
  await fs.rm(tempDir, { recursive: true, force: true });
13
13
  });
14
- describe("isValidRoadmapId", () => {
15
- it("should validate correct roadmap IDs", () => {
16
- expect(isValidRoadmapId("ROADMAP-0001")).toBe(true);
17
- expect(isValidRoadmapId("ROADMAP-1234")).toBe(true);
14
+ describe('isValidRoadmapId', () => {
15
+ it('should validate correct roadmap IDs', () => {
16
+ expect(isValidRoadmapId('ROADMAP-0001')).toBe(true);
17
+ expect(isValidRoadmapId('ROADMAP-1')).toBe(true);
18
+ expect(isValidRoadmapId('ROADMAP-1234')).toBe(true);
19
+ expect(isValidRoadmapId('ROADMAP-12345')).toBe(true);
18
20
  });
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);
21
+ it('should reject invalid roadmap IDs', () => {
22
+ expect(isValidRoadmapId('roadmap-0001')).toBe(false);
23
+ expect(isValidRoadmapId('TASK-0001')).toBe(false);
24
+ expect(isValidRoadmapId('invalid')).toBe(false);
23
25
  });
24
26
  });
25
- describe("collectRoadmapLintSuggestions", () => {
26
- it("should return lint suggestion for empty roadmap IDs", () => {
27
+ describe('collectRoadmapLintSuggestions', () => {
28
+ it('should return lint suggestion for empty roadmap IDs', () => {
27
29
  const suggestions = collectRoadmapLintSuggestions([], []);
28
- expect(suggestions.some(s => s.includes("IDS_EMPTY"))).toBe(true);
30
+ expect(suggestions.some(s => s.includes('IDS_EMPTY'))).toBe(true);
29
31
  });
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);
32
+ it('should return lint suggestion for empty tasks', () => {
33
+ const suggestions = collectRoadmapLintSuggestions(['ROADMAP-0001'], []);
34
+ expect(suggestions.some(s => s.includes('TASKS_EMPTY'))).toBe(true);
33
35
  });
34
- it("should return lint suggestion for tasks without roadmap refs", () => {
36
+ it('should return lint suggestion for tasks without roadmap refs', () => {
35
37
  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",
38
+ id: 'TASK-0001',
39
+ title: 'Test Task',
40
+ status: 'TODO',
41
+ owner: 'ai-copilot',
42
+ summary: 'Test',
43
+ updatedAt: '2026-01-01T00:00:00.000Z',
42
44
  links: [],
43
45
  roadmapRefs: [],
44
46
  }];
45
- const suggestions = collectRoadmapLintSuggestions(["ROADMAP-0001"], tasks);
46
- expect(suggestions.some(s => s.includes("TASK_REFS_EMPTY"))).toBe(true);
47
+ const suggestions = collectRoadmapLintSuggestions(['ROADMAP-0001'], tasks);
48
+ expect(suggestions.some(s => s.includes('TASK_REFS_EMPTY'))).toBe(true);
47
49
  });
48
- it("loads from governance store and rewrites roadmap markdown view", async () => {
49
- const governanceDir = path.join(tempDir, ".projitive-db");
50
+ it('loads from governance store and rewrites roadmap markdown view', async () => {
51
+ const governanceDir = path.join(tempDir, '.projitive-db');
50
52
  await fs.mkdir(governanceDir, { recursive: true });
51
- await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
53
+ await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
52
54
  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");
55
+ expect(doc.roadmapPath.endsWith('.projitive')).toBe(true);
56
+ expect(doc.markdownPath.endsWith('roadmap.md')).toBe(true);
57
+ const markdown = await fs.readFile(path.join(governanceDir, 'roadmap.md'), 'utf-8');
58
+ expect(markdown).toContain('generated from .projitive governance store');
59
+ expect(markdown).toContain('Author: yinxulai');
60
+ expect(markdown).toContain('Repository: https://github.com/yinxulai/projitive');
61
+ expect(markdown).toContain('Do not edit this file manually.');
57
62
  });
58
- it("renders milestones in newest-first order", () => {
63
+ it('renders milestones in newest-first order', () => {
59
64
  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" },
65
+ { id: 'ROADMAP-0001', title: 'Older', status: 'active', updatedAt: '2026-01-01T00:00:00.000Z' },
66
+ { id: 'ROADMAP-0002', title: 'Newer', status: 'done', updatedAt: '2026-02-01T00:00:00.000Z' },
62
67
  ]);
63
- expect(markdown.indexOf("ROADMAP-0002")).toBeLessThan(markdown.indexOf("ROADMAP-0001"));
64
- expect(markdown).toContain("[x] ROADMAP-0002");
68
+ expect(markdown.indexOf('ROADMAP-0002')).toBeLessThan(markdown.indexOf('ROADMAP-0001'));
69
+ expect(markdown).toContain('[x] ROADMAP-0002');
65
70
  });
66
- it("registers roadmapCreate tool", () => {
71
+ it('registers roadmapCreate tool', () => {
67
72
  const mockServer = {
68
- registerTool: (..._args) => undefined,
73
+ registerTool: (...args) => {
74
+ void args;
75
+ return undefined;
76
+ },
69
77
  };
70
- const spy = vi.spyOn(mockServer, "registerTool");
78
+ const spy = vi.spyOn(mockServer, 'registerTool');
71
79
  registerRoadmapTools(mockServer);
72
- expect(spy.mock.calls.some((call) => call[0] === "roadmapCreate")).toBe(true);
80
+ expect(spy.mock.calls.some((call) => call[0] === 'roadmapCreate')).toBe(true);
73
81
  });
74
82
  });
75
83
  });