kcode-pi 0.1.0 → 0.1.2
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/dist/cli/kcode.js +26 -6
- package/package.json +2 -1
- package/src/cli/kcode.ts +219 -0
- package/src/cli/main.ts +10 -0
- package/src/harness/artifacts.ts +94 -0
- package/src/harness/format.ts +30 -0
- package/src/harness/gates.ts +136 -0
- package/src/harness/paths.ts +23 -0
- package/src/harness/state.ts +117 -0
- package/src/harness/types.ts +42 -0
- package/src/knowledge/format.ts +48 -0
- package/src/knowledge/loader.ts +147 -0
- package/src/knowledge/search.ts +118 -0
- package/src/knowledge/types.ts +64 -0
- package/src/official/kingdee-skills.ts +230 -0
- package/src/product/profile.ts +115 -0
- package/src/rules/checker.ts +612 -0
- package/src/tools/build-debug.ts +214 -0
package/dist/cli/kcode.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
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
6
|
const packageRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
|
+
const packageName = readPackageName(packageRoot) ?? "kcode-pi";
|
|
8
9
|
export function runKcodeCli(args, cwd = process.cwd()) {
|
|
9
10
|
const command = args[0] ?? "help";
|
|
10
11
|
switch (command) {
|
|
@@ -26,16 +27,14 @@ export function initProject(cwd) {
|
|
|
26
27
|
const settingsPath = projectSettingsPath(cwd);
|
|
27
28
|
const settings = readSettings(settingsPath);
|
|
28
29
|
const kcodePackage = normalizePath(packageRoot);
|
|
29
|
-
const packages = settings.packages ?? [];
|
|
30
|
-
|
|
31
|
-
packages.unshift(kcodePackage);
|
|
32
|
-
}
|
|
30
|
+
const packages = (settings.packages ?? []).filter((pkg) => !isSameKcodePackage(pkg, kcodePackage));
|
|
31
|
+
packages.unshift(kcodePackage);
|
|
33
32
|
settings.packages = packages;
|
|
34
33
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
35
34
|
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
36
35
|
return {
|
|
37
36
|
exitCode: 0,
|
|
38
|
-
output: [`已更新项目级 Pi 配置:${settingsPath}`,
|
|
37
|
+
output: [`已更新项目级 Pi 配置:${settingsPath}`, `已保留当前 KCode package:${kcodePackage}`].join("\n"),
|
|
39
38
|
};
|
|
40
39
|
}
|
|
41
40
|
export function doctor(cwd) {
|
|
@@ -107,6 +106,27 @@ function readSettings(path) {
|
|
|
107
106
|
function normalizePath(path) {
|
|
108
107
|
return resolve(path);
|
|
109
108
|
}
|
|
109
|
+
function isSameKcodePackage(candidate, currentPackage) {
|
|
110
|
+
const normalized = normalizePath(candidate);
|
|
111
|
+
if (normalized === currentPackage)
|
|
112
|
+
return true;
|
|
113
|
+
const candidateName = readPackageName(normalized) ?? packageNameFromPath(normalized);
|
|
114
|
+
return candidateName === packageName;
|
|
115
|
+
}
|
|
116
|
+
function readPackageName(packagePath) {
|
|
117
|
+
try {
|
|
118
|
+
const packageJson = JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8"));
|
|
119
|
+
return packageJson.name;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function packageNameFromPath(path) {
|
|
126
|
+
const name = basename(path);
|
|
127
|
+
const scope = basename(dirname(path));
|
|
128
|
+
return scope.startsWith("@") ? `${scope}/${name}` : name;
|
|
129
|
+
}
|
|
110
130
|
function bundledPiCliPath() {
|
|
111
131
|
const directPath = join(packageRoot, "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js");
|
|
112
132
|
if (existsSync(directPath))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kcode-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Kingdee-specific package and harness for Pi Coding Agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"types": "./dist/cli/kcode.d.ts",
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
|
+
"src",
|
|
14
15
|
"extensions",
|
|
15
16
|
"skills",
|
|
16
17
|
"prompts",
|
package/src/cli/kcode.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
|
|
7
|
+
export interface KcodeCliResult {
|
|
8
|
+
exitCode: number;
|
|
9
|
+
output: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PiSettings {
|
|
13
|
+
packages?: string[];
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const packageRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const packageName = readPackageName(packageRoot) ?? "kcode-pi";
|
|
20
|
+
|
|
21
|
+
export interface PiCliCommand {
|
|
22
|
+
command: string;
|
|
23
|
+
args: string[];
|
|
24
|
+
source: "bundled" | "global";
|
|
25
|
+
displayPath: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function runKcodeCli(args: string[], cwd = process.cwd()): KcodeCliResult {
|
|
29
|
+
const command = args[0] ?? "help";
|
|
30
|
+
|
|
31
|
+
switch (command) {
|
|
32
|
+
case "init":
|
|
33
|
+
return initProject(cwd);
|
|
34
|
+
case "doctor":
|
|
35
|
+
return doctor(cwd);
|
|
36
|
+
case "start":
|
|
37
|
+
return start(cwd, args.slice(1));
|
|
38
|
+
case "help":
|
|
39
|
+
case "--help":
|
|
40
|
+
case "-h":
|
|
41
|
+
return { exitCode: 0, output: helpText() };
|
|
42
|
+
default:
|
|
43
|
+
return { exitCode: 1, output: `未知命令:${command}\n\n${helpText()}` };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function initProject(cwd: string): KcodeCliResult {
|
|
48
|
+
const settingsPath = projectSettingsPath(cwd);
|
|
49
|
+
const settings = readSettings(settingsPath);
|
|
50
|
+
const kcodePackage = normalizePath(packageRoot);
|
|
51
|
+
const packages = (settings.packages ?? []).filter((pkg) => !isSameKcodePackage(pkg, kcodePackage));
|
|
52
|
+
|
|
53
|
+
packages.unshift(kcodePackage);
|
|
54
|
+
|
|
55
|
+
settings.packages = packages;
|
|
56
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
57
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
exitCode: 0,
|
|
61
|
+
output: [`已更新项目级 Pi 配置:${settingsPath}`, `已保留当前 KCode package:${kcodePackage}`].join("\n"),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function doctor(cwd: string): KcodeCliResult {
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
const node = spawnSync("node", ["--version"], { encoding: "utf8" });
|
|
68
|
+
const piCli = resolvePiCliCommand(["--version"]);
|
|
69
|
+
const pi = piCli ? spawnSync(piCli.command, piCli.args, { encoding: "utf8" }) : undefined;
|
|
70
|
+
const settingsPath = projectSettingsPath(cwd);
|
|
71
|
+
|
|
72
|
+
lines.push(`Node:${node.status === 0 ? node.stdout.trim() : "未找到"}`);
|
|
73
|
+
lines.push(`Pi CLI:${formatPiCliStatus(piCli, pi)}`);
|
|
74
|
+
lines.push(`KCode package:${packageRoot}`);
|
|
75
|
+
lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,请先运行 kcode init"}`);
|
|
76
|
+
|
|
77
|
+
if (existsSync(settingsPath)) {
|
|
78
|
+
const settings = readSettings(settingsPath);
|
|
79
|
+
const hasKcode = (settings.packages ?? []).includes(normalizePath(packageRoot));
|
|
80
|
+
lines.push(`KCode package 已登记:${hasKcode ? "是" : "否"}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
exitCode: pi?.status === 0 ? 0 : 1,
|
|
85
|
+
output: lines.join("\n"),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function start(cwd: string, piArgs: string[]): KcodeCliResult {
|
|
90
|
+
const init = initProject(cwd);
|
|
91
|
+
const piCli = resolvePiCliCommand(piArgs);
|
|
92
|
+
|
|
93
|
+
if (!piCli) {
|
|
94
|
+
return {
|
|
95
|
+
exitCode: 1,
|
|
96
|
+
output: `${init.output}\n未找到随包 Pi CLI 或全局 pi 命令。请重新安装 kcode-cli 后再运行 kcode start。`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pi = spawnSync(piCli.command, piCli.args, { cwd, stdio: "inherit", shell: false });
|
|
101
|
+
|
|
102
|
+
if (pi.error || pi.status === null) {
|
|
103
|
+
return {
|
|
104
|
+
exitCode: 1,
|
|
105
|
+
output: `${init.output}\nPi CLI 启动失败:${pi.error?.message ?? piCli.displayPath}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
exitCode: pi.status,
|
|
111
|
+
output: init.output,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolvePiCliCommand(piArgs: string[] = []): PiCliCommand | undefined {
|
|
116
|
+
const bundledPi = bundledPiCliPath();
|
|
117
|
+
|
|
118
|
+
if (bundledPi) {
|
|
119
|
+
return {
|
|
120
|
+
command: process.execPath,
|
|
121
|
+
args: [bundledPi, ...piArgs],
|
|
122
|
+
source: "bundled",
|
|
123
|
+
displayPath: bundledPi,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
command: "pi",
|
|
129
|
+
args: piArgs,
|
|
130
|
+
source: "global",
|
|
131
|
+
displayPath: "pi",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function projectSettingsPath(cwd: string): string {
|
|
136
|
+
return join(cwd, ".pi", "settings.json");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readSettings(path: string): PiSettings {
|
|
140
|
+
if (!existsSync(path)) return {};
|
|
141
|
+
return JSON.parse(readFileSync(path, "utf8")) as PiSettings;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizePath(path: string): string {
|
|
145
|
+
return resolve(path);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isSameKcodePackage(candidate: string, currentPackage: string): boolean {
|
|
149
|
+
const normalized = normalizePath(candidate);
|
|
150
|
+
if (normalized === currentPackage) return true;
|
|
151
|
+
|
|
152
|
+
const candidateName = readPackageName(normalized) ?? packageNameFromPath(normalized);
|
|
153
|
+
return candidateName === packageName;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readPackageName(packagePath: string): string | undefined {
|
|
157
|
+
try {
|
|
158
|
+
const packageJson = JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8")) as { name?: string };
|
|
159
|
+
return packageJson.name;
|
|
160
|
+
} catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function packageNameFromPath(path: string): string | undefined {
|
|
166
|
+
const name = basename(path);
|
|
167
|
+
const scope = basename(dirname(path));
|
|
168
|
+
return scope.startsWith("@") ? `${scope}/${name}` : name;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function bundledPiCliPath(): string | undefined {
|
|
172
|
+
const directPath = join(packageRoot, "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js");
|
|
173
|
+
if (existsSync(directPath)) return directPath;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const mainPath = require.resolve("@earendil-works/pi-coding-agent");
|
|
177
|
+
const packageDir = dirname(dirname(mainPath));
|
|
178
|
+
const cliPath = join(packageDir, "dist", "cli.js");
|
|
179
|
+
return existsSync(cliPath) ? cliPath : undefined;
|
|
180
|
+
} catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatPiCliStatus(
|
|
186
|
+
piCli: PiCliCommand | undefined,
|
|
187
|
+
result: ReturnType<typeof spawnSync> | undefined,
|
|
188
|
+
): string {
|
|
189
|
+
if (!piCli || result?.status !== 0) {
|
|
190
|
+
return "未找到";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const version = result.stdout?.toString().trim() || result.stderr?.toString().trim() || piCliPackageVersion(piCli) || "可用";
|
|
194
|
+
const source = piCli.source === "bundled" ? "随包" : "全局";
|
|
195
|
+
return `${version}(${source}:${piCli.displayPath})`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function piCliPackageVersion(piCli: PiCliCommand): string | undefined {
|
|
199
|
+
if (piCli.source !== "bundled") return undefined;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const packageJsonPath = join(dirname(dirname(piCli.displayPath)), "package.json");
|
|
203
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: string };
|
|
204
|
+
return packageJson.version;
|
|
205
|
+
} catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function helpText(): string {
|
|
211
|
+
return [
|
|
212
|
+
"KCode 企业入口",
|
|
213
|
+
"",
|
|
214
|
+
"用法:",
|
|
215
|
+
" kcode init 初始化当前项目的 .pi/settings.json",
|
|
216
|
+
" kcode doctor 检查 Node、随包 Pi CLI、KCode package 和项目级配置",
|
|
217
|
+
" kcode start 初始化项目配置后启动 KCode 工作环境",
|
|
218
|
+
].join("\n");
|
|
219
|
+
}
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { ActiveRun, KdPhase } from "./types.ts";
|
|
3
|
+
import { PHASE_ARTIFACTS } from "./types.ts";
|
|
4
|
+
import { runArtifactPath, runRoot } from "./paths.ts";
|
|
5
|
+
import type { ProductProfile } from "../product/profile.ts";
|
|
6
|
+
|
|
7
|
+
export function ensureRunDirectories(cwd: string, run: ActiveRun): void {
|
|
8
|
+
mkdirSync(runRoot(cwd, run), { recursive: true });
|
|
9
|
+
mkdirSync(runArtifactPath(cwd, run, "evidence"), { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function artifactExists(cwd: string, run: ActiveRun, artifactName: string): boolean {
|
|
13
|
+
return existsSync(runArtifactPath(cwd, run, artifactName));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function phaseArtifactPath(cwd: string, run: ActiveRun, phase: KdPhase): string {
|
|
17
|
+
return runArtifactPath(cwd, run, PHASE_ARTIFACTS[phase]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readArtifact(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
21
|
+
const path = phaseArtifactPath(cwd, run, phase);
|
|
22
|
+
return existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeArtifact(cwd: string, run: ActiveRun, phase: KdPhase, content: string): string {
|
|
26
|
+
ensureRunDirectories(cwd, run);
|
|
27
|
+
const path = phaseArtifactPath(cwd, run, phase);
|
|
28
|
+
writeFileSync(path, content.endsWith("\n") ? content : `${content}\n`, "utf8");
|
|
29
|
+
run.artifacts[phase] = PHASE_ARTIFACTS[phase];
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ensureArtifact(cwd: string, run: ActiveRun, phase: KdPhase, content: string): string {
|
|
34
|
+
const existing = readArtifact(cwd, run, phase);
|
|
35
|
+
if (existing !== undefined) return phaseArtifactPath(cwd, run, phase);
|
|
36
|
+
return writeArtifact(cwd, run, phase, content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function defaultArtifactContent(phase: KdPhase, goal?: string, profile?: ProductProfile): string {
|
|
40
|
+
switch (phase) {
|
|
41
|
+
case "discuss":
|
|
42
|
+
return [
|
|
43
|
+
"# Context",
|
|
44
|
+
"",
|
|
45
|
+
`- Goal: ${goal ?? "unknown"}`,
|
|
46
|
+
`- Product: ${profile?.product ?? "unknown"}`,
|
|
47
|
+
"- Version: unknown",
|
|
48
|
+
`- Tech stack: ${profile?.techStack ?? "unknown"}`,
|
|
49
|
+
`- Language: ${profile?.language ?? "unknown"}`,
|
|
50
|
+
"- Plugin type: unknown",
|
|
51
|
+
"- Target bill/entity/form: unknown",
|
|
52
|
+
"- Out of scope: unknown",
|
|
53
|
+
"- Open questions:",
|
|
54
|
+
" - Confirm Kingdee product/version, tech stack, plugin type, and target object.",
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
case "spec":
|
|
58
|
+
return [
|
|
59
|
+
"# Spec",
|
|
60
|
+
"",
|
|
61
|
+
"## Acceptance Criteria",
|
|
62
|
+
"",
|
|
63
|
+
"## Lifecycle / Extension Point",
|
|
64
|
+
"",
|
|
65
|
+
"## Data Objects and Fields",
|
|
66
|
+
"",
|
|
67
|
+
"## Exceptions and Performance",
|
|
68
|
+
"",
|
|
69
|
+
"## Assumptions",
|
|
70
|
+
"",
|
|
71
|
+
].join("\n");
|
|
72
|
+
case "plan":
|
|
73
|
+
return [
|
|
74
|
+
"# Plan",
|
|
75
|
+
"",
|
|
76
|
+
"## Files to Inspect",
|
|
77
|
+
"",
|
|
78
|
+
"## Files to Edit",
|
|
79
|
+
"",
|
|
80
|
+
"## Required Kingdee Lookups",
|
|
81
|
+
"",
|
|
82
|
+
"## Validation Commands",
|
|
83
|
+
"",
|
|
84
|
+
"## Rollback / Containment",
|
|
85
|
+
"",
|
|
86
|
+
].join("\n");
|
|
87
|
+
case "execute":
|
|
88
|
+
return ["# Execution", "", "## Completed Steps", "", "## Changed Files", "", "## Deviations", ""].join("\n");
|
|
89
|
+
case "verify":
|
|
90
|
+
return ["# Verify", "", "## Commands", "", "## Evidence", "", "## Residual Risk", ""].join("\n");
|
|
91
|
+
case "ship":
|
|
92
|
+
return ["# Ship", "", "## Summary", "", "## Verification Evidence", "", "## Risks", "", "## Follow-ups", ""].join("\n");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { ActiveRun } from "./types.ts";
|
|
3
|
+
import { nextPhase } from "./types.ts";
|
|
4
|
+
import { runsDir } from "./paths.ts";
|
|
5
|
+
import { refreshGate } from "./state.ts";
|
|
6
|
+
import { formatProductProfile } from "../product/profile.ts";
|
|
7
|
+
|
|
8
|
+
export function formatStatus(cwd: string, run: ActiveRun | undefined): string {
|
|
9
|
+
if (!run) {
|
|
10
|
+
return ["No active Kingdee harness run.", "", "Start one with:", "/kd-start <goal>"].join("\n");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const refreshed = refreshGate(cwd, run);
|
|
14
|
+
const next = nextPhase(refreshed.phase) ?? "done";
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
`Run: ${refreshed.id}`,
|
|
18
|
+
`Phase: ${refreshed.phase}`,
|
|
19
|
+
`Next: ${next}`,
|
|
20
|
+
`Product: ${formatProductProfile(refreshed.profile)}`,
|
|
21
|
+
`Version: ${refreshed.version ?? "unselected"}`,
|
|
22
|
+
`Risk: ${refreshed.risk ?? "unknown"}`,
|
|
23
|
+
`Gate: ${refreshed.gate.passed ? "pass" : "blocked"}`,
|
|
24
|
+
refreshed.gate.reason ? `Reason: ${refreshed.gate.reason}` : undefined,
|
|
25
|
+
"",
|
|
26
|
+
`Run directory: ${join(runsDir(cwd), refreshed.id)}`,
|
|
27
|
+
]
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ActiveRun, GateResult, KdPhase } from "./types.ts";
|
|
2
|
+
import { PHASE_ARTIFACTS, PHASE_ORDER } from "./types.ts";
|
|
3
|
+
import { artifactExists, readArtifact } from "./artifacts.ts";
|
|
4
|
+
import { isKnownProduct } from "../product/profile.ts";
|
|
5
|
+
|
|
6
|
+
const REQUIRED_MARKERS: Partial<Record<KdPhase, string[]>> = {
|
|
7
|
+
plan: ["## Validation Commands"],
|
|
8
|
+
verify: ["## Evidence"],
|
|
9
|
+
ship: ["## Verification Evidence"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const COSMIC_CONFIG_EVIDENCE = "evidence/cosmic-config.txt";
|
|
13
|
+
const COSMIC_METADATA_EVIDENCE = "evidence/cosmic-metadata.json";
|
|
14
|
+
const COSMIC_API_EVIDENCE = "evidence/cosmic-api.txt";
|
|
15
|
+
const KSQL_LINT_EVIDENCE = "evidence/ksql-lint.txt";
|
|
16
|
+
|
|
17
|
+
export function inspectGate(cwd: string, run: ActiveRun): GateResult {
|
|
18
|
+
const currentIndex = PHASE_ORDER.indexOf(run.phase);
|
|
19
|
+
const missing: string[] = [];
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i <= currentIndex; i++) {
|
|
22
|
+
const phase = PHASE_ORDER[i];
|
|
23
|
+
const artifact = PHASE_ARTIFACTS[phase];
|
|
24
|
+
if (!artifactExists(cwd, run, artifact)) missing.push(artifact);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const markerProblem = inspectMarkers(cwd, run, run.phase);
|
|
28
|
+
const evidenceProblem = inspectEvidence(cwd, run, run.phase);
|
|
29
|
+
const reasonParts = [];
|
|
30
|
+
if (missing.length > 0) reasonParts.push(`缺少必需产物:${[...new Set(missing)].join(", ")}`);
|
|
31
|
+
if (markerProblem) reasonParts.push(markerProblem);
|
|
32
|
+
if (evidenceProblem) reasonParts.push(evidenceProblem);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
passed: reasonParts.length === 0,
|
|
36
|
+
reason: reasonParts.length > 0 ? reasonParts.join("; ") : undefined,
|
|
37
|
+
checkedAt: new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): GateResult {
|
|
42
|
+
const targetIndex = PHASE_ORDER.indexOf(target);
|
|
43
|
+
const missing: string[] = [];
|
|
44
|
+
const reasonParts: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (target !== "discuss" && !isKnownProduct(run.profile?.product ?? run.product)) {
|
|
47
|
+
reasonParts.push("不能离开 discuss:产品画像未知。请使用 /kd-product <flagship|cosmic|xinghan|cangqiong|enterprise>。");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
51
|
+
const phase = PHASE_ORDER[i];
|
|
52
|
+
const artifact = PHASE_ARTIFACTS[phase];
|
|
53
|
+
if (!artifactExists(cwd, run, artifact)) missing.push(artifact);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (target === "execute" && !artifactExists(cwd, run, PHASE_ARTIFACTS.plan)) {
|
|
57
|
+
missing.push(PHASE_ARTIFACTS.plan);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
missing.push(...missingEvidenceForPhase(cwd, run, target));
|
|
61
|
+
|
|
62
|
+
if (target === "ship" && !artifactExists(cwd, run, PHASE_ARTIFACTS.verify)) {
|
|
63
|
+
missing.push(PHASE_ARTIFACTS.verify);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (missing.length > 0) reasonParts.push(`不能进入 ${target}:缺少 ${[...new Set(missing)].join(", ")}`);
|
|
67
|
+
const reason = reasonParts.length > 0 ? reasonParts.join("; ") : undefined;
|
|
68
|
+
return {
|
|
69
|
+
passed: !reason,
|
|
70
|
+
reason,
|
|
71
|
+
checkedAt: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function inspectMarkers(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
76
|
+
const markers = REQUIRED_MARKERS[phase];
|
|
77
|
+
if (!markers?.length) return undefined;
|
|
78
|
+
|
|
79
|
+
const content = readArtifact(cwd, run, phase);
|
|
80
|
+
if (!content) return undefined;
|
|
81
|
+
|
|
82
|
+
const missing = markers.filter((marker) => !content.includes(marker));
|
|
83
|
+
if (missing.length === 0) return undefined;
|
|
84
|
+
return `${PHASE_ARTIFACTS[phase]} 缺少必需章节:${missing.join(", ")}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function inspectEvidence(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
88
|
+
const missing = missingEvidenceForPhase(cwd, run, phase);
|
|
89
|
+
if (missing.length === 0) return undefined;
|
|
90
|
+
return `缺少必需的官方证据:${missing.join(", ")}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isCosmicRun(run: ActiveRun): boolean {
|
|
94
|
+
return run.profile?.platform === "cosmic";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function missingEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase): string[] {
|
|
98
|
+
return requiredEvidenceForPhase(cwd, run, phase).filter((artifact) => !artifactExists(cwd, run, artifact));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase): string[] {
|
|
102
|
+
if (!isCosmicRun(run)) return [];
|
|
103
|
+
|
|
104
|
+
const required = new Set<string>();
|
|
105
|
+
const phaseIndex = PHASE_ORDER.indexOf(phase);
|
|
106
|
+
|
|
107
|
+
if (phaseIndex >= PHASE_ORDER.indexOf("execute")) {
|
|
108
|
+
required.add(COSMIC_CONFIG_EVIDENCE);
|
|
109
|
+
if (planHasMetadataRequirement(cwd, run)) required.add(COSMIC_METADATA_EVIDENCE);
|
|
110
|
+
if (planHasApiRequirement(cwd, run)) required.add(COSMIC_API_EVIDENCE);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (phaseIndex >= PHASE_ORDER.indexOf("ship") && runHasKsqlDelivery(cwd, run)) {
|
|
114
|
+
required.add(COSMIC_METADATA_EVIDENCE);
|
|
115
|
+
required.add(KSQL_LINT_EVIDENCE);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [...required];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean {
|
|
122
|
+
const plan = readArtifact(cwd, run, "plan") ?? "";
|
|
123
|
+
return /kd_cosmic_metadata|cosmic-metadata|metadata evidence|元数据|字段验证|字段确认|字段元数据/i.test(plan);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function planHasApiRequirement(cwd: string, run: ActiveRun): boolean {
|
|
127
|
+
const plan = readArtifact(cwd, run, "plan") ?? "";
|
|
128
|
+
return /kd_cosmic_api|cosmic-api|api signature|method signature|sdk.*签名|方法签名|接口签名/i.test(plan);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function runHasKsqlDelivery(cwd: string, run: ActiveRun): boolean {
|
|
132
|
+
const text = ["spec", "plan", "verify"]
|
|
133
|
+
.map((phase) => readArtifact(cwd, run, phase as KdPhase) ?? "")
|
|
134
|
+
.join("\n");
|
|
135
|
+
return /kd_ksql_lint|kd-ksql|\bksql\b|数据修复|批量更新|字段回填|备份语句|回滚语句/i.test(text);
|
|
136
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { ActiveRun } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export function kdDir(cwd: string): string {
|
|
5
|
+
return join(cwd, ".pi", "kd");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function activeRunPath(cwd: string): string {
|
|
9
|
+
return join(kdDir(cwd), "active-run.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function runsDir(cwd: string): string {
|
|
13
|
+
return join(kdDir(cwd), "runs");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function runRoot(cwd: string, run: ActiveRun): string {
|
|
17
|
+
return join(runsDir(cwd), run.id);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function runArtifactPath(cwd: string, run: ActiveRun, artifactName: string): string {
|
|
21
|
+
return join(runRoot(cwd, run), artifactName);
|
|
22
|
+
}
|
|
23
|
+
|