Anti-Cliché: tardar más corrigiendo que escribiendo

Los modelos de Anthropic, Cursor y el resto ya son parte del trabajo. También repiten arquitectura, estructura y sobre todo UI. Investigué, iteré un rato, y armé Anti-Cliché: no es la mejor skill del planeta, pero crece cada vez que aparece un tic nuevo.

Pedí un formulario. El agente me lo armó en segundos.

tsx
function preventDefault(event: { preventDefault: () => void }) {
  event.preventDefault();
}

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  preventDefault(event);
  createUser(formData);
}

const form = (
  <form onSubmit={handleSubmit}>
    <input name="email" type="email" required />
    <button type="submit">Crear cuenta</button>
  </form>
);

return <section>{form}</section>;

Dos tics en el mismo dump. Una función preventDefault de una línea. Y el form metido en un const, como si el JSX fuera un valor y no un componente.

No era un bug: compilaba, el tipo cerraba y el nombre sonaba a arquitectura. El problema era más tonto. El helper no hacía nada que no hiciera ya el navegador si escribías la llamada en el handler, una vez, sin extraerla. Y el form ya era UI: tenía markup, un rol, iba a crecer. Eso es un function, no un const.

No era que tuviera que “revisar agentes”. Era que tardaba más corrigiendo el dump que si hubiera escrito el formulario yo.

Ahí dejé de tratar el output como un error suelto. Empecé a tratarlo como un dialecto. El rechazo no vive en un comentario de PR: vive en Anti-Cliché. Esta nota es el porqué. El texto que el agente tiene que cargar es la skill.

El cansancio no era de velocidad

El agente escribía en segundos. Yo deshacía el helper, sacaba el form del const, y cuando el PR quedaba listo había tardado más que si lo hubiera implementado yo. Hoy trabajo con agentes todos los días. No es un experimento: es cómo sale el código. Claude, Cursor, los modelos de Anthropic —da lo mismo la marca— ya son parte del trabajo. El problema no es que existan. El problema es el promedio que devuelven cuando nadie les pone un borde, y el rato que te cuesta deshacerlo.

Ese promedio no es solo una función absurda. Aparece en tres capas a la vez. Arquitectura: un UserService para un fetch. Estructura: un <Box> que solo pone flex, un barrel index.ts, "use client" por costumbre. Y sobre todo UI: el gradiente violeta, Inter, tres cards con icono, glassmorphism, el botón con sparkles. Lo ves en un screenshot y ya sabes qué modelo lo armó.

Eso ya lo cubrí en otra dirección con Knip: el grafo del proyecto te dice qué dejó de usarse. El problema anterior a Knip es otro. El código entra. Pasa TypeScript. Pasa el linter. Y sigue siendo el default de 2024 copiado a un repo de 2026.

Una función preventDefault extraída al lado del handler. Un const form = (<form />) donde debería haber un componente. Un useMemo alrededor de un string. Un try/catch que hace console.error y devuelve null. Un const STYLES = { wrapper: 'flex flex-col gap-4' } para no escribir la clase en el JSX. Nada de eso rompe el build. Todo eso es el dialecto.

Me cansé de explicar lo mismo en cada review. “Esto no se extrae.” “Esto ya lo hace el formulario.” “Esto no necesita un wrapper.” “Esto parece una landing de Cursor.” El modelo no se ofende. El modelo tampoco recuerda el review anterior, salvo que se lo dejes escrito en un sitio que vuelva a leer.

Qué es un cliché de IA (y qué no)

Un cliché no es “código que no me gusta”. Es un patrón que el modelo elige porque apareció mil veces en el entrenamiento, no porque resuelva el problema de este archivo.

La función de preventDefault es el ejemplo más claro que tengo. El tutorial de React de 2018 dice: en onSubmit, llama e.preventDefault() para que el navegador no recargue. El modelo aprendió eso. Después aprendió “extrae lo repetido”. Juntó las dos lecciones y fabricó una abstracción de una línea. El resultado parece cuidado. En la práctica no aporta nada que no estuviera ya en el handler.

El principio de Anti-Cliché cabe en una frase: todo patrón prohibido sustituye forma por contenido, o complejidad por valor. El arreglo no es “escribe más limpio”. Es ser específico, respetar lo que ya existe en el repo, y usar el default de la plataforma (React 19, App Router, Tailwind en el markup).

Lo mismo pasa en UI: gradiente violeta, blob 3D, botón “Generate” con sparkles, text-gray-400 sobre blanco porque “se ve limpio”. Y en prosa: *delve*, *tapestry*, *landscape*, *pivotal*, la mandíbula que se tensa, la luz que se derrama. Distintos dominios, mismo mecanismo.

No estoy peleando con React. Estoy peleando con el reflejo de envolver un llamado nativo y ponerle un nombre de sistema.

El caso preventDefault, sin teatro

El tipo genérico { preventDefault: () => void } es el sello de la función: el modelo no sabe si es un submit, un click o un drag. Envolvió el método y se fue. El const form es el sello del markup: el linter lo ve como un valor; yo lo veo como un componente que no se atrevió a nombrarse. En la skill eso cae junto: helper que no nombra un dominio, inflación de tipos para un objeto que se usa una vez, y JSX asignado a una variable.

En un formulario de React 19 el camino es otro. El form es un componente. El action corre en una transición. No hace falta cancelar el submit nativo a mano. El navegador ya no es el enemigo:

tsx
export function SignupForm() {
  return (
    <form action={createUser}>
      <input name="email" type="email" required />
      <button type="submit">Crear cuenta</button>
    </form>
  );
}

Si todavía estás en onSubmit, la llamada va en el handler. Una vez. Sin helper. Sin HOC. Sin withPreventDefault(onSubmit). Y el markup vive en el return de una función, no en un const.

tsx
function SignupForm() {
  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    createUser(new FormData(event.currentTarget));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <button type="submit">Crear cuenta</button>
    </form>
  );
}

El intent queda en el componente: este submit no recarga, este submit crea un usuario. El form tiene nombre en el árbol.

Constantes para todo, incluido el JSX

El const form no vino solo. El modelo extrae para sentirse organizado, no para nombrar un concepto. El mismo reflejo, un paso más atrás, es el archivo de constantes.

Empieza inocente: CARD_PADDING = 'p-6'. Sigue con un objeto STYLES que es un stylesheet paralelo. wrapper, header, body, footer — cada key es un string de Tailwind que ya se describe solo. Ahora, para saber cómo se ve el componente, tienes que saltar a otro archivo. Tailwind IntelliSense no sigue la key. Y a la tercera semana ya no recuerdas qué es STYLES.hint.

ts
const STYLES = {
  wrapper: 'flex flex-col gap-4 rounded-lg border p-6',
  title: 'text-lg font-semibold',
  hint: 'text-sm text-muted-foreground',
};

<div className={STYLES.wrapper}>
  <h2 className={STYLES.title}>{title}</h2>
  <p className={STYLES.hint}>{hint}</p>
</div>

Eso no añade significado. Renombra. Una clase que no cambia se escribe en el JSX. cn() existe para lo condicional, no para concatenar dos strings estáticos. CVA existe para variantes reales, no para esconder p-6.

Si se comporta como un componente —tiene markup, un rol, va a crecer o a repetirse— se declara como componente. Con function. No con const Icon = () =>. No con un JSX guardado en una variable que después se interpola.

El criterio es corto. String de clases que no varía: inline. Número o label de dominio que se usa en tres sitios: constante con nombre de dominio, no helpers.ts. Pedazo de UI con cara y nombre: function. Si lo único que hace la extracción es alargar el scroll y ponerle un alias, no extraigas. Esa regla está en Anti-Cliché porque ese día llegaron juntas —el helper y el const form— y después las volví a ver separadas. El modelo cree que const es higiene. En el árbol, es un componente a medias.

Lo que la skill hace de verdad

preventDefault fue el que me hizo escribirla. Anti-Cliché no es esa anécdota. Es un protocolo que el agente corre *antes* de escribir, más un set de referencias por dominio: arquitectura, componentes, server, Tailwind, UI, prosa.

Primero fui a buscar si esto tenía nombre. En Reddit —sobre todo hilos de r/ClaudeAI y gente que trabaja con Cursor— el tell más repetido era visual: gradiente purple-to-blue, Inter en todo, hero centrado con tres cards, rounded-2xl y glass en cada superficie. Si el screenshot se ve así, nadie eligió el diseño. El modelo eligió el promedio de Tailwind + shadcn en el corpus.

También encontré listas más formales. Me sirvieron. No me alcanzaban. Yo no solo veía landings idénticas. Veía const form = ( al lado de una función preventDefault. El cliché de UI es el que se fotografía. El de arquitectura se mergea y se queda.

Iteré un rato. Rules del editor, después un documento más largo, después recortes porque las rules kilométricas se diluyen. Terminé en una skill. No es, ni de cerca, la mejor skill del planeta. En mis proyectos hace el trabajo: el primer draft llega sin el helper vacío y sin el hero violeta.

Eso no la deja cerrada. Cada tic nuevo en un diff entra. Si el promedio se mueve del violeta al cream + serif, la skill también se mueve. El criterio no cambia: si nadie lo eligió, no entra.

La skill no dice “escribe bonito”. Dice la pregunta. ¿Esto ya existe en el repo? ¿Se usa más de una vez? ¿Esta constante añade significado o solo nombra un string? ¿"use client" está por un API del browser o por costumbre? El resto —React 19, barrels, Result, sparkles, *delve*— está en el archivo que el agente carga. Esta nota no sustituye ese texto.

Ninguna de esas reglas es original como idea. El valor está en tenerlas juntas, antes de tocar el repo, no en un comentario de PR que ya llegó tarde.

Convertirlo en un gate

Una skill que solo se lee cuando alguien se acuerda no sirve. El mismo criterio que Git Auditor y Knip: si te importa, que corra sin que lo pidas.

El canónico está publicado. Se instala así:

bash
npx skills add enderpuentes/ai-agent-skills --skill anti-cliche-agent

El puntero en este sitio es el resource Anti-Cliché. Esta nota no sustituye ese texto. Si hay conflicto entre el artículo y la skill, gana la skill.

En la práctica, en mis repos, Anti-Cliché queda en el proyecto (globs de ts/tsx/md), el agente la carga en tareas de UI y de review, y el review humano deja de repetir “esto es un wrapper vacío”. No reemplaza a TypeScript. No reemplaza a Knip. Corta el dialecto antes de que entre al diff.

El loop queda así: Grill.me fuerza el contexto. El agente escribe. Anti-Cliché nombra el tic. Knip y los hooks miran lo que quedó. Lo mismo que ya conté con skills autohosteadas: una skill es un documento que el agente elige cuando el trabajo coincide. El frontmatter lo dice: *when in doubt, use this skill*. Ninguna de esas piezas es un chatbot con sparkles. Son verificadores.

Lo que no hace

No formatea. No discute tabs. No te obliga a un framework. Si el repo ya usa un patrón, la skill dice respetarlo — inventar un paralelo “más limpio” también es cliché.

Tampoco es un detector automático en CI. Es instrucción para el modelo, no un AST. Si quieres un failing build, eso es ESLint o un test. Yo quería otra cosa: que el primer draft no llegue con un preventDefault extraído y un form metido en un const.

Cierre

El código absurdo de la IA no es aleatorio. Es el promedio de mil tutorials —y de lo que Claude y Cursor van a volver a generar mañana. Extraer preventDefault es ese promedio con tipos. El hero violeta es el mismo promedio con CSS. Anti-Cliché existe porque me cansé de devolver ese promedio a mano, y porque el catálogo no se termina: cada tic nuevo entra.

Si el agente va a escribir más rápido que tú, alguien tiene que nombrarle los tics. Yo lo dejé por escrito: esta nota para el porqué, la skill para el protocolo. Carga la segunda. Cuando aparezca el siguiente cliché, también entra ahí.

Más notas sobre Ingeniería de IA.