Portada de «De 34 pasos a 12: simplificar el build sin quitar garantías» — Arquitectura de Blupoli.

El problema no era que el build fuera largo, sino que reparaba demasiado

El pipeline de Blupoli había crecido por acumulación. Cada nueva necesidad encontraba una solución razonable de forma aislada: un script normalizaba enlaces, otro corregía metadata, otro arreglaba branding, otro copiaba assets, otro parcheaba rutas localizadas. El problema apareció cuando el orden completo empezó a parecerse más a una cadena de reparaciones que a una compilación.

La PR #162 partió de una regla sencilla: fuentes canónicas, build determinista y validación. Eso sustituía el patrón anterior de generar, parchear, normalizar, volver a parchear y finalmente comprobar. La diferencia no era sólo estética. Si el output necesita ser corregido varias veces después de generarse, la fuente o el builder que lo creó no está asumiendo toda su responsabilidad.

El objetivo fue reducir pasos reales, no esconderlos dentro de wrappers. La PR documentó las métricas antes y después para poder demostrarlo.

De 45 scripts a 22

Antes de la simplificación había 45 scripts .mjs dentro de scripts/. Después quedaron 22. La reducción no se consiguió renombrando veinte comandos bajo un único ejecutable, sino eliminando recorridos, normalizadores y reparaciones cuya responsabilidad podía moverse a una fuente o builder existente.

Ese cambio obligó a revisar por qué existía cada script. Algunos eran restos de migraciones ya consolidadas. Otros reparaban output que ahora podía nacer correctamente. Otros recorrían todo el HTML para aplicar una transformación que un builder ya estaba haciendo casi en el mismo punto.

La pregunta dejó de ser “¿podemos eliminar este fichero?” y pasó a ser “¿quién debería ser realmente dueño de esta responsabilidad?”. Esa pregunta evitó una limpieza superficial que sólo habría ocultado complejidad.

Comparación visual del pipeline anterior de 34 pasos con el nuevo pipeline determinista de 12 pasos
La reducción elimina reparaciones reales y conserva únicamente fases con responsabilidades distintas.

De 34 pasos de build a 12

El comando npm run build pasó de 34 pasos a 12. El pipeline actual construye la base de Puzzles, enriquece el HTML estático, genera rutas localizadas, localiza juegos y blog, procesa los dos pasos editoriales todavía necesarios, construye arquitectura editorial y web, aplica el estado final de release y cierra con check-output.

La reducción importa porque cada paso global tiene coste mental y técnico. Un script que recorre todo el árbol puede introducir dependencia de orden, volver más lenta la ejecución y dificultar saber en qué fase nació un error. Con menos fases, las fronteras quedan más claras.

Doce pasos siguen siendo un pipeline real, no una función monolítica. La meta no era reducir por una cifra bonita, sino conservar separaciones que protegen responsabilidades distintas y eliminar las que sólo existían para reparar otra fase.

Check también se redujo de seis fases a tres

npm run check pasó de seis pasos a tres. check-source integra sintaxis de JavaScript, integridad de catálogo y motores, self-tests, cobertura de onboarding y validaciones de i18n y taxonomía inglesa. locale-readiness y translation-status permanecen separados porque también se utilizan como herramientas manuales.

La consolidación tiene una condición importante: un gate puede agrupar validaciones, pero no debe convertirse en una caja negra que modifica archivos. check-source verifica la fuente; no la repara para que el test pase.

Esa separación entre construir y validar fue uno de los principios más útiles de la refactorización. Un gate final que cambia el output deja de ser un gate: se convierte en otra fase de build difícil de detectar.

El Devlog se convirtió en una fuente canónica

La misma PR terminó una migración editorial pendiente. content/building-public.json pasó a content/devlog.json, editorial.json.buildingPublic pasó a editorial.json.devlog y la sección pública usa devlog y rutas /devlog/. Las antiguas rutas /building/ y /building-in-public/ quedan como compatibilidad histórica.

