kcode-pi 0.1.17 → 0.1.19

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
@@ -293,6 +293,7 @@ kcode start --provider openai --model gpt-4o
293
293
  ```text
294
294
  /kd-start [--product 产品] [--version 版本] <需求>
295
295
  /kd-product <产品> [--version 版本]
296
+ /kd-risk <low|medium|high> <原因>
296
297
  /kd-status
297
298
  /kd-runs
298
299
  /kd-switch <run-id>
@@ -300,7 +301,7 @@ kcode start --provider openai --model gpt-4o
300
301
  /kd-finish
301
302
  /kd-gate
302
303
  /kd-advance [阶段]
303
- /kd-artifact [阶段] [内容]
304
+ /kd-artifact [阶段] [内容] [--replace]
304
305
  /kd-answer Q-001 <答案>
305
306
  ```
306
307
 
@@ -312,6 +313,14 @@ kcode start --provider openai --model gpt-4o
312
313
 
313
314
  `/kd-start` 会创建新的功能点 run,并立即触发 Agent 进入 `discuss` 阶段。如果未识别出产品画像,例如显示 `(unknown/unknown)`,下一步应先确认产品、版本和技术栈,而不是直接写代码。
314
315
 
316
+ 进入 `ship` 前必须确认风险等级:
317
+
318
+ ```text
319
+ /kd-risk low 已完成本地构建和元数据检查,无残余交付风险
320
+ ```
321
+
322
+ `/kd-artifact` 默认只创建缺失阶段文档。若阶段文档已存在,带内容调用不会直接覆盖;确认要整体替换时必须追加 `--replace`。
323
+
315
324
  暂停后接续:
316
325
 
317
326
  ```text
@@ -376,6 +385,7 @@ KCode 会把金蝶开发需求纳入 Harness 工作流。你可以直接输入
376
385
  .pi/kd/runs/<run-id>/VERIFY.md
377
386
  .pi/kd/runs/<run-id>/SHIP.md
378
387
  .pi/kd/runs/<run-id>/evidence/
388
+ .pi/kd/runs/<run-id>/evidence/index.json
379
389
  ```
380
390
 
381
391
  下次重新 `kcode start` 时,KCode 会提示当前项目的 active run。执行 `/kd-resume` 后,KCode 会读取项目常驻上下文、active run 状态和已生成的阶段文档,再继续当前功能点的当前阶段。已完成或暂停的功能点仍保留在 `.pi/kd/runs/<run-id>/`,可用 `/kd-runs` 查看,用 `/kd-switch <run-id>` 切回。
@@ -403,8 +413,12 @@ ship 汇总变更、验证证据、风险和后续事项
403
413
  - 写生产源码前必须已有 `evidence/tdd-red.md`,内容可以是 API/基类/方法签名检查、元数据检查、编译检查、既有测试框架或外部接口最小验证的失败输出。
404
414
  - 进入 `execute` 后,只允许写入 `PLAN.md` 明确列出的源码文件;如果临时发现要改新文件,必须先回到 plan 更新 `PLAN.md`。
405
415
  - 进入 `verify` 前,`EXECUTION.md` 必须逐个完成 `PLAN.md` 中的所有 `STEP-###`,并为每个步骤记录真实存在的 `evidence/...` 文件;同时必须已有 `evidence/tdd-red.md` 和 `evidence/tdd-green.md`。
416
+ - evidence 文件必须登记在 `evidence/index.json` 中。KCode 内置工具和 evidence 写入路径会自动维护索引;不要手工塞文件绕过门禁。
406
417
  - `evidence/tdd-green.md` 必须包含真实成功输出和 `Exit: 0` 或 `退出码:0`;不能写“需在开发环境验证”“待验证”“未执行”等不确定结论。
407
418
  - 如果门禁提示 evidence 内容无效,不要反复改文案或补关键词;必须重新运行 `PLAN.md` 中声明的真实验证命令,并记录命令、Exit、STDOUT/STDERR 或工具输出。
419
+ - Java / 苍穹 / 星空旗舰版的语法和编译验证优先使用当前项目 Gradle 命令,例如 `.\gradlew.bat build`、`./gradlew build` 或 `.\gradlew.bat :模块:build`。
420
+ - C# / 企业版的语法和编译验证使用 `dotnet build`、`dotnet build <.sln>` 或 `dotnet build <.csproj>`。
421
+ - 不要写“Kingdee IDE 中编译”作为验证方式;如果构建命令无法运行,记录真实阻塞原因和残余风险,不能把它当作绿灯证据。
408
422
  - 旗舰版项目必须先检查当前项目结构。若存在 `code/`,跟随 `code/` 下的实际组织;若不存在,必须在 `PLAN.md` 记录实际源码根或目标文件。
409
423
  - 不允许凭空写 demo、sample、scaffold,或在不了解项目结构时新建猜测目录。
410
424
 
@@ -518,7 +532,7 @@ kd_cosmic_metadata 查询官方 Cosmic 表单/单据元数据
518
532
  kd_cosmic_api 查询随包 Cosmic API 知识线索
519
533
  kd_sdk_signature 从当前项目实际 SDK jar/dll 中读取类和方法签名
520
534
  kd_ksql_lint 运行官方 ok-ksql SQL/KSQL lint
521
- kd_build 按产品画像执行或 dry-run 构建
535
+ kd_build 按产品画像执行或 dry-run 构建;Java 使用 Gradle,C# 使用 dotnet build
522
536
  kd_debug 分析金蝶日志和堆栈
