import { existsSync } from "node:fs"; import { join } from "node:path"; import { readArtifact } from "./artifacts.ts"; import { buildControlFrame, type KdControlFrame } from "./control-frame.ts"; import { discoverMetadataSources } from "./metadata-discovery.ts"; import { readActiveRun, updatePhaseArtifact } from "./state.ts"; import type { ActiveRun } from "./types.ts"; export type KdDemandLoopStage = "observe" | "plan" | "implement" | "verify" | "repair" | "uat" | "blocked"; export interface KdDemandLoop { stage: KdDemandLoopStage; title: string; firstAction: string; continueWhen: string; stopWhen: string; allowedActions: string[]; forbiddenActions: string[]; knownInputs: string[]; } interface LitePlanFacts { targetPath?: string; metadataPath?: string; formId?: string; fieldId?: string; trigger?: string; rule?: string; } export function maybeSeedLitePlan(cwd: string, run: ActiveRun): { run: ActiveRun; changed: boolean; reason?: string } { if (run.mode !== "quick" || run.phase !== "plan") return { run, changed: false }; const existing = readArtifact(cwd, run, "plan") ?? ""; if (!planLooksTemplate(existing)) return { run, changed: false }; const facts = inferLitePlanFacts(cwd, run); if (!facts.targetPath) return { run, changed: false, reason: "未能从需求或项目中确定目标文件,保持 plan loop。" }; const plan = formatLitePlan(run, facts); updatePhaseArtifact(cwd, run, "plan", plan); return { run: readActiveRun(cwd) ?? run, changed: true }; } export function buildDemandLoop(cwd: string, run: ActiveRun, userText: string, frame: KdControlFrame = buildControlFrame(cwd, run, userText)): KdDemandLoop { if (run.repair?.status === "repairing") { return { stage: "repair", title: "修复 loop", firstAction: "读取失败证据,定位最小修复点,只修改 PLAN 批准文件;修复后运行同一验证命令。", continueWhen: "验证命令失败且仍有修复轮次。", stopWhen: "同类失败达到上限,或失败需要用户环境/账号/外部系统介入。", allowedActions: ["读取失败证据", "最小代码修复", "复跑同一验证", "记录 kd_verify_result"], forbiddenActions: ["扩大改动范围", "换一个验证命令掩盖失败", "无证据声称修复完成"], knownInputs: knownInputs(cwd, run), }; } if (run.phase === "execute") { return { stage: "implement", title: "实现 loop", firstAction: "读取 PLAN.md 和目标源码,确认允许修改文件;按 STEP 顺序实现首版代码,然后记录 EXECUTION.md 和 step evidence。", continueWhen: "当前 STEP 还未实现或实现后尚未完成本地检查。", stopWhen: "所有 PLAN STEP 完成并有 evidence,可进入 verify;或遇到缺失文件/签名/业务事实必须提问。", allowedActions: ["读取目标文件", "编辑 PLAN 批准文件", "运行轻量检查", "记录执行 evidence"], forbiddenActions: ["修改 PLAN 未批准文件", "重新回到需求讨论", "用模板代码代替真实实现"], knownInputs: knownInputs(cwd, run), }; } if (frame.focus !== "phase") { return { stage: "blocked", title: "阻塞处理 loop", firstAction: frame.nextAction.instruction, continueWhen: "阻塞项被确定性解除,gate 刷新通过。", stopWhen: "需要用户回答、状态损坏、凭证/外部系统/生产验证等必须人工介入。", allowedActions: ["处理唯一阻塞项", "记录结构化事实或 evidence", "刷新 gate"], forbiddenActions: ["跳过阻塞项", "创建无关新需求", "在非 execute 阶段写产品源码"], knownInputs: knownInputs(cwd, run), }; } if (run.phase === "verify") { return { stage: "verify", title: "验证 loop", firstAction: "运行项目可用的最小验证命令或静态检查;成功记录 verify-pass evidence,失败进入修复 loop。", continueWhen: "验证尚未执行或失败后仍可自动修复。", stopWhen: "验证通过并形成用户验收点;或验证需要用户环境/外部系统。", allowedActions: ["运行验证命令", "记录 kd_verify_result", "整理用户验收清单"], forbiddenActions: ["未运行验证就声称通过", "跳过失败原因", "修改无关代码"], knownInputs: knownInputs(cwd, run), }; } if (run.phase === "plan") { return { stage: "plan", title: "计划 loop", firstAction: "读取目标文件、项目结构和本地元数据;把 PLAN.md 改成最小可执行计划,明确允许修改文件、字段、触发点、验收样例。", continueWhen: "目标文件、关键字段、触发点和业务规则已足够生成首版计划。", stopWhen: "缺目标文件或关键业务决策,且项目/元数据无法自动查到。", allowedActions: ["读取文件", "自动采集元数据", "写最小 PLAN", "推进 execute"], forbiddenActions: ["输出空模板 PLAN", "让用户记命令", "询问可自动从项目查到的信息"], knownInputs: knownInputs(cwd, run), }; } return { stage: "observe", title: "观察 loop", firstAction: "读取项目上下文、目标文件线索和本地元数据,确定产品、目标对象、字段和最小实现入口。", continueWhen: "已拿到首版实现所需最小事实。", stopWhen: "项目中不存在目标文件且用户需求没有可定位线索。", allowedActions: ["搜索文件", "读取本地元数据", "提取事实", "推进 plan"], forbiddenActions: ["凭空猜路径", "重复提问已给出的事实", "先写模板代码"], knownInputs: knownInputs(cwd, run), }; } export function formatDemandLoop(loop: KdDemandLoop): string { return [ `Loop:${loop.stage} - ${loop.title}`, `第一动作:${loop.firstAction}`, `继续条件:${loop.continueWhen}`, `停止/交还条件:${loop.stopWhen}`, "允许动作:", ...loop.allowedActions.map((item) => `- ${item}`), "禁止动作:", ...loop.forbiddenActions.map((item) => `- ${item}`), "已知输入:", ...(loop.knownInputs.length > 0 ? loop.knownInputs.map((item) => `- ${item}`) : ["- 无。"]), ].join("\n"); } function inferLitePlanFacts(cwd: string, run: ActiveRun): LitePlanFacts { const text = run.goal ?? ""; const metadata = discoverMetadataSources(cwd, run).items.find((item) => item.kind === "project-file")?.path; return { targetPath: firstExistingPath(cwd, text) ?? firstPath(text, /\.(?:java|cs|py|xml|sql|ksql)\b/i), metadataPath: metadata ?? firstPath(text, /\.(?:xml|json)\b/i), formId: firstFormId(text), fieldId: firstFieldId(text), trigger: triggerFromText(text), rule: ruleFromText(text), }; } function formatLitePlan(run: ActiveRun, facts: LitePlanFacts): string { const target = facts.targetPath ?? "待确认"; const form = facts.formId ?? "待确认"; const field = facts.fieldId ?? "待确认"; const trigger = facts.trigger ?? "保存前"; const rule = facts.rule ?? `${field} 必须满足业务校验条件`; const metadataLine = facts.metadataPath ? `- 本地元数据:${facts.metadataPath}` : "- 本地元数据:未发现,首版实现以用户给定 FormId/字段为准,用户验收暴露偏差后修复"; return [ "# 实施计划", "", "## 已检查的项目结构", "", `- 需求:${run.goal ?? "未命名需求"}`, `- 产品:${run.profile?.displayName ?? run.profile?.product ?? "未确认"}`, `- 目标文件:${target}`, metadataLine, "", "## 待读取文件", "", `- ${target}`, facts.metadataPath ? `- ${facts.metadataPath}` : undefined, "", "## 目标源码根 / 路径", "", `- ${target}`, "", "## 允许修改的文件", "", `- ${target}`, "", "## 产品实现范围", "", "- 是否涉及产品实现、构建、元数据或 SDK 查证:是,简单表单插件首版实现。", "- 判断依据:需求包含表单插件、保存前校验、FormId/字段标识和目标源码文件。", "", "## 业务数据源上下文", "", `- 目标 FormId/单据或表单标识:${form}`, `- 插件类型和触发事件:表单插件,${trigger}`, `- 字段/实体/分录标识:${field}`, "- 数据读取写入方式:通过当前表单模型/事件上下文读取单据数据;不直接查询数据库。", "- 直连数据库:不涉及。", "", "## 通用实现契约", "", `- 触发入口或执行时机:${trigger}`, `- 源对象/输入数据:${form} 当前单据及明细行。`, `- 目标对象/输出结果:${form} 当前保存操作。`, "- 数据变化或字段映射:不修改业务字段;只做保存前校验。", `- 业务规则和适用条件:${rule}。`, "- 失败处理:阻止保存并向用户返回明确错误信息。", "- 回滚或人工处理方式:未写入数据,无需数据回滚;用户修正字段后重新保存。", `- 验收样例或测试数据:${form} 明细 ${field}=0 保存失败;${field}=1 保存成功。`, "", "## 必需的金蝶查证项", "", facts.metadataPath ? "- 元数据证据:优先使用本地元数据文件自动采集;若工具不可用,首版实现后在用户验收中校验字段绑定。" : "- 元数据证据:当前缺本地可解析证据,首版实现后在用户验收中校验字段绑定。", "- SDK 签名证据:简单首版不强制;如当前项目构建失败,再根据构建输出修复。", "", "## 需求条目 / 依赖 / 批次", "", `- 实现 ${form} ${trigger} 对 ${field} 的校验。`, "", "## 验收与验证对应关系", "", `- ${field}=0 或空值:保存失败并提示用户。`, `- ${field}=1:保存通过。`, "", "## 执行步骤", "", `- [ ] STEP-001:读取 ${target},确认现有类结构和可插入位置。`, `- [ ] STEP-002:在 ${target} 实现 ${field} 大于 0 的保存前校验首版。`, "- [ ] STEP-003:运行可用的最小语法/静态检查;无法运行时记录真实原因和用户验收方式。", "- [ ] STEP-004:记录执行结果、变更文件和用户验收点。", "", "## TDD / 红绿检查", "", "- 红灯证据:简单首版可用用户验收前置样例替代自动红灯;如项目有测试框架,先补失败样例。", "- 绿灯证据:本地语法/构建检查或用户验收通过记录。", "- 红绿检查命令或工具:优先使用项目已有构建命令;没有构建文件时用源码静态检查和用户验收清单。", "", "## 验证命令", "", "- 自动选择项目已有构建命令;没有构建文件时说明不可运行原因。", "", "## 回滚 / 影响控制", "", `- 仅修改 ${target};回滚该文件即可撤销首版实现。`, "", ] .filter((line): line is string => line !== undefined) .join("\n"); } function planLooksTemplate(plan: string): boolean { return /待确认|目标源码根 \/ 路径\s*\r?\n\s*\r?\n##|允许修改的文件\s*\r?\n\s*\r?\n##/.test(plan); } function knownInputs(cwd: string, run: ActiveRun): string[] { const facts = inferLitePlanFacts(cwd, run); return [ run.goal ? `需求:${run.goal}` : undefined, run.profile?.product && run.profile.product !== "unknown" ? `产品:${run.profile.displayName}` : undefined, facts.targetPath ? `目标文件:${facts.targetPath}` : undefined, facts.metadataPath ? `元数据:${facts.metadataPath}` : undefined, facts.formId ? `FormId:${facts.formId}` : undefined, facts.fieldId ? `字段:${facts.fieldId}` : undefined, facts.trigger ? `触发点:${facts.trigger}` : undefined, ].filter((item): item is string => Boolean(item)); } function firstExistingPath(cwd: string, text: string): string | undefined { for (const path of allPaths(text)) { if (existsSync(join(cwd, path))) return normalizePath(path); } return undefined; } function firstPath(text: string, extension: RegExp): string | undefined { return allPaths(text).find((path) => extension.test(path)); } function allPaths(text: string): string[] { return [...text.matchAll(/(?:^|[\s"'::;;,,])([A-Za-z0-9_.-]+(?:[\\/][A-Za-z0-9_.-]+)+\.(?:java|cs|py|xml|json|sql|ksql|yml|yaml|properties))(?=$|[\s"'。;;,,))])/g)].map((match) => normalizePath(match[1]), ); } function firstFormId(text: string): string | undefined { return text.match(/\b[A-Z][A-Za-z0-9]*_[A-Za-z0-9_]+\b/)?.[0]; } function firstFieldId(text: string): string | undefined { const labeled = text.match(/(?:字段|字段标识|校验字段)\s*[::=]?\s*([A-Za-z][A-Za-z0-9_]*)/i)?.[1]; if (labeled) return labeled; return text.match(/\bF[A-Z][A-Za-z0-9_]*\b/)?.[0]; } function triggerFromText(text: string): string | undefined { if (/保存前|before\s*save|BeforeSave|BeforeDoOperation/i.test(text)) return "保存前"; if (/保存后|after\s*save|AfterSave/i.test(text)) return "保存后"; if (/提交前/i.test(text)) return "提交前"; if (/提交后/i.test(text)) return "提交后"; if (/审核前/i.test(text)) return "审核前"; if (/审核后/i.test(text)) return "审核后"; return /表单插件|操作插件|列表插件/.test(text) ? "插件触发时" : undefined; } function ruleFromText(text: string): string | undefined { const match = text.match(/([A-Za-z][A-Za-z0-9_]*\s*(?:必须|需|要|大于|小于|不为空|不能为空)[^。;;\n\r]*)/); if (match) return match[1].trim(); if (/大于\s*0|> 0|>\s*0/.test(text)) { const field = firstFieldId(text) ?? "目标字段"; return `${field} 必须大于 0`; } return undefined; } function normalizePath(path: string): string { return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, ""); }