kcode-pi 0.1.15 → 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 +67 -0
- package/extensions/kingdee-harness.ts +13 -5
- package/extensions/kingdee-tools.ts +35 -6
- package/package.json +1 -1
- package/prompts/kd-execute.md +1 -1
- package/prompts/kd-plan.md +1 -1
- package/src/harness/artifacts.ts +6 -1
- package/src/harness/gates.ts +74 -14
- package/src/harness/path-policy.ts +6 -4
- package/src/harness/plan-steps.ts +9 -5
- package/src/harness/sdk-policy.ts +37 -0
- package/src/harness/tdd-policy.ts +50 -10
package/README.md
CHANGED
|
@@ -397,13 +397,50 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
397
397
|
- 进入 `execute` 前必须已有 `PLAN.md` 和必要证据。
|
|
398
398
|
- `PLAN.md` 必须包含 `## 执行步骤`,并用 `- [ ] STEP-001:...` 格式拆分可跟踪步骤。
|
|
399
399
|
- `PLAN.md` 必须包含 `## TDD / 红绿检查`,声明红灯证据、绿灯证据和测试/检查命令。
|
|
400
|
+
- Java/C# 产品代码进入 `execute` 前必须已有 `evidence/sdk-signature.md`。该证据由 `kd_sdk_signature` 成功查证当前项目真实 SDK jar/dll 后自动写入。
|
|
401
|
+
- LLM 禁止凭记忆、模型知识或随包知识库猜 SDK 方法签名。`kd_search`、`kd_cosmic_api` 只能作为线索;最终签名事实必须来自 `kd_sdk_signature`、当前项目构建输出、项目源码封装或官方元数据。
|
|
400
402
|
- 红绿检查不等于必须写 JUnit。金蝶项目不要为了满足门禁引入额外 jar 或测试框架。
|
|
401
403
|
- 写生产源码前必须已有 `evidence/tdd-red.md`,内容可以是 API/基类/方法签名检查、元数据检查、编译检查、既有测试框架或外部接口最小验证的失败输出。
|
|
402
404
|
- 进入 `execute` 后,只允许写入 `PLAN.md` 明确列出的源码文件;如果临时发现要改新文件,必须先回到 plan 更新 `PLAN.md`。
|
|
403
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 或工具输出。
|
|
404
408
|
- 旗舰版项目必须先检查当前项目结构。若存在 `code/`,跟随 `code/` 下的实际组织;若不存在,必须在 `PLAN.md` 记录实际源码根或目标文件。
|
|
405
409
|
- 不允许凭空写 demo、sample、scaffold,或在不了解项目结构时新建猜测目录。
|
|
406
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
|
+
|
|
407
444
|
示例计划步骤:
|
|
408
445
|
|
|
409
446
|
```markdown
|
|
@@ -425,6 +462,7 @@ ship 汇总变更、验证证据、风险和后续事项
|
|
|
425
462
|
- 绿灯证据:evidence/tdd-green.md
|
|
426
463
|
- 红绿检查命令或工具:kd_sdk_signature / kd_cosmic_metadata / kd_check / build
|
|
427
464
|
- 不要为了满足门禁引入第三方测试 jar 或测试框架。
|
|
465
|
+
- Java/C# SDK 签名证据:evidence/sdk-signature.md
|
|
428
466
|
```
|
|
429
467
|
|
|
430
468
|
示例执行记录:
|
|
@@ -491,6 +529,7 @@ kd_debug 分析金蝶日志和堆栈
|
|
|
491
529
|
- `kd_cosmic_metadata` 使用统一路由 API 查询真实单据/表单元数据,并在当前项目 `.pi/kd/official-skills/` 下维护 JSON 缓存。
|
|
492
530
|
- `kd_sdk_signature` 优先从当前业务项目的实际 SDK jar/dll 中读取类型和方法签名。Cosmic Java 依赖 `javap`,Enterprise C# 依赖 PowerShell 读取 DLL 元数据。
|
|
493
531
|
- `kd_cosmic_api` 查询随包金蝶知识库,只作为 API 线索和兜底;精确方法签名优先使用 `kd_sdk_signature`、当前项目 SDK 或编译输出确认。
|
|
532
|
+
- 当前项目存在 active run 时,`kd_sdk_signature` 成功后会自动写入 `.pi/kd/runs/<run-id>/evidence/sdk-signature.md`。Java/C# 产品代码缺少这份证据时,KCode 不允许进入 `execute` 或写生产源码。
|
|
494
533
|
|
|
495
534
|
示例:
|
|
496
535
|
|
|
@@ -538,6 +577,34 @@ npm view kcode-pi version
|
|
|
538
577
|
npm install -g kcode-pi@latest
|
|
539
578
|
```
|
|
540
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
|
+
|
|
541
608
|
升级后建议在业务项目根目录重新执行:
|
|
542
609
|
|
|
543
610
|
```powershell
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "../src/harness/state.ts";
|
|
19
19
|
import { readArtifact } from "../src/harness/artifacts.ts";
|
|
20
20
|
import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from "../src/harness/path-policy.ts";
|
|
21
|
+
import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
|
|
21
22
|
import { tddProductionWriteBlockReason } from "../src/harness/tdd-policy.ts";
|
|
22
23
|
import { readProjectContext } from "../src/context/project-context.ts";
|
|
23
24
|
import { windowsPathHint } from "../src/platform/path.ts";
|
|
@@ -123,9 +124,12 @@ function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof re
|
|
|
123
124
|
"",
|
|
124
125
|
phaseGuidance[run.phase],
|
|
125
126
|
"必须先理解当前业务项目已有目录、模块、包名、基类和本地封装,再决定文件位置和实现方式。",
|
|
127
|
+
"禁止凭记忆、模型知识或随包知识库直接编写 SDK 方法调用。Java/C# 代码中出现的 SDK 类、方法、构造器、枚举和属性,必须来自 kd_sdk_signature 对当前项目 jar/dll 的成功结果、项目构建输出或官方元数据证据。",
|
|
128
|
+
"kd_search、kd_cosmic_api 和随包知识只能用于找线索;没有 evidence/sdk-signature.md 或明确构建证据时,不得进入 execute,也不得写生产源码。",
|
|
126
129
|
"路径规则:在 Windows 工作区内,优先使用项目相对路径;如需绝对路径必须使用 `D:\\...` 这类 Windows 路径,禁止把路径改写成 `/mnt/d/...`、`/d/...` 等 WSL/MSYS 风格路径。",
|
|
127
130
|
"execute 阶段只能写 PLAN.md 明确列出的源码文件;如果目标文件不在计划内,必须先回到 plan 更新 PLAN.md。",
|
|
128
|
-
"写生产源码前必须先有红灯证据 evidence/tdd-red.md
|
|
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 或工具输出。",
|
|
129
133
|
].join("\n");
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -151,14 +155,18 @@ function codeWriteBlockReason(cwd: string, path: string | undefined): string | u
|
|
|
151
155
|
|
|
152
156
|
const run = readActiveRun(cwd);
|
|
153
157
|
if (!run) {
|
|
154
|
-
return "KCode
|
|
158
|
+
return "KCode 工作流未启动,不能直接写产品代码。下一步:先用自然语言说明需求让 KCode 自动进入 discuss,或手动运行 /kd-start <需求> 创建 run;完成 discuss -> spec -> plan -> execute 后再写生产源码。";
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
if (run.phase !== "execute") {
|
|
158
|
-
return `当前 KCode 阶段是 ${run.phase}
|
|
162
|
+
return `当前 KCode 阶段是 ${run.phase},不能写产品代码。下一步:先完成当前阶段文档和门禁,用 /kd-advance 推进到 execute;如果发现计划不完整,更新 PLAN.md,而不是直接写代码。`;
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
return
|
|
165
|
+
return (
|
|
166
|
+
sdkSignatureProductionWriteBlockReason(cwd, run, path) ??
|
|
167
|
+
tddProductionWriteBlockReason(cwd, run, path) ??
|
|
168
|
+
planWriteBlockReason(cwd, run, path, readArtifact(cwd, run, "plan") ?? "")
|
|
169
|
+
);
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
const kdPlanStatusTool = defineTool({
|
|
@@ -326,7 +334,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
326
334
|
const path = typeof input.path === "string" ? input.path : undefined;
|
|
327
335
|
const hint = path ? windowsPathHint(path) : undefined;
|
|
328
336
|
if (hint && ["read", "write", "edit"].includes(event.toolName)) {
|
|
329
|
-
const reason = `当前是 Windows 工作区,不能使用 WSL/MSYS 路径 ${path}
|
|
337
|
+
const reason = `当前是 Windows 工作区,不能使用 WSL/MSYS 路径 ${path}。下一步:改用项目相对路径;如果必须使用绝对路径,使用 Windows 路径 ${hint};不要再尝试 /mnt/d 或 /d 路径。`;
|
|
330
338
|
if (ctx.hasUI) ctx.ui.notify(reason, "warning");
|
|
331
339
|
return { block: true, reason };
|
|
332
340
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { Type } from "@earendil-works/pi-ai";
|
|
5
5
|
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { formatSearchResults, formatTableSchema } from "../src/knowledge/format.ts";
|
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
writeOfficialEvidence,
|
|
23
23
|
} from "../src/official/kingdee-skills.ts";
|
|
24
24
|
import { resolveWorkspacePath } from "../src/platform/path.ts";
|
|
25
|
+
import { readActiveRun } from "../src/harness/state.ts";
|
|
26
|
+
import { runArtifactPath } from "../src/harness/paths.ts";
|
|
27
|
+
import { SDK_SIGNATURE_EVIDENCE } from "../src/harness/sdk-policy.ts";
|
|
25
28
|
|
|
26
29
|
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
27
30
|
const knowledgePath = join(extensionDir, "..", "knowledge");
|
|
@@ -329,9 +332,11 @@ const kdSdkSignatureTool = defineTool({
|
|
|
329
332
|
path: params.path,
|
|
330
333
|
limit: params.limit,
|
|
331
334
|
});
|
|
335
|
+
const text = formatSdkSignatureResult(result);
|
|
336
|
+
const evidencePath = result.exitCode === 0 ? writeSdkSignatureEvidence(ctx.cwd, text) : undefined;
|
|
332
337
|
return {
|
|
333
|
-
content: [{ type: "text", text:
|
|
334
|
-
details: { product: profile.product, ...result },
|
|
338
|
+
content: [{ type: "text", text: evidencePath ? `${text}\n\n已写入 SDK 签名证据:${evidencePath}` : text }],
|
|
339
|
+
details: { product: profile.product, evidencePath, ...result },
|
|
335
340
|
};
|
|
336
341
|
},
|
|
337
342
|
});
|
|
@@ -396,9 +401,9 @@ const kdDebugTool = defineTool({
|
|
|
396
401
|
return {
|
|
397
402
|
content: [
|
|
398
403
|
{
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
404
|
+
type: "text",
|
|
405
|
+
text: `来源:${input.source}\n产品:${profile.product}/${profile.platform}/${profile.techStack}\n\n${formatDebugFindings(findings)}`,
|
|
406
|
+
},
|
|
402
407
|
],
|
|
403
408
|
details: { source: input.source, product: profile.product, findings },
|
|
404
409
|
};
|
|
@@ -423,3 +428,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
423
428
|
pi.registerTool(kdBuildTool);
|
|
424
429
|
pi.registerTool(kdDebugTool);
|
|
425
430
|
}
|
|
431
|
+
|
|
432
|
+
function writeSdkSignatureEvidence(cwd: string, content: string): string | undefined {
|
|
433
|
+
const run = readActiveRun(cwd);
|
|
434
|
+
if (!run) return undefined;
|
|
435
|
+
|
|
436
|
+
const path = runArtifactPath(cwd, run, SDK_SIGNATURE_EVIDENCE);
|
|
437
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
438
|
+
writeFileSync(
|
|
439
|
+
path,
|
|
440
|
+
[
|
|
441
|
+
"# SDK 签名证据",
|
|
442
|
+
"",
|
|
443
|
+
`- 生成时间:${new Date().toISOString()}`,
|
|
444
|
+
"- 来源:kd_sdk_signature 当前项目本地 SDK jar/dll",
|
|
445
|
+
"",
|
|
446
|
+
"```text",
|
|
447
|
+
content.trim(),
|
|
448
|
+
"```",
|
|
449
|
+
"",
|
|
450
|
+
].join("\n"),
|
|
451
|
+
"utf8",
|
|
452
|
+
);
|
|
453
|
+
return path;
|
|
454
|
+
}
|
package/package.json
CHANGED
package/prompts/kd-execute.md
CHANGED
|
@@ -4,7 +4,7 @@ description: 在 Harness 门禁约束下执行当前金蝶实施计划。
|
|
|
4
4
|
|
|
5
5
|
使用 `kd-execute` skill。
|
|
6
6
|
|
|
7
|
-
编辑代码前,先使用 `kd_plan_status` 检查当前 run。如果缺少 `PLAN.md` 或门禁被阻塞,必须停止并说明缺少的文档或证据。通过后只实现 `PLAN.md` 批准的内容,并更新 `EXECUTION.md
|
|
7
|
+
编辑代码前,先使用 `kd_plan_status` 检查当前 run。如果缺少 `PLAN.md`、`evidence/sdk-signature.md` 或门禁被阻塞,必须停止并说明缺少的文档或证据。通过后只实现 `PLAN.md` 批准的内容,并更新 `EXECUTION.md`。禁止凭记忆、模型知识或随包知识库猜 SDK 方法签名。
|
|
8
8
|
|
|
9
9
|
用户补充说明:
|
|
10
10
|
|
package/prompts/kd-plan.md
CHANGED
|
@@ -4,7 +4,7 @@ description: 为当前金蝶 Harness run 编写实施计划。
|
|
|
4
4
|
|
|
5
5
|
使用 `kd-plan` skill。
|
|
6
6
|
|
|
7
|
-
读取 `CONTEXT.md` 和 `SPEC.md`,编写或更新 `PLAN.md`。必须包含已检查的项目结构、需要查看的文件、预计修改的真实路径、必须查证的金蝶 API
|
|
7
|
+
读取 `CONTEXT.md` 和 `SPEC.md`,编写或更新 `PLAN.md`。必须包含已检查的项目结构、需要查看的文件、预计修改的真实路径、必须查证的金蝶 API/元数据、SDK 签名证据、验证命令和回滚说明。Java/C# SDK 方法签名必须来自 `kd_sdk_signature` 当前项目 jar/dll、项目构建输出或官方元数据,不能凭记忆猜。
|
|
8
8
|
|
|
9
9
|
用户补充说明:
|
|
10
10
|
|
package/src/harness/artifacts.ts
CHANGED
|
@@ -83,6 +83,10 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
|
|
|
83
83
|
"",
|
|
84
84
|
"## 必需的金蝶查证项",
|
|
85
85
|
"",
|
|
86
|
+
"- Java/C# 代码涉及 SDK 类、方法、构造器、枚举、属性时,必须先用 kd_sdk_signature 或项目构建输出确认真实签名。",
|
|
87
|
+
"- 知识库搜索、随包 Cosmic API 查询只能作为线索,不能作为最终方法签名事实。",
|
|
88
|
+
"- SDK 签名证据:evidence/sdk-signature.md",
|
|
89
|
+
"",
|
|
86
90
|
"## 执行步骤",
|
|
87
91
|
"",
|
|
88
92
|
"- [ ] STEP-001:检查现有目标文件,确认精确修改位置。",
|
|
@@ -96,9 +100,10 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
|
|
|
96
100
|
"- 红灯证据:evidence/tdd-red.md",
|
|
97
101
|
"- 绿灯证据:evidence/tdd-green.md",
|
|
98
102
|
"- 红绿检查命令或工具:未知",
|
|
99
|
-
"-
|
|
103
|
+
"- 允许的检查:本地 SDK 签名查证、官方 API/基类/方法查证、元数据查证、kd_check、构建/编译输出、项目已有测试框架、外部接口最小验证。",
|
|
100
104
|
"- 不要为了满足门禁引入第三方测试 jar 或框架。",
|
|
101
105
|
"- 如果无法自动化测试,记录一个产品相关、实现前应失败且实现后应通过的检查。",
|
|
106
|
+
"- 禁止凭记忆或随包知识库猜 SDK 方法签名;签名事实必须来自当前项目 jar/dll、构建输出或官方元数据。",
|
|
102
107
|
"",
|
|
103
108
|
"## 验证命令",
|
|
104
109
|
"",
|
package/src/harness/gates.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import type { ActiveRun, GateResult, KdPhase } from "./types.ts";
|
|
2
4
|
import { PHASE_ARTIFACTS, PHASE_ORDER } from "./types.ts";
|
|
3
5
|
import { artifactExists, readArtifact } from "./artifacts.ts";
|
|
@@ -5,6 +7,8 @@ import { isKnownProduct } from "../product/profile.ts";
|
|
|
5
7
|
import { flagshipPlanBlockReason } from "./path-policy.ts";
|
|
6
8
|
import { executionStepsBlockReason, planStepsBlockReason } from "./plan-steps.ts";
|
|
7
9
|
import { tddPlanBlockReason, tddVerifyBlockReason } from "./tdd-policy.ts";
|
|
10
|
+
import { SDK_SIGNATURE_EVIDENCE, hasValidSdkSignatureEvidence, requiresSdkSignatureEvidence } from "./sdk-policy.ts";
|
|
11
|
+
import { runRoot } from "./paths.ts";
|
|
8
12
|
|
|
9
13
|
const REQUIRED_MARKERS: Partial<Record<KdPhase, string[]>> = {
|
|
10
14
|
plan: ["## 验证命令"],
|
|
@@ -32,7 +36,7 @@ export function inspectGate(cwd: string, run: ActiveRun): GateResult {
|
|
|
32
36
|
const evidenceProblem = inspectEvidence(cwd, run, run.phase);
|
|
33
37
|
const questionProblem = inspectOpenQuestions(run);
|
|
34
38
|
const reasonParts = [];
|
|
35
|
-
if (missing.length > 0) reasonParts.push(
|
|
39
|
+
if (missing.length > 0) reasonParts.push(missingArtifactsReason([...new Set(missing)]));
|
|
36
40
|
if (markerProblem) reasonParts.push(markerProblem);
|
|
37
41
|
if (stepProblem) reasonParts.push(stepProblem);
|
|
38
42
|
if (evidenceProblem) reasonParts.push(evidenceProblem);
|
|
@@ -51,7 +55,9 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
|
|
|
51
55
|
const reasonParts: string[] = [];
|
|
52
56
|
|
|
53
57
|
if (target !== "discuss" && !isKnownProduct(run.profile?.product ?? run.product)) {
|
|
54
|
-
reasonParts.push(
|
|
58
|
+
reasonParts.push(
|
|
59
|
+
"不能离开 discuss:产品画像未知。下一步:根据需求或用户回答确认产品,然后执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>;如果无法判断,先用 kd_question 只问一个最阻塞的产品确认问题。",
|
|
60
|
+
);
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
for (let i = 0; i < targetIndex; i++) {
|
|
@@ -85,7 +91,7 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
|
|
|
85
91
|
missing.push(PHASE_ARTIFACTS.verify);
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
if (missing.length > 0) reasonParts.push(
|
|
94
|
+
if (missing.length > 0) reasonParts.push(missingForTargetReason(target, [...new Set(missing)]));
|
|
89
95
|
const reason = reasonParts.length > 0 ? reasonParts.join("; ") : undefined;
|
|
90
96
|
return {
|
|
91
97
|
passed: !reason,
|
|
@@ -97,7 +103,10 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
|
|
|
97
103
|
function inspectOpenQuestions(run: ActiveRun): string | undefined {
|
|
98
104
|
const open = (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
|
|
99
105
|
if (open.length === 0) return undefined;
|
|
100
|
-
return
|
|
106
|
+
return [
|
|
107
|
+
`存在未回答的阻断问题:${open.map((question) => `${question.id} ${question.question}`).join(";")}`,
|
|
108
|
+
"下一步:先向用户等待或获取答案,然后用 kd_question action=answer id=<问题编号> answer=<用户答案> 记录;不要绕过问题推进阶段。",
|
|
109
|
+
].join("。");
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
function inspectStepState(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
@@ -123,13 +132,13 @@ function inspectMarkers(cwd: string, run: ActiveRun, phase: KdPhase): string | u
|
|
|
123
132
|
|
|
124
133
|
const missing = markers.filter((marker) => !content.includes(marker));
|
|
125
134
|
if (missing.length === 0) return undefined;
|
|
126
|
-
return `${PHASE_ARTIFACTS[phase]} 缺少必需章节:${missing.join(", ")}
|
|
135
|
+
return `${PHASE_ARTIFACTS[phase]} 缺少必需章节:${missing.join(", ")}。下一步:更新 ${PHASE_ARTIFACTS[phase]},补齐这些章节并写入真实内容;不要只添加空标题。`;
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
function inspectEvidence(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
130
139
|
const missing = missingEvidenceForPhase(cwd, run, phase);
|
|
131
140
|
if (missing.length === 0) return undefined;
|
|
132
|
-
return
|
|
141
|
+
return missingEvidenceReason(missing);
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
function isCosmicRun(run: ActiveRun): boolean {
|
|
@@ -137,22 +146,23 @@ function isCosmicRun(run: ActiveRun): boolean {
|
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
function missingEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase): string[] {
|
|
140
|
-
return requiredEvidenceForPhase(cwd, run, phase).filter((artifact) => !
|
|
149
|
+
return requiredEvidenceForPhase(cwd, run, phase).filter((artifact) => !evidenceArtifactSatisfied(cwd, run, artifact));
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase): string[] {
|
|
144
|
-
if (!isCosmicRun(run)) return [];
|
|
145
|
-
|
|
146
153
|
const required = new Set<string>();
|
|
147
154
|
const phaseIndex = PHASE_ORDER.indexOf(phase);
|
|
148
155
|
|
|
149
156
|
if (phaseIndex >= PHASE_ORDER.indexOf("execute")) {
|
|
150
|
-
required.add(
|
|
151
|
-
if (
|
|
152
|
-
|
|
157
|
+
if (requiresSdkSignatureEvidence(run)) required.add(SDK_SIGNATURE_EVIDENCE);
|
|
158
|
+
if (isCosmicRun(run)) {
|
|
159
|
+
required.add(COSMIC_CONFIG_EVIDENCE);
|
|
160
|
+
if (planHasMetadataRequirement(cwd, run)) required.add(COSMIC_METADATA_EVIDENCE);
|
|
161
|
+
if (planHasApiRequirement(cwd, run)) required.add(COSMIC_API_EVIDENCE);
|
|
162
|
+
}
|
|
153
163
|
}
|
|
154
164
|
|
|
155
|
-
if (phaseIndex >= PHASE_ORDER.indexOf("ship") && runHasKsqlDelivery(cwd, run)) {
|
|
165
|
+
if (isCosmicRun(run) && phaseIndex >= PHASE_ORDER.indexOf("ship") && runHasKsqlDelivery(cwd, run)) {
|
|
156
166
|
required.add(COSMIC_METADATA_EVIDENCE);
|
|
157
167
|
required.add(KSQL_LINT_EVIDENCE);
|
|
158
168
|
}
|
|
@@ -160,6 +170,56 @@ function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase):
|
|
|
160
170
|
return [...required];
|
|
161
171
|
}
|
|
162
172
|
|
|
173
|
+
function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string): boolean {
|
|
174
|
+
if (artifact === SDK_SIGNATURE_EVIDENCE) return hasValidSdkSignatureEvidence(cwd, run);
|
|
175
|
+
return existsSync(join(runRoot(cwd, run), artifact));
|
|
176
|
+
}
|
|
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
|
+
|
|
163
223
|
function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
|
|
164
224
|
const plan = readArtifact(cwd, run, "plan") ?? "";
|
|
165
225
|
return /kd_cosmic_metadata|cosmic-metadata|cosmic-metadata\.json|metadata evidence|字段元数据证据|元数据证据/i.test(plan);
|
|
@@ -167,7 +227,7 @@ function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
|
|
|
167
227
|
|
|
168
228
|
function planHasApiRequirement(cwd: string, run: ActiveRun): boolean {
|
|
169
229
|
const plan = readArtifact(cwd, run, "plan") ?? "";
|
|
170
|
-
return /
|
|
230
|
+
return /kd_cosmic_api|cosmic-api|cosmic-api\.txt/i.test(plan);
|
|
171
231
|
}
|
|
172
232
|
|
|
173
233
|
function runHasKsqlDelivery(cwd: string, run: ActiveRun): boolean {
|
|
@@ -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/"))
|
|
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}
|
|
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 缺少 ##
|
|
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
|
|
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)
|
|
58
|
-
|
|
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
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
3
|
+
import type { ActiveRun } from "./types.ts";
|
|
4
|
+
import { runRoot } from "./paths.ts";
|
|
5
|
+
import { isSourceLikePath } from "./path-policy.ts";
|
|
6
|
+
|
|
7
|
+
export const SDK_SIGNATURE_EVIDENCE = "evidence/sdk-signature.md";
|
|
8
|
+
|
|
9
|
+
export function requiresSdkSignatureEvidence(run: ActiveRun): boolean {
|
|
10
|
+
const language = run.profile?.language;
|
|
11
|
+
return language === "java" || language === "csharp";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasValidSdkSignatureEvidence(cwd: string, run: ActiveRun): boolean {
|
|
15
|
+
if (!requiresSdkSignatureEvidence(run)) return true;
|
|
16
|
+
const path = join(runRoot(cwd, run), SDK_SIGNATURE_EVIDENCE);
|
|
17
|
+
if (!existsSync(path)) return false;
|
|
18
|
+
|
|
19
|
+
const content = readFileSync(path, "utf8");
|
|
20
|
+
return /(退出码|Exit)\s*[::]\s*0/i.test(content) && /(来源|Sources?)\s*[::]/i.test(content);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sdkSignatureProductionWriteBlockReason(cwd: string, run: ActiveRun | undefined, path: string | undefined): string | undefined {
|
|
24
|
+
if (!run || run.phase !== "execute") return undefined;
|
|
25
|
+
if (!requiresSdkSignatureEvidence(run)) return undefined;
|
|
26
|
+
if (!path || !isSourceLikePath(path)) return undefined;
|
|
27
|
+
|
|
28
|
+
const normalized = normalizeRelativePath(cwd && isAbsolute(path) ? relative(cwd, path) : path);
|
|
29
|
+
if (normalized.startsWith(".pi/")) return undefined;
|
|
30
|
+
if (hasValidSdkSignatureEvidence(cwd, run)) return undefined;
|
|
31
|
+
|
|
32
|
+
return `不能写生产源码 ${normalized}:缺少本地 SDK 签名证据 ${SDK_SIGNATURE_EVIDENCE}。下一步:运行 kd_sdk_signature,从当前项目真实 jar/dll 查证即将使用的 SDK 类、方法、构造器或属性签名;成功生成 evidence/sdk-signature.md 后再写代码。禁止凭记忆或随包知识库猜 SDK API。`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeRelativePath(path: string): string {
|
|
36
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
37
|
+
}
|
|
@@ -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}
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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 {
|