523
537
  ```
524
538
 
@@ -15,7 +15,9 @@ import {
15
15
  switchActiveRun,
16
16
  updateProductProfile,
17
17
  updatePhaseArtifact,
18
+ updateRisk,
18
19
  } from "../src/harness/state.ts";
20
+ import type { KdRisk } from "../src/harness/types.ts";
19
21
  import { readArtifact } from "../src/harness/artifacts.ts";
20
22
  import { flagshipWriteBlockReason, isSourceLikePath, planWriteBlockReason } from "../src/harness/path-policy.ts";
21
23
  import { sdkSignatureProductionWriteBlockReason } from "../src/harness/sdk-policy.ts";
@@ -30,18 +32,19 @@ function requireRun(cwd: string): ReturnType<typeof readActiveRun> {
30
32
  return readActiveRun(cwd);
31
33
  }
32
34
 
33
- function parseArtifactArgs(args: string, currentPhase: KdPhase): { phase: KdPhase; content?: string } | undefined {
34
- const trimmed = args.trim();
35
- if (!trimmed) return { phase: currentPhase };
35
+ function parseArtifactArgs(args: string, currentPhase: KdPhase): { phase: KdPhase; content?: string; replace: boolean } | undefined {
36
+ const replace = /\s*(^|\s)--replace(\s|$)/.test(args);
37
+ const trimmed = args.replace(/\s*(^|\s)--replace(?=\s|$)/g, " ").trim();
38
+ if (!trimmed) return { phase: currentPhase, replace };
36
39
 
37
40
  const [first, ...rest] = trimmed.split(/\s+/);
38
41
  if (isKdPhase(first)) {
39
42
  const contentStart = trimmed.indexOf(first) + first.length;
40
43
  const content = trimmed.slice(contentStart).trim();
41
- return { phase: first, content: content || undefined };
44
+ return { phase: first, content: content || undefined, replace };
42
45
  }
43
46
 
44
- return { phase: currentPhase, content: trimmed };
47
+ return { phase: currentPhase, content: trimmed, replace };
45
48
  }
46
49
 
47
50
  function parseStartArgs(args: string): { goal: string; product?: string; version?: string } {
@@ -73,6 +76,15 @@ function parseProductArgs(args: string): { product: string; version?: string } |
73
76
  return { product, version: parsed.version };
74
77
  }
75
78
 
79
+ function parseRiskArgs(args: string): { risk: KdRisk; reason: string } | undefined {
80
+ const [first, ...rest] = args.trim().split(/\s+/).filter(Boolean);
81
+ const risk = first?.toLowerCase();
82
+ if (risk !== "low" && risk !== "medium" && risk !== "high") return undefined;
83
+ const reason = rest.join(" ").trim();
84
+ if (!reason) return undefined;
85
+ return { risk, reason };
86
+ }
87
+
76
88
  function shouldStartHarnessFromInput(text: string): boolean {
77
89
  if (!text.trim() || text.trim().startsWith("/")) return false;
78
90
  return KINGDEE_INTENT_PATTERN.test(text);
@@ -129,6 +141,8 @@ function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof re
129
141
  "路径规则:在 Windows 工作区内,优先使用项目相对路径;如需绝对路径必须使用 `D:\\...` 这类 Windows 路径,禁止把路径改写成 `/mnt/d/...`、`/d/...` 等 WSL/MSYS 风格路径。",
130
142
  "execute 阶段只能写 PLAN.md 明确列出的源码文件;如果目标文件不在计划内,必须先回到 plan 更新 PLAN.md。",
131
143
  "写生产源码前必须先有红灯证据 evidence/tdd-red.md;Java/C# 还必须有 SDK 签名证据 evidence/sdk-signature.md。红绿证据可以是 kd_sdk_signature 本地 SDK 签名、API/基类/方法签名、元数据、编译、既有测试框架或外部接口最小验证,不要为了测试引入额外 jar。",
144
+ "语法/编译验证必须使用真实项目构建命令:Java/Cosmic/苍穹/星空旗舰版使用当前项目 Gradle 命令,例如 `./gradlew build`、`.\\gradlew.bat build` 或 `:模块:build`;C#/企业版使用 `dotnet build` 或 `dotnet build <.sln/.csproj>`。",
145
+ "不要写“Kingdee IDE 中编译”作为验证方式或绿灯证据;命令无法运行时,记录真实阻塞原因和残余风险,不能标记为通过。",
132
146
  "如果门禁提示 evidence 内容无效,不要通过改写结论、补关键词或反复读取文件来过关;必须重新运行 PLAN.md 中声明的真实验证命令,并记录命令、Exit、STDOUT/STDERR 或工具输出。",
133
147
  ].join("\n");
134
148
  }
@@ -318,6 +332,9 @@ export default function (pi: ExtensionAPI) {
318
332
  run = createActiveRun(ctx.cwd, event.text);
319
333
  if (ctx.hasUI) {
320
334
  ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
335
+ if (run.profile?.product === "unknown") {
336
+ ctx.ui.notify("产品画像未识别。下一步先执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>。", "warning");
337
+ }
321
338
  }
322
339
  }
323
340
 
@@ -388,6 +405,9 @@ export default function (pi: ExtensionAPI) {
388
405
 
389
406
  const run = createActiveRun(ctx.cwd, goal, parsed.product, parsed.version);
390
407
  ctx.ui.notify(`已启动 Kingdee Harness run:${run.id}(${run.profile?.product}/${run.profile?.techStack})`, "info");
408
+ if (run.profile?.product === "unknown") {
409
+ ctx.ui.notify("产品画像未识别。下一步先执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>。", "warning");
410
+ }
391
411
  sendWorkflowPrompt(pi, ctx, run, `继续 KCode Harness run ${run.id}:${goal}`);
392
412
  },
393
413
  });
@@ -465,6 +485,26 @@ export default function (pi: ExtensionAPI) {
465
485
  },
466
486
  });
467
487
 
488
+ pi.registerCommand("kd-risk", {
489
+ description: "设置当前 Kingdee run 风险等级:/kd-risk <low|medium|high> <原因>",
490
+ handler: async (args, ctx) => {
491
+ const run = requireRun(ctx.cwd);
492
+ if (!run) {
493
+ ctx.ui.notify("当前没有 active Kingdee Harness run。请使用 /kd-start <需求>。", "error");
494
+ return;
495
+ }
496
+
497
+ const risk = parseRiskArgs(args);
498
+ if (!risk) {
499
+ ctx.ui.notify("用法:/kd-risk <low|medium|high> <原因>", "error");
500
+ return;
501
+ }
502
+
503
+ const updated = updateRisk(ctx.cwd, run, risk.risk, risk.reason);
504
+ ctx.ui.notify(`风险等级:${updated.riskAssessment?.level}(${updated.riskAssessment?.reason})`, "info");
505
+ },
506
+ });
507
+
468
508
  pi.registerCommand("kd-advance", {
469
509
  description: `推进 Kingdee run 到下一阶段,或指定阶段:${PHASE_ORDER.join("|")}`,
470
510
  handler: async (args, ctx) => {
@@ -490,7 +530,7 @@ export default function (pi: ExtensionAPI) {
490
530
  });
491
531
 
492
532
  pi.registerCommand("kd-artifact", {
493
- description: "创建或更新阶段文档:/kd-artifact [阶段] [内容]",
533
+ description: "创建阶段文档,或显式覆盖阶段文档:/kd-artifact [阶段] [内容] [--replace]",
494
534
  handler: async (args, ctx) => {
495
535
  const run = requireRun(ctx.cwd);
496
536
  if (!run) {
@@ -500,7 +540,12 @@ export default function (pi: ExtensionAPI) {
500
540
 
501
541
  const parsed = parseArtifactArgs(args, run.phase);
502
542
  if (!parsed) {
503
- ctx.ui.notify("用法:/kd-artifact [阶段] [内容]", "error");
543
+ ctx.ui.notify("用法:/kd-artifact [阶段] [内容] [--replace]", "error");
544
+ return;
545
+ }
546
+
547
+ if (parsed.content && readArtifact(ctx.cwd, run, parsed.phase) !== undefined && !parsed.replace) {
548
+ ctx.ui.notify(`拒绝覆盖 ${parsed.phase} 阶段文档。若确认要整体替换,请追加 --replace;追加内容应让 Agent 更新具体章节。`, "warning");
504
549
  return;
505
550
  }
506
551
 
@@ -1,45 +1,13 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
1
  import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
4
-
5
- type KdPhase = "discuss" | "spec" | "plan" | "execute" | "verify" | "ship";
6
-
7
- interface ActiveRun {
8
- id?: string;
9
- phase?: KdPhase;
10
- product?: string;
11
- profile?: {
12
- product?: string;
13
- techStack?: string;
14
- language?: string;
15
- };
16
- risk?: "low" | "medium" | "high";
17
- gate?: {
18
- passed?: boolean;
19
- reason?: string;
20
- };
21
- }
22
-
23
- function readActiveRun(cwd: string): ActiveRun | undefined {
24
- const activeRunPath = join(cwd, ".pi", "kd", "active-run.json");
25
- if (!existsSync(activeRunPath)) return undefined;
26
-
27
- try {
28
- return JSON.parse(readFileSync(activeRunPath, "utf8")) as ActiveRun;
29
- } catch {
30
- return undefined;
31
- }
32
- }
2
+ import { readActiveRun } from "../src/harness/state.ts";
3
+ import type { ActiveRun } from "../src/harness/types.ts";
4
+ import { formatProductProfile } from "../src/product/profile.ts";
33
5
 
34
6
  function formatProduct(run: ActiveRun | undefined): string {
35
- if (!run) return "未选择";
36
- const product = run.profile?.product ?? run.product ?? "unknown";
37
- const techStack = run.profile?.techStack ?? "unknown";
38
- const language = run.profile?.language ?? "unknown";
39
- return `${product}/${techStack}/${language}`;
7
+ return run ? formatProductProfile(run.profile) : "未选择";
40
8
  }
41
9
 
42
- function formatPhase(phase: ActiveRun["phase"]): string {
10
+ function formatPhase(phase: ActiveRun["phase"] | undefined): string {
43
11
  return phase ?? "空闲";
44
12
  }
45
13
 
@@ -49,6 +17,10 @@ function formatGate(run: ActiveRun | undefined): string {
49
17
  return `门禁:阻塞${run.gate.reason ? ` - ${run.gate.reason}` : ""}`;
50
18
  }
51
19
 
20
+ function riskLevel(run: ActiveRun | undefined): string {
21
+ return run?.riskAssessment?.level ?? "未知";
22
+ }
23
+
52
24
  function padOrTrim(text: string, width: number): string {
53
25
  if (width <= 0) return "";
54
26
  if (text.length > width) return text.slice(0, Math.max(0, width - 1)) + ">";
@@ -76,7 +48,7 @@ export default function (pi: ExtensionAPI) {
76
48
  const phase = formatPhase(run?.phase);
77
49
  const product = formatProduct(run);
78
50
  const gate = formatGate(run);
79
- const risk = run?.risk ?? "未知";
51
+ const risk = riskLevel(run);
80
52
  const runId = run?.id ?? "无";
81
53
 
82
54
  const status = [
@@ -1,6 +1,6 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { readFileSync } 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";
@@ -23,7 +23,7 @@ import {
23
23
  } from "../src/official/kingdee-skills.ts";
24
24
  import { resolveWorkspacePath } from "../src/platform/path.ts";
25
25
  import { readActiveRun } from "../src/harness/state.ts";
26
- import { runArtifactPath } from "../src/harness/paths.ts";
26
+ import { writeEvidenceFile } from "../src/harness/evidence.ts";
27
27
  import { SDK_SIGNATURE_EVIDENCE } from "../src/harness/sdk-policy.ts";
28
28
 
29
29
  const extensionDir = dirname(fileURLToPath(import.meta.url));
@@ -365,7 +365,7 @@ const kdBuildTool = defineTool({
365
365
  description: "按产品画像运行或预览金蝶构建命令,支持 Cosmic Java 和企业版 C# 项目。",
366
366
  parameters: Type.Object({
367
367
  product: Type.String({ description: "金蝶产品:cangqiong、xinghan、flagship、cosmic 或 enterprise。" }),
368
- target: Type.Optional(Type.String({ description: "Java 可提供 Gradle/Maven task;C# 可提供 .sln/.csproj 路径。" })),
368
+ target: Type.Optional(Type.String({ description: "Java 可提供 Gradle task;C# 可提供 .sln/.csproj 路径。" })),
369
369
  dryRun: Type.Optional(Type.Boolean({ description: "只返回构建命令,不实际执行。" })),
370
370
  }),
371
371
 
@@ -433,10 +433,10 @@ function writeSdkSignatureEvidence(cwd: string, content: string): string | undef
433
433
  const run = readActiveRun(cwd);
434
434
  if (!run) return undefined;
435
435
 
436
- const path = runArtifactPath(cwd, run, SDK_SIGNATURE_EVIDENCE);
437
- mkdirSync(dirname(path), { recursive: true });
438
- writeFileSync(
439
- path,
436
+ return writeEvidenceFile(
437
+ cwd,
438
+ run,
439
+ SDK_SIGNATURE_EVIDENCE,
440
440
  [
441
441
  "# SDK 签名证据",
442
442
  "",
@@ -448,7 +448,6 @@ function writeSdkSignatureEvidence(cwd: string, content: string): string | undef
448
448
  "```",
