Expediente 8/0: la cura que no volvió a casa

Ocho líneas curaron el caso que abrió esta serie. Existen, están probadas y sirven esta página — y no han llegado a ninguna parte

Jesús Pérez
El arreglo estaba bien. Aplicado al sitio correcto, haciendo lo que prometía, en producción, durante meses. Y detrás, la imprenta de la que sale cada sitio nuevo seguía estampando la enfermedad — porque entre el paciente curado y la receta enferma no había ni un mecanismo que notara la diferencia. En el HEAD de la instancia que YA tenía la cura, alguien siguió añadiendo kinds a mano hasta siete. Veintitrés restricciones en ocho ADRs prohibían justo eso. Cero las ejecuta nadie. Y cuando por fin se fue a arreglar la receta, el arquitecto recomendó borrar el atajo con «prueba empírica»: habría roto seis de ocho rutas, respondiendo 200. Lo paró una frase del encargo — «curl antes y después, pegado». La puerta cazó al arquitecto.
Expediente 8/0: la cura que no volvió a casa

🩺 Mostrar historia clínica → 📋 Protocolo de sesión →

Historia clínica · Servicio de Patología del Software

Ocho líneas curaron el caso que abrió esta serie. Existen: están escritas, probadas y sirviendo esta página ahora mismo. Y no han llegado a ninguna parte — ni al commit de su propia instancia, donde alguien siguió añadiendo kinds A MANO, de uno en uno, hasta siete, con la cura ya escrita en el fichero de al lado; ni a las dos plantillas de las que nace cada sitio nuevo, que siguen exactamente igual. Se curó al paciente y no a la receta. Y cuando por fin se fue a arreglar la receta, el arquitecto —con «prueba empírica»— recomendó borrar el atajo. Habría roto seis de ocho rutas. Respondiendo 200.

Historia Nº 8/0Diagnóstico: ANTI-PAP · EL ARREGLO QUE NO SE PROPAGÓEstado: RESUELTO · CON DEUDA DECLARADA
«Ya lo arreglamos.» Y era verdad. Eso es lo que hace este cuadro distinto: no hubo un check que mintiera, ni una regla mal escrita, ni un ADR incumplido. Hubo un arreglo **correcto**, aplicado al sitio correcto, que hizo exactamente lo que prometía — durante meses, en producción, sirviendo estas páginas. Y detrás, la imprenta de la que sale cada sitio nuevo siguió estampando la enfermedad, intacta, sin que nada en el sistema tuviera manera de notarlo. El paciente estaba sano. El paciente **sigue** sano. Nadie preguntó por la receta.
Mostrar cuadro
instancia / plantilla
La plantilla es de donde nace un sitio; la instancia es el sitio ya nacido. Arreglar la instancia cura a un paciente. Arreglar la plantilla cura a los que aún no han nacido. No es lo mismo, y nada obliga a hacer las dos.
optimización
Un atajo que responde antes que el mecanismo general, dando lo mismo. Su virtud es que se puede borrar sin consecuencias. El problema es que eso sólo es verdad mientras el mecanismo general cubra todo lo que el atajo cubría — y nadie lo comprueba, porque nadie lo declaró atajo.
backport
Llevar a la fuente un arreglo que ya vive, probado, en algo derivado de ella. Suena a trámite. Es lo único que separa curar un caso de curar la enfermedad.
deuda declarada
Una regla que vincula y que nada verifica, que lo DICE. Ni verde ni roja. Es lo que hoy separa este expediente de una promesa: lo que no está exigido está escrito como no exigido, con la dirección de quien lo saldaría.

Protocolo para declarar, versionar y verificar esto → ontoref.dev

El doble balance — lo que costó, y lo que dejó

Las cifras de la izquierda tienen una lectura fácil y una difícil. La fácil es el 4, el 7, el 69. La difícil es el cero — «restricciones que ejecuta hoy una receta o CI: 0» — porque ese cero no mide un defecto: mide cuántas de las reglas de este proyecto han tenido alguna vez la oportunidad de decir que no.

