#!/usr/bin/env node /** * End-to-End Generator Benchmark v1 * * 完整链路:需求 → 前端 → 后端 → 全栈 → Electron → Release * 10 个领域全量验证,输出 end-to-end-benchmark-v1.md */ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs"; import { resolve, dirname, join } from "node:path"; import { execSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const __dirname = dirname(resolve(fileURLToPath(import.meta.url))); const WORKSPACE = resolve(__dirname, ".."); const BENCH_ROOT = resolve(WORKSPACE, ".benchmark"); const SCRIPTS = resolve(WORKSPACE, "scripts"); const REPORT_PATH = resolve(WORKSPACE, "end-to-end-benchmark-v1.md"); const DOMAINS = [ { id: "petcare", input: "做一个宠物护理管理平台,宠物主人可以管理宠物档案、健康日程、日常记录和成长相册" }, { id: "crm", input: "做一个客户关系管理系统,支持客户管理、销售漏斗、跟进记录和数据分析仪表盘" }, { id: "inventory", input: "做一个库存管理系统,支持商品入库出库、库存盘点、供应商管理和库存预警" }, { id: "ticket", input: "做一个工单系统,支持工单创建、分配、处理流程、优先级管理和工单归档" }, { id: "blog-cms", input: "做一个博客内容管理系统,支持文章发布、分类标签、评论管理和媒体库" }, { id: "project-mgmt", input: "做一个项目管理系统,支持项目看板、任务分配、甘特图和团队协作" }, { id: "hr", input: "做一个人力资源管理系统,支持员工档案、考勤管理、招聘流程和绩效评估" }, { id: "asset", input: "做一个固定资产管理系统,支持资产登记、领用归还、折旧计算和盘点统计" }, { id: "course", input: "做一个在线课程管理系统,支持课程发布、章节管理、学员进度和作业批改" }, { id: "appointment", input: "做一个预约管理系统,支持服务项目、时间段预约、客户通知和预约统计" }, ]; // ═══════════════════════════════════════════════════════ // Helpers // ═══════════════════════════════════════════════════════ function cmd(c, opts = {}) { try { return execSync(c, { cwd: WORKSPACE, encoding: "utf8", ...opts, stdio: opts.stdio ?? "pipe" }); } catch (e) { return { error: e.message, stderr: e.stderr?.toString() || "", stdout: e.stdout?.toString() || "" }; } } function runAgent(script, args) { const t0 = Date.now(); const result = cmd(`node ${join(SCRIPTS, script)} ${args}`); const ms = Date.now() - t0; if (result.error) { try { return { ms, ...JSON.parse(result.stdout?.trim() || "{}") }; } catch { return { ms, error: result.error }; } } try { return { ms, ...JSON.parse(result.trim()) }; } catch { return { ms, raw: result }; } } function countFiles(dir) { try { const r = execSync(`find ${dir} -type f 2>/dev/null | wc -l`, { encoding: "utf8", cwd: WORKSPACE }); return parseInt(r.trim(), 10); } catch { return 0; } } function loadJSON(path) { try { return JSON.parse(readFileSync(path, "utf8")); } catch { return {}; } } // ═══════════════════════════════════════════════════════ // Validation // ═══════════════════════════════════════════════════════ function validateFrontend(dir) { if (!existsSync(dir)) return { pass: false, reason: "dir missing" }; const checks = { packageJson: existsSync(join(dir, "package.json")), appDir: existsSync(join(dir, "app")), services: existsSync(join(dir, "services")) || existsSync(join(dir, "src", "services")), types: existsSync(join(dir, "types")) || existsSync(join(dir, "src", "types")), }; const fileCount = countFiles(dir); const pass = checks.packageJson && checks.appDir && fileCount > 10; return { pass, fileCount, ...checks }; } function validateBackend(dir) { if (!existsSync(dir)) return { pass: false, reason: "dir missing" }; const checks = { packageJson: existsSync(join(dir, "package.json")), routes: existsSync(join(dir, "src", "routes")), services: existsSync(join(dir, "src", "services")), auth: existsSync(join(dir, "src", "routes", "auth.ts")) || existsSync(join(dir, "src", "routes", "auth-route.ts")), db: existsSync(join(dir, "src", "db")) || existsSync(join(dir, "src", "schema")), }; const fileCount = countFiles(dir); const pass = checks.packageJson && checks.routes && checks.services && fileCount > 10; return { pass, fileCount, ...checks }; } function validateFullstack(dir) { if (!existsSync(dir)) return { pass: false, reason: "dir missing" }; const checks = { packageJson: existsSync(join(dir, "package.json")), web: existsSync(join(dir, "apps", "web")), api: existsSync(join(dir, "apps", "api")), shared: existsSync(join(dir, "packages")), }; const fileCount = countFiles(dir); const pass = checks.packageJson && checks.web && checks.api && fileCount > 30; return { pass, fileCount, ...checks }; } function validateElectron(dir) { if (!existsSync(dir)) return { pass: false, reason: "dir missing" }; const checks = { packageJson: existsSync(join(dir, "package.json")), main: existsSync(join(dir, "electron", "main.ts")), preload: existsSync(join(dir, "electron", "preload.ts")), ipc: existsSync(join(dir, "electron", "ipc.ts")), yml: existsSync(join(dir, "electron-builder.yml")), }; const fileCount = countFiles(dir); const pass = checks.packageJson && checks.main && checks.preload && fileCount >= 8; return { pass, fileCount, ...checks }; } function validateRelease(dir) { if (!existsSync(dir)) return { pass: false, reason: "dir missing" }; const rDir = join(dir, "release"); const checks = { versionJson: existsSync(join(rDir, "version.json")), manifest: existsSync(join(rDir, "manifests", "manifest.json")), checksums: existsSync(join(rDir, "checksums", "checksums.txt")), releaseNotes: existsSync(join(rDir, "release-notes", "release-notes.md")), buildInfo: existsSync(join(rDir, "build-info.json")), windows: existsSync(join(rDir, "windows")), macos: existsSync(join(rDir, "macos")), linux: existsSync(join(rDir, "linux")), }; // Validate content if (checks.versionJson) { try { const v = JSON.parse(readFileSync(join(rDir, "version.json"), "utf8")); checks.versionValid = !!(v.name && v.version && v.platforms); } catch { checks.versionValid = false; } } if (checks.manifest) { try { const m = JSON.parse(readFileSync(join(rDir, "manifests", "manifest.json"), "utf8")); checks.manifestValid = !!(m.project && m.files?.length > 0); } catch { checks.manifestValid = false; } } if (checks.checksums) { const content = readFileSync(join(rDir, "checksums", "checksums.txt"), "utf8"); const lines = content.trim().split("\n").filter(l => l.length > 0); checks.checksumCount = lines.length; checks.checksumsValid = lines.every(l => l.split(/\s+/)[0]?.length === 64); } if (checks.releaseNotes) { const notes = readFileSync(join(rDir, "release-notes", "release-notes.md"), "utf8"); checks.notesValid = notes.includes("Version") && notes.includes("Installation"); } if (checks.buildInfo) { try { const bi = JSON.parse(readFileSync(join(rDir, "build-info.json"), "utf8")); checks.buildInfoValid = !!(bi.nodeVersion && bi.generatorVersion); } catch { checks.buildInfoValid = false; } } // Platform file counts for (const p of ["windows", "macos", "linux"]) { if (checks[p]) { try { checks[p + "Count"] = readdirSync(join(rDir, p)).length; } catch { checks[p + "Count"] = 0; } } } const fileCount = countFiles(dir); const pass = Object.values(checks).every(v => v !== false && v !== 0); return { pass, fileCount, ...checks }; } // ═══════════════════════════════════════════════════════ // Pipeline // ═══════════════════════════════════════════════════════ function runPipeline(domain) { const B = join(BENCH_ROOT, domain.id); mkdirSync(B, { recursive: true }); const result = { id: domain.id, input: domain.input, stages: {} }; // SF-01: PRD const s1 = runAgent("project-intake-agent.mjs", `--input "${domain.input}" --output "${join(B, "prd.json")}"`); result.stages.prd = { ms: s1.ms, projectName: s1.projectName, domain: s1.domain, error: s1.error }; result.projectName = s1.projectName; // SF-02: Architecture const s2 = runAgent("architecture-agent.mjs", `--input "${join(B, "prd.json")}" --output "${join(B, "arch.json")}"`); result.stages.arch = { ms: s2.ms, modules: s2.moduleCount, error: s2.error }; // SF-03: Frontend const feOut = join(B, "frontend"); const s3 = runAgent("frontend-builder-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${feOut}"`); const feVal = validateFrontend(feOut); result.stages.frontend = { ms: s3.ms, files: s3.stats?.totalFiles || feVal.fileCount, pass: feVal.pass, error: s3.error }; // SF-04: Backend const beOut = join(B, "backend"); const s4 = runAgent("backend-builder-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${beOut}"`); const beVal = validateBackend(beOut); result.stages.backend = { ms: s4.ms, files: s4.stats?.totalFiles || beVal.fileCount, pass: beVal.pass, error: s4.error }; // SF-05: Fullstack const fsOut = join(B, "fullstack"); const s5 = runAgent("fullstack-composer-agent.mjs", `--prd "${join(B, "prd.json")}" --arch "${join(B, "arch.json")}" --output "${fsOut}"`); const fsVal = validateFullstack(fsOut); result.stages.fullstack = { ms: s5.ms, files: s5.stats?.totalFiles || fsVal.fileCount, pass: fsVal.pass, error: s5.error }; // SF-06: Electron const elOut = join(B, "electron"); const s6 = runAgent("electron-builder-agent.mjs", `--input "${fsOut}" --output "${elOut}" --prd "${join(B, "prd.json")}"`); const elVal = validateElectron(elOut); result.stages.electron = { ms: s6.ms, files: s6.stats?.totalFiles || elVal.fileCount, pass: elVal.pass, error: s6.error }; // SF-07: Release const rlOut = join(B, "release"); const s7 = runAgent("release-builder-agent.mjs", `--input "${fsOut}" --output "${rlOut}"`); const rlVal = validateRelease(rlOut); result.stages.release = { ms: s7.ms, files: s7.stats?.totalFiles || rlVal.fileCount, pass: rlVal.pass, error: s7.error }; // Totals result.totalMs = Object.values(result.stages).reduce((s, v) => s + (v.ms || 0), 0); result.totalFiles = Object.values(result.stages).reduce((s, v) => s + (v.files || 0), 0); result.allPass = Object.values(result.stages).every(v => v.pass !== false); return result; } // ═══════════════════════════════════════════════════════ // Report // ═══════════════════════════════════════════════════════ function generateReport(allResults) { const L = []; const h = (t) => { L.push(t); return L; }; const now = new Date(); h(`# End-to-End Generator Benchmark v1`); h(``); h(`> ${now.toISOString()}`); h(`> 链路: 需求 → 前端 → 后端 → 全栈 → Electron → Release`); h(`> 领域: ${allResults.length} 个`); h(``); // ── Summary ── const passCount = allResults.filter(r => r.allPass).length; const totalFiles = allResults.reduce((s, r) => s + r.totalFiles, 0); const avgMs = (allResults.reduce((s, r) => s + r.totalMs, 0) / allResults.length).toFixed(0); h(`## Summary`); h(``); h(`| Metric | Value |`); h(`|--------|-------|`); h(`| Domains | ${allResults.length} |`); h(`| PASS | ${passCount}/${allResults.length} |`); h(`| FAIL | ${allResults.length - passCount}/${allResults.length} |`); h(`| Total Files Generated | ${totalFiles} |`); h(`| Avg Pipeline Time | ${avgMs}ms |`); h(``); // ── Domain Results ── h(`## Domain Results`); h(``); h(`| Domain | PRD | Arch | Frontend | Backend | Fullstack | Electron | Release | Files | Time | Result |`); h(`|--------|-----|------|----------|---------|-----------|----------|---------|-------|------|--------|`); for (const r of allResults) { const s = r.stages; const p = (v) => v.pass === false ? "❌" : "✅"; const t = (v) => v.ms ? `${v.ms}ms` : "—"; h(`| ${r.id} | ${t(s.prd)} | ${t(s.arch)} | ${p(s.frontend)} ${s.frontend.files || 0}f | ${p(s.backend)} ${s.backend.files || 0}f | ${p(s.fullstack)} ${s.fullstack.files || 0}f | ${p(s.electron)} ${s.electron.files || 0}f | ${p(s.release)} ${s.release.files || 0}f | ${r.totalFiles} | ${r.totalMs}ms | **${r.allPass ? "PASS" : "FAIL"}** |`); } h(``); // ── Per-Stage Stats ── h(`## Per-Stage Statistics`); h(``); const stages = ["prd", "arch", "frontend", "backend", "fullstack", "electron", "release"]; const stageNames = ["SF-01 PRD", "SF-02 Arch", "SF-03 Frontend", "SF-04 Backend", "SF-05 Fullstack", "SF-06 Electron", "SF-07 Release"]; h(`| Stage | Avg Time | Min | Max | Avg Files | Pass |`); h(`|-------|----------|-----|-----|-----------|------|`); for (let i = 0; i < stages.length; i++) { const key = stages[i]; const times = allResults.map(r => r.stages[key]?.ms || 0); const files = allResults.map(r => r.stages[key]?.files || 0); const passes = allResults.filter(r => r.stages[key]?.pass !== false).length; const avg = (times.reduce((a, b) => a + b, 0) / times.length).toFixed(0); const min = Math.min(...times); const max = Math.max(...times); const avgF = (files.reduce((a, b) => a + b, 0) / files.length).toFixed(0); h(`| ${stageNames[i]} | ${avg}ms | ${min}ms | ${max}ms | ${avgF} | ${passes}/${allResults.length} |`); } h(``); // ── File Generation ── h(`## File Generation`); h(``); h(`| Domain | Frontend | Backend | Fullstack | Electron | Release | Total |`); h(`|--------|----------|---------|-----------|----------|---------|-------|`); for (const r of allResults) { const s = r.stages; h(`| ${r.id} | ${s.frontend.files || 0} | ${s.backend.files || 0} | ${s.fullstack.files || 0} | ${s.electron.files || 0} | ${s.release.files || 0} | ${r.totalFiles} |`); } h(``); // ── Failure Details ── const failures = allResults.filter(r => !r.allPass); if (failures.length > 0) { h(`## Failure Details`); h(``); for (const r of failures) { h(`### ${r.id}`); h(``); for (const [stage, data] of Object.entries(r.stages)) { if (data.pass === false) { h(`- **${stage}:** FAIL${data.error ? " — " + data.error : ""}`); } } h(``); } } // ── Final Status ── h(`## Final Status`); h(``); if (passCount === allResults.length) { h(`🎉 **End-to-End Generator — PASS** (${passCount}/${allResults.length})`); h(``); h(`完整链路 需求 → 前端 → 后端 → 全栈 → Electron → Release 全部通过。`); h(`系统具备跨领域泛化能力,可作为通用 Web 全栈 + 桌面应用生成器。`); } else { h(`⚠️ **End-to-End Generator — PARTIAL** (${passCount}/${allResults.length})`); h(``); h(`Failed: ${failures.map(r => r.id).join(", ")}`); } h(``); h(`---`); h(`*End-to-End Generator Benchmark v1 — ${now.toISOString()}*`); return L.join("\n"); } // ═══════════════════════════════════════════════════════ // Main // ═══════════════════════════════════════════════════════ async function main() { console.log(`\n🧪 End-to-End Generator Benchmark v1`); console.log(` ${DOMAINS.length} domains × 7 stages = ${DOMAINS.length * 7} checks`); console.log(` 链路: 需求 → 前端 → 后端 → 全栈 → Electron → Release\n`); const allResults = []; for (const domain of DOMAINS) { console.log(`${"─".repeat(50)}`); console.log(`🏗️ ${domain.id}`); const result = runPipeline(domain); allResults.push(result); const status = result.allPass ? "✅ PASS" : "❌ FAIL"; console.log(` ${result.projectName || "?"} | ${result.totalFiles} files | ${result.totalMs}ms | ${status}`); } const report = generateReport(allResults); writeFileSync(REPORT_PATH, report, "utf8"); const passCount = allResults.filter(r => r.allPass).length; console.log(`\n${"═".repeat(50)}`); console.log(`📊 ${passCount}/${DOMAINS.length} PASS`); console.log(`📄 ${REPORT_PATH}`); } main().catch(e => { console.error("Benchmark failed:", e); process.exit(1); });