874 lines
36 KiB
JavaScript
874 lines
36 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Domain Benchmark Suite
|
||
*
|
||
* 验证现有系统的泛化能力:
|
||
* project-intake-agent → architecture-agent
|
||
* → frontend-builder-agent → backend-builder-agent → fullstack-composer-agent
|
||
*
|
||
* 对 10 个领域生成完整项目,验证 Build + API,输出 benchmark-report.md
|
||
*
|
||
* Usage:
|
||
* node scripts/domain-benchmark.mjs [--domain <name>] [--skip-build] [--skip-api]
|
||
*/
|
||
|
||
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } 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, "benchmark-report.md");
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Domain Definitions
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
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(agentPath, args) {
|
||
const result = cmd(`node ${agentPath} ${args}`);
|
||
if (result.error) {
|
||
// Try to extract JSON from stdout even on 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 rmDirSafe(dir) {
|
||
try { if (existsSync(dir)) rmSync(dir, { recursive: true, force: true }); } catch {}
|
||
}
|
||
|
||
function listFilesRecursive(dir, acc = []) {
|
||
if (!existsSync(dir)) return acc;
|
||
const entries = readFileSync(dir)?.toString?.(); // not for dirs
|
||
const { readdirSync, statSync } = require("node:fs") || {};
|
||
// Use shell for simplicity
|
||
const result = execSync(`find ${dir} -type f | head -500`, { encoding: "utf8", cwd: WORKSPACE });
|
||
return result.trim().split("\n").filter(Boolean);
|
||
}
|
||
|
||
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 fileExists(dir, pattern) {
|
||
try {
|
||
const r = execSync(`find ${dir} -path "*${pattern}*" -type f 2>/dev/null | head -1`, { encoding: "utf8", cwd: WORKSPACE });
|
||
return r.trim().length > 0;
|
||
} catch { return false; }
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Step 1: RequirementPackage (SF-01 + SF-02)
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function loadJSON(path) {
|
||
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return {}; }
|
||
}
|
||
|
||
function generateRequirementPackage(domain) {
|
||
console.log(`\n📋 [${domain.id}] Generating RequirementPackage...`);
|
||
const t0 = Date.now();
|
||
|
||
// SF-01: PRD
|
||
const prdOut = join(BENCH_ROOT, domain.id, "prd.json");
|
||
mkdirSync(dirname(prdOut), { recursive: true });
|
||
runAgent(
|
||
join(SCRIPTS, "project-intake-agent.mjs"),
|
||
`--input "${domain.input}" --output "${prdOut}"`
|
||
);
|
||
const prd = loadJSON(prdOut);
|
||
|
||
// SF-02: Architecture
|
||
const archOut = join(BENCH_ROOT, domain.id, "arch.json");
|
||
runAgent(
|
||
join(SCRIPTS, "architecture-agent.mjs"),
|
||
`--input "${prdOut}" --output "${archOut}"`
|
||
);
|
||
const arch = loadJSON(archOut);
|
||
|
||
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
||
console.log(` ✅ PRD + Architecture generated (${elapsed}s)`);
|
||
console.log(` Project: ${prd.projectName || "?"}, Domain: ${prd.domain || "?"}`);
|
||
console.log(` Features: ${prd.features?.length || 0}, Pages: ${prd.pages?.length || 0}, APIs: ${prd.apiRequirements?.length || 0}`);
|
||
|
||
return { prd, arch, elapsed: parseFloat(elapsed), prdPath: prdOut, archPath: archOut };
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Step 2: Frontend Builder
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function generateFrontend(domain, prdPath, archPath) {
|
||
console.log(` 🎨 Generating Frontend...`);
|
||
const t0 = Date.now();
|
||
const outDir = join(BENCH_ROOT, domain.id, "frontend");
|
||
|
||
const result = runAgent(
|
||
join(SCRIPTS, "frontend-builder-agent.mjs"),
|
||
`--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`
|
||
);
|
||
|
||
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
||
const fileCount = countFiles(outDir);
|
||
|
||
// Validation checks
|
||
const checks = {
|
||
hasPackageJson: fileExists(outDir, "package.json"),
|
||
hasNextConfig: fileExists(outDir, "next.config.*"),
|
||
hasAppPage: fileExists(outDir, "app/page.*"),
|
||
hasEntityPages: fileExists(outDir, "app/") && countFiles(join(outDir, "app")) > 2,
|
||
hasServices: fileExists(outDir, "services/"),
|
||
hasTypes: fileExists(outDir, "types/"),
|
||
fileCount,
|
||
};
|
||
|
||
const allPassed = checks.hasPackageJson && checks.hasNextConfig && checks.hasAppPage && checks.hasEntityPages;
|
||
|
||
console.log(` ${allPassed ? "✅" : "❌"} Frontend generated (${elapsed}s, ${fileCount} files)`);
|
||
if (!allPassed) {
|
||
console.log(` Missing: ${Object.entries(checks).filter(([,v]) => !v).map(([k]) => k).join(", ")}`);
|
||
}
|
||
|
||
return { ...checks, elapsed: parseFloat(elapsed), allPassed, result };
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Step 3: Backend Builder
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function generateBackend(domain, prdPath, archPath) {
|
||
console.log(` ⚙️ Generating Backend...`);
|
||
const t0 = Date.now();
|
||
const outDir = join(BENCH_ROOT, domain.id, "backend");
|
||
|
||
const result = runAgent(
|
||
join(SCRIPTS, "backend-builder-agent.mjs"),
|
||
`--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`
|
||
);
|
||
|
||
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
||
const fileCount = countFiles(outDir);
|
||
|
||
const checks = {
|
||
hasPackageJson: fileExists(outDir, "package.json"),
|
||
hasDbSchema: fileExists(outDir, "schema") || fileExists(outDir, "db"),
|
||
hasRoutes: fileExists(outDir, "routes/"),
|
||
hasServices: fileExists(outDir, "services/"),
|
||
hasAuth: fileExists(outDir, "auth"),
|
||
hasMiddleware: fileExists(outDir, "middleware"),
|
||
hasTypes: fileExists(outDir, "types/"),
|
||
fileCount,
|
||
};
|
||
|
||
const allPassed = checks.hasPackageJson && checks.hasRoutes && checks.hasServices && checks.hasAuth;
|
||
|
||
console.log(` ${allPassed ? "✅" : "❌"} Backend generated (${elapsed}s, ${fileCount} files)`);
|
||
if (!allPassed) {
|
||
console.log(` Missing: ${Object.entries(checks).filter(([,v]) => !v).map(([k]) => k).join(", ")}`);
|
||
}
|
||
|
||
return { ...checks, elapsed: parseFloat(elapsed), allPassed, result };
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Step 4: Fullstack Composer
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function generateFullstack(domain, prdPath, archPath) {
|
||
console.log(` 🔗 Composing Fullstack...`);
|
||
const t0 = Date.now();
|
||
const outDir = join(BENCH_ROOT, domain.id, "fullstack");
|
||
|
||
const result = runAgent(
|
||
join(SCRIPTS, "fullstack-composer-agent.mjs"),
|
||
`--prd "${prdPath}" --arch "${archPath}" --output "${outDir}"`
|
||
);
|
||
|
||
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
||
const fileCount = countFiles(outDir);
|
||
|
||
const checks = {
|
||
hasWeb: fileExists(outDir, "apps/web/"),
|
||
hasApi: fileExists(outDir, "apps/api/"),
|
||
hasSharedTypes: fileExists(outDir, "packages/shared-types/"),
|
||
hasSharedConfig: fileExists(outDir, "packages/shared-config/"),
|
||
hasRootPackageJson: fileExists(outDir, "package.json"),
|
||
hasReadme: fileExists(outDir, "README.md"),
|
||
fileCount,
|
||
};
|
||
|
||
const allPassed = checks.hasWeb && checks.hasApi && checks.hasSharedTypes && checks.hasRootPackageJson;
|
||
|
||
console.log(` ${allPassed ? "✅" : "❌"} Fullstack composed (${elapsed}s, ${fileCount} files)`);
|
||
if (!allPassed) {
|
||
console.log(` Missing: ${Object.entries(checks).filter(([,v]) => !v).map(([k]) => k).join(", ")}`);
|
||
}
|
||
|
||
return { ...checks, elapsed: parseFloat(elapsed), allPassed, result };
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Step 5: Build Validation
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function validateBuild(domain) {
|
||
console.log(` 🔨 Build Validation...`);
|
||
|
||
const results = { frontend: null, backend: null, fullstack: null };
|
||
|
||
// Frontend build
|
||
const feDir = join(BENCH_ROOT, domain.id, "frontend");
|
||
if (existsSync(join(feDir, "package.json"))) {
|
||
console.log(` Frontend: npm install...`);
|
||
const install = cmd("npm install --legacy-peer-deps 2>&1", { cwd: feDir, timeout: 120_000 });
|
||
if (install.error) {
|
||
results.frontend = { build: "FAIL", reason: `npm install failed: ${install.stderr?.slice(0, 200) || install.error}` };
|
||
console.log(` ❌ npm install failed`);
|
||
} else {
|
||
console.log(` Frontend: npm run build...`);
|
||
// Next.js build may need specific config; try build, if no build script, try next build
|
||
const buildResult = cmd("npm run build 2>&1 || npx next build 2>&1", { cwd: feDir, timeout: 180_000 });
|
||
const buildFailed = buildResult.error || (typeof buildResult === 'string' && buildResult.includes('error'));
|
||
const buildErrText = typeof buildResult === 'string' ? buildResult.split('\n').filter(l => l.includes('error') || l.includes('Error') || l.includes('⨯')).slice(0,5).join(' || ') : (buildResult.stderr?.slice(0, 500) || buildResult.error);
|
||
results.frontend = {
|
||
build: buildFailed ? "FAIL" : "PASS",
|
||
error: buildFailed ? buildErrText : null
|
||
};
|
||
console.log(` ${buildFailed ? "❌" : "✅"} Frontend build ${results.frontend.build}`);
|
||
}
|
||
} else {
|
||
results.frontend = { build: "SKIP", reason: "No package.json" };
|
||
}
|
||
|
||
// Backend build
|
||
const beDir = join(BENCH_ROOT, domain.id, "backend");
|
||
if (existsSync(join(beDir, "package.json"))) {
|
||
console.log(` Backend: npm install...`);
|
||
const install = cmd("npm install --legacy-peer-deps 2>&1", { cwd: beDir, timeout: 120_000 });
|
||
if (install.error) {
|
||
results.backend = { build: "FAIL", reason: `npm install failed: ${install.stderr?.slice(0, 200) || install.error}` };
|
||
console.log(` ❌ npm install failed`);
|
||
} else {
|
||
console.log(` Backend: tsc --noEmit...`);
|
||
const buildResult = cmd("npx tsc --noEmit 2>&1", { cwd: beDir, timeout: 120_000 });
|
||
const buildFailed = buildResult.error || (typeof buildResult === 'string' && buildResult.includes('error TS'));
|
||
const buildErrText = typeof buildResult === 'string' ? buildResult.split('\n').filter(l => l.includes('error TS')).slice(0,5).join(' || ') : (buildResult.stderr?.slice(0, 500) || buildResult.error);
|
||
results.backend = {
|
||
build: buildFailed ? "FAIL" : "PASS",
|
||
error: buildFailed ? buildErrText : null
|
||
};
|
||
console.log(` ${buildFailed ? "❌" : "✅"} Backend typecheck ${results.backend.build}`);
|
||
}
|
||
} else {
|
||
results.backend = { build: "SKIP", reason: "No package.json" };
|
||
}
|
||
|
||
// Fullstack build (just verify structure, don't run full monorepo build)
|
||
const fsDir = join(BENCH_ROOT, domain.id, "fullstack");
|
||
if (existsSync(join(fsDir, "package.json"))) {
|
||
results.fullstack = { build: "PASS", reason: "Structure validated (monorepo)" };
|
||
console.log(` ✅ Fullstack structure validated`);
|
||
} else {
|
||
results.fullstack = { build: "FAIL", reason: "No root package.json" };
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Step 6: API Validation
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function validateAPI(domain) {
|
||
console.log(` 🧪 API Validation...`);
|
||
|
||
const beDir = join(BENCH_ROOT, domain.id, "backend");
|
||
const results = {};
|
||
|
||
if (!existsSync(join(beDir, "package.json"))) {
|
||
return { error: "No backend package.json" };
|
||
}
|
||
|
||
// Check route files for expected endpoints
|
||
const routesDir = join(beDir, "src", "routes");
|
||
const routeFiles = [];
|
||
try {
|
||
const find = execSync(`find ${routesDir} -name "*.ts" -type f 2>/dev/null`, { encoding: "utf8", cwd: WORKSPACE });
|
||
routeFiles.push(...find.trim().split("\n").filter(Boolean));
|
||
} catch {}
|
||
|
||
// Check for auth routes
|
||
const hasAuthRoute = routeFiles.some(f => f.includes("auth"));
|
||
const hasCRUDRoutes = routeFiles.filter(f => !f.includes("auth")).length;
|
||
|
||
// Check for health endpoint
|
||
const indexFile = join(beDir, "src", "index.ts");
|
||
let hasHealth = false;
|
||
try {
|
||
hasHealth = readFileSync(indexFile, "utf8").includes("health");
|
||
} catch {}
|
||
|
||
// Check CRUD route content
|
||
const crudOps = { create: false, read: false, update: false, delete: false };
|
||
for (const f of routeFiles) {
|
||
try {
|
||
const content = readFileSync(f, "utf8");
|
||
if (content.includes(".post(") || content.includes("app.post") || content.includes("router.post")) crudOps.create = true;
|
||
if (content.includes(".get(") || content.includes("app.get") || content.includes("router.get")) crudOps.read = true;
|
||
if (content.includes(".put(") || content.includes("app.put") || content.includes("router.put") || content.includes(".patch(") || content.includes("app.patch") || content.includes("router.patch")) crudOps.update = true;
|
||
if (content.includes(".delete(") || content.includes("app.delete") || content.includes("router.delete")) crudOps.delete = true;
|
||
} catch {}
|
||
}
|
||
|
||
// Check auth route content
|
||
let authOps = { register: false, login: false, me: false };
|
||
const authFile = routeFiles.find(f => f.includes("auth"));
|
||
if (authFile) {
|
||
try {
|
||
const content = readFileSync(authFile, "utf8");
|
||
authOps.register = content.includes("register") || content.includes("Register");
|
||
authOps.login = content.includes("login") || content.includes("Login");
|
||
authOps.me = content.includes("/me") || content.includes("'me'") || content.includes('"me"');
|
||
} catch {}
|
||
}
|
||
|
||
const crudPass = Object.values(crudOps).every(Boolean);
|
||
const authPass = authOps.register && authOps.login;
|
||
|
||
results.health = hasHealth;
|
||
results.register = authOps.register;
|
||
results.login = authOps.login;
|
||
results.me = authOps.me;
|
||
results.create = crudOps.create;
|
||
results.read = crudOps.read;
|
||
results.update = crudOps.update;
|
||
results.delete = crudOps.delete;
|
||
results.crudAllPass = crudPass;
|
||
results.authAllPass = authPass;
|
||
results.routeCount = routeFiles.length;
|
||
|
||
console.log(` Health: ${hasHealth ? "✅" : "❌"} | Auth: ${authPass ? "✅" : "❌"} | CRUD: ${crudPass ? "✅" : "❌"} | Routes: ${routeFiles.length}`);
|
||
|
||
return results;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Step 7: Report Generation
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function generateReport(allResults) {
|
||
console.log("\n\n📊 Generating Benchmark Report...\n");
|
||
|
||
const lines = [];
|
||
const h = (text) => { lines.push(text); return lines; };
|
||
|
||
h(`# Domain Benchmark Report`);
|
||
h(``);
|
||
h(`> Generated: ${new Date().toISOString()}`);
|
||
h(`> System: project-intake-agent → architecture-agent → frontend-builder → backend-builder → fullstack-composer`);
|
||
h(``);
|
||
|
||
// Summary metrics
|
||
const total = allResults.length;
|
||
const fePass = allResults.filter(r => r.frontend?.allPassed).length;
|
||
const bePass = allResults.filter(r => r.backend?.allPassed).length;
|
||
const fsPass = allResults.filter(r => r.fullstack?.allPassed).length;
|
||
const buildPass = allResults.filter(r => r.build?.frontend?.build === "PASS").length;
|
||
const typecheckPass = allResults.filter(r => r.build?.backend?.build === "PASS").length;
|
||
const crudPass = allResults.filter(r => r.api?.crudAllPass).length;
|
||
const authPass = allResults.filter(r => r.api?.authAllPass).length;
|
||
|
||
h(`## Summary`);
|
||
h(``);
|
||
h(`| Metric | Value |`);
|
||
h(`|--------|-------|`);
|
||
h(`| Total Domains | ${total} |`);
|
||
h(`| Frontend Generated | ${fePass}/${total} |`);
|
||
h(`| Backend Generated | ${bePass}/${total} |`);
|
||
h(`| Fullstack Composed | ${fsPass}/${total} |`);
|
||
h(`| Frontend Build PASS | ${buildPass}/${total} |`);
|
||
h(`| Backend Typecheck PASS | ${typecheckPass}/${total} |`);
|
||
h(`| CRUD PASS | ${crudPass}/${total} |`);
|
||
h(`| Auth PASS | ${authPass}/${total} |`);
|
||
h(``);
|
||
|
||
const avgFrontendFiles = allResults.reduce((s, r) => s + (r.frontend?.fileCount || 0), 0) / total;
|
||
const avgBackendFiles = allResults.reduce((s, r) => s + (r.backend?.fileCount || 0), 0) / total;
|
||
const avgFullstackFiles = allResults.reduce((s, r) => s + (r.fullstack?.fileCount || 0), 0) / total;
|
||
const avgReqTime = allResults.reduce((s, r) => s + (r.reqTime || 0), 0) / total;
|
||
const avgFeTime = allResults.reduce((s, r) => s + (r.frontend?.elapsed || 0), 0) / total;
|
||
const avgBeTime = allResults.reduce((s, r) => s + (r.backend?.elapsed || 0), 0) / total;
|
||
const avgFsTime = allResults.reduce((s, r) => s + (r.fullstack?.elapsed || 0), 0) / total;
|
||
const avgTotalTime = allResults.reduce((s, r) => s + (r.totalTime || 0), 0) / total;
|
||
|
||
h(`## Timing`);
|
||
h(``);
|
||
h(`| Phase | Avg Time |`);
|
||
h(`|-------|----------|`);
|
||
h(`| RequirementPackage | ${avgReqTime.toFixed(1)}s |`);
|
||
h(`| Frontend Builder | ${avgFeTime.toFixed(1)}s |`);
|
||
h(`| Backend Builder | ${avgBeTime.toFixed(1)}s |`);
|
||
h(`| Fullstack Composer | ${avgFsTime.toFixed(1)}s |`);
|
||
h(`| **Total Pipeline** | **${avgTotalTime.toFixed(1)}s** |`);
|
||
h(``);
|
||
|
||
h(`## Files Generated`);
|
||
h(``);
|
||
h(`| Layer | Avg Files |`);
|
||
h(`|-------|-----------|`);
|
||
h(`| Frontend | ${avgFrontendFiles.toFixed(0)} |`);
|
||
h(`| Backend | ${avgBackendFiles.toFixed(0)} |`);
|
||
h(`| Fullstack | ${avgFullstackFiles.toFixed(0)} |`);
|
||
h(``);
|
||
|
||
// Domain Results Table
|
||
h(`## Domain Results`);
|
||
h(``);
|
||
h(`| Domain | FE Gen | BE Gen | FS Gen | FE Build | BE Typecheck | CRUD | Auth | Total Files | Time |`);
|
||
h(`|--------|--------|--------|--------|----------|-------------|------|------|-------------|------|`);
|
||
|
||
for (const r of allResults) {
|
||
const feOk = r.frontend?.allPassed ? "✅" : "❌";
|
||
const beOk = r.backend?.allPassed ? "✅" : "❌";
|
||
const fsOk = r.fullstack?.allPassed ? "✅" : "❌";
|
||
const feBuild = r.build?.frontend?.build === "PASS" ? "✅" : r.build?.frontend?.build === "SKIP" ? "—" : "❌";
|
||
const beTC = r.build?.backend?.build === "PASS" ? "✅" : r.build?.backend?.build === "SKIP" ? "—" : "❌";
|
||
const crudOk = r.api?.crudAllPass ? "✅" : "❌";
|
||
const authOk = r.api?.authAllPass ? "✅" : "❌";
|
||
const totalFiles = (r.frontend?.fileCount || 0) + (r.backend?.fileCount || 0) + (r.fullstack?.fileCount || 0);
|
||
h(`| ${r.id} | ${feOk} | ${beOk} | ${fsOk} | ${feBuild} | ${beTC} | ${crudOk} | ${authOk} | ${totalFiles} | ${(r.totalTime || 0).toFixed(1)}s |`);
|
||
}
|
||
h(``);
|
||
|
||
// Detailed Domain Results
|
||
h(`## Detailed Results`);
|
||
h(``);
|
||
|
||
for (const r of allResults) {
|
||
h(`### ${r.id}`);
|
||
h(``);
|
||
h(`**Input:** "${r.input}"`);
|
||
h(`**Project:** ${r.projectName || "N/A"} | **Domain:** ${r.domain || "N/A"}`);
|
||
h(``);
|
||
|
||
h(`#### Frontend`);
|
||
if (r.frontend?.allPassed === false) {
|
||
const missing = Object.entries(r.frontend || {})
|
||
.filter(([k, v]) => k.startsWith("has") && !v)
|
||
.map(([k]) => k.replace("has", ""));
|
||
h(`❌ Missing: ${missing.join(", ")}`);
|
||
} else {
|
||
h(`✅ ${r.frontend?.fileCount || 0} files generated`);
|
||
}
|
||
h(``);
|
||
|
||
h(`#### Backend`);
|
||
if (r.backend?.allPassed === false) {
|
||
const missing = Object.entries(r.backend || {})
|
||
.filter(([k, v]) => k.startsWith("has") && !v)
|
||
.map(([k]) => k.replace("has", ""));
|
||
h(`❌ Missing: ${missing.join(", ")}`);
|
||
} else {
|
||
h(`✅ ${r.backend?.fileCount || 0} files generated`);
|
||
}
|
||
h(``);
|
||
|
||
h(`#### Fullstack`);
|
||
if (r.fullstack?.allPassed === false) {
|
||
const missing = Object.entries(r.fullstack || {})
|
||
.filter(([k, v]) => k.startsWith("has") && !v)
|
||
.map(([k]) => k.replace("has", ""));
|
||
h(`❌ Missing: ${missing.join(", ")}`);
|
||
} else {
|
||
h(`✅ ${r.fullstack?.fileCount || 0} files generated`);
|
||
}
|
||
h(``);
|
||
|
||
h(`#### Build`);
|
||
if (r.build?.frontend?.build === "FAIL") {
|
||
h(`❌ Frontend build failed: ${r.build.frontend.reason || r.build.frontend.error || ""}`);
|
||
} else {
|
||
h(`✅ Frontend build: ${r.build?.frontend?.build || "SKIP"}`);
|
||
}
|
||
if (r.build?.backend?.build === "FAIL") {
|
||
h(`❌ Backend typecheck failed: ${r.build.backend.reason || r.build.backend.error || ""}`);
|
||
} else {
|
||
h(`✅ Backend typecheck: ${r.build?.backend?.build || "SKIP"}`);
|
||
}
|
||
h(``);
|
||
|
||
h(`#### API`);
|
||
if (r.api) {
|
||
h(`| Endpoint | Status |`);
|
||
h(`|----------|--------|`);
|
||
h(`| Health | ${r.api.health ? "✅" : "❌"} |`);
|
||
h(`| Register | ${r.api.register ? "✅" : "❌"} |`);
|
||
h(`| Login | ${r.api.login ? "✅" : "❌"} |`);
|
||
h(`| Me | ${r.api.me ? "✅" : "❌"} |`);
|
||
h(`| Create | ${r.api.create ? "✅" : "❌"} |`);
|
||
h(`| Read | ${r.api.read ? "✅" : "❌"} |`);
|
||
h(`| Update | ${r.api.update ? "✅" : "❌"} |`);
|
||
h(`| Delete | ${r.api.delete ? "✅" : "❌"} |`);
|
||
}
|
||
h(``);
|
||
}
|
||
|
||
// Failure Analysis
|
||
h(`## Failure Analysis`);
|
||
h(``);
|
||
|
||
const failures = allResults.filter(r =>
|
||
!r.frontend?.allPassed || !r.backend?.allPassed || !r.fullstack?.allPassed ||
|
||
r.build?.frontend?.build === "FAIL" || r.build?.backend?.build === "FAIL" ||
|
||
!r.api?.crudAllPass || !r.api?.authAllPass
|
||
);
|
||
|
||
if (failures.length === 0) {
|
||
h(`✅ **All domains passed all checks!**`);
|
||
h(``);
|
||
} else {
|
||
h(`### Failure Categories`);
|
||
h(``);
|
||
|
||
// Categorize failures
|
||
const feGenFails = allResults.filter(r => !r.frontend?.allPassed);
|
||
const beGenFails = allResults.filter(r => !r.backend?.allPassed);
|
||
const fsGenFails = allResults.filter(r => !r.fullstack?.allPassed);
|
||
const feBuildFails = allResults.filter(r => r.build?.frontend?.build === "FAIL");
|
||
const beBuildFails = allResults.filter(r => r.build?.backend?.build === "FAIL");
|
||
const crudFails = allResults.filter(r => !r.api?.crudAllPass);
|
||
const authFails = allResults.filter(r => !r.api?.authAllPass);
|
||
|
||
h(`| Category | Count | Domains |`);
|
||
h(`|----------|-------|---------|`);
|
||
h(`| Frontend Generation | ${feGenFails.length} | ${feGenFails.map(r => r.id).join(", ") || "—"} |`);
|
||
h(`| Backend Generation | ${beGenFails.length} | ${beGenFails.map(r => r.id).join(", ") || "—"} |`);
|
||
h(`| Fullstack Composition | ${fsGenFails.length} | ${fsGenFails.map(r => r.id).join(", ") || "—"} |`);
|
||
h(`| Frontend Build | ${feBuildFails.length} | ${feBuildFails.map(r => r.id).join(", ") || "—"} |`);
|
||
h(`| Backend Typecheck | ${beBuildFails.length} | ${beBuildFails.map(r => r.id).join(", ") || "—"} |`);
|
||
h(`| CRUD Missing | ${crudFails.length} | ${crudFails.map(r => r.id).join(", ") || "—"} |`);
|
||
h(`| Auth Missing | ${authFails.length} | ${authFails.map(r => r.id).join(", ") || "—"} |`);
|
||
h(``);
|
||
|
||
// Detailed failure reasons
|
||
h(`### Failure Details`);
|
||
h(``);
|
||
for (const r of allResults) {
|
||
const issues = [];
|
||
if (!r.frontend?.allPassed) {
|
||
const missing = Object.entries(r.frontend || {}).filter(([k, v]) => k.startsWith("has") && !v).map(([k]) => k.replace("has", ""));
|
||
issues.push(`Frontend: missing ${missing.join(", ")}`);
|
||
}
|
||
if (!r.backend?.allPassed) {
|
||
const missing = Object.entries(r.backend || {}).filter(([k, v]) => k.startsWith("has") && !v).map(([k]) => k.replace("has", ""));
|
||
issues.push(`Backend: missing ${missing.join(", ")}`);
|
||
}
|
||
if (!r.fullstack?.allPassed) {
|
||
const missing = Object.entries(r.fullstack || {}).filter(([k, v]) => k.startsWith("has") && !v).map(([k]) => k.replace("has", ""));
|
||
issues.push(`Fullstack: missing ${missing.join(", ")}`);
|
||
}
|
||
if (r.build?.frontend?.build === "FAIL") issues.push(`Frontend build: ${r.build.frontend.error || r.build.frontend.reason}`);
|
||
if (r.build?.backend?.build === "FAIL") issues.push(`Backend typecheck: ${r.build.backend.error || r.build.backend.reason}`);
|
||
if (!r.api?.crudAllPass) {
|
||
const missing = Object.entries({create: r.api?.create, read: r.api?.read, update: r.api?.update, delete: r.api?.delete})
|
||
.filter(([,v]) => !v).map(([k]) => k);
|
||
issues.push(`CRUD: missing ${missing.join(", ")}`);
|
||
}
|
||
if (!r.api?.authAllPass) {
|
||
const missing = Object.entries({register: r.api?.register, login: r.api?.login, me: r.api?.me})
|
||
.filter(([,v]) => !v).map(([k]) => k);
|
||
issues.push(`Auth: missing ${missing.join(", ")}`);
|
||
}
|
||
|
||
if (issues.length > 0) {
|
||
h(`**${r.id}:**`);
|
||
for (const i of issues) h(`- ${i}`);
|
||
h(``);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Pattern Analysis
|
||
h(`## Pattern Analysis`);
|
||
h(``);
|
||
|
||
// Best domains
|
||
const scored = allResults.map(r => {
|
||
let score = 0;
|
||
if (r.frontend?.allPassed) score += 2;
|
||
if (r.backend?.allPassed) score += 2;
|
||
if (r.fullstack?.allPassed) score += 1;
|
||
if (r.build?.frontend?.build === "PASS") score += 2;
|
||
if (r.build?.backend?.build === "PASS") score += 2;
|
||
if (r.api?.crudAllPass) score += 2;
|
||
if (r.api?.authAllPass) score += 2;
|
||
return { ...r, score };
|
||
}).sort((a, b) => b.score - a.score);
|
||
|
||
h(`### Best Performing Domains`);
|
||
h(``);
|
||
h(`| Domain | Score | Key Strength |`);
|
||
h(`|--------|-------|-------------|`);
|
||
for (const r of scored.slice(0, 3)) {
|
||
h(`| ${r.id} | ${r.score}/13 | ${r.domain || "N/A"} |`);
|
||
}
|
||
h(``);
|
||
|
||
h(`### Weakest Domains`);
|
||
h(``);
|
||
h(`| Domain | Score | Key Issue |`);
|
||
h(`|--------|-------|-----------|`);
|
||
for (const r of scored.slice(-3).reverse()) {
|
||
const issues = [];
|
||
if (!r.frontend?.allPassed) issues.push("FE gen");
|
||
if (!r.backend?.allPassed) issues.push("BE gen");
|
||
if (r.build?.frontend?.build === "FAIL") issues.push("FE build");
|
||
if (r.build?.backend?.build === "FAIL") issues.push("BE typecheck");
|
||
if (!r.api?.crudAllPass) issues.push("CRUD");
|
||
if (!r.api?.authAllPass) issues.push("Auth");
|
||
h(`| ${r.id} | ${r.score}/13 | ${issues.join(", ") || "—"} |`);
|
||
}
|
||
h(``);
|
||
|
||
h(`### Domain Type Analysis`);
|
||
h(``);
|
||
h(`| Domain Type | Count | Avg Score |`);
|
||
h(`|-------------|-------|-----------|`);
|
||
const byDomain = {};
|
||
for (const r of allResults) {
|
||
const d = r.domain || "unknown";
|
||
if (!byDomain[d]) byDomain[d] = { count: 0, scores: [] };
|
||
byDomain[d].count++;
|
||
byDomain[d].scores.push(r.score);
|
||
}
|
||
for (const [domain, data] of Object.entries(byDomain)) {
|
||
const avg = (data.scores.reduce((a,b) => a + b, 0) / data.scores.length).toFixed(1);
|
||
h(`| ${domain} | ${data.count} | ${avg} |`);
|
||
}
|
||
h(``);
|
||
|
||
// Failure categories (defined above already, but recompute for Fixes section)
|
||
const feGenFailsR = allResults.filter(r => !r.frontend?.allPassed);
|
||
const beGenFailsR = allResults.filter(r => !r.backend?.allPassed);
|
||
const fsGenFailsR = allResults.filter(r => !r.fullstack?.allPassed);
|
||
const feBuildFailsR = allResults.filter(r => r.build?.frontend?.build === "FAIL");
|
||
const beBuildFailsR = allResults.filter(r => r.build?.backend?.build === "FAIL");
|
||
const crudFailsR = allResults.filter(r => !r.api?.crudAllPass);
|
||
const authFailsR = allResults.filter(r => !r.api?.authAllPass);
|
||
|
||
// Required Fixes
|
||
h(`## Required Fixes`);
|
||
h(``);
|
||
|
||
const criticalFixes = [];
|
||
const highFixes = [];
|
||
const mediumFixes = [];
|
||
|
||
// Analyze patterns
|
||
if (feBuildFailsR.length > 0) {
|
||
criticalFixes.push(`**Frontend Build Failures** (${feBuildFailsR.length}/${total}): Fix Next.js build issues in frontend-builder-agent — check tsconfig, module resolution, component imports`);
|
||
}
|
||
if (beBuildFailsR.length > 0) {
|
||
criticalFixes.push(`**Backend Typecheck Failures** (${beBuildFailsR.length}/${total}): Fix TypeScript errors in backend-builder-agent — check type definitions, import paths`);
|
||
}
|
||
if (beGenFailsR.length > 0) {
|
||
highFixes.push(`**Backend Generation Gaps** (${beGenFailsR.length}/${total}): Missing routes/services/auth in some domains`);
|
||
}
|
||
if (crudFailsR.length > 0) {
|
||
highFixes.push(`**CRUD Completeness** (${crudFailsR.length}/${total}): Some domains missing full CRUD operations`);
|
||
}
|
||
if (authFailsR.length > 0) {
|
||
highFixes.push(`**Auth Endpoint Coverage** (${authFailsR.length}/${total}): Missing register/login/me in some domains`);
|
||
}
|
||
if (feGenFailsR.length > 0) {
|
||
mediumFixes.push(`**Frontend Generation Gaps** (${feGenFailsR.length}/${total}): Missing pages or services in some domains`);
|
||
}
|
||
|
||
h(`### P0 — Critical`);
|
||
h(``);
|
||
if (criticalFixes.length === 0) {
|
||
h(`✅ No critical issues found.`);
|
||
} else {
|
||
for (const f of criticalFixes) h(`1. ${f}`);
|
||
}
|
||
h(``);
|
||
|
||
h(`### P1 — High`);
|
||
h(``);
|
||
if (highFixes.length === 0) {
|
||
h(`✅ No high-priority issues found.`);
|
||
} else {
|
||
for (let i = 0; i < highFixes.length; i++) h(`${i + 1}. ${highFixes[i]}`);
|
||
}
|
||
h(``);
|
||
|
||
h(`### P2 — Medium`);
|
||
h(``);
|
||
if (mediumFixes.length === 0) {
|
||
h(`✅ No medium-priority issues found.`);
|
||
} else {
|
||
for (let i = 0; i < mediumFixes.length; i++) h(`${i + 1}. ${mediumFixes[i]}`);
|
||
}
|
||
h(``);
|
||
|
||
// Conclusion
|
||
h(`## Conclusion`);
|
||
h(``);
|
||
const passRate = allResults.filter(r => r.score >= 11).length;
|
||
if (passRate === total) {
|
||
h(`🎉 **All ${total} domains passed!** The system demonstrates robust generalization capability across diverse business domains.`);
|
||
h(``);
|
||
h(`The RequirementPackage → Frontend Builder → Backend Builder → Fullstack Composer pipeline is production-ready for Web Fullstack generation.`);
|
||
h(``);
|
||
h(`**Next: Electron Builder can now proceed.**`);
|
||
} else {
|
||
h(`⚠️ **${passRate}/${total} domains fully passed.**`);
|
||
h(``);
|
||
h(`The pipeline shows partial generalization. ${total - passRate} domains need fixes before the system can be considered a truly general Web Fullstack Generator.`);
|
||
h(``);
|
||
h(`**Electron Builder should wait until fixes above are addressed.**`);
|
||
}
|
||
h(``);
|
||
|
||
h(`---`);
|
||
h(`*Report generated by Domain Benchmark Suite*`);
|
||
|
||
return lines.join("\n");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Main
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
async function main() {
|
||
const args = process.argv.slice(2);
|
||
const filterDomain = args[0] === "--domain" ? args[1] : null;
|
||
const skipBuild = args.includes("--skip-build");
|
||
const skipAPI = args.includes("--skip-api");
|
||
|
||
// Filter domains if specified
|
||
const domains = filterDomain
|
||
? DOMAINS.filter(d => d.id === filterDomain)
|
||
: DOMAINS;
|
||
|
||
if (domains.length === 0) {
|
||
console.error(`Unknown domain: ${filterDomain}`);
|
||
console.error(`Available: ${DOMAINS.map(d => d.id).join(", ")}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(`\n🧪 Domain Benchmark Suite`);
|
||
console.log(` Testing ${domains.length} domain(s)`);
|
||
console.log(` Pipeline: project-intake → architecture → frontend → backend → fullstack`);
|
||
if (skipBuild) console.log(` ⏭️ Skipping build validation`);
|
||
if (skipAPI) console.log(` ⏭️ Skipping API validation`);
|
||
console.log(``);
|
||
|
||
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();
|
||
|
||
// Step 1: RequirementPackage
|
||
const { prd, arch, elapsed: reqTime, prdPath, archPath } = generateRequirementPackage(domain);
|
||
|
||
// Step 2: Frontend
|
||
const frontend = generateFrontend(domain, prdPath, archPath);
|
||
|
||
// Step 3: Backend
|
||
const backend = generateBackend(domain, prdPath, archPath);
|
||
|
||
// Step 4: Fullstack
|
||
const fullstack = generateFullstack(domain, prdPath, archPath);
|
||
|
||
// Step 5: Build (optional)
|
||
let build = null;
|
||
if (!skipBuild) {
|
||
build = validateBuild(domain);
|
||
}
|
||
|
||
// Step 6: API (optional)
|
||
let api = null;
|
||
if (!skipAPI) {
|
||
api = validateAPI(domain);
|
||
}
|
||
|
||
const totalTime = ((Date.now() - tTotal) / 1000).toFixed(1);
|
||
|
||
allResults.push({
|
||
id: domain.id,
|
||
input: domain.input,
|
||
projectName: prd.projectName,
|
||
domain: prd.domain,
|
||
reqTime,
|
||
frontend,
|
||
backend,
|
||
fullstack,
|
||
build,
|
||
api,
|
||
totalTime: parseFloat(totalTime),
|
||
});
|
||
|
||
console.log(`\n ⏱️ Total: ${totalTime}s`);
|
||
}
|
||
|
||
// Generate report
|
||
const report = generateReport(allResults);
|
||
writeFileSync(REPORT_PATH, report, "utf8");
|
||
console.log(`\n📄 Report written to: ${REPORT_PATH}`);
|
||
|
||
// Print summary
|
||
const passCount = allResults.filter(r => {
|
||
return r.frontend?.allPassed && r.backend?.allPassed && r.fullstack?.allPassed;
|
||
}).length;
|
||
|
||
console.log(`\n📊 Summary: ${passCount}/${domains.length} domains generated successfully`);
|
||
|
||
// Also output JSON for programmatic consumption
|
||
const jsonPath = resolve(WORKSPACE, "benchmark-results.json");
|
||
writeFileSync(jsonPath, JSON.stringify(allResults, null, 2), "utf8");
|
||
console.log(`📄 JSON results: ${jsonPath}`);
|
||
}
|
||
|
||
main().catch(e => {
|
||
console.error("Benchmark failed:", e);
|
||
process.exit(1);
|
||
});
|