diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebd1da8ba6..77e182729e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: - run: npm run check:provider-consistency - run: npm run check:fetch-targets - run: npm run check:deps + - run: npm run check:file-size - run: npm run check:docs-sync - run: npm run typecheck:core # typecheck:noimplicit:core is a forward-looking gate (noImplicitAny). diff --git a/file-size-baseline.json b/file-size-baseline.json new file mode 100644 index 0000000000..7059cd822d --- /dev/null +++ b/file-size-baseline.json @@ -0,0 +1,97 @@ +{ + "_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", + "cap": 800, + "frozen": { + "open-sse/config/providerRegistry.ts": 4677, + "open-sse/executors/antigravity.ts": 1533, + "open-sse/executors/base.ts": 1175, + "open-sse/executors/chatgpt-web.ts": 2870, + "open-sse/executors/claude-web.ts": 1057, + "open-sse/executors/codex.ts": 1439, + "open-sse/executors/cursor.ts": 1391, + "open-sse/executors/deepseek-web.ts": 1116, + "open-sse/executors/duckduckgo-web.ts": 917, + "open-sse/executors/grok-web.ts": 1871, + "open-sse/executors/muse-spark-web.ts": 1284, + "open-sse/executors/perplexity-web.ts": 867, + "open-sse/handlers/audioSpeech.ts": 952, + "open-sse/handlers/chatCore.ts": 5978, + "open-sse/handlers/imageGeneration.ts": 3777, + "open-sse/handlers/responseSanitizer.ts": 1080, + "open-sse/handlers/search.ts": 1441, + "open-sse/handlers/videoGeneration.ts": 1026, + "open-sse/mcp-server/schemas/tools.ts": 1437, + "open-sse/mcp-server/server.ts": 1457, + "open-sse/mcp-server/tools/advancedTools.ts": 1118, + "open-sse/services/accountFallback.ts": 1631, + "open-sse/services/batchProcessor.ts": 828, + "open-sse/services/browserBackedChat.ts": 850, + "open-sse/services/claudeCodeCompatible.ts": 1202, + "open-sse/services/combo.ts": 4457, + "open-sse/services/rateLimitManager.ts": 1017, + "open-sse/services/tokenRefresh.ts": 1896, + "open-sse/services/usage.ts": 3042, + "open-sse/translator/request/openai-to-gemini.ts": 822, + "open-sse/translator/response/openai-responses.ts": 873, + "open-sse/utils/cursorAgentProtobuf.ts": 1499, + "open-sse/utils/stream.ts": 2593, + "src/app/(dashboard)/dashboard/HomePageClient.tsx": 1417, + "src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1020, + "src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 2680, + "src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1105, + "src/app/(dashboard)/dashboard/cache/page.tsx": 841, + "src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": 894, + "src/app/(dashboard)/dashboard/cloud-agents/page.tsx": 913, + "src/app/(dashboard)/dashboard/combos/page.tsx": 4350, + "src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1481, + "src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1007, + "src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2570, + "src/app/(dashboard)/dashboard/health/page.tsx": 1091, + "src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847, + "src/app/(dashboard)/dashboard/providers/[id]/page.tsx": 12883, + "src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906, + "src/app/(dashboard)/dashboard/providers/page.tsx": 1925, + "src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1127, + "src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": 819, + "src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 932, + "src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 880, + "src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012, + "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1072, + "src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 851, + "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1580, + "src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924, + "src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016, + "src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148, + "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1015, + "src/app/api/oauth/[provider]/[action]/route.ts": 897, + "src/app/api/providers/[id]/models/route.ts": 2287, + "src/app/api/providers/[id]/test/route.ts": 842, + "src/app/api/usage/analytics/route.ts": 1355, + "src/app/api/v1/models/catalog.ts": 1403, + "src/lib/cloudflaredTunnel.ts": 934, + "src/lib/db/apiKeys.ts": 1490, + "src/lib/db/core.ts": 1813, + "src/lib/db/migrationRunner.ts": 1100, + "src/lib/db/models.ts": 1132, + "src/lib/db/providers.ts": 993, + "src/lib/db/proxies.ts": 1031, + "src/lib/db/settings.ts": 1101, + "src/lib/evals/evalRunner.ts": 961, + "src/lib/memory/retrieval.ts": 1171, + "src/lib/modelsDevSync.ts": 934, + "src/lib/providers/validation.ts": 4201, + "src/lib/tailscaleTunnel.ts": 1189, + "src/lib/usage/callLogs.ts": 971, + "src/shared/components/OAuthModal.tsx": 956, + "src/shared/components/RequestLoggerV2.tsx": 951, + "src/shared/components/analytics/charts.tsx": 1558, + "src/shared/constants/cliTools.ts": 875, + "src/shared/constants/pricing.ts": 1447, + "src/shared/constants/providers.ts": 3121, + "src/shared/constants/sidebarVisibility.ts": 990, + "src/shared/services/cliRuntime.ts": 1073, + "src/shared/validation/schemas.ts": 2490, + "src/sse/handlers/chat.ts": 1381, + "src/sse/services/auth.ts": 2198 + } +} diff --git a/package.json b/package.json index 1fab5d5809..14c6dbddeb 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "check:fetch-targets": "node scripts/check/check-fetch-targets.mjs", "check:openapi-routes": "node scripts/check/check-openapi-routes.mjs", "check:deps": "node scripts/check/check-deps.mjs", + "check:file-size": "node scripts/check/check-file-size.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", diff --git a/scripts/check/check-file-size.mjs b/scripts/check/check-file-size.mjs new file mode 100644 index 0000000000..220c4001b1 --- /dev/null +++ b/scripts/check/check-file-size.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +// scripts/check/check-file-size.mjs +// Catraca de tamanho de arquivo (mata o god-component). Modelado no +// check-t11-any-budget.mjs: um baseline congelado por arquivo (file-size-baseline.json). +// - arquivo congelado: só pode ENCOLHER (nunca crescer); +// - arquivo NOVO (fora do baseline): não pode passar do CAP. +// Assim o próximo arquivo de 12.760 linhas é impossível, e os 91 atuais só melhoram. +// --update ratcheta o baseline para baixo (encolhimentos + remove quem caiu < cap). +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const ROOT = 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 = path.resolve(getArg("--baseline", path.join(ROOT, "file-size-baseline.json"))); +const UPDATE = process.argv.includes("--update"); +const SCAN_DIRS = ["src", "open-sse"]; + +/** + * Avalia LOC atuais contra o baseline congelado. + * @returns {{violations: string[], improvements: [string, number][]}} + */ +export function evaluateFileSizes(currentLocByFile, frozen, cap) { + const violations = []; + const improvements = []; + for (const [file, loc] of Object.entries(currentLocByFile)) { + if (file in frozen) { + if (loc > frozen[file]) violations.push(`${file}: ${loc} > congelado ${frozen[file]} (não pode crescer)`); + else if (loc < frozen[file]) improvements.push([file, loc]); + } else if (loc > cap) { + violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`); + } + } + return { violations, improvements }; +} + +function countLines(file) { + return fs.readFileSync(file, "utf8").split("\n").length; +} + +function walk(dir, acc = []) { + if (!fs.existsSync(dir)) return acc; + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p, acc); + else if (/\.(ts|tsx)$/.test(e.name) && !/\.test\.tsx?$/.test(e.name) && !/\.d\.ts$/.test(e.name)) acc.push(p); + } + return acc; +} + +function collectLoc() { + const out = {}; + for (const d of SCAN_DIRS) + for (const f of walk(path.join(ROOT, d))) out[path.relative(ROOT, f).replace(/\\/g, "/")] = countLines(f); + return out; +} + +function main() { + if (!fs.existsSync(BASELINE_PATH)) { + console.error(`[file-size] FAIL — ${path.basename(BASELINE_PATH)} ausente.`); + process.exit(2); + } + const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8")); + const cap = baseline.cap; + const frozen = baseline.frozen || {}; + const current = collectLoc(); + const { violations, improvements } = evaluateFileSizes(current, frozen, cap); + + if (UPDATE && violations.length === 0 && improvements.length) { + for (const [file, loc] of improvements) { + if (loc <= cap) delete frozen[file]; // caiu para dentro do cap → sai do baseline + else frozen[file] = loc; // continua grande mas encolheu → trava no novo valor + } + baseline.frozen = Object.fromEntries(Object.entries(frozen).sort()); + fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2) + "\n"); + console.log(`[file-size] baseline ratcheado: ${improvements.length} arquivo(s) encolheram`); + } + + if (violations.length) { + console.error( + `[file-size] ${violations.length} violação(ões):\n` + + violations.map((v) => " ✗ " + v).join("\n") + + `\n → modularize/extraia (DRY) para encolher, ou (último caso) ajuste file-size-baseline.json com justificativa.` + ); + process.exit(1); + } + console.log( + `[file-size] OK — ${Object.keys(frozen).length} arquivos congelados, cap ${cap} para novos (${Object.keys(current).length} arquivos verificados)` + ); +} + +if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main(); diff --git a/tests/unit/check-file-size.test.ts b/tests/unit/check-file-size.test.ts new file mode 100644 index 0000000000..fb80ccccd8 --- /dev/null +++ b/tests/unit/check-file-size.test.ts @@ -0,0 +1,34 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { evaluateFileSizes } from "../../scripts/check/check-file-size.mjs"; + +const cap = 800; + +test("frozen file at exactly its baseline passes", () => { + const r = evaluateFileSizes({ "a.ts": 1000 }, { "a.ts": 1000 }, cap); + assert.deepEqual(r.violations, []); + assert.deepEqual(r.improvements, []); +}); + +test("frozen file that grew is a violation", () => { + const r = evaluateFileSizes({ "a.ts": 1001 }, { "a.ts": 1000 }, cap); + assert.equal(r.violations.length, 1); + assert.match(r.violations[0], /a\.ts/); +}); + +test("frozen file that shrank is an improvement, not a violation", () => { + const r = evaluateFileSizes({ "a.ts": 950 }, { "a.ts": 1000 }, cap); + assert.deepEqual(r.violations, []); + assert.deepEqual(r.improvements, [["a.ts", 950]]); +}); + +test("new file over the cap is a violation", () => { + const r = evaluateFileSizes({ "new.ts": 801 }, {}, cap); + assert.equal(r.violations.length, 1); + assert.match(r.violations[0], /new\.ts/); +}); + +test("new file at or under the cap passes", () => { + const r = evaluateFileSizes({ "new.ts": 800 }, {}, cap); + assert.deepEqual(r.violations, []); +});