Skip to content

lint never ran on PRs, and turbo skipped 7 packages — three error ratchets were inert #2923

Description

@os-zhuang

Rewritten. This issue originally recommended against wiring lint.yml, arguing it would be a no-op gate. That was wrong — the reasoning missed three error-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/purity x8 — Date.now() called during render in ApprovalsInboxPage.tsx
  • react-hooks/static-components x4 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 .js file, 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

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

  1. os-zhuang commented on Jul 28, 2026

    @os-zhuang
    ContributorAuthor

    Correcting my own recommendation above: wiring lint.yml is worth doing now, and I had this backwards.

    I said it would be a no-op gate. It is not. eslint.config.js already carries three error-level rules whose stated purpose is to fail CI — and because lint.yml only runs on workflow_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.ts type-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 lint reports 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.yml on 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-vars is '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-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%
    react-hooks/use-memo 33
    react-hooks/preserve-manual-memoization 32
    react-hooks/immutability 12
    react-hooks/incompatible-library 2

    One rule is 81% of the noise. That reframes option 2 above: a --max-warnings baseline of 7,724 is really "6,262 anys plus 1,462 of everything else," and no-explicit-any is a style/discipline call on a codebase whose adapters legitimately use any. 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-vars are the 'X' is defined but never used / is assigned a value but never used form — i.e. exactly the #2911 class.

    Revised plan

    1. Wire lint.yml to pull_request / push as-is. Free, no cleanup, activates three dead ratchets. Do this first and separately — it is a pure win and easy to review.
    2. Then, optionally, promote @typescript-eslint/no-unused-vars to error after 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.
    3. Leave no-explicit-any and the React Compiler rules at warn. 6,262 + ~440, both deliberate. Do not put them behind --max-warnings without a separate decision.

    Do not add --max-warnings=7724 as a blanket cap — it reads as a health signal while being dominated by one stylistic rule.

  2. 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions