Repository navigation
lint never ran on PRs, and turbo skipped 7 packages — three error ratchets were inert #2923
Description
Rewritten. This issue originally recommended against wiring
lint.yml, arguing it would be a no-op gate. That was wrong — the reasoning missed threeerror-level ratchets that were silently inert. The original analysis and the correction are preserved in the comments below; this body is the accurate statement of the defect. Fixed by #2928.
Two layers, the same shape as #2911
Layer 1 — the workflow never ran. lint.yml's only trigger was workflow_dispatch, so ESLint had never gated a PR.
Layer 2 — turbo run lint silently skips packages with no lint script. 7 of 45 had none, including apps/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.js sets three object-ui/* rules to error specifically so a new violation fails CI:
| rule | origin | comment in config |
|---|---|---|
object-ui/no-synthetic-event-trigger |
ADR-0054 Phase 5 | "Error so a new violation fails CI; the existing surfaces were converted … first, so this lints clean today." |
object-ui/no-try-catch-around-hook |
#2879 | "Error so a tenth copy fails CI; all known sites were converted first, so this lints clean today." |
object-ui/no-inline-spec-config |
packages/types/src/objectql.ts |
"Error so a new inline mirror fails CI; the covered fields were converted first, so this lints clean today." |
Three 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) and packages/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 error because 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/site was not broken
Of the 25 errors first measured across the unlinted packages, 8 were artifacts:
- 6 in
apps/site/.source/— fumadocs-mdx generated output, already gitignored - 1 stale
eslint-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 rule - 1
apps/console/tailwind.config.js— TypeScript syntax in a.jsfile, so the base JS parser choked
Second time apps/site looked 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 shrinks- lint scripts for the 5 already-clean packages; coverage 38 → 43 of 45
- console + runner declared in
DEBT, tracked in console + runner: 17 ESLint errors that were never seen, incl. 12 React-correctness bugs #2927 - the three config fixes above
Verified by planting a hook inside try/catch in a linted package: pnpm lint exit 1, Failed: @object-ui/i18n#lint — impossible before.
Deliberately not done: --max-warnings
Measured composition of the 7,724 warnings:
| rule | count | share |
|---|---|---|
@typescript-eslint/no-explicit-any |
6,262 | 81% |
react-refresh/only-export-components |
486 | 6% |
@typescript-eslint/no-unused-vars |
322 | 4% |
react-hooks/set-state-in-effect |
252 | 3% |
react-hooks/exhaustive-deps |
177 | 2% |
react-hooks/refs |
111 | 1% |
four more react-hooks/* |
79 |
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 tracking no-explicit-any. Left open rather than answered badly.
Promoting no-unused-vars to error (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.
Activity
os-zhuang commentedon Jul 28, 2026
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.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:
@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/use-memoreact-hooks/preserve-manual-memoizationreact-hooks/immutabilityreact-hooks/incompatible-libraryOne 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
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.@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.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.[-]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[/+]feat(lint): run ESLint on PRs, and cover every package (#2923) (#2928)
fix(console,runner): render the approvals inbox against one ticking c…