Fase 7 finalize — 3 catracas advisory→bloqueante + re-baseline consciente v3.8.25 (#3809)

Integrated into release/v3.8.25 — Fase 7 finalize: 3 catracas advisory→bloqueante (dead-code/cognitive-complexity/type-coverage) + re-baseline consciente.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-14 18:06:56 -03:00
committed by GitHub
parent c4f2af70f0
commit cbb332d355
10 changed files with 500 additions and 45 deletions

View File

@@ -1,30 +1,39 @@
#!/usr/bin/env node
// scripts/check/check-cognitive-complexity.mjs
// Advisory gate para complexidade cognitiva (sonarjs/cognitive-complexity).
// Ratchet bloqueante para complexidade cognitiva (sonarjs/cognitive-complexity).
// Fase 7 INT: promovido de ADVISORY para RATCHET.
//
// Roda o ESLint sobre src+open-sse usando um config flat STANDALONE
// (eslint.sonarjs.config.mjs) que liga APENAS `sonarjs/cognitive-complexity` —
// mantendo a contagem ISOLADA do orçamento de warnings do lint principal (3653).
// mantendo a contagem ISOLADA do orçamento de warnings do lint principal.
//
// Modo advisory: sai com código 0 independente da contagem. Imprime o valor
// para anotação do baseline conceitual. O ratchet INT virá quando o baseline
// for congelado em quality-baseline.json.
// Lê o baseline de quality-baseline.json (metrics.cognitiveComplexity).
// Falha com exit 1 se a contagem SUBIR. Suporta --update.
//
// Saída canônica: cognitiveComplexity=N (parseable por collect-metrics.mjs)
//
// Uso:
// node scripts/check/check-cognitive-complexity.mjs
// node scripts/check/check-cognitive-complexity.mjs --quiet # só a linha canônica
// node scripts/check/check-cognitive-complexity.mjs --update # ratcheta baseline se melhorou
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
const ROOT = process.cwd();
const QUIET = process.argv.includes("--quiet");
const UPDATE = process.argv.includes("--update");
const CONFIG_PATH = path.join(ROOT, "eslint.sonarjs.config.mjs");
const ESLINT_BIN = path.join(ROOT, "node_modules", ".bin", "eslint");
const BASELINE_PATH = path.resolve(
process.argv.includes("--baseline")
? process.argv[process.argv.indexOf("--baseline") + 1]
: path.join(ROOT, "quality-baseline.json")
);
const ESLINT_ARGS = [
"--no-config-lookup",
"--config",
@@ -56,6 +65,23 @@ export function countCognitiveViolations(report) {
return count;
}
/**
* Avalia a contagem atual de violações cognitivas contra o baseline.
* Direction: down (contagem só pode CAIR).
*
* Exported for unit testing.
*
* @param {number} current
* @param {number} baseline
* @returns {{ regressed: boolean, improved: boolean }}
*/
export function evaluateCognitiveComplexity(current, baseline) {
return {
regressed: current > baseline,
improved: current < baseline,
};
}
function runEslint() {
let stdout;
try {
@@ -73,22 +99,56 @@ function runEslint() {
}
function main() {
if (!fs.existsSync(BASELINE_PATH)) {
process.stderr.write(
`[cognitive-complexity] FAIL — ${path.basename(BASELINE_PATH)} ausente.\n`
);
process.exit(2);
}
const baselineJson = JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8"));
const baselineMetric = baselineJson.metrics && baselineJson.metrics.cognitiveComplexity;
if (!baselineMetric || typeof baselineMetric.value !== "number") {
process.stderr.write(
"[cognitive-complexity] FAIL — metrics.cognitiveComplexity ausente em quality-baseline.json.\n"
);
process.exit(2);
}
const baselineValue = baselineMetric.value;
const report = runEslint();
const count = countCognitiveViolations(report);
// Canonical machine-readable output consumed by collect-metrics.mjs and shell scripts.
console.log(`cognitiveComplexity=${count}`);
if (!QUIET) {
console.log(
`[cognitive-complexity] advisory — ${count} function(s) exceed the cognitive-complexity threshold (15).`
);
console.log(
`[cognitive-complexity] Annotate this value as the baseline in quality-baseline.json when the INT ratchet is wired.`
`[cognitive-complexity] ${count} function(s) exceed the cognitive-complexity threshold (15).`
);
}
// Canonical machine-readable output consumed by collect-metrics.mjs
console.log(`cognitiveComplexity=${count}`);
const { regressed, improved } = evaluateCognitiveComplexity(count, baselineValue);
if (UPDATE && improved) {
baselineJson.metrics.cognitiveComplexity.value = count;
fs.writeFileSync(BASELINE_PATH, JSON.stringify(baselineJson, null, 2) + "\n");
console.log(`[cognitive-complexity] baseline ratcheado: ${count} (era ${baselineValue})`);
}
if (regressed) {
process.stderr.write(
`[cognitive-complexity] REGRESSÃO — ${count} violações > baseline ${baselineValue}\n` +
` → Quebre as funções complexas em helpers menores, ou rode\n` +
` 'node scripts/check/check-cognitive-complexity.mjs --update' se a contagem caiu legitimamente.\n`
);
process.exit(1);
}
if (!QUIET) {
console.log(`[cognitive-complexity] OK — ${count} violações (baseline ${baselineValue})`);
}
// Advisory: always exit 0
process.exit(0);
}

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env node
// scripts/check/check-dead-code.mjs
// Gate de dead-code via knip — unused exports, unused files.
// Esta versão é ADVISORY (sai 0 sempre, exceto erro de execução).
// O ratchet no quality-baseline.json entra no bloco INT da Fase 7.
// Fase 7 INT: promovido de ADVISORY para RATCHET bloqueante.
// Lê o baseline de quality-baseline.json (metrics.deadExports), compara e
// falha com exit 1 se a contagem SUBIR. Suporta --update para ratchetar o baseline.
//
// Saída (stdout):
// DEAD_EXPORTS=<n> — exports/re-exports/tipos não utilizados
@@ -11,8 +12,10 @@
//
// Use --json para imprimir o relatório completo do knip em JSON.
// Use --quiet para suprimir logs de diagnóstico.
// Use --update para ratchetar o baseline quando a contagem cair legitimamente.
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
@@ -20,6 +23,13 @@ const ROOT = process.cwd();
const KNIP_BIN = path.join(ROOT, "node_modules", ".bin", "knip");
const QUIET = process.argv.includes("--quiet");
const PRINT_JSON = process.argv.includes("--json");
const UPDATE = process.argv.includes("--update");
const BASELINE_PATH = path.resolve(
process.argv.includes("--baseline")
? process.argv[process.argv.indexOf("--baseline") + 1]
: path.join(ROOT, "quality-baseline.json")
);
/**
* Conta dead exports e dead files a partir do output JSON do knip.
@@ -53,7 +63,15 @@ export function parseKnipMetrics(knipJson) {
// (conservador: só contar quando files[] está presente e populado)
// Dead exports: somar todos os símbolos mortos por tipo de export
const exportFields = ["exports", "types", "nsExports", "nsTypes", "enumMembers", "namespaceMembers", "duplicates"];
const exportFields = [
"exports",
"types",
"nsExports",
"nsTypes",
"enumMembers",
"namespaceMembers",
"duplicates",
];
for (const field of exportFields) {
if (Array.isArray(fileEntry[field])) {
deadExports += fileEntry[field].length;
@@ -68,11 +86,29 @@ export function parseKnipMetrics(knipJson) {
};
}
/**
* Avalia a contagem atual de dead-code total contra o baseline.
* Direction: down (contagem só pode CAIR).
*
* Exported for unit testing.
*
* @param {number} current
* @param {number} baseline
* @returns {{ regressed: boolean, improved: boolean }}
*/
export function evaluateDeadCode(current, baseline) {
return {
regressed: current > baseline,
improved: current < baseline,
};
}
function runKnip() {
const args = [
"--reporter", "json",
"--reporter",
"json",
"--no-progress",
"--no-exit-code", // não falha por contagem — só coletamos métricas
"--no-exit-code", // não falha por contagem — só coletamos métricas
];
if (!QUIET) {
@@ -109,6 +145,21 @@ function runKnip() {
}
function main() {
if (!fs.existsSync(BASELINE_PATH)) {
process.stderr.write(`[dead-code] FAIL — ${path.basename(BASELINE_PATH)} ausente.\n`);
process.exit(2);
}
const baselineJson = JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8"));
const baselineMetric = baselineJson.metrics && baselineJson.metrics.deadExports;
if (!baselineMetric || typeof baselineMetric.value !== "number") {
process.stderr.write(
"[dead-code] FAIL — metrics.deadExports ausente em quality-baseline.json.\n"
);
process.exit(2);
}
const baselineValue = baselineMetric.value;
const knipJson = runKnip();
if (PRINT_JSON) {
@@ -123,16 +174,24 @@ function main() {
console.log(`DEAD_FILES=${deadFiles}`);
console.log(`DEAD_TOTAL=${deadTotal}`);
if (!QUIET) {
process.stderr.write(
`[dead-code] exports mortos: ${deadExports} | arquivos mortos: ${deadFiles} | total: ${deadTotal}\n`
);
process.stderr.write(
`[dead-code] ADVISORY — esta versão não falha pela contagem (ratchet entra no INT da Fase 7).\n`
);
const { regressed, improved } = evaluateDeadCode(deadTotal, baselineValue);
if (UPDATE && improved) {
baselineJson.metrics.deadExports.value = deadTotal;
fs.writeFileSync(BASELINE_PATH, JSON.stringify(baselineJson, null, 2) + "\n");
console.log(`[dead-code] baseline ratcheado: ${deadTotal} (era ${baselineValue})`);
}
// Sai 0 sempre nesta versão (advisory)
if (regressed) {
process.stderr.write(
`[dead-code] REGRESSÃO — ${deadTotal} símbolos mortos > baseline ${baselineValue}\n` +
` → Remova exports/arquivos não utilizados ou rode\n` +
` 'node scripts/check/check-dead-code.mjs --update' se a contagem caiu legitimamente.\n`
);
process.exit(1);
}
console.log(`[dead-code] OK — ${deadTotal} símbolos mortos (baseline ${baselineValue})`);
process.exitCode = 0;
}

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env node
// scripts/check/check-type-coverage.mjs
// Type-coverage ratchet (Task 6 of Fase 7).
// Fase 7 INT: promovido de ADVISORY para RATCHET bloqueante.
//
// Measures the % of typed symbols across the codebase using the `type-coverage`
// tool and prints `typeCoveragePct=<N>`. This is advisory in Phase-INT (exits 0)
// — it complements the per-file explicit-any budget in check-t11-any-budget.mjs
// with a project-wide %-typed view.
// tool and prints `typeCoveragePct=<N>`. Lê o baseline de quality-baseline.json
// (metrics.typeCoveragePct) e falha com exit 1 se a % CAIR além do eps.
//
// tsconfig used: open-sse/tsconfig.json
// - Rationale: the only tsconfig that covers the full open-sse workspace
@@ -15,12 +15,11 @@
// resolves both workspaces correctly and yields a representative global %.
//
// Direction: up (% can only improve; ratchet blocks drops once wired into INT).
// Eps: 0.05 (float noise tolerance — type-coverage may vary by ~0.01% between runs).
//
// Run:
// node scripts/check/check-type-coverage.mjs
// node scripts/check/check-type-coverage.mjs --update # ratchet baseline up
//
// Exit codes: 0 = advisory pass (current version), 1 = ratchet regression.
import { execFileSync } from "node:child_process";
import fs from "node:fs";
@@ -29,6 +28,16 @@ import { pathToFileURL } from "node:url";
const ROOT = process.cwd();
const TSCONFIG = path.join(ROOT, "open-sse", "tsconfig.json");
const UPDATE = process.argv.includes("--update");
const BASELINE_PATH = path.resolve(
process.argv.includes("--baseline")
? process.argv[process.argv.indexOf("--baseline") + 1]
: path.join(ROOT, "quality-baseline.json")
);
// Small epsilon to absorb float noise between runs (type-coverage can vary ~0.01%).
const DEFAULT_EPS = 0.05;
/**
* Parse the JSON output produced by `type-coverage --json-output`.
@@ -54,6 +63,23 @@ export function parseTypeCoverageOutput(jsonText) {
return parsed.percent;
}
/**
* Avalia a % de type-coverage atual contra o baseline.
* Direction: up (% só pode SUBIR; queda além de eps é regressão).
*
* Exported for unit testing.
*
* @param {number} current
* @param {number} baseline
* @param {number} [eps=0] - tolerance for float noise
* @returns {{ regressed: boolean, improved: boolean }}
*/
export function evaluateTypeCoverage(current, baseline, eps = 0) {
const regressed = current < baseline - eps;
const improved = current > baseline + eps;
return { regressed, improved };
}
function runTypeCoverage() {
const typeCoverageBin = path.join(ROOT, "node_modules", ".bin", "type-coverage");
@@ -82,6 +108,22 @@ function runTypeCoverage() {
}
function main() {
if (!fs.existsSync(BASELINE_PATH)) {
process.stderr.write(`[type-coverage] FAIL — ${path.basename(BASELINE_PATH)} ausente.\n`);
process.exit(2);
}
const baselineJson = JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8"));
const baselineMetric = baselineJson.metrics && baselineJson.metrics.typeCoveragePct;
if (!baselineMetric || typeof baselineMetric.value !== "number") {
process.stderr.write(
"[type-coverage] FAIL — metrics.typeCoveragePct ausente em quality-baseline.json.\n"
);
process.exit(2);
}
const baselineValue = baselineMetric.value;
const eps = typeof baselineMetric.eps === "number" ? baselineMetric.eps : DEFAULT_EPS;
console.log("[type-coverage] Running type-coverage (this may take ~30-60 s)…");
console.log(`[type-coverage] tsconfig: ${path.relative(ROOT, TSCONFIG)}`);
@@ -89,17 +131,33 @@ function main() {
try {
pct = runTypeCoverage();
} catch (err) {
console.error(`[type-coverage] FAIL — ${err.message}`);
// Advisory: exit 0 so CI is not blocked until INT wiring.
process.exit(0);
process.stderr.write(`[type-coverage] FAIL — ${err.message}\n`);
process.exit(2);
}
// Canonical output line consumed by collect-metrics.mjs and shell scripts.
console.log(`typeCoveragePct=${pct}`);
console.log(`[type-coverage] Advisory OK — ${pct}% symbols typed (direction: up)`);
// Advisory: always exit 0 in this version.
// Once wired into quality-baseline.json (INT), exit 1 on regression here.
const { regressed, improved } = evaluateTypeCoverage(pct, baselineValue, eps);
if (UPDATE && improved) {
baselineJson.metrics.typeCoveragePct.value = pct;
fs.writeFileSync(BASELINE_PATH, JSON.stringify(baselineJson, null, 2) + "\n");
console.log(`[type-coverage] baseline ratcheado: ${pct} (era ${baselineValue})`);
}
if (regressed) {
process.stderr.write(
`[type-coverage] REGRESSÃO — ${pct}% < baseline ${baselineValue}% (eps=${eps})\n` +
` → Adicione anotações de tipo ou rode\n` +
` 'node scripts/check/check-type-coverage.mjs --update' se a % subiu legitimamente.\n`
);
process.exit(1);
}
console.log(
`[type-coverage] OK — ${pct}% symbols typed (baseline ${baselineValue}%, eps=${eps})`
);
process.exit(0);
}