AUDIT / src /lib /buildFailureHandler.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
8.91 kB
/**
* 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 };
}