/** * Goal-kind-based verify policy (B020). * * Cost of the skeptic panel should scale with stakes: * - analysis / research → fewer skeptics (default N=1) * - code-change → full panel (default N=3) * * Explicit config `skepticN` remains the clamp bounds and the fallback when * plan kind is missing/unknown. Callers may still pass an explicit override. */ import { parsePlan, type GoalKind } from "../plan/template.ts"; /** Default skeptic counts by goal kind (before config clamp). */ export const SKEPTIC_N_BY_KIND: Readonly> = { analysis: 1, research: 1, "code-change": 3, }; export interface VerifyPolicyInput { /** Parsed or declared goal kind (from plan.md `## Goal kind`). */ goalKind?: GoalKind | string | null; /** * Explicit override (e.g. extension config `skepticN` when the operator * forced a count). When set, wins over kind defaults. * Pass `undefined` to derive purely from kind. */ overrideN?: number | null; /** Fallback when kind is missing/unknown and no override (default 3). */ defaultN?: number; } /** * Select skeptic panel size for a goal. * * Priority: * 1. `overrideN` if a finite number (clamped 1–5) * 2. kind table (analysis/research → 1, code-change → 3) * 3. `defaultN` (default 3), clamped 1–5 */ export function selectSkepticN(input: VerifyPolicyInput = {}): number { const clamp = (n: number) => Math.min(5, Math.max(1, Math.trunc(n))); if (input.overrideN != null && Number.isFinite(input.overrideN)) { return clamp(Number(input.overrideN)); } const kind = normalizeGoalKind(input.goalKind); if (kind && kind in SKEPTIC_N_BY_KIND) { return clamp(SKEPTIC_N_BY_KIND[kind]); } const fallback = input.defaultN ?? 3; return clamp(fallback); } /** Normalize free-form kind text to a known GoalKind, or null. */ export function normalizeGoalKind( raw: GoalKind | string | null | undefined, ): GoalKind | null { if (raw == null) return null; const k = String(raw).trim().toLowerCase(); if (k === "code-change" || k === "analysis" || k === "research") return k; // Common aliases if (k === "code" || k === "codechange" || k === "implementation") return "code-change"; if (k === "analyse" || k === "investigat" || k.startsWith("investigat")) return "analysis"; if (k === "docs" || k === "documentation" || k === "read") return "research"; return null; } /** * Read goal kind from plan.md markdown. * Prefer the raw `## Goal kind` first line so aliases (`code` → `code-change`) * work; parsePlan only accepts exact GoalKind tokens. * Returns null when the section is missing or unrecognized. */ export function readGoalKindFromPlan(planMarkdown: string | null | undefined): GoalKind | null { if (!planMarkdown?.trim()) return null; const re = /^##\s+Goal kind\s*$/im; const match = re.exec(planMarkdown); if (match) { const start = match.index + match[0].length; const rest = planMarkdown.slice(start); const next = /^##\s+/im.exec(rest); const body = (next ? rest.slice(0, next.index) : rest).trim(); const first = body.split("\n")[0]?.trim(); const fromRaw = normalizeGoalKind(first); if (fromRaw) return fromRaw; } // Fallback via parsePlan for exact tokens if heading shape differs slightly. const parsed = parsePlan(planMarkdown); return normalizeGoalKind(parsed.goalKind ?? null); } /** * Resolve skeptic N for a live verify: prefer plan kind defaults unless the * caller forces an override. When `forceConfigN` is true, config.skepticN wins * (operator override). Default is kind-derived with config as fallback only * for unknown kinds. * * Typical call site for `completed:true`: * resolveSkepticNForVerify({ planMarkdown, configSkepticN: config.skepticN }) */ export function resolveSkepticNForVerify(opts: { planMarkdown?: string | null; goalKind?: GoalKind | string | null; /** Config default / operator preference — used as fallback, not hard override. */ configSkepticN?: number; /** When true, configSkepticN is treated as an explicit override. */ forceConfigN?: boolean; }): number { if (opts.forceConfigN && opts.configSkepticN != null) { return selectSkepticN({ overrideN: opts.configSkepticN }); } const kind = opts.goalKind != null ? normalizeGoalKind(opts.goalKind) : readGoalKindFromPlan(opts.planMarkdown); return selectSkepticN({ goalKind: kind, defaultN: opts.configSkepticN ?? 3, }); }