kcode-pi 0.1.7 → 0.1.8

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
@@ -277,6 +277,7 @@ kcode start --provider openai --model gpt-4o
277
277
  /kd-gate
278
278
  /kd-advance [phase]
279
279
  /kd-artifact [phase] [content]
280
+ /kd-answer Q-001 <answer>
280
281
  ```
281
282
 
282
283
  典型开始方式:
@@ -369,7 +370,7 @@ ship 汇总变更、验证证据、风险和后续事项
369
370
 
370
371
  - Red evidence: evidence/tdd-red.md
371
372
  - Green evidence: evidence/tdd-green.md
372
- - Red/green command or tool: kd_cosmic_api detail / kd_cosmic_metadata / kd_check / build
373
+ - Red/green command or tool: kd_sdk_signature / kd_cosmic_metadata / kd_check / build
373
374
  - Do not add third-party test jars or frameworks only for this gate.
374
375
  ```
375
376
 
@@ -387,6 +388,12 @@ ship 汇总变更、验证证据、风险和后续事项
387
388
 
388
389
  如果 LLM 跳过步骤、没有记录 evidence,或只是口头声明完成,KCode 不允许进入 `verify`。
389
390
 
391
+ 结构化问题也会进入门禁。Agent 需要确认产品、目标单据、插件位置、高风险 SQL 或方案选择时,会使用 `kd_question` 记录问题;未回答的阻断问题会阻止进入下一阶段。用户回复后,Agent 会记录答案;也可以手动执行:
392
+
393
+ ```text
394
+ /kd-answer Q-001 采购订单
395
+ ```
396
+
390
397
  运行状态保存在当前业务项目:
391
398
 
392
399
  ```text
@@ -400,12 +407,14 @@ KCode 会向 Pi 注册这些金蝶工具:
400
407
 
401
408
  ```text
402
409
  kd_plan_status 查看当前 Harness run、阶段、产物和门禁
410
+ kd_question 记录/回答/查看阻断问题,未回答时阻止阶段推进
403
411
  kd_search 搜索内置金蝶 SDK 知识和代码模式
404
412
  kd_table 查询内置金蝶表结构
405
413
  kd_check 检查金蝶 Java/C# 插件代码
406
414
  kd_cosmic_config 运行官方 ok-cosmic 配置预检
407
415
  kd_cosmic_metadata 查询官方 Cosmic 表单/单据元数据
408
- kd_cosmic_api 查询官方 Cosmic API 签名和方法
416
+ kd_cosmic_api 查询随包 Cosmic API 知识线索
417
+ kd_sdk_signature 从当前项目实际 SDK jar/dll 中读取类和方法签名
409
418
  kd_ksql_lint 运行官方 ok-ksql SQL/KSQL lint
410
419
  kd_build 按产品画像执行或 dry-run 构建
411
420
  kd_debug 分析金蝶日志和堆栈
@@ -416,7 +425,17 @@ kd_debug 分析金蝶日志和堆栈
416
425
  - `kd_ksql_lint` 是内置 Node 静态检查器。
417
426
  - `kd_cosmic_config` 使用 Node 校验 Cosmic 官方能力配置;项目没有 `ok-cosmic.json` 时会自动使用 KCode 随包默认配置。
418
427
  - `kd_cosmic_metadata` 使用统一路由 API 查询真实单据/表单元数据,并在当前项目 `.pi/kd/official-skills/` 下维护 JSON 缓存。
419
- - `kd_cosmic_api` 查询随包金蝶知识库;需要精确方法签名时,仍要结合当前项目 SDK、编译输出或红绿证据确认。
428
+ - `kd_sdk_signature` 优先从当前业务项目的实际 SDK jar/dll 中读取类型和方法签名。Cosmic Java 依赖 `javap`,Enterprise C# 依赖 PowerShell 读取 DLL 元数据。
429
+ - `kd_cosmic_api` 查询随包金蝶知识库,只作为 API 线索和兜底;精确方法签名优先使用 `kd_sdk_signature`、当前项目 SDK 或编译输出确认。
430
+
431
+ 示例:
432
+
433
+ ```text
434
+ kd_sdk_signature product=flagship query=QueryServiceHelper method=loadSingle path=lib
435
+ kd_sdk_signature product=enterprise query=DynamicObject method=GetDynamicObject path=bin
436
+ ```
437
+
438
+ `method` 只是在已匹配的类/类型里过滤方法或属性;如果不知道类名,先用 `query` 搜类型关键词,再结合项目源码和构建文件缩小范围。
420
439
 
421
440
  `ok-cosmic.json` 是可选的 KCode 官方能力覆盖配置,不是苍穹工程模板里的 `cosmic.json`。业务项目里的 `cosmic.json` 通常只包含开发者标识、工程标识、MC 资源地址等运行环境信息,不能替代 `ok-cosmic.json`。
422
441
 
@@ -32,6 +32,7 @@ npm run smoke:checker
32
32
  npm run smoke:harness
33
33
  npm run smoke:official
34
34
  npm run smoke:build-debug
35
+ npm run smoke:sdk-signature
35
36
  npm run smoke:package
36
37
  npm run smoke:kcode-cli
