Inicio / Artículos

Revisando las refacturaciones a gran escala en IA: por qué pasar las pruebas no detecta la intención eliminada

Aprenda por qué los refactorizaciones asistidas por IA eliminan silenciosamente el código esencial, por qué las pruebas de integración no lo detectan, y cinco prácticas de revisión que permiten lanzar refactorizaciones a gran escala de forma segura.

Editor: REACTAPP.TOP7 min de lectura1618 palabras

Fuentes: I Let Claude Refactor 4,000 Lines. Here’s the Part I Had to Undo.

Los asistentes de programación basados en IA son ahora lo suficientemente buenos como para mejorar un módulo descuidado en una tarde, y el resultado suele ser mejor que lo que reemplazó. El peligro no radica en código descuidado, sino en código que parece correcto pero que silenciosamente elimina funcionalidades que nadie anotó. Esta guía analiza un fallo real de este tipo, explica por qué tanto el conjunto de pruebas como el revisor lo pasaron por alto, y ofrece un proceso concreto de revisión para refactores a gran escala realizados con ayuda de IA.

Un refactor que mejoró el código y dañó el sistema

Imaginemos un módulo en el que se han ido acumulando cambios durante dos años: unas 4,000 líneas distribuidas en cuatro archivos, la parte del códigobase que los ingenieros evitan tocar siempre que pueden. Cuenta con un manejo inconsistente de errores, tres formas diferentes de parsear fechas y varias funciones cuya existencia aún se duda si realmente siguen siendo llamadas.

Un desarrollador se lo entrega a un asistente de IA en dos sesiones y recibe algo claramente mejor: patrones uniformes, indicaciones de tipo adecuadas, ramas innecesarias eliminadas y unas tres veces menos líneas de código. Las pruebas pasan. El desarrollador revisa las diferencias, o cree haberlo hecho, y lo envía.

Seis días después, una integración con un socio comienza a perder registros. No hay errores evidentes; simplemente alrededor del cuatro por ciento de los registros deja de llegar. La causa es la refactorización, y más concretamente un cambio que el desarrollador había leído y aprobado.

Dónde el asistente destacó realmente