449
449
  "",
450
450
  ].join("\n"),
451
- "utf8",
451
+ { kind: "sdk-signature", command: "kd_sdk_signature", exitCode: 0 },
452
452
  );
453
- return path;
454
453
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "面向金蝶开发的 Pi Coding Agent 启动器、工具包和 Harness 工作流",
5
5
  "type": "module",
6
6
  "private": false,
@@ -6,6 +6,8 @@ description: 在 Harness 门禁约束下执行当前金蝶实施计划。
6
6
 
7
7
  编辑代码前,先使用 `kd_plan_status` 检查当前 run。如果缺少 `PLAN.md`、`evidence/sdk-signature.md` 或门禁被阻塞,必须停止并说明缺少的文档或证据。通过后只实现 `PLAN.md` 批准的内容,并更新 `EXECUTION.md`。禁止凭记忆、模型知识或随包知识库猜 SDK 方法签名。
8
8
 
9
+ 实现后要按计划运行真实构建检查语法问题:Java 使用当前项目 Gradle 命令,例如 `.\gradlew.bat build` 或 `.\gradlew.bat :模块:build`;C# 使用 `dotnet build` 或 `dotnet build <.sln/.csproj>`。不能写“Kingdee IDE 中编译”作为验证结果;命令未运行就不能记录为绿灯证据。
10
+
9
11
  用户补充说明:
10
12
 
11
13
  {{args}}
@@ -6,6 +6,12 @@ description: 为当前金蝶 Harness run 编写实施计划。
6
6
 
7
7
  读取 `CONTEXT.md` 和 `SPEC.md`,编写或更新 `PLAN.md`。必须包含已检查的项目结构、需要查看的文件、预计修改的真实路径、必须查证的金蝶 API/元数据、SDK 签名证据、验证命令和回滚说明。Java/C# SDK 方法签名必须来自 `kd_sdk_signature` 当前项目 jar/dll、项目构建输出或官方元数据,不能凭记忆猜。
