Files
16gagent/scripts/release-benchmark.mjs
T
2026-06-06 10:40:48 +08:00

395 lines
17 KiB
JavaScript

#!/usr/bin/env node
/**
* Release Benchmark — SF-07
*
* 验证完整链路:Frontend → Backend → Fullstack → Electron → Release
* 对 PetCare / CRM / Inventory 三个领域执行,输出 release-benchmark-report.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, "release-benchmark-report.md");
const DOMAINS = [
{ id: "petcare", input: "做一个宠物护理管理平台,宠物主人可以管理宠物档案、健康日程、日常记录和成长相册" },
{ id: "crm", input: "做一个客户关系管理系统,支持客户管理、销售漏斗、跟进记录和数据分析仪表盘" },
{ id: "inventory", 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(agentPath, args) {
const result = cmd(`node ${agentPath} ${args}`);
if (result.error) {
try { return JSON.parse(result.stdout?.trim() || "{}"); } catch { return { error: result.error, stderr: result.stderr }; }
}
try { return JSON.parse(result.trim()); } catch { return { 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 {}; }
}
// ═══════════════════════════════════════════════════════
// Step 1-4: Full Pipeline (reuse existing agents)
// ═══════════════════════════════════════════════════════
function generateRequirementPackage(domain) {
console.log(`\n📋 [${domain.id}] Generating RequirementPackage...`);
const t0 = Date.now();
const prdOut = join(BENCH_ROOT, domain.id, "prd.json");
const archOut = join(BENCH_ROOT, domain.id, "arch.json");
mkdirSync(dirname(prdOut), { recursive: true });
runAgent(join(SCRIPTS, "project-intake-agent.mjs"), `--input "${domain.input}" --output "${prdOut}"`);
runAgent(join(SCRIPTS, "architecture-agent.mjs"), `--input "${prdOut}" --output "${archOut}"`);
const prd = loadJSON(prdOut);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
console.log(` ✅ PRD + Architecture (${elapsed}s) — ${prd.projectName || "?"}`);
return { prd, prdPath: prdOut, archPath: archOut, elapsed: parseFloat(elapsed) };
}
function generateFrontend(domain, prdPath, archPath) {
console.log(` 🎨 Generating Frontend...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "frontend");
runAgent(join(SCRIPTS, "frontend-builder-agent.mjs"), `--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Frontend (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc };
}
function generateBackend(domain, prdPath, archPath) {
console.log(` ⚙️ Generating Backend...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "backend");
runAgent(join(SCRIPTS, "backend-builder-agent.mjs"), `--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Backend (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc };
}
function generateFullstack(domain, prdPath, archPath) {
console.log(` 🔗 Composing Fullstack...`);
const t0 = Date.now();
const outDir = join(BENCH_ROOT, domain.id, "fullstack");
runAgent(join(SCRIPTS, "fullstack-composer-agent.mjs"), `--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Fullstack (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc };
}
function generateElectron(domain, prdPath) {
console.log(` 🖥️ Generating Electron...`);
const t0 = Date.now();
const fullstackDir = join(BENCH_ROOT, domain.id, "fullstack");
const outDir = join(BENCH_ROOT, domain.id, "electron");
runAgent(join(SCRIPTS, "electron-builder-agent.mjs"), `--input "${fullstackDir}" --output "${outDir}" --prd "${prdPath}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Electron (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc };
}
// ═══════════════════════════════════════════════════════
// Step 5: Release Builder
// ═══════════════════════════════════════════════════════
function generateRelease(domain) {
console.log(` 📦 Generating Release...`);
const t0 = Date.now();
const fullstackDir = join(BENCH_ROOT, domain.id, "fullstack");
const outDir = join(BENCH_ROOT, domain.id, "release");
const result = runAgent(join(SCRIPTS, "release-builder-agent.mjs"), `--input "${fullstackDir}" --output "${outDir}"`);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
const fc = countFiles(outDir);
console.log(` ✅ Release (${elapsed}s, ${fc} files)`);
return { elapsed: parseFloat(elapsed), fileCount: fc, result };
}
// ═══════════════════════════════════════════════════════
// Validation
// ═══════════════════════════════════════════════════════
function validateRelease(domain) {
const releaseDir = join(BENCH_ROOT, domain.id, "release");
const checks = {
hasVersionJson: existsSync(join(releaseDir, "release", "version.json")),
hasManifestJson: existsSync(join(releaseDir, "release", "manifests", "manifest.json")),
hasChecksums: existsSync(join(releaseDir, "release", "checksums", "checksums.txt")),
hasReleaseNotes: existsSync(join(releaseDir, "release", "release-notes", "release-notes.md")),
hasBuildInfo: existsSync(join(releaseDir, "release", "build-info.json")),
hasReadme: existsSync(join(releaseDir, "release", "README.md")),
hasWindowsDir: existsSync(join(releaseDir, "release", "windows")),
hasMacosDir: existsSync(join(releaseDir, "release", "macos")),
hasLinuxDir: existsSync(join(releaseDir, "release", "linux")),
};
// Validate version.json
if (checks.hasVersionJson) {
try {
const v = JSON.parse(readFileSync(join(releaseDir, "release", "version.json"), "utf8"));
checks.versionValid = !!(v.name && v.version && v.platforms);
} catch { checks.versionValid = false; }
}
// Validate manifest
if (checks.hasManifestJson) {
try {
const m = JSON.parse(readFileSync(join(releaseDir, "release", "manifests", "manifest.json"), "utf8"));
checks.manifestValid = !!(m.project && m.version && m.files && m.files.length > 0);
checks.manifestFileCount = m.files?.length || 0;
} catch { checks.manifestValid = false; }
}
// Validate checksums
if (checks.hasChecksums) {
const content = readFileSync(join(releaseDir, "release", "checksums", "checksums.txt"), "utf8");
const lines = content.trim().split("\n").filter(l => l.length > 0);
checks.checksumCount = lines.length;
checks.checksumsValid = lines.every(l => {
const parts = l.split(/\s+/);
return parts[0]?.length === 64 && parts[1]?.length > 0;
});
}
// Validate release notes
if (checks.hasReleaseNotes) {
const notes = readFileSync(join(releaseDir, "release", "release-notes", "release-notes.md"), "utf8");
checks.releaseNotesValid = notes.includes("Version") && notes.includes("Features") && notes.includes("Installation");
}
// Validate build-info
if (checks.hasBuildInfo) {
try {
const bi = JSON.parse(readFileSync(join(releaseDir, "release", "build-info.json"), "utf8"));
checks.buildInfoValid = !!(bi.nodeVersion && bi.generatorVersion && bi.buildTime);
} catch { checks.buildInfoValid = false; }
}
// Platform placeholder counts
if (checks.hasWindowsDir) {
try { checks.windowsFileCount = readdirSync(join(releaseDir, "release", "windows")).length; } catch { checks.windowsFileCount = 0; }
}
if (checks.hasMacosDir) {
try { checks.macosFileCount = readdirSync(join(releaseDir, "release", "macos")).length; } catch { checks.macosFileCount = 0; }
}
if (checks.hasLinuxDir) {
try { checks.linuxFileCount = readdirSync(join(releaseDir, "release", "linux")).length; } catch { checks.linuxFileCount = 0; }
}
checks.allPassed = checks.hasVersionJson && checks.hasManifestJson && checks.hasChecksums &&
checks.hasReleaseNotes && checks.hasBuildInfo && checks.hasWindowsDir &&
checks.hasMacosDir && checks.hasLinuxDir && checks.versionValid &&
checks.manifestValid && checks.checksumsValid && checks.releaseNotesValid && checks.buildInfoValid;
return checks;
}
// ═══════════════════════════════════════════════════════
// Report
// ═══════════════════════════════════════════════════════
function generateReport(allResults) {
const lines = [];
const h = (t) => { lines.push(t); return lines; };
h(`# Release Benchmark Report — SF-07`);
h(``);
h(`> Generated: ${new Date().toISOString()}`);
h(`> Pipeline: Frontend → Backend → Fullstack → Electron → Release`);
h(``);
// ── Domain Results ──
h(`## Domain Results`);
h(``);
h(`| Domain | Release | Manifest | Notes | Result |`);
h(`|--------|---------|----------|-------|--------|`);
for (const r of allResults) {
const rel = r.releaseChecks?.allPassed ? "✅" : "❌";
const man = r.releaseChecks?.manifestValid ? "✅" : "❌";
const notes = r.releaseChecks?.releaseNotesValid ? "✅" : "❌";
const result = r.releaseChecks?.allPassed ? "PASS" : "FAIL";
h(`| ${r.id} | ${rel} | ${man} | ${notes} | **${result}** |`);
}
h(``);
// ── Release Artifacts ──
h(`## Release Artifacts`);
h(``);
h(`| Domain | Files | Manifest Files | Checksum Lines | Release Notes |`);
h(`|--------|-------|----------------|----------------|---------------|`);
for (const r of allResults) {
const rc = r.releaseChecks || {};
h(`| ${r.id} | ${r.release?.fileCount || 0} | ${rc.manifestFileCount || 0} | ${rc.checksumCount || 0} | ${rc.hasReleaseNotes ? "✅" : "❌"} |`);
}
h(``);
// ── Pipeline Timing ──
h(`## Pipeline Timing`);
h(``);
h(`| Domain | Frontend | Backend | Fullstack | Electron | Release | Total |`);
h(`|--------|----------|---------|-----------|----------|---------|-------|`);
for (const r of allResults) {
h(`| ${r.id} | ${r.frontend?.elapsed || 0}s | ${r.backend?.elapsed || 0}s | ${r.fullstack?.elapsed || 0}s | ${r.electron?.elapsed || 0}s | ${r.release?.elapsed || 0}s | ${r.totalTime || 0}s |`);
}
h(``);
// ── Validation Details ──
h(`## Validation Details`);
h(``);
for (const r of allResults) {
h(`### ${r.id}`);
h(``);
const rc = r.releaseChecks || {};
h(`| Check | Status |`);
h(`|-------|--------|`);
h(`| version.json | ${rc.hasVersionJson ? "✅" : "❌"} |`);
h(`| version.json valid | ${rc.versionValid ? "✅" : "❌"} |`);
h(`| manifest.json | ${rc.hasManifestJson ? "✅" : "❌"} |`);
h(`| manifest valid | ${rc.manifestValid ? "✅" : "❌"} |`);
h(`| checksums.txt | ${rc.hasChecksums ? "✅" : "❌"} |`);
h(`| checksums valid | ${rc.checksumsValid ? "✅" : "❌"} |`);
h(`| release-notes.md | ${rc.hasReleaseNotes ? "✅" : "❌"} |`);
h(`| release notes valid | ${rc.releaseNotesValid ? "✅" : "❌"} |`);
h(`| build-info.json | ${rc.hasBuildInfo ? "✅" : "❌"} |`);
h(`| build-info valid | ${rc.buildInfoValid ? "✅" : "❌"} |`);
h(`| windows/ | ${rc.hasWindowsDir ? "✅" : "❌"} (${rc.windowsFileCount || 0} files) |`);
h(`| macos/ | ${rc.hasMacosDir ? "✅" : "❌"} (${rc.macosFileCount || 0} files) |`);
h(`| linux/ | ${rc.hasLinuxDir ? "✅" : "❌"} (${rc.linuxFileCount || 0} files) |`);
h(``);
}
// ── Final Status ──
h(`## Final Status`);
h(``);
const passCount = allResults.filter(r => r.releaseChecks?.allPassed).length;
const total = allResults.length;
h(`| Metric | Value |`);
h(`|--------|-------|`);
h(`| Domains Tested | ${total} |`);
h(`| Release PASS | ${passCount}/${total} |`);
h(`| Release FAIL | ${total - passCount}/${total} |`);
h(``);
if (passCount === total) {
h(`🎉 **Release Builder Agent — PASS** (${passCount}/${total})`);
h(``);
h(`All domains successfully generated complete release packages.`);
} else {
h(`⚠️ **Release Builder Agent — FAIL** (${passCount}/${total})`);
h(``);
const failed = allResults.filter(r => !r.releaseChecks?.allPassed);
h(`Failed domains: ${failed.map(r => r.id).join(", ")}`);
}
h(``);
h(`---`);
h(`*Report generated by Release Benchmark — SF-07*`);
return lines.join("\n");
}
// ═══════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════
async function main() {
console.log(`\n🧪 Release Benchmark — SF-07`);
console.log(` Testing ${DOMAINS.length} domains`);
console.log(` Pipeline: Frontend → Backend → Fullstack → Electron → Release\n`);
const allResults = [];
for (const domain of DOMAINS) {
console.log(`\n${"=".repeat(60)}`);
console.log(`🏗️ Domain: ${domain.id}`);
console.log(`${"=".repeat(60)}`);
const tTotal = Date.now();
// Steps 1-4: Generate full pipeline
const { prd, prdPath, archPath, elapsed: reqTime } = generateRequirementPackage(domain);
const frontend = generateFrontend(domain, prdPath, archPath);
const backend = generateBackend(domain, prdPath, archPath);
const fullstack = generateFullstack(domain, prdPath, archPath);
const electron = generateElectron(domain, prdPath);
// Step 5: Release
const release = generateRelease(domain);
// Validate
const releaseChecks = validateRelease(domain);
const totalTime = ((Date.now() - tTotal) / 1000).toFixed(1);
allResults.push({
id: domain.id,
input: domain.input,
projectName: prd.projectName,
reqTime,
frontend,
backend,
fullstack,
electron,
release,
releaseChecks,
totalTime: parseFloat(totalTime),
});
console.log(` ⏱️ Total: ${totalTime}s | Release: ${releaseChecks.allPassed ? "✅ PASS" : "❌ FAIL"}`);
}
// Generate report
const report = generateReport(allResults);
writeFileSync(REPORT_PATH, report, "utf8");
console.log(`\n📄 Report written to: ${REPORT_PATH}`);
// Summary
const passCount = allResults.filter(r => r.releaseChecks?.allPassed).length;
console.log(`\n📊 Summary: ${passCount}/${DOMAINS.length} domains PASS`);
console.log(` Release Builder: ${passCount === DOMAINS.length ? "✅ PASS" : "❌ FAIL"}`);
}
main().catch(e => {
console.error("Benchmark failed:", e);
process.exit(1);
});