Logotipo de ConstructiCat
CodeBust.
Explorar la sección ▾

Duplicación en lugar de extracción.

Al escribir código nuevo, los asistentes de IA tienden a pegar o volver a derivar una copia ligeramente modificada de una lógica que ya existe en lugar de llamar a una función compartida o extraerla, de modo que el código duplicado crece con cada nueva funcionalidad.

##Signos y síntomas

Un revisor reconoce este olor cuando un diff escrito por IA añade un bloque que ya existe en algún lugar del repositorio, ligeramente renombrado, en lugar de llamar a la función que ya está ahí. La forma reveladora son dos o más cuerpos casi idénticos que difieren solo en un sustantivo (user/order), un endpoint o una constante: el tipo de copia que un humano habría factorizado.

Señales comunes:

  • Nuevos manejadores/servicios que repiten textualmente la misma validación, reintento, mapeo de errores o código repetitivo de fetch.
  • Cadenas mágicas y literales de configuración reescritos en cada bloque nuevo en lugar de referenciarlos desde un solo lugar.
  • Cada turno del agente reimplementa lógica que el turno anterior ya escribió (el agente no recuerda que existe un ayudante).
  • Un detector de clones (jscpd, PMD CPD, las métricas de duplicación de Sonar) informa de nuevos bloques duplicados —o de un salto en duplicated_lines_density— en el PR que añadió la funcionalidad.
// La IA añadió esto para la nueva ruta — pero getUser ya existe 30 líneas más arriba
async function getOrder(id: string) {
  const res = await fetch(`${API}/orders/${id}`, { headers: authHeaders() });
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  const json = await res.json();
  if (!json?.data) throw new Error("Malformed response");
  return json.data;
}

async function getUser(id: string) {                 // <-- cuerpo 95 % idéntico
  const res = await fetch(`${API}/users/${id}`, { headers: authHeaders() });
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  const json = await res.json();
  if (!json?.data) throw new Error("Malformed response");
  return json.data;
}

Es el clásico olor de Código Duplicado, pero producido sistemáticamente en lugar de por accidente.

Relacionado, pero distinto: Evitación de la refactorización trata de no reestructurar nunca el código que ya existe: el comportamiento nuevo se atornilla encima en lugar de remodelar el diseño. Este olor es más acotado y surge al crear el código: el modelo vuelve a derivar una lógica que ya tiene su sitio en lugar de llamarla o extraerla.

##Causas del problema

Por qué los modelos lo producen

  • Localidad del siguiente token. Un LLM completa el fragmento actual a partir del texto más similar en su ventana de contexto. Reproducir un bloque cercano y conocido como bueno es la continuación de mayor probabilidad; "detente, ve a definir un ayudante y luego llámalo" es un desvío más largo y de menor probabilidad que abarca varios archivos.
  • Sin modelo de todo el repositorio. El asistente rara vez sabe que ya existe un ayudante adecuado fuera de su contexto. Sin recuperación no puede reutilizar lo que no ve, así que lo regenera. Eno Reyes (Factory) vincula el beneficio de la IA con el propio código base —"cuanto mayor era la calidad del código base, más aceleraba la IA a la organización" (entrevista en el blog de Stack Overflow)—, y los clones que ya hay en el repositorio se convierten en el patrón que la siguiente generación vuelve a copiar.
  • Amnesia por turno + alcance del prompt. Cada petición se trata como una unidad autónoma ("haz que esto funcione"), así que el agente sobreajusta una solución estrecha y de un solo uso en lugar de generalizar. OX Security lo llama sobreespecificación (Over-Specification) —"soluciones hiperespecíficas de un solo uso que no se pueden adaptar a otros fines"— y la encontró en el 80–90 % del código de IA que analizó (informe de OX).
  • Adulación / mínima fricción. Los modelos optimizan por una salida que satisfaga visiblemente la petición sin tocar archivos no relacionados. Copiar un bloque no sale del archivo que se está editando; llamar a un ayudante compartido —o extraerlo— obliga a meterse en otro módulo que el modelo no ha visto del todo, así que gana la copia local.
  • Las herramientas lo abaratan. Como señala el resumen de DevClass del informe de GitClear, aceptar una sugerencia con la tecla Tab hace que insertar un bloque nuevo sea casi gratis, eliminando la fricción que antes empujaba a los desarrolladores hacia la reutilización (DevClass).

