feat(quality): unblock R1 — test-redundancy measurement via disableBail (#4322)

* feat(quality): config stryker disableBail para medir redundância (R1)

Estende stryker.conf.json com disableBail:true (killedBy lista TODOS os killers,
não só o primeiro) + incremental:false (medição limpa). One-off, não toca o
nightly — só roda via mutation-redundancy.yml.

* feat(quality): workflow on-demand de mutação disableBail (R1)

mutation-redundancy.yml (workflow_dispatch): roda os 6 leaf-batches combo+chatCore
do nightly com o override disableBail e sobe os reports. Batches granulares (d/e/f/
g/h/i) em vez de 2 mega-batches: disableBail é mais caro e Stryker só grava o report
se COMPLETAR, então cada batch cabe nos 300min.
zizmor 139->145: +3 drift de base + 3 do workflow (@vN, convenção do repo). Ver nota.

* feat(quality): mutation-radiography --candidates (lista de prune R1)

Reusa aggregateRadiography (DRY) em vez de um script novo: redundancyCandidates()
retorna 🔴 empty ∪ 🟠 redundant (zero kills únicos) = candidatos a prune. Sob
disableBail o killedBy é completo, então 🟠 redundant fica ACURADO. CLI --candidates
emite a lista com o aviso do gate humano (excluir segurança/contrato/repro).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-19 21:40:57 -03:00
committed by GitHub
parent 0d6c5686d2
commit facbb1964f
5 changed files with 168 additions and 5 deletions

View File

@@ -137,6 +137,34 @@ export function aggregateRadiography(reports, allTestFiles) {
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() {
@@ -206,18 +234,54 @@ function renderMarkdown(classification) {
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 args = argv.filter((a) => a !== "--no-conf-universe");
const wantCandidates = argv.includes("--candidates");
const useConfUniverse = !argv.includes("--no-conf-universe");
const paths = args.slice(2);
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> ...] [--no-conf-universe]\n"
"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");
}