add-coder 0.2.8 → 0.3.0
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 +66 -27
- package/dist/index.js +158 -11
- package/package.json +9 -12
- package/templates/.add-coder-src-hash.json +256 -0
- package/templates/adapters/claude/hooks/doc-format-guard.sh +17 -0
- package/templates/adapters/claude/hooks/lib/notify.sh +34 -0
- package/templates/adapters/claude/hooks/pre-tool-use.sh +26 -7
- package/templates/adapters/claude/hooks/prompt-submit.sh +16 -0
- package/templates/adapters/codex/hooks/doc-format-guard.sh +17 -0
- package/templates/adapters/codex/hooks/lib/notify.sh +34 -0
- package/templates/adapters/codex/hooks/pre-tool-use.sh +48 -18
- package/templates/adapters/codex/hooks/prompt-submit.sh +16 -0
- package/templates/adapters/qoder/hooks/doc-format-guard.sh +17 -0
- package/templates/adapters/qoder/hooks/lib/notify.sh +34 -0
- package/templates/adapters/qoder/hooks/pre-tool-use.sh +28 -9
- package/templates/adapters/qoder/hooks/prompt-submit.sh +18 -1
- package/templates/adapters/trae/hooks/doc-format-guard.sh +17 -0
- package/templates/adapters/trae/hooks/lib/notify.sh +34 -0
- package/templates/adapters/trae/hooks/pre-tool-use.sh +48 -18
- package/templates/adapters/trae/hooks/prompt-submit.sh +16 -0
- package/templates/adapters/vscode/hooks/doc-format-guard.sh +16 -0
- package/templates/adapters/vscode/hooks/lib/notify.sh +34 -0
- package/templates/adapters/vscode/hooks/pre-tool-use.sh +26 -7
- package/templates/adapters/vscode/hooks/prompt-submit.sh +16 -0
- package/templates/core/hooks/doc-format-guard.sh +17 -0
- package/templates/core/hooks/lib/notify.sh +34 -0
- package/templates/core/hooks/pre-tool-use.sh +48 -18
- package/templates/core/hooks/prompt-submit.sh +16 -0
- package/templates/core/reviews/.gitkeep +0 -0
- package/templates/core/scripts/mcp-server/elicitation/confirm.ts +30 -0
- package/templates/core/scripts/mcp-server/elicitation/index.ts +1 -0
- package/templates/core/scripts/mcp-server/index.ts +15 -0
- package/templates/core/scripts/mcp-server/notifications/hitl.ts +49 -0
- package/templates/core/scripts/mcp-server/notifications/hook.ts +268 -0
- package/templates/core/scripts/mcp-server/notifications/index.ts +8 -0
- package/templates/core/scripts/mcp-server/resources/add-coder-version.ts +25 -0
- package/templates/core/scripts/mcp-server/resources/add-state.ts +44 -0
- package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +104 -0
- package/templates/core/scripts/mcp-server/resources/index.ts +12 -0
- package/templates/core/scripts/mcp-server/resources/round-task.ts +22 -0
- package/templates/core/scripts/mcp-server/sampling/index.ts +1 -0
- package/templates/core/scripts/mcp-server/sampling/review.ts +47 -0
- package/templates/core/scripts/mcp-server/shared/env.ts +26 -0
- package/templates/core/scripts/mcp-server/shared/fs.ts +40 -0
- package/templates/core/scripts/mcp-server/shared/prisma.ts +35 -0
- package/templates/core/scripts/mcp-server/shared/response.ts +9 -0
- package/templates/core/scripts/mcp-server/tasks/index.ts +2 -0
- package/templates/core/scripts/mcp-server/tasks/runner.ts +20 -0
- package/templates/core/scripts/mcp-server/tasks/store.ts +20 -0
- package/templates/core/scripts/mcp-server/tools/audit.ts +73 -0
- package/templates/core/scripts/mcp-server/tools/context.ts +337 -0
- package/templates/core/scripts/mcp-server/tools/docs.ts +42 -0
- package/templates/core/scripts/mcp-server/tools/gateway.ts +135 -0
- package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +92 -0
- package/templates/core/scripts/mcp-server/tools/index.ts +17 -0
- package/templates/core/scripts/mcp-server/tools/quality.ts +93 -0
- package/templates/core/scripts/mcp-server/types.ts +7 -0
- package/templates/core/scripts/mcp-server.ts +8 -3455
|
@@ -1,3460 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
6
|
-
const __dirname = dirname(__filename)
|
|
7
|
-
const PROJECT_ROOT = resolve(__dirname, "..", "..")
|
|
8
|
-
const MAGIC_DIR = basename(dirname(__dirname)) // ".qoder" / ".claude" / ".vscode" / ".trae" / ".codex" / ".add"
|
|
9
|
-
|
|
10
|
-
// 环境变量加载优先级: .env.development.local > .env.development > .env.local > .env
|
|
11
|
-
const ENV_CANDIDATES = [".env.development.local", ".env.development", ".env.local", ".env"];
|
|
12
|
-
for (const f of ENV_CANDIDATES) {
|
|
13
|
-
const p = resolve(PROJECT_ROOT, f);
|
|
14
|
-
if (existsSync(p)) { dotenv.config({ path: p, override: true }); break; }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const DATABASE_URL = process.env.DATABASE_URL
|
|
18
|
-
if (!DATABASE_URL) {
|
|
19
|
-
throw new Error("DATABASE_URL 未设置,请在 .env 中配置数据库连接串")
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
23
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
24
|
-
import { z } from "zod"
|
|
25
|
-
import { readFile, readdir, stat, mkdir, writeFile } from "fs/promises"
|
|
26
|
-
import { join, relative } from "path"
|
|
27
|
-
import { existsSync } from "fs"
|
|
28
|
-
import { spawnSync } from "child_process"
|
|
29
|
-
|
|
30
|
-
const { PrismaClient } = await import("@prisma/client").catch(() =>
|
|
31
|
-
import("../../generated/prisma/client.js").catch(() =>
|
|
32
|
-
import("../../src/generated/prisma/client.js"))
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
// Prisma 7 按 DATABASE_URL 前缀自动选 adapter
|
|
36
|
-
let adapter = undefined
|
|
37
|
-
if (DATABASE_URL.startsWith("postgresql://") || DATABASE_URL.startsWith("postgres://")) {
|
|
38
|
-
const { PrismaPg } = await import("@prisma/adapter-pg")
|
|
39
|
-
adapter = new PrismaPg({ connectionString: DATABASE_URL })
|
|
40
|
-
} else if (DATABASE_URL.startsWith("file:") || DATABASE_URL.startsWith("sqlite:")) {
|
|
41
|
-
const { PrismaLibSQL } = await import("@prisma/adapter-libsql")
|
|
42
|
-
const url = DATABASE_URL.replace(/^file:/, "")
|
|
43
|
-
adapter = new PrismaLibSQL({ url })
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const prisma = new PrismaClient({
|
|
47
|
-
...(adapter ? { adapter } : {}),
|
|
48
|
-
log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
type ToolResponse = Array<{ type: "text"; text: string }>
|
|
52
|
-
|
|
53
|
-
function textResponse(text: string): { content: ToolResponse } {
|
|
54
|
-
return { content: [{ type: "text", text }] }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function errorResponse(message: string): { content: ToolResponse; isError: boolean } {
|
|
58
|
-
return { content: [{ type: "text", text: message }], isError: true }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function readFileSafe(filePath: string): Promise<string | null> {
|
|
62
|
-
try {
|
|
63
|
-
return await readFile(filePath, "utf-8")
|
|
64
|
-
} catch {
|
|
65
|
-
return null
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── ADD 文档模板校验(共享函数,供 create_plan / check_add_compliance / check_spec_sync 等复用)──
|
|
70
|
-
|
|
71
|
-
interface GuardResult { ok: boolean; issues: string }
|
|
72
|
-
|
|
73
|
-
async function validateDocWithGuard(filePath: string): Promise<GuardResult> {
|
|
74
|
-
const guardScript = join(PROJECT_ROOT, MAGIC_DIR, "hooks", "doc-format-guard.sh")
|
|
75
|
-
if (!existsSync(guardScript)) return { ok: true, issues: "" }
|
|
76
|
-
const content = await readFileSafe(filePath)
|
|
77
|
-
if (!content) return { ok: false, issues: "文件无法读取" }
|
|
78
|
-
const guardInput = JSON.stringify({
|
|
79
|
-
tool_input: { file_path: filePath, file_content: content },
|
|
80
|
-
})
|
|
81
|
-
const guard = spawnSync("/bin/bash", [guardScript], {
|
|
82
|
-
input: guardInput,
|
|
83
|
-
encoding: "utf-8",
|
|
84
|
-
timeout: 15000,
|
|
85
|
-
cwd: PROJECT_ROOT,
|
|
86
|
-
env: { ...process.env, MAGIC_DIR },
|
|
87
|
-
})
|
|
88
|
-
if (guard.status === 2 || guard.stderr?.includes("校验不通过")) {
|
|
89
|
-
return { ok: false, issues: guard.stderr || guard.stdout || "校验失败" }
|
|
90
|
-
}
|
|
91
|
-
return { ok: true, issues: "" }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 递归读取目录下所有文件(扁平化返回),支持 ${MAGIC_DIR}/plans/2026-06/05/ 等分层结构 */
|
|
95
|
-
async function readdirRecursive(dir: string): Promise<string[]> {
|
|
96
|
-
const results: string[] = []
|
|
97
|
-
async function walk(d: string) {
|
|
98
|
-
const entries = await readdir(d, { withFileTypes: true })
|
|
99
|
-
for (const e of entries) {
|
|
100
|
-
const full = join(d, e.name)
|
|
101
|
-
if (e.isDirectory()) {
|
|
102
|
-
await walk(full)
|
|
103
|
-
} else {
|
|
104
|
-
results.push(relative(dir, full))
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
await walk(dir)
|
|
109
|
-
return results
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 从 Review 修复建议文本中提取用于 Plan 回流的代表关键词。
|
|
114
|
-
* 过滤掉通用动词(生成/补建/新增/在/中/补充),保留语义核心词(文件名/概念/术语)。
|
|
115
|
-
*/
|
|
116
|
-
function extractBackflowKeywords(fixText: string): string[] {
|
|
117
|
-
const stopWords = new Set([
|
|
118
|
-
"回退", "生成", "补建", "新增", "补充", "在", "中", "给出", "定义",
|
|
119
|
-
"明确", "需", "不可", "应", "可", "并", "与", "的", "了", "后",
|
|
120
|
-
])
|
|
121
|
-
// 提取关键名词短语:英文标识符、中文专用名词、文件路径片段
|
|
122
|
-
const keywords: string[] = []
|
|
123
|
-
|
|
124
|
-
// 英文标识符(如 perExpertTopK, agri_tech, SearchOptions)
|
|
125
|
-
const enMatches = fixText.match(/\b(perExpertTopK|RetrieveBudget|GroundingStatus|SearchOptions|add-route|handoff|spec\.md|tasks\.md|checklist\.md|collectionName|Phase\s*\d+|ChromaCollectionManager|agri_tech|crop_compare|roi_analysis|pest_risk|daily_report|weekly_report|session-start|prompt-submit|pre-tool-use|post-tool-use|stop-check|pre-compact|subagent-stop|renderer\.ts|SessionStart|UserPromptSubmit|hooks\/lib|github\/hooks)\b/gi)
|
|
126
|
-
if (enMatches) keywords.push(...enMatches.map(m => m.toLowerCase()))
|
|
127
|
-
|
|
128
|
-
// 中文专用名词
|
|
129
|
-
const cnPatterns = [
|
|
130
|
-
"迁移路线图", "迁移roadmap", "多collection", "多 collection",
|
|
131
|
-
"冗余策略", "复制", "引用方案", "新签名",
|
|
132
|
-
"汇总专家", "per-collection", "per collection",
|
|
133
|
-
"不阻塞", "降级路径", "回滚", "回流",
|
|
134
|
-
// add-coder 领域关键词
|
|
135
|
-
"轮次", "实施顺序", "增量插入", "模板", "预读", "renderer", "共享脚本", "Layer",
|
|
136
|
-
"stderr", "stdout", "注入", "标记文件", "tpl-injected", "验收阻断",
|
|
137
|
-
]
|
|
138
|
-
for (const pat of cnPatterns) {
|
|
139
|
-
if (fixText.includes(pat)) keywords.push(pat)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 去重并过滤停用词
|
|
143
|
-
const result = [...new Set(keywords.filter(k => k.length > 1 && !stopWords.has(k)))]
|
|
144
|
-
|
|
145
|
-
// 兜底:若关键词提取为空,用 fix 原文作为搜索串(防止新领域术语永不被识别)
|
|
146
|
-
if (result.length === 0 && fixText.length > 0) {
|
|
147
|
-
const fallback = fixText.replace(/[✅⬜\[\]\*]/g, "").trim().slice(0, 60)
|
|
148
|
-
if (fallback.length > 3) result.push(fallback)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return result
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const server = new McpServer(
|
|
155
|
-
{
|
|
156
|
-
name: "add-dev-tools",
|
|
157
|
-
version: "1.0.0",
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
capabilities: {
|
|
161
|
-
tools: {},
|
|
162
|
-
},
|
|
163
|
-
}
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
server.registerTool(
|
|
167
|
-
"get_project_context",
|
|
168
|
-
{
|
|
169
|
-
description: "获取项目的完整上下文信息:目录结构、技术栈、包管理信息、项目规则(ADD 范式约束)、${MAGIC_DIR}/ 工作流产物(templates/specs/reviews/plans)。AI 助手在 MCP-1(上下文优先)中应在生成代码前调用此工具获取项目真实信息,避免幻觉。\n\nscope='add-state' 返回 ADD 工作流状态快照:当前活跃 Plan、Review 未闭环问题、DPS 门禁状态、待执行操作清单。用于空白对话开局时快速介入 ADD 范式。",
|
|
170
|
-
inputSchema: {
|
|
171
|
-
scope: z.string().optional().describe("获取信息的范围: 'structure' 仅目录结构, 'rules' 仅项目规则, 'package' 仅包信息, 'add-state' ADD 工作流状态, 'all' 全部"),
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
async (args: { scope?: string }) => {
|
|
175
|
-
try {
|
|
176
|
-
const scope = args?.scope || "all"
|
|
177
|
-
const parts: string[] = []
|
|
178
|
-
|
|
179
|
-
if (scope === "all" || scope === "package") {
|
|
180
|
-
const pkg = await readFileSafe(join(PROJECT_ROOT, "package.json"))
|
|
181
|
-
if (pkg) {
|
|
182
|
-
const parsed = JSON.parse(pkg)
|
|
183
|
-
parts.push("=== 项目信息 ===")
|
|
184
|
-
parts.push(`名称: ${parsed.name}`)
|
|
185
|
-
parts.push(`版本: ${parsed.version}`)
|
|
186
|
-
parts.push(`技术栈: Next.js ${parsed.dependencies?.next || "unknown"} + TypeScript + Prisma + LangGraph`)
|
|
187
|
-
parts.push("")
|
|
188
|
-
parts.push("核心依赖:")
|
|
189
|
-
const keyDeps = [
|
|
190
|
-
"next", "@langchain/langgraph", "@prisma/client", "prisma",
|
|
191
|
-
"zustand", "@tanstack/react-query", "chromadb", "zod",
|
|
192
|
-
]
|
|
193
|
-
for (const dep of keyDeps) {
|
|
194
|
-
const ver = parsed.dependencies?.[dep] || parsed.devDependencies?.[dep]
|
|
195
|
-
if (ver) parts.push(` ${dep}: ${ver}`)
|
|
196
|
-
}
|
|
197
|
-
parts.push("")
|
|
198
|
-
parts.push("可用脚本:")
|
|
199
|
-
for (const [name, script] of Object.entries(parsed.scripts || {})) {
|
|
200
|
-
parts.push(` ${name}: ${script}`)
|
|
201
|
-
}
|
|
202
|
-
parts.push("")
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (scope === "all" || scope === "rules") {
|
|
207
|
-
const rules = await readFileSafe(join(PROJECT_ROOT, MAGIC_DIR, "rules", "project_rules.md"))
|
|
208
|
-
if (rules) {
|
|
209
|
-
parts.push("=== 项目规则 (ADD 范式强制约束) ===")
|
|
210
|
-
const lines = rules.split("\n")
|
|
211
|
-
let inCodeBlock = false
|
|
212
|
-
for (const line of lines) {
|
|
213
|
-
if (line.startsWith("```")) {
|
|
214
|
-
inCodeBlock = !inCodeBlock
|
|
215
|
-
continue
|
|
216
|
-
}
|
|
217
|
-
if (inCodeBlock) continue
|
|
218
|
-
if (line.startsWith("## ADD-") || line.startsWith("## 项目")) {
|
|
219
|
-
parts.push("")
|
|
220
|
-
parts.push(line)
|
|
221
|
-
} else if (line.startsWith("###") || line.startsWith("####")) {
|
|
222
|
-
parts.push(line)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
parts.push("")
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (scope === "all" || scope === "structure" || scope === "add-state") {
|
|
230
|
-
// === ADD 工作流状态快照 ===
|
|
231
|
-
// 扫描 plans/ 找最近激活的 Plan;扫描 reviews/ 找关联 Review 及其未闭环问题
|
|
232
|
-
parts.push("=== ADD 工作流状态 ===")
|
|
233
|
-
|
|
234
|
-
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
|
|
235
|
-
const reviewsDir = join(PROJECT_ROOT, MAGIC_DIR, "reviews")
|
|
236
|
-
const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
|
|
237
|
-
|
|
238
|
-
// 1. 扫描 Plan 文件
|
|
239
|
-
let activePlan = ""
|
|
240
|
-
let activePlanPath = ""
|
|
241
|
-
if (existsSync(plansDir)) {
|
|
242
|
-
const planFiles = (await readdirRecursive(plansDir))
|
|
243
|
-
.filter(f => f.endsWith(".md") && !f.includes("add-route") && !f.includes("handoff"))
|
|
244
|
-
.sort()
|
|
245
|
-
if (planFiles.length > 0) {
|
|
246
|
-
activePlan = planFiles[planFiles.length - 1]
|
|
247
|
-
activePlanPath = join(plansDir, activePlan)
|
|
248
|
-
parts.push(`最近 Plan: ${activePlan}`)
|
|
249
|
-
|
|
250
|
-
const allPlanFiles = await readdirRecursive(plansDir)
|
|
251
|
-
const addRouteFile = allPlanFiles.find(f => f.includes("add-route"))
|
|
252
|
-
const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
|
|
253
|
-
const handoffFiles = allPlanFiles.filter(f => f.includes("handoff"))
|
|
254
|
-
const hasHandoff = handoffFiles.some(f => f.toLowerCase().includes(planKeyword.toLowerCase()))
|
|
255
|
-
|
|
256
|
-
parts.push(` add-route: ${addRouteFile ? "✅ " + addRouteFile : "❌ 缺失(需回退 Step 0.5)"}`)
|
|
257
|
-
parts.push(` handoff: ${hasHandoff ? "✅ 已生成" : "❌ 缺失(需回退 Step 0.5)"}`)
|
|
258
|
-
} else {
|
|
259
|
-
parts.push("最近 Plan: 无")
|
|
260
|
-
}
|
|
261
|
-
} else {
|
|
262
|
-
parts.push("最近 Plan: 无")
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// 2. 扫描 Review 文件
|
|
266
|
-
let reviewFileName = ""
|
|
267
|
-
let reviewP0Count = 0
|
|
268
|
-
let reviewP1Count = 0
|
|
269
|
-
let reviewBackflowRate = 0
|
|
270
|
-
if (existsSync(reviewsDir) && activePlan) {
|
|
271
|
-
const reviewFiles = await readdir(reviewsDir)
|
|
272
|
-
const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
|
|
273
|
-
const matchingReview = reviewFiles.find(f =>
|
|
274
|
-
f.toLowerCase().includes(planKeyword.toLowerCase()) && f.includes("-review-v")
|
|
275
|
-
) || reviewFiles.find(f =>
|
|
276
|
-
f.toLowerCase().includes(planKeyword.toLowerCase())
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
if (matchingReview) {
|
|
280
|
-
reviewFileName = matchingReview
|
|
281
|
-
const reviewContent = await readFileSafe(join(reviewsDir, matchingReview)) || ""
|
|
282
|
-
const reviewLines = reviewContent.split("\n")
|
|
283
|
-
let inP0 = false
|
|
284
|
-
let inP1 = false
|
|
285
|
-
for (const line of reviewLines) {
|
|
286
|
-
if (line.match(/P0|ADD.*合规|阻断/)) { inP0 = true; inP1 = false; continue }
|
|
287
|
-
if (line.match(/P1|架构设计.*缺口/)) { inP0 = false; inP1 = true; continue }
|
|
288
|
-
if (line.match(/P2|中等|影响评估|决策结论|方案对比/)) { inP0 = false; inP1 = false; continue }
|
|
289
|
-
if ((inP0 || inP1) && line.trim().startsWith("|") && !line.includes("---")) {
|
|
290
|
-
const cols = line.split("|").map(c => c.trim()).filter(Boolean)
|
|
291
|
-
if (cols.length >= 4 && cols[0].match(/^\d+$/)) {
|
|
292
|
-
if (inP0) reviewP0Count++
|
|
293
|
-
else if (inP1) reviewP1Count++
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// 检查回流:Plan 中是否含 Review 修复建议关键词
|
|
299
|
-
const planContent = await readFileSafe(activePlanPath) || ""
|
|
300
|
-
if ((reviewP0Count + reviewP1Count) > 0 && planContent) {
|
|
301
|
-
const indicators = ["add-route", "specs/", "handoff", "Task Group",
|
|
302
|
-
"perExpertTopK", "agri_tech", "ChromaCollectionManager",
|
|
303
|
-
"迁移路线图", "8 个 Expert", "冗余策略"]
|
|
304
|
-
let hit = 0
|
|
305
|
-
for (const ind of indicators) {
|
|
306
|
-
if (planContent.toLowerCase().includes(ind.toLowerCase())) hit++
|
|
307
|
-
}
|
|
308
|
-
reviewBackflowRate = Math.min(100, Math.round((hit / Math.max(reviewP0Count + reviewP1Count, 1)) * 100))
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
parts.push("")
|
|
312
|
-
parts.push(`关联 Review: ${matchingReview}`)
|
|
313
|
-
parts.push(` P0 问题: ${reviewP0Count} 个, P1 问题: ${reviewP1Count} 个`)
|
|
314
|
-
parts.push(` 回流状态: 约 ${reviewBackflowRate}%`)
|
|
315
|
-
if (reviewBackflowRate < 70) {
|
|
316
|
-
parts.push(" ⚠️ Review 结论未充分回流至 Plan — 需执行 0.6.5 卡位")
|
|
317
|
-
} else {
|
|
318
|
-
parts.push(" ✅ Review 结论基本回流至 Plan")
|
|
319
|
-
}
|
|
320
|
-
} else {
|
|
321
|
-
parts.push("")
|
|
322
|
-
parts.push("关联 Review: 无(该 Plan 尚未生成方案评审)")
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// 3. 检查 Specs
|
|
327
|
-
if (activePlan) {
|
|
328
|
-
const planKeyword = activePlan.replace(/-plan-v\d+\.md$/, "")
|
|
329
|
-
const specDirs = existsSync(specsDir) ? await readdir(specsDir) : []
|
|
330
|
-
const matchingSpec = specDirs.find(d => d.toLowerCase().includes(planKeyword.toLowerCase()))
|
|
331
|
-
parts.push("")
|
|
332
|
-
if (matchingSpec) {
|
|
333
|
-
const hasSpec = existsSync(join(specsDir, matchingSpec, "spec.md"))
|
|
334
|
-
const hasTasks = existsSync(join(specsDir, matchingSpec, "tasks.md"))
|
|
335
|
-
const hasChecklist = existsSync(join(specsDir, matchingSpec, "checklist.md"))
|
|
336
|
-
parts.push(`Specs: ${matchingSpec}/`)
|
|
337
|
-
parts.push(` spec.md: ${hasSpec ? "✅" : "❌"}`)
|
|
338
|
-
parts.push(` tasks.md: ${hasTasks ? "✅" : "❌"}`)
|
|
339
|
-
parts.push(` checklist.md: ${hasChecklist ? "✅" : "❌"}`)
|
|
340
|
-
} else {
|
|
341
|
-
parts.push("Specs: ❌ 缺失")
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// 4. 待执行操作清单
|
|
346
|
-
parts.push("")
|
|
347
|
-
parts.push("=== 待执行 ADD 操作(按流程顺序) ===")
|
|
348
|
-
const todoItems: string[] = []
|
|
349
|
-
if (!activePlan) {
|
|
350
|
-
todoItems.push("1. [未开始] 用户提出需求 → 生成 Plan")
|
|
351
|
-
} else {
|
|
352
|
-
if (!reviewFileName) {
|
|
353
|
-
todoItems.push("1. [Step 0] 生成 Plan Review(ADD-9 方案评审)")
|
|
354
|
-
} else if (reviewP0Count + reviewP1Count > 0 && reviewBackflowRate < 70) {
|
|
355
|
-
todoItems.push("1. [0.6.5] Review 结论回流至 Plan — P0/P1 未写入 Plan 体")
|
|
356
|
-
todoItems.push("2. [check_dps] DPS 门禁预计不通过(回流完整度 < 70%)")
|
|
357
|
-
}
|
|
358
|
-
const allPlanDir = await readdirRecursive(plansDir)
|
|
359
|
-
const hasAddRoute = allPlanDir.some(f => f.includes("add-route"))
|
|
360
|
-
if (!hasAddRoute && reviewBackflowRate >= 70) {
|
|
361
|
-
todoItems.push("2. [Step 0.5] 生成 add-route")
|
|
362
|
-
}
|
|
363
|
-
const planKw = activePlan.replace(/-plan-v\d+\.md$/, "")
|
|
364
|
-
const sd = existsSync(specsDir) ? await readdir(specsDir) : []
|
|
365
|
-
if (!sd.some(d => d.toLowerCase().includes(planKw.toLowerCase())) && reviewBackflowRate >= 70) {
|
|
366
|
-
todoItems.push("3. [Step 0] 生成 Specs 三元组")
|
|
367
|
-
}
|
|
368
|
-
if (todoItems.length === 0) {
|
|
369
|
-
todoItems.push("✅ ADD 就绪 — check_dps ≥ 85 后可进入 Step 1")
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
for (const item of todoItems) parts.push(item)
|
|
373
|
-
parts.push("")
|
|
374
|
-
parts.push("快速指令: 说「执行 add-paradigm Step 0」进入文档先行流程")
|
|
375
|
-
parts.push(" 说「将 Review 结论回流至 Plan」触发 0.6.5 卡位")
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (scope === "all" || scope === "structure") {
|
|
379
|
-
parts.push("=== 项目目录结构(顶层) ===")
|
|
380
|
-
const topDirs = [MAGIC_DIR, "src", "prisma", "scripts", "docs", "data", "public"]
|
|
381
|
-
for (const dir of topDirs) {
|
|
382
|
-
const fullPath = join(PROJECT_ROOT, dir)
|
|
383
|
-
if (existsSync(fullPath)) {
|
|
384
|
-
const entries = await readdir(fullPath)
|
|
385
|
-
parts.push(` ${dir}/ (${entries.length} 项)`)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
parts.push("")
|
|
389
|
-
parts.push("=== src/ 子目录结构 ===")
|
|
390
|
-
const srcDirs = ["agents", "app", "components", "lib", "services", "stores", "types"]
|
|
391
|
-
for (const dir of srcDirs) {
|
|
392
|
-
const fullPath = join(PROJECT_ROOT, "src", dir)
|
|
393
|
-
if (existsSync(fullPath)) {
|
|
394
|
-
const entries = await readdir(fullPath)
|
|
395
|
-
const items = (await Promise.all(
|
|
396
|
-
entries.slice(0, 15).map(async (e) => {
|
|
397
|
-
const s = await stat(join(fullPath, e))
|
|
398
|
-
return s.isDirectory() ? `${e}/` : e
|
|
399
|
-
})
|
|
400
|
-
)).join(", ")
|
|
401
|
-
parts.push(` src/${dir}/ (${entries.length} 项): ${items})`)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
parts.push("")
|
|
405
|
-
|
|
406
|
-
// ADD 工作流产物目录
|
|
407
|
-
parts.push(`=== ${MAGIC_DIR}/ ADD 工作流产物 ===`)
|
|
408
|
-
const magicDirs = [
|
|
409
|
-
{ dir: "templates", desc: "ADD 文档模板(11 个)" },
|
|
410
|
-
{ dir: "specs", desc: "specs 三元组(spec+tasks+checklist)" },
|
|
411
|
-
{ dir: "reviews", desc: "方案审查 + 实现审查 + 运行时审查" },
|
|
412
|
-
{ dir: "plans", desc: "Plan + handoff 交接手册" },
|
|
413
|
-
{ dir: "rules", desc: "项目规则 + 理论→实践映射" },
|
|
414
|
-
{ dir: "skills", desc: "SKILL 行为定义(add-paradigm / session-init)" },
|
|
415
|
-
{ dir: "scripts", desc: "工具脚本 + MCP 服务器" },
|
|
416
|
-
]
|
|
417
|
-
for (const { dir, desc } of magicDirs) {
|
|
418
|
-
const fullPath = join(PROJECT_ROOT, MAGIC_DIR, dir)
|
|
419
|
-
if (existsSync(fullPath)) {
|
|
420
|
-
const entries = await readdir(fullPath)
|
|
421
|
-
parts.push(` ${MAGIC_DIR}/${dir}/ (${entries.length} 项) — ${desc}`)
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return textResponse(parts.join("\n"))
|
|
427
|
-
} catch (error) {
|
|
428
|
-
return errorResponse(`获取项目上下文失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
server.registerTool(
|
|
434
|
-
"get_db_schema",
|
|
435
|
-
{
|
|
436
|
-
description: "获取 Prisma 数据库 Schema 定义(MCP-1 上下文优先)。返回指定模型的结构、字段、关系。AI 助手在编写数据库查询代码时应调用此工具获取真实的 Schema 信息,避免凭记忆假设。",
|
|
437
|
-
inputSchema: {
|
|
438
|
-
model: z.string().optional().describe("可选的模型名称(不区分大小写)。不指定则返回所有模型概况。指定则返回该模型的完整字段定义。"),
|
|
439
|
-
},
|
|
440
|
-
},
|
|
441
|
-
async (args: { model?: string }) => {
|
|
442
|
-
try {
|
|
443
|
-
const schemaPath = join(PROJECT_ROOT, "prisma", "schema.prisma")
|
|
444
|
-
const schema = await readFileSafe(schemaPath)
|
|
445
|
-
if (!schema) {
|
|
446
|
-
return errorResponse("未找到 prisma/schema.prisma 文件")
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const modelName = args?.model?.toLowerCase()
|
|
450
|
-
if (modelName) {
|
|
451
|
-
const modelRegex = new RegExp(`model\\s+${modelName}\\s*\{`, "i")
|
|
452
|
-
const match = schema.match(modelRegex)
|
|
453
|
-
if (match) {
|
|
454
|
-
const startIdx = match.index ?? 0
|
|
455
|
-
const braceIdx = schema.indexOf("{", startIdx)
|
|
456
|
-
if (braceIdx !== -1) {
|
|
457
|
-
let depth = 1
|
|
458
|
-
let endIdx = braceIdx + 1
|
|
459
|
-
while (depth > 0 && endIdx < schema.length) {
|
|
460
|
-
if (schema[endIdx] === "{") depth++
|
|
461
|
-
else if (schema[endIdx] === "}") depth--
|
|
462
|
-
endIdx++
|
|
463
|
-
}
|
|
464
|
-
const body = schema.slice(braceIdx, endIdx)
|
|
465
|
-
return textResponse(`=== Model: ${args.model} ===\n\nmodel ${args.model} ${body}`)
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
const enumMatch = schema.match(new RegExp(`enum\\s+${modelName}\\s*\{`, "i"))
|
|
469
|
-
if (enumMatch) {
|
|
470
|
-
return textResponse(`=== Enum: ${args.model} ===\n\n可用的枚举值见不传参调用结果。`)
|
|
471
|
-
}
|
|
472
|
-
return errorResponse(`未找到模型或枚举: ${args.model}。可用模型见不传参调用结果。`)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const models: Array<{ name: string; fieldCount: number }> = []
|
|
476
|
-
const modelRegex = /model\s+(\w+)\s*\{/g
|
|
477
|
-
let m
|
|
478
|
-
while ((m = modelRegex.exec(schema)) !== null) {
|
|
479
|
-
const name = m[1]
|
|
480
|
-
const startIdx = m.index
|
|
481
|
-
const braceIdx = schema.indexOf("{", startIdx)
|
|
482
|
-
if (braceIdx !== -1) {
|
|
483
|
-
let depth = 1
|
|
484
|
-
let endIdx = braceIdx + 1
|
|
485
|
-
while (depth > 0 && endIdx < schema.length) {
|
|
486
|
-
if (schema[endIdx] === "{") depth++
|
|
487
|
-
else if (schema[endIdx] === "}") depth--
|
|
488
|
-
endIdx++
|
|
489
|
-
}
|
|
490
|
-
const body = schema.slice(braceIdx, endIdx)
|
|
491
|
-
const fieldCount = body.split("\n").filter(
|
|
492
|
-
(l) => l.trim() && !l.trim().startsWith("//") && !l.trim().startsWith("@@")
|
|
493
|
-
).length
|
|
494
|
-
models.push({ name, fieldCount })
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const enums: string[] = []
|
|
499
|
-
const enumRegex = /enum\s+(\w+)\s*\{/g
|
|
500
|
-
while ((m = enumRegex.exec(schema)) !== null) {
|
|
501
|
-
enums.push(m[1])
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const parts: string[] = ["=== Prisma Schema 概况 ==="]
|
|
505
|
-
parts.push("")
|
|
506
|
-
parts.push(`模型 (${models.length} 个):`)
|
|
507
|
-
for (const model of models) {
|
|
508
|
-
parts.push(` ${model.name} (${model.fieldCount} 字段)`)
|
|
509
|
-
}
|
|
510
|
-
if (enums.length > 0) {
|
|
511
|
-
parts.push("")
|
|
512
|
-
parts.push(`枚举 (${enums.length} 个): ${enums.join(", ")}`)
|
|
513
|
-
}
|
|
514
|
-
parts.push("")
|
|
515
|
-
parts.push('提示: 指定 model 参数获取完整字段定义,例如: get_db_schema({ model: "User" })')
|
|
516
|
-
|
|
517
|
-
return textResponse(parts.join("\n"))
|
|
518
|
-
} catch (error) {
|
|
519
|
-
return errorResponse(`获取 Schema 失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
server.registerTool(
|
|
525
|
-
"get_audit_logger_pattern",
|
|
526
|
-
{
|
|
527
|
-
description: "获取指定域的审计日志器完整代码或模式(MCP-1 上下文优先)。历史域('knowledge-base', 'agent')返回已有的混合式日志器代码;新域返回三层分离式模板(ADD-4 三层可插拔架构):\n- Layer 1 开发审计(dev-logger): console + file + DB metadata,可插拔\n- Layer 2 运行时审计(audit): console + AuditLog 表,始终开启\n- Layer 3 调试日志: console only,LOG_LEVEL 控制\n新业务域必须使用三层分离模式。",
|
|
528
|
-
inputSchema: {
|
|
529
|
-
domain: z.string().describe("审计日志器域: 'knowledge-base', 'agent'(历史混合式),或新域如 'personnel'(三层分离式)"),
|
|
530
|
-
},
|
|
531
|
-
},
|
|
532
|
-
async (args: { domain: string }) => {
|
|
533
|
-
try {
|
|
534
|
-
const domain = args?.domain
|
|
535
|
-
if (!domain) return errorResponse("domain 参数不能为空")
|
|
536
|
-
|
|
537
|
-
// 历史混合式日志器(从文件读取)
|
|
538
|
-
const legacyFileMap: Record<string, string> = {
|
|
539
|
-
"knowledge-base": join(PROJECT_ROOT, "src", "lib", "audit-logger.ts"),
|
|
540
|
-
"agent": join(PROJECT_ROOT, "src", "lib", "agent-audit-logger.ts"),
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// 新域三层分离式日志器(生成模板)
|
|
544
|
-
const featureNameTemplate = domain.split("-").map(s => s.charAt(0).toUpperCase() + s.slice(1)).join("")
|
|
545
|
-
const fnPrefixTemplate = featureNameTemplate.charAt(0).toLowerCase() + featureNameTemplate.slice(1)
|
|
546
|
-
const threeLayerPatternTemplate = `=== ${domain} 三层分离式审计日志器(新模式,ADD-4) ===
|
|
547
|
-
|
|
548
|
-
新建 ${domain} 业务域应调用 generate_audit_logger 生成两个文件:
|
|
549
|
-
|
|
550
|
-
--- 文件1: src/lib/${domain}-dev-logger.ts (Layer 1 开发审计,可插拔) ---
|
|
551
|
-
仅在 NODE_ENV=development 时生效,用于 AI 合规检查。
|
|
552
|
-
ADD-4 三通道输出:
|
|
553
|
-
1. console.log — ${fnPrefixTemplate}Audit / ${fnPrefixTemplate}AuditPhaseStart / ${fnPrefixTemplate}AuditPhaseEnd
|
|
554
|
-
2. fs.appendFile — writeToFile(自动)
|
|
555
|
-
3. DB metadata — build${featureNameTemplate}DevAudit() 构建记录 → 业务服务层 saveAuditData 写入
|
|
556
|
-
包含函数: auditPhaseStart / auditPhaseEnd / audit / buildXxxDevAudit / readRecentLogs / clearLogs
|
|
557
|
-
|
|
558
|
-
--- 文件2: src/lib/${domain}-audit.ts (Layer 2 运行时业务审计,不可插拔) ---
|
|
559
|
-
始终开启,用于业务记录和前端查询。
|
|
560
|
-
ADD-4 三通道输出:
|
|
561
|
-
1. console.log — record${featureNameTemplate}Audit
|
|
562
|
-
2. fs.appendFile — (AuditLog 表替代文件写入)
|
|
563
|
-
3. DB AuditLog 表 — prisma.auditLog.create
|
|
564
|
-
包含函数: record${featureNameTemplate}Audit()
|
|
565
|
-
|
|
566
|
-
新建业务域必须使用三层分离模式(而非历史混合式)。详见项目规则 ADD-4「三层可插拔架构」。`
|
|
567
|
-
|
|
568
|
-
if (legacyFileMap[domain]) {
|
|
569
|
-
// 返回历史混合式日志器
|
|
570
|
-
const filePath = legacyFileMap[domain]
|
|
571
|
-
const content = await readFileSafe(filePath)
|
|
572
|
-
if (!content) {
|
|
573
|
-
return errorResponse(`未找到 ${domain} 审计日志器文件: ${filePath}`)
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const metaMap: Record<string, { prefix: string; logDir: string; logFile: string }> = {
|
|
577
|
-
"knowledge-base": { prefix: "[KB-AUDIT]", logDir: "logs/knowledge-base/", logFile: "kb-audit.log" },
|
|
578
|
-
"agent": { prefix: "[AGENT-AUDIT]", logDir: "logs/agent/", logFile: "agent-audit.log" },
|
|
579
|
-
}
|
|
580
|
-
const meta = metaMap[domain]
|
|
581
|
-
|
|
582
|
-
const parts: string[] = [
|
|
583
|
-
`=== ${domain} 审计日志器(历史混合式,Layer 1 + Layer 2 未分离) ===`,
|
|
584
|
-
`前缀: ${meta.prefix}`,
|
|
585
|
-
`日志目录: ${meta.logDir}`,
|
|
586
|
-
`日志文件: ${meta.logFile}`,
|
|
587
|
-
`文件路径: ${relative(PROJECT_ROOT, filePath)}`,
|
|
588
|
-
"",
|
|
589
|
-
"=== 完整代码 ===",
|
|
590
|
-
content,
|
|
591
|
-
"",
|
|
592
|
-
"=== 模式要点 ===",
|
|
593
|
-
"1. PREFIX 常量: [DOMAIN-AUDIT] 格式",
|
|
594
|
-
`2. LOG_DIR: logs/domain/ 目录 (当前: ${meta.logDir})`,
|
|
595
|
-
"3. AuditPhase 类型: 枚举所有业务阶段",
|
|
596
|
-
"4. audit() / auditPhaseStart() / auditPhaseEnd() 三函数",
|
|
597
|
-
"5. readRecentLogs() / clearLogs() 读写函数",
|
|
598
|
-
"6. ENABLE_FILE_LOG 环境变量控制,开发环境默认启用",
|
|
599
|
-
"7. 三通道输出: console.log + fs.appendFile + 数据库回写",
|
|
600
|
-
"",
|
|
601
|
-
"⚠️ 注意: 这是历史混合式模式。新建业务域应使用三层分离模式(调用 generate_audit_logger 生成)。",
|
|
602
|
-
]
|
|
603
|
-
|
|
604
|
-
return textResponse(parts.join("\n"))
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// 返回三层分离式模板
|
|
608
|
-
return textResponse(threeLayerPatternTemplate)
|
|
609
|
-
} catch (error) {
|
|
610
|
-
return errorResponse(`获取审计日志器失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
server.registerTool(
|
|
616
|
-
"check_phase_symmetry",
|
|
617
|
-
{
|
|
618
|
-
description: "检查代码中的阶段标记对称性(ADD-2 阶段标记对称,MCP-3 编码中验证)。统计 auditPhaseStart/End 配对情况,返回不对称列表。AI 助手在生成包含审计阶段的代码后必须调用此工具验证。",
|
|
619
|
-
inputSchema: {
|
|
620
|
-
code: z.string().describe("要检查的 TypeScript 代码文本"),
|
|
621
|
-
},
|
|
622
|
-
},
|
|
623
|
-
async (args: { code: string }) => {
|
|
624
|
-
try {
|
|
625
|
-
const code = args?.code
|
|
626
|
-
if (!code) return errorResponse("code 参数不能为空")
|
|
627
|
-
|
|
628
|
-
const startRegex = /auditPhaseStart\(["']([^"']+)["']/g
|
|
629
|
-
const endRegex = /auditPhaseEnd\(["']([^"']+)["']/g
|
|
630
|
-
|
|
631
|
-
const starts: string[] = []
|
|
632
|
-
const ends: string[] = []
|
|
633
|
-
let m
|
|
634
|
-
|
|
635
|
-
while ((m = startRegex.exec(code)) !== null) starts.push(m[1])
|
|
636
|
-
while ((m = endRegex.exec(code)) !== null) ends.push(m[1])
|
|
637
|
-
|
|
638
|
-
const startCounts: Record<string, number> = {}
|
|
639
|
-
const endCounts: Record<string, number> = {}
|
|
640
|
-
for (const s of starts) startCounts[s] = (startCounts[s] || 0) + 1
|
|
641
|
-
for (const e of ends) endCounts[e] = (endCounts[e] || 0) + 1
|
|
642
|
-
|
|
643
|
-
const allPhases = new Set([...Object.keys(startCounts), ...Object.keys(endCounts)])
|
|
644
|
-
const asymmetric: string[] = []
|
|
645
|
-
|
|
646
|
-
allPhases.forEach((phase) => {
|
|
647
|
-
const sc = startCounts[phase] || 0
|
|
648
|
-
const ec = endCounts[phase] || 0
|
|
649
|
-
if (sc !== ec) {
|
|
650
|
-
asymmetric.push(` ⚠️ ${phase}: Start=${sc}, End=${ec} (${sc > ec ? "缺少 End" : "缺少 Start"})`)
|
|
651
|
-
}
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
const lines: string[] = [
|
|
655
|
-
"=== ADD-2 阶段标记对称性检查 ===",
|
|
656
|
-
`审计阶段 Start 总数: ${starts.length}`,
|
|
657
|
-
`审计阶段 End 总数: ${ends.length}`,
|
|
658
|
-
"",
|
|
659
|
-
]
|
|
660
|
-
|
|
661
|
-
if (asymmetric.length === 0) {
|
|
662
|
-
lines.push("✅ 阶段标记完全对称")
|
|
663
|
-
} else {
|
|
664
|
-
lines.push(`❌ 发现 ${asymmetric.length} 个不对称阶段:`)
|
|
665
|
-
lines.push(...asymmetric)
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
lines.push("")
|
|
669
|
-
lines.push("=== 所有阶段明细 ===")
|
|
670
|
-
allPhases.forEach((phase) => {
|
|
671
|
-
lines.push(` ${phase}: Start=${startCounts[phase] || 0}, End=${endCounts[phase] || 0}`)
|
|
672
|
-
})
|
|
673
|
-
|
|
674
|
-
return textResponse(lines.join("\n"))
|
|
675
|
-
} catch (error) {
|
|
676
|
-
return errorResponse(`检查阶段对称性失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
server.registerTool(
|
|
682
|
-
"check_failure_path",
|
|
683
|
-
{
|
|
684
|
-
description: "检查代码中的失败路径审计信息密度(ADD-6 失败路径等价审计,MCP-3 编码中验证)。对比 try 块与 catch 块的 extra 字段数,确保失败路径的审计信息不少于成功路径。AI 助手在生成包含 try/catch 的代码后必须调用此工具验证。",
|
|
685
|
-
inputSchema: {
|
|
686
|
-
code: z.string().describe("要检查的 TypeScript 代码文本"),
|
|
687
|
-
},
|
|
688
|
-
},
|
|
689
|
-
async (args: { code: string }) => {
|
|
690
|
-
try {
|
|
691
|
-
const code = args?.code
|
|
692
|
-
if (!code) return errorResponse("code 参数不能为空")
|
|
693
|
-
|
|
694
|
-
const sections = code.split(/catch\s*\(/)
|
|
695
|
-
if (sections.length <= 1) {
|
|
696
|
-
return textResponse("=== ADD-6 失败路径审计检查 ===\n\n未检测到 try/catch 块,无需检查失败路径。")
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const lines: string[] = [
|
|
700
|
-
"=== ADD-6 失败路径审计信息密度检查 ===",
|
|
701
|
-
`检测到 ${sections.length - 1} 个 catch 块`,
|
|
702
|
-
"",
|
|
703
|
-
]
|
|
704
|
-
|
|
705
|
-
let allPass = true
|
|
706
|
-
for (let i = 1; i < sections.length; i++) {
|
|
707
|
-
const catchBlock = sections[i]
|
|
708
|
-
const catchEnd = catchBlock.indexOf("{")
|
|
709
|
-
if (catchEnd === -1) continue
|
|
710
|
-
|
|
711
|
-
const tryBlock = sections[i - 1]
|
|
712
|
-
const tryExtraMatches = tryBlock.match(/extra[:\s]*\{[^}]*\}/g)
|
|
713
|
-
const tryExtraFieldCount = tryExtraMatches
|
|
714
|
-
? tryExtraMatches.reduce((sum, m) => sum + (m.match(/\w+:/g)?.length || 0), 0)
|
|
715
|
-
: 0
|
|
716
|
-
|
|
717
|
-
const closeIdx = catchBlock.indexOf("}")
|
|
718
|
-
const catchBody = catchBlock.slice(catchEnd, closeIdx + 1)
|
|
719
|
-
const catchExtraMatches = catchBody.match(/extra[:\s]*\{[^}]*\}/g)
|
|
720
|
-
const catchExtraFieldCount = catchExtraMatches
|
|
721
|
-
? catchExtraMatches.reduce((sum, m) => sum + (m.match(/\w+:/g)?.length || 0), 0)
|
|
722
|
-
: 0
|
|
723
|
-
|
|
724
|
-
const catchHasThrow = catchBody.includes("throw")
|
|
725
|
-
const catchHasErrorLog = catchBody.includes("audit") || catchBody.includes("Audit")
|
|
726
|
-
const catchInfoDensity = catchExtraFieldCount + (catchHasThrow ? 2 : 0) + (catchHasErrorLog ? 2 : 0)
|
|
727
|
-
|
|
728
|
-
lines.push(`--- Catch 块 #${i} ---`)
|
|
729
|
-
if (catchInfoDensity >= tryExtraFieldCount && catchHasErrorLog) {
|
|
730
|
-
lines.push(` ✅ 失败路径审计信息密度充足`)
|
|
731
|
-
} else {
|
|
732
|
-
allPass = false
|
|
733
|
-
if (!catchHasErrorLog) lines.push(` ❌ catch 块缺少审计调用(audit/Audit)`)
|
|
734
|
-
if (catchInfoDensity < tryExtraFieldCount) {
|
|
735
|
-
lines.push(` ❌ 信息密度不足: catch extra 字段=${catchExtraFieldCount}, try extra 字段=${tryExtraFieldCount}`)
|
|
736
|
-
lines.push(" 建议: 在 catch 块中添加与 try 块同级的 extra 字段")
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
lines.push(` try extra 字段数: ${tryExtraFieldCount}`)
|
|
740
|
-
lines.push(` catch extra 字段数: ${catchExtraFieldCount}`)
|
|
741
|
-
lines.push(` 有审计调用: ${catchHasErrorLog ? "是" : "否"}`)
|
|
742
|
-
lines.push(` 有 throw: ${catchHasThrow ? "是" : "否"}`)
|
|
743
|
-
lines.push("")
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (allPass) {
|
|
747
|
-
lines.push("✅ 所有 catch 块审计信息密度满足 ADD-6 要求")
|
|
748
|
-
} else {
|
|
749
|
-
lines.push("⚠️ 部分 catch 块需要补充审计信息")
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
return textResponse(lines.join("\n"))
|
|
753
|
-
} catch (error) {
|
|
754
|
-
return errorResponse(`检查失败路径失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
server.registerTool(
|
|
760
|
-
"generate_audit_logger",
|
|
761
|
-
{
|
|
762
|
-
description: "生成符合三层分离模式的新审计日志器完整代码(MCP-2 生成优先于手写)。遵循 ADD-1~6 原则,生成两个文件:\n- Layer 1 开发审计(dev-logger.ts): console + file + DB metadata,可插拔(NODE_ENV=development)\n- Layer 2 运行时审计(audit.ts): console + AuditLog 表 + traceId 关联,始终开启\nAI 助手在新建功能时必须调用此工具生成审计日志器,而非手写。",
|
|
763
|
-
inputSchema: {
|
|
764
|
-
domain: z.string().describe("审计日志器域名(小写中划线), 例如: 'chat-persistence', 'worldline'"),
|
|
765
|
-
phases: z.string().describe("业务阶段枚举列表(逗号分隔,大写蛇形), 例如: 'CHAT_SAVE,CHAT_SAVE_MSG,CHAT_LOAD,CHAT_DONE,CHAT_FAIL'"),
|
|
766
|
-
prefix: z.string().describe("审计前缀标识, 例如: 'CHAT-PERSISTENCE-AUDIT'"),
|
|
767
|
-
},
|
|
768
|
-
},
|
|
769
|
-
async (args: { domain: string; phases: string; prefix: string }) => {
|
|
770
|
-
try {
|
|
771
|
-
const { domain, phases, prefix } = args
|
|
772
|
-
if (!domain || !phases || !prefix) {
|
|
773
|
-
return errorResponse("domain, phases, prefix 参数均不能为空")
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const phaseList = phases.split(",").map(p => p.trim()).filter(Boolean)
|
|
777
|
-
if (phaseList.length === 0) {
|
|
778
|
-
return errorResponse("phases 必须包含至少一个阶段")
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const featureName = domain.split("-").map(s => s.charAt(0).toUpperCase() + s.slice(1)).join("")
|
|
782
|
-
const domainUpper = prefix
|
|
783
|
-
const logDir = `logs/${domain}/`
|
|
784
|
-
const logFile = `${domain}.log`
|
|
785
|
-
|
|
786
|
-
const typeEntries = phaseList.map(p => ` | "${p}"`).join("\n")
|
|
787
|
-
const envPrefix = domainUpper.replace(/-/g, "_")
|
|
788
|
-
const fnPrefix = featureName.charAt(0).toLowerCase() + featureName.slice(1)
|
|
789
|
-
|
|
790
|
-
// === Layer 1: 开发审计日志器 (dev-logger.ts) ===
|
|
791
|
-
const devLoggerCode = `import * as fs from "fs/promises"
|
|
792
|
-
import * as path from "path"
|
|
793
|
-
|
|
794
|
-
// ADD-4 Layer 1 开发审计(可插拔): console + file + DB metadata
|
|
795
|
-
// 消费者: AI 助手 + 开发者 | 开关: NODE_ENV=development
|
|
796
|
-
|
|
797
|
-
const PREFIX = "[${prefix}]"
|
|
798
|
-
|
|
799
|
-
const LOG_DIR = process.env.${envPrefix}_LOG_DIR || path.join(process.cwd(), "${logDir}")
|
|
800
|
-
const LOG_FILE = process.env.${envPrefix}_LOG_FILE || "${logFile}"
|
|
801
|
-
const ENABLE_FILE_LOG = process.env.${envPrefix}_ENABLE_FILE_LOG === "true" || process.env.NODE_ENV === "development"
|
|
802
|
-
|
|
803
|
-
// Layer 1: 开发审计 — 仅在 NODE_ENV=development 时生效
|
|
804
|
-
const IS_DEV = process.env.NODE_ENV === "development"
|
|
805
|
-
|
|
806
|
-
type ${featureName}AuditPhase =
|
|
807
|
-
${typeEntries}
|
|
808
|
-
|
|
809
|
-
async function ensureLogDir(): Promise<void> {
|
|
810
|
-
if (!ENABLE_FILE_LOG) return
|
|
811
|
-
try {
|
|
812
|
-
await fs.mkdir(LOG_DIR, { recursive: true })
|
|
813
|
-
} catch {
|
|
814
|
-
// Ignore if directory already exists
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
async function writeToFile(message: string): Promise<void> {
|
|
819
|
-
if (!ENABLE_FILE_LOG) return
|
|
820
|
-
try {
|
|
821
|
-
await ensureLogDir()
|
|
822
|
-
const logPath = path.join(LOG_DIR, LOG_FILE)
|
|
823
|
-
await fs.appendFile(logPath, message + "\\n", "utf-8")
|
|
824
|
-
} catch (error) {
|
|
825
|
-
console.error("\${PREFIX} Failed to write to log file:", error)
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function formatMessage(phase: ${featureName}AuditPhase, detail: string, extra?: Record<string, unknown>): string {
|
|
830
|
-
const ts = new Date().toISOString()
|
|
831
|
-
const extraStr = extra ? \` | \${JSON.stringify(extra)}\` : ""
|
|
832
|
-
return \`\${PREFIX} [\${ts}] [\${phase}] \${detail}\${extraStr}\`
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
export function ${fnPrefix}Audit(phase: ${featureName}AuditPhase, detail: string, extra?: Record<string, unknown>) {
|
|
836
|
-
if (!IS_DEV) return
|
|
837
|
-
const message = formatMessage(phase, detail, extra)
|
|
838
|
-
console.log(message)
|
|
839
|
-
writeToFile(message)
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
export function ${fnPrefix}AuditPhaseStart(phase: ${featureName}AuditPhase, description: string, count?: number) {
|
|
843
|
-
if (!IS_DEV) return
|
|
844
|
-
const countStr = count !== undefined ? \` (\${count}个)\` : ""
|
|
845
|
-
const message = \`\${PREFIX} ═══ [\${phase}] 开始\${countStr}: \${description} ═══\`
|
|
846
|
-
console.log(message)
|
|
847
|
-
writeToFile(message)
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
export function ${fnPrefix}AuditPhaseEnd(phase: ${featureName}AuditPhase, detail: string) {
|
|
851
|
-
if (!IS_DEV) return
|
|
852
|
-
const message = \`\${PREFIX} ═══ [\${phase}] 结束: \${detail} ═══\`
|
|
853
|
-
console.log(message)
|
|
854
|
-
writeToFile(message)
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
export async function read${featureName}Logs(lines: number = 100): Promise<string[]> {
|
|
858
|
-
try {
|
|
859
|
-
await ensureLogDir()
|
|
860
|
-
const logPath = path.join(LOG_DIR, LOG_FILE)
|
|
861
|
-
const content = await fs.readFile(logPath, "utf-8")
|
|
862
|
-
const allLines = content.split("\\n").filter(Boolean)
|
|
863
|
-
return allLines.slice(-lines)
|
|
864
|
-
} catch {
|
|
865
|
-
return []
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
export async function clear${featureName}Logs(): Promise<void> {
|
|
870
|
-
try {
|
|
871
|
-
await ensureLogDir()
|
|
872
|
-
const logPath = path.join(LOG_DIR, LOG_FILE)
|
|
873
|
-
await fs.writeFile(logPath, "", "utf-8")
|
|
874
|
-
} catch {
|
|
875
|
-
// Ignore
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// ADD-4 Layer 1 三通道输出: DB metadata 回写(由业务服务层调用)
|
|
880
|
-
export type ${featureName}DevAuditRecord = {
|
|
881
|
-
phase: ${featureName}AuditPhase
|
|
882
|
-
detail: string
|
|
883
|
-
extra?: Record<string, unknown>
|
|
884
|
-
timestamp: string
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
export function build${featureName}DevAudit(
|
|
888
|
-
phase: ${featureName}AuditPhase,
|
|
889
|
-
detail: string,
|
|
890
|
-
extra?: Record<string, unknown>
|
|
891
|
-
): ${featureName}DevAuditRecord {
|
|
892
|
-
return {
|
|
893
|
-
phase,
|
|
894
|
-
detail,
|
|
895
|
-
extra,
|
|
896
|
-
timestamp: new Date().toISOString(),
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
`
|
|
900
|
-
|
|
901
|
-
// === Layer 2: 运行时业务审计 (audit.ts) ===
|
|
902
|
-
const runtimeAuditCode = `import { prisma } from "@/lib/prisma"
|
|
903
|
-
import { randomUUID } from "crypto"
|
|
904
|
-
|
|
905
|
-
const PREFIX = "[${prefix}:RUNTIME]"
|
|
906
|
-
|
|
907
|
-
type ${featureName}AuditAction =
|
|
908
|
-
${phaseList.map(p => ` | "${p}"`).join("\n")}
|
|
909
|
-
|
|
910
|
-
export type ${featureName}AuditRecord = {
|
|
911
|
-
action: ${featureName}AuditAction
|
|
912
|
-
entityId: string
|
|
913
|
-
detail: Record<string, unknown>
|
|
914
|
-
traceId?: string
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function formatMessage(record: ${featureName}AuditRecord): string {
|
|
918
|
-
const ts = new Date().toISOString()
|
|
919
|
-
const extraStr = Object.keys(record.detail).length > 0 ? \` | \${JSON.stringify(record.detail)}\` : ""
|
|
920
|
-
const traceStr = record.traceId ? \` trace=\${record.traceId}\` : ""
|
|
921
|
-
return \`\${PREFIX} [\${ts}] [\${record.action}] entity=\${record.entityId}\${traceStr}\${extraStr}\`
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// ADD-4 traceId 运行时排查体系: 同一请求生命周期内所有审计事件共享 traceId
|
|
925
|
-
export function generate${featureName}TraceId(): string {
|
|
926
|
-
return \\\`trace-\\\${randomUUID().slice(0, 8)}\\\`
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
export async function record${featureName}Audit(record: ${featureName}AuditRecord): Promise<void> {
|
|
930
|
-
// Layer 2: 三通道输出 — console
|
|
931
|
-
console.log(formatMessage(record))
|
|
932
|
-
|
|
933
|
-
// Layer 2: 三通道输出 — AuditLog 表(含 traceId 关联)
|
|
934
|
-
try {
|
|
935
|
-
await prisma.auditLog.create({
|
|
936
|
-
data: {
|
|
937
|
-
userId: "system",
|
|
938
|
-
action: record.action,
|
|
939
|
-
targetType: "${featureName}",
|
|
940
|
-
targetId: record.entityId,
|
|
941
|
-
traceId: record.traceId || null,
|
|
942
|
-
afterState: record.detail as Record<string, unknown>,
|
|
943
|
-
reason: \`\${record.action} on \${record.entityId}\`,
|
|
944
|
-
},
|
|
945
|
-
})
|
|
946
|
-
} catch (error) {
|
|
947
|
-
console.error(\`\${PREFIX} Failed to write AuditLog: \${error instanceof Error ? error.message : String(error)}\`)
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
`
|
|
951
|
-
|
|
952
|
-
const parts: string[] = [
|
|
953
|
-
`=== 三层分离式审计日志器: ${domain} ===`,
|
|
954
|
-
`域名: ${domain}`,
|
|
955
|
-
`前缀: [${prefix}]`,
|
|
956
|
-
`日志目录: ${logDir}`,
|
|
957
|
-
`日志文件: ${logFile}`,
|
|
958
|
-
`阶段数: ${phaseList.length}`,
|
|
959
|
-
`阶段列表: ${phaseList.join(", ")}`,
|
|
960
|
-
"",
|
|
961
|
-
"=== 文件1: src/lib/${domain}-dev-logger.ts (Layer 1 开发审计) ===",
|
|
962
|
-
"ADD-4 三通道输出: console.log + fs.appendFile + DB metadata 回写",
|
|
963
|
-
"- console + file: auditPhaseStart/End/Audit 自动输出",
|
|
964
|
-
"- DB metadata: buildXxxDevAudit() 构建记录 → 业务服务层调用 saveAuditData 写入业务表",
|
|
965
|
-
"仅在 NODE_ENV=development 时生效(可插拔),用于 AI 合规检查。",
|
|
966
|
-
"",
|
|
967
|
-
devLoggerCode,
|
|
968
|
-
"",
|
|
969
|
-
"=== 文件2: src/lib/${domain}-audit.ts (Layer 2 运行时业务审计) ===",
|
|
970
|
-
"ADD-4 三通道输出: console.log + fs.appendFile + AuditLog 表写入",
|
|
971
|
-
"始终开启(不可插拔),用于业务记录和前端查询。",
|
|
972
|
-
"",
|
|
973
|
-
runtimeAuditCode,
|
|
974
|
-
"",
|
|
975
|
-
"=== 使用方式 ===",
|
|
976
|
-
"业务服务层同时导入两个文件:",
|
|
977
|
-
` import { ${fnPrefix}AuditPhaseStart, ${fnPrefix}AuditPhaseEnd, ${fnPrefix}Audit } from "@/lib/${domain}-dev-logger"`,
|
|
978
|
-
` import { record${featureName}Audit, generate${featureName}TraceId } from "@/lib/${domain}-audit"`,
|
|
979
|
-
"",
|
|
980
|
-
"开发审计层: auditPhaseStart/End/Audit(仅开发环境生效)+ buildXxxDevAudit → saveAuditData",
|
|
981
|
-
"运行时审计层: recordXxxAudit(始终开启,写入 AuditLog 表)",
|
|
982
|
-
"traceId 体系: generateXxxTraceId() 生成 → 同一请求内所有 recordXxxAudit 传入 traceId → query_audit_logs({ traceId }) 查全链",
|
|
983
|
-
]
|
|
984
|
-
|
|
985
|
-
return textResponse(parts.join("\n"))
|
|
986
|
-
} catch (error) {
|
|
987
|
-
return errorResponse(`生成审计日志器失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
)
|
|
991
|
-
|
|
992
|
-
server.registerTool(
|
|
993
|
-
"query_audit_logs",
|
|
994
|
-
{
|
|
995
|
-
description: "稀疏查询开发操作审计日志(DevOperation 表,MCP-5 稀疏推理恢复)。支持多维度检索,AI 可在不同对话会话中通过任意维度组合查询之前的开发操作记录,实现跨会话的上下文恢复。\n\n典型用法:\n- query_audit_logs({ targetType: \"API_ROUTE\" }) — 查所有 API 路由改动\n- query_audit_logs({ targetId: \"src/app/api/knowledge/route.ts\" }) — 查特定文件改动\n- query_audit_logs({ keyword: \"pagination\" }) — 按关键词搜索\n- query_audit_logs({ planKeyword: \"add-coder\" }) — 按 Plan 关键词查该 Plan 下所有 devlog\n- query_audit_logs({}) — 查最近的记录(session-init 会话恢复)",
|
|
996
|
-
inputSchema: {
|
|
997
|
-
targetType: z.string().optional().describe("按目标类型精确过滤"),
|
|
998
|
-
action: z.string().optional().describe("按操作类型精确过滤"),
|
|
999
|
-
targetId: z.string().optional().describe("按目标标识精确过滤"),
|
|
1000
|
-
planKeyword: z.string().optional().describe("按 Plan 关键词过滤,同 check_dps/check_rahs 的定位键"),
|
|
1001
|
-
keyword: z.string().optional().describe("关键词搜索,在 action/targetType/targetId/reason 字段中模糊匹配"),
|
|
1002
|
-
sinceMinutes: z.number().optional().describe("时间窗口起始(分钟前),不传则不限制时间范围"),
|
|
1003
|
-
limit: z.number().optional().default(20).describe("返回最大条数,默认 20,最大 100"),
|
|
1004
|
-
},
|
|
1005
|
-
},
|
|
1006
|
-
async (args: { targetType?: string; action?: string; targetId?: string; planKeyword?: string; keyword?: string; sinceMinutes?: number; limit?: number }) => {
|
|
1007
|
-
try {
|
|
1008
|
-
const { targetType, action, targetId, planKeyword, keyword, sinceMinutes, limit = 20 } = args
|
|
1009
|
-
|
|
1010
|
-
const where: Record<string, unknown> = {}
|
|
1011
|
-
|
|
1012
|
-
if (sinceMinutes !== undefined) {
|
|
1013
|
-
const since = new Date(Date.now() - sinceMinutes * 60 * 1000)
|
|
1014
|
-
where.createdAt = { gte: since }
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (targetType) where.targetType = targetType
|
|
1018
|
-
if (action) where.action = action
|
|
1019
|
-
if (targetId) where.targetId = targetId
|
|
1020
|
-
if (planKeyword) where.planKeyword = planKeyword
|
|
1021
|
-
|
|
1022
|
-
if (keyword) {
|
|
1023
|
-
where.OR = [
|
|
1024
|
-
{ action: { contains: keyword, mode: "insensitive" } },
|
|
1025
|
-
{ targetType: { contains: keyword, mode: "insensitive" } },
|
|
1026
|
-
{ targetId: { contains: keyword, mode: "insensitive" } },
|
|
1027
|
-
{ reason: { contains: keyword, mode: "insensitive" } },
|
|
1028
|
-
{ planKeyword: { contains: keyword, mode: "insensitive" } },
|
|
1029
|
-
]
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
const logs = await prisma.devOperation.findMany({
|
|
1033
|
-
where,
|
|
1034
|
-
orderBy: { createdAt: "desc" },
|
|
1035
|
-
take: Math.min(limit, 100),
|
|
1036
|
-
include: { user: { select: { username: true } } },
|
|
1037
|
-
})
|
|
1038
|
-
|
|
1039
|
-
if (logs.length === 0) {
|
|
1040
|
-
const parts: string[] = []
|
|
1041
|
-
if (targetType) parts.push(`targetType=${targetType}`)
|
|
1042
|
-
if (action) parts.push(`action=${action}`)
|
|
1043
|
-
if (targetId) parts.push(`targetId=${targetId}`)
|
|
1044
|
-
if (planKeyword) parts.push(`planKeyword=${planKeyword}`)
|
|
1045
|
-
if (keyword) parts.push(`keyword="${keyword}"`)
|
|
1046
|
-
if (sinceMinutes !== undefined) parts.push(`sinceMinutes=${sinceMinutes}`)
|
|
1047
|
-
const filterDesc = parts.length > 0 ? `条件: ${parts.join(", ")}` : "无条件"
|
|
1048
|
-
|
|
1049
|
-
return textResponse(
|
|
1050
|
-
`=== 开发操作审计日志 ===\n\n未找到匹配的审计记录(${filterDesc})。\n\n` +
|
|
1051
|
-
`可能原因: 数据库未运行、尚无相关开发操作记录、或 filter 过于严格。\n` +
|
|
1052
|
-
`建议: 尝试放宽过滤条件,或检查数据库是否正常运行。`
|
|
1053
|
-
)
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
const filters: string[] = []
|
|
1057
|
-
if (targetType) filters.push(`targetType=${targetType}`)
|
|
1058
|
-
if (action) filters.push(`action=${action}`)
|
|
1059
|
-
if (targetId) filters.push(`targetId=${targetId}`)
|
|
1060
|
-
if (planKeyword) filters.push(`planKeyword=${planKeyword}`)
|
|
1061
|
-
if (keyword) filters.push(`keyword="${keyword}"`)
|
|
1062
|
-
if (sinceMinutes !== undefined) filters.push(`最近${sinceMinutes}分钟`)
|
|
1063
|
-
const filterDesc = filters.length > 0 ? `条件: ${filters.join(", ")}` : "无过滤条件(最近全部)"
|
|
1064
|
-
|
|
1065
|
-
const timeRange = logs.length > 0
|
|
1066
|
-
? `${logs[0].createdAt.toISOString()} ~ ${logs[logs.length - 1].createdAt.toISOString()}`
|
|
1067
|
-
: ""
|
|
1068
|
-
|
|
1069
|
-
const lines: string[] = [
|
|
1070
|
-
`=== 开发操作审计日志 (${filterDesc}) ===`,
|
|
1071
|
-
`共 ${logs.length} 条记录`,
|
|
1072
|
-
...(timeRange ? [`时间跨度: ${timeRange}`] : []),
|
|
1073
|
-
"",
|
|
1074
|
-
]
|
|
1075
|
-
|
|
1076
|
-
for (let i = 0; i < logs.length; i++) {
|
|
1077
|
-
const log = logs[i]
|
|
1078
|
-
lines.push(`[${i + 1}] ${log.createdAt.toISOString()}`)
|
|
1079
|
-
lines.push(` action: ${log.action} | targetType: ${log.targetType} | targetId: ${log.targetId || "(无)"}`)
|
|
1080
|
-
if (log.reason) lines.push(` reason: ${log.reason}`)
|
|
1081
|
-
if (log.planKeyword) lines.push(` planKeyword: ${log.planKeyword}`)
|
|
1082
|
-
if (log.beforeState || log.afterState) {
|
|
1083
|
-
lines.push(` beforeState: ${log.beforeState ? JSON.stringify(log.beforeState).slice(0, 200) : "(无)"}`)
|
|
1084
|
-
lines.push(` afterState: ${log.afterState ? JSON.stringify(log.afterState).slice(0, 200) : "(无)"}`)
|
|
1085
|
-
}
|
|
1086
|
-
lines.push("")
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
if (planKeyword) {
|
|
1090
|
-
lines.push("=== Plan 分组 ===")
|
|
1091
|
-
lines.push(`planKeyword=${planKeyword} 的操作链:`)
|
|
1092
|
-
const actions = logs.map((l: typeof logs[0]) => ` ${l.createdAt.toISOString().slice(11, 19)} ${l.action}`)
|
|
1093
|
-
lines.push(...actions)
|
|
1094
|
-
lines.push("")
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
lines.push("=== 稀疏推理建议 ===")
|
|
1098
|
-
lines.push("基于以上审计日志,可以恢复开发上下文。")
|
|
1099
|
-
lines.push("如果新对话的 context 不足,可进一步调用 query_audit_logs 细化查询。")
|
|
1100
|
-
|
|
1101
|
-
return textResponse(lines.join("\n"))
|
|
1102
|
-
} catch (error) {
|
|
1103
|
-
return errorResponse(
|
|
1104
|
-
`查询审计日志失败: ${error instanceof Error ? error.message : String(error)}\n` +
|
|
1105
|
-
`可能原因: 数据库未运行或 AuditLog 表不存在。请先运行 npm run db:start。`
|
|
1106
|
-
)
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
)
|
|
1110
|
-
|
|
1111
|
-
server.registerTool(
|
|
1112
|
-
"record_dev_operation",
|
|
1113
|
-
{
|
|
1114
|
-
description: "记录一次开发操作到 DevOperation 表(ADD-7)。AI 助手在对代码进行任何修改/创建/删除操作后,必须调用此工具记录操作审计。这是稀疏推理(Sparse Inference)的基础——后续 AI Session 通过查询这些记录恢复开发上下文。\n\n**targetId 路径格式(强制)**:必须使用相对于 workspace 根目录的路径,禁止绝对路径。\n- ✅ src/middleware.ts\n- ✅ ${MAGIC_DIR}/plans/xxx.md\n- ✅ agrisynapse/src/api/agent/types.ts(跨项目)\n- ❌ /absolute/path/to/src/middleware.ts(绝对路径,Linux 下不可移植)\n\n**记录后必须回查确认落库**(附录 A.5 规则 6):调用 query_audit_logs({ targetId: \"...\" }) 确认记录已写入。",
|
|
1115
|
-
inputSchema: {
|
|
1116
|
-
action: z.string().describe("操作类型,如 'MODIFY', 'CREATE', 'DELETE', 'API_PAGINATION_ENABLED', 'DOC_UPDATED', 'DOC_CREATED'"),
|
|
1117
|
-
targetType: z.string().describe("目标类型,如 'API_ROUTE', 'COMPONENT', 'SCHEMA', 'RULE', 'DOC', 'DEPENDENCY', 'MCP_TOOL', 'PLAN', 'SPEC', 'HANDOFF', 'SKILL', 'AGENT'"),
|
|
1118
|
-
targetId: z.string().optional().describe("目标标识(相对路径),如 'src/app/api/knowledge/documents/route.ts'"),
|
|
1119
|
-
planKeyword: z.string().optional().describe("关联 Plan 的关键词(同 check_dps/check_rahs 的定位键),同一 Plan 下所有 devlog 共享。用于 session-init 恢复时精准拉取"),
|
|
1120
|
-
beforeState: z.string().optional().describe("操作前的状态(JSON 字符串),描述修改前的关键信息"),
|
|
1121
|
-
afterState: z.string().optional().describe("操作后的状态(JSON 字符串),描述修改后的关键信息"),
|
|
1122
|
-
reason: z.string().optional().describe("操作原因,为什么做这个改动"),
|
|
1123
|
-
},
|
|
1124
|
-
},
|
|
1125
|
-
async (args: { action: string; targetType: string; targetId?: string; planKeyword?: string; beforeState?: string; afterState?: string; reason?: string }) => {
|
|
1126
|
-
try {
|
|
1127
|
-
const { action, targetType, targetId, planKeyword, beforeState, afterState, reason } = args
|
|
1128
|
-
|
|
1129
|
-
// ADD-7: targetId 相对路径校验
|
|
1130
|
-
const pathWarnings: string[] = []
|
|
1131
|
-
if (targetId) {
|
|
1132
|
-
if (targetId.startsWith("/") || /^[A-Z]:\\/.test(targetId)) {
|
|
1133
|
-
pathWarnings.push(`⚠️ targetId 使用了绝对路径: "${targetId}"。ADD-7 要求使用相对于 workspace 根目录的路径(如 src/app/...),请修正后重新记录。`)
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
let parsedBefore: any | undefined
|
|
1138
|
-
let parsedAfter: any | undefined
|
|
1139
|
-
try {
|
|
1140
|
-
if (beforeState) parsedBefore = JSON.parse(beforeState)
|
|
1141
|
-
if (afterState) parsedAfter = JSON.parse(afterState)
|
|
1142
|
-
} catch {
|
|
1143
|
-
const beforePreview = beforeState ? beforeState.slice(0, 80) : "(未传)"
|
|
1144
|
-
const afterPreview = afterState ? afterState.slice(0, 80) : "(未传)"
|
|
1145
|
-
return errorResponse(
|
|
1146
|
-
`beforeState/afterState 必须是有效的 JSON 字符串。\n` +
|
|
1147
|
-
`提示: 请使用 JSON.stringify() 包裹,如 JSON.stringify({"key":"value"})\n` +
|
|
1148
|
-
`接收到: beforeState=${beforePreview}, afterState=${afterPreview}`
|
|
1149
|
-
)
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
let systemUser = await prisma.addUser.findUnique({
|
|
1153
|
-
where: { username: "ai-assistant" },
|
|
1154
|
-
select: { id: true },
|
|
1155
|
-
})
|
|
1156
|
-
|
|
1157
|
-
if (!systemUser) {
|
|
1158
|
-
systemUser = await prisma.addUser.create({
|
|
1159
|
-
data: {
|
|
1160
|
-
id: "ai-assistant",
|
|
1161
|
-
username: "ai-assistant",
|
|
1162
|
-
email: "ai-assistant@internal"
|
|
1163
|
-
},
|
|
1164
|
-
select: { id: true },
|
|
1165
|
-
})
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const log = await prisma.devOperation.create({
|
|
1169
|
-
data: {
|
|
1170
|
-
userId: systemUser.id,
|
|
1171
|
-
planKeyword: planKeyword || "unknown",
|
|
1172
|
-
action,
|
|
1173
|
-
targetType,
|
|
1174
|
-
targetId: targetId || "unknown",
|
|
1175
|
-
beforeState: parsedBefore ?? null,
|
|
1176
|
-
afterState: parsedAfter ?? null,
|
|
1177
|
-
reason: reason || null,
|
|
1178
|
-
},
|
|
1179
|
-
})
|
|
1180
|
-
|
|
1181
|
-
const ts = new Date().toISOString()
|
|
1182
|
-
console.log(`[DEV-AUDIT] [${ts}] [${action}] ${targetType}:${targetId || "unknown"} | plan:${planKeyword || "unknown"} | ${reason || ""} | ${JSON.stringify({ before: parsedBefore, after: parsedAfter })}`)
|
|
1183
|
-
|
|
1184
|
-
const responseLines: string[] = [
|
|
1185
|
-
`✅ 开发操作已记录`,
|
|
1186
|
-
` ID: ${log.id}`,
|
|
1187
|
-
` action: ${action}`,
|
|
1188
|
-
` targetType: ${targetType}`,
|
|
1189
|
-
` targetId: ${targetId || "unknown"}`,
|
|
1190
|
-
` planKeyword: ${planKeyword || "unknown"}`,
|
|
1191
|
-
` createdAt: ${log.createdAt.toISOString()}`,
|
|
1192
|
-
]
|
|
1193
|
-
|
|
1194
|
-
if (pathWarnings.length > 0) {
|
|
1195
|
-
responseLines.push("")
|
|
1196
|
-
responseLines.push(...pathWarnings)
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
// ADD-7 回查提示(附录 A.5 规则 6)
|
|
1200
|
-
responseLines.push("")
|
|
1201
|
-
responseLines.push(`📋 落库回查(必须执行):`)
|
|
1202
|
-
if (targetId) {
|
|
1203
|
-
responseLines.push(` query_audit_logs({ targetId: "${targetId}" }) — 确认本条记录已写入`)
|
|
1204
|
-
}
|
|
1205
|
-
responseLines.push(` 后续 AI Session 可通过 query_audit_logs 查询到此记录,实现跨会话上下文恢复。`)
|
|
1206
|
-
|
|
1207
|
-
return textResponse(responseLines.join("\n"))
|
|
1208
|
-
} catch (error) {
|
|
1209
|
-
return errorResponse(`记录开发操作失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
)
|
|
1213
|
-
|
|
1214
|
-
server.registerTool(
|
|
1215
|
-
"find_related_docs",
|
|
1216
|
-
{
|
|
1217
|
-
description: "搜索与当前变更相关的项目文档(ADD-0.1 广义文档先行)。搜索范围:\n1. docs/ 目录 — 需求文档、架构文档、规范文档\n2. ${MAGIC_DIR}/ 产物 — plans/(Plan+add-route+handoff)、specs/(三元组)、reviews/(方案审查+实现审查+运行时审查)\n\nAI 助手在 ADD 范式 Step 0 中应调用此工具查找需要更新的 docs/ 文档,同时检查 ${MAGIC_DIR}/ 下相关 plan/add-route/spec/review/handoff 文件。",
|
|
1218
|
-
inputSchema: {
|
|
1219
|
-
query: z.string().describe("搜索关键词,如功能名、模块名、API 名等"),
|
|
1220
|
-
category: z.string().optional().describe("文档类别过滤: 'requirement' 需求, 'architecture' 架构, 'standard' 规范, 'plan' 方案, 'add-route' 执行路线图, 'spec' 功能规格, 'review' 审查, 'handoff' 交接"),
|
|
1221
|
-
},
|
|
1222
|
-
},
|
|
1223
|
-
async (args: { query: string; category?: string }) => {
|
|
1224
|
-
try {
|
|
1225
|
-
const { query, category } = args
|
|
1226
|
-
|
|
1227
|
-
const docsDir = join(PROJECT_ROOT, "docs")
|
|
1228
|
-
|
|
1229
|
-
// 类别到目录前缀的映射(docs/ 目录)
|
|
1230
|
-
const categoryPrefixes: Record<string, string[]> = {
|
|
1231
|
-
requirement: ["00-需求"],
|
|
1232
|
-
architecture: ["01-架构", "02-架构"],
|
|
1233
|
-
standard: ["02-规范", "03-规范"],
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// ${MAGIC_DIR}/ 产物类别映射
|
|
1237
|
-
const magicCategoryMap: Record<string, string> = {
|
|
1238
|
-
plans: "plan",
|
|
1239
|
-
specs: "spec",
|
|
1240
|
-
reviews: "review",
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
// 收集所有 Markdown 文件
|
|
1244
|
-
const allFiles: Array<{ path: string; relativePath: string; category: string }> = []
|
|
1245
|
-
|
|
1246
|
-
const walkDir = async (dir: string, relativeDir: string, sourceType: "docs" | "magic" = "docs"): Promise<void> => {
|
|
1247
|
-
try {
|
|
1248
|
-
const entries = await readdir(dir, { withFileTypes: true })
|
|
1249
|
-
for (const entry of entries) {
|
|
1250
|
-
const fullPath = join(dir, entry.name)
|
|
1251
|
-
const relPath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name
|
|
1252
|
-
if (entry.isDirectory()) {
|
|
1253
|
-
await walkDir(fullPath, relPath, sourceType)
|
|
1254
|
-
} else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".html"))) {
|
|
1255
|
-
let docCategory = "unknown"
|
|
1256
|
-
if (sourceType === "magic") {
|
|
1257
|
-
// ${MAGIC_DIR}/ 产物按目录分类
|
|
1258
|
-
const topDir = relPath.split("/")[0]
|
|
1259
|
-
docCategory = magicCategoryMap[topDir] || "unknown"
|
|
1260
|
-
// handoff 文件特殊识别(存放在 plans/ 下但含 handoff 关键词)
|
|
1261
|
-
if (topDir === "plans" && entry.name.includes("handoff")) {
|
|
1262
|
-
docCategory = "handoff"
|
|
1263
|
-
}
|
|
1264
|
-
// add-route 文件特殊识别(存放在 plans/ 下但含 add-route 关键词)
|
|
1265
|
-
if (topDir === "plans" && entry.name.includes("add-route")) {
|
|
1266
|
-
docCategory = "add-route"
|
|
1267
|
-
}
|
|
1268
|
-
} else {
|
|
1269
|
-
// docs/ 按目录前缀分类
|
|
1270
|
-
for (const [cat, prefixes] of Object.entries(categoryPrefixes)) {
|
|
1271
|
-
if (prefixes.some(p => relPath.includes(p))) {
|
|
1272
|
-
docCategory = cat
|
|
1273
|
-
break
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
allFiles.push({ path: fullPath, relativePath: relPath, category: docCategory })
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
} catch {
|
|
1281
|
-
// Ignore errors for non-existent dirs
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
// 搜索 docs/ 目录
|
|
1286
|
-
await walkDir(docsDir, "", "docs")
|
|
1287
|
-
// 搜索 ${MAGIC_DIR}/ 产物目录(plans/specs/reviews)
|
|
1288
|
-
const magicSearchDirs = ["plans", "specs", "reviews"]
|
|
1289
|
-
for (const mDir of magicSearchDirs) {
|
|
1290
|
-
const mPath = join(PROJECT_ROOT, MAGIC_DIR, mDir)
|
|
1291
|
-
if (existsSync(mPath)) {
|
|
1292
|
-
await walkDir(mPath, mDir, "magic")
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// 如果指定了类别,先过滤
|
|
1297
|
-
const allCategories: Record<string, string[]> = {
|
|
1298
|
-
...categoryPrefixes,
|
|
1299
|
-
plan: ["plan"],
|
|
1300
|
-
"add-route": ["add-route"],
|
|
1301
|
-
spec: ["spec"],
|
|
1302
|
-
review: ["review"],
|
|
1303
|
-
handoff: ["handoff"],
|
|
1304
|
-
}
|
|
1305
|
-
let filtered = allFiles
|
|
1306
|
-
if (category && allCategories[category]) {
|
|
1307
|
-
filtered = allFiles.filter(f => f.category === category)
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// 关键词匹配(文件名和内容第一行标题)
|
|
1311
|
-
const queryLower = query.toLowerCase()
|
|
1312
|
-
const matchingDocs: Array<{
|
|
1313
|
-
path: string
|
|
1314
|
-
relativePath: string
|
|
1315
|
-
category: string
|
|
1316
|
-
title: string
|
|
1317
|
-
relevance: number
|
|
1318
|
-
}> = []
|
|
1319
|
-
|
|
1320
|
-
for (const doc of filtered) {
|
|
1321
|
-
const fileName = doc.relativePath.toLowerCase()
|
|
1322
|
-
let title = ""
|
|
1323
|
-
let relevance = 0
|
|
1324
|
-
|
|
1325
|
-
// 文件名匹配
|
|
1326
|
-
if (fileName.includes(queryLower)) {
|
|
1327
|
-
relevance += 3
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// 读取文件第一行获取标题
|
|
1331
|
-
try {
|
|
1332
|
-
const content = await readFileSafe(doc.path)
|
|
1333
|
-
if (content) {
|
|
1334
|
-
const firstLine = content.split("\n")[0]
|
|
1335
|
-
// 尝试获取 Markdown 标题
|
|
1336
|
-
const titleMatch = content.match(/^#\s+(.+)/m)
|
|
1337
|
-
if (titleMatch) {
|
|
1338
|
-
title = titleMatch[1].trim()
|
|
1339
|
-
} else {
|
|
1340
|
-
title = firstLine.replace(/^#+\s*/, "").replace(/[#*]/g, "").trim()
|
|
1341
|
-
}
|
|
1342
|
-
// 内容匹配
|
|
1343
|
-
const contentLower = content.toLowerCase()
|
|
1344
|
-
if (contentLower.includes(queryLower)) {
|
|
1345
|
-
relevance += 2
|
|
1346
|
-
}
|
|
1347
|
-
// 提取相关段落摘要
|
|
1348
|
-
const lines = content.split("\n")
|
|
1349
|
-
const matchingLines = lines.filter(l => l.toLowerCase().includes(queryLower))
|
|
1350
|
-
if (matchingLines.length > 0) {
|
|
1351
|
-
relevance += Math.min(matchingLines.length, 5)
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
} catch {
|
|
1355
|
-
// Ignore read errors
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
if (relevance > 0) {
|
|
1359
|
-
matchingDocs.push({
|
|
1360
|
-
path: doc.path,
|
|
1361
|
-
relativePath: doc.relativePath,
|
|
1362
|
-
category: doc.category,
|
|
1363
|
-
title: title || doc.relativePath.split("/").pop() || doc.relativePath,
|
|
1364
|
-
relevance,
|
|
1365
|
-
})
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// 按相关性降序排序
|
|
1370
|
-
matchingDocs.sort((a, b) => b.relevance - a.relevance)
|
|
1371
|
-
|
|
1372
|
-
// 构建输出
|
|
1373
|
-
const parts: string[] = [
|
|
1374
|
-
`=== 项目文档搜索: "${query}" ===`,
|
|
1375
|
-
`搜索范围: docs/ + ${MAGIC_DIR}/ 产物(plans/specs/reviews)`,
|
|
1376
|
-
category ? `文档类别: ${category}(${allCategories[category]?.join(", ") || category})` : "文档类别: 全部",
|
|
1377
|
-
`匹配文档数: ${matchingDocs.length}`,
|
|
1378
|
-
"",
|
|
1379
|
-
]
|
|
1380
|
-
|
|
1381
|
-
if (matchingDocs.length === 0) {
|
|
1382
|
-
parts.push("未找到匹配的文档。建议:")
|
|
1383
|
-
parts.push("- 检查关键词拼写")
|
|
1384
|
-
parts.push("- 尝试使用更宽泛的关键词")
|
|
1385
|
-
parts.push(`- 确认 docs/ 或 ${MAGIC_DIR}/ 下存在相关文档`)
|
|
1386
|
-
parts.push("")
|
|
1387
|
-
// 列出所有可用文档作为参考
|
|
1388
|
-
parts.push("=== 可用文档列表 ===")
|
|
1389
|
-
for (const doc of allFiles) {
|
|
1390
|
-
const catLabel: Record<string, string> = {
|
|
1391
|
-
requirement: "[需求]",
|
|
1392
|
-
architecture: "[架构]",
|
|
1393
|
-
standard: "[规范]",
|
|
1394
|
-
plan: "[Plan]",
|
|
1395
|
-
spec: "[Spec]",
|
|
1396
|
-
review: "[Review]",
|
|
1397
|
-
handoff: "[Handoff]",
|
|
1398
|
-
unknown: "[其他]",
|
|
1399
|
-
}
|
|
1400
|
-
parts.push(` ${catLabel[doc.category] || "[其他]"} ${doc.relativePath}`)
|
|
1401
|
-
}
|
|
1402
|
-
} else {
|
|
1403
|
-
// 按类别分组展示
|
|
1404
|
-
const byCategory: Record<string, typeof matchingDocs> = {}
|
|
1405
|
-
for (const doc of matchingDocs) {
|
|
1406
|
-
const cat = doc.category || "unknown"
|
|
1407
|
-
if (!byCategory[cat]) byCategory[cat] = []
|
|
1408
|
-
byCategory[cat].push(doc)
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
const catLabelFull: Record<string, string> = {
|
|
1412
|
-
requirement: "需求文档",
|
|
1413
|
-
architecture: "架构文档",
|
|
1414
|
-
standard: "规范文档",
|
|
1415
|
-
plan: "Plan 方案",
|
|
1416
|
-
spec: "Spec 功能规格",
|
|
1417
|
-
review: "Review 审查",
|
|
1418
|
-
handoff: "Handoff 交接",
|
|
1419
|
-
unknown: "其他文档",
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
for (const [cat, docs] of Object.entries(byCategory)) {
|
|
1423
|
-
parts.push(`--- ${catLabelFull[cat] || "其他文档"} (${docs.length}篇) ---`)
|
|
1424
|
-
for (const doc of docs) {
|
|
1425
|
-
parts.push(` [相关度 ${doc.relevance}] ${doc.relativePath}`)
|
|
1426
|
-
parts.push(` 标题: ${doc.title}`)
|
|
1427
|
-
parts.push("")
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
parts.push("=== 使用提示 ===")
|
|
1432
|
-
parts.push("1. 阅读文档确认需要更新的章节")
|
|
1433
|
-
parts.push("2. 在修改代码前先更新文档内容(ADD-0.1 文档先行)")
|
|
1434
|
-
parts.push("3. 更新完成后调用 record_dev_operation 记录文档变更")
|
|
1435
|
-
parts.push(" → targetType: \"DOC\", action: \"DOC_UPDATED\" 或 \"DOC_CREATED\"")
|
|
1436
|
-
parts.push(` → targetId: 相对路径(如 docs/.../xxx.md 或 ${MAGIC_DIR}/specs/xxx/spec.md)`)
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
return textResponse(parts.join("\n"))
|
|
1440
|
-
} catch (error) {
|
|
1441
|
-
return errorResponse(`搜索文档失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
)
|
|
1445
|
-
|
|
1446
|
-
// ========== 新增工具: check_add_route_status ==========
|
|
1447
|
-
// ADD 范式关键守卫点:Step 3 前强制执行 add-route 文件存在性交叉校验
|
|
1448
|
-
server.registerTool(
|
|
1449
|
-
"check_add_route_status",
|
|
1450
|
-
{
|
|
1451
|
-
description: "ADD 范式守卫工具:交叉校验 add-route 文件的审计日志记录与文件系统存在性,并扫描文件内容统计 Step 完成度([ ] vs [x])。\n必须在 Plan 进入 Handoff 或 Step 3 前调用,防止 add-route 丢失导致执行路线图缺失。\n\n三种基础返回状态:\n- 'normal' — 审计日志有记录、文件存在、Step 全部闭环([x]),可继续执行\n- 'warn_step_incomplete' — 文件存在但存在未勾选的 Step 产出项,应自检完成后继续\n- 'file_missing' — 审计日志有记录但文件不存在(可能被误删),应中断并询问用户是否重建\n- 'never_generated' — 审计日志无记录且文件不存在,禁止进入 Step 3,强制回退至 Step 0.5 生成\n\n升级说明(2026-06-11):v2 新增文件内容扫描,在 normal/warn 状态中自动统计 checkbox 完成度,未闭环时返回 warn_step_incomplete。\n\n注意:审计日志查询使用 keyword 模糊匹配,文件系统检查使用 glob 精确匹配。",
|
|
1452
|
-
inputSchema: {
|
|
1453
|
-
planKeyword: z.string().describe("Plan 文件的关键词(用于审计日志搜索和文件匹配),通常为需求域名中的核心关键词,如 'rag-audit-stacktrace'、'多轮对话能力专家'。此关键词会在 ${MAGIC_DIR}/plans/ 目录下匹配 *add-route* 文件,并在审计日志中搜索相关记录。"),
|
|
1454
|
-
},
|
|
1455
|
-
},
|
|
1456
|
-
async (args: { planKeyword: string }) => {
|
|
1457
|
-
try {
|
|
1458
|
-
const { planKeyword } = args
|
|
1459
|
-
if (!planKeyword) return errorResponse("planKeyword 参数不能为空")
|
|
1460
|
-
|
|
1461
|
-
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
|
|
1462
|
-
|
|
1463
|
-
// === 1. 查询审计日志:是否有 add-route 相关记录 ===
|
|
1464
|
-
let auditHasRecord = false
|
|
1465
|
-
const auditRecords: Array<{ action: string; targetId: string; createdAt: Date }> = []
|
|
1466
|
-
try {
|
|
1467
|
-
const auditLogs = await prisma.auditLog.findMany({
|
|
1468
|
-
where: {
|
|
1469
|
-
OR: [
|
|
1470
|
-
{ targetId: { contains: "add-route", mode: "insensitive" } },
|
|
1471
|
-
{ targetId: { contains: planKeyword, mode: "insensitive" } },
|
|
1472
|
-
{ reason: { contains: "add-route", mode: "insensitive" } },
|
|
1473
|
-
],
|
|
1474
|
-
},
|
|
1475
|
-
orderBy: { createdAt: "desc" },
|
|
1476
|
-
take: 20,
|
|
1477
|
-
})
|
|
1478
|
-
// 二次过滤:确认记录与当前 planKeyword 相关
|
|
1479
|
-
const planLower = planKeyword.toLowerCase()
|
|
1480
|
-
for (const log of auditLogs) {
|
|
1481
|
-
const targetId = (log.targetId || "").toLowerCase()
|
|
1482
|
-
const reason = (log.reason || "").toLowerCase()
|
|
1483
|
-
if (
|
|
1484
|
-
targetId.includes("add-route") && targetId.includes(planLower) ||
|
|
1485
|
-
reason.includes("add-route") && reason.includes(planLower)
|
|
1486
|
-
) {
|
|
1487
|
-
auditHasRecord = true
|
|
1488
|
-
auditRecords.push({
|
|
1489
|
-
action: log.action,
|
|
1490
|
-
targetId: log.targetId || "unknown",
|
|
1491
|
-
createdAt: log.createdAt,
|
|
1492
|
-
})
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
} catch {
|
|
1496
|
-
// 审计日志查询失败(如数据库未运行),不阻塞后续文件系统检查
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// === 2. 检查文件系统:是否存在 add-route 文件 ===
|
|
1500
|
-
let fileExists = false
|
|
1501
|
-
const matchedFiles: string[] = []
|
|
1502
|
-
try {
|
|
1503
|
-
const entries = await readdirRecursive(plansDir)
|
|
1504
|
-
const planLower = planKeyword.toLowerCase()
|
|
1505
|
-
for (const entry of entries) {
|
|
1506
|
-
const entryLower = entry.toLowerCase()
|
|
1507
|
-
if (entryLower.includes("add-route") && entryLower.includes(planLower)) {
|
|
1508
|
-
fileExists = true
|
|
1509
|
-
matchedFiles.push(entry)
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
} catch {
|
|
1513
|
-
// plans 目录不存在
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
// === 3. 交叉比对,返回结构化状态 ===
|
|
1517
|
-
const parts: string[] = [
|
|
1518
|
-
"=== ADD 守卫:add-route 存在性交叉校验 ===",
|
|
1519
|
-
`Plan 关键词: "${planKeyword}"`,
|
|
1520
|
-
`预期路径: ${MAGIC_DIR}/plans/{需求域名}-{核心内容}-add-route-v1.md`,
|
|
1521
|
-
"",
|
|
1522
|
-
]
|
|
1523
|
-
|
|
1524
|
-
if (auditHasRecord && fileExists) {
|
|
1525
|
-
// ✅ 正常:审计有记录 + 文件存在
|
|
1526
|
-
|
|
1527
|
-
// === 2.5 内容扫描:统计 add-route Step 完成度 ===
|
|
1528
|
-
let stepTotal = 0, stepChecked = 0, stepUnchecked = 0
|
|
1529
|
-
let scanError: string | null = null
|
|
1530
|
-
const incompleteSteps: string[] = []
|
|
1531
|
-
const stepStatuses: Array<{ step: string; checked: number; unchecked: number }> = []
|
|
1532
|
-
|
|
1533
|
-
try {
|
|
1534
|
-
const fullPath = join(plansDir, matchedFiles[0])
|
|
1535
|
-
const routeContent = await readFileSafe(fullPath)
|
|
1536
|
-
if (routeContent) {
|
|
1537
|
-
const routeLines = routeContent.split("\n")
|
|
1538
|
-
let currentStepName = ""
|
|
1539
|
-
let stepC = 0, stepU = 0
|
|
1540
|
-
|
|
1541
|
-
for (const line of routeLines) {
|
|
1542
|
-
// 追踪当前 Step
|
|
1543
|
-
const stepMatch = line.match(/^##\s+Step\s+(\d+(?:\.\d+)?)/)
|
|
1544
|
-
if (stepMatch) {
|
|
1545
|
-
if (currentStepName && (stepC > 0 || stepU > 0)) {
|
|
1546
|
-
stepStatuses.push({ step: currentStepName, checked: stepC, unchecked: stepU })
|
|
1547
|
-
}
|
|
1548
|
-
currentStepName = stepMatch[1]
|
|
1549
|
-
stepC = 0
|
|
1550
|
-
stepU = 0
|
|
1551
|
-
continue
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
// 统计 checkbox
|
|
1555
|
-
const cbMatch = line.match(/^\s*-\s+\[([ xX])\]\s/)
|
|
1556
|
-
if (cbMatch) {
|
|
1557
|
-
stepTotal++
|
|
1558
|
-
if (cbMatch[1] === "x" || cbMatch[1] === "X") {
|
|
1559
|
-
stepChecked++
|
|
1560
|
-
stepC++
|
|
1561
|
-
} else {
|
|
1562
|
-
stepUnchecked++
|
|
1563
|
-
stepU++
|
|
1564
|
-
if (currentStepName && !incompleteSteps.includes(currentStepName)) {
|
|
1565
|
-
incompleteSteps.push(currentStepName)
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
// 保存最后一个 Step
|
|
1571
|
-
if (currentStepName && (stepC > 0 || stepU > 0)) {
|
|
1572
|
-
stepStatuses.push({ step: currentStepName, checked: stepC, unchecked: stepU })
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
} catch (e) {
|
|
1576
|
-
scanError = e instanceof Error ? e.message : String(e)
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
// === 3. 组装返回 ===
|
|
1580
|
-
const isStepComplete = stepUnchecked === 0
|
|
1581
|
-
parts.push(`状态: ${isStepComplete ? "✅ normal" : "⚠️ warn_step_incomplete"} — add-route 文件存在${isStepComplete ? "且 Step 全部闭环" : "但存在未勾选 Step"}`)
|
|
1582
|
-
parts.push(`操作: ${isStepComplete ? "继续执行后续流程" : "⚠️ 存在未闭环 Step,建议先自检完成后再进入验收阶段(Step 8)"}`)
|
|
1583
|
-
parts.push("")
|
|
1584
|
-
|
|
1585
|
-
parts.push("=== 审计记录 ===")
|
|
1586
|
-
for (const r of auditRecords.slice(0, 5)) {
|
|
1587
|
-
parts.push(` [${r.createdAt.toISOString()}] ${r.action} → ${r.targetId}`)
|
|
1588
|
-
}
|
|
1589
|
-
parts.push("")
|
|
1590
|
-
|
|
1591
|
-
parts.push("=== 匹配文件 ===")
|
|
1592
|
-
for (const f of matchedFiles) {
|
|
1593
|
-
parts.push(` ${MAGIC_DIR}/plans/${f}`)
|
|
1594
|
-
}
|
|
1595
|
-
parts.push("")
|
|
1596
|
-
|
|
1597
|
-
// 内容扫描结果
|
|
1598
|
-
parts.push("=== Step 完成度扫描 ===")
|
|
1599
|
-
if (scanError) {
|
|
1600
|
-
parts.push(` ⚠️ 扫描失败: ${scanError}`)
|
|
1601
|
-
} else if (stepTotal === 0) {
|
|
1602
|
-
parts.push(" ⚠️ 未检测到 checkbox(可能 add-route 使用非标准格式)")
|
|
1603
|
-
} else {
|
|
1604
|
-
const rate = Math.round((stepChecked / stepTotal) * 100)
|
|
1605
|
-
parts.push(` 整体: ${stepChecked}/${stepTotal} (${rate}%)`)
|
|
1606
|
-
if (stepStatuses.length > 0) {
|
|
1607
|
-
for (const s of stepStatuses) {
|
|
1608
|
-
const icon = s.unchecked === 0 ? "✅" : "⬜"
|
|
1609
|
-
parts.push(` Step ${s.step}: ${icon} ${s.checked}/${s.checked + s.unchecked}`)
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
if (incompleteSteps.length > 0) {
|
|
1613
|
-
parts.push("")
|
|
1614
|
-
parts.push(` ⚠️ 未闭环 Step: ${incompleteSteps.join(", ")}`)
|
|
1615
|
-
parts.push(" 建议: 调用 check_add_route_completeness 获取详细清单,完成后重新调用本工具验证")
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
return textResponse(parts.join("\n"))
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
if (auditHasRecord && !fileExists) {
|
|
1623
|
-
// ❌ 文件丢失:审计有记录,但文件不存在
|
|
1624
|
-
parts.push("状态: ❌ file_missing — add-route 文件丢失")
|
|
1625
|
-
parts.push("操作: 中断推理,询问用户原因")
|
|
1626
|
-
parts.push("")
|
|
1627
|
-
parts.push("审计日志显示 add-route 曾经存在,但文件系统中找不到。")
|
|
1628
|
-
parts.push("可能原因: 文件被误删、手动移动、或未正确提交。")
|
|
1629
|
-
parts.push("")
|
|
1630
|
-
parts.push("=== 审计记录(最后 5 条) ===")
|
|
1631
|
-
for (const r of auditRecords.slice(0, 5)) {
|
|
1632
|
-
parts.push(` [${r.createdAt.toISOString()}] ${r.action} → ${r.targetId}`)
|
|
1633
|
-
}
|
|
1634
|
-
parts.push("")
|
|
1635
|
-
parts.push("=== 建议 ===")
|
|
1636
|
-
parts.push("1. 确认文件是否被手动删除或移动")
|
|
1637
|
-
parts.push("2. 如确认丢失,需从 add-route-template.md 重新生成")
|
|
1638
|
-
parts.push("3. 生成后调用 record_dev_operation 记录恢复操作")
|
|
1639
|
-
return errorResponse(parts.join("\n"))
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
if (!auditHasRecord && !fileExists) {
|
|
1643
|
-
// ❌ 从未生成:审计无记录 + 文件不存在
|
|
1644
|
-
parts.push("状态: ❌ never_generated — add-route 文件从未生成")
|
|
1645
|
-
parts.push("操作: 禁止进入 Step 3,强制回退至 Step 0.5 生成 add-route")
|
|
1646
|
-
parts.push("")
|
|
1647
|
-
parts.push(`在 ${MAGIC_DIR}/plans/ 下未找到包含 "${planKeyword}" 和 "add-route" 的文件。`)
|
|
1648
|
-
parts.push("")
|
|
1649
|
-
parts.push("=== 必须执行的步骤 ===")
|
|
1650
|
-
parts.push("1. 回退到 Step 0.5(生成 ADD 执行路线图)")
|
|
1651
|
-
parts.push('2. 调用 get_add_template({ template: "add-route-template" }) 获取模板')
|
|
1652
|
-
parts.push("3. 按模板填充:元信息 + Step 状态 + Task 映射表 + ADD-7 审计策略 + 文件清单")
|
|
1653
|
-
parts.push(`4. 保存为 ${MAGIC_DIR}/plans/{需求域名}-{核心内容}-add-route-v1.md`)
|
|
1654
|
-
parts.push("5. 调用 record_dev_operation 记录创建事件")
|
|
1655
|
-
parts.push("6. 重新调用本工具验证 add-route 已就位,方可进入 Step 3")
|
|
1656
|
-
return errorResponse(parts.join("\n"))
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
// 边缘情况:审计无记录但文件存在(可能是手动创建或从别处复制)
|
|
1660
|
-
// 这是合法但非标准路径,标记为警告但允许继续
|
|
1661
|
-
|
|
1662
|
-
// 内容扫描
|
|
1663
|
-
let wsTotal = 0, wsChecked = 0, wsUnchecked = 0
|
|
1664
|
-
const wsIncomplete: string[] = []
|
|
1665
|
-
try {
|
|
1666
|
-
const fullPath = join(plansDir, matchedFiles[0])
|
|
1667
|
-
const routeContent = await readFileSafe(fullPath)
|
|
1668
|
-
if (routeContent) {
|
|
1669
|
-
let currentStep = ""
|
|
1670
|
-
const routeLines = routeContent.split("\n")
|
|
1671
|
-
for (const line of routeLines) {
|
|
1672
|
-
const sm = line.match(/^##\s+Step\s+(\d+(?:\.\d+)?)/)
|
|
1673
|
-
if (sm) { currentStep = sm[1]; continue }
|
|
1674
|
-
const cm = line.match(/^\s*-\s+\[([ xX])\]\s/)
|
|
1675
|
-
if (cm) {
|
|
1676
|
-
wsTotal++
|
|
1677
|
-
if (cm[1] === "x" || cm[1] === "X") wsChecked++
|
|
1678
|
-
else { wsUnchecked++; if (currentStep && !wsIncomplete.includes(currentStep)) wsIncomplete.push(currentStep) }
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
} catch { /* ignore scan errors in warn path */ }
|
|
1683
|
-
|
|
1684
|
-
const isWarnComplete = wsUnchecked === 0
|
|
1685
|
-
parts.push(`状态: ⚠️ warn${wsTotal > 0 && !isWarnComplete ? "_step_incomplete" : ""} — add-route 文件存在但审计日志无记录${!isWarnComplete ? ",且存在未勾选 Step" : ""}`)
|
|
1686
|
-
parts.push("操作: 允许继续,但建议补记录")
|
|
1687
|
-
parts.push("")
|
|
1688
|
-
parts.push("文件系统中存在 add-route 文件,但审计日志未找到相关记录。")
|
|
1689
|
-
parts.push("可能是手动创建或从其他位置复制而来。")
|
|
1690
|
-
parts.push("")
|
|
1691
|
-
parts.push("=== 匹配文件 ===")
|
|
1692
|
-
for (const f of matchedFiles) {
|
|
1693
|
-
parts.push(` ${MAGIC_DIR}/plans/${f}`)
|
|
1694
|
-
}
|
|
1695
|
-
parts.push("")
|
|
1696
|
-
if (wsTotal > 0) {
|
|
1697
|
-
const rate = Math.round((wsChecked / wsTotal) * 100)
|
|
1698
|
-
parts.push("=== Step 完成度扫描 ===")
|
|
1699
|
-
parts.push(` 整体: ${wsChecked}/${wsTotal} (${rate}%)`)
|
|
1700
|
-
if (wsIncomplete.length > 0) {
|
|
1701
|
-
parts.push(` ⚠️ 未闭环 Step: ${wsIncomplete.join(", ")}`)
|
|
1702
|
-
}
|
|
1703
|
-
parts.push("")
|
|
1704
|
-
}
|
|
1705
|
-
parts.push("=== 建议 ===")
|
|
1706
|
-
parts.push("1. 调用 record_dev_operation 补记录,确保后续 AI Session 可通过审计日志恢复上下文。")
|
|
1707
|
-
if (wsIncomplete.length > 0) {
|
|
1708
|
-
parts.push("2. 调用 check_add_route_completeness 获取未闭环 Step 详细清单,完成后重新调用本工具验证。")
|
|
1709
|
-
}
|
|
1710
|
-
return textResponse(parts.join("\n"))
|
|
1711
|
-
} catch (error) {
|
|
1712
|
-
return errorResponse(`add-route 存在性校验失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
)
|
|
1716
|
-
|
|
1717
|
-
// ========== 新增工具: get_add_template ==========
|
|
1718
|
-
server.registerTool(
|
|
1719
|
-
"get_add_template",
|
|
1720
|
-
{
|
|
1721
|
-
description: "获取 ADD 工作流的文档模板(ADD-0.1 广义文档先行)。ADD 范式要求每次生成文档产物前必须先读取对应模板,禁止凭记忆生成——模板可能在迭代中已更新。\n\n可用模板(12 个):\n- plan-template.md — 需求方案\n- add-route-template.md — Plan→ADD 九阶段执行映射(轻量模式,适合前端/小项目)\n- add-route-template-heavyweight.md — Plan→ADD 九阶段执行映射(重型模式,适合后端/管线/合规场景,强制 check_spec_sync 文档-代码交叉校验)\n- spec-template.md — 功能规格\n- tasks-template.md — 任务拆分\n- checklist-template.md — 验收清单\n- review-template.md — 方案审查(ADD-9)\n- review-implementation-template.md — 实现审查(ADD-10)\n- review-runtime-template.md — 运行时纠偏(ADD-11)\n- handoff-template.md — 交接总览索引\n- handoff-single-round-template.md — 单轮交接(9 章节)\n- handoff-multi-round-template.md — 多轮交接(13 子章节/轮)\n\n本项目默认使用重型 add-route 模板。",
|
|
1722
|
-
inputSchema: {
|
|
1723
|
-
template: z.string().describe("模板名称(不含 .md 后缀),如 'plan-template', 'add-route-template', 'spec-template', 'review-template'。传 'list' 获取所有模板列表。"),
|
|
1724
|
-
},
|
|
1725
|
-
},
|
|
1726
|
-
async (args: { template: string }) => {
|
|
1727
|
-
try {
|
|
1728
|
-
const { template } = args
|
|
1729
|
-
if (!template) return errorResponse("template 参数不能为空")
|
|
1730
|
-
|
|
1731
|
-
const templatesDir = join(PROJECT_ROOT, MAGIC_DIR, "templates")
|
|
1732
|
-
|
|
1733
|
-
if (template === "list") {
|
|
1734
|
-
const entries = await readdir(templatesDir)
|
|
1735
|
-
const mdFiles = entries.filter(f => f.endsWith(".md"))
|
|
1736
|
-
const parts: string[] = [
|
|
1737
|
-
`=== ADD 模板列表(${mdFiles.length} 个) ===`,
|
|
1738
|
-
"",
|
|
1739
|
-
]
|
|
1740
|
-
for (const f of mdFiles) {
|
|
1741
|
-
const content = await readFileSafe(join(templatesDir, f))
|
|
1742
|
-
const firstLine = content?.split("\n")[0] || ""
|
|
1743
|
-
const title = firstLine.replace(/^#+\s*/, "").trim() || f
|
|
1744
|
-
parts.push(` ${f.replace(".md", "")} — ${title}`)
|
|
1745
|
-
}
|
|
1746
|
-
parts.push("")
|
|
1747
|
-
parts.push('用法: get_add_template({ template: "plan-template" })')
|
|
1748
|
-
return textResponse(parts.join("\n"))
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
const fileName = template.endsWith(".md") ? template : `${template}.md`
|
|
1752
|
-
const filePath = join(templatesDir, fileName)
|
|
1753
|
-
|
|
1754
|
-
if (!existsSync(filePath)) {
|
|
1755
|
-
return errorResponse(
|
|
1756
|
-
`未找到模板: ${fileName}\n` +
|
|
1757
|
-
`可用模板请调用: get_add_template({ template: "list" })`
|
|
1758
|
-
)
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
const content = await readFile(filePath, "utf-8")
|
|
1762
|
-
const parts: string[] = [
|
|
1763
|
-
`=== ADD 模板: ${fileName} ===`,
|
|
1764
|
-
`路径: ${MAGIC_DIR}/templates/${fileName}`,
|
|
1765
|
-
"",
|
|
1766
|
-
content,
|
|
1767
|
-
"",
|
|
1768
|
-
"=== 使用提示 ===",
|
|
1769
|
-
"1. 复制模板到目标路径,替换 {占位符} 为实际内容",
|
|
1770
|
-
"2. 禁止凭记忆生成——模板可能在迭代中已更新",
|
|
1771
|
-
"3. 生成文档后调用 record_dev_operation 记录(targetType 视产物类型而定)",
|
|
1772
|
-
]
|
|
1773
|
-
|
|
1774
|
-
return textResponse(parts.join("\n"))
|
|
1775
|
-
} catch (error) {
|
|
1776
|
-
return errorResponse(`获取 ADD 模板失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
)
|
|
1780
|
-
|
|
1781
|
-
// ========== 新增工具: get_spec_context ==========
|
|
1782
|
-
server.registerTool(
|
|
1783
|
-
"get_spec_context",
|
|
1784
|
-
{
|
|
1785
|
-
description: "获取 ${MAGIC_DIR}/specs/ 下指定任务的 specs 三元组上下文(spec.md + tasks.md + checklist.md)。ADD 工作流中每个原子事务对应一个 specs 三元组目录,形成「需求→执行→验收」闭环。\n\nAI 助手在 ADD 范式 Step 3 执行业务逻辑时,应调用此工具获取当前任务的 spec/tasks/checklist 上下文,确保实现与规格一致。",
|
|
1786
|
-
inputSchema: {
|
|
1787
|
-
task: z.string().describe("任务名(即 ${MAGIC_DIR}/specs/ 下的目录名),如 'co-agent-response-strategy'。传 'list' 获取所有任务列表。"),
|
|
1788
|
-
file: z.string().optional().describe("指定读取三元组中的某个文件: 'spec', 'tasks', 'checklist'。不传则返回全部三个文件。"),
|
|
1789
|
-
},
|
|
1790
|
-
},
|
|
1791
|
-
async (args: { task: string; file?: string }) => {
|
|
1792
|
-
try {
|
|
1793
|
-
const { task, file } = args
|
|
1794
|
-
if (!task) return errorResponse("task 参数不能为空")
|
|
1795
|
-
|
|
1796
|
-
const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
|
|
1797
|
-
|
|
1798
|
-
if (task === "list") {
|
|
1799
|
-
const entries = await readdir(specsDir, { withFileTypes: true })
|
|
1800
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
1801
|
-
const parts: string[] = [
|
|
1802
|
-
`=== Specs 任务列表(${dirs.length} 个) ===`,
|
|
1803
|
-
"",
|
|
1804
|
-
]
|
|
1805
|
-
for (const dir of dirs) {
|
|
1806
|
-
const specPath = join(specsDir, dir, "spec.md")
|
|
1807
|
-
const content = await readFileSafe(specPath)
|
|
1808
|
-
const firstLine = content?.split("\n")[0] || ""
|
|
1809
|
-
const title = firstLine.replace(/^#+\s*/, "").trim() || dir
|
|
1810
|
-
// 检查三元组完整性
|
|
1811
|
-
const hasTasks = existsSync(join(specsDir, dir, "tasks.md"))
|
|
1812
|
-
const hasChecklist = existsSync(join(specsDir, dir, "checklist.md"))
|
|
1813
|
-
const status = (hasTasks && hasChecklist) ? "✅" : "⚠️ 不完整"
|
|
1814
|
-
parts.push(` ${status} ${dir} — ${title}`)
|
|
1815
|
-
}
|
|
1816
|
-
parts.push("")
|
|
1817
|
-
parts.push('用法: get_spec_context({ task: "co-agent-response-strategy" })')
|
|
1818
|
-
return textResponse(parts.join("\n"))
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
const taskDir = join(specsDir, task)
|
|
1822
|
-
if (!existsSync(taskDir)) {
|
|
1823
|
-
return errorResponse(
|
|
1824
|
-
`未找到 specs 任务: ${task}\n` +
|
|
1825
|
-
`可用任务请调用: get_spec_context({ task: "list" })`
|
|
1826
|
-
)
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
const trinityFiles: Record<string, string> = {
|
|
1830
|
-
spec: "spec.md",
|
|
1831
|
-
tasks: "tasks.md",
|
|
1832
|
-
checklist: "checklist.md",
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
const parts: string[] = [
|
|
1836
|
-
`=== Specs 三元组: ${task} ===`,
|
|
1837
|
-
`路径: ${MAGIC_DIR}/specs/${task}/`,
|
|
1838
|
-
"",
|
|
1839
|
-
]
|
|
1840
|
-
|
|
1841
|
-
if (file && trinityFiles[file]) {
|
|
1842
|
-
// 只读取指定文件
|
|
1843
|
-
const filePath = join(taskDir, trinityFiles[file])
|
|
1844
|
-
const content = await readFileSafe(filePath)
|
|
1845
|
-
if (content) {
|
|
1846
|
-
parts.push(`=== ${trinityFiles[file]} ===`)
|
|
1847
|
-
parts.push(content)
|
|
1848
|
-
} else {
|
|
1849
|
-
parts.push(`⚠️ ${trinityFiles[file]} 不存在`)
|
|
1850
|
-
}
|
|
1851
|
-
} else {
|
|
1852
|
-
// 读取全部三个文件
|
|
1853
|
-
for (const [key, fileName] of Object.entries(trinityFiles)) {
|
|
1854
|
-
const filePath = join(taskDir, fileName)
|
|
1855
|
-
const content = await readFileSafe(filePath)
|
|
1856
|
-
if (content) {
|
|
1857
|
-
parts.push(`=== ${fileName} ===`)
|
|
1858
|
-
parts.push(content)
|
|
1859
|
-
parts.push("")
|
|
1860
|
-
} else {
|
|
1861
|
-
parts.push(`⚠️ ${fileName} 不存在(三元组不完整)`)
|
|
1862
|
-
parts.push("")
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
return textResponse(parts.join("\n"))
|
|
1868
|
-
} catch (error) {
|
|
1869
|
-
return errorResponse(`获取 Spec 上下文失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
)
|
|
1873
|
-
|
|
1874
|
-
// ========== 新增工具: check_add_compliance ==========
|
|
1875
|
-
server.registerTool(
|
|
1876
|
-
"check_add_compliance",
|
|
1877
|
-
{
|
|
1878
|
-
description: "综合 ADD 合规检查(ADD-1~6 一键验证)。对给定的 TypeScript 代码文件执行以下检查:\n1. ADD-2 阶段标记对称性(auditPhaseStart/End 配对 或 event-based 审计调用一致性)\n2. ADD-6 失败路径审计信息密度(catch 块 vs try 块)\n3. ADD-3 最小可观测单元(循环体内是否有审计调用)\n4. ADD-1 审计基础设施优先(是否导入了审计日志器)\n5. ADD-4 三通道输出(Layer 1 + Layer 2 导入完整性)\n\nAI 助手在 ADD 范式 Step 4(审计数据验证)和 Step 5(AI 自动合规检查)中应调用此工具。",
|
|
1879
|
-
inputSchema: {
|
|
1880
|
-
code: z.string().describe("要检查的 TypeScript 代码文本"),
|
|
1881
|
-
featureName: z.string().optional().describe("功能名称(用于识别对应的审计日志器导入),如 'knowledgeBase'"),
|
|
1882
|
-
projectPattern: z.string().optional().default("phase-marker").describe("项目审计模式: 'phase-marker' (默认, auditPhaseStart/End), 'event-based' (agentAudit() / 自定义审计函数)"),
|
|
1883
|
-
},
|
|
1884
|
-
},
|
|
1885
|
-
async (args: { code: string; featureName?: string; projectPattern?: string }) => {
|
|
1886
|
-
try {
|
|
1887
|
-
const { code, featureName, projectPattern = "phase-marker" } = args
|
|
1888
|
-
if (!code) return errorResponse("code 参数不能为空")
|
|
1889
|
-
|
|
1890
|
-
const isEventBased = projectPattern === "event-based"
|
|
1891
|
-
|
|
1892
|
-
const lines: string[] = [
|
|
1893
|
-
"=== ADD 综合合规检查 ===",
|
|
1894
|
-
` 审计模式: ${isEventBased ? "event-based (agentAudit)" : "phase-marker (auditPhaseStart/End)"}`,
|
|
1895
|
-
"",
|
|
1896
|
-
]
|
|
1897
|
-
|
|
1898
|
-
let passCount = 0
|
|
1899
|
-
let failCount = 0
|
|
1900
|
-
let warnCount = 0
|
|
1901
|
-
|
|
1902
|
-
// === 检查 1: ADD-2 阶段标记对称性 ===
|
|
1903
|
-
const starts: string[] = []
|
|
1904
|
-
const ends: string[] = []
|
|
1905
|
-
let m
|
|
1906
|
-
|
|
1907
|
-
if (isEventBased) {
|
|
1908
|
-
// event-based 模式: 检测 agentAudit() / xxxAudit() 调用
|
|
1909
|
-
const auditCallRegex = /(?:agentAudit|\w+Audit)\s*\(\s*["']([^"']+)["']/g
|
|
1910
|
-
while ((m = auditCallRegex.exec(code)) !== null) starts.push(m[1])
|
|
1911
|
-
} else {
|
|
1912
|
-
const startRegex = /auditPhaseStart\(["']([^"']+)["']/g
|
|
1913
|
-
const endRegex = /auditPhaseEnd\(["']([^"']+)["']/g
|
|
1914
|
-
while ((m = startRegex.exec(code)) !== null) starts.push(m[1])
|
|
1915
|
-
while ((m = endRegex.exec(code)) !== null) ends.push(m[1])
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
const startCounts: Record<string, number> = {}
|
|
1919
|
-
const endCounts: Record<string, number> = {}
|
|
1920
|
-
for (const s of starts) startCounts[s] = (startCounts[s] || 0) + 1
|
|
1921
|
-
for (const e of ends) endCounts[e] = (endCounts[e] || 0) + 1
|
|
1922
|
-
|
|
1923
|
-
const allPhases = new Set([...Object.keys(startCounts), ...Object.keys(endCounts)])
|
|
1924
|
-
const asymmetricPhases: string[] = []
|
|
1925
|
-
allPhases.forEach((phase) => {
|
|
1926
|
-
const sc = startCounts[phase] || 0
|
|
1927
|
-
const ec = endCounts[phase] || 0
|
|
1928
|
-
if (sc !== ec) {
|
|
1929
|
-
asymmetricPhases.push(` ❌ ${phase}: Start=${sc}, End=${ec}`)
|
|
1930
|
-
}
|
|
1931
|
-
})
|
|
1932
|
-
|
|
1933
|
-
lines.push("--- ADD-2 阶段标记对称性 ---")
|
|
1934
|
-
if (isEventBased) {
|
|
1935
|
-
lines.push(` 审计调用总数: ${starts.length}`)
|
|
1936
|
-
if (starts.length > 0) {
|
|
1937
|
-
lines.push(` ✅ 检测到 ${starts.length} 处审计函数调用 (event-based 模式)`)
|
|
1938
|
-
const phaseCounts: Record<string, number> = {}
|
|
1939
|
-
for (const s of starts) phaseCounts[s] = (phaseCounts[s] || 0) + 1
|
|
1940
|
-
lines.push(` 调用分布: ${Object.entries(phaseCounts).map(([k, v]) => `${k}=${v}`).join(", ")}`)
|
|
1941
|
-
passCount++
|
|
1942
|
-
} else {
|
|
1943
|
-
lines.push(" ⚠️ 未检测到 agentAudit() / xxxAudit() 调用")
|
|
1944
|
-
warnCount++
|
|
1945
|
-
}
|
|
1946
|
-
} else {
|
|
1947
|
-
lines.push(` Start 总数: ${starts.length}, End 总数: ${ends.length}`)
|
|
1948
|
-
if (asymmetricPhases.length === 0 && starts.length > 0) {
|
|
1949
|
-
lines.push(" ✅ 阶段标记完全对称")
|
|
1950
|
-
passCount++
|
|
1951
|
-
} else if (starts.length === 0) {
|
|
1952
|
-
lines.push(" ⚠️ 未检测到 auditPhaseStart/End 调用")
|
|
1953
|
-
warnCount++
|
|
1954
|
-
} else {
|
|
1955
|
-
lines.push(` ❌ ${asymmetricPhases.length} 个不对称阶段:`)
|
|
1956
|
-
lines.push(...asymmetricPhases)
|
|
1957
|
-
failCount++
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
lines.push("")
|
|
1961
|
-
|
|
1962
|
-
// === 检查 2: ADD-6 失败路径信息密度 ===
|
|
1963
|
-
const sections = code.split(/catch\s*\(/)
|
|
1964
|
-
lines.push("--- ADD-6 失败路径审计信息密度 ---")
|
|
1965
|
-
if (sections.length <= 1) {
|
|
1966
|
-
lines.push(" ⚠️ 未检测到 try/catch 块")
|
|
1967
|
-
warnCount++
|
|
1968
|
-
} else {
|
|
1969
|
-
let allCatchPass = true
|
|
1970
|
-
for (let i = 1; i < sections.length; i++) {
|
|
1971
|
-
const catchBlock = sections[i]
|
|
1972
|
-
const catchEnd = catchBlock.indexOf("{")
|
|
1973
|
-
if (catchEnd === -1) continue
|
|
1974
|
-
|
|
1975
|
-
const tryBlock = sections[i - 1]
|
|
1976
|
-
const tryExtraMatches = tryBlock.match(/extra[:\s]*\{[^}]*\}/g)
|
|
1977
|
-
const tryExtraFieldCount = tryExtraMatches
|
|
1978
|
-
? tryExtraMatches.reduce((sum, mm) => sum + (mm.match(/\w+:/g)?.length || 0), 0)
|
|
1979
|
-
: 0
|
|
1980
|
-
|
|
1981
|
-
const closeIdx = catchBlock.indexOf("}")
|
|
1982
|
-
const catchBody = catchBlock.slice(catchEnd, closeIdx + 1)
|
|
1983
|
-
const catchExtraMatches = catchBody.match(/extra[:\s]*\{[^}]*\}/g)
|
|
1984
|
-
const catchExtraFieldCount = catchExtraMatches
|
|
1985
|
-
? catchExtraMatches.reduce((sum, mm) => sum + (mm.match(/\w+:/g)?.length || 0), 0)
|
|
1986
|
-
: 0
|
|
1987
|
-
|
|
1988
|
-
const catchHasAudit = catchBody.includes("audit") || catchBody.includes("Audit")
|
|
1989
|
-
const catchInfoDensity = catchExtraFieldCount + (catchHasAudit ? 2 : 0)
|
|
1990
|
-
|
|
1991
|
-
if (!catchHasAudit) {
|
|
1992
|
-
lines.push(` ❌ Catch 块 #${i}: 缺少审计调用`)
|
|
1993
|
-
allCatchPass = false
|
|
1994
|
-
} else if (catchInfoDensity < tryExtraFieldCount) {
|
|
1995
|
-
lines.push(` ❌ Catch 块 #${i}: 信息密度不足 (catch=${catchExtraFieldCount}, try=${tryExtraFieldCount})`)
|
|
1996
|
-
allCatchPass = false
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
if (allCatchPass) {
|
|
2000
|
-
lines.push(` ✅ 所有 ${sections.length - 1} 个 catch 块审计信息密度充足`)
|
|
2001
|
-
passCount++
|
|
2002
|
-
} else {
|
|
2003
|
-
failCount++
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
lines.push("")
|
|
2007
|
-
|
|
2008
|
-
// === 检查 3: ADD-3 最小可观测单元 ===
|
|
2009
|
-
lines.push("--- ADD-3 最小可观测单元 ---")
|
|
2010
|
-
const loopRegex = /for\s*\(|while\s*\(|\.forEach\s*\(|\.map\s*\(/g
|
|
2011
|
-
const loopCount = (code.match(loopRegex) || []).length
|
|
2012
|
-
if (loopCount === 0) {
|
|
2013
|
-
lines.push(" ⚠️ 未检测到循环体,跳过此项")
|
|
2014
|
-
warnCount++
|
|
2015
|
-
} else if (isEventBased) {
|
|
2016
|
-
// event-based 模式: 查找循环体内是否有审计调用(批次级别审计)
|
|
2017
|
-
const loopAuditRegex = /\b\w*Audit\s*\(["']/g
|
|
2018
|
-
const hasLoopAudit = loopAuditRegex.test(code)
|
|
2019
|
-
if (hasLoopAudit) {
|
|
2020
|
-
lines.push(` ✅ 批次级审计调用检测通过 (event-based 模式),循环数: ${loopCount}`)
|
|
2021
|
-
passCount++
|
|
2022
|
-
} else {
|
|
2023
|
-
lines.push(` ⚠️ event-based 模式: ${loopCount} 个循环体,未检测到批次级审计调用。检查是否有批次级 agentAuditXxx() 包裹`)
|
|
2024
|
-
warnCount++
|
|
2025
|
-
}
|
|
2026
|
-
} else {
|
|
2027
|
-
const chunkRegex = /audit\(["']\w*_CHUNK/i
|
|
2028
|
-
const hasChunkAudit = chunkRegex.test(code)
|
|
2029
|
-
if (hasChunkAudit) {
|
|
2030
|
-
lines.push(` ✅ 循环体内有审计调用(检测到 CHUNK 阶段),循环数: ${loopCount}`)
|
|
2031
|
-
passCount++
|
|
2032
|
-
} else {
|
|
2033
|
-
lines.push(` ❌ 检测到 ${loopCount} 个循环体,但未发现循环内审计调用(CHUNK 阶段)`)
|
|
2034
|
-
failCount++
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
lines.push("")
|
|
2038
|
-
|
|
2039
|
-
// === 检查 4: ADD-1 审计基础设施导入 ===
|
|
2040
|
-
lines.push("--- ADD-1 审计基础设施导入 ---")
|
|
2041
|
-
let hasDevLoggerImport = false
|
|
2042
|
-
let hasRuntimeAuditImport = false
|
|
2043
|
-
|
|
2044
|
-
if (isEventBased) {
|
|
2045
|
-
// event-based 模式: 检查 agentAudit 导入或 agent-audit-logger 导入
|
|
2046
|
-
hasDevLoggerImport = /from\s+["'].*agent-audit-logger["']/.test(code) || /import\s*\{\s*agentAudit\b/.test(code)
|
|
2047
|
-
hasRuntimeAuditImport = hasDevLoggerImport // event-based 模式通常二合一
|
|
2048
|
-
} else {
|
|
2049
|
-
hasDevLoggerImport = /from\s+["'].*dev-logger["']/.test(code) || /from\s+["'].*logger["']/.test(code)
|
|
2050
|
-
hasRuntimeAuditImport = /from\s+["'].*-audit["']/.test(code)
|
|
2051
|
-
}
|
|
2052
|
-
if (hasDevLoggerImport && hasRuntimeAuditImport) {
|
|
2053
|
-
if (isEventBased) {
|
|
2054
|
-
lines.push(" ✅ 审计基础设施导入完整 (event-based: agentAudit/agent-audit-logger)")
|
|
2055
|
-
} else {
|
|
2056
|
-
lines.push(" ✅ Layer 1 (dev-logger) + Layer 2 (runtime-audit) 导入完整")
|
|
2057
|
-
}
|
|
2058
|
-
passCount++
|
|
2059
|
-
} else if (hasDevLoggerImport) {
|
|
2060
|
-
lines.push(isEventBased
|
|
2061
|
-
? " ⚠️ 检测到 agentAudit 导入,确认审计覆盖完整"
|
|
2062
|
-
: " ⚠️ 仅导入 Layer 1 (dev-logger),缺少 Layer 2 (runtime-audit)"
|
|
2063
|
-
)
|
|
2064
|
-
warnCount++
|
|
2065
|
-
} else if (hasRuntimeAuditImport) {
|
|
2066
|
-
lines.push(" ⚠️ 仅导入 Layer 2 (runtime-audit),缺少 Layer 1 (dev-logger)")
|
|
2067
|
-
warnCount++
|
|
2068
|
-
} else {
|
|
2069
|
-
lines.push(isEventBased
|
|
2070
|
-
? " ⚠️ 未检测到 agentAudit 导入 (event-based 模式)"
|
|
2071
|
-
: " ❌ 未检测到审计日志器导入"
|
|
2072
|
-
)
|
|
2073
|
-
if (isEventBased) warnCount++
|
|
2074
|
-
else failCount++
|
|
2075
|
-
}
|
|
2076
|
-
lines.push("")
|
|
2077
|
-
|
|
2078
|
-
// === 汇总 ===
|
|
2079
|
-
const total = passCount + failCount + warnCount
|
|
2080
|
-
lines.push("=== 合规检查汇总 ===")
|
|
2081
|
-
lines.push(` 总计 ${total} 项检查: ✅ 通过 ${passCount} | ❌ 不合规 ${failCount} | ⚠️ 警告 ${warnCount}`)
|
|
2082
|
-
if (failCount === 0) {
|
|
2083
|
-
lines.push(" 🎉 ADD 合规检查全部通过(或仅有警告)")
|
|
2084
|
-
} else {
|
|
2085
|
-
lines.push(" ⚠️ 存在不合规项,请修正后重新检查")
|
|
2086
|
-
}
|
|
2087
|
-
lines.push("")
|
|
2088
|
-
lines.push("=== 修正建议 ===")
|
|
2089
|
-
if (failCount > 0) {
|
|
2090
|
-
lines.push("1. 补充缺失的 auditPhaseEnd 调用(ADD-2)")
|
|
2091
|
-
lines.push("2. 在 catch 块中添加与 try 块等价的信息密度(ADD-6)")
|
|
2092
|
-
lines.push("3. 在循环体内添加 CHUNK 阶段审计(ADD-3)")
|
|
2093
|
-
lines.push("4. 导入 dev-logger 和 runtime-audit 两个文件(ADD-1/4)")
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
return textResponse(lines.join("\n"))
|
|
2097
|
-
} catch (error) {
|
|
2098
|
-
return errorResponse(`ADD 合规检查失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
)
|
|
2102
|
-
|
|
2103
|
-
server.registerTool(
|
|
2104
|
-
"check_spec_sync",
|
|
2105
|
-
{
|
|
2106
|
-
description: "ADD 重型模式文档-代码交叉校验工具。扫描 Plan → tasks.md → checklist.md → git diff → ADD-7 审计记录,报告四者之间的不一致。用于重型 add-route 的 Step 3.5 / Step 4 / Step 8 验证并更新项目状态。轻量模式项目可跳过此工具。",
|
|
2107
|
-
inputSchema: {
|
|
2108
|
-
planKeyword: z.string().describe("Plan 文件的关键词,用于定位 Plan 和关联的 specs 目录"),
|
|
2109
|
-
},
|
|
2110
|
-
},
|
|
2111
|
-
async (args: { planKeyword: string }) => {
|
|
2112
|
-
try {
|
|
2113
|
-
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
|
|
2114
|
-
const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
|
|
2115
|
-
const lines: string[] = []
|
|
2116
|
-
lines.push("=== check_spec_sync 文档-代码交叉校验 ===")
|
|
2117
|
-
lines.push("")
|
|
2118
|
-
|
|
2119
|
-
// 1. 查找 Plan 文件
|
|
2120
|
-
if (!existsSync(plansDir)) {
|
|
2121
|
-
return errorResponse(`plans 目录不存在: ${plansDir}`)
|
|
2122
|
-
}
|
|
2123
|
-
const planFiles = (await readdirRecursive(plansDir)).filter(f => f.endsWith(".md"))
|
|
2124
|
-
// 优先匹配 -plan-v 文件(避免 add-route 文件名干扰)
|
|
2125
|
-
let planMatch = planFiles.find(f => f.toLowerCase().includes(args.planKeyword.toLowerCase()) && f.includes("-plan-v"))
|
|
2126
|
-
if (!planMatch) {
|
|
2127
|
-
planMatch = planFiles.find(f => f.toLowerCase().includes(args.planKeyword.toLowerCase()))
|
|
2128
|
-
}
|
|
2129
|
-
if (!planMatch) {
|
|
2130
|
-
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${args.planKeyword})`)
|
|
2131
|
-
}
|
|
2132
|
-
const planPath = join(plansDir, planMatch)
|
|
2133
|
-
const planGuard = await validateDocWithGuard(planPath)
|
|
2134
|
-
lines.push(`Plan: ${planMatch}`)
|
|
2135
|
-
if (!planGuard.ok) lines.push(` ⚠️ 模板校验: ${planGuard.issues.split("\n")[0]}`)
|
|
2136
|
-
|
|
2137
|
-
// 2. 从 Plan 中提取关联的 spec 目录名
|
|
2138
|
-
const planContent = await readFileSafe(planPath)
|
|
2139
|
-
let specDirName = ""
|
|
2140
|
-
if (planContent) {
|
|
2141
|
-
const specMatch = planContent.match(/Spec:\s*\.(qoder|claude|add|vscode)\/specs\/([^/\s]+)/)
|
|
2142
|
-
if (specMatch) specDirName = specMatch[1]
|
|
2143
|
-
}
|
|
2144
|
-
if (!specDirName) {
|
|
2145
|
-
// 退而求其次:从 tasks.md 引用提取
|
|
2146
|
-
const taskMatch = planContent?.match(/Tasks:\s*\.(qoder|claude|add|vscode)\/specs\/([^/\s]+)/)
|
|
2147
|
-
if (taskMatch) specDirName = taskMatch[1]
|
|
2148
|
-
}
|
|
2149
|
-
if (!specDirName) {
|
|
2150
|
-
// 再退:用 plan 文件名推断
|
|
2151
|
-
specDirName = basename(planMatch).replace(/-plan-v\d+\.md$/, "").replace(/-add-route-v\d+\.md$/, "")
|
|
2152
|
-
}
|
|
2153
|
-
lines.push(`Spec 目录: ${specDirName}`)
|
|
2154
|
-
lines.push("")
|
|
2155
|
-
|
|
2156
|
-
// === Policy 读取:${MAGIC_DIR}/sync-policy.json ===
|
|
2157
|
-
type CheckName = "tasks" | "checklist" | "handoff" | "addRoute" | "crossRef" | "rollback" | "gitDiff" | "auditLog"
|
|
2158
|
-
interface PolicyCheck { enabled: boolean; severity: "block" | "warn" | "info" }
|
|
2159
|
-
interface SyncPolicy { checks: Partial<Record<CheckName, PolicyCheck>> }
|
|
2160
|
-
const policyPath = join(PROJECT_ROOT, MAGIC_DIR, "sync-policy.json")
|
|
2161
|
-
let policy: SyncPolicy | null = null
|
|
2162
|
-
if (existsSync(policyPath)) {
|
|
2163
|
-
try {
|
|
2164
|
-
policy = JSON.parse(await readFileSafe(policyPath) || "{}")
|
|
2165
|
-
lines.push(`Policy: ${MAGIC_DIR}/sync-policy.json 已加载`)
|
|
2166
|
-
} catch {
|
|
2167
|
-
lines.push(`Policy: ${MAGIC_DIR}/sync-policy.json 解析失败,使用默认策略`)
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
if (!policy) {
|
|
2171
|
-
// 降级:是否有关联 add-route → heavyweight;否则 lightweight
|
|
2172
|
-
const addRouteCandidate = planMatch.replace(/-plan-v\d+\.md$/, "-add-route-v1.md")
|
|
2173
|
-
const hasAddRoute = existsSync(join(plansDir, addRouteCandidate))
|
|
2174
|
-
const isHeavy = hasAddRoute
|
|
2175
|
-
policy = {
|
|
2176
|
-
checks: isHeavy ? {
|
|
2177
|
-
tasks: { enabled: true, severity: "block" },
|
|
2178
|
-
checklist: { enabled: true, severity: "block" },
|
|
2179
|
-
handoff: { enabled: true, severity: "block" },
|
|
2180
|
-
addRoute: { enabled: true, severity: "warn" },
|
|
2181
|
-
crossRef: { enabled: true, severity: "warn" },
|
|
2182
|
-
rollback: { enabled: true, severity: "block" },
|
|
2183
|
-
gitDiff: { enabled: true, severity: "warn" },
|
|
2184
|
-
auditLog: { enabled: true, severity: "block" },
|
|
2185
|
-
} : {
|
|
2186
|
-
tasks: { enabled: true, severity: "block" },
|
|
2187
|
-
checklist: { enabled: true, severity: "block" },
|
|
2188
|
-
handoff: { enabled: false, severity: "warn" },
|
|
2189
|
-
addRoute: { enabled: false, severity: "warn" },
|
|
2190
|
-
crossRef: { enabled: false, severity: "warn" },
|
|
2191
|
-
rollback: { enabled: false, severity: "warn" },
|
|
2192
|
-
gitDiff: { enabled: true, severity: "warn" },
|
|
2193
|
-
auditLog: { enabled: true, severity: "warn" },
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
lines.push(`Policy: 默认${isHeavy ? "重型" : "轻型"}策略(${isHeavy ? "发现 add-route" : "无 add-route"})`)
|
|
2197
|
-
}
|
|
2198
|
-
const isEnabled = (name: CheckName) => policy!.checks[name]?.enabled !== false
|
|
2199
|
-
const isBlock = (name: CheckName) => policy!.checks[name]?.severity === "block"
|
|
2200
|
-
const isWarn = (name: CheckName) => policy!.checks[name]?.severity === "warn"
|
|
2201
|
-
const blockingFailures: string[] = []
|
|
2202
|
-
lines.push("")
|
|
2203
|
-
|
|
2204
|
-
// 3. 读取 tasks.md
|
|
2205
|
-
let tasksContent = ""
|
|
2206
|
-
let uncheckedTasks = 0
|
|
2207
|
-
let checkedTasks = 0
|
|
2208
|
-
if (isEnabled("tasks")) {
|
|
2209
|
-
const tasksPath = join(specsDir, specDirName, "tasks.md")
|
|
2210
|
-
tasksContent = await readFileSafe(tasksPath) || ""
|
|
2211
|
-
if (tasksContent) {
|
|
2212
|
-
const unchecked = tasksContent.match(/^- \[ \] Task/gm)
|
|
2213
|
-
const checked = tasksContent.match(/^- \[x\] Task/gm)
|
|
2214
|
-
uncheckedTasks = unchecked ? unchecked.length : 0
|
|
2215
|
-
checkedTasks = checked ? checked.length : 0
|
|
2216
|
-
lines.push(`tasks.md: ${checkedTasks} 已完成 / ${uncheckedTasks} 未完成`)
|
|
2217
|
-
if (uncheckedTasks > 0 && isBlock("tasks")) blockingFailures.push("tasks.md 有未完成项")
|
|
2218
|
-
} else {
|
|
2219
|
-
lines.push(`tasks.md: 文件不存在`)
|
|
2220
|
-
if (isBlock("tasks")) blockingFailures.push("tasks.md 不存在")
|
|
2221
|
-
}
|
|
2222
|
-
} else {
|
|
2223
|
-
lines.push("tasks.md: 已禁用(policy)")
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
// 4. 读取 checklist.md
|
|
2227
|
-
let checklistContent = ""
|
|
2228
|
-
let uncheckedChecklist = 0
|
|
2229
|
-
let checkedChecklist = 0
|
|
2230
|
-
if (isEnabled("checklist")) {
|
|
2231
|
-
const checklistPath = join(specsDir, specDirName, "checklist.md")
|
|
2232
|
-
checklistContent = await readFileSafe(checklistPath) || ""
|
|
2233
|
-
if (checklistContent) {
|
|
2234
|
-
const unchecked = checklistContent.match(/^- \[ \] /gm)
|
|
2235
|
-
const checked = checklistContent.match(/^- \[x\] /gm)
|
|
2236
|
-
uncheckedChecklist = unchecked ? unchecked.length : 0
|
|
2237
|
-
checkedChecklist = checked ? checked.length : 0
|
|
2238
|
-
lines.push(`checklist.md: ${checkedChecklist} 已勾选 / ${uncheckedChecklist} 未勾选`)
|
|
2239
|
-
if (uncheckedChecklist > 0 && isBlock("checklist")) blockingFailures.push("checklist.md 有未勾选项")
|
|
2240
|
-
} else {
|
|
2241
|
-
lines.push(`checklist.md: 文件不存在`)
|
|
2242
|
-
if (isBlock("checklist")) blockingFailures.push("checklist.md 不存在")
|
|
2243
|
-
}
|
|
2244
|
-
} else {
|
|
2245
|
-
lines.push("checklist.md: 已禁用(policy)")
|
|
2246
|
-
}
|
|
2247
|
-
lines.push("")
|
|
2248
|
-
|
|
2249
|
-
// 4.5. 扫描 Handoff 交接文档
|
|
2250
|
-
let hfUnchecked = -1
|
|
2251
|
-
let hfChecked = 0
|
|
2252
|
-
let hfPreChecksUnchecked = 0
|
|
2253
|
-
let hfPostChecksUnchecked = 0
|
|
2254
|
-
let handoffContent = ""
|
|
2255
|
-
let actualHandoffPath: string | null = null
|
|
2256
|
-
// L1 约定路径(提前计算,crossRef 区块也需要)
|
|
2257
|
-
const handoffFileName = planMatch.replace(/-plan-v\d+\.md$/, "-handoff-v1.md")
|
|
2258
|
-
if (isEnabled("handoff")) {
|
|
2259
|
-
// ★ 搜索策略(三级 fallback,修复路径匹配 bug)
|
|
2260
|
-
// L1: 约定路径 {planName}-handoff-v1.md
|
|
2261
|
-
const handoffPath = join(plansDir, handoffFileName)
|
|
2262
|
-
// L2: specDirName 约定 {specDirName}-handoff-v1.md 或 {specDirName}-handoff.md
|
|
2263
|
-
const hasVersionedVariant = specDirName ? `${specDirName}-handoff-v1.md` : null
|
|
2264
|
-
const hasUnversionedVariant = specDirName ? `${specDirName}-handoff.md` : null
|
|
2265
|
-
// L3: 模糊搜索 plansDir 中所有 handoff 文件,用 planKeyword 匹配
|
|
2266
|
-
const allHandoffFiles = planFiles.filter(f => f.includes("handoff"))
|
|
2267
|
-
|
|
2268
|
-
if (existsSync(handoffPath)) {
|
|
2269
|
-
actualHandoffPath = handoffPath
|
|
2270
|
-
} else if (hasVersionedVariant && existsSync(join(plansDir, hasVersionedVariant))) {
|
|
2271
|
-
actualHandoffPath = join(plansDir, hasVersionedVariant)
|
|
2272
|
-
} else if (hasUnversionedVariant && existsSync(join(plansDir, hasUnversionedVariant))) {
|
|
2273
|
-
actualHandoffPath = join(plansDir, hasUnversionedVariant)
|
|
2274
|
-
} else {
|
|
2275
|
-
// L3 fallback: 关键词模糊匹配
|
|
2276
|
-
actualHandoffPath = allHandoffFiles
|
|
2277
|
-
.map(f => join(plansDir, f))
|
|
2278
|
-
.find(p => {
|
|
2279
|
-
const filename = p.split("/").pop()?.toLowerCase() || ""
|
|
2280
|
-
return filename.includes(args.planKeyword.toLowerCase().replace(/\s+/g, "-"))
|
|
2281
|
-
|| filename.includes(specDirName.toLowerCase())
|
|
2282
|
-
}) || null
|
|
2283
|
-
}
|
|
2284
|
-
if (actualHandoffPath) {
|
|
2285
|
-
handoffContent = await readFileSafe(actualHandoffPath) || ""
|
|
2286
|
-
if (handoffContent) {
|
|
2287
|
-
hfUnchecked = (handoffContent.match(/^- \[ \] /gm) || []).length
|
|
2288
|
-
hfChecked = (handoffContent.match(/^- \[x\] /gm) || []).length
|
|
2289
|
-
const hfPreSection = handoffContent.match(/执行前置检查[\s\S]*?(?=##\s|$)/)
|
|
2290
|
-
const hfPostSection = handoffContent.match(/后置确认[\s\S]*?(?=##\s|---|$)/)
|
|
2291
|
-
if (hfPreSection) {
|
|
2292
|
-
hfPreChecksUnchecked = (hfPreSection[0].match(/^- \[ \] /gm) || []).length
|
|
2293
|
-
}
|
|
2294
|
-
if (hfPostSection) {
|
|
2295
|
-
hfPostChecksUnchecked = (hfPostSection[0].match(/^- \[ \] /gm) || []).length
|
|
2296
|
-
}
|
|
2297
|
-
lines.push(`Handoff: ${hfChecked} 已勾选 / ${hfUnchecked} 未勾选(前置:${hfPreChecksUnchecked} 未完成, 后置:${hfPostChecksUnchecked} 未完成)`)
|
|
2298
|
-
if (hfUnchecked > 0 && isBlock("handoff")) blockingFailures.push("Handoff 有未勾选项")
|
|
2299
|
-
} else {
|
|
2300
|
-
lines.push("Handoff: 文件存在但无法读取")
|
|
2301
|
-
}
|
|
2302
|
-
} else {
|
|
2303
|
-
lines.push("Handoff: 未找到交接文档")
|
|
2304
|
-
if (isBlock("handoff")) blockingFailures.push("Handoff 文档不存在")
|
|
2305
|
-
}
|
|
2306
|
-
} else {
|
|
2307
|
-
lines.push("Handoff: 已禁用(policy)")
|
|
2308
|
-
}
|
|
2309
|
-
lines.push("")
|
|
2310
|
-
|
|
2311
|
-
// 4.6. 扫描 add-route 残余 [ ] 项
|
|
2312
|
-
let arUnchecked = 0
|
|
2313
|
-
let arChecked = 0
|
|
2314
|
-
if (isEnabled("addRoute")) {
|
|
2315
|
-
// ★ 搜索策略(三级 fallback,修复路径匹配 bug)
|
|
2316
|
-
// L1: 约定路径 {planName}-add-route-v1.md
|
|
2317
|
-
const addRouteMatch = planMatch.replace(/-plan-v\d+\.md$/, "-add-route-v1.md")
|
|
2318
|
-
let addRoutePath = join(plansDir, addRouteMatch)
|
|
2319
|
-
if (!existsSync(addRoutePath)) {
|
|
2320
|
-
// L2: specDirName 约定 {specDirName}-add-route-v1.md
|
|
2321
|
-
const specAddRouteName = specDirName ? `${specDirName}-add-route-v1.md` : null
|
|
2322
|
-
if (specAddRouteName && existsSync(join(plansDir, specAddRouteName))) {
|
|
2323
|
-
addRoutePath = join(plansDir, specAddRouteName)
|
|
2324
|
-
} else {
|
|
2325
|
-
// L3: 模糊搜索 plansDir 中所有 add-route 文件,用 planKeyword 匹配
|
|
2326
|
-
const foundByKeyword = planFiles
|
|
2327
|
-
.filter(f => f.includes("add-route"))
|
|
2328
|
-
.find(f => {
|
|
2329
|
-
const filename = f.toLowerCase()
|
|
2330
|
-
return filename.includes(args.planKeyword.toLowerCase().replace(/\s+/g, "-"))
|
|
2331
|
-
|| filename.includes(specDirName.toLowerCase())
|
|
2332
|
-
})
|
|
2333
|
-
if (foundByKeyword) {
|
|
2334
|
-
addRoutePath = join(plansDir, foundByKeyword)
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
if (existsSync(addRoutePath)) {
|
|
2339
|
-
const arContent = await readFileSafe(addRoutePath)
|
|
2340
|
-
if (arContent) {
|
|
2341
|
-
arUnchecked = (arContent.match(/^- \[ \] /gm) || []).length
|
|
2342
|
-
arChecked = (arContent.match(/^- \[x\] /gm) || []).length
|
|
2343
|
-
lines.push(`add-route: ${arChecked} 已勾选 / ${arUnchecked} 未勾选`)
|
|
2344
|
-
if (arUnchecked > 0 && isBlock("addRoute")) blockingFailures.push("add-route 有未勾选项")
|
|
2345
|
-
}
|
|
2346
|
-
} else {
|
|
2347
|
-
lines.push("add-route: 未找到")
|
|
2348
|
-
}
|
|
2349
|
-
} else {
|
|
2350
|
-
lines.push("add-route: 已禁用(policy)")
|
|
2351
|
-
}
|
|
2352
|
-
lines.push("")
|
|
2353
|
-
|
|
2354
|
-
// 4.7. 反向引用检查:Handoff → 下游 Plan 是否引用了本 Handoff
|
|
2355
|
-
const crossRefIssues: string[] = []
|
|
2356
|
-
if (isEnabled("crossRef")) {
|
|
2357
|
-
if (actualHandoffPath && handoffContent) {
|
|
2358
|
-
const targetPlanRefs = handoffContent.match(/\]\(([^\)]+)\)/g) || []
|
|
2359
|
-
for (const ref of targetPlanRefs.slice(0, 5)) {
|
|
2360
|
-
const planName = ref.replace(/^\]\(/, "").replace(/\)$/, "")
|
|
2361
|
-
if (!planName.endsWith("-plan-v1.md") && !planName.includes("Expert-Grounding")) continue
|
|
2362
|
-
const planFile = planFiles.find(f => planName.endsWith(f))
|
|
2363
|
-
if (planFile) {
|
|
2364
|
-
const downstreamContent = await readFileSafe(join(plansDir, planFile))
|
|
2365
|
-
if (downstreamContent) {
|
|
2366
|
-
const handoffShortName = actualHandoffPath.split("/").pop() || handoffFileName
|
|
2367
|
-
if (!downstreamContent.includes(handoffShortName)) {
|
|
2368
|
-
crossRefIssues.push(`${planFile} 未反向引用 ${handoffShortName}`)
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
if (crossRefIssues.length > 0) {
|
|
2375
|
-
lines.push("=== 反向引用检查 ===")
|
|
2376
|
-
for (const issue of crossRefIssues) {
|
|
2377
|
-
lines.push(` ⚠️ ${issue}`)
|
|
2378
|
-
}
|
|
2379
|
-
if (isBlock("crossRef")) blockingFailures.push("反向引用断裂")
|
|
2380
|
-
} else if (actualHandoffPath) {
|
|
2381
|
-
lines.push("=== 反向引用检查 ===")
|
|
2382
|
-
lines.push(" ✅ 下游 Plan 均正确引用了本 Handoff")
|
|
2383
|
-
}
|
|
2384
|
-
} else {
|
|
2385
|
-
lines.push("反向引用检查: 已禁用(policy)")
|
|
2386
|
-
}
|
|
2387
|
-
lines.push("")
|
|
2388
|
-
|
|
2389
|
-
// 4.8. 回滚命令覆盖校验:§3 改动清单 vs §4 回滚方案
|
|
2390
|
-
const rollbackIssues: string[] = []
|
|
2391
|
-
if (isEnabled("rollback")) {
|
|
2392
|
-
if (actualHandoffPath && handoffContent) {
|
|
2393
|
-
const changeTable = handoffContent.match(/## 3\. 改动清单[\s\S]*?(?=###|## 4\.)/)
|
|
2394
|
-
const filePathsInTable: string[] = []
|
|
2395
|
-
if (changeTable) {
|
|
2396
|
-
const pathMatches = Array.from(changeTable[0].matchAll(/`(src\/[^`]+)`/g))
|
|
2397
|
-
for (const m of pathMatches) {
|
|
2398
|
-
filePathsInTable.push(m[1])
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
const rollbackSection = handoffContent.match(/### 代码回滚[\s\S]*?```bash([\s\S]*?)```/)
|
|
2402
|
-
const rollbackPaths: string[] = []
|
|
2403
|
-
if (rollbackSection) {
|
|
2404
|
-
const cmds = Array.from(rollbackSection[1].matchAll(/(?:git checkout --|git rm)\s+(\S+)/g))
|
|
2405
|
-
for (const m of cmds) {
|
|
2406
|
-
rollbackPaths.push(m[1].replace(/\\/g, ""))
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
if (filePathsInTable.length > 0 && rollbackPaths.length > 0) {
|
|
2410
|
-
for (const fp of filePathsInTable) {
|
|
2411
|
-
const covered = rollbackPaths.some(rp => fp.endsWith(rp.replace(/^src\//, "")) || rp.endsWith(fp.replace(/^src\//, "")) || fp === rp)
|
|
2412
|
-
if (!covered) {
|
|
2413
|
-
rollbackIssues.push(`${fp} 在改动清单中但回滚方案未覆盖`)
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
for (const rp of rollbackPaths) {
|
|
2417
|
-
const inTable = filePathsInTable.some(fp => fp.endsWith(rp.replace(/^src\//, "")) || rp.endsWith(fp.replace(/^src\//, "")) || fp === rp)
|
|
2418
|
-
if (!inTable) {
|
|
2419
|
-
rollbackIssues.push(`${rp} 在回滚方案中但改动清单未提及`)
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
if (rollbackIssues.length > 0) {
|
|
2425
|
-
lines.push("=== 回滚命令覆盖校验 ===")
|
|
2426
|
-
for (const issue of rollbackIssues) {
|
|
2427
|
-
lines.push(` ⚠️ ${issue}`)
|
|
2428
|
-
}
|
|
2429
|
-
if (isBlock("rollback")) blockingFailures.push("回滚方案覆盖不完整")
|
|
2430
|
-
} else if (actualHandoffPath && handoffContent) {
|
|
2431
|
-
lines.push("=== 回滚命令覆盖校验 ===")
|
|
2432
|
-
lines.push(" ✅ §3 改动清单与 §4 回滚方案完全一致")
|
|
2433
|
-
}
|
|
2434
|
-
} else {
|
|
2435
|
-
lines.push("回滚命令覆盖校验: 已禁用(policy)")
|
|
2436
|
-
}
|
|
2437
|
-
lines.push("")
|
|
2438
|
-
|
|
2439
|
-
// 5. Git diff 统计
|
|
2440
|
-
let changedFiles: string[] = []
|
|
2441
|
-
if (isEnabled("gitDiff")) {
|
|
2442
|
-
const { spawnSync } = await import("child_process")
|
|
2443
|
-
try {
|
|
2444
|
-
const diff = spawnSync("git", ["diff", "--name-only"], { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
|
|
2445
|
-
const diffStat = diff.stdout || ""
|
|
2446
|
-
changedFiles = diffStat.trim().split("\n").filter(Boolean)
|
|
2447
|
-
} catch {
|
|
2448
|
-
lines.push("Git diff: 无法获取(可能无 git 仓库或无暂存变更)")
|
|
2449
|
-
}
|
|
2450
|
-
lines.push(`Git diff 变更文件: ${changedFiles.length} 个`)
|
|
2451
|
-
for (const f of changedFiles.slice(0, 20)) {
|
|
2452
|
-
lines.push(` ${f}`)
|
|
2453
|
-
}
|
|
2454
|
-
if (changedFiles.length > 20) lines.push(` ... 还有 ${changedFiles.length - 20} 个文件`)
|
|
2455
|
-
} else {
|
|
2456
|
-
lines.push("Git diff: 已禁用(policy)")
|
|
2457
|
-
}
|
|
2458
|
-
lines.push("")
|
|
2459
|
-
|
|
2460
|
-
// 6. 交叉比对:检查 tasks.md 预期改动文件是否在实际 git diff 中
|
|
2461
|
-
if (isEnabled("gitDiff") && tasksContent && changedFiles.length > 0) {
|
|
2462
|
-
lines.push("=== 交叉比对:tasks.md 预期文件 vs git diff 实际变更 ===")
|
|
2463
|
-
const taskFileRefs = tasksContent.match(/`([^`]+\.ts[x]?)`/g) || []
|
|
2464
|
-
const expectedFiles = taskFileRefs.map(r => r.replace(/`/g, ""))
|
|
2465
|
-
const setChanged = new Set(changedFiles)
|
|
2466
|
-
let mismatchCount = 0
|
|
2467
|
-
for (const f of expectedFiles) {
|
|
2468
|
-
const found = changedFiles.some(cf => cf.endsWith(f.replace(/^src\//, "")) || cf === f)
|
|
2469
|
-
if (!found) {
|
|
2470
|
-
lines.push(` ⚠️ tasks.md 提及但 git diff 未变更: ${f}`)
|
|
2471
|
-
mismatchCount++
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
// 反向检查:git diff 中有变更但 tasks.md 未提及
|
|
2475
|
-
for (const cf of changedFiles) {
|
|
2476
|
-
if (cf.endsWith(".md") || cf.endsWith(".toml")) continue // 文档文件不算
|
|
2477
|
-
const mentioned = expectedFiles.some(ef => cf.endsWith(ef.replace(/^src\//, "")) || cf === ef)
|
|
2478
|
-
if (!mentioned) {
|
|
2479
|
-
lines.push(` ⚠️ git diff 有变更但 tasks.md 未提及: ${cf}`)
|
|
2480
|
-
mismatchCount++
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
if (mismatchCount === 0) {
|
|
2484
|
-
lines.push(" ✅ tasks.md 预期文件与 git diff 实际变更一致")
|
|
2485
|
-
}
|
|
2486
|
-
lines.push("")
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
// 7. 检查 ADD-7 审计记录
|
|
2490
|
-
if (isEnabled("auditLog")) {
|
|
2491
|
-
try {
|
|
2492
|
-
const sourceFiles = changedFiles.filter(f => !f.endsWith(".md") && !f.endsWith(".toml"))
|
|
2493
|
-
let auditedCount = 0
|
|
2494
|
-
const missingAudit: string[] = []
|
|
2495
|
-
for (const f of sourceFiles.slice(0, 10)) {
|
|
2496
|
-
const auditRecords = await prisma.auditLog.findMany({
|
|
2497
|
-
where: {
|
|
2498
|
-
targetId: { contains: f.split("/").pop()?.replace(/\.(ts|tsx)$/, "") || f },
|
|
2499
|
-
action: "MODIFY",
|
|
2500
|
-
},
|
|
2501
|
-
select: { id: true, targetId: true },
|
|
2502
|
-
take: 1,
|
|
2503
|
-
})
|
|
2504
|
-
if (auditRecords.length > 0) {
|
|
2505
|
-
auditedCount++
|
|
2506
|
-
} else {
|
|
2507
|
-
missingAudit.push(f)
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
lines.push("=== ADD-7 审计记录检查 ===")
|
|
2511
|
-
lines.push(` 已审计: ${auditedCount} / ${Math.min(sourceFiles.length, 10)} 个源文件`)
|
|
2512
|
-
if (missingAudit.length > 0) {
|
|
2513
|
-
lines.push(` ⚠️ 缺少 ADD-7 记录的文件:`)
|
|
2514
|
-
for (const f of missingAudit) {
|
|
2515
|
-
lines.push(` - ${f}`)
|
|
2516
|
-
}
|
|
2517
|
-
if (isBlock("auditLog")) blockingFailures.push("ADD-7 审计记录不完整")
|
|
2518
|
-
} else {
|
|
2519
|
-
lines.push(" ✅ 所有源文件均有 ADD-7 审计记录")
|
|
2520
|
-
}
|
|
2521
|
-
} catch (dbErr) {
|
|
2522
|
-
lines.push("=== ADD-7 审计记录检查 ===")
|
|
2523
|
-
lines.push(` ⚠️ 无法查询数据库: ${dbErr instanceof Error ? dbErr.message : String(dbErr)}`)
|
|
2524
|
-
}
|
|
2525
|
-
} else {
|
|
2526
|
-
lines.push("ADD-7 审计记录检查: 已禁用(policy)")
|
|
2527
|
-
}
|
|
2528
|
-
lines.push("")
|
|
2529
|
-
|
|
2530
|
-
// 8. 综合建议 + 最终裁定
|
|
2531
|
-
lines.push("=== 综合同步建议 ===")
|
|
2532
|
-
if (uncheckedTasks > 0) {
|
|
2533
|
-
lines.push(` 📝 tasks.md 有 ${uncheckedTasks} 个未完成 Task,如已实现请勾选为 [x]`)
|
|
2534
|
-
}
|
|
2535
|
-
if (uncheckedChecklist > 0) {
|
|
2536
|
-
lines.push(` 📝 checklist.md 有 ${uncheckedChecklist} 个未勾选项,如已验证请勾选`)
|
|
2537
|
-
}
|
|
2538
|
-
if (hfUnchecked > 0) {
|
|
2539
|
-
lines.push(` 📝 Handoff 文档有 ${hfUnchecked} 个未勾选项(前置:${hfPreChecksUnchecked}, 后置:${hfPostChecksUnchecked}),如已验证请勾选`)
|
|
2540
|
-
}
|
|
2541
|
-
if (arUnchecked > 0) {
|
|
2542
|
-
lines.push(` 📝 add-route 文档有 ${arUnchecked} 个未勾选项,如已完成请勾选`)
|
|
2543
|
-
}
|
|
2544
|
-
if (crossRefIssues.length > 0) {
|
|
2545
|
-
lines.push(` ⚠️ 反向引用断裂:${crossRefIssues.length} 个下游 Plan 未引用本 Handoff(${crossRefIssues.join("、")})——需在下游 Plan 的 §7 添加引用`)
|
|
2546
|
-
}
|
|
2547
|
-
if (rollbackIssues.length > 0) {
|
|
2548
|
-
lines.push(` ⚠️ 回滚方案覆盖不完整:${rollbackIssues.length} 处不一致——需修正 §4 回滚命令`)
|
|
2549
|
-
}
|
|
2550
|
-
if (uncheckedTasks === 0 && uncheckedChecklist === 0 && hfUnchecked === 0 && arUnchecked === 0 && crossRefIssues.length === 0 && rollbackIssues.length === 0) {
|
|
2551
|
-
lines.push(" ✅ tasks.md、checklist.md、Handoff 全部项已勾选")
|
|
2552
|
-
} else if (uncheckedTasks === 0 && uncheckedChecklist === 0) {
|
|
2553
|
-
lines.push(" ✅ tasks.md 和 checklist.md 全部项已勾选")
|
|
2554
|
-
}
|
|
2555
|
-
if (isEnabled("gitDiff") && changedFiles.length === 0) {
|
|
2556
|
-
lines.push(" ⚠️ git diff 无变更——可能所有变更已提交,或代码尚未实际修改")
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
// 最终裁定
|
|
2560
|
-
lines.push("")
|
|
2561
|
-
lines.push("=== 最终裁定 ===")
|
|
2562
|
-
if (blockingFailures.length > 0) {
|
|
2563
|
-
lines.push(` ❌ BLOCKED — 以下阻断项必须修复后再收敛:`)
|
|
2564
|
-
for (const bf of blockingFailures) {
|
|
2565
|
-
lines.push(` - ${bf}`)
|
|
2566
|
-
}
|
|
2567
|
-
lines.push(` exitCode: 1`)
|
|
2568
|
-
} else {
|
|
2569
|
-
lines.push(" ✅ PASS — 所有 block 级检查通过")
|
|
2570
|
-
lines.push(" exitCode: 0")
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
return textResponse(lines.join("\n"))
|
|
2574
|
-
} catch (error) {
|
|
2575
|
-
return errorResponse(`check_spec_sync 失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
)
|
|
2579
|
-
|
|
2580
|
-
// ========== 新增工具: check_add_route_completeness ==========
|
|
2581
|
-
// ADD 范式 Step 完成度扫描:统计 add-route 中 [ ] vs [x] 的 Step 勾选进度
|
|
2582
|
-
server.registerTool(
|
|
2583
|
-
"check_add_route_completeness",
|
|
2584
|
-
{
|
|
2585
|
-
description: "ADD 范式守卫工具:扫描 add-route 文件的 Step 完成度。\n统计 add-route 中每个 Step 的 [ ](未完成)和 [x](已完成)勾选项数量,返回逐 Step 完成率及整体状态。\n\n四种返回状态:\n- 'complete' — 所有 Step 的产出检查项全部 [x],add-route 完整闭环\n- 'incomplete' — 存在未勾选的 Step 产出项,需继续执行\n- 'file_missing' — 未找到匹配的 add-route 文件\n- 'errors' — 文件存在但解析出错\n\nAI 助手在 Step 3 代码实现完成后、进入 Step 4 之前必须调用此工具自检。",
|
|
2586
|
-
inputSchema: {
|
|
2587
|
-
planKeyword: z.string().describe("Plan 文件的关键词,用于在 ${MAGIC_DIR}/plans/ 目录下匹配 *add-route* 文件"),
|
|
2588
|
-
},
|
|
2589
|
-
},
|
|
2590
|
-
async (args: { planKeyword: string }) => {
|
|
2591
|
-
try {
|
|
2592
|
-
const { planKeyword } = args
|
|
2593
|
-
if (!planKeyword) return errorResponse("planKeyword 参数不能为空")
|
|
2594
|
-
|
|
2595
|
-
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
|
|
2596
|
-
|
|
2597
|
-
// === 1. 查找匹配的 add-route 文件 ===
|
|
2598
|
-
let matchedFile: string | null = null
|
|
2599
|
-
try {
|
|
2600
|
-
const entries = await readdirRecursive(plansDir)
|
|
2601
|
-
const planLower = planKeyword.toLowerCase()
|
|
2602
|
-
for (const entry of entries) {
|
|
2603
|
-
const entryLower = entry.toLowerCase()
|
|
2604
|
-
if (entryLower.includes("add-route") && entryLower.includes(planLower)) {
|
|
2605
|
-
matchedFile = entry
|
|
2606
|
-
break
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
} catch {
|
|
2610
|
-
return errorResponse(`add-route 完整性扫描失败:无法读取 ${MAGIC_DIR}/plans/ 目录`)
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
if (!matchedFile) {
|
|
2614
|
-
const parts = [
|
|
2615
|
-
"=== ADD 守卫:add-route Step 完成度扫描 ===",
|
|
2616
|
-
`Plan 关键词: "${planKeyword}"`,
|
|
2617
|
-
"",
|
|
2618
|
-
"状态: ❌ file_missing — 未找到 add-route 文件",
|
|
2619
|
-
"操作: 禁止进入后续 Step,先回退至 Step 0.5 生成 add-route",
|
|
2620
|
-
]
|
|
2621
|
-
return errorResponse(parts.join("\n"))
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
|
-
const filePath = join(plansDir, matchedFile)
|
|
2625
|
-
|
|
2626
|
-
// === 2. 读取文件内容 ===
|
|
2627
|
-
const content = await readFileSafe(filePath)
|
|
2628
|
-
if (!content) {
|
|
2629
|
-
return errorResponse(`add-route 完整性扫描失败:无法读取文件 ${matchedFile}`)
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
// 模板格式校验
|
|
2633
|
-
const routeGuard = await validateDocWithGuard(filePath)
|
|
2634
|
-
|
|
2635
|
-
const lines = content.split("\n")
|
|
2636
|
-
|
|
2637
|
-
// === 3. 解析 Step 结构和勾选状态 ===
|
|
2638
|
-
// 匹配 Step 头:## Step 0、## Step 3.5、## Step 8 等
|
|
2639
|
-
const stepHeaderRe = /^##\s+Step\s+(\d+(?:\.\d+)?)/
|
|
2640
|
-
// 匹配 checkbox:- [ ] 或 - [x]
|
|
2641
|
-
const checkboxRe = /^\s*-\s+\[([ xX])\]\s/
|
|
2642
|
-
// 匹配状态行:**状态**:⬜ 或 **状态**:✅ 或 **状态**:🔄
|
|
2643
|
-
const statusRe = /\*\*状态\*\*[::]\s*(⬜|✅|🔄|⚠️)/
|
|
2644
|
-
|
|
2645
|
-
interface StepInfo {
|
|
2646
|
-
step: string
|
|
2647
|
-
total: number
|
|
2648
|
-
checked: number
|
|
2649
|
-
unchecked: number
|
|
2650
|
-
status: string
|
|
2651
|
-
headerLine: number
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
const steps: StepInfo[] = []
|
|
2655
|
-
let currentStep: StepInfo | null = null
|
|
2656
|
-
let globalTotal = 0
|
|
2657
|
-
let globalChecked = 0
|
|
2658
|
-
let globalUnchecked = 0
|
|
2659
|
-
let inStepBlock = false
|
|
2660
|
-
|
|
2661
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2662
|
-
const line = lines[i]
|
|
2663
|
-
|
|
2664
|
-
// 检测 Step 头
|
|
2665
|
-
const stepMatch = line.match(stepHeaderRe)
|
|
2666
|
-
if (stepMatch) {
|
|
2667
|
-
// 保存上一个 Step
|
|
2668
|
-
if (currentStep) {
|
|
2669
|
-
steps.push(currentStep)
|
|
2670
|
-
}
|
|
2671
|
-
currentStep = {
|
|
2672
|
-
step: stepMatch[1],
|
|
2673
|
-
total: 0,
|
|
2674
|
-
checked: 0,
|
|
2675
|
-
unchecked: 0,
|
|
2676
|
-
status: "unknown",
|
|
2677
|
-
headerLine: i + 1,
|
|
2678
|
-
}
|
|
2679
|
-
inStepBlock = true
|
|
2680
|
-
continue
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
// 检测下一个 Step 头(## Step)跳出当前 block
|
|
2684
|
-
if (inStepBlock && /^##\s+Step\s+\d/.test(line)) {
|
|
2685
|
-
inStepBlock = false
|
|
2686
|
-
continue
|
|
2687
|
-
}
|
|
2688
|
-
|
|
2689
|
-
// 在 Step block 内检测状态行
|
|
2690
|
-
if (inStepBlock && currentStep) {
|
|
2691
|
-
const statusMatch = line.match(statusRe)
|
|
2692
|
-
if (statusMatch) {
|
|
2693
|
-
const s = statusMatch[1]
|
|
2694
|
-
currentStep.status = s === "✅" ? "done" : s === "⬜" ? "pending" : s === "🔄" ? "in_progress" : "unknown"
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
// 检测 checkbox(全局扫描,不限于 Step block)
|
|
2699
|
-
const cbMatch = line.match(checkboxRe)
|
|
2700
|
-
if (cbMatch) {
|
|
2701
|
-
globalTotal++
|
|
2702
|
-
if (cbMatch[1] === "x" || cbMatch[1] === "X") {
|
|
2703
|
-
globalChecked++
|
|
2704
|
-
if (currentStep) {
|
|
2705
|
-
currentStep.checked++
|
|
2706
|
-
currentStep.total++
|
|
2707
|
-
}
|
|
2708
|
-
} else {
|
|
2709
|
-
globalUnchecked++
|
|
2710
|
-
if (currentStep) {
|
|
2711
|
-
currentStep.unchecked++
|
|
2712
|
-
currentStep.total++
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
|
|
2717
|
-
// 检测产出检查块(- [ ] / - [x] within 产出检查 section)
|
|
2718
|
-
// 已经在上面的 checkboxRe 中覆盖
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
// 保存最后一个 Step
|
|
2722
|
-
if (currentStep) {
|
|
2723
|
-
steps.push(currentStep)
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
// === 4. 生成报告 ===
|
|
2727
|
-
const completionRate = globalTotal > 0
|
|
2728
|
-
? Math.round((globalChecked / globalTotal) * 100)
|
|
2729
|
-
: 100
|
|
2730
|
-
|
|
2731
|
-
const isComplete = globalUnchecked === 0
|
|
2732
|
-
|
|
2733
|
-
const parts: string[] = [
|
|
2734
|
-
"=== ADD 守卫:add-route Step 完成度扫描 ===",
|
|
2735
|
-
`Plan 关键词: "${planKeyword}"`,
|
|
2736
|
-
`匹配文件: ${MAGIC_DIR}/plans/${matchedFile}`,
|
|
2737
|
-
"",
|
|
2738
|
-
`整体完成度: ${globalChecked}/${globalTotal} (${completionRate}%)`,
|
|
2739
|
-
`状态: ${isComplete ? "✅ complete — add-route 完整闭环" : "⚠️ incomplete — 存在未勾选 Step"}`,
|
|
2740
|
-
`模板校验: ${routeGuard.ok ? "✅ 通过" : `⚠️ ${routeGuard.issues.split("\n")[0]}`}`,
|
|
2741
|
-
"",
|
|
2742
|
-
]
|
|
2743
|
-
|
|
2744
|
-
if (steps.length > 0) {
|
|
2745
|
-
parts.push("=== 逐 Step 完成度 ===")
|
|
2746
|
-
parts.push("| Step | 勾选 | 未勾选 | 完成率 | 状态 |")
|
|
2747
|
-
parts.push("|------|------|--------|--------|------|")
|
|
2748
|
-
for (const s of steps) {
|
|
2749
|
-
const rate = s.total > 0 ? Math.round((s.checked / s.total) * 100) : 100
|
|
2750
|
-
const statusIcon = s.status === "done" ? "✅" : s.status === "pending" ? "⬜" : s.status === "in_progress" ? "🔄" : "—"
|
|
2751
|
-
parts.push(`| Step ${s.step} | ${s.checked} | ${s.unchecked} | ${rate}% | ${statusIcon} |`)
|
|
2752
|
-
}
|
|
2753
|
-
parts.push("")
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
// 列出未勾选的 Step
|
|
2757
|
-
const incompleteSteps = steps.filter(s => s.unchecked > 0)
|
|
2758
|
-
if (incompleteSteps.length > 0) {
|
|
2759
|
-
parts.push("=== 未闭环 Step(需继续执行)===")
|
|
2760
|
-
for (const s of incompleteSteps) {
|
|
2761
|
-
parts.push(` Step ${s.step}: ${s.checked}/${s.total} 已勾选(${s.unchecked} 项未完成,L${s.headerLine})`)
|
|
2762
|
-
}
|
|
2763
|
-
parts.push("")
|
|
2764
|
-
parts.push("操作: ⚠️ 禁止进入验收阶段(Step 8 收敛判断),回退完成以上未闭环 Step 后再调用本工具自检。")
|
|
2765
|
-
} else {
|
|
2766
|
-
parts.push("=== 全部 Step 已闭环 ✅ ===")
|
|
2767
|
-
parts.push("操作: 可进入验收阶段(Step 8 收敛判断)")
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
return textResponse(parts.join("\n"))
|
|
2771
|
-
} catch (error) {
|
|
2772
|
-
return errorResponse(`add-route 完整性扫描失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
)
|
|
2776
|
-
|
|
2777
|
-
// ========== 新增工具: check_dps(Documentation Precision Score)==========
|
|
2778
|
-
// ADD 范式上游文档质量量化:在 Step 0 末尾调用,评估 Plan → Review → Specs 三级文档的精确度和覆盖度。
|
|
2779
|
-
// DPS < 70 → Plan 需细化
|
|
2780
|
-
// DPS 70-84 → 回退补齐 Review/Specs
|
|
2781
|
-
// DPS ≥ 85 → 可进入 Step 1
|
|
2782
|
-
server.registerTool(
|
|
2783
|
-
"check_dps",
|
|
2784
|
-
{
|
|
2785
|
-
description: "ADD 范式上游文档质量量化工具(Documentation Precision Score)。在 Step 0 文档先行完成后、进入 Step 1 前调用,评估 Plan → Review → Specs 三级文档的精确度和覆盖度,并校验 Review 结论是否已回流至 Plan,防止因 Plan 概括度过高导致下游注意力稀释、Specs 结构性遗漏、Review 结论悬空(Review 发现问题但 Plan 未修正)。\n\n四维评分(各 25% 等权):\n- Plan 可执行粒度(25%):每个 Phase 是否有独立验收标准、每个 Task 是否指定具体文件、是否有占位词\n- Review 覆盖完备度(25%):Review 是否覆盖 Plan 的全部架构维度(数据模型/API签名/错误路径/数据迁移/兼容性/性能/存储)\n- Specs 精确度(25%):Specs Requirements 数与 Plan Phase 数是否 1:1 映射\n- Review 回流完整度(25%):Review 中 P0/P1 问题的修复建议是否在 Plan/Specs 中有对应修正文字(0.6.5 卡位闭环)\n\n判定阈值:\n- DPS ≥ 85 → 🟢 可进入 Step 1\n- DPS 70-84 → 🟡 回退补齐短板\n- DPS < 70 → 🔴 回退细化 Plan 本身",
|
|
2786
|
-
inputSchema: {
|
|
2787
|
-
planKeyword: z.string().describe("Plan 文件的关键词,用于在 ${MAGIC_DIR}/plans/ 目录下匹配 Plan 文件和关联的 Review、Specs"),
|
|
2788
|
-
},
|
|
2789
|
-
},
|
|
2790
|
-
async (args: { planKeyword: string }) => {
|
|
2791
|
-
try {
|
|
2792
|
-
const { planKeyword } = args
|
|
2793
|
-
if (!planKeyword) return errorResponse("planKeyword 参数不能为空")
|
|
2794
|
-
|
|
2795
|
-
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
|
|
2796
|
-
const specsDir = join(PROJECT_ROOT, MAGIC_DIR, "specs")
|
|
2797
|
-
const reviewsDir = join(PROJECT_ROOT, MAGIC_DIR, "reviews")
|
|
2798
|
-
|
|
2799
|
-
const parts: string[] = [
|
|
2800
|
-
"=== DPS:Documentation Precision Score(上游文档质量量化)===",
|
|
2801
|
-
`Plan 关键词: "${planKeyword}"`,
|
|
2802
|
-
"",
|
|
2803
|
-
]
|
|
2804
|
-
|
|
2805
|
-
// === 1. 定位 Plan 文件 ===
|
|
2806
|
-
if (!existsSync(plansDir)) return errorResponse(`plans 目录不存在: ${plansDir}`)
|
|
2807
|
-
const allPlanFiles = (await readdirRecursive(plansDir)).filter(f => f.endsWith(".md"))
|
|
2808
|
-
const planMatch = allPlanFiles.find(f =>
|
|
2809
|
-
f.toLowerCase().includes(planKeyword.toLowerCase()) && f.includes("-plan-v")
|
|
2810
|
-
)
|
|
2811
|
-
if (!planMatch) return errorResponse(`未找到匹配的 Plan 文件(关键词: ${planKeyword})`)
|
|
2812
|
-
const planPath = join(plansDir, planMatch)
|
|
2813
|
-
const planContent = await readFileSafe(planPath)
|
|
2814
|
-
if (!planContent) return errorResponse(`无法读取 Plan 文件: ${planMatch}`)
|
|
2815
|
-
parts.push(`Plan: ${planMatch}`)
|
|
2816
|
-
|
|
2817
|
-
// === 2. 定位 Review 文件 ===
|
|
2818
|
-
let reviewName = ""
|
|
2819
|
-
let reviewContent = ""
|
|
2820
|
-
if (existsSync(reviewsDir)) {
|
|
2821
|
-
const reviewFiles = await readdir(reviewsDir)
|
|
2822
|
-
// 从 Plan 全文提取所有 Review 引用,优先匹配含 planKeyword 的
|
|
2823
|
-
const allReviewRefs = Array.from(planContent.matchAll(/\.(qoder|claude|add|vscode)\/reviews\/([^\s)]+\.md)/g))
|
|
2824
|
-
.map(m => m[2].split("/").pop() || "")
|
|
2825
|
-
.filter(Boolean)
|
|
2826
|
-
// 策略1: 在所有引用中优先选含 planKeyword 的
|
|
2827
|
-
reviewName = allReviewRefs.find(f =>
|
|
2828
|
-
f.toLowerCase().includes(planKeyword.toLowerCase())
|
|
2829
|
-
) || ""
|
|
2830
|
-
// 策略2: 退而取第一个引用
|
|
2831
|
-
if (!reviewName && allReviewRefs.length > 0) {
|
|
2832
|
-
reviewName = allReviewRefs[0]
|
|
2833
|
-
}
|
|
2834
|
-
// 策略3: 按目录文件名关键词匹配
|
|
2835
|
-
if (!reviewName) {
|
|
2836
|
-
reviewName = reviewFiles.find(f =>
|
|
2837
|
-
f.toLowerCase().includes(planKeyword.toLowerCase()) && f.includes("-review-v")
|
|
2838
|
-
) || ""
|
|
2839
|
-
}
|
|
2840
|
-
if (reviewName) {
|
|
2841
|
-
reviewContent = await readFileSafe(join(reviewsDir, reviewName)) || ""
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
parts.push(`Review: ${reviewName || "未找到"}`)
|
|
2845
|
-
|
|
2846
|
-
// === 3. 定位 Specs 目录 ===
|
|
2847
|
-
let specDirName = ""
|
|
2848
|
-
let specContent = ""
|
|
2849
|
-
let tasksContent = ""
|
|
2850
|
-
// 从 Plan 绑定或 §7 提取(兼容 Spec: / Spec | / `path` 三种格式)
|
|
2851
|
-
const specRef = planContent.match(/Spec[:|\s`]+\.?(qoder|claude|add|vscode)\/specs\/([^/`\s]+)/)
|
|
2852
|
-
if (specRef) specDirName = specRef[2]
|
|
2853
|
-
if (!specDirName) {
|
|
2854
|
-
const taskRef = planContent.match(/Tasks[:|\s`]+\.?(qoder|claude|add|vscode)\/specs\/([^/`\s]+)/)
|
|
2855
|
-
if (taskRef) specDirName = taskRef[2]
|
|
2856
|
-
}
|
|
2857
|
-
if (!specDirName) {
|
|
2858
|
-
specDirName = basename(planMatch).replace(/-plan-v\d+\.md$/, "")
|
|
2859
|
-
}
|
|
2860
|
-
if (specDirName && existsSync(join(specsDir, specDirName))) {
|
|
2861
|
-
specContent = await readFileSafe(join(specsDir, specDirName, "spec.md")) || ""
|
|
2862
|
-
tasksContent = await readFileSafe(join(specsDir, specDirName, "tasks.md")) || ""
|
|
2863
|
-
}
|
|
2864
|
-
parts.push(`Specs: ${specDirName || "未找到"}`)
|
|
2865
|
-
parts.push("")
|
|
2866
|
-
|
|
2867
|
-
// ====== 维度一:Plan 可执行粒度(权重 30%) ======
|
|
2868
|
-
let planScore = 100
|
|
2869
|
-
const planPenalties: string[] = []
|
|
2870
|
-
|
|
2871
|
-
// 1a. 统计 Plan 中的 Phase(按 ## 数字标题)
|
|
2872
|
-
const planPhases = planContent.match(/###\s+Phase\s+\d/g) || []
|
|
2873
|
-
const phaseTotal = planPhases.length
|
|
2874
|
-
// 检查每个 Phase 是否有验收标准(Plan §6 或 Phase 下的 checklist)
|
|
2875
|
-
const acceptanceMatch = planContent.match(/##\s+6\.\s+验收标准/)
|
|
2876
|
-
const planHasAcceptance = !!acceptanceMatch
|
|
2877
|
-
if (!planHasAcceptance && phaseTotal > 0) {
|
|
2878
|
-
planScore -= 15
|
|
2879
|
-
planPenalties.push(`Plan 缺少 §6 验收标准(${phaseTotal} 个 Phase 无独立验收)`)
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
// 1b. 统计 Task 是否指定了具体文件(含表格内路径引用)
|
|
2883
|
-
// 匹配常见路径前缀:templates/ src/ .qoder/ .claude/ .vscode/ docs/ scripts/ tests/
|
|
2884
|
-
const pathPrefixes = '(?:templates\/|src\/|\\.qoder\/|\\.claude\/|\\.vscode\/|\\.add\/|docs\/|scripts\/|tests\/|prisma\/|queries\/)'
|
|
2885
|
-
const taskFileRefs = planContent.match(new RegExp(pathPrefixes + '[^\\s|)\\n]+', 'g')) || []
|
|
2886
|
-
const taskCount = (planContent.match(/Task\s+\d+/g) || []).length
|
|
2887
|
-
// 统计 Task 标题数(用于 Specs 精确度维度三),不含正文中 Task 引用
|
|
2888
|
-
const taskHeadingCount = (planContent.match(/###\s+Task\s+\d+/g) || []).length
|
|
2889
|
-
if (taskCount > 0) {
|
|
2890
|
-
// 去重后的文件引用数
|
|
2891
|
-
const uniqueFileRefs = new Set(taskFileRefs.map(r => r.replace(/`/g, "")))
|
|
2892
|
-
if (uniqueFileRefs.size < taskCount * 0.5) {
|
|
2893
|
-
planScore -= 20
|
|
2894
|
-
planPenalties.push(`${taskCount} 个 Task 但仅 ${uniqueFileRefs.size} 个文件引用(覆盖率 ${Math.round(uniqueFileRefs.size / taskCount * 100)}%)`)
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
// 1c. 检测占位词
|
|
2899
|
-
const placeholders = ["待定", "TBD", "TODO", "后续讨论", "暂不处理", "待评估"]
|
|
2900
|
-
for (const ph of placeholders) {
|
|
2901
|
-
if (planContent.includes(ph)) {
|
|
2902
|
-
planScore -= 20
|
|
2903
|
-
planPenalties.push(`Plan 含占位词: "${ph}"`)
|
|
2904
|
-
break
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
planScore = Math.max(0, planScore)
|
|
2909
|
-
parts.push("=== 维度一:Plan 可执行粒度(30%)===")
|
|
2910
|
-
parts.push(` 分数: ${planScore}/100`)
|
|
2911
|
-
if (planPenalties.length > 0) {
|
|
2912
|
-
for (const p of planPenalties) parts.push(` - ${p}`)
|
|
2913
|
-
} else {
|
|
2914
|
-
parts.push(" ✅ 无扣分项")
|
|
2915
|
-
}
|
|
2916
|
-
parts.push("")
|
|
2917
|
-
|
|
2918
|
-
// ====== 维度二:Review 覆盖完备度(权重 35%) ======
|
|
2919
|
-
const dimensions = [
|
|
2920
|
-
{ name: "数据模型/类型定义", patterns: ["类型", "接口", "interface", "GroundingStatus", "Grounding", "RetrieveBudget"] },
|
|
2921
|
-
{ name: "API 签名/函数接口", patterns: ["签名", "SearchOptions", "collectionName", "searchKnowledgeDocuments", "getChromaCollection"] },
|
|
2922
|
-
{ name: "错误路径/降级处理", patterns: ["降级", "fallback", "degraded", "错误路径", "异常", "catch"] },
|
|
2923
|
-
{ name: "数据迁移策略", patterns: ["迁移", "migrat", "复制", "collection", "同步"] },
|
|
2924
|
-
{ name: "兼容性/向后兼容", patterns: ["兼容", "compatib", "向后", "旧路径", "保留"] },
|
|
2925
|
-
{ name: "性能影响", patterns: ["性能", "perf", "延迟", "latency", "API 调用", "embedding"] },
|
|
2926
|
-
{ name: "存储/索引成本", patterns: ["存储", "stor", "索引", "冗余", "MB", "GB", "副本"] },
|
|
2927
|
-
]
|
|
2928
|
-
let coveredDims = 0
|
|
2929
|
-
let totalDims = 0
|
|
2930
|
-
const uncoveredDims: string[] = []
|
|
2931
|
-
const dimDetails: string[] = []
|
|
2932
|
-
|
|
2933
|
-
// 从 Plan 提取涉及的架构维度
|
|
2934
|
-
const planDimensions: string[] = []
|
|
2935
|
-
for (const dim of dimensions) {
|
|
2936
|
-
for (const pat of dim.patterns) {
|
|
2937
|
-
if (planContent.toLowerCase().includes(pat.toLowerCase())) {
|
|
2938
|
-
planDimensions.push(dim.name)
|
|
2939
|
-
break
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
|
|
2944
|
-
totalDims = planDimensions.length
|
|
2945
|
-
if (totalDims === 0) {
|
|
2946
|
-
// Plan 未明确涉及维度,用所有维度
|
|
2947
|
-
totalDims = dimensions.length
|
|
2948
|
-
for (const dim of dimensions) {
|
|
2949
|
-
let covered = false
|
|
2950
|
-
if (reviewContent) {
|
|
2951
|
-
for (const pat of dim.patterns) {
|
|
2952
|
-
if (reviewContent.toLowerCase().includes(pat.toLowerCase())) {
|
|
2953
|
-
covered = true
|
|
2954
|
-
break
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
if (covered) {
|
|
2959
|
-
coveredDims++
|
|
2960
|
-
} else {
|
|
2961
|
-
uncoveredDims.push(dim.name)
|
|
2962
|
-
}
|
|
2963
|
-
dimDetails.push(` ${covered ? "✅" : "⬜"} ${dim.name}`)
|
|
2964
|
-
}
|
|
2965
|
-
} else {
|
|
2966
|
-
for (const dimName of planDimensions) {
|
|
2967
|
-
const dim = dimensions.find(d => d.name === dimName)!
|
|
2968
|
-
let covered = false
|
|
2969
|
-
if (reviewContent) {
|
|
2970
|
-
for (const pat of dim.patterns) {
|
|
2971
|
-
if (reviewContent.toLowerCase().includes(pat.toLowerCase())) {
|
|
2972
|
-
covered = true
|
|
2973
|
-
break
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
if (covered) {
|
|
2978
|
-
coveredDims++
|
|
2979
|
-
} else {
|
|
2980
|
-
uncoveredDims.push(dim.name)
|
|
2981
|
-
}
|
|
2982
|
-
dimDetails.push(` ${covered ? "✅" : "⬜"} ${dim.name}`)
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
|
-
const reviewCoverage = totalDims > 0 ? Math.round((coveredDims / totalDims) * 100) : 0
|
|
2987
|
-
let reviewScore = reviewCoverage
|
|
2988
|
-
|
|
2989
|
-
parts.push("=== 维度二:Review 覆盖完备度(35%)===")
|
|
2990
|
-
parts.push(` 覆盖度: ${coveredDims}/${totalDims} (${reviewCoverage}%)`)
|
|
2991
|
-
parts.push(` 分数: ${reviewScore}/100`)
|
|
2992
|
-
for (const d of dimDetails) parts.push(d)
|
|
2993
|
-
if (uncoveredDims.length > 0) {
|
|
2994
|
-
parts.push(` ⚠️ 未覆盖维度: ${uncoveredDims.join(", ")}`)
|
|
2995
|
-
}
|
|
2996
|
-
if (!reviewContent) {
|
|
2997
|
-
parts.push(" ⚠️ Review 文件未找到,覆盖度计为 0")
|
|
2998
|
-
reviewScore = 0
|
|
2999
|
-
}
|
|
3000
|
-
parts.push("")
|
|
3001
|
-
|
|
3002
|
-
// ====== 维度三:Specs 精确度(权重 35%) ======
|
|
3003
|
-
let specScore = 0
|
|
3004
|
-
if (specContent) {
|
|
3005
|
-
// 统计 Specs Requirements 数
|
|
3006
|
-
const reqCount = (specContent.match(/###\s+Requirement:/g) || []).length
|
|
3007
|
-
// 预期 Requirement 数 = Plan Phase 数
|
|
3008
|
-
const expectedReqs = phaseTotal || taskHeadingCount || 1
|
|
3009
|
-
const specPrecision = Math.min(Math.round((reqCount / expectedReqs) * 100), 100)
|
|
3010
|
-
|
|
3011
|
-
// 检查每个 Plan Phase 是否有对应 Requirement
|
|
3012
|
-
let specPenalties = 0
|
|
3013
|
-
if (reqCount < expectedReqs) {
|
|
3014
|
-
specPenalties = (expectedReqs - reqCount) * 10
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
specScore = Math.max(0, specPrecision - specPenalties)
|
|
3018
|
-
parts.push("=== 维度三:Specs 精确度(35%)===")
|
|
3019
|
-
parts.push(` Requirements 数: ${reqCount} / 预期 ${expectedReqs}(基于 Plan Phase 数)`)
|
|
3020
|
-
parts.push(` 精确度: ${specPrecision}%`)
|
|
3021
|
-
if (specPenalties > 0) {
|
|
3022
|
-
parts.push(` 缺失惩罚: -${specPenalties}(${expectedReqs - reqCount} 个 Phase 无对应 Requirement)`)
|
|
3023
|
-
}
|
|
3024
|
-
parts.push(` 分数: ${specScore}/100`)
|
|
3025
|
-
} else {
|
|
3026
|
-
specScore = 0
|
|
3027
|
-
parts.push("=== 维度三:Specs 精确度(35%)===")
|
|
3028
|
-
parts.push(" ⚠️ Specs 目录/文件未找到,精确度计为 0")
|
|
3029
|
-
}
|
|
3030
|
-
parts.push("")
|
|
3031
|
-
|
|
3032
|
-
// ====== 维度四:Review 回流完整度(权重 25%) ======
|
|
3033
|
-
// 策略:数 Review 中 P0/P1 条目数 vs Plan 中 [回流: 标记数,简单计数比对
|
|
3034
|
-
let backflowScore = 0
|
|
3035
|
-
|
|
3036
|
-
if (reviewContent) {
|
|
3037
|
-
const reviewLines = reviewContent.split("\n")
|
|
3038
|
-
let reviewP0P1Count = 0
|
|
3039
|
-
for (const line of reviewLines) {
|
|
3040
|
-
if (/^\|\s*\d+\s*\|\s*(P0|P1)\s*\|/.test(line)) reviewP0P1Count++
|
|
3041
|
-
}
|
|
3042
|
-
const planBackflowCount = (planContent.match(/\[回流\s*[::]/g) || []).length
|
|
3043
|
-
parts.push("=== 维度四:Review 回流完整度(25%)===")
|
|
3044
|
-
parts.push(" Review P0/P1 条目总数: " + reviewP0P1Count)
|
|
3045
|
-
parts.push(" Plan [回流:] 标记数: " + planBackflowCount)
|
|
3046
|
-
if (reviewP0P1Count === 0) {
|
|
3047
|
-
backflowScore = 100
|
|
3048
|
-
parts.push(" ✅ Review 无 P0/P1 问题,跳过回流检查")
|
|
3049
|
-
} else if (planBackflowCount >= reviewP0P1Count) {
|
|
3050
|
-
backflowScore = 100
|
|
3051
|
-
parts.push(" ✅ 回流完整:" + planBackflowCount + "/" + reviewP0P1Count + " 条全部对应")
|
|
3052
|
-
} else {
|
|
3053
|
-
backflowScore = Math.round((planBackflowCount / reviewP0P1Count) * 100)
|
|
3054
|
-
parts.push(" ⚠️ 回流不完整:" + planBackflowCount + "/" + reviewP0P1Count + ",缺 " + (reviewP0P1Count-planBackflowCount) + " 条")
|
|
3055
|
-
parts.push(" ⚠️ 修复建议: 执行 0.6.5 卡位,在 Plan 对应章节添加 [回流: Review PN #N 简述] 标记")
|
|
3056
|
-
}
|
|
3057
|
-
} else {
|
|
3058
|
-
parts.push("=== 维度四:Review 回流完整度(25%)===")
|
|
3059
|
-
parts.push(" ⚠️ Review 文件未找到,回流检查不可用")
|
|
3060
|
-
}
|
|
3061
|
-
parts.push(" 分数: " + backflowScore + "/100")
|
|
3062
|
-
parts.push("")
|
|
3063
|
-
|
|
3064
|
-
// ====== DPS 复合计算 ======
|
|
3065
|
-
const dps = Math.round(planScore * 0.25 + reviewScore * 0.25 + specScore * 0.25 + backflowScore * 0.25)
|
|
3066
|
-
|
|
3067
|
-
let verdict: string, verdictIcon: string, action: string
|
|
3068
|
-
if (dps >= 85) {
|
|
3069
|
-
verdict = "PASS"
|
|
3070
|
-
verdictIcon = "🟢"
|
|
3071
|
-
action = "可进入 Step 1"
|
|
3072
|
-
} else if (dps >= 70) {
|
|
3073
|
-
verdict = "WARN"
|
|
3074
|
-
verdictIcon = "🟡"
|
|
3075
|
-
action = "回退补齐短板:"
|
|
3076
|
-
if (planScore < 70) action += " Plan 粒度不足;"
|
|
3077
|
-
if (reviewScore < 70) action += " Review 覆盖维度缺失;"
|
|
3078
|
-
if (specScore < 70) action += " Specs Requirements 缺失;"
|
|
3079
|
-
if (backflowScore < 70) action += " Review 结论未回流至 Plan(0.6.5 卡位);"
|
|
3080
|
-
} else {
|
|
3081
|
-
verdict = "BLOCKED"
|
|
3082
|
-
verdictIcon = "🔴"
|
|
3083
|
-
action = "回退细化 Plan 本身(Plan 粒度不足是下游注意力漂移的根因)"
|
|
3084
|
-
}
|
|
3085
|
-
|
|
3086
|
-
parts.push("=== DPS 复合计算 ===")
|
|
3087
|
-
parts.push(` Plan 粒度: ${planScore} × 0.25 = ${(planScore * 0.25).toFixed(1)}`)
|
|
3088
|
-
parts.push(` Review 覆盖度: ${reviewScore} × 0.25 = ${(reviewScore * 0.25).toFixed(1)}`)
|
|
3089
|
-
parts.push(` Specs 精确度: ${specScore} × 0.25 = ${(specScore * 0.25).toFixed(1)}`)
|
|
3090
|
-
parts.push(` Review 回流完整度: ${backflowScore} × 0.25 = ${(backflowScore * 0.25).toFixed(1)}`)
|
|
3091
|
-
parts.push(` ─────────────────────────────────`)
|
|
3092
|
-
parts.push(` DPS = ${dps} ${verdictIcon} ${verdict}`)
|
|
3093
|
-
parts.push("")
|
|
3094
|
-
parts.push("=== 判定 ===")
|
|
3095
|
-
parts.push(` 结果: ${verdictIcon} ${verdict}`)
|
|
3096
|
-
parts.push(` 动作: ${action}`)
|
|
3097
|
-
|
|
3098
|
-
return textResponse(parts.join("\n"))
|
|
3099
|
-
} catch (error) {
|
|
3100
|
-
return errorResponse(`check_dps 失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
)
|
|
3104
|
-
|
|
3105
|
-
// ========== 新增工具: check_rahs(Round Attention Health Score)==========
|
|
3106
|
-
// ADD 范式下游执行健康度量化:在 Step 4/8 调用,量化本轮实现的范围保真度、类型安全、审计完整度、Spec合规、阶段对称性。
|
|
3107
|
-
// RAHS ≥ 90 → 🟢 健康
|
|
3108
|
-
// RAHS 70-89 → 🟡 亚健康
|
|
3109
|
-
// RAHS < 70 → 🔴 漂移,强制返工
|
|
3110
|
-
server.registerTool(
|
|
3111
|
-
"check_rahs",
|
|
3112
|
-
{
|
|
3113
|
-
description: "ADD 范式轮次注意力健康度量化工具(Round Attention Health Score)。在 Step 4(审计数据验证)和 Step 8(收敛判断)调用,聚合范围保真度、类型安全、审计完整度、Spec 合规、阶段对称性五维指标,量化本轮实现的注意力漂移程度。\n\n五维评分(加权):\n- 范围保真度(30%):计划修改文件 vs 实际 git diff 文件的交集率——文件扩散是注意力漂移最直接的信号\n- 类型安全(20%):tsc --noEmit 错误数,每 +1 error = -10 分\n- 审计完整度(25%):record_dev_operation 记录数 / 计划文件数\n- Spec 合规(15%):check_spec_sync 通过/失败\n- 阶段对称性(10%):check_phase_symmetry 通过/失败\n\n判定阈值:\n- RAHS ≥ 90 → 🟢 健康,进入下一 Step\n- RAHS 70-89 → 🟡 亚健康,自检后决定是否返工\n- RAHS < 70 → 🔴 漂移,强制返工",
|
|
3114
|
-
inputSchema: {
|
|
3115
|
-
planKeyword: z.string().describe("Plan 文件的关键词,用于定位 add-route 文件和关联的 specs 目录"),
|
|
3116
|
-
},
|
|
3117
|
-
},
|
|
3118
|
-
async (args: { planKeyword: string }) => {
|
|
3119
|
-
try {
|
|
3120
|
-
const { planKeyword } = args
|
|
3121
|
-
if (!planKeyword) return errorResponse("planKeyword 参数不能为空")
|
|
3122
|
-
|
|
3123
|
-
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans")
|
|
3124
|
-
const parts: string[] = [
|
|
3125
|
-
"=== RAHS:Round Attention Health Score(轮次注意力健康度量化)===",
|
|
3126
|
-
`Plan 关键词: "${planKeyword}"`,
|
|
3127
|
-
"",
|
|
3128
|
-
]
|
|
3129
|
-
|
|
3130
|
-
// === 1. 定位 add-route 文件,提取计划文件清单 ===
|
|
3131
|
-
let plannedFiles: string[] = []
|
|
3132
|
-
let arFile: string | null = null
|
|
3133
|
-
try {
|
|
3134
|
-
const entries = await readdirRecursive(plansDir)
|
|
3135
|
-
const planLower = planKeyword.toLowerCase()
|
|
3136
|
-
arFile = entries.find(f =>
|
|
3137
|
-
f.toLowerCase().includes("add-route") && f.toLowerCase().includes(planLower)
|
|
3138
|
-
) || null
|
|
3139
|
-
if (arFile) {
|
|
3140
|
-
const arContent = await readFileSafe(join(plansDir, arFile))
|
|
3141
|
-
if (arContent) {
|
|
3142
|
-
// 从附录文件清单提取计划修改的文件
|
|
3143
|
-
const tableMatch = arContent.match(/##\s+附录:文件清单[\s\S]*?(?=---|\n\n$|$)/)
|
|
3144
|
-
if (tableMatch) {
|
|
3145
|
-
const pathRefs = tableMatch[0].match(/`([^`]+\.ts[x]?)`/g) || []
|
|
3146
|
-
const rawPaths = pathRefs.map(r => r.replace(/`/g, ""))
|
|
3147
|
-
// 去重、过滤脚本路径(运维类不影响核心代码 RAHS)
|
|
3148
|
-
plannedFiles = Array.from(new Set(rawPaths)).filter(f =>
|
|
3149
|
-
f.startsWith("src/") || f.startsWith("scripts/")
|
|
3150
|
-
)
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
}
|
|
3154
|
-
} catch { /* ignore */ }
|
|
3155
|
-
|
|
3156
|
-
parts.push(`add-route: ${arFile || "未找到"}`)
|
|
3157
|
-
parts.push(`计划文件: ${plannedFiles.length} 个`)
|
|
3158
|
-
for (const f of plannedFiles.slice(0, 10)) parts.push(` - ${f}`)
|
|
3159
|
-
parts.push("")
|
|
3160
|
-
|
|
3161
|
-
// ====== 维度一:范围保真度(权重 30%) ======
|
|
3162
|
-
let scopeScore = 100
|
|
3163
|
-
let changedFiles: string[] = []
|
|
3164
|
-
try {
|
|
3165
|
-
const { spawnSync } = await import("child_process")
|
|
3166
|
-
const diff = spawnSync("git", ["diff", "--name-only"], { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
|
|
3167
|
-
const diffStat = diff.stdout || ""
|
|
3168
|
-
changedFiles = diffStat.trim().split("\n").filter(Boolean)
|
|
3169
|
-
} catch { /* ignore */ }
|
|
3170
|
-
|
|
3171
|
-
if (plannedFiles.length > 0 && changedFiles.length > 0) {
|
|
3172
|
-
const plannedSet = new Set(plannedFiles.map(f => f.replace(/^src\//, "")))
|
|
3173
|
-
const changedSet = new Set(changedFiles.map(f => f.replace(/^src\//, "")))
|
|
3174
|
-
// 交集
|
|
3175
|
-
let intersectCount = 0
|
|
3176
|
-
const plannedArr = Array.from(plannedSet)
|
|
3177
|
-
for (let i = 0; i < plannedArr.length; i++) {
|
|
3178
|
-
if (changedSet.has(plannedArr[i])) intersectCount++
|
|
3179
|
-
}
|
|
3180
|
-
scopeScore = Math.round((intersectCount / plannedSet.size) * 100)
|
|
3181
|
-
|
|
3182
|
-
// 检测范围扩散(改多了没计划的文件)
|
|
3183
|
-
const unplannedChanges: string[] = []
|
|
3184
|
-
const changedArr = Array.from(changedSet)
|
|
3185
|
-
for (let i = 0; i < changedArr.length; i++) {
|
|
3186
|
-
const cf = changedArr[i]
|
|
3187
|
-
if (!plannedSet.has(cf) && !cf.endsWith(".md") && !cf.endsWith(".toml")) {
|
|
3188
|
-
unplannedChanges.push(cf)
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
parts.push("=== 维度一:范围保真度(30%)===")
|
|
3192
|
-
parts.push(` 计划文件: ${plannedSet.size} 个`)
|
|
3193
|
-
parts.push(` 实际变更: ${changedSet.size} 个`)
|
|
3194
|
-
parts.push(` 交集: ${intersectCount} 个`)
|
|
3195
|
-
parts.push(` 分数: ${scopeScore}/100`)
|
|
3196
|
-
if (unplannedChanges.length > 0) {
|
|
3197
|
-
parts.push(` ⚠️ 计划外变更: ${unplannedChanges.join(", ")}`)
|
|
3198
|
-
}
|
|
3199
|
-
} else {
|
|
3200
|
-
parts.push("=== 维度一:范围保真度(30%)===")
|
|
3201
|
-
parts.push(` 分数: ${scopeScore}/100(${plannedFiles.length === 0 ? "无计划文件" : "无 git diff"},默认满分)`)
|
|
3202
|
-
}
|
|
3203
|
-
parts.push("")
|
|
3204
|
-
|
|
3205
|
-
// ====== 维度二:类型安全(权重 20%) ======
|
|
3206
|
-
let typeScore = 100
|
|
3207
|
-
try {
|
|
3208
|
-
const { spawnSync } = await import("child_process")
|
|
3209
|
-
const tsc = spawnSync("npx", ["tsc", "--noEmit"], {
|
|
3210
|
-
cwd: PROJECT_ROOT,
|
|
3211
|
-
encoding: "utf-8",
|
|
3212
|
-
timeout: 30000,
|
|
3213
|
-
})
|
|
3214
|
-
const tscOut = (tsc.stdout || "") + (tsc.stderr || "")
|
|
3215
|
-
// 统计 error TS 行数
|
|
3216
|
-
const errorLines = tscOut.split("\n").filter(l => l.includes("error TS")).length
|
|
3217
|
-
typeScore = Math.max(0, 100 - errorLines * 10)
|
|
3218
|
-
parts.push("=== 维度二:类型安全(20%)===")
|
|
3219
|
-
parts.push(` tsc errors: ${errorLines}`)
|
|
3220
|
-
parts.push(` 分数: ${typeScore}/100`)
|
|
3221
|
-
} catch {
|
|
3222
|
-
parts.push("=== 维度二:类型安全(20%)===")
|
|
3223
|
-
parts.push(` 分数: ${typeScore}/100(无法执行 tsc,默认满分——请在项目环境中自行验证)`)
|
|
3224
|
-
}
|
|
3225
|
-
parts.push("")
|
|
3226
|
-
|
|
3227
|
-
// ====== 维度三:审计完整度(权重 25%) ======
|
|
3228
|
-
let auditScore = 100
|
|
3229
|
-
try {
|
|
3230
|
-
const plannedSourceFiles = plannedFiles.filter(f => f.startsWith("src/"))
|
|
3231
|
-
let auditedCount = 0
|
|
3232
|
-
if (plannedSourceFiles.length > 0) {
|
|
3233
|
-
for (const f of plannedSourceFiles) {
|
|
3234
|
-
const records = await prisma.auditLog.findMany({
|
|
3235
|
-
where: {
|
|
3236
|
-
targetId: { contains: f.split("/").pop()?.replace(/\.(ts|tsx)$/, "") || f },
|
|
3237
|
-
action: { in: ["MODIFY", "CREATE"] },
|
|
3238
|
-
},
|
|
3239
|
-
select: { id: true },
|
|
3240
|
-
take: 1,
|
|
3241
|
-
})
|
|
3242
|
-
if (records.length > 0) auditedCount++
|
|
3243
|
-
}
|
|
3244
|
-
auditScore = Math.round((auditedCount / plannedSourceFiles.length) * 100)
|
|
3245
|
-
}
|
|
3246
|
-
parts.push("=== 维度三:审计完整度(25%)===")
|
|
3247
|
-
parts.push(` 已审计: ${auditedCount} / ${plannedSourceFiles.length} 个源文件`)
|
|
3248
|
-
parts.push(` 分数: ${auditScore}/100`)
|
|
3249
|
-
if (auditedCount < plannedSourceFiles.length) {
|
|
3250
|
-
const missingFiles = plannedSourceFiles.filter(f => {
|
|
3251
|
-
// 简单标记(实际查询在上面的循环中)
|
|
3252
|
-
return true
|
|
3253
|
-
}).slice(auditedCount)
|
|
3254
|
-
parts.push(` ⚠️ 缺少 ADD-7 记录: ${plannedSourceFiles.length - auditedCount} 个文件`)
|
|
3255
|
-
}
|
|
3256
|
-
} catch {
|
|
3257
|
-
parts.push("=== 维度三:审计完整度(25%)===")
|
|
3258
|
-
parts.push(` 分数: ${auditScore}/100(无法查询数据库,默认满分)`)
|
|
3259
|
-
}
|
|
3260
|
-
parts.push("")
|
|
3261
|
-
|
|
3262
|
-
// ====== 维度四:Spec 合规(权重 15%) ======
|
|
3263
|
-
let specSyncScore = 100
|
|
3264
|
-
try {
|
|
3265
|
-
// 简化版 spec_sync 检查:只检查 tasks.md 和 checklist.md 是否有未勾选项
|
|
3266
|
-
const specDirName = arFile?.replace(/-add-route-v\d+\.md$/, "") || planKeyword.replace(/\s+/g, "-").toLowerCase()
|
|
3267
|
-
const tasksPath = join(PROJECT_ROOT, MAGIC_DIR, "specs", specDirName, "tasks.md")
|
|
3268
|
-
const checklistPath = join(PROJECT_ROOT, MAGIC_DIR, "specs", specDirName, "checklist.md")
|
|
3269
|
-
|
|
3270
|
-
let uncheckedCount = 0
|
|
3271
|
-
const tasksContent = await readFileSafe(tasksPath)
|
|
3272
|
-
if (tasksContent) {
|
|
3273
|
-
uncheckedCount += (tasksContent.match(/^- \[ \] /gm) || []).length
|
|
3274
|
-
}
|
|
3275
|
-
const checklistContent = await readFileSafe(checklistPath)
|
|
3276
|
-
if (checklistContent) {
|
|
3277
|
-
uncheckedCount += (checklistContent.match(/^- \[ \] /gm) || []).length
|
|
3278
|
-
}
|
|
3279
|
-
|
|
3280
|
-
specSyncScore = uncheckedCount === 0 ? 100 : Math.max(0, 100 - uncheckedCount * 5)
|
|
3281
|
-
parts.push("=== 维度四:Spec 合规(15%)===")
|
|
3282
|
-
parts.push(` tasks.md + checklist.md 未勾选: ${uncheckedCount} 项`)
|
|
3283
|
-
parts.push(` 分数: ${specSyncScore}/100`)
|
|
3284
|
-
} catch {
|
|
3285
|
-
parts.push("=== 维度四:Spec 合规(15%)===")
|
|
3286
|
-
parts.push(` 分数: ${specSyncScore}/100(无法读取 specs,默认满分)`)
|
|
3287
|
-
}
|
|
3288
|
-
parts.push("")
|
|
3289
|
-
|
|
3290
|
-
// ====== 维度五:阶段对称性(权重 10%) ======
|
|
3291
|
-
let phaseScore = 100
|
|
3292
|
-
try {
|
|
3293
|
-
let asymmetricCount = 0
|
|
3294
|
-
for (const f of plannedFiles.filter(f => f.startsWith("src/"))) {
|
|
3295
|
-
const filePath = join(PROJECT_ROOT, f)
|
|
3296
|
-
const fileContent = await readFileSafe(filePath)
|
|
3297
|
-
if (fileContent) {
|
|
3298
|
-
const startCount = (fileContent.match(/auditPhaseStart/g) || []).length
|
|
3299
|
-
const endCount = (fileContent.match(/auditPhaseEnd/g) || []).length
|
|
3300
|
-
if (startCount !== endCount) asymmetricCount++
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
phaseScore = asymmetricCount === 0 ? 100 : Math.max(0, 100 - asymmetricCount * 20)
|
|
3304
|
-
parts.push("=== 维度五:阶段对称性(10%)===")
|
|
3305
|
-
parts.push(` 不对称文件: ${asymmetricCount} 个`)
|
|
3306
|
-
parts.push(` 分数: ${phaseScore}/100`)
|
|
3307
|
-
} catch {
|
|
3308
|
-
parts.push("=== 维度五:阶段对称性(10%)===")
|
|
3309
|
-
parts.push(` 分数: ${phaseScore}/100(无法检查,默认满分)`)
|
|
3310
|
-
}
|
|
3311
|
-
parts.push("")
|
|
3312
|
-
|
|
3313
|
-
// ====== RAHS 复合计算 ======
|
|
3314
|
-
const rahs = Math.round(
|
|
3315
|
-
scopeScore * 0.30 +
|
|
3316
|
-
typeScore * 0.20 +
|
|
3317
|
-
auditScore * 0.25 +
|
|
3318
|
-
specSyncScore * 0.15 +
|
|
3319
|
-
phaseScore * 0.10
|
|
3320
|
-
)
|
|
3321
|
-
|
|
3322
|
-
let verdict: string, verdictIcon: string, action: string
|
|
3323
|
-
if (rahs >= 90) {
|
|
3324
|
-
verdict = "HEALTHY"
|
|
3325
|
-
verdictIcon = "🟢"
|
|
3326
|
-
action = "进入下一 Step"
|
|
3327
|
-
} else if (rahs >= 70) {
|
|
3328
|
-
verdict = "WARNING"
|
|
3329
|
-
verdictIcon = "🟡"
|
|
3330
|
-
action = "自检后决定:"
|
|
3331
|
-
if (scopeScore < 70) action += " 范围扩散严重;"
|
|
3332
|
-
if (auditScore < 70) action += " 审计记录缺失;"
|
|
3333
|
-
if (typeScore < 70) action += " 类型错误过多;"
|
|
3334
|
-
} else {
|
|
3335
|
-
verdict = "DRIFT"
|
|
3336
|
-
verdictIcon = "🔴"
|
|
3337
|
-
action = "强制返工——注意力漂移严重,本轮不可交付"
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
parts.push("=== RAHS 复合计算 ===")
|
|
3341
|
-
parts.push(` 范围保真度: ${scopeScore} × 0.30 = ${(scopeScore * 0.30).toFixed(1)}`)
|
|
3342
|
-
parts.push(` 类型安全: ${typeScore} × 0.20 = ${(typeScore * 0.20).toFixed(1)}`)
|
|
3343
|
-
parts.push(` 审计完整度: ${auditScore} × 0.25 = ${(auditScore * 0.25).toFixed(1)}`)
|
|
3344
|
-
parts.push(` Spec 合规: ${specSyncScore} × 0.15 = ${(specSyncScore * 0.15).toFixed(1)}`)
|
|
3345
|
-
parts.push(` 阶段对称性: ${phaseScore} × 0.10 = ${(phaseScore * 0.10).toFixed(1)}`)
|
|
3346
|
-
parts.push(` ─────────────────────────────────`)
|
|
3347
|
-
parts.push(` RAHS = ${rahs} ${verdictIcon} ${verdict}`)
|
|
3348
|
-
parts.push("")
|
|
3349
|
-
parts.push("=== 判定 ===")
|
|
3350
|
-
parts.push(` 结果: ${verdictIcon} ${verdict}`)
|
|
3351
|
-
parts.push(` 动作: ${action}`)
|
|
3352
|
-
|
|
3353
|
-
return textResponse(parts.join("\n"))
|
|
3354
|
-
} catch (error) {
|
|
3355
|
-
return errorResponse(`check_rahs 失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
)
|
|
3359
|
-
|
|
3360
|
-
// ========== 升级工具: check_add_route_status(内容扫描增强) ==========
|
|
3361
|
-
// 在原 L1188 check_add_route_status 基础上,增加 Step 完成度扫描(3→4 状态)
|
|
3362
|
-
// 保持向后兼容:normal/file_missing/never_generated 三级不变,normal 时追加 completeness 报告
|
|
3363
|
-
|
|
3364
|
-
// ========== 新增工具: create_plan ==========
|
|
3365
|
-
// 在 ADD 工作流中创建 Plan 文件,自动处理日期路径 + index.md 更新
|
|
3366
|
-
server.registerTool(
|
|
3367
|
-
"create_plan",
|
|
3368
|
-
{
|
|
3369
|
-
description: "创建 ADD Plan 文件到 ${MAGIC_DIR}/plans/{YYYY-MM}/{DD}/ 目录,自动调用 gen-plan-index.sh 更新 index.md。AI 助手在 ADD 范式 Step 0(方案设计)完成后应调用此工具落盘 Plan 文件。\n\n自动处理:\n1. 根据当前日期创建目录 ${MAGIC_DIR}/plans/{YYYY-MM}/{DD}/\n2. 写入 planName-v{version}.md(markdown 正文)\n3. 执行 scripts/gen-plan-index.sh 更新 index.md\n4. 自动调用 record_dev_operation 记录创建操作",
|
|
3370
|
-
inputSchema: {
|
|
3371
|
-
planName: z.string().describe("Plan 名称(kebab-case),如 '{{projectName}}-domain-vocabulary'"),
|
|
3372
|
-
version: z.string().describe("版本号,如 'v1', 'v2'"),
|
|
3373
|
-
title: z.string().describe("Plan 标题(markdown H1 第一行),如 '# {{projectName}}-domain-vocabulary-plan-v2'"),
|
|
3374
|
-
content: z.string().describe("Plan 完整 markdown 正文(从 H1 标题行开始)"),
|
|
3375
|
-
planKeyword: z.string().optional().describe("Plan 关键词(用于 session-init 恢复定位),默认使用 planName"),
|
|
3376
|
-
},
|
|
3377
|
-
},
|
|
3378
|
-
async (args: { planName: string; version: string; title: string; content: string; planKeyword?: string }) => {
|
|
3379
|
-
try {
|
|
3380
|
-
const { planName, version, title, content, planKeyword } = args
|
|
3381
|
-
|
|
3382
|
-
// 计算日期路径
|
|
3383
|
-
const now = new Date()
|
|
3384
|
-
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`
|
|
3385
|
-
const day = String(now.getDate()).padStart(2, "0")
|
|
3386
|
-
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans", yearMonth, day)
|
|
3387
|
-
|
|
3388
|
-
// 确保目录存在
|
|
3389
|
-
await mkdir(plansDir, { recursive: true })
|
|
3390
|
-
|
|
3391
|
-
// 生成文件名
|
|
3392
|
-
const fileName = `${planName}-plan-${version}.md`
|
|
3393
|
-
const filePath = join(plansDir, fileName)
|
|
3394
|
-
const relativePath = `${MAGIC_DIR}/plans/${yearMonth}/${day}/${fileName}`
|
|
3395
|
-
|
|
3396
|
-
const parts: string[] = []
|
|
3397
|
-
|
|
3398
|
-
// 写入 Plan 文件
|
|
3399
|
-
await writeFile(filePath, content, "utf-8")
|
|
3400
|
-
|
|
3401
|
-
// 写后校验
|
|
3402
|
-
const guard = await validateDocWithGuard(filePath)
|
|
3403
|
-
if (!guard.ok) {
|
|
3404
|
-
try { await import("fs/promises").then(m => m.unlink(filePath)) } catch { }
|
|
3405
|
-
return errorResponse(`Plan 模板校验不通过,拒绝写入:\n${guard.issues}`)
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
parts.push(`✅ Plan 文件已创建: ${relativePath}`)
|
|
3409
|
-
|
|
3410
|
-
// 更新 index.md
|
|
3411
|
-
const indexScript = join(PROJECT_ROOT, "scripts", "gen-plan-index.sh")
|
|
3412
|
-
if (existsSync(indexScript)) {
|
|
3413
|
-
const result = spawnSync("/bin/bash", [indexScript], {
|
|
3414
|
-
cwd: PROJECT_ROOT,
|
|
3415
|
-
encoding: "utf-8",
|
|
3416
|
-
timeout: 10000,
|
|
3417
|
-
})
|
|
3418
|
-
if (result.status === 0) {
|
|
3419
|
-
parts.push(`📋 index.md 已更新: ${result.stdout?.trim() || "ok"}`)
|
|
3420
|
-
} else {
|
|
3421
|
-
parts.push(`⚠️ index.md 更新失败: ${result.stderr?.trim() || result.error?.message || "unknown"}`)
|
|
3422
|
-
}
|
|
3423
|
-
} else {
|
|
3424
|
-
parts.push(`⚠️ gen-plan-index.sh 不存在,index.md 将在 crontab 自动更新`)
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
// 自动记录 devlog
|
|
3428
|
-
try {
|
|
3429
|
-
const keyword = planKeyword || planName
|
|
3430
|
-
await prisma.devOperation.create({
|
|
3431
|
-
data: {
|
|
3432
|
-
userId: "ai-assistant",
|
|
3433
|
-
planKeyword: keyword,
|
|
3434
|
-
action: "CREATE",
|
|
3435
|
-
targetType: "PLAN",
|
|
3436
|
-
targetId: relativePath,
|
|
3437
|
-
afterState: { title, version, planName },
|
|
3438
|
-
reason: `ADD Step 0: 创建 Plan ${title}`,
|
|
3439
|
-
},
|
|
3440
|
-
})
|
|
3441
|
-
parts.push(`📝 devOperation 已记录 (planKeyword: ${keyword})`)
|
|
3442
|
-
} catch (auditErr) {
|
|
3443
|
-
parts.push(`⚠️ devOperation 记录失败: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`)
|
|
3444
|
-
}
|
|
3445
|
-
|
|
3446
|
-
parts.push("")
|
|
3447
|
-
parts.push(`用法提示:`)
|
|
3448
|
-
parts.push(` 后续可通过 query_audit_logs({ planKeyword: "${planKeyword || planName}" }) 查询本 Plan 相关操作`)
|
|
3449
|
-
|
|
3450
|
-
return textResponse(parts.join("\n"))
|
|
3451
|
-
} catch (error) {
|
|
3452
|
-
return errorResponse(`创建 Plan 失败: ${error instanceof Error ? error.message : String(error)}`)
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
)
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server"
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
|
|
3
|
+
import { registerAll } from "./mcp-server/index.js"
|
|
3456
4
|
|
|
3457
5
|
async function main() {
|
|
6
|
+
const server = new McpServer(
|
|
7
|
+
{ name: "add-dev-tools", version: "1.0.0" },
|
|
8
|
+
{ capabilities: { tools: {}, resources: { subscribe: true } } }
|
|
9
|
+
)
|
|
10
|
+
registerAll(server)
|
|
3458
11
|
const transport = new StdioServerTransport()
|
|
3459
12
|
await server.connect(transport)
|
|
3460
13
|
console.error("[ADD-MCP] add-dev-tools MCP server started on stdio")
|