8
8
 
9
+ 验证命令必须贴近真实项目:
10
+
11
+ - Java / Cosmic / 苍穹 / 星空旗舰版:优先使用当前项目 Gradle 构建做语法和编译检查,例如 `./gradlew build`、`./gradlew :模块:build` 或 Windows 下的 `.\gradlew.bat build`。
12
+ - C# / 企业版:使用 `dotnet build` 或 `dotnet build <.sln/.csproj>` 做语法和编译检查。
13
+ - 不要写“Kingdee IDE 中编译”。如果当前机器缺少依赖导致命令无法运行,要记录真实阻塞原因,不能把未执行的编译当作绿灯证据。
14
+
9
15
  用户补充说明:
10
16
 
11
17
  {{args}}
@@ -6,6 +6,12 @@ description: 验证当前金蝶实现并收集证据。
6
6
 
7
7
  执行计划中的验证命令,收集证据,运行可用检查,并更新 `VERIFY.md`。如果某项验证无法运行,记录具体阻塞原因和残余风险。
8
8
 
9
+ 验证优先使用真实构建命令检查语法和编译:
10
+
11
+ - Java / Cosmic / 苍穹 / 星空旗舰版:运行当前项目 Gradle 命令,例如 `.\gradlew.bat build`、`.\gradlew.bat :模块:build` 或同等 Gradle task。
12
+ - C# / 企业版:运行 `dotnet build`、`dotnet build <.sln>` 或 `dotnet build <.csproj>`。
13
+ - 绿灯证据必须包含实际命令、`Exit: 0` 和输出摘要。不能用“Kingdee IDE 中编译”“需在开发环境验证”替代真实证据。
14
+
9
15
  用户补充说明:
10
16
 
11
17
  {{args}}
@@ -58,7 +58,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
58
58
 
59
59
  5. 验证结果。
60
60
  - 对修改的 Java 代码运行 `kd_check`。
61
- - 条件允许时运行 `kd_build` 或计划中的 Gradle/Maven 命令。
61
+ - 条件允许时运行 `kd_build` 或计划中的 Gradle 命令,优先用 `.\gradlew.bat build`、`./gradlew build` 或最窄可行的 `:模块:build` 检查语法和编译。
62
62
  - 把验证证据写进 `EXECUTION.md`,最终验收交给 `kd-verify`。
63
63
 
64
64
  ## 硬性规则
@@ -79,7 +79,7 @@ POJO 或简单枚举场景可以简要说明后直接实现。
79
79
  写完测试后:
80
80
 
81
81
  - 使用 `kd_build` 或计划中的命令运行最窄可行 Gradle test 任务。
82
- - 如果本机缺少业务 jar 或本地配置导致 Gradle 不能运行,明确说明原因,并给出用户应在 IDE 中运行的准确任务。
82
+ - 如果本机缺少业务 jar 或本地配置导致 Gradle 不能运行,明确说明真实阻塞原因,并给出应该运行的 Gradle task;不要写“在 IDE 中运行”作为验证结论。
83
83
  - 存在 harness run 时,把测试文件和验证结果写入 `.pi/kd/runs/<run-id>/EXECUTION.md`。
84
84
 
85
85
  ## 输出要求
@@ -22,5 +22,7 @@ Rules:
22
22
  - Do not skip planned steps. Every `STEP-###` in `PLAN.md` must be marked complete in `EXECUTION.md` with a real `evidence/...` file before entering verify.
23
23
  - Before writing production source files, run the planned red check and record failing output in `evidence/tdd-red.md`. This can be `kd_sdk_signature` local SDK signature, metadata, compile/build, existing project test, or minimal external-interface evidence.
24
24
  - Before entering verify, rerun the same check and record passing output in `evidence/tdd-green.md`.
25
+ - After implementation, run the planned real build command for syntax/compile validation when available: Java uses the project Gradle command such as `.\gradlew.bat build` or `.\gradlew.bat :module:build`; C# uses `dotnet build` or `dotnet build <.sln/.csproj>`.
25
26
  - Do not add JUnit, Mockito, NUnit, xUnit, or any extra test jar/framework only to satisfy the gate. Use existing approved project test infrastructure if it already exists.
27
+ - Do not record "compile in Kingdee IDE" or "needs local development environment verification" as green evidence. If the command cannot run, record the blocker instead of marking verification passed.
26
28
  - If implementation needs a plan change, update `PLAN.md` first.
@@ -14,6 +14,8 @@ Goal:
14
14
  - List the inspected project layout and the exact target source root or file path before editing.
15
15
  - List expected files to modify.
16
16
  - List required `kd_sdk_signature`, `kd_search`, `kd_table`, metadata, and build/compile checks.
17
+ - For Java/Cosmic/Cangqiong/Flagship projects, plan a Gradle build check for syntax/compile validation, using the current project command such as `.\gradlew.bat build`, `./gradlew build`, or `.\gradlew.bat :module:build`.
18
+ - For C#/Enterprise projects, plan `dotnet build` or `dotnet build <.sln/.csproj>` for syntax/compile validation.
17
19
  - List `## Execution Steps` using `- [ ] STEP-001: ...` style IDs.
18
20
  - List `## TDD / Red-Green Checks` with red evidence, green evidence, and the command/tool or product-specific check.
19
21
  - Do not plan to add third-party test jars or frameworks only for red/green checks. For Kingdee plugin work, prefer `kd_sdk_signature` against current project jars/dlls, metadata checks, compile/build checks, existing project tests, or minimal external-interface tests.
@@ -25,6 +27,7 @@ Gate:
25
27
  - Execution must not start without `PLAN.md`.
26
28
  - A plan for 星空旗舰版 is incomplete unless it records the existing project layout and exact target path to edit. If `code/` exists, follow its actual structure; if it does not, record the discovered source root or existing target file.
27
29
  - A plan without validation commands is incomplete.
30
+ - A Java/C# plan that uses vague wording such as "compile in Kingdee IDE" instead of a real Gradle or dotnet command is incomplete.
28
31
  - A plan without structured `STEP-001` execution steps is incomplete.
29
32
  - A plan without TDD red/green checks is incomplete.
30
33
  - A plan that relies on unverified Kingdee API names is incomplete; bundled knowledge alone is not enough when local SDK jars/dlls or compile evidence are available.
@@ -10,6 +10,8 @@ Use this skill after implementation.
10
10
  Goal:
11
11
 
12
12
  - Run planned validation commands.
13
+ - For Java/Cosmic/Cangqiong/Flagship projects, run the planned Gradle command to catch syntax/compile errors, for example `.\gradlew.bat build`, `./gradlew build`, or a narrow `:module:build` task.
14
+ - For C#/Enterprise projects, run `dotnet build` or `dotnet build <.sln/.csproj>` to catch syntax/compile errors.
13
15
  - Run `kd_check` when code is available.
14
16
  - Collect evidence into `.pi/kd/runs/<run-id>/VERIFY.md`.
