/** * 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 = { 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 { 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; devDependencies?: Record; scripts?: Record; }; 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; devDependencies?: Record } = {}; 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, ): Promise { 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 } = {}; 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)}`; } }