mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
Owner decision (2026-08-30): shipping speed matters more than holding the debt line
until the v4.0 LTS modularization; the base was going red on every merge batch and
each red baseline cost a sweep.
Relaxation (one auditable pass, scripts/quality/relax-baselines.mjs):
- quality-baseline.json metrics: lower-is-better ×1.2, higher-is-better ÷1.2
(coverage floor 60 kept; eslintErrors stays 0; eslintWarnings 0 → 1050 = 20% of
the 5,247 frozen suppressions). Adds `_policy {phase: velocity, until: 4.0.0,
relaxPct: 20, requireTighten: false}` + a `_relax_velocity_2026_08_30` note
listing every before → after.
- complexity count 2681 → 3218; duplication 5.72 → 6.86; file-size cap/testCap
1000 → 1200 and all 127 frozen caps ×1.2; api/dashboard/open-sse typecheck
per-file counts ×1.2; openapi-coverage THRESHOLD 36 → 30.
- check-quality-ratchet: --require-tighten is advisory while _policy.requireTighten
is false (2 new tests); nightly bank-ratchet-shrinks pauses during the phase (it
would bank the measured shrink and undo the headroom every night).
Monitoring (scripts/quality/baseline-headroom.mjs, npm run quality:headroom):
measures each numeric gate the way CI does, prints live / baseline / headroom per
gate (ok ≥10%, warn <10%, critical <0); the new nightly `baseline-headroom` job
posts the table to the living issue "📈 Baseline headroom (velocity phase)" and
toggles the `headroom-alert` label. 6 unit tests on the pure helpers.
Also aligns the remaining red tests on the tip to contracts already merged:
#11775 (FREE lease-capable connections are ordinary capacity: gate inventory 48/97/99,
sse-auth selection, warmup scheduler), #11794 (dual-loopback readiness probe), and the
8 vi strings #11775 left as __MISSING__.
Docs: QUALITY_GATES.md → "Velocity phase" (what changed, tooling, how to close the
phase at 4.0), AGENTS.md quick reference.
165 lines
6.9 KiB
JavaScript
165 lines
6.9 KiB
JavaScript
#!/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.
|
|
//
|
|
// v2 (6A.5): --require-tighten, eps por métrica, warning de métricas órfãs.
|
|
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, "config/quality/quality-baseline.json"))
|
|
);
|
|
const METRICS = path.resolve(
|
|
getArg("--metrics", path.join(cwd, "config/quality/quality-metrics.json"))
|
|
);
|
|
const SUMMARY = getArg("--summary", null);
|
|
const UPDATE = process.argv.includes("--update");
|
|
// --allow-missing: pula métricas do baseline ausentes do metrics (em vez de falhar).
|
|
// Uso local: cobertura só existe no CI; localmente quality:gate roda com este flag.
|
|
// No CI o job quality-gate roda SEM o flag (estrito — baixa o coverage mergeado antes).
|
|
const ALLOW_MISSING = process.argv.includes("--allow-missing");
|
|
// --require-tighten: falha quando uma métrica melhorou além de tightenSlack sem que o
|
|
// baseline tenha sido apertado. Garante que melhorias permanentes sejam capturadas.
|
|
// Sem esta flag, melhorias são apenas registradas (comportamento v1 — retrocompat).
|
|
const REQUIRE_TIGHTEN = process.argv.includes("--require-tighten");
|
|
|
|
// Global fallback eps. Cada métrica pode sobrepor via `eps` no baseline.
|
|
const GLOBAL_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);
|
|
// Velocity phase (relax-baselines.mjs writes `_policy`): while it is active the
|
|
// --require-tighten gate is advisory — the baselines were loosened on purpose, so
|
|
// "measured better than baseline" is the expected state, not a forgotten tighten.
|
|
// Closing the phase (v4.0) deletes `_policy` and the flag bites again.
|
|
const POLICY = baseline._policy && typeof baseline._policy === "object" ? baseline._policy : null;
|
|
const TIGHTEN_ENFORCED = REQUIRE_TIGHTEN && !(POLICY && POLICY.requireTighten === false);
|
|
if (REQUIRE_TIGHTEN && !TIGHTEN_ENFORCED) {
|
|
console.log(
|
|
`[quality-ratchet] --require-tighten advisory: velocity phase active (phase=${POLICY.phase}, until ${POLICY.until}, relax ${POLICY.relaxPct}%)`
|
|
);
|
|
}
|
|
const failures = [];
|
|
const tightenFailures = [];
|
|
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
|
|
|
|
// Per-metric eps; falls back to global EPS if not specified in the spec.
|
|
const eps = spec.eps !== undefined ? spec.eps : GLOBAL_EPS;
|
|
|
|
// Per-metric tightenSlack; falls back to eps (same tolerance as regression check).
|
|
const tightenSlack = spec.tightenSlack !== undefined ? spec.tightenSlack : eps;
|
|
|
|
if (current === undefined) {
|
|
if (ALLOW_MISSING || spec.dedicatedGate === true) {
|
|
const reason = spec.dedicatedGate === true ? "SKIP (dedicated gate)" : "SKIP (ausente)";
|
|
rows.push([key, base, "—", reason]);
|
|
} else {
|
|
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";
|
|
if (REQUIRE_TIGHTEN && base - current > tightenSlack) {
|
|
tightenFailures.push(
|
|
`${key}: melhorou de ${base} para ${current} (delta ${(base - current).toFixed(4)} > slack ${tightenSlack}) — rode 'npm run quality:ratchet -- --update' e commite o baseline apertado neste PR`
|
|
);
|
|
}
|
|
}
|
|
} 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";
|
|
if (REQUIRE_TIGHTEN && current - base > tightenSlack) {
|
|
tightenFailures.push(
|
|
`${key}: melhorou de ${base} para ${current} (delta ${(current - base).toFixed(4)} > slack ${tightenSlack}) — rode 'npm run quality:ratchet -- --update' e commite o baseline apertado neste PR`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
rows.push([key, base, current, status]);
|
|
}
|
|
|
|
// Behavior 3: warn about orphan metrics (present in collected metrics but absent in baseline).
|
|
const baselineKeys = new Set(Object.keys(baseline.metrics));
|
|
const orphans = Object.keys(metrics).filter((k) => !baselineKeys.has(k));
|
|
if (orphans.length > 0) {
|
|
console.warn(
|
|
`[quality-ratchet] WARN: ${orphans.length} métrica(s) órfã(s) — presente(s) em ${path.basename(METRICS)} mas sem entrada no baseline: ${orphans.join(", ")}`
|
|
);
|
|
console.warn(
|
|
`[quality-ratchet] WARN: adicione ${orphans.length === 1 ? "essa métrica" : "essas métricas"} ao baseline (com value/direction) para que sejam catraceadas.`
|
|
);
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
// Tighten check runs only when there are no regressions (regressions take priority).
|
|
// With --update, improvements are captured into the baseline, so tighten check
|
|
// is bypassed (the update itself is the required action).
|
|
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);
|
|
}
|
|
|
|
// Behavior 1: --require-tighten gate (only triggers when there are no regressions and no --update).
|
|
if (TIGHTEN_ENFORCED && !UPDATE && tightenFailures.length > 0) {
|
|
console.error(
|
|
"[quality-ratchet] FALHOU (--require-tighten): métrica(s) melhoraram mas o baseline não foi apertado:\n" +
|
|
tightenFailures.map((f) => " ✗ " + f).join("\n")
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`[quality-ratchet] OK (${rows.length} métricas, ${improvements.length} melhoraram)`);
|