Files
OmniRoute/scripts/quality/relax-baselines.mjs
Diego Rodrigues de Sa e Souza 1f4dc830f3 chore(quality): velocity phase — loosen every numeric baseline by 20% until v4.0, monitor headroom nightly (#12125)
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.
2026-08-30 10:39:01 -03:00

231 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// scripts/quality/relax-baselines.mjs
// Velocity-phase baseline relaxation (owner decision, 2026-08-30).
//
// Every numeric ratchet baseline under config/quality/ is loosened by --pct percent in
// ONE auditable pass, and quality-baseline.json gets a `_policy` block that the ratchet
// engine reads (check-quality-ratchet.mjs skips --require-tighten while the phase is
// active). The intent: ship fast until v4.0 LTS, where the phase closes and every
// baseline is re-measured on the pure tip and tightened again (see
// docs/architecture/QUALITY_GATES.md → "Velocity phase").
//
// What it touches (all counts / caps / percentages, never allowlists):
// quality-baseline.json metrics.*.value (down: ×(1+pct); up: ÷(1+pct), floors kept)
// complexity-baseline.json count
// duplication-baseline.json percentage
// file-size-baseline.json cap, testCap, frozen[*], testFrozen[*]
// api-typecheck-baseline.json, dashboard-typecheck-baseline.json,
// open-sse-typecheck-baseline.json per-file / per-TS-code counts
//
// Floors that survive the relaxation (hard rules, not ratchets):
// coverage.* ≥ 60 (AGENTS.md Hard Rule #9 — absolute floor)
// eslintErrors stays 0 (errors are defects, not debt)
// eslintWarnings at 0 becomes pct% of the frozen suppression count (a budget for
// new warnings; 0 × 1.2 would be no relaxation at all)
//
// Usage:
// node scripts/quality/relax-baselines.mjs --pct 20 --note velocity_2026_08_30 [--dry-run]
// Refuses to run twice with the same note unless --force (idempotency guard).
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const Q = (f) => path.join(ROOT, "config", "quality", f);
const argv = process.argv.slice(2);
const getArg = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : dflt;
};
const PCT = Number(getArg("--pct", "20"));
const NOTE = getArg(
"--note",
`velocity_${new Date().toISOString().slice(0, 10).replace(/-/g, "_")}`
);
const DRY = argv.includes("--dry-run");
const FORCE = argv.includes("--force");
const UNTIL = getArg("--until", "4.0.0");
if (!Number.isFinite(PCT) || PCT <= 0 || PCT > 100) {
console.error(`[relax-baselines] --pct must be in (0, 100], got ${PCT}`);
process.exit(2);
}
const FACTOR = 1 + PCT / 100;
const NOTE_KEY = `_relax_${NOTE}`;
const COVERAGE_FLOOR = 60; // AGENTS.md Hard Rule #9
const changes = []; // [file, key, before, after]
const read = (f) => JSON.parse(fs.readFileSync(Q(f), "utf8"));
const write = (f, j) => {
if (DRY) return;
fs.writeFileSync(Q(f), JSON.stringify(j, null, 2) + "\n");
};
const upCount = (n) => Math.ceil(n * FACTOR);
const downPct = (n, floor = 0) => Math.max(floor, Math.round((n / FACTOR) * 100) / 100);
const keepType = (orig, n) => (typeof orig === "string" ? String(n) : n);
function guard(json, file) {
if (json[NOTE_KEY] && !FORCE) {
console.error(`[relax-baselines] ${file} already carries ${NOTE_KEY} — refusing (use --force)`);
process.exit(3);
}
}
// ── quality-baseline.json ────────────────────────────────────────────────────
function relaxQualityBaseline(json, { pct = PCT, suppressedCount = 0 } = {}) {
const factor = 1 + pct / 100;
const out = [];
for (const [key, spec] of Object.entries(json.metrics)) {
if (!spec || typeof spec.value !== "number") continue;
const dir = spec.direction === "up" ? "up" : "down"; // undefined direction = a count
const before = spec.value;
let after = before;
if (key === "eslintErrors") {
continue; // errors are defects, never a budget
} else if (dir === "down") {
after =
before === 0 && key === "eslintWarnings"
? Math.ceil((suppressedCount * pct) / 100)
: Math.ceil(before * factor);
} else {
const floor = key.startsWith("coverage.") ? COVERAGE_FLOOR : 0;
after = Math.max(floor, Math.round((before / factor) * 100) / 100);
}
if (after !== before) {
spec.value = after;
out.push([key, before, after]);
}
}
return out;
}
const suppressedCount = (() => {
try {
const sup = read("eslint-suppressions.json");
let n = 0;
for (const rules of Object.values(sup)) for (const r of Object.values(rules)) n += r.count || 0;
return n;
} catch {
return 0;
}
})();
{
const f = "quality-baseline.json";
const j = read(f);
guard(j, f);
for (const c of relaxQualityBaseline(j, { pct: PCT, suppressedCount })) changes.push([f, ...c]);
j._policy = {
phase: "velocity",
since: NOTE.replace(/^velocity_/, "").replace(/_/g, "-"),
until: UNTIL,
relaxPct: PCT,
requireTighten: false,
monitor:
"scripts/quality/baseline-headroom.mjs (nightly-release-green → baseline-headroom job)",
note:
"Owner decision 2026-08-30: speed matters more than debt until the v4.0 LTS modularization. " +
"Every numeric baseline was loosened by relaxPct in one pass (see _relax_* note). " +
"check-quality-ratchet skips --require-tighten while this block exists; headroom is " +
"monitored nightly. At 4.0 the phase closes: re-measure on the pure tip, tighten, delete this block.",
};
j[NOTE_KEY] = summarize(changes.filter((c) => c[0] === f));
write(f, j);
}
// ── complexity-baseline.json ─────────────────────────────────────────────────
{
const f = "complexity-baseline.json";
const j = read(f);
guard(j, f);
const before = j.count;
j.count = upCount(before);
changes.push([f, "count", before, j.count]);
j[NOTE_KEY] = summarize(changes.filter((c) => c[0] === f));
write(f, j);
}
// ── duplication-baseline.json ────────────────────────────────────────────────
{
const f = "duplication-baseline.json";
const j = read(f);
guard(j, f);
const before = j.percentage;
j.percentage = Math.round(before * FACTOR * 100) / 100;
changes.push([f, "percentage", before, j.percentage]);
j[NOTE_KEY] = summarize(changes.filter((c) => c[0] === f));
write(f, j);
}
// ── file-size-baseline.json ──────────────────────────────────────────────────
{
const f = "file-size-baseline.json";
const j = read(f);
guard(j, f);
for (const capKey of ["cap", "testCap"]) {
if (typeof j[capKey] === "number") {
const before = j[capKey];
j[capKey] = upCount(before);
changes.push([f, capKey, before, j[capKey]]);
}
}
let n = 0;
for (const mapKey of ["frozen", "testFrozen"]) {
const map = j[mapKey];
if (!map || typeof map !== "object") continue;
for (const [file, v] of Object.entries(map)) {
if (file.startsWith("_")) continue; // rebaseline notes live inside the map
const num = Number(v);
if (!Number.isFinite(num)) continue;
map[file] = keepType(v, upCount(num));
n++;
}
}
changes.push([f, "frozen+testFrozen entries", n, `×${FACTOR}`]);
j[NOTE_KEY] =
`${n} frozen line caps and cap/testCap raised by ${PCT}% (velocity phase; see quality-baseline.json _policy).`;
write(f, j);
}
// ── per-file typecheck baselines ─────────────────────────────────────────────
for (const f of [
"api-typecheck-baseline.json",
"dashboard-typecheck-baseline.json",
"open-sse-typecheck-baseline.json",
]) {
if (!fs.existsSync(Q(f))) continue;
const j = read(f);
guard(j, f);
let files = 0;
let before = 0;
let after = 0;
for (const [file, codes] of Object.entries(j)) {
if (file.startsWith("_") || !codes || typeof codes !== "object") continue;
files++;
for (const [code, count] of Object.entries(codes)) {
if (typeof count !== "number") continue;
before += count;
codes[code] = upCount(count);
after += codes[code];
}
}
changes.push([f, `${files} files (sum of counts)`, before, after]);
j[NOTE_KEY] =
`per-file TS diagnostic counts raised by ${PCT}% (${before}${after}); velocity phase, see quality-baseline.json _policy.`;
write(f, j);
}
function summarize(list) {
return (
`Velocity phase (${PCT}% relaxation, until ${UNTIL}): ` +
list.map(([, k, b, a]) => `${k} ${b}${a}`).join("; ")
);
}
// ── report ───────────────────────────────────────────────────────────────────
console.log(`[relax-baselines] ${DRY ? "DRY RUN — " : ""}pct=${PCT} note=${NOTE_KEY}`);
for (const [f, k, b, a] of changes)
console.log(` ${f.padEnd(34)} ${String(k).padEnd(40)} ${b}${a}`);
console.log(`[relax-baselines] ${changes.length} entries ${DRY ? "would change" : "changed"}.`);