@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.
- package/README.md +44 -20
- 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 +21 -2
- package/output/projitive.test.js +1 -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 +100 -80
- package/output/tasks.test.js +32 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Language: English | [简体中文](README_CN.md)
|
|
4
4
|
|
|
5
|
-
**Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.
|
|
5
|
+
**Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.3**
|
|
6
6
|
|
|
7
7
|
Projitive MCP server (semantic interface edition) helps agents discover projects, select tasks, locate evidence, and execute under governance workflows.
|
|
8
8
|
|
|
@@ -50,6 +50,17 @@ taskNext
|
|
|
50
50
|
-> taskNext (next cycle)
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
When no actionable task exists (`actionableTasks: 0`), use bootstrap path:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
taskNext
|
|
57
|
+
-> projectContext
|
|
58
|
+
-> create 1-3 TODO tasks in tasks.md marker block (from roadmap/readme/report gaps)
|
|
59
|
+
-> taskNext
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Optional customization: add `hooks/task_no_actionable.md` in governance root to override the default no-task discovery checklist.
|
|
63
|
+
|
|
53
64
|
When the agent starts inside a project:
|
|
54
65
|
|
|
55
66
|
```text
|
|
@@ -58,17 +69,43 @@ projectLocate -> projectContext -> taskList -> taskContext
|
|
|
58
69
|
|
|
59
70
|
## Quick Start
|
|
60
71
|
|
|
72
|
+
Use npm package directly in MCP client configuration:
|
|
73
|
+
|
|
61
74
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
npx -y @projitive/mcp
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
MCP client config example (`mcp.json`):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"projitive": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": ["-y", "@projitive/mcp"],
|
|
86
|
+
"env": {
|
|
87
|
+
"PROJITIVE_SCAN_ROOT_PATH": "/absolute/path/to/your/workspace",
|
|
88
|
+
"PROJITIVE_SCAN_MAX_DEPTH": "3"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
66
93
|
```
|
|
67
94
|
|
|
68
|
-
|
|
95
|
+
Environment variables:
|
|
96
|
+
|
|
97
|
+
- `PROJITIVE_SCAN_ROOT_PATH`: fallback scan root for discovery methods when `rootPath` is omitted.
|
|
98
|
+
- `PROJITIVE_SCAN_MAX_DEPTH`: fallback scan depth when `maxDepth` is omitted (`0-8`, default `3`).
|
|
99
|
+
|
|
100
|
+
Local path startup is not the recommended usage mode in this README.
|
|
101
|
+
|
|
102
|
+
For maintainers/contributors only:
|
|
69
103
|
|
|
70
104
|
```bash
|
|
71
|
-
|
|
105
|
+
cd packages/mcp
|
|
106
|
+
npm ci
|
|
107
|
+
npm run build
|
|
108
|
+
npm run test
|
|
72
109
|
```
|
|
73
110
|
|
|
74
111
|
## Spec Version
|
|
@@ -332,17 +369,9 @@ node /absolute/path/to/packages/mcp/output/index.js
|
|
|
332
369
|
|
|
333
370
|
- **Purpose**: return task detail + related evidence locations in one call (replacing `trace.references`).
|
|
334
371
|
- **Input**: `projectPath`, `taskId`
|
|
335
|
-
- **HOOK Injection**:
|
|
336
|
-
- If `hooks/task_get_head.md` exists, its content is prepended to result.
|
|
337
|
-
- If `hooks/task_get_footer.md` exists, its content is appended to result.
|
|
338
|
-
- Used for project-level custom guidance without changing core `taskContext` shape.
|
|
339
372
|
- **Output Example (Markdown)**:
|
|
340
373
|
|
|
341
374
|
```markdown
|
|
342
|
-
[hooks/task_get_head.md content (if present)]
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
375
|
# taskContext
|
|
347
376
|
|
|
348
377
|
## Summary
|
|
@@ -354,7 +383,6 @@ node /absolute/path/to/packages/mcp/output/index.js
|
|
|
354
383
|
- updatedAt: 2026-02-17T12:00:00.000Z
|
|
355
384
|
- roadmapRefs: ROADMAP-0001
|
|
356
385
|
- taskLocation: /workspace/proj-a/tasks.md#L42
|
|
357
|
-
- hookStatus: head=loaded, footer=missing
|
|
358
386
|
|
|
359
387
|
## Evidence
|
|
360
388
|
### Related Artifacts
|
|
@@ -379,10 +407,6 @@ node /absolute/path/to/packages/mcp/output/index.js
|
|
|
379
407
|
|
|
380
408
|
## Next Call
|
|
381
409
|
- taskContext(projectPath="/workspace/proj-a", taskId="TASK-0003")
|
|
382
|
-
|
|
383
|
-
---
|
|
384
|
-
|
|
385
|
-
[hooks/task_get_footer.md content (if present)]
|
|
386
410
|
```
|
|
387
411
|
|
|
388
412
|
### Roadmap Layer
|
package/output/hooks.js
CHANGED
|
@@ -30,20 +30,7 @@ async function readHookFile(filePath) {
|
|
|
30
30
|
}
|
|
31
31
|
return { ok: true, content };
|
|
32
32
|
}
|
|
33
|
-
export async function resolveHookForEvent(governanceDir,
|
|
34
|
-
const taskHookPath = task.hooks[event];
|
|
35
|
-
if (taskHookPath) {
|
|
36
|
-
const resolvedTaskPath = path.resolve(governanceDir, taskHookPath);
|
|
37
|
-
const taskFile = await readHookFile(resolvedTaskPath);
|
|
38
|
-
if (taskFile.ok) {
|
|
39
|
-
return {
|
|
40
|
-
event,
|
|
41
|
-
source: "task",
|
|
42
|
-
path: resolvedTaskPath,
|
|
43
|
-
content: taskFile.content,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
33
|
+
export async function resolveHookForEvent(governanceDir, event) {
|
|
47
34
|
const globalPath = path.join(governanceDir, "hooks", GLOBAL_EVENT_FILES[event]);
|
|
48
35
|
const globalFile = await readHookFile(globalPath);
|
|
49
36
|
if (globalFile.ok) {
|
package/output/hooks.test.js
CHANGED
|
@@ -3,7 +3,6 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
5
|
import { detectHookEvent, resolveHookForEvent } from "./hooks.js";
|
|
6
|
-
import { normalizeTask } from "./tasks.js";
|
|
7
6
|
const tempPaths = [];
|
|
8
7
|
async function createTempDir() {
|
|
9
8
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
|
|
@@ -22,30 +21,20 @@ describe("hooks module", () => {
|
|
|
22
21
|
expect(detectHookEvent("IN_PROGRESS", "BLOCKED")).toBe("onBlocked");
|
|
23
22
|
expect(detectHookEvent("BLOCKED", "TODO")).toBe(null);
|
|
24
23
|
});
|
|
25
|
-
it("
|
|
24
|
+
it("loads global hook when global file exists", async () => {
|
|
26
25
|
const root = await createTempDir();
|
|
27
26
|
const governanceDir = path.join(root, "gov");
|
|
28
27
|
await fs.mkdir(path.join(governanceDir, "hooks"), { recursive: true });
|
|
29
28
|
await fs.writeFile(path.join(governanceDir, "hooks", "on_task_assigned.md"), "global assigned", "utf-8");
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
title: "Task",
|
|
34
|
-
status: "TODO",
|
|
35
|
-
hooks: { onAssigned: "./hooks/custom-assigned.md" },
|
|
36
|
-
});
|
|
37
|
-
const hook = await resolveHookForEvent(governanceDir, task, "onAssigned");
|
|
38
|
-
expect(hook.source).toBe("task");
|
|
39
|
-
expect(hook.content).toContain("task assigned");
|
|
29
|
+
const hook = await resolveHookForEvent(governanceDir, "onAssigned");
|
|
30
|
+
expect(hook.source).toBe("global");
|
|
31
|
+
expect(hook.content).toContain("global assigned");
|
|
40
32
|
});
|
|
41
|
-
it("
|
|
33
|
+
it("returns none when global hook is missing", async () => {
|
|
42
34
|
const root = await createTempDir();
|
|
43
35
|
const governanceDir = path.join(root, "gov");
|
|
44
36
|
await fs.mkdir(path.join(governanceDir, "hooks"), { recursive: true });
|
|
45
|
-
await
|
|
46
|
-
|
|
47
|
-
const hook = await resolveHookForEvent(governanceDir, task, "onCompleted");
|
|
48
|
-
expect(hook.source).toBe("global");
|
|
49
|
-
expect(hook.content).toContain("global completed");
|
|
37
|
+
const hook = await resolveHookForEvent(governanceDir, "onCompleted");
|
|
38
|
+
expect(hook.source).toBe("none");
|
|
50
39
|
});
|
|
51
40
|
});
|
package/output/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import process from "node:process";
|
|
@@ -10,14 +11,29 @@ import { registerProjectTools } from "./projitive.js";
|
|
|
10
11
|
import { registerTaskTools } from "./tasks.js";
|
|
11
12
|
import { registerRoadmapTools } from "./roadmap.js";
|
|
12
13
|
const PROJITIVE_SPEC_VERSION = "1.0.0";
|
|
14
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
15
|
+
const sourceDir = path.dirname(currentFilePath);
|
|
16
|
+
const repoRoot = path.resolve(sourceDir, "..", "..", "..");
|
|
17
|
+
function resolveRuntimeVersion() {
|
|
18
|
+
const packageJsonPath = path.resolve(sourceDir, "..", "package.json");
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(packageJsonPath, "utf-8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
|
|
23
|
+
return parsed.version.trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// fallback handled below
|
|
28
|
+
}
|
|
29
|
+
return PROJITIVE_SPEC_VERSION;
|
|
30
|
+
}
|
|
31
|
+
const MCP_RUNTIME_VERSION = resolveRuntimeVersion();
|
|
13
32
|
const server = new McpServer({
|
|
14
33
|
name: "projitive",
|
|
15
|
-
version:
|
|
34
|
+
version: MCP_RUNTIME_VERSION,
|
|
16
35
|
description: "Semantic Projitive MCP for project/task discovery and agent guidance with markdown-first outputs",
|
|
17
36
|
});
|
|
18
|
-
const currentFilePath = fileURLToPath(import.meta.url);
|
|
19
|
-
const sourceDir = path.dirname(currentFilePath);
|
|
20
|
-
const repoRoot = path.resolve(sourceDir, "..", "..", "..");
|
|
21
37
|
function resolveRepoFile(relativePath) {
|
|
22
38
|
return path.join(repoRoot, relativePath);
|
|
23
39
|
}
|
|
@@ -170,7 +186,7 @@ function registerGovernancePrompts() {
|
|
|
170
186
|
"Checklist:",
|
|
171
187
|
"- Transition is valid per status machine.",
|
|
172
188
|
"- links/roadmapRefs remain parseable and consistent.",
|
|
173
|
-
"-
|
|
189
|
+
"- Only `hooks/task_no_actionable.md` is used as global background hook for no-task discovery.",
|
|
174
190
|
].join("\n");
|
|
175
191
|
return asUserPrompt(text);
|
|
176
192
|
});
|
|
@@ -200,6 +216,8 @@ registerRoadmapTools(server);
|
|
|
200
216
|
registerGovernanceResources();
|
|
201
217
|
registerGovernancePrompts();
|
|
202
218
|
async function main() {
|
|
219
|
+
console.error(`[projitive-mcp] starting server`);
|
|
220
|
+
console.error(`[projitive-mcp] version=${MCP_RUNTIME_VERSION} spec=${PROJITIVE_SPEC_VERSION} transport=stdio pid=${process.pid}`);
|
|
203
221
|
const transport = new StdioServerTransport();
|
|
204
222
|
await server.connect(transport);
|
|
205
223
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@projitive/mcp",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Projitive MCP Server for project and task discovery/update",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp": "output/index.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "./output/index.js",
|
|
12
|
+
"types": "./output/index.d.ts",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"output"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.17.5",
|
|
28
|
+
"zod": "^3.23.8"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^24.3.0",
|
|
32
|
+
"tsx": "^4.20.5",
|
|
33
|
+
"typescript": "^5.9.2",
|
|
34
|
+
"vitest": "^3.2.4"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/output/projitive.js
CHANGED
|
@@ -201,12 +201,30 @@ function defaultTasksMarkdown() {
|
|
|
201
201
|
"- owner: unassigned",
|
|
202
202
|
"- summary: Create initial governance artifacts and confirm task execution loop.",
|
|
203
203
|
`- updatedAt: ${updatedAt}`,
|
|
204
|
-
"- links:",
|
|
205
204
|
"- roadmapRefs: ROADMAP-0001",
|
|
206
|
-
"-
|
|
205
|
+
"- links:",
|
|
206
|
+
" - (none)",
|
|
207
207
|
"<!-- PROJITIVE:TASKS:END -->",
|
|
208
208
|
].join("\n");
|
|
209
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
|
+
}
|
|
210
228
|
export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
|
|
211
229
|
const rootPath = normalizePath(inputPath);
|
|
212
230
|
const governanceDirName = normalizeGovernanceDirName(governanceDir);
|
|
@@ -231,6 +249,7 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
|
|
|
231
249
|
writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
|
|
232
250
|
writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(), force),
|
|
233
251
|
writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(), force),
|
|
252
|
+
writeTextFile(path.join(governancePath, "hooks", "task_no_actionable.md"), defaultNoTaskDiscoveryHookMarkdown(), force),
|
|
234
253
|
]);
|
|
235
254
|
return {
|
|
236
255
|
rootPath,
|
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,38 @@
|
|
|
1
|
+
import { isValidRoadmapId } from "./roadmap.js";
|
|
2
|
+
import { isValidTaskId } from "./tasks.js";
|
|
3
|
+
export function parseDesignMetadata(markdown) {
|
|
4
|
+
const lines = markdown.split(/\r?\n/);
|
|
5
|
+
const metadata = {};
|
|
6
|
+
for (const line of lines) {
|
|
7
|
+
const [rawKey, ...rawValue] = line.split(":");
|
|
8
|
+
if (!rawKey || rawValue.length === 0) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const key = rawKey.trim().toLowerCase();
|
|
12
|
+
const value = rawValue.join(":").trim();
|
|
13
|
+
if (key === "task")
|
|
14
|
+
metadata.task = value;
|
|
15
|
+
if (key === "roadmap")
|
|
16
|
+
metadata.roadmap = value;
|
|
17
|
+
if (key === "owner")
|
|
18
|
+
metadata.owner = value;
|
|
19
|
+
if (key === "status")
|
|
20
|
+
metadata.status = value;
|
|
21
|
+
if (key === "last updated")
|
|
22
|
+
metadata.lastUpdated = value;
|
|
23
|
+
}
|
|
24
|
+
return metadata;
|
|
25
|
+
}
|
|
26
|
+
export function validateDesignMetadata(metadata) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
if (!metadata.task) {
|
|
29
|
+
errors.push("Missing Task metadata");
|
|
30
|
+
}
|
|
31
|
+
else if (!isValidTaskId(metadata.task)) {
|
|
32
|
+
errors.push(`Invalid Task metadata format: ${metadata.task}`);
|
|
33
|
+
}
|
|
34
|
+
if (metadata.roadmap && !isValidRoadmapId(metadata.roadmap)) {
|
|
35
|
+
errors.push(`Invalid Roadmap metadata format: ${metadata.roadmap}`);
|
|
36
|
+
}
|
|
37
|
+
return { ok: errors.length === 0, errors };
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function candidateFilesFromArtifacts(artifacts) {
|
|
2
|
+
return artifacts
|
|
3
|
+
.filter((item) => item.exists)
|
|
4
|
+
.flatMap((item) => {
|
|
5
|
+
if (item.kind === "file") {
|
|
6
|
+
return [item.path];
|
|
7
|
+
}
|
|
8
|
+
return (item.markdownFiles ?? []).map((entry) => entry.path);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { candidateFilesFromArtifacts } from "./artifacts.js";
|
|
3
|
+
describe("candidateFilesFromArtifacts", () => {
|
|
4
|
+
it("collects existing file artifacts and markdown files from existing directories", () => {
|
|
5
|
+
const candidates = candidateFilesFromArtifacts([
|
|
6
|
+
{ name: "README.md", kind: "file", path: "/a/README.md", exists: true, lineCount: 3 },
|
|
7
|
+
{ name: "tasks.md", kind: "file", path: "/a/tasks.md", exists: false },
|
|
8
|
+
{
|
|
9
|
+
name: "designs",
|
|
10
|
+
kind: "directory",
|
|
11
|
+
path: "/a/designs",
|
|
12
|
+
exists: true,
|
|
13
|
+
markdownFiles: [{ path: "/a/designs/d1.md", lineCount: 10 }],
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
16
|
+
expect(candidates).toEqual(["/a/README.md", "/a/designs/d1.md"]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./artifacts.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 辅助函数:检查是否为 PromiseLike
|
|
2
|
+
function isPromiseLike(value) {
|
|
3
|
+
return value != null && typeof value === 'object' && 'then' in value && typeof value.then === 'function';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 构造成功结果对象
|
|
7
|
+
* isError 始终返回 false
|
|
8
|
+
*/
|
|
9
|
+
function createSuccess(value) {
|
|
10
|
+
return {
|
|
11
|
+
value,
|
|
12
|
+
error: undefined,
|
|
13
|
+
isError() { return false; }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 构造失败结果对象
|
|
18
|
+
* isError 始终返回 true
|
|
19
|
+
*/
|
|
20
|
+
function createFailure(error) {
|
|
21
|
+
return {
|
|
22
|
+
error,
|
|
23
|
+
value: undefined,
|
|
24
|
+
isError() { return true; }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function catchIt(input) {
|
|
28
|
+
try {
|
|
29
|
+
if (isPromiseLike(input)) {
|
|
30
|
+
const result = await Promise.resolve(input);
|
|
31
|
+
return createSuccess(result);
|
|
32
|
+
}
|
|
33
|
+
else if (typeof input === 'function') {
|
|
34
|
+
const result = input();
|
|
35
|
+
if (isPromiseLike(result)) {
|
|
36
|
+
return catchIt(result);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return createSuccess(result);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return createFailure(error);
|
|
45
|
+
}
|
|
46
|
+
// 理论上不会到达这里,兜底类型安全
|
|
47
|
+
return createFailure(new Error('Unexpected input type'));
|
|
48
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { catchIt } from './catch.js';
|
|
3
|
+
describe('catchIt', () => {
|
|
4
|
+
it('同步函数返回值应为 value,error 为 undefined,isError 为 false', async () => {
|
|
5
|
+
const result = await catchIt(() => 123);
|
|
6
|
+
expect(result.value).toBe(123);
|
|
7
|
+
expect(result.error).toBeUndefined();
|
|
8
|
+
expect(result.isError()).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('异步函数返回值应为 value,error 为 undefined,isError 为 false', async () => {
|
|
11
|
+
const result = await catchIt(async () => 456);
|
|
12
|
+
expect(result.value).toBe(456);
|
|
13
|
+
expect(result.error).toBeUndefined();
|
|
14
|
+
expect(result.isError()).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
it('同步抛出异常时应返回 error,value 为 undefined,isError 为 true', async () => {
|
|
17
|
+
const error = new Error('fail');
|
|
18
|
+
const result = await catchIt(() => { throw error; });
|
|
19
|
+
expect(result.value).toBeUndefined();
|
|
20
|
+
expect(result.error).toBe(error);
|
|
21
|
+
expect(result.isError()).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it('异步抛出异常时应返回 error,value 为 undefined,isError 为 true', async () => {
|
|
24
|
+
const error = new Error('fail-async');
|
|
25
|
+
const result = await catchIt(() => Promise.reject(error));
|
|
26
|
+
expect(result.value).toBeUndefined();
|
|
27
|
+
expect(result.error).toBe(error);
|
|
28
|
+
expect(result.isError()).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('PromiseLike resolve 时应返回 value,error 为 undefined,isError 为 false', async () => {
|
|
31
|
+
const result = await catchIt(Promise.resolve('ok'));
|
|
32
|
+
expect(result.value).toBe('ok');
|
|
33
|
+
expect(result.error).toBeUndefined();
|
|
34
|
+
expect(result.isError()).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it('PromiseLike reject 时应返回 error,value 为 undefined,isError 为 true', async () => {
|
|
37
|
+
const error = new Error('promise-fail');
|
|
38
|
+
const result = await catchIt(Promise.reject(error));
|
|
39
|
+
expect(result.value).toBeUndefined();
|
|
40
|
+
expect(result.error).toBe(error);
|
|
41
|
+
expect(result.isError()).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './catch.js';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { catchIt } from "../catch/index.js";
|
|
4
|
+
const FILE_ARTIFACTS = ["README.md", "roadmap.md", "tasks.md"];
|
|
5
|
+
const DIRECTORY_ARTIFACTS = ["designs", "reports", "hooks"];
|
|
6
|
+
async function fileLineCount(filePath) {
|
|
7
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
8
|
+
if (!content) {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
return content.split(/\r?\n/).length;
|
|
12
|
+
}
|
|
13
|
+
async function listMarkdownFiles(dirPath) {
|
|
14
|
+
const entriesResult = await catchIt(fs.readdir(dirPath, { withFileTypes: true }));
|
|
15
|
+
if (entriesResult.isError()) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const entries = entriesResult.value;
|
|
19
|
+
const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"));
|
|
20
|
+
const result = [];
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const fullPath = path.join(dirPath, file.name);
|
|
23
|
+
result.push({ path: fullPath, lineCount: await fileLineCount(fullPath) });
|
|
24
|
+
}
|
|
25
|
+
return result.sort((a, b) => a.path.localeCompare(b.path));
|
|
26
|
+
}
|
|
27
|
+
export async function discoverGovernanceArtifacts(governanceDir) {
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const artifact of FILE_ARTIFACTS) {
|
|
30
|
+
const artifactPath = path.join(governanceDir, artifact);
|
|
31
|
+
const accessResult = await catchIt(fs.access(artifactPath));
|
|
32
|
+
if (!accessResult.isError()) {
|
|
33
|
+
result.push({
|
|
34
|
+
name: artifact,
|
|
35
|
+
kind: "file",
|
|
36
|
+
path: artifactPath,
|
|
37
|
+
exists: true,
|
|
38
|
+
lineCount: await fileLineCount(artifactPath),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
result.push({ name: artifact, kind: "file", path: artifactPath, exists: false });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const artifact of DIRECTORY_ARTIFACTS) {
|
|
46
|
+
const artifactPath = path.join(governanceDir, artifact);
|
|
47
|
+
const accessResult = await catchIt(fs.access(artifactPath));
|
|
48
|
+
if (!accessResult.isError()) {
|
|
49
|
+
result.push({
|
|
50
|
+
name: artifact,
|
|
51
|
+
kind: "directory",
|
|
52
|
+
path: artifactPath,
|
|
53
|
+
exists: true,
|
|
54
|
+
markdownFiles: await listMarkdownFiles(artifactPath),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
result.push({ name: artifact, kind: "directory", path: artifactPath, exists: false, markdownFiles: [] });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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 } from "vitest";
|
|
5
|
+
import { discoverGovernanceArtifacts } from "./files.js";
|
|
6
|
+
const tempPaths = [];
|
|
7
|
+
async function createTempDir() {
|
|
8
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
|
|
9
|
+
tempPaths.push(dir);
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await Promise.all(tempPaths.splice(0).map(async (dir) => {
|
|
14
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
15
|
+
}));
|
|
16
|
+
});
|
|
17
|
+
describe("files module", () => {
|
|
18
|
+
it("discovers governance artifacts with paths and line counts", async () => {
|
|
19
|
+
const root = await createTempDir();
|
|
20
|
+
await fs.writeFile(path.join(root, "README.md"), "# Readme\n", "utf-8");
|
|
21
|
+
await fs.writeFile(path.join(root, "tasks.md"), "# Tasks\n## TODO\n", "utf-8");
|
|
22
|
+
await fs.mkdir(path.join(root, "designs"), { recursive: true });
|
|
23
|
+
await fs.writeFile(path.join(root, "designs", "feature-design.md"), "# Design\n", "utf-8");
|
|
24
|
+
const artifacts = await discoverGovernanceArtifacts(root);
|
|
25
|
+
const readme = artifacts.find((item) => item.name === "README.md");
|
|
26
|
+
const designs = artifacts.find((item) => item.name === "designs");
|
|
27
|
+
expect(readme?.exists).toBe(true);
|
|
28
|
+
expect(readme?.lineCount).toBe(2);
|
|
29
|
+
expect(designs?.exists).toBe(true);
|
|
30
|
+
expect(designs?.markdownFiles?.[0].path.endsWith("feature-design.md")).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './files.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const TASK_LINT_CODES = {
|
|
2
|
+
DUPLICATE_ID: "TASK_DUPLICATE_ID",
|
|
3
|
+
IN_PROGRESS_OWNER_EMPTY: "TASK_IN_PROGRESS_OWNER_EMPTY",
|
|
4
|
+
DONE_LINKS_MISSING: "TASK_DONE_LINKS_MISSING",
|
|
5
|
+
BLOCKED_SUMMARY_EMPTY: "TASK_BLOCKED_SUMMARY_EMPTY",
|
|
6
|
+
UPDATED_AT_INVALID: "TASK_UPDATED_AT_INVALID",
|
|
7
|
+
ROADMAP_REFS_EMPTY: "TASK_ROADMAP_REFS_EMPTY",
|
|
8
|
+
OUTSIDE_MARKER: "TASK_OUTSIDE_MARKER",
|
|
9
|
+
LINK_TARGET_MISSING: "TASK_LINK_TARGET_MISSING",
|
|
10
|
+
HOOK_FILE_MISSING: "TASK_HOOK_FILE_MISSING",
|
|
11
|
+
FILTER_EMPTY: "TASK_FILTER_EMPTY",
|
|
12
|
+
CONTEXT_HOOK_HEAD_MISSING: "TASK_CONTEXT_HOOK_HEAD_MISSING",
|
|
13
|
+
CONTEXT_HOOK_FOOTER_MISSING: "TASK_CONTEXT_HOOK_FOOTER_MISSING",
|
|
14
|
+
};
|
|
15
|
+
export const ROADMAP_LINT_CODES = {
|
|
16
|
+
IDS_EMPTY: "ROADMAP_IDS_EMPTY",
|
|
17
|
+
TASKS_EMPTY: "ROADMAP_TASKS_EMPTY",
|
|
18
|
+
TASK_REFS_EMPTY: "ROADMAP_TASK_REFS_EMPTY",
|
|
19
|
+
UNKNOWN_REFS: "ROADMAP_UNKNOWN_REFS",
|
|
20
|
+
ZERO_LINKED_TASKS: "ROADMAP_ZERO_LINKED_TASKS",
|
|
21
|
+
CONTEXT_RELATED_TASKS_EMPTY: "ROADMAP_CONTEXT_RELATED_TASKS_EMPTY",
|
|
22
|
+
};
|
|
23
|
+
export const PROJECT_LINT_CODES = {
|
|
24
|
+
TASKS_FILE_MISSING: "PROJECT_TASKS_FILE_MISSING",
|
|
25
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderLintSuggestions } from "./linter.js";
|
|
3
|
+
describe("renderLintSuggestions", () => {
|
|
4
|
+
it("renders lint lines with code and message", () => {
|
|
5
|
+
const lines = renderLintSuggestions([
|
|
6
|
+
{ code: "TASK_001", message: "Example lint" },
|
|
7
|
+
]);
|
|
8
|
+
expect(lines).toEqual(["- [TASK_001] Example lint"]);
|
|
9
|
+
});
|
|
10
|
+
it("appends fixHint when provided", () => {
|
|
11
|
+
const lines = renderLintSuggestions([
|
|
12
|
+
{ code: "TASK_002", message: "Missing field.", fixHint: "Set owner." },
|
|
13
|
+
]);
|
|
14
|
+
expect(lines).toEqual(["- [TASK_002] Missing field. Set owner."]);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './markdown.js';
|