kcode-pi 0.1.16 → 0.1.17

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 CHANGED
@@ -403,9 +403,44 @@ ship 汇总变更、验证证据、风险和后续事项
403
403
  - 写生产源码前必须已有 `evidence/tdd-red.md`,内容可以是 API/基类/方法签名检查、元数据检查、编译检查、既有测试框架或外部接口最小验证的失败输出。
404
404
  - 进入 `execute` 后,只允许写入 `PLAN.md` 明确列出的源码文件;如果临时发现要改新文件,必须先回到 plan 更新 `PLAN.md`。
405
405
  - 进入 `verify` 前,`EXECUTION.md` 必须逐个完成 `PLAN.md` 中的所有 `STEP-###`,并为每个步骤记录真实存在的 `evidence/...` 文件;同时必须已有 `evidence/tdd-red.md` 和 `evidence/tdd-green.md`。
406
+ - `evidence/tdd-green.md` 必须包含真实成功输出和 `Exit: 0` 或 `退出码:0`;不能写“需在开发环境验证”“待验证”“未执行”等不确定结论。
407
+ - 如果门禁提示 evidence 内容无效,不要反复改文案或补关键词;必须重新运行 `PLAN.md` 中声明的真实验证命令,并记录命令、Exit、STDOUT/STDERR 或工具输出。
406
408
  - 旗舰版项目必须先检查当前项目结构。若存在 `code/`,跟随 `code/` 下的实际组织;若不存在,必须在 `PLAN.md` 记录实际源码根或目标文件。
407
409
  - 不允许凭空写 demo、sample、scaffold,或在不了解项目结构时新建猜测目录。
408
410
 
411
+ ### SDK 签名门禁怎么用
412
+
413
+ 当需求涉及 Java/C# 插件代码时,KCode 会强制要求先查证当前项目真实 SDK 的类、方法、构造器、枚举或属性签名。这个门禁是为了避免 LLM 根据记忆编出不存在的方法。
414
+
415
+ 推荐操作顺序:
416
+
417
+ ```text
418
+ 1. discuss/spec 阶段确认需求、产品、插件类型和目标单据。
419
+ 2. plan 阶段检查当前项目源码结构、构建文件和 SDK 依赖位置。
420
+ 3. 用 kd_sdk_signature 查证要调用的 SDK 类型和方法。
421
+ 4. 确认生成 .pi/kd/runs/<run-id>/evidence/sdk-signature.md。
422
+ 5. 记录 evidence/tdd-red.md。
423
+ 6. 进入 execute 后再写 PLAN.md 中列出的生产源码。
424
+ 7. 写完后记录 evidence/tdd-green.md,再进入 verify。
425
+ ```
426
+
427
+ 示例:
428
+
429
+ ```text
430
+ kd_sdk_signature product=flagship query=SaveServiceHelper method=save
431
+ kd_sdk_signature product=flagship query=OperationServiceHelper method=executeOperate
432
+ kd_sdk_signature product=enterprise query=DynamicObject method=GetDynamicObject
433
+ ```
434
+
435
+ 如果 `kd_sdk_signature` 找不到结果,不要让 Agent 直接写代码。先检查:
436
+
437
+ - 当前是否在业务项目根目录启动 `kcode start`。
438
+ - 项目是否能看到 SDK jar/dll,例如 `lib/`、`libs/`、`bin/`、`WebSite/bin/` 或构建缓存。
439
+ - `PLAN.md` 是否记录了真实源码根和 SDK 依赖位置。
440
+ - 是否可以通过当前项目已有封装类、编译输出或官方元数据反查签名。
441
+
442
+ 只有 `kd_sdk_signature`、当前项目构建输出、项目源码封装或官方元数据能作为签名事实。`kd_search`、`kd_cosmic_api` 和 README 里的示例只用于找线索。
443
+
409
444
  示例计划步骤:
410
445
 
411
446
  ```markdown
@@ -542,6 +577,34 @@ npm view kcode-pi version
542
577
  npm install -g kcode-pi@latest
543
578
  ```
544
579
 