37
38
  ```
@@ -45,6 +46,7 @@ npm run smoke:kcode-cli
45
46
  - Harness 阶段和门禁。
46
47
  - 官方能力 Node 适配器。
47
48
  - 构建/调试诊断。
49
+ - 当前项目 SDK jar/dll 签名读取。
48
50
  - package manifest、skills、vendor 文件完整性。
49
51
  - `kcode` 项目级入口逻辑。
50
52
 
@@ -4,6 +4,8 @@ import { formatStatus } from "../src/harness/format.ts";
4
4
  import { PHASE_ORDER, isKdPhase, type KdPhase } from "../src/harness/types.ts";
5
5
  import {
6
6
  advanceRun,
7
+ addQuestion,
8
+ answerQuestion,
7
9
  createActiveRun,
8
10
  ensurePhaseArtifact,
9
11
  readActiveRun,
@@ -107,7 +109,7 @@ function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof re
107
109
  "必须先理解当前业务项目已有目录、模块、包名、基类和本地封装,再决定文件位置和实现方式。",
108
110
  "路径规则:在 Windows 工作区内,优先使用项目相对路径;如需绝对路径必须使用 `D:\\...` 这类 Windows 路径,禁止把路径改写成 `/mnt/d/...`、`/d/...` 等 WSL/MSYS 风格路径。",
109
111
  "execute 阶段只能写 PLAN.md 明确列出的源码文件;如果目标文件不在计划内,必须先回到 plan 更新 PLAN.md。",
110
- "写生产源码前必须先有红灯证据 evidence/tdd-red.md;红绿证据可以是 API/基类/方法签名、元数据、编译、既有测试框架或外部接口最小验证,不要为了测试引入额外 jar。",
112
+ "写生产源码前必须先有红灯证据 evidence/tdd-red.md;红绿证据可以是 kd_sdk_signature 本地 SDK 签名、API/基类/方法签名、元数据、编译、既有测试框架或外部接口最小验证,不要为了测试引入额外 jar。",
111
113
  ].join("\n");
112
114
  }
113
115
 
@@ -158,8 +160,86 @@ const kdPlanStatusTool = defineTool({
158
160
  },
159
161
  });
160
162
 
163
+ const kdQuestionTool = defineTool({
164
+ name: "kd_question",
165
+ label: "KD Question",
166
+ description:
167
+ "Create, answer, or list structured Kingdee Harness questions. Open blocking questions stop phase advancement until answered.",
168
+ parameters: Type.Object({
169
+ action: Type.Optional(Type.String({ description: "ask, answer, or list. Defaults to ask." })),
170
+ id: Type.Optional(Type.String({ description: "Question id when action=answer, for example Q-001." })),
171
+ question: Type.Optional(Type.String({ description: "Question to ask when action=ask." })),
172
+ answer: Type.Optional(Type.String({ description: "User answer when action=answer." })),
173
+ reason: Type.Optional(Type.String({ description: "Why this question blocks or matters." })),
174
+ choices: Type.Optional(Type.Array(Type.String(), { description: "Optional concrete choices for the user." })),
175
+ blocking: Type.Optional(Type.Boolean({ description: "Whether the question blocks phase advancement. Defaults to true." })),
176
+ }),
177
+
178
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
179
+ const run = readActiveRun(ctx.cwd);
180
+ if (!run) {
181
+ return {
182
+ content: [{ type: "text", text: "No active Kingdee harness run. Start one with /kd-start <goal> first." }],
183
+ details: { error: "no-active-run" },
184
+ };
185
+ }
186
+
187
+ const action = (params.action ?? "ask").toLowerCase();
188
+ if (action === "list") {
189
+ const text = formatQuestions(run);
190
+ return { content: [{ type: "text", text }], details: { questions: run.questions ?? [] } };
191
+ }
192
+
193
+ if (action === "answer") {
194
+ if (!params.id || !params.answer) {
195
+ return {
196
+ content: [{ type: "text", text: "kd_question action=answer requires id and answer." }],
197
+ details: { error: "missing-answer-params" },
198
+ };
199
+ }
200
+ const answered = answerQuestion(ctx.cwd, run, params.id, params.answer);
201
+ if (!answered) {
202
+ return {
203
+ content: [{ type: "text", text: `Question not found: ${params.id}` }],
204
+ details: { error: "question-not-found", id: params.id },
205
+ };
206
+ }
207
+ appendQuestionEventToArtifact(ctx.cwd, run, [`- Answered ${answered.id}: ${answered.answer}`]);
208
+ return {
209
+ content: [{ type: "text", text: `Recorded answer for ${answered.id}. Gate refreshed.` }],
210
+ details: { question: answered, gate: readActiveRun(ctx.cwd)?.gate },
211
+ };
212
+ }
213
+
214
+ if (action !== "ask") {
215
+ return {
216
+ content: [{ type: "text", text: `Unknown kd_question action: ${params.action}. Use ask, answer, or list.` }],
217
+ details: { error: "unknown-action", action: params.action },
218
+ };
219
+ }
220
+
221
+ if (!params.question?.trim()) {
222
+ return {
223
+ content: [{ type: "text", text: "kd_question action=ask requires question." }],
224
+ details: { error: "missing-question" },
225
+ };
226
+ }
227
+
228
+ const question = addQuestion(ctx.cwd, run, {
229
+ question: params.question,
230
+ reason: params.reason,
231
+ choices: params.choices,
232
+ blocking: params.blocking,
233
+ });
234
+ appendQuestionEventToArtifact(ctx.cwd, run, formatQuestionArtifactLines(question));
235
+ const text = formatQuestionCard(question);
236
+ return { content: [{ type: "text", text }], details: { question, gate: readActiveRun(ctx.cwd)?.gate } };
237
+ },
238
+ });
239
+
161
240
  export default function (pi: ExtensionAPI) {
162
241
  pi.registerTool(kdPlanStatusTool);
242
+ pi.registerTool(kdQuestionTool);
163
243
 
164
244
  pi.on("input", async (event, ctx) => {
165
245
  if (event.source === "extension") return { action: "continue" };
@@ -299,4 +379,62 @@ export default function (pi: ExtensionAPI) {
299
379
  ctx.ui.notify(`Artifact ready: ${path}`, "info");
300
380
  },
301
381
  });
382
+
383
+ pi.registerCommand("kd-answer", {
384
+ description: "Answer a blocking Kingdee harness question: /kd-answer Q-001 <answer>",
385
+ handler: async (args, ctx) => {
386
+ const run = requireRun(ctx.cwd);
387
+ if (!run) {
388
+ ctx.ui.notify("No active Kingdee harness run. Use /kd-start <goal>.", "error");
389
+ return;
390
+ }
391
+ const [id, ...answerParts] = args.trim().split(/\s+/);
392
+ const answer = answerParts.join(" ").trim();
393
+ if (!id || !answer) {
394
+ ctx.ui.notify("Usage: /kd-answer Q-001 <answer>", "error");
395
+ return;
396
+ }
397
+ const answered = answerQuestion(ctx.cwd, run, id, answer);
398
+ if (!answered) {
399
+ ctx.ui.notify(`Question not found: ${id}`, "error");
400
+ return;
401
+ }
402
+ appendQuestionEventToArtifact(ctx.cwd, run, [`- Answered ${answered.id}: ${answered.answer}`]);
403
+ ctx.ui.notify(`Recorded answer for ${answered.id}`, "info");
404
+ },
405
+ });
406
+ }
407
+
408
+ function formatQuestions(run: NonNullable<ReturnType<typeof readActiveRun>>): string {
409
+ const questions = run.questions ?? [];
410
+ if (questions.length === 0) return "No harness questions recorded.";
411
+ return questions.map(formatQuestionCard).join("\n\n");
412
+ }
413
+
414
+ function formatQuestionCard(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string {
415
+ const lines = [
416
+ `Question ${question.id} [${question.status}${question.blocking ? ", blocking" : ""}]`,
417
+ `Phase: ${question.phase}`,
418
+ `Question: ${question.question}`,
419
+ question.reason ? `Reason: ${question.reason}` : undefined,
420
+ question.choices?.length ? `Choices: ${question.choices.join(" | ")}` : undefined,
421
+ question.answer ? `Answer: ${question.answer}` : undefined,
422
+ question.status === "open" ? `Reply with the answer, then record it using kd_question action=answer id=${question.id} answer=<answer>.` : undefined,
423
+ ];
424
+ return lines.filter(Boolean).join("\n");
425
+ }
426
+
427
+ function formatQuestionArtifactLines(question: NonNullable<NonNullable<ReturnType<typeof readActiveRun>>["questions"]>[number]): string[] {
428
+ return [
429
+ `- ${question.id} [${question.blocking ? "blocking" : "non-blocking"}] ${question.question}`,
430
+ question.reason ? ` - Reason: ${question.reason}` : undefined,
431
+ question.choices?.length ? ` - Choices: ${question.choices.join(" | ")}` : undefined,
432
+ " - Status: open",
433
+ ].filter(Boolean) as string[];
434
+ }
435
+
436
+ function appendQuestionEventToArtifact(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>, lines: string[]): void {
437
+ const existing = readArtifact(cwd, run, run.phase) ?? "";
438
+ const section = ["", "## Harness Questions", "", ...lines, ""].join("\n");
439
+ updatePhaseArtifact(cwd, run, run.phase, existing.includes("## Harness Questions") ? `${existing.trimEnd()}\n${lines.join("\n")}\n` : `${existing.trimEnd()}${section}`);
302
440
  }
@@ -9,6 +9,7 @@ import type { Edition, KnowledgeScope } from "../src/knowledge/types.ts";
9
9
  import { resolveProductProfile, type ProductProfile } from "../src/product/profile.ts";
10
10
  import { checkCode, formatCheckResults, type CheckLanguage } from "../src/rules/checker.ts";
11
11
  import { analyzeDebugText, formatDebugFindings, planBuild, readDebugInput, runBuild } from "../src/tools/build-debug.ts";
12
+ import { formatSdkSignatureResult, inspectSdkSignature, type SdkSignatureLanguage } from "../src/tools/sdk-signature.ts";
12
13
  import {
13
14
  cosmicApiCommand,
14
15
  cosmicConfigCommand,
@@ -64,6 +65,13 @@ function languageForProfile(profile: ProductProfile, value: string | undefined):
64
65
  return "java";
65
66
  }
66
67
 
68
+ function sdkLanguageForProfile(profile: ProductProfile, value: string | undefined): SdkSignatureLanguage {
69
+ if (value === "csharp" || value === "cs") return "csharp";
70
+ if (value === "java") return "java";
71
+ if (profile.platform === "enterprise-csharp") return "csharp";
72
+ return "java";
73
+ }
74
+
67
75
  function rejectNonCosmic(profile: ProductProfile): string | undefined {
68
76
  if (isCosmicFamily(profile)) return undefined;
69
77
  if (profile.product === "unknown") return "Provide a Cosmic-family product first: cangqiong, xinghan, flagship, or cosmic.";
@@ -265,7 +273,7 @@ const kdCosmicMetadataTool = defineTool({
265
273
  const kdCosmicApiTool = defineTool({
266
274
  name: "kd_cosmic_api",
267
275
  label: "KD Cosmic API",
268
- description: "Query official Cosmic API knowledge for class names, method names, and method signatures.",
276
+ description: "Query bundled Cosmic API knowledge for class and method clues. Use kd_sdk_signature or build output for final local SDK facts.",
269
277
  parameters: Type.Object({
270
278
  product: Type.String({ description: "Cosmic-family product: cangqiong, xinghan, flagship, or cosmic." }),
271
279
  mode: Type.String({ description: "search, search-method, or detail." }),
@@ -295,6 +303,39 @@ const kdCosmicApiTool = defineTool({
295
303
  },
296
304
  });
297
305
 
306
+ const kdSdkSignatureTool = defineTool({
307
+ name: "kd_sdk_signature",
308
+ label: "KD SDK Signature",
309
+ description:
310
+ "Inspect method/type signatures from SDK jars or dlls actually present in the current project. Prefer this over bundled knowledge for factual API signatures.",
311
+ parameters: Type.Object({
312
+ product: Type.Optional(Type.String({ description: "Kingdee product. Used to derive Java or C# when language is omitted." })),
313
+ language: Type.Optional(Type.String({ description: "java or csharp. Defaults from product profile." })),
314
+ query: Type.Optional(Type.String({ description: "Class/type keyword to search, for example QueryServiceHelper or DynamicObject." })),
315
+ className: Type.Optional(Type.String({ description: "Fully qualified Java/C# type name when known." })),
316
+ method: Type.Optional(Type.String({ description: "Optional method/property name filter within the matched class/type. Does not scan all methods globally." })),
317
+ path: Type.Optional(Type.String({ description: "Optional SDK lib/bin directory or specific dependency root. Defaults to current project." })),
318
+ limit: Type.Optional(Type.Number({ description: "Maximum jars/dlls/classes to inspect. Defaults to 20 result classes and 200 files." })),
319
+ }),
320
+
321
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
322
+ const profile = resolveToolProfile(params.product, undefined);
323
+ const language = sdkLanguageForProfile(profile, params.language);
324
+ const result = await inspectSdkSignature(ctx.cwd, {
325
+ language,
326
+ query: params.query,
327
+ className: params.className,
328
+ method: params.method,
329
+ path: params.path,
330
+ limit: params.limit,
331
+ });
332
+ return {
333
+ content: [{ type: "text", text: formatSdkSignatureResult(result) }],
334
+ details: { product: profile.product, ...result },
335
+ };
336
+ },
337
+ });
338
+
298
339
  const kdKsqlLintTool = defineTool({
299
340
  name: "kd_ksql_lint",
300
341
  label: "KD KSQL Lint",
@@ -377,6 +418,7 @@ export default function (pi: ExtensionAPI) {
377
418
  pi.registerTool(kdCosmicConfigTool);
378
419
  pi.registerTool(kdCosmicMetadataTool);
379
420
  pi.registerTool(kdCosmicApiTool);
421
+ pi.registerTool(kdSdkSignatureTool);
380
422
  pi.registerTool(kdKsqlLintTool);
381
423
  pi.registerTool(kdBuildTool);
382
424
  pi.registerTool(kdDebugTool);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Kingdee-specific package and harness for Pi Coding Agent",
5
5
  "type": "module",
6
6
  "private": false,
@@ -61,6 +61,7 @@
61
61
  "smoke:harness": "tsx scripts/smoke-harness.ts",
62
62
  "smoke:official": "tsx scripts/smoke-official-tools.ts",
63
63
  "smoke:build-debug": "tsx scripts/smoke-build-debug.ts",
64
+ "smoke:sdk-signature": "tsx scripts/smoke-sdk-signature.ts",
64
65
  "smoke:package": "tsx scripts/smoke-package.ts",
65
66
  "smoke:kcode-cli": "tsx scripts/smoke-kcode-cli.ts",
66
67
  "kcode": "tsx scripts/kcode.ts",
@@ -12,7 +12,7 @@ Checklist:
12
12
  - Magic values: status codes, operation names, field keys, table names, numbers, and business strings.
13
13
  - Naming: Java/C# naming conventions and project-local naming.
14
14
  - Lifecycle: correct plugin base class and lifecycle method for the target behavior.
15
- - SDK usage: no invented API names; uncertain APIs must be verified through `kd_search`.
15
+ - SDK usage: no invented API names; uncertain Java/C# APIs must be verified through `kd_sdk_signature` against current project jars/dlls, or by compile/build evidence. Use `kd_search` only for guidance.
16
16
  - Database access: no DB query or save operation inside loops unless explicitly justified.
17
17
  - Exception handling: no empty catch blocks; errors should be logged or converted to business exceptions as appropriate.
18
18
  - Resource cleanup: close or dispose resources where the SDK requires it.
@@ -23,4 +23,3 @@ Output expectations:
23
23
  - Lead with concrete findings and file/line references when reviewing code.
24
24
  - Separate errors, warnings, and improvement suggestions.
25
25
  - State which checks were not possible because context or tools were missing.
26
-
@@ -46,7 +46,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
46
46
 
47
47
  3. 通过官方适配器验证产品事实。
48
48
  - 代码引用字段、表单 ID、单据名称、枚举/下拉值、实体 ID、操作编码、SQL 字段时,使用 `kd_cosmic_metadata`。
49
- - 类名、方法名、方法签名、继承关系、Override 签名未被内置知识验证时,使用 `kd_cosmic_api`。
49
+ - 类名、方法名、方法签名、继承关系、Override 签名不确定时,优先使用 `kd_sdk_signature` 从当前项目实际 SDK jar 中验证;`kd_cosmic_api` 只作为随包知识线索和兜底。
50
50
  - 多个表单或字段尽量合并到一次元数据查询。
51
51
  - 元数据查询和 API 签名查询互不依赖时并行执行。
52
52
 
@@ -63,7 +63,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
63
63
 
64
64
  ## 硬性规则
65
65
 
66
- - 不臆造字段 key、表单 ID、操作名、枚举值、表名或 SDK 方法。
66
+ - 不臆造字段 key、表单 ID、操作名、枚举值、表名或 SDK 方法;本地 SDK 查不到且编译未验证时,不把知识库结果当作最终签名事实。
67
67
  - 不把 Enterprise C# 命名空间或生命周期假设用于 Cosmic Java。
68
68
  - 星空旗舰版不允许猜目录或写 demo/sample;如果无法判断真实代码位置,先询问或更新计划,不要直接创建新目录。
69
69
  - 不在数据绑定前的初始化阶段操作 UI 控件。
@@ -78,7 +78,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
78
78
  完成执行工作时说明:
79
79
 
80
80
  - 修改了哪些文件。
81
- - 使用了哪些官方检查:配置、元数据、API
81
+ - 使用了哪些检查:配置、元数据、本地 SDK 签名、API 知识、编译。
82
82
  - 哪些事实仍无法验证。
83
83
  - 验证命令和结果。
84
84
  - `EXECUTION.md` 中记录相对 `PLAN.md` 的偏差。
@@ -17,7 +17,7 @@ description: 金蝶 Cosmic 体系 Java 插件和 KSQL 代码审查技能,按
17
17
 
18
18
  - 存在 harness run 时,先用 `kd_plan_status` 查看产品画像。
19
19
  - 用 `kd_search` 查询 Cosmic 审查清单、生命周期、平台约束、KSQL 和单测指导。
20
- - SDK 签名或生命周期方法不确定时,用 `kd_cosmic_api` 验证。
20
+ - SDK 签名或生命周期方法不确定时,优先用 `kd_sdk_signature` 从当前项目实际 SDK jar 验证;查不到时再用 `kd_cosmic_api` 获取知识库线索,并要求编译或人工证据兜底。
21
21
  - 变更中用到字段、操作、枚举值、表名、数据库列时,用 `kd_cosmic_metadata` 验证。
22
22
  - 先运行 `kd_check` 做基础静态检查,再按下方清单深入审查。
23
23
  - 对新增或修改的 SQL/KSQL 文件运行 `kd_ksql_lint`。
@@ -84,7 +84,7 @@ description: 金蝶 Cosmic 体系 Java 插件和 KSQL 代码审查技能,按
84
84
  随后说明:
85
85
 
86
86
  - 已运行和未运行的检查。
87
- - 已验证的产品、元数据和 API 事实。
87
+ - 已验证的产品、元数据、本地 SDK 签名和 API 线索。
88
88
  - 剩余风险或缺失上下文。
89
89
 
90
90
  如果没有发现问题,明确说明未发现问题,并列出剩余测试或证据缺口。
@@ -25,7 +25,7 @@ description: 金蝶 Cosmic 体系 Java 单元测试生成和审查技能,适
25
25
  - 阅读被测类和最近的测试基类。
26
26
  - 新增测试风格前,先搜索同模块同包下已有测试。
27
27
  - 用 `kd_search` 查询 Cosmic 单测指导和模块测试模式。
28
- - 仅在测试或被测代码使用的 SDK 方法签名不确定时,使用 `kd_cosmic_api`。
28
+ - 仅在测试或被测代码使用的 SDK 方法签名不确定时,优先使用 `kd_sdk_signature` 从当前项目实际 SDK jar 验证;查不到时再用 `kd_cosmic_api` 获取线索,并以编译结果兜底。
29
29
 
30
30
  ## 测试模式选择
31
31
 
@@ -56,7 +56,7 @@ POJO 或简单枚举场景可以简要说明后直接实现。
56
56
  ## 测试质量规则
57
57
 
58
58
  - 使用项目已有 JUnit 和 Mockito 版本。
59
- - 如果项目没有既有且允许使用的单测框架,不要新增 JUnit、Mockito 或额外 jar;改用 Harness 的红绿验证门禁,例如 API/基类/方法签名、元数据、编译或最小外部接口验证。
59
+ - 如果项目没有既有且允许使用的单测框架,不要新增 JUnit、Mockito 或额外 jar;改用 Harness 的红绿验证门禁,例如 `kd_sdk_signature` 本地 SDK 签名、元数据、编译或最小外部接口验证。
60
60
  - import 保持显式;项目风格允许时也不要使用宽泛静态通配导入。
61
61
  - 每个测试方法必须有真实断言或交互验证。
62
62
  - 避免 `assertTrue(true)`、`assertEquals(x, x)` 或只执行不验证的测试。
@@ -19,7 +19,7 @@ Analysis approach:
19
19
  - permission or authorization failure
20
20
  - null dynamic object or missing entry rows
21
21
  - repeated DB calls in loops causing slow saves/submits
22
- - Use `kd_search` for uncertain SDK behavior and `kd_table` for table assumptions.
22
+ - Use `kd_sdk_signature` for uncertain SDK types/methods against current project jars/dlls, `kd_search` for guidance, and `kd_table`/metadata tools for table assumptions.
23
23
 
24
24
  Output expectations:
25
25
 
@@ -27,4 +27,3 @@ Output expectations:
27
27
  - Provide the next concrete inspection step.
28
28
  - Suggest a minimal fix, then verification evidence needed to prove it.
29
29
  - If evidence is insufficient, say exactly what log, stack trace, metadata, or code file is missing.
30
-
@@ -10,7 +10,7 @@ Use this skill only after `PLAN.md` exists.
10
10
  Goal:
11
11
 
12
12
  - Implement only the approved plan.
13
- - Use `kd_search` and `kd_table` before relying on Kingdee APIs or table schemas.
13
+ - Use `kd_sdk_signature` before relying on uncertain Java/C# SDK APIs. Use `kd_search` for guidance and `kd_table`/metadata tools for table schemas.
14
14
  - Update `.pi/kd/runs/<run-id>/EXECUTION.md` with every planned `STEP-###`, changed files, and evidence files.
