Spaces:
Sleeping
Sleeping
| /** | |
| * buildValidationLoop.ts — S407 Tier 3: Build/Test/Fix Loop | |
| * | |
| * Problema risolto: l'agente si fermava dopo "Generate → Partial Validation → Done". | |
| * Il ciclo ideale è: Generate → Build → Run → Test → Fix → Retest → Green. | |
| * | |
| * Tool esposto: validate_project | |
| * Esegue in sequenza: | |
| * 1. Struttura VFS — file critici presenti? (package.json, entry point, ecc.) | |
| * 2. Dipendenze — package.json valido? deps compatibili? | |
| * 3. Test strutturali — lint sintattico sui file più grandi | |
| * 4. Goal acceptance tests — requisiti utente soddisfatti? | |
| * 5. Deploy status — se c'è un progetto Vercel/CF, è live? | |
| * | |
| * Output: report testuale con pass/fail + hint "COSA FIXARE" per il prossimo iter. | |
| * | |
| * Integrazione: | |
| * - networkTools.ts tryExecute: include "validate_project" | |
| * - toolDefinitions.ts: aggiunge il tool alla lista AGENT_TOOLS | |
| * - agentLoop.ts: auto-trigger validate_project prima di DONE su task con VFS | |
| */ | |
| import { vfsAsync } from "@/lib/vfsDb"; | |
| import { runAcceptanceTests, formatAcceptanceReport } from "@/lib/goalTestDeriver"; | |
| // ─── Configurazione ─────────────────────────────────────────────────────────── | |
| const CRITICAL_FILES_BY_STACK: Record<string, string[]> = { | |
| react: ["package.json", "src/main.tsx", "src/App.tsx", "index.html"], | |
| next: ["package.json", "next.config.js"], | |
| vite: ["package.json", "vite.config.ts", "index.html"], | |
| express: ["package.json", "src/index.ts"], | |
| fastapi: ["requirements.txt", "main.py"], | |
| flask: ["requirements.txt", "app.py"], | |
| django: ["requirements.txt", "manage.py", "settings.py"], | |
| nestjs: ["package.json", "src/main.ts", "src/app.module.ts"], | |
| nuxt: ["package.json", "nuxt.config.ts"], | |
| svelte: ["package.json", "src/App.svelte"], | |
| remix: ["package.json", "app/root.tsx", "app/entry.client.tsx"], | |
| astro: ["package.json", "astro.config.mjs", "src/pages/index.astro"], | |
| deno: ["deno.json", "main.ts"], | |
| rails: ["Gemfile", "config/routes.rb", "app/controllers/application_controller.rb"], | |
| laravel: ["composer.json", "routes/web.php", "app/Http/Controllers/Controller.php"], | |
| generic: ["package.json"], | |
| }; | |
| const KNOWN_BROKEN_DEPS: Array<{ name: string; reason: string; minVersion?: string }> = [ | |
| // Build tool obsolescenza | |
| { name: "react-scripts", reason: "deprecated nel 2024 — usa Vite" }, | |
| { name: "create-react-app", reason: "non mantenuto — usa Vite" }, | |
| { name: "webpack", reason: "considera Vite per nuovi progetti (10x più veloce)" }, | |
| // HTTP client deprecati | |
| { name: "request", reason: "deprecato dal 2020 — usa axios o fetch nativo" }, | |
| { name: "node-fetch", reason: "usa fetch nativo (Node 18+) o axios" }, | |
| // Utility pesanti con alternative native | |
| { name: "moment", reason: "25KB gzipped — usa date-fns o Temporal API" }, | |
| { name: "lodash", reason: "usa metodi nativi ES2022+ o lodash-es tree-shakeable" }, | |
| { name: "underscore", reason: "usa metodi nativi ES2022+" }, | |
| // UUID obsoleto | |
| { name: "node-uuid", reason: "usa crypto.randomUUID() nativo (Node 14.17+)" }, | |
| { name: "uuid", reason: "considera crypto.randomUUID() nativo per semplicità" }, | |
| // Test runner vecchio | |
| { name: "enzyme", reason: "non compatibile con React 18 — usa @testing-library/react" }, | |
| { name: "mocha", reason: "considera Vitest (più veloce, zero config con Vite)" }, | |
| // CSS obsoleto | |
| { name: "node-sass", reason: "deprecato — usa sass (Dart Sass)" }, | |
| { name: "sass-loader", reason: "solo se webpack — con Vite usa css.preprocessorOptions" }, | |
| // Typi vecchi | |
| { name: "@types/node", reason: "ok solo se versione corrisponde a Node runtime usato", minVersion: "18.0.0" }, | |
| ]; | |
| // ─── Helper: detecta stack dal VFS ──────────────────────────────────────────── | |
| async function detectStack(files: Array<{ path: string; content: string }>): Promise<string> { | |
| const paths = files.map(f => f.path.replace(/^\//, "")); | |
| // Ruby on Rails | |
| if (paths.includes("Gemfile") && paths.some(p => p.startsWith("app/controllers"))) return "rails"; | |
| // Laravel | |
| if (paths.includes("composer.json") && paths.some(p => p.startsWith("routes/"))) return "laravel"; | |
| // Deno | |
| if (paths.includes("deno.json") || paths.includes("deno.jsonc")) return "deno"; | |
| const pkg = files.find(f => f.path === "package.json" || f.path === "/package.json"); | |
| if (!pkg) { | |
| // Python: detect Django vs FastAPI vs Flask | |
| const hasPy = files.some(f => f.path.endsWith(".py")); | |
| if (hasPy) { | |
| const allPy = files.filter(f => f.path.endsWith(".py")).map(f => f.content).join("\n"); | |
| if (/manage\.py|django\.conf|from django/.test(allPy)) return "django"; | |
| if (/from flask|Flask\(/.test(allPy)) return "flask"; | |
| return "fastapi"; | |
| } | |
| // Astro | |
| if (paths.some(p => p.endsWith(".astro"))) return "astro"; | |
| return "generic"; | |
| } | |
| try { | |
| const p = JSON.parse(pkg.content) as { | |
| dependencies?: Record<string, string>; | |
| devDependencies?: Record<string, string>; | |
| scripts?: Record<string, string>; | |
| }; | |
| const deps = { ...(p.dependencies ?? {}), ...(p.devDependencies ?? {}) }; | |
| if ("next" in deps) return "next"; | |
| if ("nuxt" in deps) return "nuxt"; | |
| if ("@remix-run/node" in deps || "@remix-run/react" in deps) return "remix"; | |
| if ("astro" in deps) return "astro"; | |
| if ("@nestjs/core" in deps) return "nestjs"; | |
| if ("vite" in deps && "svelte" in deps) return "svelte"; | |
| if ("vite" in deps) return "vite"; | |
| if ("react" in deps) return "react"; | |
| if ("express" in deps) return "express"; | |
| } catch { /* JSON non valido */ } | |
| return "generic"; | |
| } | |
| // ─── Check 1: struttura VFS ─────────────────────────────────────────────────── | |
| interface CheckResult { | |
| name: string; | |
| pass: boolean; | |
| detail: string; | |
| } | |
| function checkVfsStructure( | |
| files: Array<{ path: string }>, | |
| stack: string, | |
| ): CheckResult { | |
| const paths = new Set(files.map(f => f.path.replace(/^\//, ""))); | |
| const critical = CRITICAL_FILES_BY_STACK[stack] ?? CRITICAL_FILES_BY_STACK.generic; | |
| const missing = critical.filter(c => !paths.has(c) && !paths.has("/" + c)); | |
| return { | |
| name: "Struttura progetto", | |
| pass: missing.length === 0, | |
| detail: missing.length === 0 | |
| ? `File critici presenti (stack: ${stack})` | |
| : `File mancanti: ${missing.join(", ")}`, | |
| }; | |
| } | |
| // ─── Check 2: dipendenze ────────────────────────────────────────────────────── | |
| function checkDependencies(files: Array<{ path: string; content: string }>): CheckResult { | |
| const pkg = files.find(f => f.path === "package.json" || f.path === "/package.json"); | |
| if (!pkg) { | |
| const req = files.find(f => f.path === "requirements.txt" || f.path === "/requirements.txt"); | |
| return { | |
| name: "Dipendenze", | |
| pass: !!req, | |
| detail: req ? "requirements.txt trovato" : "Manca package.json / requirements.txt", | |
| }; | |
| } | |
| let parsed: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> } = {}; | |
| try { parsed = JSON.parse(pkg.content); } catch { | |
| return { name: "Dipendenze", pass: false, detail: "package.json non è JSON valido" }; | |
| } | |
| const allDeps = { ...(parsed.dependencies ?? {}), ...(parsed.devDependencies ?? {}) }; | |
| const broken = KNOWN_BROKEN_DEPS.filter(d => d.name in allDeps); | |
| const total = Object.keys(allDeps).length; | |
| if (broken.length > 0) { | |
| return { | |
| name: "Dipendenze", | |
| pass: false, | |
| detail: `Dipendenze problematiche: ${broken.map(d => `${d.name} (${d.reason})`).join("; ")}`, | |
| }; | |
| } | |
| return { | |
| name: "Dipendenze", | |
| pass: true, | |
| detail: `${total} dipendenze OK`, | |
| }; | |
| } | |
| // ─── Check 3: sintassi basilare ─────────────────────────────────────────────── | |
| function checkBasicSyntax(files: Array<{ path: string; content: string }>): CheckResult { | |
| const issues: string[] = []; | |
| const codeFiles = files.filter(f => | |
| /\.(ts|tsx|js|jsx)$/.test(f.path) && f.content && f.content.length > 100 | |
| ).slice(0, 15); | |
| for (const f of codeFiles) { | |
| const c = f.content; | |
| const name = f.path.split("/").pop() ?? f.path; | |
| // Parentesi sbilanciate (check leggero) | |
| const opens = (c.match(/\{/g) ?? []).length; | |
| const closes = (c.match(/\}/g) ?? []).length; | |
| if (Math.abs(opens - closes) > 10) { | |
| issues.push(`${name}: parentesi graffe sbilanciate (${opens} aperte, ${closes} chiuse)`); | |
| } | |
| // import senza from (pattern tipico di errore LLM) | |
| if (/^import\s+[^'"{\n]+\s*$/m.test(c) && !/from\s+['"]/.test(c.split("\n").find(l => /^import\s+[^'"{\n]+\s*$/.test(l)) ?? "")) { | |
| // skip — falso positivo frequente | |
| } | |
| // Console.log con variabile undefined comune | |
| if (/console\.log\(undefined\)/.test(c)) { | |
| issues.push(`${name}: console.log(undefined) — probabile variabile non inizializzata`); // check-console-log: ok | |
| } | |
| } | |
| return { | |
| name: "Sintassi", | |
| pass: issues.length === 0, | |
| detail: issues.length === 0 | |
| ? `${codeFiles.length} file controllati — nessun problema evidente` | |
| : issues.slice(0, 3).join("; "), | |
| }; | |
| } | |
| // ─── Check 4: file vuoti / stub ─────────────────────────────────────────────── | |
| function checkNoStubs(files: Array<{ path: string; content: string }>): CheckResult { | |
| const stubs = files.filter(f => { | |
| const c = f.content?.trim() ?? ""; | |
| return c.length < 30 && /\.(ts|tsx|js|jsx|py)$/.test(f.path); | |
| }); | |
| return { | |
| name: "File completi", | |
| pass: stubs.length === 0, | |
| detail: stubs.length === 0 | |
| ? "Nessun file stub/vuoto" | |
| : `File stub/incompleti: ${stubs.map(f => f.path).join(", ")}`, | |
| }; | |
| } | |
| // ─── Main: tryExecute ───────────────────────────────────────────────────────── | |
| export async function tryExecute( | |
| name: string, | |
| args: Record<string, unknown>, | |
| ): Promise<string | null> { | |
| if (name !== "validate_project") return null; | |
| const goal = (args.goal as string | undefined) ?? ""; | |
| const verbose = (args.verbose as boolean | undefined) ?? false; | |
| try { | |
| const rawFiles = await vfsAsync.list(); | |
| const files = rawFiles | |
| .filter(f => f.path && f.content) | |
| .map(f => ({ path: f.path, content: f.content as string })); | |
| if (files.length === 0) { | |
| return "⚠️ Nessun file nel VFS — genera prima il progetto con write_file / create_webpage."; | |
| } | |
| const stack = await detectStack(files); | |
| const checks: CheckResult[] = [ | |
| checkVfsStructure(files, stack), | |
| checkDependencies(files), | |
| checkBasicSyntax(files), | |
| checkNoStubs(files), | |
| ]; | |
| // Acceptance tests dal goal | |
| let acceptanceReport = ""; | |
| if (goal.trim().length > 0) { | |
| const report = runAcceptanceTests(goal, files); | |
| if (report.total > 0) { | |
| acceptanceReport = formatAcceptanceReport(report); | |
| checks.push({ | |
| name: "Requisiti utente", | |
| pass: report.allGreen, | |
| detail: `${report.passed}/${report.total} test accettazione`, | |
| }); | |
| } | |
| } | |
| // Fix 3: validazione reale — se package.json ha script "build", | |
| // esegui npm run build via la sessione persistente (Fix 1 — stessa sandbox). | |
| // Inietta exit_code/stderr reali nel report invece di affidarsi solo a heuristic. | |
| // Best-effort: se backend non disponibile, il check è omesso (graceful degradation). | |
| { | |
| const _pkgFile = files.find(f => f.path === "package.json" || f.path === "/package.json"); | |
| if (_pkgFile) { | |
| let _pkgParsed: { scripts?: Record<string, string> } = {}; | |
| try { _pkgParsed = JSON.parse(_pkgFile.content); } catch { /* ignora */ } | |
| const _hasBuild = !!_pkgParsed.scripts?.build; | |
| if (_hasBuild) { | |
| try { | |
| const { backend, getExecSessionId } = await import("../backendClient"); | |
| if (backend.isExecAvailable()) { | |
| const _sid = getExecSessionId(); | |
| const _br = await backend.executeShell("npm run build 2>&1", 60, _sid); | |
| const _buildOk = _br.exit_code === 0; | |
| const _out = (_br.stderr || _br.stdout || "").slice(0, 400); | |
| checks.push({ | |
| name: "Build reale (npm run build)", | |
| pass: _buildOk, | |
| detail: _buildOk | |
| ? "Build completata con successo (exit 0)" | |
| : `Build fallita (exit ${_br.exit_code}): ${_out}`, | |
| }); | |
| } | |
| } catch { /* backend non disponibile — check omesso */ } | |
| } | |
| } | |
| } | |
| const passed = checks.filter(c => c.pass).length; | |
| const total = checks.length; | |
| const allOk = passed === total; | |
| // Build report | |
| const lines: string[] = [ | |
| `## 🔍 Validazione Progetto — ${passed}/${total} check superati`, | |
| `Stack rilevato: **${stack}** | File VFS: **${files.length}**`, | |
| "", | |
| ]; | |
| for (const c of checks) { | |
| lines.push(`${c.pass ? "✅" : "❌"} **${c.name}**: ${c.detail}`); | |
| } | |
| if (acceptanceReport) lines.push(acceptanceReport); | |
| if (!allOk) { | |
| const failing = checks.filter(c => !c.pass); | |
| lines.push(""); | |
| lines.push("### 🔧 Azioni correttive necessarie:"); | |
| for (const f of failing) { | |
| lines.push(` → **${f.name}**: ${f.detail}`); | |
| } | |
| lines.push(""); | |
| lines.push("Correggi i punti sopra, poi chiama `validate_project` di nuovo per verificare."); | |
| } else { | |
| lines.push(""); | |
| lines.push("### ✅ Progetto valido — pronto per il deploy."); | |
| } | |
| if (verbose) { | |
| lines.push("", `_File analizzati: ${files.map(f => f.path).slice(0, 20).join(", ")}${files.length > 20 ? "..." : ""}_`); | |
| } | |
| return lines.join("\n"); | |
| } catch (e) { | |
| return `❌ validate_project: ${e instanceof Error ? e.message : String(e)}`; | |
| } | |
| } | |