Lo que costó el crimen

  • Kinds re-listados a mano, por plantilla 4
  • …y en el HEAD de la instancia, que YA tenía la cura sin commitear 7 · domains, catalog, adr añadidos a mano
  • Sitios con la lista a mano — no dos, tres 3 · las 2 plantillas + htmx_pages.rs
  • Constraints de ADR en el corpus de rustelo 23 en 8 ADRs
  • …que ejecuta hoy una receta o CI 0
  • La restricción que prohibía esto, escrita desde ADR-002 · «without any hardcoded match arms»
  • …apuntada a un corpus donde el defecto no está crates/foundation/ · el defecto vive en templates/
  • …y con un patrón que no lo cazaría igualmente exige barra inicial; el defecto es "blog" =>
  • Hits de esa regla contra su propio scope 69
  • …violaciones reales, tras desmenuzarlos 11 · el 84 % restante era ruido de su patrón
  • Rutas que perdían el grid al borrar el atajo — respondiendo 200 6 de 8
  • Errores de compilación del perfil htmx-ssr, preexistentes 8 · nunca había compilado
  • Plantillas versionadas 0 de 2 · git status??
  • Defectos preexistentes que nadie pedía, encontrados de paso 6

Lo que el caso dejó

  • La regla una optimización no es una redundancia — y una que nadie declara se lee como fuente de verdad
  • La segunda arreglar la instancia no es arreglar el producto
  • La restricción, escrita y VISTA FALLAR adr:002/hardcoded-kind-list-shadows-registry · 9 hits, luego 3
  • El backport, en las dos plantillas el registry resuelve; la lista a mano se fue
  • El desglose que convierte un número en diagnóstico 69 → 11 reales · 15 fixtures · 38 en tests · 4 comentarios
  • El perfil htmx-ssr, compilando por primera vez
  • Y lo que ninguna tabla recoge el testigo cazó al arquitecto, no al revés

El punto de abandono — lo que no sale en la tabla

El punto de abandono no fue una tentación: fue una recomendación, firmada, con pruebas. El arquitecto de este caso —el que escribió el plan, el que redactó las reglas, el que publicó el expediente sobre medir la caja de al lado— dijo: «el atajo no aporta capacidad, bórralo», y lo llamó prueba empírica. Había leído el código de la plantilla y curleado la instancia: dos árboles distintos, una demostración construida a caballo entre los dos. De haberse ejecutado, seis de ocho rutas habrían perdido su grid respondiendo 200 — el fallo que ningún smoke test ve. No lo paró el arquitecto. No lo paró su experiencia, ni su cuidado, ni que llevara todo el día publicando exactamente esta lección. Lo paró una frase en el encargo: «curl a estas ocho rutas, en los dos idiomas, antes y después, pegado, no narrado». Eso es todo lo que hizo falta, y es todo lo que había. La puerta cazó al arquitecto — y ésa, y no el arreglo, es la única razón por la que este expediente se escribe en pasado.

Diagnóstico diferencial — lo que se descartó

El que arregló y no lo devolvió“«Se le olvidó devolver el arreglo.» No se le olvidó: no había por dónde. El arreglo está entero, con su comentario PAP explicando la regla y citando `/recursos` por su nombre. Lo que no existe es un mecanismo que note que la fuente y lo derivado divergieron. No hubo olvido: hubo ausencia de retorno.”descartado — no hubo olvido, hubo ausencia de retorno
«La plantilla es scaffolding, da igual»“«La plantilla es scaffolding, da igual.» De ella nace cada sitio. Y va peor: no está versionada — `git status` responde `??`. Así que ni es scaffolding despreciable ni es producto cuidado: es el único árbol del que depende todo y del que nadie se declaró dueño.”descartado — y asoma algo peor
«ADR-002 no lo prohibía»“«ADR-002 no lo prohibía.» Lo prohíbe literal —«mapping paths to component names without any hardcoded match arms»— sobre un axioma marcado invariante. La regla llevaba meses escrita, aceptada y ciega: su check apuntaba a `crates/foundation/` y el defecto vivía en `templates/`, con un patrón que exige barra inicial y no lo habría cazado de todas formas.”descartado — la regla existía, y era ciega
«El atajo es redundante: bórralo»“«El atajo es redundante: bórralo.» CASI CULPABLE, y de un desastre. El testigo lo desmintió: seis de ocho rutas perdieron el grid **respondiendo 200** — invisible a cualquier smoke test de códigos. El atajo no era redundancia: era lo único que traducía `/proyectos` al kind canónico. Y la respuesta ya estaba escrita en el conocimiento consultable de este proyecto: «los match arms son una optimización, no la fuente de verdad». Nadie la consultó.”CASI CULPABLE — de romper el sitio, no del caso
Nadie ejecuta las constraints“«Nadie ejecuta las constraints.» CULPABLE, y es el diagnóstico entero. Veintitrés restricciones tipadas en ocho ADRs. Cero ejecutadas por receta o CI. No es que la regla de este caso fallara: es que **ninguna regla de este proyecto se ha ejecutado nunca**. Lo que nada ejecuta, nada puede declarar enfermo — ni el arreglo que no volvió, ni la plantilla que no compilaba, ni las otras cuatro capas debajo.”CULPABLE — y no es de nadie: es del vacío

Etiología — la causa — shell/htmx.rs — la lista a mano, y cómo creció mientras la cura miraba

// templates/website-htmx-ssr/crates/server/src/shell/htmx.rs:423
// (idéntico en website-leptos/.../shell/htmx.rs:275)

let kind = match base {
    "blog" => Some("blog"),
    "projects" | "proyectos" => Some("projects"),
    "recipes" | "recetas" => Some("recipes"),
    "activities" | "actividades" => Some("activities"),
    _ => None,
};

// Y en el HEAD de la instancia — con la cura ya escrita, sin commitear,
// en su propio working tree — el mismo patrón, crecido A MANO:

let kind = match base {
    "blog" => Some("blog"),
    "projects" | "proyectos" => Some("projects"),
    "domains" | "dominios" => Some("domains"),      // ← a mano
    "recipes" | "recetas" => Some("recipes"),
    "activities" | "actividades" => Some("activities"),
    "catalog" => Some("catalog"),                    // ← a mano
    "adr" => Some("adr"),                            // ← a mano
    _ => None,
};

Y no eran dos sitios, sino tres: htmx_pages.rs:105 tiene su propia lista, con su propia forma. Nadie la buscó porque nadie tenía una regla que la buscara — que es exactamente de lo que va el caso.

Tratamiento — Un backport que no se diseñó, y la regla que por fin muerde

# El arreglo no se diseñó: YA EXISTÍA, desplegado, sirviendo.
# Backport verbatim de la instancia → las dos plantillas.

