/** * Centralized Error Handling * Standardizes error handling across the codebase */ import pino from 'pino' const logger = pino({ level: process.env.LOG_LEVEL || 'info' }) /** * Error severity levels */ export type ErrorSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info' /** * Standardized error structure */ export interface PRIXError { message: string severity: ErrorSeverity code: string context?: Record cause?: Error } /** * Error codes for different failure modes */ export const ErrorCodes = { // AI/LLM errors AI_RATE_LIMIT: 'AI_RATE_LIMIT', AI_CONTEXT_TOO_LARGE: 'AI_CONTEXT_TOO_LARGE', AI_TIMEOUT: 'AI_TIMEOUT', AI_AUTHENTICATION: 'AI_AUTHENTICATION', AI_SERVICE_UNAVAILABLE: 'AI_SERVICE_UNAVAILABLE', // GitHub API errors GITHUB_RATE_LIMIT: 'GITHUB_RATE_LIMIT', GITHUB_AUTHENTICATION: 'GITHUB_AUTHENTICATION', GITHUB_NOT_FOUND: 'GITHUB_NOT_FOUND', GITHUB_CONFLICT: 'GITHUB_CONFLICT', GITHUB_TIMEOUT: 'GITHUB_TIMEOUT', // File system errors FS_READ_ERROR: 'FS_READ_ERROR', FS_WRITE_ERROR: 'FS_WRITE_ERROR', FS_NOT_FOUND: 'FS_NOT_FOUND', // Parse/analysis errors PARSE_ERROR: 'PARSE_ERROR', AST_ANALYSIS_ERROR: 'AST_ANALYSIS_ERROR', PATCH_PARSE_ERROR: 'PATCH_PARSE_ERROR', // Context errors CONTEXT_MISSING: 'CONTEXT_MISSING', INVALID_PAYLOAD: 'INVALID_PAYLOAD', // Circuit breaker CIRCUIT_OPEN: 'CIRCUIT_OPEN', // Unknown UNKNOWN: 'UNKNOWN' } as const /** * Create a standardized error */ export const createError = ( message: string, severity: ErrorSeverity, code: string, context?: Record, cause?: Error ): PRIXError => ({ message, severity, code, context, cause }) /** * Log error with appropriate level based on severity */ export const logError = (error: PRIXError): void => { const logData = { code: error.code, severity: error.severity, context: error.context, cause: error.cause?.message } switch (error.severity) { case 'critical': logger.fatal(logData, error.message) break case 'high': logger.error(logData, error.message) break case 'medium': logger.warn(logData, error.message) break case 'low': logger.info(logData, error.message) break case 'info': logger.debug(logData, error.message) break } } /** * Safe wrapper for async functions * Standardizes error handling */ export const safeExecute = async ( fn: () => Promise, errorCode: string, severity: ErrorSeverity = 'medium', context?: Record, fallback?: T ): Promise => { try { return await fn() } catch (error) { const prixError = createError( error instanceof Error ? error.message : String(error), severity, errorCode, context, error instanceof Error ? error : undefined ) logError(prixError) return fallback } } /** * Safe wrapper for sync functions */ export const safeExecuteSync = ( fn: () => T, errorCode: string, severity: ErrorSeverity = 'medium', context?: Record, fallback?: T ): T | undefined => { try { return fn() } catch (error) { const prixError = createError( error instanceof Error ? error.message : String(error), severity, errorCode, context, error instanceof Error ? error : undefined ) logError(prixError) return fallback } } /** * Classify error from external service */ export const classifyError = (error: any): PRIXError => { // AI API errors if (error?.status === 429) { return createError('AI rate limit exceeded', 'high', ErrorCodes.AI_RATE_LIMIT, { status: 429 }) } if (error?.status === 413) { return createError('AI context too large', 'high', ErrorCodes.AI_CONTEXT_TOO_LARGE, { status: 413 }) } if (error?.code === 'ETIMEDOUT' || error?.code === 'ECONNABORTED') { return createError('AI timeout', 'medium', ErrorCodes.AI_TIMEOUT, { code: error.code }) } if (error?.status === 401) { return createError('AI authentication failed', 'critical', ErrorCodes.AI_AUTHENTICATION, { status: 401 }) } if (error?.status === 503 || error?.status === 502) { return createError('AI service unavailable', 'high', ErrorCodes.AI_SERVICE_UNAVAILABLE, { status: error.status }) } // GitHub API errors if (error?.status === 403 && error?.message?.includes('rate limit')) { return createError('GitHub rate limit exceeded', 'high', ErrorCodes.GITHUB_RATE_LIMIT, { status: 403 }) } if (error?.status === 401) { return createError('GitHub authentication failed', 'critical', ErrorCodes.GITHUB_AUTHENTICATION, { status: 401 }) } if (error?.status === 404) { return createError('GitHub resource not found', 'medium', ErrorCodes.GITHUB_NOT_FOUND, { status: 404 }) } if (error?.status === 409) { return createError('GitHub conflict', 'medium', ErrorCodes.GITHUB_CONFLICT, { status: 409 }) } // Default return createError( error?.message || 'Unknown error', 'medium', ErrorCodes.UNKNOWN, { originalError: error } ) } /** * Retry configuration */ export interface RetryConfig { maxRetries: number baseDelay: number maxDelay: number retryableCodes: string[] } /** * Default retry configuration */ export const defaultRetryConfig: RetryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 30000, retryableCodes: [ ErrorCodes.AI_RATE_LIMIT, ErrorCodes.AI_TIMEOUT, ErrorCodes.AI_SERVICE_UNAVAILABLE, ErrorCodes.GITHUB_RATE_LIMIT, ErrorCodes.GITHUB_TIMEOUT ] } /** * Check if error should be retried */ export const shouldRetry = (error: PRIXError, config: RetryConfig = defaultRetryConfig): boolean => { return config.retryableCodes.includes(error.code) } /** * Calculate delay for retry with exponential backoff */ export const calculateRetryDelay = (attempt: number, config: RetryConfig = defaultRetryConfig): number => { const delay = Math.min( config.baseDelay * Math.pow(2, attempt), config.maxDelay ) // Add jitter to prevent thundering herd return delay + Math.random() * 1000 }