15
17
  - Record failures, fixes, skipped checks, and residual risk.
@@ -18,5 +20,5 @@ Rules:
18
20
 
19
21
  - Passing unit tests is not enough if acceptance criteria require workflow behavior.
20
22
  - If validation cannot run, state the exact blocker.
23
+ - Do not use "Kingdee IDE" as a verification target. Evidence must show the real command, `Exit: 0`, and useful output summary.
21
24
  - Do not ship while verification evidence is missing.
22
-
@@ -100,8 +100,11 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
100
100
  "- 红灯证据:evidence/tdd-red.md",
101
101
  "- 绿灯证据:evidence/tdd-green.md",
102
102
  "- 红绿检查命令或工具:未知",
103
- "- 允许的检查:本地 SDK 签名查证、官方 API/基类/方法查证、元数据查证、kd_check、构建/编译输出、项目已有测试框架、外部接口最小验证。",
103
+ "- Java 语法/编译检查:优先使用当前项目 Gradle 命令,例如 `./gradlew build`、`.\\gradlew.bat build` 或 `./gradlew :模块:build`。",
104
+ "- C# 语法/编译检查:使用 `dotnet build`、`dotnet build <.sln>` 或 `dotnet build <.csproj>`。",
105
+ "- 允许的检查:本地 SDK 签名查证、官方 API/基类/方法查证、元数据查证、kd_check、Gradle/dotnet 构建输出、项目已有测试框架、外部接口最小验证。",
104
106
  "- 不要为了满足门禁引入第三方测试 jar 或框架。",
107
+ "- 不要写“Kingdee IDE 中编译”作为验证方式;命令无法运行时记录真实阻塞原因,不能作为绿灯证据。",
105
108
  "- 如果无法自动化测试,记录一个产品相关、实现前应失败且实现后应通过的检查。",
106
109
  "- 禁止凭记忆或随包知识库猜 SDK 方法签名;签名事实必须来自当前项目 jar/dll、构建输出或官方元数据。",
107
110
  "",
