395 lines
17 KiB
JavaScript
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);
|
|
});
|