mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +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.
323 lines
12 KiB
JavaScript
323 lines
12 KiB
JavaScript
// scripts/quality/baseline-headroom.mjs
|
||
// Baseline headroom monitor (velocity phase, 2026-08-30 → v4.0).
|
||
//
|
||
// The ratchet gates only speak when a baseline is CROSSED. During the velocity phase the
|
||
// baselines were loosened by 20% on purpose, so the interesting question is no longer
|
||
// "did we regress?" but "how much of the loosened budget is already consumed, and how
|
||
// fast?". This script measures every numeric ratchet the same way its gate does, compares
|
||
// it with the frozen baseline and prints the remaining headroom per gate.
|
||
//
|
||
// headroom = (baseline − live) / baseline for lower-is-better counts
|
||
// headroom = (live − baseline) / baseline for higher-is-better percentages
|
||
//
|
||
// status: ok headroom ≥ --warn (default 10%)
|
||
// warn 0 ≤ headroom < --warn → the next few PRs will trip the gate
|
||
// critical headroom < 0 → the gate is already red on this tip
|
||
//
|
||
// Runs nightly (nightly-release-green.yml → baseline-headroom job) and posts the table to
|
||
// the issue "📈 Baseline headroom". Locally:
|
||
// node scripts/quality/baseline-headroom.mjs # table on stdout
|
||
// node scripts/quality/baseline-headroom.mjs --json out.json --md out.md
|
||
// node scripts/quality/baseline-headroom.mjs --only deadExports,fileSize
|
||
// node scripts/quality/baseline-headroom.mjs --strict # exit 1 on any critical
|
||
// Heavy measurements (knip, tsc, eslint) take a few minutes each; --only keeps it cheap.
|
||
|
||
import { execFileSync } from "node:child_process";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||
const Q = (f) => path.join(ROOT, "config", "quality", f);
|
||
const readJson = (f) => JSON.parse(fs.readFileSync(Q(f), "utf8"));
|
||
|
||
// ── pure helpers (unit-tested) ───────────────────────────────────────────────
|
||
|
||
/**
|
||
* Headroom of one gate as a fraction of its baseline (can be negative = over budget).
|
||
* @param {number} live
|
||
* @param {number} baseline
|
||
* @param {"down"|"up"} direction "down" = lower is better (counts), "up" = higher is better (%)
|
||
*/
|
||
export function headroomOf(live, baseline, direction) {
|
||
if (!Number.isFinite(live) || !Number.isFinite(baseline)) return null;
|
||
if (baseline === 0) return direction === "down" ? (live === 0 ? 1 : -1) : live >= 0 ? 0 : -1;
|
||
return direction === "down" ? (baseline - live) / baseline : (live - baseline) / baseline;
|
||
}
|
||
|
||
/** @returns {"ok"|"warn"|"critical"|"unknown"} */
|
||
export function statusOf(headroom, warnFraction = 0.1) {
|
||
if (headroom === null || headroom === undefined) return "unknown";
|
||
if (headroom < 0) return "critical";
|
||
if (headroom < warnFraction) return "warn";
|
||
return "ok";
|
||
}
|
||
|
||
/** Sum of every per-file / per-TS-code count in a typecheck baseline (notes ignored). */
|
||
export function sumTypecheckBaseline(json) {
|
||
let n = 0;
|
||
for (const [file, codes] of Object.entries(json)) {
|
||
if (file.startsWith("_") || !codes || typeof codes !== "object") continue;
|
||
for (const c of Object.values(codes)) if (typeof c === "number") n += c;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/**
|
||
* Frozen-file headroom: the worst (most consumed) frozen file and how many sit within
|
||
* `warnFraction` of their cap. `locOf(file)` returns the live line count or null.
|
||
*/
|
||
export function fileSizeHeadroom(frozen, locOf, warnFraction = 0.1) {
|
||
let worst = null;
|
||
let nearCap = 0;
|
||
let over = 0;
|
||
let measured = 0;
|
||
for (const [file, capRaw] of Object.entries(frozen)) {
|
||
if (file.startsWith("_")) continue;
|
||
const cap = Number(capRaw);
|
||
const loc = locOf(file);
|
||
if (!Number.isFinite(cap) || loc === null || loc === undefined) continue;
|
||
measured++;
|
||
const h = headroomOf(loc, cap, "down");
|
||
if (h < 0) over++;
|
||
else if (h < warnFraction) nearCap++;
|
||
if (!worst || h < worst.headroom) worst = { file, loc, cap, headroom: h };
|
||
}
|
||
return { measured, nearCap, over, worst };
|
||
}
|
||
|
||
/** Render the markdown table the nightly job posts. */
|
||
export function renderMarkdown(rows, { policy, generatedAt = new Date().toISOString() } = {}) {
|
||
const icon = { ok: "🟢", warn: "🟡", critical: "🔴", unknown: "⚪" };
|
||
const lines = [
|
||
`## 📈 Baseline headroom — ${generatedAt.slice(0, 16).replace("T", " ")}Z`,
|
||
"",
|
||
policy
|
||
? `Velocity phase since ${policy.since} (relax ${policy.relaxPct}%, until v${policy.until}). ` +
|
||
`Headroom = budget still free before the gate turns red.`
|
||
: "No velocity policy in quality-baseline.json (normal ratchet mode).",
|
||
"",
|
||
"| gate | live | baseline | headroom | status |",
|
||
"|---|---:|---:|---:|:--:|",
|
||
...rows.map(
|
||
(r) =>
|
||
`| \`${r.id}\` | ${fmt(r.live)} | ${fmt(r.baseline)} | ${
|
||
r.headroom === null ? "—" : `${(r.headroom * 100).toFixed(1)}%`
|
||
} | ${icon[r.status]} ${r.status}${r.note ? ` — ${r.note}` : ""} |`
|
||
),
|
||
"",
|
||
];
|
||
const bad = rows.filter((r) => r.status === "critical" || r.status === "warn");
|
||
lines.push(
|
||
bad.length
|
||
? `**${bad.length} gate(s) need attention:** ${bad.map((r) => `\`${r.id}\` (${r.status})`).join(", ")}.`
|
||
: "**All measured gates have ≥ warn headroom.**"
|
||
);
|
||
return lines.join("\n") + "\n";
|
||
}
|
||
|
||
function fmt(v) {
|
||
if (v === null || v === undefined) return "—";
|
||
return Number.isInteger(v) ? String(v) : Number(v).toFixed(2);
|
||
}
|
||
|
||
// ── measurements ─────────────────────────────────────────────────────────────
|
||
|
||
function runAndMatch(script, regex, { timeoutMs = 20 * 60 * 1000, args = [] } = {}) {
|
||
let out = "";
|
||
try {
|
||
out = execFileSync(process.execPath, [path.join(ROOT, script), ...args], {
|
||
cwd: ROOT,
|
||
encoding: "utf8",
|
||
timeout: timeoutMs,
|
||
maxBuffer: 64 * 1024 * 1024,
|
||
env: { ...process.env, FORCE_COLOR: "0" },
|
||
stdio: ["ignore", "pipe", "pipe"],
|
||
});
|
||
} catch (err) {
|
||
// A red gate exits 1 but still prints its measurement — keep reading.
|
||
out = `${err.stdout || ""}\n${err.stderr || ""}`;
|
||
}
|
||
const m = out.match(regex);
|
||
return m ? Number(m[1]) : null;
|
||
}
|
||
|
||
function countLines(file) {
|
||
try {
|
||
const text = fs.readFileSync(path.join(ROOT, file), "utf8");
|
||
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** Gate manifest — one entry per numeric ratchet the CI enforces. */
|
||
export function buildManifest() {
|
||
const qb = readJson("quality-baseline.json");
|
||
const m = qb.metrics || {};
|
||
const val = (k) => (m[k] && typeof m[k].value === "number" ? m[k].value : null);
|
||
return [
|
||
{
|
||
id: "deadExports",
|
||
direction: "down",
|
||
baseline: () => val("deadExports"),
|
||
measure: () => runAndMatch("scripts/check/check-dead-code.mjs", /DEAD_TOTAL=(\d+)/),
|
||
},
|
||
{
|
||
id: "apiTypecheck",
|
||
direction: "down",
|
||
baseline: () => sumTypecheckBaseline(readJson("api-typecheck-baseline.json")),
|
||
measure: () =>
|
||
runAndMatch("scripts/check/check-api-typecheck.mjs", /apiTypecheckErrors=(\d+)/),
|
||
},
|
||
{
|
||
id: "dashboardTypecheck",
|
||
direction: "down",
|
||
baseline: () => sumTypecheckBaseline(readJson("dashboard-typecheck-baseline.json")),
|
||
measure: () =>
|
||
runAndMatch(
|
||
"scripts/check/check-dashboard-typecheck.mjs",
|
||
/dashboardTypecheckErrors=(\d+)/
|
||
),
|
||
},
|
||
{
|
||
id: "openSseTypecheck",
|
||
direction: "down",
|
||
baseline: () => sumTypecheckBaseline(readJson("open-sse-typecheck-baseline.json")),
|
||
measure: () =>
|
||
runAndMatch("scripts/check/check-open-sse-typecheck.mjs", /openSseTypecheckErrors=(\d+)/),
|
||
},
|
||
{
|
||
id: "cognitiveComplexity",
|
||
direction: "down",
|
||
baseline: () => val("cognitiveComplexity"),
|
||
measure: () =>
|
||
runAndMatch("scripts/check/check-cognitive-complexity.mjs", /cognitiveComplexity=(\d+)/),
|
||
},
|
||
{
|
||
id: "complexity",
|
||
direction: "down",
|
||
baseline: () => readJson("complexity-baseline.json").count,
|
||
measure: () => runAndMatch("scripts/check/check-complexity.mjs", /(\d+) violaç/),
|
||
},
|
||
{
|
||
id: "duplicationPct",
|
||
direction: "down",
|
||
baseline: () => readJson("duplication-baseline.json").percentage,
|
||
measure: () => runAndMatch("scripts/check/check-duplication.mjs", /(\d+(?:\.\d+)?)%/),
|
||
},
|
||
{
|
||
id: "rtlPhysicalClasses",
|
||
direction: "down",
|
||
baseline: () => val("rtlPhysicalClasses"),
|
||
measure: () => runAndMatch("scripts/check/check-rtl-ratchet.mjs", /rtlPhysicalClasses=(\d+)/),
|
||
},
|
||
{
|
||
id: "typeCoveragePct",
|
||
direction: "up",
|
||
baseline: () => val("typeCoveragePct"),
|
||
measure: () =>
|
||
runAndMatch("scripts/check/check-type-coverage.mjs", /typeCoveragePct=(\d+(?:\.\d+)?)/),
|
||
},
|
||
{
|
||
id: "fileSize",
|
||
direction: "down",
|
||
// Reported as the WORST frozen file: live = its lines, baseline = its cap.
|
||
baseline: () => null,
|
||
measure: () => null,
|
||
custom: (warnFraction) => {
|
||
const fsz = readJson("file-size-baseline.json");
|
||
const r = fileSizeHeadroom(fsz.frozen || {}, countLines, warnFraction);
|
||
const t = fileSizeHeadroom(fsz.testFrozen || {}, countLines, warnFraction);
|
||
const worst = [r.worst, t.worst].filter(Boolean).sort((a, b) => a.headroom - b.headroom)[0];
|
||
return {
|
||
live: worst ? worst.loc : null,
|
||
baseline: worst ? worst.cap : null,
|
||
headroom: worst ? worst.headroom : null,
|
||
note: worst
|
||
? `worst: ${worst.file}; ${r.nearCap + t.nearCap} file(s) within warn, ${r.over + t.over} over (${r.measured + t.measured} frozen)`
|
||
: "no frozen files measured",
|
||
};
|
||
},
|
||
},
|
||
];
|
||
}
|
||
|
||
export function collect({ only = null, warnFraction = 0.1, manifest = buildManifest() } = {}) {
|
||
const rows = [];
|
||
for (const gate of manifest) {
|
||
if (only && !only.includes(gate.id)) continue;
|
||
let live = null;
|
||
let baseline = null;
|
||
let headroom = null;
|
||
let note = "";
|
||
try {
|
||
if (gate.custom) {
|
||
({ live, baseline, headroom, note } = gate.custom(warnFraction));
|
||
} else {
|
||
baseline = gate.baseline();
|
||
live = gate.measure();
|
||
headroom = headroomOf(live, baseline, gate.direction);
|
||
if (live === null) note = "measurement unavailable";
|
||
}
|
||
} catch (err) {
|
||
note = `error: ${err && err.message ? err.message.split("\n")[0] : err}`;
|
||
}
|
||
rows.push({
|
||
id: gate.id,
|
||
direction: gate.direction,
|
||
live,
|
||
baseline,
|
||
headroom,
|
||
status: statusOf(headroom, warnFraction),
|
||
note,
|
||
});
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
// ── CLI ──────────────────────────────────────────────────────────────────────
|
||
|
||
function main() {
|
||
const argv = process.argv.slice(2);
|
||
const getArg = (n, d) => {
|
||
const i = argv.indexOf(n);
|
||
return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : d;
|
||
};
|
||
const warnFraction = Number(getArg("--warn", "10")) / 100;
|
||
const only =
|
||
getArg("--only", null)
|
||
?.split(",")
|
||
.map((s) => s.trim())
|
||
.filter(Boolean) ?? null;
|
||
const jsonOut = getArg("--json", null);
|
||
const mdOut = getArg("--md", null);
|
||
const strict = argv.includes("--strict");
|
||
|
||
const qb = readJson("quality-baseline.json");
|
||
const rows = collect({ only, warnFraction });
|
||
const md = renderMarkdown(rows, { policy: qb._policy || null });
|
||
process.stdout.write(md);
|
||
if (jsonOut) {
|
||
fs.mkdirSync(path.dirname(jsonOut), { recursive: true });
|
||
fs.writeFileSync(
|
||
jsonOut,
|
||
JSON.stringify(
|
||
{ generatedAt: new Date().toISOString(), policy: qb._policy || null, warnFraction, rows },
|
||
null,
|
||
2
|
||
) + "\n"
|
||
);
|
||
}
|
||
if (mdOut) {
|
||
fs.mkdirSync(path.dirname(mdOut), { recursive: true });
|
||
fs.writeFileSync(mdOut, md);
|
||
}
|
||
const critical = rows.filter((r) => r.status === "critical").length;
|
||
if (strict && critical) process.exit(1);
|
||
}
|
||
|
||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||
main();
|
||
}
|