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.
@@ -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
+ }
@@ -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() || "unknown",
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();
@@ -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 = answer.trim();
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 legacyEdition = (parsed as ActiveRun & { edition?: string }).edition;
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
+ }
@@ -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"];