15
15
 
16
16
  Rules:
@@ -20,7 +20,7 @@ Rules:
20
20
  - For 星空旗舰版, edit only the real target path recorded in `PLAN.md` after inspecting the project. If `code/` exists, follow its actual layout; if it does not, follow the discovered source root or existing target file. Do not create demo/sample code or root-level `src/main/java` by guesswork.
21
21
  - Do not mark work complete until verification runs.
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
- - Before writing production source files, run the planned red check and record failing output in `evidence/tdd-red.md`. This can be API/base-class/method-signature, metadata, compile/build, existing project test, or minimal external-interface evidence.
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
25
  - 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.
26
26
  - If implementation needs a plan change, update `PLAN.md` first.
@@ -13,12 +13,13 @@ Inputs to establish before coding:
13
13
  - Plugin type and correct base class.
14
14
  - Target bill/entity/form and lifecycle event.
15
15
  - Business rule and acceptance criteria.
16
- - Relevant SDK/table facts from `kd_search` or `kd_table`.
16
+ - Relevant SDK/table facts from `kd_sdk_signature`, `kd_search`, `kd_table`, or product metadata tools.
17
17
 
18
18
  Rules:
19
19
 
20
20
  - Generate code only after the active run has a `PLAN.md`.
21
21
  - Use the correct base class only when verified for the target product family.
