add-coder 0.3.37 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-coder",
3
- "version": "0.3.37",
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": {
@@ -112,9 +112,13 @@
112
112
  "lint": "eslint src/",
113
113
  "lint:fix": "eslint src/ --fix",
114
114
  "generate": "tsx src/caijuehub/transcribe/index.ts",
115
+ "contributors": "tsx scripts/gen-contributors.ts --write",
116
+ "contributors:check": "tsx scripts/gen-contributors.ts --check",
117
+ "contributors:audit": "tsx scripts/gen-contributors.ts --check --audit",
115
118
  "podman-add-coder-up": "podman-compose --env-file .env.development -f podman-compose.add.yml up -d",
116
119
  "podman-add-coder-down": "podman-compose --env-file .env.development -f podman-compose.add.yml down",
117
120
  "test": "vitest run",
118
- "generate:check": "tsx scripts/generate-idempotency-check.ts"
121
+ "generate:check": "tsx scripts/generate-idempotency-check.ts",
122
+ "release:preflight": "tsx scripts/release-preflight.ts"
119
123
  }
120
124
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_version": "0.3.37",
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",
@@ -229,7 +229,7 @@
229
229
  "core/rules/profiles/webapp-profile.md": "e493ae7b",
230
230
  "core/rules/project_rules.md": "094821ad",
231
231
  "core/rules/theory-practice-map.toml": "b1db99fd",
232
- "core/scripts/db-ensure.sh": "8f7ba569",
232
+ "core/scripts/db-ensure.sh": "d46a43c1",
233
233
  "core/scripts/gen-plan-index.sh": "47836bec",
234
234
  "core/scripts/mcp-server/elicitation/confirm.ts": "924857b8",
235
235
  "core/scripts/mcp-server/elicitation/index.ts": "44c5dc3f",
@@ -280,7 +280,7 @@
280
280
  "core/scripts/mcp-server/shared/memory/metrics/stage-words.ts": "a81db64b",
281
281
  "core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts": "ea83c131",
282
282
  "core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts": "1275eef4",
283
- "core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql": "8fade96f",
283
+ "core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql": "1a439c1a",
284
284
  "core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts": "09a6cf59",
285
285
  "core/scripts/mcp-server/shared/memory/retrieval/fusion.ts": "c3aa6a4f",
286
286
  "core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts": "dbe4a472",
@@ -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": "b58e76ee",
323
- "core/scripts/mcp-server/tools/gateway/check_add_route_status.ts": "4caf552c",
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": "f52ef32c",
326
- "core/scripts/mcp-server/tools/gateway/check_rahs.ts": "88d2c549",
327
- "core/scripts/mcp-server/tools/gateway/check_spec_sync.ts": "75c0d1e3",
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": "c1ea7aaf",
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": "6d7a46cb",
378
- "core/templates/review-implementation-template.schema.json": "d97b9eb6",
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",
@@ -37,8 +37,24 @@ EOF
37
37
  echo ">>> 已创建 .env.development"
38
38
  fi
39
39
 
40
- # ── SQLite:无需容器 ──
41
- if [ "$ENGINE" = "sqlite" ]; then exit 0; fi
40
+ # ── SQLite:无需容器;但 FTS5 原生层不在 Prisma schema 内,必须显式应用期望态 ──
41
+ # 背景(Plan add-coder-sqlite-memory-flow §3.2):`prisma db push` 只建 Prisma 表,
42
+ # `add_memory_fts` 虚表 + 3 个同步触发器是原生 DDL → 此前 sqlite 项目 recall_memory 直接
43
+ # `no such table`。此处与 init.ts(src/lib/memory-expected-state.ts)消费**同一份** SQL 真源。
44
+ # 失败语义:仅告警不阻断(exit 0 语义不变)——记忆检索是能力项,不该让环境准备硬失败。
45
+ if [ "$ENGINE" = "sqlite" ]; then
46
+ MAGIC_DIR="${MAGIC_DIR:-.codex}"
47
+ FTS_SQL="$MAGIC_DIR/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql"
48
+ if [ -f "$PROJECT_DIR/$FTS_SQL" ]; then
49
+ echo ">>> 应用记忆 FTS5 期望态($FTS_SQL)..."
50
+ # Prisma 7:db execute 的 datasource 由项目 prisma.config.ts 提供,`--schema` 已移除(带=unknown option)
51
+ (cd "$PROJECT_DIR" && npm exec prisma -- db execute --file "$PROJECT_DIR/$FTS_SQL") \
52
+ || echo "!!! 记忆 FTS5 期望态应用失败(不阻断):recall_memory 将按 fts-only 降级;修复入口 add-coder memory:reindex --apply"
53
+ else
54
+ echo ">>> 未找到 $FTS_SQL(跳过,不阻断)——请先执行 add-coder sync"
55
+ fi
56
+ exit 0
57
+ fi
42
58
 
