mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
typecheck:core (the only blocking CI typecheck gate) runs against a curated 27-file allowlist that excludes all src/app/(dashboard) TSX, and next.config.mjs sets typescript.ignoreBuildErrors: true so next build never type-checks it either. Orphaned-identifier regressions there (the exact class fixed in #6625/#6909) were invisible to CI. Adds tsconfig.typecheck-dashboard.json (extends tsconfig.json, scoped to src/app/(dashboard)/**/*.ts(x)) plus check:dashboard-typecheck, a gate script that runs tsc against it and diffs per-file/per-TS-code error counts against a frozen baseline (config/quality/dashboard-typecheck-baseline.json, 262 pre-existing errors), following the same stale-enforcement allowlist pattern as check-known-symbols. Only NEW errors beyond the baselined count fail the gate; wired as a new blocking step in ci.yml (lint job) and quality.yml (fast-gates). Regression test (tests/unit/build/check-dashboard-typecheck.test.ts, 8 tests) reproduces the #6625/#6909 orphaned-identifier bug class against the pure parseTscOutput/diffAgainstBaseline helpers.
This commit is contained in:
committed by
GitHub
parent
fc6063679c
commit
c859931314
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -135,6 +135,13 @@ jobs:
|
|||||||
# check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the
|
# check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the
|
||||||
# husky pre-commit hook; the standalone copy here was redundant (ROI dedup).
|
# husky pre-commit hook; the standalone copy here was redundant (ROI dedup).
|
||||||
- run: npm run typecheck:core
|
- run: npm run typecheck:core
|
||||||
|
# #7033: typecheck:core's curated file allowlist does not cover
|
||||||
|
# src/app/(dashboard) TSX (and next.config.mjs sets ignoreBuildErrors:
|
||||||
|
# true, so `next build` never type-checks it either) — orphaned
|
||||||
|
# identifiers there (see #6625/#6909) were invisible to CI. This gate
|
||||||
|
# runs tsc scoped to the dashboard tree against a frozen baseline of
|
||||||
|
# pre-existing errors; only NEW errors fail it.
|
||||||
|
- run: npm run check:dashboard-typecheck
|
||||||
# typecheck:noimplicit:core dropped from this job (2026-07 optimize):
|
# typecheck:noimplicit:core dropped from this job (2026-07 optimize):
|
||||||
# it was advisory (continue-on-error) and largely subsumed by the blocking
|
# it was advisory (continue-on-error) and largely subsumed by the blocking
|
||||||
# check:type-coverage ratchet in quality-gate. Local: npm run typecheck:noimplicit:core.
|
# check:type-coverage ratchet in quality-gate. Local: npm run typecheck:noimplicit:core.
|
||||||
|
|||||||
4
.github/workflows/quality.yml
vendored
4
.github/workflows/quality.yml
vendored
@@ -143,6 +143,10 @@ jobs:
|
|||||||
- run: npm run check:complexity-ratchets
|
- run: npm run check:complexity-ratchets
|
||||||
- name: Typecheck (core)
|
- name: Typecheck (core)
|
||||||
run: npm run typecheck:core
|
run: npm run typecheck:core
|
||||||
|
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
|
||||||
|
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
|
||||||
|
- name: Typecheck (dashboard)
|
||||||
|
run: npm run check:dashboard-typecheck
|
||||||
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
|
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
|
||||||
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
|
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
|
||||||
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
|
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
|
||||||
|
|||||||
1
changelog.d/fixes/7033-dashboard-typecheck-gate.md
Normal file
1
changelog.d/fixes/7033-dashboard-typecheck-gate.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- fix(ci): add a dashboard-scoped typecheck gate covering `src/app/(dashboard)` TSX, previously invisible to `typecheck:core` and `next build` (#7033)
|
||||||
259
config/quality/dashboard-typecheck-baseline.json
Normal file
259
config/quality/dashboard-typecheck-baseline.json
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
{
|
||||||
|
"open-sse/services/payloadRules.ts": {
|
||||||
|
"TS2677": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/HomePageClient.tsx": {
|
||||||
|
"TS2339": 16
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx": {
|
||||||
|
"TS2503": 3
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/agent-skills/components/McpA2aLinksBar.tsx": {
|
||||||
|
"TS2503": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/agent-skills/components/SkillCard.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane.tsx": {
|
||||||
|
"TS2503": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cache/__tests__/CachePage.test.tsx": {
|
||||||
|
"TS2305": 3,
|
||||||
|
"TS1117": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cache/__tests__/CachePerformance.test.tsx": {
|
||||||
|
"TS2305": 1,
|
||||||
|
"TS2322": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cache/__tests__/CacheTrends.test.tsx": {
|
||||||
|
"TS2305": 1,
|
||||||
|
"TS2322": 6
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cache/__tests__/IdempotencyLayer.test.tsx": {
|
||||||
|
"TS2305": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cache/__tests__/MemoryCards.test.tsx": {
|
||||||
|
"TS2305": 1,
|
||||||
|
"TS2322": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cache/components/CachePerformance.tsx": {
|
||||||
|
"TS2339": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard.tsx": {
|
||||||
|
"TS2339": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": {
|
||||||
|
"TS2345": 3
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cli-code/components/CustomCliCard.tsx": {
|
||||||
|
"TS2345": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/cli-code/components/DroidToolCard.tsx": {
|
||||||
|
"TS2554": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/combos/page.tsx": {
|
||||||
|
"TS2339": 4,
|
||||||
|
"TS2345": 5,
|
||||||
|
"TS2698": 1,
|
||||||
|
"TS2322": 13
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/compression/studio/EncoderComparisonTable.tsx": {
|
||||||
|
"TS2322": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": {
|
||||||
|
"TS2304": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/costs/quota-share/QuotaSharePageClient.tsx": {
|
||||||
|
"TS2551": 7,
|
||||||
|
"TS2322": 2,
|
||||||
|
"TS2719": 2,
|
||||||
|
"TS2739": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/costs/quota-share/components/StackedAllocationBar.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/costs/quota-share/components/UsageLogCard.tsx": {
|
||||||
|
"TS2869": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/discovery/__tests__/DiscoveryPageClient.test.tsx": {
|
||||||
|
"TS2305": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": {
|
||||||
|
"TS2322": 18
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab.tsx": {
|
||||||
|
"TS2322": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/omni-skills/OmniSkillsPageClient.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/omni-skills/components/OmniExecutionsTab.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/omni-skills/components/OmniMarketplaceTab.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/omni-skills/components/OmniSandboxTab.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/omni-skills/components/OmniSkillCard.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/omni-skills/components/OmniSkillsList.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/omni-skills/components/SkillInspectorPane.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/playground/components/PresetPicker.tsx": {
|
||||||
|
"TS2352": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/playground/components/ToolsBuilder.tsx": {
|
||||||
|
"TS2339": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": {
|
||||||
|
"TS2322": 4
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/__tests__/phase1e.test.tsx": {
|
||||||
|
"TS2741": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleModelsSection.tsx": {
|
||||||
|
"TS2741": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": {
|
||||||
|
"TS2345": 3,
|
||||||
|
"TS2322": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": {
|
||||||
|
"TS2322": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx": {
|
||||||
|
"TS2739": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover.tsx": {
|
||||||
|
"TS2304": 5
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx": {
|
||||||
|
"TS2322": 3,
|
||||||
|
"TS2739": 1,
|
||||||
|
"TS2345": 3
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModelsSection.tsx": {
|
||||||
|
"TS2322": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderParamFilterSection.tsx": {
|
||||||
|
"TS2339": 6
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/__tests__/phase1d.test.tsx": {
|
||||||
|
"TS2739": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": {
|
||||||
|
"TS2322": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts": {
|
||||||
|
"TS2339": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts": {
|
||||||
|
"TS2339": 15
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts": {
|
||||||
|
"TS2339": 4,
|
||||||
|
"TS2345": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/providerPageUtils.ts": {
|
||||||
|
"TS2345": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/providers/services/components/CliproxyModelMappingEditor.tsx": {
|
||||||
|
"TS2339": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/quota/page.tsx": {
|
||||||
|
"TS2339": 4
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": {
|
||||||
|
"TS2304": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": {
|
||||||
|
"TS2339": 4
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/settings/components/RedisLauncherPanel.tsx": {
|
||||||
|
"TS2345": 11
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx": {
|
||||||
|
"TS2322": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/settings/components/proxy/FreePoolTab.tsx": {
|
||||||
|
"TS2304": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/tools/traffic-inspector/components/CustomHostsManager.tsx": {
|
||||||
|
"TS2339": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/ResponseBodyTab.tsx": {
|
||||||
|
"TS2339": 5
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx": {
|
||||||
|
"TS2345": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion.tsx": {
|
||||||
|
"TS4104": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/translator/components/advanced/StreamTransformerAccordion.tsx": {
|
||||||
|
"TS2345": 1
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": {
|
||||||
|
"TS2339": 2
|
||||||
|
},
|
||||||
|
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaEnvGroup.tsx": {
|
||||||
|
"TS2739": 1
|
||||||
|
},
|
||||||
|
"src/lib/combos/builderDraft.ts": {
|
||||||
|
"TS2741": 1
|
||||||
|
},
|
||||||
|
"src/lib/providers/codexFastTier.ts": {
|
||||||
|
"TS2367": 1
|
||||||
|
},
|
||||||
|
"src/lib/services/htmlRewriter.ts": {
|
||||||
|
"TS2322": 2,
|
||||||
|
"TS2345": 2
|
||||||
|
},
|
||||||
|
"src/mitm/inspector/sseMerger.ts": {
|
||||||
|
"TS2352": 1
|
||||||
|
},
|
||||||
|
"src/shared/components/Header.tsx": {
|
||||||
|
"TS2353": 1
|
||||||
|
},
|
||||||
|
"src/shared/components/MonacoEditor.tsx": {
|
||||||
|
"TS2307": 1
|
||||||
|
},
|
||||||
|
"src/shared/components/OAuthModal.tsx": {
|
||||||
|
"TS2769": 4,
|
||||||
|
"TS2345": 4
|
||||||
|
},
|
||||||
|
"src/shared/components/SkillsConceptCard.tsx": {
|
||||||
|
"TS2503": 1
|
||||||
|
},
|
||||||
|
"src/shared/components/analytics/charts.tsx": {
|
||||||
|
"TS2345": 1
|
||||||
|
},
|
||||||
|
"src/shared/components/analytics/rechartsDonuts.tsx": {
|
||||||
|
"TS2739": 2
|
||||||
|
},
|
||||||
|
"src/shared/hooks/useElectron.ts": {
|
||||||
|
"TS2339": 19
|
||||||
|
},
|
||||||
|
"src/shared/providers/webSessionCredentials.ts": {
|
||||||
|
"TS2353": 1,
|
||||||
|
"TS2322": 1
|
||||||
|
},
|
||||||
|
"src/shared/schemas/cliCatalog.ts": {
|
||||||
|
"TS2554": 2
|
||||||
|
},
|
||||||
|
"src/shared/services/opencodeConfig.ts": {
|
||||||
|
"TS2345": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ Runs on every PR to `main`. Blocks merge on failure.
|
|||||||
| `check:docs-sync` | CHANGELOG version, OpenAPI version, and `llm.txt` are in sync | Yes |
|
| `check:docs-sync` | CHANGELOG version, OpenAPI version, and `llm.txt` are in sync | Yes |
|
||||||
| `typecheck:core` | TypeScript compilation without errors (advisory warnings only) | Yes |
|
| `typecheck:core` | TypeScript compilation without errors (advisory warnings only) | Yes |
|
||||||
| `typecheck:noimplicit:core` | Strict `noImplicitAny` — forward-looking; many pre-existing call sites still need annotations | **Advisory** (`continue-on-error: true`) |
|
| `typecheck:noimplicit:core` | Strict `noImplicitAny` — forward-looking; many pre-existing call sites still need annotations | **Advisory** (`continue-on-error: true`) |
|
||||||
|
| `check:dashboard-typecheck` | `tsc` scoped to `src/app/(dashboard)/**` (#7033) — `typecheck:core`'s curated 27-file allowlist does not include any dashboard TSX, and `next build` never type-checks it either (`next.config.mjs` sets `ignoreBuildErrors: true`), so orphaned-identifier regressions there (#6625/#6909) were invisible to CI. Diffs against a frozen per-file/per-TS-code count baseline (`config/quality/dashboard-typecheck-baseline.json`, same stale-enforcement pattern as `check:known-symbols`) — only NEW errors beyond the baselined count fail the gate; ratchet down with `--update` when a pre-existing error is fixed. | Yes |
|
||||||
|
|
||||||
### Job: `quality-gate`
|
### Job: `quality-gate`
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,7 @@
|
|||||||
"audit:electron": "npm --prefix electron audit --audit-level=critical && (npm --prefix electron audit --audit-level=high || echo '::warning::electron high-severity advisories present (non-blocking)')",
|
"audit:electron": "npm --prefix electron audit --audit-level=critical && (npm --prefix electron audit --audit-level=high || echo '::warning::electron high-severity advisories present (non-blocking)')",
|
||||||
"typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json",
|
"typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json",
|
||||||
"typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json",
|
"typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json",
|
||||||
|
"check:dashboard-typecheck": "node scripts/check/check-dashboard-typecheck.mjs",
|
||||||
"backfill-aggregation": "node --import tsx src/scripts/backfillAggregation.ts",
|
"backfill-aggregation": "node --import tsx src/scripts/backfillAggregation.ts",
|
||||||
"env:sync": "node scripts/dev/sync-env.mjs",
|
"env:sync": "node scripts/dev/sync-env.mjs",
|
||||||
"test:integration": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 tests/integration/*.test.ts \"tests/integration/combo-matrix/*.test.ts\"",
|
"test:integration": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 tests/integration/*.test.ts \"tests/integration/combo-matrix/*.test.ts\"",
|
||||||
|
|||||||
177
scripts/check/check-dashboard-typecheck.mjs
Normal file
177
scripts/check/check-dashboard-typecheck.mjs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// scripts/check/check-dashboard-typecheck.mjs
|
||||||
|
// Dashboard-scoped typecheck gate (#7033).
|
||||||
|
//
|
||||||
|
// `typecheck:core` (the only blocking CI typecheck gate) runs against a curated
|
||||||
|
// 27-file `"files"` allowlist in tsconfig.typecheck-core.json — none of it lives
|
||||||
|
// under `src/app/(dashboard)`, and `next.config.mjs` sets
|
||||||
|
// `typescript.ignoreBuildErrors: true`, so `next build` never type-checks either.
|
||||||
|
// Net effect: orphaned-identifier regressions in dashboard TSX (deleted `useState`
|
||||||
|
// decls with live usages left behind) are invisible to both CI type-check paths
|
||||||
|
// and only surface at runtime — exactly what happened in #6625/#6909.
|
||||||
|
//
|
||||||
|
// This gate runs `tsc` scoped to `src/app/(dashboard)/**/*.{ts,tsx}` via
|
||||||
|
// tsconfig.typecheck-dashboard.json and diffs the result against a frozen
|
||||||
|
// per-file/per-TS-code count baseline (config/quality/dashboard-typecheck-baseline.json),
|
||||||
|
// following this repo's stale-enforcement allowlist convention (see
|
||||||
|
// scripts/check/check-known-symbols.ts). A live count that EXCEEDS the baselined
|
||||||
|
// count for a given (file, TS code) pair is a regression and fails the gate; a
|
||||||
|
// live count that is lower is an improvement and does not fail (use --update to
|
||||||
|
// ratchet the baseline down).
|
||||||
|
//
|
||||||
|
// Run:
|
||||||
|
// node scripts/check/check-dashboard-typecheck.mjs
|
||||||
|
// node scripts/check/check-dashboard-typecheck.mjs --update # re-freeze baseline
|
||||||
|
|
||||||
|
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 TSCONFIG = path.join(ROOT, "tsconfig.typecheck-dashboard.json");
|
||||||
|
const BASELINE_PATH = path.join(ROOT, "config/quality/dashboard-typecheck-baseline.json");
|
||||||
|
const UPDATE = process.argv.includes("--update");
|
||||||
|
|
||||||
|
// Matches tsc --pretty false output lines, e.g.:
|
||||||
|
// src/app/(dashboard)/dashboard/foo.tsx(12,7): error TS2304: Cannot find name 'bar'.
|
||||||
|
const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+):/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses raw `tsc --pretty false` stdout into a nested count map:
|
||||||
|
* { "<relative file path>": { "<TS code>": <count> } }
|
||||||
|
*
|
||||||
|
* Pure/exported for unit testing against synthetic tsc output — no child
|
||||||
|
* process involved here.
|
||||||
|
*/
|
||||||
|
export function parseTscOutput(raw) {
|
||||||
|
const counts = {};
|
||||||
|
const lines = String(raw).split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
const match = TSC_ERROR_LINE.exec(line);
|
||||||
|
if (!match) continue;
|
||||||
|
const [, file, , , code] = match;
|
||||||
|
if (!counts[file]) counts[file] = {};
|
||||||
|
counts[file][code] = (counts[file][code] || 0) + 1;
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares live (file, TS code) error counts against a frozen baseline.
|
||||||
|
* Returns `{ regressions, improvements }`:
|
||||||
|
* - regressions: entries where live count > baselined count (or the pair is
|
||||||
|
* entirely new/unbaselined) — these fail the gate.
|
||||||
|
* - improvements: entries where live count < baselined count — informational,
|
||||||
|
* do not fail (use --update to ratchet the baseline down).
|
||||||
|
*
|
||||||
|
* Exported for unit testing.
|
||||||
|
*/
|
||||||
|
export function diffAgainstBaseline(live, baseline) {
|
||||||
|
const regressions = [];
|
||||||
|
const improvements = [];
|
||||||
|
|
||||||
|
for (const [file, codes] of Object.entries(live)) {
|
||||||
|
for (const [code, liveCount] of Object.entries(codes)) {
|
||||||
|
const baselineCount = (baseline[file] && baseline[file][code]) || 0;
|
||||||
|
if (liveCount > baselineCount) {
|
||||||
|
regressions.push({ file, code, liveCount, baselineCount });
|
||||||
|
} else if (liveCount < baselineCount) {
|
||||||
|
improvements.push({ file, code, liveCount, baselineCount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [file, codes] of Object.entries(baseline)) {
|
||||||
|
for (const [code, baselineCount] of Object.entries(codes)) {
|
||||||
|
const liveCount = (live[file] && live[file][code]) || 0;
|
||||||
|
if (liveCount === 0 && baselineCount > 0) {
|
||||||
|
improvements.push({ file, code, liveCount: 0, baselineCount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { regressions, improvements };
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTsc() {
|
||||||
|
try {
|
||||||
|
const stdout = execFileSync(
|
||||||
|
process.platform === "win32" ? "npx.cmd" : "npx",
|
||||||
|
["tsc", "--pretty", "false", "--noEmit", "-p", TSCONFIG],
|
||||||
|
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024, cwd: ROOT }
|
||||||
|
);
|
||||||
|
return stdout;
|
||||||
|
} catch (err) {
|
||||||
|
// tsc exits non-zero when there are type errors — stdout still has the report.
|
||||||
|
if (err.stdout) return String(err.stdout);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBaseline() {
|
||||||
|
if (!fs.existsSync(BASELINE_PATH)) return {};
|
||||||
|
return JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeBaseline(counts) {
|
||||||
|
fs.writeFileSync(BASELINE_PATH, JSON.stringify(counts, null, 2) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
if (!fs.existsSync(TSCONFIG)) {
|
||||||
|
process.stderr.write(`[dashboard-typecheck] FAIL — tsconfig not found at ${TSCONFIG}\n`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[dashboard-typecheck] Running tsc scoped to src/app/(dashboard)/**…");
|
||||||
|
const stdout = runTsc();
|
||||||
|
const live = parseTscOutput(stdout);
|
||||||
|
const baseline = loadBaseline();
|
||||||
|
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||||
|
|
||||||
|
const liveErrorCount = Object.values(live).reduce(
|
||||||
|
(sum, codes) => sum + Object.values(codes).reduce((s, c) => s + c, 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
console.log(`dashboardTypecheckErrors=${liveErrorCount}`);
|
||||||
|
|
||||||
|
if (UPDATE) {
|
||||||
|
writeBaseline(live);
|
||||||
|
console.log(`[dashboard-typecheck] baseline rewritten (${liveErrorCount} errors frozen).`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (improvements.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`[dashboard-typecheck] ${improvements.length} baselined error(s) no longer present ` +
|
||||||
|
`— run 'node scripts/check/check-dashboard-typecheck.mjs --update' to ratchet the baseline down:\n` +
|
||||||
|
improvements
|
||||||
|
.map((i) => ` - ${i.file} ${i.code} (baseline ${i.baselineCount} -> live ${i.liveCount})`)
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regressions.length > 0) {
|
||||||
|
process.stderr.write(
|
||||||
|
`[dashboard-typecheck] FAIL — ${regressions.length} new/regressed TypeScript error(s) ` +
|
||||||
|
`under src/app/(dashboard)/ not covered by the frozen baseline:\n` +
|
||||||
|
regressions
|
||||||
|
.map((r) => ` ✗ ${r.file} ${r.code} (baseline ${r.baselineCount}, live ${r.liveCount})`)
|
||||||
|
.join("\n") +
|
||||||
|
`\n\nIf this is a genuine new dashboard TSX bug (e.g. an orphaned identifier), fix it.\n` +
|
||||||
|
`If it's pre-existing type looseness you're intentionally not fixing in this PR,\n` +
|
||||||
|
`do NOT widen the baseline for new regressions — that defeats the gate.\n`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[dashboard-typecheck] OK — ${liveErrorCount} pre-existing error(s), all within frozen baseline.`
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
111
tests/unit/build/check-dashboard-typecheck.test.ts
Normal file
111
tests/unit/build/check-dashboard-typecheck.test.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// tests/unit/build/check-dashboard-typecheck.test.ts
|
||||||
|
// Unit tests for the pure parsing/diff helpers in check-dashboard-typecheck.mjs.
|
||||||
|
// No child process is spawned — synthetic tsc-style output only, so the suite is
|
||||||
|
// fast and hermetic. Proves the gate actually DETECTS the #6625/#6909 bug class
|
||||||
|
// (an orphaned identifier — used but not declared — in a dashboard TSX file),
|
||||||
|
// not just that the script runs.
|
||||||
|
|
||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import {
|
||||||
|
parseTscOutput,
|
||||||
|
diffAgainstBaseline,
|
||||||
|
} from "../../../scripts/check/check-dashboard-typecheck.mjs";
|
||||||
|
|
||||||
|
test("parseTscOutput: parses a TS2304 orphaned-identifier error (the #6625/#6909 bug class)", () => {
|
||||||
|
const raw =
|
||||||
|
`src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx(564,7): error TS2304: Cannot find name 'setPoolLoaded'.\n` +
|
||||||
|
`src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx(1204,12): error TS2304: Cannot find name 'poolLoaded'.\n`;
|
||||||
|
|
||||||
|
const counts = parseTscOutput(raw);
|
||||||
|
|
||||||
|
assert.deepEqual(counts, {
|
||||||
|
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": {
|
||||||
|
TS2304: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseTscOutput: ignores non-error lines (summary/info output)", () => {
|
||||||
|
const raw =
|
||||||
|
`Some info line that is not an error\n` +
|
||||||
|
`src/app/(dashboard)/dashboard/foo.tsx(1,1): error TS2339: Property 'bar' does not exist.\n` +
|
||||||
|
`Found 1 error in 1 file.\n`;
|
||||||
|
|
||||||
|
const counts = parseTscOutput(raw);
|
||||||
|
|
||||||
|
assert.deepEqual(counts, {
|
||||||
|
"src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 1 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseTscOutput: returns empty map for clean output", () => {
|
||||||
|
assert.deepEqual(parseTscOutput(""), {});
|
||||||
|
assert.deepEqual(parseTscOutput("Found 0 errors.\n"), {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("diffAgainstBaseline: flags a brand-new orphaned-identifier error as a regression", () => {
|
||||||
|
const baseline = {};
|
||||||
|
const live = {
|
||||||
|
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": {
|
||||||
|
TS2304: 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||||
|
|
||||||
|
assert.equal(regressions.length, 1);
|
||||||
|
assert.equal(
|
||||||
|
regressions[0].file,
|
||||||
|
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx"
|
||||||
|
);
|
||||||
|
assert.equal(regressions[0].code, "TS2304");
|
||||||
|
assert.equal(regressions[0].liveCount, 5);
|
||||||
|
assert.equal(regressions[0].baselineCount, 0);
|
||||||
|
assert.equal(improvements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("diffAgainstBaseline: does NOT flag a frozen pre-existing error within its baselined count", () => {
|
||||||
|
const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } };
|
||||||
|
const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } };
|
||||||
|
|
||||||
|
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||||
|
|
||||||
|
assert.equal(regressions.length, 0);
|
||||||
|
assert.equal(improvements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("diffAgainstBaseline: flags a count INCREASE beyond the frozen baseline as a regression", () => {
|
||||||
|
const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 2 } };
|
||||||
|
const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } };
|
||||||
|
|
||||||
|
const { regressions } = diffAgainstBaseline(live, baseline);
|
||||||
|
|
||||||
|
assert.equal(regressions.length, 1);
|
||||||
|
assert.equal(regressions[0].baselineCount, 2);
|
||||||
|
assert.equal(regressions[0].liveCount, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("diffAgainstBaseline: reports (does not fail on) a count DECREASE as an improvement", () => {
|
||||||
|
const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } };
|
||||||
|
const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 1 } };
|
||||||
|
|
||||||
|
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||||
|
|
||||||
|
assert.equal(regressions.length, 0);
|
||||||
|
assert.equal(improvements.length, 1);
|
||||||
|
assert.equal(improvements[0].baselineCount, 3);
|
||||||
|
assert.equal(improvements[0].liveCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("diffAgainstBaseline: a baselined error that fully disappears is reported as an improvement, not a failure", () => {
|
||||||
|
const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 2 } };
|
||||||
|
const live = {};
|
||||||
|
|
||||||
|
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||||
|
|
||||||
|
assert.equal(regressions.length, 0);
|
||||||
|
assert.equal(improvements.length, 1);
|
||||||
|
assert.equal(improvements[0].liveCount, 0);
|
||||||
|
assert.equal(improvements[0].baselineCount, 2);
|
||||||
|
});
|
||||||
8
tsconfig.typecheck-dashboard.json
Normal file
8
tsconfig.typecheck-dashboard.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": false
|
||||||
|
},
|
||||||
|
"include": ["src/app/(dashboard)/**/*.ts", "src/app/(dashboard)/**/*.tsx"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user