@projitive/mcp 1.0.0-beta.1
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 +318 -0
- package/output/designs.js +38 -0
- package/output/helpers/catch/catch.js +48 -0
- package/output/helpers/catch/catch.test.js +43 -0
- package/output/helpers/catch/index.js +1 -0
- package/output/helpers/files/files.js +62 -0
- package/output/helpers/files/files.test.js +32 -0
- package/output/helpers/files/index.js +1 -0
- package/output/helpers/index.js +3 -0
- package/output/helpers/markdown/index.js +1 -0
- package/output/helpers/markdown/markdown.js +33 -0
- package/output/helpers/markdown/markdown.test.js +36 -0
- package/output/hooks.js +62 -0
- package/output/hooks.test.js +51 -0
- package/output/index.js +562 -0
- package/output/projitive.js +55 -0
- package/output/projitive.test.js +46 -0
- package/output/readme.js +26 -0
- package/output/reports.js +36 -0
- package/output/roadmap.js +4 -0
- package/output/tasks.js +242 -0
- package/output/tasks.test.js +78 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# @projitive/mcp
|
|
2
|
+
|
|
3
|
+
**Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.0**
|
|
4
|
+
|
|
5
|
+
Projitive MCP server(语义化接口设计版),用于帮助 Agent 发现项目、发现任务、定位证据,并按治理流程推进。
|
|
6
|
+
|
|
7
|
+
## 规范版本
|
|
8
|
+
|
|
9
|
+
- 当前遵循规范版本:`projitive-spec v1.0.0`
|
|
10
|
+
- 说明:Projitive 是通用治理规范,MCP 只是该规范的一种工具实现。
|
|
11
|
+
- 对齐规则:MCP 的主版本必须与规范主版本一致(当前均为 `v1.x`)
|
|
12
|
+
|
|
13
|
+
## 设计边界
|
|
14
|
+
|
|
15
|
+
- MCP 负责:发现、定位、汇总、推进指导。
|
|
16
|
+
- AI 负责:读取与更新 Markdown 正文内容。
|
|
17
|
+
- MCP 不提供:`task.update_*`、`roadmap.update_*`、`sync_*` 这类直接写入治理工件的方法。
|
|
18
|
+
- 所有工具返回内容:面向 Agent 的 Markdown(不是 JSON 对象)。
|
|
19
|
+
- 输出结构统一为:`Summary` / `Evidence` / `Agent Guidance`。
|
|
20
|
+
- 错误结构统一为:`Error` / `Next Step`。
|
|
21
|
+
|
|
22
|
+
## Tools Methods
|
|
23
|
+
|
|
24
|
+
### 发现层
|
|
25
|
+
|
|
26
|
+
#### `project.next`
|
|
27
|
+
|
|
28
|
+
- **作用**:直接拉取最近可推进的项目(按可执行任务数和最近更新时间排序)。
|
|
29
|
+
- **输入**:`rootPath?`、`maxDepth?`、`limit?`
|
|
30
|
+
- **输出示例(Markdown)**:
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
# project.next
|
|
34
|
+
|
|
35
|
+
## Summary
|
|
36
|
+
- rootPath: /workspace
|
|
37
|
+
- maxDepth: 3
|
|
38
|
+
- matchedProjects: 8
|
|
39
|
+
- actionableProjects: 3
|
|
40
|
+
- limit: 10
|
|
41
|
+
|
|
42
|
+
## Evidence
|
|
43
|
+
- rankedProjects:
|
|
44
|
+
1. /workspace/proj-a | actionable=5 | in_progress=2 | todo=3 | blocked=1 | done=4 | latest=2026-02-17T12:00:00.000Z | tasksPath=/workspace/proj-a/tasks.md
|
|
45
|
+
2. /workspace/proj-b | actionable=3 | in_progress=1 | todo=2 | blocked=0 | done=7 | latest=2026-02-16T09:00:00.000Z | tasksPath=/workspace/proj-b/tasks.md
|
|
46
|
+
|
|
47
|
+
## Agent Guidance
|
|
48
|
+
- Pick top 1 project and call `project.overview` with its governanceDir.
|
|
49
|
+
- Then call `task.list` and `task.get` to continue execution.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### `project.scan`
|
|
53
|
+
|
|
54
|
+
- **作用**:扫描目录并发现可治理项目。
|
|
55
|
+
- **输入**:`rootPath?`、`maxDepth?`
|
|
56
|
+
- **输出示例(Markdown)**:
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
# project.scan
|
|
60
|
+
|
|
61
|
+
## Summary
|
|
62
|
+
- rootPath: /workspace
|
|
63
|
+
- maxDepth: 3
|
|
64
|
+
- discoveredCount: 2
|
|
65
|
+
|
|
66
|
+
## Evidence
|
|
67
|
+
- projects:
|
|
68
|
+
1. /workspace/proj-a
|
|
69
|
+
2. /workspace/proj-b
|
|
70
|
+
|
|
71
|
+
## Agent Guidance
|
|
72
|
+
- Next: call `project.locate` with one target path to lock the active governance root.
|
|
73
|
+
- Then: call `project.overview` to view artifact and task status.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### `project.locate`
|
|
77
|
+
|
|
78
|
+
- **作用**:当 Agent 已经在某个项目目录内时,向上定位最近 `.projitive`,确定当前项目治理根目录。
|
|
79
|
+
- **输入**:`inputPath`
|
|
80
|
+
- **输出示例(Markdown)**:
|
|
81
|
+
|
|
82
|
+
```markdown
|
|
83
|
+
# project.locate
|
|
84
|
+
|
|
85
|
+
## Summary
|
|
86
|
+
- resolvedFrom: /workspace/proj-a/packages/mcp
|
|
87
|
+
- governanceDir: /workspace/proj-a
|
|
88
|
+
- markerPath: /workspace/proj-a/.projitive
|
|
89
|
+
|
|
90
|
+
## Agent Guidance
|
|
91
|
+
- Next: call `project.overview` with this governanceDir to get task and roadmap summaries.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### `project.overview`
|
|
95
|
+
|
|
96
|
+
- **作用**:自动汇总治理状态,而不是只返回文件列表。
|
|
97
|
+
- **输入**:`projectPath`
|
|
98
|
+
- **输出示例(Markdown)**:
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
# project.overview
|
|
102
|
+
|
|
103
|
+
## Summary
|
|
104
|
+
- governanceDir: /workspace/proj-a
|
|
105
|
+
- tasksFile: /workspace/proj-a/tasks.md
|
|
106
|
+
- roadmapIds: 3
|
|
107
|
+
|
|
108
|
+
## Evidence
|
|
109
|
+
### Task Summary
|
|
110
|
+
- total: 12
|
|
111
|
+
- TODO: 4
|
|
112
|
+
- IN_PROGRESS: 3
|
|
113
|
+
- BLOCKED: 1
|
|
114
|
+
- DONE: 4
|
|
115
|
+
|
|
116
|
+
### Artifacts
|
|
117
|
+
- ✅ README.md
|
|
118
|
+
- ✅ roadmap.md
|
|
119
|
+
- ✅ tasks.md
|
|
120
|
+
- ✅ designs/
|
|
121
|
+
- ✅ reports/
|
|
122
|
+
- ✅ hooks/
|
|
123
|
+
|
|
124
|
+
## Agent Guidance
|
|
125
|
+
- Next: call `task.list` to choose a target task.
|
|
126
|
+
- Then: call `task.get` with a task ID to retrieve evidence locations and reading order.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 任务层
|
|
130
|
+
|
|
131
|
+
#### `task.next`
|
|
132
|
+
|
|
133
|
+
- **作用**:一步完成“发现项目 + 选择最可推进任务 + 返回证据定位与阅读顺序”,用于 Agent 直接开工。
|
|
134
|
+
- **输入**:`rootPath?`、`maxDepth?`、`topCandidates?`
|
|
135
|
+
- **输出示例(Markdown)**:
|
|
136
|
+
|
|
137
|
+
```markdown
|
|
138
|
+
# task.next
|
|
139
|
+
|
|
140
|
+
## Summary
|
|
141
|
+
- rootPath: /workspace
|
|
142
|
+
- maxDepth: 3
|
|
143
|
+
- matchedProjects: 8
|
|
144
|
+
- actionableTasks: 12
|
|
145
|
+
- selectedProject: /workspace/proj-a
|
|
146
|
+
- selectedTaskId: TASK-0003
|
|
147
|
+
- selectedTaskStatus: IN_PROGRESS
|
|
148
|
+
|
|
149
|
+
## Evidence
|
|
150
|
+
### Selected Task
|
|
151
|
+
- id: TASK-0003
|
|
152
|
+
- title: Build MCP tools
|
|
153
|
+
- taskLocation: /workspace/proj-a/tasks.md#L42
|
|
154
|
+
|
|
155
|
+
### Top Candidates
|
|
156
|
+
1. TASK-0003 | IN_PROGRESS | Build MCP tools | project=/workspace/proj-a | projectScore=6
|
|
157
|
+
2. TASK-0007 | TODO | Add docs examples | project=/workspace/proj-b | projectScore=5
|
|
158
|
+
|
|
159
|
+
### Suggested Read Order
|
|
160
|
+
1. /workspace/proj-a/tasks.md
|
|
161
|
+
2. /workspace/proj-a/designs/mcp-design.md
|
|
162
|
+
3. /workspace/proj-a/reports/mcp-progress.md
|
|
163
|
+
|
|
164
|
+
## Agent Guidance
|
|
165
|
+
- Start immediately with Suggested Read Order and execute the selected task.
|
|
166
|
+
- Re-run `task.get` for the selectedTaskId after edits to verify evidence consistency.
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- **推荐路径**:优先调用 `task.next`,避免 `project.next -> project.overview -> task.list -> task.get` 的多跳链路。
|
|
170
|
+
|
|
171
|
+
#### `task.list`
|
|
172
|
+
|
|
173
|
+
- **作用**:返回当前项目任务清单,支持按状态过滤与排序。
|
|
174
|
+
- **输入**:`projectPath`、`status?`、`limit?`
|
|
175
|
+
- **输出示例(Markdown)**:
|
|
176
|
+
|
|
177
|
+
```markdown
|
|
178
|
+
# task.list
|
|
179
|
+
|
|
180
|
+
## Summary
|
|
181
|
+
- governanceDir: /workspace/proj-a
|
|
182
|
+
- tasksPath: /workspace/proj-a/tasks.md
|
|
183
|
+
- filter.status: IN_PROGRESS
|
|
184
|
+
- returned: 2
|
|
185
|
+
|
|
186
|
+
## Evidence
|
|
187
|
+
- tasks:
|
|
188
|
+
- TASK-0003 | IN_PROGRESS | Build MCP tools | owner=alice | updatedAt=2026-02-17T12:00:00.000Z
|
|
189
|
+
- TASK-0007 | IN_PROGRESS | Add docs examples | owner=bob | updatedAt=2026-02-17T13:30:00.000Z
|
|
190
|
+
|
|
191
|
+
## Agent Guidance
|
|
192
|
+
- Next: pick one task ID and call `task.get`.
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### `task.get`
|
|
196
|
+
|
|
197
|
+
- **作用**:基于任务 ID 一次性返回任务详情 + 关联证据位置(替代 `trace.references`)。
|
|
198
|
+
- **输入**:`projectPath`、`taskId`
|
|
199
|
+
- **HOOKS 注入**:
|
|
200
|
+
- 若存在 `hooks/task_get_head.md`,其内容会自动追加到返回结果最前面。
|
|
201
|
+
- 若存在 `hooks/task_get_footer.md`,其内容会自动追加到返回结果最后面。
|
|
202
|
+
- 用于给 Agent 注入项目级自定义提示,不改变 task.get 核心结构。
|
|
203
|
+
- **输出示例(Markdown)**:
|
|
204
|
+
|
|
205
|
+
```markdown
|
|
206
|
+
[hooks/task_get_head.md 内容(如果存在)]
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
# task.get
|
|
211
|
+
|
|
212
|
+
## Summary
|
|
213
|
+
- governanceDir: /workspace/proj-a
|
|
214
|
+
- taskId: TASK-0003
|
|
215
|
+
- title: Build MCP tools
|
|
216
|
+
- status: IN_PROGRESS
|
|
217
|
+
- owner: alice
|
|
218
|
+
- updatedAt: 2026-02-17T12:00:00.000Z
|
|
219
|
+
- roadmapRefs: ROADMAP-0001
|
|
220
|
+
- taskLocation: /workspace/proj-a/tasks.md#L42
|
|
221
|
+
- hookStatus: head=loaded, footer=missing
|
|
222
|
+
|
|
223
|
+
## Evidence
|
|
224
|
+
### Related Artifacts
|
|
225
|
+
- /workspace/proj-a/tasks.md
|
|
226
|
+
- /workspace/proj-a/designs/mcp-design.md
|
|
227
|
+
- /workspace/proj-a/reports/mcp-progress.md
|
|
228
|
+
|
|
229
|
+
### Reference Locations
|
|
230
|
+
- /workspace/proj-a/tasks.md#L42: "id": "TASK-0003"
|
|
231
|
+
- /workspace/proj-a/designs/mcp-design.md#L18: Ref: TASK-0003
|
|
232
|
+
|
|
233
|
+
### Suggested Read Order
|
|
234
|
+
1. /workspace/proj-a/tasks.md
|
|
235
|
+
2. /workspace/proj-a/designs/mcp-design.md
|
|
236
|
+
3. /workspace/proj-a/reports/mcp-progress.md
|
|
237
|
+
|
|
238
|
+
## Agent Guidance
|
|
239
|
+
- Read the files in Suggested Read Order.
|
|
240
|
+
- Verify whether current status and evidence are consistent.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
[hooks/task_get_footer.md 内容(如果存在)]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 路线层
|
|
248
|
+
|
|
249
|
+
#### `roadmap.list`
|
|
250
|
+
|
|
251
|
+
- **作用**:列出路线图项目及其关联任务概览。
|
|
252
|
+
- **输入**:`projectPath`
|
|
253
|
+
- **输出示例(Markdown)**:
|
|
254
|
+
|
|
255
|
+
```markdown
|
|
256
|
+
# roadmap.list
|
|
257
|
+
|
|
258
|
+
## Summary
|
|
259
|
+
- governanceDir: /workspace/proj-a
|
|
260
|
+
- roadmapCount: 2
|
|
261
|
+
|
|
262
|
+
## Evidence
|
|
263
|
+
- roadmaps:
|
|
264
|
+
- ROADMAP-0001 | linkedTasks=6
|
|
265
|
+
- ROADMAP-0002 | linkedTasks=3
|
|
266
|
+
|
|
267
|
+
## Agent Guidance
|
|
268
|
+
- Next: call `roadmap.get` with a roadmap ID to inspect references and related tasks.
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### `roadmap.get`
|
|
272
|
+
|
|
273
|
+
- **作用**:获取单个路线图详情与证据定位信息。
|
|
274
|
+
- **输入**:`projectPath`、`roadmapId`
|
|
275
|
+
- **输出示例(Markdown)**:
|
|
276
|
+
|
|
277
|
+
```markdown
|
|
278
|
+
# roadmap.get
|
|
279
|
+
|
|
280
|
+
## Summary
|
|
281
|
+
- governanceDir: /workspace/proj-a
|
|
282
|
+
- roadmapId: ROADMAP-0001
|
|
283
|
+
- relatedTasks: 6
|
|
284
|
+
- references: 9
|
|
285
|
+
|
|
286
|
+
## Evidence
|
|
287
|
+
### Related Tasks
|
|
288
|
+
- TASK-0001 | DONE | Bootstrap governance
|
|
289
|
+
- TASK-0003 | IN_PROGRESS | Build MCP tools
|
|
290
|
+
|
|
291
|
+
### Reference Locations
|
|
292
|
+
- /workspace/proj-a/roadmap.md#L21: ROADMAP-0001
|
|
293
|
+
- /workspace/proj-a/tasks.md#L42: "roadmapRefs": ["ROADMAP-0001"]
|
|
294
|
+
|
|
295
|
+
## Agent Guidance
|
|
296
|
+
- Read roadmap references first, then related tasks.
|
|
297
|
+
- Keep ROADMAP/TASK IDs unchanged while updating markdown files.
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## 统一错误输出示例
|
|
301
|
+
|
|
302
|
+
```markdown
|
|
303
|
+
# task.get
|
|
304
|
+
|
|
305
|
+
## Error
|
|
306
|
+
- cause: Invalid task ID format: TASK-12
|
|
307
|
+
|
|
308
|
+
## Next Step
|
|
309
|
+
- expected format: TASK-0001
|
|
310
|
+
- retry with a valid task ID
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Agent 推荐调用流程
|
|
314
|
+
|
|
315
|
+
1. `task.next`:一步发现并选择最可推进任务,直接开工(默认路径)。
|
|
316
|
+
2. `task.get`:在需要更完整上下文时,按 taskId 拉取详细证据。
|
|
317
|
+
3. `project.next`:仅在需要“先选项目、再选任务”的项目级调度场景使用(可选)。
|
|
318
|
+
4. 当 Agent 已在项目内时,用 `project.locate` 快速定位当前项目治理根目录。
|
|
@@ -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,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 @@
|
|
|
1
|
+
export * from './markdown.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
export async function readMarkdownSections(filePath) {
|
|
3
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
4
|
+
const lines = content.split(/\r?\n/);
|
|
5
|
+
const headers = [];
|
|
6
|
+
lines.forEach((line, index) => {
|
|
7
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
8
|
+
if (match) {
|
|
9
|
+
headers.push({ level: match[1].length, heading: match[2].trim(), startLine: index + 1 });
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
const sections = headers.map((header, index) => {
|
|
13
|
+
const next = headers[index + 1];
|
|
14
|
+
return {
|
|
15
|
+
heading: header.heading,
|
|
16
|
+
level: header.level,
|
|
17
|
+
startLine: header.startLine,
|
|
18
|
+
endLine: next ? next.startLine - 1 : lines.length,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
return { filePath, lineCount: lines.length, sections };
|
|
22
|
+
}
|
|
23
|
+
export async function findTextReferences(filePath, needle) {
|
|
24
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
25
|
+
const lines = content.split(/\r?\n/);
|
|
26
|
+
const result = [];
|
|
27
|
+
lines.forEach((line, index) => {
|
|
28
|
+
if (line.includes(needle)) {
|
|
29
|
+
result.push({ filePath, line: index + 1, text: line.trim() });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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 { findTextReferences, readMarkdownSections } from "./markdown.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("markdown module", () => {
|
|
18
|
+
it("locates markdown sections with line ranges", async () => {
|
|
19
|
+
const root = await createTempDir();
|
|
20
|
+
const file = path.join(root, "tasks.md");
|
|
21
|
+
await fs.writeFile(file, ["# Tasks", "", "## TODO", "- TASK-0001", "## DONE", "- TASK-0002"].join("\n"), "utf-8");
|
|
22
|
+
const located = await readMarkdownSections(file);
|
|
23
|
+
expect(located.lineCount).toBe(6);
|
|
24
|
+
expect(located.sections[0].heading).toBe("Tasks");
|
|
25
|
+
expect(located.sections[1].heading).toBe("TODO");
|
|
26
|
+
expect(located.sections[1].startLine).toBe(3);
|
|
27
|
+
});
|
|
28
|
+
it("finds ID references with exact line number", async () => {
|
|
29
|
+
const root = await createTempDir();
|
|
30
|
+
const file = path.join(root, "reports.md");
|
|
31
|
+
await fs.writeFile(file, ["Task: TASK-0001", "Roadmap: ROADMAP-0001"].join("\n"), "utf-8");
|
|
32
|
+
const refs = await findTextReferences(file, "TASK-0001");
|
|
33
|
+
expect(refs).toHaveLength(1);
|
|
34
|
+
expect(refs[0].line).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
});
|
package/output/hooks.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { catchIt } from "./helpers/catch/index.js";
|
|
4
|
+
const GLOBAL_EVENT_FILES = {
|
|
5
|
+
onAssigned: "on_task_assigned.md",
|
|
6
|
+
onCompleted: "on_task_completed.md",
|
|
7
|
+
onBlocked: "on_task_blocked.md",
|
|
8
|
+
onReopened: "on_task_reopened.md",
|
|
9
|
+
};
|
|
10
|
+
export function detectHookEvent(from, to) {
|
|
11
|
+
if (to === "IN_PROGRESS") {
|
|
12
|
+
return from === "DONE" ? "onReopened" : "onAssigned";
|
|
13
|
+
}
|
|
14
|
+
if (to === "DONE") {
|
|
15
|
+
return "onCompleted";
|
|
16
|
+
}
|
|
17
|
+
if (to === "BLOCKED") {
|
|
18
|
+
return "onBlocked";
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
async function readHookFile(filePath) {
|
|
23
|
+
const contentResult = await catchIt(fs.readFile(filePath, "utf-8"));
|
|
24
|
+
if (contentResult.isError()) {
|
|
25
|
+
return { ok: false, warning: `Hook file not found or unreadable: ${filePath}` };
|
|
26
|
+
}
|
|
27
|
+
const content = contentResult.value;
|
|
28
|
+
if (content.trim().length === 0) {
|
|
29
|
+
return { ok: true, content: "" };
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, content };
|
|
32
|
+
}
|
|
33
|
+
export async function resolveHookForEvent(governanceDir, task, event) {
|
|
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
|
+
}
|
|
47
|
+
const globalPath = path.join(governanceDir, "hooks", GLOBAL_EVENT_FILES[event]);
|
|
48
|
+
const globalFile = await readHookFile(globalPath);
|
|
49
|
+
if (globalFile.ok) {
|
|
50
|
+
return {
|
|
51
|
+
event,
|
|
52
|
+
source: "global",
|
|
53
|
+
path: globalPath,
|
|
54
|
+
content: globalFile.content,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
event,
|
|
59
|
+
source: "none",
|
|
60
|
+
warning: globalFile.warning,
|
|
61
|
+
};
|
|
62
|
+
}
|