Repository navigation
lint never ran on PRs, and turbo skipped 7 packages — three error ratchets were inert #2923
Description
Activity
Correcting my own recommendation above: wiring
lint.ymlis worth doing now, and I had this backwards.I said it would be a no-op gate. It is not.
eslint.config.jsalready carries threeerror-level rules whose stated purpose is to fail CI — and becauselint.ymlonly runs onworkflow_dispatch, all three have never gated anything.// ADR-0054 Phase 5 ratchet — ban synthetic-event triggers (C1). Error so a // new violation fails CI; the existing surfaces were converted to direct // idempotent commands first, so this lints clean today. 'object-ui/no-synthetic-event-trigger': 'error', // objectui#2879 ratchet — a hook called inside try/catch desyncs hook order // when the catch swallows a throw. ... Error so a tenth copy fails CI; all // known sites were converted first, so this lints clean today. 'object-ui/no-try-catch-around-hook': 'error', // ... Error so a new inline mirror fails CI; the covered fields were // converted first, so this lints clean today. 'object-ui/no-inline-spec-config': 'error', // scoped to packages/types/src/objectql.ts
Three separate deliberate efforts — ADR-0054 Phase 5, #2879, and the
objectql.tstype-discipline ratchet — each written on the assumption that CI enforces it. Each author even pre-cleaned the violations so the rule would "lint clean today." The enforcement they were built against does not exist. That is the same shape as #2911, one layer up: the rule is authored, the runtime never runs it.And the cost of switching it on is zero:
pnpm lintreports 0 errors across all 38 packages today, precisely because those sites were pre-cleaned. Wiring the workflow activates three intended ratchets immediately with no cleanup.So the corrected recommendation is wire
lint.ymlon PRs now — not for the reason I originally gave (it still would not have caught #2911; see below), but because three ratchets are currently inert.What wiring it does and does not buy
Does: activates the three
object-ui/*ratchets, so a new synthetic-event trigger, a new hook-in-try/catch, or a new inline spec mirror fails a PR — as their authors intended.Does not: catch #2911.
no-unused-varsis'warn', and warnings never fail without--max-warnings. That part of my original analysis stands.Actual composition of the 7,724 warnings
Now measured properly rather than sampled:
rule count share @typescript-eslint/no-explicit-any6,262 81% react-refresh/only-export-components486 6% @typescript-eslint/no-unused-vars322 4% react-hooks/set-state-in-effect252 3% react-hooks/exhaustive-deps177 2% react-hooks/refs111 1% react-hooks/use-memo33 react-hooks/preserve-manual-memoization32 react-hooks/immutability12 react-hooks/incompatible-library2 One rule is 81% of the noise. That reframes option 2 above: a
--max-warningsbaseline of 7,724 is really "6,262anys plus 1,462 of everything else," andno-explicit-anyis a style/discipline call on a codebase whose adapters legitimately useany. Ratcheting the aggregate mostly ratchets that one rule.Note also that five of the
react-hooks/*rules are explicitly downgraded with the comment "Downgrade new React Compiler rules to warnings (codebase predates these rules)" — a deliberate choice, not neglect. Those ~440 should stay warnings.All 322
no-unused-varsare the'X' is defined but never used/is assigned a value but never usedform — i.e. exactly the #2911 class.Revised plan
- Wire
lint.ymltopull_request/pushas-is. Free, no cleanup, activates three dead ratchets. Do this first and separately — it is a pure win and easy to review. - Then, optionally, promote
@typescript-eslint/no-unused-varstoerrorafter clearing its 322 violations.argsIgnorePattern: '^_'is already configured, so unused params just need_prefixes; unused imports/locals are mostly deletions. This is the piece that would have caught plugin-map 类型检查在 main 上是红的(maplibre-gl@6 移除默认导出);根因是 CI 根本不跑 type-check,22 个包的类型错误对 CI 完全不可见 #2911. - Leave
no-explicit-anyand the React Compiler rules atwarn. 6,262 + ~440, both deliberate. Do not put them behind--max-warningswithout a separate decision.
Do not add
--max-warnings=7724as a blanket cap — it reads as a health signal while being dominated by one stylistic rule.- Wire
- changed the title
[-]lint never runs on PRs — but wiring it as-is would be a no-op gate (7,724 warnings, 0 errors)[/-][+]lint never ran on PRs, and turbo skipped 7 packages — three `error` ratchets were inert[/+]on Jul 28, 2026 - added a commit that references this issue
on Jul 28, 2026 - added a commit that references this issue
on Jul 28, 2026
Two layers, the same shape as #2911
Layer 1 — the workflow never ran.
lint.yml's only trigger wasworkflow_dispatch, so ESLint had never gated a PR.Layer 2 —
turbo run lintsilently skips packages with nolintscript. 7 of 45 had none, includingapps/console, the largest surface in the repo. So even a wired workflow would have been blind to it. Identical to the type-check hole in #2911.Why layer 1 was not harmless
eslint.config.jssets threeobject-ui/*rules toerrorspecifically so a new violation fails CI:object-ui/no-synthetic-event-triggerobject-ui/no-try-catch-around-hookobject-ui/no-inline-spec-configpackages/types/src/objectql.tsThree separate deliberate efforts, each written on the assumption that CI enforces it, each having pre-cleaned its violations so the rule would lint clean. All three were worthless. The rule was authored; nothing ran it.
What it hid
apps/console(14 errors) andpackages/runner(3) carried 17 ESLint errors — error severity under the repo's own config, not warnings — that nobody had ever seen. 12 are React-correctness bugs:react-hooks/purityx8 —Date.now()called during render inApprovalsInboxPage.tsxreact-hooks/static-componentsx4 in console, x3 in runner — components created during render, which React remounts every render: state loss, focus loss. Three of console's four are Settings pages.Both rules are at
errorbecause the config downgrades five other React Compiler rules but deliberately not these two. Split out to #2927; they are pre-existing, not a regression.Also:
apps/sitewas not brokenOf the 25 errors first measured across the unlinted packages, 8 were artifacts:
apps/site/.source/— fumadocs-mdx generated output, already gitignoredeslint-disable-next-line @next/next/no-img-element— the Next plugin isn't in the flat config, so the directive itself errored as an unknown ruleapps/console/tailwind.config.js— TypeScript syntax in a.jsfile, so the base JS parser chokedSecond time
apps/sitelooked like debt purely because of generated artifacts (cf. #2924, where its 7 phantom type errors were a missing.next/types).Fixed in #2928
lint.ymlruns onpull_request+pushscripts/check-lint-coverage.mjs— every package lints or is a declared gap; the list only shrinksDEBT, tracked in console + runner: 17 ESLint errors that were never seen, incl. 12 React-correctness bugs #2927Verified by planting a hook inside try/catch in a linted package:
pnpm lintexit 1,Failed: @object-ui/i18n#lint— impossible before.Deliberately not done:
--max-warningsMeasured composition of the 7,724 warnings:
@typescript-eslint/no-explicit-anyreact-refresh/only-export-components@typescript-eslint/no-unused-varsreact-hooks/set-state-in-effectreact-hooks/exhaustive-depsreact-hooks/refsreact-hooks/*One stylistic rule is 81%, and five
react-hooks/*rules are downgraded on purpose ("codebase predates these rules"). A blanket cap of 7,724 reads as a health signal while really trackingno-explicit-any. Left open rather than answered badly.Promoting
no-unused-varstoerror(322 sites) was also dropped: #2911's actual failure mode was TS1192, now covered by the type-check gate from #2915. Unusedness itself has no demonstrated harm here, so the cleanup would be cosmetic.