Evitación de la refactorización.
Los asistentes de IA casi nunca reestructuran el código existente: le añaden encima nuevas ramas, parámetros bandera y capas envoltorio, de modo que funciones y condicionales no paran de crecer mientras la refactorización —el retrabajo que mantiene sana una base de código— desaparece sin hacer ruido.
##Signos y síntomas
El revisor ve un diff compuesto casi por completo de adiciones a código que ya existe. El modelo resolvió el prompt apilando una rama, una bandera o una capa más sobre la función que se le indicó, en lugar de remodelar esa función para que el cambio encaje. Señales reveladoras:
- Diffs de solo adiciones: ninguna línea movida, renombrada ni borrada; la función tocada solo se alarga.
- Parámetros bandera:
isWholesale,skipValidation,legacy = falseañadidos a una firma existente, cada uno cambiando el comportamiento en lo profundo del cuerpo. - Proliferación de ramas: cada requisito nuevo se convierte en otro
else ifocaseen la misma función en lugar de una tabla de búsqueda, una estrategia o una llamada polimórfica. - Un envoltorio encima:
processOrderV2llama aprocessOrdery parchea su resultado en vez de cambiarlo; el camino viejo y el nuevo conviven. - Métricas que suben en un mismo sitio: la complejidad cognitiva, la longitud y el número de parámetros de las mismas funciones aumentan commit tras commit: un punto caliente que crece.
- Banderas muertas que nunca se eliminan: parámetros que todos los llamadores pasan ya del mismo modo, conservados "por si acaso".
// Cada petición añadió un parámetro y una rama; nunca se reestructuró nada.
function priceOrder(
items: Item[],
discount?: number,
isWholesale = false,
applyVat = true,
couponCode?: string,
) {
let total = items.reduce((s, i) => s + i.price * i.qty, 0);
if (discount) total = total * (1 - discount);
if (isWholesale) {
if (total > 1000) total = total * 0.9;
else if (total > 500) total = total * 0.95;
}
if (couponCode === "SPRING") total = total - 10;
else if (couponCode === "VIP") total = total * 0.85;
if (applyVat) total = total * 1.2;
return total;
}
Cada petición añadió un parámetro y una rama, y cada diff tocó solo el final de la función. No hay nada duplicado, así que un detector de clones no dice nada; el olor se manifiesta como crecimiento de la complejidad y del tamaño.
##Causas del problema
Por qué lo producen los modelos
- Sesgo aditivo de la predicción del siguiente token. Un LLM completa el prompt que tiene delante. Añadir una rama o un parámetro en el punto del cambio es la continuación localmente más probable y menos arriesgada; reestructurar la función exige mantener en el contexto de trabajo todo el módulo y sus llamadores y predecir una edición globalmente coherente, algo más difícil y que el prompt inmediato no recompensa.
- Contexto escaso del repositorio. Los asistentes de código rara vez cargan todos los llamadores y tests. Sin ver quién depende de una función, el modelo no puede saber si reestructurarla es seguro, así que deja la estructura como está y añade un
ifdonde puede ver. OX Security describe la IA como "un ejército de desarrolladores júnior con talento pero sin experiencia": productivos a nivel local, sin atender a la arquitectura. - Refactorizar es arriesgado y no se recompensa. Reestructurar toca código que no se pidió cambiar y puede romper llamadores y tests. Un asistente complaciente, de "tú haz que funcione", minimiza el radio de impacto sin tocar el código que funciona: justo el comportamiento que OX midió como "evitación de refactorizaciones" (Avoidance of Refactors) en el 80–90% del código generado por IA, que "destaca generando código nuevo, pero nunca vuelve atrás para mejorar la arquitectura".
- Generar es barato; borrar da miedo. Producir 40 líneas nuevas no le cuesta nada al modelo; convencerlo de borrar y remodelar 40 líneas existentes va contra su instinto de conservar.
Por qué perjudica
- La refactorización desaparece del historial. El análisis de GitClear de 2025 sobre 211 millones de líneas modificadas halló que las líneas asociadas a refactorización ("movidas") cayeron del 25% de las líneas cambiadas en 2021 a menos del 10% en 2024, mientras que las copiadas y pegadas subieron del 8,3% al 12,3% y en 2024 superaron por primera vez a las movidas.
- La complejidad se acumula. Cada bandera añadida duplica las combinaciones que la función debe manejar y que los tests deben cubrir; cada
else ifextra hace más arriesgado el siguiente cambio, así que el siguiente prompt también añade en vez de reestructurar. - Acumulación de deuda técnica. La refactorización es el retrabajo que mantiene baja la entropía; si se suprime, la deuda se crea pero nunca se paga. La base de código crece más rápido de lo que mejora.
- Carga de revisión. Los revisores reciben diffs que solo añaden líneas a funciones ya largas; juzgar cómo interactúa una rama nueva con la docena que tiene encima es más difícil que revisar una función remodelada para absorber el cambio.
##Tratamiento
Tácticas de prompting / revisión
- Primero haz fácil el cambio. El consejo de Kent Beck —"para cada cambio deseado, haz que el cambio sea fácil (advertencia: esto puede ser difícil) y luego haz el cambio fácil"— se traduce directamente en un prompt: "Antes de añadir la regla del cupón, reestructura
priceOrderpara que la nueva regla sea una adición de una línea; mantén idéntico el comportamiento y ejecuta los tests. Después añade la funcionalidad". - Prohíbe nuevos parámetros bandera. "No añadas parámetros booleanos ni de modo a funciones existentes; si el comportamiento debe variar, propone primero una reestructuración".
- Pon presupuesto a la complejidad, no solo a la corrección. Exige que el asistente ejecute
complexity,max-paramsymax-lines-per-functionde ESLint y la regla de complejidad cognitiva de Sonar, y que informe de los valores antes y después para cada función que tocó; cualquier aumento necesita justificación. - Pide la refactorización como paso aparte. Generar y reestructurar son tareas distintas: primero "haz que funcione", luego un cambio aparte que conserve el comportamiento —"ahora reestructura esta función para que las ramas se conviertan en datos o en funciones separadas"—, con los tests en verde antes y después.
- Vigila los puntos calientes. Sigue la frecuencia de cambios × complejidad (Code Maat o el historial de git sin más) de los archivos que crecen en cada commit escrito por IA y planifica su reestructuración antes de que llegue ahí la siguiente funcionalidad.
La refactorización en sí: Descomponer Condicional y Extraer método dan nombre a cada rama; Reemplazar Parámetro con Métodos Explícitos o Introducir Objeto Parámetro sustituyen los parámetros bandera; y Reemplazar condicional con polimorfismo (o una tabla de búsqueda) se encarga de las ramas que eligen comportamiento según un tipo o un código.
// Después: cada regla es una función pequeña; priceOrder solo las compone.
type PriceRule = (total: number) => number;
const subtotalOf = (items: Item[]) =>
items.reduce((s, i) => s + i.price * i.qty, 0);
const VAT_RATE = 1.2;
const discount = (rate: number): PriceRule => (t) => t * (1 - rate);
const wholesale: PriceRule = (t) => (t > 1000 ? t * 0.9 : t > 500 ? t * 0.95 : t);
const COUPONS: Record<string, PriceRule> = {
SPRING: (t) => t - 10,
VIP: (t) => t * 0.85,
};
const vat: PriceRule = (t) => t * VAT_RATE;
function priceOrder(items: Item[], rules: PriceRule[]): number {
return rules.reduce((total, rule) => rule(total), subtotalOf(items));
}
// Quien llama indica qué reglas se aplican y en qué orden:
// priceOrder(items, [discount(0.1), wholesale, COUPONS.SPRING, vat]);
Una regla de precios nueva es ahora una entrada más, no un parámetro y una rama más, y cada regla puede probarse por separado.
No hay que confundirlo con Duplicación en lugar de extracción: allí el modelo copia lógica existente en un bloque nuevo casi idéntico, y los detectores de clones lo encuentran. Aquí no se copia nada: el modelo sigue haciendo crecer la misma función con ramas, banderas y capas, y lo revelan las métricas de complejidad y tamaño.
##Detectado por
- SonarQube / SonarSource S3776 — La complejidad cognitiva de las funciones no debe ser demasiado alta — señala funciones que siguen absorbiendo ramas y banderas nuevas
- ESLint complexity — Límite de complejidad ciclomática por función (opción max)
- ESLint max-lines-per-function — Límite de longitud de función — detecta funciones que solo crecen
- ESLint max-params — Límite de número de parámetros — detecta banderas y opciones añadidas a una firma existente
- ESLint max-depth — Límite de profundidad de anidamiento — detecta escaleras if/else apiladas dentro de ramas antiguas
- Code Maat entity-churn — Frecuencia de cambios por archivo según el historial de git — combinada con la complejidad, revela puntos calientes que crecen commit tras commit