386 lines
17 KiB
JavaScript
386 lines
17 KiB
JavaScript
#!/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);
|
||
});
|