mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 22:02:08 +03:00
feat(quality): generic ratchet comparator (multi-metric, regression-only)
This commit is contained in:
89
scripts/quality/check-quality-ratchet.mjs
Normal file
89
scripts/quality/check-quality-ratchet.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/quality/check-quality-ratchet.mjs
|
||||
// Catraca genérica multi-métrica. Clona o espírito de check-t11-any-budget.mjs:
|
||||
// um baseline congelado por métrica; falha em qualquer regressão; só anda num sentido.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const cwd = process.cwd();
|
||||
function getArg(name, fallback) {
|
||||
const i = process.argv.indexOf(name);
|
||||
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
|
||||
}
|
||||
const BASELINE = path.resolve(getArg("--baseline", path.join(cwd, "quality-baseline.json")));
|
||||
const METRICS = path.resolve(getArg("--metrics", path.join(cwd, "quality-metrics.json")));
|
||||
const SUMMARY = getArg("--summary", null);
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
const EPS = 0.01;
|
||||
|
||||
function load(p) {
|
||||
if (!fs.existsSync(p)) {
|
||||
console.error(`[quality-ratchet] arquivo ausente: ${p}`);
|
||||
process.exit(2);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(p, "utf8"));
|
||||
}
|
||||
|
||||
const baseline = load(BASELINE);
|
||||
const metrics = load(METRICS);
|
||||
const failures = [];
|
||||
const improvements = [];
|
||||
const rows = [];
|
||||
|
||||
for (const [key, spec] of Object.entries(baseline.metrics)) {
|
||||
const current = metrics[key];
|
||||
const base = spec.value;
|
||||
const dir = spec.direction; // "down" = menor-é-melhor | "up" = maior-é-melhor
|
||||
if (current === undefined) {
|
||||
failures.push(`métrica "${key}" ausente em ${path.basename(METRICS)}`);
|
||||
rows.push([key, base, "—", "MISSING"]);
|
||||
continue;
|
||||
}
|
||||
let status = "ok";
|
||||
if (dir === "down") {
|
||||
if (current > base + EPS) {
|
||||
failures.push(`${key}: ${current} > baseline ${base} (não pode aumentar)`);
|
||||
status = "REGRESSÃO";
|
||||
} else if (current < base - EPS) {
|
||||
improvements.push([key, current]);
|
||||
status = "↑ melhorou";
|
||||
}
|
||||
} else {
|
||||
if (current < base - EPS) {
|
||||
failures.push(`${key}: ${current} < baseline ${base} (não pode cair)`);
|
||||
status = "REGRESSÃO";
|
||||
} else if (current > base + EPS) {
|
||||
improvements.push([key, current]);
|
||||
status = "↑ melhorou";
|
||||
}
|
||||
}
|
||||
rows.push([key, base, current, status]);
|
||||
}
|
||||
|
||||
if (SUMMARY) {
|
||||
const md = [
|
||||
"# Quality Ratchet",
|
||||
"",
|
||||
"| Métrica | Baseline | Atual | Status |",
|
||||
"|---|---|---|---|",
|
||||
...rows.map(([k, b, c, s]) => `| ${k} | ${b} | ${c} | ${s} |`),
|
||||
"",
|
||||
failures.length
|
||||
? `**${failures.length} regressão(ões) — gate BLOQUEADO.**`
|
||||
: "**Sem regressões — gate OK.**",
|
||||
].join("\n");
|
||||
fs.mkdirSync(path.dirname(SUMMARY), { recursive: true });
|
||||
fs.writeFileSync(SUMMARY, md + "\n");
|
||||
}
|
||||
|
||||
if (UPDATE && failures.length === 0 && improvements.length) {
|
||||
for (const [key, val] of improvements) baseline.metrics[key].value = val;
|
||||
fs.writeFileSync(BASELINE, JSON.stringify(baseline, null, 2) + "\n");
|
||||
console.log(`[quality-ratchet] baseline ratcheado: ${improvements.length} métrica(s) melhoraram`);
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error("[quality-ratchet] FALHOU:\n" + failures.map((f) => " ✗ " + f).join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`[quality-ratchet] OK (${rows.length} métricas, ${improvements.length} melhoraram)`);
|
||||
59
tests/unit/quality-ratchet.test.ts
Normal file
59
tests/unit/quality-ratchet.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const SCRIPT = path.resolve("scripts/quality/check-quality-ratchet.mjs");
|
||||
|
||||
function run(baseline: unknown, metrics: unknown, extraArgs: string[] = []) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ratchet-"));
|
||||
const bPath = path.join(dir, "baseline.json");
|
||||
const mPath = path.join(dir, "metrics.json");
|
||||
fs.writeFileSync(bPath, JSON.stringify(baseline));
|
||||
fs.writeFileSync(mPath, JSON.stringify(metrics));
|
||||
try {
|
||||
const out = execFileSync("node", [SCRIPT, "--baseline", bPath, "--metrics", mPath, ...extraArgs], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
return { code: 0, out, dir, bPath };
|
||||
} catch (e: any) {
|
||||
return { code: e.status as number, out: (e.stdout || "") + (e.stderr || ""), dir, bPath };
|
||||
}
|
||||
}
|
||||
|
||||
test("passes when metrics equal baseline", () => {
|
||||
const b = {
|
||||
metrics: {
|
||||
eslintWarnings: { value: 100, direction: "down" },
|
||||
"coverage.lines": { value: 80, direction: "up" },
|
||||
},
|
||||
};
|
||||
assert.equal(run(b, { eslintWarnings: 100, "coverage.lines": 80 }).code, 0);
|
||||
});
|
||||
|
||||
test("fails when a 'down' metric regresses (more warnings)", () => {
|
||||
const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
|
||||
const r = run(b, { eslintWarnings: 101 });
|
||||
assert.equal(r.code, 1);
|
||||
assert.match(r.out, /eslintWarnings/);
|
||||
});
|
||||
|
||||
test("fails when an 'up' metric regresses (coverage drops)", () => {
|
||||
const b = { metrics: { "coverage.lines": { value: 80, direction: "up" } } };
|
||||
assert.equal(run(b, { "coverage.lines": 79 }).code, 1);
|
||||
});
|
||||
|
||||
test("passes on improvement; --update ratchets the baseline", () => {
|
||||
const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
|
||||
const r = run(b, { eslintWarnings: 90 }, ["--update"]);
|
||||
assert.equal(r.code, 0);
|
||||
const updated = JSON.parse(fs.readFileSync(r.bPath, "utf8"));
|
||||
assert.equal(updated.metrics.eslintWarnings.value, 90);
|
||||
});
|
||||
|
||||
test("fails when a baseline metric is missing from collected metrics", () => {
|
||||
const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
|
||||
assert.equal(run(b, {}).code, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user