Skip to content
All field notes

3 min read

1,839 to 14 — Purging any from a 228k-Line Codebase in Four Days

Running an AI-agent refactoring campaign with a verifiable metric, stacked PRs, and the compiler as the ratchet

  • TypeScript
  • Angular
  • AI Agents
  • Refactoring
  • Code Quality

The CattleUSA web app is seven years old, talks to three different backend generations, and had — as of the second week of May — 1,839 annotated any types in its source. Four days later it had 14. This post is about how that campaign ran, because the interesting part isn't the type annotations. It's how you make large-scale mechanical refactoring safe when AI agents are doing most of the typing.

Why any was worth a campaign

any isn't a style problem, it's a boundary problem. Our app consumes a legacy PHP API, a Go API, and a modern Laravel API — three vintages of response shapes, and every any at those boundaries is a place where a renamed field or a string-that-should-be-a-number sails through the compiler and detonates at runtime, usually during a live sale. A year earlier we'd flipped the codebase to TypeScript strict mode (373 files changed in one commit, including an honest inline comment where exactOptionalPropertyTypes stayed off: //easier). Strict mode made new code sound. The 1,839 legacy anys were the remaining debt, and they were exactly the kind of work that's too tedious for a human to do well and too risky to do carelessly: perfect agent work, if you can verify it.

The harness matters more than the agent

Three rules made the campaign safe:

1. A single verifiable metric. The lint rule no-explicit-any gave us a number that could only be gamed by actually fixing the code. 1,839 at the start, checked after every batch. You cannot hand an agent "improve the types" — you can hand it "make this number go down without adding suppressions," and audit exactly that.

2. The compiler as the ratchet. Every substitution had to type-check against strict mode. Replacing any with the wrong specific type doesn't compile once real call sites are checked against it — which converts "did the agent guess right?" from a judgment call into a build result. The strict-mode migration a year earlier is what made this campaign possible; without it, wrong types would have slotted in silently.

3. Small, stacked, reviewable chunks. The work merged as ten stacked branches — weekend-changes-pt1 through pt10 — instead of one heroic diff. Peak throughput was 149 commits in a single day; the final tally was 443 files, +7,619/−6,854. Ten bounded PRs meant review stayed real: each one small enough to actually read, each one green before the next stacked on top.

What the agents actually found

Most anys fell into three buckets. Response types at API boundaries (the majority — now typed interfaces that document three backends' quirks in one place). Event-handler and template plumbing where the correct type existed and nobody had written it. And a small hard core of genuinely dynamic code — the message-bus payload paths — where the honest answer was a discriminated union or, in 14 remaining cases, a deliberate any with a reason. Those 14 survivors are a feature, not a failure: they're the documented boundary of what the type system can express here, instead of 1,839 places where nobody had decided.

What I'd tell someone running an agent campaign

  • Pick work with a mechanical definition of done. Type-coverage, lint counts, deprecation removals. If success needs judgment on every file, agents amplify your throughput and your error rate.
  • Build the ratchet first. Strict mode, CI, and lint rules were in place before the campaign. The agents ran inside a corridor where most wrong moves fail loudly.
  • Keep merges human. Stacked PRs with a person at each gate cost a little speed and bought total confidence in the result.

Four days, one metric, ten PRs. The codebase now states what it means at every API boundary — and the next refactor, human or agent, starts from types instead of archaeology.