GenAIHub
← Back to Technical Section

Uncle Bob's Code Validation

Objective Gates for Reviewing AI- and Agent-Generated Code — Beyond "It Looks Fine"

The Problem with AI-Generated Code

AI agents and coding copilots can produce hundreds of lines in seconds. The bottleneck shifts from writing code to validating it — and line-by-line review doesn't scale. Robert C. Martin ("Uncle Bob"), author of Clean Code and the SOLID principles, argues that when code is generated at machine speed, human review must move up a level: from reading lines to enforcing objective, automated metrics as CI/CD gates that the AI itself must pass before its code is accepted.

"The code only merges when the metrics pass. Cyclomatic complexity, coverage, dependency direction — these are not suggestions. They are the gate."

— Robert C. Martin (Uncle Bob)

Key Insight: Metrics don't replace architectural judgment or business-logic understanding — they block the obvious bad stuff automatically so human review can focus on intent, security and domain correctness.

The Five Validation Gates

1. Test Coverage

Every function and branch must be exercised by a test. Uncle Bob's position: high coverage is necessary but not sufficient — a test that never asserts anything can hit 100% coverage without catching a single bug. Treat the coverage threshold as a floor, not a trophy.

target ≥ 90% Python: coverage.py + pytest JS: Jest / Istanbul Java: JaCoCo

2. Cyclomatic Complexity

Measures the number of independent execution paths through a function (each if, for, while adds one path). AI models often produce deeply nested, branchy functions. A CC of 10+ is a warning sign; Uncle Bob recommends a hard limit of 4–6 per function. High CC means the function is doing too many things and is hard to test fully.

limit ≤ 4–6 Python: radon cc JS: ESLint complexity rule Java: Checkstyle

3. Dependency Structure (Clean Architecture Dependency Rule)

Uncle Bob's Dependency Rule: source code dependencies must point inward — toward higher-level policies. Business rules must never import frameworks, databases or UI. AI agents routinely violate this by importing ORM models into domain logic, wiring database calls into use-case functions, or calling third-party APIs directly from the core. Automated architecture tests catch this before review.

domain/  ← use_cases/  ← adapters/  ← frameworks/
(inner layers must never import outer layers)

Python: dependency-injector, import-linter Java: ArchUnit

4. Module & Function Size

Small functions do one thing and do it well. Uncle Bob's rule: functions should rarely exceed 20 lines; classes should rarely exceed 200. AI-generated code frequently violates this — producing "god functions" that handle parsing, validation, business logic and persistence in one block. Lint rules can enforce maximum line counts automatically.

fn ≤ 20 lines | class ≤ 200 lines Python: ruff / radon mi JS: ESLint max-lines-per-function

5. Mutation Testing

Mutation testing answers: "If I deliberately break this logic, do the tests catch it?" The tool introduces small changes (mutations) — flipping > to , removing a return value, negating a condition — and runs the test suite. A surviving mutant means a real bug in that location would go undetected. Uncle Bob considers surviving mutants evidence of weak or missing tests, which is especially dangerous for AI-generated code because the agent tends to produce plausible-looking assertions.

mutation score ≥ 80% Python: mutmut / cosmic-ray JS: Stryker Java: PIT

The CRAP Score — Composite Risk Metric

Uncle Bob has referenced the CRAP score (Change Risk Anti-Patterns) as a practical composite metric. It combines cyclomatic complexity and test coverage into a single risk number per function: a complex function with low coverage scores much higher (worse) than a simple or well-tested one.

# CRAP(f) = CC(f)² × (1 - coverage(f))³ + CC(f)
# CC = cyclomatic complexity, coverage = test coverage fraction
#
# Interpretation:
#   CRAP ≤ 5   → low risk
#   5 – 10     → moderate risk, review required
#   > 10       → high risk, must refactor before merge
#
# Examples:
#   CC=2,  coverage=0.95  → CRAP ≈ 2.0  ✅
#   CC=10, coverage=0.50  → CRAP ≈ 22.5 ❌ refactor
#   CC=10, coverage=1.00  → CRAP = 10   ⚠️  still borderline

Python: radon cc -s gives CC per function; combine with coverage.py to compute CRAP. Some CI plugins (e.g. SonarQube) report CRAP out of the box.

