Expediente 69/58: el espejo que revertía su propio trabajo
La herramienta decía 69. El disco tenía 58. Nadie comparaba las dos cosas — durante cuatro semanas
🩺 Mostrar historia clínica → 📋 Protocolo de sesión →
Historia clínica · Patología del Software
El paciente no tenía síntomas. Publicaba sin errores, la herramienta confirmaba el éxito, los logs salían en verde. Llevaba cuatro semanas publicando en el vacío.
Mostrar cuadro
- PAP
- Project's Architecture Principles — las reglas que sostienen la arquitectura: la fuente única que todo el código debe respetar.
- anti-PAP
- Código escrito en contra de esas reglas. Aquí: un mirror que decidía por dirección en vez de por propiedad.
- el check decide, nunca el reporter
- ADR-066, aceptado el 4-jul-2026: donde exista una comprobación mecánica, decide la comprobación; la palabra de quien informa puede negarse a ser contradicha, jamás imponerse. Este expediente es lo que ocurre en la única superficie donde esa regla no se aplicó.
- testigo
- Una constancia verificable de forma independiente de que algo es lo que dice ser. Un backup sin testigo no es un backup: es una copia en la que confías.
Protocolo para declarar, versionar y verificar esto → ontoref.dev
El doble balance — lo que costó, y lo que dejó
Lo que costó el crimen
- Lo que la herramienta informaba "index.json with 69 posts"
- Lo que había en el disco 58
- ADRs escritos y jamás publicados 11 (059→069)
- Ventana de invisibilidad 13-jun → 11-jul (fechas de los propios ADR)
- Contenido llegado a producción en ese tiempo 0
- Ficheros que el repo del site tenía trackeados 1 (README.md)
- git_sha estampado en TODOS los packs de deploy 5619a40 (siempre el mismo)
- Líneas de trabajo vivas sin un solo testigo 214
- Comandos de distancia hasta borrarlas 1 (<span class='mono'>just templates</span>)
- Tamaño del arreglo final 60 <i>líneas</i>
Lo que el caso dejó
- Decisión ADR-070 (aceptado)
- Gates adr-check · templates-check · posts-check
- Migración 0044 (rutas ancladas)
- La ranura que faltaba site/_htmx/templates/
- Restricciones curadas al anclar 29 (88✓ → 117✓)
- Ontología el converso en enforcement-vs-emergence
El punto de abandono — lo que no sale en la tabla
El punto de abandono de este caso no llegó con un error, sino con una frase: «he ido crate por crate por un sistema de codegen complejo y estoy muy pasado del punto razonable para seguir a ciegas». Es lo que dice una máquina cuando se le acaba el mapa y sigue caminando. Y la respuesta que lo desbloqueó no fue técnica: fue que alguien dibujara la jerarquía de niveles —rustelo → website-htmx-rustelo → outreach/site— que existía en una cabeza y en ningún fichero. Ese es el diagnóstico entero, dicho antes de tenerlo: lo que no está declarado, alguien lo sostiene con su memoria. Y la memoria se cansa.
Diagnóstico diferencial — lo que se descartó
| Los 6 huérfanos en site/r | “Sí, soy contenido muerto que sigue sirviéndose. Pero yo no impido publicar.” | síntoma, no causa |
| gen-adr-pages roto | “Yo funciono perfectamente. Genero los 69 ADR cuando alguien me llama.” | inocente — nadie lo llamaba |
| content_processor no procesa expedientes | “Sí los proceso. Lo que pasa es que me leíste con un head -20 y te cortó la salida.” | coartada firme (el error fue del investigador) |
| El índice no se regenera | “Me regenero cada vez. Con 69. Lo que pasa después no es cosa mía.” | inocente — y la clave del caso |
| El generador del grafo | “Yo exporto lo que hay en el árbol de contenido. Si me metéis un contrato entre los datos, reviento. Y hago bien.” | inocente — tenía razón |
Etiología — la causa — El mirror que decidía por dirección, no por propiedad
# La receta `content`, tal y como estaba: content_processor # escribe los índices en site/r rsync -a "site/public/r/" "site/r/" # ← el arma # Dos árboles. Cada uno DUEÑO de ficheros distintos: # site/r ← content_processor escribe aquí (lo lee el servidor) # site/public/r ← los generadores nu escriben aquí (y es lo que EMBARCA el deploy) # # El rsync corría en un solo sentido, y en el sentido equivocado: # · pisaba los índices recién generados con los fósiles del árbol de deploy # · y NUNCA llevaba el contenido nuevo al árbol que viaja a producción # # rsync -a sin --update no respeta "el destino es más nuevo": compara tamaño y # mtime, y si difieren, COPIA LA FUENTE. Un mirror cuya fuente se ha fosilizado # no es un no-op: es una máquina de resucitar estado viejo. # # La herramienta imprimía "Generated index.json with 69 posts". # El disco se quedaba con 58. # Nadie comparaba las dos cosas.
just — receta content
Tratamiento — El mirror se tipa por propiedad, y el check lee el disco
content_processor
# Índices de contenido → árbol de deploy. --delete: sin él, un slug renombrado
# sobrevive como URL viva EN PRODUCCIÓN.
rsync -a --delete \
--exclude about.json --exclude adr-map.json \
--exclude taglines.json --exclude content_graph.json \
"site/r/" "site/public/r/"
# Los cuatro artefactos nu → árbol servido. Van excluidos del tramo de ida, o las
# copias viejas de site/r machacarían los originales recién generados.
for f in about.json adr-map.json taglines.json content_graph.json; do
cp -f "site/public/r/$f" "site/r/$f"
done
# Y tres gates que ASERTAN CONTRA EL DISCO, nunca contra el stdout de la herramienta:
# adr-check el spine no tiene ningún ADR que el site no publique
# templates-check el árbol ensamblado se reproduce exacto desde su fuente declarada
# posts-check linkify + cobertura del grafo
#
# Los tres se pueden ejecutar MIENTRAS el daño sigue siendo hipotético.
# Un check que solo puedes correr después del paso destructivo no es un gate:
# es una autopsia.
just — receta content
Pronóstico
La serie hace una pregunta: ¿con ontoref no hubiera sido así? Casi siempre la respuesta es sí, y por eso hay serie. Este expediente es el que responde que NO — y por eso es el que más vale. Esto pasó DENTRO de ontoref, con la ontología cargada, los gates escritos y ADR-066 —«el check decide, nunca el reporter»— aceptado una semana antes y probado con una ejecución de falsación de ocho caminos. La regla que nombra exactamente esta patología ya existía mientras el pipeline que publica ontoref al mundo seguía dejando decidir al reporter. El protocolo no falló: es que la difusión era el punto ciego, la única superficie que el protocolo no gobernaba. Cada mecanismo que habría atrapado cada uno de estos fallos ya existía y estaba aceptado en este mismo repo. Ninguno estaba apuntado aquí. Y esa es la respuesta útil, la que un caso cómodo no da: no basta con tener el mecanismo — hay que apuntarlo a la superficie que duele. Un enforcement en el que no puedes confiar que falle es indistinguible de no tener enforcement, y es más peligroso, porque se cree.
| El mirror corría por dirección, no por propiedad de cada fichero | estado declarado → ADR-070: todo árbol declara su DUEÑO |
| La herramienta informaba éxito y nadie comprobaba el disco | review contra invariante → ADR-066: el check decide |
| Un generador existía y ninguna receta lo llamaba (11 ADR invisibles) | estado declarado → ADR-070: la REPRODUCCIÓN, en la cadena |
| 214 líneas vivían en el único árbol que el build borra | estado declarado → ADR-070: todo árbol declara su TESTIGO |
| Un contrato archivado entre los datos que tipa (just graph muerto) | PAP + anti-pattern → contract-in-the-instance-tree |
| Un check significaba una cosa según desde dónde lo lanzaras | review contra invariante → migración 0044: rutas ancladas |
La pauta — la sesión, repetida con protocolo
Lo que se pidió — reconstruido de refs:sessions/2026-07-12-expedientes-render-roto — extracto acotado en custodia, con el prompt verbatim y la cifra que lo respalda (23 `<h1>` frente a 0). La URL que el prompt lleva dentro nombra este mismo caso, que es lo que hace incuestionable el emparejamiento.
qué pasó con este contenido que sale mal ? http://localhost:3030/expedientes/casos/el-espejo-que-revertia-su-trabajo
Lo que había que pedir
Algo se ve mal en una página. Empieza por la página, no por la fuente. 1. Reproduce el defecto sobre lo SERVIDO y cuéntalo con un número, no con «se ve raro». Un síntoma sin cifra no se puede dar por resuelto después. 2. Contrasta contra un caso que sí funcione: la diferencia entre los dos es el hallazgo, y ahorra la mitad de las hipótesis. 3. Antes de tocar nada, pregunta de qué árbol lee el servidor. Si el fichero está bien en disco y mal en la página, el defecto no está en el fichero. 4. Y si el arreglo pasa por una copia entre árboles, di ANTES quién es dueño de cada fichero. Una copia por dirección revierte trabajo ajeno sin decirlo.
| Microtarea | Verificable |
| Medir el síntoma en la página servida, no en el fuente | curl -s --compressed localhost:3030/expedientes/casos/el-espejo-que-revertia-su-trabajo | grep -c '<h1>' — 23 aquí, 0 en el caso que renderiza bien |
| Aislar la causa comparando con el caso sano | el mismo grep sobre la-deriva-hardcoded: si da 0, la diferencia está en el contenido del <pre>, no en la plantilla |
| Nombrar de qué árbol lee el servidor antes de copiar nada | grep -r SITE_SERVER_ROOT_CONTENT justfile — el servidor lee site/public/r, no site/r |
| Declarar la propiedad de cada fichero antes de espejar | just cases-check compara los dos árboles y falla si un caso está en el canónico y no en el servido |
| Comprobar que el generador que arregla esto lo llama alguien | grep -n expedientes justfile — un generador fuera de la cadena es un generador que nadie ejecuta |
La puerta antes de delegar: Antes de soltar al agente sobre un «se ve mal», el síntoma tiene que estar convertido en un número medido sobre la PÁGINA, y tiene que estar escrito de qué árbol lee el servidor. Sin lo primero no hay forma de saber cuándo parar; sin lo segundo, cualquier arreglo se copia al árbol equivocado y desaparece en el siguiente build sin avisar.
El disparador de ADR: Sí, y dos: la regla de que un espejo se tipa por PROPIEDAD de cada fichero y no por dirección (ADR-070), y la de que el único árbol que existe es el que se sirve (ADR-072). Las dos salieron de este caso — y ninguna de las dos se declaró en la sesión que las descubrió, que es por lo que hubo un 63/0 después.
La profilaxis — qué exige hoy la lección
- ✓Cada árbol de entrega declara dueño y testigo; un mirror corre por propiedad, no por dirección
adr:070/mirror-typed-by-ownership - ✓Un generador que ninguna receta llama no existe: entra en la cadena
adr:070/generator-in-the-chain - ✓El contrato vive fuera del árbol de instancias que tipa
adr:070/contract-in-the-instance-tree - ✓El árbol servido es el único que existe, y el check compara los dos
adr:072/the-served-tree-is-the-only-one-that-exists - ✓En la cadena, con su caso negativo por eje
gate:just cases-check
Del vocabulario del proyecto (4)
- Gate
- Prerrequisitos tipados y políticas que controlan las transiciones de estado de la máquina de estados de un proyecto.
- PAP
- Project's Architecture Principles (Principios de Arquitectura del Proyecto).
- anti-PAP
- Enfoque que viola frontalmente el PAP del proyecto.
- ontoref
- El protocolo en sí: una superficie tipada y consultable en la que un proyecto declara LO QUE ES (ontología) y CÓMO ACTÚA (reflexión), de modo que una afirmación sobre el proyecto pueda ser contradicha por una máquina y no sólo por un lector.