22
+ - Verify uncertain Java/C# class names, base classes, methods, and overloads against current project SDK jars/dlls with `kd_sdk_signature`; bundled knowledge is not enough for final signature facts.
22
23
  - For 星空旗舰版, generate or edit product code only under the real target path selected in `PLAN.md` after project inspection. Follow the existing layout, whether it uses `code/`, app modules, cloud modules, or no module split; never create demo/sample code or root-level `src/main/java` by guesswork.
23
24
  - Use `kd.bos.*` style packages for Cosmic-family Java code and `Kingdee.BOS.*` style namespaces for enterprise C# code.
24
25
  - Do not reuse Cosmic/Xinghan/Cangqiong APIs for enterprise C#.
@@ -13,10 +13,10 @@ Goal:
13
13
  - List files to inspect before editing.
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
- - List required `kd_search` and `kd_table` lookups.
16
+ - List required `kd_sdk_signature`, `kd_search`, `kd_table`, metadata, and build/compile checks.
17
17
  - List `## Execution Steps` using `- [ ] STEP-001: ...` style IDs.
18
18
  - List `## TDD / Red-Green Checks` with red evidence, green evidence, and the command/tool or product-specific check.
19
- - Do not plan to add third-party test jars or frameworks only for red/green checks. For Kingdee plugin work, prefer API/base-class/method-signature checks, metadata checks, compile/build checks, existing project tests, or minimal external-interface tests.
19
+ - 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.
20
20
  - Define validation commands and expected evidence.
