Free

Dejar que Claude haga los refactors

Un refactor no tiene síntomas — cuando Claude se equivoca no lo ves. Tres barandas: tests, commits atómicos, clic manual.


Refactorizar es la tarea donde Claude es más peligroso. Un bug tiene síntomas — el botón no hace nada, el valor es undefined, stack trace — así que puedes saber si el fix de Claude funcionó. Un refactor no los tiene. "Todavía funciona" puede significar "los tests aún pasan" mientras el comportamiento cambió en silencio, no te diste cuenta, y producción explota una semana después.

Hice hace poco un refactor bastante grande con Claude en how2claude: migré los pagos crypto de x402 desde un PaymentHandler + FacilitatorClient hechos a mano (139 líneas) al gem x402-rails, y al mismo tiempo extraje el mapeo de campos de Purchase.create! / Subscription.create! — duplicado en dos controllers — a métodos de clase del model. Un commit, 4 archivos cambiados, 2 borrados, 2 añadidos.

Mi prompt fue una palabra: "refactor."

Podía ser así de corto porque había barandas alrededor.

Baranda #1: Nunca refactorizar sin tests

Cuando la rama llegó a ese punto, tenía 221 tests. Todos los caminos críticos del flujo de pago estaban cubiertos.

La acción por defecto de Claude antes de un refactor no es "mirar los tests primero." Así que le dije que corriera bin/rails test primero, confirmara verde, y luego tocara algo.

Después del refactor, correrlo otra vez. Aún verde. Eso no significa cero regresiones — significa que el comportamiento conocido no está roto.

Si tu codepath no tiene cobertura de tests, haz que Claude escriba un test mínimo que fije el comportamiento actual. Correrlo, commitearlo. Después refactorizar. Si no, lo que está haciendo no es refactorizar, es reescribir — y no tienes forma de verificar equivalencia.

Baranda #2: Haz que divida el cambio en commits atómicos

Este refactor eran en realidad dos cosas:

  1. Backend x402: hecho a mano → gem
  2. Mapeo de campos de Purchase / Subscription: controller → métodos de clase del model

Había una tercera en el frontend: reescribir el flujo de firma en JS con viem + x402-fetch.

Hice que Claude dividiera por límites naturales: backend + extracción de model en un commit (9f3e239), frontend en otro separado (93746d8). Cada commit trae descripción completa, lista de archivos, y el porqué del cambio.

Beneficios:
- Los diffs se mantienen legibles. Un commit, una cosa.
- Granularidad de rollback. Si producción pega un bug de frontend, git revert 93746d8 rollbackea solo el frontend, dejando el backend.
- La atención del propio Claude se mantiene enfocada. Un commit, una cosa — y su atención cubre solo esa cosa.

Baranda #3: Lee el diff antes de dar por hecho

Una vez terminado el refactor, hago que Claude pare y me muestre git diff --staged. No corras los tests. No corras la app. Lee primero el diff.

Señales que escaneo:

  • ¿Qué borró? app/services/x402/payment_handler.rb eliminado entero — bien, ese es el punto de migrar al gem. Pero si borra algo que no le pedí que tocara, paro y pregunto.
  • ¿Cambió el mapeo de campos? Purchase.create!(wallet_address: verify_result["payer"], ...)Purchase.record_x402!(payment:, settlement:) ahora lee payment[:payer]. La fuente cambió (request.env del gem vs valor de retorno del cliente viejo), pero los campos tienen que mapear uno a uno.
  • Cambios "de paso." A Claude le encanta arreglar cosas que "se ven mal" mientras refactoriza — reformular mensajes de error, renombrar variables, extraer un método que cree que debería existir. Cuidado con estos. La promesa de un refactor es "equivalente en comportamiento." Un cambio de paso la rompe.

Dos cosas que Claude se equivocó esta vez

Trampa 1: Los Stimulus controllers del gem no cargaron en silencio

El gem x402-rails trae sus propios Stimulus controllers. Claude escribió el código, los tests pasaron verdes. Cliqueé manualmente el botón de pago — no pasó nada.

Razón: config/importmap.rb tenía un pin para @hotwired/stimulus apuntando a un archivo vendor que no existía, e importmap descartó el pin silenciosamente. Los controllers del gem nunca cargaron. Los tests no pueden atrapar esto porque bin/rails test no ejecuta JS.

Trampa 2: YAML parseó 0x... como entero

wallet_address: 0x833589... — sin comillas. YAML vio el prefijo 0x, lo leyó como entero hexadecimal. El facilitator recibió un no-string y rechazó. Claude no paró a pensar en las reglas de parseo de YAML al escribir la config.

Ambas trampas fueron detectadas porque cliqueé el botón real. Tests pasando no es lo mismo que el feature funcionando. La verificación manual después de un refactor no es opcional.

El flujo completo de refactor con Claude

  1. Corre toda la suite de tests. Confirma verde. Si el código objetivo no tiene cobertura, haz que Claude escriba un test mínimo que fije el comportamiento actual, y comitéalo.
  2. Di qué superficie quieres extraer. "Refactor" funciona cuando la duplicación es obvia; cuando no lo es, di "extrae el mapeo de campos de X a métodos de clase en Y."
  3. Divide en commits atómicos. Un commit, una cosa.
  4. Lee el diff tú mismo. Qué se borró, si el mapeo de campos se sostiene, si cambió algo de paso.
  5. Corre los tests otra vez. Verde.
  6. Si toca un camino visible al usuario, cliquea el feature a mano. Las capas que los tests no alcanzan — JS, importmap, CDN, parseo YAML — las tienes que ver con tus propios ojos.

Refactorizar es el escenario "dejar que Claude maneje" de más alto riesgo. Las barandas no son para Claude. Son para ti — para que cuando Claude se equivoque, lo atrapes en 5 minutos en lugar de en un incendio de producción.