AUDIT / src /lib /agent /diffPipeline.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
7.46 kB
/**
* 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<boolean>;
}
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<DiffPipelineResult> {
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 };
}