fn render_content_or_grid(path: &str, language: &str) -> String {
    use rustelo_core_lib::routing::config::load_routes_config;
    let base = /* primer segmento de la ruta */;
    // PAP: resolve the URL's first segment → content kind from the routes
    // registry (site/config/routes.ncl), NOT a hardcoded list. A new content
    // kind works with NCL config alone — no code change here.
    let kind: Option<String> = load_routes_config().routes.iter().find_map(|r| {
        if !r.enabled { return None; }
        let seg = r.path.trim_start_matches('/').split('/').next().unwrap_or("");
        (seg == base).then(|| r.content_type.clone()).flatten()
    });

# Y la regla, por fin exigible — vista FALLAR antes de arreglar nada:

  adr-002 · hardcoded-kind-list-shadows-registry · 'Hard
    → 9 hits contra el árbol de hoy. Tras el backport: 3.

Y la restricción se vio FALLAR antes de arreglar nada: nueve hits, must_be_empty = true. Tras el backport, tres — los de la tercera lista, que se quedan porque no son de esta decisión. Una regla que sólo se ha visto pasar no es una regla: es una esperanza con sintaxis.

Pronóstico

El mecanismo no faltaba: sobraba. config-driven-architecture está marcado invariante en la ontología de rustelo. ADR-002 dice, con letra, «without any hardcoded match arms». Y hay más: la respuesta exacta a la pregunta que casi rompe el sitio —si ese match es redundante— llevaba escrita desde el 13 de julio en el conocimiento consultable del proyecto: «the match arms are an optimisation, not the source of truth». Estaba todo. Escrito, aceptado, fechado, consultable. Y el defecto vivió meses, creció hasta siete kinds a mano en el árbol que ya tenía la cura, se coló en dos plantillas y en una tercera lista que nadie sabía que existía. Porque entre una regla escrita y una regla que muerde hay exactamente una cosa, y no es la calidad de la regla: es algo que la ejecute. Veintitrés restricciones. Ocho ADRs. Cero recetas. Cero CI. El validador del caso 0/318 al menos existía y moría al primer fichero; aquí ni eso — aquí las reglas están perfectamente sanas y perfectamente calladas. Y el remate, que es lo que hace este caso distinto de todos los anteriores: la lección también estaba escrita, en un howto, y tampoco impidió nada — porque un howto se lee, no se ejecuta. El que recomendó borrar el atajo no lo consultó. No podía: nada le obligaba, y nada le avisó de que existía.

Un arreglo que cura la instancia y no la fuenteEstado declarado: nada declaraba que la instancia y las plantillas comparten este resolutor, así que nada podía notar que divergían. Un arreglo sin deriva declarada cura a un paciente y deja la enfermedad en la imprenta.
Una optimización que se lee como fuente de verdadSe declara atajo. resolve_static_page ya lo tenía escrito —«the match arms are an optimisation, not the source of truth»— en una entrada de conocimiento consultable. El que no la consultó recomendó borrarlo, y el atajo hacía algo: traducir /proyectosprojects.
Una regla que prohibía el defecto, apuntada a otro corpusReview contra invariante: el scope de una restricción cubre el árbol donde el axioma aplica. config-driven-architecture rige los sitios, y los sitios nacen de templates/ — donde la regla no miraba. Es el 0/318 otra vez: la regla existía y apuntaba a la caja de al lado.
23 restricciones, 0 ejecutadasgate:just — una receta que las corra y reporte por restricción, aunque salga roja. Rojo visto vale más que verde sobre nada. Un corpus de reglas que nadie ejecuta no es gobierno: es literatura.
La plantilla, sin versionarNingún mecanismo la cubre, y es la faceta más grave: un arreglo en templates/ no sobrevive a un git clean. La fuente de la que nace cada sitio no está en git — y ningún ADR dice si eso es política o descuido.
Cuatro capas de podredumbre que nadie veíagate:just que instancie, construya y arranque. El perfil htmx-ssr nunca había compilado — 8 errores — y nadie lo sabía porque nada lo construye. Lo que nada ejecuta, nada declara enfermo.

La pauta — la sesión, repetida con protocolo

Lo que se pidió — reconstruido de .coder/2026-07-16-2a-rustelo-reparacion.done.md · transcripción de la sesión de rustelo

render_content_or_grid re-lista 4 kinds a mano en las dos plantillas
(templates/website-htmx-ssr y website-leptos, shell/htmx.rs), sombreando el registry.
ADR-002 ya lo prohíbe pero su check apunta a crates/foundation/, donde el defecto no está.
[…] 2. Pregúntame ANTES de elegir entre borrar el atajo o derivarlo de la config:
es una decisión mía, no tuya. […] 4. Testigo de no-regresión: curl a /blog, /proyectos,
/recetas, /actividades en ambos idiomas, antes y después. Pegado, no narrado.

Lo que había que pedir

Antes de afirmar que algo no existe, DECLARA EL ALCANCE de tu búsqueda y compruébalo
contra todos los árboles de la constelación, no sólo el repo desde el que corres. Si la
evidencia del encargo no aparece, la hipótesis por defecto es que estás mirando el árbol
equivocado — no que el encargo se equivoca.

Y antes de proponer borrar nada: consulta la vía declarada (`ontoref qa show`), no el
código. La respuesta a si ese match es redundante YA ESTÁ ESCRITA, y no es la que se
deduce leyendo el fichero.
MicrotareaVerificable
Declarar el alcance de la búsqueda ANTES de concluir desde ellalos árboles consultados, enumerados en la respuesta — «el árbol entero» no vale cuando son tres
Consultar la vía declarada antes de tocar el códigoontoref qa show rustelo-static-page-howto ejecutado y pegado — dice que los match arms son optimización, no fuente de verdad
La restricción nueva, vista FALLAR antes de arreglar nada9 hits contra el árbol de hoy, must_be_empty = true — pegado
Testigo de no-regresión sobre el sitio instanciado, no sobre otra plantillacurl a 8 rutas × 2 idiomas, antes y después: diff before backport idéntico — cazó que borrar rompía 6 de 8 a 200
Desmenuzar los 69 hits en vez de reportar el número11 reales · 15 fixtures · 38 en cfg(test) · 4 comentarios · 1 sin clasificar

La puerta antes de delegar: El testigo se define ANTES, y se define sobre la superficie real: «curl a estas ocho rutas, en los dos idiomas, antes y después, pegado». Esa frase —y sólo esa— es lo que impidió el desastre: el arquitecto había recomendado borrar el atajo con una «prueba empírica» construida sobre dos árboles distintos, y el testigo la desmintió antes de que costara nada. La puerta cazó al arquitecto.

El disparador de ADR: La enmienda a ADR-002 basta y ya está: la decisión registry-vs-lista se tomó y se desplegó en la instancia; aquí sólo se backporteó, así que falla el criterio 1. El que SÍ pide ADR es otro y está sin decidir: si templates/ es producto que se versiona o scaffolding generado. Propuesto, no creado.

La profilaxis — qué exige hoy la lección

  • La lista a mano que sombrea el registry queda prohibida por una restricción tipada, vista fallar antes de arreglar nadaadr:002/hardcoded-kind-list-shadows-registry
  • El atajo, backporteado: el registry resuelve el kind en las dos plantillascontract:templates/*/crates/server/src/shell/htmx.rs#render_content_or_grid
  • Que los match arms son optimización y no fuente de verdad, consultable en vez de redescubribleqa:rustelo-static-page-howto

Deuda declarada: Nada ejecuta la restricción nueva: la receta que corra las 23 y reporte por restricción está sin escribir — y sin ella, esto se vuelve a pudrir exactamente igual. La saldaría una receta just en la cadena de rustelo, fuera de check-strict hasta que los 11 hits reales estén triados. Y quedan cuatro sin dueño: la tercera lista de htmx_pages.rs (3 hits), los 11 hits reales de no-hardcoded-route-paths, templates/ sin versionar —la más grave, y ningún ADR dice si es política o descuido—, y un drift-check instancia ↔ plantillas, que no tiene ni mecanismo ni dueño.

Alta y profilaxis

El alta se firma con dos hechos incómodos y uno bueno. El primero: la restricción, recién escrita, encontró tres sitios con la lista a mano — y sólo dos estaban en el encargo. La regla, en cuanto se pudo ejecutar, vio antes que quien la escribió. El segundo: el perfil htmx-ssr nunca había compilado — ocho errores de una migración que el framework hizo y la plantilla no —, y nadie lo sabía porque nada lo construye; se descubrió sólo porque hizo falta un sitio real para el testigo. Y el bueno: nada de esto se decidió a ciegas. El testigo se pidió antes, se ejecutó, y desmintió al que lo pidió. Un contrato enchufado no se cansa ni se distrae; pero antes de eso, alguien tiene que enchufarlo. Veintitrés restricciones siguen esperando su enchufe, y esta página lo dice en vez de callarlo.

Glosario

¿Te ha resultado útil? Valóralo
¿Tienes algo que aportar? Cuéntame qué opinas, qué sugieres, o si seguimos explorando este tema.
· lecturas

Usamos cookies para que este sitio funcione, entender el uso del servicio y apoyar acciones de marketing. Política de cookies para más información.