File size: 14,710 Bytes
cc11e77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
/**
 * 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)}`;
  }
}