/** * diffPipeline.ts — GAP-6: Pipeline end-to-end per applicazione sicura di diff * * Esegue la catena completa: * 1. Pre-check sintassi diff (validateDiffSyntax) * 2. Apply in-process (applySearchReplace per SEARCH/REPLACE) * 3. Post-apply validation (validatePostApply — syntax balance, shrinkage) * 4. Build check opzionale (callback esterno — typecheck/lint) * * Rollback implicito: se success=false → result = originalContent. * Il chiamante NON deve mai fare rollback manuale. * * Unified diff: non applicabile in-process — la pipeline esegue solo * pre/post validation; l'apply è delegato al backend (toolExecutor). * * Zero side-effects, async per il build check — Safari-safe. * * @module diffPipeline */ import { validateDiffSyntax, applySearchReplace, validatePostApply, detectDiffFormat, type DiffValidationResult, type PostApplyValidation, } from "./diffValidator"; // ─── Tipi ──────────────────────────────────────────────────────────────────── export interface DiffPipelineParams { /** Contenuto originale del file PRIMA del patch */ originalContent: string; /** Diff da applicare (SEARCH/REPLACE o unified) */ diff: string; /** Path del file — usato per syntax check specifico per linguaggio */ filePath: string; /** * Callback opzionale per build/typecheck post-apply. * Riceve il contenuto patchato, deve restituire true se il build è OK. * Non-blocking: se crasha → ignorato (buildCheck = skipped). */ runBuildCheck?: (patchedContent: string) => Promise; } export type PipelineStage = "pre" | "apply" | "post" | "build" | "ok"; export interface DiffPipelineResult { success: boolean; /** Contenuto finale: originalContent se fallito, patchedContent se ok */ result: string; /** true se il rollback è stato applicato (apply OK ma post/build falliti) */ rolledBack: boolean; preValidation: DiffValidationResult | null; postValidation: PostApplyValidation | null; /** Fase in cui si è fermata la pipeline */ stage: PipelineStage; /** Messaggio descrittivo — iniettabile nel tool result */ message: string; } // ─── Pipeline ──────────────────────────────────────────────────────────────── /** * Applica un diff con validazione completa pre/post e rollback automatico. * * @example * const result = await applyDiffSafe({ originalContent, diff, filePath }); * if (!result.success) { * // usa result.result (= originalContent) — rollback già applicato * console.warn(result.message); * } */ export async function applyDiffSafe(params: DiffPipelineParams): Promise { const { originalContent, diff, filePath, runBuildCheck } = params; // ── Fase 1: Pre-check sintassi diff ────────────────────────────────────── let preValidation: DiffValidationResult; try { preValidation = validateDiffSyntax(diff); } catch { return _fail("pre", "Errore interno validateDiffSyntax — diff non applicato", null, null, originalContent); } if (!preValidation.valid) { return _fail( "pre", `Pre-validazione fallita: ${preValidation.issues.join("; ")}`, preValidation, null, originalContent, false, // non c'è stato rollback — patch mai applicata ); } // ── Fase 2: Apply ───────────────────────────────────────────────────────── const format = detectDiffFormat(diff); let applied: string; if (format === "search_replace") { let _applied: string | null = null; try { _applied = applySearchReplace(originalContent, diff); } catch { /* handled below */ } if (_applied === null) { return _fail("apply", "SEARCH block non trovato nel file — diff non applicabile", preValidation, null, originalContent, false); } applied = _applied; } else { // Unified diff: apply delegato al backend; qui eseguiamo solo pre-validazione // Restituiamo success=true con il contenuto originale non toccato // (il chiamante userà il backend per apply effettivo) return { success: true, result: originalContent, // NON patchato — apply via backend rolledBack: false, preValidation, postValidation: null, stage: "ok", message: "Unified diff: pre-validazione OK — apply delegato al backend", }; } // ── Fase 3: Post-apply validation ───────────────────────────────────────── let postValidation: PostApplyValidation; try { postValidation = validatePostApply(originalContent, applied, filePath); } catch { // Non-blocking: se post-validation crasha, accetta il patch con warning return { success: true, result: applied, rolledBack: false, preValidation, postValidation: null, stage: "ok", message: "⚠️ Post-validation skipped (errore interno) — patch applicato comunque", }; } if (!postValidation.syntaxOk && postValidation.syntaxError) { return _fail( "post", `Post-apply syntax error: ${postValidation.syntaxError}`, preValidation, postValidation, originalContent, true, // rollback: non usiamo applied ); } // ── Fase 4: Build check (opzionale) ────────────────────────────────────── if (runBuildCheck) { let buildOk = false; try { buildOk = await runBuildCheck(applied); } catch { /* non-blocking — skip check */ } if (!buildOk) { return _fail( "build", "Build/typecheck fallito dopo apply — rollback applicato automaticamente", preValidation, postValidation, originalContent, true, ); } } // ── Successo ────────────────────────────────────────────────────────────── const delta = postValidation.lengthDelta; return { success: true, result: applied, rolledBack: false, preValidation, postValidation, stage: "ok", message: `Diff applicato: ${delta >= 0 ? "+" : ""}${delta} chars${postValidation.warnings.length > 0 ? " ⚠️ " + postValidation.warnings[0] : ""}`, }; } // ─── Helper ─────────────────────────────────────────────────────────────────── function _fail( stage: PipelineStage, message: string, preValidation: DiffValidationResult | null, postValidation: PostApplyValidation | null, originalContent: string, rolledBack = false, ): DiffPipelineResult { return { success: false, result: originalContent, rolledBack, preValidation, postValidation, stage, message }; }