Antes existían scripts cuyo trabajo era migrar, normalizar o renombrar esa identidad después de construir. Al mover la decisión a la fuente, desaparecieron normalize-devlog-name, migrate-devlog-editorial y varias correcciones asociadas.

Es un buen ejemplo de la filosofía completa: si sabemos que el producto se llama Devlog, el builder debe recibir Devlog. No tiene sentido generar primero otra identidad y gastar fases en convertirla después.

Las reparaciones post-build fueron la primera gran categoría en desaparecer

Entre los scripts eliminados estaban link-game-control-sizing, normalize-localized-blog-links, normalize-public-game-counts, normalize-editorial-metadata, normalize-devlog-name, migrate-devlog-editorial, separate-blupoli-content, normalize-blupoli-branding y link-journal-diagram-css.

Cada nombre cuenta una pequeña historia de deuda: algo ya se había generado y una fase posterior tenía que enlazar, normalizar, separar o corregir. La refactorización movió esas decisiones hacia el builder o fuente que podía producir el resultado correcto desde el principio.

Eliminar un reparador sólo es seguro cuando su responsabilidad reaparece en el lugar apropiado. Borrar el script sin mover la lógica habría reducido líneas y aumentado bugs. Por eso la PR se revisó como cambio de propiedad, no como limpieza masiva.

La arquitectura editorial perdió una capa de parches

También desaparecieron build-public-journal, localize-editorial-covers, normalize-editorial-covers, editorial-quality-gate, prepare-web-editorial-locales, finalize-blog-assets, enrich-journal-index y serve-dist. Las portadas canónicas se generan una vez y la validación editorial vive ahora en finalize-editorial.

Esto conecta directamente con write-devlog y write-blog. La dirección deseada es que cada skill produzca una fuente editorial completa —contenido, metadatos y visuales— y que el build se limite a transformar y validar, no a inventar piezas editoriales que faltan.

Todavía no hemos llegado al extremo puro de esa arquitectura. Dos pasos permanecen deliberadamente como deuda conocida, y documentarlos fue tan importante como eliminar los anteriores.

blog-editorial-visuals sigue porque aún crea contenido real

blog-editorial-visuals.mjs no se eliminó. Hoy todavía genera infografías que no existen en las fuentes de algunos artículos. Si lo retiráramos sin migrar esas visualizaciones, perderíamos contenido real. La fase es temporal, pero su responsabilidad todavía no tiene un reemplazo completo.

El siguiente paso correcto no es absorber el script en otro wrapper. Es mover esas visualizaciones a assets canónicos producidos por las skills editoriales. Cuando la fuente ya contenga lo necesario, el build podrá convertirse en mera validación y copia.

Esta excepción fue útil porque evitó convertir “menos scripts” en un objetivo absoluto. La arquitectura debe ser más simple, pero no a costa de esconder o eliminar capacidades que todavía son necesarias.

seo-finalize también conserva una responsabilidad pendiente

seo-finalize.mjs se solapa parcialmente con el builder editorial, pero todavía genera navegación de artículos relacionados y series. Por eso tampoco se eliminó. Gran parte del SEO ya se puede resolver antes, pero esa responsabilidad concreta necesita moverse primero.

El criterio es el mismo: no borrar hasta que exista un dueño mejor. La deuda queda explícita en lugar de disfrazada. Eso convierte el pipeline en un mapa del trabajo pendiente, no sólo en una lista de comandos.

Cuando esas relaciones editoriales nazcan de la fuente o del builder canónico, seo-finalize podrá reducirse o desaparecer sin perder comportamiento.

Menos recorridos globales del HTML

copy-shared-footer-assets, inject-observability y audit-improvements dejaron de existir como fases independientes. Los builders ya recorrían documentos en puntos donde podían copiar assets deterministas, añadir observabilidad o aplicar metadata sin forzar otra pasada global.

Los recorridos globales son tentadores porque permiten implementar una función rápido sin tocar arquitectura existente. Su coste aparece después: orden implícito, tiempo extra y dificultad para saber quién modificó un nodo por última vez.