The AI-Code Validation Pipeline

The recommended workflow lets the AI agent iterate on its own output until it passes objective gates, then hands off to a human reviewer who focuses on architecture, security and business intent.

1

Agent generates code

Raw output from AI agent, copilot or code-gen model

2

Lint & format

ruff / ESLint / Checkstyle — enforce style, remove dead code

3

Run tests

pytest / Jest / JUnit — all tests must pass

4

Coverage gate

coverage.py / Istanbul — fail if below threshold (e.g. 90%)

5

Complexity & size gate

radon / ESLint — fail if CC > 6 or function > 20 lines

6

Dependency direction gate

import-linter / ArchUnit — fail if inner layers import outer

7

Mutation testing gate

mutmut / Stryker — fail if mutation score < 80%

Gate failed? Agent self-corrects

Feed the failure report back to the agent; it refactors and reruns from step 2

Human reviews architecture, security & business intent

All objective gates passed — reviewer focuses on what metrics can't catch

Prompt: Asking the Agent to Self-Correct

When a gate fails, feed the metric report back to the agent with explicit targets. This closes the loop and often resolves issues without human intervention.

"""
Analyze this module and apply the following corrections:

1. Reduce cyclomatic complexity to ≤ 4 per function. Break large functions into
   smaller, single-responsibility ones.

2. Increase test coverage to ≥ 90%. Add missing test cases for uncovered branches.

3. Add or strengthen tests until the mutation score is ≥ 80%. Focus on:
   - Boundary conditions (off-by-one, empty inputs, null values)
   - Business logic branches (each if/else path)

4. Enforce the Dependency Rule: domain/business-logic code must NOT import
   framework, database or UI modules. Introduce abstractions (interfaces/protocols)
   where needed.

5. Keep all functions under 20 lines. Split longer ones by responsibility.

Preserve all existing public behaviour and document any architectural changes.
"""

Tooling Reference

Gate Python JavaScript / TS Java
Lint & format ruff, black ESLint, Prettier Checkstyle, SpotBugs
Test runner pytest Jest / Vitest JUnit 5
Coverage coverage.py Istanbul / c8 JaCoCo
Cyclomatic complexity radon cc ESLint complexity Checkstyle / PMD
Dependency rules import-linter eslint-plugin-import ArchUnit
Mutation testing mutmut, cosmic-ray Stryker PIT (Pitest)
CRAP score / composite radon + coverage.py SonarQube

GitHub Actions Example (Python)

name: Code Quality Gates
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lint
        run: ruff check . && ruff format --check .

      - name: Tests + Coverage
        run: |
          pytest --cov=src --cov-fail-under=90 --cov-report=term-missing

      - name: Cyclomatic Complexity
        run: |
          radon cc src/ -n B --show-closures
          # exits non-zero if any function scores B or above (CC > 5)

      - name: Dependency Rules
        run: lint-imports
          # enforces rules in .importlinter config

      - name: Mutation Testing
        run: mutmut run && mutmut results
          # fail if surviving mutants exist

What Metrics Don't Catch — The Human Layer

Objective gates block a lot of bad AI code automatically, but metrics have blind spots. The human reviewer must still focus on:

  • Wrong business logic — code can pass all metrics and still solve the wrong problem
  • Security vulnerabilities — injection, broken auth, unsafe deserialization
  • Performance — N+1 queries, O(n²) algorithms in hot paths
  • Domain model coherence — wrong abstractions, naming that misleads future developers
  • Hallucinated APIs — AI sometimes calls library methods that don't exist or uses deprecated signatures

Best Practices

Do This

  • Set hard CI gates — PR can't merge if any gate fails
  • Feed gate failures back to the agent automatically
  • Start strict; loosen thresholds intentionally with justification
  • Run mutation testing on business-critical paths first
  • Write architecture tests (import rules) before the agent touches the codebase

Avoid This

  • Treating 100% coverage as a quality guarantee
  • Skipping mutation testing "because coverage is already high"
  • Letting metrics replace architectural review entirely
  • Adding noqa / eslint-disable without justification
  • Running mutation testing over the entire codebase on every PR (too slow)

Related Topics