chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:12 +08:00
commit d8dcd5f6d1
8604 changed files with 2479390 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globSync } from "tinyglobby";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const SRC_ROOTS = ["src", "open-sse"];
const IMPORT_RE =
/(?:import|export)[^'"]*from\s*['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)|import\(\s*['"]([^'"]+)['"]\s*\)/g;
const EXTS = [".ts", ".tsx", ".mts", ".js", ".mjs"];
function resolveImport(spec, fromFile) {
let base;
if (spec.startsWith("@/")) base = path.join(ROOT, "src", spec.slice(2));
else if (spec.startsWith("@omniroute/open-sse"))
base = path.join(ROOT, "open-sse", spec.replace(/^@omniroute\/open-sse\/?/, ""));
else if (spec.startsWith(".")) base = path.resolve(path.dirname(fromFile), spec);
else return null;
for (const e of EXTS) {
if (fs.existsSync(base + e)) return base + e;
}
for (const e of EXTS) {
const idx = path.join(base, "index" + e);
if (fs.existsSync(idx)) return idx;
}
return fs.existsSync(base) && fs.statSync(base).isFile() ? base : null;
}
function sourceDepsOf(entry) {
const seen = new Set();
const stack = [entry];
const sources = new Set();
while (stack.length) {
const f = stack.pop();
if (seen.has(f)) continue;
seen.add(f);
let code;
try {
code = fs.readFileSync(f, "utf8");
} catch {
continue;
}
for (const m of code.matchAll(IMPORT_RE)) {
const spec = m[1] || m[2] || m[3];
if (!spec) continue;
const r = resolveImport(spec, f);
if (!r) continue;
const rel = path.relative(ROOT, r);
if (SRC_ROOTS.some((s) => rel.startsWith(s + path.sep))) sources.add(rel);
stack.push(r);
}
}
return sources;
}
// Mirror EXACTLY the `npm run test:unit` glob — the curated set of node:test files.
// The TIA step runs the selected subset via `node --test`, so it must NOT include
// vitest files (`.test.tsx`, `open-sse/**/__tests__`, `tests/unit/autoCombo`), nor
// e2e/integration tests, which can't run under node:test (they 99-false-failed before).
const testFiles = globSync(
[
"tests/unit/*.test.ts",
"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts",
// Quarentena serial (P0.3): também são node:test — a TIA precisa mapeá-los.
"tests/unit/serial/**/*.test.ts",
],
{ cwd: ROOT, absolute: true }
);
const map = {};
for (const tf of testFiles) {
const relTest = path.relative(ROOT, tf);
for (const src of sourceDepsOf(tf)) {
(map[src] ||= []).push(relTest);
}
}
for (const k of Object.keys(map)) map[k].sort();
const out = path.join(ROOT, "config/quality/test-impact-map.json");
fs.writeFileSync(out, JSON.stringify({ generatedFrom: "import-graph", sources: map }, null, 2) + "\n");
console.log(
`test-impact-map: ${Object.keys(map).length} source files mapped from ${testFiles.length} test files`
);
+153
View File
@@ -0,0 +1,153 @@
#!/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);
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 (REQUIRE_TIGHTEN && !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)`);
+268
View File
@@ -0,0 +1,268 @@
#!/usr/bin/env node
// scripts/quality/collect-metrics.mjs — emite quality-metrics.json
// Coletores incrementais: Fase 1 traz ESLint warnings + cobertura.
// Fases 3/4 estendem com duplicação (jscpd), tamanho de arquivo e cobertura por módulo.
// Fase 6A.11: openapiCoverage.pct + i18nUiCoverage.pct (mínimo entre locales).
// Task 7.9: coverage.<modulo>.lines para ~8 módulos críticos, lidos do
// coverage/coverage-summary.json se existir (sem erro se ausente).
import fs from "node:fs";
import { promises as fsAsync } from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { execFileSync } from "node:child_process";
import * as yaml from "js-yaml";
const cwd = process.cwd();
const out = {};
// 1) ESLint: contagem de warnings/errors NOVOS além do baseline congelado.
// Pacote 4 (plano mestre testes+CI, 2026-07-04): a dívida pré-existente vive em
// config/quality/eslint-suppressions.json (ESLint bulk suppressions nativo) e é
// bloqueada no PR que a introduziria (job lint-guard + lint-staged). A métrica do
// ratchet passa a medir a dívida LÍQUIDA nova — em regime, ~0 — em vez do estoque
// bruto (que driftava +41/+88 por ciclo e era rebaselinado às cegas na release).
// O aperto do estoque acontece via --prune-suppressions na release.
function eslintCounts() {
let stdout;
const args = ["eslint", ".", "--format", "json"];
if (fs.existsSync(path.join(cwd, "config/quality/eslint-suppressions.json"))) {
args.push("--suppressions-location", "config/quality/eslint-suppressions.json");
}
try {
stdout = execFileSync("npx", args, {
encoding: "utf8",
maxBuffer: 256 * 1024 * 1024,
});
} catch (e) {
// eslint sai com código != 0 quando há errors; o JSON ainda vem no stdout
stdout = e.stdout?.toString() || "[]";
}
const results = JSON.parse(stdout);
out.eslintWarnings = results.reduce((n, r) => n + (r.warningCount || 0), 0);
out.eslintErrors = results.reduce((n, r) => n + (r.errorCount || 0), 0);
}
// 2) Cobertura: lê coverage/coverage-summary.json se existir (gerado por c8)
function coverage() {
const p = path.join(cwd, "coverage", "coverage-summary.json");
if (!fs.existsSync(p)) return;
const t = JSON.parse(fs.readFileSync(p, "utf8")).total;
out["coverage.statements"] = t.statements.pct;
out["coverage.lines"] = t.lines.pct;
out["coverage.functions"] = t.functions.pct;
out["coverage.branches"] = t.branches.pct;
}
// 3) Coverage per critical module (Task 7.9)
// Reads coverage/coverage-summary.json (produced by `npm run test:coverage` via c8).
// If the file is absent → silently skips (no error). This allows the gate to
// function normally in environments where coverage was not run (e.g. lint-only CI).
//
// The summary JSON produced by c8 looks like:
// { "total": {...}, "/abs/path/to/file.ts": { lines: { pct: 78 }, ... }, ... }
//
// modulePaths is a record of { metricSuffix: relPathFromRoot[] } — the first
// matching key in the summary wins.
/**
* Pure function — extracts per-module line-coverage percentages from a
* coverage-summary.json object.
*
* @param {Record<string, { lines?: { pct: number } }>} summaryJson
* The parsed coverage-summary.json (keys are absolute file paths or "total").
* @param {Record<string, string[]>} modulePaths
* Map of { metricKey: [relPath, ...fallbacks] } where relPath is relative to
* the repo root (forward slashes). Returns the lines.pct of the first match.
* @param {string} repoRoot Absolute path to the repo root (used to build keys).
* @returns {Record<string, number>} Map of metricKey → lines.pct (0-100).
*/
export function extractModuleCoverage(summaryJson, modulePaths, repoRoot) {
const result = {};
// Build a normalised lookup: absolute path (forward slashes) → pct
const lookup = new Map();
for (const [rawKey, data] of Object.entries(summaryJson)) {
if (rawKey === "total") continue;
const norm = rawKey.replace(/\\/g, "/");
const pct = data?.lines?.pct;
if (typeof pct === "number") lookup.set(norm, pct);
}
const normRoot = repoRoot.replace(/\\/g, "/").replace(/\/$/, "");
for (const [metricKey, candidates] of Object.entries(modulePaths)) {
for (const rel of candidates) {
const abs = `${normRoot}/${rel.replace(/\\/g, "/").replace(/^\//, "")}`;
if (lookup.has(abs)) {
result[metricKey] = lookup.get(abs);
break;
}
}
}
return result;
}
/** The 8 critical modules tracked by Task 7.9 (relative paths from repo root). */
export const CRITICAL_MODULE_PATHS = {
"coverage.chatCore.lines": ["open-sse/handlers/chatCore.ts"],
"coverage.combo.lines": ["open-sse/services/combo.ts"],
"coverage.accountFallback.lines": ["open-sse/services/accountFallback.ts"],
"coverage.auth.lines": ["src/sse/services/auth.ts"],
"coverage.routeGuard.lines": ["src/server/authz/routeGuard.ts"],
"coverage.error.lines": ["open-sse/utils/error.ts"],
"coverage.publicCreds.lines": ["open-sse/utils/publicCreds.ts"],
"coverage.circuitBreaker.lines": ["src/shared/utils/circuitBreaker.ts"],
};
function coverageByModule() {
const p = path.join(cwd, "coverage", "coverage-summary.json");
if (!fs.existsSync(p)) return; // absent → skip silently (Task 7.9 spec)
let summaryJson;
try {
summaryJson = JSON.parse(fs.readFileSync(p, "utf8"));
} catch {
return; // malformed file → skip
}
const moduleMetrics = extractModuleCoverage(summaryJson, CRITICAL_MODULE_PATHS, cwd);
Object.assign(out, moduleMetrics);
}
// 4) OpenAPI coverage: percentage of implemented routes documented in openapi.yaml
function openapiCoverage() {
const API_ROOT = path.join(cwd, "src", "app", "api");
const OPENAPI_PATH = path.join(cwd, "docs", "openapi.yaml");
if (!fs.existsSync(API_ROOT) || !fs.existsSync(OPENAPI_PATH)) return;
function collectRoutePaths(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const paths = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
paths.push(...collectRoutePaths(fullPath));
continue;
}
if (entry.isFile() && entry.name === "route.ts") {
const apiPath = path
.dirname(fullPath)
.replace(API_ROOT, "")
.replace(/\[([^\]]+)\]/g, "{$1}");
paths.push(`/api${apiPath}`);
}
}
return paths;
}
function normalizePath(p) {
return p.replace(/\/\[\.\.\.([^\]]+)\]/g, "/{$1}").replace(/\[([^\]]+)\]/g, "{$1}");
}
const implementedPaths = collectRoutePaths(API_ROOT).map(normalizePath);
const raw = yaml.load(fs.readFileSync(OPENAPI_PATH, "utf-8"));
const documentedPaths = new Set(Object.keys(raw.paths || {}));
const covered = implementedPaths.filter((p) => documentedPaths.has(p)).length;
const total = implementedPaths.length;
if (total > 0) out["openapiCoverage.pct"] = parseFloat(((covered / total) * 100).toFixed(1));
}
// 4) i18n UI coverage: minimum real coverage across all non-en locales
async function i18nUiCoverage() {
const MESSAGES_DIR = path.join(cwd, "src", "i18n", "messages");
const CONFIG_PATH = path.join(cwd, "config", "i18n.json");
const SOURCE_LOCALE = "en";
const PLACEHOLDER_PREFIX = "__MISSING__:";
if (!fs.existsSync(MESSAGES_DIR)) return;
const sourcePath = path.join(MESSAGES_DIR, `${SOURCE_LOCALE}.json`);
if (!fs.existsSync(sourcePath)) return;
function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function collectLeafPaths(obj, prefix = []) {
const paths = [];
for (const [key, value] of Object.entries(obj)) {
const next = [...prefix, key];
if (isPlainObject(value)) {
paths.push(...collectLeafPaths(value, next));
} else {
paths.push(next);
}
}
return paths;
}
const FORBIDDEN_KEY_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
function lookupPath(obj, parts) {
let cur = obj;
for (const part of parts) {
if (!isPlainObject(cur)) return undefined;
if (FORBIDDEN_KEY_SEGMENTS.has(part)) return undefined;
if (!Object.prototype.hasOwnProperty.call(cur, part)) return undefined;
const entry = Object.entries(cur).find(([k]) => k === part);
cur = entry ? entry[1] : undefined;
}
return cur;
}
const source = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
const enPaths = collectLeafPaths(source);
const totalEn = enPaths.length;
if (totalEn === 0) return;
let configCodes = null;
if (fs.existsSync(CONFIG_PATH)) {
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
if (Array.isArray(cfg.locales)) configCodes = new Set(cfg.locales.map((l) => l.code));
} catch {
/* ignore */
}
}
const onDisk = (await fsAsync.readdir(MESSAGES_DIR))
.filter((f) => f.endsWith(".json") && f !== `${SOURCE_LOCALE}.json`)
.map((f) => f.slice(0, -5))
.filter((code) => (configCodes ? configCodes.has(code) : true));
let minCoverage = 100;
for (const locale of onDisk) {
const localePath = path.join(MESSAGES_DIR, `${locale}.json`);
let target;
try {
target = JSON.parse(fs.readFileSync(localePath, "utf8"));
} catch {
minCoverage = 0;
continue;
}
let present = 0;
let placeholder = 0;
for (const pathParts of enPaths) {
const value = lookupPath(target, pathParts);
if (value === undefined || isPlainObject(value)) continue;
present++;
if (typeof value === "string" && value.startsWith(PLACEHOLDER_PREFIX)) placeholder++;
}
const coverage = ((present - placeholder) / totalEn) * 100;
if (coverage < minCoverage) minCoverage = coverage;
}
if (onDisk.length > 0) out["i18nUiCoverage.pct"] = parseFloat(minCoverage.toFixed(1));
}
// Only run the collection pipeline when this file is executed directly.
// When imported (e.g. in tests), only the exported pure functions are available
// without triggering the expensive ESLint + i18n filesystem walks.
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
eslintCounts();
coverage();
coverageByModule();
openapiCoverage();
await i18nUiCoverage();
fs.writeFileSync(
path.join(cwd, "config/quality/quality-metrics.json"),
JSON.stringify(out, null, 2) + "\n"
);
console.log("[collect-metrics]", JSON.stringify(out));
}
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env node
/**
* Mutation radiography (Quality Gate v2 / Fase 9 T5 — Onda 2, Task 1).
*
* Classifies every COVERING test file by its mutation-kill contribution, using the
* `killedBy` attribution that the Stryker tap-runner emits per mutant
* (`coverageAnalysis: perTest`, validated by the Task 12 spike):
*
* 🔴 empty — the test file never appears in any `killedBy` (kills no mutant
* of the mutated modules). Prime R1-prune candidate (Task 2).
* 🟠 redundant — every mutant it kills is ALSO killed by ≥1 other test file
* (zero unique kills).
* 🟡 overlapping — kills ≥1 unique mutant, but the MAJORITY of its kills are shared.
* 🟢 unique — kills ≥1 mutant that NO other test file kills (and unique kills
* are not outnumbered by shared kills).
*
* CAVEAT — bail-on-first-kill: Stryker bails after the first test kills a mutant
* (we do NOT set `disableBail`), so `killedBy` lists the FIRST killer, not every
* killer. Consequence: 🔴 empty is RELIABLE (a sole killer is always recorded, so a
* file that never appears in killedBy is never the sole killer of any mutant → safe
* R1-prune candidate w.r.t. mutationScore), but 🟢/🟠/🟡 are OPTIMISTIC — "unique" is
* overstated and "redundant" understated, because a non-first coverer that WOULD also
* kill is never recorded. Use 🟢/🟠/🟡 as advisory only; an accurate redundancy split
* (for R2) needs a `disableBail: true` run. R1 (Task 2) acts on 🔴 alone + a line-
* coverage cross-check + human review, so bail-on-first is sufficient there.
*
* IMPORTANT — multi-batch merge: the nightly splits `mutate` across parallel batches
* (one mutation.json per batch). Stryker assigns numeric test ids PER RUN, so id "12"
* in batch c is unrelated to id "12" in batch d. Each report is therefore resolved
* (id -> file name, via its own `testFiles` section) and classified independently;
* `aggregateRadiography` then sums the per-FILE kill counts across batches and
* reclassifies. A file empty in one batch but unique in another is unique overall.
*
* Usage:
* node scripts/quality/mutation-radiography.mjs <mutation-c.json> [<mutation-d.json> ...]
* The universe of test files (so 🔴 empty files are detectable) defaults to
* `stryker.conf.json:tap.testFiles`; pass --no-conf-universe to use only the union
* of the reports' own `testFiles` sections instead.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(SCRIPT_DIR, "..", "..");
export function loadMutationReport(reportPath) {
return JSON.parse(fs.readFileSync(reportPath, "utf8"));
}
/**
* Threshold rules shared by single-report and aggregated classification.
* @param {number} uniqueKills mutants this file kills ALONE
* @param {number} sharedKills mutants this file kills together with others
*/
export function classifyFromCounts(uniqueKills, sharedKills) {
if (uniqueKills === 0 && sharedKills === 0) return "empty";
if (uniqueKills === 0) return "redundant";
if (sharedKills > uniqueKills) return "overlapping";
return "unique";
}
// Map each numeric test id to its file name via the report's `testFiles` section.
// Real tap-runner reports key killedBy by id; the synthetic test fixtures key it by
// file name directly (no testFiles section) — those pass through unchanged.
function buildIdToFile(report) {
const map = new Map();
for (const [file, data] of Object.entries(report.testFiles || {})) {
for (const t of data.tests || []) {
map.set(String(t.id), t.name || file);
}
}
return map;
}
// Raw per-file kill counts for ONE report (no universe, no classification).
function countKills(report) {
const idToFile = buildIdToFile(report);
const counts = new Map();
const bump = (file, key) => {
const c = counts.get(file) || { uniqueKills: 0, sharedKills: 0 };
c[key] += 1;
counts.set(file, c);
};
for (const data of Object.values(report.files || {})) {
for (const m of data.mutants || []) {
if (m.status !== "Killed") continue;
const killers = [...new Set((m.killedBy || []).map((id) => idToFile.get(String(id)) ?? id))];
if (killers.length === 0) continue;
if (killers.length === 1) bump(killers[0], "uniqueKills");
else for (const k of killers) bump(k, "sharedKills");
}
}
return counts;
}
function materialize(counts, universe) {
const files = new Set(universe || []);
for (const f of counts.keys()) files.add(f);
const out = {};
for (const file of files) {
const { uniqueKills = 0, sharedKills = 0 } = counts.get(file) || {};
out[file] = { class: classifyFromCounts(uniqueKills, sharedKills), uniqueKills, sharedKills };
}
return out;
}
/**
* Classify the test files of a SINGLE mutation report.
* @param {object} report parsed mutation.json
* @param {string[]} [allTestFiles] universe; defaults to the report's testFiles keys
*/
export function classifyTestFiles(report, allTestFiles) {
const universe = allTestFiles || Object.keys(report.testFiles || {});
return materialize(countKills(report), universe);
}
/**
* Merge several per-batch reports at the file level, then classify.
* @param {object[]} reports parsed mutation.json objects (one per batch)
* @param {string[]} [allTestFiles] universe; defaults to the union of testFiles keys
*/
export function aggregateRadiography(reports, allTestFiles) {
const total = new Map();
const universe = new Set(allTestFiles || []);
for (const report of reports) {
if (!allTestFiles) for (const f of Object.keys(report.testFiles || {})) universe.add(f);
for (const [file, c] of countKills(report)) {
const acc = total.get(file) || { uniqueKills: 0, sharedKills: 0 };
acc.uniqueKills += c.uniqueKills;
acc.sharedKills += c.sharedKills;
total.set(file, acc);
}
}
return materialize(total, [...universe]);
}
/**
* R1 prune-candidate list: the test files with ZERO unique kills — 🔴 empty (kills no
* mutant) 🟠 redundant (every mutant it kills is also killed by ≥1 other file). Files
* with ≥1 unique kill (🟢 unique / 🟡 overlapping) are NEVER candidates — removing one
* would drop a mutant's only killer and lower the mutation score.
*
* IMPORTANT: 🟠 redundant is only ACCURATE when the reports come from a `disableBail:true`
* run (killedBy lists EVERY killer). Under the bail-on-first nightly, redundant is
* understated — see the module caveat. Pass disableBail reports here (mutation-redundancy.yml).
*
* @param {object[]} reports parsed mutation.json objects (one per batch)
* @param {string[]} [allTestFiles] universe; defaults to the union of testFiles keys
* @returns {{ classification: object, empty: string[], redundant: string[], candidates: string[] }}
*/
export function redundancyCandidates(reports, allTestFiles) {
const classification = aggregateRadiography(reports, allTestFiles);
const empty = [];
const redundant = [];
for (const [file, info] of Object.entries(classification)) {
if (info.class === "empty") empty.push(file);
else if (info.class === "redundant") redundant.push(file);
}
empty.sort((a, b) => a.localeCompare(b));
redundant.sort((a, b) => a.localeCompare(b));
const candidates = [...empty, ...redundant].sort((a, b) => a.localeCompare(b));
return { classification, empty, redundant, candidates };
}
// ── CLI ──────────────────────────────────────────────────────────────────────
function tapTestFilesUniverse() {
try {
const conf = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "stryker.conf.json"), "utf8"));
return conf?.tap?.testFiles || null;
} catch {
return null;
}
}
const CLASS_LABEL = {
empty: "🔴 empty",
redundant: "🟠 redundant",
overlapping: "🟡 overlapping",
unique: "🟢 unique",
};
const CLASS_ORDER = ["empty", "redundant", "overlapping", "unique"];
function renderMarkdown(classification) {
const byClass = { empty: [], redundant: [], overlapping: [], unique: [] };
for (const [file, info] of Object.entries(classification))
byClass[info.class].push({ file, ...info });
for (const k of CLASS_ORDER) byClass[k].sort((a, b) => a.file.localeCompare(b.file));
const total = Object.keys(classification).length;
const lines = [];
lines.push("# Mutation Radiography");
lines.push("");
lines.push(
`Test files classified by mutation-kill contribution (\`killedBy\`). Total: **${total}**.`
);
lines.push("");
lines.push("| Class | Count | Meaning |");
lines.push("| --- | --- | --- |");
lines.push(
`| 🔴 empty | ${byClass.empty.length} | kills no mutant of the mutated modules (R1-prune candidate) |`
);
lines.push(
`| 🟠 redundant | ${byClass.redundant.length} | every kill is shared with another file |`
);
lines.push(
`| 🟡 overlapping | ${byClass.overlapping.length} | kills ≥1 unique but mostly shared |`
);
lines.push(`| 🟢 unique | ${byClass.unique.length} | kills ≥1 mutant no other file kills |`);
lines.push("");
lines.push(
"> **Bail caveat:** Stryker bails on the first kill (no `disableBail`), so `killedBy` is the " +
"FIRST killer only. 🔴 empty is reliable (safe R1-prune candidate w.r.t. mutationScore); " +
"🟢/🟠/🟡 are optimistic (unique overstated, redundant understated) — advisory until a " +
"`disableBail` run. R1 prunes 🔴 only, with a line-coverage cross-check + human review."
);
lines.push("");
for (const k of CLASS_ORDER) {
const rows = byClass[k];
lines.push(`## ${CLASS_LABEL[k]} (${rows.length})`);
lines.push("");
if (rows.length === 0) {
lines.push("_none_");
} else {
lines.push("| Test file | unique | shared |");
lines.push("| --- | --- | --- |");
for (const r of rows) lines.push(`| ${r.file} | ${r.uniqueKills} | ${r.sharedKills} |`);
}
lines.push("");
}
return lines.join("\n");
}
const FLAGS = new Set(["--no-conf-universe", "--candidates"]);
function renderCandidates({ empty, redundant, candidates }) {
const lines = [];
lines.push("# R1 — Test-redundancy prune candidates (disableBail)");
lines.push("");
lines.push(
`Test files with ZERO unique kills: **${candidates.length}** ` +
`(🔴 empty ${empty.length} + 🟠 redundant ${redundant.length}).`
);
lines.push("");
lines.push(
"> Accurate ONLY for a `disableBail:true` run (killedBy lists every killer). " +
"These are CANDIDATES, not deletions: exclude security/contract/repro tests " +
"(routeGuard, OAuth, error-sanitization, *-repro*/*-regression*/issue-linked) and " +
"require human review before removing any (R1 human gate)."
);
lines.push("");
lines.push(`## 🔴 empty — kills no mutant (${empty.length})`);
lines.push("");
if (empty.length === 0) lines.push("_none_");
else for (const f of empty) lines.push(`- ${f}`);
lines.push("");
lines.push(`## 🟠 redundant — every kill shared with another file (${redundant.length})`);
lines.push("");
if (redundant.length === 0) lines.push("_none_");
else for (const f of redundant) lines.push(`- ${f}`);
lines.push("");
return lines.join("\n");
}
function main(argv) {
const wantCandidates = argv.includes("--candidates");
const useConfUniverse = !argv.includes("--no-conf-universe");
const paths = argv.slice(2).filter((a) => !FLAGS.has(a));
if (paths.length === 0) {
process.stderr.write(
"usage: mutation-radiography.mjs <mutation-1.json> [<mutation-2.json> ...] " +
"[--candidates] [--no-conf-universe]\n"
);
process.exit(2);
}
const reports = paths.map(loadMutationReport);
const universe = useConfUniverse ? tapTestFilesUniverse() : null;
if (wantCandidates) {
process.stdout.write(
renderCandidates(redundancyCandidates(reports, universe || undefined)) + "\n"
);
return;
}
const classification = aggregateRadiography(reports, universe || undefined);
process.stdout.write(renderMarkdown(classification) + "\n");
}
if (import.meta.url === `file://${process.argv[1]}`) {
main(process.argv);
}
+3
View File
@@ -0,0 +1,3 @@
// Fixed seed for PR runs (deterministic). Nightly overrides via FC_SEED=random.
export const PROPERTY_SEED = 42424242;
export const PROPERTY_NUM_RUNS = 200;
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env node
// 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();
+62
View File
@@ -0,0 +1,62 @@
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const HUB_RE = /(setupPolyfill|tsconfig|package\.json|package-lock\.json|\.env|vitest\.config|stryker\.conf)/;
// A changed file counts as a "run-it" test ONLY if it is a node:test unit file the TIA
// step can actually run via `node --test` — i.e. it mirrors the `npm run test:unit` glob.
// This EXCLUDES vitest files (`.test.tsx`, `tests/unit/autoCombo/**`), e2e and integration
// tests, and `src/**/__tests__`/`open-sse/**/__tests__`, which can't run under node:test.
const UNIT_SUBDIRS =
"api|auth|authz|build|cli|cli-helper|compression|correctness|cors|dashboard|db|db-adapters|docs|gamification|guardrails|lib|mcp|runtime|security|services|settings|shared|ui";
const TEST_RE = new RegExp(`^tests/unit/([^/]+\\.test\\.ts$|(${UNIT_SUBDIRS})/.*\\.test\\.ts$)`);
export function selectImpacted({ changed, map }) {
const out = new Set();
for (const f of changed) {
if (HUB_RE.test(f)) return ["__RUN_ALL__"];
if (TEST_RE.test(f)) {
out.add(f);
continue;
}
const isSource =
f.startsWith("src/") ||
f.startsWith("open-sse/") ||
f.startsWith("electron/") ||
f.startsWith("bin/");
if (!isSource) continue;
const hits = map.sources[f];
if (!hits) return ["__RUN_ALL__"];
hits.forEach((t) => out.add(t));
}
return [...out].sort();
}
function changedFiles() {
const baseRef = process.env.GITHUB_BASE_REF;
const baseTarget = process.env.GITHUB_BASE_SHA || (baseRef ? `origin/${baseRef}` : "HEAD~1");
const stdout = execFileSync(
"git",
["diff", "--name-only", "--diff-filter=ACMR", `${baseTarget}...HEAD`],
{ cwd: ROOT, encoding: "utf8" }
);
return stdout
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
}
if (import.meta.url === `file://${process.argv[1]}`) {
const mapPath = path.join(ROOT, "config/quality/test-impact-map.json");
let map;
try {
map = JSON.parse(fs.readFileSync(mapPath, "utf8"));
} catch {
console.log("__RUN_ALL__");
process.exit(0);
}
const sel = selectImpacted({ changed: changedFiles(), map });
process.stdout.write(sel.join("\n") + "\n");
}
+511
View File
@@ -0,0 +1,511 @@
#!/usr/bin/env node
// scripts/quality/validate-release-green.mjs
//
// "Release-green" pre-flight validator (Solution C).
//
// WHY: the full gate (ci.yml — unit shards, vitest, ratchets, package-artifact)
// runs ONLY on the release PR (PR → main). PRs into release/** only get the
// fast-gates (quality.yml: TIA-impacted tests + typecheck + lint checks). So
// reds accumulate silently on the release branch and explode — in layers — at
// release time. This script reproduces the release-equivalent validation against
// the CURRENT working tree so the maintainer (or the nightly, Solution D) can see
// the real state of the release branch at any time.
//
// DESIGN — never blocking to contributors:
// • HARD checks (typecheck, lint errors, db-rules, public-creds, docs-all,
// unit, vitest, integration, optionally package-artifact) → a failure here is
// a real defect; exit 1.
// • DRIFT checks (eslint WARNINGS, cognitive-complexity, file-size, cyclomatic
// complexity, dead-code, type-coverage, compression-budget, openapi-coverage,
// workflow-lint/zizmor, codeql-ratchet) → ratchet drift accrued across the
// cycle is NOT a contributor's fault; it is reported and rebaselined by the
// maintainer at release. Drift NEVER changes the exit code, so wiring this as
// a check can never block anyone on drift.
//
// COMPLETENESS: this mirrors the FULL release-PR gate set (quality-gate +
// quality-extended + docs-sync-strict + integration), not a subset — and reports
// EVERY red in one pass (the report is collected, not fail-fast), so the release
// PR is green on its first CI run instead of revealing reds in ~40-min layers. The
// only release-PR gates it cannot reproduce locally are GitHub-side CodeQL semantic
// analysis and SonarQube/SonarCloud (external services).
//
// This script DIAGNOSES + REPORTS only (no auto-fix). The fix-to-green
// orchestration lives in the /green-prs + review-prs flows that call it.
//
// Usage:
// node scripts/quality/validate-release-green.mjs [--json] [--with-build] [--quick] [--hermetic]
// --json emit machine-readable JSON to stdout (report goes to stderr)
// --with-build also run check:pack-artifact (needs a dist/ build — slow)
// --quick skip the slow unit + vitest + integration suites (drift + fast
// gates only)
// --hermetic scrub OMNIROUTE_API_KEY/OMNIROUTE_URL from gate env so live
// tests self-skip exactly like CI (dev machines otherwise run
// them against localhost and produce false-positive reds)
//
// Per-gate output is saved to _artifacts/release-green/<gate>.log (gitignored) —
// diagnose a red from the file instead of re-running the gate.
import { execFile, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..");
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
// Per-gate captured output. execFileSync buffers everything and the report only
// shows a one-line summary, so without these files every red requires RE-RUNNING
// the gate just to see the detail (the dominant cost of the 2026-07-05 pre-flight).
const LOG_DIR = join(ROOT, "_artifacts", "release-green");
function saveGateLog(id, out) {
try {
mkdirSync(LOG_DIR, { recursive: true });
writeFileSync(join(LOG_DIR, `${id}.log`), String(out ?? ""));
} catch {
/* log persistence is best-effort — never fails a gate */
}
}
// ─── Pure helpers (exported for tests) ──────────────────────────────────────
/** Read the committed ratchet baseline value for a metric (null if unknown). */
export function baselineValue(metric, root = ROOT) {
try {
const raw = JSON.parse(
readFileSync(join(root, "config/quality/quality-baseline.json"), "utf8")
);
const metrics = raw.metrics || raw;
const v = metrics?.[metric]?.value;
return typeof v === "number" ? v : null;
} catch {
return null;
}
}
/** Best-effort "first meaningful failure line" from captured command output. */
export function firstFailureLine(out) {
const lines = String(out || "")
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
const hit = lines.find((l) => /|not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l));
return (hit || lines[lines.length - 1] || "failed").slice(0, 200);
}
/** Sum {errorCount,warningCount} across an eslint --format json result array. */
export function eslintCounts(parsed) {
let errors = 0;
let warnings = 0;
for (const f of parsed || []) {
errors += f.errorCount || 0;
warnings += f.warningCount || 0;
}
return { errors, warnings };
}
/** Parse the eslint JSON array out of mixed stdout (tolerates a leading banner). */
export function parseEslintJson(out) {
const start = String(out || "").indexOf("[");
if (start < 0) return null;
try {
return JSON.parse(String(out).slice(start));
} catch {
return null;
}
}
/** Pull the cognitive-complexity violation count from the gate's output. */
export function parseCognitiveCount(out) {
const m = String(out || "").match(/(\d+)\s+(?:function\(s\) exceed|violações|violations)/i);
return m ? Number(m[1]) : null;
}
/**
* Drift verdict for a ratchet: a metric that grew past its committed baseline is
* "drift" (reported, never blocking). `direction:"down"` metrics (warnings,
* complexity, file-size counts) regress when current > baseline.
*/
export function isDrift(current, baseline) {
if (typeof current !== "number" || typeof baseline !== "number") return false;
return current > baseline;
}
/** releaseGreen iff there are zero failing HARD checks (drift never blocks). */
export function computeVerdict(results) {
const hardFailures = results.filter((r) => r.kind === "hard" && !r.ok);
const drift = results.filter((r) => r.kind === "drift" && !r.ok);
return { releaseGreen: hardFailures.length === 0, hardFailures, drift };
}
// ─── Orchestration (only when run directly) ─────────────────────────────────
/**
* Map a thrown `execFileSync` error to a {code, out} gate result. Exported as a pure helper
* so the timeout/hang path has a regression test: a gate that exceeds its ceiling (e.g. the unit
* suite wedged on an unreleased SQLite handle — see CLAUDE.md "Database Handles in Tests") is
* killed by `execFileSync` (`err.killed === true`) and MUST surface as a visible non-zero gate,
* never an infinite block that the release captain mistakes for a hang and kills the pre-flight.
*/
export function classifyRunError(err, timeoutMs) {
if (err && err.killed && timeoutMs) {
return {
code: 124,
out: `gate exceeded its ${Math.round(timeoutMs / 1000)}s ceiling and was killed — treat as a hung/failed gate (e.g. an unreleased DB handle in the unit suite); does NOT pass`,
};
}
return {
code: typeof err?.status === "number" ? err.status : 1,
out: `${err?.stdout || ""}${err?.stderr || ""}`,
};
}
// --hermetic: scrub the live-test trigger vars so the pre-flight behaves like CI
// (a dev machine with OMNIROUTE_API_KEY set runs 17+ live tests that CI skips —
// every one a false-positive red against the release branch).
const HERMETIC_SCRUB = ["OMNIROUTE_API_KEY", "OMNIROUTE_URL"];
let hermetic = false;
function buildGateEnv(extra) {
const env = { ...process.env, FORCE_COLOR: "0", ...(extra || {}) };
if (hermetic) for (const k of HERMETIC_SCRUB) delete env[k];
return env;
}
function run(cmd, cmdArgs, opts = {}) {
try {
const out = execFileSync(cmd, cmdArgs, {
cwd: ROOT,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 256 * 1024 * 1024,
env: buildGateEnv(opts.env),
// A hard ceiling for the long, silent test suites (execFileSync buffers all output until
// exit, so they show no progress while running). undefined = no timeout for fast gates.
...(opts.timeout ? { timeout: opts.timeout } : {}),
});
return { code: 0, out };
} catch (err) {
return classifyRunError(err, opts.timeout);
}
}
const execFileAsync = promisify(execFile);
// Async twin of run() — same {code, out} contract, so the slow suites (unit /
// vitest / integration / pack-artifact) can run CONCURRENTLY instead of in
// series. Sequentially they dominate the pre-flight wall time (~2h in the
// v3.8.45 run); they are independent processes with per-process DATA_DIR
// isolation, so overlapping them cuts the pre-flight to ~the slowest single one.
async function runAsync(cmd, cmdArgs, opts = {}) {
try {
const { stdout, stderr } = await execFileAsync(cmd, cmdArgs, {
cwd: ROOT,
encoding: "utf8",
maxBuffer: 256 * 1024 * 1024,
env: buildGateEnv(opts.env),
...(opts.timeout ? { timeout: opts.timeout } : {}),
});
return { code: 0, out: `${stdout || ""}${stderr || ""}` };
} catch (err) {
return classifyRunError(err, opts.timeout);
}
}
async function main() {
const args = new Set(process.argv.slice(2));
const JSON_OUT = args.has("--json");
const WITH_BUILD = args.has("--with-build");
const QUICK = args.has("--quick");
hermetic = args.has("--hermetic");
const results = [];
const record = (r) => {
results.push(r);
const icon = r.ok ? "✅" : r.kind === "drift" ? "🟡" : "❌";
process.stderr.write(`${icon} [${r.kind}] ${r.label}${r.detail ? `${r.detail}` : ""}\n`);
};
// Announce a gate BEFORE running it. The long suites (unit/vitest/integration) run silently
// for many minutes (execFileSync buffers their output until exit), which previously looked like
// a hang and got the pre-flight killed before it surfaced the unit reds (the v3.8.42 miss).
const announce = (label) => process.stderr.write(`${label}\n`);
const hardCmd = (id, label, cmd, cmdArgs, opts) => {
announce(label);
const { code, out } = run(cmd, cmdArgs, opts);
saveGateLog(id, out);
record({
id,
label,
kind: "hard",
ok: code === 0,
detail: code === 0 ? "pass" : firstFailureLine(out),
});
};
// A ratchet command (check:complexity, check:dead-code, …) exits 1 ONLY on a
// measured regression and self-skips (exit 0) when its tooling is absent — so a
// non-zero exit here is drift to rebaseline at release, never a contributor block.
// ALL checks run regardless of earlier failures (the report is collected, not
// fail-fast) so one pass surfaces every red instead of revealing them in layers.
const driftCmd = (id, label, cmd, cmdArgs, okDetail = "within baseline", opts) => {
announce(label);
const { code, out } = run(cmd, cmdArgs, opts);
saveGateLog(id, out);
record({
id,
label,
kind: "drift",
ok: code === 0,
detail: code === 0 ? okDetail : firstFailureLine(out),
});
};
process.stderr.write("🔎 Release-green validation (current working tree)\n\n");
hardCmd("typecheck", "Typecheck (core)", npmCmd, ["run", "typecheck:core"]);
// ESLint: ONE pass → errors (hard) + warnings (drift)
{
announce("ESLint (errors + warnings — ~5-15min)");
// Suppressions-aware, matching `npm run lint` (Pacote 4 no-new-warnings): the frozen
// pre-existing debt in config/quality/eslint-suppressions.json must not count as
// errors here — only NET-NEW violations are release reds. Timeout raised: a full
// repo pass takes ~14min alone and this pre-flight often runs alongside test suites.
const { out } = run(
"npx",
[
"eslint",
".",
"--format",
"json",
"--suppressions-location",
"config/quality/eslint-suppressions.json",
],
{ timeout: 30 * 60 * 1000 }
);
saveGateLog("lint", out);
const parsed = parseEslintJson(out);
if (!parsed) {
record({
id: "lint",
label: "ESLint",
kind: "hard",
ok: false,
detail: "could not parse eslint json",
});
} else {
const { errors, warnings } = eslintCounts(parsed);
record({
id: "lint-errors",
label: "ESLint errors",
kind: "hard",
ok: errors === 0,
detail: `${errors} error(s)`,
});
const base = baselineValue("eslintWarnings");
const over = isDrift(warnings, base);
record({
id: "eslint-warnings",
label: "ESLint warnings (ratchet)",
kind: "drift",
ok: !over,
detail:
base == null
? `${warnings} (no baseline)`
: `${warnings} vs baseline ${base}${over ? ` (+${warnings - base} drift → rebaseline at release)` : ""}`,
});
}
}
hardCmd("db-rules", "DB rules", npmCmd, ["run", "check:db-rules"]);
hardCmd("public-creds", "Public creds", npmCmd, ["run", "check:public-creds"]);
// Cognitive-complexity (drift)
{
announce("Cognitive complexity (ratchet)");
const { out } = run(npmCmd, ["run", "check:cognitive-complexity"]);
saveGateLog("cognitive", out);
const current = parseCognitiveCount(out);
const base = baselineValue("cognitiveComplexity");
const over = isDrift(current, base);
record({
id: "cognitive-complexity",
label: "Cognitive complexity (ratchet)",
kind: "drift",
ok: !over,
detail:
current == null
? "could not parse count"
: `${current} vs baseline ${base}${over ? ` (+${current - base} drift → rebaseline at release)` : ""}`,
});
}
// file-size (drift)
{
const { code, out } = run(npmCmd, ["run", "check:file-size"]);
record({
id: "file-size",
label: "File-size ratchet",
kind: "drift",
ok: code === 0,
detail: code === 0 ? "within frozen caps" : firstFailureLine(out),
});
}
// test-masking (hard) — a PR-context gate: it only runs on the release PR (PR→main) in CI, so
// net-assert reductions accrue unseen on release/** and explode on the release PR. Reproduce it
// here against origin/main so a non-allowlisted reduction surfaces in the pre-flight, not in a
// ~40-min CI layer (v3.8.43 cost 3 such round-trips). Legitimate reductions get allowlisted in
// config/quality/test-masking-allowlist.json; tautology/skip/deletion signals are never allowlistable.
if (!QUICK) {
announce("Test-masking (weakened-assert guard vs main)");
// best-effort fetch so the merge-base diff is accurate; ignore fetch failure (offline pre-flight)
run("git", ["fetch", "--no-tags", "origin", "main", "--depth=200"], { timeout: 60 * 1000 });
const { code, out } = run(npmCmd, ["run", "check:test-masking"], {
env: { GITHUB_BASE_REF: "main" },
});
saveGateLog("test-masking", out);
record({
id: "test-masking",
label: "Test-masking (weakened-assert guard)",
kind: "hard",
ok: code === 0,
detail: code === 0 ? "no weakening" : firstFailureLine(out),
});
}
// Remaining quality-gate / quality-extended ratchets that the PR→release
// fast-gates skip and that historically surfaced — one at a time, because the
// CI Quality Ratchet job is fail-fast — only on the release PR. Running them all
// here (drift, never blocking) means a single rebaseline pass at release.
driftCmd("complexity", "Cyclomatic complexity (ratchet)", npmCmd, ["run", "check:complexity"]);
driftCmd("dead-code", "Dead-code (ratchet)", npmCmd, ["run", "check:dead-code"]);
driftCmd("type-coverage", "Type coverage (ratchet)", npmCmd, ["run", "check:type-coverage"]);
driftCmd("compression-budget", "Compression budget (ratchet)", npmCmd, [
"run",
"check:compression-budget",
]);
driftCmd("openapi-coverage", "OpenAPI route coverage (ratchet)", npmCmd, [
"run",
"check:openapi-coverage",
]);
driftCmd("workflow-lint", "Workflow lint (zizmor ratchet)", npmCmd, [
"run",
"check:workflows",
"--",
"--ratchet",
]);
driftCmd("codeql-ratchet", "CodeQL alerts (ratchet)", npmCmd, ["run", "check:codeql-ratchet"]);
// Docs sync + fabricated-docs (strict) is a real-defect gate (invented env vars /
// routes, i18n mirror drift) — HARD.
hardCmd("docs-all", "Docs sync + fabricated-docs (strict)", npmCmd, ["run", "check:docs-all"]);
if (!QUICK) {
// These are the gates that catch inherited base-red tests from cycle PRs (the fast-path
// PR→release does NOT run unit/vitest/integration per-PR — the v3.8.42 release PR exploded
// with 15 such reds). They run SILENTLY for many minutes; the announce line above + these
// hard ceilings keep a long-but-healthy run from being mistaken for a hang (the ceiling also
// converts a genuine DB-handle hang into a visible failure instead of an infinite block).
// The slow suites are INDEPENDENT processes (each self-isolates DATA_DIR) with
// no shared state, so they run CONCURRENTLY — the pre-flight wall time becomes
// ~the slowest single suite instead of their sum (unit ~25-35min + vitest
// ~3-8min + integration ~3-10min + pack-artifact ~15min was ~1h serial in the
// v3.8.45 run). pack-artifact (--with-build) joins the same wave. Integration
// runs ONLY on the release PR full CI, so a regression here is invisible until
// release — that is why it is a HARD pre-flight gate.
const slow = [
{
id: "unit",
label: "Unit tests (full suite, CI concurrency — runs ~20-35min silently)",
args: ["run", "test:unit:ci"],
timeout: 45 * 60 * 1000,
},
{
id: "vitest",
label: "Vitest (MCP / autoCombo / cache — ~3-8min)",
args: ["run", "test:vitest"],
timeout: 15 * 60 * 1000,
},
{
id: "integration",
label: "Integration tests (~3-10min)",
args: ["run", "test:integration"],
timeout: 20 * 60 * 1000,
},
];
if (WITH_BUILD) {
slow.push({
id: "pack-artifact",
label: "Package artifact (npm pack policy)",
args: ["run", "check:pack-artifact"],
timeout: 20 * 60 * 1000,
});
}
slow.forEach((g) => announce(`${g.label} [parallel]`));
const slowResults = await Promise.all(
slow.map((g) => runAsync(npmCmd, g.args, { timeout: g.timeout }))
);
slow.forEach((g, i) => {
const { code, out } = slowResults[i];
saveGateLog(g.id, out);
record({
id: g.id,
label: g.label,
kind: "hard",
ok: code === 0,
detail: code === 0 ? "pass" : firstFailureLine(out),
});
});
} else if (WITH_BUILD) {
// --with-build without the suites (--quick): still verify the package artifact.
const { code, out } = await runAsync(npmCmd, ["run", "check:pack-artifact"], {
timeout: 20 * 60 * 1000,
});
saveGateLog("pack-artifact", out);
record({
id: "pack-artifact",
label: "Package artifact (npm pack policy)",
kind: "hard",
ok: code === 0,
detail: code === 0 ? "pass" : firstFailureLine(out),
});
}
const { releaseGreen, hardFailures, drift } = computeVerdict(results);
process.stderr.write("\n──────── verdict ────────\n");
process.stderr.write(`HARD failures (block — real defects): ${hardFailures.length}\n`);
hardFailures.forEach((r) => process.stderr.write(`${r.label}: ${r.detail}\n`));
process.stderr.write(`Ratchet drift (non-blocking — rebaseline at release): ${drift.length}\n`);
drift.forEach((r) => process.stderr.write(` 🟡 ${r.label}: ${r.detail}\n`));
process.stderr.write(
releaseGreen
? "\n✅ RELEASE-GREEN (no hard failures). Any drift above is rebaselined at release, not a contributor concern.\n"
: "\n❌ NOT release-green — hard failures must be fixed (in the originating PR branch, via co-authorship).\n"
);
if (JSON_OUT) {
process.stdout.write(
JSON.stringify(
{
releaseGreen,
hardFailures: hardFailures.map((r) => ({ id: r.id, label: r.label, detail: r.detail })),
drift: drift.map((r) => ({ id: r.id, label: r.label, detail: r.detail })),
checks: results.map((r) => ({ id: r.id, kind: r.kind, ok: r.ok, detail: r.detail })),
},
null,
2
) + "\n"
);
}
process.exit(releaseGreen ? 0 : 1);
}
// Run only when invoked directly (so tests can import the pure helpers).
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
main();
}