/** * SourceWritePolicy — blocks source writes when not in execute phase, * when gate not passed, when plan not approved, etc. * * Checkpoints: pre-write * * Logic migrated from: * - src/harness/action-policy.ts: sourceWriteBlockReason * - src/harness/path-policy.ts: isSourceLikePath */ import type { GateContext, GateFinding } from "../findings.ts"; import type { ActiveRun } from "../../types.ts"; export const SOURCE_WRITE_POLICY_NAME = "source-write-policy"; const SOURCE_EXTENSIONS = new Set([ ".java", ".kt", ".kts", ".xml", ".properties", ".yml", ".yaml", ".sql", ".ksql", ".cs", ".py", ]); export function evaluateSourceWritePolicy(ctx: GateContext): GateFinding[] { const findings: GateFinding[] = []; const { path, run, cwd } = ctx; if (!path || !isSourceLikePath(path)) return findings; // No active run if (!run) { findings.push({ id: "SWP-001", severity: "hard-deny", policy: SOURCE_WRITE_POLICY_NAME, message: "当前没有正在处理的需求,禁止直接写产品代码。", nextAction: "先用 /kd <需求> 开始;按当前流程完成计划并进入 execute 后再写生产源码。", }); return findings; } // Not in execute phase if (run.phase !== "execute") { findings.push({ id: "SWP-002", severity: "hard-deny", policy: SOURCE_WRITE_POLICY_NAME, message: `当前阶段是 ${run.phase},禁止写产品代码。`, nextAction: "先完成当前阶段文档和检查,用 /kd phase execute 进入 execute;计划不完整时更新 PLAN.md,禁止直接写代码。", }); return findings; } // Open blocking question const openQuestion = (run.questions ?? []).find((q) => q.status === "open" && q.blocking); if (openQuestion) { findings.push({ id: "SWP-004", severity: "hard-deny", policy: SOURCE_WRITE_POLICY_NAME, message: `存在未回答阻断问题 ${openQuestion.id},禁止写产品代码。`, nextAction: "先记录该问题答案,再重新检查门禁。", }); } // Open resume snapshot if (run.resumeSnapshot?.status === "open") { findings.push({ id: "SWP-005", severity: "hard-deny", policy: SOURCE_WRITE_POLICY_NAME, message: `存在 open resumeSnapshot,禁止写产品代码。`, nextAction: `先恢复断点并处理 ${run.resumeSnapshot.pendingQuestionId ?? "当前阻塞问题"}。`, }); } return findings; } // ── Utility functions (reimplemented from src/harness/path-policy.ts) ──────── function isSourceLikePath(path: string): boolean { const normalized = normalizeRelativePath(path); const lastSegment = normalized.split("/").at(-1) ?? normalized; const dotIndex = lastSegment.lastIndexOf("."); if (dotIndex < 0) return false; return SOURCE_EXTENSIONS.has(lastSegment.slice(dotIndex).toLowerCase()); } function normalizeRelativePath(path: string): string { return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, ""); }