kcode-pi 0.1.2 → 0.1.4
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 +353 -171
- package/dist/cli/kcode.d.ts +1 -0
- package/dist/cli/kcode.js +15 -1
- package/dist/context/project-context.d.ts +9 -0
- package/dist/context/project-context.js +192 -0
- package/docs/DEVELOPMENT.md +162 -0
- package/docs/KCODE_DISTRIBUTION.md +1 -1
- package/extensions/kingdee-harness.ts +112 -0
- package/extensions/kingdee-tools.ts +5 -2
- package/knowledge/enterprise-python/python-plugin.md +134 -0
- package/package.json +2 -2
- package/skills/kd-cosmic-dev/SKILL.md +2 -0
- package/skills/kd-cosmic-unittest/SKILL.md +1 -0
- package/skills/kd-enterprise-python-plugin/SKILL.md +43 -0
- package/skills/kd-execute/SKILL.md +6 -2
- package/skills/kd-gen/SKILL.md +1 -0
- package/skills/kd-plan/SKILL.md +7 -1
- package/src/cli/kcode.ts +16 -1
- package/src/context/project-context.ts +214 -0
- package/src/harness/artifacts.ts +35 -1
- package/src/harness/gates.ts +29 -0
- package/src/harness/path-policy.ts +83 -0
- package/src/harness/plan-steps.ts +79 -0
- package/src/harness/tdd-policy.ts +62 -0
- package/src/knowledge/types.ts +1 -1
- package/src/product/profile.ts +29 -5
- package/src/rules/checker.ts +1 -1
- package/src/tools/build-debug.ts +5 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ActiveRun } from "./types.ts";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
6
|
+
".java",
|
|
7
|
+
".kt",
|
|
8
|
+
".kts",
|
|
9
|
+
".xml",
|
|
10
|
+
".properties",
|
|
11
|
+
".yml",
|
|
12
|
+
".yaml",
|
|
13
|
+
".sql",
|
|
14
|
+
".ksql",
|
|
15
|
+
".py",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function flagshipWriteBlockReason(run: ActiveRun | undefined, path: string | undefined, cwd?: string): string | undefined {
|
|
19
|
+
if (run?.profile?.product !== "flagship") return undefined;
|
|
20
|
+
if (!path || !isSourceLikePath(path)) return undefined;
|
|
21
|
+
|
|
22
|
+
const normalized = normalizeRelativePath(cwd && isAbsolute(path) ? relative(cwd, path) : path);
|
|
23
|
+
if (normalized.startsWith(".pi/")) return undefined;
|
|
24
|
+
if (cwd && !hasWorkspaceCodeDir(cwd)) return undefined;
|
|
25
|
+
if (!normalized.startsWith("code/")) return `星空旗舰版代码必须跟随当前项目结构写入 code/ 下,不能写到 ${path}`;
|
|
26
|
+
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function planWriteBlockReason(cwd: string, run: ActiveRun | undefined, path: string | undefined, plan: string): string | undefined {
|
|
31
|
+
if (!run || run.phase !== "execute") return undefined;
|
|
32
|
+
if (!path || !isSourceLikePath(path)) return undefined;
|
|
33
|
+
|
|
34
|
+
const normalized = normalizeRelativePath(cwd && isAbsolute(path) ? relative(cwd, path) : path);
|
|
35
|
+
if (normalized.startsWith(".pi/")) return undefined;
|
|
36
|
+
if (planMentionsPath(plan, normalized)) return undefined;
|
|
37
|
+
|
|
38
|
+
return `PLAN.md 未批准写入 ${normalized}。请先回到 plan 阶段更新 PLAN.md,明确列出该目标文件后再执行。`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function flagshipPlanBlockReason(cwd: string, run: ActiveRun | undefined, plan: string): string | undefined {
|
|
42
|
+
if (run?.profile?.product !== "flagship") return undefined;
|
|
43
|
+
|
|
44
|
+
if (hasWorkspaceCodeDir(cwd)) {
|
|
45
|
+
if (/(?:^|[\s`"'(])code[\\/][^\s`"')]+/i.test(plan)) return undefined;
|
|
46
|
+
return "不能进入 execute:星空旗舰版 PLAN.md 必须先记录当前项目 code/ 下的实际目标路径;不要按固定模块规则猜路径。";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (planMentionsDiscoveredSourcePath(plan)) return undefined;
|
|
50
|
+
return "不能进入 execute:PLAN.md 必须先记录已检查当前项目结构,并写明实际源码根或目标文件路径;当前项目没有 code/ 时更不能猜路径。";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasWorkspaceCodeDir(cwd: string): boolean {
|
|
54
|
+
return existsSync(join(cwd, "code"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function planMentionsDiscoveredSourcePath(plan: string): boolean {
|
|
58
|
+
return /(?:^|[\s`"'(])(?:src[\\/]main[\\/]java|src[\\/]main[\\/]resources|[^`\n]*[\\/](?:src[\\/]main[\\/]java|src[\\/]main[\\/]resources)|[^`\n]*\.(?:java|xml|properties|yml|yaml|sql|ksql|py))(?:[\s`"')]|$)/i.test(
|
|
59
|
+
plan,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function planMentionsPath(plan: string, normalizedPath: string): boolean {
|
|
64
|
+
const normalizedPlan = normalizeRelativePath(plan);
|
|
65
|
+
const escaped = escapeRegExp(normalizedPath);
|
|
66
|
+
return new RegExp(`(?:^|[\\s\`"'(])${escaped}(?:[\\s\`"')]|$)`, "i").test(normalizedPlan);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeRelativePath(path: string): string {
|
|
70
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isSourceLikePath(path: string): boolean {
|
|
74
|
+
const normalized = normalizeRelativePath(path);
|
|
75
|
+
const lastSegment = normalized.split("/").at(-1) ?? normalized;
|
|
76
|
+
const dotIndex = lastSegment.lastIndexOf(".");
|
|
77
|
+
if (dotIndex < 0) return false;
|
|
78
|
+
return SOURCE_EXTENSIONS.has(lastSegment.slice(dotIndex).toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function escapeRegExp(value: string): string {
|
|
82
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
83
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ActiveRun } from "./types.ts";
|
|
4
|
+
import { runRoot } from "./paths.ts";
|
|
5
|
+
|
|
6
|
+
export interface PlanStep {
|
|
7
|
+
id: string;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STEP_LINE_PATTERN = /^\s*[-*]\s*\[[ xX]\]\s*(STEP-\d{3,})\s*[::]\s*(.+)$/gim;
|
|
12
|
+
const EVIDENCE_PATTERN = /\bevidence[\\/][^\s`"')]+/gi;
|
|
13
|
+
|
|
14
|
+
export function parsePlanSteps(plan: string): PlanStep[] {
|
|
15
|
+
const steps: PlanStep[] = [];
|
|
16
|
+
for (const match of plan.matchAll(STEP_LINE_PATTERN)) {
|
|
17
|
+
steps.push({ id: match[1].toUpperCase(), text: match[2].trim() });
|
|
18
|
+
}
|
|
19
|
+
return steps;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function planStepsBlockReason(plan: string): string | undefined {
|
|
23
|
+
if (!/##\s*Execution Steps/i.test(plan)) {
|
|
24
|
+
return "PLAN.md 缺少 ## Execution Steps。必须把计划拆成 STEP-001 这种可跟踪步骤。";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const steps = parsePlanSteps(plan);
|
|
28
|
+
if (steps.length === 0) {
|
|
29
|
+
return "PLAN.md 没有可执行步骤。请使用 `- [ ] STEP-001: ...` 列出步骤。";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const duplicate = firstDuplicate(steps.map((step) => step.id));
|
|
33
|
+
if (duplicate) return `PLAN.md 存在重复步骤编号:${duplicate}`;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function executionStepsBlockReason(cwd: string, run: ActiveRun, plan: string, execution: string): string | undefined {
|
|
38
|
+
const steps = parsePlanSteps(plan);
|
|
39
|
+
if (steps.length === 0) return planStepsBlockReason(plan);
|
|
40
|
+
|
|
41
|
+
const missing: string[] = [];
|
|
42
|
+
const missingEvidence: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const step of steps) {
|
|
45
|
+
const line = findExecutionLine(execution, step.id);
|
|
46
|
+
if (!line) {
|
|
47
|
+
missing.push(step.id);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const evidencePaths = [...line.matchAll(EVIDENCE_PATTERN)].map((match) => normalizeEvidencePath(match[0]));
|
|
52
|
+
if (evidencePaths.length === 0 || !evidencePaths.some((path) => existsSync(join(runRoot(cwd, run), path)))) {
|
|
53
|
+
missingEvidence.push(step.id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (missing.length > 0) return `不能进入 verify:EXECUTION.md 未完成计划步骤 ${missing.join(", ")}。`;
|
|
58
|
+
if (missingEvidence.length > 0) return `不能进入 verify:步骤 ${missingEvidence.join(", ")} 缺少已落地的 evidence 文件。`;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findExecutionLine(execution: string, stepId: string): string | undefined {
|
|
63
|
+
return execution
|
|
64
|
+
.split(/\r?\n/)
|
|
65
|
+
.find((line) => line.toUpperCase().includes(stepId) && /(\[[xX]\]|done|完成|completed)/i.test(line));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function firstDuplicate(values: string[]): string | undefined {
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
for (const value of values) {
|
|
71
|
+
if (seen.has(value)) return value;
|
|
72
|
+
seen.add(value);
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeEvidencePath(path: string): string {
|
|
78
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
79
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
3
|
+
import type { ActiveRun } from "./types.ts";
|
|
4
|
+
import { runRoot } from "./paths.ts";
|
|
5
|
+
import { isSourceLikePath } from "./path-policy.ts";
|
|
6
|
+
|
|
7
|
+
export const TDD_RED_EVIDENCE = "evidence/tdd-red.md";
|
|
8
|
+
export const TDD_GREEN_EVIDENCE = "evidence/tdd-green.md";
|
|
9
|
+
|
|
10
|
+
export function tddPlanBlockReason(plan: string): string | undefined {
|
|
11
|
+
if (/##\s*TDD\s*\/\s*Red-Green Checks/i.test(plan)) return undefined;
|
|
12
|
+
return "PLAN.md 缺少 ## TDD / Red-Green Checks。必须声明红灯验证、绿灯验证和无法自动化时的产品验证替代方案。";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function tddProductionWriteBlockReason(cwd: string, run: ActiveRun | undefined, path: string | undefined): string | undefined {
|
|
16
|
+
if (!run || run.phase !== "execute") return undefined;
|
|
17
|
+
if (!path || !isSourceLikePath(path)) return undefined;
|
|
18
|
+
|
|
19
|
+
const normalized = normalizeRelativePath(cwd && isAbsolute(path) ? relative(cwd, path) : path);
|
|
20
|
+
if (normalized.startsWith(".pi/")) return undefined;
|
|
21
|
+
if (isTestLikePath(normalized)) return undefined;
|
|
22
|
+
if (hasValidTddEvidence(cwd, run, "red")) return undefined;
|
|
23
|
+
|
|
24
|
+
return `不能写生产源码 ${normalized}:缺少红灯证据 ${TDD_RED_EVIDENCE}。请先记录失败的测试、API/基类/方法签名检查、元数据检查、编译检查或外部接口最小验证输出。`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function tddVerifyBlockReason(cwd: string, run: ActiveRun): string | undefined {
|
|
28
|
+
const missing: string[] = [];
|
|
29
|
+
if (!hasValidTddEvidence(cwd, run, "red")) missing.push(TDD_RED_EVIDENCE);
|
|
30
|
+
if (!hasValidTddEvidence(cwd, run, "green")) missing.push(TDD_GREEN_EVIDENCE);
|
|
31
|
+
if (missing.length === 0) return undefined;
|
|
32
|
+
return `不能进入 verify:缺少 TDD 红绿证据 ${missing.join(", ")}。`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasValidTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green"): boolean {
|
|
36
|
+
const evidenceName = kind === "red" ? TDD_RED_EVIDENCE : TDD_GREEN_EVIDENCE;
|
|
37
|
+
const evidencePath = join(runRoot(cwd, run), evidenceName);
|
|
38
|
+
if (!existsSync(evidencePath)) return false;
|
|
39
|
+
|
|
40
|
+
const content = readFileSync(evidencePath, "utf8");
|
|
41
|
+
if (kind === "red") return /red|fail|failed|failure|error|失败|未通过|Exit:\s*[1-9]/i.test(content);
|
|
42
|
+
return /green|pass|passed|success|成功|通过|Exit:\s*0/i.test(content);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isTestLikePath(path: string): boolean {
|
|
46
|
+
const normalized = normalizeRelativePath(path).toLowerCase();
|
|
47
|
+
const filename = normalized.split("/").at(-1) ?? normalized;
|
|
48
|
+
return (
|
|
49
|
+
normalized.includes("/src/test/") ||
|
|
50
|
+
normalized.includes("/test/") ||
|
|
51
|
+
normalized.includes("/tests/") ||
|
|
52
|
+
normalized.includes("/__tests__/") ||
|
|
53
|
+
/[._-](test|spec)\./.test(filename) ||
|
|
54
|
+
/test\.(java|kt|kts|cs)$/i.test(filename) ||
|
|
55
|
+
/tests\.(cs)$/i.test(filename) ||
|
|
56
|
+
/_test\.py$/i.test(filename)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeRelativePath(path: string): string {
|
|
61
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
62
|
+
}
|
package/src/knowledge/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type KnowledgeScope = "enterprise" | "flagship" | "cosmic" | "xinghan" | "cangqiong";
|
|
1
|
+
export type KnowledgeScope = "enterprise" | "enterprise-python" | "flagship" | "cosmic" | "xinghan" | "cangqiong";
|
|
2
2
|
export type Edition = Extract<KnowledgeScope, "enterprise" | "flagship">;
|
|
3
3
|
|
|
4
4
|
export type KnowledgeFileType = "markdown" | "json";
|
package/src/product/profile.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type KdProduct = "unknown" | "flagship" | "cosmic" | "xinghan" | "cangqiong" | "enterprise";
|
|
2
|
-
export type KdPlatform = "unknown" | "cosmic" | "enterprise-csharp";
|
|
3
|
-
export type KdTechStack = "unknown" | "java-bos" | "java-cosmic" | "csharp-bos" | "ksql";
|
|
4
|
-
export type KdLanguage = "unknown" | "java" | "csharp" | "sql";
|
|
5
|
-
export type KnowledgeScope = "common" | "flagship" | "enterprise" | "cosmic" | "xinghan" | "cangqiong";
|
|
2
|
+
export type KdPlatform = "unknown" | "cosmic" | "enterprise-csharp" | "enterprise-python";
|
|
3
|
+
export type KdTechStack = "unknown" | "java-bos" | "java-cosmic" | "csharp-bos" | "python-bos" | "ksql";
|
|
4
|
+
export type KdLanguage = "unknown" | "java" | "csharp" | "python" | "sql";
|
|
5
|
+
export type KnowledgeScope = "common" | "flagship" | "enterprise" | "enterprise-python" | "cosmic" | "xinghan" | "cangqiong";
|
|
6
6
|
|
|
7
7
|
export interface ProductProfile {
|
|
8
8
|
product: KdProduct;
|
|
@@ -34,7 +34,11 @@ const PROFILES: Record<KdProduct, ProductProfile> = {
|
|
|
34
34
|
language: "java",
|
|
35
35
|
knowledgeScope: "flagship",
|
|
36
36
|
requiresMetadataVerification: true,
|
|
37
|
-
notes: [
|
|
37
|
+
notes: [
|
|
38
|
+
"Xingkong Flagship is Cosmic-family. Use Cosmic metadata, plugin lifecycle, SDK, and post-check constraints.",
|
|
39
|
+
"Place product code under the workspace code/ directory when that directory exists.",
|
|
40
|
+
"Before creating or editing code, inspect the existing project structure under code/ and follow its actual layout, whether it is organized by cloud, app, or no module split.",
|
|
41
|
+
],
|
|
38
42
|
},
|
|
39
43
|
cosmic: {
|
|
40
44
|
product: "cosmic",
|
|
@@ -86,6 +90,24 @@ const PRODUCT_ALIASES: Array<[RegExp, KdProduct]> = [
|
|
|
86
90
|
[/星空旗舰版|星空旗舰|旗舰版|旗舰|flagship/i, "flagship"],
|
|
87
91
|
];
|
|
88
92
|
|
|
93
|
+
const ENTERPRISE_PYTHON_PATTERN =
|
|
94
|
+
/(?:python|py)\s*(?:插件|plugin)|(?:python|py).*?(?:插件|plugin)|ironpython|(?:企业版|enterprise|星空|金蝶云星空|BOS).*?python\s*脚本|python\s*脚本.*?(?:企业版|enterprise|星空|金蝶云星空|BOS)/i;
|
|
95
|
+
|
|
96
|
+
function enterprisePythonProfile(): ProductProfile {
|
|
97
|
+
return {
|
|
98
|
+
...PROFILES.enterprise,
|
|
99
|
+
platform: "enterprise-python",
|
|
100
|
+
techStack: "python-bos",
|
|
101
|
+
language: "python",
|
|
102
|
+
knowledgeScope: "enterprise-python",
|
|
103
|
+
notes: [
|
|
104
|
+
"Use Kingdee Cloud Enterprise / BOS IronPython plugin patterns only when the user explicitly asks for Python plugin or IronPython script.",
|
|
105
|
+
"Default Enterprise plugin work remains C# unless Python plugin is explicitly requested.",
|
|
106
|
+
"Python plugins run in BOS through IronPython and call Kingdee.BOS .NET assemblies; verify plugin type, event, FormId, field keys, and registration steps before coding.",
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
export function profileForProduct(product: KdProduct | undefined): ProductProfile {
|
|
90
112
|
return PROFILES[product ?? "unknown"] ?? PROFILES.unknown;
|
|
91
113
|
}
|
|
@@ -94,6 +116,8 @@ export function resolveProductProfile(input: string | undefined): ProductProfile
|
|
|
94
116
|
const text = input?.trim();
|
|
95
117
|
if (!text) return profileForProduct("unknown");
|
|
96
118
|
|
|
119
|
+
if (ENTERPRISE_PYTHON_PATTERN.test(text)) return enterprisePythonProfile();
|
|
120
|
+
|
|
97
121
|
for (const [pattern, product] of PRODUCT_ALIASES) {
|
|
98
122
|
if (pattern.test(text)) return profileForProduct(product);
|
|
99
123
|
}
|
package/src/rules/checker.ts
CHANGED
package/src/tools/build-debug.ts
CHANGED
|
@@ -24,6 +24,11 @@ export function planBuild(cwd: string, profile: ProductProfile, target?: string)
|
|
|
24
24
|
|
|
25
25
|
if (profile.platform === "cosmic") return planJavaBuild(cwd, profile, target);
|
|
26
26
|
if (profile.platform === "enterprise-csharp") return planCsharpBuild(cwd, profile, target);
|
|
27
|
+
if (profile.platform === "enterprise-python") {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Enterprise Python plugins usually have no local build step. Verify by BOS registration and functional testing; record the script path, plugin type, FormId, field/entity keys, and test data in VERIFY.md.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
27
32
|
throw new Error(`No build strategy for ${profile.product}/${profile.platform}/${profile.techStack}.`);
|
|
28
33
|
}
|
|
29
34
|
|