43
59
  # ── 自行管理 PostgreSQL ──
44
60
  if [ "$CONTAINER" = "none" ] || [ "$CONTAINER" = "manual" ]; then
@@ -1,3 +1,6 @@
1
+ -- 本文件由 scripts/memory/gen-sqlite-fts-sql.ts 生成(真源:src/lib/memory-fts-objects.ts 的 SQLITE_FTS_OBJECTS)
2
+ -- 请勿手工编辑;新增/修改 DDL 请改真源后重跑生成脚本。
3
+ --
1
4
  -- Agent Memory FTS 原生层(SQLite 后端)
2
5
  -- Plan §8.3 + §17-3 定案:FTS5 trigram 分词器(CJK 友好,≥3 字符 n-gram 匹配;
3
6
  -- 短于 3 字符的查询由 adapter 层回退 LIKE —— 见 retrieval/fts/sqlite.ts)
@@ -6,24 +9,10 @@
6
9
  -- (其要求 INTEGER rowid),故采用独立 FTS 表 + 触发器同步。
7
10
  -- 幂等:全部 IF NOT EXISTS,可重复应用。
8
11
 
9
- CREATE VIRTUAL TABLE IF NOT EXISTS add_memory_fts USING fts5(
10
- memory_id UNINDEXED,
11
- topic,
12
- content,
13
- tokenize = 'trigram'
14
- );
12
+ CREATE VIRTUAL TABLE IF NOT EXISTS add_memory_fts USING fts5(memory_id UNINDEXED, topic, content, tokenize = 'trigram');
15
13
 
16
- CREATE TRIGGER IF NOT EXISTS add_memory_fts_ai AFTER INSERT ON "AddMemory" BEGIN
17
- INSERT INTO add_memory_fts(memory_id, topic, content)
18
- VALUES (new.id, new.topic, new.content);
19
- END;
14
+ CREATE TRIGGER IF NOT EXISTS add_memory_fts_ai AFTER INSERT ON "AddMemory" BEGIN INSERT INTO add_memory_fts(memory_id, topic, content) VALUES (new.id, new.topic, new.content); END;
20
15
 
21
- CREATE TRIGGER IF NOT EXISTS add_memory_fts_au AFTER UPDATE ON "AddMemory" BEGIN
22
- DELETE FROM add_memory_fts WHERE memory_id = old.id;
23
- INSERT INTO add_memory_fts(memory_id, topic, content)
24
- VALUES (new.id, new.topic, new.content);
25
- END;
16
+ CREATE TRIGGER IF NOT EXISTS add_memory_fts_au AFTER UPDATE ON "AddMemory" BEGIN DELETE FROM add_memory_fts WHERE memory_id = old.id; INSERT INTO add_memory_fts(memory_id, topic, content) VALUES (new.id, new.topic, new.content); END;
26
17
 