580
+ 如果 `npm install -g` 没有更新到新版本,或提示 `EEXIST: file already exists ... kcode.cmd`,通常是全局命令 shim 或旧包目录没有被 npm 清干净。按下面顺序处理:
581
+
582
+ ```powershell
583
+ npm uninstall -g kcode-pi
584
+ npm install -g kcode-pi@latest
585
+ kcode version
586
+ ```
587
+
588
+ 如果仍提示 `kcode.cmd` 已存在,先确认它属于旧的 `kcode-pi` 全局安装:
589
+
590
+ ```powershell
591
+ npm root -g
592
+ where kcode
593
+ ```
594
+
595
+ 删除 `where kcode` 指向的旧 `kcode.cmd`、`kcode.ps1` 后再重装:
596
+
597
+ ```powershell
598
+ npm install -g kcode-pi@latest
599
+ ```
600
+
601
+ 如果 npm 仍使用缓存或企业镜像没有同步新版本,先确认 registry 上真实可安装版本:
602
+
603
+ ```powershell
604
+ npm view kcode-pi version
605
+ npm view kcode-pi versions --json
606
+ ```
607
+
545
608
  升级后建议在业务项目根目录重新执行:
546
609
 
547
610
  ```powershell
@@ -129,6 +129,7 @@ function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof re
129
129
  "路径规则:在 Windows 工作区内,优先使用项目相对路径;如需绝对路径必须使用 `D:\\...` 这类 Windows 路径,禁止把路径改写成 `/mnt/d/...`、`/d/...` 等 WSL/MSYS 风格路径。",
130
130
  "execute 阶段只能写 PLAN.md 明确列出的源码文件;如果目标文件不在计划内,必须先回到 plan 更新 PLAN.md。",
131
131
  "写生产源码前必须先有红灯证据 evidence/tdd-red.md;Java/C# 还必须有 SDK 签名证据 evidence/sdk-signature.md。红绿证据可以是 kd_sdk_signature 本地 SDK 签名、API/基类/方法签名、元数据、编译、既有测试框架或外部接口最小验证,不要为了测试引入额外 jar。",
132
+ "如果门禁提示 evidence 内容无效,不要通过改写结论、补关键词或反复读取文件来过关;必须重新运行 PLAN.md 中声明的真实验证命令,并记录命令、Exit、STDOUT/STDERR 或工具输出。",
132
133
  ].join("\n");
133
134
  }
134
135
 
@@ -154,11 +155,11 @@ function codeWriteBlockReason(cwd: string, path: string | undefined): string | u
154
155
 
155
156
  const run = readActiveRun(cwd);
156
157
  if (!run) {
157
- return "KCode 工作流未启动,不能直接写产品代码。请先用自然语言说明需求,KCode 会进入 discuss,或手动运行 /kd-start <需求>。";
158
+ return "KCode 工作流未启动,不能直接写产品代码。下一步:先用自然语言说明需求让 KCode 自动进入 discuss,或手动运行 /kd-start <需求> 创建 run;完成 discuss -> spec -> plan -> execute 后再写生产源码。";
158
159
  }
159
160
 
160
161
  if (run.phase !== "execute") {
161
- return `当前 KCode 阶段是 ${run.phase},不能写产品代码。请先完成 discuss -> spec -> plan 并进入 execute。`;
162
+ return `当前 KCode 阶段是 ${run.phase},不能写产品代码。下一步:先完成当前阶段文档和门禁,用 /kd-advance 推进到 execute;如果发现计划不完整,更新 PLAN.md,而不是直接写代码。`;
162
163
  }
163
164
 
164
165
  return (
@@ -333,7 +334,7 @@ export default function (pi: ExtensionAPI) {
333
334
  const path = typeof input.path === "string" ? input.path : undefined;
334
335
  const hint = path ? windowsPathHint(path) : undefined;
335
336
  if (hint && ["read", "write", "edit"].includes(event.toolName)) {
336
- const reason = `当前是 Windows 工作区,不能使用 WSL/MSYS 路径 ${path}。请改用项目相对路径,或使用 Windows 路径 ${hint}。`;
337
+ const reason = `当前是 Windows 工作区,不能使用 WSL/MSYS 路径 ${path}。下一步:改用项目相对路径;如果必须使用绝对路径,使用 Windows 路径 ${hint};不要再尝试 /mnt/d 或 /d 路径。`;
337
338
  if (ctx.hasUI) ctx.ui.notify(reason, "warning");
338
339
  return { block: true, reason };
339
340
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "面向金蝶开发的 Pi Coding Agent 启动器、工具包和 Harness 工作流",
5
5
  "type": "module",
6
6
  "private": false,
@@ -36,7 +36,7 @@ export function inspectGate(cwd: string, run: ActiveRun): GateResult {
36
36
  const evidenceProblem = inspectEvidence(cwd, run, run.phase);
37
37
  const questionProblem = inspectOpenQuestions(run);
38
38
  const reasonParts = [];
39
- if (missing.length > 0) reasonParts.push(`缺少必需产物:${[...new Set(missing)].join(", ")}`);
39
+ if (missing.length > 0) reasonParts.push(missingArtifactsReason([...new Set(missing)]));
40
40
  if (markerProblem) reasonParts.push(markerProblem);
41
41
  if (stepProblem) reasonParts.push(stepProblem);
42
42
  if (evidenceProblem) reasonParts.push(evidenceProblem);
@@ -55,7 +55,9 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
55
55
  const reasonParts: string[] = [];
56
56
 
57
57
  if (target !== "discuss" && !isKnownProduct(run.profile?.product ?? run.product)) {
58
- reasonParts.push("不能离开 discuss:产品画像未知。请使用 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>。");
58
+ reasonParts.push(
59
+ "不能离开 discuss:产品画像未知。下一步:根据需求或用户回答确认产品,然后执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>;如果无法判断,先用 kd_question 只问一个最阻塞的产品确认问题。",
60
+ );
59
61
  }
60
62
 
61
63
  for (let i = 0; i < targetIndex; i++) {
@@ -89,7 +91,7 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
89
91
  missing.push(PHASE_ARTIFACTS.verify);
90
92
  }
91
93
 
92
- if (missing.length > 0) reasonParts.push(`不能进入 ${target}:缺少 ${[...new Set(missing)].join(", ")}`);
94
+ if (missing.length > 0) reasonParts.push(missingForTargetReason(target, [...new Set(missing)]));
93
95
  const reason = reasonParts.length > 0 ? reasonParts.join("; ") : undefined;
94
96
  return {
95
97
  passed: !reason,
@@ -101,7 +103,10 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
101
103
  function inspectOpenQuestions(run: ActiveRun): string | undefined {
102
104
  const open = (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
103
105
  if (open.length === 0) return undefined;
104
- return `存在未回答的阻断问题:${open.map((question) => `${question.id} ${question.question}`).join(";")}`;
106
+ return [
107
+ `存在未回答的阻断问题:${open.map((question) => `${question.id} ${question.question}`).join(";")}`,
108
+ "下一步:先向用户等待或获取答案,然后用 kd_question action=answer id=<问题编号> answer=<用户答案> 记录;不要绕过问题推进阶段。",
109
+ ].join("。");
105
110
  }
106
111
 
107
112
  function inspectStepState(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
@@ -127,13 +132,13 @@ function inspectMarkers(cwd: string, run: ActiveRun, phase: KdPhase): string | u
127
132
 
128
133
  const missing = markers.filter((marker) => !content.includes(marker));
129
134
  if (missing.length === 0) return undefined;
130
- return `${PHASE_ARTIFACTS[phase]} 缺少必需章节:${missing.join(", ")}`;
135
+ return `${PHASE_ARTIFACTS[phase]} 缺少必需章节:${missing.join(", ")}。下一步:更新 ${PHASE_ARTIFACTS[phase]},补齐这些章节并写入真实内容;不要只添加空标题。`;
131
136
  }
132
137
 
133
138
  function inspectEvidence(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
134
139
  const missing = missingEvidenceForPhase(cwd, run, phase);
135
140
  if (missing.length === 0) return undefined;
136
- return `缺少必需证据:${missing.join(", ")}`;
141
+ return missingEvidenceReason(missing);
137
142
  }
138
143
 
139
144
  function isCosmicRun(run: ActiveRun): boolean {
@@ -170,6 +175,51 @@ function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string
170
175
  return existsSync(join(runRoot(cwd, run), artifact));
171
176
  }
172
177
 
178
+ function missingArtifactsReason(artifacts: string[]): string {
179
+ return [
180
+ `缺少必需产物:${artifacts.join(", ")}`,
181
+ `下一步:使用 /kd-artifact <阶段> 创建或更新阶段文档,或按当前阶段要求补写 ${artifacts.join(", ")} 的真实内容后再刷新门禁。`,
182
+ ].join("。");
183
+ }
184
+
185
+ function missingForTargetReason(target: KdPhase, artifacts: string[]): string {
186
+ const evidence = artifacts.filter((artifact) => artifact.startsWith("evidence/"));
187
+ const documents = artifacts.filter((artifact) => !artifact.startsWith("evidence/"));
188
+ const actions: string[] = [];
189
+ if (documents.length > 0) {
190
+ actions.push(`先补齐阶段文档 ${documents.join(", ")},可用 /kd-artifact 创建模板后填入真实分析、计划或验证内容`);
191
+ }
192
+ if (evidence.length > 0) {
193
+ actions.push(evidenceAction(evidence));
194
+ }
195
+ return `不能进入 ${target}:缺少 ${artifacts.join(", ")}。下一步:${actions.join(";")}。`;
196
+ }
197
+
198
+ function missingEvidenceReason(artifacts: string[]): string {
199
+ return `缺少必需证据:${artifacts.join(", ")}。下一步:${evidenceAction(artifacts)}。`;
200
+ }
201
+
202
+ function evidenceAction(artifacts: string[]): string {
203
+ return artifacts.map(evidenceArtifactAction).join(";");
204
+ }
205
+
206
+ function evidenceArtifactAction(artifact: string): string {
207
+ switch (artifact) {
208
+ case SDK_SIGNATURE_EVIDENCE:
209
+ return "运行 kd_sdk_signature,从当前项目真实 SDK jar/dll 查证类、方法、构造器或属性签名,成功后自动写入 evidence/sdk-signature.md";
210
+ case COSMIC_CONFIG_EVIDENCE:
211
+ return "运行 kd_cosmic_config 生成 evidence/cosmic-config.txt;如果项目没有 ok-cosmic.json,先使用 KCode 默认配置,不要手写假结果";
212
+ case COSMIC_METADATA_EVIDENCE:
213
+ return "运行 kd_cosmic_metadata 查询目标表单/单据/字段元数据并生成 evidence/cosmic-metadata.json";
214
+ case COSMIC_API_EVIDENCE:
215
+ return "运行 kd_cosmic_api 查询相关 Cosmic API 线索并生成 evidence/cosmic-api.txt,再用 kd_sdk_signature 或构建输出确认签名";
216
+ case KSQL_LINT_EVIDENCE:
217
+ return "运行 kd_ksql_lint 校验 KSQL/SQL 交付内容并生成 evidence/ksql-lint.txt";
218
+ default:
219
+ return `运行 PLAN.md 中声明的验证命令,记录命令、Exit、STDOUT/STDERR 或工具输出到 ${artifact}`;
220
+ }
221
+ }
222
+
173
223
  function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
174
224
  const plan = readArtifact(cwd, run, "plan") ?? "";
175
225
  return /kd_cosmic_metadata|cosmic-metadata|cosmic-metadata\.json|metadata evidence|字段元数据证据|元数据证据/i.test(plan);
@@ -22,7 +22,9 @@ export function flagshipWriteBlockReason(run: ActiveRun | undefined, path: strin
22
22
  const normalized = normalizeRelativePath(cwd && isAbsolute(path) ? relative(cwd, path) : path);
23
23
  if (normalized.startsWith(".pi/")) return undefined;
24
24
  if (cwd && !hasWorkspaceCodeDir(cwd)) return undefined;
25
- if (!normalized.startsWith("code/")) return `星空旗舰版代码必须跟随当前项目结构写入 code/ 下,不能写到 ${path}`;
25
+ if (!normalized.startsWith("code/")) {
26
+ return `星空旗舰版代码必须跟随当前项目结构写入 code/ 下,不能写到 ${path}。下一步:先读取当前项目 code/ 下的真实模块结构,在 PLAN.md 记录目标源码路径,再把写入路径改为 code/... 下的项目相对路径。`;
27
+ }
26
28
 
27
29
  return undefined;
28
30
  }
@@ -35,7 +37,7 @@ export function planWriteBlockReason(cwd: string, run: ActiveRun | undefined, pa
35
37
  if (normalized.startsWith(".pi/")) return undefined;
36
38
  if (planMentionsPath(plan, normalized)) return undefined;
37
39
 
38
- return `PLAN.md 未批准写入 ${normalized}。请先回到 plan 阶段更新 PLAN.md,明确列出该目标文件后再执行。`;
40
+ return `PLAN.md 未批准写入 ${normalized}。下一步:停止写入该文件,回到 plan 阶段检查项目结构和影响范围,把该文件加入 PLAN.md 的 ## 允许修改的文件 和执行步骤;重新通过门禁后再写。`;
39
41
  }
40
42
 
41
43
  export function flagshipPlanBlockReason(cwd: string, run: ActiveRun | undefined, plan: string): string | undefined {
@@ -43,11 +45,11 @@ export function flagshipPlanBlockReason(cwd: string, run: ActiveRun | undefined,
43
45
 
44
46
  if (hasWorkspaceCodeDir(cwd)) {
45
47
  if (/(?:^|[\s`"'(])code[\\/][^\s`"')]+/i.test(plan)) return undefined;
46
- return "不能进入 execute:星空旗舰版 PLAN.md 必须先记录当前项目 code/ 下的实际目标路径;不要按固定模块规则猜路径。";
48
+ return "不能进入 execute:星空旗舰版 PLAN.md 必须先记录当前项目 code/ 下的实际目标路径;不要按固定模块规则猜路径。下一步:列出 code/ 下模块,识别当前项目是按云、按应用还是不分模块组织,在 PLAN.md 写明真实目标文件。";
47
49
  }
48
50
 
49
51
  if (planMentionsDiscoveredSourcePath(plan)) return undefined;
50
- return "不能进入 execute:PLAN.md 必须先记录已检查当前项目结构,并写明实际源码根或目标文件路径;当前项目没有 code/ 时更不能猜路径。";
52
+ return "不能进入 execute:PLAN.md 必须先记录已检查当前项目结构,并写明实际源码根或目标文件路径;当前项目没有 code/ 时更不能猜路径。下一步:读取构建文件和 src/lib/bin 等目录,确认源码根后写入 PLAN.md 的 ## 已检查的项目结构、## 目标源码根 / 路径 和 ## 允许修改的文件。";
51
53
  }
52
54
 
53
55
  function hasWorkspaceCodeDir(cwd: string): boolean {
@@ -21,16 +21,16 @@ export function parsePlanSteps(plan: string): PlanStep[] {
21
21
 
22
22
  export function planStepsBlockReason(plan: string): string | undefined {
23
23
  if (!/##\s*执行步骤/i.test(plan)) {
24
- return "PLAN.md 缺少 ## 执行步骤。必须把计划拆成 STEP-001 这种可跟踪步骤。";
24
+ return "PLAN.md 缺少 ## 执行步骤。下一步:回到 plan,补充 `## 执行步骤`,把工作拆成 `- [ ] STEP-001:...` 这种可跟踪步骤;每一步都应能产生代码变更或 evidence。";
25
25
  }
26
26
 
27
27
  const steps = parsePlanSteps(plan);
28
28
  if (steps.length === 0) {
29
- return "PLAN.md 没有可执行步骤。请使用 `- [ ] STEP-001:...` 列出步骤。";
29
+ return "PLAN.md 没有可执行步骤。下一步:在 `## 执行步骤` 下使用 `- [ ] STEP-001:...` 列出步骤;不要用普通段落代替可勾选步骤。";
30
30
  }
31
31
 
32
32
  const duplicate = firstDuplicate(steps.map((step) => step.id));
33
- if (duplicate) return `PLAN.md 存在重复步骤编号:${duplicate}`;
33
+ if (duplicate) return `PLAN.md 存在重复步骤编号:${duplicate}。下一步:重新编号执行步骤,确保 STEP-001、STEP-002 等编号唯一且顺序稳定。`;
34
34
  return undefined;
35
35
  }
36
36
 
@@ -54,8 +54,12 @@ export function executionStepsBlockReason(cwd: string, run: ActiveRun, plan: str
54
54
  }
55
55
  }
56
56
 
57
- if (missing.length > 0) return `不能进入 verify:EXECUTION.md 未完成计划步骤 ${missing.join(", ")}。`;
58
- if (missingEvidence.length > 0) return `不能进入 verify:步骤 ${missingEvidence.join(", ")} 缺少已落地的 evidence 文件。`;
57
+ if (missing.length > 0) {
58
+ return `不能进入 verify:EXECUTION.md 未完成计划步骤 ${missing.join(", ")}。下一步:继续执行这些步骤,完成后在 EXECUTION.md 的 ## 步骤结果 中用 \`- [x] STEP-###:已完成。证据:evidence/step-###.md\` 记录;未真正执行时不要勾选。`;
59
+ }
60
+ if (missingEvidence.length > 0) {
61
+ return `不能进入 verify:步骤 ${missingEvidence.join(", ")} 缺少已落地的 evidence 文件。下一步:为每个步骤创建真实 evidence 文件,写入检查命令、结果、改动文件或验证输出,然后在 EXECUTION.md 对应步骤行引用该 evidence 路径。`;
62
+ }
59
63
  return undefined;
60
64
  }
61
65
 
@@ -29,7 +29,7 @@ export function sdkSignatureProductionWriteBlockReason(cwd: string, run: ActiveR
29
29
  if (normalized.startsWith(".pi/")) return undefined;
30
30
  if (hasValidSdkSignatureEvidence(cwd, run)) return undefined;
31
31
 
32
- return `不能写生产源码 ${normalized}:缺少本地 SDK 签名证据 ${SDK_SIGNATURE_EVIDENCE}。请先用 kd_sdk_signature 从当前项目真实 jar/dll 查证类、方法、构造器或属性签名;禁止凭记忆猜 SDK API。`;
32
+ return `不能写生产源码 ${normalized}:缺少本地 SDK 签名证据 ${SDK_SIGNATURE_EVIDENCE}。下一步:运行 kd_sdk_signature,从当前项目真实 jar/dll 查证即将使用的 SDK 类、方法、构造器或属性签名;成功生成 evidence/sdk-signature.md 后再写代码。禁止凭记忆或随包知识库猜 SDK API。`;
33
33
  }
34
34
 
35
35
  function normalizeRelativePath(path: string): string {
@@ -9,7 +9,7 @@ export const TDD_GREEN_EVIDENCE = "evidence/tdd-green.md";
9
9
 
10
10
  export function tddPlanBlockReason(plan: string): string | undefined {
11
11
  if (/##\s*TDD\s*\/\s*红绿检查/i.test(plan)) return undefined;
12
- return "PLAN.md 缺少 ## TDD / 红绿检查。必须声明红灯验证、绿灯验证和无法自动化时的产品验证替代方案。";
12
+ return "PLAN.md 缺少 ## TDD / 红绿检查。下一步:回到 plan 补充该章节,明确红灯证据、绿灯证据、验证命令或无法自动化时的产品验证替代方案;至少写明 evidence/tdd-red.md、evidence/tdd-green.md 和要运行的真实检查。";
13
13
  }
14
14
 
15
15
  export function tddProductionWriteBlockReason(cwd: string, run: ActiveRun | undefined, path: string | undefined): string | undefined {
@@ -21,25 +21,65 @@ export function tddProductionWriteBlockReason(cwd: string, run: ActiveRun | unde
21
21
  if (isTestLikePath(normalized)) return undefined;
22
22
  if (hasValidTddEvidence(cwd, run, "red")) return undefined;
23
23
 
24
- return `不能写生产源码 ${normalized}:缺少红灯证据 ${TDD_RED_EVIDENCE}。请先记录失败的测试、API/基类/方法签名检查、元数据检查、编译检查或外部接口最小验证输出。`;
24
+ return `不能写生产源码 ${normalized}:缺少红灯证据 ${TDD_RED_EVIDENCE}。下一步:先运行一个实现前应失败的检查,例如 kd_sdk_signature 方法不存在检查、元数据/API 检查、编译检查、kd_check、项目已有测试或外部接口最小验证;把命令、非 0 Exit 或失败输出写入 evidence/tdd-red.md 后再写生产源码。`;
25
25
  }
26
26
 
27
27
  export function tddVerifyBlockReason(cwd: string, run: ActiveRun): string | undefined {
28
- const missing: string[] = [];
29
- if (!hasValidTddEvidence(cwd, run, "red")) missing.push(TDD_RED_EVIDENCE);
30
- if (!hasValidTddEvidence(cwd, run, "green")) missing.push(TDD_GREEN_EVIDENCE);
31
- if (missing.length === 0) return undefined;
32
- return `不能进入 verify:缺少 TDD 红绿证据 ${missing.join(", ")}。`;
28
+ const problems = [
29
+ validateTddEvidence(cwd, run, "red"),
30
+ validateTddEvidence(cwd, run, "green"),
31
+ ].filter((result) => !result.valid);
32
+ if (problems.length === 0) return undefined;
33
+ return [
34
+ `不能进入 verify:${problems.map((problem) => problem.reason).join(";")}。`,
35
+ "修复方式:不要反复修改 evidence 文案;必须重新运行 PLAN.md 中声明的同一验证命令或等价的产品验证命令,",
36
+ "并把命令、Exit、STDOUT/STDERR 或明确的工具输出写入对应 evidence 文件。",
37
+ ].join("");
33
38
  }
34
39
 
35
40
  function hasValidTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green"): boolean {
41
+ return validateTddEvidence(cwd, run, kind).valid;
42
+ }
43
+
44
+ function validateTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green"): { valid: boolean; reason?: string } {
36
45
  const evidenceName = kind === "red" ? TDD_RED_EVIDENCE : TDD_GREEN_EVIDENCE;
37
46
  const evidencePath = join(runRoot(cwd, run), evidenceName);
38
- if (!existsSync(evidencePath)) return false;
47
+ if (!existsSync(evidencePath)) return { valid: false, reason: `缺少 ${evidenceName}` };
39
48
 
40
49
  const content = readFileSync(evidencePath, "utf8");
41
- if (kind === "red") return /red|fail|failed|failure|error|失败|未通过|Exit:\s*[1-9]/i.test(content);
42
- return /green|pass|passed|success|成功|通过|Exit:\s*0/i.test(content);
50
+ const uncertainty = uncertaintyReason(content);
51
+ if (uncertainty) {
52
+ return { valid: false, reason: `${evidenceName} 内容无效:${uncertainty}` };
53
+ }
54
+
55
+ if (kind === "red") {
56
+ const hasFailure = /red|fail|failed|failure|error|失败|未通过|Exit\s*[::]\s*[1-9]/i.test(content);
57
+ return hasFailure
58
+ ? { valid: true }
59
+ : { valid: false, reason: `${evidenceName} 内容无效:必须包含真实失败输出或非 0 退出码,不能只写结论` };
60
+ }
61
+
62
+ const hasGreenExit = /Exit\s*[::]\s*0|退出码\s*[::]\s*0/i.test(content);
63
+ const hasSuccess = /green|pass|passed|success|成功|通过/i.test(content);
64
+ if (hasGreenExit && hasSuccess) return { valid: true };
65
+
66
+ return {
67
+ valid: false,
68
+ reason: `${evidenceName} 内容无效:绿灯证据必须同时包含成功结论和 Exit: 0/退出码:0,不能只写“通过”或人工结论`,
69
+ };
70
+ }
71
+
72
+ function uncertaintyReason(content: string): string | undefined {
73
+ const patterns: Array<[RegExp, string]> = [
74
+ [/需.*验证|需要.*验证|待.*验证|后续.*验证/i, "包含待验证措辞"],
75
+ [/未验证|未执行|没有执行|无法执行|无法验证/i, "声明未完成验证"],
76
+ [/TODO|待补充|待完善|假设|预计|理论上|应该可以/i, "包含占位或推测性结论"],
77
+ [/编译验证通过[^。\n]*(需|需要|待).*验证/i, "同时声明通过和仍需验证,结论矛盾"],
78
+ ];
79
+ for (const [pattern, reason] of patterns) {
80
+ if (pattern.test(content)) return reason;
81
+ }
82
+ return undefined;
43
83
  }
44
84
 
45
85
  function isTestLikePath(path: string): boolean {