import type { RoutesManifest } from "../spec/schema"; import type { GuardViolation } from "./rules"; import { GUARD_RULES } from "./rules"; import { runGuardCheck } from "./check"; import { generateRoutes } from "../generator/generate"; import { beginChange, commitChange, rollbackChange } from "../change"; export interface AutoCorrectStep { ruleId: string; action: string; success: boolean; message: string; } export interface AutoCorrectResult { fixed: boolean; steps: AutoCorrectStep[]; remainingViolations: GuardViolation[]; retriedCount: number; /** 롤백이 발생했는지 여부 */ rolledBack?: boolean; /** 관련 트랜잭션 ID */ changeId?: string; } // 자동 수정 가능한 규칙들 const AUTO_CORRECTABLE_RULES = new Set([ GUARD_RULES.GENERATED_MANUAL_EDIT.id, GUARD_RULES.SLOT_NOT_FOUND.id, ]); export function isAutoCorrectableViolation(violation: GuardViolation): boolean { return AUTO_CORRECTABLE_RULES.has(violation.ruleId); } export async function runAutoCorrect( violations: GuardViolation[], manifest: RoutesManifest, rootDir: string, maxRetries: number = 3 ): Promise { const steps: AutoCorrectStep[] = []; let currentViolations = violations; let retriedCount = 0; // 1. 수정 전 스냅샷 생성 (트랜잭션 시작) let change; try { change = await beginChange(rootDir, { message: `Auto-correct: ${violations.length} violations`, autoGenerated: true, }); } catch { // 이미 활성 트랜잭션이 있는 경우 그대로 진행 (사용자가 수동으로 시작한 경우) // 다른 오류는 트랜잭션 없이 진행 } try { while (retriedCount < maxRetries) { const autoCorrectableViolations = currentViolations.filter(isAutoCorrectableViolation); if (autoCorrectableViolations.length === 0) { break; } // 각 위반에 대해 수정 시도 let anyFixed = false; for (const violation of autoCorrectableViolations) { const step = await correctViolation(violation, manifest, rootDir); steps.push(step); if (step.success) { anyFixed = true; } } if (!anyFixed) { // 아무것도 수정하지 못했으면 루프 종료 break; } // Guard 재검사 retriedCount++; const recheckResult = await runGuardCheck(manifest, rootDir); currentViolations = recheckResult.violations; if (recheckResult.passed) { // 2. 성공 시 커밋 if (change) { await commitChange(rootDir, change.id); } return { fixed: true, steps, remainingViolations: [], retriedCount, changeId: change?.id, }; } } // 3. 실패 시 (maxRetries 도달 또는 수정 불가) const fixed = currentViolations.length === 0; if (fixed && change) { // 모든 위반이 해결된 경우 커밋 await commitChange(rootDir, change.id); } else if (!fixed && change) { // 실패 시 롤백 await rollbackChange(rootDir, change.id); return { fixed: false, steps, remainingViolations: currentViolations, retriedCount, rolledBack: true, changeId: change.id, }; } return { fixed, steps, remainingViolations: currentViolations, retriedCount, changeId: change?.id, }; } catch (error) { // 4. 오류 발생 시 롤백 if (change) { try { await rollbackChange(rootDir, change.id); } catch { // 롤백 실패는 무시 (원본 오류가 더 중요) } } throw error; } } async function correctViolation( violation: GuardViolation, manifest: RoutesManifest, rootDir: string ): Promise { switch (violation.ruleId) { case GUARD_RULES.GENERATED_MANUAL_EDIT.id: return await correctGeneratedManualEdit(manifest, rootDir); case GUARD_RULES.SLOT_NOT_FOUND.id: return await correctSlotNotFound(manifest, rootDir); default: return { ruleId: violation.ruleId, action: "skip", success: false, message: `자동 수정 불가능한 규칙: ${violation.ruleId}`, }; } } async function correctGeneratedManualEdit( manifest: RoutesManifest, rootDir: string ): Promise { try { const result = await generateRoutes(manifest, rootDir); if (result.success) { return { ruleId: GUARD_RULES.GENERATED_MANUAL_EDIT.id, action: "generate", success: true, message: `코드 재생성 완료 (${result.created.length}개 파일)`, }; } else { return { ruleId: GUARD_RULES.GENERATED_MANUAL_EDIT.id, action: "generate", success: false, message: `코드 재생성 실패: ${result.errors.join(", ")}`, }; } } catch (error) { return { ruleId: GUARD_RULES.GENERATED_MANUAL_EDIT.id, action: "generate", success: false, message: `코드 재생성 실패: ${error instanceof Error ? error.message : String(error)}`, }; } } async function correctSlotNotFound( manifest: RoutesManifest, rootDir: string ): Promise { try { const result = await generateRoutes(manifest, rootDir); if (result.success) { const slotCount = result.created.filter((f) => f.includes("slots")).length; return { ruleId: GUARD_RULES.SLOT_NOT_FOUND.id, action: "generate-slot", success: true, message: `Slot 파일 생성 완료 (${slotCount}개 파일)`, }; } else { return { ruleId: GUARD_RULES.SLOT_NOT_FOUND.id, action: "generate-slot", success: false, message: `Slot 파일 생성 실패: ${result.errors.join(", ")}`, }; } } catch (error) { return { ruleId: GUARD_RULES.SLOT_NOT_FOUND.id, action: "generate-slot", success: false, message: `Slot 파일 생성 실패: ${error instanceof Error ? error.message : String(error)}`, }; } }