Free

Deixando o Claude refatorar

Refactor não tem sintomas — você não vê quando o Claude erra. Três guardrails: testes, commits atômicos, clique manual.


Refatorar é a tarefa em que o Claude é mais perigoso. Um bug tem sintomas — o botão não faz nada, o valor é undefined, stack trace — então dá pra saber se o fix do Claude funcionou. Um refactor não tem. "Ainda roda" pode significar "os testes ainda passam" enquanto o comportamento mudou em silêncio, você não percebeu, e produção explode uma semana depois.

Fiz um refactor considerável com o Claude no how2claude recentemente: migrei os pagamentos crypto x402 de um PaymentHandler + FacilitatorClient feitos à mão (139 linhas) para a gem x402-rails e, ao mesmo tempo, extraí o mapeamento de campos de Purchase.create! / Subscription.create! — duplicado em dois controllers — para métodos de classe do model. Um commit, 4 arquivos alterados, 2 apagados, 2 adicionados.

Meu prompt foi uma palavra: "refactor."

Podia ser tão curto porque havia guardrails ao redor.

Guardrail #1: Nunca refatorar sem testes

Quando a branch chegou nesse ponto, tinha 221 testes. Todos os caminhos críticos do fluxo de pagamento cobertos.

A ação padrão do Claude antes de um refactor não é "olhar os testes primeiro." Então mandei ele rodar bin/rails test antes, confirmar verde, e aí sim mexer em algo.

Depois do refactor, rodar de novo. Ainda verde. Isso não significa zero regressão — significa que o comportamento conhecido não quebrou.

Se seu codepath não tem cobertura, faça o Claude escrever um teste mínimo que trave o comportamento atual. Rodar, comitar. Aí sim refatorar. Senão o que ele está fazendo não é refactor, é reescrita — e você não tem como verificar equivalência.

Guardrail #2: Faça ele dividir a mudança em commits atômicos

Esse refactor eram na verdade duas coisas:

  1. Backend x402: feito à mão → gem
  2. Mapeamento de campos de Purchase / Subscription: controller → métodos de classe do model

Havia uma terceira no frontend: reescrever o fluxo de assinatura JS com viem + x402-fetch.

Fiz o Claude dividir pelas fronteiras naturais: backend + extração de model em um commit (9f3e239), frontend em outro separado (93746d8). Cada commit carrega descrição completa, lista de arquivos, e o porquê da mudança.

Benefícios:
- Diffs continuam legíveis. Um commit, uma coisa.
- Granularidade de rollback. Se produção pega um bug de frontend, git revert 93746d8 reverte só o frontend, deixando o backend.
- A atenção do próprio Claude fica focada. Um commit, uma coisa — e a atenção dele cobre só aquela coisa.

Guardrail #3: Leia o diff antes de dizer que acabou

Uma vez completo o refactor, faço o Claude parar e me mostrar git diff --staged. Não roda os testes. Não roda a app. Lê o diff primeiro.

Sinais que eu procuro:

  • O que ele apagou? app/services/x402/payment_handler.rb apagado inteiro — ok, esse é o ponto de migrar para a gem. Mas se apagou algo que eu não pedi pra mexer, paro e pergunto.
  • O mapeamento de campos mudou? Purchase.create!(wallet_address: verify_result["payer"], ...)Purchase.record_x402!(payment:, settlement:) agora lê payment[:payer]. A fonte mudou (request.env da gem vs retorno do client antigo), mas os campos têm que mapear um-pra-um.
  • Mudanças "no caminho." O Claude adora arrumar coisas que "parecem erradas" enquanto refatora — reescrever mensagens de erro, renomear variáveis, extrair um método que ele acha que deveria existir. Fica de olho nisso. A promessa de um refactor é "equivalente em comportamento." Uma mudança de passagem quebra isso.

Duas coisas que o Claude errou dessa vez

Armadilha 1: Os Stimulus controllers da gem não carregaram em silêncio

A gem x402-rails traz seus próprios Stimulus controllers. O Claude escreveu o código, os testes passaram verdes. Cliquei manualmente no botão de pagamento — nada.

Motivo: config/importmap.rb tinha um pin pro @hotwired/stimulus apontando pra um arquivo vendor que não existia, e o importmap descartou o pin em silêncio. Os controllers da gem nunca carregaram. Testes não pegam isso porque bin/rails test não executa JS.

Armadilha 2: YAML parseou 0x... como inteiro

wallet_address: 0x833589... — sem aspas. YAML viu o prefixo 0x, leu como inteiro hexadecimal. O facilitator recebeu uma não-string e rejeitou. O Claude não parou pra pensar nas regras de parse do YAML enquanto escrevia a config.

As duas armadilhas foram pegas porque eu cliquei no botão real. Teste passando não é a mesma coisa que feature funcionando. Verificação manual depois de um refactor não é opcional.

O fluxo completo de refactor com Claude

  1. Rode a suíte de testes inteira. Confirme verde. Se o código alvo não tem cobertura, faça o Claude escrever um teste mínimo que trave o comportamento atual e comite isso.
  2. Diga que superfície você quer extrair. "Refactor" funciona quando a duplicação é óbvia; quando não é, diga "extrai o mapeamento de campos de X pra métodos de classe em Y."
  3. Divide em commits atômicos. Um commit, uma coisa.
  4. Lê o diff você mesmo. O que foi apagado, se o mapeamento de campos se mantém, se mudou algo de passagem.
  5. Roda os testes de novo. Verde.
  6. Se toca um caminho visível pro usuário, clica pelo feature manualmente. Camadas que os testes não alcançam — JS, importmap, CDN, parse YAML — você tem que ver com seus próprios olhos.

Refactor é o cenário "deixa o Claude dirigir" de mais alto risco. Os guardrails não são pro Claude. São pra você — pra quando o Claude errar algo, você pegar em 5 minutos em vez de num incêndio de produção.