@projitive/mcp 1.0.2 → 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.
Files changed (40) hide show
  1. package/README.md +44 -20
  2. package/output/hooks.js +1 -14
  3. package/output/hooks.test.js +7 -18
  4. package/output/index.js +23 -5
  5. package/output/package.json +36 -0
  6. package/output/projitive.js +21 -2
  7. package/output/projitive.test.js +1 -0
  8. package/output/source/designs.js +38 -0
  9. package/output/source/helpers/artifacts/artifacts.js +10 -0
  10. package/output/source/helpers/artifacts/artifacts.test.js +18 -0
  11. package/output/source/helpers/artifacts/index.js +1 -0
  12. package/output/source/helpers/catch/catch.js +48 -0
  13. package/output/source/helpers/catch/catch.test.js +43 -0
  14. package/output/source/helpers/catch/index.js +1 -0
  15. package/output/source/helpers/files/files.js +62 -0
  16. package/output/source/helpers/files/files.test.js +32 -0
  17. package/output/source/helpers/files/index.js +1 -0
  18. package/output/source/helpers/index.js +6 -0
  19. package/output/source/helpers/linter/codes.js +25 -0
  20. package/output/source/helpers/linter/index.js +2 -0
  21. package/output/source/helpers/linter/linter.js +6 -0
  22. package/output/source/helpers/linter/linter.test.js +16 -0
  23. package/output/source/helpers/markdown/index.js +1 -0
  24. package/output/source/helpers/markdown/markdown.js +33 -0
  25. package/output/source/helpers/markdown/markdown.test.js +36 -0
  26. package/output/source/helpers/response/index.js +1 -0
  27. package/output/source/helpers/response/response.js +73 -0
  28. package/output/source/helpers/response/response.test.js +50 -0
  29. package/output/source/index.js +215 -0
  30. package/output/source/projitive.js +488 -0
  31. package/output/source/projitive.test.js +75 -0
  32. package/output/source/readme.js +26 -0
  33. package/output/source/reports.js +36 -0
  34. package/output/source/roadmap.js +165 -0
  35. package/output/source/roadmap.test.js +11 -0
  36. package/output/source/tasks.js +762 -0
  37. package/output/source/tasks.test.js +152 -0
  38. package/output/tasks.js +100 -80
  39. package/output/tasks.test.js +32 -8
  40. package/package.json +1 -1
@@ -0,0 +1,165 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { candidateFilesFromArtifacts } from "./helpers/artifacts/index.js";
5
+ import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
6
+ import { ROADMAP_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
7
+ import { findTextReferences } from "./helpers/markdown/index.js";
8
+ import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
9
+ import { resolveGovernanceDir } from "./projitive.js";
10
+ import { loadTasks } from "./tasks.js";
11
+ export const ROADMAP_ID_REGEX = /^ROADMAP-\d{4}$/;
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;
55
+ }
56
+ export function collectRoadmapLintSuggestions(roadmapIds, tasks) {
57
+ return renderLintSuggestions(collectRoadmapLintSuggestionItems(roadmapIds, tasks));
58
+ }
59
+ export function isValidRoadmapId(id) {
60
+ return ROADMAP_ID_REGEX.test(id);
61
+ }
62
+ async function readRoadmapIds(governanceDir) {
63
+ const roadmapPath = path.join(governanceDir, "roadmap.md");
64
+ try {
65
+ const markdown = await fs.readFile(roadmapPath, "utf-8");
66
+ const matches = markdown.match(/ROADMAP-\d{4}/g) ?? [];
67
+ return Array.from(new Set(matches));
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ export function registerRoadmapTools(server) {
74
+ server.registerTool("roadmapList", {
75
+ title: "Roadmap List",
76
+ description: "List roadmap IDs and related tasks for project planning",
77
+ inputSchema: {
78
+ projectPath: z.string(),
79
+ },
80
+ }, async ({ projectPath }) => {
81
+ const governanceDir = await resolveGovernanceDir(projectPath);
82
+ const roadmapIds = await readRoadmapIds(governanceDir);
83
+ const { tasks } = await loadTasks(governanceDir);
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
+ });
106
+ return asText(markdown);
107
+ });
108
+ server.registerTool("roadmapContext", {
109
+ title: "Roadmap Context",
110
+ description: "Get one roadmap with related tasks, references, and execution context",
111
+ inputSchema: {
112
+ projectPath: z.string(),
113
+ roadmapId: z.string(),
114
+ },
115
+ }, async ({ projectPath, roadmapId }) => {
116
+ if (!isValidRoadmapId(roadmapId)) {
117
+ return {
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\")`)),
119
+ isError: true,
120
+ };
121
+ }
122
+ const governanceDir = await resolveGovernanceDir(projectPath);
123
+ const artifacts = await discoverGovernanceArtifacts(governanceDir);
124
+ const fileCandidates = candidateFilesFromArtifacts(artifacts);
125
+ const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, roadmapId)))).flat();
126
+ const { tasks } = await loadTasks(governanceDir);
127
+ const relatedTasks = tasks.filter((task) => task.roadmapRefs.includes(roadmapId));
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
+ });
163
+ return asText(markdown);
164
+ });
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
+ });