@@ -0,0 +1,106 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import type { ActiveRun } from "./types.ts";
4
+ import { runArtifactPath, runRoot } from "./paths.ts";
5
+
6
+ export interface EvidenceEntry {
7
+ path: string;
8
+ kind: string;
9
+ command?: string;
10
+ exitCode?: number;
11
+ createdAt: string;
12
+ updatedAt: string;
13
+ size: number;
14
+ }
15
+
16
+ export interface EvidenceIndex {
17
+ version: 1;
18
+ entries: EvidenceEntry[];
19
+ }
20
+
21
+ export const EVIDENCE_INDEX = "evidence/index.json";
22
+
23
+ export function evidenceIndexPath(cwd: string, run: ActiveRun): string {
24
+ return runArtifactPath(cwd, run, EVIDENCE_INDEX);
25
+ }
26
+
27
+ export function readEvidenceIndex(cwd: string, run: ActiveRun): EvidenceIndex {
28
+ const path = evidenceIndexPath(cwd, run);
29
+ if (!existsSync(path)) return { version: 1, entries: [] };
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as EvidenceIndex;
32
+ return parsed.version === 1 && Array.isArray(parsed.entries) ? parsed : { version: 1, entries: [] };
33
+ } catch {
34
+ return { version: 1, entries: [] };
35
+ }
36
+ }
37
+
38
+ export function hasEvidenceEntry(cwd: string, run: ActiveRun, path: string): boolean {
39
+ const normalized = normalizeEvidencePath(path);
40
+ return readEvidenceIndex(cwd, run).entries.some((entry) => normalizeEvidencePath(entry.path) === normalized);
41
+ }
42
+
43
+ export function writeEvidenceFile(
44
+ cwd: string,
45
+ run: ActiveRun,
46
+ path: string,
47
+ content: string,
48
+ options: { kind?: string; command?: string; exitCode?: number } = {},
49
+ ): string {
50
+ const normalized = normalizeEvidencePath(path);
51
+ if (!normalized.startsWith("evidence/") || normalized === EVIDENCE_INDEX) {
52
+ throw new Error(`非法 evidence 路径:${path}`);
53
+ }
54
+
55
+ const absolutePath = runArtifactPath(cwd, run, normalized);
56
+ mkdirSync(dirname(absolutePath), { recursive: true });
57
+ writeFileSync(absolutePath, content.endsWith("\n") ? content : `${content}\n`, "utf8");
58
+ recordEvidence(cwd, run, normalized, options);
59
+ return absolutePath;
60
+ }
61
+
62
+ export function recordEvidence(
63
+ cwd: string,
64
+ run: ActiveRun,
65
+ path: string,
66
+ options: { kind?: string; command?: string; exitCode?: number } = {},
67
+ ): void {
68
+ const normalized = normalizeEvidencePath(path);
69
+ const absolutePath = join(runRoot(cwd, run), normalized);
70
+ if (!existsSync(absolutePath)) return;
71
+
72
+ const now = new Date().toISOString();
73
+ const size = statSync(absolutePath).size;
74
+ const index = readEvidenceIndex(cwd, run);
75
+ const existing = index.entries.find((entry) => normalizeEvidencePath(entry.path) === normalized);
76
+ if (existing) {
77
+ existing.kind = options.kind ?? existing.kind;
78
+ existing.command = options.command ?? existing.command;
79
+ existing.exitCode = options.exitCode ?? existing.exitCode;
80
+ existing.updatedAt = now;
81
+ existing.size = size;
82
+ } else {
83
+ index.entries.push({
84
+ path: normalized,
85
+ kind: options.kind ?? inferKind(normalized),
86
+ command: options.command,
87
+ exitCode: options.exitCode,
88
+ createdAt: now,
89
+ updatedAt: now,
90
+ size,
91
+ });
92
+ }
93
+
94
+ const indexPath = evidenceIndexPath(cwd, run);
95
+ mkdirSync(dirname(indexPath), { recursive: true });
96
+ writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8");
97
+ }
98
+
99
+ function inferKind(path: string): string {
100
+ const name = path.split("/").at(-1) ?? path;
101
+ return name.replace(/\.(md|txt|json)$/i, "");
102
+ }
103
+
104
+ function normalizeEvidencePath(path: string): string {
105
+ return path.replace(/\\/g, "/").replace(/^\/+/, "").replace(/^\.\//, "");
106
+ }
@@ -21,7 +21,7 @@ export function formatStatus(cwd: string, run: ActiveRun | undefined): string {
21
21
  `下一阶段:${next}`,
22
22
  `产品:${formatProductProfile(refreshed.profile)}`,
23
23
  `版本:${refreshed.version ?? "未选择"}`,
24
- `风险:${refreshed.risk ?? "未知"}`,
24
+ `风险:${formatRisk(refreshed)}`,
25
25
  `门禁:${refreshed.gate.passed ? "通过" : "阻塞"}`,
26
26
  refreshed.gate.reason ? `原因:${refreshed.gate.reason}` : undefined,
27
27
  "",
@@ -30,3 +30,9 @@ export function formatStatus(cwd: string, run: ActiveRun | undefined): string {
30
30
  .filter(Boolean)
31
31
  .join("\n");
32
32
  }
33
+
34
+ function formatRisk(run: ActiveRun): string {
35
+ const level = run.riskAssessment?.level ?? "未知";
36
+ const reason = run.riskAssessment?.reason?.trim();
37
+ return reason ? `${level}(${reason})` : level;
38
+ }
@@ -9,6 +9,7 @@ import { executionStepsBlockReason, planStepsBlockReason } from "./plan-steps.ts
9
9
  import { tddPlanBlockReason, tddVerifyBlockReason } from "./tdd-policy.ts";
10
10
  import { SDK_SIGNATURE_EVIDENCE, hasValidSdkSignatureEvidence, requiresSdkSignatureEvidence } from "./sdk-policy.ts";
11
11
  import { runRoot } from "./paths.ts";
12
+ import { EVIDENCE_INDEX, hasEvidenceEntry } from "./evidence.ts";
12
13
 
13
14
  const REQUIRED_MARKERS: Partial<Record<KdPhase, string[]>> = {
14
15
  plan: ["## 验证命令"],
@@ -21,77 +22,79 @@ const COSMIC_METADATA_EVIDENCE = "evidence/cosmic-metadata.json";
21
22
  const COSMIC_API_EVIDENCE = "evidence/cosmic-api.txt";
22
23
  const KSQL_LINT_EVIDENCE = "evidence/ksql-lint.txt";
23
24
 
24
- export function inspectGate(cwd: string, run: ActiveRun): GateResult {
25
- const currentIndex = PHASE_ORDER.indexOf(run.phase);
26
- const missing: string[] = [];
27
-
28
- for (let i = 0; i <= currentIndex; i++) {
29
- const phase = PHASE_ORDER[i];
30
- const artifact = PHASE_ARTIFACTS[phase];
31
- if (!artifactExists(cwd, run, artifact)) missing.push(artifact);
32
- }
25
+ type GateMode = "inspect" | "enter";
33
26
 
34
- const markerProblem = inspectMarkers(cwd, run, run.phase);
35
- const stepProblem = inspectStepState(cwd, run, run.phase);
36
- const evidenceProblem = inspectEvidence(cwd, run, run.phase);
37
- const questionProblem = inspectOpenQuestions(run);
38
- const reasonParts = [];
39
- if (missing.length > 0) reasonParts.push(missingArtifactsReason([...new Set(missing)]));
40
- if (markerProblem) reasonParts.push(markerProblem);
41
- if (stepProblem) reasonParts.push(stepProblem);
42
- if (evidenceProblem) reasonParts.push(evidenceProblem);
43
- if (questionProblem) reasonParts.push(questionProblem);
44
-
45
- return {
46
- passed: reasonParts.length === 0,
47
- reason: reasonParts.length > 0 ? reasonParts.join("; ") : undefined,
48
- checkedAt: new Date().toISOString(),
49
- };
27
+ export function inspectGate(cwd: string, run: ActiveRun): GateResult {
28
+ return gateResult(collectGateProblems(cwd, run, run.phase, "inspect"));
50
29
  }
51
30
 
52
31
  export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): GateResult {
53
- const targetIndex = PHASE_ORDER.indexOf(target);
32
+ return gateResult(collectGateProblems(cwd, run, target, "enter"));
33
+ }
34
+
35
+ function collectGateProblems(cwd: string, run: ActiveRun, phase: KdPhase, mode: GateMode): string[] {
36
+ const phaseIndex = PHASE_ORDER.indexOf(phase);
54
37
  const missing: string[] = [];
55
38
  const reasonParts: string[] = [];
56
39
 
57
- if (target !== "discuss" && !isKnownProduct(run.profile?.product ?? run.product)) {
58
- reasonParts.push(
59
- "不能离开 discuss:产品画像未知。下一步:根据需求或用户回答确认产品,然后执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>;如果无法判断,先用 kd_question 只问一个最阻塞的产品确认问题。",
60
- );
40
+ if (phase !== "discuss" && !isKnownProduct(run.profile?.product ?? run.product)) {
41
+ reasonParts.push(unknownProductReason());
61
42
  }
62
43
 
63
- for (let i = 0; i < targetIndex; i++) {
64
- const phase = PHASE_ORDER[i];
65
- const artifact = PHASE_ARTIFACTS[phase];
44
+ if (phaseIndex >= PHASE_ORDER.indexOf("ship") && !hasRiskAssessment(cwd, run)) {
45
+ reasonParts.push(unknownRiskReason());
46
+ }
47
+
48
+ const lastRequiredArtifactIndex = mode === "inspect" ? phaseIndex : phaseIndex - 1;
49
+ for (let i = 0; i <= lastRequiredArtifactIndex; i++) {
50
+ const requiredPhase = PHASE_ORDER[i];
51
+ const artifact = PHASE_ARTIFACTS[requiredPhase];
66
52
  if (!artifactExists(cwd, run, artifact)) missing.push(artifact);
67
53
  }
68
54
 
69
- if (target === "execute" && !artifactExists(cwd, run, PHASE_ARTIFACTS.plan)) {
55
+ if (mode === "enter" && phase === "execute" && !artifactExists(cwd, run, PHASE_ARTIFACTS.plan)) {
70
56
  missing.push(PHASE_ARTIFACTS.plan);
71
57
  }
72
58
 
73
- const flagshipPathProblem = target === "execute" ? flagshipPlanBlockReason(cwd, run, readArtifact(cwd, run, "plan") ?? "") : undefined;
59
+ const markerProblem = mode === "inspect" ? inspectMarkers(cwd, run, phase) : undefined;
60
+ const flagshipPathProblem = mode === "enter" && phase === "execute" ? flagshipPlanBlockReason(cwd, run, readArtifact(cwd, run, "plan") ?? "") : undefined;
61
+ const stepProblem = inspectStepState(cwd, run, phase);
62
+ const evidenceProblem = mode === "inspect" ? inspectEvidence(cwd, run, phase) : undefined;
63
+ const questionProblem = inspectOpenQuestions(run);
64
+
74
65
  if (flagshipPathProblem) {
75
66
  reasonParts.push(flagshipPathProblem);
76
67
  }
77
-
78
- const stepProblem = inspectStepState(cwd, run, target);
68
+ if (markerProblem) {
69
+ reasonParts.push(markerProblem);
70
+ }
79
71
  if (stepProblem) {
80
72
  reasonParts.push(stepProblem);
81
73
  }
82
-
83
- const questionProblem = inspectOpenQuestions(run);
74
+ if (evidenceProblem) {
75
+ reasonParts.push(evidenceProblem);
76
+ }
84
77
  if (questionProblem) {
85
78
  reasonParts.push(questionProblem);
86
79
  }
87
80
 
88
- missing.push(...missingEvidenceForPhase(cwd, run, target));
81
+ if (mode === "enter") {
82
+ missing.push(...missingEvidenceForPhase(cwd, run, phase));
89
83
 
90
- if (target === "ship" && !artifactExists(cwd, run, PHASE_ARTIFACTS.verify)) {
91
- missing.push(PHASE_ARTIFACTS.verify);
84
+ if (phase === "ship" && !artifactExists(cwd, run, PHASE_ARTIFACTS.verify)) {
85
+ missing.push(PHASE_ARTIFACTS.verify);
86
+ }
87
+ }
88
+
89
+ if (missing.length > 0) {
90
+ const uniqueMissing = [...new Set(missing)];
91
+ reasonParts.push(mode === "inspect" ? missingArtifactsReason(uniqueMissing) : missingForTargetReason(phase, uniqueMissing));
92
92
  }
93
93
 
94
- if (missing.length > 0) reasonParts.push(missingForTargetReason(target, [...new Set(missing)]));
94
+ return reasonParts;
95
+ }
96
+
97
+ function gateResult(reasonParts: string[]): GateResult {
95
98
  const reason = reasonParts.length > 0 ? reasonParts.join("; ") : undefined;
96
99
  return {
97
100
  passed: !reason,
@@ -100,6 +103,30 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
100
103
  };
101
104
  }
102
105
 
106
+ function unknownProductReason(): string {
107
+ return "不能离开 discuss:产品画像未知。下一步:根据需求或用户回答确认产品,然后执行 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>;如果无法判断,先用 kd_question 只问一个最阻塞的产品确认问题。";
108
+ }
109
+
110
+ function hasRiskAssessment(cwd: string, run: ActiveRun): boolean {
111
+ const level = run.riskAssessment?.level;
112
+ if (!level) return false;
113
+ if (run.riskAssessment?.reason.trim()) return true;
114
+ return riskSectionHasContent(readArtifact(cwd, run, "verify") ?? "") || riskSectionHasContent(readArtifact(cwd, run, "ship") ?? "");
115
+ }
116
+
117
+ function unknownRiskReason(): string {
118
+ return "不能进入 ship:风险等级或风险原因未知。下一步:根据 VERIFY.md 和 SHIP.md 的残余风险执行 /kd-risk <low|medium|high> <原因>,或在风险章节写入真实风险说明后再刷新门禁。";
119
+ }
120
+
121
+ function riskSectionHasContent(content: string): boolean {
122
+ const match = content.match(/##\s*(残余风险|风险)\s*\r?\n([\s\S]*?)(?=\r?\n##\s+|$)/);
123
+ if (!match) return false;
124
+ return match[2]
125
+ .split(/\r?\n/)
126
+ .map((line) => line.trim())
127
+ .some((line) => line && !/^[-*]?\s*(无|未知|待确认|待补充|none|unknown)$/i.test(line));
128
+ }
129
+
103
130
  function inspectOpenQuestions(run: ActiveRun): string | undefined {
104
131
  const open = (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
105
132
  if (open.length === 0) return undefined;
@@ -172,7 +199,8 @@ function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase):
172
199
 
173
200
  function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string): boolean {
174
201
  if (artifact === SDK_SIGNATURE_EVIDENCE) return hasValidSdkSignatureEvidence(cwd, run);
175
- return existsSync(join(runRoot(cwd, run), artifact));
202
+ if (artifact === EVIDENCE_INDEX) return existsSync(join(runRoot(cwd, run), artifact));
203
+ return existsSync(join(runRoot(cwd, run), artifact)) && hasEvidenceEntry(cwd, run, artifact);
176
204
  }
177
205
 
178
206
  function missingArtifactsReason(artifacts: string[]): string {
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import type { ActiveRun } from "./types.ts";
4
4
  import { runRoot } from "./paths.ts";
5
+ import { hasEvidenceEntry } from "./evidence.ts";
5
6
 
6
7
  export interface PlanStep {
7
8
  id: string;
@@ -49,7 +50,7 @@ export function executionStepsBlockReason(cwd: string, run: ActiveRun, plan: str
49
50
  }
50
51
 
51
52
  const evidencePaths = [...line.matchAll(EVIDENCE_PATTERN)].map((match) => normalizeEvidencePath(match[0]));
52
- if (evidencePaths.length === 0 || !evidencePaths.some((path) => existsSync(join(runRoot(cwd, run), path)))) {
53
+ if (evidencePaths.length === 0 || !evidencePaths.some((path) => existsSync(join(runRoot(cwd, run), path)) && hasEvidenceEntry(cwd, run, path))) {
53
54
  missingEvidence.push(step.id);
54
55
  }
55
56
  }
@@ -3,6 +3,7 @@ import { isAbsolute, join, relative } from "node:path";
3
3
  import type { ActiveRun } from "./types.ts";
4
4
  import { runRoot } from "./paths.ts";
5
5
  import { isSourceLikePath } from "./path-policy.ts";
6
+ import { hasEvidenceEntry } from "./evidence.ts";
6
7
 
7
8
  export const SDK_SIGNATURE_EVIDENCE = "evidence/sdk-signature.md";
8
9
 
@@ -15,6 +16,7 @@ export function hasValidSdkSignatureEvidence(cwd: string, run: ActiveRun): boole
15
16
  if (!requiresSdkSignatureEvidence(run)) return true;
16
17
  const path = join(runRoot(cwd, run), SDK_SIGNATURE_EVIDENCE);
17
18
  if (!existsSync(path)) return false;
19
+ if (!hasEvidenceEntry(cwd, run, SDK_SIGNATURE_EVIDENCE)) return false;
18
20
 
19
21
  const content = readFileSync(path, "utf8");
20
22
  return /(退出码|Exit)\s*[::]\s*0/i.test(content) && /(来源|Sources?)\s*[::]/i.test(content);
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
- import type { ActiveRun, KdPhase, KdQuestion } from "./types.ts";
2
+ import type { ActiveRun, KdPhase, KdQuestion, KdRisk, KdRiskSource } from "./types.ts";
3
3
  import { PHASE_ARTIFACTS, isKdPhase, nextPhase } from "./types.ts";
4
4
  import { activeRunPath, kdDir, runStatePath, runsDir } from "./paths.ts";
5
5
  import { defaultArtifactContent, ensureArtifact, ensureRunDirectories, writeArtifact } from "./artifacts.ts";
@@ -79,7 +79,6 @@ export function createActiveRun(cwd: string, goal: string, productInput?: string
79
79
  product: profile.product,
80
80
  version,
81
81
  profile,
82
- risk: "unknown",
83
82
  artifacts: {},
84
83
  questions: [],
85
84
  gate: {
@@ -143,6 +142,18 @@ export function updateProductProfile(cwd: string, run: ActiveRun, productInput:
143
142
  return run;
144
143
  }
145
144
 
145
+ export function updateRisk(cwd: string, run: ActiveRun, risk: KdRisk, reason: string, source: KdRiskSource = "manual"): ActiveRun {
146
+ run.riskAssessment = {
147
+ level: risk,
148
+ reason: reason.trim(),
149
+ source,
150
+ updatedAt: new Date().toISOString(),
151
+ };
152
+ run.gate = inspectGate(cwd, run);
153
+ writeActiveRun(cwd, run);
154
+ return run;
155
+ }
156
+
146
157
  export function ensurePhaseArtifact(cwd: string, run: ActiveRun, phase: KdPhase): string {
147
158
  const path = ensureArtifact(cwd, run, phase, defaultArtifactContent(phase));
148
159
  run.artifacts[phase] = PHASE_ARTIFACTS[phase];
@@ -3,6 +3,7 @@ import { isAbsolute, join, relative } from "node:path";
3
3
  import type { ActiveRun } from "./types.ts";
4
4
  import { runRoot } from "./paths.ts";
5
5
  import { isSourceLikePath } from "./path-policy.ts";
6
+ import { hasEvidenceEntry } from "./evidence.ts";
6
7
 
7
8
  export const TDD_RED_EVIDENCE = "evidence/tdd-red.md";
8
9
  export const TDD_GREEN_EVIDENCE = "evidence/tdd-green.md";
@@ -45,6 +46,7 @@ function validateTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green")
45
46
  const evidenceName = kind === "red" ? TDD_RED_EVIDENCE : TDD_GREEN_EVIDENCE;
46
47
  const evidencePath = join(runRoot(cwd, run), evidenceName);
47
48
  if (!existsSync(evidencePath)) return { valid: false, reason: `缺少 ${evidenceName}` };
49
+ if (!hasEvidenceEntry(cwd, run, evidenceName)) return { valid: false, reason: `${evidenceName} 未登记到 evidence/index.json` };
48
50
 
49
51
  const content = readFileSync(evidencePath, "utf8");
50
52
  const uncertainty = uncertaintyReason(content);
@@ -1,8 +1,16 @@
1
1
  import type { KdProduct, ProductProfile } from "../product/profile.ts";
2
2
 
3
3
  export type KdPhase = "discuss" | "spec" | "plan" | "execute" | "verify" | "ship";
4
- export type KdRisk = "unknown" | "low" | "medium" | "high";
4
+ export type KdRisk = "low" | "medium" | "high";
5
5
  export type KdRunStatus = "active" | "paused" | "done";
6
+ export type KdRiskSource = "manual" | "verify" | "ship";
7
+
8
+ export interface RiskAssessment {
9
+ level: KdRisk;
10
+ reason: string;
11
+ source: KdRiskSource;
12
+ updatedAt: string;
13
+ }
6
14
 
7
15
  export interface GateResult {
8
16
  passed: boolean;
@@ -34,7 +42,7 @@ export interface ActiveRun {
34
42
  product?: KdProduct;
35
43
  version?: string;
36
44
  profile?: ProductProfile;
37
- risk?: KdRisk;
45
+ riskAssessment?: RiskAssessment;
38
46
  artifacts: Record<string, string>;
39
47
  gate: GateResult;
40
48
  questions?: KdQuestion[];
@@ -5,7 +5,7 @@ import { promisify } from "node:util";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import type { ProductProfile } from "../product/profile.ts";
7
7
  import { readActiveRun } from "../harness/state.ts";
8
- import { runArtifactPath } from "../harness/paths.ts";
8
+ import { writeEvidenceFile } from "../harness/evidence.ts";
9
9
  import { searchKnowledge } from "../knowledge/search.ts";
10
10
  import { formatSearchResults } from "../knowledge/format.ts";
11
11
  import { resolveWorkspacePath } from "../platform/path.ts";
@@ -174,15 +174,15 @@ export function writeOfficialEvidence(cwd: string, evidenceFile: OfficialEvidenc
174
174
  const run = readActiveRun(cwd);
175
175
  if (!run) return undefined;
176
176
 
177
- const path = runArtifactPath(cwd, run, join("evidence", evidenceFile));
178
- mkdirSync(dirname(path), { recursive: true });
179
-
180
177
  const content =
181
178
  evidenceFile === "cosmic-metadata.json"
182
179
  ? formatJsonEvidence(evidenceFile, result)
183
180
  : `${formatCommandResult(result)}\nCaptured: ${new Date().toISOString()}\n`;
184
- writeFileSync(path, content.endsWith("\n") ? content : `${content}\n`, "utf8");
185
- return path;
181
+ return writeEvidenceFile(cwd, run, join("evidence", evidenceFile), content, {
182
+ kind: evidenceFile.replace(/\.(txt|json)$/i, ""),
183
+ command: result.command,
184
+ exitCode: result.exitCode,
185
+ });
186
186
  }
187
187
 
188
188
  function formatJsonEvidence(evidenceFile: OfficialEvidenceFile, result: CommandResult): string {
@@ -148,8 +148,6 @@ function planJavaBuild(cwd: string, profile: ProductProfile, target?: string): B
148
148
  const task = target ?? "build";
149
149
  const gradlewBat = join(cwd, "gradlew.bat");
150
150
  const gradlew = join(cwd, "gradlew");
151
- const mvnwBat = join(cwd, "mvnw.cmd");
152
- const mvnw = join(cwd, "mvnw");
153
151
 
154
152
  if (existsSync(gradlewBat)) {
155
153
  return buildPlan(profile, gradlewBat, [task], cwd, "检测到 Cosmic 家族 Java 项目的 Gradle wrapper。");
@@ -157,20 +155,11 @@ function planJavaBuild(cwd: string, profile: ProductProfile, target?: string): B
157
155
  if (existsSync(gradlew)) {
158
156
  return buildPlan(profile, gradlew, [task], cwd, "检测到 Cosmic 家族 Java 项目的 Gradle wrapper。");
159
157
  }
160
- if (existsSync(mvnwBat)) {
161
- return buildPlan(profile, mvnwBat, [target ?? "test"], cwd, "检测到 Cosmic 家族 Java 项目的 Maven wrapper。");
162
- }
163
- if (existsSync(mvnw)) {
164
- return buildPlan(profile, mvnw, [target ?? "test"], cwd, "检测到 Cosmic 家族 Java 项目的 Maven wrapper。");
165
- }
166
158
  if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) {
167
159
  return buildPlan(profile, "gradle", [task], cwd, "检测到 Gradle 构建文件,但未找到 wrapper。");
168
160
  }
169
- if (existsSync(join(cwd, "pom.xml"))) {
170
- return buildPlan(profile, "mvn", [target ?? "test"], cwd, "检测到 Maven pom.xml,但未找到 wrapper。");
171
- }
172
161
 
173
- throw new Error("未找到 Java 构建入口。期望在工作区根目录看到 gradlew、build.gradle、mvnwpom.xml。");
162
+ throw new Error("未找到 Java Gradle 构建入口。期望在工作区根目录看到 gradlew、gradlew.bat、build.gradle 或 build.gradle.kts。");
174
163
  }
175
164
 
176
165
  function planCsharpBuild(cwd: string, profile: ProductProfile, target?: string): BuildPlan {