Por qué resulta perjudicial

  • Erosión de calidad medida. El análisis de GitClear de 2025 sobre 211 millones de líneas modificadas (2020–2024) concluyó que las líneas asociadas a la refactorización (líneas «movidas») cayeron del 25 % de las líneas modificadas en 2021 a menos del 10 % en 2024, mientras que las líneas copiadas/pegadas subieron del 8,3 % al 12,3 % en el mismo periodo; 2024 fue el primer año en que las líneas copiadas/pegadas superaron a las movidas, y los bloques con cinco o más líneas duplicadas se multiplicaron aproximadamente por ocho durante 2024 (GitClear 2025).
  • Deuda de corrección: "déjà-vu de errores". OX Security lo encontró en el 70–80 % del código de IA: un error corregido en un clon sobrevive en silencio en todos los demás, así que defectos idénticos reaparecen y cada uno necesita una corrección redundante. La duplicación convierte un error en N errores.
  • Mantenibilidad y carga de revisión. Cada cambio de comportamiento debe encontrarse y editarse en muchos lugares; los diffs se hinchan con bloques casi idénticos que los revisores deben comparar línea a línea para confirmar que son realmente idénticos (y no sutil y peligrosamente distintos).
  • Superficie de seguridad. Un patrón vulnerable (falta de comprobación de autenticación, entrada sin escapar) se propaga a cada copia, y una corrección de endurecimiento posterior puede pasar por alto algún clon: la duplicación multiplica la superficie del parche.

##Tratamiento

Tácticas de revisión y de prompting

  • Dile al modelo dónde mirar primero: "Antes de escribir código nuevo, busca en el repositorio un ayudante existente que haga esto y reutilízalo; no dupliques." Pega el módulo relevante para que esté en el contexto.
  • Exige la consolidación de forma explícita: "Si dos bloques difieren solo en los parámetros, extrae una función parametrizada y llámala desde ambos." Es decir, Extraer método seguido de Parametrizar método.
  • Hazlo ejecutar las herramientas: "Ejecuta el linter / jscpd / la comprobación de duplicación y resuelve cualquier clon que informe antes de devolver el diff." Los agentes se autocorrigen bien cuando se les da una señal determinista.
  • En la revisión, trata un bloque con aspecto de pegado como una invitación a hacer grep: busca una línea distintiva; si ya existe, devuélvelo para su extracción.

La refactorización — Extraer método + Parametrizar método

// después: una única fuente de verdad, llamada desde ambos
async function getResource<T>(path: string): Promise<T> {
  const res = await fetch(`${API}/${path}`, { headers: authHeaders() });
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  const json = await res.json();
  if (!json?.data) throw new Error("Malformed response");
  return json.data as T;
}

const getOrder = (id: string) => getResource<Order>(`orders/${id}`);
const getUser  = (id: string) => getResource<User>(`users/${id}`);

Ahora una corrección al manejo de errores o al parseo de respuestas se aplica una sola vez. Donde los duplicados difieren en un paso en lugar de en un valor, prefiere Formar Método Plantilla o pasa el paso variable como callback. Los movimientos canónicos aquí son Extraer método, Extraer Variable o Reemplazar número mágico por constante simbólica (para los literales reescritos) y Subir Método cuando los clones viven en clases hermanas: todas curas estándar para el Código Duplicado.

Barrera de protección, no solo limpieza: conecta un detector de clones a CI con un umbral bajo para que el siguiente duplicado generado por IA rompa la compilación en lugar de fusionarse. Combínalo con una regla de la casa de una línea en CLAUDE.md/AGENTS.md ("reutiliza los ayudantes existentes; nunca pegues un casi-duplicado") para que la restricción viaje en el contexto del modelo en cada turno.

##Detectado por

  • jscpd --min-tokens / --min-lines / --threshold — Duplicación por copiar y pegar (umbrales min-tokens / min-lines configurables; admite la comprobación --threshold en CI)
  • PMD CPD — CPD — detector de duplicaciones basado en tokens para más de 30 lenguajes
  • SonarQube / SonarSource S4144 — Los métodos no deberían tener implementaciones idénticas
  • SonarQube / SonarSource S1192 — Los literales de cadena no deben duplicarse
  • SonarQube duplicated_lines_density — Bloques duplicados / métrica duplicated_lines_density (motor integrado de detección de duplicaciones; falla el control de calidad al superar el umbral)
  • eslint-plugin-sonarjs sonarjs/no-identical-functions
  • eslint-plugin-sonarjs sonarjs/no-duplicate-string