kcode-pi 0.1.7 → 0.1.9
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 +55 -4
- package/docs/DEVELOPMENT.md +2 -0
- package/extensions/kingdee-harness.ts +271 -2
- package/extensions/kingdee-tools.ts +43 -1
- package/package.json +2 -1
- package/skills/kd-check/SKILL.md +1 -2
- package/skills/kd-cosmic-dev/SKILL.md +3 -3
- package/skills/kd-cosmic-review/SKILL.md +2 -2
- package/skills/kd-cosmic-unittest/SKILL.md +2 -2
- package/skills/kd-debug/SKILL.md +1 -2
- package/skills/kd-discuss/SKILL.md +1 -0
- package/skills/kd-execute/SKILL.md +2 -2
- package/skills/kd-gen/SKILL.md +2 -1
- package/skills/kd-plan/SKILL.md +3 -3
- package/skills/kd-spec/SKILL.md +1 -2
- package/src/harness/format.ts +2 -0
- package/src/harness/gates.ts +14 -1
- package/src/harness/paths.ts +5 -1
- package/src/harness/state.ts +113 -11
- package/src/harness/types.ts +19 -0
- package/src/tools/sdk-signature.ts +309 -0
|
@@ -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 `
|
|
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
|
|
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.
|
package/skills/kd-gen/SKILL.md
CHANGED
|
@@ -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
|
|
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#.
|
package/skills/kd-plan/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
package/skills/kd-spec/SKILL.md
CHANGED
|
@@ -20,5 +20,4 @@ Rules:
|
|
|
20
20
|
- Separate confirmed facts from assumptions.
|
|
21
21
|
- Mark every API/table assumption as requiring lookup with `kd_search` or `kd_table`.
|
|
22
22
|
- Do not edit product code in this phase.
|
|
23
|
-
- If acceptance criteria are vague, ask targeted questions or state explicit assumptions.
|
|
24
|
-
|
|
23
|
+
- If acceptance criteria are vague, ask targeted questions one at a time or state explicit assumptions. When using `kd_question`, ask only the single most blocking question and use at most 3 short choices; never submit a numbered checklist of questions.
|
package/src/harness/format.ts
CHANGED
|
@@ -15,6 +15,8 @@ export function formatStatus(cwd: string, run: ActiveRun | undefined): string {
|
|
|
15
15
|
|
|
16
16
|
return [
|
|
17
17
|
`Run: ${refreshed.id}`,
|
|
18
|
+
refreshed.goal ? `Goal: ${refreshed.goal}` : undefined,
|
|
19
|
+
`Status: ${refreshed.status ?? "active"}`,
|
|
18
20
|
`Phase: ${refreshed.phase}`,
|
|
19
21
|
`Next: ${next}`,
|
|
20
22
|
`Product: ${formatProductProfile(refreshed.profile)}`,
|
package/src/harness/gates.ts
CHANGED
|
@@ -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 {
|
package/src/harness/paths.ts
CHANGED
|
@@ -17,7 +17,11 @@ export function runRoot(cwd: string, run: ActiveRun): string {
|
|
|
17
17
|
return join(runsDir(cwd), run.id);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function runStatePath(cwd: string, runOrId: ActiveRun | string): string {
|
|
21
|
+
const id = typeof runOrId === "string" ? runOrId : runOrId.id;
|
|
22
|
+
return join(runsDir(cwd), id, "RUN.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export function runArtifactPath(cwd: string, run: ActiveRun, artifactName: string): string {
|
|
21
26
|
return join(runRoot(cwd, run), artifactName);
|
|
22
27
|
}
|
|
23
|
-
|
package/src/harness/state.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import type { ActiveRun, KdPhase } from "./types.ts";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { ActiveRun, KdPhase, KdQuestion } from "./types.ts";
|
|
3
3
|
import { PHASE_ARTIFACTS, isKdPhase, nextPhase } from "./types.ts";
|
|
4
|
-
import { activeRunPath, kdDir, runsDir } from "./paths.ts";
|
|
4
|
+
import { activeRunPath, kdDir, runStatePath, runsDir } from "./paths.ts";
|
|
5
5
|
import { defaultArtifactContent, ensureArtifact, ensureRunDirectories, writeArtifact } from "./artifacts.ts";
|
|
6
6
|
import { canEnterPhase, inspectGate } from "./gates.ts";
|
|
7
7
|
import { profileForProduct, resolveProductProfile } from "../product/profile.ts";
|
|
@@ -11,14 +11,9 @@ export function readActiveRun(cwd: string): ActiveRun | undefined {
|
|
|
11
11
|
if (!existsSync(path)) return undefined;
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
16
|
-
|
|
17
|
-
const legacyEdition = (parsed as ActiveRun & { edition?: string }).edition;
|
|
18
|
-
parsed.profile = parsed.profile ?? profileForProduct(parsed.product ?? resolveProductProfile(legacyEdition).product);
|
|
19
|
-
parsed.product = parsed.profile.product;
|
|
20
|
-
parsed.gate ??= { passed: false, checkedAt: new Date().toISOString() };
|
|
21
|
-
return parsed;
|
|
14
|
+
const active = hydrateRun(JSON.parse(readFileSync(path, "utf8")) as ActiveRun);
|
|
15
|
+
if (!active) return undefined;
|
|
16
|
+
return readRun(cwd, active.id) ?? active;
|
|
22
17
|
} catch {
|
|
23
18
|
return undefined;
|
|
24
19
|
}
|
|
@@ -27,20 +22,66 @@ export function readActiveRun(cwd: string): ActiveRun | undefined {
|
|
|
27
22
|
export function writeActiveRun(cwd: string, run: ActiveRun): void {
|
|
28
23
|
mkdirSync(kdDir(cwd), { recursive: true });
|
|
29
24
|
mkdirSync(runsDir(cwd), { recursive: true });
|
|
25
|
+
run.status = "active";
|
|
26
|
+
run.updatedAt = new Date().toISOString();
|
|
27
|
+
writeRunState(cwd, run);
|
|
30
28
|
writeFileSync(activeRunPath(cwd), `${JSON.stringify(run, null, 2)}\n`, "utf8");
|
|
31
29
|
}
|
|
32
30
|
|
|
31
|
+
export function readRun(cwd: string, id: string): ActiveRun | undefined {
|
|
32
|
+
const path = runStatePath(cwd, id);
|
|
33
|
+
if (!existsSync(path)) return undefined;
|
|
34
|
+
try {
|
|
35
|
+
return hydrateRun(JSON.parse(readFileSync(path, "utf8")) as ActiveRun);
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function listRuns(cwd: string): ActiveRun[] {
|
|
42
|
+
const root = runsDir(cwd);
|
|
43
|
+
if (!existsSync(root)) return [];
|
|
44
|
+
return readdirSync(root, { withFileTypes: true })
|
|
45
|
+
.filter((entry) => entry.isDirectory())
|
|
46
|
+
.map((entry) => readRun(cwd, entry.name))
|
|
47
|
+
.filter((run): run is ActiveRun => Boolean(run))
|
|
48
|
+
.sort((a, b) => (b.updatedAt ?? b.createdAt ?? b.id).localeCompare(a.updatedAt ?? a.createdAt ?? a.id));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function switchActiveRun(cwd: string, id: string): ActiveRun | undefined {
|
|
52
|
+
const run = readRun(cwd, id);
|
|
53
|
+
if (!run) return undefined;
|
|
54
|
+
writeActiveRun(cwd, run);
|
|
55
|
+
return readActiveRun(cwd);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function finishActiveRun(cwd: string, run: ActiveRun): ActiveRun {
|
|
59
|
+
run.status = "done";
|
|
60
|
+
run.updatedAt = new Date().toISOString();
|
|
61
|
+
run.gate = inspectGate(cwd, run);
|
|
62
|
+
writeRunState(cwd, run);
|
|
63
|
+
const activePath = activeRunPath(cwd);
|
|
64
|
+
if (existsSync(activePath)) unlinkSync(activePath);
|
|
65
|
+
return run;
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
export function createActiveRun(cwd: string, goal: string, productInput?: string, version?: string): ActiveRun {
|
|
34
69
|
const profile = resolveProductProfile(productInput ?? goal);
|
|
70
|
+
const now = new Date().toISOString();
|
|
35
71
|
const run: ActiveRun = {
|
|
36
72
|
id: createRunId(goal),
|
|
73
|
+
goal,
|
|
37
74
|
phase: "discuss",
|
|
38
75
|
cwd,
|
|
76
|
+
status: "active",
|
|
77
|
+
createdAt: now,
|
|
78
|
+
updatedAt: now,
|
|
39
79
|
product: profile.product,
|
|
40
80
|
version,
|
|
41
81
|
profile,
|
|
42
82
|
risk: "unknown",
|
|
43
83
|
artifacts: {},
|
|
84
|
+
questions: [],
|
|
44
85
|
gate: {
|
|
45
86
|
passed: false,
|
|
46
87
|
reason: "CONTEXT.md and product profile are required before moving to spec",
|
|
@@ -55,6 +96,43 @@ export function createActiveRun(cwd: string, goal: string, productInput?: string
|
|
|
55
96
|
return run;
|
|
56
97
|
}
|
|
57
98
|
|
|
99
|
+
export function addQuestion(
|
|
100
|
+
cwd: string,
|
|
101
|
+
run: ActiveRun,
|
|
102
|
+
input: { question: string; reason?: string; choices?: string[]; blocking?: boolean },
|
|
103
|
+
): KdQuestion {
|
|
104
|
+
const existing = run.questions ?? [];
|
|
105
|
+
const question: KdQuestion = {
|
|
106
|
+
id: createQuestionId(existing.length + 1),
|
|
107
|
+
phase: run.phase,
|
|
108
|
+
question: input.question.trim(),
|
|
109
|
+
reason: input.reason?.trim() || undefined,
|
|
110
|
+
choices: input.choices?.map((choice) => choice.trim()).filter(Boolean),
|
|
111
|
+
blocking: input.blocking ?? true,
|
|
112
|
+
status: "open",
|
|
113
|
+
createdAt: new Date().toISOString(),
|
|
114
|
+
};
|
|
115
|
+
run.questions = [...existing, question];
|
|
116
|
+
run.gate = inspectGate(cwd, run);
|
|
117
|
+
writeActiveRun(cwd, run);
|
|
118
|
+
return question;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function answerQuestion(cwd: string, run: ActiveRun, id: string, answer: string): KdQuestion | undefined {
|
|
122
|
+
const question = (run.questions ?? []).find((item) => item.id === id);
|
|
123
|
+
if (!question) return undefined;
|
|
124
|
+
question.status = "answered";
|
|
125
|
+
question.answer = answer.trim();
|
|
126
|
+
question.answeredAt = new Date().toISOString();
|
|
127
|
+
run.gate = inspectGate(cwd, run);
|
|
128
|
+
writeActiveRun(cwd, run);
|
|
129
|
+
return question;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function openBlockingQuestions(run: ActiveRun): KdQuestion[] {
|
|
133
|
+
return (run.questions ?? []).filter((question) => question.status === "open" && question.blocking);
|
|
134
|
+
}
|
|
135
|
+
|
|
58
136
|
export function updateProductProfile(cwd: string, run: ActiveRun, productInput: string, version?: string): ActiveRun {
|
|
59
137
|
const profile = resolveProductProfile(productInput);
|
|
60
138
|
run.product = profile.product;
|
|
@@ -106,6 +184,26 @@ export function refreshGate(cwd: string, run: ActiveRun): ActiveRun {
|
|
|
106
184
|
return run;
|
|
107
185
|
}
|
|
108
186
|
|
|
187
|
+
function writeRunState(cwd: string, run: ActiveRun): void {
|
|
188
|
+
mkdirSync(runsDir(cwd), { recursive: true });
|
|
189
|
+
ensureRunDirectories(cwd, run);
|
|
190
|
+
writeFileSync(runStatePath(cwd, run), `${JSON.stringify(run, null, 2)}\n`, "utf8");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function hydrateRun(parsed: ActiveRun): ActiveRun | undefined {
|
|
194
|
+
if (!parsed.id || !isKdPhase(parsed.phase)) return undefined;
|
|
195
|
+
parsed.artifacts ??= {};
|
|
196
|
+
parsed.questions ??= [];
|
|
197
|
+
parsed.status ??= "active";
|
|
198
|
+
parsed.createdAt ??= parsed.updatedAt;
|
|
199
|
+
parsed.updatedAt ??= parsed.createdAt;
|
|
200
|
+
const legacyEdition = (parsed as ActiveRun & { edition?: string }).edition;
|
|
201
|
+
parsed.profile = parsed.profile ?? profileForProduct(parsed.product ?? resolveProductProfile(legacyEdition).product);
|
|
202
|
+
parsed.product = parsed.profile.product;
|
|
203
|
+
parsed.gate ??= { passed: false, checkedAt: new Date().toISOString() };
|
|
204
|
+
return parsed;
|
|
205
|
+
}
|
|
206
|
+
|
|
109
207
|
function createRunId(goal: string): string {
|
|
110
208
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-");
|
|
111
209
|
const slug = goal
|
|
@@ -115,3 +213,7 @@ function createRunId(goal: string): string {
|
|
|
115
213
|
.slice(0, 40);
|
|
116
214
|
return slug ? `${stamp}-${slug}` : stamp;
|
|
117
215
|
}
|
|
216
|
+
|
|
217
|
+
function createQuestionId(sequence: number): string {
|
|
218
|
+
return `Q-${String(sequence).padStart(3, "0")}`;
|
|
219
|
+
}
|
package/src/harness/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { KdProduct, ProductProfile } from "../product/profile.ts";
|
|
|
2
2
|
|
|
3
3
|
export type KdPhase = "discuss" | "spec" | "plan" | "execute" | "verify" | "ship";
|
|
4
4
|
export type KdRisk = "unknown" | "low" | "medium" | "high";
|
|
5
|
+
export type KdRunStatus = "active" | "paused" | "done";
|
|
5
6
|
|
|
6
7
|
export interface GateResult {
|
|
7
8
|
passed: boolean;
|
|
@@ -9,16 +10,34 @@ export interface GateResult {
|
|
|
9
10
|
checkedAt: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
export interface KdQuestion {
|
|
14
|
+
id: string;
|
|
15
|
+
phase: KdPhase;
|
|
16
|
+
question: string;
|
|
17
|
+
reason?: string;
|
|
18
|
+
choices?: string[];
|
|
19
|
+
blocking: boolean;
|
|
20
|
+
status: "open" | "answered";
|
|
21
|
+
answer?: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
answeredAt?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
export interface ActiveRun {
|
|
13
27
|
id: string;
|
|
28
|
+
goal?: string;
|
|
14
29
|
phase: KdPhase;
|
|
15
30
|
cwd: string;
|
|
31
|
+
status?: KdRunStatus;
|
|
32
|
+
createdAt?: string;
|
|
33
|
+
updatedAt?: string;
|
|
16
34
|
product?: KdProduct;
|
|
17
35
|
version?: string;
|
|
18
36
|
profile?: ProductProfile;
|
|
19
37
|
risk?: KdRisk;
|
|
20
38
|
artifacts: Record<string, string>;
|
|
21
39
|
gate: GateResult;
|
|
40
|
+
questions?: KdQuestion[];
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
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
|
+
}
|