kcode-pi 0.1.34 → 0.1.38
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 +10 -10
- package/dist/cli/kcode.js +3 -3
- package/dist/context/project-context.js +4 -5
- package/dist/harness/prompt-policy.d.ts +19 -0
- package/dist/harness/prompt-policy.js +206 -0
- package/dist/harness/types.d.ts +74 -0
- package/dist/harness/types.js +16 -0
- package/dist/product/profile.d.ts +20 -0
- package/dist/product/profile.js +103 -0
- package/docs/CHANGELOG.md +90 -2
- package/docs/COMMAND_REFERENCE.md +27 -12
- package/docs/DEVELOPMENT.md +3 -3
- package/docs/EVIDENCE_AND_GATES.md +15 -8
- package/docs/HARNESS_WORKFLOW.md +32 -12
- package/docs/KCODE_DISTRIBUTION.md +7 -7
- package/docs/PRODUCT_PROFILE.md +9 -9
- package/docs/TROUBLESHOOTING.md +8 -8
- package/docs/USER_GUIDE.md +10 -10
- package/extensions/kingdee-harness.ts +141 -86
- package/extensions/kingdee-header.ts +1 -1
- package/extensions/kingdee-subagents.ts +1 -1
- package/extensions/kingdee-tools.ts +44 -44
- package/package.json +1 -1
- package/src/cli/kcode.ts +3 -3
- package/src/context/project-context.ts +4 -5
- package/src/harness/artifacts.ts +6 -7
- package/src/harness/data-source-policy.ts +346 -0
- package/src/harness/delegation.ts +28 -23
- package/src/harness/gates.ts +16 -1
- package/src/harness/messages.ts +65 -11
- package/src/harness/path-policy.ts +1 -0
- package/src/harness/plan-steps.ts +3 -3
- package/src/harness/prompt-policy.ts +227 -0
- package/src/harness/prompt.ts +12 -16
- package/src/harness/question-memory.ts +220 -0
- package/src/harness/repair.ts +18 -3
- package/src/harness/state.ts +93 -6
- package/src/harness/types.ts +19 -0
- package/src/official/kingdee-skills.ts +4 -4
- package/src/product/profile.ts +2 -2
- package/src/rules/checker.ts +27 -27
- package/src/tools/build-debug.ts +5 -5
- package/src/tools/sdk-signature.ts +4 -4
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { ActiveRun, KdFact, KdQuestion } from "./types.ts";
|
|
2
|
+
import { canonicalFactLabel } from "./prompt-policy.ts";
|
|
3
|
+
|
|
4
|
+
export function formatQuestionMemory(run: ActiveRun): string {
|
|
5
|
+
const questions = Array.isArray(run.questions) ? run.questions : [];
|
|
6
|
+
const facts = currentFactsForRun(run);
|
|
7
|
+
const rejected = rejectedFactsForRun(run);
|
|
8
|
+
if (questions.length === 0 && facts.length === 0 && rejected.length === 0) return "无。";
|
|
9
|
+
|
|
10
|
+
const answered = questions.filter((question) => question.status === "answered");
|
|
11
|
+
const open = questions.filter((question) => question.status === "open");
|
|
12
|
+
const sections: string[] = [];
|
|
13
|
+
|
|
14
|
+
if (facts.length > 0) {
|
|
15
|
+
sections.push("## 当前结构化事实", formatCurrentFacts(run));
|
|
16
|
+
}
|
|
17
|
+
if (rejected.length > 0) {
|
|
18
|
+
sections.push("## 已否定候选事实", rejected.map((fact) => `- ${fact.label}:${fact.value}`).join("\n"));
|
|
19
|
+
}
|
|
20
|
+
if (answered.length > 0) {
|
|
21
|
+
sections.push("## 已回答问题", answered.map(formatAnsweredQuestion).join("\n"));
|
|
22
|
+
}
|
|
23
|
+
if (open.length > 0) {
|
|
24
|
+
sections.push("## 未回答问题", open.map(formatOpenQuestion).join("\n"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return sections.join("\n\n") || "无。";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatAnsweredQuestionFacts(run: ActiveRun): string {
|
|
31
|
+
return formatCurrentFacts(run);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatCurrentFacts(run: ActiveRun): string {
|
|
35
|
+
const lines = currentFactsForRun(run).map((fact) => `- ${fact.label}:${fact.value}`);
|
|
36
|
+
return lines.length > 0 ? dedupe(lines).join("\n") : "无。";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function currentFactsForRun(run: ActiveRun): KdFact[] {
|
|
40
|
+
return latestFactsByKey(persistedFactsForRun(run).filter((fact) => fact.status === "current" && fact.value.trim()));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function rejectedFactsForRun(run: ActiveRun): KdFact[] {
|
|
44
|
+
return latestFactsByKey(persistedFactsForRun(run).filter((fact) => fact.status === "rejected" && fact.value.trim()));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function currentFactForLabel(run: ActiveRun, label: string): KdFact | undefined {
|
|
48
|
+
const canonical = canonicalFactLabel(label) ?? label;
|
|
49
|
+
const key = factKey(canonical);
|
|
50
|
+
return currentFactsForRun(run).find((fact) => fact.key === key);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function hasCurrentFactForLabel(run: ActiveRun, label: string): boolean {
|
|
54
|
+
return Boolean(currentFactForLabel(run, label));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function factFromAnsweredQuestion(question: KdQuestion, now = new Date().toISOString()): KdFact | undefined {
|
|
58
|
+
if (question.status !== "answered" || !question.factLabel) return undefined;
|
|
59
|
+
const answer = question.answer?.trim();
|
|
60
|
+
if (!answer) return undefined;
|
|
61
|
+
if (isPlaceholderAnswer(answer)) return undefined;
|
|
62
|
+
const label = canonicalFactLabel(question.factLabel);
|
|
63
|
+
if (!label) return undefined;
|
|
64
|
+
if (question.proposedFactValue) {
|
|
65
|
+
if (!isAffirmativeOnlyAnswer(answer) && !isNegativeAnswer(answer)) return undefined;
|
|
66
|
+
const value = question.proposedFactValue.trim();
|
|
67
|
+
if (!value) return undefined;
|
|
68
|
+
return {
|
|
69
|
+
key: factKey(label),
|
|
70
|
+
label,
|
|
71
|
+
value,
|
|
72
|
+
status: isAffirmativeOnlyAnswer(answer) ? "current" : "rejected",
|
|
73
|
+
source: "question",
|
|
74
|
+
sourceQuestionId: question.id,
|
|
75
|
+
reason: question.reason,
|
|
76
|
+
createdAt: now,
|
|
77
|
+
updatedAt: now,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isConcreteAnswer(answer)) {
|
|
82
|
+
return {
|
|
83
|
+
key: factKey(label),
|
|
84
|
+
label,
|
|
85
|
+
value: answer,
|
|
86
|
+
status: "current",
|
|
87
|
+
source: "question",
|
|
88
|
+
sourceQuestionId: question.id,
|
|
89
|
+
reason: question.reason,
|
|
90
|
+
createdAt: now,
|
|
91
|
+
updatedAt: now,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function questionAskBlockReason(run: ActiveRun, input: { factLabel?: string }): string | undefined {
|
|
98
|
+
const rawLabel = input.factLabel?.trim();
|
|
99
|
+
if (!rawLabel) return undefined;
|
|
100
|
+
const label = canonicalFactLabel(rawLabel);
|
|
101
|
+
if (!label) return `未知 factLabel:${rawLabel}。必须使用 KCode 集中定义的实现契约、数据源上下文或第三方对接事实标签。`;
|
|
102
|
+
const key = factKey(label);
|
|
103
|
+
const open = (Array.isArray(run.questions) ? run.questions : []).find(
|
|
104
|
+
(question) => question.status === "open" && question.blocking && question.factLabel && factKey(question.factLabel) === key,
|
|
105
|
+
);
|
|
106
|
+
if (open) {
|
|
107
|
+
return `事实标签 ${label} 已有未回答问题 ${open.id},禁止重复提问。下一步:获取用户答案后用 kd_question action=answer id=${open.id} answer=<答案> 记录。`;
|
|
108
|
+
}
|
|
109
|
+
const fact = currentFactForLabel(run, label);
|
|
110
|
+
if (fact) {
|
|
111
|
+
return `事实标签 ${label} 已确认为 ${fact.value},禁止重复提问。用户明确更正时使用 kd_question action=revise factLabel="${label}" answer="<新值>" reason="<更正原因>"。`;
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function invalidFactValueReason(value: string): string | undefined {
|
|
117
|
+
return isInvalidFactValue(value) ? "事实值不是可核验内容,不能使用待确认、未知、按实际环境、TODO/TBD、单独确认/否定或口头确认语解除门禁。" : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function invalidConfirmationAnswerReason(value: string): string | undefined {
|
|
121
|
+
return isAffirmativeOnlyAnswer(value) || isNegativeAnswer(value) ? undefined : "确认题答案必须是明确肯定或明确否定,不能使用口头确认语或其他模糊回复。";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function factKey(label: string): string {
|
|
125
|
+
return label.trim().toLowerCase().replace(/\s+/g, "");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function upsertFact(facts: KdFact[] | undefined, next: KdFact): KdFact[] {
|
|
129
|
+
const existing = Array.isArray(facts) ? facts : [];
|
|
130
|
+
const now = next.updatedAt || new Date().toISOString();
|
|
131
|
+
const normalizedNext = { ...next, key: factKey(next.label), updatedAt: now, createdAt: next.createdAt || now };
|
|
132
|
+
const updated = existing.map((fact) => ({ ...fact }));
|
|
133
|
+
|
|
134
|
+
if (normalizedNext.status === "current") {
|
|
135
|
+
for (const fact of updated) {
|
|
136
|
+
if (fact.key === normalizedNext.key && fact.status === "current" && fact.value !== normalizedNext.value) {
|
|
137
|
+
fact.status = "superseded";
|
|
138
|
+
fact.updatedAt = now;
|
|
139
|
+
fact.supersededBy = normalizedNext.value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const duplicate = updated.find(
|
|
145
|
+
(fact) =>
|
|
146
|
+
fact.key === normalizedNext.key &&
|
|
147
|
+
fact.value === normalizedNext.value &&
|
|
148
|
+
fact.status === normalizedNext.status &&
|
|
149
|
+
fact.sourceQuestionId === normalizedNext.sourceQuestionId,
|
|
150
|
+
);
|
|
151
|
+
if (duplicate) {
|
|
152
|
+
duplicate.updatedAt = now;
|
|
153
|
+
duplicate.reason = normalizedNext.reason ?? duplicate.reason;
|
|
154
|
+
return updated;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return [...updated, normalizedNext];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isConcreteAnswer(answer: string): boolean {
|
|
161
|
+
return !isInvalidFactValue(answer);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isAffirmativeOnlyAnswer(answer: string): boolean {
|
|
165
|
+
return /^(是|是的|对|正确|确认|yes|true)$/i.test(answer.trim());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isNegativeAnswer(answer: string): boolean {
|
|
169
|
+
return /^(否|不是|不对|错误|no|false)(\s|。|,|,|;|;|$)/i.test(answer.trim());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isPlaceholderAnswer(answer: string): boolean {
|
|
173
|
+
return /^(待确认|未知|不清楚|未提供|无|没有|none|unknown|todo|tbd|n\/a|按实际|按实际环境|以后再说)(\s|。|,|,|;|;|$)/i.test(answer.trim());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isAcknowledgementOnlyAnswer(answer: string): boolean {
|
|
177
|
+
return /^(可以|好的|好|行|嗯|嗯嗯|收到|明白|ok|okay|行的)(\s|。|,|,|;|;|$)/i.test(answer.trim());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isInvalidFactValue(answer: string): boolean {
|
|
181
|
+
return isPlaceholderAnswer(answer) || isAffirmativeOnlyAnswer(answer) || isNegativeAnswer(answer) || isAcknowledgementOnlyAnswer(answer);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatAnsweredQuestion(question: KdQuestion): string {
|
|
185
|
+
return `- ${question.id}:${question.question} => ${question.answer?.trim() ?? ""}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function formatOpenQuestion(question: KdQuestion): string {
|
|
189
|
+
return [
|
|
190
|
+
`- ${question.id}${question.blocking ? " [blocking]" : ""}:${question.question}`,
|
|
191
|
+
question.reason ? ` - 原因:${question.reason}` : undefined,
|
|
192
|
+
question.choices?.length ? ` - 选项:${question.choices.join(" | ")}` : undefined,
|
|
193
|
+
].filter(Boolean).join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function dedupe(values: string[]): string[] {
|
|
197
|
+
return [...new Set(values)];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function latestFactsByKey(facts: KdFact[]): KdFact[] {
|
|
201
|
+
const byKey = new Map<string, KdFact>();
|
|
202
|
+
for (const fact of facts) {
|
|
203
|
+
const current = byKey.get(fact.key);
|
|
204
|
+
if (!current || (fact.updatedAt || fact.createdAt).localeCompare(current.updatedAt || current.createdAt) >= 0) {
|
|
205
|
+
byKey.set(fact.key, fact);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return [...byKey.values()];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function persistedFactsForRun(run: ActiveRun): KdFact[] {
|
|
212
|
+
return (Array.isArray(run.facts) ? run.facts : []).flatMap(normalizePersistedFact);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizePersistedFact(fact: KdFact): KdFact[] {
|
|
216
|
+
const label = canonicalFactLabel(fact.label);
|
|
217
|
+
const value = typeof fact.value === "string" ? fact.value.trim() : "";
|
|
218
|
+
if (!label || !value || invalidFactValueReason(value)) return [];
|
|
219
|
+
return [{ ...fact, key: factKey(label), label, value }];
|
|
220
|
+
}
|
package/src/harness/repair.ts
CHANGED
|
@@ -40,6 +40,14 @@ export function recordVerifyResult(cwd: string, run: ActiveRun, input: VerifyRes
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const normalized = normalizeInput(input);
|
|
43
|
+
const inputProblem = verifyInputBlockReason(normalized);
|
|
44
|
+
if (inputProblem) {
|
|
45
|
+
return {
|
|
46
|
+
status: "rejected",
|
|
47
|
+
run,
|
|
48
|
+
message: inputProblem,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
43
51
|
if (normalized.exitCode === 0) {
|
|
44
52
|
return recordVerifyPass(cwd, run, normalized);
|
|
45
53
|
}
|
|
@@ -99,7 +107,7 @@ function recordVerifyFailure(cwd: string, run: ActiveRun, input: NormalizedVerif
|
|
|
99
107
|
{
|
|
100
108
|
id: `Q-${String((run.questions?.length ?? 0) + 1).padStart(3, "0")}`,
|
|
101
109
|
phase: "verify",
|
|
102
|
-
question: `验证失败已达到 ${maxAttempts}
|
|
110
|
+
question: `验证失败已达到 ${maxAttempts} 轮。选择继续修复、回到 plan 调整范围或停止。`,
|
|
103
111
|
reason: `最近失败证据:${evidence}`,
|
|
104
112
|
choices: ["继续修复", "回到 plan", "停止"],
|
|
105
113
|
blocking: true,
|
|
@@ -161,7 +169,7 @@ function appendExecuteRepairRecord(cwd: string, run: ActiveRun, evidence: string
|
|
|
161
169
|
"",
|
|
162
170
|
`- 修复轮次:${attempts}/${maxAttempts}`,
|
|
163
171
|
`- 失败证据:${evidence}`,
|
|
164
|
-
"- 下一步:读取失败证据,分析失败原因,只在 PLAN.md
|
|
172
|
+
"- 下一步:读取失败证据,分析失败原因,只在 PLAN.md 批准文件内修复;涉及未批准文件时回到 plan 更新计划。",
|
|
165
173
|
"",
|
|
166
174
|
].join("\n");
|
|
167
175
|
writeArtifact(cwd, run, "execute", `${existing.trimEnd()}\n${section}`);
|
|
@@ -184,7 +192,7 @@ function formatVerifyEvidence(title: string, input: NormalizedVerifyResultInput)
|
|
|
184
192
|
function normalizeInput(input: VerifyResultInput): NormalizedVerifyResultInput {
|
|
185
193
|
const exitCode = typeof input.exitCode === "number" ? input.exitCode : Number(input.exitCode);
|
|
186
194
|
return {
|
|
187
|
-
command: normalizeText(input.command).trim()
|
|
195
|
+
command: normalizeText(input.command).trim(),
|
|
188
196
|
exitCode: Number.isFinite(exitCode) ? Math.trunc(exitCode) : 1,
|
|
189
197
|
stdout: normalizeText(input.stdout),
|
|
190
198
|
stderr: normalizeText(input.stderr),
|
|
@@ -202,6 +210,13 @@ function verifyResultBlockReason(run: ActiveRun): string | undefined {
|
|
|
202
210
|
return `kd_verify_result 只能在 verify 阶段记录,或在自动修复循环的 execute 阶段记录。当前阶段:${run.phase}。`;
|
|
203
211
|
}
|
|
204
212
|
|
|
213
|
+
function verifyInputBlockReason(input: NormalizedVerifyResultInput): string | undefined {
|
|
214
|
+
if (!input.command || /^(unknown|待确认|未知|todo|tbd|n\/a)$/i.test(input.command)) {
|
|
215
|
+
return "kd_verify_result 缺少真实验证命令,拒绝记录证据。下一步:运行 PLAN.md 中声明的验证命令;外部系统或人工验证必须记录用户提供的命令、环境、测试数据和证据来源。";
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
function closeRepairQuestions(run: ActiveRun, evidence: string): void {
|
|
206
221
|
if (!Array.isArray(run.questions)) return;
|
|
207
222
|
const now = new Date().toISOString();
|
package/src/harness/state.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import type { ActiveRun, KdPhase, KdQuestion, KdRisk, KdRiskSource } from "./types.ts";
|
|
2
|
+
import type { ActiveRun, KdFact, KdPhase, KdQuestion, KdRisk, KdRiskSource } from "./types.ts";
|
|
3
3
|
import { PHASE_ARTIFACTS, isKdPhase, nextPhase } from "./types.ts";
|
|
4
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";
|
|
8
|
+
import { factFromAnsweredQuestion, factKey, invalidConfirmationAnswerReason, invalidFactValueReason, upsertFact } from "./question-memory.ts";
|
|
9
|
+
import { canonicalFactLabel } from "./prompt-policy.ts";
|
|
8
10
|
|
|
9
11
|
export function readActiveRun(cwd: string): ActiveRun | undefined {
|
|
10
12
|
const path = activeRunPath(cwd);
|
|
@@ -81,9 +83,10 @@ export function createActiveRun(cwd: string, goal: string, productInput?: string
|
|
|
81
83
|
profile,
|
|
82
84
|
artifacts: {},
|
|
83
85
|
questions: [],
|
|
86
|
+
facts: [],
|
|
84
87
|
gate: {
|
|
85
88
|
passed: false,
|
|
86
|
-
reason: "进入 spec
|
|
89
|
+
reason: "进入 spec 前必须完成 CONTEXT.md 并确认产品画像",
|
|
87
90
|
checkedAt: new Date().toISOString(),
|
|
88
91
|
},
|
|
89
92
|
};
|
|
@@ -98,14 +101,25 @@ export function createActiveRun(cwd: string, goal: string, productInput?: string
|
|
|
98
101
|
export function addQuestion(
|
|
99
102
|
cwd: string,
|
|
100
103
|
run: ActiveRun,
|
|
101
|
-
input: { question: string; reason?: string; choices?: string[]; blocking?: boolean },
|
|
104
|
+
input: { question: string; reason?: string; factLabel?: string; proposedFactValue?: string; choices?: string[]; blocking?: boolean },
|
|
102
105
|
): KdQuestion {
|
|
103
106
|
const existing = Array.isArray(run.questions) ? run.questions : [];
|
|
107
|
+
const canonicalLabel = input.factLabel?.trim() ? canonicalFactLabel(input.factLabel) : undefined;
|
|
108
|
+
const duplicate = existing.find(
|
|
109
|
+
(question) =>
|
|
110
|
+
question.status === "open" &&
|
|
111
|
+
question.blocking &&
|
|
112
|
+
((canonicalLabel && question.factLabel && factKey(question.factLabel) === factKey(canonicalLabel)) ||
|
|
113
|
+
normalizeQuestion(question.question) === normalizeQuestion(input.question)),
|
|
114
|
+
);
|
|
115
|
+
if (duplicate) return duplicate;
|
|
104
116
|
const question: KdQuestion = {
|
|
105
117
|
id: createQuestionId(existing.length + 1),
|
|
106
118
|
phase: run.phase,
|
|
107
119
|
question: input.question.trim(),
|
|
108
120
|
reason: input.reason?.trim() || undefined,
|
|
121
|
+
factLabel: canonicalLabel,
|
|
122
|
+
proposedFactValue: input.proposedFactValue?.trim() || undefined,
|
|
109
123
|
choices: input.choices?.map((choice) => choice.trim()).filter(Boolean),
|
|
110
124
|
blocking: input.blocking ?? true,
|
|
111
125
|
status: "open",
|
|
@@ -120,15 +134,49 @@ export function addQuestion(
|
|
|
120
134
|
export function answerQuestion(cwd: string, run: ActiveRun, id: string, answer: string): KdQuestion | undefined {
|
|
121
135
|
const question = (Array.isArray(run.questions) ? run.questions : []).find((item) => item.id === id);
|
|
122
136
|
if (!question) return undefined;
|
|
137
|
+
if (question.status !== "open") return undefined;
|
|
138
|
+
const trimmed = answer.trim();
|
|
139
|
+
if (!trimmed) return undefined;
|
|
140
|
+
if (question.factLabel && question.proposedFactValue && invalidConfirmationAnswerReason(trimmed)) return undefined;
|
|
141
|
+
if (question.factLabel && !question.proposedFactValue && invalidFactValueReason(trimmed)) return undefined;
|
|
123
142
|
question.status = "answered";
|
|
124
|
-
question.answer =
|
|
143
|
+
question.answer = trimmed;
|
|
125
144
|
question.answeredAt = new Date().toISOString();
|
|
145
|
+
applyQuestionFact(run, question);
|
|
126
146
|
applyRepairQuestionAnswer(cwd, run, question);
|
|
127
147
|
run.gate = inspectGate(cwd, run);
|
|
128
148
|
writeActiveRun(cwd, run);
|
|
129
149
|
return question;
|
|
130
150
|
}
|
|
131
151
|
|
|
152
|
+
export function reviseFact(
|
|
153
|
+
cwd: string,
|
|
154
|
+
run: ActiveRun,
|
|
155
|
+
input: { factLabel: string; value: string; reason?: string },
|
|
156
|
+
): KdFact | undefined {
|
|
157
|
+
const label = input.factLabel.trim();
|
|
158
|
+
const value = input.value.trim();
|
|
159
|
+
if (!label || !value) return undefined;
|
|
160
|
+
const canonicalLabel = canonicalFactLabel(label);
|
|
161
|
+
if (!canonicalLabel) return undefined;
|
|
162
|
+
if (invalidFactValueReason(value)) return undefined;
|
|
163
|
+
const now = new Date().toISOString();
|
|
164
|
+
const fact: KdFact = {
|
|
165
|
+
key: factKey(canonicalLabel),
|
|
166
|
+
label: canonicalLabel,
|
|
167
|
+
value,
|
|
168
|
+
status: "current",
|
|
169
|
+
source: "manual",
|
|
170
|
+
reason: input.reason?.trim() || undefined,
|
|
171
|
+
createdAt: now,
|
|
172
|
+
updatedAt: now,
|
|
173
|
+
};
|
|
174
|
+
run.facts = upsertFact(run.facts, fact);
|
|
175
|
+
run.gate = inspectGate(cwd, run);
|
|
176
|
+
writeActiveRun(cwd, run);
|
|
177
|
+
return fact;
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
export function openBlockingQuestions(run: ActiveRun): KdQuestion[] {
|
|
133
181
|
return (Array.isArray(run.questions) ? run.questions : []).filter((question) => question.status === "open" && question.blocking);
|
|
134
182
|
}
|
|
@@ -217,6 +265,12 @@ function writeRunState(cwd: string, run: ActiveRun): void {
|
|
|
217
265
|
writeFileSync(runStatePath(cwd, run), `${JSON.stringify(run, null, 2)}\n`, "utf8");
|
|
218
266
|
}
|
|
219
267
|
|
|
268
|
+
function applyQuestionFact(run: ActiveRun, question: KdQuestion): void {
|
|
269
|
+
const fact = factFromAnsweredQuestion(question, question.answeredAt);
|
|
270
|
+
if (!fact) return;
|
|
271
|
+
run.facts = upsertFact(run.facts, fact);
|
|
272
|
+
}
|
|
273
|
+
|
|
220
274
|
function applyRepairQuestionAnswer(cwd: string, run: ActiveRun, question: KdQuestion): void {
|
|
221
275
|
if (!isRepairLimitQuestion(question)) return;
|
|
222
276
|
const answer = question.answer?.trim() ?? "";
|
|
@@ -280,13 +334,13 @@ function hydrateRun(parsed: ActiveRun): ActiveRun | undefined {
|
|
|
280
334
|
parsed.id = parsed.id.trim();
|
|
281
335
|
parsed.artifacts = isPlainObject(parsed.artifacts) ? sanitizeArtifacts(parsed.artifacts) : {};
|
|
282
336
|
parsed.questions = Array.isArray(parsed.questions) ? parsed.questions.map(sanitizeQuestion).filter((question): question is KdQuestion => Boolean(question)) : [];
|
|
337
|
+
parsed.facts = Array.isArray(parsed.facts) ? parsed.facts.map(sanitizeFact).filter((fact): fact is KdFact => Boolean(fact)) : [];
|
|
283
338
|
parsed.status = parsed.status === "paused" || parsed.status === "done" ? parsed.status : "active";
|
|
284
339
|
parsed.goal = typeof parsed.goal === "string" ? parsed.goal : undefined;
|
|
285
340
|
parsed.version = typeof parsed.version === "string" ? parsed.version : undefined;
|
|
286
341
|
parsed.createdAt = typeof parsed.createdAt === "string" ? parsed.createdAt : typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined;
|
|
287
342
|
parsed.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : parsed.createdAt;
|
|
288
|
-
const
|
|
289
|
-
const product = typeof parsed.profile?.product === "string" ? parsed.profile.product : typeof parsed.product === "string" ? parsed.product : resolveProductProfile(typeof legacyEdition === "string" ? legacyEdition : undefined).product;
|
|
343
|
+
const product = typeof parsed.profile?.product === "string" ? parsed.profile.product : typeof parsed.product === "string" ? parsed.product : resolveProductProfile(undefined).product;
|
|
290
344
|
parsed.profile = profileForProduct(product);
|
|
291
345
|
parsed.product = parsed.profile.product;
|
|
292
346
|
parsed.riskAssessment = sanitizeRiskAssessment(parsed.riskAssessment);
|
|
@@ -305,11 +359,15 @@ function sanitizeQuestion(value: unknown): KdQuestion | undefined {
|
|
|
305
359
|
const question = typeof value.question === "string" ? value.question.trim() : "";
|
|
306
360
|
if (!phase || !question) return undefined;
|
|
307
361
|
const id = typeof value.id === "string" && value.id.trim() ? value.id.trim() : createQuestionId(1);
|
|
362
|
+
const rawFactLabel = typeof value.factLabel === "string" && value.factLabel.trim() ? value.factLabel.trim() : undefined;
|
|
363
|
+
const factLabel = rawFactLabel ? canonicalFactLabel(rawFactLabel) : undefined;
|
|
308
364
|
return {
|
|
309
365
|
id,
|
|
310
366
|
phase,
|
|
311
367
|
question,
|
|
312
368
|
reason: typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : undefined,
|
|
369
|
+
factLabel,
|
|
370
|
+
proposedFactValue: typeof value.proposedFactValue === "string" && value.proposedFactValue.trim() ? value.proposedFactValue.trim() : undefined,
|
|
313
371
|
choices: Array.isArray(value.choices) ? value.choices.filter((choice): choice is string => typeof choice === "string" && Boolean(choice.trim())).map((choice) => choice.trim()) : undefined,
|
|
314
372
|
blocking: value.blocking !== false,
|
|
315
373
|
status: value.status === "answered" ? "answered" : "open",
|
|
@@ -319,6 +377,31 @@ function sanitizeQuestion(value: unknown): KdQuestion | undefined {
|
|
|
319
377
|
};
|
|
320
378
|
}
|
|
321
379
|
|
|
380
|
+
function sanitizeFact(value: unknown): KdFact | undefined {
|
|
381
|
+
if (!isPlainObject(value)) return undefined;
|
|
382
|
+
const label = typeof value.label === "string" ? value.label.trim() : "";
|
|
383
|
+
const factValue = typeof value.value === "string" ? value.value.trim() : "";
|
|
384
|
+
if (!label || !factValue) return undefined;
|
|
385
|
+
const canonicalLabel = canonicalFactLabel(label);
|
|
386
|
+
if (!canonicalLabel) return undefined;
|
|
387
|
+
if (invalidFactValueReason(factValue)) return undefined;
|
|
388
|
+
const status = value.status === "superseded" || value.status === "rejected" ? value.status : "current";
|
|
389
|
+
const source = value.source === "manual" ? "manual" : "question";
|
|
390
|
+
const now = new Date().toISOString();
|
|
391
|
+
return {
|
|
392
|
+
key: factKey(canonicalLabel),
|
|
393
|
+
label: canonicalLabel,
|
|
394
|
+
value: factValue,
|
|
395
|
+
status,
|
|
396
|
+
source,
|
|
397
|
+
sourceQuestionId: typeof value.sourceQuestionId === "string" && value.sourceQuestionId.trim() ? value.sourceQuestionId.trim() : undefined,
|
|
398
|
+
reason: typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : undefined,
|
|
399
|
+
createdAt: typeof value.createdAt === "string" ? value.createdAt : now,
|
|
400
|
+
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : now,
|
|
401
|
+
supersededBy: typeof value.supersededBy === "string" && value.supersededBy.trim() ? value.supersededBy.trim() : undefined,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
322
405
|
function sanitizeRiskAssessment(value: unknown): ActiveRun["riskAssessment"] {
|
|
323
406
|
if (!isPlainObject(value)) return undefined;
|
|
324
407
|
const level = value.level;
|
|
@@ -371,3 +454,7 @@ function createRunId(goal: string): string {
|
|
|
371
454
|
function createQuestionId(sequence: number): string {
|
|
372
455
|
return `Q-${String(sequence).padStart(3, "0")}`;
|
|
373
456
|
}
|
|
457
|
+
|
|
458
|
+
function normalizeQuestion(question: string): string {
|
|
459
|
+
return question.trim().replace(/\s+/g, "").replace(/[??。;;,,]/g, "");
|
|
460
|
+
}
|
package/src/harness/types.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface KdQuestion {
|
|
|
32
32
|
phase: KdPhase;
|
|
33
33
|
question: string;
|
|
34
34
|
reason?: string;
|
|
35
|
+
factLabel?: string;
|
|
36
|
+
proposedFactValue?: string;
|
|
35
37
|
choices?: string[];
|
|
36
38
|
blocking: boolean;
|
|
37
39
|
status: "open" | "answered";
|
|
@@ -40,6 +42,22 @@ export interface KdQuestion {
|
|
|
40
42
|
answeredAt?: string;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
export type KdFactStatus = "current" | "superseded" | "rejected";
|
|
46
|
+
export type KdFactSource = "question" | "manual";
|
|
47
|
+
|
|
48
|
+
export interface KdFact {
|
|
49
|
+
key: string;
|
|
50
|
+
label: string;
|
|
51
|
+
value: string;
|
|
52
|
+
status: KdFactStatus;
|
|
53
|
+
source: KdFactSource;
|
|
54
|
+
sourceQuestionId?: string;
|
|
55
|
+
reason?: string;
|
|
56
|
+
createdAt: string;
|
|
57
|
+
updatedAt: string;
|
|
58
|
+
supersededBy?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
export interface ActiveRun {
|
|
44
62
|
id: string;
|
|
45
63
|
goal?: string;
|
|
@@ -56,6 +74,7 @@ export interface ActiveRun {
|
|
|
56
74
|
artifacts: Record<string, string>;
|
|
57
75
|
gate: GateResult;
|
|
58
76
|
questions?: KdQuestion[];
|
|
77
|
+
facts?: KdFact[];
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
export const PHASE_ORDER: KdPhase[] = ["discuss", "spec", "plan", "execute", "verify", "ship"];
|
|
@@ -379,7 +379,7 @@ async function fetchMetadata(config: Record<string, unknown>, target: string): P
|
|
|
379
379
|
const route = objectValue(config.route);
|
|
380
380
|
const routeUrl = routeUrlFromConfig(route);
|
|
381
381
|
if (!routeUrl) {
|
|
382
|
-
throw new Error("未配置表单元数据查询 API
|
|
382
|
+
throw new Error("未配置表单元数据查询 API。必须在 ok-cosmic.json 的 route.apiUrl 中配置统一路由,或设置 COSMIC_ROUTE_API。");
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
const hasCjk = /[\u3400-\u9fff]/.test(target);
|
|
@@ -524,7 +524,7 @@ function runCosmicApi(cwd: string, params: { mode: "search" | "search-method" |
|
|
|
524
524
|
const results = searchKnowledge(query, { scopes: ["cosmic", "cangqiong", "xinghan", "flagship"], topK: params.compact ? 5 : 10, minScore: 1 }, knowledgePath);
|
|
525
525
|
const header = [
|
|
526
526
|
`KCode Node Cosmic API query (${params.mode})`,
|
|
527
|
-
"说明: 当前 npm 包不再调用 Python/SQLite
|
|
527
|
+
"说明: 当前 npm 包不再调用 Python/SQLite 脚本;这里查询随包金蝶知识库。精确方法签名必须结合项目 SDK/编译输出做红绿验证。",
|
|
528
528
|
"",
|
|
529
529
|
].join("\n");
|
|
530
530
|
const stdout = `${header}${formatSearchResults(query, results, knowledgePath)}\n`;
|
|
@@ -690,13 +690,13 @@ function lintStatement(stmt: KsqlStatement): KsqlFinding[] {
|
|
|
690
690
|
findings.push({ severity: "WARN", line: lineOfOffset(stmt.text, stmt.line, match.index ?? 0), message: "SQL 可读性偏好:成员关系/半连接默认使用 IN,只有 IN 改变语义时才保留 EXISTS 并说明原因。" });
|
|
691
691
|
}
|
|
692
692
|
if (/\bUPDATE\b.+\bJOIN\b/i.test(compact)) {
|
|
693
|
-
findings.push({ severity: "WARN", line: stmt.line, message: "PostgreSQL
|
|
693
|
+
findings.push({ severity: "WARN", line: stmt.line, message: "PostgreSQL 多表更新默认使用 UPDATE ... FROM ... WHERE ...,禁止使用 MySQL 风格 UPDATE ... JOIN。" });
|
|
694
694
|
}
|
|
695
695
|
if (/=\s*NULL\b|\bNULL\s*=/i.test(stmt.text)) {
|
|
696
696
|
findings.push({ severity: "ERROR", line: stmt.line, message: "NULL 判断必须使用 IS NULL / IS NOT NULL,不能使用 = NULL。" });
|
|
697
697
|
}
|
|
698
698
|
if (/<>|!=/.test(stmt.text) && hasToken(stmt.text, "NULL")) {
|
|
699
|
-
findings.push({ severity: "WARN", line: stmt.line, message: "涉及 NULL
|
|
699
|
+
findings.push({ severity: "WARN", line: stmt.line, message: "涉及 NULL 的不等比较必须确认语义;PostgreSQL 默认使用 IS DISTINCT FROM。" });
|
|
700
700
|
}
|
|
701
701
|
return findings;
|
|
702
702
|
}
|
package/src/product/profile.ts
CHANGED
|
@@ -24,7 +24,7 @@ const PROFILES: Record<KdProduct, ProductProfile> = {
|
|
|
24
24
|
language: "unknown",
|
|
25
25
|
knowledgeScope: "common",
|
|
26
26
|
requiresMetadataVerification: true,
|
|
27
|
-
notes: ["
|
|
27
|
+
notes: ["尚未选择产品;禁止假设插件技术栈、BOS/Cosmic 平台规则或 KSQL/SQL 交付规则。"],
|
|
28
28
|
},
|
|
29
29
|
flagship: {
|
|
30
30
|
product: "flagship",
|
|
@@ -36,7 +36,7 @@ const PROFILES: Record<KdProduct, ProductProfile> = {
|
|
|
36
36
|
requiresMetadataVerification: true,
|
|
37
37
|
notes: [
|
|
38
38
|
"星空旗舰版基于苍穹/Cosmic 平台。使用平台元数据、插件生命周期、SDK 和后置检查约束,但接口可能存在旗舰版差异。",
|
|
39
|
-
"
|
|
39
|
+
"当前工作区存在 code/ 目录时,产品代码必须放在 code/ 下。",
|
|
40
40
|
"创建或编辑代码前,必须检查 code/ 下真实项目结构,并跟随其实际布局;可能按云、按应用组织,也可能不分模块。",
|
|
41
41
|
],
|
|
42
42
|
},
|