Blog
5 min read

We built Uncle Bob a crap detector for TypeScript. Then we pointed it at the internet's vibe-coded apps

Uncle Bob says don't read your agents' code, surround them with constraints instead. We built one of those constraints for TypeScript in a day, ran it on ten vibe-coded apps and four pillars of the ecosystem, and found the AI tell isn't `any`. It's console.log.

We built Uncle Bob a crap detector for TypeScript. Then we pointed it at the internet's vibe-coded apps

Yesterday Uncle Bob Martin told 3.4 million readers that he no longer reads the code his AI agents write. His strategy is to "surround the agents with extreme constraints": unit tests, mutation testing, coverage, quality metrics. He trusts the gauntlet instead of his eyes. When someone asked which deterministic tools he actually uses, he linked crap4java, his analyzer built on the old CRAP metric: change risk as a function of complexity and test coverage.

TypeScript didn't have one. So we built crap4ts and replied to the thread. Then we did the thing a metrics tool has to survive: we ran an experiment with it.

A crap detector, built by the thing it detects

Full disclosure first: crap4ts was written almost entirely by Claude, over about a day of closely supervised sessions. An AI wrote the tool that scores AI-generated crap. We think that's the point rather than a gotcha. Uncle Bob's argument is that provenance stops mattering once the constraints are hard enough, and the first codebase we ran the gauntlet on was the tool itself.

The tool scores every method two ways. The CRAP score is CC² · (1 − coverage)³ + CC: complex and untested is exponentially bad, complex and fully tested is merely its complexity. On top of that sits a slop score from 14 smell detectors, split into escape hatches (any, as casts, @ts-ignore, non-null !, muted catches) and style residue (console.log, optional-chaining soup, guard ladders, blanket try/catch). Any method over a CRAP threshold of 8.0 fails the build with a non-zero exit code, with a file:line findings list telling you what to fix.

The constraints on the tool itself: 115 tests, 100% coverage on every metric, the strictest compiler flags TypeScript offers, and a CI gate that runs crap4ts on its own source. Its worst method scores about 4.0 against the 8.0 threshold. If it ever generates crap by its own definition, its build fails.

It flagged its own cleanest code first

The first self-run reported 154 slop points. An external review found 68 of them were false positives, all from one detector. It flagged guard clauses by naming convention, so any well-named predicate starting with is or has counted against the score. The detector was punishing exactly the code we most wanted to keep. We rewrote it to be structural. It now counts consecutive if-return ladders instead of names, and the 68 findings dropped to 3, all real.

That mistake shaped how we read everything that follows: a heuristic that recognizes crap by its spelling will convict clean code. You want detectors that recognize it by its shape.

Then we pointed it at vibe-coded apps

We measured slop density across ten open-source vibe-coded TypeScript projects, drawn from the awesome-vibecoded-apps list plus vibe-tagged supplements, against four references: VS Code, the TypeScript compiler, NestJS, and tRPC. Third-party repos don't hand you coverage data, so this experiment measures smells only, normalized per method. One finding arrived before any code was analyzed: of the 32 web apps on that list, only 3 have public source. Vibe coding ships products; it rarely shares code.

We expected total slop to separate the cohorts. It didn't. Vibe apps average 1.42 slop points per method; the references average 1.33. Escape hatches went the wrong way entirely: vibe code averages 0.28 per method while the references average 0.45. NestJS is the heaviest codebase in the whole sample at 0.74, because decorators and HTTP adapters legitimately need any and as, and the TypeScript compiler leans hard on casts for its internals.

What did separate them, by a wide margin, is residue, the marks of code nobody read after it was generated:

  • console.* calls: 16× more frequent in vibe code
  • ?? fallbacks: 6× more frequent
  • try/catch wrappers: 3.6× more frequent
  • ?. chains: 2.4× more frequent

VS Code is the cleanest codebase in the sample at 0.73 slop per method. The most extreme vibe project has 385 console hits across 683 methods. Debug output became a permanent resident. Mean cyclomatic complexity tilts the same way: about 4.0 per method in vibe code against 2.8 in the references.

The tell is residue, not escape hatches

AI-generated code doesn't cheat the type system more than framework authors do. It leaves residue: debug prints, null-soup fallbacks, defensive wrappers around code paths nobody traced. If, like Uncle Bob, you've decided not to read your agents' code, this is precisely what your gauntlet needs to catch. A naive total-slop number won't catch it, because a framework codebase full of honest as casts scores about the same as a vibe app full of stray logging.

We also ran the full coverage-weighted gate on our own production codebase, a human-reviewed backend and frontend of a few thousand methods. On slop density it beat both cohorts, at 0.63 and 0.72 per method. It still failed the CRAP gate: 37 methods over the threshold, the worst scoring 90.0, a complexity-9 method with zero test coverage. The metric caught reviewed human code doing exactly what AI code gets blamed for. That felt fair.

The honest caveats: style detectors are soft signals. ?. and ?? are idiomatic TypeScript, and high density is suggestive rather than proof. Ten projects is a small sample, and it only covers vibe-coded apps that publish their source. The next experiment we want is human-labeled vibe versus reviewed pull requests inside one monorepo, which would turn this correlation into something closer to a causal claim.

Uncle Bob's gauntlet only works if the bars measure the right thing. Right now the signal is not in how AI code escapes the type system. It is in what the code leaves behind. crap4ts is open source at github.com/alperlabs/crap4ts. Point it at your own code before you point it at anyone else's. Ours failed.