/** * src/core/errors/index.ts — Error taxonomy professionale * * Ogni errore AI/network ha un codice, un messaggio user-facing, * e una severity. Usare sempre questi invece di string generici. * * Checklist 1.3 — error taxonomy */ // ─── Error codes ──────────────────────────────────────────────────────────── export enum ErrorCode { NETWORK_ERROR = "NETWORK_ERROR", RATE_LIMIT = "RATE_LIMIT", INVALID_KEY = "INVALID_KEY", MODEL_UNAVAILABLE = "MODEL_UNAVAILABLE", EXECUTION_FAILED = "EXECUTION_FAILED", MEMORY_ERROR = "MEMORY_ERROR", OFFLINE_MODE = "OFFLINE_MODE", TIMEOUT = "TIMEOUT", QUOTA_EXCEEDED = "QUOTA_EXCEEDED", PROVIDER_SWITCHING = "PROVIDER_SWITCHING", UNKNOWN_ERROR = "UNKNOWN_ERROR", } export type ErrorSeverity = "info" | "warn" | "error" | "fatal"; // ─── Structured app error ─────────────────────────────────────────────────── export interface AppError { code: ErrorCode; message: string; // user-facing (breve) detail?: string; // tecnico / debug provider?: string; // es. "Groq", "Gemini" severity: ErrorSeverity; retryable: boolean; timestamp: number; } // ─── Factory ───────────────────────────────────────────────────────────────── export function createError( code: ErrorCode, opts: Partial> = {}, ): AppError { return { code, message: opts.message ?? ERROR_MESSAGES[code], detail: opts.detail, provider: opts.provider, severity: opts.severity ?? ERROR_SEVERITY[code], retryable: opts.retryable ?? ERROR_RETRYABLE[code], timestamp: Date.now(), }; } // ─── Default user-facing messages ──────────────────────────────────────────── const ERROR_MESSAGES: Record = { [ErrorCode.NETWORK_ERROR]: "Errore di rete — controlla la connessione", [ErrorCode.RATE_LIMIT]: "Limite richieste raggiunto — ritento automaticamente", [ErrorCode.INVALID_KEY]: "Chiave API non valida — verifica nelle Impostazioni", [ErrorCode.MODEL_UNAVAILABLE]: "Modello non disponibile — passo al prossimo", [ErrorCode.EXECUTION_FAILED]: "Esecuzione fallita", [ErrorCode.MEMORY_ERROR]: "Errore di memoria — prova a liberare spazio", [ErrorCode.OFFLINE_MODE]: "Modalità offline — funzionalità limitate", [ErrorCode.TIMEOUT]: "Timeout — la risposta ha impiegato troppo", [ErrorCode.QUOTA_EXCEEDED]: "Quota esaurita — cambio provider", [ErrorCode.PROVIDER_SWITCHING]: "Provider non disponibile — cambio in corso", [ErrorCode.UNKNOWN_ERROR]: "Errore sconosciuto", }; // ─── Default severity ──────────────────────────────────────────────────────── const ERROR_SEVERITY: Record = { [ErrorCode.NETWORK_ERROR]: "warn", [ErrorCode.RATE_LIMIT]: "warn", [ErrorCode.INVALID_KEY]: "error", [ErrorCode.MODEL_UNAVAILABLE]: "warn", [ErrorCode.EXECUTION_FAILED]: "error", [ErrorCode.MEMORY_ERROR]: "error", [ErrorCode.OFFLINE_MODE]: "info", [ErrorCode.TIMEOUT]: "warn", [ErrorCode.QUOTA_EXCEEDED]: "warn", [ErrorCode.PROVIDER_SWITCHING]: "info", [ErrorCode.UNKNOWN_ERROR]: "error", }; // ─── Default retryable ─────────────────────────────────────────────────────── const ERROR_RETRYABLE: Record = { [ErrorCode.NETWORK_ERROR]: true, [ErrorCode.RATE_LIMIT]: true, [ErrorCode.INVALID_KEY]: false, [ErrorCode.MODEL_UNAVAILABLE]: true, [ErrorCode.EXECUTION_FAILED]: false, [ErrorCode.MEMORY_ERROR]: false, [ErrorCode.OFFLINE_MODE]: false, [ErrorCode.TIMEOUT]: true, [ErrorCode.QUOTA_EXCEEDED]: true, [ErrorCode.PROVIDER_SWITCHING]: true, [ErrorCode.UNKNOWN_ERROR]: false, }; // ─── Classifier: HTTP status + body → ErrorCode ────────────────────────────── export function classifyHttpError(status: number, body = ""): ErrorCode { const b = body.toLowerCase(); if (status === 401 || status === 403) return ErrorCode.INVALID_KEY; if (status === 429 || status === 402) return ErrorCode.RATE_LIMIT; if (status === 404) return ErrorCode.MODEL_UNAVAILABLE; if (status === 408) return ErrorCode.TIMEOUT; if (b.includes("quota") || b.includes("exceeded")) return ErrorCode.QUOTA_EXCEEDED; if (b.includes("rate limit")) return ErrorCode.RATE_LIMIT; if (b.includes("model not found")) return ErrorCode.MODEL_UNAVAILABLE; if (status >= 500) return ErrorCode.NETWORK_ERROR; return ErrorCode.UNKNOWN_ERROR; } // ─── Classifier: catch(err) → ErrorCode ───────────────────────────────────── export function classifyJsError(err: unknown): ErrorCode { if (!err) return ErrorCode.UNKNOWN_ERROR; const e = err as { name?: string; message?: string; status?: number; body?: string }; if (e.name === "AbortError") return ErrorCode.TIMEOUT; if (e.message?.includes("completion-timeout")) return ErrorCode.TIMEOUT; if (e.message?.toLowerCase().includes("network")) return ErrorCode.NETWORK_ERROR; if (e.message?.toLowerCase().includes("offline")) return ErrorCode.OFFLINE_MODE; if (e.status && e.body !== undefined) return classifyHttpError(e.status, e.body); return ErrorCode.UNKNOWN_ERROR; } // ─── Format for display ────────────────────────────────────────────────────── export function formatErrorBanner(err: AppError): string { const prefix = err.provider ? `${err.provider}: ` : ""; return `${prefix}${err.message}`; }