kcode-pi 0.1.2 → 0.1.5
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 +378 -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 +193 -0
- package/docs/DEVELOPMENT.md +162 -0
- package/docs/KCODE_DISTRIBUTION.md +1 -1
- package/extensions/kingdee-harness.ts +122 -0
- package/extensions/kingdee-tools.ts +9 -5
- 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 +215 -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/official/kingdee-skills.ts +549 -38
- package/src/platform/path.ts +38 -0
- package/src/product/profile.ts +29 -5
- package/src/rules/checker.ts +1 -1
- package/src/tools/build-debug.ts +7 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Enterprise Python Plugin
|
|
2
|
+
|
|
3
|
+
Enterprise Python plugin work is enabled only when the user explicitly asks for a Python plugin, IronPython, or a BOS/Enterprise Python script. Normal enterprise plugin requests use the C# BOS stack.
|
|
4
|
+
|
|
5
|
+
## Runtime And Scope
|
|
6
|
+
|
|
7
|
+
Kingdee Cloud Enterprise Python plugins run through IronPython inside BOS and call .NET assemblies such as `Kingdee.BOS`, `Kingdee.BOS.Core`, `Kingdee.BOS.App`, `Kingdee.BOS.Contracts`, and `Kingdee.BOS.ServiceHelper`.
|
|
8
|
+
|
|
9
|
+
Use Python plugins for form/list/operation/report/convert scripting when BOS registration supports it. Do not use Python for server interfaces, scheduled execution plans, or scenarios that require inheriting and overriding standard C# plugins; those remain C# work.
|
|
10
|
+
|
|
11
|
+
## Required Planning Facts
|
|
12
|
+
|
|
13
|
+
Before coding, confirm:
|
|
14
|
+
|
|
15
|
+
- The user explicitly requested a Python plugin, IronPython, or a BOS/Enterprise Python script.
|
|
16
|
+
- Plugin type: bill/form, list, operation service, report service, report form, or convert plugin.
|
|
17
|
+
- Target FormId, bill/entity, field keys, entity keys, operation code, and lifecycle event.
|
|
18
|
+
- Whether the script will be registered in BOS and where the project stores Python scripts.
|
|
19
|
+
- How to test in a BOS test data center.
|
|
20
|
+
|
|
21
|
+
## Common References
|
|
22
|
+
|
|
23
|
+
Typical imports:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import clr
|
|
27
|
+
clr.AddReference('System')
|
|
28
|
+
clr.AddReference('System.Data')
|
|
29
|
+
clr.AddReference('Kingdee.BOS')
|
|
30
|
+
clr.AddReference('Kingdee.BOS.Core')
|
|
31
|
+
clr.AddReference('Kingdee.BOS.App')
|
|
32
|
+
clr.AddReference('Kingdee.BOS.Contracts')
|
|
33
|
+
clr.AddReference('Kingdee.BOS.ServiceHelper')
|
|
34
|
+
|
|
35
|
+
from Kingdee.BOS import *
|
|
36
|
+
from Kingdee.BOS.Core import *
|
|
37
|
+
from Kingdee.BOS.Core.Bill import *
|
|
38
|
+
from Kingdee.BOS.Core.DynamicForm.PlugIn import *
|
|
39
|
+
from Kingdee.BOS.Core.DynamicForm.PlugIn.Args import *
|
|
40
|
+
from Kingdee.BOS.Core.DynamicForm.PlugIn.ControlModel import *
|
|
41
|
+
from Kingdee.BOS.App.Data import *
|
|
42
|
+
from Kingdee.BOS.ServiceHelper import *
|
|
43
|
+
from System.Collections.Generic import List
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use English punctuation and strict indentation. IronPython code does not use `new`; instantiate with `TypeName()`. Generic lists use forms such as `List[object]()` or `List[DynamicObject]()`.
|
|
47
|
+
|
|
48
|
+
## DynamicObject Patterns
|
|
49
|
+
|
|
50
|
+
Form plugins can read the current bill data from:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
billObj = this.View.Model.DataObject
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Base data fields return `DynamicObject`; read properties such as `Number`, `Name`, or `Id`. Base data ID fields often use the suffix `_Id`.
|
|
57
|
+
|
|
58
|
+
Entry entities are usually `DynamicObjectCollection`:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
entryRows = billObj["POOrderEntry"]
|
|
62
|
+
for row in entryRows:
|
|
63
|
+
materialId = row["MaterialId_Id"]
|
|
64
|
+
qty = row["Qty"]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
When assigning base data, set both the ID and the data object when required:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
field = this.View.BillBusinessInfo.GetField("FMaterialId")
|
|
71
|
+
material = BusinessDataServiceHelper.LoadSingle(this.Context, materialId, field.RefFormDynamicObjectType)
|
|
72
|
+
row["MaterialId_Id"] = materialId
|
|
73
|
+
row["MaterialId"] = material
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Refresh only the necessary view areas with `this.View.UpdateView("FFieldKey")` or the entity key.
|
|
77
|
+
|
|
78
|
+
## Form And Bill Plugin Events
|
|
79
|
+
|
|
80
|
+
Common bill/form plugin events:
|
|
81
|
+
|
|
82
|
+
- `AfterCreateModelData`: initialize default values after new bill data is created.
|
|
83
|
+
- `AfterBindData`: UI state changes after data binding.
|
|
84
|
+
- `BarItemClick`: toolbar/menu click handling.
|
|
85
|
+
- `ButtonClick`: button or hyperlink handling.
|
|
86
|
+
- `EntityRowClick` / `EntityRowDoubleClick`: entry row interactions.
|
|
87
|
+
- `DataChanged`: field value linkage when immediate trigger is enabled in BOS.
|
|
88
|
+
- `BeforeF7Select`: dynamic F7 filter.
|
|
89
|
+
- `BeforeDoOperation`: validate before an operation.
|
|
90
|
+
- `AfterDoOperation`: handle after an operation.
|
|
91
|
+
|
|
92
|
+
For UI messages in form/list plugins, use `this.View.ShowMessage`, `ShowWarnningMessage`, or `ShowErrMessage`.
|
|
93
|
+
|
|
94
|
+
## Operation Service Plugin Events
|
|
95
|
+
|
|
96
|
+
Operation service plugins do not have `this.View`.
|
|
97
|
+
|
|
98
|
+
Common events:
|
|
99
|
+
|
|
100
|
+
- `OnPreparePropertys`: declare required fields before loading data.
|
|
101
|
+
- `OnAddValidators`: register validators.
|
|
102
|
+
- `BeforeExecuteOperationTransaction`: before platform transaction execution.
|
|
103
|
+
- `BeginOperationTransaction`: transaction begins.
|
|
104
|
+
- `EndOperationTransaction`: transaction has completed and data is updated.
|
|
105
|
+
- `AfterExecuteOperationTransaction`: after transaction execution.
|
|
106
|
+
|
|
107
|
+
For service/report/convert plugins, raise an exception for blocking messages:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
raise Exception("提示信息")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Avoid independent save calls inside platform transaction hooks unless the plan explicitly accepts the transaction risk. Prefer modifying the data entities passed by the platform.
|
|
114
|
+
|
|
115
|
+
## Query And Save Patterns
|
|
116
|
+
|
|
117
|
+
Use platform helpers:
|
|
118
|
+
|
|
119
|
+
- `MetaDataServiceHelper.Load` or `GetFormMetaData` for form metadata.
|
|
120
|
+
- `BusinessDataServiceHelper.LoadSingle`, `Load`, `Save`, `Submit`, `Audit`, and `DoNothing`.
|
|
121
|
+
- `QueryServiceHelper.GetDynamicObjectCollection` for query builder results.
|
|
122
|
+
- `DBUtils.ExecuteDataSet` or `ExecuteDynamicObject` when direct SQL is required.
|
|
123
|
+
|
|
124
|
+
When writing SQL, use platform-safe patterns and avoid user-input string concatenation. Mark SQL dialect intentionally when using dialect SQL.
|
|
125
|
+
|
|
126
|
+
## Verification
|
|
127
|
+
|
|
128
|
+
Python plugin verification is usually BOS registration plus functional testing in a test data center. The plan and verification record should include:
|
|
129
|
+
|
|
130
|
+
- Script file path.
|
|
131
|
+
- Plugin type and BOS registration target.
|
|
132
|
+
- FormId, field keys, entity keys, and operation code evidence.
|
|
133
|
+
- Test data and operation steps.
|
|
134
|
+
- Runtime error or message evidence.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kcode-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Kingdee-specific package and harness for Pi Coding Agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"themes",
|
|
19
19
|
"knowledge",
|
|
20
20
|
"vendor",
|
|
21
|
-
"docs
|
|
21
|
+
"docs",
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"keywords": [
|
|
@@ -52,6 +52,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
|
|
|
52
52
|
|
|
53
53
|
4. 只有插件类型、生命周期方法、字段元数据和 API 签名都明确后,才生成或修改代码。
|
|
54
54
|
- 遵循项目已有包名、基类、常量、日志风格和 helper 封装。
|
|
55
|
+
- 星空旗舰版项目先检查当前目录结构,再选择真实目标路径。若存在 `code/`,跟随 `code/` 下既有组织;若不存在,跟随已发现的源码根或目标文件,不要在项目根目录新建 `src/main/java`。
|
|
55
56
|
- 优先使用项目本地封装和已批准的 Cosmic helper,再考虑底层 BOS API。
|
|
56
57
|
- 事件处理器只做当前事件阶段该做的事。
|
|
57
58
|
|
|
@@ -64,6 +65,7 @@ description: 金蝶 Cosmic 体系 Java 插件开发技能,适用于苍穹、
|
|
|
64
65
|
|
|
65
66
|
- 不臆造字段 key、表单 ID、操作名、枚举值、表名或 SDK 方法。
|
|
66
67
|
- 不把 Enterprise C# 命名空间或生命周期假设用于 Cosmic Java。
|
|
68
|
+
- 星空旗舰版不允许猜目录或写 demo/sample;如果无法判断真实代码位置,先询问或更新计划,不要直接创建新目录。
|
|
67
69
|
- 不在数据绑定前的初始化阶段操作 UI 控件。
|
|
68
70
|
- 不在平台期望修改入参数据实体的事务钩子里另起保存。
|
|
69
71
|
- 不在循环中查询或保存,除非计划中明确说明无法批量处理并接受风险。
|
|
@@ -56,6 +56,7 @@ POJO 或简单枚举场景可以简要说明后直接实现。
|
|
|
56
56
|
## 测试质量规则
|
|
57
57
|
|
|
58
58
|
- 使用项目已有 JUnit 和 Mockito 版本。
|
|
59
|
+
- 如果项目没有既有且允许使用的单测框架,不要新增 JUnit、Mockito 或额外 jar;改用 Harness 的红绿验证门禁,例如 API/基类/方法签名、元数据、编译或最小外部接口验证。
|
|
59
60
|
- import 保持显式;项目风格允许时也不要使用宽泛静态通配导入。
|
|
60
61
|
- 每个测试方法必须有真实断言或交互验证。
|
|
61
62
|
- 避免 `assertTrue(true)`、`assertEquals(x, x)` 或只执行不验证的测试。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kd-enterprise-python-plugin
|
|
3
|
+
description: Enterprise / Kingdee Cloud BOS Python plugin development with IronPython. Use only when the user explicitly asks for Python plugin, IronPython, or a BOS/Enterprise Python script.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Enterprise Python Plugin
|
|
7
|
+
|
|
8
|
+
Use this skill only when the user explicitly requests an Enterprise Python plugin, IronPython, or a BOS/Enterprise Python script. Default Enterprise plugin work is C# and must not use this skill unless Python is explicit.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
- Start from `kd_plan_status`.
|
|
13
|
+
- Keep the harness workflow: discuss -> spec -> plan -> execute -> verify.
|
|
14
|
+
- Do not write `.py` code before execute.
|
|
15
|
+
- In plan, inspect the current project and record the actual script/source path. Do not invent a demo path.
|
|
16
|
+
|
|
17
|
+
## Required Facts
|
|
18
|
+
|
|
19
|
+
Before coding, establish:
|
|
20
|
+
|
|
21
|
+
- Plugin type: form/bill, list, operation service, report service, report form, or convert plugin.
|
|
22
|
+
- Target FormId, bill/entity, field keys, entity keys, and operation code.
|
|
23
|
+
- Lifecycle event such as `AfterBindData`, `DataChanged`, `BeforeF7Select`, `BeforeDoOperation`, `BeginOperationTransaction`, or `EndOperationTransaction`.
|
|
24
|
+
- BOS registration location and test data center verification steps.
|
|
25
|
+
|
|
26
|
+
## Coding Rules
|
|
27
|
+
|
|
28
|
+
- Use IronPython syntax and .NET assembly references.
|
|
29
|
+
- Use `Kingdee.BOS.*` imports and helpers; do not use Cosmic Java `kd.bos.*` APIs.
|
|
30
|
+
- Form/list plugins may use `this.View`; operation service/report/convert plugins must not assume `this.View`.
|
|
31
|
+
- Use `this.View.ShowMessage` or warning/error variants for form/list UI messages.
|
|
32
|
+
- Use `raise Exception(...)` for service/report/convert blocking messages.
|
|
33
|
+
- Do not write sample/demo code. Adapt to actual FormId, field keys, entity keys, and existing project structure.
|
|
34
|
+
- Avoid independent saves inside transaction hooks unless explicitly planned.
|
|
35
|
+
|
|
36
|
+
## Verification
|
|
37
|
+
|
|
38
|
+
Record in `EXECUTION.md` and `VERIFY.md`:
|
|
39
|
+
|
|
40
|
+
- Changed `.py` file paths.
|
|
41
|
+
- BOS registration target.
|
|
42
|
+
- Confirmed FormId/field/entity/operation facts.
|
|
43
|
+
- Functional test steps and results.
|
|
@@ -11,12 +11,16 @@ Goal:
|
|
|
11
11
|
|
|
12
12
|
- Implement only the approved plan.
|
|
13
13
|
- Use `kd_search` and `kd_table` before relying on Kingdee APIs or table schemas.
|
|
14
|
-
- Update `.pi/kd/runs/<run-id>/EXECUTION.md` with
|
|
14
|
+
- Update `.pi/kd/runs/<run-id>/EXECUTION.md` with every planned `STEP-###`, changed files, and evidence files.
|
|
15
15
|
|
|
16
16
|
Rules:
|
|
17
17
|
|
|
18
18
|
- Do not broaden scope silently.
|
|
19
19
|
- Do not rewrite unrelated modules.
|
|
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.
|
|
20
21
|
- Do not mark work complete until verification runs.
|
|
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.
|
|
24
|
+
- Before entering verify, rerun the same check and record passing output in `evidence/tdd-green.md`.
|
|
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.
|
|
21
26
|
- If implementation needs a plan change, update `PLAN.md` first.
|
|
22
|
-
|
package/skills/kd-gen/SKILL.md
CHANGED
|
@@ -19,6 +19,7 @@ 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
|
+
- 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.
|
|
22
23
|
- Use `kd.bos.*` style packages for Cosmic-family Java code and `Kingdee.BOS.*` style namespaces for enterprise C# code.
|
|
23
24
|
- Do not reuse Cosmic/Xinghan/Cangqiong APIs for enterprise C#.
|
|
24
25
|
- Do not reuse enterprise C# namespaces or lifecycle assumptions for Java products.
|
package/skills/kd-plan/SKILL.md
CHANGED
|
@@ -11,14 +11,20 @@ Goal:
|
|
|
11
11
|
|
|
12
12
|
- Produce or update `.pi/kd/runs/<run-id>/PLAN.md`.
|
|
13
13
|
- List files to inspect before editing.
|
|
14
|
+
- List the inspected project layout and the exact target source root or file path before editing.
|
|
14
15
|
- List expected files to modify.
|
|
15
16
|
- List required `kd_search` and `kd_table` lookups.
|
|
17
|
+
- List `## Execution Steps` using `- [ ] STEP-001: ...` style IDs.
|
|
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.
|
|
16
20
|
- Define validation commands and expected evidence.
|
|
17
21
|
- Add rollback or containment notes for medium/high risk work.
|
|
18
22
|
|
|
19
23
|
Gate:
|
|
20
24
|
|
|
21
25
|
- Execution must not start without `PLAN.md`.
|
|
26
|
+
- A plan for 星空旗舰版 is incomplete unless it records the existing project layout and exact target path to edit. If `code/` exists, follow its actual structure; if it does not, record the discovered source root or existing target file.
|
|
22
27
|
- A plan without validation commands is incomplete.
|
|
28
|
+
- A plan without structured `STEP-001` execution steps is incomplete.
|
|
29
|
+
- A plan without TDD red/green checks is incomplete.
|
|
23
30
|
- A plan that relies on unverified Kingdee API names is incomplete.
|
|
24
|
-
|
package/src/cli/kcode.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { ensureProjectContext, writeProjectContext } from "../context/project-context.js";
|
|
6
7
|
|
|
7
8
|
export interface KcodeCliResult {
|
|
8
9
|
exitCode: number;
|
|
@@ -31,6 +32,8 @@ export function runKcodeCli(args: string[], cwd = process.cwd()): KcodeCliResult
|
|
|
31
32
|
switch (command) {
|
|
32
33
|
case "init":
|
|
33
34
|
return initProject(cwd);
|
|
35
|
+
case "context":
|
|
36
|
+
return context(cwd, args.slice(1));
|
|
34
37
|
case "doctor":
|
|
35
38
|
return doctor(cwd);
|
|
36
39
|
case "start":
|
|
@@ -55,10 +58,20 @@ export function initProject(cwd: string): KcodeCliResult {
|
|
|
55
58
|
settings.packages = packages;
|
|
56
59
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
57
60
|
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
61
|
+
const projectContext = ensureProjectContext(cwd);
|
|
58
62
|
|
|
59
63
|
return {
|
|
60
64
|
exitCode: 0,
|
|
61
|
-
output: [`已更新项目级 Pi 配置:${settingsPath}`, `已保留当前 KCode package:${kcodePackage}`].join("\n"),
|
|
65
|
+
output: [`已更新项目级 Pi 配置:${settingsPath}`, `已保留当前 KCode package:${kcodePackage}`, `项目上下文:${projectContext.path}`].join("\n"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function context(cwd: string, args: string[]): KcodeCliResult {
|
|
70
|
+
const refresh = args.includes("--refresh") || args.includes("-r");
|
|
71
|
+
const projectContext = refresh ? writeProjectContext(cwd) : ensureProjectContext(cwd);
|
|
72
|
+
return {
|
|
73
|
+
exitCode: 0,
|
|
74
|
+
output: [`项目上下文已${refresh ? "刷新" : "就绪"}:${projectContext.path}`, "", projectContext.content].join("\n"),
|
|
62
75
|
};
|
|
63
76
|
}
|
|
64
77
|
|
|
@@ -73,6 +86,7 @@ export function doctor(cwd: string): KcodeCliResult {
|
|
|
73
86
|
lines.push(`Pi CLI:${formatPiCliStatus(piCli, pi)}`);
|
|
74
87
|
lines.push(`KCode package:${packageRoot}`);
|
|
75
88
|
lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,请先运行 kcode init"}`);
|
|
89
|
+
lines.push(`项目上下文:${existsSync(join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md")) ? join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md") : "未创建,请运行 kcode context"}`);
|
|
76
90
|
|
|
77
91
|
if (existsSync(settingsPath)) {
|
|
78
92
|
const settings = readSettings(settingsPath);
|
|
@@ -213,6 +227,7 @@ function helpText(): string {
|
|
|
213
227
|
"",
|
|
214
228
|
"用法:",
|
|
215
229
|
" kcode init 初始化当前项目的 .pi/settings.json",
|
|
230
|
+
" kcode context 生成或刷新 .pi/kd/PROJECT_CONTEXT.md",
|
|
216
231
|
" kcode doctor 检查 Node、随包 Pi CLI、KCode package 和项目级配置",
|
|
217
232
|
" kcode start 初始化项目配置后启动 KCode 工作环境",
|
|
218
233
|
].join("\n");
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, extname, join, relative } from "node:path";
|
|
3
|
+
|
|
4
|
+
const IGNORED_DIRS = new Set([
|
|
5
|
+
".git",
|
|
6
|
+
".idea",
|
|
7
|
+
".pi",
|
|
8
|
+
".tmp",
|
|
9
|
+
".vscode",
|
|
10
|
+
"bin",
|
|
11
|
+
"build",
|
|
12
|
+
"dist",
|
|
13
|
+
"node_modules",
|
|
14
|
+
"out",
|
|
15
|
+
"target",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const SOURCE_EXTENSIONS = new Set([".java", ".kt", ".kts", ".cs", ".py", ".xml", ".properties", ".yml", ".yaml", ".sql", ".ksql"]);
|
|
19
|
+
const BUILD_FILE_NAMES = new Set(["pom.xml", "build.gradle", "build.gradle.kts", "gradlew", "gradlew.bat", "mvnw", "mvnw.cmd", "package.json"]);
|
|
20
|
+
const MAX_DEPTH = 5;
|
|
21
|
+
const MAX_ENTRIES = 3000;
|
|
22
|
+
const MAX_SOURCE_SAMPLES = 60;
|
|
23
|
+
const MAX_MODULES = 80;
|
|
24
|
+
|
|
25
|
+
export interface ProjectContextResult {
|
|
26
|
+
path: string;
|
|
27
|
+
content: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function projectContextPath(cwd: string): string {
|
|
31
|
+
return join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readProjectContext(cwd: string): string | undefined {
|
|
35
|
+
const path = projectContextPath(cwd);
|
|
36
|
+
return existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ensureProjectContext(cwd: string): ProjectContextResult {
|
|
40
|
+
const path = projectContextPath(cwd);
|
|
41
|
+
if (existsSync(path)) return { path, content: readFileSync(path, "utf8") };
|
|
42
|
+
return writeProjectContext(cwd);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function writeProjectContext(cwd: string): ProjectContextResult {
|
|
46
|
+
const path = projectContextPath(cwd);
|
|
47
|
+
const content = generateProjectContext(cwd);
|
|
48
|
+
mkdirSync(join(cwd, ".pi", "kd"), { recursive: true });
|
|
49
|
+
writeFileSync(path, content, "utf8");
|
|
50
|
+
return { path, content };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function generateProjectContext(cwd: string): string {
|
|
54
|
+
const scan = scanProject(cwd);
|
|
55
|
+
const codeDir = scan.directories.includes("code");
|
|
56
|
+
const likelyRoots = detectLikelySourceRoots(scan);
|
|
57
|
+
const modules = detectModules(scan);
|
|
58
|
+
const buildFiles = scan.files.filter((file) => BUILD_FILE_NAMES.has(basename(file).toLowerCase()) || isSolutionOrProject(file));
|
|
59
|
+
const sourceSamples = scan.files.filter((file) => SOURCE_EXTENSIONS.has(extname(file).toLowerCase())).slice(0, MAX_SOURCE_SAMPLES);
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
"# KCode Project Context",
|
|
63
|
+
"",
|
|
64
|
+
`- Project root: ${cwd}`,
|
|
65
|
+
`- Project name: ${basename(cwd)}`,
|
|
66
|
+
`- Generated at: ${new Date().toISOString()}`,
|
|
67
|
+
"",
|
|
68
|
+
"## Persistent Rules",
|
|
69
|
+
"",
|
|
70
|
+
"- This file is project memory for KCode. Read it before planning or editing code.",
|
|
71
|
+
"- Do not create demo/sample/scaffold code for business requirements.",
|
|
72
|
+
"- Do not assume module layout. Follow the actual paths below and verify target files before editing.",
|
|
73
|
+
"- Use project-relative paths when calling file tools. On Windows, do not rewrite paths to /mnt/<drive>/... or /<drive>/...; use Windows paths only when an absolute path is necessary.",
|
|
74
|
+
"- If this file is stale, regenerate with `kcode context --refresh` before planning.",
|
|
75
|
+
"- Write product code only after the Harness reaches `execute` and PLAN.md names the real target path.",
|
|
76
|
+
"",
|
|
77
|
+
"## Layout Summary",
|
|
78
|
+
"",
|
|
79
|
+
`- Has code directory: ${codeDir ? "yes" : "no"}`,
|
|
80
|
+
`- Likely source roots: ${formatList(likelyRoots)}`,
|
|
81
|
+
`- Build files: ${formatList(buildFiles)}`,
|
|
82
|
+
`- Detected modules: ${formatList(modules)}`,
|
|
83
|
+
"",
|
|
84
|
+
"## Source Samples",
|
|
85
|
+
"",
|
|
86
|
+
formatBlockList(sourceSamples),
|
|
87
|
+
"",
|
|
88
|
+
"## Top-Level Directories",
|
|
89
|
+
"",
|
|
90
|
+
formatBlockList(scan.directories.filter((dir) => !dir.includes("/") && !dir.includes("\\")).sort()),
|
|
91
|
+
"",
|
|
92
|
+
].join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface ProjectScan {
|
|
96
|
+
files: string[];
|
|
97
|
+
directories: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function scanProject(cwd: string): ProjectScan {
|
|
101
|
+
const files: string[] = [];
|
|
102
|
+
const directories: string[] = [];
|
|
103
|
+
const queue: Array<{ path: string; depth: number }> = [{ path: cwd, depth: 0 }];
|
|
104
|
+
let entries = 0;
|
|
105
|
+
|
|
106
|
+
while (queue.length > 0 && entries < MAX_ENTRIES) {
|
|
107
|
+
const current = queue.shift();
|
|
108
|
+
if (!current) break;
|
|
109
|
+
|
|
110
|
+
let children: string[];
|
|
111
|
+
try {
|
|
112
|
+
children = readdirSync(current.path);
|
|
113
|
+
} catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const child of children) {
|
|
118
|
+
if (entries >= MAX_ENTRIES) break;
|
|
119
|
+
const fullPath = join(current.path, child);
|
|
120
|
+
let stat;
|
|
121
|
+
try {
|
|
122
|
+
stat = statSync(fullPath);
|
|
123
|
+
} catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rel = normalize(relative(cwd, fullPath));
|
|
128
|
+
entries++;
|
|
129
|
+
|
|
130
|
+
if (stat.isDirectory()) {
|
|
131
|
+
if (IGNORED_DIRS.has(child)) continue;
|
|
132
|
+
directories.push(rel);
|
|
133
|
+
if (current.depth < MAX_DEPTH) queue.push({ path: fullPath, depth: current.depth + 1 });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (stat.isFile()) files.push(rel);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { files: files.sort(), directories: directories.sort() };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function detectLikelySourceRoots(scan: ProjectScan): string[] {
|
|
145
|
+
const roots = new Set<string>();
|
|
146
|
+
const knownRoots = [
|
|
147
|
+
"code/src/main/java",
|
|
148
|
+
"code/src/main/resources",
|
|
149
|
+
"src/main/java",
|
|
150
|
+
"src/main/resources",
|
|
151
|
+
"src",
|
|
152
|
+
"script",
|
|
153
|
+
"scripts",
|
|
154
|
+
"plugins",
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
for (const root of knownRoots) {
|
|
158
|
+
if (scan.directories.includes(root) || scan.files.some((file) => file.startsWith(`${root}/`))) roots.add(root);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const file of scan.files) {
|
|
162
|
+
if (!SOURCE_EXTENSIONS.has(extname(file).toLowerCase())) continue;
|
|
163
|
+
const marker = sourceRootFromFile(file);
|
|
164
|
+
if (marker) roots.add(marker);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [...roots].slice(0, MAX_MODULES);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function detectModules(scan: ProjectScan): string[] {
|
|
171
|
+
const modules = new Set<string>();
|
|
172
|
+
|
|
173
|
+
for (const file of scan.files) {
|
|
174
|
+
const name = basename(file).toLowerCase();
|
|
175
|
+
if (!BUILD_FILE_NAMES.has(name) && !isSolutionOrProject(file)) continue;
|
|
176
|
+
const dir = normalize(file.slice(0, -basename(file).length).replace(/[\\/]$/, ""));
|
|
177
|
+
modules.add(dir || ".");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const sourceRoot of detectLikelySourceRoots(scan)) {
|
|
181
|
+
const parts = sourceRoot.split("/");
|
|
182
|
+
if (parts.length > 3) modules.add(parts.slice(0, -3).join("/") || ".");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return [...modules].filter(Boolean).sort().slice(0, MAX_MODULES);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sourceRootFromFile(file: string): string | undefined {
|
|
189
|
+
const normalized = normalize(file);
|
|
190
|
+
const javaIndex = normalized.indexOf("/src/main/java/");
|
|
191
|
+
if (javaIndex >= 0) return normalized.slice(0, javaIndex + "/src/main/java".length);
|
|
192
|
+
const resourcesIndex = normalized.indexOf("/src/main/resources/");
|
|
193
|
+
if (resourcesIndex >= 0) return normalized.slice(0, resourcesIndex + "/src/main/resources".length);
|
|
194
|
+
const srcIndex = normalized.indexOf("/src/");
|
|
195
|
+
if (srcIndex >= 0) return normalized.slice(0, srcIndex + "/src".length);
|
|
196
|
+
const first = normalized.split("/")[0];
|
|
197
|
+
return first && SOURCE_EXTENSIONS.has(extname(normalized).toLowerCase()) ? first : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isSolutionOrProject(file: string): boolean {
|
|
201
|
+
return [".sln", ".csproj"].includes(extname(file).toLowerCase());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatList(values: string[]): string {
|
|
205
|
+
return values.length > 0 ? values.join(", ") : "none detected";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function formatBlockList(values: string[]): string {
|
|
209
|
+
if (values.length === 0) return "- none detected";
|
|
210
|
+
return values.map((value) => `- ${value}`).join("\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalize(path: string): string {
|
|
214
|
+
return path.replace(/\\/g, "/");
|
|
215
|
+
}
|
package/src/harness/artifacts.ts
CHANGED
|
@@ -73,19 +73,53 @@ export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?:
|
|
|
73
73
|
return [
|
|
74
74
|
"# Plan",
|
|
75
75
|
"",
|
|
76
|
+
"## Project Structure Checked",
|
|
77
|
+
"",
|
|
76
78
|
"## Files to Inspect",
|
|
77
79
|
"",
|
|
80
|
+
"## Target Source Root / Path",
|
|
81
|
+
"",
|
|
78
82
|
"## Files to Edit",
|
|
79
83
|
"",
|
|
80
84
|
"## Required Kingdee Lookups",
|
|
81
85
|
"",
|
|
86
|
+
"## Execution Steps",
|
|
87
|
+
"",
|
|
88
|
+
"- [ ] STEP-001: Inspect the existing target files and confirm exact edit locations.",
|
|
89
|
+
"- [ ] STEP-002: Run a failing red check and record red evidence.",
|
|
90
|
+
"- [ ] STEP-003: Implement only the approved file changes.",
|
|
91
|
+
"- [ ] STEP-004: Run the same check again and record green evidence.",
|
|
92
|
+
"- [ ] STEP-005: Record changed files and step evidence.",
|
|
93
|
+
"",
|
|
94
|
+
"## TDD / Red-Green Checks",
|
|
95
|
+
"",
|
|
96
|
+
"- Red evidence: evidence/tdd-red.md",
|
|
97
|
+
"- Green evidence: evidence/tdd-green.md",
|
|
98
|
+
"- Red/green command or tool: unknown",
|
|
99
|
+
"- Allowed checks: official API/base-class/method lookup, metadata lookup, kd_check, build/compile output, existing project test framework, or a minimal external-interface test.",
|
|
100
|
+
"- Do not add third-party test jars or frameworks just to satisfy this gate.",
|
|
101
|
+
"- If automated tests are impossible, document the product-specific check that can fail before implementation and pass after implementation.",
|
|
102
|
+
"",
|
|
82
103
|
"## Validation Commands",
|
|
83
104
|
"",
|
|
84
105
|
"## Rollback / Containment",
|
|
85
106
|
"",
|
|
86
107
|
].join("\n");
|
|
87
108
|
case "execute":
|
|
88
|
-
return [
|
|
109
|
+
return [
|
|
110
|
+
"# Execution",
|
|
111
|
+
"",
|
|
112
|
+
"## Step Results",
|
|
113
|
+
"",
|
|
114
|
+
"Record each planned step as:",
|
|
115
|
+
"",
|
|
116
|
+
"- [x] STEP-001: completed. Evidence: evidence/step-001.md",
|
|
117
|
+
"",
|
|
118
|
+
"## Changed Files",
|
|
119
|
+
"",
|
|
120
|
+
"## Deviations",
|
|
121
|
+
"",
|
|
122
|
+
].join("\n");
|
|
89
123
|
case "verify":
|
|
90
124
|
return ["# Verify", "", "## Commands", "", "## Evidence", "", "## Residual Risk", ""].join("\n");
|
|
91
125
|
case "ship":
|
package/src/harness/gates.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { ActiveRun, GateResult, KdPhase } from "./types.ts";
|
|
|
2
2
|
import { PHASE_ARTIFACTS, PHASE_ORDER } from "./types.ts";
|
|
3
3
|
import { artifactExists, readArtifact } from "./artifacts.ts";
|
|
4
4
|
import { isKnownProduct } from "../product/profile.ts";
|
|
5
|
+
import { flagshipPlanBlockReason } from "./path-policy.ts";
|
|
6
|
+
import { executionStepsBlockReason, planStepsBlockReason } from "./plan-steps.ts";
|
|
7
|
+
import { tddPlanBlockReason, tddVerifyBlockReason } from "./tdd-policy.ts";
|
|
5
8
|
|
|
6
9
|
const REQUIRED_MARKERS: Partial<Record<KdPhase, string[]>> = {
|
|
7
10
|
plan: ["## Validation Commands"],
|
|
@@ -25,10 +28,12 @@ export function inspectGate(cwd: string, run: ActiveRun): GateResult {
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const markerProblem = inspectMarkers(cwd, run, run.phase);
|
|
31
|
+
const stepProblem = inspectStepState(cwd, run, run.phase);
|
|
28
32
|
const evidenceProblem = inspectEvidence(cwd, run, run.phase);
|
|
29
33
|
const reasonParts = [];
|
|
30
34
|
if (missing.length > 0) reasonParts.push(`缺少必需产物:${[...new Set(missing)].join(", ")}`);
|
|
31
35
|
if (markerProblem) reasonParts.push(markerProblem);
|
|
36
|
+
if (stepProblem) reasonParts.push(stepProblem);
|
|
32
37
|
if (evidenceProblem) reasonParts.push(evidenceProblem);
|
|
33
38
|
|
|
34
39
|
return {
|
|
@@ -57,6 +62,16 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
|
|
|
57
62
|
missing.push(PHASE_ARTIFACTS.plan);
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
const flagshipPathProblem = target === "execute" ? flagshipPlanBlockReason(cwd, run, readArtifact(cwd, run, "plan") ?? "") : undefined;
|
|
66
|
+
if (flagshipPathProblem) {
|
|
67
|
+
reasonParts.push(flagshipPathProblem);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const stepProblem = inspectStepState(cwd, run, target);
|
|
71
|
+
if (stepProblem) {
|
|
72
|
+
reasonParts.push(stepProblem);
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
missing.push(...missingEvidenceForPhase(cwd, run, target));
|
|
61
76
|
|
|
62
77
|
if (target === "ship" && !artifactExists(cwd, run, PHASE_ARTIFACTS.verify)) {
|
|
@@ -72,6 +87,20 @@ export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): Gat
|
|
|
72
87
|
};
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
function inspectStepState(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
91
|
+
if (phase === "execute") {
|
|
92
|
+
const plan = readArtifact(cwd, run, "plan") ?? "";
|
|
93
|
+
return planStepsBlockReason(plan) ?? tddPlanBlockReason(plan);
|
|
94
|
+
}
|
|
95
|
+
if (phase === "verify" || phase === "ship") {
|
|
96
|
+
return (
|
|
97
|
+
executionStepsBlockReason(cwd, run, readArtifact(cwd, run, "plan") ?? "", readArtifact(cwd, run, "execute") ?? "") ??
|
|
98
|
+
tddVerifyBlockReason(cwd, run)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
function inspectMarkers(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
76
105
|
const markers = REQUIRED_MARKERS[phase];
|
|
77
106
|
if (!markers?.length) return undefined;
|