feat(quality): Fase 4b — coverage ratchet (conservative floors, CI consumes merged coverage)

- quality-baseline.json: coverage.{statements,lines,functions,branches} floors (80/80/82/73, real ~82.58/82.58/84.23/75.22 with margin; tighten via --update after a green main run)
- check-quality-ratchet.mjs: --allow-missing (local quality:gate skips coverage.* without a coverage run; CI runs strict)
- ci.yml quality-gate job: needs test-coverage + downloads merged coverage-report so the ratchet enforces 'coverage cannot drop'
- TDD +1 test (6 total)
This commit is contained in:
diegosouzapw
2026-06-09 08:03:51 -03:00
parent 2f81c35265
commit bb5ed0c0da
5 changed files with 35 additions and 5 deletions

View File

@@ -52,6 +52,8 @@ jobs:
quality-gate:
name: Quality Ratchet
runs-on: ubuntu-latest
needs: test-coverage
if: ${{ always() && needs.test-coverage.result == 'success' }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
@@ -59,6 +61,11 @@ jobs:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura.
- uses: actions/download-artifact@v8
with:
name: coverage-report
path: coverage/
- run: npm run quality:collect
# Catraca: falha se qualquer métrica regredir vs quality-baseline.json (commitado).
# Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do

View File

@@ -121,7 +121,7 @@
"check:test-masking": "node scripts/check/check-test-masking.mjs",
"quality:collect": "node scripts/quality/collect-metrics.mjs",
"quality:ratchet": "node scripts/quality/check-quality-ratchet.mjs",
"quality:gate": "npm run quality:collect && npm run quality:ratchet",
"quality:gate": "npm run quality:collect && npm run quality:ratchet -- --allow-missing",
"audit:deps": "npm audit --audit-level=critical && (npm audit --audit-level=high || echo '::warning::high-severity advisories present (non-blocking)') && npm run audit:electron",
"audit:electron": "npm --prefix electron audit --audit-level=moderate",
"typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json",

View File

@@ -1,6 +1,11 @@
{
"_comment": "Catraca de qualidade. 'down' = nao pode aumentar; 'up' = nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (somente quando melhora). Cada valor e um numero REAL medido, nunca um chute. Cobertura entra na Fase 4 a partir de um run de cobertura mergeada no CI.",
"metrics": {
"eslintWarnings": { "value": 3482, "direction": "down" }
}
"eslintWarnings": { "value": 3482, "direction": "down" },
"coverage.statements": { "value": 80, "direction": "up" },
"coverage.lines": { "value": 80, "direction": "up" },
"coverage.functions": { "value": 82, "direction": "up" },
"coverage.branches": { "value": 73, "direction": "up" }
},
"_coverage_note": "Pisos conservadores (real ~82,58/82,58/84,23/75,22 em COVERAGE_PLAN.md 2026-05-13, ~2pt de margem p/ evitar false-fail antes de calibrar no CI). Apos o 1o run verde de coverage mergeada na main, aperte com 'npm run quality:ratchet -- --update'."
}

View File

@@ -14,6 +14,10 @@ const BASELINE = path.resolve(getArg("--baseline", path.join(cwd, "quality-basel
const METRICS = path.resolve(getArg("--metrics", path.join(cwd, "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");
const EPS = 0.01;
function load(p) {
@@ -35,8 +39,12 @@ for (const [key, spec] of Object.entries(baseline.metrics)) {
const base = spec.value;
const dir = spec.direction; // "down" = menor-é-melhor | "up" = maior-é-melhor
if (current === undefined) {
failures.push(`métrica "${key}" ausente em ${path.basename(METRICS)}`);
rows.push([key, base, "—", "MISSING"]);
if (ALLOW_MISSING) {
rows.push([key, base, "—", "SKIP (ausente)"]);
} else {
failures.push(`métrica "${key}" ausente em ${path.basename(METRICS)}`);
rows.push([key, base, "—", "MISSING"]);
}
continue;
}
let status = "ok";

View File

@@ -58,3 +58,13 @@ test("fails when a baseline metric is missing from collected metrics", () => {
const b = { metrics: { eslintWarnings: { value: 100, direction: "down" } } };
assert.equal(run(b, {}).code, 1);
});
test("--allow-missing skips absent metrics instead of failing", () => {
const b = {
metrics: {
eslintWarnings: { value: 100, direction: "down" },
"coverage.lines": { value: 80, direction: "up" },
},
};
assert.equal(run(b, { eslintWarnings: 100 }, ["--allow-missing"]).code, 0);
});