27
- CREATE TRIGGER IF NOT EXISTS add_memory_fts_ad AFTER DELETE ON "AddMemory" BEGIN
28
- DELETE FROM add_memory_fts WHERE memory_id = old.id;
29
- END;
18
+ CREATE TRIGGER IF NOT EXISTS add_memory_fts_ad AFTER DELETE ON "AddMemory" BEGIN DELETE FROM add_memory_fts WHERE memory_id = old.id; END;
@@ -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
- const arFile = allFiles.find(
40
- (f) =>
41
- f.toLowerCase().includes(pp.toLowerCase()) &&
42
- f.includes("add-route"),
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
- // 多版本(-plan-vN)共存时取版本号最高者:活跃 Plan 优先,避免评分旧版(2026-08-12 修复)
72
- const matched = apf.filter(
73
- (f) =>
74
- f.toLowerCase().includes(pp.toLowerCase()) && f.includes("-plan-v"),
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 (!matched.length)
79
+ if (!resolvedPlan)
77
80
  return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
78
- const pm = matched.sort((a, b) => {
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(`Plan: ${pm}`);
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
- const rkNoVersion = pp.replace(/-plan-v\d+$/i, "");
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 = apf.find(
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
- const pm = apf.find(
54
- (f) =>
55
- f.toLowerCase().includes(pp.toLowerCase()) &&
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,27 +144,23 @@ export function registerCheckRahs(server: ToolRegistrar) {
142
144
  } catch { /* checklist progress unavailable */ }
143
145
  try {
144
146
  // 范围保真度:git diff 变更文件与 add-route 附录清单匹配率
145
- const arFile = apf.find(
146
- (f) =>
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
- arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) ||
155
- []
156
- ).map((f: string) => f.replace(/`/g, "").toLowerCase());
157
- const diff = runCommand("git", ["diff", "--name-only"], {
158
- cwd: PROJECT_ROOT,
159
- timeout: 5000,
160
- });
161
- const diffFiles = diff.stdout
162
- .trim()
163
- .split("\n")
164
- .filter(Boolean)
165
- .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
+ );
166
164
  if (appendix.length > 0 && diffFiles.length > 0) {
167
165
  const matched = diffFiles.filter((f) =>
168
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,51 +51,40 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
44
51
  f.endsWith(".md"),
45
52
  );
46
53
  const kw = args.planKeyword as string;
47
- // 多版本(-plan-vN)共存时取版本号最高者:活跃 Plan 优先,避免评分旧版(2026-08-12 修复,同 check_dps R9)
48
- let planMatch: string | undefined = planFiles
49
- .filter(
50
- (f) =>
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: ${planMatch}`);
59
+ lines.push(`Plan: ${plan.file}${plan.version > 0 ? ` (v${plan.version})` : ""}`);
65
60
 
66
- // 定位 add-route 文件
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(`add-route: ${arFile}`);
78
- const arContent = (await readFileSafe(join(plansDir, arFile))) || "";
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
- const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml|toml)`/g) || [])
81
- .map((f: string) => f.replace(/`/g, ""));
71
+ // sql/prisma 纳入(2026-09-16 修复):原生 DDL 与 schema 是常见交付物,此前不在白名单 →
72
+ // 附录里明明登记了 `.../sqlite-fts5.sql` 仍被判"不在附录中"(假告警)。
73
+ const appendixFiles = extractAppendixFiles(arContent);
82
74
  lines.push(`附录文件: ${appendixFiles.length} 个`);
83
75
 
84
76
  // git diff 变更文件(win32 下 git 为 .cmd → runCommand 自动解析,issue #10 跨端修复)
85
77
  let diffFiles: string[] = [];
86
78
  try {
87
- const diff = runCommand("git", ["diff", "--name-only"], {
88
- cwd: PROJECT_ROOT,
89
- timeout: 5000,
90
- });
91
- diffFiles = diff.stdout.trim().split("\n").filter(Boolean);
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);
92
88
  } catch {
93
89
  lines.push("Git diff: 无法获取");
94
90
  }
@@ -106,8 +102,34 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
106
102
  !appendixSet.has(f.toLowerCase()),
107
103
  );
108
104
  if (unmatched.length > 0) {
109
- lines.push(`⚠️ ${unmatched.length} 个文件在 git diff 中但不在 add-route 附录中:`);
110
- unmatched.forEach((f: string) => lines.push(` - ${f}`));
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
+ }
111
133
  } else {
112
134
  lines.push("✅ git diff 变更文件全部在 add-route 附录中");
113
135
  }