Vale la pena ser preciso sobre lo que funcionó bien, porque la conclusión no es “la IA es mala para la refactorización”. El asistente fue excelente en cada transformación que dependía únicamente del código que tenía ante sí:

  • combinar las tres funciones auxiliares para el análisis de fechas en una sola función
  • convertir las condiciones anidadas en cláusulas de protección
  • anotar todo el módulo con indicaciones de tipo
  • identificar dos ramas que realmente eran inaccesibles
  • elegir nombres más claros
  • Para cambios locales y mecánicos que pueden verificarse al leerlos, fue más rápido y consistente que un humano, además no perdía el enfoque tras leer cientos de líneas, algo que suele ocurrir con las refactorizaciones manuales. Cuando la tarea es “hacer que este código exprese el mismo comportamiento de manera más clara”, estas herramientas rinden muy bien. Los problemas comienzan en los límites de lo que realmente incluye “el mismo comportamiento”.

    Por qué se elimina el código esencial

    El código que no tiene una razón visible parece pura basura. Aquí está el cambio aprobado:

    # before
    def normalise_timestamp(raw):
        ts = raw.strip()
        if ts.endswith("Z") and "." in ts:
            ts = ts.split(".")[0] + "Z"          # partner sends 7-digit microseconds
        return datetime.fromisoformat(ts.replace("Z", "+00:00"))
    
    # after
    def normalise_timestamp(raw):
        return datetime.fromisoformat(raw.strip().replace("Z", "+00:00"))
    

    La rama eliminada parece ser un objetivo típico de refactorización: un caso especial, una verificación de sufijo extrañamente específica y una división de cadena. Parece redundante, ya que fromisoformat ya analiza las marcas de tiempo ISO.

    El problema radica en la entrada de datos. Un socio envía marcas de tiempo con siete dígitos fraccionarios. Antes de Python 3.11, fromisoformat solo interpretaba los formatos que emitía isoformat() en sí, lo que significaba que había un máximo de seis dígitos fraccionarios; por eso, un valor con siete dígitos generaba un ValueError. Python 3.11 y versiones posteriores aceptan un subconjunto mucho más amplio de ISO 8601 y son más tolerantes en este aspecto, por lo que si este cambio causa problemas depende del entorno de ejecución, lo cual es otra razón más para verificar en lugar de asumir. La rama existía debido a un ticket de soporte de dieciocho meses antes, y su breve comentario no fue suficiente para que nadie, ya sea humano o modelo, evaluara adecuadamente si era realmente necesario teniendo en cuenta lo poco relevante que parecía. También hay que notar que la antigua rama hacía más de lo que parecía: descartaba por completo la parte fraccionaria, por lo que incluso la salida de la versión “posterior” difiere para las entradas que sí se pueden interpretar.

    El problema fundamental es sencillo: el modelo podía ver el código, pero no el incidente que lo generó. La solicitud de soporte, el análisis posterior al incidente, el hilo de chat y las horas dedicadas a depurar estaban todos fuera del archivo. Ejecutar git blame habría revelado un mensaje de commit como “fix timestamp parsing for partner feed”, que era toda la justificación, pero nadie pidió al asistente que lo revisara. El revisor aprobó la eliminación por la misma razón que el asistente la propuso: ambos estaban leyendo la diferencia entre versiones, no el historial.

    Por qué el conjunto de pruebas siguió funcionando correctamente

    Las pruebas verificaban qué devolvía la función con entradas que alguien había imaginado, y la entrada problemática era, por definición, una que nadie había considerado.

    Los archivos de configuración contenían marcas de tiempo con tres decimales, porque eso es lo que la gente escribe a mano al crear datos de prueba. Los valores de siete dígitos solo aparecían en la transmisión en vivo de un socio.

    Esto es lo que hace arriesgados los grandes refactores, con o sin participación de la IA: un conjunto de pruebas que pasa solo demuestra que el comportamiento descrito por esas pruebas sobrevivió. No dice nada sobre los comportamientos que no describen. Cuando refactores treinta líneas a mano, piensas en cada una de ellas. Cuando cuatro mil líneas cambian al mismo tiempo, el conjunto de pruebas acaba haciendo casi toda la verificación, y nunca fue diseñado para soportar esa carga.

    Las métricas de cobertura no ayudaron. La función tenía cobertura total por líneas: cada línea se ejecutó durante las pruebas. Una rama simplemente nunca se ejecutó con la entrada relevante. La cobertura por líneas mide qué código se ejecutó, no qué formas de entrada se probaron.

    Clasificación de los cambios generados por la IA según el riesgo

    • Bajo riesgo: anotaciones de tipo, renombramientos, formato, devoluciones anticipadas y reescrituras mecánicas similares. Son locales y fáciles de verificar al leerlas.
    • Riesgo moderado: consolidación de ayudantes duplicados. Por lo general está bien, pero hay que comprobar que la versión fusionada maneje las entradas de cada llamante.
    • Riesgo alto: eliminación de código que parece no estar en uso, supresión de casos especiales que parecen redundantes y unificación de comportamientos que parecen inconsistentes.

    El último punto también afecta de manera más sutil. Imagine un módulo que formatea los errores de forma diferente al resto, y una refactorización para que se ajuste. La diferencia era intencionada, ya que un consumidor posterior interpreta esa forma específica de error. Buscar la uniformidad fue un instinto acertado; aplicarlo aquí fue un error.

    Cinco prácticas para la próxima gran refactorización

    Estas están ordenadas aproximadamente según el grado en que cada una habría evitado el incidente mencionado anteriormente.

    Mantenga cada diferencia lo suficientemente pequeña como para revisarla realmente

    Cuatro mil líneas no constituyen un cambio que se pueda revisar adecuadamente. Pasados unos cientos de líneas, la mayoría de las personas solo buscan problemas obvios en lugar de analizar cada parte en detalle, por más que digan lo contrario. Limite cada sesión a un módulo y cada solicitud de integración a los cambios de un solo módulo. El cuello de botella no es el rendimiento del modelo; es la atención del revisor.

    Exigir una justificación para cada eliminación

    Antes de aceptar el resultado, pida al asistente una lista de todo lo que eliminó y, para cada elemento, por qué concluyó que la eliminación era segura. Forzar al asistente a expresar su razonamiento por escrito es uno de los pasos más valiosos que puede tomar. Cualquier justificación del tipo “esto parecía redundante” debe considerarse como una señal para una investigación manual.

    Proporcionar al modelo el historial

    Pida al asistente que utilice la orden git blame para cada línea que proponga eliminar y que informe el mensaje del commit correspondiente. Con frecuencia, la razón de una rama inusual está registrada allí, y el modelo aprovecha bien ese contexto cuando lo tiene a su disposición; simplemente no buscará por sí mismo sin que se le indique.

    Escribir los comentarios “por qué” antes de refactorizar

    Agregue primero los comentarios explicativos, y no después. Un comentario preciso que mencionara al socio, las fracciones de siete dígitos y el límite del analizador de seis dígitos probablemente habría evitado esta eliminación. Una nota vaga sobre las marcas de tiempo del socio no sirvió de nada. Si quien lea el código a continuación, ya sea una persona o una herramienta, no puede determinar por qué existe una línea, esa línea está a un paso de desaparecer.

    Compare lo antiguo y lo nuevo con el tráfico real

    Pase los datos reales de un día de producción por tanto la implementación antigua como la nueva y compare los resultados obtenidos. Para transformaciones puras como el análisis y la normalización esto es sencillo, y permite detectar con precisión el tipo de problemas que se pasan por alto en los casos de prueba fijos, ya que el tráfico real contiene entradas que nadie se molestó en anotar. Si no es posible reproducir el tráfico, tomar muestras de cargas reales para usarlas en casos de prueba de regresión es la mejor alternativa.

    Puntos clave

    La refactorización aún valió la pena hacerla: el módulo está mejor, el trabajo se completó en una tarde en lugar de en una semana, y una sola regresión entre cuatro mil líneas modificadas es algo insignificante en comparación con muchas refactorizaciones manuales a gran escala. Calificar el resultado como “el error de la IA” no capta la esencia del asunto. La IA llegó a una conclusión razonable basada en lo que podía ver, un caso especial sin explicación alguna, y el revisor humano llegó a la misma conclusión a partir del mismo artefacto incompleto.

    • El repositorio es solo una parte del sistema; las razones detrás de un código extraño se encuentran en los tickets, los mensajes de commit, los análisis posteriores y la memoria de las personas.
    • Una herramienta que solo ve el código eliminará con confianza cualquier elemento cuya justificación nunca se haya anotado.
    • Las pruebas positivas y una cobertura total de líneas no demuestran que el comportamiento se haya mantenido para entradas que nadie modeló.
  • Clasificar las diferencias generadas por la IA según el nivel de riesgo, exigir justificaciones escritas para las eliminaciones y proporcionar al modelo el historial correspondiente.
  • Escribir por qué existe código inusual ya era una buena práctica mucho antes del refactoring con IA; ahora es la medida de protección más económica que se tiene.
  • Lecturas relacionadas