import { existsSync, readFileSync } from "node:fs"; import { isAbsolute, join, relative } from "node:path"; import type { ActiveRun } from "./types.ts"; import { isPathInsideWorkspace } from "../platform/path.ts"; /** * @deprecated Write postcondition checks. Source write blocking is now handled by * V2 SourceWritePolicy via GateRunner pre-write checkpoint. * This module remains for backward-compatible file existence and template checks. */ export type ToolPostconditionStatus = "passed" | "blocked" | "not-applicable"; export interface ToolPostconditionInput { cwd: string; toolName: string; path?: string; run?: ActiveRun; expected?: "write-file" | "read-file" | "verify-evidence"; } export interface ToolPostconditionResult { status: ToolPostconditionStatus; reason: string; checks: string[]; } export function evaluateToolPostcondition(input: ToolPostconditionInput): ToolPostconditionResult { if (!input.expected && input.toolName !== "write" && input.toolName !== "edit" && input.toolName !== "read") { return { status: "not-applicable", reason: "该工具无 KCode 后置条件。", checks: [] }; } if ((input.toolName === "write" || input.toolName === "edit" || input.expected === "write-file") && input.path) { return evaluateWritePostcondition(input); } if ((input.toolName === "read" || input.expected === "read-file") && input.path) { return evaluateReadPostcondition(input); } return { status: "not-applicable", reason: "缺少可校验路径或预期。", checks: [] }; } function evaluateWritePostcondition(input: ToolPostconditionInput): ToolPostconditionResult { const checks: string[] = []; const path = input.path!; const resolved = resolvePath(input.cwd, path); checks.push("path-present"); if (!isPathInsideWorkspace(input.cwd, resolved)) { return { status: "blocked", reason: `写入后置条件失败:路径不在当前工作区内:${path}`, checks }; } checks.push("inside-workspace"); // Source write blocking delegated to V2 GateRunner pre-write checkpoint if (!existsSync(resolved)) { return { status: "blocked", reason: `写入后置条件失败:目标文件不存在:${path}`, checks }; } checks.push("file-exists"); const content = safeRead(resolved); if (content !== undefined && hasTemplatePlaceholder(content)) { return { status: "blocked", reason: "写入后置条件失败:文件仍包含模板占位内容。", checks }; } checks.push("no-template-placeholder"); return { status: "passed", reason: "写入后置条件通过。", checks }; } function evaluateReadPostcondition(input: ToolPostconditionInput): ToolPostconditionResult { const checks: string[] = []; const path = input.path!; checks.push("path-present"); const resolved = resolvePath(input.cwd, path); if (!existsSync(resolved)) { return { status: "blocked", reason: `读取后置条件失败:文件不存在:${path}`, checks }; } checks.push("file-exists"); const relativePath = isAbsolute(path) ? relative(input.cwd, path) : path; if (/^\.\./.test(relativePath.replace(/\\/g, "/"))) { return { status: "blocked", reason: `读取后置条件失败:读取目标位于工作区外:${path}`, checks }; } checks.push("inside-workspace"); return { status: "passed", reason: "读取后置条件通过。", checks }; } function resolvePath(cwd: string, path: string): string { return isAbsolute(path) ? path : join(cwd, path); } function safeRead(path: string): string | undefined { try { return readFileSync(path, "utf8"); } catch { return undefined; } } function hasTemplatePlaceholder(content: string): boolean { return /TODO|TBD|待确认|按实际环境|your[-_ ]?(form|field|token|url)|示例代码|sample only/i.test(content); }