/** * buildFailureHandler.ts — Build Failure Handler (S40) * * Punto di ingresso per gestire i fallimenti di build in agentLoop. * Coordina: classifyBuildFailure → analyzeFailed (CriticMode) → decisione azione. * * Responsabilità: * - Decidere l'azione concreta: patch / rewrite / split / escalate * - Assegnare priorità: low / medium / high / critical * - Estrarre i file target * - Generare istruzioni leggibili per l'agente * - Segnalare se CriticMode è stato attivato * * Design: * - Pure: nessun side-effect, nessun LLM * - DI via _setTestHandler / _resetHandler * - Safari-safe: nessun Worker * - Wire-in agentLoop: 4 righe max (S42) * * @module buildFailureHandler */ import { classifyBuildFailure } from "@/lib/confidenceEngine"; import { analyzeFailed, shouldEnterCriticMode, extractAffectedFiles, type CriticContext, type RecoveryStrategy, } from "@/lib/criticMode"; // ── Types ────────────────────────────────────────────────────────────────── export type BuildAction = "patch" | "rewrite" | "split" | "escalate"; export type BuildPriority = "low" | "medium" | "high" | "critical"; export interface BuildFailureDecision { /** Azione che l'agente deve eseguire */ action: BuildAction; /** Priorità / urgenza */ priority: BuildPriority; /** File da toccare per questa azione */ targetFiles: string[]; /** Istruzioni concrete per l'agente (leggibili) */ instructions: string; /** true se CriticMode è stato attivato per questa decisione */ usesCriticMode: boolean; /** Messaggio di errore originale sintetizzato (prime 200 char) */ errorSummary: string; } export interface HandleOptions { retryCount?: number; taskType?: string; touchedFiles?: string[]; } // ── DI (Dependency Injection) ───────────────────────────────────────────── export interface HandlerDeps { classify: (errorText: string) => "trivial" | "serious"; analyze: typeof analyzeFailed; } const defaultDeps: HandlerDeps = { classify: classifyBuildFailure, analyze: analyzeFailed, }; let _deps: HandlerDeps = { ...defaultDeps }; export function _setTestHandler(deps: Partial): void { _deps = { ...defaultDeps, ...deps }; } export function _resetHandler(): void { _deps = { ...defaultDeps }; } // ── Core function ────────────────────────────────────────────────────────── /** * Analizza un errore di build e ritorna la decisione di recovery. * * @param errorText Testo grezzo del build failure * @param options Contesto: retryCount, taskType, touchedFiles */ export function handleBuildFailure( errorText: string, options: HandleOptions = {}, ): BuildFailureDecision { const { retryCount = 0, taskType, touchedFiles = [] } = options; const severity = _deps.classify(errorText); const criticContext: CriticContext = { retryCount, taskType, touchedFiles }; const usesCriticMode = shouldEnterCriticMode(errorText, retryCount); // Sempre analyza — CriticMode o meno, l'analisi serve per il decision tree const analysis = _deps.analyze(errorText, criticContext); // ── Mappa strategy → action ──────────────────────────────────────────── let action: BuildAction; if (analysis.escalate) { action = "escalate"; } else { action = strategyToAction(analysis.strategy); } // ── Priorità ────────────────────────────────────────────────────────── const priority = computePriority(action, severity, retryCount); // ── Target files ────────────────────────────────────────────────────── const targetFiles = analysis.affectedFiles.length > 0 ? analysis.affectedFiles : extractAffectedFiles(errorText); // ── Istruzioni ──────────────────────────────────────────────────────── const instructions = buildInstructions(action, targetFiles, analysis.suggestedAction); // ── Error summary ────────────────────────────────────────────────────── const errorSummary = errorText.slice(0, 200).replace(/\s+/g, " ").trim(); return { action, priority, targetFiles, instructions, usesCriticMode, errorSummary, }; } // ── Guard pubblici ──────────────────────────────────────────────────────── /** * true se CriticMode deve essere attivato per questo errore. * Shortcut per agentLoop (wire-in S42). */ export function shouldUseCriticMode( errorText: string, retryCount = 0, ): boolean { return shouldEnterCriticMode(errorText, retryCount); } /** * Estrae i file coinvolti direttamente dall'errore. * Riusa l'estrattore di criticMode (già testato). */ export { extractAffectedFiles as extractTargetFiles } from "@/lib/criticMode"; // ── Helpers ──────────────────────────────────────────────────────────────── function strategyToAction(strategy: RecoveryStrategy): BuildAction { switch (strategy) { case "patch": return "patch"; case "rewrite": return "rewrite"; case "split_task": return "split"; } } function computePriority( action: BuildAction, severity: "trivial" | "serious", retryCount: number, ): BuildPriority { if (action === "escalate") return "critical"; if (action === "split") return "high"; if (severity === "serious") { return retryCount >= 2 ? "high" : "medium"; } return "low"; } function buildInstructions( action: BuildAction, targetFiles: string[], suggestedAction: string, ): string { const fileList = targetFiles.length > 0 ? `\nFile: ${targetFiles.slice(0, 3).join(", ")}` : ""; switch (action) { case "patch": return `[PATCH] Applica correzione chirurgica al punto esatto dell'errore.${fileList}\n${suggestedAction}`; case "rewrite": return `[REWRITE] Riscrivi il componente interessato partendo dalla causa radice.${fileList}\n${suggestedAction}`; case "split": return `[SPLIT] Task troppo complesso per un singolo tentativo — dividi in sotto-task.\n${suggestedAction}`; case "escalate": return `[ESCALATE] Troppi retry falliti — sospendi e mostra l'errore all'utente.\n${suggestedAction}`; } } // ─── Fix C: Blast Radius da BuildFailureDecision ────────────────────────────── // // Collega il concetto di "file coinvolti" del buildFailureHandler al // checkpointManager.markFileTouch(). Il chiamante (agentLoop / toolExecutor) // può usare questa funzione per decidere il restore scope PRIMA di eseguire // l'azione di recovery — evita che un restore di checkpoint cancelli file // non coinvolti nel fallimento. // // risk: // 'high' → patch / rewrite su file critici — restore chirurgico necessario // 'medium' → split task — blast radius parzialmente noto // 'low' → escalate / log — nessun file modificato, restore a basso impatto export interface DecisionBlastRadius { /** File con blast radius minimo per questa decisione */ files: string[]; /** Livello di rischio: guida la strategia di restore */ risk: 'low' | 'medium' | 'high'; } /** * Estrae il blast radius minimo da una BuildFailureDecision. * * Usato da checkpointManager per sapere quali file registrare con markFileTouch * prima di eseguire il recovery — limita il perimetro di un eventuale rollback. * * @example * const br = getDecisionBlastRadius(decision); * br.files.forEach(f => markFileTouch(taskId, f)); * // ora getBlastRadius(taskId) copre esattamente i file a rischio */ export function getDecisionBlastRadius(decision: BuildFailureDecision): DecisionBlastRadius { const risk: DecisionBlastRadius['risk'] = decision.priority === 'critical' || decision.priority === 'high' ? 'high' : decision.priority === 'medium' ? 'medium' : 'low'; return { files: decision.targetFiles.slice(0, 20), risk }; }