kcode-pi 0.1.35 → 0.1.39
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/context/project-context.js +2 -3
- package/dist/harness/prompt-policy.d.ts +19 -0
- package/dist/harness/prompt-policy.js +209 -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 +78 -0
- package/docs/COMMAND_REFERENCE.md +17 -2
- package/docs/EVIDENCE_AND_GATES.md +7 -0
- package/docs/HARNESS_WORKFLOW.md +23 -0
- package/extensions/kingdee-harness.ts +97 -45
- package/package.json +1 -1
- package/src/context/project-context.ts +2 -3
- package/src/harness/data-source-policy.ts +52 -8
- package/src/harness/delegation.ts +8 -0
- package/src/harness/messages.ts +1 -1
- package/src/harness/prompt-policy.ts +35 -1
- package/src/harness/prompt.ts +4 -0
- package/src/harness/question-memory.ts +220 -0
- package/src/harness/repair.ts +16 -1
- package/src/harness/state.ts +92 -5
- package/src/harness/types.ts +19 -0
|
@@ -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
|
}
|
|
@@ -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,6 +83,7 @@ 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
89
|
reason: "进入 spec 前必须完成 CONTEXT.md 并确认产品画像",
|
|
@@ -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"];
|