/** * adaptiveSolver.ts — Adaptive Problem Solver (S50) * * Pattern: quando un'azione fallisce per un BLOCCO SPECIFICO, * non si ripete ne si chiede aiuto. Si IDENTIFICA il blocco * e si ESEGUE un'alternativa nota automaticamente. * * Esempio reale che ispira questa feature: * Operazione push bloccata → rilevato blocco "permission" → * strategia "permission_rest" → usa API REST invece → successo. * * Strategie built-in (7): * 1. cors_proxy — fetch CORS bloccato → 3 proxy in ordine * 2. file_fuzzy — file not found → ricerca fuzzy VFS * 3. rate_limit_wait — 429 → backoff esponenziale + retry * 4. chunk_payload — payload troppo grande → suggerimento chunking * 5. permission_rest — operazione negata → equivalente REST API * 6. network_retry — errore rete transitorio → retry timeout aumentato * 7. syntax_hint — errore sintattico → analisi riga + hint preciso * * Invarianti: * - Safari-safe: no SharedArrayBuffer, no Worker * - DI via _setTestDeps() per unit test senza vi.mock * - Max 2 tentativi per blocco (no loop infiniti) * - Ogni strategia e IDEMPOTENTE: fallire non peggiora lo stato * - Emette sempre un messaggio leggibile su cosa sta facendo */ import { vfsAsync } from "@/lib/vfsDb"; import { agentMemory } from "@/lib/agentMemory"; // ─── Tipi pubblici (estratti in adaptiveSolverTypes.ts — S552 — re-esportati per backward compat) ── import type { BlockadeKind, Blockade, SolveResult } from "./adaptiveSolverTypes"; export type { BlockadeKind, Blockade, SolveResult } from "./adaptiveSolverTypes"; interface Strategy { id: string; label: string; handles: BlockadeKind[]; tryFix( blockade: Blockade, originalInput: string, context: string, ): Promise; } // ─── DI per test ───────────────────────────────────────────────────────────── interface SolverDeps { vfsListFn: () => Promise<{ path: string; content: string }[]>; memorySetFn: (key: string, val: string, cat: string) => void; /** Gap-5: lettura last_winner e win-count dal ring buffer di agentMemory. * Separato da memorySetFn per DI pulita nei test — il reader può essere mockato * indipendentemente dallo writer (pattern read-write separation). */ memoryGetFn: (key: string) => string | undefined; fetchFn: (url: string, opts?: RequestInit) => Promise; waitFn: (ms: number) => Promise; } let _deps: SolverDeps = { vfsListFn: () => vfsAsync.list() as Promise<{ path: string; content: string }[]>, memorySetFn: (key, val, cat) => agentMemory.set(key, val, cat), memoryGetFn: (key) => agentMemory.get(key) ?? undefined, // Fix-5a: agentMemory.get()→string|null; interfaccia dichiara string|undefined → null coerced fetchFn: (url, opts) => fetch(url, opts), waitFn: (ms) => new Promise(res => setTimeout(res, ms)), }; /** Dependency injection per unit test */ export function _setTestDeps(deps: Partial): void { _deps = { ..._deps, ...deps }; } // ─── Rilevamento blocco ─────────────────────────────────────────────────────── const BLOCKADE_PATTERNS: Array<{ re: RegExp; kind: BlockadeKind; hint: string }> = [ { re: /cors|cross.?origin|blocked by cors|no.*access-control/i, kind: "cors", hint: "Richiesta bloccata da policy CORS" }, { re: /404|not found|no such file|cannot find|enoent|file non trovato/i, kind: "not_found", hint: "Risorsa non trovata — path errato o file mancante" }, { re: /429|rate limit|too many requests/i, kind: "rate_limit", hint: "Rate limit raggiunto" }, { re: /403|401|access denied|permission denied|not allowed|unauthorized/i, kind: "permission", hint: "Operazione non consentita" }, { re: /timeout|timed out|aborted|etimedout/i, kind: "timeout", hint: "Timeout — operazione troppo lenta" }, { re: /quota|storage full|no space|exceeded.*storage/i, kind: "quota", hint: "Storage quota superata" }, { re: /syntaxerror|unexpected token|unterminated|parse error/i, kind: "syntax", hint: "Errore di sintassi" }, { re: /network error|failed to fetch|net::err|econnrefused/i, kind: "network", hint: "Errore di rete" }, // Fix-1.1: import_error sblocca dependency_probe (riga ~332) — era mai attivata { re: /ModuleNotFoundError|No module named|ImportError|Cannot find module|Module not found/i, kind: "import_error", hint: "Libreria mancante — necessita installazione" }, ]; export function detectBlockade(errorMessage: string): Blockade { for (const { re, kind, hint } of BLOCKADE_PATTERNS) { if (re.test(errorMessage)) { return { kind, message: errorMessage.slice(0, 300), hint }; } } return { kind: "unknown", message: errorMessage.slice(0, 300), hint: "Errore non riconosciuto" }; } // ─── Helper: Proxy CORS ─────────────────────────────────────────────────────── // S320-FIX: corsproxy.io rimosso (403 pagamento obbligatorio — test 2026-06-01) const CORS_PROXIES: Array<(u: string) => string> = [ u => "https://api.allorigins.win/get?url=" + encodeURIComponent(u), u => "https://api.codetabs.com/v1/proxy?quest=" + encodeURIComponent(u), u => "https://api.allorigins.win/raw?url=" + encodeURIComponent(u), ]; async function viaProxy(url: string, timeoutMs = 8000): Promise { for (const make of CORS_PROXIES) { try { const ctrl = new AbortController(); const tid = setTimeout(() => ctrl.abort(), timeoutMs); const res = await _deps.fetchFn(make(url), { signal: ctrl.signal }); clearTimeout(tid); if (!res.ok) continue; const raw = await res.text(); if (raw.startsWith('{"contents"')) { const d = JSON.parse(raw) as { contents?: string }; if ((d.contents?.length ?? 0) > 20) return d.contents!; } else if (raw.length > 20) { return raw; } } catch { continue; } } throw new Error("Tutti i proxy CORS falliti"); } // ─── Helper: fuzzy match VFS ────────────────────────────────────────────────── async function fuzzyFindPath(target: string): Promise { const files = await _deps.vfsListFn(); if (!files.length) return null; const t = target.toLowerCase().replace(/^\/+/, ""); const exact = files.find(f => f.path === target || f.path === "/" + t); if (exact) return exact.path; const fname = t.split("/").pop() ?? t; const byName = files.find(f => f.path.toLowerCase().endsWith("/" + fname)); if (byName) return byName.path; const bySub = files.find(f => f.path.toLowerCase().includes(fname)); return bySub?.path ?? null; } // ─── Strategie ──────────────────────────────────────────────────────────────── const STRATEGIES: Strategy[] = [ // 1. CORS → proxy automatico { id: "cors_proxy", label: "Proxy CORS automatico", handles: ["cors", "network"], async tryFix(_blockade, originalInput) { const m = originalInput.match(/https?:\/\/[^\s"'`\)]+/); if (!m) return null; const url = m[0].replace(/[,;)]+$/, ""); try { const content = await viaProxy(url); return "✅ [proxy CORS] " + url + "\n" + content.slice(0, 3000); } catch { return null; } }, }, // 2. Not found → fuzzy search VFS { id: "file_fuzzy", label: "Ricerca fuzzy file nel workspace", handles: ["not_found"], async tryFix(_blockade, originalInput) { const m = originalInput.match(/['"]([/a-zA-Z0-9_\-\.]+\.[a-zA-Z]{1,5})['"]/); if (!m) return null; const searchPath = m[1]; const found = await fuzzyFindPath(searchPath); if (!found) { const list = (await _deps.vfsListFn()).slice(0, 10).map(f => f.path).join(", "); return "❌ File \"" + searchPath + "\" non trovato.\nFile disponibili nel workspace: " + (list || "nessuno"); } const file = (await _deps.vfsListFn()).find(f => f.path === found); const content = (file?.content ?? "").slice(0, 3000); return "✅ [file trovato] " + found + "\n" + content; }, }, // 3. Rate limit → backoff esponenziale { id: "rate_limit_wait", label: "Backoff esponenziale e retry", handles: ["rate_limit"], async tryFix(_blockade, originalInput) { const m = originalInput.match(/https?:\/\/[^\s"'`\)]+/); if (!m) return null; const url = m[0].replace(/[,;)]+$/, ""); // S324: cap a 2s iniziale (era 2s), abort dopo 4s — evita watchdog kill su iOS Safari // (JS fermo >5s può essere terminato dal watchdog; 2+2=4s totale di attesa massima) await _deps.waitFn(2000); try { const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 4000); // S324: 10000→4000ms, Safari watchdog safe const res = await _deps.fetchFn(url, { signal: ctrl.signal }); if (res.status === 429) { await _deps.waitFn(2000); return null; } // S324: 5000→2000ms if (!res.ok) return null; return "✅ [retry dopo backoff] " + url + "\n" + (await res.text()).slice(0, 2000); } catch { return null; } }, }, // 4. Quota/timeout → suggerimento chunking { id: "chunk_payload", label: "Suddivisione in chunk", handles: ["quota", "timeout"], async tryFix(_blockade, originalInput) { if (originalInput.length < 5000) return null; const CHUNK = 3000; const parts = Math.ceil(originalInput.length / CHUNK); return ( "⚠️ Input troppo grande (" + originalInput.length + " chars) — strategia chunk.\n" + "Dividi in " + parts + " chunk da " + CHUNK + " chars ciascuno.\n" + "Chunk 1:\n" + originalInput.slice(0, CHUNK) ); }, }, // 5. Permission negata → alternativa REST API { id: "permission_rest", label: "Alternativa via REST API", handles: ["permission"], async tryFix(blockade, originalInput, context) { const lc = (originalInput + " " + context).toLowerCase(); if (lc.includes("push") || lc.includes("commit")) { return [ "⚠️ Operazione bloccata. Alternativa: usa API REST.", "Per operazioni su repository:", " 1. POST /repos/OWNER/REPO/git/blobs → carica contenuto file", " 2. POST /repos/OWNER/REPO/git/trees → crea albero con i blob", " 3. POST /repos/OWNER/REPO/git/commits → crea commit", " 4. PATCH /repos/OWNER/REPO/git/refs/heads/main → aggiorna branch", "Usa fetch con Authorization: token TOKEN_VALUE", ].join("\n"); } if (lc.includes("localstorage") || lc.includes("storage")) { return "⚠️ Storage bloccato. Alternativa: IndexedDB via Dexie (vfsDb.ts) o sessionStorage."; } if (lc.includes("clipboard")) { return "⚠️ Clipboard API bloccata. Alternativa: mostra testo in