Absorberlos no significa meter todo en un único loop gigante. Significa aprovechar la fase que ya tiene el contexto correcto y hacer que el output salga completo de ella.

i18n también se simplificó con una excepción deliberada

localize-interface.mjs se absorbió realmente en localize-routes.mjs. En cambio, localize-games.mjs permaneció separado porque protege y sustituye estructuralmente el payload de cada juego. No todas las localizaciones son la misma operación.

Esta diferencia es importante. Una simplificación mal entendida habría fusionado ambos pasos sólo porque contienen la palabra localize. La arquitectura correcta mantiene separado lo que tiene invariantes distintos.

El resultado es menos fases sin perder claridad: rutas e interfaz comparten contexto, mientras los juegos conservan un tratamiento que protege datos embebidos y estructura de motor.

Release status dejó de tener una fase prepare

Antes existía una preparación específica de release. Ahora build.mjs genera directamente releaseStatus y catalog-stats.json, mientras apply-release-status queda únicamente como finalizador de publicación. Se elimina así una ida y vuelta sobre información que el builder ya conoce.

El cambio hace más fácil razonar sobre qué catálogo se está construyendo y cuándo un juego se considera publicable. El finalizador puede concentrarse en retirar superficies o enlaces de juegos no publicados y verificar conteos.

De nuevo, la mejora no es sólo un comando menos. Es una frontera más clara: el build produce estado, el finalizador aplica la política de publicación.

check-output pasó a ser un gate que no repara

El gate final integra ausencia de branding heredado, branding correcto de Blupoli, fronteras entre Blog y Puzzles, CSS de motores nativos y paridad de configuración visual, scripts y estilos entre locales. El antiguo enforce-game-visual-parity desapareció.

Antes de eliminarlo, la corrección de CSS nativo se movió al builder. Eso es clave: check-output puede detectar que el output está mal, pero no debe arreglarlo silenciosamente. Si falla, hay que corregir la fuente o el constructor responsable.

Un gate que no muta deja trazabilidad. El error aparece donde debe resolverse y no queda enmascarado por una fase final que transforma un build inválido en aparentemente válido.

La simplificación del CI necesitó varias iteraciones

Los cambios de build coincidieron con un problema práctico en GitHub Actions: una PR podía lanzar validación completa y preview de Firebase por caminos separados, duplicando trabajo en el runner self-hosted. Las PR #165 y #166 consolidaron Quality y preview en un único flujo y eliminaron el workflow duplicado de preview automático.

Quality quedó como gate general: verify más Playwright, y el preview de Firebase se ejecuta al final reutilizando dist. Si Firebase devuelve un error de cuota de canales, como HTTP 429, el preview puede fallar sin convertir en roja una PR cuyo código ya pasó validación funcional.

Castle Wall también dejó de ejecutar su stress test por cambios editoriales o E2E sin relación. El workflow específico se activa cuando cambia el motor, su test de lógica o el propio workflow.

Cancelar trabajo obsoleto sí, cancelar un deploy válido no

La reducción de ruido añadió cancel-in-progress para actualizaciones de una misma PR. Si llega un commit nuevo, no tiene sentido que el runner self-hosted siga gastando tiempo en validar uno obsoleto. Esa política reduce ejecuciones canceladas tarde y cola innecesaria.

Sin embargo, la interacción entre Quality y producción mostró un borde más delicado. Tras el merge del tema de Zebra, un despliegue validado quedó bloqueado por ejecuciones posteriores. La PR #169 corrigió el flujo para que producción despliegue una revisión de main cuyo Quality haya terminado correctamente y para que un Quality fallido no cancele un despliegue previamente validado.

La lección fue que concurrency no puede tratar todos los jobs como equivalentes. Una PR vieja sí es obsoleta; un artefacto de producción ya validado no debe desaparecer sólo porque otra revisión empezó a comprobarse.

Quality volvió a ejecutarse sobre main

Una iteración previa había reducido trabajo evitando Quality tras el merge. El incidente de producción demostró que eso quitaba una garantía útil. PR #169 volvió a ejecutar Quality en cada push a main y conectó el deploy de producción al éxito de ese workflow.

