kcode-pi 0.1.34 → 0.1.35

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.
@@ -77,7 +77,7 @@ function sdkLanguageForProfile(profile: ProductProfile, value: string | undefine
77
77
 
78
78
  function rejectNonCosmic(profile: ProductProfile): string | undefined {
79
79
  if (isCosmicFamily(profile)) return undefined;
80
- if (profile.product === "unknown") return "请先提供支持 Cosmic 平台能力的产品:cangqiong、xinghan 或 flagship。";
80
+ if (profile.product === "unknown") return "必须提供支持 Cosmic 平台能力的产品:cangqiong、xinghan 或 flagship。";
81
81
  return `当前产品 ${profile.product} 使用 ${profile.platform}/${profile.techStack},不适用 Cosmic 官方能力。`;
82
82
  }
83
83
 
@@ -90,7 +90,7 @@ async function runOrDryRun(
90
90
  const command = await commandPromise;
91
91
  if (dryRun) {
92
92
  return {
93
- content: [{ type: "text" as const, text: `仅展示命令,不执行:\n${command.display}` }],
93
+ content: [{ type: "text" as const, text: `输出命令预览;不执行:\n${command.display}` }],
94
94
  details: { command: command.display, dryRun: true },
95
95
  };
96
96
  }
@@ -119,11 +119,11 @@ function autoAdvanceAfterEvidence(cwd: string): { text: string; details: ReturnT
119
119
  const kdSearchTool = defineTool({
120
120
  name: "kd_search",
121
121
  label: "KD 搜索",
122
- description: "搜索 KCode 随包金蝶知识库,包括 SDK、插件生命周期、代码模式和常见实现建议。",
122
+ description: "搜索 KCode 随包金蝶知识库,包括 SDK、插件生命周期、代码模式和实现约束。",
123
123
  parameters: Type.Object({
124
- query: Type.String({ description: "要搜索的关键词、API、类名、表名或生命周期术语。" }),
124
+ query: Type.String({ description: "搜索关键词、API、类名、表名或生命周期术语。" }),
125
125
  product: Type.Optional(Type.String({ description: "金蝶产品:flagship、xinghan、cangqiong 或 enterprise。" })),
126
- edition: Type.Optional(Type.String({ description: "旧参数,等同于 product。优先使用 product。" })),
126
+ edition: Type.Optional(Type.String({ description: "旧参数,等同于 product;product 覆盖 edition。" })),
127
127
  limit: Type.Optional(Type.Number({ description: "最大结果数,默认 5。" })),
128
128
  }),
129
129
 
@@ -133,7 +133,7 @@ const kdSearchTool = defineTool({
133
133
  if (!scopes) {
134
134
  const guidance =
135
135
  profile.product === "unknown"
136
- ? "请先提供 product,例如 flagship、enterprise、xinghan 或 cangqiong。"
136
+ ? "必须提供 product,例如 flagship、enterprise、xinghan 或 cangqiong。"
137
137
  : "当前产品画像未配置可搜索知识范围。";
138
138
  return {
139
139
  content: [
@@ -141,7 +141,7 @@ const kdSearchTool = defineTool({
141
141
  type: "text",
142
142
  text: [
143
143
  `产品画像:${profile.product}/${profile.techStack}/${profile.language}`,
144
- "KCode 随包知识库搜索需要明确产品画像。",
144
+ "KCode 随包知识库搜索必须明确产品画像。",
145
145
  guidance,
146
146
  ].join("\n"),
147
147
  },
@@ -166,7 +166,7 @@ const kdTableTool = defineTool({
166
166
  parameters: Type.Object({
167
167
  table: Type.String({ description: "表名,例如 T_PUR_POORDER。" }),
168
168
  product: Type.Optional(Type.String({ description: "金蝶产品:flagship、xinghan、cangqiong 或 enterprise。" })),
169
- edition: Type.Optional(Type.String({ description: "旧参数,等同于 product。优先使用 product。" })),
169
+ edition: Type.Optional(Type.String({ description: "旧参数,等同于 product;product 覆盖 edition。" })),
170
170
  }),
171
171
 
172
172
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
@@ -175,8 +175,8 @@ const kdTableTool = defineTool({
175
175
  if (!edition) {
176
176
  const guidance =
177
177
  profile.product === "unknown"
178
- ? "请先提供 product。不要跨金蝶产品族猜测表结构。"
179
- : "没有元数据查证前,不要把旗舰版/企业版表结构假设复用到苍穹/星瀚/Cosmic。";
178
+ ? "必须提供 product。禁止跨金蝶产品族猜测表结构。"
179
+ : "没有元数据查证前,禁止把旗舰版/企业版表结构假设复用到苍穹/星瀚/Cosmic。";
180
180
  return {
181
181
  content: [
182
182
  {
@@ -204,12 +204,12 @@ const kdCheckTool = defineTool({
204
204
  name: "kd_check",
205
205
  label: "KD 检查",
206
206
  description:
207
- "检查金蝶 Java/C#/Python 插件代码中的魔法值、命名问题、循环内 DB 调用和空 catch 等问题。",
207
+ "检查金蝶 Java/C#/Python 插件代码中的魔法值、命名问题、循环内 DB 调用和空 catch",
208
208
  parameters: Type.Object({
209
- code: Type.Optional(Type.String({ description: "要检查的源码。与 path 二选一。" })),
210
- path: Type.Optional(Type.String({ description: "要检查的源码文件路径。与 code 二选一。" })),
209
+ code: Type.Optional(Type.String({ description: "待检查源码。与 path 二选一。" })),
210
+ path: Type.Optional(Type.String({ description: "待检查源码文件路径。与 code 二选一。" })),
211
211
  product: Type.Optional(Type.String({ description: "金蝶产品。未提供 language 时用于推导 Java 或 C#。" })),
212
- language: Type.Optional(Type.String({ description: "语言:java、csharp 或 python。会覆盖产品推导结果。" })),
212
+ language: Type.Optional(Type.String({ description: "语言:java、csharp 或 python。覆盖产品推导结果。" })),
213
213
  }),
214
214
 
215
215
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -226,7 +226,7 @@ const kdCheckTool = defineTool({
226
226
 
227
227
  if (!code) {
228
228
  return {
229
- content: [{ type: "text", text: "kd_check 需要提供 code 或 path。" }],
229
+ content: [{ type: "text", text: "kd_check 必须提供 code 或 path。" }],
230
230
  details: { error: "missing-code-or-path" },
231
231
  };
232
232
  }
@@ -250,8 +250,8 @@ const kdCosmicConfigTool = defineTool({
250
250
  description: "运行 Cosmic 家族金蝶产品的官方能力配置预检查。",
251
251
  parameters: Type.Object({
252
252
  product: Type.String({ description: "支持 Cosmic 平台能力的产品:cangqiong、xinghan 或 flagship。" }),
253
- config: Type.Optional(Type.String({ description: "可选 ok-cosmic.json 路径。默认按当前工作目录解析。" })),
254
- dryRun: Type.Optional(Type.Boolean({ description: "只返回命令,不实际执行。" })),
253
+ config: Type.Optional(Type.String({ description: "ok-cosmic.json 路径;省略时按当前工作目录解析。" })),
254
+ dryRun: Type.Optional(Type.Boolean({ description: "输出命令预览;不实际执行。" })),
255
255
  }),
256
256
 
257
257
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -268,14 +268,14 @@ const kdCosmicMetadataTool = defineTool({
268
268
  description: "查询官方 Cosmic 表单/单据元数据,包括字段、枚举值、操作和 SQL 表信息。",
269
269
  parameters: Type.Object({
270
270
  product: Type.String({ description: "支持 Cosmic 平台能力的产品:cangqiong、xinghan 或 flagship。" }),
271
- form: Type.String({ description: "Form ID、单据 ID 或中文单据名;多个目标可用逗号分隔。" }),
272
- config: Type.Optional(Type.String({ description: "可选 ok-cosmic.json 路径。" })),
273
- fuzzy: Type.Optional(Type.String({ description: "可选字段关键词,用空格或逗号分隔。" })),
274
- typeFilter: Type.Optional(Type.String({ description: "可选字段类型正则,例如 combo|check 或 decimal。" })),
275
- sql: Type.Optional(Type.Boolean({ description: "是否包含数据库表和字段信息。" })),
276
- op: Type.Optional(Type.Boolean({ description: "是否显示表单/单据操作。" })),
277
- showDetail: Type.Optional(Type.Boolean({ description: "是否显示详细元数据输出。" })),
278
- dryRun: Type.Optional(Type.Boolean({ description: "只返回命令,不实际执行。" })),
271
+ form: Type.String({ description: "Form ID、单据 ID 或中文单据名;多个目标使用逗号分隔。" }),
272
+ config: Type.Optional(Type.String({ description: "ok-cosmic.json 路径;省略时使用默认配置。" })),
273
+ fuzzy: Type.Optional(Type.String({ description: "字段关键词,使用空格或逗号分隔。" })),
274
+ typeFilter: Type.Optional(Type.String({ description: "字段类型正则,例如 combo|check 或 decimal。" })),
275
+ sql: Type.Optional(Type.Boolean({ description: "包含数据库表和字段信息。" })),
276
+ op: Type.Optional(Type.Boolean({ description: "显示表单/单据操作。" })),
277
+ showDetail: Type.Optional(Type.Boolean({ description: "显示详细元数据输出。" })),
278
+ dryRun: Type.Optional(Type.Boolean({ description: "输出命令预览;不实际执行。" })),
279
279
  }),
280
280
 
281
281
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -289,15 +289,15 @@ const kdCosmicMetadataTool = defineTool({
289
289
  const kdCosmicApiTool = defineTool({
290
290
  name: "kd_cosmic_api",
291
291
  label: "KD Cosmic API",
292
- description: "查询随包 Cosmic API 知识,获取类和方法线索;最终签名事实应以 kd_sdk_signature 或项目构建输出为准。",
292
+ description: "查询随包 Cosmic API 知识,输出类和方法线索;最终签名事实必须以 kd_sdk_signature 或项目构建输出为准。",
293
293
  parameters: Type.Object({
294
294
  product: Type.String({ description: "支持 Cosmic 平台能力的产品:cangqiong、xinghan 或 flagship。" }),
295
295
  mode: Type.String({ description: "查询模式:search、search-method 或 detail。" }),
296
296
  query: Type.String({ description: "类名、方法名或完整限定类名。" }),
297
- config: Type.Optional(Type.String({ description: "可选 ok-cosmic.json 路径。" })),
298
- method: Type.Optional(Type.String({ description: "detail 模式下的可选方法过滤条件。" })),
299
- compact: Type.Optional(Type.Boolean({ description: "支持时请求紧凑详情输出。" })),
300
- dryRun: Type.Optional(Type.Boolean({ description: "只返回命令,不实际执行。" })),
297
+ config: Type.Optional(Type.String({ description: "ok-cosmic.json 路径;省略时使用默认配置。" })),
298
+ method: Type.Optional(Type.String({ description: "detail 模式下的方法过滤条件。" })),
299
+ compact: Type.Optional(Type.Boolean({ description: "启用紧凑详情输出;命令支持时生效。" })),
300
+ dryRun: Type.Optional(Type.Boolean({ description: "输出命令预览;不实际执行。" })),
301
301
  }),
302
302
 
303
303
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -323,14 +323,14 @@ const kdSdkSignatureTool = defineTool({
323
323
  name: "kd_sdk_signature",
324
324
  label: "KD SDK 签名",
325
325
  description:
326
- "从当前项目真实存在的 SDK jar 或 dll 中检查方法/类型签名。涉及 API 签名事实时,优先使用它而不是随包知识库。",
326
+ "从当前项目真实存在的 SDK jar 或 dll 中检查方法/类型签名。API 签名事实必须使用此工具查证,不以随包知识库替代。",
327
327
  parameters: Type.Object({
328
328
  product: Type.Optional(Type.String({ description: "金蝶产品。未提供 language 时用于推导 Java 或 C#。" })),
329
329
  language: Type.Optional(Type.String({ description: "java 或 csharp。默认由产品画像推导。" })),
330
- query: Type.Optional(Type.String({ description: "要搜索的类/类型关键词,例如 QueryServiceHelper 或 DynamicObject。" })),
331
- className: Type.Optional(Type.String({ description: "已知时提供完整限定 Java/C# 类型名。" })),
332
- method: Type.Optional(Type.String({ description: "在匹配类/类型内过滤方法或属性;不会全局扫描所有方法。" })),
333
- path: Type.Optional(Type.String({ description: "可选 SDK lib/bin 目录或依赖根路径。默认从当前项目查找。" })),
330
+ query: Type.Optional(Type.String({ description: "类/类型搜索关键词,例如 QueryServiceHelper 或 DynamicObject。" })),
331
+ className: Type.Optional(Type.String({ description: "完整限定 Java/C# 类型名。" })),
332
+ method: Type.Optional(Type.String({ description: "在匹配类/类型内过滤方法或属性;禁止全局扫描所有方法。" })),
333
+ path: Type.Optional(Type.String({ description: "SDK lib/bin 目录或依赖根路径;省略时从当前项目查找。" })),
334
334
  limit: Type.Optional(Type.Number({ description: "最大检查 jar/dll/class 数量。默认 20 个结果类、200 个文件。" })),
335
335
  }),
336
336
 
@@ -361,8 +361,8 @@ const kdKsqlLintTool = defineTool({
361
361
  description: "对生成的 KSQL/SQL 文件运行官方 ok-ksql lint 检查。",
362
362
  parameters: Type.Object({
363
363
  product: Type.String({ description: "支持 Cosmic 平台能力的产品:cangqiong、xinghan 或 flagship。" }),
364
- path: Type.String({ description: "SQL/KSQL 文件路径,可为工作区相对路径或绝对路径。" }),
365
- dryRun: Type.Optional(Type.Boolean({ description: "只返回命令,不实际执行。" })),
364
+ path: Type.String({ description: "SQL/KSQL 文件路径;支持工作区相对路径或绝对路径。" }),
365
+ dryRun: Type.Optional(Type.Boolean({ description: "输出命令预览;不实际执行。" })),
366
366
  }),
367
367
 
368
368
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -379,8 +379,8 @@ const kdBuildTool = defineTool({
379
379
  description: "按产品画像运行或预览金蝶构建命令,支持 Cosmic Java 和企业版 C# 项目。",
380
380
  parameters: Type.Object({
381
381
  product: Type.String({ description: "金蝶产品:cangqiong、xinghan、flagship 或 enterprise。" }),
382
- target: Type.Optional(Type.String({ description: "Java 可提供 Gradle task;C# 可提供 .sln/.csproj 路径。" })),
383
- dryRun: Type.Optional(Type.Boolean({ description: "只返回构建命令,不实际执行。" })),
382
+ target: Type.Optional(Type.String({ description: "Java 使用 Gradle task;C# 使用 .sln/.csproj 路径。" })),
383
+ dryRun: Type.Optional(Type.Boolean({ description: "输出构建命令预览;不实际执行。" })),
384
384
  }),
385
385
 
386
386
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -400,11 +400,11 @@ const kdBuildTool = defineTool({
400
400
  const kdDebugTool = defineTool({
401
401
  name: "kd_debug",
402
402
  label: "KD 调试",
403
- description: "分析金蝶构建/运行日志或堆栈,给出可能原因和下一步检查建议。",
403
+ description: "分析金蝶构建/运行日志或堆栈,输出推断原因和检查指令。",
404
404
  parameters: Type.Object({
405
405
  text: Type.Optional(Type.String({ description: "日志文本或堆栈。与 path 二选一。" })),
406
406
  path: Type.Optional(Type.String({ description: "日志文件路径。与 text 二选一。" })),
407
- product: Type.Optional(Type.String({ description: "可选产品提示,用于补充上下文。" })),
407
+ product: Type.Optional(Type.String({ description: "产品上下文。" })),
408
408
  }),
409
409
 
410
410
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -434,10 +434,10 @@ const kdDocReadTool = defineTool({
434
434
  name: "kd_doc_read",
435
435
  label: "KD 文档读取",
436
436
  description:
437
- "读取 PDF、Word (.docx/.doc)、Excel (.xlsx/.xls) 或 CSV 文件并提取文本内容。对于 .pdf、.docx、.doc、.xlsx、.xls、.csv 文件,必须使用此工具而非 read 工具,因为 read 无法解析这些二进制格式。PDF 目前只支持可复制文本抽取;扫描型 PDF 需要另行提供图片或 OCR 结果。",
437
+ "读取 PDF、Word (.docx/.doc)、Excel (.xlsx/.xls) 或 CSV 文件并提取文本内容。对于 .pdf、.docx、.doc、.xlsx、.xls、.csv 文件,必须使用此工具而非 read 工具,因为 read 无法解析这些二进制格式。PDF 目前仅支持可复制文本抽取;扫描型 PDF 必须另行提供图片或 OCR 结果。",
438
438
  parameters: Type.Object({
439
439
  path: Type.String({ description: "文档文件路径,支持 .pdf、.docx、.doc、.xlsx、.xls、.csv。" }),
440
- sheet: Type.Optional(Type.String({ description: "Excel 工作表名或序号(从 1 开始)。默认读取第一个工作表。" })),
440
+ sheet: Type.Optional(Type.String({ description: "Excel 工作表名或序号(从 1 开始)。默认第一个工作表。" })),
441
441
  maxRows: Type.Optional(Type.Number({ description: "Excel/CSV 最大返回行数。默认 200。" })),
442
442
  maxPages: Type.Optional(Type.Number({ description: "PDF 最大提取页数。默认 50。" })),
443
443
  }),
@@ -516,7 +516,7 @@ function extractXlsx(filePath: string, sheetNameOrIndex?: string, maxRows?: numb
516
516
  } else if (sheetNames.includes(sheetNameOrIndex)) {
517
517
  sheetName = sheetNameOrIndex;
518
518
  } else {
519
- return `[Excel] 工作表 "${sheetNameOrIndex}" 不存在。可用:${sheetNames.join(", ")}`;
519
+ return `[Excel] 工作表 "${sheetNameOrIndex}" 不存在。有效工作表:${sheetNames.join(", ")}`;
520
520
  }
521
521
  } else {
522
522
  sheetName = sheetNames[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "面向金蝶开发的 Pi Coding Agent 启动器、工具包和 Harness 工作流",
5
5
  "type": "module",
6
6
  "private": false,
package/src/cli/kcode.ts CHANGED
@@ -98,8 +98,8 @@ export function doctor(cwd: string, args: string[] = []): KcodeCliResult {
98
98
  lines.push(`Pi CLI:${formatPiCliStatus(piCli, pi)}`);
99
99
  lines.push(`KCode version:${packageName}@${packageVersion}`);
100
100
  lines.push(`KCode package:${packageRoot}`);
101
- lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,请先运行 kcode init"}`);
102
- lines.push(`项目上下文:${existsSync(projectContextPath) ? projectContextPath : "未创建,请运行 kcode context"}`);
101
+ lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,运行 kcode init"}`);
102
+ lines.push(`项目上下文:${existsSync(projectContextPath) ? projectContextPath : "未创建,运行 kcode context"}`);
103
103
 
104
104
  if (existsSync(settingsPath)) {
105
105
  const settingsResult = readSettingsSafe(settingsPath);
@@ -199,7 +199,7 @@ export function start(cwd: string, piArgs: string[]): KcodeCliResult {
199
199
  if (!piCli) {
200
200
  return {
201
201
  exitCode: 1,
202
- output: `${init.output}\n未找到随包 Pi CLI 或全局 pi 命令。请重新安装 kcode-pi 后再运行 kcode start。`,
202
+ output: `${init.output}\n未找到随包 Pi CLI 或全局 pi 命令。重新安装 kcode-pi 后再运行 kcode start。`,
203
203
  };
204
204
  }
205
205
 
@@ -69,9 +69,9 @@ export function generateProjectContext(cwd: string): string {
69
69
  "",
70
70
  "- 本文件是 KCode 的项目记忆,计划或编辑代码前必须读取。",
71
71
  "- 业务需求不得生成 demo/sample/scaffold 代码。",
72
- "- 不要假设模块结构。必须基于下方真实路径,并在编辑前确认目标文件。",
73
- "- 调用文件工具时优先使用项目相对路径。在 Windows 中,不要把路径改写为 /mnt/<drive>/... 或 /<drive>/...;只有确需绝对路径时才使用 Windows 路径。",
74
- "- 如果本文件过期,计划前先运行 `kcode context --refresh` 重新生成。",
72
+ "- 禁止假设模块结构。必须基于下方真实路径,并在编辑前确认目标文件。",
73
+ "- 调用文件工具时默认使用项目相对路径。在 Windows 中禁止把路径改写为 /mnt/<drive>/... 或 /<drive>/...;绝对路径只允许使用 Windows 路径。",
74
+ "- 本文件过期时,计划前运行 `kcode context --refresh` 重新生成。",
75
75
  "- 只有 Harness 进入 `execute` 且 PLAN.md 写明真实目标路径后,才能写产品代码。",
76
76
  "",
77
77
  "## 项目结构摘要",
@@ -3,6 +3,7 @@ import type { ActiveRun, KdPhase } from "./types.ts";
3
3
  import { PHASE_ARTIFACTS } from "./types.ts";
4
4
  import { runArtifactPath, runRoot } from "./paths.ts";
5
5
  import type { ProductProfile } from "../product/profile.ts";
6
+ import { PLAN_REQUIRED_CHECK_LINES, formatPromptLines } from "./prompt-policy.ts";
6
7
 
7
8
  export function ensureRunDirectories(cwd: string, run: ActiveRun): void {
8
9
  mkdirSync(runRoot(cwd, run), { recursive: true });
@@ -86,7 +87,7 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
86
87
  "",
87
88
  "## 已检查的项目结构",
88
89
  "",
89
- "## 需要查看的文件",
90
+ "## 待读取文件",
90
91
  "",
91
92
  "## 目标源码根 / 路径",
92
93
  "",
@@ -99,9 +100,7 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
99
100
  "",
100
101
  "## 必需的金蝶查证项",
101
102
  "",
102
- "- Java/C# 代码涉及 SDK 类、方法、构造器、枚举、属性时,必须先用 kd_sdk_signature 或项目构建输出确认真实签名。",
103
- "- 知识库搜索、随包 Cosmic API 查询只能作为线索,不能作为最终方法签名事实。",
104
- "- SDK 签名证据:evidence/sdk-signature.md",
103
+ ...formatPromptLines(PLAN_REQUIRED_CHECK_LINES),
105
104
  "",
106
105
  "## 需求条目 / 依赖 / 批次",
107
106
  "",
@@ -120,12 +119,12 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
120
119
  "- 红灯证据:evidence/tdd-red.md",
121
120
  "- 绿灯证据:evidence/tdd-green.md",
122
121
  "- 红绿检查命令或工具:未知",
123
- "- Java 语法/编译检查:优先使用当前项目 Gradle 命令,例如 `./gradlew build`、`.\\gradlew.bat build` 或 `./gradlew :模块:build`。",
122
+ "- Java 语法/编译检查:使用当前项目 Gradle 命令,例如 `./gradlew build`、`.\\gradlew.bat build` 或 `./gradlew :模块:build`。",
124
123
  "- C# 语法/编译检查:使用 `dotnet build`、`dotnet build <.sln>` 或 `dotnet build <.csproj>`。",
125
124
  "- 允许的检查:本地 SDK 签名查证、官方 API/基类/方法查证、元数据查证、kd_check、Gradle/dotnet 构建输出、项目已有测试框架、外部接口最小验证。",
126
- "- 测试框架:优先使用项目已有测试基础设施。",
125
+ "- 测试框架:使用项目已有测试基础设施。",
127
126
  "- 命令无法运行时记录真实阻塞原因和残余风险,不能作为绿灯证据。",
128
- "- 如果无法自动化测试,记录一个产品相关、实现前应失败且实现后应通过的检查。",
127
+ "- 自动化测试不可执行时,记录一个产品相关、实现前应失败且实现后应通过的检查。",
129
128
  "- SDK 方法签名事实必须来自当前项目 jar/dll、构建输出或官方元数据。",
130
129
  "",
131
130
  "## 验证命令",
@@ -0,0 +1,302 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join, relative } from "node:path";
3
+ import type { ActiveRun } from "./types.ts";
4
+ import { readArtifact } from "./artifacts.ts";
5
+ import { hasEvidenceEntry } from "./evidence.ts";
6
+ import { runRoot } from "./paths.ts";
7
+ import { isSourceLikePath } from "./path-policy.ts";
8
+ import {
9
+ dataSourcePlanBlockedReason,
10
+ dataSourceWriteBlockedReason,
11
+ implementationPlanBlockedReason,
12
+ implementationWriteBlockedReason,
13
+ integrationPlanBlockedReason,
14
+ integrationWriteBlockedReason,
15
+ } from "./messages.ts";
16
+ import {
17
+ DATA_SOURCE_CONTEXT_FIELDS,
18
+ IMPLEMENTATION_CONTRACT_FIELDS,
19
+ INTEGRATION_CONTEXT_FIELDS,
20
+ type ContractField,
21
+ } from "./prompt-policy.ts";
22
+
23
+ export const COSMIC_METADATA_EVIDENCE = "evidence/cosmic-metadata.json";
24
+ export const DATA_SOURCE_EVIDENCE = "evidence/data-source.md";
25
+
26
+ const DATA_SOURCE_KEYWORDS =
27
+ /数据源|元数据|表单|单据|字段|实体|分录|单据体|基础资料|物料|客户|供应商|Form\s*ID|FormId|formid|DynamicObject|DataSet|SQL|KSQL|查询|保存|校验|插件|事件|操作|列表|表名|数据库|dbName|dbKey|下推|上推|转换|生成单据/i;
28
+ const IMPLEMENTATION_KEYWORDS =
29
+ /插件|服务|处理器|二开|开发|实现|新增|修改|保存|校验|联动|下推|上推|转换|生成|同步|接口|审批|提交|审核|操作|按钮|定时|任务|脚本|SQL|KSQL/i;
30
+ const INTEGRATION_KEYWORDS =
31
+ /第三方|外部系统|外部接口|接口对接|对接|接口文档|HTTP|HTTPS|REST|SOAP|Webhook|webhook|回调|推送|拉取|同步|上报|下发|开放平台|API\s*接口/i;
32
+
33
+ const IMPLEMENTATION_FIELDS = Object.fromEntries(IMPLEMENTATION_CONTRACT_FIELDS.map((field) => [field.id, field])) as Record<string, ContractField>;
34
+ const DATA_SOURCE_FIELDS = Object.fromEntries(DATA_SOURCE_CONTEXT_FIELDS.map((field) => [field.id, field])) as Record<string, ContractField>;
35
+ const INTEGRATION_FIELDS = Object.fromEntries(INTEGRATION_CONTEXT_FIELDS.map((field) => [field.id, field])) as Record<string, ContractField>;
36
+
37
+ export function requiresDataSourceEvidence(cwd: string, run: ActiveRun): boolean {
38
+ if (!run.profile?.requiresMetadataVerification) return false;
39
+ if (run.profile.product === "unknown") return false;
40
+ return DATA_SOURCE_KEYWORDS.test(dataSourcePlanningText(cwd, run, { includePlan: false })) || planDeclaresConcreteDataSourceWork(cwd, run);
41
+ }
42
+
43
+ export function dataSourceEvidenceForRun(run: ActiveRun): string | undefined {
44
+ if (!run.profile?.requiresMetadataVerification) return undefined;
45
+ if (run.profile.platform === "cosmic") return COSMIC_METADATA_EVIDENCE;
46
+ if (run.profile.product === "enterprise") return DATA_SOURCE_EVIDENCE;
47
+ return DATA_SOURCE_EVIDENCE;
48
+ }
49
+
50
+ export function hasValidDataSourceEvidence(cwd: string, run: ActiveRun): boolean {
51
+ const evidence = dataSourceEvidenceForRun(run);
52
+ if (!evidence) return true;
53
+ const path = join(runRoot(cwd, run), evidence);
54
+ if (!existsSync(path)) return false;
55
+ if (!hasEvidenceEntry(cwd, run, evidence)) return false;
56
+ if (evidence === COSMIC_METADATA_EVIDENCE) return true;
57
+ return dataSourceEvidenceContentLooksConcrete(readFileSync(path, "utf8"));
58
+ }
59
+
60
+ export function dataSourceProductionWriteBlockReason(cwd: string, run: ActiveRun | undefined, path: string | undefined): string | undefined {
61
+ if (!run || run.phase !== "execute") return undefined;
62
+ if (!path || !isSourceLikePath(path)) return undefined;
63
+
64
+ const normalized = normalizeRelativePath(cwd && isAbsolute(path) ? relative(cwd, path) : path);
65
+ if (normalized.startsWith(".pi/")) return undefined;
66
+
67
+ const implementationMissing = missingImplementationContractItems(cwd, run);
68
+ if (implementationMissing.length > 0) return implementationWriteBlockedReason(normalized, implementationMissing);
69
+
70
+ const integrationMissing = missingIntegrationContextItems(cwd, run);
71
+ if (integrationMissing.length > 0) return integrationWriteBlockedReason(normalized, integrationMissing);
72
+
73
+ if (!requiresDataSourceEvidence(cwd, run)) return undefined;
74
+ if (hasValidDataSourceEvidence(cwd, run)) return undefined;
75
+
76
+ return dataSourceWriteBlockedReason(normalized, dataSourceEvidenceForRun(run) ?? DATA_SOURCE_EVIDENCE);
77
+ }
78
+
79
+ export function dataSourceContextBlockReason(cwd: string, run: ActiveRun): string | undefined {
80
+ const implementationMissing = missingImplementationContractItems(cwd, run);
81
+ if (implementationMissing.length > 0) return implementationPlanBlockedReason(implementationMissing);
82
+
83
+ const integrationMissing = missingIntegrationContextItems(cwd, run);
84
+ if (integrationMissing.length > 0) return integrationPlanBlockedReason(integrationMissing);
85
+
86
+ if (!requiresDataSourceEvidence(cwd, run)) return undefined;
87
+ const missing = missingDataSourceContextItems(cwd, run);
88
+ return missing.length > 0 ? dataSourcePlanBlockedReason(missing) : undefined;
89
+ }
90
+
91
+ function dataSourcePlanningText(cwd: string, run: ActiveRun, options: { includePlan?: boolean } = { includePlan: true }): string {
92
+ return [run.goal ?? "", readArtifact(cwd, run, "spec") ?? "", options.includePlan === false ? "" : readArtifact(cwd, run, "plan") ?? ""].join("\n");
93
+ }
94
+
95
+ function planDeclaresConcreteDataSourceWork(cwd: string, run: ActiveRun): boolean {
96
+ const plan = readArtifact(cwd, run, "plan") ?? "";
97
+ const withoutBoilerplate = plan
98
+ .split(/\r?\n/)
99
+ .filter((line) => !/必须先确认真实数据源|数据源证据|进入 execute 前必须写明|不能只根据 API 文档|必需的金蝶查证项/.test(line))
100
+ .join("\n");
101
+ return DATA_SOURCE_KEYWORDS.test(withoutBoilerplate);
102
+ }
103
+
104
+ function missingImplementationContractItems(cwd: string, run: ActiveRun): string[] {
105
+ if (!requiresImplementationContract(cwd, run)) return [];
106
+ const text = dataSourcePlanningText(cwd, run);
107
+ const missing: string[] = [];
108
+
109
+ if (!hasTriggerOrEntry(text)) missing.push(IMPLEMENTATION_FIELDS.trigger.label);
110
+ if (!hasSourceObject(text)) missing.push(IMPLEMENTATION_FIELDS.source.label);
111
+ if (!hasTargetObject(text)) missing.push(IMPLEMENTATION_FIELDS.target.label);
112
+ if (!hasDataChangeContract(text)) missing.push(IMPLEMENTATION_FIELDS["data-change"].label);
113
+ if (!hasRuleOrCondition(text)) missing.push(IMPLEMENTATION_FIELDS.rules.label);
114
+ if (!hasFailureContract(text)) missing.push(IMPLEMENTATION_FIELDS.failure.label);
115
+ if (!hasAcceptanceSample(text)) missing.push(IMPLEMENTATION_FIELDS.acceptance.label);
116
+
117
+ return missing;
118
+ }
119
+
120
+ function requiresImplementationContract(cwd: string, run: ActiveRun): boolean {
121
+ if (!run.profile?.requiresMetadataVerification || run.profile.product === "unknown") return false;
122
+ return IMPLEMENTATION_KEYWORDS.test(dataSourcePlanningText(cwd, run, { includePlan: false })) || planDeclaresConcreteImplementationWork(cwd, run);
123
+ }
124
+
125
+ function planDeclaresConcreteImplementationWork(cwd: string, run: ActiveRun): boolean {
126
+ const plan = readArtifact(cwd, run, "plan") ?? "";
127
+ const withoutBoilerplate = plan
128
+ .split(/\r?\n/)
129
+ .filter((line) => !/实现就绪|进入 execute 前必须写明|第三方对接必须写明|必需的金蝶查证项|不要写模板代码/.test(line))
130
+ .join("\n");
131
+ return IMPLEMENTATION_KEYWORDS.test(withoutBoilerplate);
132
+ }
133
+
134
+ function missingDataSourceContextItems(cwd: string, run: ActiveRun): string[] {
135
+ const text = dataSourcePlanningText(cwd, run);
136
+ const missing: string[] = [];
137
+
138
+ if (!hasTargetObjectIdentifier(text)) missing.push(DATA_SOURCE_FIELDS["target-form"].label);
139
+ if (!hasPluginHook(text)) missing.push(DATA_SOURCE_FIELDS["plugin-hook"].label);
140
+ if (!hasFieldOrEntityIdentifier(text)) missing.push(DATA_SOURCE_FIELDS["field-entity"].label);
141
+ if (!hasDataAccessStrategy(text)) missing.push(DATA_SOURCE_FIELDS["data-access"].label);
142
+ if (usesSql(text) && !hasSqlIdentifier(text)) missing.push(DATA_SOURCE_FIELDS["sql-identifiers"].label);
143
+
144
+ return missing;
145
+ }
146
+
147
+ function hasTriggerOrEntry(text: string): boolean {
148
+ if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.trigger)) return true;
149
+ return /(?:触发入口|执行时机|触发时机|入口|事件|生命周期|插件类型|按钮|操作|定时|任务|保存前|保存后|提交后|审核后|Before\w+|After\w+)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:保存前|保存后|提交后|审核后|定时|按钮点击|操作执行)/i.test(text);
150
+ }
151
+
152
+ function hasSourceObject(text: string): boolean {
153
+ if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.source)) return true;
154
+ return /(?:源对象|源单|来源单据|来源表单|输入数据|数据来源|取数来源|源 FormId|源FormId|源表|源实体)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:从|读取|基于).{0,20}(?:单据|表单|实体|字段|DynamicObject|DataSet|SQL|KSQL)/i.test(text);
155
+ }
156
+
157
+ function hasTargetObject(text: string): boolean {
158
+ if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.target)) return true;
159
+ return /(?:目标对象|目标单据|目标表单|目标 FormId|目标FormId|输出结果|生成结果|下游单据|目标表|目标实体)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:下推到|生成|写入|更新|推送到).{0,20}(?:单据|表单|实体|字段|接口|系统|表)/i.test(text);
160
+ }
161
+
162
+ function hasDataChangeContract(text: string): boolean {
163
+ if (hasLabeledValue(text, IMPLEMENTATION_FIELDS["data-change"])) return true;
164
+ return /(?:数据变化|字段映射|字段对应|字段改写|修改字段|写入字段|赋值规则|转换规则|映射关系|更新字段)\s*[::=]\s*[^。\n\r;;]{2,}|(?:字段|field).{0,20}(?:->|映射|对应|写入|修改|赋值|更新)/i.test(text);
165
+ }
166
+
167
+ function hasRuleOrCondition(text: string): boolean {
168
+ if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.rules)) return true;
169
+ return /(?:业务规则|适用条件|过滤条件|执行条件|前置条件|判断条件|范围|何时执行|哪些数据)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:当|如果|仅|只处理|满足).{0,40}(?:时|则|才)/i.test(text);
170
+ }
171
+
172
+ function hasFailureContract(text: string): boolean {
173
+ if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.failure)) return true;
174
+ return /(?:失败处理|异常处理|错误处理|回滚|补偿|人工处理|失败后|重复执行|幂等|日志|告警)\s*[::=]\s*[^。\n\r,,;;]{2,}/i.test(text);
175
+ }
176
+
177
+ function missingIntegrationContextItems(cwd: string, run: ActiveRun): string[] {
178
+ const text = dataSourcePlanningText(cwd, run);
179
+ if (!requiresIntegrationContext(cwd, run)) return [];
180
+
181
+ const missing: string[] = [];
182
+ if (!hasInterfaceDocument(text)) missing.push(INTEGRATION_FIELDS["interface-doc"].label);
183
+ if (!hasIntegrationDirection(text)) missing.push(INTEGRATION_FIELDS.direction.label);
184
+ if (!hasEndpointAndAuth(text)) missing.push(INTEGRATION_FIELDS["endpoint-auth"].label);
185
+ if (!hasFieldMapping(text)) missing.push(INTEGRATION_FIELDS["field-mapping"].label);
186
+ if (!hasConcurrencyStrategy(text)) missing.push(INTEGRATION_FIELDS.concurrency.label);
187
+ if (!hasRetryTimeoutStrategy(text)) missing.push(INTEGRATION_FIELDS.retry.label);
188
+ if (!hasErrorHandlingStrategy(text)) missing.push(INTEGRATION_FIELDS.error.label);
189
+ if (!hasLoggingStrategy(text)) missing.push(INTEGRATION_FIELDS.logging.label);
190
+ if (!hasAcceptanceSample(text)) missing.push(INTEGRATION_FIELDS.samples.label);
191
+ return missing;
192
+ }
193
+
194
+ function requiresIntegrationContext(cwd: string, run: ActiveRun): boolean {
195
+ return INTEGRATION_KEYWORDS.test(dataSourcePlanningText(cwd, run, { includePlan: false })) || planDeclaresConcreteIntegrationWork(cwd, run);
196
+ }
197
+
198
+ function planDeclaresConcreteIntegrationWork(cwd: string, run: ActiveRun): boolean {
199
+ const plan = readArtifact(cwd, run, "plan") ?? "";
200
+ const withoutBoilerplate = plan
201
+ .split(/\r?\n/)
202
+ .filter((line) => !/第三方对接必须写明|接口文档来源\/版本|没有这些信息只能写模板代码|必需的金蝶查证项/.test(line))
203
+ .join("\n");
204
+ return INTEGRATION_KEYWORDS.test(withoutBoilerplate);
205
+ }
206
+
207
+ function hasInterfaceDocument(text: string): boolean {
208
+ if (hasLabeledValue(text, INTEGRATION_FIELDS["interface-doc"])) return true;
209
+ return /(?:接口文档|API\s*文档|文档来源|协议文档|OpenAPI|Swagger)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:https?:\/\/|\.pdf|\.docx?|\.xlsx?|swagger\.json|openapi\.json)/i.test(text);
210
+ }
211
+
212
+ function hasIntegrationDirection(text: string): boolean {
213
+ if (hasLabeledValue(text, INTEGRATION_FIELDS.direction)) return true;
214
+ return /(?:对接方向|触发时机|同步方向|数据流向|调用方|被调用方)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}|(?:金蝶调用第三方|第三方调用金蝶|入站|出站|推送|拉取|回调|定时|保存后|审核后|提交后)/i.test(text);
215
+ }
216
+
217
+ function hasEndpointAndAuth(text: string): boolean {
218
+ if (hasLabeledValue(text, INTEGRATION_FIELDS["endpoint-auth"])) return true;
219
+ return /(?:接口地址|Endpoint|URL|Base\s*URL|认证|鉴权|Token|AK|SK|AppKey|AppSecret|密钥|签名|OAuth)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text);
220
+ }
221
+
222
+ function hasFieldMapping(text: string): boolean {
223
+ if (hasLabeledValue(text, INTEGRATION_FIELDS["field-mapping"])) return true;
224
+ return /(?:字段映射|映射关系|字段对应|字段对照|mapping)\s*[::=]\s*[^。\n\r;;]{2,}|(?:第三方字段|外部字段).*(?:金蝶字段|单据字段|字段标识)/is.test(text);
225
+ }
226
+
227
+ function hasConcurrencyStrategy(text: string): boolean {
228
+ if (hasLabeledValue(text, INTEGRATION_FIELDS.concurrency)) return true;
229
+ return /(?:并发|幂等|去重|唯一键|业务主键|重复提交|锁|乐观锁|重入)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text);
230
+ }
231
+
232
+ function hasRetryTimeoutStrategy(text: string): boolean {
233
+ if (hasLabeledValue(text, INTEGRATION_FIELDS.retry)) return true;
234
+ return /(?:超时|timeout|重试|retry|限流|频率|rate\s*limit|熔断)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text);
235
+ }
236
+
237
+ function hasErrorHandlingStrategy(text: string): boolean {
238
+ if (hasLabeledValue(text, INTEGRATION_FIELDS.error)) return true;
239
+ return /(?:错误处理|异常处理|失败处理|失败补偿|补偿|回滚|死信|告警|人工处理)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text);
240
+ }
241
+
242
+ function hasLoggingStrategy(text: string): boolean {
243
+ if (hasLabeledValue(text, INTEGRATION_FIELDS.logging)) return true;
244
+ return /(?:日志|审计|留痕|脱敏|敏感信息|traceId|requestId|请求日志|响应日志)[^::=\n\r]{0,24}[::=]\s*[^。\n\r,,;;]{2,}/i.test(text);
245
+ }
246
+
247
+ function hasAcceptanceSample(text: string): boolean {
248
+ if (hasLabeledValue(text, IMPLEMENTATION_FIELDS.acceptance) || hasLabeledValue(text, INTEGRATION_FIELDS.samples)) return true;
249
+ return /(?:请求样例|响应样例|报文样例|验收样例|测试数据|示例报文|payload)\s*[::=]\s*[^。\n\r;;]{2,}/i.test(text);
250
+ }
251
+
252
+ function hasTargetObjectIdentifier(text: string): boolean {
253
+ if (hasLabeledValue(text, DATA_SOURCE_FIELDS["target-form"])) return true;
254
+ return /(?:Form\s*ID|FormId|formid|表单标识|单据标识|目标单据|目标表单)\s*[::=]\s*[A-Za-z0-9_.-]{2,}|(?:单据|表单)\s*[::=]\s*[^。\n\r,,;;]{2,}/i.test(text);
255
+ }
256
+
257
+ function hasPluginHook(text: string): boolean {
258
+ if (hasLabeledValue(text, DATA_SOURCE_FIELDS["plugin-hook"])) return true;
259
+ return /(?:插件类型|插件|事件|生命周期|触发时机|挂载点)\s*[::=]\s*[^。\n\r,,;;]{2,}|Before\w+|After\w+|Save|保存前|保存后|提交|审核|字段值改变|F7|列表/i.test(text);
260
+ }
261
+
262
+ function hasFieldOrEntityIdentifier(text: string): boolean {
263
+ if (hasLabeledValue(text, DATA_SOURCE_FIELDS["field-entity"])) return true;
264
+ return /(?:字段标识|字段|实体标识|实体|分录标识|单据体标识|Entry|Entity)\s*[::=]\s*[^。\n\r,,;;]{2,}|[A-Za-z][A-Za-z0-9_]*(?:Id|ID|No|Name|Qty|Amount)\b/.test(text);
265
+ }
266
+
267
+ function hasDataAccessStrategy(text: string): boolean {
268
+ if (hasLabeledValue(text, DATA_SOURCE_FIELDS["data-access"])) return true;
269
+ return /(?:读取方式|写入方式|数据访问|取数方式|数据源|读写策略)\s*[::=]\s*[^。\n\r,,;;]{2,}|(?:表单数据|模型数据|单据数据|DynamicObject|DataSet|QueryService|BusinessDataService|SQL|KSQL|直接读表|数据库查询)/i.test(text);
270
+ }
271
+
272
+ function usesSql(text: string): boolean {
273
+ const normalized = text.replace(/不(?:需要|使用|直接)?读表|不使用\s*(?:SQL|KSQL)|无需\s*(?:SQL|KSQL)|非\s*(?:SQL|KSQL)/gi, "");
274
+ return /\bSQL\b|\bKSQL\b|直接读表|数据库查询|表名/i.test(normalized);
275
+ }
276
+
277
+ function hasSqlIdentifier(text: string): boolean {
278
+ if (hasLabeledValue(text, DATA_SOURCE_FIELDS["sql-identifiers"])) return true;
279
+ return /(?:表名|数据库表|SQL\s*表|KSQL\s*表)\s*[::=]\s*[A-Za-z0-9_.$-]{2,}/i.test(text) && /(?:数据库字段|SQL\s*字段|字段名|列名)\s*[::=]\s*[A-Za-z0-9_.$,\s-]{2,}/i.test(text);
280
+ }
281
+
282
+ function dataSourceEvidenceContentLooksConcrete(content: string): boolean {
283
+ const text = content.trim();
284
+ if (text.length < 30) return false;
285
+ if (/待确认|未知|TODO|TBD|未提供|无数据源/i.test(text)) return false;
286
+ return /Form\s*ID|FormId|formid|单据|表单|字段|实体|表名|数据源|BOS|DynamicObject|DataSet|数据库|测试数据/i.test(text);
287
+ }
288
+
289
+ function normalizeRelativePath(path: string): string {
290
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
291
+ }
292
+
293
+ function hasLabeledValue(text: string, field: ContractField): boolean {
294
+ return field.aliases.some((alias) => {
295
+ const escaped = escapeRegExp(alias);
296
+ return new RegExp(`(?:^|[\\r\\n\\-\\*\\s])${escaped}\\s*[::=]\\s*[^\\r\\n。;;,,]{2,}`, "i").test(text);
297
+ });
298
+ }
299
+
300
+ function escapeRegExp(value: string): string {
301
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
302
+ }