diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index f3f5fa0a1e..7a167afcd0 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -155,7 +155,18 @@ jobs: - run: npm run check:fetch-targets # docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered). - run: npm run check:deps - - run: npm run check:file-size + # #8522: --base-ref mode for PR events — compare against max(frozen, base) so + # inherited drift (base already over frozen cap) doesn't red an innocent PR. + # workflow_dispatch (no PR base) falls back to absolute comparison. + - name: File-size ratchet (base-relative on PR) + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + if [ -n "$PR_BASE_SHA" ]; then + npm run check:file-size -- --base-ref "$PR_BASE_SHA" + else + npm run check:file-size + fi - run: npm run check:error-helper - run: npm run check:migration-numbering - run: npm run check:public-creds diff --git a/changelog.d/fixes/8522-fix.plan.md b/changelog.d/fixes/8522-fix.plan.md new file mode 100644 index 0000000000..41a25b266b --- /dev/null +++ b/changelog.d/fixes/8522-fix.plan.md @@ -0,0 +1 @@ +- fix(quality): add base-relative file-size check so inherited drift does not red innocent PRs (#8522) diff --git a/changelog.d/maintenance/basereds-eslint-baseline-tighten.md b/changelog.d/maintenance/basereds-eslint-baseline-tighten.md new file mode 100644 index 0000000000..d2085f85c9 --- /dev/null +++ b/changelog.d/maintenance/basereds-eslint-baseline-tighten.md @@ -0,0 +1 @@ +- fix(quality): tighten eslintWarnings baseline 5000->0 to match the gate's suppressions-applied measurement (unblocks require-tighten on every code PR) diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index e20f17c04c..b25bdbca91 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -1,5 +1,4 @@ { - "_comment": "Congelamento em massa gerado em 2026-08-05 durante a migracao para TypeScript 7 (branch release/v3.8.50). A mudanca de toolchain elevou a contagem de violacoes ESLint de forma ampla e mecanica: 4344 violacoes em 676 arquivos, concentradas em @typescript-eslint/no-explicit-any (4063) e no-restricted-imports (203). Todas sao PRE-EXISTENTES ao congelamento - nenhuma foi introduzida para passar o gate. Politica (CLAUDE.md): novas violacoes DEVEM ser corrigidas, nunca adicionadas aqui; esta allowlist so cobre a divida herdada da migracao. As entradas devem ser reduzidas conforme a divida for paga (o gate quality-ratchet impede crescimento). Regenerado a partir de `npx eslint . --format json`; entradas individuais nao levam justificativa propria por serem de origem unica e mecanica - a justificativa e esta.", "open-sse/executors/blackbox-web.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -1292,11 +1291,6 @@ "count": 1 } }, - "src/lib/usage/providerLimits.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "src/lib/usage/providerWindowCosts.ts": { "no-restricted-syntax": { "count": 1 @@ -3388,4 +3382,4 @@ "count": 5 } } -} +} \ No newline at end of file diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index e0795368ec..aac7ef514a 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -2,9 +2,9 @@ "_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": 5000, + "value": 0, "direction": "down", - "_rebaseline_2026_08_05_ts7_migration": "Rebaseline para 5000 (direction: down) em 2026-08-05 por conta da migracao para TypeScript 7 na release/v3.8.50. A mudanca de toolchain elevou a contagem de warnings de forma ampla e mecanica. Medicao no tip: 4139 warnings (folga de ~860 para o teto). A divida esta congelada em config/quality/eslint-suppressions.json (ver _comment la). Apertar via `npm run quality:ratchet -- --update` conforme a divida for paga." + "_rebaseline_2026_08_05_post_prune": "Apertado 5000->0 em 2026-08-05: o gate mede via lint:json COM as suppressions aplicadas (config/quality/eslint-suppressions.json congela a divida da migracao TS7), entao a contagem real do gate e 0. O 5000 anterior foi medido SEM suppressions (4139 brutos) e fazia o require-tighten reprovar todo PR de codigo (delta 5000>slack). Divida TS7 continua rastreada nas suppressions; warning NOVO (fora delas) agora e red imediato, que e a politica." }, "eslintErrors": { "value": 0, diff --git a/scripts/check/check-file-size.mjs b/scripts/check/check-file-size.mjs index 3a3cc3a2d0..4bfc87a7a5 100644 --- a/scripts/check/check-file-size.mjs +++ b/scripts/check/check-file-size.mjs @@ -11,6 +11,7 @@ // igual ao próprio teto ficava presa no baseline para sempre — ver #8584. import fs from "node:fs"; import path from "node:path"; +import { execFileSync } from "node:child_process"; import { pathToFileURL } from "node:url"; const ROOT = process.cwd(); @@ -22,6 +23,7 @@ const BASELINE_PATH = path.resolve( getArg("--baseline", path.join(ROOT, "config/quality/file-size-baseline.json")) ); const UPDATE = process.argv.includes("--update"); +const BASE_REF = getArg("--base-ref"); // SHA for PR base-relative mode (#8522) const SCAN_DIRS = ["src", "open-sse", "electron", "bin"]; // Test files live under tests/ plus co-located *.test.ts(x) inside the source dirs. const TEST_SCAN_DIRS = ["tests", ...SCAN_DIRS]; @@ -37,20 +39,39 @@ const SKIP_DIRS = new Set(["node_modules", "dist-electron", ".next", ".build", " * (loc < frozen), entao uma entrada igual ao proprio teto nunca saia da lista, * por mais abaixo do cap que estivesse (3 casos reais no v3.8.49). * + * Quando `baseLocByFile` e fornecido (modo PR), a violacao e computada contra + * o MAIOR entre o valor congelado e o valor na base -- assim um PR inocente + * (head === base no arquivo) nao e penalizado por drift herdado (#8522). + * + * @param {Object} currentLocByFile — LOC atuais (head) + * @param {Object} frozen — baseline congelado + * @param {number} cap — teto para arquivos novos + * @param {Object} [baseLocByFile] — LOC na branch base (opcional, modo PR) * @returns {{violations: string[], improvements: [string, number][], redundant: string[]}} */ -export function evaluateFileSizes(currentLocByFile, frozen, cap) { +export function evaluateFileSizes(currentLocByFile, frozen, cap, baseLocByFile) { const violations = []; const improvements = []; const redundant = []; for (const [file, loc] of Object.entries(currentLocByFile)) { if (file in frozen) { - if (loc > frozen[file]) + const threshold = baseLocByFile + ? Math.max(frozen[file], baseLocByFile[file] ?? frozen[file]) + : frozen[file]; + if (loc > threshold) violations.push(`${file}: ${loc} > congelado ${frozen[file]} (não pode crescer)`); else if (loc < frozen[file]) improvements.push([file, loc]); else if (loc <= cap) redundant.push(file); } else if (loc > cap) { - violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`); + if (!baseLocByFile) { + violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`); + } else { + // Modo PR: so viola se cresceu alem do que ja estava na base + const baseLoc = baseLocByFile[file] ?? 0; + const prThreshold = Math.max(cap, baseLoc); + if (loc > prThreshold) + violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`); + } } } return { violations, improvements, redundant }; @@ -108,6 +129,30 @@ function collectTestLoc() { return out; } +/** + * Computa LOC por arquivo a partir de um ref git (branch, SHA, tag). + * Usado pelo modo --base-ref para obter a contagem na base do PR (#8522). + * @param {string} ref — git ref (e.g. SHA da branch base) + * @param {string[]} files — lista de paths relativos ao ROOT + * @returns {Object} mapa file → line count + */ +function getBaseLoc(ref, files) { + const out = {}; + for (const file of files) { + try { + const buf = execFileSync("git", ["show", `${ref}:${file}`], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 5000, + }); + out[file] = buf.split("\n").length; + } catch { + // Arquivo nao existe na base (novo no PR) — tratado como 0 + } + } + return out; +} + function main() { if (!fs.existsSync(BASELINE_PATH)) { console.error(`[file-size] FAIL — ${path.basename(BASELINE_PATH)} ausente.`); @@ -117,7 +162,17 @@ function main() { const cap = baseline.cap; const frozen = baseline.frozen || {}; const current = collectLoc(); - const { violations, improvements, redundant } = evaluateFileSizes(current, frozen, cap); + + // Modo PR: computa LOC na branch base para comparacao relativa (#8522) + const baseLoc = BASE_REF ? getBaseLoc(BASE_REF, Object.keys(current)) : undefined; + if (BASE_REF) { + const baseKeys = Object.keys(baseLoc).length; + console.log( + `[file-size] modo PR (--base-ref ${BASE_REF.slice(0, 12)}): ${baseKeys} arquivos da base computados` + ); + } + + const { violations, improvements, redundant } = evaluateFileSizes(current, frozen, cap, baseLoc); // Test-file gate (Layer 1 anti-reinflation): same shrink-only + new-≤cap semantics, // reusing evaluateFileSizes against the testFrozen baseline + testCap. @@ -129,7 +184,7 @@ function main() { improvements: testImprovements, redundant: testRedundant, } = typeof testCap === "number" - ? evaluateFileSizes(currentTests, testFrozen, testCap) + ? evaluateFileSizes(currentTests, testFrozen, testCap, BASE_REF ? baseLoc : undefined) : { violations: [], improvements: [], redundant: [] }; if (UPDATE) { diff --git a/tests/unit/repro-8522.test.ts b/tests/unit/repro-8522.test.ts index c6d4dd6c30..c122bf3807 100644 --- a/tests/unit/repro-8522.test.ts +++ b/tests/unit/repro-8522.test.ts @@ -25,21 +25,22 @@ test("8522: innocent PR (base already over frozen cap) must NOT be a violation", const frozen = { "src/foo.ts": 100 }; const cap = 100; - const { violations } = evaluateFileSizes(currentLocByFile, frozen, cap); + // With baseLocByFile, the gate compares against max(frozen, base) = max(100, 110) = 110, + // so 110 > 110 is false — innocent PR passes. + const { violations } = evaluateFileSizes(currentLocByFile, frozen, cap, baseLocByFile); - // The gate has no base-ref input; it compares head LOC (110) to frozen (100) - // and flags a violation. But the PR introduced ZERO growth — it is a false - // positive on inherited drift. assert.deepEqual(violations, [], "innocent PR flagged for inherited drift"); }); test("8522: PR that DOES grow a frozen file above frozen cap is a violation", () => { // Sanity: the gate must still catch a PR that grows the file above its cap. + // Base is at the frozen cap (100), but PR grew it to 112. const baseLocByFile = { "src/foo.ts": 100 }; const currentLocByFile = { "src/foo.ts": 112 }; // PR grew it +12 const frozen = { "src/foo.ts": 100 }; const cap = 100; - const { violations } = evaluateFileSizes(currentLocByFile, frozen, cap); + // With baseLocByFile: threshold = max(100, 100) = 100, 112 > 100 → violation + const { violations } = evaluateFileSizes(currentLocByFile, frozen, cap, baseLocByFile); assert.equal(violations.length, 1, "own-growth PR must be a violation"); });