Uncle Bob não revisa mais o código da IA. Ele mede.
Por que o autor do Clean Code parou de ler o código dos agentes — e o sistema de métricas que torna isso possível sem virar fé cega.
- Clean Code
- IA
- Prompt

Em abril de 2026, Robert C. Martin — o Uncle Bob, autor do Clean Code — soltou uma frase no X que dividiu a comunidade. Respondendo a uma pergunta do podcast do Wookash, ele disse, sem rodeios, que não revisa mais o código escrito por agentes de IA. Em vez de ler linha a linha, ele mede: cobertura de testes, estrutura de dependências, complexidade ciclomática, tamanho de módulos, mutation testing. A partir dessas métricas, infere a qualidade. O código em si, ele deixa para a IA.
O argumento dele é direto: humanos são lentos para ler código. Se a IA gera volume numa velocidade que nenhum revisor humano acompanha, insistir no review tradicional é criar um gargalo. Para ganhar produtividade, diz Bob, precisamos nos desligar do código e gerenciar de um nível mais alto.
A reação foi imediata. Para uns, é o futuro inevitável. Para outros, é irresponsabilidade pura. Este artigo não vai ficar em cima do muro: vou mostrar exatamente quais validações sustentam essa ideia, com exemplos em JavaScript, e onde está o furo que a crítica corretamente aponta.
A tese: spec como código-fonte
A proposta do Uncle Bob não é "confiar cegamente na IA". É mudar o que o humano autora e revisa com cuidado.
A analogia é do próprio Uncle Bob: o compilador. Você não lê o código de baixo nível que um compilador gera — confia nele porque a entrada é rigorosa (seu código-fonte) e a saída é checada (o programa roda e passa nos testes). O compilador cuida da tradução; você cuida da lógica.
Na era dos agentes, o artefato que o humano autora deixa de ser o código e passa a ser a especificação: critérios de aceitação em linguagem de domínio (o famoso Given/When/Then do ATDD), regras de negócio, arquitetura. O agente escreve os "toques de tecla". Como ele mesmo resume: as specs serão co-autoradas por humanos e IA, mas com aprovação final defendida ferozmente pelos humanos.
O que é ATDD? Acceptance Test-Driven Development é a prática de definir os critérios de aceitação de uma funcionalidade — exemplos concretos do comportamento esperado — antes de escrever o código, e transformá-los em testes automatizados. Enquanto o TDD tradicional testa unidades pequenas por dentro ("construí certo?"), o ATDD testa o comportamento completo do jeito que o usuário percebe ("construí a coisa certa?"). Esses critérios costumam ser escritos no formato Given/When/Then (Dado/Quando/Então): o contexto inicial, a ação que dispara o comportamento e o resultado esperado — tudo em linguagem de negócio, sem citar nomes de classe, endpoint ou tabela. É justamente esse contrato de aceitação que serve de âncora para o agente: ele define, de forma objetiva, o que está "pronto" e o que o agente não pode furar.
Soa elegante. Mas tem um problema prático sério.
O furo: testes que passam e não testam nada
A crítica mais afiada veio do Hacker News, e ela é justa. Agentes de IA adoram escrever testes perfunctórios: testes que existem só para bater meta de cobertura. Eles mockam tantas entradas, métodos e efeitos colaterais que, no fim, não verificam comportamento nenhum.
Veja o problema na prática:
// código sob teste
function calcularDesconto(valor, cliente) {
if (cliente.vip) return valor * 0.8;
return valor;
}
// teste que o agente "adora" escrever
test('calcularDesconto retorna um número', () => {
const resultado = calcularDesconto(100, { vip: true });
expect(typeof resultado).toBe('number'); // passa, mas não verifica NADA útil
});
Esse teste dá 100% de cobertura na primeira linha da função se você só testar o caminho VIP, e mesmo assim não garante que o desconto está certo. Se a IA trocar 0.8 por 0.9, o teste continua verde.
É aqui que mora o perigo de "medir sem ler": se a métrica pode ser enganada por testes vazios, você está cego, não no controle. A resposta para esse furo não é voltar ao review linha a linha — é usar as métricas certas, em conjunto, onde uma cobre a fraqueza da outra.
Vamos a elas.
As validações recomendadas
A regra de ouro: nenhuma métrica isolada basta. Elas só funcionam como sistema.
1. Cobertura de testes (test coverage)
É o ponto de partida e o mais fraco sozinho. Cobertura mede qual percentual do código foi executado durante os testes — não se ele foi verificado.
function classificarIdade(idade) {
if (idade < 18) return 'menor';
if (idade < 60) return 'adulto';
return 'idoso';
}
test('cobre o caminho adulto', () => {
classificarIdade(30); // executa 2 das 3 ramificações
// SEM nenhum expect! Cobertura sobe, verificação = zero
});Ferramentas como o Istanbul (embutido no Jest) reportam cobertura por linha, por branch e por função. O detalhe crucial: o exemplo acima aumenta a cobertura mesmo sem um único expect. Por isso o próprio Bob trata 100% de cobertura como uma meta alcançável em coisas pequenas, mas péssima como critério de release.
Posição no sistema: condição necessária, nunca suficiente. Sirva-a sempre acompanhada da próxima métrica.
2. Mutation testing (o herói da história)
Esta é a métrica que fecha o furo da crítica. A ideia é genial pela simplicidade: a ferramenta introduz pequenas alterações no seu código — os mutantes — e roda a sua suíte de testes contra cada versão alterada.
- Se algum teste falha com o mutante, ótimo: o mutante foi "morto". Seus testes pegaram a mudança.
- Se nenhum teste falha, o mutante "sobreviveu": significa que aquele teste não verifica de verdade aquele comportamento.
O mutation score mede a eficácia da suíte, não a mera presença de testes.
Voltando ao exemplo do desconto. O Stryker lê o seu código-fonte, encontra pontos mutáveis e gera variações automaticamente. Um dos mutadores padrão é o de operador aritmético: ele troca o * por /. Então, a partir desta linha:
function calcularDesconto(valor, cliente) {
if (cliente.vip) return valor * 0.8;
return valor;
}O Stryker gera um mutante onde valor * 0.8 vira valor / 0.8 e roda a sua suíte contra ele. Para um cliente VIP comprando R$ 100:
- Código original:
100 * 0.8 = 80-Mutante:100 / 0.8 = 125
Agora repare no teste fraco que vimos lá em cima:
test('calcularDesconto retorna um número', () => {
const resultado = calcularDesconto(100, { vip: true });
expect(typeof resultado).toBe('number');
});Tanto 80 quanto 125 são números. O teste passa nas duas versões — então o mutante sobreviveu, e o Stryker te entrega a prova de que esse teste é decorativo: ele nunca verificou o valor do desconto, só o tipo do retorno. Um teste de verdade (expect(resultado).toBe(80)) mataria o mutante, porque falharia ao ver 125.
Não é preciso referenciar o 0.8 manualmente em lugar nenhum: o Stryker descobre os pontos mutáveis sozinho, varrendo a árvore sintática do código. Outros mutadores padrão fariam, por exemplo, a condição cliente.vip virar sempre true ou sempre false, expondo testes que não cobrem os dois caminhos.
No ecossistema JS, a ferramenta padrão é o Stryker:
npx stryker run// stryker.conf.js
export default {
mutate: ['src/**/*.js', '!src/**/*.test.js'],
testRunner: 'jest',
thresholds: { high: 80, low: 60, break: 50 }, // CI quebra abaixo de 50%
};Uma otimização importante para não tornar o CI lento: differential mutation testing — mutar apenas o código que mudou no commit, em vez do projeto inteiro. É exatamente o tipo de truque que torna "medir em vez de ler" viável em escala.
Posição no sistema: é o que separa teste real de teste de fachada. Sem mutation testing, cobertura é teatro.
3. Complexidade ciclomática (cyclomatic complexity)
Conta o número de caminhos independentes através de uma função. Na prática, comece em 1 e some 1 para cada ponto de decisão (if, else if, &&, ||, for, while, case, catch, ?:).
// complexidade ciclomática = 1 (base) + 4 decisões = 5
function precoFrete(peso, regiao, cliente) {
let preco = 0;
if (peso > 10) preco += 20; // +1
if (regiao === 'norte') preco += 15; // +1
if (cliente.vip) preco *= 0.5; // +1
return preco > 0 ? preco : 10; // +1 (ternário)
}Quanto maior o número, mais caminhos precisam de teste e mais difícil é manter. É um sinal direto de que o agente "encheu" uma função de lógica. A regra prática mais usada: acima de 10, acenda o alerta amarelo; acima de 15, exija refatoração.
No JS você impõe isso direto no ESLint, sem ferramenta extra:
// eslint.config.js
rules: {
'complexity': ['error', 10], // falha o build se passar de 10
}
Posição no sistema: alarme de "aqui a IA exagerou". Funções complexas são onde bugs se escondem e onde a próxima métrica vai gritar.
4. A métrica CRAP (Change Risk Anti-Patterns)
Esta é a mais elegante, porque combina complexidade e cobertura numa fórmula só. A intuição: um trecho de código só é realmente perigoso quando é complexo E mal testado ao mesmo tempo.
A fórmula:
CRAP(m) = complexidade(m)² × (1 − cobertura(m))³ + complexidade(m)Onde a cobertura é um valor de 0 a 1. Veja como ela se comporta:
Repare na beleza disso:
- Código complexo, mas muito bem testado, tem CRAP baixo. Está sob controle.
- Código simples, mesmo sem teste, tem CRAP moderado. O risco é pequeno.
- Só o que é complexo e mal coberto explode o número.
O limiar mais citado é CRAP > 30: acima disso, o método entra na fila de refatoração. Em vez de revisar tudo, você revisa exatamente onde o risco está concentrado. É a métrica que transforma "validar sem ler" de fé cega em priorização inteligente.
5. Estrutura de dependências (dependency structure)
Aqui validamos se as setas de dependência apontam na direção certa. Numa Clean Architecture, a Regra da Dependência diz que tudo aponta para dentro: o domínio nunca conhece a infraestrutura.
O problema é que um agente, sem supervisão, importa o que for mais conveniente — e isso fura a arquitetura silenciosamente. A solução são testes de arquitetura: regras automatizadas que falham o build se alguém importar o que não devia.
No JS, o dependency-cruiser faz isso muito bem:
// .dependency-cruiser.js
module.exports = {
forbidden: [
{
name: 'dominio-nao-conhece-infra',
severity: 'error',
from: { path: '^src/domain' },
to: { path: '^src/infra' }, // domínio importando infra = build quebra
},
],
};npx depcruise src --config .dependency-cruiser.jsPosição no sistema: protege as decisões arquiteturais que você, como humano, defende ferozmente. O agente escreve dentro das cercas; o teste de arquitetura é a cerca.
6. Detecção de ciclos e de duplicação
Dois cheiros clássicos que agentes introduzem sem perceber.
Ciclos de dependência: o módulo A importa B, que importa A. Quebra a testabilidade e o entendimento. O mesmo dependency-cruiser detecta:
{
name: 'sem-ciclos',
severity: 'error',
from: {},
to: { circular: true },
}Duplicação: a IA tende a recriar lógica em vez de reusar a existente. O jscpd (Copy/Paste Detector) acha blocos repetidos:
npx jscpd src --threshold 5 # falha se duplicação passar de 5%Posição no sistema: gates automáticos baratos que pegam dois dos vícios mais comuns de código gerado.
7. Tamanho de módulos e funções
A métrica mais simples, e surpreendentemente eficaz. Funções e arquivos pequenos são um proxy de coesão e contêm a tendência do agente de "plopar" código em qualquer lugar.
// eslint.config.js
rules: {
'max-lines-per-function': ['error', 40],
'max-lines': ['error', 300],
'max-params': ['error', 4],
}Posição no sistema: força a IA a decompor. Função grande é onde complexidade e duplicação se acumulam — cortar pela raiz é mais barato que corrigir depois.
Montando o pipeline na prática
Isoladas, essas métricas são relatórios. Juntas e automatizadas, viram um portão de qualidade que roda sozinho. O fluxo tem quatro passos, cada um com um responsável claro:
- Humano — escreve a spec. Critérios de aceitação em linguagem de domínio (Given/When/Then), sem citar classe, endpoint ou tabela.
- Agente — gera testes e código a partir da spec.
- CI — roda os gates, do mais barato ao mais decisivo:- cobertura mínima (necessária, não suficiente)-**mutation score** acima do limiar (a verificação de verdade)- complexidade ciclomática e CRAP dentro do limite- testes de arquitetura (sem importação proibida, sem ciclos)- duplicação e tamanho sob controle
- Humano — entra só onde a métrica aponta. Gate vermelho barra o merge; você lê apenas o trecho sinalizado, não o diff inteiro.
Vale notar que o próprio Bob está colocando isso em prática com ferramentas próprias — ele mantém repositórios públicos com um mutation tester e um analisador de CRAP para seus projetos em Java, além de skills para Claude Code. Não é teoria de palco; é o workflow que ele usa.
Num setup com subagents e Git worktrees, dá para ir além: um subagent dedicado a rodar e interpretar as métricas, devolvendo ao agente executor um relatório do que precisa melhorar antes mesmo de o código chegar a você.
Então, ele está certo?
Em parte. A proposta brilha onde o código é **bem especificado** e o **volume é alto**: CRUD, integrações, refatorações mecânicas. Nesses casos, review linha a linha é desperdício de um recurso caro (atenção humana sênior), e o conjunto de métricas dá uma rede de segurança mais consistente do que o olho cansado de um revisor às cinco da tarde.
Mas o "medir em vez de ler" tem limites honestos. Arquitetura nova, requisitos ambíguos, decisões de produto com implicação de compliance — nada disso aparece num mutation score. Uma suíte de métricas perfeita ainda pode estar validando, com excelência, a solução **errada** para o problema.
A síntese, então, não é "pare de ler código". É: **suba o nível em que você aplica seu cuidado.** O humano autora e defende a spec e a arquitetura; as métricas, em conjunto, garantem que o agente respeitou os dois. Mutation testing cobre o furo da cobertura. CRAP prioriza onde olhar. Os testes de arquitetura protegem o desenho. É isso que o Uncle Bob chama de camada de disciplina para a era da spec-como-código-fonte.
E talvez seja essa a habilidade que define o engenheiro sênior em 2026: não escrever o código mais rápido que a máquina — isso é impossível — mas construir o sistema de validação que faz o código da máquina ser confiável.
// COMENTÁRIOS
Deixe seu comentário
Comentários (0)
Nenhum comentário ainda. Seja o primeiro.