@projitive/mcp 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -20
- package/output/helpers/artifacts/artifacts.js +10 -0
- package/output/helpers/artifacts/artifacts.test.js +18 -0
- package/output/helpers/artifacts/index.js +1 -0
- package/output/helpers/index.js +3 -0
- package/output/helpers/linter/codes.js +25 -0
- package/output/helpers/linter/index.js +2 -0
- package/output/helpers/linter/linter.js +6 -0
- package/output/helpers/linter/linter.test.js +16 -0
- package/output/helpers/response/index.js +1 -0
- package/output/helpers/response/response.js +73 -0
- package/output/helpers/response/response.test.js +50 -0
- package/output/hooks.js +1 -14
- package/output/hooks.test.js +7 -18
- package/output/index.js +23 -5
- package/output/package.json +36 -0
- package/output/projitive.js +158 -124
- package/output/projitive.test.js +1 -0
- package/output/rendering-input-guard.test.js +20 -0
- package/output/roadmap.js +106 -80
- package/output/roadmap.test.js +11 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectContext.md +48 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectInit.md +40 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectLocate.md +22 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectNext.md +31 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectScan.md +28 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/roadmapContext.md +33 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/roadmapList.md +25 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/summary.json +90 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/summary.md +17 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskContext.md +47 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskList.md +27 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskNext.md +64 -0
- package/output/source/designs.js +38 -0
- package/output/source/helpers/artifacts/artifacts.js +10 -0
- package/output/source/helpers/artifacts/artifacts.test.js +18 -0
- package/output/source/helpers/artifacts/index.js +1 -0
- package/output/source/helpers/catch/catch.js +48 -0
- package/output/source/helpers/catch/catch.test.js +43 -0
- package/output/source/helpers/catch/index.js +1 -0
- package/output/source/helpers/files/files.js +62 -0
- package/output/source/helpers/files/files.test.js +32 -0
- package/output/source/helpers/files/index.js +1 -0
- package/output/source/helpers/index.js +6 -0
- package/output/source/helpers/linter/codes.js +25 -0
- package/output/source/helpers/linter/index.js +2 -0
- package/output/source/helpers/linter/linter.js +6 -0
- package/output/source/helpers/linter/linter.test.js +16 -0
- package/output/source/helpers/markdown/index.js +1 -0
- package/output/source/helpers/markdown/markdown.js +33 -0
- package/output/source/helpers/markdown/markdown.test.js +36 -0
- package/output/source/helpers/response/index.js +1 -0
- package/output/source/helpers/response/response.js +73 -0
- package/output/source/helpers/response/response.test.js +50 -0
- package/output/source/index.js +215 -0
- package/output/source/projitive.js +488 -0
- package/output/source/projitive.test.js +75 -0
- package/output/source/readme.js +26 -0
- package/output/source/reports.js +36 -0
- package/output/source/roadmap.js +165 -0
- package/output/source/roadmap.test.js +11 -0
- package/output/source/tasks.js +762 -0
- package/output/source/tasks.test.js +152 -0
- package/output/tasks.js +403 -204
- package/output/tasks.test.js +78 -4
- package/package.json +1 -1
package/output/projitive.js
CHANGED
|
@@ -4,17 +4,14 @@ import process from "node:process";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
|
|
6
6
|
import { catchIt } from "./helpers/catch/index.js";
|
|
7
|
-
import {
|
|
7
|
+
import { PROJECT_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
|
|
8
|
+
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
|
|
9
|
+
import { collectTaskLintSuggestions, loadTasksDocument } from "./tasks.js";
|
|
8
10
|
export const PROJECT_MARKER = ".projitive";
|
|
9
11
|
const DEFAULT_GOVERNANCE_DIR = ".projitive";
|
|
10
12
|
const ignoreNames = new Set(["node_modules", ".git", ".next", "dist", "build"]);
|
|
11
13
|
const DEFAULT_SCAN_DEPTH = 3;
|
|
12
14
|
const MAX_SCAN_DEPTH = 8;
|
|
13
|
-
function asText(markdown) {
|
|
14
|
-
return {
|
|
15
|
-
content: [{ type: "text", text: markdown }],
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
15
|
function normalizePath(inputPath) {
|
|
19
16
|
return inputPath ? path.resolve(inputPath) : process.cwd();
|
|
20
17
|
}
|
|
@@ -71,11 +68,22 @@ async function readTasksSnapshot(governanceDir) {
|
|
|
71
68
|
const tasksPath = path.join(governanceDir, "tasks.md");
|
|
72
69
|
const markdown = await fs.readFile(tasksPath, "utf-8").catch(() => undefined);
|
|
73
70
|
if (typeof markdown !== "string") {
|
|
74
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
tasksPath,
|
|
73
|
+
exists: false,
|
|
74
|
+
tasks: [],
|
|
75
|
+
lintSuggestions: renderLintSuggestions([
|
|
76
|
+
{
|
|
77
|
+
code: PROJECT_LINT_CODES.TASKS_FILE_MISSING,
|
|
78
|
+
message: "tasks.md is missing.",
|
|
79
|
+
fixHint: "Initialize governance tasks structure first.",
|
|
80
|
+
},
|
|
81
|
+
]),
|
|
82
|
+
};
|
|
75
83
|
}
|
|
76
84
|
const { parseTasksBlock } = await import("./tasks.js");
|
|
77
85
|
const tasks = await parseTasksBlock(markdown);
|
|
78
|
-
return { tasksPath, exists: true, tasks };
|
|
86
|
+
return { tasksPath, exists: true, tasks, lintSuggestions: collectTaskLintSuggestions(tasks, markdown) };
|
|
79
87
|
}
|
|
80
88
|
function latestTaskUpdatedAt(tasks) {
|
|
81
89
|
const timestamps = tasks
|
|
@@ -193,12 +201,30 @@ function defaultTasksMarkdown() {
|
|
|
193
201
|
"- owner: unassigned",
|
|
194
202
|
"- summary: Create initial governance artifacts and confirm task execution loop.",
|
|
195
203
|
`- updatedAt: ${updatedAt}`,
|
|
196
|
-
"- links:",
|
|
197
204
|
"- roadmapRefs: ROADMAP-0001",
|
|
198
|
-
"-
|
|
205
|
+
"- links:",
|
|
206
|
+
" - (none)",
|
|
199
207
|
"<!-- PROJITIVE:TASKS:END -->",
|
|
200
208
|
].join("\n");
|
|
201
209
|
}
|
|
210
|
+
function defaultNoTaskDiscoveryHookMarkdown() {
|
|
211
|
+
return [
|
|
212
|
+
"Objective:",
|
|
213
|
+
"- When no actionable task exists, proactively discover meaningful work and convert it into TODO tasks.",
|
|
214
|
+
"",
|
|
215
|
+
"Checklist:",
|
|
216
|
+
"- Check whether code violates project guides/specs; create tasks for each actionable gap.",
|
|
217
|
+
"- Check test coverage improvement opportunities; create tasks for high-value missing tests.",
|
|
218
|
+
"- Check development/testing workflow bottlenecks; create tasks for reliability and speed improvements.",
|
|
219
|
+
"- Check TODO/FIXME/HACK comments; turn feasible items into governed tasks.",
|
|
220
|
+
"- Check dependency/security hygiene and stale tooling; create tasks where upgrades are justified.",
|
|
221
|
+
"",
|
|
222
|
+
"Output Format:",
|
|
223
|
+
"- Candidate findings (3-10)",
|
|
224
|
+
"- Proposed tasks (TASK-xxxx style)",
|
|
225
|
+
"- Priority rationale",
|
|
226
|
+
].join("\n");
|
|
227
|
+
}
|
|
202
228
|
export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
|
|
203
229
|
const rootPath = normalizePath(inputPath);
|
|
204
230
|
const governanceDirName = normalizeGovernanceDirName(governanceDir);
|
|
@@ -223,6 +249,7 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
|
|
|
223
249
|
writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
|
|
224
250
|
writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(), force),
|
|
225
251
|
writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(), force),
|
|
252
|
+
writeTextFile(path.join(governancePath, "hooks", "task_no_actionable.md"), defaultNoTaskDiscoveryHookMarkdown(), force),
|
|
226
253
|
]);
|
|
227
254
|
return {
|
|
228
255
|
rootPath,
|
|
@@ -248,31 +275,35 @@ export function registerProjectTools(server) {
|
|
|
248
275
|
updated: initialized.files.filter((item) => item.action === "updated"),
|
|
249
276
|
skipped: initialized.files.filter((item) => item.action === "skipped"),
|
|
250
277
|
};
|
|
251
|
-
const markdown =
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
const markdown = renderToolResponseMarkdown({
|
|
279
|
+
toolName: "projectInit",
|
|
280
|
+
sections: [
|
|
281
|
+
summarySection([
|
|
282
|
+
`- rootPath: ${initialized.rootPath}`,
|
|
283
|
+
`- governanceDir: ${initialized.governanceDir}`,
|
|
284
|
+
`- markerPath: ${initialized.markerPath}`,
|
|
285
|
+
`- force: ${force === true ? "true" : "false"}`,
|
|
286
|
+
]),
|
|
287
|
+
evidenceSection([
|
|
288
|
+
`- createdFiles: ${filesByAction.created.length}`,
|
|
289
|
+
`- updatedFiles: ${filesByAction.updated.length}`,
|
|
290
|
+
`- skippedFiles: ${filesByAction.skipped.length}`,
|
|
291
|
+
"- directories:",
|
|
292
|
+
...initialized.directories.map((item) => ` - ${item.action}: ${item.path}`),
|
|
293
|
+
"- files:",
|
|
294
|
+
...initialized.files.map((item) => ` - ${item.action}: ${item.path}`),
|
|
295
|
+
]),
|
|
296
|
+
guidanceSection([
|
|
297
|
+
"- If files were skipped and you want to overwrite templates, rerun with force=true.",
|
|
298
|
+
"- Continue with projectContext and taskList for execution.",
|
|
299
|
+
]),
|
|
300
|
+
lintSection([
|
|
301
|
+
"- After init, fill owner/roadmapRefs/links in tasks.md before marking DONE.",
|
|
302
|
+
"- Keep task source-of-truth inside marker block only.",
|
|
303
|
+
]),
|
|
304
|
+
nextCallSection(`projectContext(projectPath=\"${initialized.governanceDir}\")`),
|
|
305
|
+
],
|
|
306
|
+
});
|
|
276
307
|
return asText(markdown);
|
|
277
308
|
});
|
|
278
309
|
server.registerTool("projectScan", {
|
|
@@ -286,27 +317,30 @@ export function registerProjectTools(server) {
|
|
|
286
317
|
const root = resolveScanRoot(rootPath);
|
|
287
318
|
const depth = resolveScanDepth(maxDepth);
|
|
288
319
|
const projects = await discoverProjects(root, depth);
|
|
289
|
-
const markdown =
|
|
290
|
-
"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
320
|
+
const markdown = renderToolResponseMarkdown({
|
|
321
|
+
toolName: "projectScan",
|
|
322
|
+
sections: [
|
|
323
|
+
summarySection([
|
|
324
|
+
`- rootPath: ${root}`,
|
|
325
|
+
`- maxDepth: ${depth}`,
|
|
326
|
+
`- discoveredCount: ${projects.length}`,
|
|
327
|
+
]),
|
|
328
|
+
evidenceSection([
|
|
329
|
+
"- projects:",
|
|
330
|
+
...projects.map((project, index) => `${index + 1}. ${project}`),
|
|
331
|
+
]),
|
|
332
|
+
guidanceSection([
|
|
333
|
+
"- Use one discovered project path and call `projectLocate` to lock governance root.",
|
|
334
|
+
"- Then call `projectContext` to inspect current governance state.",
|
|
335
|
+
]),
|
|
336
|
+
lintSection(projects.length === 0
|
|
337
|
+
? ["- No governance root discovered. Add `.projitive` marker and baseline artifacts before execution."]
|
|
338
|
+
: ["- Run `projectContext` on a discovered project to receive module-level lint suggestions."]),
|
|
339
|
+
nextCallSection(projects[0]
|
|
340
|
+
? `projectLocate(inputPath=\"${projects[0]}\")`
|
|
341
|
+
: undefined),
|
|
342
|
+
],
|
|
343
|
+
});
|
|
310
344
|
return asText(markdown);
|
|
311
345
|
});
|
|
312
346
|
server.registerTool("projectNext", {
|
|
@@ -332,7 +366,7 @@ export function registerProjectTools(server) {
|
|
|
332
366
|
governanceDir,
|
|
333
367
|
tasksPath: snapshot.tasksPath,
|
|
334
368
|
tasksExists: snapshot.exists,
|
|
335
|
-
|
|
369
|
+
lintSuggestions: snapshot.lintSuggestions,
|
|
336
370
|
inProgress,
|
|
337
371
|
todo,
|
|
338
372
|
blocked,
|
|
@@ -351,32 +385,31 @@ export function registerProjectTools(server) {
|
|
|
351
385
|
return b.latestUpdatedAt.localeCompare(a.latestUpdatedAt);
|
|
352
386
|
})
|
|
353
387
|
.slice(0, limit ?? 10);
|
|
354
|
-
const markdown =
|
|
355
|
-
"
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
].join("\n");
|
|
388
|
+
const markdown = renderToolResponseMarkdown({
|
|
389
|
+
toolName: "projectNext",
|
|
390
|
+
sections: [
|
|
391
|
+
summarySection([
|
|
392
|
+
`- rootPath: ${root}`,
|
|
393
|
+
`- maxDepth: ${depth}`,
|
|
394
|
+
`- matchedProjects: ${projects.length}`,
|
|
395
|
+
`- actionableProjects: ${ranked.length}`,
|
|
396
|
+
`- limit: ${limit ?? 10}`,
|
|
397
|
+
]),
|
|
398
|
+
evidenceSection([
|
|
399
|
+
"- rankedProjects:",
|
|
400
|
+
...ranked.map((item, index) => `${index + 1}. ${item.governanceDir} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt} | tasksPath=${item.tasksPath}${item.tasksExists ? "" : " (missing)"}`),
|
|
401
|
+
]),
|
|
402
|
+
guidanceSection([
|
|
403
|
+
"- Pick top 1 project and call `projectContext` with its governanceDir.",
|
|
404
|
+
"- Then call `taskList` and `taskContext` to continue execution.",
|
|
405
|
+
"- If `tasksPath` is missing, create tasks.md using project convention before task-level operations.",
|
|
406
|
+
]),
|
|
407
|
+
lintSection(ranked[0]?.lintSuggestions ?? []),
|
|
408
|
+
nextCallSection(ranked[0]
|
|
409
|
+
? `projectContext(projectPath=\"${ranked[0].governanceDir}\")`
|
|
410
|
+
: undefined),
|
|
411
|
+
],
|
|
412
|
+
});
|
|
380
413
|
return asText(markdown);
|
|
381
414
|
});
|
|
382
415
|
server.registerTool("projectLocate", {
|
|
@@ -389,20 +422,19 @@ export function registerProjectTools(server) {
|
|
|
389
422
|
const resolvedFrom = normalizePath(inputPath);
|
|
390
423
|
const governanceDir = await resolveGovernanceDir(resolvedFrom);
|
|
391
424
|
const markerPath = path.join(governanceDir, ".projitive");
|
|
392
|
-
const markdown =
|
|
393
|
-
"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
].join("\n");
|
|
425
|
+
const markdown = renderToolResponseMarkdown({
|
|
426
|
+
toolName: "projectLocate",
|
|
427
|
+
sections: [
|
|
428
|
+
summarySection([
|
|
429
|
+
`- resolvedFrom: ${resolvedFrom}`,
|
|
430
|
+
`- governanceDir: ${governanceDir}`,
|
|
431
|
+
`- markerPath: ${markerPath}`,
|
|
432
|
+
]),
|
|
433
|
+
guidanceSection(["- Call `projectContext` with this governanceDir to get task and roadmap summaries."]),
|
|
434
|
+
lintSection(["- Run `projectContext` to get governance/module lint suggestions for this project."]),
|
|
435
|
+
nextCallSection(`projectContext(projectPath=\"${governanceDir}\")`),
|
|
436
|
+
],
|
|
437
|
+
});
|
|
406
438
|
return asText(markdown);
|
|
407
439
|
});
|
|
408
440
|
server.registerTool("projectContext", {
|
|
@@ -414,8 +446,9 @@ export function registerProjectTools(server) {
|
|
|
414
446
|
}, async ({ projectPath }) => {
|
|
415
447
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
416
448
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
417
|
-
const { tasksPath, tasks } = await
|
|
449
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
418
450
|
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
451
|
+
const lintSuggestions = collectTaskLintSuggestions(tasks, tasksMarkdown);
|
|
419
452
|
const taskSummary = {
|
|
420
453
|
total: tasks.length,
|
|
421
454
|
TODO: tasks.filter((task) => task.status === "TODO").length,
|
|
@@ -423,32 +456,33 @@ export function registerProjectTools(server) {
|
|
|
423
456
|
BLOCKED: tasks.filter((task) => task.status === "BLOCKED").length,
|
|
424
457
|
DONE: tasks.filter((task) => task.status === "DONE").length,
|
|
425
458
|
};
|
|
426
|
-
const markdown =
|
|
427
|
-
"
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
459
|
+
const markdown = renderToolResponseMarkdown({
|
|
460
|
+
toolName: "projectContext",
|
|
461
|
+
sections: [
|
|
462
|
+
summarySection([
|
|
463
|
+
`- governanceDir: ${governanceDir}`,
|
|
464
|
+
`- tasksFile: ${tasksPath}`,
|
|
465
|
+
`- roadmapIds: ${roadmapIds.length}`,
|
|
466
|
+
]),
|
|
467
|
+
evidenceSection([
|
|
468
|
+
"### Task Summary",
|
|
469
|
+
`- total: ${taskSummary.total}`,
|
|
470
|
+
`- TODO: ${taskSummary.TODO}`,
|
|
471
|
+
`- IN_PROGRESS: ${taskSummary.IN_PROGRESS}`,
|
|
472
|
+
`- BLOCKED: ${taskSummary.BLOCKED}`,
|
|
473
|
+
`- DONE: ${taskSummary.DONE}`,
|
|
474
|
+
"",
|
|
475
|
+
"### Artifacts",
|
|
476
|
+
renderArtifactsMarkdown(artifacts),
|
|
477
|
+
]),
|
|
478
|
+
guidanceSection([
|
|
479
|
+
"- Start from `taskList` to choose a target task.",
|
|
480
|
+
"- Then call `taskContext` with a task ID to retrieve evidence locations and reading order.",
|
|
481
|
+
]),
|
|
482
|
+
lintSection(lintSuggestions),
|
|
483
|
+
nextCallSection(`taskList(projectPath=\"${governanceDir}\")`),
|
|
484
|
+
],
|
|
485
|
+
});
|
|
452
486
|
return asText(markdown);
|
|
453
487
|
});
|
|
454
488
|
}
|
package/output/projitive.test.js
CHANGED
|
@@ -52,6 +52,7 @@ describe("projitive module", () => {
|
|
|
52
52
|
path.join(root, ".projitive", "README.md"),
|
|
53
53
|
path.join(root, ".projitive", "roadmap.md"),
|
|
54
54
|
path.join(root, ".projitive", "tasks.md"),
|
|
55
|
+
path.join(root, ".projitive", "hooks", "task_no_actionable.md"),
|
|
55
56
|
path.join(root, ".projitive", "designs"),
|
|
56
57
|
path.join(root, ".projitive", "reports"),
|
|
57
58
|
path.join(root, ".projitive", "hooks"),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
const MODULE_FILES = [
|
|
5
|
+
"tasks.ts",
|
|
6
|
+
"roadmap.ts",
|
|
7
|
+
"projitive.ts",
|
|
8
|
+
];
|
|
9
|
+
const INVALID_LITERAL_PATTERN = /["'`]\s*-\s+(#{1,6}\s|>\s*|```)/g;
|
|
10
|
+
describe("rendering input guard", () => {
|
|
11
|
+
it("does not contain accidental bullet-prefixed markdown literals in module outputs", async () => {
|
|
12
|
+
const sourceDir = path.resolve(import.meta.dirname);
|
|
13
|
+
for (const file of MODULE_FILES) {
|
|
14
|
+
const filePath = path.join(sourceDir, file);
|
|
15
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
16
|
+
const matches = content.match(INVALID_LITERAL_PATTERN) ?? [];
|
|
17
|
+
expect(matches, `invalid literals in ${filePath}`).toHaveLength(0);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
package/output/roadmap.js
CHANGED
|
@@ -1,39 +1,60 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { candidateFilesFromArtifacts } from "./helpers/artifacts/index.js";
|
|
4
5
|
import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
|
|
6
|
+
import { ROADMAP_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
|
|
5
7
|
import { findTextReferences } from "./helpers/markdown/index.js";
|
|
8
|
+
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
|
|
6
9
|
import { resolveGovernanceDir } from "./projitive.js";
|
|
7
10
|
import { loadTasks } from "./tasks.js";
|
|
8
11
|
export const ROADMAP_ID_REGEX = /^ROADMAP-\d{4}$/;
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
function collectRoadmapLintSuggestionItems(roadmapIds, tasks) {
|
|
13
|
+
const suggestions = [];
|
|
14
|
+
if (roadmapIds.length === 0) {
|
|
15
|
+
suggestions.push({
|
|
16
|
+
code: ROADMAP_LINT_CODES.IDS_EMPTY,
|
|
17
|
+
message: "No roadmap IDs found in roadmap.md.",
|
|
18
|
+
fixHint: "Add at least one ROADMAP-xxxx milestone.",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (tasks.length === 0) {
|
|
22
|
+
suggestions.push({
|
|
23
|
+
code: ROADMAP_LINT_CODES.TASKS_EMPTY,
|
|
24
|
+
message: "No tasks found in tasks.md.",
|
|
25
|
+
fixHint: "Add task cards and bind roadmapRefs for traceability.",
|
|
26
|
+
});
|
|
27
|
+
return suggestions;
|
|
28
|
+
}
|
|
29
|
+
const roadmapSet = new Set(roadmapIds);
|
|
30
|
+
const unboundTasks = tasks.filter((task) => task.roadmapRefs.length === 0);
|
|
31
|
+
if (unboundTasks.length > 0) {
|
|
32
|
+
suggestions.push({
|
|
33
|
+
code: ROADMAP_LINT_CODES.TASK_REFS_EMPTY,
|
|
34
|
+
message: `${unboundTasks.length} task(s) have empty roadmapRefs.`,
|
|
35
|
+
fixHint: "Bind ROADMAP-xxxx where applicable.",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const unknownRefs = Array.from(new Set(tasks.flatMap((task) => task.roadmapRefs).filter((id) => !roadmapSet.has(id))));
|
|
39
|
+
if (unknownRefs.length > 0) {
|
|
40
|
+
suggestions.push({
|
|
41
|
+
code: ROADMAP_LINT_CODES.UNKNOWN_REFS,
|
|
42
|
+
message: `Unknown roadmapRefs detected: ${unknownRefs.join(", ")}.`,
|
|
43
|
+
fixHint: "Add missing roadmap IDs or fix task references.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const noLinkedRoadmaps = roadmapIds.filter((id) => !tasks.some((task) => task.roadmapRefs.includes(id)));
|
|
47
|
+
if (noLinkedRoadmaps.length > 0) {
|
|
48
|
+
suggestions.push({
|
|
49
|
+
code: ROADMAP_LINT_CODES.ZERO_LINKED_TASKS,
|
|
50
|
+
message: `${noLinkedRoadmaps.length} roadmap ID(s) have zero linked tasks.`,
|
|
51
|
+
fixHint: `Consider binding tasks to: ${noLinkedRoadmaps.slice(0, 3).join(", ")}${noLinkedRoadmaps.length > 3 ? ", ..." : ""}.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return suggestions;
|
|
27
55
|
}
|
|
28
|
-
function
|
|
29
|
-
return
|
|
30
|
-
.filter((item) => item.exists)
|
|
31
|
-
.flatMap((item) => {
|
|
32
|
-
if (item.kind === "file") {
|
|
33
|
-
return [item.path];
|
|
34
|
-
}
|
|
35
|
-
return (item.markdownFiles ?? []).map((entry) => entry.path);
|
|
36
|
-
});
|
|
56
|
+
export function collectRoadmapLintSuggestions(roadmapIds, tasks) {
|
|
57
|
+
return renderLintSuggestions(collectRoadmapLintSuggestionItems(roadmapIds, tasks));
|
|
37
58
|
}
|
|
38
59
|
export function isValidRoadmapId(id) {
|
|
39
60
|
return ROADMAP_ID_REGEX.test(id);
|
|
@@ -60,30 +81,28 @@ export function registerRoadmapTools(server) {
|
|
|
60
81
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
61
82
|
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
62
83
|
const { tasks } = await loadTasks(governanceDir);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
"",
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
: ["- (none)"]),
|
|
86
|
-
].join("\n");
|
|
84
|
+
const lintSuggestions = collectRoadmapLintSuggestions(roadmapIds, tasks);
|
|
85
|
+
const markdown = renderToolResponseMarkdown({
|
|
86
|
+
toolName: "roadmapList",
|
|
87
|
+
sections: [
|
|
88
|
+
summarySection([
|
|
89
|
+
`- governanceDir: ${governanceDir}`,
|
|
90
|
+
`- roadmapCount: ${roadmapIds.length}`,
|
|
91
|
+
]),
|
|
92
|
+
evidenceSection([
|
|
93
|
+
"- roadmaps:",
|
|
94
|
+
...roadmapIds.map((id) => {
|
|
95
|
+
const linkedTasks = tasks.filter((task) => task.roadmapRefs.includes(id));
|
|
96
|
+
return `- ${id} | linkedTasks=${linkedTasks.length}`;
|
|
97
|
+
}),
|
|
98
|
+
]),
|
|
99
|
+
guidanceSection(["- Pick one roadmap ID and call `roadmapContext`."]),
|
|
100
|
+
lintSection(lintSuggestions),
|
|
101
|
+
nextCallSection(roadmapIds[0]
|
|
102
|
+
? `roadmapContext(projectPath=\"${governanceDir}\", roadmapId=\"${roadmapIds[0]}\")`
|
|
103
|
+
: undefined),
|
|
104
|
+
],
|
|
105
|
+
});
|
|
87
106
|
return asText(markdown);
|
|
88
107
|
});
|
|
89
108
|
server.registerTool("roadmapContext", {
|
|
@@ -96,7 +115,7 @@ export function registerRoadmapTools(server) {
|
|
|
96
115
|
}, async ({ projectPath, roadmapId }) => {
|
|
97
116
|
if (!isValidRoadmapId(roadmapId)) {
|
|
98
117
|
return {
|
|
99
|
-
...asText(renderErrorMarkdown("roadmapContext", `Invalid roadmap ID format: ${roadmapId}`, ["
|
|
118
|
+
...asText(renderErrorMarkdown("roadmapContext", `Invalid roadmap ID format: ${roadmapId}`, ["expected format: ROADMAP-0001", "retry with a valid roadmap ID"], `roadmapContext(projectPath=\"${projectPath}\", roadmapId=\"ROADMAP-0001\")`)),
|
|
100
119
|
isError: true,
|
|
101
120
|
};
|
|
102
121
|
}
|
|
@@ -106,34 +125,41 @@ export function registerRoadmapTools(server) {
|
|
|
106
125
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, roadmapId)))).flat();
|
|
107
126
|
const { tasks } = await loadTasks(governanceDir);
|
|
108
127
|
const relatedTasks = tasks.filter((task) => task.roadmapRefs.includes(roadmapId));
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
129
|
+
const lintSuggestionItems = collectRoadmapLintSuggestionItems(roadmapIds, tasks);
|
|
130
|
+
if (relatedTasks.length === 0) {
|
|
131
|
+
lintSuggestionItems.push({
|
|
132
|
+
code: ROADMAP_LINT_CODES.CONTEXT_RELATED_TASKS_EMPTY,
|
|
133
|
+
message: `relatedTasks=0 for ${roadmapId}.`,
|
|
134
|
+
fixHint: "Batch bind task roadmapRefs to improve execution traceability.",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const lintSuggestions = renderLintSuggestions(lintSuggestionItems);
|
|
138
|
+
const markdown = renderToolResponseMarkdown({
|
|
139
|
+
toolName: "roadmapContext",
|
|
140
|
+
sections: [
|
|
141
|
+
summarySection([
|
|
142
|
+
`- governanceDir: ${governanceDir}`,
|
|
143
|
+
`- roadmapId: ${roadmapId}`,
|
|
144
|
+
`- relatedTasks: ${relatedTasks.length}`,
|
|
145
|
+
`- references: ${referenceLocations.length}`,
|
|
146
|
+
]),
|
|
147
|
+
evidenceSection([
|
|
148
|
+
"### Related Tasks",
|
|
149
|
+
...relatedTasks.map((task) => `- ${task.id} | ${task.status} | ${task.title}`),
|
|
150
|
+
"",
|
|
151
|
+
"### Reference Locations",
|
|
152
|
+
...referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`),
|
|
153
|
+
]),
|
|
154
|
+
guidanceSection([
|
|
155
|
+
"- Read roadmap references first, then related tasks.",
|
|
156
|
+
"- Keep ROADMAP/TASK IDs unchanged while updating markdown files.",
|
|
157
|
+
"- Re-run `roadmapContext` after edits to confirm references remain consistent.",
|
|
158
|
+
]),
|
|
159
|
+
lintSection(lintSuggestions),
|
|
160
|
+
nextCallSection(`roadmapContext(projectPath=\"${governanceDir}\", roadmapId=\"${roadmapId}\")`),
|
|
161
|
+
],
|
|
162
|
+
});
|
|
137
163
|
return asText(markdown);
|
|
138
164
|
});
|
|
139
165
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { collectRoadmapLintSuggestions } from "./roadmap.js";
|
|
3
|
+
import { normalizeTask } from "./tasks.js";
|
|
4
|
+
describe("roadmap lint rendering alignment", () => {
|
|
5
|
+
it("renders roadmap lint in code-prefixed markdown lines", () => {
|
|
6
|
+
const lint = collectRoadmapLintSuggestions(["ROADMAP-0001"], [
|
|
7
|
+
normalizeTask({ id: "TASK-0001", title: "x", status: "TODO", roadmapRefs: [] }),
|
|
8
|
+
]);
|
|
9
|
+
expect(lint.some((line) => line.startsWith("- [ROADMAP_TASK_REFS_EMPTY]"))).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
});
|