/** * GoalAdapter — bridges old goal-loop.ts functions into LoopKernel. * * Provides structured goal parsing, gate-validated execution, and * LoopKernel-compatible interfaces. The old goal-loop.ts is referenced * for parse/resolve logic, but all execution goes through GateRunner. * * @see src/harness/goal-loop.ts — legacy goal parsing and execution * @see src/harness/gates/gate-runner.ts — gate policy evaluation */ import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import type { ActiveRun } from "../types.ts"; import type { GateDecision } from "../gates/findings.ts"; import type { GateRunner } from "../gates/gate-runner.ts"; import { createVerifyPlan, validateVerifyPlan, type VerifyPlan, type VerifyPlanValidationResult } from "./verify-plan.ts"; // --------------------------------------------------------------------------- // Parsed goal (mirrors old goal-loop ParsedGoal) // --------------------------------------------------------------------------- /** Goal 解析结果。 */ export interface ParsedGoal { /** 解析出的 package.json script 名称。 */ script: string; /** 原始目标文本。 */ rawText: string; } // --------------------------------------------------------------------------- // Goal execution result // --------------------------------------------------------------------------- /** Goal 执行结果,包含门禁决策。 */ export interface GoalExecutionResult { /** 是否达成目标。 */ achieved: boolean; /** 尝试次数。 */ attempts: number; /** 结果消息。 */ message: string; /** 门禁决策(仅当 gateRunner 可用时存在)。 */ gateDecision?: GateDecision; /** 关联的 VerifyPlan(仅当成功创建时存在)。 */ verifyPlan?: VerifyPlan; /** 完整校验结果。 */ validationResult?: VerifyPlanValidationResult; } // --------------------------------------------------------------------------- // GoalAdapter interface (LoopKernel contract) // --------------------------------------------------------------------------- /** * GoalAdapter — LoopKernel 消费的目标适配器接口。 * * LoopKernel 通过此接口解析、验证和执行目标, * 不直接依赖旧 goal-loop.ts 的全局函数。 */ export interface GoalAdapter { /** * 解析目标文本为结构化 ParsedGoal。 * * @param text - 原始目标文本(如 "npm run check 通过")。 * @returns 解析结果,无法解析时返回 null。 */ parseGoal(text: string): ParsedGoal | null; /** * 解析目标对应的验证脚本名。 * * @param goal - 目标文本。 * @param cwd - 工作区根目录。 * @returns 脚本名称,无法解析时返回空字符串。 */ resolveVerifyScript(goal: string, cwd: string): string; /** * 校验目标命令是否可通过门禁。 * * 运行 pre-verify-command 门禁检查点, * 返回门禁决策和校验结果。 * * @param goal - 目标文本。 * @param cwd - 工作区根目录。 * @param gateRunner - GateRunner 实例。 * @param run - 当前 ActiveRun。 * @returns 完整校验结果,包含门禁决策。 */ validateGoal( goal: string, cwd: string, gateRunner: GateRunner, run?: ActiveRun, ): Promise; /** * 通过门禁执行目标。 * * 流程: * 1. 解析目标 → scriptName。 * 2. 创建 VerifyPlan。 * 3. 通过 GateRunner 运行 pre-verify-command 门禁。 * 4. 门禁通过后返回结果(不实际执行 shell 命令)。 * * @param goal - 目标文本。 * @param cwd - 工作区根目录。 * @param gateRunner - GateRunner 实例。 * @param run - 当前 ActiveRun。 * @returns 执行结果,包含门禁决策和 VerifyPlan。 */ executeGoal( goal: string, cwd: string, gateRunner: GateRunner, run?: ActiveRun, ): Promise; } // --------------------------------------------------------------------------- // ParseGoalAdapter — wraps old parseGoal() logic // --------------------------------------------------------------------------- /** * 从 package.json 读取 scripts 名称列表。 */ function readPackageScripts(cwd: string): Record { try { const pkgPath = join(cwd, "package.json"); if (!existsSync(pkgPath)) return {}; const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { scripts?: Record }; return pkg.scripts ?? {}; } catch { return {}; } } /** * ParseGoalAdapter — 实现 GoalAdapter 的解析部分。 * * 封装旧 goal-loop.ts 的 parseGoal() 和 resolveVerifyScript(), * 提供结构化接口供 LoopKernel 消费。 */ export class ParseGoalAdapter implements GoalAdapter { /** * 解析目标文本为结构化 ParsedGoal。 * * 支持的格式: * - "npm run {script}" → { script } * - "npm test" → { script: "test" } * * @param text - 原始目标文本。 * @returns 解析结果,无法解析时返回 null。 */ parseGoal(text: string): ParsedGoal | null { const runMatch = text.match(/\bnpm\s+run\s+([A-Za-z0-9:_-]+)\b/i); if (runMatch) { return { script: runMatch[1], rawText: text }; } if (/\bnpm\s+test\b/i.test(text)) { return { script: "test", rawText: text }; } return null; } /** * 解析目标对应的验证脚本名。 * * 按优先级匹配: * 1. 直接 npm run {script} 匹配。 * 2. type/check/tsc 关键词 → check 类脚本。 * 3. test 关键词 → test 类脚本。 * 4. lint 关键词 → lint 类脚本。 * 5. build 关键词 → build 类脚本。 * * @param goal - 目标文本。 * @param cwd - 工作区根目录。 * @returns 脚本名称,无法解析时返回空字符串。 */ resolveVerifyScript(goal: string, cwd: string): string { const packageScripts = readPackageScripts(cwd); const scripts = Object.keys(packageScripts); const goalLower = goal.toLowerCase(); const parsed = this.parseGoal(goal); if (parsed?.script && packageScripts[parsed.script]) return parsed.script; if (/type(script|check)|tsc/.test(goalLower)) { const found = scripts.find((s) => /check|type/.test(s)); if (found) return found; } if (/test/.test(goalLower)) { const found = scripts.find((s) => /test/.test(s)); if (found) return found; } if (/lint/.test(goalLower)) { const found = scripts.find((s) => /lint/.test(s)); if (found) return found; } if (/build/.test(goalLower)) { const found = scripts.find((s) => /build/.test(s)); if (found) return found; } return ""; } /** * 校验目标命令是否可通过门禁。 * * @param goal - 目标文本。 * @param cwd - 工作区根目录。 * @param gateRunner - GateRunner 实例。 * @param run - 当前 ActiveRun。 * @returns 完整校验结果。 */ async validateGoal( goal: string, cwd: string, gateRunner: GateRunner, run?: ActiveRun, ): Promise { const script = this.resolveVerifyScript(goal, cwd); if (!script) { return { errors: [{ severity: "hard-deny", checkpoint: "pre-verify-command", message: `无法从目标文本中解析验证脚本:${goal}`, }], allowed: false, }; } const plan = createVerifyPlan(script, "goal-adapter", "goal-adapter"); return validateVerifyPlan(plan, cwd, { gateRunner, run }); } /** * 通过门禁执行目标。 * * @param goal - 目标文本。 * @param cwd - 工作区根目录。 * @param gateRunner - GateRunner 实例。 * @param run - 当前 ActiveRun。 * @returns 执行结果。 */ async executeGoal( goal: string, cwd: string, gateRunner: GateRunner, run?: ActiveRun, ): Promise { const script = this.resolveVerifyScript(goal, cwd); if (!script) { return { achieved: false, attempts: 0, message: "无法解析目标中的验证脚本。请使用 package.json 中已声明的脚本,如:npm run check", }; } // Create VerifyPlan const plan = createVerifyPlan(script, "goal-adapter", "goal-adapter"); // Validate through GateRunner const validationResult = await validateVerifyPlan(plan, cwd, { gateRunner, run }); if (!validationResult.allowed) { const errorMessages = validationResult.errors.map((e) => e.message).join("; "); return { achieved: false, attempts: 0, message: `门禁拒绝执行:${errorMessages}`, gateDecision: validationResult.gateDecision, verifyPlan: plan, validationResult, }; } // Gate passed — return success (caller is responsible for actual execution) return { achieved: true, attempts: 0, message: `门禁通过,验证命令已就绪:npm run ${script}`, gateDecision: validationResult.gateDecision, verifyPlan: plan, validationResult, }; } } // --------------------------------------------------------------------------- // ExecuteGoalAdapter — full GoalAdapter with gate-validated execution // --------------------------------------------------------------------------- /** * ExecuteGoalAdapter — 完整的 GoalAdapter 实现。 * * 继承 ParseGoalAdapter 的解析能力, * 扩展 executeGoal() 以支持 repair loop(最大重试次数)。 * * 与旧 goal-loop.ts 的区别: * - 所有命令必须通过 GateRunner 的 pre-verify-command 门禁。 * - 不直接执行 shell 命令,只返回门禁通过的 VerifyPlan。 * - repair loop 由 LoopKernel 控制,adapter 只负责单次验证。 */ export class ExecuteGoalAdapter extends ParseGoalAdapter { /** * 通过门禁执行目标,支持 repair loop 上下文。 * * @param goal - 目标文本。 * @param cwd - 工作区根目录。 * @param gateRunner - GateRunner 实例。 * @param run - 当前 ActiveRun(用于 repair state 恢复)。 * @returns 执行结果,包含 repair 上下文信息。 */ override async executeGoal( goal: string, cwd: string, gateRunner: GateRunner, run?: ActiveRun, ): Promise { // Check for interrupted repair if (run?.repair?.status === "repairing" && run.repair.goal) { return super.executeGoal(run.repair.goal, cwd, gateRunner, run); } return super.executeGoal(goal, cwd, gateRunner, run); } }