Spaces:
Sleeping
Sleeping
| /** | |
| * 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<HandlerDeps>): 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 }; | |
| } | |