Aprendizajes de una migración de 1 millón de líneas de código
Un ingeniero. Once días. Un diff de +1.009.272 líneas.
Jarred Sumner, el creador de Bun, ha reescrito su runtime entero de Zig a Rust y lo ha contado con una honestidad poco común: los números, los
falsos comienzos, las 19 regresiones que se le colaron y hasta la factura de tokens. Son 165.000 dólares a precio de API.
No te voy a vender que esto lo puedes replicar el lunes en tu curro. Casi seguro que no. Pero dentro de esa historia hay un método, y ese método
sí se copia.
Esto es lo que vamos a desmenuzar:
- Qué migró exactamente y por qué el problema no era el lenguaje que usaba
- El bucle de trabajo que repitió cincuenta veces: implementador, revisores adversarios, aplicador
- La preparación previa que hizo posibles los once días (y sin la cual habría sido un desastre)
- Los cuatro tipos de bug que se colaron pese a todo, con su código
- Una lista de 12 cosas que deberías hacer si trabajas con una base de código grande
El método que sí se copia empieza por darle un plan al agente
Antes de soltar 64 agentes, Sumner escribió las guías. Practica ese mismo reflejo: un ciclo completo de Spec Driven Development con OpenSpec sobre un proyecto de juguete, en modo asistido, antes de coger tú el volante.
Entra en el curso gratis →Los números que hay debajo de la historia ¶
Bun no es un proyecto de fin de semana. Su CLI supera los 22 millones de descargas mensuales y herramientas como Claude Code u OpenCode lo usan
como runtime. Antes de la migración, eran 535.496 líneas de Zig sin contar comentarios, repartidas en 1.448 ficheros.
La migración se hizo con un modelo preliminar de Claude Fable 5 orquestando unos 64 agentes en paralelo. Cuatro workflows, cada uno en su propio
worktree, con 16 agentes dentro. En el pico, escribían unas 1.300 líneas de código por minuto.
El resultado del ciclo completo, del 3 al 14 de mayo de 2026:
| Métrica | Valor |
|---|---|
| Commits (sin merges) | 6.502 |
| Líneas del diff final | +1.009.272 |
| Errores de compilador que hubo que resolver | ~16.000 |
| Tests borrados o saltados | 0 |
| Tokens de salida | 690 millones |
| Coste a precio de API | ~165.000 $ |
Ese cero de la cuarta fila es el dato más importante de la tabla. Volvemos a él más abajo.
🔑 La suite de tests de Bun está escrita en TypeScript, no en Zig. Por eso sobrevivió al cambio de lenguaje sin tocar una línea. Esa decisión,
tomada años antes, es la que hizo posible la migración.
El problema no era Zig, era quién manda sobre la memoria ¶
Aquí está la parte que más se malinterpreta cuando alguien lee “reescribimos X en Rust”.
Bun mezcla dos mundos que se llevan fatal. Por un lado, JavaScriptCore, un motor de JavaScript con su recolector de basura y sus reglas estrictas
sobre excepciones. Por otro, Zig, un lenguaje donde tú liberas la memoria a mano y no hay destructores. Cada asignación de memoria pasaba por el
mismo interrogatorio: dónde se libera esto, cómo garantizo que solo se libere una vez, comprobé la excepción de JavaScript, ¿este puntero lo ve el
escáner conservador del recolector?
La lista de bugs corregidos en una sola versión menor de Bun, la v1.3.14, da escalofríos: use-after-free en node:zlib, use-after-free en
node:http2 cuando un callback reentrante provoca un rehash del hashmap, fugas de memoria en crypto.scrypt, double-free en el parser de CSS con
background-clip, una condición de carrera en MessageEvent.
Todos son la misma familia de bug con distinto disfraz.
Y no es que el equipo no hiciera nada. Parchearon el compilador de Zig para tener Address Sanitizer. Lanzaban Fuzzilli 24/7 contra el runtime.
Tenían tests de fugas de memoria de punta a punta. Es más de lo que hace la mayoría de proyectos.
El diagnóstico honesto de Sumner es este: la respuesta habitual a estos problemas es una guía de estilo. TigerStyle en Zig, la guía de C++ de
Google con sus 31.000 palabras. ¿Y el problema de una guía de estilo? Que hay que hacerla cumplir. Y ahí solo tienes revisión de código y linters,
siempre a destiempo.
💡 Un error de compilación es un ciclo de feedback mejor que una guía de estilo. El fuzzing llega después del merge. La CI llega al hacer push.
El compilador llega mientras escribes.
Esa frase resume la decisión entera. No es “Rust mola más”. Es “quiero que la máquina rechace el código malo en lugar de pedirle a un humano
cansado que lo detecte en una revisión de PR”.
Todo de golpe o poco a poco ¶
La primera pregunta grande de cualquier migración.
Sumner ya había hecho un port sin LLMs de por medio: el transpilador de esbuild, de Go a Zig, línea por línea. Su conclusión de aquella vez es la
misma que aplicó ahora: todo de golpe. Una migración incremental te obliga a escribir código temporal que esperas borrar algún día, y ese
“algún día” te acompaña durante meses.
La segunda pregunta grande: cómo. Y la respuesta es tan aburrida que da rabia. Hacer una migración que parezca que has transpilado el Zig a Rust.
Nada de reescribir a Rust idiomático. Nada de aprovechar para arreglar aquella arquitectura que nunca te gustó. Mismo diseño, misma arquitectura,
mismas prestaciones, mismas funcionalidades.
¿Por qué? Por dos razones que van juntas:
- El equipo tiene que poder mantenerlo después. Quien entendía el Zig original entiende el Rust traducido. Se lee igual.
- La suite de tests solo sirve como red de seguridad si el comportamiento no cambia. Si además refactorizas, ya no sabes si un test rojo es
un bug del port o una decisión nueva.
Lo idiomático llega después. Refactorizar para reducir unsafe es trabajo de la v1.5, no de la migración.
Solo esas dos preguntas son grandes. Todo lo demás, según sus palabras, son tácticas.
El bucle que se repitió cincuenta veces ¶
Buena parte del trabajo de un programador cabe en un bucle. Coges una tarea, produces un resultado, alguien lo revisa, aplicas el feedback.
Repites.
// Pseudocódigo: el bucle de cualquier equipo de desarrollo
let task;
while ((task = todoList.pop())) {
const result = task();
const feedback = await Promise.all([review(result), review(result)]);
await apply(feedback, result);
}
Toda la migración son unos 50 bucles de estos, ejecutados sin parar durante 11 días con dynamic workflows en Claude
Code. Es loop engineering del
bueno, montado sobre un harness que decide qué puede tocar el agente: alguien reparte el trabajo,
alguien lo comprueba y alguien decide cuándo se para. Un bucle para generar la guía de porting. Otro para traducir cada .zig a .rs. Otro para
arreglar los errores de compilación de cada crate. Otro para que bun test funcionara. Otro para poner la suite en verde.
La pieza que hace que el bucle no se derrumbe tiene nombre: revisión adversarial.
Montar bucles de agentes que no se derrumban es justo lo que estamos aprendiendo en directo. Cada domingo, +6.700 developers compartimos qué funciona y qué no al trabajar con IA. Gratis, desde 2018.
Quiero esa dinamita 🧨Revisión adversarial: separa a quien escribe de quien critica ¶
Con humanos, la persona que revisa el código no es la que lo escribió. Hay un motivo: quien escribe quiere mergear. Ese sesgo empuja a dar por
bueno lo que no está listo.
Los agentes de IA tienen exactamente el mismo sesgo. El agente que escribió el código quiere que se acepte. Y aquí llega el truco entero del
asunto:
- 1 implementador. Su contexto: el
.zigoriginal, el plan de porting, su propio razonamiento. - 2 o más revisores adversarios. Su contexto: solo el diff. Nada del razonamiento del implementador. Su instrucción: asume que este código
está mal y encuentra por qué. - 1 aplicador. Coge el feedback y lo aplica.
El implementador no revisa. El revisor no implementa.
Y funciona. Los tres bugs que Sumner enseña en el post habían compilado sin una queja. Los tres parecían razonables. Este, por ejemplo:
// El Box se destruye al final del brazo del match...
// pero uv_close es asíncrono y libuv sigue usando el puntero
pipe.close(Subprocess::on_pipe_close)
uv_close guarda el puntero crudo hasta el siguiente tick del bucle de eventos. Para entonces, el Box ya se ha destruido y ha liberado la
memoria. Use-after-free y, cuando llega el callback, double-free encima. El arreglo cabe en una línea:
// Fugamos el Box a propósito: libuv lo liberará en el callback
Box::leak(pipe).close(Subprocess::on_pipe_close)
Otro: unwrap_or evalúa su argumento de inmediato, aunque no lo necesite. Un color-mix(in srgb, red 40%, blue) reventaba antes de que
unwrap_or llegara siquiera a ignorar el valor. Cambio a unwrap_or_else, que recibe una clausura y es perezoso.
⚠️ Ninguno de estos bugs lo habría cazado el compilador. Compilaban limpios. Los cazó un segundo agente al que le dijeron “esto está mal,
demuéstralo”.
Si quieres montar este patrón en tu terminal, tienes la mecánica completa en el post sobre cómo crear subagentes con doce prompts listos para
copiar y en el de Agent Teams en Claude
Code.
Contextos limpios: no construyas y verifiques en la misma sesión ¶
Mi parte favorita de todo el asunto es el detalle más pequeño. Y no lo he visto comentar casi a nadie.
El revisor no recibe una sesión larga con todo el historial. Recibe solo el diff. Ni el .zig original, ni el plan de porting, ni una línea
del razonamiento del implementador. Y una instrucción: asume que esto está mal, demuéstralo.
Piénsalo al revés. ¿Qué pasa si el revisor sí tiene ese historial?
Pues que hereda la justificación. Ha “visto” cómo se llegó al unwrap_or, ha leído el párrafo donde el implementador explicaba por qué era
correcto, y ahora está evaluando código con la explicación pegada al lado. Eso no es una revisión. Es una relectura.
Y aquí es donde esto deja de ser una anécdota sobre Bun y se convierte en algo que te afecta hoy:
Una sesión larga con auto-compactación no sirve para construir y verificar a la vez.
Cuando el contexto se llena y el agente compacta, no guarda el diff crudo. Guarda un resumen. Y el resumen lo escribe quien acaba de escribir el
código, con su marco mental intacto: “implementé X porque Y”. Cuando esa misma sesión llega a la fase de verificación, arrastra ese “porque Y”
durante todo el proceso. El resumen se convierte en la coartada.
Es lo mismo que te pasa cuando relees un texto tuyo. No lees lo que está escrito. Lees lo que querías escribir.
🔑 El agente que verifica no debe haber visto nunca el razonamiento que produjo lo que verifica. Solo el artefacto.
Traducido a tu terminal, y esto lo puedes aplicar esta tarde:
- Sesión nueva para verificar. Cuando el agente diga “listo”, no le preguntes en la misma conversación si está bien. Abre otra y pásale el
diff. - Dale el mínimo contexto posible. El diff. Los tests. La especificación si acaso. Nada del hilo anterior.
- Cambia la instrucción por defecto. “Revisa esto” invita a aprobar. “Encuentra por qué esto está roto” invita a buscar.
- Desconfía de un revisor que nunca rechaza nada. Si tu verificador da el visto bueno a todo, no está verificando. Está acompañando.
- Compilación y verificación final, en procesos separados. Aunque sean el mismo modelo. Sobre todo si son el mismo modelo.
Hay un efecto secundario agradable. Un contexto pequeño es un contexto barato y un contexto que no se degrada. Menos tokens, menos ruido, menos
context rot. El revisor que solo ve un diff de 200 líneas rinde mejor que el que arrastra
180.000 tokens de conversación.
Separar contextos no es una manía de purista. Es la única forma de que la segunda opinión sea una segunda opinión.
El verificador que sí rechaza
La revisión adversarial de Sumner, montada en tu propia terminal
Verás el ciclo completo para revisar y verificar lo que generan tus agentes: adversarial review entre modelos, pruebas en navegador y casos Gherkin para que tu segunda opinión sea de verdad una segunda opinión.
Ver el método entero →Masterclass en directo · adversarial review entre modelos · Web Reactiva Premium
Tres horas de preparación antes de escribir una línea ¶
Si vas a hacer algo grande y caro, quitarle riesgo primero sale barato.
Antes de tocar código, Sumner pasó unas tres horas hablando con Claude sobre cómo mapear los patrones de Zig a Rust. De esa conversación salió un
PORTING.md. Después vino la pregunta difícil: cómo le pones lifetimes de Rust a un código que gestiona la memoria a mano.
Su solución fue lanzar un workflow que leyera cada campo de cada struct del codebase, trazara el flujo de control, propusiera un lifetime, lo
pasara por dos revisores adversarios y lo escribiera todo en un LIFETIMES.tsv.
Después, una ronda de revisión adversarial sobre el PORTING.md y el LIFETIMES.tsv juntos, para cazar contradicciones entre ambos. Y una
lectura manual suya por encima.
Solo entonces vino la prueba de fuego: en vez de lanzar los 1.448 ficheros, empezó con tres. Un implementador por fichero, dos revisores
comprobando que el .rs se comportaba como el .zig y que seguía las dos guías, un fixer aplicando sugerencias.
Tres ficheros. Ese es el gesto que separa esto de una imprudencia cara.
Los falsos comienzos, que son la mejor parte ¶
Nada salió bien a la primera. Esto es lo que se rompió y cómo se arregló, y es oro puro si trabajas con agentes.
Los agentes se pisaban entre sí. A los dos minutos de lanzar el workflow sobre los 1.448 ficheros, un agente ejecutó git stash. Otro hizo
git stash pop. Otro, git reset HEAD --hard. La solución fue editar el workflow: prohibido cualquier comando de git que no sea un commit de un
fichero concreto. Prohibido cargo. Prohibido cualquier comando lento.
Los worktrees no eran gratis. Meter cada agente en su propio worktree resolvía las colisiones, pero el repositorio de Bun es tan grande que se
quedaba sin disco. Solución de compromiso: 4 worktrees, 16 agentes en cada uno.
“Que compile” no significa lo que crees. Cuando pidió “vamos a hacer que todos los crates compilen”, los agentes interpretaron “pon stubs en
las funciones que dan error de compilación”. También empezaron a escribir comentarios larguísimos justificando los apaños. La regla que añadió a
los revisores adversarios es de las que te guardas:
🛡️ Si necesitas un comentario de un párrafo para justificar por qué un workaround está bien, el código está mal. Arregla el código.
Una edición del prompt y unas horas después, aquello dejó de pasar.
Los tests se comían la máquina. La suite de Bun tiene tests que agotan los sockets TCP de la máquina, otros que escriben gigabytes en disco y
algunos que lanzan unos 10.000 procesos. Pedir por favor no bastaba: acabó usando systemd-run con cgroups para limitar memoria, CPU y aislar los
namespaces de procesos. Y aun así se quedó sin disco varias veces.
Un grep lento congeló la fiesta. Se le olvidó subir las IOPS de la instancia EC2. Un solo comando de búsqueda pesado bloqueaba las lecturas
y escrituras de disco durante minutos.
Ninguno de estos cinco problemas es un problema de IA. Son problemas de infraestructura y de definición de tarea. Y esa es la lección: cuando algo
se rompe, arregla el proceso que genera el código, no el código.
Los errores del compilador como cola de trabajo ¶
Aquí viene mi parte favorita del método, porque es la más transferible.
El codebase de Zig era una unidad de compilación. Sumner quería partir el Rust en unos 100 crates para que compilara antes. Eso destapó
dependencias cíclicas. Y resolver las dependencias cíclicas destapó unos 16.000 errores de compilación.
Un número imposible para una persona. Un número normal para 64 agentes.
El workflow, por cada crate:
- Ejecutar
cargo checky agrupar la salida por fichero, guardándola en un archivo - Arreglar todos los errores de ese crate
- Dos revisores adversarios sobre los cambios del crate
- Un fixer aplica las correcciones
Fíjate en el detalle del paso 1. cargo check se ejecutaba una sola vez, al principio. Nada de agentes lanzando el compilador a la vez. Los
errores se congelan en un fichero de texto y ese fichero es la cola de trabajo.
Es el mismo principio que subyace al loop engineering: lo difícil de un bucle no es el while, es el check que decide si el trabajo está bien
hecho. Aquí el check lo da el compilador gratis.
Después vinieron los smoke tests. Que bun --version arrancara. Que bun test <fichero> funcionara. Y a partir de ahí, otro bucle: lanzar unos
100 ficheros de test al azar, guardar cada stacktrace fallido, un implementador propone el arreglo, dos revisores, un fixer.
Dos días después del primer intento de CI, la lista de ficheros de test rojos bajó de 972 a 23. Día y medio más tarde, Linux entero en verde.
Windows fue el último en caer.
Trucos como usar el compilador de cola de trabajo son de los que seleccionamos cada semana entre 12 recursos sobre productividad con IA. Únete a los +6.700 que reciben la newsletter cada domingo.
Quiero esa dinamita 🧨Las 19 regresiones que sí se colaron ¶
Sería mentira decir que un cambio de este tamaño no rompe nada. Se colaron 19 regresiones conocidas y todas están corregidas. Lo interesante es de
dónde venían: casi todas de código que se escribe igual en los dos lenguajes pero significa cosas distintas.
Cuatro ejemplos que te van a doler:
El assert que era una función y pasó a ser una macro. En Zig, assert(x) es una función: su argumento se evalúa siempre, en todas las
builds. En Rust, debug_assert!(x) es una macro: en release, la expresión entera desaparece. Dentro de ese assert había una llamada a
insert_stale que registraba un fichero en el grafo de hot reload. En builds de release dejó de ejecutarse y el HMR se rompió en proyectos con
React. En debug todo iba bien, claro.
El byte impar. Un helper de Zig ignoraba el último byte si la longitud era impar. El equivalente en Rust, bytemuck::cast_slice, entra en
pánico. Blob.text() sobre un BOM UTF-16 seguido de un número impar de bytes tumbaba el proceso.
Los bounds checks que ya no estaban desactivados. El Zig se compilaba con ReleaseFast en macOS y Linux, que elimina las comprobaciones de
límites. Las builds de release de Rust las mantienen. El port dejó un valor placeholder de 64 donde tenía que ir count / 4, y eso bajó el techo
de nombres de fichero de 8,4 millones a 270.272. Proyectos reales lo tocaban.
Los format strings de comptime. En Zig, el formato es comptime: los marcadores de color <r> desaparecen antes de sustituir los
argumentos. En Rust no hay parámetros comptime, así que Output::pretty recibía la cadena ya montada y reescribía los marcadores también sobre
los argumentos. Resultado: bun update -i imprimía oxfmtr en vez de oxfmt.
💡 Ninguna de las cuatro es un fallo del agente. Son fallos de equivalencia semántica entre lenguajes. Un humano habría cometido los mismos.
La diferencia es que aquí hay 1.448 oportunidades de cometerlos.
Hablemos de dinero: qué costó esto de verdad ¶
165.000 dólares a precio de API, antes del merge. Es la cifra que todo el mundo ha retuiteado. Pero la cifra sola no dice nada. Lo interesante es
su desglose.
| Concepto | Volumen |
|---|---|
| Tokens de entrada sin cachear | 5.900 millones |
| Lecturas de entrada desde caché | 72.000 millones |
| Tokens de salida | 690 millones |
Haz la división. De los 77.900 millones de tokens de entrada, el 92,4% se sirvió desde caché. Y la salida, lo único que es código de verdad,
apenas es un 0,9% del total de tokens movidos.
Ahí está la primera lección de la factura y no tiene nada de romántica: esto no sale a cuenta por lo listo que sea el modelo. Sale a cuenta porque
el PORTING.md, el LIFETIMES.tsv y el prompt del workflow no se pagan 6.502 veces. Se pagan una y se leen desde caché las otras 6.501.
Si te llevas un solo número operativo de esta sección, que sea ese 92,4%.
El precio por unidad de trabajo ¶
Dividir el total entre lo producido da unas cifras que se entienden solas:
- 0,16 dólares por línea de código en el diff final
- 25,4 dólares por commit, revisión adversarial incluida
- Unos 15.000 dólares al día durante los once días
Ese coste por commit incluye a cuatro agentes: uno escribe, dos revisan, uno aplica. Dicho de otra forma, tres de cada cuatro dólares no se van
en escribir código, se van en criticarlo y arreglarlo. Es exactamente la proporción inversa a como la mayoría de gente usa un agente.
💡 Si tu factura de tokens se va casi entera en generar, no estás haciendo revisión adversarial. Estás haciendo autocompletado caro.
Lo que no aparece en la factura ¶
Aquí es donde el titular de los 165.000 se queda corto por abajo. Hay costes reales que no llevan etiqueta de API:
- Once días de una persona sin hacer otra cosa. Sumner estuvo leyendo salidas de workflows a mano y ajustando prompts casi todo ese tiempo.
No es dinero, es la agenda entera de un ingeniero. - La infraestructura. Una instancia EC2 (con las IOPS mal configuradas, por cierto), cuatro worktrees de un repositorio enorme y una CI que
ejecuta unos 60.000 tests en seis plataformas. Solo la CI, durante 420 builds, tiene un precio. - Once rondas de revisión de seguridad después del merge, más fuzzing continuo de todos los parsers.
- Diecinueve regresiones. Todas corregidas, sí. Pero alguien las sufrió primero.
Y hay un coste de tercer tipo, el más difícil de meter en una hoja de cálculo: la deuda de comprensión. Ahora mismo hay un millón de líneas de
Rust en producción que ningún humano ha leído entero. La suite de tests dice que se comportan igual. El compilador dice que no hay use-after-free.
Nadie dice que las entienda.
La comparación honesta ¶
La tentación es comparar 165.000 dólares con el sueldo de tres ingenieros durante un año y sacar pecho. Sumner no hace esa comparación, y hace
bien.
Porque esa alternativa no existía. Tres personas con contexto completo, doce meses, sin arreglar bugs de seguridad, sin mejorar la
compatibilidad con Node.js, sin sacar funcionalidades nuevas. Sus palabras: nunca lo habríamos hecho. La alternativa realista era no migrar nunca
y seguir arreglando use-after-frees hasta el fin de los tiempos.
Con lo cual, la comparación buena no es “165.000 dólares contra tres salarios”. Es “165.000 dólares contra cero”. Contra un proyecto que jamás se
aprueba en una reunión de planificación.
⚠️ Un cálculo tramposo que verás mucho estos meses: dividir el coste en tokens entre el coste de un equipo y concluir que la IA es 20 veces más
barata. Los 165.000 no incluyen al ingeniero que escribió el código original, ni al que vigiló los once días, ni al equipo que revisó la seguridad
después.
Cómo pagar menos si haces algo parecido ¶
Cuatro palancas concretas, todas visibles en el relato de Bun:
- Estabiliza el prefijo del prompt. Las guías, las reglas y los ejemplos van al principio y no cambian. Es lo que hace que el 92% de tu
entrada venga de caché. - Congela la salida del compilador en un fichero. Nadie lanza
cargo checksesenta y cuatro veces. Se ejecuta una vez, se agrupan los errores
por crate y eso es la cola de trabajo. - Contexto mínimo al revisor. Solo el diff. Menos tokens de entrada y, encima, mejor revisión.
- Ensaya con tres ficheros. El experimento barato antes del experimento caro.
Las 25 técnicas para ahorrar tokens aplican aquí igual que en tu sesión del martes por la tarde.
La diferencia de escala no cambia los hábitos, solo multiplica lo que te cuesta no tenerlos.
Lo que ganó Bun con el cambio ¶
La v1.4.0 arregla 128 bugs que se reproducen en la 1.3.14. Van desde fugas de memoria hasta crashes y texto de ayuda con los colores mal.
En consumo de memoria, el ejemplo es brutal. Cada llamada a Bun.build() fugaba unos 3 MB para siempre:
| Builds | Bun v1.3.14 | Bun v1.4.0 |
|---|---|---|
| 500 | 1.914 MB | 526 MB |
| 1.000 | 3.506 MB | 586 MB |
| 2.000 | 6.745 MB | 609 MB |
En la columna nueva, la memoria se estabiliza. Eso lo hace Drop: se ejecuta cuando el valor deja de ser accesible, sin que nadie tenga que
acordarse. Un intento anterior de arreglar esto mismo en Zig no llegó a mergearse, porque sin Drop nadie se sentía lo bastante seguro.
El resto del marcador: el binario encoge un 20% en Linux y Windows, los parsers recursivos usan menos pila, y el rendimiento sube entre un 2% y un
5% gracias a que Rust permite optimización en tiempo de enlazado entre lenguajes. Bun.serve pasa de 169,6k a 177,7k peticiones por segundo.
Claude Code usa el port de Rust desde la v2.1.181. El arranque en Linux mejoró un 10%, de 517 ms a 464 ms. Por lo demás, nadie se enteró. Que
nadie se entere era justo el objetivo.
Hoy, un 4% del código Rust de Bun vive dentro de un bloque unsafe, y el 78% de esos bloques ocupan una sola línea: un puntero que viene de C++ o
una llamada a una librería en C.
14 cosas que hacer si trabajas con una base de código grande ¶
Vamos a lo práctico. Esto es lo que yo me llevo, ordenado de lo que puedes aplicar hoy a lo que necesita músculo de equipo.
-
Escribe tus tests en un lenguaje que no sea el de tu código. Suena raro hasta que necesitas cambiar de lenguaje. La suite de Bun está en
TypeScript y sobrevivió intacta a la migración. Si tus tests están acoplados a tu implementación, no son una red de seguridad: son parte de la
carga. -
Cuenta tus assertions, no tus tests. Bun tiene 1.386.826 llamadas a
expect()sobre 60.624 tests. Esa densidad es lo que permite confiar
en un diff de un millón de líneas. Un test que comprueba una cosa no te salva de nada. -
Cero tests borrados, cero tests saltados. Es la regla que hace honesto todo lo demás. En el momento en que un agente puede saltarse un test
para ponerlo en verde, tu suite deja de significar nada. Sumner verificó a mano que los tests se estaban ejecutando de verdad y no siendo
omitidos. -
Separa quien escribe de quien revisa, también con IA. Un agente en su propia ventana de contexto, que solo ve el diff, sin acceso al
razonamiento de quien lo escribió, con la instrucción de asumir que el código está roto. Dos revisores por implementador. Cuesta tokens y los
vale. -
No construyas y verifiques en la misma sesión larga. La auto-compactación guarda el resumen de quien escribió el código, con su
justificación incluida. Abre una sesión nueva, pásale solo el artefacto y pídele que lo rompa. Si tu verificador nunca rechaza nada, no es un
verificador. -
Prohíbe explícitamente los comandos destructivos.
git stash,git reset, cualquier cosa que no sea un commit de ficheros concretos. Si
varios agentes comparten un worktree, uno solo de estos comandos borra el trabajo de los demás. Escríbelo en el prompt del workflow. -
Convierte la salida del compilador en una cola de trabajo. Ejecuta el compilador una vez, vuelca los errores agrupados por fichero o por
módulo, y reparte esa cola. No dejes que cada agente lance el compilador. Vale igual paratsc, para tu linter o para tu suite de tests. -
Prueba con tres ficheros antes que con mil. El coste de un ensayo pequeño es ridículo comparado con el de descubrir el problema a los 800
ficheros. Y las conclusiones son casi idénticas. -
Prepara la guía antes de escribir código. Un documento con los patrones equivalentes entre origen y destino. Otro con las decisiones
difíciles resueltas de antemano. Pásalos por revisión adversarial y léelos tú. Sin esePORTING.md, mil ficheros migrados son mil
interpretaciones distintas del mismo problema. -
Ataca la semántica que se parece pero no es igual. Haz una lista explícita de trampas: qué se evalúa siempre y qué se elimina en release,
qué entra en pánico y qué ignora el error, qué comprobaciones de límites tenías desactivadas. Las cuatro regresiones que hemos visto salen de
aquí. -
Aísla la ejecución antes de dejar suelta la maquinaria. Límites de memoria, de CPU y de procesos. Un test que lanza 10.000 procesos y otro
que agota los sockets TCP no conviven bien con 64 agentes en la misma máquina. -
Cuando algo se rompa, arregla el proceso, no el código. Es la regla que más se repite en todo el relato. Si el agente hace stubs, cambia
el prompt del revisor. Si escribe comentarios justificando apaños, prohíbe los comentarios justificativos. Unifque arreglas a mano hoy vuelve
mañana en otro fichero. -
No refactorices y migres a la vez. Nunca. La migración debe parecer una transpilación. Lo bonito lo dejas para la versión siguiente,
cuando ya tengas la suite en verde y sepas que el comportamiento no ha cambiado. -
Diseña el prompt pensando en la caché. Lo que no cambia va delante y no se toca: guías, reglas, ejemplos. Lo que cambia va al final. En
Bun, esa disciplina convirtió 78.000 millones de tokens de entrada en una factura de cinco cifras en vez de una de seis o siete.
Si quieres el porqué técnico de todo esto, en la guía del AI harness está el detalle de por qué el entorno que rodea al agente importa más que el
modelo que tengas debajo.
Lo que no deberías copiar de esta historia ¶
Una cosa es aprender del caso y otra es leerlo como una promesa.
Sumner es el autor original del 100% del código que migró. Sabe qué hace cada fichero y por qué. Cuando un revisor adversario levantaba una
bandera, él tenía criterio para decidir. Durante la mayor parte de esos once días estuvo leyendo salidas de workflows a mano y ajustando prompts.
No fue un “dale al botón y vuelve el jueves”.
También hay una asimetría de recursos difícil de ignorar. 165.000 dólares a precio de API, una instancia de EC2 con IOPS a estrenar, una CI que
ejecuta 60.000 tests en seis plataformas y un modelo preliminar que aún no estaba disponible para nadie más.
Y hay algo más incómodo: la alternativa realista, según él mismo, no era hacer la migración a mano. Era no hacerla nunca y seguir arreglando
use-after-frees uno a uno para siempre. Tres ingenieros con contexto completo, un año entero, sin arreglar bugs de seguridad ni mejorar la
compatibilidad con Node.js durante ese tiempo. Eso no se aprueba en ninguna empresa.
🔑 Lo que ha cambiado no es que la IA escriba código. Es que ahora se puede plantear un proyecto que antes ni se planteaba, porque su coste de
oportunidad era prohibitivo.
Ahí está la pregunta que te dejo. No es si tu base de código necesita reescribirse en otro lenguaje. Casi nunca lo necesita.
Es qué proyecto llevas dos años descartando en la reunión de planificación con un “eso son seis meses de trabajo y no lo podemos parar todo”.
Ese. Ve a por ese.
Fuentes ¶
- Jarred Sumner, “Rewriting Bun in Rust”
- Repositorio de Bun en GitHub
- Auditoría pública de bloques
unsafeen Bun - Prisma, “Prisma Compute sobre la reescritura de Bun en Rust”
- Documentación de LeakSanitizer
- Fuzzilli, el fuzzer de motores de JavaScript
🧨 Última oprtunidad para recibir la dinamita que mereces sobre programación con IA el próximo domingo: Suscríbete gratis a Web Reactiva en https://webreactiva.com/newsletter
12 recursos para developers cada domingo en tu bandeja de entrada
Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.