No es una contradicción con simplificar CI. La meta nunca fue ejecutar menos por principio, sino eliminar duplicación y conservar las comprobaciones en los puntos donde realmente aportan seguridad. Validar el commit exacto de main que se va a publicar sí aporta esa seguridad.

La versión final del flujo es por tanto más selectiva: una validación completa por PR, otra del commit final en main y deploy únicamente después de esa señal correcta.

Los deploys también se restringieron a cambios relevantes

Los workflows de producción se ajustaron para no desplegar por cambios que no afectan al sitio. Esta condición evita usar el runner y Firebase cuando el commit modifica documentación interna u otras rutas sin impacto publicable.

La optimización parece pequeña comparada con reducir 22 scripts, pero en un runner self-hosted la cola acumulada importa. Cada ejecución innecesaria retrasa trabajo que sí necesita navegador, build completo o stress tests.

El criterio de relevancia convierte CI en parte de la arquitectura: no todos los commits merecen las mismas fases, y esa diferencia debe estar expresada de forma legible.

La simplificación hizo más visible la fuente de verdad

Cuando un output pasa por diez reparadores, es difícil responder qué archivo es realmente canónico. Después de esta iteración, más decisiones viven en games/, content/, static/ y builders concretos. Los validadores comprueban esas decisiones en lugar de fabricarlas.

Esto mejora el trabajo de agentes y personas. Una tarea editorial puede modificar content/devlog.json y el artículo correspondiente sin adivinar qué normalizador cambiará después el resultado. Una tarea de juego puede buscar la fuente de configuración en lugar de inspeccionar dist.

Menos transformación implícita significa también diffs más explicables. Si una salida cambia, hay menos fases candidatas que puedan haberla reescrito.

Lo que no hicimos: esconder complejidad bajo un orchestrator nuevo

Era posible reducir package.json a uno o dos comandos creando un gran script que llamara internamente a los 34 pasos anteriores. Eso habría mejorado una métrica superficial y empeorado la inspección. La PR evitó esa trampa.

Los 12 pasos actuales siguen visibles porque representan responsabilidades reales. Cuando una fase puede desaparecer de verdad, su comportamiento se mueve a la fuente o al builder que ya debería poseerlo. Cuando no puede, permanece documentada como deuda.

La reducción por tanto se puede auditar: 45 scripts pasan a 22, build de 34 a 12, check de 6 a 3 y workflows de 5 a 4. Son menos unidades reales de trabajo, no sólo menos texto en un comando.

Qué cambia para el desarrollo diario

Un pipeline más corto reduce el coste de hacer preguntas. Si falla el branding, check-output señala una salida inválida. Si falla i18n de fuente, check-source o los gates de locale tienen un ámbito claro. Si falta una pieza editorial, finalize-editorial puede responsabilizarse sin que otro normalizador la invente más tarde.

También mejora la capacidad de añadir arquitectura nueva. Las PR abiertas de mobile-first, temporizador común u otras capas compartidas entran en un sistema donde la fuente y el builder tienen fronteras más visibles. Eso no hace que esas tareas sean simples, pero reduce las sorpresas del build.

La simplificación es infraestructura para las siguientes iteraciones. Su éxito se nota menos por una pantalla concreta que por cuántos lugares hay que tocar para hacer un cambio correctamente.

La lección: un build saludable produce, luego valida

La mejor descripción de la nueva dirección es breve: fuente canónica, transformación determinista y validación que no muta. Cuando una fase final tiene que reparar algo, la primera pregunta debería ser por qué el error no se evitó en la fuente o en el builder responsable.

No todas las excepciones pueden desaparecer a la vez. blog-editorial-visuals y parte de seo-finalize siguen siendo deuda explícita porque todavía producen valor real. Mantenerlas visibles es más sano que fingir que el pipeline ya llegó a su forma final.