21
21
  - Add rollback or containment notes for medium/high risk work.
22
22
 
@@ -27,4 +27,4 @@ Gate:
27
27
  - A plan without validation commands is incomplete.
28
28
  - A plan without structured `STEP-001` execution steps is incomplete.
29
29
  - A plan without TDD red/green checks is incomplete.
30
- - A plan that relies on unverified Kingdee API names is incomplete.
30
+ - 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.
@@ -30,11 +30,13 @@ export function inspectGate(cwd: string, run: ActiveRun): GateResult {
30
30
  const markerProblem = inspectMarkers(cwd, run, run.phase);
31
31
  const stepProblem = inspectStepState(cwd, run, run.phase);
32
32
  const evidenceProblem = inspectEvidence(cwd, run, run.phase);
33
+ const questionProblem = inspectOpenQuestions(run);
33
34
  const reasonParts = [];
34
35
  if (missing.length > 0) reasonParts.push(`缺少必需产物:${[...new Set(missing)].join(", ")}`);
35
36
  if (markerProblem) reasonParts.push(markerProblem);
36
37
  if (stepProblem) reasonParts.push(stepProblem);
37
38
  if (evidenceProblem) reasonParts.push(evidenceProblem);
39
+ if (questionProblem) reasonParts.push(questionProblem);
38
40
 
39
41
  return {
40
42
  passed: reasonParts.length === 0,
@@ -72,6 +74,11 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
72
74
  reasonParts.push(stepProblem);
73
75
  }
74
76
 
77
+ const questionProblem = inspectOpenQuestions(run);
78
+ if (questionProblem) {
79
+ reasonParts.push(questionProblem);
80
+ }
81
+
75
82
  missing.push(...missingEvidenceForPhase(cwd, run, target));
76
83
 
77
84
  if (target === "ship" && !artifactExists(cwd, run, PHASE_ARTIFACTS.verify)) {
@@ -87,6 +94,12 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
87
94
  };
88
95
  }
