🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user