Pasar de 34 pasos a 12 no es el final de la arquitectura. Es la señal de que Blupoli está dejando de crecer mediante parches acumulativos y empieza a exigir que cada capa sea dueña del resultado que produce.

Medir antes y después evitó una simplificación subjetiva

La refactorización se apoyó en números simples porque “el pipeline parece más limpio” no es una garantía suficiente. Contar scripts, pasos de build, fases de check y workflows permitió comparar una arquitectura concreta antes y después. Esas métricas no describen toda la calidad, pero obligan a demostrar que realmente desapareció trabajo y no sólo cambió de nombre.

También ayudan a vigilar el rebote. Un monorepo activo puede volver a acumular scripts auxiliares con rapidez si cada incidencia se resuelve añadiendo otra pasada global. Tener una referencia —22 scripts, 12 pasos de build, 3 de check y 4 workflows tras la PR— hace más visible cuándo empezamos a reconstruir el mismo patrón.

La cifra correcta no tiene por qué permanecer fija. Si una responsabilidad nueva merece una fase propia, añadirla puede ser la decisión correcta. Lo importante es que cada incremento tenga una razón arquitectónica y no sea la salida automática ante un output incorrecto.

El runner self-hosted hizo visible el coste de cada duplicación

Con recursos ilimitados, dos workflows que hacen casi lo mismo pueden parecer un problema menor. En un runner self-hosted, la duplicación se siente enseguida: builds completos esperando en cola, navegadores Playwright compitiendo por turno y previews que consumen tiempo después de que la validación equivalente ya haya terminado en otro job.

Por eso consolidar Quality y preview no fue sólo limpieza YAML. Liberó capacidad real para las tareas que necesitan el runner. Cancelar ejecuciones obsoletas de una PR también evita dedicar minutos a una revisión que ya no puede mergearse porque existe un commit más nuevo.

Esta experiencia reforzó una idea útil para el proyecto: CI también es producto interno. Su latencia cambia la velocidad de iteración y la confianza con la que se pueden revisar PRs. Optimizarlo no es sólo ahorrar minutos de máquina; es reducir el tiempo entre una decisión y una evidencia fiable.

Firebase preview es útil, pero no debe redefinir la calidad del código

La cuota de canales de preview puede devolver errores 429 aunque el build, los tests y Playwright estén correctos. Mezclar esa limitación externa con el resultado funcional de una PR hacía que un problema de infraestructura pareciera una regresión del producto.

El nuevo flujo mantiene el preview al final porque sigue siendo valioso para revisión visual, pero permite que un fallo de cuota no anule una validación funcional ya superada. La diferencia queda visible en el workflow: sabemos que el preview no se publicó, pero no mentimos diciendo que los tests del código fallaron.

Separar estas señales hace el CI más informativo. Un rojo debe ayudar a localizar una regresión; una incidencia externa debe conservar su contexto. Cuanto más precisa es esa semántica, menos tiempo se pierde investigando el tipo de fallo equivocado.

La arquitectura más simple también mejora la documentación

Un pipeline con 34 pasos obliga a la documentación a explicar demasiadas excepciones: qué script se ejecuta antes de cuál, qué reparador debe correr después de una localización o qué normalizador arregla una ruta concreta. Cuando esas dependencias desaparecen, la documentación puede volver a describir conceptos en lugar de memorizar una receta.

Esto es especialmente importante para agentes de desarrollo. Un AGENTS.md o una skill editorial funciona mejor cuando puede señalar fuentes canónicas y comandos estables. Si el comportamiento real depende de una secuencia histórica de parches, un agente puede hacer un cambio aparentemente correcto y descubrir al final que otro script lo reescribió.

La simplificación del build es por tanto también una mejora de interfaz para quien desarrolla. Menos comportamiento implícito significa instrucciones más cortas, revisiones más directas y una probabilidad menor de tocar la capa equivocada.

Lecturas relacionadas

El problema se anticipó en De añadir juegos a construir una plataforma verificable. La automatización editorial se explica en De Git a Devlog y los gates internacionales en i18n en seis idiomas y gates de calidad.