#!/usr/bin/env node /** * Regression Gate — SF-08 * * 执行所有验证 gate,全部通过才允许发布。 * Exit 0 = PASS, Exit 1 = FAIL. * * Usage: * node scripts/regression-gate.mjs [--strict] * * Gates: * 1. 10 Domain E2E Benchmark * 2. 170 Regression Tests * 3. 5 Domain Semantic Spot-Check * * @module regression-gate */ import { execSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(resolve(fileURLToPath(import.meta.url))); const WORKSPACE = resolve(__dirname, ".."); const STRICT = process.argv.includes("--strict"); // ═══════════════════════════════════════════════════════ // Gate Definitions // ═══════════════════════════════════════════════════════ const SEMANTIC_SPOTCHECK = [ { name: "BookShelf", input: "做一个图书管理系统,支持书籍库、阅读状态、阅读进度、书评和统计", expectTables: ["books", "reading_status"] }, { name: "TeamFlow", input: "做一个团队协作工具,支持看板视图、任务管理、团队成员和活动日志", expectTables: ["boards", "tasks"] }, { name: "Contract", input: "做一个合同管理工具,支持合同创建、审批流程、到期提醒和合同归档", expectTables: ["contracts", "approvals"] }, { name: "Inspection", input: "做一个设备巡检系统,支持巡检计划、巡检记录、故障上报和设备台账", expectTables: ["equipment", "faults"] }, { name: "Warehouse", input: "做一个仓库出入库工具,支持入库登记、出库审批、库存盘点和库存预警", expectTables: ["stock_in", "stock_out"] }, ]; // ═══════════════════════════════════════════════════════ // Helpers // ═══════════════════════════════════════════════════════ function run(cmd, label) { const start = Date.now(); try { const out = execSync(cmd, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe", timeout: 180_000 }); const ms = Date.now() - start; return { pass: true, ms, output: out }; } catch (e) { const ms = Date.now() - start; return { pass: false, ms, output: e.stderr || e.stdout || e.message }; } } function gateHeader(n, label) { console.log(`\n${"═".repeat(60)}`); console.log(` GATE ${n}: ${label}`); console.log(`${"═".repeat(60)}`); } // ═══════════════════════════════════════════════════════ // Gate 1: 10 Domain E2E Benchmark // ═══════════════════════════════════════════════════════ function gateBenchmark() { gateHeader(1, "10 Domain E2E Benchmark"); const r = run("node scripts/e2e-benchmark-v1.mjs", "benchmark"); const passMatch = r.output.match(/(\d+)\/(\d+) PASS/); const passCount = passMatch ? parseInt(passMatch[1]) : 0; const total = passMatch ? parseInt(passMatch[2]) : 0; const pass = passCount === 10 && total === 10; console.log(` Result: ${passCount}/${total} PASS (${r.ms}ms)`); console.log(` Status: ${pass ? "✅ PASS" : "❌ FAIL"}`); return pass; } // ═══════════════════════════════════════════════════════ // Gate 2: 170 Regression Tests // ═══════════════════════════════════════════════════════ function gateRegression() { gateHeader(2, "170 Regression Tests"); const r = run("node --test test/e2e-regression.test.mjs", "regression"); const passMatch = r.output.match(/pass (\d+)/); const failMatch = r.output.match(/fail (\d+)/); const passCount = passMatch ? parseInt(passMatch[1]) : 0; const failCount = failMatch ? parseInt(failMatch[1]) : 0; const pass = passCount === 170 && failCount === 0; console.log(` Result: ${passCount} pass, ${failCount} fail (${r.ms}ms)`); console.log(` Status: ${pass ? "✅ PASS" : "❌ FAIL"}`); return pass; } // ═══════════════════════════════════════════════════════ // Gate 3: Semantic Spot-Check // ═══════════════════════════════════════════════════════ function gateSemantic() { gateHeader(3, "5 Domain Semantic Spot-Check"); let allPass = true; for (const tc of SEMANTIC_SPOTCHECK) { const tmpDir = `/tmp/gate-${tc.name.toLowerCase()}`; try { execSync(`node scripts/project-intake-agent.mjs --input "${tc.input}" --output ${tmpDir}/prd.json`, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe" }); execSync(`node scripts/architecture-agent.mjs --input ${tmpDir}/prd.json --output ${tmpDir}/arch.json`, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe" }); const arch = JSON.parse(readFileSync(`${tmpDir}/arch.json`, "utf8")); const tables = arch.databaseSchema.map(t => t.table); const hasChinese = tables.some(t => /[\u4e00-\u9fff]/.test(t)); const hasExpected = tc.expectTables.every(et => tables.includes(et)); const hasDomainFields = arch.databaseSchema.filter(t => t.table !== "users").some(t => { const fields = t.fields.filter(f => !["id", "user_id", "created_at", "updated_at"].includes(f.name)); return fields.some(f => !["title", "description", "status", "data"].includes(f.name)); }); const pass = !hasChinese && hasExpected && hasDomainFields; console.log(` ${pass ? "✅" : "❌"} ${tc.name}: tables=[${tables.join(", ")}]${hasChinese ? " [CHINESE!]" : ""}${!hasExpected ? " [MISSING TABLES!]" : ""}${!hasDomainFields ? " [GENERIC FIELDS!]" : ""}`); if (!pass) allPass = false; } catch (e) { console.log(` ❌ ${tc.name}: ${e.message.split("\n")[0]}`); allPass = false; } } console.log(`\n Status: ${allPass ? "✅ PASS" : "❌ FAIL"}`); return allPass; } // ═══════════════════════════════════════════════════════ // Main // ═══════════════════════════════════════════════════════ async function main() { console.log("🔒 Regression Gate — v1.1.0-semantic"); console.log(` Mode: ${STRICT ? "STRICT" : "NORMAL"}`); console.log(` Time: ${new Date().toISOString()}`); const results = { benchmark: gateBenchmark(), regression: gateRegression(), semantic: gateSemantic(), }; console.log(`\n${"═".repeat(60)}`); console.log(" SUMMARY"); console.log(`${"═".repeat(60)}`); for (const [name, pass] of Object.entries(results)) { console.log(` ${pass ? "✅" : "❌"} ${name}`); } const allPass = Object.values(results).every(Boolean); console.log(`\n ${allPass ? "🟢 ALL GATES PASS — Release approved" : "🔴 GATE FAILED — Release blocked"}`); console.log(""); process.exit(allPass ? 0 : 1); } main().catch(e => { console.error("Gate runner crashed:", e); process.exit(1); });