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."
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.
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.
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)
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.
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.
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.
Agent generates code
Raw output from AI agent, copilot or code-gen model
Lint & format
ruff / ESLint / Checkstyle — enforce style, remove dead code
Run tests
pytest / Jest / JUnit — all tests must pass
Coverage gate
coverage.py / Istanbul — fail if below threshold (e.g. 90%)
Complexity & size gate
radon / ESLint — fail if CC > 6 or function > 20 lines
Dependency direction gate
import-linter / ArchUnit — fail if inner layers import outer
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-disablewithout justification - Running mutation testing over the entire codebase on every PR (too slow)