89
96
 
97
+ function inspectOpenQuestions(run: ActiveRun): string | undefined {
98
+ const open = (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
99
+ if (open.length === 0) return undefined;
100
+ return `存在未回答的阻断问题:${open.map((question) => `${question.id} ${question.question}`).join(";")}`;
101
+ }
102
+
90
103
  function inspectStepState(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
91
104
  if (phase === "execute") {
92
105
  const plan = readArtifact(cwd, run, "plan") ?? "";
@@ -154,7 +167,7 @@ function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
154
167
 
155
168
  function planHasApiRequirement(cwd: string, run: ActiveRun): boolean {
156
169
  const plan = readArtifact(cwd, run, "plan") ?? "";
157
- return /kd_cosmic_api|cosmic-api|api signature|method signature|sdk.*签名|方法签名|接口签名/i.test(plan);
170
+ return /kd_sdk_signature|kd_cosmic_api|cosmic-api|api signature|method signature|sdk.*签名|方法签名|接口签名/i.test(plan);
158
171
  }
159
172
 
160
173
  function runHasKsqlDelivery(cwd: string, run: ActiveRun): boolean {
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import type { ActiveRun, KdPhase } from "./types.ts";
2
+ import type { ActiveRun, KdPhase, KdQuestion } from "./types.ts";
3
3
  import { PHASE_ARTIFACTS, isKdPhase, nextPhase } from "./types.ts";
4
4
  import { activeRunPath, kdDir, runsDir } from "./paths.ts";
5
5
  import { defaultArtifactContent, ensureArtifact, ensureRunDirectories, writeArtifact } from "./artifacts.ts";
@@ -14,6 +14,7 @@ export function readActiveRun(cwd: string): ActiveRun | undefined {
14
14
  const parsed = JSON.parse(readFileSync(path, "utf8")) as ActiveRun;
15
15
  if (!parsed.id || !isKdPhase(parsed.phase)) return undefined;
16
16
  parsed.artifacts ??= {};
17
+ parsed.questions ??= [];
17
18
  const legacyEdition = (parsed as ActiveRun & { edition?: string }).edition;
18
19
  parsed.profile = parsed.profile ?? profileForProduct(parsed.product ?? resolveProductProfile(legacyEdition).product);
19
20
  parsed.product = parsed.profile.product;
@@ -41,6 +42,7 @@ export function createActiveRun(cwd: string, goal: string, productInput?: string
41
42
  profile,
42
43
  risk: "unknown",
43
44
  artifacts: {},
45
+ questions: [],
44
46
  gate: {
45
47
  passed: false,
46
48
  reason: "CONTEXT.md and product profile are required before moving to spec",
@@ -55,6 +57,43 @@ export function createActiveRun(cwd: string, goal: string, productInput?: string
55
57
  return run;
56
58
  }
57
59
 
60
+ export function addQuestion(
61
+ cwd: string,
62
+ run: ActiveRun,
63
+ input: { question: string; reason?: string; choices?: string[]; blocking?: boolean },
64
+ ): KdQuestion {
65
+ const existing = run.questions ?? [];
66
+ const question: KdQuestion = {
67
+ id: createQuestionId(existing.length + 1),
68
+ phase: run.phase,
69
+ question: input.question.trim(),
70
+ reason: input.reason?.trim() || undefined,
71
+ choices: input.choices?.map((choice) => choice.trim()).filter(Boolean),
72
+ blocking: input.blocking ?? true,
73
+ status: "open",
74
+ createdAt: new Date().toISOString(),
75
+ };
76
+ run.questions = [...existing, question];
77
+ run.gate = inspectGate(cwd, run);
78
+ writeActiveRun(cwd, run);
79
+ return question;
80
+ }
81
+
82
+ export function answerQuestion(cwd: string, run: ActiveRun, id: string, answer: string): KdQuestion | undefined {
83
+ const question = (run.questions ?? []).find((item) => item.id === id);
84
+ if (!question) return undefined;
85
+ question.status = "answered";
86
+ question.answer = answer.trim();
87
+ question.answeredAt = new Date().toISOString();
88
+ run.gate = inspectGate(cwd, run);
89
+ writeActiveRun(cwd, run);
90
+ return question;
91
+ }
92
+
93
+ export function openBlockingQuestions(run: ActiveRun): KdQuestion[] {
94
+ return (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
95
+ }
96
+
58
97
  export function updateProductProfile(cwd: string, run: ActiveRun, productInput: string, version?: string): ActiveRun {
59
98
  const profile = resolveProductProfile(productInput);
60
99
  run.product = profile.product;
@@ -115,3 +154,7 @@ function createRunId(goal: string): string {
115
154
  .slice(0, 40);
116
155
  return slug ? `${stamp}-${slug}` : stamp;
117
156
  }
157
+
158
+ function createQuestionId(sequence: number): string {
159
+ return `Q-${String(sequence).padStart(3, "0")}`;
160
+ }
@@ -9,6 +9,19 @@ export interface GateResult {
9
9
  checkedAt: string;
10
10
  }
11
11
 
12
+ export interface KdQuestion {
13
+ id: string;
14
+ phase: KdPhase;
15
+ question: string;
16
+ reason?: string;
17
+ choices?: string[];
18
+ blocking: boolean;
19
+ status: "open" | "answered";
20
+ answer?: string;
21
+ createdAt: string;
22
+ answeredAt?: string;
23
+ }
24
+
12
25
  export interface ActiveRun {
13
26
  id: string;
14
27
  phase: KdPhase;
@@ -19,6 +32,7 @@ export interface ActiveRun {
19
32
  risk?: KdRisk;
20
33
  artifacts: Record<string, string>;
21
34
  gate: GateResult;
35
+ questions?: KdQuestion[];
22
36
  }
23
37
 
24
38
  export const PHASE_ORDER: KdPhase[] = ["discuss", "spec", "plan", "execute", "verify", "ship"];
@@ -0,0 +1,309 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { extname, join, relative } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { resolveWorkspacePath } from "../platform/path.ts";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export type SdkSignatureLanguage = "java" | "csharp";
10
+
11
+ export interface SdkSignatureParams {
12
+ language: SdkSignatureLanguage;
13
+ query?: string;
14
+ className?: string;
15
+ method?: string;
16
+ path?: string;
17
+ limit?: number;
18
+ }
19
+
20
+ export interface SdkSignatureResult {
21
+ language: SdkSignatureLanguage;
22
+ query: string;
23
+ exitCode: number;
24
+ stdout: string;
25
+ stderr: string;
26
+ sources: string[];
27
+ }
28
+
29
+ const SKIP_DIRS = new Set([
30
+ ".git",
31
+ ".gradle",
32
+ ".idea",
33
+ ".pi",
34
+ ".tmp",
35
+ "dist",
36
+ "node_modules",
37
+ "out",
38
+ "target",
39
+ ]);
40
+
41
+ export async function inspectSdkSignature(cwd: string, params: SdkSignatureParams): Promise<SdkSignatureResult> {
42
+ const query = (params.className || params.query || "").trim();
43
+ if (!query) {
44
+ return {
45
+ language: params.language,
46
+ query,
47
+ exitCode: 2,
48
+ stdout: "",
49
+ stderr: "Provide query or className for kd_sdk_signature. method is only a filter within a matched class/type.",
50
+ sources: [],
51
+ };
52
+ }
53
+
54
+ if (params.language === "java") return inspectJavaSignature(cwd, params, query);
55
+ return inspectCsharpSignature(cwd, params, query);
56
+ }
57
+
58
+ export function formatSdkSignatureResult(result: SdkSignatureResult): string {
59
+ return [
60
+ `Language: ${result.language}`,
61
+ `Query: ${result.query}`,
62
+ `Exit: ${result.exitCode}`,
63
+ result.sources.length ? `Sources:\n${result.sources.map((source) => `- ${source}`).join("\n")}` : "Sources: none",
64
+ result.stdout.trim() ? `\nSTDOUT:\n${result.stdout.trim()}` : undefined,
65
+ result.stderr.trim() ? `\nSTDERR:\n${result.stderr.trim()}` : undefined,
66
+ "",
67
+ "Use this as local SDK evidence only when Exit is 0. If it fails, use build output or project SDK configuration before trusting bundled knowledge.",
68
+ ]
69
+ .filter(Boolean)
70
+ .join("\n");
71
+ }
72
+
73
+ async function inspectJavaSignature(cwd: string, params: SdkSignatureParams, query: string): Promise<SdkSignatureResult> {
74
+ const roots = scanRoots(cwd, params.path);
75
+ const jars = roots.flatMap((root) => findFiles(root, ".jar", params.limit ?? 200));
76
+ if (jars.length === 0) {
77
+ return {
78
+ language: "java",
79
+ query,
80
+ exitCode: 2,
81
+ stdout: "",
82
+ stderr: "No jar files found in the current project. Build/copy dependencies first or pass path=<sdk/lib directory>.",
83
+ sources: [],
84
+ };
85
+ }
86
+
87
+ const classNames = params.className ? [params.className] : await findJavaClasses(jars, query, params.limit ?? 20);
88
+ if (classNames.length === 0) {
89
+ return {
90
+ language: "java",
91
+ query,
92
+ exitCode: 1,
93
+ stdout: "",
94
+ stderr: `No class matching "${query}" found in project jars.`,
95
+ sources: jars.map((jar) => relativeOrSelf(cwd, jar)).slice(0, 20),
96
+ };
97
+ }
98
+
99
+ const classpath = jars.join(process.platform === "win32" ? ";" : ":");
100
+ const outputs: string[] = [];
101
+ const errors: string[] = [];
102
+ const inspectedSources = new Set<string>();
103
+
104
+ for (const className of classNames.slice(0, params.limit ?? 10)) {
105
+ try {
106
+ const result = await execFileAsync("javap", ["-classpath", classpath, "-public", className], {
107
+ cwd,
108
+ timeout: 60_000,
109
+ maxBuffer: 1024 * 1024 * 4,
110
+ });
111
+ const text = filterMethodLines(result.stdout || "", params.method);
112
+ if (text.trim()) outputs.push(`## ${className}\n${text.trim()}`);
113
+ for (const source of jarsContainingClass(jars, className)) inspectedSources.add(relativeOrSelf(cwd, source));
114
+ } catch (error) {
115
+ const err = error as { message?: string; stderr?: string };
116
+ errors.push(`${className}: ${err.stderr || err.message || String(error)}`);
117
+ }
118
+ }
119
+
120
+ return {
121
+ language: "java",
122
+ query,
123
+ exitCode: outputs.length ? 0 : 1,
124
+ stdout: outputs.join("\n\n"),
125
+ stderr: errors.join("\n").trim(),
126
+ sources: [...inspectedSources].sort(),
127
+ };
128
+ }
129
+
130
+ async function inspectCsharpSignature(cwd: string, params: SdkSignatureParams, query: string): Promise<SdkSignatureResult> {
131
+ const roots = scanRoots(cwd, params.path);
132
+ const dlls = roots.flatMap((root) => findFiles(root, ".dll", params.limit ?? 200));
133
+ if (dlls.length === 0) {
134
+ return {
135
+ language: "csharp",
136
+ query,
137
+ exitCode: 2,
138
+ stdout: "",
139
+ stderr: "No dll files found in the current project. Build/restore references first or pass path=<sdk/bin directory>.",
140
+ sources: [],
141
+ };
142
+ }
143
+
144
+ const script = [
145
+ "$ErrorActionPreference = 'SilentlyContinue'",
146
+ "$query = $args[0]",
147
+ "$method = $args[1]",
148
+ "$dlls = $args[2..($args.Length-1)]",
149
+ "$hits = New-Object System.Collections.Generic.List[string]",
150
+ "foreach ($dll in $dlls) {",
151
+ " try { $asm = [System.Reflection.Assembly]::LoadFrom($dll) } catch { continue }",
152
+ " try { $types = $asm.GetExportedTypes() } catch { continue }",
153
+ " foreach ($type in $types) {",
154
+ " if ($type.FullName -notlike \"*$query*\" -and $type.Name -notlike \"*$query*\") { continue }",
155
+ " $hits.Add(\"## \" + $type.FullName + \"`nAssembly: \" + $dll)",
156
+ " foreach ($m in $type.GetMethods([System.Reflection.BindingFlags]'Public,Instance,Static,DeclaredOnly')) {",
157
+ " if ($method -and $m.Name -notlike \"*$method*\") { continue }",
158
+ " $params = ($m.GetParameters() | ForEach-Object { $_.ParameterType.Name + ' ' + $_.Name }) -join ', '",
159
+ " $hits.Add(' ' + $m.ReturnType.Name + ' ' + $m.Name + '(' + $params + ')')",
160
+ " }",
161
+ " foreach ($p in $type.GetProperties([System.Reflection.BindingFlags]'Public,Instance,Static,DeclaredOnly')) {",
162
+ " if ($method -and $p.Name -notlike \"*$method*\") { continue }",
163
+ " $hits.Add(' property ' + $p.PropertyType.Name + ' ' + $p.Name)",
164
+ " }",
165
+ " }",
166
+ "}",
167
+ "$hits | Select-Object -First 200",
168
+ ].join("; ");
169
+
170
+ try {
171
+ const executable = process.platform === "win32" ? "powershell" : "pwsh";
172
+ const result = await execFileAsync(executable, ["-NoProfile", "-Command", script, query, params.method ?? "", ...dlls], {
173
+ cwd,
174
+ timeout: 60_000,
175
+ maxBuffer: 1024 * 1024 * 4,
176
+ });
177
+ const stdout = result.stdout || "";
178
+ return {
179
+ language: "csharp",
180
+ query,
181
+ exitCode: stdout.trim() ? 0 : 1,
182
+ stdout,
183
+ stderr: stdout.trim() ? "" : `No public type matching "${query}" found in project dlls.`,
184
+ sources: dlls.map((dll) => relativeOrSelf(cwd, dll)).slice(0, 20),
185
+ };
186
+ } catch (error) {
187
+ const err = error as { message?: string; stderr?: string };
188
+ return {
189
+ language: "csharp",
190
+ query,
191
+ exitCode: 1,
192
+ stdout: "",
193
+ stderr: err.stderr || err.message || String(error),
194
+ sources: dlls.map((dll) => relativeOrSelf(cwd, dll)).slice(0, 20),
195
+ };
196
+ }
197
+ }
198
+
199
+ async function findJavaClasses(jars: string[], query: string, limit: number): Promise<string[]> {
200
+ const basenameQuery = query.includes(".") ? query.split(".").at(-1) || query : query;
201
+ const classPattern = `${basenameQuery}.class`;
202
+ const results = new Set<string>();
203
+
204
+ for (const jar of jars) {
205
+ try {
206
+ const output = await execFileAsync("jar", ["tf", jar], { timeout: 30_000, maxBuffer: 1024 * 1024 * 4 });
207
+ for (const line of (output.stdout || "").split(/\r?\n/)) {
208
+ if (!line.endsWith(".class") || line.includes("$")) continue;
209
+ if (!line.toLowerCase().includes(classPattern.toLowerCase())) continue;
210
+ results.add(line.replace(/\.class$/, "").replace(/\//g, "."));
211
+ if (results.size >= limit) return [...results];
212
+ }
213
+ } catch {
214
+ const matched = findClassNamesInJarBytes(jar, basenameQuery, limit - results.size);
215
+ for (const className of matched) results.add(className);
216
+ if (results.size >= limit) return [...results];
217
+ }
218
+ }
219
+
220
+ return [...results];
221
+ }
222
+
223
+ function findClassNamesInJarBytes(jar: string, query: string, limit: number): string[] {
224
+ const data = readFileSync(jar).toString("latin1");
225
+ const pattern = `${query}.class`;
226
+ const results = new Set<string>();
227
+ let index = data.indexOf(pattern);
228
+ while (index >= 0 && results.size < limit) {
229
+ let start = index;
230
+ while (start > 0 && /[A-Za-z0-9_/$-]/.test(data[start - 1])) start--;
231
+ const entry = data.slice(start, index + pattern.length);
232
+ if (entry.includes("/") && !entry.includes("$")) {
233
+ results.add(entry.replace(/\.class$/, "").replace(/\//g, "."));
234
+ }
235
+ index = data.indexOf(pattern, index + pattern.length);
236
+ }
237
+ return [...results];
238
+ }
239
+
240
+ function jarsContainingClass(jars: string[], className: string): string[] {
241
+ const entry = `${className.replace(/\./g, "/")}.class`;
242
+ return jars.filter((jar) => {
243
+ try {
244
+ const data = readFileSync(jar).toString("latin1");
245
+ return data.includes(entry);
246
+ } catch {
247
+ return false;
248
+ }
249
+ });
250
+ }
251
+
252
+ function filterMethodLines(output: string, method?: string): string {
253
+ if (!method) return output;
254
+ const lines = output.split(/\r?\n/);
255
+ const kept = lines.filter((line) => line.includes(`${method}(`) || /^(Compiled from|public class|public interface)/.test(line.trim()));
256
+ return kept.join("\n");
257
+ }
258
+
259
+ function scanRoots(cwd: string, path?: string): string[] {
260
+ if (path) {
261
+ const resolved = resolveWorkspacePath(cwd, path);
262
+ return existsSync(resolved) ? [resolved] : [];
263
+ }
264
+ return [cwd];
265
+ }
266
+
267
+ function findFiles(root: string, extension: string, limit: number): string[] {
268
+ if (existsSync(root)) {
269
+ try {
270
+ const stat = statSync(root);
271
+ if (stat.isFile()) return extname(root).toLowerCase() === extension ? [root] : [];
272
+ } catch {
273
+ return [];
274
+ }
275
+ }
276
+ const results: string[] = [];
277
+ walk(root, extension, results, limit, 0);
278
+ return results;
279
+ }
280
+
281
+ function walk(dir: string, extension: string, results: string[], limit: number, depth: number): void {
282
+ if (results.length >= limit || depth > 8 || !existsSync(dir)) return;
283
+ let entries: string[];
284
+ try {
285
+ entries = readdirSync(dir);
286
+ } catch {
287
+ return;
288
+ }
289
+ for (const entry of entries) {
290
+ if (results.length >= limit) return;
291
+ const full = join(dir, entry);
292
+ let stat;
293
+ try {
294
+ stat = statSync(full);
295
+ } catch {
296
+ continue;
297
+ }
298
+ if (stat.isDirectory()) {
299
+ if (!SKIP_DIRS.has(entry)) walk(full, extension, results, limit, depth + 1);
300
+ continue;
301
+ }
302
+ if (stat.isFile() && extname(entry).toLowerCase() === extension) results.push(full);
303
+ }
304
+ }
305
+
306
+ function relativeOrSelf(cwd: string, path: string): string {
307
+ const rel = relative(cwd, path);
308
+ return rel && !rel.startsWith("..") ? rel : path;
309
+ }