#!/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); });