import { existsSync } from "node:fs"; import { join } from "node:path"; import type { ActiveRun } from "./types.ts"; import { runRoot } from "./paths.ts"; import { hasEvidenceEntry } from "./evidence.ts"; export interface PlanStep { id: string; text: string; } const STEP_LINE_PATTERN = /^\s*[-*]\s*\[[ xX]\]\s*(STEP-\d{3,})\s*[::]\s*(.+)$/gim; const EVIDENCE_PATTERN = /\bevidence[\\/][^\s`"')]+/gi; export function parsePlanSteps(plan: string): PlanStep[] { const steps: PlanStep[] = []; for (const match of plan.matchAll(STEP_LINE_PATTERN)) { steps.push({ id: match[1].toUpperCase(), text: match[2].trim() }); } return steps; } export function planStepsBlockReason(plan: string): string | undefined { if (!/##\s*执行步骤/i.test(plan)) { return "PLAN.md 缺少 ## 执行步骤。下一步:回到 plan,补充 `## 执行步骤`;使用 `- [ ] STEP-001:...` 拆分可跟踪步骤;每一步必须产生代码变更或 evidence。"; } const steps = parsePlanSteps(plan); if (steps.length === 0) { return "PLAN.md 没有可执行步骤。下一步:在 `## 执行步骤` 下使用 `- [ ] STEP-001:...` 列出步骤;禁止用普通段落代替可勾选步骤。"; } const duplicate = firstDuplicate(steps.map((step) => step.id)); if (duplicate) return `PLAN.md 存在重复步骤编号:${duplicate}。下一步:重新编号执行步骤,确保 STEP-001、STEP-002 等编号唯一且顺序稳定。`; return undefined; } export function executionStepsBlockReason(cwd: string, run: ActiveRun, plan: string, execution: string): string | undefined { const steps = parsePlanSteps(plan); if (steps.length === 0) return planStepsBlockReason(plan); const missing: string[] = []; const missingEvidence: string[] = []; for (const step of steps) { const line = findExecutionLine(execution, step.id); if (!line) { missing.push(step.id); continue; } const evidencePaths = [...line.matchAll(EVIDENCE_PATTERN)].map((match) => normalizeEvidencePath(match[0])); if (evidencePaths.length === 0 || !evidencePaths.some((path) => existsSync(join(runRoot(cwd, run), path)) && hasEvidenceEntry(cwd, run, path))) { missingEvidence.push(step.id); } } if (missing.length > 0) { return `不能进入 verify:EXECUTION.md 未完成计划步骤 ${missing.join(", ")}。下一步:执行缺失步骤;完成后在 EXECUTION.md 的 ## 步骤结果 中使用 \`- [x] STEP-###:已完成。证据:evidence/step-###.md\` 记录;未执行完成时禁止勾选。`; } if (missingEvidence.length > 0) { return `不能进入 verify:步骤 ${missingEvidence.join(", ")} 缺少已落地的 evidence 文件。下一步:为每个步骤创建真实 evidence 文件,写入检查命令、结果、改动文件或验证输出,然后在 EXECUTION.md 对应步骤行引用该 evidence 路径。`; } return undefined; } function findExecutionLine(execution: string, stepId: string): string | undefined { return execution .split(/\r?\n/) .find((line) => line.toUpperCase().includes(stepId) && /(\[[xX]\]|done|完成|completed)/i.test(line)); } function firstDuplicate(values: string[]): string | undefined { const seen = new Set(); for (const value of values) { if (seen.has(value)) return value; seen.add(value); } return undefined; } function normalizeEvidencePath(path: string): string { return path.replace(/\\/g, "/").replace(/^\/+/, ""); }