add-coder 0.3.38 → 0.3.39
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/package.json +1 -1
- package/templates/.add-coder-src-hash.json +10 -9
- package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_completeness.ts +17 -4
- package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +34 -6
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +15 -25
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +23 -26
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +56 -38
- package/templates/core/scripts/mcp-server/tools/gateway/plan-resolve.ts +270 -0
- package/templates/core/templates/checklist-template.md +2 -0
- package/templates/core/templates/review-implementation-template.md +18 -0
- package/templates/core/templates/review-implementation-template.schema.json +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "add-coder",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.39",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "[codein2027](https://github.com/xiaomingming92/codein2027) - A complete scaffolding for building the ADD programming paradigm, the implementation layer for AI code governance. Core principle: Audit as Infrastructure. Breaks the black-box programming process and cross-session amnesia, evolving programming paradigms into an auditable, traceable, and convergent new era. npx-ready.\n\n[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
|
|
6
6
|
"repository": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_version": "0.3.
|
|
2
|
+
"_version": "0.3.39",
|
|
3
3
|
"adapters/claude/hooks/doc-format-guard.mjs": "b21350a0",
|
|
4
4
|
"adapters/claude/hooks/doc-format-guard.ts": "2abf8013",
|
|
5
5
|
"adapters/claude/hooks/lib/claude-env.ts": "170b40ca",
|
|
@@ -319,14 +319,15 @@
|
|
|
319
319
|
"core/scripts/mcp-server/tools/context.ts": "d7d4e546",
|
|
320
320
|
"core/scripts/mcp-server/tools/contract.ts": "14b6ab8f",
|
|
321
321
|
"core/scripts/mcp-server/tools/docs.ts": "034e473c",
|
|
322
|
-
"core/scripts/mcp-server/tools/gateway/check_add_route_completeness.ts": "
|
|
323
|
-
"core/scripts/mcp-server/tools/gateway/check_add_route_status.ts": "
|
|
322
|
+
"core/scripts/mcp-server/tools/gateway/check_add_route_completeness.ts": "a888c711",
|
|
323
|
+
"core/scripts/mcp-server/tools/gateway/check_add_route_status.ts": "1b6884bc",
|
|
324
324
|
"core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts": "2cba51c5",
|
|
325
|
-
"core/scripts/mcp-server/tools/gateway/check_dps.ts": "
|
|
326
|
-
"core/scripts/mcp-server/tools/gateway/check_rahs.ts": "
|
|
327
|
-
"core/scripts/mcp-server/tools/gateway/check_spec_sync.ts": "
|
|
325
|
+
"core/scripts/mcp-server/tools/gateway/check_dps.ts": "d4120356",
|
|
326
|
+
"core/scripts/mcp-server/tools/gateway/check_rahs.ts": "00a51678",
|
|
327
|
+
"core/scripts/mcp-server/tools/gateway/check_spec_sync.ts": "5dd01970",
|
|
328
328
|
"core/scripts/mcp-server/tools/gateway/helpers.ts": "f8794a92",
|
|
329
329
|
"core/scripts/mcp-server/tools/gateway/index.ts": "cffe9783",
|
|
330
|
+
"core/scripts/mcp-server/tools/gateway/plan-resolve.ts": "5c742655",
|
|
330
331
|
"core/scripts/mcp-server/tools/gateway.backup": "65eae393",
|
|
331
332
|
"core/scripts/mcp-server/tools/hitl.ts": "4488c338",
|
|
332
333
|
"core/scripts/mcp-server/tools/hook-event-report.ts": "1d825b3f",
|
|
@@ -351,7 +352,7 @@
|
|
|
351
352
|
"core/templates/add-route-template-heavyweight.md": "0e107ac0",
|
|
352
353
|
"core/templates/add-route-template.md": "da7022df",
|
|
353
354
|
"core/templates/add-route-template.schema.json": "cca77a6e",
|
|
354
|
-
"core/templates/checklist-template.md": "
|
|
355
|
+
"core/templates/checklist-template.md": "8e3ddc28",
|
|
355
356
|
"core/templates/checklist-template.schema.json": "fb3a73e8",
|
|
356
357
|
"core/templates/collab-contract-template.md": "b8a52503",
|
|
357
358
|
"core/templates/collab-contract-template.schema.json": "ae0f482c",
|
|
@@ -374,8 +375,8 @@
|
|
|
374
375
|
"core/templates/prd-standard-template.schema.json": "2b76a919",
|
|
375
376
|
"core/templates/report-template.md": "bc4602ac",
|
|
376
377
|
"core/templates/report-template.schema.json": "44dd2bd0",
|
|
377
|
-
"core/templates/review-implementation-template.md": "
|
|
378
|
-
"core/templates/review-implementation-template.schema.json": "
|
|
378
|
+
"core/templates/review-implementation-template.md": "bac5be0b",
|
|
379
|
+
"core/templates/review-implementation-template.schema.json": "aeb598de",
|
|
379
380
|
"core/templates/review-runtime-template.md": "c5960af1",
|
|
380
381
|
"core/templates/review-runtime-template.schema.json": "c346ea7d",
|
|
381
382
|
"core/templates/review-template.md": "644d2772",
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
PROJECT_ROOT,
|
|
18
18
|
MAGIC_DIR,
|
|
19
19
|
} from "../../shared/fs.js";
|
|
20
|
+
import { resolvePlanArtifact } from "./plan-resolve.js";
|
|
20
21
|
|
|
21
22
|
export function registerCheckAddRouteCompleteness(server: ToolRegistrar) {
|
|
22
23
|
server.registerTool(
|
|
@@ -36,15 +37,26 @@ export function registerCheckAddRouteCompleteness(server: ToolRegistrar) {
|
|
|
36
37
|
if (!existsSync(plansDir))
|
|
37
38
|
return errorResponse(`plans 目录不存在: ${plansDir}`);
|
|
38
39
|
const allFiles = await readdirRecursive(plansDir);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// 版本配对(2026-09-18 修复):与 Plan 同目录同版本优先,不再"去版本取首个匹配"(永远命中 v1)
|
|
41
|
+
const { plan: resolvedPlan, artifact: resolvedAr } = resolvePlanArtifact(
|
|
42
|
+
allFiles,
|
|
43
|
+
pp,
|
|
44
|
+
"add-route",
|
|
43
45
|
);
|
|
46
|
+
const arFile = resolvedAr?.file;
|
|
44
47
|
if (!arFile)
|
|
45
48
|
return errorResponse(`未找到匹配的 add-route 文件(关键词: ${pp})`);
|
|
46
49
|
const content = await readFileSafe(join(plansDir, arFile));
|
|
47
50
|
if (!content) return errorResponse("add-route 文件无法读取");
|
|
51
|
+
const pairing: string[] = [];
|
|
52
|
+
if (resolvedPlan)
|
|
53
|
+
pairing.push(
|
|
54
|
+
`Plan: ${resolvedPlan.file}${resolvedPlan.version > 0 ? ` (v${resolvedPlan.version})` : ""}`,
|
|
55
|
+
);
|
|
56
|
+
pairing.push(
|
|
57
|
+
`add-route: ${arFile}${resolvedAr && resolvedAr.version > 0 ? ` (v${resolvedAr.version})` : ""} 〔配对依据: ${resolvedAr?.via}〕`,
|
|
58
|
+
);
|
|
59
|
+
if (resolvedAr?.warning) pairing.push(`⚠️ ${resolvedAr.warning}`);
|
|
48
60
|
const steps: Record<string, { checked: number; unchecked: number }> =
|
|
49
61
|
{};
|
|
50
62
|
let cur = "";
|
|
@@ -71,6 +83,7 @@ export function registerCheckAddRouteCompleteness(server: ToolRegistrar) {
|
|
|
71
83
|
const parts = [
|
|
72
84
|
`=== add-route Step 完成度扫描 ===`,
|
|
73
85
|
`文件: ${MAGIC_DIR}/plans/${arFile}`,
|
|
86
|
+
...pairing,
|
|
74
87
|
`整体: ${tu}/${tc} (${tc > 0 ? Math.round((tu / tc) * 100) : 0}%)`,
|
|
75
88
|
"",
|
|
76
89
|
];
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "../../shared/fs.js";
|
|
19
19
|
import { prisma } from "../../shared/prisma.js";
|
|
20
20
|
import { getRuntimeContext } from "../../shared/env.js";
|
|
21
|
+
import { parseArtifactName, resolvePlanArtifact } from "./plan-resolve.js";
|
|
21
22
|
|
|
22
23
|
function scanCheckboxes(content: string) {
|
|
23
24
|
let t = 0,
|
|
@@ -162,8 +163,10 @@ export function registerCheckAddRouteStatus(server: ToolRegistrar) {
|
|
|
162
163
|
}
|
|
163
164
|
let fileExists = false;
|
|
164
165
|
const matchedFiles: string[] = [];
|
|
166
|
+
let planDirEntries: string[] = [];
|
|
165
167
|
try {
|
|
166
168
|
const entries = await readdirRecursive(plansDir);
|
|
169
|
+
planDirEntries = entries;
|
|
167
170
|
for (const e of entries) {
|
|
168
171
|
const el = e.toLowerCase();
|
|
169
172
|
if (
|
|
@@ -177,6 +180,33 @@ export function registerCheckAddRouteStatus(server: ToolRegistrar) {
|
|
|
177
180
|
} catch {
|
|
178
181
|
/* empty */
|
|
179
182
|
}
|
|
183
|
+
// 版本配对(2026-09-18 修复):存在性判定保留"任一命中"(不把 warn 翻成 never_generated),
|
|
184
|
+
// 但**扫描对象**改为与 Plan 配对的版本 —— 旧实现用 readdir 顺序里的 matchedFiles[0],
|
|
185
|
+
// 多版本共存时扫的是最早的 v1,完成度与版本关系都失真。
|
|
186
|
+
const matchedSorted = matchedFiles
|
|
187
|
+
.map((f) => ({ f, version: parseArtifactName(f, "add-route")?.version ?? 0 }))
|
|
188
|
+
.sort((a, b) => b.version - a.version || (a.f < b.f ? 1 : -1));
|
|
189
|
+
const { plan: resolvedPlan, artifact: paired } = resolvePlanArtifact(
|
|
190
|
+
planDirEntries,
|
|
191
|
+
planKeyword,
|
|
192
|
+
"add-route",
|
|
193
|
+
);
|
|
194
|
+
const scanTarget =
|
|
195
|
+
paired?.file ?? matchedSorted[0]?.f ?? matchedFiles[0] ?? "";
|
|
196
|
+
const pairingNotes: string[] = [];
|
|
197
|
+
if (resolvedPlan)
|
|
198
|
+
pairingNotes.push(
|
|
199
|
+
`Plan: ${resolvedPlan.file}${resolvedPlan.version > 0 ? ` (v${resolvedPlan.version})` : ""}`,
|
|
200
|
+
);
|
|
201
|
+
if (paired)
|
|
202
|
+
pairingNotes.push(
|
|
203
|
+
`add-route: ${paired.file}${paired.version > 0 ? ` (v${paired.version})` : ""} 〔配对依据: ${paired.via}〕`,
|
|
204
|
+
);
|
|
205
|
+
if (matchedFiles.length > 1)
|
|
206
|
+
pairingNotes.push(
|
|
207
|
+
`ℹ️ 多版本共存:${matchedSorted.map((e) => `${e.f}(v${e.version})`).join(" / ")} —— 本次扫描 ${scanTarget}`,
|
|
208
|
+
);
|
|
209
|
+
if (paired?.warning) pairingNotes.push(`⚠️ ${paired.warning}`);
|
|
180
210
|
const parts = [
|
|
181
211
|
`=== ADD 守卫:add-route 存在性交叉校验 ===`,
|
|
182
212
|
`Plan 关键词: "${planKeyword}"`,
|
|
@@ -184,14 +214,13 @@ export function registerCheckAddRouteStatus(server: ToolRegistrar) {
|
|
|
184
214
|
"",
|
|
185
215
|
];
|
|
186
216
|
if (auditHasRecord && fileExists) {
|
|
187
|
-
const sc = scanCheckboxes(
|
|
188
|
-
(await readFileSafe(join(plansDir, matchedFiles[0]))) || "",
|
|
189
|
-
);
|
|
217
|
+
const sc = scanCheckboxes((await readFileSafe(join(plansDir, scanTarget))) || "");
|
|
190
218
|
const ok = sc.unchecked === 0;
|
|
191
219
|
parts.push(
|
|
192
220
|
`状态: ${ok ? "✅ normal" : "⚠️ warn_step_incomplete"}`,
|
|
193
221
|
ok ? "操作: 继续执行后续流程" : "操作: ⚠️ 存在未闭环 Step",
|
|
194
222
|
"",
|
|
223
|
+
...pairingNotes,
|
|
195
224
|
);
|
|
196
225
|
parts.push("=== 审计记录 ===");
|
|
197
226
|
for (const r of auditRecords.slice(0, 5))
|
|
@@ -248,14 +277,13 @@ export function registerCheckAddRouteStatus(server: ToolRegistrar) {
|
|
|
248
277
|
);
|
|
249
278
|
return errorResponse(parts.join("\n"));
|
|
250
279
|
}
|
|
251
|
-
const scW = scanCheckboxes(
|
|
252
|
-
(await readFileSafe(join(plansDir, matchedFiles[0]))) || "",
|
|
253
|
-
);
|
|
280
|
+
const scW = scanCheckboxes((await readFileSafe(join(plansDir, scanTarget))) || "");
|
|
254
281
|
const wOk = scW.unchecked === 0;
|
|
255
282
|
parts.push(
|
|
256
283
|
`状态: ⚠️ warn${scW.total > 0 && !wOk ? "_step_incomplete" : ""} — 文件存在但审计日志无记录`,
|
|
257
284
|
"操作: 允许继续,但建议补记录",
|
|
258
285
|
"",
|
|
286
|
+
...pairingNotes,
|
|
259
287
|
"=== 匹配文件 ===",
|
|
260
288
|
);
|
|
261
289
|
for (const f of matchedFiles) parts.push(` ${MAGIC_DIR}/plans/${f}`);
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
writeGateMetric,
|
|
39
39
|
GATE_METRIC_TYPE,
|
|
40
40
|
} from "../../shared/memory/metrics/gate-writer.js";
|
|
41
|
+
import { resolvePlanArtifact } from "./plan-resolve.js";
|
|
41
42
|
|
|
42
43
|
export function registerCheckDps(server: ToolRegistrar) {
|
|
43
44
|
const runtimeContext = getRuntimeContext();
|
|
@@ -68,22 +69,22 @@ export function registerCheckDps(server: ToolRegistrar) {
|
|
|
68
69
|
const apf = (await readdirRecursive(plansDir)).filter(
|
|
69
70
|
(f) => f.endsWith(".md") && !f.includes(".hitl"),
|
|
70
71
|
);
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
// 版本配对解析(单一真源 plan-resolve):Plan 取最高版本且排除 .hitl 提案;
|
|
73
|
+
// add-route / review 与 Plan 同目录同版本优先(2026-09-18 修复:旧实现去版本取首个匹配,永远命中 v1)
|
|
74
|
+
const { plan: resolvedPlan, artifact: resolvedAr } = resolvePlanArtifact(
|
|
75
|
+
apf,
|
|
76
|
+
pp,
|
|
77
|
+
"add-route",
|
|
75
78
|
);
|
|
76
|
-
if (!
|
|
79
|
+
if (!resolvedPlan)
|
|
77
80
|
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
|
|
78
|
-
const pm =
|
|
79
|
-
const va = parseInt(a.match(/-plan-v(\d+)/)?.[1] ?? "0", 10);
|
|
80
|
-
const vb = parseInt(b.match(/-plan-v(\d+)/)?.[1] ?? "0", 10);
|
|
81
|
-
return vb - va;
|
|
82
|
-
})[0];
|
|
81
|
+
const pm = resolvedPlan.file;
|
|
83
82
|
const planPath = join(plansDir, pm);
|
|
84
83
|
const pc = await readFileSafe(planPath);
|
|
85
84
|
if (!pc) return errorResponse(`无法读取 Plan 文件: ${pm}`);
|
|
86
|
-
parts.push(
|
|
85
|
+
parts.push(
|
|
86
|
+
`Plan: ${pm}${resolvedPlan.version > 0 ? ` (v${resolvedPlan.version})` : ""}`,
|
|
87
|
+
);
|
|
87
88
|
|
|
88
89
|
let sn = basename(pm).replace(/-plan-v\d+\.md$/, ""),
|
|
89
90
|
sc = "";
|
|
@@ -97,25 +98,14 @@ export function registerCheckDps(server: ToolRegistrar) {
|
|
|
97
98
|
rc = "";
|
|
98
99
|
if (existsSync(reviewsDir)) {
|
|
99
100
|
const rfs = await readdirRecursive(reviewsDir);
|
|
100
|
-
|
|
101
|
-
rn =
|
|
102
|
-
rfs.find(
|
|
103
|
-
(f) =>
|
|
104
|
-
f.toLowerCase().includes(rkNoVersion.toLowerCase()) &&
|
|
105
|
-
f.includes("-review-v"),
|
|
106
|
-
) || "";
|
|
101
|
+
rn = resolvePlanArtifact(rfs, pp, "review").artifact?.file || "";
|
|
107
102
|
if (rn) rc = (await readFileSafe(join(reviewsDir, rn))) || "";
|
|
108
103
|
}
|
|
109
|
-
// ★ 兼容精简版和标准版 Plan:剥离 -plan-vN 后缀匹配 add-route 和 review
|
|
110
|
-
const kwNoVersion = pp.replace(/-plan-v\d+$/i, "");
|
|
111
104
|
let arContent = "";
|
|
112
|
-
const arFile =
|
|
113
|
-
(f) =>
|
|
114
|
-
f.toLowerCase().includes(kwNoVersion.toLowerCase()) &&
|
|
115
|
-
f.toLowerCase().includes("add-route"),
|
|
116
|
-
);
|
|
105
|
+
const arFile = resolvedAr?.file;
|
|
117
106
|
if (arFile)
|
|
118
107
|
arContent = (await readFileSafe(join(plansDir, arFile))) || "";
|
|
108
|
+
if (resolvedAr?.warning) parts.push(`⚠️ ${resolvedAr.warning}`);
|
|
119
109
|
|
|
120
110
|
// 维度一: 语义相关性
|
|
121
111
|
// 子维度 A: 映射结构 (40%) — 映射表是否存在 + 覆盖行数
|
|
@@ -28,6 +28,11 @@ import {
|
|
|
28
28
|
writeGateMetric,
|
|
29
29
|
GATE_METRIC_TYPE,
|
|
30
30
|
} from "../../shared/memory/metrics/gate-writer.js";
|
|
31
|
+
import {
|
|
32
|
+
extractAppendixFiles,
|
|
33
|
+
resolvePlanArtifact,
|
|
34
|
+
splitGitPathList,
|
|
35
|
+
} from "./plan-resolve.js";
|
|
31
36
|
|
|
32
37
|
export function registerCheckRahs(server: ToolRegistrar) {
|
|
33
38
|
const runtimeContext = getRuntimeContext();
|
|
@@ -50,14 +55,11 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
50
55
|
const apf = (await readdirRecursive(plansDir)).filter((f) =>
|
|
51
56
|
f.endsWith(".md"),
|
|
52
57
|
);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
f.includes("-plan-v") &&
|
|
57
|
-
!f.includes(".hitl"),
|
|
58
|
-
);
|
|
59
|
-
if (!pm)
|
|
58
|
+
// 版本配对解析(2026-09-18 修复):Plan 取版本最高者(旧实现取首个命中 → 多版本时命中旧版)
|
|
59
|
+
const resolvedPlan = resolvePlanArtifact(apf, pp, "plan").plan;
|
|
60
|
+
if (!resolvedPlan)
|
|
60
61
|
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
|
|
62
|
+
const pm = resolvedPlan.file;
|
|
61
63
|
// Plan 正文在多个维度(Spec 引用解析、采证 seed)共用,提到此处只读一次
|
|
62
64
|
const planContent = (await readFileSafe(join(plansDir, pm))) || "";
|
|
63
65
|
let scopeScore = 80,
|
|
@@ -142,28 +144,23 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
142
144
|
} catch { /* checklist progress unavailable */ }
|
|
143
145
|
try {
|
|
144
146
|
// 范围保真度:git diff 变更文件与 add-route 附录清单匹配率
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
148
|
-
f.toLowerCase().includes("add-route"),
|
|
149
|
-
);
|
|
147
|
+
// 版本配对(2026-09-18 修复):与 Plan 同目录同版本优先,不再"去版本取首个匹配"
|
|
148
|
+
const arFile = resolvePlanArtifact(apf, pp, "add-route").artifact?.file;
|
|
150
149
|
if (arFile) {
|
|
151
150
|
const arContent =
|
|
152
151
|
(await readFileSafe(join(plansDir, arFile))) || "";
|
|
153
|
-
const appendix = (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
timeout: 5000,
|
|
161
|
-
|
|
162
|
-
const diffFiles = diff.stdout
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
.filter(Boolean)
|
|
166
|
-
.map((f: string) => f.toLowerCase());
|
|
152
|
+
const appendix = extractAppendixFiles(arContent).map((f) =>
|
|
153
|
+
f.toLowerCase(),
|
|
154
|
+
);
|
|
155
|
+
// -z(NUL 分隔):git 不加引号/八进制转义,非 ASCII 路径与附录可直接比对(与 check_spec_sync 同源修复)
|
|
156
|
+
const diff = runCommand(
|
|
157
|
+
"git",
|
|
158
|
+
["-c", "core.quotepath=false", "diff", "--name-only", "-z"],
|
|
159
|
+
{ cwd: PROJECT_ROOT, timeout: 5000 },
|
|
160
|
+
);
|
|
161
|
+
const diffFiles = splitGitPathList(diff.stdout).map((f) =>
|
|
162
|
+
f.toLowerCase(),
|
|
163
|
+
);
|
|
167
164
|
if (appendix.length > 0 && diffFiles.length > 0) {
|
|
168
165
|
const matched = diffFiles.filter((f) =>
|
|
169
166
|
appendix.some((a) => f === a || f.endsWith(`/${a}`)),
|
|
@@ -20,6 +20,13 @@ import {
|
|
|
20
20
|
MAGIC_DIR,
|
|
21
21
|
} from "../../shared/fs.js";
|
|
22
22
|
import { runCommand } from "../../shared/run-command.js";
|
|
23
|
+
import {
|
|
24
|
+
attributeToOtherPlans,
|
|
25
|
+
extractAppendixFiles,
|
|
26
|
+
parseArtifactName,
|
|
27
|
+
resolvePlanArtifact,
|
|
28
|
+
splitGitPathList,
|
|
29
|
+
} from "./plan-resolve.js";
|
|
23
30
|
|
|
24
31
|
export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
25
32
|
server.registerTool(
|
|
@@ -44,55 +51,40 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
44
51
|
f.endsWith(".md"),
|
|
45
52
|
);
|
|
46
53
|
const kw = args.planKeyword as string;
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
f.toLowerCase().includes(kw.toLowerCase()) && f.includes("-plan-v"),
|
|
52
|
-
)
|
|
53
|
-
.sort((a, b) => {
|
|
54
|
-
const va = parseInt(a.match(/-plan-v(\d+)/)?.[1] ?? "0", 10);
|
|
55
|
-
const vb = parseInt(b.match(/-plan-v(\d+)/)?.[1] ?? "0", 10);
|
|
56
|
-
return vb - va;
|
|
57
|
-
})[0];
|
|
58
|
-
if (!planMatch)
|
|
59
|
-
planMatch = planFiles.find((f) =>
|
|
60
|
-
f.toLowerCase().includes(kw.toLowerCase()),
|
|
61
|
-
);
|
|
62
|
-
if (!planMatch)
|
|
54
|
+
// 版本配对解析(单一真源 plan-resolve):Plan 取最高版本且排除 .hitl 提案;
|
|
55
|
+
// add-route 与 Plan 同目录同版本优先 —— 旧实现"去版本取首个匹配"永远命中 v1(2026-09-18 修复)
|
|
56
|
+
const { plan, artifact: ar } = resolvePlanArtifact(planFiles, kw, "add-route");
|
|
57
|
+
if (!plan)
|
|
63
58
|
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${kw})`);
|
|
64
|
-
lines.push(`Plan: ${
|
|
59
|
+
lines.push(`Plan: ${plan.file}${plan.version > 0 ? ` (v${plan.version})` : ""}`);
|
|
65
60
|
|
|
66
|
-
|
|
67
|
-
const kwNoVersion = kw.replace(/-plan-v\d+$/i, "");
|
|
68
|
-
const arFile = planFiles.find(
|
|
69
|
-
(f) =>
|
|
70
|
-
f.toLowerCase().includes(kwNoVersion.toLowerCase()) &&
|
|
71
|
-
f.toLowerCase().includes("add-route"),
|
|
72
|
-
);
|
|
73
|
-
if (!arFile) {
|
|
61
|
+
if (!ar) {
|
|
74
62
|
lines.push("add-route: 未找到", "");
|
|
75
63
|
lines.push("💡 提示:tasks.md/checklist.md 进度请用 plan_track 或 plan_status 查询");
|
|
76
64
|
} else {
|
|
77
|
-
lines.push(
|
|
78
|
-
|
|
65
|
+
lines.push(
|
|
66
|
+
`add-route: ${ar.file}${ar.version > 0 ? ` (v${ar.version})` : ""} 〔配对依据: ${ar.via}〕`,
|
|
67
|
+
);
|
|
68
|
+
if (ar.warning) lines.push(`⚠️ ${ar.warning}`);
|
|
69
|
+
const arContent = (await readFileSafe(join(plansDir, ar.file))) || "";
|
|
79
70
|
// 提取 add-route 附录文件清单(toml 纳入:sync-magic-rules.toml 等控制面文件,2026-08-18 修复)
|
|
80
71
|
// sql/prisma 纳入(2026-09-16 修复):原生 DDL 与 schema 是常见交付物,此前不在白名单 →
|
|
81
72
|
// 附录里明明登记了 `.../sqlite-fts5.sql` 仍被判"不在附录中"(假告警)。
|
|
82
|
-
const appendixFiles = (arContent
|
|
83
|
-
.map((f: string) => f.replace(/`/g, ""));
|
|
73
|
+
const appendixFiles = extractAppendixFiles(arContent);
|
|
84
74
|
lines.push(`附录文件: ${appendixFiles.length} 个`);
|
|
85
75
|
|
|
86
76
|
// git diff 变更文件(win32 下 git 为 .cmd → runCommand 自动解析,issue #10 跨端修复)
|
|
87
77
|
let diffFiles: string[] = [];
|
|
88
78
|
try {
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
// -z(NUL 分隔)下 git 永不加引号/八进制转义 —— 比 core.quotepath=false 更硬:
|
|
80
|
+
// 后者在路径含特殊字符时仍可能加引号,而带引号的 `".codex/..."` 既躲过 magic 前缀豁免,
|
|
81
|
+
// 也与附录里的真实路径比不中(2026-09-18 修复:check_spec_sync / check_rahs 同源)
|
|
82
|
+
const diff = runCommand(
|
|
83
|
+
"git",
|
|
84
|
+
["-c", "core.quotepath=false", "diff", "--name-only", "-z"],
|
|
85
|
+
{ cwd: PROJECT_ROOT, timeout: 5000 },
|
|
86
|
+
);
|
|
87
|
+
diffFiles = splitGitPathList(diff.stdout);
|
|
96
88
|
} catch {
|
|
97
89
|
lines.push("Git diff: 无法获取");
|
|
98
90
|
}
|
|
@@ -110,8 +102,34 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
110
102
|
!appendixSet.has(f.toLowerCase()),
|
|
111
103
|
);
|
|
112
104
|
if (unmatched.length > 0) {
|
|
113
|
-
|
|
114
|
-
|
|
105
|
+
// 工作区常有多个 Plan 同时在飞 —— 先分摊归属,把"其它 Plan 已登记"从本 Plan 的
|
|
106
|
+
// 未登记噪声里摘出来(按日期新者优先读,全部命中即提前结束)
|
|
107
|
+
const otherRoutes = planFiles
|
|
108
|
+
.filter((f) => {
|
|
109
|
+
const parsed = parseArtifactName(f, "add-route");
|
|
110
|
+
return parsed !== null && f !== ar.file;
|
|
111
|
+
})
|
|
112
|
+
.sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
|
|
113
|
+
const { owners, unowned } = await attributeToOtherPlans(
|
|
114
|
+
unmatched,
|
|
115
|
+
otherRoutes,
|
|
116
|
+
(rel) => readFileSafe(join(plansDir, rel)),
|
|
117
|
+
);
|
|
118
|
+
if (unowned.length > 0) {
|
|
119
|
+
lines.push(`⚠️ ${unowned.length} 个文件在 git diff 中但不在本 Plan 附录中:`);
|
|
120
|
+
unowned.forEach((f: string) => lines.push(` - ${f}`));
|
|
121
|
+
} else {
|
|
122
|
+
lines.push("✅ 本 Plan 附录已覆盖全部非其它 Plan 的变更文件");
|
|
123
|
+
}
|
|
124
|
+
if (owners.size > 0) {
|
|
125
|
+
lines.push(
|
|
126
|
+
"",
|
|
127
|
+
`ℹ️ ${owners.size} 个文件属于其它 Plan 已登记的交付物(本 Plan 不判定):`,
|
|
128
|
+
);
|
|
129
|
+
for (const { file, route } of owners.values()) {
|
|
130
|
+
lines.push(` - ${file} ← ${route}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
115
133
|
} else {
|
|
116
134
|
lines.push("✅ git diff 变更文件全部在 add-route 附录中");
|
|
117
135
|
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Plan ↔ 兄弟制品(add-route / handoff / review)版本配对解析 —— 单一真源
|
|
3
|
+
*
|
|
4
|
+
* 动因(2026-09-18,farm-agent 多 v2 Plan 暴露):
|
|
5
|
+
* 旧实现按 planKeyword **去掉版本后缀取首个匹配** → 命中日期最早的 v1 add-route;
|
|
6
|
+
* 且 Plan 解析只要求文件名含 "-plan-v" → `...-plan-v2.hitl.md` 提案文件会顶替真正的 Plan。
|
|
7
|
+
* 后果:check_spec_sync 拿旧版路线图的附录比对当前 git diff,报出几十条「未登记」假告警;
|
|
8
|
+
* check_dps / check_add_route_status / check_add_route_completeness 同样读错文档。
|
|
9
|
+
* 同类问题第三例:git diff 路径被引号+八进制转义(`".codex/plans/...\346\236..."`),
|
|
10
|
+
* 既躲过 magic 前缀豁免、也与附录里的真实中文路径比不中 → 统一走 `-z`(NUL 分隔,永不加引号)。
|
|
11
|
+
*
|
|
12
|
+
* 解析优先级(可否证伪、不静默降级):
|
|
13
|
+
* Plan : 关键词命中且 `-plan-vN.md` **结尾**(`.hitl.md`/devlog/handoff/add-route/review 天然排除)
|
|
14
|
+
* → 版本号最大;同版本按路径降序(日期新者优先)
|
|
15
|
+
* 兜底:关键词命中、含 "plan"、非兄弟产物的 `.md`
|
|
16
|
+
* add-route : ① 同目录 + 同基名 + 同版本 ② 同基名最高版本(同版本优先 Plan 所在目录)
|
|
17
|
+
* ③ 关键词命中最高版本(基名与 Plan 不同时的兼容路径)
|
|
18
|
+
* 版本落后(add-route vN < Plan vM)→ 显式 warning,不静默
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type ArtifactKind = "plan" | "add-route" | "handoff" | "review";
|
|
22
|
+
|
|
23
|
+
export type ResolveVia =
|
|
24
|
+
| "plan-max-version"
|
|
25
|
+
| "plan-fallback"
|
|
26
|
+
| "paired-same-dir-version"
|
|
27
|
+
| "paired-base-max-version"
|
|
28
|
+
| "keyword-max-version";
|
|
29
|
+
|
|
30
|
+
export interface ResolvedArtifact {
|
|
31
|
+
/** 相对 plans 目录的路径(POSIX 分隔符) */
|
|
32
|
+
file: string;
|
|
33
|
+
kind: ArtifactKind;
|
|
34
|
+
/** 文件名解析出的版本号;无 `-vN` 后缀 = 0 */
|
|
35
|
+
version: number;
|
|
36
|
+
/** 去掉 `-<kind>-vN.md` 后的基名(review 的 `-review-implementation` 变体一并剥离) */
|
|
37
|
+
base: string;
|
|
38
|
+
/** 所在目录(相对,POSIX;根目录 = "") */
|
|
39
|
+
dir: string;
|
|
40
|
+
/** 解析依据(可审计,便于区分「配对命中」与「关键词兜底」) */
|
|
41
|
+
via: ResolveVia;
|
|
42
|
+
/** 与 Plan 版本不匹配时的显式告警(null = 无) */
|
|
43
|
+
warning: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResolveResult {
|
|
47
|
+
plan: ResolvedArtifact | null;
|
|
48
|
+
/** kind 非 plan 时:与已解析 Plan 配对的制品;解析不到 = null */
|
|
49
|
+
artifact: ResolvedArtifact | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const KIND_SUFFIX: Record<ArtifactKind, RegExp> = {
|
|
53
|
+
plan: /-plan-v(\d+)\.md$/i,
|
|
54
|
+
"add-route": /-add-route-v(\d+)\.md$/i,
|
|
55
|
+
handoff: /-handoff-v(\d+)\.md$/i,
|
|
56
|
+
review: /-review(?:-[a-z0-9-]+)?-v(\d+)\.md$/i,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Plan 解析必须排除的兄弟产物(提案/日志/契约/下游文档) */
|
|
60
|
+
const NON_PLAN_SUFFIX =
|
|
61
|
+
/(\.hitl\.md$)|(-add-route-v\d+\.md$)|(-handoff-v\d+\.md$)|(-review(?:-[a-z0-9-]+)?-v\d+\.md$)|(-contract-v\d+\.md$)|(^devlog-)/i;
|
|
62
|
+
|
|
63
|
+
export function toPosix(p: string): string {
|
|
64
|
+
return p.replace(/\\/g, "/");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function baseOf(p: string): string {
|
|
68
|
+
const posix = toPosix(p);
|
|
69
|
+
const i = posix.lastIndexOf("/");
|
|
70
|
+
return i < 0 ? posix : posix.slice(i + 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dirOf(p: string): string {
|
|
74
|
+
const posix = toPosix(p);
|
|
75
|
+
const i = posix.lastIndexOf("/");
|
|
76
|
+
return i < 0 ? "" : posix.slice(0, i);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 路径降序(日期新者优先,确定性;避免依赖 readdir 顺序) */
|
|
80
|
+
function byPathDesc(a: string, b: string): number {
|
|
81
|
+
return a < b ? 1 : a > b ? -1 : 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseArtifactName(
|
|
85
|
+
name: string,
|
|
86
|
+
kind: ArtifactKind,
|
|
87
|
+
): { base: string; version: number } | null {
|
|
88
|
+
const base = baseOf(name);
|
|
89
|
+
const m = base.match(KIND_SUFFIX[kind]);
|
|
90
|
+
if (!m || m.index === undefined) return null;
|
|
91
|
+
return { base: base.slice(0, m.index), version: parseInt(m[1], 10) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `git ... --name-only -z` 输出 → 路径数组。
|
|
96
|
+
* `-z` 下 git **永不**加引号/转义(非 ASCII 路径保持原样),因此这里只需切分。
|
|
97
|
+
* 无 NUL 时按行切分(兼容不支持的旧 git),并清掉 CR。
|
|
98
|
+
*/
|
|
99
|
+
export function splitGitPathList(stdout: string): string[] {
|
|
100
|
+
const parts = stdout.includes("\u0000") ? stdout.split("\u0000") : stdout.split("\n");
|
|
101
|
+
return parts
|
|
102
|
+
.map((p) => p.replace(/\r$/, ""))
|
|
103
|
+
.filter((p) => p !== "")
|
|
104
|
+
.map(toPosix);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** add-route / review 附录里登记的文件清单(反引号内的路径) */
|
|
108
|
+
export function extractAppendixFiles(content: string): string[] {
|
|
109
|
+
return (
|
|
110
|
+
content.match(/`[^`]+\.(ts|tsx|js|jsx|sh|sql|prisma|md|json|yml|yaml|toml)`/g) || []
|
|
111
|
+
).map((f) => f.replace(/`/g, ""));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function build(
|
|
115
|
+
file: string,
|
|
116
|
+
kind: ArtifactKind,
|
|
117
|
+
parsed: { base: string; version: number },
|
|
118
|
+
via: ResolveVia,
|
|
119
|
+
warning: string | null = null,
|
|
120
|
+
): ResolvedArtifact {
|
|
121
|
+
return {
|
|
122
|
+
file: toPosix(file),
|
|
123
|
+
kind,
|
|
124
|
+
version: parsed.version,
|
|
125
|
+
base: parsed.base,
|
|
126
|
+
dir: dirOf(file),
|
|
127
|
+
via,
|
|
128
|
+
warning,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 解析 Plan 文件:版本最高者优先;`.hitl.md` 等兄弟产物不入选 */
|
|
133
|
+
export function resolvePlan(
|
|
134
|
+
files: readonly string[],
|
|
135
|
+
keyword: string,
|
|
136
|
+
): ResolvedArtifact | null {
|
|
137
|
+
const kw = keyword.trim().toLowerCase();
|
|
138
|
+
if (!kw) return null;
|
|
139
|
+
const pool = files.map(toPosix).filter((f) => f.toLowerCase().includes(kw));
|
|
140
|
+
|
|
141
|
+
const versioned = pool
|
|
142
|
+
.map((f) => ({ f, parsed: parseArtifactName(f, "plan") }))
|
|
143
|
+
.filter(
|
|
144
|
+
(e): e is { f: string; parsed: { base: string; version: number } } =>
|
|
145
|
+
e.parsed !== null && !NON_PLAN_SUFFIX.test(baseOf(e.f)),
|
|
146
|
+
)
|
|
147
|
+
.sort((a, b) => b.parsed.version - a.parsed.version || byPathDesc(a.f, b.f));
|
|
148
|
+
if (versioned.length > 0) {
|
|
149
|
+
const { f, parsed } = versioned[0];
|
|
150
|
+
return build(f, "plan", parsed, "plan-max-version");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 兜底:无 `-plan-vN` 后缀的 Plan(精简版/历史命名)——仍需排除兄弟产物
|
|
154
|
+
const fallback = pool
|
|
155
|
+
.filter(
|
|
156
|
+
(f) =>
|
|
157
|
+
f.toLowerCase().endsWith(".md") &&
|
|
158
|
+
/plan/i.test(baseOf(f)) &&
|
|
159
|
+
!NON_PLAN_SUFFIX.test(baseOf(f)),
|
|
160
|
+
)
|
|
161
|
+
.sort(byPathDesc);
|
|
162
|
+
if (fallback.length > 0) {
|
|
163
|
+
return build(fallback[0], "plan", { base: baseOf(fallback[0]).replace(/\.md$/i, ""), version: 0 }, "plan-fallback");
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 与 Plan 配对的制品:同目录同版本 → 同基名最高版本 → 关键词最高版本 */
|
|
169
|
+
export function resolvePairedArtifact(
|
|
170
|
+
files: readonly string[],
|
|
171
|
+
plan: ResolvedArtifact,
|
|
172
|
+
kind: ArtifactKind,
|
|
173
|
+
keyword: string,
|
|
174
|
+
): ResolvedArtifact | null {
|
|
175
|
+
const posix = files.map(toPosix);
|
|
176
|
+
const suffix = `-${kind}-v${plan.version}.md`;
|
|
177
|
+
const expected = plan.dir ? `${plan.dir}/${plan.base}${suffix}` : `${plan.base}${suffix}`;
|
|
178
|
+
const exact = posix.find((f) => f.toLowerCase() === expected.toLowerCase());
|
|
179
|
+
if (exact) return build(exact, kind, { base: plan.base, version: plan.version }, "paired-same-dir-version");
|
|
180
|
+
|
|
181
|
+
const sameDirScore = (f: string) => (dirOf(f) === plan.dir ? 0 : 1);
|
|
182
|
+
const sameBase = posix
|
|
183
|
+
.map((f) => ({ f, parsed: parseArtifactName(f, kind) }))
|
|
184
|
+
.filter(
|
|
185
|
+
(e): e is { f: string; parsed: { base: string; version: number } } =>
|
|
186
|
+
e.parsed !== null && e.parsed.base.toLowerCase() === plan.base.toLowerCase(),
|
|
187
|
+
)
|
|
188
|
+
.sort(
|
|
189
|
+
(a, b) =>
|
|
190
|
+
b.parsed.version - a.parsed.version ||
|
|
191
|
+
sameDirScore(a.f) - sameDirScore(b.f) ||
|
|
192
|
+
byPathDesc(a.f, b.f),
|
|
193
|
+
);
|
|
194
|
+
if (sameBase.length > 0) {
|
|
195
|
+
const { f, parsed } = sameBase[0];
|
|
196
|
+
return build(f, kind, parsed, "paired-base-max-version", versionWarning(plan, parsed.version, kind));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const kw = keyword.trim().toLowerCase();
|
|
200
|
+
const byKeyword = posix
|
|
201
|
+
.filter((f) => f.toLowerCase().includes(kw))
|
|
202
|
+
.map((f) => ({ f, parsed: parseArtifactName(f, kind) }))
|
|
203
|
+
.filter(
|
|
204
|
+
(e): e is { f: string; parsed: { base: string; version: number } } => e.parsed !== null,
|
|
205
|
+
)
|
|
206
|
+
.sort((a, b) => b.parsed.version - a.parsed.version || byPathDesc(a.f, b.f));
|
|
207
|
+
if (byKeyword.length > 0) {
|
|
208
|
+
const { f, parsed } = byKeyword[0];
|
|
209
|
+
const warn = versionWarning(plan, parsed.version, kind);
|
|
210
|
+
const base = warn
|
|
211
|
+
? warn
|
|
212
|
+
: `基名不同:Plan 基名 "${plan.base}",${kind} 基名 "${parsed.base}"(按关键词兜底命中,请确认对应关系)`;
|
|
213
|
+
return build(f, kind, parsed, "keyword-max-version", base);
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function versionWarning(
|
|
219
|
+
plan: ResolvedArtifact,
|
|
220
|
+
artifactVersion: number,
|
|
221
|
+
kind: ArtifactKind,
|
|
222
|
+
): string | null {
|
|
223
|
+
if (plan.version > 0 && artifactVersion < plan.version) {
|
|
224
|
+
return `${kind} v${artifactVersion} 落后于 Plan v${plan.version}(Plan 基名 "${plan.base}")——补齐对应版本,或确认沿用旧版路线图`;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 工具入口:一次拿到「Plan + 配对制品」 */
|
|
230
|
+
export function resolvePlanArtifact(
|
|
231
|
+
files: readonly string[],
|
|
232
|
+
keyword: string,
|
|
233
|
+
kind: ArtifactKind,
|
|
234
|
+
): ResolveResult {
|
|
235
|
+
const plan = resolvePlan(files, keyword);
|
|
236
|
+
if (!plan) return { plan: null, artifact: null };
|
|
237
|
+
if (kind === "plan") return { plan, artifact: plan };
|
|
238
|
+
return { plan, artifact: resolvePairedArtifact(files, plan, kind, keyword) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 未登记文件的归属分摊:逐个读其它 add-route 的附录,命中即记为「属于该 Plan」。
|
|
243
|
+
* 全部命中即提前返回(不做无谓的全量读取)。顺序由调用方给定(建议新者优先)。
|
|
244
|
+
*/
|
|
245
|
+
export async function attributeToOtherPlans(
|
|
246
|
+
unmatched: readonly string[],
|
|
247
|
+
otherRoutes: readonly string[],
|
|
248
|
+
readRoute: (rel: string) => Promise<string | null>,
|
|
249
|
+
): Promise<{
|
|
250
|
+
/** key = 小写路径(比对用),value = 原名 + 归属 add-route */
|
|
251
|
+
owners: Map<string, { file: string; route: string }>;
|
|
252
|
+
/** 任何 add-route 都未登记的变更文件(原名) */
|
|
253
|
+
unowned: string[];
|
|
254
|
+
}> {
|
|
255
|
+
const owners = new Map<string, { file: string; route: string }>();
|
|
256
|
+
let remaining = unmatched.map((f) => ({ key: f.toLowerCase(), file: f }));
|
|
257
|
+
for (const route of otherRoutes) {
|
|
258
|
+
if (remaining.length === 0) break;
|
|
259
|
+
const content = await readRoute(route);
|
|
260
|
+
if (!content) continue;
|
|
261
|
+
const appendix = new Set(extractAppendixFiles(content).map((f) => f.toLowerCase()));
|
|
262
|
+
const stillRemaining: Array<{ key: string; file: string }> = [];
|
|
263
|
+
for (const e of remaining) {
|
|
264
|
+
if (appendix.has(e.key)) owners.set(e.key, { file: e.file, route });
|
|
265
|
+
else stillRemaining.push(e);
|
|
266
|
+
}
|
|
267
|
+
remaining = stillRemaining;
|
|
268
|
+
}
|
|
269
|
+
return { owners, unowned: remaining.map((e) => e.file) };
|
|
270
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
> - `[T]` = 编译期验证—证据: 命令+结果(如 `tsc=0` / `vitest 18/18`)
|
|
5
5
|
> - `[R]` = 运行时验证—证据: 部署后确认(如 `curl 200`)
|
|
6
6
|
> - `[E]` = 静态检查—证据: grep/diff 输出
|
|
7
|
+
> - `[W]` = **接线验证(Wiring)**—证据: 生产调用点存在的 grep / 端到端一次真实产出(**实现存在 ≠ 生产可达**)
|
|
7
8
|
>
|
|
8
9
|
> **审计链(证据→devlog→checklist)**:
|
|
9
10
|
> - 初验规则: 先找证据(命令+结果)→ 调 `record_dev_operation` 落库 → 将返回的真实 cuid(25位)写入 checklist。**禁止抄写 `cmq...` 占位符**。
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
- [ ] [T] 检查项1 — 证据: (待填写)|审计: (初验从 record_dev_operation 返回值写入真实 cuid,**禁止写 `cmq...` 占位符**)
|
|
15
16
|
- [ ] [T] 检查项2 — 证据: (待填写)|审计: (初验从 record_dev_operation 返回值写入真实 cuid,**禁止写 `cmq...` 占位符**)
|
|
16
17
|
- [ ] [T] schema 变更后运行实例 client 新鲜度(`db:generate` + 重启校验)— 证据: (待填写)|审计: (待填写) [来源: farm-agent 9737ab4 回灌;Prisma+Atlas 项目通用]
|
|
18
|
+
- [ ] [W] 新增的"被调用方"(策略/服务/工具/证据产生者)存在**生产调用点** — 证据: `grep -rn "<symbol>" src/ --include=*.ts` 有**非测试**命中且位于真实执行路径(非仅导出)| 审计: (待填写) [来源: farm-agent 2026-09-18 回流 P0 #R9;同类三例均为「纯函数+单测齐全、生产不可达」]
|
|
17
19
|
|
|
18
20
|
## ADD 规则合规检查
|
|
19
21
|
|
|
@@ -98,6 +98,24 @@
|
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
101
|
+
## 6.2 接线可达性核对(`[W]` 接线判据 · 2026-09-18 增补)
|
|
102
|
+
|
|
103
|
+
> **动因**:同类事故已发生**三次**(回流源:压缩通道 / 补向量 / 沉淀链),三次都是"**纯函数与单测齐全、生产不可达**",
|
|
104
|
+
> 而三项漂移指纹(范围扩散 / 类型错误 / 审计漏记)**全绿**——它们测的是"改得规不规矩",不是"能力是否可达"。
|
|
105
|
+
> 本节的判据是**可否证伪的 grep 与端到端产出**;"实现了 / 有单测 / 已导出"**不作为通过理由**。
|
|
106
|
+
> [来源: farm-agent 2026-09-18 回流 P0 #R9;域符号名已泛化为占位符]
|
|
107
|
+
|
|
108
|
+
对本次变更涉及的每个"被调用方"(策略 / 服务 / 工具 / 证据产生者 / 审计字面量)逐项核对:
|
|
109
|
+
|
|
110
|
+
| # | 被调用方(symbol) | 期望调用点 | grep 证据(非测试命中) | 端到端产出证据 | 判定 |
|
|
111
|
+
|---|-------------------|-----------|------------------------|---------------|:----:|
|
|
112
|
+
| 1 | {symbol} | {文件:行} | `grep -rn "<symbol>" src/ --include=*.ts` 命中数 = {N} | {DB 行数 / SSE 帧 / 审计记录} | ✅/❌ |
|
|
113
|
+
|
|
114
|
+
- [ ] 每个新增 symbol 有**非测试**调用点(**仅导出 / 仅被测试 import = 未接线 ⇒ 判 ❌**)
|
|
115
|
+
- [ ] 涉及审计字面量时:该字面量在本次运行时**至少被写出过一次**(`query_audit_logs` 可查,而非"代码里有这行")
|
|
116
|
+
- [ ] 端到端产出可证(落库行数 / 帧 / 文件产物),而非"单测断言通过"
|
|
117
|
+
- [ ] 本节表格已填;若本次变更**不含**新增被调用方,须显式写"本变更无新增被调用方",不得静默跳过
|
|
118
|
+
|
|
101
119
|
## 7. 关联 Checklist
|
|
102
120
|
|
|
103
121
|
- 本 review 的检查项与 `{checklist 文档路径}` 的"跨项目联调检查"章节一一对应
|
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
"heading": "## 6. E2E 逐端点 curl",
|
|
43
43
|
"required": true
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
"id": "wiring",
|
|
47
|
+
"heading": "## 6.2 接线可达性核对",
|
|
48
|
+
"required": true,
|
|
49
|
+
"note": "2026-09-18 增补(回流 farm-agent P0 #R9):逐项核对新增被调用方的生产调用点与端到端产出([W] 判据)。三次同类事故(纯函数与单测齐全、生产不可达)均在此节可被拦截。"
|
|
50
|
+
},
|
|
45
51
|
{
|
|
46
52
|
"id": "checklist",
|
|
47
53
|
"heading": "## 7. 关联 Checklist",
|
|
@@ -61,4 +67,4 @@
|
|
|
61
67
|
"阶段",
|
|
62
68
|
"步骤"
|
|
63
69
|
]
|
|
64
|
-
}
|
|
70
|
+
}
|