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
🩺 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.
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,adrañ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 entemplates/ - …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 fuente | Estado 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 verdad | Se 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 /proyectos → projects. |
| Una regla que prohibía el defecto, apuntada a otro corpus | Review 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 ejecutadas | gate: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 versionar | Ningú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ía | gate: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.
| Microtarea | Verificable |
| Declarar el alcance de la búsqueda ANTES de concluir desde ella | los árboles consultados, enumerados en la respuesta — «el árbol entero» no vale cuando son tres |
| Consultar la vía declarada antes de tocar el código | ontoref 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 nada | 9 hits contra el árbol de hoy, must_be_empty = true — pegado |
| Testigo de no-regresión sobre el sitio instanciado, no sobre otra plantilla | curl 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úmero | 11 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 nada
adr:002/hardcoded-kind-list-shadows-registry - ✓El atajo, backporteado: el registry resuelve el kind en las dos plantillas
contract: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 redescubrible
qa: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
Sin coincidencias.