Spaces:
Runtime error
Runtime error
| // scripts/quality/run-all-gates.mjs | |
| // Agregador paralelo de quality gates determinΓsticos. | |
| // Roda os gates filesystem-only em paralelo (pool ~4), agrega resultados, e exibe | |
| // uma tabela consolidada com {gate, status, durationMs}. Sai com cΓ³digo 1 se | |
| // qualquer gate falhar. Alvo: < 3 min no total. | |
| // | |
| // Usage: | |
| // node scripts/quality/run-all-gates.mjs # run all gates | |
| // node scripts/quality/run-all-gates.mjs --fast # skip slow gates (duplication) | |
| // | |
| // Via npm: npm run quality:scan | |
| import { spawn } from "node:child_process"; | |
| import { pathToFileURL } from "node:url"; | |
| const FAST_ONLY = process.argv.includes("--fast"); | |
| // Gates determinΓsticos filesystem-only, agrupados por tempo estimado. | |
| // Cada entrada: { name: string, cmd: string[], label?: string } | |
| // "slow" gates sΓ£o omitidos com --fast. | |
| const GATES = [ | |
| // Group A β instant (<1s) | |
| { name: "check:tracked-artifacts", cmd: ["node", "scripts/check/check-tracked-artifacts.mjs"] }, | |
| { name: "check:any-budget:t11", cmd: ["node", "scripts/check/check-t11-any-budget.mjs"] }, | |
| { name: "check:migration-numbering", cmd: ["node", "scripts/check/check-migration-numbering.mjs"] }, | |
| { name: "check:node-runtime", cmd: ["node", "--import", "tsx", "scripts/check/check-supported-node-runtime.ts"] }, | |
| // Group B β fast (<5s) | |
| { name: "check:provider-consistency", cmd: ["node", "--import", "tsx", "scripts/check/check-provider-consistency.ts"] }, | |
| { name: "check:provider-assets", cmd: ["node", "scripts/check/check-provider-assets.mjs"] }, | |
| { name: "check:public-creds", cmd: ["node", "scripts/check/check-public-creds.mjs"] }, | |
| { name: "check:error-helper", cmd: ["node", "scripts/check/check-error-helper.mjs"] }, | |
| { name: "check:fetch-targets", cmd: ["node", "scripts/check/check-fetch-targets.mjs"] }, | |
| { name: "check:openapi-routes", cmd: ["node", "scripts/check/check-openapi-routes.mjs"] }, | |
| { name: "check:deps", cmd: ["node", "scripts/check/check-deps.mjs"] }, | |
| // Group C β moderate (<15s) | |
| { name: "check:db-rules", cmd: ["node", "scripts/check/check-db-rules.mjs"] }, | |
| { name: "check:file-size", cmd: ["node", "scripts/check/check-file-size.mjs"] }, | |
| { name: "check:complexity", cmd: ["node", "scripts/check/check-complexity.mjs"] }, | |
| { name: "check:docs-symbols", cmd: ["node", "scripts/check/check-docs-symbols.mjs"] }, | |
| { name: "check:known-symbols", cmd: ["node", "--import", "tsx", "scripts/check/check-known-symbols.ts"] }, | |
| { name: "check:route-guard-membership", cmd: ["node", "--import", "tsx", "scripts/check/check-route-guard-membership.ts"] }, | |
| { name: "check:test-discovery", cmd: ["node", "scripts/check/check-test-discovery.mjs"] }, | |
| { name: "check:test-masking", cmd: ["node", "scripts/check/check-test-masking.mjs"] }, | |
| // Group D β slow (>15s); skipped with --fast | |
| { name: "check:duplication", cmd: ["node", "scripts/check/check-duplication.mjs"], slow: true }, | |
| { name: "check:cycles", cmd: ["node", "scripts/check/check-cycles.mjs"], slow: true }, | |
| ]; | |
| const CONCURRENCY = 4; | |
| /** | |
| * Run a single gate command, capturing last line of stdout/stderr. | |
| * @param {{ name: string, cmd: string[] }} gate | |
| * @returns {Promise<{ name: string, exitCode: number, durationMs: number, lastLine: string }>} | |
| */ | |
| function runGate(gate) { | |
| return new Promise((resolve) => { | |
| const start = Date.now(); | |
| const [bin, ...args] = gate.cmd; | |
| const proc = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], cwd: process.cwd() }); | |
| const lines = []; | |
| const collectLine = (chunk) => { | |
| const text = chunk.toString(); | |
| for (const line of text.split("\n")) { | |
| const t = line.trim(); | |
| if (t) lines.push(t); | |
| } | |
| }; | |
| proc.stdout.on("data", collectLine); | |
| proc.stderr.on("data", collectLine); | |
| proc.on("close", (code) => { | |
| const durationMs = Date.now() - start; | |
| const lastLine = lines[lines.length - 1] ?? ""; | |
| resolve({ name: gate.name, exitCode: code ?? 1, durationMs, lastLine }); | |
| }); | |
| proc.on("error", (err) => { | |
| const durationMs = Date.now() - start; | |
| resolve({ name: gate.name, exitCode: 1, durationMs, lastLine: err.message }); | |
| }); | |
| }); | |
| } | |
| /** | |
| * Run gates with a concurrency pool. | |
| * @param {typeof GATES} gates | |
| * @param {number} concurrency | |
| * @returns {Promise<ReturnType<typeof runGate>[]>} | |
| */ | |
| async function runWithPool(gates, concurrency) { | |
| const results = []; | |
| let idx = 0; | |
| async function worker() { | |
| while (idx < gates.length) { | |
| const gate = gates[idx++]; | |
| const result = await runGate(gate); | |
| results.push(result); | |
| } | |
| } | |
| const workers = Array.from({ length: Math.min(concurrency, gates.length) }, () => worker()); | |
| await Promise.all(workers); | |
| return results; | |
| } | |
| function formatTable(results) { | |
| const COL_NAME = 35; | |
| const COL_STATUS = 8; | |
| const COL_MS = 10; | |
| const COL_MSG = 60; | |
| const header = | |
| " " + | |
| "GATE".padEnd(COL_NAME) + | |
| "STATUS".padEnd(COL_STATUS) + | |
| "TIME(ms)".padEnd(COL_MS) + | |
| "LAST LINE"; | |
| const separator = " " + "β".repeat(COL_NAME + COL_STATUS + COL_MS + COL_MSG); | |
| const rows = results.map((r) => { | |
| const status = r.exitCode === 0 ? "PASS" : "FAIL"; | |
| const name = r.name.padEnd(COL_NAME); | |
| const statusCol = (r.exitCode === 0 ? "β " + status : "β " + status).padEnd(COL_STATUS + 2); | |
| const ms = String(r.durationMs).padStart(6) + "ms "; | |
| const msg = r.lastLine.slice(0, COL_MSG); | |
| return ` ${name}${statusCol}${ms}${msg}`; | |
| }); | |
| return [separator, header, separator, ...rows, separator].join("\n"); | |
| } | |
| async function main() { | |
| const gates = FAST_ONLY ? GATES.filter((g) => !g.slow) : GATES; | |
| console.log(`\n[quality:scan] Running ${gates.length} gate(s) with concurrency=${CONCURRENCY}...\n`); | |
| const wallStart = Date.now(); | |
| const results = await runWithPool(gates, CONCURRENCY); | |
| const wallMs = Date.now() - wallStart; | |
| const failed = results.filter((r) => r.exitCode !== 0); | |
| const passed = results.filter((r) => r.exitCode === 0); | |
| console.log(formatTable(results)); | |
| console.log( | |
| `\n Summary: ${passed.length} passed, ${failed.length} failed` + | |
| ` | Total: ${(wallMs / 1000).toFixed(1)}s (wall clock)\n` | |
| ); | |
| if (failed.length > 0) { | |
| console.error(`[quality:scan] FAIL β ${failed.length} gate(s) failed:`); | |
| for (const r of failed) { | |
| console.error(` β ${r.name}`); | |
| } | |
| process.exit(1); | |
| } | |
| console.log("[quality:scan] All gates passed."); | |
| } | |
| if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main(); | |