/** * Config Guard - 설정 무결성 검증 * * Lockfile을 사용한 설정 무결성 검증을 Guard 시스템에 통합 * * @see docs/plans/09_lockfile_integration_plan.md */ import { readLockfile, readMcpConfig, validateLockfile, validateWithPolicy, detectMode, type LockfileMode, } from "../lockfile"; import type { ConfigDiff } from "../utils/differ"; // ============================================ // 타입 // ============================================ export interface ConfigGuardError { code: string; message: string; details?: Record; } export interface ConfigGuardWarning { code: string; message: string; details?: Record; } export interface ConfigGuardResult { /** 설정 로드 성공 여부 */ configValid: boolean; /** lockfile 검증 통과 여부 */ lockfileValid: boolean; /** lockfile 존재 여부 */ lockfileExists: boolean; /** 심각한 오류 */ errors: ConfigGuardError[]; /** 경고 */ warnings: ConfigGuardWarning[]; /** 설정 변경 사항 */ diff?: ConfigDiff; /** 현재 해시 */ currentHash?: string; /** lockfile 해시 */ lockedHash?: string; /** 정책 액션 */ action: "pass" | "warn" | "error" | "block"; /** 우회 여부 */ bypassed: boolean; } export interface ConfigGuardOptions { /** 검증 모드 (환경 자동 감지가 기본) */ mode?: LockfileMode; } // ============================================ // 메인 함수 // ============================================ /** * 설정 무결성 검증 (Guard 통합용) * * @param rootDir 프로젝트 루트 디렉토리 * @param config 현재 설정 객체 * @param options 검증 옵션 * @returns 검증 결과 * * @example * ```typescript * const result = await guardConfig(rootDir, config); * if (!result.lockfileValid) { * console.error("설정 무결성 검증 실패"); * } * ``` */ export async function guardConfig( rootDir: string, config: Record, options: ConfigGuardOptions = {} ): Promise { const errors: ConfigGuardError[] = []; const warnings: ConfigGuardWarning[] = []; // 1. Lockfile 읽기 const lockfile = await readLockfile(rootDir); const lockfileExists = lockfile !== null; // 1-1. MCP 설정 읽기 (선택) let mcpConfig: Record | null = null; try { mcpConfig = await readMcpConfig(rootDir); } catch (error) { warnings.push({ code: "MCP_CONFIG_PARSE_ERROR", message: `MCP 설정 로드 실패: ${error instanceof Error ? error.message : String(error)}`, }); } // 2. 정책 기반 검증 const mode = options.mode ?? detectMode(); const { result, action, bypassed } = validateWithPolicy(config, lockfile, mode, mcpConfig); // 3. 결과 처리 if (!lockfileExists) { warnings.push({ code: "LOCKFILE_NOT_FOUND", message: "Lockfile이 존재하지 않습니다. 'mandu lock'으로 생성하세요.", }); } if (result) { // 오류 변환 for (const error of result.errors) { errors.push({ code: error.code, message: error.message, details: error.details, }); } // 경고 변환 for (const warning of result.warnings) { warnings.push({ code: warning.code, message: warning.message, details: warning.details, }); } } return { configValid: true, lockfileValid: result?.valid ?? false, lockfileExists, errors, warnings, diff: result?.diff, currentHash: result?.currentHash, lockedHash: result?.lockedHash, action, bypassed, }; } /** * 빠른 무결성 검증 (해시만 비교) */ export async function quickConfigGuard( rootDir: string, config: Record ): Promise { const lockfile = await readLockfile(rootDir); if (!lockfile) return true; // lockfile 없으면 통과 let mcpConfig: Record | null = null; try { mcpConfig = await readMcpConfig(rootDir); } catch { // ignore } const result = validateLockfile(config, lockfile, mcpConfig); return result.valid; } // ============================================ // 포맷팅 // ============================================ /** * Config Guard 결과를 콘솔 메시지로 변환 */ export function formatConfigGuardResult(result: ConfigGuardResult): string { const lines: string[] = []; if (result.lockfileValid) { lines.push("✅ 설정 무결성 확인됨"); if (result.currentHash) { lines.push(` 해시: ${result.currentHash}`); } } else if (!result.lockfileExists) { lines.push("💡 Lockfile 없음"); lines.push(" 'mandu lock'으로 생성 권장"); } else { lines.push("❌ 설정 무결성 검증 실패"); for (const error of result.errors) { lines.push(` 🔴 ${error.message}`); } lines.push(""); lines.push(" 다음 단계:"); lines.push(" ↳ 변경 확인: mandu lock --diff"); lines.push(" ↳ 의도한 변경이면: mandu lock"); lines.push(" ↳ 패키지 업데이트 직후라면 새 Mandu 기본값으로 lockfile을 갱신하세요."); } if (result.warnings.length > 0 && result.lockfileExists) { lines.push(""); lines.push(" 경고:"); for (const warning of result.warnings) { lines.push(` ⚠️ ${warning.message}`); } } if (result.bypassed) { lines.push(""); lines.push(" ⚡ MANDU_LOCK_BYPASS=1로 우회됨"); } return lines.join("\n"); } /** * Config Guard 결과를 JSON으로 변환 (에이전트용) */ export function formatConfigGuardAsJSON(result: ConfigGuardResult): string { return JSON.stringify( { ok: result.lockfileValid, lockfileExists: result.lockfileExists, action: result.action, bypassed: result.bypassed, currentHash: result.currentHash, lockedHash: result.lockedHash, errors: result.errors, warnings: result.warnings, hasDiff: result.diff?.hasChanges ?? false, }, null, 2 ); } // ============================================ // 통합 헬스 체크 // ============================================ export interface UnifiedHealthResult { /** 전체 통과 여부 */ ok: boolean; /** 건강 점수 (0-100) */ healthScore: number; /** 아키텍처 검증 */ architecture: { violations: number; errors: number; warnings: number; }; /** 설정 검증 */ config: ConfigGuardResult; } /** * 통합 헬스 점수 계산 */ export function calculateHealthScore( archViolations: number, archErrors: number, configResult: ConfigGuardResult ): number { let score = 100; // 아키텍처 위반 감점 score -= archErrors * 10; score -= (archViolations - archErrors) * 2; // 설정 무결성 감점 if (!configResult.lockfileExists) { score -= 5; // lockfile 없음 } else if (!configResult.lockfileValid) { score -= 20; // 불일치 } // 경고 감점 score -= configResult.warnings.length * 1; return Math.max(0, Math.min(100, score)); }