Skip to content

ci(lint): ratchet as-any / @ts-ignore / eslint-disable counts#2658

Closed
jordan-simonovski wants to merge 2 commits into
mainfrom
jordansimonovski/escape-hatch-ratchet
Closed

ci(lint): ratchet as-any / @ts-ignore / eslint-disable counts#2658
jordan-simonovski wants to merge 2 commits into
mainfrom
jordansimonovski/escape-hatch-ratchet

Conversation

@jordan-simonovski

Copy link
Copy Markdown
Contributor

Type-safety escape hatches (as any, @ts-ignore, eslint-disable) accumulate silently — nothing stops the count creeping up over time. This adds a ratchet that fails CI when a package's count of any of those rises above a committed baseline, so they can only trend down.

What changed

  • scripts/ci/ratchet.mjs counts the three patterns per package and compares against scripts/ci/ratchet-baseline.json.
  • Root scripts yarn ratchet (check) and yarn ratchet:update (re-baseline).
  • Wired into make ci-lint so the existing lint CI job enforces it.

Key decisions

  • The ratchet is two-way strict: it also fails when a count drops below baseline, with a message to run yarn ratchet:update. This forces improvements to be locked into the baseline rather than silently drifting, so the baseline always reflects reality.

Impact

  • A PR that adds a new as any/@ts-ignore/eslint-disable fails the lint job until it's removed or the baseline is deliberately updated.
  • No production code — no changeset.
  • Merge-order note (important): PR "cache and scope the lint job with nx affected" changes the lint CI job to call nx directly instead of make ci-lint. Whichever of these two PRs merges second must add an explicit - run: node scripts/ci/ratchet.mjs step to the lint job — otherwise the ratchet stops running in CI (it's a whole-repo check, so it must run unconditionally, not gated by nx affected).
Implementation detail
  • grep exit code 1 (no matches) is treated as count 0, not an error.
  • Verified the gate fails on a regression: adding const x = 1 as any; to a packages/api source file makes yarn ratchet exit 1 naming api/as-any; reverting restores "ratchet ok".

Adds scripts/ci/ratchet.mjs, which counts tracked escape hatches (as any,
@ts-ignore, eslint-disable) per package and compares them against a committed
baseline. Counts above baseline fail CI (a new occurrence was added); counts
below baseline also fail, prompting the contributor to run yarn ratchet:update
so the improvement is locked in. This two-way-strict behaviour keeps the
baseline honest and stops the escape-hatch count from silently creeping up.

The check is wired into the ci-lint Makefile target so the existing lint CI job
enforces it, and yarn ratchet / yarn ratchet:update scripts are exposed for
local use.
@changeset-bot

changeset-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7b7ae02

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hyperdx-oss Ignored Ignored Preview Jul 16, 2026 5:39am
hyperdx-storybook Ignored Ignored Preview Jul 16, 2026 5:39am

Request Review

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label Jul 16, 2026
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 4
  • Production lines changed: 110
  • Branch: jordansimonovski/escape-hatch-ratchet
  • Author: jordan-simonovski

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a CI ratchet that counts type-safety escape hatches (as any, @ts-ignore, eslint-disable) per package and fails the lint job when any count rises above a committed baseline, preventing silent accumulation of these patterns over time.

  • scripts/ci/ratchet.mjs: New script that greps each package's src/ directory, compares against ratchet-baseline.json, exits non-zero on regressions, and supports --update to re-baseline improvements. Below-baseline counts produce a non-fatal warning (not a failure), despite the PR description saying "two-way strict" — the code comment explains the correct rationale.
  • scripts/ci/ratchet-baseline.json: Committed initial snapshot establishing current debt levels across app, api, common-utils, cli, and hdx-eval.
  • Makefile / package.json: Wire the ratchet into make ci-lint and expose yarn ratchet / yarn ratchet:update for local use. The PR description contains a merge-order note about a parallel PR that replaces make ci-lint with nx directly — that follow-up step should be tracked.

Confidence Score: 5/5

No production code is touched; the ratchet is a standalone CI script that only reads source files and exits non-zero on regressions.

All changes are confined to a new CI utility script, its baseline snapshot, and two wiring lines. The script correctly handles missing directories, zero-match grep exits, and the below-baseline non-fatal path.

The as any grep pattern in ratchet.mjs line 22 is the only line worth a second look; everything else is straightforward.

Important Files Changed

Filename Overview
scripts/ci/ratchet.mjs New CI ratchet script; handles missing directories and zero-match exits correctly. The as any grep pattern lacks a word boundary so it can match identifiers like anyType, though the risk is low in practice since the same inflated baseline is committed. The below-baseline path is correctly non-fatal (warn only), which contradicts the PR description's "two-way strict" claim but is the right design choice.
scripts/ci/ratchet-baseline.json Initial baseline snapshot of escape-hatch counts per package; cli and hdx-eval are clean (zero as-any/ts-ignore), while app/api/common-utils carry existing debt that is now tracked.
Makefile Appends node scripts/ci/ratchet.mjs to the ci-lint target so the ratchet runs unconditionally in CI after nx linting.
package.json Adds ratchet and ratchet:update root-level yarn scripts for local developer use.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[yarn ratchet / make ci-lint] --> B{--update flag?}
    B -- Yes --> C[Count patterns in each package src/]
    C --> D[Write ratchet-baseline.json]
    D --> E[Exit 0]
    B -- No --> F[Count patterns in each package src/]
    F --> G[Read ratchet-baseline.json]
    G --> H{For each pkg + pattern}
    H --> I{now > baseline?}
    I -- Yes --> J[console.error + set failed=true]
    I -- No --> K{now < baseline?}
    K -- Yes --> L[console.warn non-fatal]
    K -- No --> M[OK, continue]
    J --> H
    L --> H
    M --> H
    H -- Done --> N{failed?}
    N -- Yes --> O[Exit 1 CI fails]
    N -- No --> P[Exit 0 ratchet ok]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[yarn ratchet / make ci-lint] --> B{--update flag?}
    B -- Yes --> C[Count patterns in each package src/]
    C --> D[Write ratchet-baseline.json]
    D --> E[Exit 0]
    B -- No --> F[Count patterns in each package src/]
    F --> G[Read ratchet-baseline.json]
    G --> H{For each pkg + pattern}
    H --> I{now > baseline?}
    I -- Yes --> J[console.error + set failed=true]
    I -- No --> K{now < baseline?}
    K -- Yes --> L[console.warn non-fatal]
    K -- No --> M[OK, continue]
    J --> H
    L --> H
    M --> H
    H -- Done --> N{failed?}
    N -- Yes --> O[Exit 1 CI fails]
    N -- No --> P[Exit 0 ratchet ok]
Loading

Reviews (2): Last reviewed commit: "ci(lint): soften below-baseline ratchet ..." | Re-trigger Greptile

Comment thread scripts/ci/ratchet.mjs
Comment thread scripts/ci/ratchet.mjs
'../..',
);
const BASELINE = path.join(ROOT, 'scripts/ci/ratchet-baseline.json');
const PACKAGES = ['app', 'api', 'common-utils', 'cli', 'hdx-eval'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded package list requires manual maintenance

PACKAGES lists every tracked package by name. When a new package is added to the monorepo it will be silently skipped until someone remembers to update both this array and ratchet-baseline.json. Auto-discovering packages from the packages/ directory (e.g. reading its subdirectories or package.json workspaces) would make the ratchet self-updating, though it would also require that new packages always start with a baseline entry.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional for now: the explicit PACKAGES list is mirrored 1:1 by the keys in ratchet-baseline.json, so the two stay in lockstep and a new package is a deliberate, reviewed addition rather than silently folded in. Auto-discovery via packages/*/src is a reasonable follow-up but would need the baseline generation to track it too — out of scope for this PR.

Comment thread scripts/ci/ratchet.mjs
Comment on lines +20 to +22
'ts-ignore': '@ts-ignore',
'eslint-disable': 'eslint-disable',
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 eslint-disable pattern matches eslint-disable-next-line and eslint-disable-line

Because grep -Eo does prefix matching, the literal eslint-disable pattern matches all three directive forms (block-disable, eslint-disable-next-line, eslint-disable-line) and counts each separately. This is probably the intended behaviour — all three are escape hatches — but it means the reported eslint-disable count is not the number of block-level suppressions, which may surprise maintainers trying to interpret it. A brief comment clarifying this is intentional would help.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional — block eslint-disable, eslint-disable-next-line and eslint-disable-line are all escape hatches we want the ratchet to count, so prefix matching catching all three is the desired behaviour, not a bug.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Deep Review

This PR adds a CI ratchet (scripts/ci/ratchet.mjs) that counts as any / @ts-ignore / eslint-disable per package via grep and fails make ci-lint when a package's count rises above a committed baseline. No production code, self-consistent baseline at HEAD (ratchet ok). Findings concern the robustness of the grep-based counting, not correctness of this PR's happy path.

✅ No critical issues found.

🟡 P2 — recommended

  • scripts/ci/ratchet.mjs:22 — The as any pattern has no word boundaries, so grep counts comment/string substrings like "has anything" as escape hatches; verified three such false positives in packages/app/src inflate the app as-any count from 214 to 214+3=217, meaning an unrelated future PR that adds a comment containing "has any"/"was any" trips the ratchet with a misleading "you added an as any" failure.
    • Fix: Anchor the patterns (e.g. \bas any\b) and regenerate the baseline with yarn ratchet:update so counts track real casts, not prose.
    • correctness, maintainability
  • scripts/ci/ratchet.mjs:22 — Counting by shelling out to grep -rEo couples the committed baseline to one grep implementation's behavior, so a baseline generated in CI (GNU/Linux) can diverge from a developer's local yarn ratchet (BSD/macOS) on lines with multiple matches, producing confusing above/below-baseline results that don't reflect real code changes.
    • Fix: Count in-process by reading each .ts/.tsx file and applying a matchAll regex, removing the external-tool dependency and guaranteeing identical counts everywhere.
    • maintainability
🔵 P3 nitpicks (3)
  • scripts/ci/ratchet.mjs:20PACKAGES is hardcoded and hand-synced against the baseline JSON keys; a package added to PACKAGES but absent from the baseline silently gets max = 0 and fails on any occurrence, while one dropped from PACKAGES stops being tracked with no signal.
    • Fix: Derive the package set from a single source and assert the two lists agree.
  • scripts/ci/ratchet.mjs:22-E (extended regex) is passed but none of the three literal patterns use ERE metacharacters, implying regex semantics the strings don't need.
    • Fix: Drop -E, or intentionally adopt regex patterns (pairs naturally with adding word boundaries above).
  • scripts/ci/ratchet.mjs:1 — No automated test covers the count logic, the above/below/equal-baseline branches, --update regeneration, exit codes, or the grep exit-1-vs-2 handling; the behavior rests on manual verification only.
    • Fix: Add a lightweight node:test spec running the script against a temp fixture directory; low priority given CI-only tooling.
    • testing, correctness

Reviewers (3): correctness, maintainability, testing.

Testing gaps: No test asserts prose substrings ("has anything") are excluded from counts; no test covers the count() grep exit-code branches (1→0 vs 2→throw) or the missing-dir short-circuit; no test covers the baseline[pkg]?.[name] ?? 0 fallback for a package present in PACKAGES but missing from the baseline.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 241 passed • 3 skipped • 1602s

Status Count
✅ Passed 241
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@jordan-simonovski

Copy link
Copy Markdown
Contributor Author

Thanks — actioned in 7b7ae02:

  • P1 (below-baseline reds main under concurrent merges): fixed. Below-baseline is now non-fatal — it prints a ! warning nudging yarn ratchet:update but exits 0, so an improvement can never turn main (or other open PRs) red. Above-baseline still hard-fails, which is the gate that matters.
  • P2 (grep exit-2 crash on a missing/renamed dir): fixed with an existsSync(dir) guard returning 0.

Deliberately not changed (dev-only gate, keeping it simple):

  • Hardcoded PACKAGES — kept in lockstep with the explicit ratchet-baseline.json keys; auto-discovery is a possible follow-up.
  • @ts-expect-error/@ts-nocheck, .mts/.cts/.js globs, whitespace/comment precision, PATH-dependent grep — all accepted simplifications for a coarse escape-hatch counter; a genuinely precise count would want an ESLint rule, which is a larger follow-up. The ratchet's job is "don't let the count creep up", which prefix+substring counting does well enough.
  • --update can raise the baseline — intentional; the baseline diff is reviewed in the PR that runs it.

The Makefile-vs-nx point (ratchet stops running if the lint job goes nx-direct) is real and tracked against the sister lint PR (#2661) — whichever merges second adds an explicit ratchet step.

@jordan-simonovski

Copy link
Copy Markdown
Contributor Author

Folded into #2661 (combined lint + ratchet PR) via cherry-pick, with the ratchet now wired as an explicit lint-job step so it survives the nx-direct lint change. Closing to reduce merge complexity between the two.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant