


Hacer que la IA escriba código de sistemas con cuidado, hasta los tipos y el manejo de errores
Para cuando una IA ayuda en un código de sistemas exigente. Revisa su código al menos el triple de lo que tardó en escribirlo, cubre todos los errores, lleva las reglas a los tipos, mantiene compatibles los formatos viejos y nuevos y hace commits pequeños que compilan.
Para usar estas reglas de IA
## Revisar el código escrito por la IA al menos el triple del tiempo que llevó escribirlo
Trata al asistente como un multiplicador de calidad, no como un multiplicador de velocidad. Invierte el tiempo que te ahorra en un rigor superior al que alcanzaría el trabajo sin asistencia.
- Apunta a la excelencia: cubre más casos límite, refactoriza para que se entienda, resuelve los TODO pendientes, haz las tareas tediosas y ten como objetivo cero fallos.
- Revisa durante mucho más tiempo del que dedicas a generar. Dedica al menos el triple de tiempo a revisar el código generado del que costó producirlo, sopesando cada línea y cada decisión de diseño, y buscando activamente maneras de romperlo.
- El autor es el responsable del resultado. No entregues un primer borrador como trabajo terminado, salvo que estés pidiendo explícitamente solo una opinión sobre una dirección.
- El trabajo que muestre señales de haberse producido sin cuidado, como casos omitidos que una revisión humana atenta habría detectado, puede rechazarse de plano en lugar de revisarse.
- Declara el estándar al comienzo de cualquier conversación que implique cambios de código, y de nuevo antes de abrir un pull request, para que autor y revisor compartan el mismo listón.
## Llevar la corrección a los tipos y cubrir todos los casos límite
Prefiere la implementación correcta a la cómoda, y traslada al sistema de tipos toda la corrección posible, de modo que los errores fallen al compilar en vez de fallar en producción.
- Modela todo el espacio de errores. Sin atajos y sin manejo de errores simplificado.
- Gestiona todos los casos límite, incluidas las condiciones de carrera, la temporización de las señales y las diferencias de comportamiento entre sistemas operativos.
- Codifica las restricciones de corrección en los tipos: tipos envoltorio para los valores de dominio, patrones builder para la construcción compleja, type states en los genéricos cuando importen las transiciones de estado, y préstamo en lugar de clonado.
- Prefiere las garantías en tiempo de compilación a las comprobaciones en tiempo de ejecución siempre que exista la opción.
- Mantén la visibilidad lo más restringida posible. Amplía la de un elemento solo cuando algo fuera de su módulo lo necesite de verdad.
- Marca como extensibles los tipos públicos de los paquetes estables y consumidos desde fuera, por compatibilidad futura. Los paquetes internos que no prometen estabilidad no lo necesitan, aunque a los tipos de error puede seguir conviniéndoles.
- Usa paso de mensajes o el modelo de actores en lugar de estado mutable compartido, para que no puedan surgir carreras de datos.
- Acertar en los detalles importa más que terminar rápido.
## Dar mensajes de error útiles y el mismo comportamiento en cada sistema operativo
El comportamiento visible para el usuario es una entrada de diseño, no un retoque final.
- Produce diagnósticos estructurados y útiles en lugar de simples cadenas de error, y usa una biblioteca de diagnósticos enriquecidos allí donde el mensaje llegue a una persona.
- Haz que el informe de progreso sea reactivo e informativo.
- Mantén un comportamiento coherente entre plataformas incluso donde difieran las capacidades subyacentes, e implementa cada plataforma con sus propios mecanismos nativos en lugar de emular una plataforma sobre otra.
- Escribe los mensajes dirigidos al usuario en un presente claro: «la herramienta ahora admite», no «la herramienta ahora admitía».
- Centraliza la pluralización en un módulo auxiliar compartido cuyas funciones devuelvan la forma singular o plural para un recuento. Añade allí los nuevos auxiliares en lugar de incrustar la lógica de pluralización en los puntos de llamada.
## Hacer crecer el diseño paso a paso y anotar por qué se eligió
Mantén el diseño específico y componible en lugar de abstracto y demasiado genérico.
- Haz evolucionar el diseño de forma incremental en lugar de intentar una arquitectura perfecta desde el principio.
- Registra las decisiones de diseño y sus compensaciones en documentos de diseño guardados junto al proyecto, para que quien lea después pueda ver por qué se eligió una forma.
- Ante la incertidumbre, explora e itera en lugar de congelar una suposición en una abstracción.
## Separar los errores del usuario de los fallos internos y adjuntar contexto detallado
Los errores forman parte de la superficie que ve el usuario y merecen la misma atención de diseño que el camino de éxito.
- Usa tipos de error basados en derive en lugar de errores tipados como cadenas, y agrupa los errores relacionados bajo una enumeración de categoría cuando el conjunto crezca.
- Lleva en los errores un contexto rico y estructurado en lugar de aplanar el detalle en un único mensaje.
- Usa un modelo de dos niveles. Los errores causados por el usuario y los externos forman un tipo distinto que se asigna a códigos de salida semánticos; los errores internos de programación son aparte y pueden provocar un panic.
- Escribe el texto de visualización del error como un fragmento de frase en minúsculas que se lea correctamente al incrustarse en una frase envolvente como «no se pudo ...».
## Avisar de claves desconocidas en la configuración y dar errores de configuración claros
Las decisiones de deserialización determinan la calidad de todos los errores de configuración que un usuario llegará a ver.
- Registra y reporta las rutas ignoradas o desconocidas de la configuración, para que una errata aparezca como advertencia en vez de no hacer nada en silencio.
- Nunca aplanes estructuras anidadas durante la deserialización. El almacenamiento intermedio interno que necesita degrada las advertencias de campos desconocidos; en su lugar, copia los campos en la estructura de forma explícita.
- Nunca dependas de la deserialización de variantes sin etiqueta, que produce mensajes de error pobres. En su lugar, escribe un visitor personalizado con una cadena de expectativa precisa.
## Antes de cambiar un formato guardado, comprobar que versiones viejas y nuevas leen los datos de la otra
Todo cambio en una estructura que se escribe en disco o se envía por la red debe recorrerse a través de la matriz de compatibilidad completa antes de integrarse.
- Lector antiguo con datos nuevos: ¿puede seguir deserializando y pierde información?
- Lector nuevo con datos antiguos: ¿los valores por defecto declarados producen valores correctos, y no solo válidos?
- Escritor antiguo con datos nuevos: ¿puede completar el ciclo de ida y vuelta sin descartar campos? Es el caso que más a menudo se pasa por alto.
- Sube la versión del formato de forma proactiva. Si un campo nuevo va a ser semánticamente importante, eleva la versión cuando se añada el campo, no cuando se escriba por primera vez un valor distinto del predeterminado; de lo contrario, las compilaciones antiguas corromperán los datos en silencio al volver a escribirlos.
- Trata los valores por defecto como necesarios pero no suficientes. Permiten que un lector antiguo analice datos nuevos, pero un escritor antiguo sigue descartando los campos que desconoce.
## Usar async solo donde haga falta y no lanzar tareas sin límite
La asincronía es una herramienta para la entrada y salida y para la concurrencia, no un estilo por defecto.
- Usa un runtime asíncrono multihilo, pero confina lo asíncrono al código que lo necesita y mantén todo lo demás síncrono.
- Usa concurrencia estructurada para que al trabajo concurrente no haya que darle un tiempo de vida artificialmente largo.
- Planifica el trabajo en cola teniendo en cuenta la contrapresión en lugar de lanzar tareas sin límite.
- Proporciona primitivas de temporización pausables allí donde el control de trabajos pueda suspender y reanudar el proceso, de modo que el tiempo suspendido no se compute contra los tiempos de espera.
## No poner lógica en módulos agregadores y escribir las importaciones arriba
Los límites entre módulos se imponen mediante la estructura y la visibilidad, no solo por convención.
- Usa los archivos de módulo agregadores únicamente para reexportar elementos públicos. La lógica no trivial pertenece a un submódulo de implementación o a otro más específico.
- Mantén el código específico de plataforma en archivos separados por plataforma tras compilación condicional, con una interfaz compartida entre ellos.
- Mantén los auxiliares de prueba en módulos o archivos dedicados.
- Importa los tipos y las funciones justo al principio del módulo. Nunca importes dentro del cuerpo de una función; la única excepción es una función compilada condicionalmente que necesita una importación condicionada.
- Prefiere importar el tipo en sí, e importa un módulo en su lugar solo donde esa sea la forma convencional. Evita las rutas completamente cualificadas en las firmas: importa el módulo padre y cualifica solo con él.
- Importar variantes de enumeración para la coincidencia de patrones está bien.
## Prestar o compartir datos en lugar de copiarlos
Elige la representación que evita trabajo en lugar de optimizar a posteriori.
- Comparte los datos inmutables mediante conteo de referencias o préstamo en lugar de copiarlos.
- Usa un tipo optimizado para cadenas pequeñas allí donde el código maneje muchas cadenas cortas.
- Presta atención a la clonación y a las referencias. Evita clonar cuando los datos tienen una estructura natural de árbol o de propiedad que el préstamo ya expresa.
- Transmite los datos a través de iteradores siempre que sea posible en lugar de almacenarlos todos en memoria.
## Formatear antes de cada commit y gestionar las versiones de dependencias en un solo lugar
La coherencia mecánica es barata de imponer y cara de aplicar a posteriori.
- Comienza cada archivo fuente con la línea de copyright y el identificador de licencia del proyecto.
- Fija la edición del lenguaje y ejecuta el comando de formateo del proyecto antes de cada commit. El formateo se exige en la integración continua, así que un commit sin formatear falla allí.
- Suprime un lint con la forma de expectativa en lugar de la forma de permiso, para que la propia supresión avise cuando el lint deje de dispararse y no se acumulen supresiones muertas.
- Donde un lint prohíba añadir texto formateado a una cadena, usa la macro de escritura del proyecto en lugar de agregar una cadena formateada.
- Gestiona todas las versiones de las dependencias de forma centralizada en la raíz del workspace, y fija los paquetes internos a versiones exactas.
- Comenta las elecciones no obvias de dependencias y de características, especialmente allí donde una característica se desactiva por un motivo de seguridad o de tamaño.
## Escribir comentarios que expliquen por qué, no solo qué
- Los comentarios explican el porqué, no solo el qué.
- No narres dentro del cuerpo de las funciones. Añade un comentario solo donde algo sea no evidente o especial, o donde la intención necesite una explicación más profunda.
- La documentación a nivel de módulo expone el propósito y las responsabilidades del módulo.
- Termina siempre los comentarios de código con un punto.
- Nunca uses mayúscula inicial en cada palabra de los encabezados y títulos; usa siempre mayúscula solo al inicio de la frase.
- Usa siempre la coma de Oxford.
- No omitas los artículos. Escribe «el archivo tiene una versión más reciente», no «archivo tiene versión más reciente».
## Ejecutar las pruebas con el runner del proyecto y reutilizar los fixtures existentes
Prueba de la forma en que el proyecto ejecuta las pruebas, y reutiliza el modelo de pruebas que ya existe.
- Usa el runner de pruebas designado por el proyecto para las pruebas unitarias y de integración. No recurras al runner por defecto del lenguaje cuando la suite dependa del modelo de ejecución del runner designado.
- Usa una copia de la herramienta compilada localmente solo cuando verifiques cambios en el comportamiento de la propia herramienta, como un nuevo flag o formato de salida. Ejecuta la suite en sí con el runner estándar, ya que las pruebas de integración lanzan sus propios procesos internos a partir de los artefactos de compilación.
- Ejecuta las pruebas de documentación con el comando que las admite cuando el runner principal no lo hace.
- Mantén las pruebas unitarias junto al código que prueban, las pruebas de integración en un paquete dedicado, los fixtures compartidos en un paquete de fixtures y las utilidades de prueba en un paquete de soporte.
- Prefiere el modelo de resultados esperados del paquete de fixtures antes que comprobaciones puntuales escritas a mano.
- Recurre a las herramientas que ya existen: casos parametrizados, pruebas basadas en propiedades, aserciones de snapshot, harnesses personalizados y salida de aserciones mejorada. Comprueba qué hay disponible antes de añadir un mecanismo nuevo.
## Poner el ámbito al inicio del asunto del commit y que cada commit compile
El historial es un artefacto revisable, así que trata cada commit como una unidad de revisión.
- Antepón a cada asunto de commit, entre corchetes, el scope que toca, seguido de una descripción breve. Usa un scope meta para el trabajo transversal, como los mínimos del toolchain, las releases y los cambios de integración continua.
- Los commits de subida de versión y de preparación de release siguen sus propios asuntos fijos y normalmente los produce la herramienta de release.
- Escribe los cuerpos en pasado simple y en presente: qué ocurría antes, qué ocurre ahora y qué pruebas se añadieron.
- Los mensajes de commit son Markdown. No uses comillas invertidas en la línea de asunto, pero sí úsalas en el cuerpo.
- Haz los commits atómicos. Cada commit es una unidad lógica de cambio.
- Mantén el historial bisecable. Cada commit debe compilar y pasar todas las comprobaciones.
- Separa las responsabilidades. Los arreglos de formato y las refactorizaciones van en sus propios commits, aparte de los cambios de comportamiento.
## Dividir un runner concurrente en despachador y ejecutor que se comunican por mensajes
Sobre un runner concurrente es más fácil razonar cuando un componente es dueño del mundo exterior y otro es dueño de la ejecución.
- Un dispatcher interactúa con el mundo exterior, lineariza los eventos de todas las fuentes, multiplexa sobre esas fuentes con una construcción select y mantiene el estado autoritativo de lo que se está ejecutando en ese momento. Los puntos de sincronización mantienen el reporte libre de carreras.
- Un executor planifica y ejecuta unidades de trabajo como máquinas de estados asíncronas, gestionando los reintentos, los tiempos de espera, el lanzamiento de procesos y la captura de la salida. Cada unidad es la fuente de verdad de su propio estado y tiene un canal dedicado hacia el dispatcher en lugar de una difusión.
- No compartas ningún estado directamente; todo cruza entre componentes en forma de mensajes.
- Modela todo el espacio de fallos y usa temporizadores pausables para que las señales de suspensión y reanudación no distorsionen la temporización.
- Usa los mecanismos nativos de contención de procesos y de señales de cada plataforma, protégelos con compilación condicional y módulos por plataforma tras una interfaz compartida, y documenta en comentarios las diferencias y compensaciones entre plataformas.
