From e7225bc758fe9c09ee48b2028f7c37908c43f51e Mon Sep 17 00:00:00 2001 From: sirxavor Date: Wed, 19 Aug 2026 19:39:55 +0200 Subject: [PATCH] =?UTF-8?q?docs(metodo):=20re-estampar=20el=20M=C3=A9todo?= =?UTF-8?q?=20v2026-08-19=20(cutover=20del=20m=C3=A1ster)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La fuente del Método pasa del árbol de trabajo a la matriz (repo `metodo`). Esta copia deja de llevar los 10 raíles base EN UNA LÍNEA con un puntero a `redes/workflows/metodo.md` — nombre viejo, ruta que en un clon ajeno no existe. Ahora los raíles viajan COMPLETOS, con sus cicatrices, más el §0 «si no escala no vale para nada» y el raíl de §8 del 2026-08-19 («un fallo benigno solo protege si alguien LEE la línea»). Las 6 adiciones que estaban «pendientes de inlinar» viven ya dentro de su raíl. Estampado con `metodo update`: byte a byte idéntico al máster. Co-Authored-By: Claude Opus 5 (1M context) --- .metodo/CHECKSUMS | 4 +- .metodo/VERSION | 2 +- .metodo/bin/metodo | 9 +- .metodo/metodo.md | 1198 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 1150 insertions(+), 63 deletions(-) diff --git a/.metodo/CHECKSUMS b/.metodo/CHECKSUMS index e7158c5..f7ce312 100644 --- a/.metodo/CHECKSUMS +++ b/.metodo/CHECKSUMS @@ -1,2 +1,2 @@ -4e1afa3b3338bc9ffa97da89559ed56fab05f799281a202cdc286cd527dca8f8 metodo.md -3046e8f13185e4c6a97aaccedeb2f81a958bdc7ceb6b395d411b743c86950f50 bin/metodo +37cd8e9792f5cad5e57827068fe6cb00ca5602eca7692ef2ca943a48806bc8a8 metodo.md +653e618441c519689753e709df9c8e886db78d84faab4d55167883c5026872a9 bin/metodo diff --git a/.metodo/VERSION b/.metodo/VERSION index ff90776..cc3bc00 100644 --- a/.metodo/VERSION +++ b/.metodo/VERSION @@ -1,5 +1,5 @@ # Copia vendorizada del Método (raíl C11). Gestionada por 'metodo init/update'. master: metodo -version: 2026-08-15 +version: 2026-08-19 estado: vendored adopted_commit: 6a10eab342a477846ab5f3e67f8ab17169f12807 diff --git a/.metodo/bin/metodo b/.metodo/bin/metodo index aced463..8857129 100644 --- a/.metodo/bin/metodo +++ b/.metodo/bin/metodo @@ -154,8 +154,13 @@ check_commits() { # ── plantillas de doc/ (sólo se crean si faltan — brownfield: no pisar) ─────── _scaffold_docs() { # $1 = repo destino - # Si el repo ya tiene casa de docs propia (doc/ o docs/), NO scaffoldear: no duplicar estructura. - { [ -d "$1/doc" ] || [ -d "$1/docs" ]; } && { echo " (doc/ o docs/ ya existe → no scaffoldeo el corpus)"; return 0; } + # La casa de docs es UNA y se llama doc/ (el linter ancla ahí el corpus de estado). + [ -d "$1/doc" ] && { echo " (doc/ ya existe → no scaffoldeo el corpus)"; return 0; } + # Una casa con otro nombre NO se duplica en silencio: se pide el rename (cicatriz: kubernetes + # tenía documentacion/, el guard no la conocía y el init dejó DOS carpetas de documentación). + local alt; for alt in docs documentacion documentation; do + [ -d "$1/$alt" ] && { echo " ⚠ $alt/ existe pero la convención es doc/ — haz 'git mv $alt doc' (si no, el linter no ve el corpus de estado). No scaffoldeo."; return 0; } + done local d="$1/doc"; mkdir -p "$d" [ -f "$d/backlog.md" ] || cat > "$d/backlog.md" <<'EOF' # Backlog diff --git a/.metodo/metodo.md b/.metodo/metodo.md index 423be9c..f1889a2 100644 --- a/.metodo/metodo.md +++ b/.metodo/metodo.md @@ -1,33 +1,1153 @@ # El Método — constitución -> **Versión 2026-08-15.** Documento **autocontenido**: es lo que se vendoriza, byte a byte, a cada +Principios que valen para **cualquier** sesión (exploración, ejecución, despliegue, +incidente, orquestación) y para cualquier backlog. Los workflows concretos referencian este +doc en vez de repetirlo. No son teoría: cada uno lleva el caso real que lo enseñó. + +> **Versión 2026-08-19.** Documento **autocontenido**: es lo que se vendoriza, byte a byte, a cada > repo (no lleva enlaces a rutas de un árbol concreto, para que funcione clonado en solitario). > Cada regla lleva su **cicatriz** (meta-principio: un raíl se escribe con el incidente que lo > enseñó, no a priori). Procedencia, fuerza (nº de repos) y partición linter/juicio: fichero > `constitucion-de-facto.md` de la matriz. > -> ℹ️ Los 10 raíles base van hoy **en una línea** (su texto completo con cicatrices está pendiente de -> inlinar); lo promovido de las cosechas (2026-08-08 y 2026-08-15) va **completo**. El árbol `redes` -> conserva su copia `redes/workflows/metodo.md` hasta que se desmantele. +> ⛔ **Este fichero ES la fuente**, y vive en el repo `metodo` (la matriz). Se reparte con +> `metodo update`, que lo copia byte a byte a `.metodo/metodo.md` de cada repo. **La copia no se +> edita nunca** (§7): se edita aquí y se re-estampa. *(Cutover 2026-08-19: hasta ese día la fuente +> era la copia del árbol de trabajo y esta constitución llevaba los 10 raíles base **en una línea**, +> con un puntero a una ruta que en un clon ajeno no existe. Coste medido: el raíl nuevo de §8 —«un +> fallo benigno solo protege si alguien LEE la línea»— nació el 2026-08-19 y **no estaba en ninguna +> de las 18 copias**. Ahora los raíles viajan COMPLETOS y las adiciones que estaban «pendientes de +> inlinar» viven dentro de su raíl.)* -## Raíles base (1–10) +> Origen: destilados de la racha Hermes de julio 2026 (Fases 11/13, convergencia UI) **unificados +> con el método ya afinado en los workflows de Solidaria** (la fuente más madura). La lección de +> fondo: **una UI/gate honesto actúa como auditor del resto del sistema** — casi todos los raíles de +> abajo nacieron de un "verde que mentía" cazado al construir bien la capa de encima. +> +> **Meta-principio (de Solidaria): cada raíl se escribe CON LA CICATRIZ.** El humano (o un gate) +> caza lo que no cuadra → el raíl se escribe con el caso real que lo enseñó → no vuelve a pasar. Por +> eso cada punto lleva su ejemplo: sin la cicatriz, un principio abstracto no se respeta. Este doc +> crece añadiendo cicatrices, no teoría. -1. Verifica el **RESULTADO**, no la intención (testigo independiente que sabe ponerse rojo). -2. El dato **CRUDO**, no el cocinado (backend mide, consumidor pinta). -3. **Mutation testing**: ataca el CABLEADO, no las funciones puras. -4. Ejercita el **FLUJO REAL** (abre el navegador, toca el device). -5. No inventes estados ni fallos **no medidos** ("sin dato", degrada a gris no a rojo). -6. Verifica la **PREMISA** del prompt (enmendar es SUSTITUIR; premisa falsa = PARAR). **← +C34** -7. **Fuente única** + copias vendidas + red anti-deriva (`--check`). -8. **Fallo benigno EXPLÍCITO**, nunca degradación silenciosa. -9. **Sesiones frescas** por stack; la continuidad la lleva el orquestador. -10. Lo que **"funciona por casualidad"** está roto. ---- +## 0. Si no escala, no vale para nada *(Xavier — la máxima que gobierna a las demás)* -## Raíles nuevos (promovidos 2026-08-08) +> *«Estamos en Kubernetes, GitOps con Helm, y lo estamos haciendo así **pensando en la masa**: masa +> de edges, masa de hubs, redundancia barata. La clave es que sea **desechable y barato de +> levantar**. Alquilar un VPS es barato; poner un Kubernetes en el edge con IP pública y un nombre +> de dominio, también. Si dejo escritos los manifiestos, puedo levantar **todos los hubs que quiera +> y todos los edges que quiera** de manera masiva. **No quiero cosas difíciles ni centralizables.**»* -### 11. El orden de lo irreversible: añadir antes de quitar · *C14 (4 fuentes)* +⇒ **Y la consecuencia sobre la seguridad, que es la que más cambia el criterio**: *«si me capturan un +Vault me doy por jodido, pero si me capturan un token lo revoco, **tiro el hub a la basura y levanto +otro**»*. La respuesta a un compromiso **no es prevenirlo a cualquier precio: es que reponerlo sea +barato**. Un componente que hay que defender porque cuesta reconstruirlo ya ha fallado el criterio. + +**Cómo se aplica, y en qué se nota:** +- ⛔ **Lo que no se puede levantar en masa desde manifiestos, no vale** — aunque funcione. Un paso + manual por instancia es un tope de escala disfrazado de detalle. +- ⛔ **Lo centralizable se mira dos veces**: un secreto compartido por todas las instancias, un + registro único, un nodo del que dependen los demás. Cada uno convierte «tiro uno y levanto otro» + en «tengo un problema». +- ⭐ **Y la pregunta antes de aceptar cualquier mecanismo**: *¿esto sigue funcionando con **cien**?* + Si la respuesta exige un gesto humano por instancia, no escala — y por `§0` **no vale**. + *(Familia de `A20`: la colisión de loopbacks era 1,9 % a 50 nodos y **26 % a 200**; lo que a escala + de banco es una anécdota, a escala de flota es el trabajo de una tarde.)* + +⚠️ **Y lo que NO cubre lo desechable, dicho una vez para que conste**: tirar un componente **no +deshace lo que ese componente FIRMÓ o PUBLICÓ mientras estaba vivo**. Un artefacto firmado sobrevive +a su emisor —se propagó, es monótono y no caduca—, así que ahí la reposición barata no es la +respuesta. Es el único sitio donde hay que mirar el ciclo de vida de la **credencial**, no el de la +**máquina**. + + +### ⭐ La calibración: contra qué se mide «suficientemente bueno» *(Xavier, 2026-08-18)* + +Antes de proponer una fortificación, **el punto de comparación no es un ideal: es lo que hoy se usa +en su sector**. Y es esto, dicho por él: + +> *«Tengo aquí cifradores físicos del trabajo que **no son tan complicados como este sistema, y mucho +> menos útiles y mucho menos seguros**. Llevan tokens físicos, y si alguien pierde uno se lía de tal +> magnitud que hay que **llevarlos todos a que les cambien las claves**. Y en el sitio donde trabajo +> **a nadie se le ha pasado por la cabeza que capturen en la vida el cifrador expuesto a internet** — +> y a mí también me cuesta. **Y los tenemos.**»* + +⇒ **Dos consecuencias, y las dos cortan discusiones enteras:** +- **Sobre el coste de recuperarse**: *«si hay que hacer 1.000 o 10.000 ceremonias, sigue siendo + infinitamente más barato que lo que tendríamos que hacer hoy si nos capturaran un cifrador físico + edge»*. ⇒ Un procedimiento de reposición **caro pero existente** ya es una mejora enorme sobre el + estado del arte, que es **no tener ninguno**. No se compara contra cero coste: se compara contra + *«llevarlos todos al taller»*. +- **Sobre qué amenazas modelar**: la captura del nodo expuesto a internet **no está en el modelo de + nadie** en su sector, con equipos que protegen más y valen más. ⇒ Insistir en ella aquí no es rigor: + es **gastar el foco de Xavier en el escenario que su propio sector descarta**, y él ya lo aplazó + explícitamente tres veces el mismo día. + +⛔ **Regla operativa**: antes de escribir *«pero si te comprometen X…»*, pregúntate **contra qué lo +comparas**. Si la alternativa real que Xavier tiene hoy es peor, la propuesta no mejora nada — solo +añade dificultad, y eso lo prohíbe `D23`. + + +⇒ ⛔ **Y el error que esto evita, cometido CUATRO veces el 2026-08-18: una decisión lleva ÁMBITO DE +AMENAZA, y leerla fuera de su ámbito la hace parecer mala.** *(Xavier, ordenando: «no estoy diciendo +que no valore la captura de un hub — lo pondremos en el backlog por detrás, con su apartado. Ahora +estamos en el apartado de **“y si capturan un edge”**, y para eso hay una CRL y unos procedimientos +que mis equipos de hoy **ni tienen**».)* La orquestadora evaluó `D25` —del apartado del **edge**— +contra la captura de un **hub**, y la decisión salía mal cuatro veces seguidas. **No estaba mal: se +estaba midiendo con la vara de otra sección.** +⇒ **Regla**: antes de objetar, pregúntate **de qué apartado es la decisión**. Si tu objeción vive en +otro, no es una objeción: es una **entrada para ese otro apartado** — y se escribe allí, no aquí. +⭐ Y el corolario que lo hace útil en vez de burocrático: **objetar sigue siendo bienvenido**. Lo que +Xavier gestiona no es si la objeción vale, es **su alcance y su prioridad en el backlog**. +⭐ **Y el porqué, en sus palabras: «buscamos MEJORAR, no la perfección desde el principio».** Una +mejora entregada bate a una solución completa que no existe todavía — y la que no existe **cuesta +foco**, que es el recurso escaso. ⇒ Lo que convierte esto en método y no en excusa es lo de siempre: +la mejora se **mide** y lo que queda fuera se **escribe** (un ⏸️ con su motivo, como el aplazamiento +de la captura de hub), para que nadie lo confunda con un olvido. + +## 1. Verifica el resultado, no la intención + +Comprueba lo que el sistema **sirve/hace**, no lo que su fuente **dice** que hará. Una señal +heredada, un booleano cocinado o "el comando salió 0" no son prueba de la operación de ahora. + +- El *verify version-aware* del deploy: `/health.version` == el digest pineado, no "el pod + arrancó". +- El `state` del DNS en el hub se calcula contra **el fichero de zona que dnsmasq parsea**, no + contra la BD otra vez (es el único sitio donde se ve un `write_zone` fallido o un soft-state + caducado que sigue resolviendo). +- El snapshot de BD se acepta tras `pg_restore --list`, no tras "pg_dump salió 0". +- ⛔ Antipatrón: "redes placebo" — dar por buena una operación por una señal que no la mide. + +⭐ **Predice también lo que NO debe cambiar, y compruébalo.** Es lo que convierte un aviso en un +hecho. *(F11.13: meter una LAN en la malla «toca el firewall, como en F9.2 que tiró el acceso de +gestión». En vez de ir con cuidado, se **predijo** que el `md5` de `uci show firewall` no cambiaría +—solo `ospfd.conf` gana una línea `network`— y se midió antes y después: idéntico. El miedo pasó a +ser un invariante verificado.)* + + +⛔ **Una partición se verifica por SUS DOS CARAS: lo que debe estar cerrado Y lo que debe estar +abierto.** Comprobar solo la mitad cerrada **no distingue «bien particionado» de «todo roto»** — y +en autorización eso es peligrosísimo, porque el fallo se disfraza del resultado bueno. *(Cicatriz +2026-07-27: `athena-hub` daba **401 en toda la administración** y parecía protegida. **No lo estaba: +estaba rota.** Su Ingress reclamaba `/api`, que una imagen pre-F1 no sirve, así que el catch-all +autenticado se tragaba también **`/links/pair`** — el mismo accidente que cerraba `/admin/pair` +impedía que **ningún device pudiera emparejarse**. Se vio al medir la cara abierta: `/links/pair` +pasó de `401` a **`422` con cuerpo de la aplicación**.)* +⇒ Es la misma familia que *«un panel que muestra "nada" y uno que no puede ver nada se leen igual +desde fuera»* (F10.3), aplicada a la autorización. **Un `401` no es una prueba de nada por sí solo.** + +⛔ **Y la partición NO se razona sobre el enrutador: se DERIVA de lo montado, y discrimina por +MÉTODO además de por ruta.** El guard corre **antes** de enrutar, así que no ve rutas — ve +**cadenas**. *(Cicatriz `R10`, 2026-07-28: la orquestadora afirmó «los espacios no chocan» mirando +el enrutador —el panel usa `/`, `/pending`, `/devices/…`; la API `/links/*`— y era cierto **para el +enrutador y falso para el guard**: `POST /devices/enroll` **casa con la plantilla** +`GET /devices/{device_id}`, que es una página del panel. Una lista pública por **prefijo** habría +abierto esa página a internet — el accidente de athena, en espejo: allí el catch-all cerró de más, +aquí habría abierto de más.)* ⇒ La lista de lo público se genera **del montaje real**, nunca de un +razonamiento sobre qué prefijos «son de la API»; y `GET /x` y `POST /x` son **dos entradas +distintas**. +⇒ ⛔ **Y su aplicación al DESPLIEGUE, que es donde más engaña: «¿existe esta ruta?» NO se contesta +con un código HTTP si delante hay un guard de sesión.** *(Cicatriz 2026-08-18: la orquestadora puso +como testigo estrella de un despliegue que `GET /crl/afirmar` pasara de **404** a **302/401**, y +escribió que sin esa comprobación *«un “desplegado” no distingue nada»*. Medido antes de usarlo: la +ruta **no daba 404 ni antes** —daba `303 → /login`— y `/zzz-inventada` daba **exactamente lo mismo**. +El testigo habría tomado el mismo valor con la pantalla puesta y sin ella.)* +⇒ **El testigo que sí discrimina es la TABLA DE RUTAS MONTADAS dentro del pod** —lo derivado del +montaje, `R10` otra vez— y se lee **por el esquema OpenAPI, no recorriendo `app.routes` en plano**, +que es la cicatriz `F11.11b` un piso más abajo: los routers incluidos van **anidados** y el recorrido +plano ve seis rutas y ninguna `crl`. *(La orquestadora tropezó con las dos el mismo día: escribió el +testigo ciego y luego midió el sustituto por el camino plano.)* +⇒ Y se comprueba por **sus dos caras**: que la ruta **existe** (tabla) y que **pide sesión** +(`303 → /login`). La primera sin la segunda certifica una puerta abierta; la segunda sin la primera +no certifica nada. + +⚠️ **Cambiar la configuración no cambia lo que el cliente ya tiene.** *(F11.13: la concesión DHCP +vieja siguió sirviendo `1.0.0.1` y `search lan` durante **12 h** después de que el router ya +repartiera otra cosa.)* Renueva la concesión —o espera el lease— **antes** de medir, o estarás +midiendo el pasado. + +**Prefiere la prueba POSITIVA a la ausencia.** «No hay hits» es débil —puede ser una ventana corta, +un log rotado o un filtro mal puesto—; «el consumidor **se mudó**» es fuerte. *(F11.11b: en vez de +concluir que nadie llamaba al endpoint viejo, se enseñó que **el mismo device**, minutos después de +instalarse la versión nueva, pidió el endpoint nuevo con token y no volvió a tocar el viejo.)* + +⛔ **Si hay dos caminos que hacen lo mismo, el que prueban tus tests se conserva y el que usa el +usuario se pudre — y el andamio de los tests es justo lo que lo esconde.** *(Cicatriz 2026-07-27: +al ir a borrar el duplicado `/api/admin/*` apareció que las dos copias habían **derivado**, y la +correcta era **la que no llamaba nadie**. Desde el panel —la única puerta que usa un humano— +aprobar un nodo **no lo publicaba** hasta el barrido siguiente, y revocar una sede dejaba +resolviendo sus nombres **hasta 24 h**. Los tests iban por `/api/admin`, así que todo salía verde.)* +⇒ **Borrar un duplicado es un MERGE, no una limpieza**: antes de tirar una copia, averigua cuál de +las dos es la correcta y porta lo que falte. Y **haz que los tests entren por la puerta del +usuario**, o seguirán certificando la copia equivocada. + +- 🔎 **Y para separar tu propio ruido del tráfico real, genera un evento conocido y búscalo.** En + esa misma sesión, la pista fácil era falsa: los hits «sospechosos» salían de la **misma IP + pública y el mismo `curl`** que los del device (el Brume y el PC salen por el mismo NAT), así que + *«curl = humano»* no valía. Lo que los separó fue lanzar un `curl` propio, ver **qué versión de + User-Agent** aparecía, y comprobar que el llamador periódico casaba **exactamente** con el + intervalo de descubrimiento del edge. + +⛔ **Un artefacto que GATEA un build tiene que vivir DENTRO del contexto de ese build.** Si el gate +lee un fichero que la imagen no incluye, no estás comprobando nada: estás comprobando tu árbol de +trabajo, que es justo lo que el gate existe para no creerse. *(Cicatriz `H35`, 2026-08-04, y salió +bien porque el build se puso ROJO: el censo de la superficie pública se escribió en `doc/`, fuera del +contexto `src/api` ⇒ dentro de la imagen **no existía**, `IndexError`, artefacto no pusheado. Se mudó +a `src/api/tests/` y en `doc/` quedó un puntero.)* +⇒ La pregunta antes de escribir cualquier fichero del que dependa una comprobación: **¿entra en la +imagen?** Y su gemela, que es la buena noticia: **un gate que revienta porque le falta su insumo está +funcionando** — el fallo silencioso habría sido que lo encontrara vacío y siguiera. + +**Corolario (de Solidaria): el verde lo certifica el GATE/la medida, no tu palabra.** "Los tests +pasan" no es tu afirmación, es lo que dice el gate — y **antes de que exista artefacto** (gate rojo = +no hay imagen que desplegar). Vale igual para cualquier "está hecho": lo certifica una comprobación, +no la confianza en que lo hiciste bien. + +⛔ **Un veredicto necesita un TESTIGO INDEPENDIENTE, o es decorativo.** Toda señal que gatea un +`ready`/`verified`/`ok` debe poder **nombrar en el código**: (a) el observable independiente que lee +—NO derivado de lo que verifica: ni el `control` del propio artefacto, ni la versión que TÚ +inyectaste en el values, ni el status de una ejecución anterior, ni la **ausencia** de algo que +comprobar, ni un exit-code sin mirar el efecto—; y (b) un **estado roto concreto, que PUEDE ocurrir +en producción**, que la pone roja, demostrado mutando el cableado a ese estado (§3). Si no existe tal +estado, la señal **no puede gatear**: se degrada a dato crudo. Corolarios: «no comprobado» es un +veredicto **distinto** de «comprobado y verde» (y no admisible por defecto en una puerta); la +**palabra** del veredicto codifica esa diferencia, no un campo lateral; una credencial acuñada prueba +que **autentica** antes de `ready`. Pregunta única para cada señal: *«¿QUÉ tendría que estar roto para +que esto se pusiera rojo, y ese estado PUEDE ocurrir de verdad?»* Si la respuesta es «no puede», es +una tautología. *(Cicatriz 2026-07-28, barrido de la colección MCP: la MISMA clase apareció en los +CUATRO — la rotación de mcp-secrets daba verde por una señal pegajosa + `kv_exists` [el path ya +existía]; el `version_matches` de mcp-edge comparaba el `.ipk` con su propio `control`; el +`mint_token(git)` no probaba que el token AUTENTICARA; el `verified-ok` de mcp-build daba verde con +los pods VIEJOS sirviendo `200`; su gate A3 admitía `ungated` por ausencia de gates. Dos se cazaron +por casualidad mirando otra cosa; el barrido encontró el resto. Dos de cuatro habría sido casualidad; +cuatro de cuatro es un raíl.)* + +⛔ **Leer el código de TU capa no dice qué hace la capa de encima con tu salida.** *(Cicatriz `I8`, +2026-07-29: la ficha estimaba —leyendo `wg_hub.py`— que un re-alta anónimo dejaba el peer WG en pie +y que la pérdida llegaría «en el siguiente reinicio de `wg-hub`». **Son 36 segundos**: la API +reescribe `wg0.conf` sin ese peer, y `gre-sync` —**un script del chart, en OTRO repo**— borra el +`grehub` y con él la adyacencia OSPF. El peer sí sobrevivía; **moría todo lo construido encima**, y +eso convertía una bomba latente en una denegación de servicio en vivo.)* ⇒ Una predicción sacada de +tu propio código describe **tu función**, no el sistema. Y con las capas repartidas entre repos, el +que consume tu salida **no aparece en tu `grep`**: se mide, o se mide. + +⛔ **Dos copias que coinciden entre sí no son una verificación — el aparato es el único que +desempata.** *(Cicatriz Fase 14, 2026-07-28: `transport-spec.md` daba la derivación v2 de la +loopback por «congelado ⏳» y describía la v1 —«la teclea el operador; el hub la asigna por +`peer_index`»— con las **dos mitades falsas**. Su espejo decía lo mismo, así que **leyendo docs era +indetectable**: solo el hardware llevaba la contraria, con `10.100.203.26` en el Brume y +`10.100.255.154` en el hub. De la misma tirada cayó un problema que **la spec inventaba**: el puerto +WG no es «443 RUTOS / 51820 OpenWrt» sino `51234` de escucha en ambas, así que el conflicto de +despliegues mixtos que describía **no existe**.)* +⇒ Y la técnica que lo resolvió: **ejecuta el código INSTALADO, no lo reimplementes para +comprobarlo.** Se corrió el módulo `wgt.overlay` del propio device; reimplementar el hash aquí +habría verificado la reimplementación, no el sistema. + +⛔ **Y el testigo tiene que ser independiente TAMBIÉN del acto de medir.** *(Cicatriz `P9c`, +2026-07-28: el panel comprobaba si `zebra` corría buscándolo en `ps` — y la contraseña del VTY de +quagga **es `zebra`**, así que **cada sonda dejaba en `ps` un proceso propio que casaba**. El módulo +se veía a sí mismo: cuanto más se miraba el panel, más imposible era que arrancara.)* Una sonda que +casa con su propia invocación no mide el sistema, **te mide a ti mirándolo**. Señal de alarma: un +observable que **mejora cuando lo consultas** — y, en general, cualquier comprobación por `ps`/`grep` +sobre una cadena que también aparece en el comando que la busca. + +⛔ **Un testigo que no puede ponerse ROJO no es un testigo — y el que se pone rojo SIEMPRE mata lo +que venía a proteger.** Si la señal que esperas la produce una capa **DELANTE** de lo que verificas, +no está mirando tu sistema: está mirando al portero. *(Cicatriz `engine`, 2026-07-30, cazada **antes +de armarla**: el `verify` de mcp-build sale por internet y el Ingress de `engine` tiene whitelist de +IP ⇒ devuelve **403 pase lo que pase**. Un `expect_status:200` habría fallado **siempre** → auto- +rollback → re-pin del digest anterior, que daba **404** → `engine` muerto y sin vuelta atrás: el +mecanismo de seguridad era el asesino. Y `expect_status:403` es **peor**: el ingress-controller +devuelve 403 **aunque el pod esté en `ImagePullBackOff`** — verde eterno sobre un servicio caído.)* +⇒ Antes de elegir el valor esperado, pregunta **quién lo produce**. Si lo produce el proxy, el +firewall o el balanceador, cámbialo por uno que solo pueda salir **de dentro**. Y si no hay ninguno +alcanzable, la respuesta correcta es **la palabra degradada** (`deployed-unverified`) más una +comprobación a mano — no un `verify` decorativo, que es un rollback automático esperando su turno. +⇒ **Y el espejo: un testigo que se pone rojo SIN causa enseña a no creérselo.** *(Cicatriz +2026-07-31: `mcp-edge` dio `failed` sin que nada estuviera roto — su smoke midió durante el hueco de +`uhttpd`.)* Falla hacia el lado seguro, sí, pero **un rojo que hay que desmentir a mano entrena a +ignorar los rojos**, y el siguiente será de verdad. Un sondeo que puede caer en un hueco conocido +**reintenta durante una ventana**; no informa del primer intento. + +⛔ **Cuando una sonda te engaña, el arreglo es OTRO OBSERVABLE — no más reintentos.** Reintentar +sobre un valor que el propio sujeto escribe solo es **esperar a que la tautología se complete**. +*(Cicatriz `A16`, 2026-08-01: `mcp-edge` leía la versión con `head -1` —**por posición**— y +`/api/settings` tenía **dos claves `version`**; se quedaba con la del **install-report, un fichero que +escribe el `postinst` del propio `.ipk`**. Resultado: `installed 2.1.58 · served 2.1.58 · HTTP 200 · +4/4 enabled`, **verde entero, con el panel ejecutando 2.1.57**. El arreglo no fue reintentar: fue +esperar a **la marca de fin del instalador**, que es un observable distinto del que gatea.)* +⇒ Dos formas concretas de caer, las dos vistas: +- **Leer por POSICIÓN y no por NOMBRE**: `head -1`, `[0]`, «el primer campo que casa». El día que + aparece un segundo valor con el mismo nombre, gana el que no querías **y nadie se entera**. +- **Leer un valor que el sujeto escribe de sí mismo**: un informe de instalación, un `/health` que + repite lo que le inyectaron, un `ps` que casa con tu propia invocación. +- ⭐ **Usar un cliente que SIGUE REDIRECCIONES, y leer el destino como si fuera el origen.** Un + guard que redirige a `/login` es indistinguible de *«no hay guard»* si tu herramienta te da el + `200` del final del camino. *(Cicatriz 2026-08-13, y la cometió la orquestadora: midió el panel + del hub del trabajo con `urllib.request.urlopen` —que sigue los `3xx` por defecto y no lo dice— y + concluyó **«la aplicación no comprueba nada, el panel está abierto dentro del cluster»**. Era + falso: contestaba `303 → /login`. Encima **tumbó un hallazgo correcto** de la sesión de + ejecución —que sí había medido con `wget -S`, que enseña la traza entera— y estuvo a punto de + cambiar el motivo de una decisión por uno inventado.)* ⇒ Cuando midas una autorización, **exige + ver la traza**: el código de CADA salto y sus cabeceras. Y desconfía por defecto de todo cliente + que «te lo pone fácil» — `urlopen`, `requests.get`, un navegador: la comodidad es exactamente lo + que te esconde el peldaño que estabas midiendo. +- ⭐ **Preguntar por un NOMBRE que resuelve distinto según quién pregunta.** *(Cicatriz `H12`, + 2026-08-02: `harbor.manabo.org` es **Valhalla** desde el constructor y **Athena** desde los nodos + que tiran. Una sonda de «¿ya está la imagen?» lanzada desde el pod habría preguntado **al registro + que acababa de recibir el push** ⇒ **200 siempre**. Por eso el destino de la sonda tiene que ser un + **endpoint explícito**, no el nombre.)* Si un nombre es split-horizon, **no sirve como dirección de + un testigo**: solo sirve para hablar, no para comprobar. + +⛔ **En un reconciliador, «lo que yo no escribo» NO es «lo que sobra».** El conjunto que **enumeras** +para decidir qué conservar tiene que ser **más ancho** que el que **escribes** — reutilizar el filtro +estrecho convierte *«esto no lo gestiono yo»* en *«esto se borra»*. *(Cicatriz evitada el 2026-08-02 +construyendo la poda de `allowed-ips`: el peer `yomi` lo pone `wg-setup` **desde un Secret** y el API +**nunca** lo escribe en la conf. Una poda de «borro todo lo que no reconozco» se lo habría llevado — +que es el error de `H13` con el `lo` del nodo, y por eso hizo falta una lista `PRESERVE_PEERS` +explícita.)* +⇒ Antes de escribir una poda, pregunta: **¿quién MÁS escribe aquí?** Un recurso compartido casi +siempre tiene más de un dueño, y el que no ves es el que vas a romper. +⇒ Y su gemelo: **si la lectura del estado vivo FALLA, no se poda nada.** Un `wg show` que no responde +devuelve **ausencia**, no **conjunto vacío** — y tratarlos igual borra la instalación entera. +⇒ ⛔ **Y esto también le pasa a Kubernetes, así que no lo des por resuelto porque lo haga el +sistema.** *(Cicatriz 2026-08-02: un `kubectl delete ns karmada-system` lanzado con el plano de +control revuelto por los reinicios de esa misma sesión —`connection refused` contra el apiserver a la +misma hora— se llevó el objeto `Namespace` y dejó **vivos los 18 objetos de dentro**, listables y sin +`deletionTimestamp`, en un namespace que ya no existía. El `namespace-controller` **enumera por +discovery** lo que debe borrar; una enumeración vacía se leyó como «no queda nada», retiró el +finalizador y el `Namespace` desapareció antes que su contenido.)* +⇒ ⭐ **Y lo que lo hace peligroso no es el huérfano, es que sea INVISIBLE**: casi todo se audita +**por namespace**, y ese namespace ya no está. Es la cicatriz de kaniko en variante nueva — *quitar +el gestor no quita lo gestionado*. +⇒ **Regla operativa: ninguna poda que dependa de ENUMERAR se ejecuta con el plano de control +inestable.** Si hay que borrar algo el día que se toca etcd o un apiserver, se borra **antes** de +empezar o **después** de comprobar que los apiservers responden — y la comprobación del borrado no es +que el contenedor padre ya no esté: es que **los objetos ya no están**, incluidos los *cluster-scoped*. + +⛔ **De un contador se deduce TRÁFICO, no PROPÓSITO. «Nadie lo usa» no es una medida: es una +inferencia — y es la que justifica retirar cosas.** *(Cicatriz 2026-08-02, cazada por Xavier antes de +ejecutarse: un aviso describía un `wg0` con **RX 0 y TX 56 MiB** como «un túnel muerto que nadie +usa», y la orquestadora ofreció «levantarlo o quitarlo» como dos opciones equivalentes. Xavier: +**«ese túnel es con el que me conecto yo a casa»** — y estaba **fuera de casa** en ese momento. +Quitarlo le habría cortado el acceso.)* +⇒ ⭐ **Y el caso peor es sistemático: un camino de último recurso parece ocioso PORQUE funciona.** +Solo se usa el día que hace falta, así que «sin tráfico» es su estado normal — y es justo lo que lo +hace parecer muerto. Acceso de emergencia, rutas de respaldo, credenciales de rescate: todas se ven +igual que la basura. +⇒ Antes de proponer retirar algo, **pregunta para qué existe a quien lo puso**. Y si el aviso lo +escribió una sesión que solo vio contadores, **la propuesta de retirada no está fundada**: lo que +midió es que no pasa tráfico, no que no sirva. + +⛔ **Un testigo que no DISCRIMINA es peor que no tener testigo**, porque da permiso. *(El mismo +`A16`: se propuso «comprobar que el PID cambió» como prueba de reemplazo — y **el respawn de procd +también estrena PID**, así que habría salido verde sobre el árbol viejo. El testigo bueno era **la +versión que el panel SIRVE**, que es lo único que solo puede decir el árbol que corre.)* +⇒ Antes de aceptar un testigo, pregunta: **¿qué valor tomaría si el fallo estuviera presente?** Si es +el mismo, no es un testigo: es un adorno. +⇒ ⭐ **Y el caso más común de todos: una alerta construida sobre «¿responde?» no puede distinguir +«el sujeto está caído» de «mi sonda está rota».** *(Cicatriz 2026-08-02, cluster `home`: dos reglas +—`etcdMembersDown` y `etcdInsufficientMembers`— contaban **targets de scrape** y los llamaban +«miembros». Llevaban **56 días** en rojo porque los cuatro componentes del plano de control escuchaban +sus métricas en `127.0.0.1`, con los cuatro **perfectamente sanos**.)* Lo que lo demostró **no fue +razonarlo, fue un banco de usar y tirar**: tres etcd de juguete, **matando 2 de 3** — y el +superviviente **siguió sirviendo `/metrics`**, o sea que `up` se habría quedado en **1**. Lo que sí +se movió fue el estado interno (`etcd_server_has_leader` 1→0, `peer_sent_failures` 0→16→95). +⇒ **La regla útil lee el ESTADO que el sujeto publica de su propio consenso, no si contestó al +teléfono.** Y la forma de saberlo es la misma de siempre: **provoca el fallo real y mira qué serie se +mueve**. Si la tuya no se mueve, tu alerta no vigila lo que crees. + +⛔ **Un campo de `status` que suena a «lo que hay AHORA» puede ser «lo que había la última vez que el +controlador lo tocó» — y creerse ese nombre lleva a arreglar la declaración en vez del sistema.** +*(Cicatriz 2026-08-02, y la cometió la orquestadora: dedujo que Vault llevaba «29 días corriendo sin +el `hostAliases`» porque `currentRevision ≠ updateRevision`, y recomendó **revertirlo en git para que +la alerta se apagara sola**. Medido en el aparato: hay **12** ControllerRevisions, el pod corre la +**10**, `currentRevision` apunta a la **4** y el deseado es la **12**. Con `updateStrategy: OnDelete` +ese campo no describe nada real, y el pod **sí** llevaba el `hostAliases` — en su `/etc/hosts`.)* +⇒ El testigo de qué corre es **la etiqueta del propio pod** (`controller-revision-hash`), o su +`/etc/hosts`: se mide en el sujeto, no en la opinión del controlador. +⇒ ⭐ **Y el corolario general, que es el peligroso: cuando una alerta compara DESEADO con ACTUAL, la +salida barata es tocar el deseado.** Eso la pone verde **haciendo que la declaración mienta sobre lo +que corre** — se pierde la alerta *y* la verdad de la spec, a cambio de nada. Si el actual no se +puede mover (aquí: reiniciar Vault lo deja **sellado**, shamir 3/5 sin auto-unseal, y se lleva 83 +`ExternalSecret`), la respuesta correcta es **la excepción escrita con fecha de caducidad**, no +editar la intención hasta que cuadre. + +⇒ ⛔ **Y su espejo, que engaña en la dirección que NADIE vigila: un estado de ERROR es tan pegajoso +como uno verde.** De un verde heredado ya desconfiamos; un **rojo** heredado se lee como *«sigue +roto»* — y eso hace perseguir un problema **ya resuelto** y «arreglar» lo que estaba bien. *(Cicatriz +2026-08-12: repuesto el token de ESO de `hermes`, **20 de 30 `ExternalSecret` seguían en +`SecretSyncedError`** y la lectura obvia era «no funcionó»; se estuvo a un paso de reiniciar ESO y de +reescribir la política. Lo que lo desmintió fue **la HORA del último error en el log: siete minutos +de silencio**. El campo contaba el ciclo anterior — su `refreshInterval` es de **una hora**. Forzando +el refresco de los treinta: **30/30 verdes**.)* +⇒ El `status` de un objeto narra **su última reconciliación**, no el presente — en los dos colores. +Para saber si algo sigue roto: mira **cuándo** fue el último fallo, o **provoca** la reconciliación. +Nunca leas el campo y ya. + +⛔ **Cuando el riesgo es una PÉRDIDA SILENCIOSA, el testigo tiene que ser DIFFEABLE.** Una +comparación que solo puede hacer un humano mirando siempre concluye *«se ve igual»* — y esa es +exactamente la conclusión que el riesgo necesita para pasar. *(Cicatriz al revés, 2026-08-01, hecho +BIEN: vaciando de texto las pantallas de dos paneles, el peligro era llevarse **estado** junto con los +comentarios. En vez de capturas —que **no se pueden diffear**— se volcó **el texto de todas las +pantallas antes y después** en los dos aparatos, con un clasificador prosa/medida: **0 medidas +cambiadas**, y los únicos cambios fueron los seis textos que se querían mover.)* +⇒ Antes de una poda, un refactor o una migración, pregunta: **¿qué artefacto puedo comparar a +máquina?** Si la respuesta es «ninguno», invéntalo **antes** de tocar — un volcado, un censo, un +hash. Lo que no se puede diffear no se puede echar de menos. +⇒ ⭐ **Y el corolario que justifica el gasto: «COSMÉTICO» NO ES UNA CATEGORÍA DEL SISTEMA, ES UNA +CATEGORÍA TUYA.** Tú decides que un cambio no toca nada; el artefacto no comparte tus categorías, y +un testigo diffeable es lo único que os pone de acuerdo. *(Cicatriz 2026-08-13, y salió BIEN: una +sesión documentó el porqué de una perilla con comentarios `#`… **dentro de `spec:`**, o sea que +salen en el render. El gate de bytes se puso **rojo en los tres hubs vivos sin que cambiara un solo +valor**. Pasaron a `{{/* */}}` y volvieron los bytes exactos. Sus palabras: «el gate se ganó el +sueldo en el cambio que yo habría llamado cosmético».)* +⇒ Por eso un gate de artefacto **no se salta «porque esto solo es un comentario»**: si de verdad no +cambia nada, pasa solo y no cuesta; y si cuesta, es que cambiaba algo. +⇒ ⛔ **Pero un hash de algo GENERADO solo vale si el generador es determinista.** *(Cicatriz `F12.1`, +2026-08-02: se eligió el `md5` de `ospfd.conf` como testigo de *«esto no ha cambiado»*, y el fichero +lo escribe un bucle sobre `pairs()` de Lua, que **no ordena**. El hash bailaba sin que cambiara una +sola línea ⇒ el testigo habría gritado en cada medida, y a la tercera nadie se lo cree.)* El arreglo +no es cambiar de testigo: es **normalizar antes de comparar** (ordenar) — y entonces el diff vuelve a +significar algo, que es como se cerró la desistencia: `ospfd.conf` **idéntico al byte**, ordenado. +⇒ Pregunta previa a usar un hash: **¿dos ejecuciones sin cambios dan el mismo byte?** Si no lo has +comprobado, no tienes un testigo, tienes un generador de ruido. +⇒ ⛔ **Y aunque normalices: guarda el ARTEFACTO, no su huella.** Un hash dice **que** algo cambió y +**nunca qué** — así que el día que se mueve estás ciego justo cuando necesitas mirar. *(Cicatriz +`E9`, 2026-08-03, y es la segunda vez en dos días: el `md5` de un `ospfd.conf` se movió, la sesión +pudo demostrar por reconstrucción que no era suya… pero **no pudo diffearlo**, porque había guardado +el hash y no el contenido normalizado.)* Un hash cuesta 32 bytes y un fichero de configuración cuesta +dos kilos: **no hay ninguna razón para guardar el resumen en vez del original.** + +⛔ **Una carrera no se ESPERA: se PROVOCA.** Si el fallo depende de coincidir con un bucle, cronometrar +es un sorteo — y tres «no apareció» seguidos no dicen nada. *(Cicatriz `H24`, 2026-07-31: la ventana +del zombi no era de 10 s fijos, iba de **~0 a ~10 s** según dónde cayera el revoke dentro del +`sleep 10` de `pq-sync`; con la rotación de Rosenpass en 131 s, acertar era un **~8%**. Tres intentos +fallaron por 50 s, 3 s y 1 s.)* +⇒ Lo que lo hizo repetible: **dejar de cronometrar y disparar el intercambio**, y **calcular la fase +del bucle a partir del `T0` que el propio supervisor escribe en su log** — el sistema suele publicar +su reloj; úsalo en vez de adivinarlo desde fuera. +⇒ Corolario duro: **hasta que no lo hayas reproducido, no puedes cerrar nada arreglándolo.** «No +apareció después del arreglo» sobre una ventana en la que tampoco habría aparecido antes **no es +evidencia** — es la misma medida sin discriminación. + +⛔ **Una nota de cierre tiene que nombrar QUÉ COPIA cerró.** «Arreglado en `hermes`» no dice si habla +del **código**, de la **imagen** o de la **instancia que corre** — y son tres cosas distintas que se +desincronizan solas. *(Cicatriz `H16`, 2026-07-30, y es la **tercera** reincidencia de `H6` — +casillas rancias— tras el 26 y el 27 de julio. El modo de fallo nuevo: la casilla no se podía +contradecir **leyendo los docs**; hubo que entrar en el contenedor a comparar `sha256sum` contra +`git show HEAD:`.)* +⇒ Y el testigo se elige por quién lo produce: **`/health` NO sirve** — devuelve el string que le +inyectó el `values`, o sea un auto-informe. Lo que decide es el `imageID` del contenedor (qué se +empaquetó) y, mejor aún, **una huella que solo pueda haber escrito el camino de código nuevo** (una +fila `promoted` en `pair_claims`). Un fichero dentro de una imagen prueba **qué se empaquetó, no qué +corrió**. +⛔ **Y el caso más traicionero: un artefacto producido por el CÓDIGO DEFECTUOSO no prueba nada +sobre el estado que ese defecto ignoraba.** Si el fallo consiste en *«no mirar X»*, entonces lo que +ese código escribe sale **igual valga X lo que valga** — así que leer su salida para deducir X es +leer el defecto y llamarlo dato. *(Cicatriz 2026-08-03, y la cometió la orquestadora: dedujo que una +LAN estaba en la malla porque el fichero de opciones DHCP existía. Cierto con el código de hoy —que +solo lo escribe para las LAN de la malla— y **falso con el que corría cuando lo midió**, que lo +escribía **incondicionalmente y sin consultar el eje**. La existencia del fichero probaba justo lo +contrario: que el eje se ignoraba.)* +⇒ ⭐ **Y arreglar el defecto cambia el significado de la evidencia hacia atrás.** Antes de inferir +estado de un artefacto, pregunta **qué versión lo escribió y si esa versión miraba lo que quieres +deducir**. Si no lo miraba, el artefacto es un testigo de la avería, no del sistema. + +⇒ Generalizado: **una medida sin su procedencia no es una medida.** Falta decir *qué copia* y +*cuándo*, y las dos mataron un dato el 2026-07-30: +- **El árbol de trabajo NO es el repo.** Dos veces el mismo día: un default `harbor.c2et.com` + reportado como bomba estaba en un WIP **sin commitear**, y «hay **4** `build.yml`» eran **3** en + `origin/main` (el cuarto vivía en un worktree de otra rama, con 0 commits propios). Si afirmas + sobre el repo, **mide sobre `origin/`**, no sobre lo que tienes delante. + ⇒ ⛔ **Y AL REVÉS, que es el que no se espera: medir el ARTEFACTO correcto puede esconder que la + COPIA que usa la gente está rota.** Son dos preguntas distintas —*«¿qué despliega ArgoCD?»* y + *«¿qué le pasa a quien clona esto?»*— y **ninguna receta contesta las dos**. *(Cicatriz + 2026-08-06: `pq-secret.sh` estaba en **CRLF en el worktree** y así ni arranca —`set -uo + pipefail\r`—, o sea que con `core.autocrlf=true` un clon en Windows deja **todos** los bancos + muertos. El censo no lo vio porque medía —bien— sobre un `git archive` en LF: **la receta correcta + tapó el defecto que la rodeaba**. Se cosió con un `.gitattributes`.)* ⇒ Cuando elijas dónde medir, + di **qué pregunta estás contestando**; y si la herramienta tiene una salida de emergencia (un + `tr -d '\r'`, un fallback), pregúntate **qué está haciendo tolerable que no deberías tolerar**. +- **Si un inventario lista dos medidas del mismo día, la hora es parte del dato.** Un robot + «vivo por la mañana» y «borrado por la tarde» no es una contradicción que resolver: es un **antes y + un después**, y tratarlo como conflicto te hace corregir dos veces y acertar ninguna. + +⛔ **Si delegas una decisión en el operador, TIENES QUE DARLE EL GESTO. Si no, no has delegado: has +cerrado la puerta.** *(Cicatriz `H23`, 2026-07-31: `I1` decidió —bien— que re-admitir a un nodo +revocado es **«una autorización NUEVA del operador»**… y construyó solo la **negativa**. Medido en el +pod que corre: `POST /links/pair` da **403** permanente, `approve_pair` exige `pending`, y **ninguna +pantalla pinta una fila revocada** ⇒ **no hay botón que pulsar**. Hubo que tocar `status` en la base +de datos. Y lo que lo vuelve grave: el procedimiento documentado de `H21` dice **«revoca los edges +antes de renombrar un hub»** — seguirlo en el cliente dejaría sus sedes fuera **sin marcha atrás**.)* +⇒ Prueba barata, y hazla al diseñar, no al depurar: **para cada estado en el que tu sistema puede +entrar, ¿cuál es la salida y quién la pulsa?** Un control que solo se puede **aplicar** y nunca +deshacer no es un control, es una trampa. Y si el estado terminal no aparece en ninguna pantalla, +para el operador **no existe** — da igual lo que diga la fila en la base de datos. + +⛔ **Un identificador que además es CLAVE DE BÚSQUEDA no se puede renombrar: sepáralo antes.** Si un +mismo valor da el nombre público **y** direcciona secretos, rutas o etiquetas, cambiar la cara +visible **re-direcciona lo invisible**, y en silencio. *(Cicatriz 2026-07-31, cazada antes de +aplicar: `hubName` gobernaba también los **paths de Vault** — con `hermes.hub` el `trimSuffix "-hub"` +no recortaba nada y el hub habría ido a buscar su clave WireGuard y su **clave Rosenpass** —la que +`D10` prohíbe regenerar— a rutas inexistentes. La sesión partió **identidad** de **nombre público** +antes de tocar nada.)* +⇒ Antes de renombrar, **escribe qué se deriva de ese valor**. Lo que dice quién eres y lo que dice +dónde están tus cosas tienen que ser campos distintos, aunque hoy coincidan. Y donde el derivado +tenga formato obligado (una etiqueta de kernel, un path), **que la plantilla falle al renderizar** — +probado que falla, no supuesto. + +⛔ **Un comentario que JUSTIFICA saltarse una puerta sobrevive al motivo, y convierte el bypass en +rutina.** Es el peor sitio donde se puede pudrir una nota: el resto de la documentación rancia hace +perder el tiempo; ésta **desarma un gate y encima explica por qué está bien**. *(Cicatriz 2026-07-30: +el catálogo de `mcp-build` en asgard llevaba un CAVEAT diciendo que la suite de `hermes-hub` estaba +obsoleta —12/28 en rojo— y que por eso «el deploy se valida con `skip_tests`, bypass **auditado**». +Había dejado de ser cierto el **2026-07-24**: las 12 aserciones se reescribieron y la suite va a +**90 passed**. Seis días en los que la razón ya no existía y el permiso seguía escrito.)* +⇒ Toda excusa escrita para desactivar una comprobación **nace con fecha de caducidad**: dice qué la +justifica y **qué la cancelaría**. Y quien arregla la causa tiene que borrar el permiso — si no, lo +que queda no es una excepción, es la nueva costumbre. + +⛔ **Y su hermano, que es peor porque parece inofensivo: un PENDIENTE rancio no es ruido, es una +INSTRUCCIÓN EQUIVOCADA esperando a que alguien la ejecute.** Una casilla `[ ]` no solo describe un +problema: casi siempre trae **cómo arreglarlo**, y ese «cómo» está anclado al diseño de cuando se +escribió. *(Cicatriz 2026-08-04: bajo una cabecera marcada ✅ sobrevivía un pendiente sobre una +carrera `revoke + enroll` **que `I1` había vuelto inconstruible** —los dos `db.delete` ya no +existen—, y su arreglo propuesto era *«un SELECT+DELETE atómico»*. Quien lo hubiera cogido de buena +fe habría **reintroducido exactamente lo que `I1` quitó**, que era el borrado de la fila y toda la +vulnerabilidad con él.)* +⇒ **Al cerrar algo, barre los pendientes que describían su arreglo**: desde ese momento describen el +arreglo **equivocado**. Cerrar una decisión sin barrer su cola deja instrucciones armadas. + +⛔ **`Synced` es un veredicto sobre lo que la Application MIRA — no sobre el directorio, ni sobre lo +que un día gestionó.** Dos caras, las dos medidas el 2026-07-30 desmontando kaniko: + +- **Quitar el gestor no quita lo gestionado: lo deja HUÉRFANO.** Las tres Applications no tenían + `resources-finalizer`, así que el prune se llevó **los objetos `Application`** y dejó vivos el pod + del runner, su PVC, el ns `kaniko` con credenciales de push y el Deployment del image-updater. + ⇒ El resultado de fiarse de la predicción habría sido **peor que no tocar nada**: procesos con + credencial de escritura corriendo **sin dueño ni en git ni en ArgoCD**. Borrar es un `delete` + explícito **después** del commit; el commit solo retira la gestión. +- **Un fichero que la Application no incluye no existe para ArgoCD.** El appset de velero monta su + directorio con `directory.include: "externalsecrets.yaml"` ⇒ `velero-valhalla` salía + **`Synced/Healthy` sin haber mirado nunca los `Schedule`**. Quitar un namespace del YAML en git no + quitó nada: el objeto vivo seguía respaldándolo. + +⇒ Antes de creerte un `Synced`, pregunta **qué ficheros entran** en esa Application y **qué recursos +siguen vivos sin ella**. Y la comprobación de un desmontaje no es «la Application ya no está»: es +**que los objetos ya no están**. + +⛔ **Y borrando DOCUMENTOS pasa lo mismo, con una moneda peor: antes de retirar un documento de +estado, cuenta lo que está ABIERTO en él — no sus ids, no sus líneas.** Ids y líneas miden **tamaño**; +las casillas `[ ]` miden **obligaciones**, y son lo único que no se puede reconstruir después. +*(Cicatriz 2026-08-03, y la cazó la propia sesión al aplicar su propuesta, no la propuesta: al retirar +`BACKLOG-HERMES.md` —826 líneas, con el invariante de ids comprobado y cuadrado— quedaban **cuatro +ítems abiertos de la red del trabajo** que el borrado se habría llevado **en silencio**: excluir una +IP de la pool de MetalLB, identificar físicamente al okupa de un puerto de switch, renombrar un +address-object del firewall y un puerto de destino. Nada de eso tenía id; eran casillas.)* +⇒ El censo previo a un borrado tiene **dos columnas**: qué ids se van *(y a dónde)* **y qué queda sin +hacer** *(y a qué backlog se muda)*. La segunda es la que salva trabajo; la primera solo salva +referencias. +⇒ Y el corolario para quien busca esas referencias: **búscalas en TODO el árbol, no solo en los +`.md`**. En esa misma retirada apareció un puntero al documento **en un comentario de código** +(`lib/wgt/config.lua`), que es donde un enlace muerto sobrevive más tiempo porque nadie relee los +comentarios. + +⛔ **La réplica PROPAGA la decisión, no la juzga: una réplica fiel de una decisión mala la hace +global.** Mejorar el mecanismo que distribuye, sobre un procedimiento que decide mal, **no arregla — +amplifica**. *(Cicatriz `A20`, 2026-08-01: la `/32` que un hub se inventó al resolver una colisión no +se quedó ahí — el edge la adoptó (`A17`), la re-declaró al otro hub (`A18`), que la aceptó, y el +nombre del router acabó resolviendo a la dirección equivocada **por los dos caminos**. Cuanto mejor +funcionaba la réplica, más lejos llegaba el error.)* +⇒ Antes de mejorar una distribución, pregunta **quién decide lo que se distribuye**. Si el generador +puede equivocarse, la respuesta no es replicar mejor: es **quitarle la decisión** — el arreglo fue +que el hub dejara de asignar, no que asignara con más cuidado. + +⛔ **Cuando un estado vive en DOS sitios, el que sobrevive es el que miente.** No es un caso: es la +forma en que fallan los sistemas con estado duplicado, y apareció **tres veces el 2026-07-28**. + +- **Un validador de caché guardado APARTE del dato sobrevive al dato.** El reconciliador PQ guardaba + el `ETag` de la pubkey del peer en `data/mesh.json`, y el fichero que certifica — + `data/pq/peers/.pk`— en otro sitio. Se borró el fichero y **no volvió nunca**: 8 vueltas + pidiendo con `If-None-Match`, el hub contestando `304`, y el enlace `blocked` **para siempre**, + con todos los indicadores en verde. ⇒ Un validador tiene que invalidarse cuando **desaparece lo + que valida**: guárdalo junto al dato, o compruébalo contra su existencia antes de usarlo. +- **El kernel emparejado y el panel a cero.** Tras desmontar un edge «a cero», `data/mesh.json` + quedó en `{"hubs":{}}` mientras `wgtrans` conservaba **dos peers con handshake fresco y PSK + puesta**. Desde fuera, vivo; desde su propio panel, virgen — y sin links en su estado, el + reconciliador no levanta quagga, así que **no hay OSPF y nadie lo dice**. +- **El `lo` del nodo acumula direcciones que ningún manifiesto declara** (§ hub): el chart las + añade y no las quita, así que un nodo lleva encima todo valor con el que se desplegó **alguna + vez**, y ArgoCD dice `Synced/Healthy`. + +⇒ **Regla**: si un dato tiene dos copias, di **cuál manda** y haz que la otra **no pueda +sobrevivirla**. Y para probarlo no basta con razonar que ya no puede pasar: hay que **volver a +ponerse en el estado roto** y ver que se recupera — es lo que hizo el arreglo del `ETag`, y por eso +se sabe que sirve. + +## 2. El dato crudo, no el cocinado + +El backend **mide y traduce a atributos**; el consumidor (UI, componente, otro servicio) +**interpreta y pinta**. No cocines el estado antes de tiempo ni lo re-derives en la capa de +presentación (si no, dos consumidores divergen). + +- Apareció **cuatro veces** en la convergencia UI: la API servía `wg_status` (up/idle/down) y + el badge necesitaba `wg_handshake_age` para su contador vivo; igual con IP, OSPF, y el + `state` de los nombres de malla. +- **Edad, no timestamp**, en routers sin RTC fiable. `0` es *ausencia de medida*, no "época". +- El estado se publica una vez (backend, testeable) y el componente sólo lo colorea. + +## 3. Mutation testing: ataca el CABLEADO, no las funciones puras + +Las funciones puras se diseñaron junto a sus tests → caer es inevitable, no prueba nada. +Los bugs viven en las **costuras donde el dato se ensambla**. Testea *qué* se ejecuta/ensambla, +no sólo *cómo* se parsea. + +- F10.3: el parser de rutas tenía 8 aserciones verdes y el panel salía **mudo** — el bug estaba + en qué `proto` se consultaba. El test bueno usa un doble que registra el comando ejecutado. +- Dos listas de la misma forma (`to_publish`/`published`): intercambiarlas no rompe ninguna + función pura, sólo **invierte la verdad** → stubea y afirma sobre el resultado con un estado + conocido. +- `pq_psk = True` fijo pasaba la suite entera → habría afirmado protección PQ inexistente. + +⛔ **Cuando un cambio de configuración pretende cambiar un COMPORTAMIENTO, el testigo es el +ARTEFACTO GENERADO: guardado ANTES, comparado DESPUÉS, y el veredicto es su `diff` por caso.** +**Un caso cuyo diff sale vacío no está construido, aunque no dé error: es un campo-señuelo.** +*(Cicatriz infraserver, cosecha 2026-08-15: `day1.mode` — dos modos producían **el mismo +artefacto**; y así llegó `day0.luks` a existir declarado y sin consumir, sin que nada se pusiera +rojo.)* +⇒ Es el testigo independiente de §1 aplicado a la configuración: sin el artefacto de antes, «cambié +la perilla y no dio error» no distingue **construido** de **decorativo**. + +⛔ **Y el modo de fallo más traicionero: los FIXTURES de la suite son más completos que la realidad.** +Un test que **construye él mismo el argumento** no puede cazar a un llamante que lo construye mal, y +la suite sale verde sobre un cableado roto. Dos veces en la misma semana (2026-07-31): +- `GET /links` —el endpoint del panel— llamaba a `LNK.status` con un `cfg` **declarado y sin + asignar**, así que el peldaño de identidad llegaba **vacío**. Los tests pasaban **porque le pasan + `cfg` a mano**. +- Un mutante que derivaba el sufijo de una LAN del dominio del router —la invención que `D8` + prohíbe— **no rompió ni una aserción**, porque los tests de `D8` **no pasaban registro de router**. +⇒ Pregunta por cada test que pasa: **¿de dónde sale ese argumento en producción?** Si lo fabrica el +test, ahí no hay cobertura — hay una simulación de que la hay. El testigo tiene que **entrar por +donde entra el usuario**. + +⛔ **El NÚMERO de rojos es la señal, no el rojo.** Una mutación que da **menos** rojos de los que +esperabas acusa a **tu test**, no a la mutación. *(Cicatriz F11.11b, 2026-07-27: el test de +cableado recorría `app.routes` **en plano**, pero en esa versión de FastAPI los routers incluidos +van **anidados** → el recorrido veía 8 rutas y **ninguna** `/api/…`, así que **pasaba con el +endpoint abierto de par en par**. Se delató porque la mutación dio **1 rojo donde tenían que ser +2**. Ahora las rutas se descubren por el esquema OpenAPI.)* +⇒ **Escribe cuántos rojos esperas ANTES de mutar.** Sin esa predicción, un rojo de menos pasa por +éxito y el placebo sobrevive. +⇒ **Y para que el conteo SEA posible, las aserciones tienen que ser a prueba de `nil`.** *(F11.12, +2026-07-27: tal cual estaban, la primera mutación **reventaba el bloque entero** y escondía cuántas +aserciones habría tumbado — el número dejaba de ser medible justo cuando lo necesitabas.)* Una +suite que explota en vez de suspender no te deja contar rojos. +⇒ **Y un 0 rojos puede significar «la mutación NO se aplicó».** Distínguelo, o una mutación que no +casó se lee como *«el test no cubre esto»* y sale a la basura una comprobación buena. *(2026-07-27: +una quinta mutación dio 0 donde se esperaban 2 — **el arnés avisó de que el `sed` no había casado**, +así que el cero quedó explicado en vez de pasar por bueno.)* El arnés de mutación tiene que +**afirmar que el fichero cambió** antes de correr la suite. +⇒ ⭐ **Y una forma concreta de que no case, que no es un `sed` mal escrito: mutar donde el valor se +HEREDA en vez de donde se DECLARA.** *(2026-08-13: una mutación apuntaba a `values-hermes.yaml` para +tocar la clase de Ingress — y esa instancia **no la declara, la toma del default** ⇒ no había nada +que mutar. El arnés dijo «mutación no aplicada» en vez de un `0 rojos` que se habría leído como +«esto no está cubierto», y la mutación se reapuntó al default, donde sí enrojece las tres.)* ⇒ Antes +de mutar un valor, pregunta **dónde está escrito de verdad** para la instancia que vas a medir. +⇒ **Y aun cambiando el fichero, el cero puede ser del CONTADOR.** *(2026-07-28, hub `I6`: la +mutación quitaba un `{% if %}` y dejaba su `{% endif %}` → la plantilla no compilaba, la suite moría +en el fixture y salían **31 `errors` y 0 `failed`**; el arnés leía solo `N failed` ⇒ **0 rojos**, +indistinguible de «este test no cubre nada», y por poco se tira una comprobación buena.)* Dos +consecuencias: **cuenta los `errors`** —una suite que explota no es una suite que aprueba (es la +misma familia que el `nil` de arriba, un piso más abajo)— y **muta a algo sintácticamente válido**: +una mutación que impide arrancar no mide el sistema, mide el arnés. +⇒ Y **no mutes a un valor que tu test ya espera.** *(Misma sesión: la mutación puso `10.99.0.2` a +pelo… que era exactamente el valor contra el que afirmaba el test → **verde por coincidencia**, con +el número de rojos acertado y la identidad equivocada. Con `10.99.0.77` salieron 3.)* Muta a algo +que **no pueda ocurrir**, no a lo que el sistema usa de verdad. + +⛔ **Y el propio ARNÉS es código sin tests — y falla hacia el VERDE.** Todo lo de arriba supone que +la comprobación dice lo que crees; cuando no, el error es asimétrico, porque en un arnés el camino +que no se ejecuta suele ser el que declara `ok`. *(Cicatriz 2026-08-05, construyendo el gate de +`R14`, y lo cazó la mutación —no el gate en verde— que es la única razón por la que se sabe:* +- ***casar por SUBCADENA y no por LÍNEA***: `grep -q 'port: 444'` salía **verde contra + `port: 4440`**, o sea que la guarda del puerto decía que sí a un valor que no era. Es el + `head -1` de `A16` en otra sintaxis: *leer por posición o por trozo, nunca por identidad*. + ⇒ ⭐ **Y su forma PEOR, medida el 2026-08-13: cuando las dos cadenas son valores LEGÍTIMOS del + sistema, la subcadena engaña en las DOS direcciones.** `nginx-inet` **contiene** `nginx`, y las + dos son clases de Ingress reales de instancias distintas ⇒ una guarda sin ancla `$` decía + `ok [hermes] … emiten nginx` **sobre un render que ponía `nginx-inet`**. Aquí el falso positivo no + es ruido: es *el valor de otro hub*. ⇒ Ancla siempre, y si puedes **compara la cadena exacta en + vez de buscarla** — el `pytest` de esa misma sesión aguantó justo por eso. +- ***`cmd | grep -q` bajo `set -o pipefail`***: `grep -q` sale al primer acierto y cierra la + tubería ⇒ **SIGPIPE al productor** ⇒ la tubería devuelve distinto de cero ⇒ **rojos + intermitentes**. Y lo grave no fue el caso nuevo: estaba **latente en cinco comprobaciones + preexistentes cuyo «ok» es la rama `else`** — o sea que llevaban tiempo pudiendo aprobar por el + motivo equivocado.*)* +⇒ Dos preguntas antes de fiarte de una comprobación, y las dos se contestan leyéndola: **¿casa por +línea o por subcadena?** y **¿cuál de mis dos ramas es el `ok`?** Si el `ok` es el `else`, cualquier +error en la condición **pasa por éxito**. ⭐ Y el corolario que lo hace barato: **muta también el +arnés**, no solo el sujeto — una guarda que no puede ponerse roja es tan decorativa como un testigo +que no discrimina (§1), y aquí encima *da permiso a todo el gate*. +⇒ ⛔ **Y dos formas más de que el arnés mienta, las dos medidas el 2026-08-17 construyendo `I11.2`:** +- ⭐ **Un `cmp` prueba que el fichero CAMBIÓ; nunca que cambió a LO QUE QUERÍAS.** Es la guarda que + §3 ya prescribe —*afirmar que el fichero cambió antes de correr la suite*— y **no basta**: dos + mutaciones de esa sesión dieron **0 rojos pasando la guarda**, o sea indistinguibles de *«esto no + está cubierto»* cuando lo que pasaba es que el `sed` había casado **en otro sitio**. ⇒ La guarda + completa no es *«¿cambió?»* sino **«¿está el texto nuevo, y ha desaparecido el viejo?»** — y para + una mutación que debe ser única, **cuántas veces**. +- ⛔ **Una función del arnés usada ANTES de definirse no falla: sale `command not found`, y eso no + para el script ni cuenta como `FAIL`.** *(Misma sesión: un helper `quiere_linea` definido 300 + líneas después de su primer uso ⇒ la cara **positiva** de esas comprobaciones no se ejecutaba y + **nadie se enteraba**.)* Es el *«¿cuál de mis dos ramas es el `ok`?»* un piso más abajo: aquí no se + ejecutaba **ninguna** de las dos. ⇒ Un banco en shell necesita **suelo de comprobaciones + EJECUTADAS**, no solo de fallos: si esperas 40 y corren 33, el arnés está roto aunque salga verde. + ⭐ **Y lo que lo cazó fue la predicción por NOMBRE**: el conteo dio **+7 donde se predijeron +8**. + Con la predicción hecha solo con el número, un 7 razonable habría pasado por bueno. + + +⛔ **Antes de leer un solo conteo, comprueba que la BASE está VERDE.** Un banco cuya línea base ya +suspende **no mide nada**: los rojos rancios viajan dentro de cada mutación y —esto es lo que nadie +espera— **una mutación puede TAPAR uno**, así que el número sube o baja por motivos que no tienen +que ver con lo que mutaste. *(Cicatriz `H19`, 2026-08-05: el banco de `pq-sync.sh` llevaba **ocho +días** con la base en `17 ok / 4 fallos`, porque `62707e1` retiró del chart la semilla PQ legacy +—tocó **cinco ficheros y no tocó el banco**— y cuatro casos quedaron afirmando sobre algo que ya no +existe. Con esa base, los «más rojos de los previstos» **no eran una cascada**: N2 hacía pasar un +rancio y N4 otro. Y el aviso que lo describía prescribía **re-derivar los conteos**, o sea maquillar +la avería — §6, *un pendiente rancio trae el arreglo equivocado*.)* + +⇒ ⛔ **Y predice por NOMBRE, no por número: un conteo es un hash con pérdida de la verdad.** *(Misma +cicatriz, y es la mitad que asusta: `N1` era la **única** mutación en verde del banco… y lo era +**por coincidencia**. Sus dos rojos eran dos rancios, y **las dos aserciones que esa mutación existe +para romper salían VERDES** — el aviso colado por stdout subía el conjunto de 1 a 2, justo lo que se +esperaba. El número acertaba y la identidad estaba entera equivocada: no eran 4 de 5 mal, eran +**5 de 5**.)* Escribe **qué aserción, por su nombre**, debe ponerse roja y por qué; el número sale de +esa lista, nunca al revés. + +⇒ ⛔ **Y su generalización, que es la que hace daño: si en un fichero conviven DOS FAMILIAS de +comprobación, el conteo global no es comparable con nada.** No es que el número mienta un poco: es +que **suma peras y manzanas**, así que una predicción hecha sobre una familia se caree contra el +total y salga un abismo. *(Cicatriz 2026-08-06, y salió bien **porque la predicción iba por +nombre**: al coser seis comprobaciones de `guards.sh` se esperaban **6 `ok`** y salieron **37**. Los +31 de sobra eran `quiere_fallo` —otra familia: lee `stderr` y afirma en positivo—. Con la +predicción hecha solo con el número, la conclusión habría sido «me faltan 31» y se habrían +«arreglado» **31 comprobaciones sanas**.)* +⇒ Antes de comparar dos conteos, pregunta: **¿cuentan lo mismo?** Y si el fichero mezcla familias, +el testigo no es el total — es **el subconjunto nombrado**. + +⇒ Y el corolario que cierra el círculo: **si nada CORRE el banco, esto no es mala suerte, es +inevitable.** Los cuatro bancos de ese chart se lanzan a mano, y por eso se pudrió ocho días sin que +nadie se enterara. **Un banco sin gate no es una comprobación: es documentación ejecutable, y +caduca.** + +⇒ ⛔ **Y una base roja no solo falsea los conteos: ESCONDE MUTACIONES MUERTAS.** El banco se corta +antes de llegar a mutar, así que una mutación que dejó de casar **nunca llega a quejarse** — no sale +como «0 rojos», no sale de ninguna forma. *(Cicatriz 2026-08-06, y apareció **al poner la base en +verde**, no antes: la mutación 4 de `pq-secret.sh` llevaba desde `R14` sin casar, porque `R14` movió +los volúmenes a `_helpers.tpl` y en `deployment.yaml` ya no queda ningún `items:`. Dos días de censo +no la vieron porque los dos rojos de la base la tapaban.)* ⇒ Arreglar la base no es el final del +trabajo: **es cuando empieza a haber datos**. + +## 4. Ejercita el flujo real (abre el navegador, toca el device) + +Tests verdes ≠ funciona. Conduce el flujo de punta a punta en el medio real. + +- El «Editar» de la br-lan llevaba **roto desde F9.2** (promesa muerta en silencio); sin abrir + el navegador, F11.0/F11.1 se habrían dado por buenas siendo **inaccesibles**. +- El PSK PQ, el `.ipk`, el DNS: validados en el Brume real, no en mock. + +⛔ **Sondear una escritura ES escribir.** Un cuerpo malformado **no** es una sonda segura: una API +cuyos campos son todos opcionales lo acepta encantada como *«pon todo por defecto»*. *(Cicatriz +2026-07-26: comprobar si `PUT /api/config/global` pedía credencial con `{"__probe__":1}` **vació la +Global Config** del hub y disparó el `write_zones()` — el hub dejó de servir sus tres estáticos +durante ~45 s.)* Sondea con un método que **no pueda mutar** (`OPTIONS`, un id inexistente), o +contra una copia. Y si la lías, **restaura desde el `GET` de hace un segundo y cuéntalo**, que es +lo que hizo esa sesión: 45 s, verificado, y la única huella un contador de versión. + +**Y por el MISMO CAMINO que el usuario.** Un verde obtenido por otra ruta no dice nada de lo que él +ve. *(Cicatriz F11.7, 2026-07-26: «Guardar cambios» no guardaba… y sí guardaba. `apply_default` +hacía `ifup lan` incondicional, y la br-lan **es la interfaz por la que se administra el panel** → +el `ifup` se llevaba el socket de **su propia respuesta**. El backend aplicaba y persistía, y se +quedaba sin poder contestar; en el navegador, `r.json()` reventaba y la promesa del handler moría +sin dueño. Silencio absoluto.)* + +- 🔑 **Instrumento de diagnóstico: la MISMA petición por DOS caminos.** Fue lo que lo destapó — + por la **LAN** (la interfaz editada): `RESET` a 0,78 s; por la **WAN**: `201` en 3,5 s. Misma + petición, mismo device, veredictos opuestos. Cuando el canal de gestión **es** lo que estás + tocando, hace falta un segundo canal para poder verlo. +- ⚠️ **Corolario incómodo**: el probe de UI daba **verde sobre la versión rota**, porque iba por la + WAN. Una herramienta de prueba que no usa el camino del usuario **certifica lo que no es**. +- Familia: `restart_frr` que se lleva `zebra`/`ospfd` (F9.2), el `restart` de uhttpd que tarda un + minuto en devolver las rutas (U3.3a), y la regla de oro de + `backlog-ejecucion.md` (capa `workflows/`): **lo que no se puede perder es el acceso de + administración**. Toda operación que reconfigura el canal por el que te la pidieron es de esta + familia — y **no puede confirmarse por ese canal**. + +## 5. No inventes estados ni fallos que no has medido + +Honestidad de datos. Si no puedes afirmar algo, dilo ("sin dato"), no lo pintes de verde ni de +rojo. No afirmes en verde algo que **dejó de ser cierto**. + +- La escalera de badges degrada a **"bloqueado" (gris)**, no a rojo: no inventa un fallo, dice + "este dato no puede ser válido si el de abajo cae". +- "Publicado con otra IP" → **pendiente**, no verde (el hub sirve algo que ya no es verdad). +- `rechazado` no aplica en el hub (su rechazo es síncrono 400/409, no deja estado) → **no se + inventó** el caso por simetría. + +## 6. No des por hecho; mira la salida/estado real — incluida la premisa del prompt + +El plan escrito puede partir de una foto obsoleta o de otro entorno. + +- F10.3 en FRR: el hub tiene **cero rutas OSPF en la FIB** (las conectadas ganan) → la fuente es + la RIB de FRR, no `ip route`. Copiar el filtro del edge habría dado tabla vacía con todo OK. +- Los puertos del `br-lan` no salen de `network.br_lan.ports` (el Brume usa sección UCI + **anónima**) → se miden del kernel (`brif`). +- Higiene de git de asgard: la premisa ("la doc no está en el remoto") era **falsa**; un rebase + literal habría degradado doc buena. Verifica antes de reescribir. + +**Una decisión ya cerrada en la ficha es una premisa: contradecirla es PARAR y reportar, nunca +escribir una segunda «decisión cerrada» encima.** Cicatriz (Hermes F11.3, 2026-07-26): la ficha de la +Fase 11 decía *"FQDN = hostname + `dns_suffix`"* y *"zona plana, **sin namespacing**"*; ocho días +después la sesión de ejecución escribió en el MISMO fichero su propia «DECISIÓN CERRADA» con lo +contrario (`..`) y sin señalar el conflicto. Lo peligroso es que **su motivo era +correcto dentro del mecanismo que ella misma acababa de elegir** (reenvío de un solo dominio) — una +decisión que se auto-justifica y nunca sube al usuario. Señal de alarma: *"esto que voy a cerrar, +¿lo decidió el usuario o lo estoy decidiendo yo porque me conviene al mecanismo?"*. Si es lo segundo, +va al resumen como pregunta, no al código como hecho. + +⛔ **La corrección va DONDE ESTABA EL ERROR, no debajo — y un documento «ya corregido» con el +artefacto viejo intacto es PEOR que uno sin corregir**, porque la enmienda hace creer que está +resuelto. *(Cicatriz 2026-08-03, y es `A1` en versión nueva: `dns-malla.md` corrigió el 2026-08-01 su +modelo de resolución en un bloque «⛔ CORREGIDO»… y **dejó vivo el diagrama de arriba**, que dibujaba +la cadena vieja. Nadie lee el párrafo de enmienda: se lee **el diagrama**. Dos días después ese +diagrama era lo que Xavier tenía en la cabeza al describir el diseño, lo que la orquestadora escribió +como **premisa autoritativa** de un encargo, y lo que obligó a la sesión receptora a **parar**. El +modelo derogado sobrevivió a su propia derogación y volvió a entrar por la puerta grande.)* +⇒ Regla: **enmendar es SUSTITUIR.** Si lo que estaba mal era un diagrama, se corrige el diagrama; si +era una tabla, la tabla. Un párrafo debajo que dice *«esto de arriba ya no vale»* deja el error en el +sitio donde la gente mira, y encima le pone un sello de revisado. +⇒ Y el corolario para quien ordena docs: **busca los artefactos VISUALES rancios primero** — +diagramas, tablas, ejemplos de configuración—. Son los que se copian, los que se citan y los que +nadie diffea. + +**Y una decisión tiene que PARECER una decisión.** *(Segunda causa de la misma cicatriz, vista al +ordenar el fichero entero.)* La decisión original vivía **en prosa**, dentro de un párrafo de +objetivo, sin número, sin dueño y sin marca visual. La que la sobreescribió iba en negrita y +mayúsculas: «**DECISIÓN CERRADA**». **Ganó la que parecía una decisión.** Por eso el formato no es +cosmético ni solo un remedio contra el tamaño: un bloque **`D` numerado, con dueño explícito +(usuario / técnica) y separado del texto normal** se distingue a un golpe de vista, y eso vale +**también en un documento corto**. + +⭐ **Un fallo de RED transitorio se lee como un fallo de PERMISOS si no reintentas.** *(Cicatriz +2026-07-27: dos sesiones concluyeron «esta máquina no tiene credenciales para `git.manabo.org`» y +dejaron un chart de producción sin aterrizar. Era falso — cinco pushes ese mismo día desde ese mismo +PC. El mismo día, un push a `git.c2et.com` falló con `Recv failure: Connection was reset` y **entró +al segundo intento**.)* Antes de convertir un fallo en una conclusión: **reintenta**, y si vuelve a +fallar, reporta el error **literal** en vez de tu interpretación de él — «connection reset» y «no +autorizado» no son lo mismo y llevan a sitios opuestos. + +**Y (de Solidaria) si una premisa del encargo es falsa: PARA y repórtalo — no la "arregles" sobre la +marcha.** El prompt puede dar por hecho algo que no se sostiene; corregirlo en silencio esconde el +problema. Además: **lo que verificaste en una sesión anterior PUEDE haber cambiado** (otras sesiones, +otros commits entre medias) → de lo heredado tienes MÁS que re-verificar, no menos, justo porque +crees que ya lo sabes. Vuelve a mirar el estado real. + + +⭐ **Y la forma concreta de trabajarlas: las premisas del encargo se NUMERAN** (`P1`, `P2`…) con +veredicto explícito —verdadera / falsa / matizada, con `fichero:línea`—, y el **ratio de premisas +falsas** es el termómetro de *«estoy escribiendo de memoria»*. **«El doc lo dice» es una premisa a +verificar**, no una fuente. +⇒ ⛔ **Y hacia atrás, que es la mitad que se olvida: al ESCRIBIR un hallazgo se nombra la RUTA +EXACTA que se midió, nunca la categoría.** Una categoría con **una sola medición** detrás se lee +después como universal, y de ahí se hereda como premisa falsa. *(Cicatriz cosecha 2026-08-15: «el +camino Rocky» eran **dos** caminos y solo se había medido uno; la frase viajó por tres documentos +antes de que alguien la careara.)* + +⛔ **Y una forma de romper un documento que no deja rastro: la CIRUGÍA POR NÚMERO DE LÍNEA.** Un +`sed '1342,1453d'` o un `sed "${L}r fichero"` acierta o destroza según un número que **cambia con +cada edición anterior de la misma sesión** — y el daño es *texto que desaparece*, que es justo lo que +no se nota releyendo por encima. *(Cicatriz 2026-08-18: la orquestadora editó así la ficha `I11` +durante toda una sesión y la rompió **dos veces** — un borrado se llevó la línea ⛔ *«que nadie +construya un des-aplicar»* (**el raíl que su propio encargo repetía**) y una inserción metió `I11.9` +**dentro de la viñeta de `I11.8`**, cuya cola quedó leyéndose como suya. Lo cazó la sesión siguiente +al ir a trabajar sobre la ficha; la orquestadora había «verificado» cada edición con un `grep` del +marcador nuevo, que **siempre casa** aunque el resto haya quedado hecho trizas.)* +⇒ **Un `grep` del marcador que acabas de insertar no verifica una edición: verifica tu inserción.** +Lo que hay que mirar es **el texto de alrededor**, y en concreto **la línea anterior y la posterior +al corte**. ⇒ Y siempre que se pueda, edita por **anclas de contenido** (buscar y sustituir una +cadena única) en vez de por número de línea; los números se recalculan solos y las anclas no. + +## 7. Fuente única + copias vendidas + red anti-deriva + +Un artefacto compartido vive en **un** sitio; los consumidores llevan copia **commiteada** +(build sin red/Node) y un `--check` detecta la deriva. Nunca se edita la copia. + +- `manabo-ui` (design system propio) → `sync.sh` copia y `--check` verifica; el `.ipk` y la + imagen del hub construyen sin dependencias externas. Tres consumidores, cero cambios en el + componente = el contrato estaba bien puesto. + +⛔ **Y cuando coses una cicatriz, BUSCA A LOS HERMANOS: un arreglo que no se propaga deja al gemelo +armado, y encima con la prueba de que el fallo es real.** *(Cicatriz 2026-08-06, censo de los bancos +del chart del hub: `aip-sync.sh` y `lo-reconcile.sh` son **dos drivers del mismo directorio que hacen +lo mismo** —extraer un bloque del render, mandarlo a un host y ejecutarlo como root—. El 2026-08-02 +`aip-sync.sh` se comió una extracción rota y se cosió bien: `awk`, anclas **afirmadas** y una +aserción negativa. A su hermano no lo miró nadie. Cuatro días después, en `mode: loadBalancer` el +ancla de cierre de `lo-reconcile.sh` **ya no existe** —`R14` quitó el reconciliador a propósito— +pero la de apertura sí, así que el rango del `sed` corre hasta el final del render: **893 líneas +donde son 36**, con los `ExternalSecret` dentro. Su única guarda es `[ -s ]`, que 893 líneas pasan +de sobra, y lo siguiente de ese script es `scp` + **`sudo bash`**.)* +⇒ La pregunta, y es la misma de la poda (§1): **¿quién MÁS hace esto?** Un arreglo se termina cuando +se ha buscado al resto de sitios con esa forma — no cuando el fichero que fallaba pasa. +⇒ Y el sub-raíl que lo hace concreto: **una guarda de «no vino vacío» no protege de «vino de más»**. +Si de lo extraído depende un `bash`, la guarda tiene que **afirmar las dos anclas** y llevar una +**aserción negativa** (*esto NO debe aparecer aquí dentro*). El tamaño no es un testigo. + +**Si tocas artefactos del OTRO repo, le debes a ese repo su entrada — en la MISMA sesión.** El +espejo cross-repo no se pudre por deriva de redacción: se pudre **asimétricamente**, porque el +trabajo hecho desde el repo A sobre cosas de B se documenta solo en A. Síntoma: casillas `[ ]` en B +para cosas **desplegadas** en B. *(Cicatriz Hermes 2026-07-26: el sidecar Rosenpass corriendo en el +chart del hub y el backlog del hub dándolo por pendiente, con un plan que nunca ocurrió; y un fix +del hub —`F13.0-c`— que solo existía en el backlog del edge.)* La disciplina del espejo falló justo +para el trabajo cross-repo, que es **para lo que existe el espejo**. + + +⛔ **Y el caso más caro de fuente doble no es un artefacto compartido: es el RELATO escrito DOS +VECES, en el backlog y en la bitácora.** *(Cicatriz 2026-08-18: la orquestadora documentó cada +hallazgo de dos días en los dos sitios; la ficha `I11` llegó a **464 líneas** y Xavier dijo que +**había dejado de poder leer el backlog**. La bitácora ya lo tenía todo: lo del backlog era una +copia pura.)* +⇒ La regla que lo evita **ya existía** —*«las decisiones se quedan, la EVIDENCIA se va»*, +`sesion-ordenacion-docs.md` (capa `workflows/`)— pero vivía **en el workflow de la sesión +que LIMPIA, no en el de las que ESCRIBEN**, así que se incumplía sistemáticamente y se pagaba +después en una pasada. Por eso está aquí ahora. +⇒ **Al escribir, decide el destino UNA vez**, con el test del workflow: *¿una sesión futura necesita +esto para NO contradecir una decisión?* → **backlog**. *¿Es cómo nos enteramos —lo medido, las +predicciones, los mutantes, las premisas falsas, el relato—?* → **bitácora**, y en el backlog **un +puntero fechado**. ⭐ Escribirlo en los dos **no es prudencia: es crear la deriva** que §7 existe +para impedir, porque el día que uno de los dos se corrija el otro seguirá ahí. +⛔ **Un valor por DEFECTO declarado como dato en la capa base solo es fuente única si TODOS los +consumidores pasan por el merge.** El que se lo salta no es un descuido: es una **segunda fuente** +—con su propia verdad— y hay que buscarlo **explícitamente al introducir el default**, no el día que +los dos valores diverjan. *(Cicatriz infraserver, cosecha 2026-08-15: un endpoint construía su vista +**sin mergear la capa base** y devolvía otro valor.)* +⇒ Es la pregunta de la poda (§1) en la capa de datos: **¿quién MÁS lee esto, y por dónde?** Declarar +el default no reparte el default. + +⛔ **El docstring/manual ES la interfaz — así que también tiene fuente única.** Cambia el +comportamiento → cambia el docstring **en el mismo commit**. Un doc que miente no es un doc +desactualizado: **ES el bug**, porque es lo que el llamante lee **en vez** de leer el código, y no +hay ningún gate detrás que lo desmienta. + + +## 8. Fallo benigno explícito, nunca degradación silenciosa + +Diseña qué pasa cuando algo cae, y hazlo verificable. Y si algo se rompe en silencio, que grite. + +- Rosenpass cae → se **mantiene la última PSK** (WG sigue cifrando); NO se baja a sin-PSK. +- `gre-sync` reconcilia la ruta al spoke **en cada vuelta** (no sólo al crear) — el opuesto era + perder la ruta de vuelta tras cada reinicio, en silencio. +- El guard de build falla si falta la copia del bundle, en vez de servir un `