AUDIT / src /lib /agentLoop /buildValidationLoop.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
14.7 kB
/**
* 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)}`;
}
}