metodo.md describe las CUATRO capas (backlog · decisiones · AVISOS · bitácora) y la regla de refinement de D7; el linter estampa doc/decisiones.md en los init nuevos y gana tres checks de la capa de decisiones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1250 lines
98 KiB
Markdown
1250 lines
98 KiB
Markdown
# El Método — constitución
|
||
|
||
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-22.** 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.
|
||
>
|
||
> ⛔ **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.)*
|
||
|
||
> 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.
|
||
|
||
|
||
## 0. Si no escala, no vale para nada *(Xavier — la máxima que gobierna a las demás)*
|
||
|
||
> *«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.**»*
|
||
|
||
⇒ **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.
|
||
|
||
⛔ **Y el refinement que lo hace ejecutable: una decisión que acarrea trabajo FUERA del alcance del
|
||
producto o de la fase NO se queda como casilla.** Se convierte en **épica nueva** si es grande, o se
|
||
va **a su fase** si tiene una — y se prioriza como todo lo demás: **por valor de producto**, no por
|
||
lo bien argumentada que esté. *(Xavier, `D7`, 2026-08-22. Estrenada el mismo día: `I11.11` salió de
|
||
su ficha a una épica propia **con su valor declarado — cero, porque no hay nadie esperándola**, que
|
||
es justo el dato que una casilla escondida dentro de otra ficha no da.)*
|
||
⇒ Es el mismo movimiento que el del ámbito de amenaza, una capa más arriba: **lo que no es de aquí
|
||
no se descarta ni se cuela — se muda, con su valor escrito.**
|
||
|
||
## 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/<rama>`**, 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/<pid>.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 (`<host>.<sede>.<dominio>`) 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**, y los destinos son **CUATRO**: el corpus de un repo
|
||
tiene **cuatro capas**, no tres. *¿Es una decisión cerrada, de las que otra sesión no puede
|
||
contradecir sin PARAR?* → **`doc/decisiones.md`**, con su texto **íntegro**. *¿Es trabajo — un item
|
||
que cabe en una sesión?* → **`doc/backlog.md`**, que es **el eje del troceo**. *¿Es lo que hay que
|
||
saber el primer día?* → **`doc/AVISOS.md`**, que se lee antes que el backlog. *¿Es cómo nos
|
||
enteramos —lo medido, las predicciones, los mutantes, las premisas falsas, el relato—?* →
|
||
**`doc/bitacora.md`**, append-only, y en el backlog **un puntero fechado**. ⭐ Escribirlo en 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í.
|
||
⛔ **Y la cuarta capa existe porque una decisión guardada en el backlog se numera POR FASE, y la
|
||
numeración por fase colisiona.** *(Cicatriz 2026-08-22, `D6` de Xavier: **18 decisiones distintas
|
||
llamadas `D1`** en un solo `backlog.md` — y cuatro fallos de una semana que salen de ahí: un
|
||
**resumen** que gobernó en lugar de la decisión, un «precio aceptado» que Xavier nunca pensó, dos
|
||
ids que colisionaban, y una sustitución con las **dos versiones vivas** a 90 líneas de distancia.
|
||
En sus palabras, para qué sirve la capa: «así tienes todo lo peligroso en un único documento».)*
|
||
⇒ Por eso se ordena **por ámbito, no por fase**; cada entrada lleva **texto íntegro** (nunca un
|
||
resumen — un resumen adquiere invariantes que la decisión no tiene), **dueño visible** (*Xavier* o
|
||
*técnica*: una técnica escalada como suya ya coló), **estado**, y el **testigo en la misma línea**
|
||
si afirma algo medible (§1). Una sustitución **edita la entrada vieja** —⚰️ y puntero—, jamás deja
|
||
las dos vivas.
|
||
⇒ Y «todo lo peligroso en un único documento» solo protege si **alguien lo recorre** (§8): toda
|
||
sesión de diseño mira ahí **qué contradice** antes de cerrar nada, y si contradice una `vigente`,
|
||
**PARA y reporta** (§6) o la sustituye explícitamente.
|
||
⛔ **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 `<script>` 404.
|
||
|
||
⛔ **Un mecanismo de resiliencia que NUNCA se ha ejercitado puede ser autodestructivo justo en el
|
||
momento de usarse.** *(Cicatriz 2026-07-27: el multi-hub existe para **sobrevivir a la caída de un
|
||
hub** — y hasta v2.1.34, **añadir el segundo tumbaba el túnel del primero ~40 s**, porque el
|
||
endpoint del hub nuevo es un **nombre** y, si no resolvía, `netifd` rehacía la interfaz WG entera.
|
||
La propiedad que existía para dar resiliencia **te costaba un hub al activarla**. Llevaba ahí desde
|
||
que existe el multi-hub, y solo apareció cuando por primera vez hubo **dos hubs vivos en el mismo
|
||
edge**.)*
|
||
⇒ Un camino de recuperación que no se ha recorrido **no es un camino de recuperación**, es una
|
||
hipótesis. Ejercítalo **antes** de necesitarlo: el día que haga falta, ya es tarde para descubrir
|
||
que el remedio quita lo que venía a proteger.
|
||
|
||
⛔ **La REDUNDANCIA oculta el fallo que la va a necesitar.** Un defecto tapado por el camino
|
||
alternativo no se ve mientras el camino alternativo esté — y aparece **exactamente** el día que ya no
|
||
está, o sea en el peor momento y sin historial. *(Cicatriz `A18`, 2026-07-31: con la identidad del
|
||
edge divergiendo entre los dos hubs, el nombre **seguía respondiendo 3/3** porque el tráfico rodeaba
|
||
por el otro Brume —`G8` funde los dominios de inundación—. Con ese aparato apagado, el hub habría
|
||
perdido al edge **del todo**.)*
|
||
⇒ Para ver el fallo, **quita la redundancia y vuelve a medir**: apaga el camino alterno, tira el
|
||
segundo hub, desconecta el vecino. Un verde obtenido con todas las rutas vivas **no dice si el
|
||
servicio funciona: dice que alguna de ellas funciona.**
|
||
|
||
⛔ **Y su hermano, que es el que nadie busca: la ABUNDANCIA de recursos esconde el derroche.** Medir
|
||
siempre en el aparato más holgado no certifica nada del más apretado — y el defecto no está latente,
|
||
está **ocurriendo**, solo que sobra sitio para pagarlo. *(Cicatriz `P12`, 2026-08-02: con `D10` un
|
||
edge **sin binario de Rosenpass** seguía bajándose la pubkey del hub —**524.160 B**— en el bucle de
|
||
20 s **y por cada hub**. En un Brume 2 (6,8 GB de overlay) no se nota **jamás**; en un GL-AR300M eran
|
||
**1 MB sobre 212 KB libres**. Se descubrió porque el aparato pequeño lo hizo visible, no porque
|
||
alguien lo buscara.)*
|
||
⇒ Pregunta cuando una medida salga limpia: **¿en qué aparato la he tomado, y qué le sobraba?** Disco,
|
||
RAM, CPU y ancho de banda son todos amortiguadores que convierten un fallo en una estadística que
|
||
nadie mira. La flota real se parece al más pobre, no al banco.
|
||
|
||
⛔ **«0 reinicios» no es estabilidad: es un estado NO EJERCITADO.** Un proceso vivo sostiene en su
|
||
memoria cosas que ya no existen fuera — y mientras no reinicie, nadie se entera. El reinicio no
|
||
*rompe* nada: es el momento en que se descubre lo que llevaba roto. *(Cinco cicatrices en tres días,
|
||
2026-07-27/30: los seis `postconf` del smtp-relay vivían en un contenedor y se perdían al reiniciar ·
|
||
el `argocd-image-updater` falla `unauthorized` en cada ciclo con una **credencial cacheada en
|
||
memoria**, y lo único que frenaba que reescribiera un pin bueno era esa avería — reiniciarlo **arma**
|
||
el riesgo · `kernel-engine` corre un digest que **ya no está en el registro**: lleva 62 días arriba y
|
||
un reinicio lo deja en `ImagePullBackOff` · `tang` y `tftp` son el mismo caso sin verificar.)*
|
||
⇒ Cuando midas algo que lleva mucho arriba, la pregunta **no** es «¿funciona?» — es **«¿sobrevive a
|
||
un reinicio?»**, y se contesta careando lo que el proceso usa contra lo que hay **hoy** en su origen
|
||
(el registro, el fichero, el secreto). Un `uptime` largo es motivo de **sospecha**, no de confianza.
|
||
⇒ Corolario para retiradas: antes de quitar un mecanismo, comprueba si algo vivo **depende de que no
|
||
se reinicie**. Quitar la última forma de reconstruir algo que hoy solo existe en una caché convierte
|
||
una limpieza en una pérdida.
|
||
⇒ **Y el mismo raíl al revés: un arreglo COMMITEADO no es un arreglo APLICADO.** Todo lo que se lee
|
||
**al arrancar** —ConfigMap, variables de entorno, fichero de configuración— sigue siendo el viejo
|
||
hasta que el proceso reinicie, y el `git log` dice que está arreglado. *(Cicatriz 2026-07-31:
|
||
`mcp-build` llevaba **21 h** con el config anterior al renombrado de los hubs. El fix se commiteó a
|
||
las 00:49, pero **editar un ConfigMap no rueda el Deployment** ⇒ seguía sondeando un nombre que hoy
|
||
es `NXDOMAIN` ⇒ el `verify` no respondía nunca ⇒ **auto-rollback que deshizo un pin bueno**. Tercera
|
||
vez esta semana que **el mecanismo de seguridad es el asesino**.)*
|
||
⇒ Cierra el cambio **midiendo lo que el proceso vivo cree**, no lo que dice el repositorio.
|
||
|
||
⛔ **REVOCAR una credencial se lleva a sus HIJAS — así que «revocar el root» solo es seguro cuando
|
||
todo lo que importa es HUÉRFANO, y eso se mide ANTES.** Una limpieza de credenciales no es una
|
||
limpieza si no sabes qué cuelga de lo que vas a quitar. *(Cicatriz 2026-08-12, y la cadena empieza en
|
||
una recomendación de la orquestadora: se propuso **quitar el root permanente de Vault** —correcto
|
||
como higiene— y se ejecutó rotándolo con `generate-root` y revocando el viejo. En Vault los tokens
|
||
son **hijos de quien los crea**, y `revoke` del padre **borra el árbol**: 14 minutos después el
|
||
`ClusterSecretStore` del cluster `hermes` pasaba a `InvalidProviderConfig` y **30 `ExternalSecret`
|
||
dejaban de refrescarse** — entre ellos la base de datos, el SMTP y el Twilio de una aplicación en
|
||
producción.)*
|
||
⇒ ⭐ **Y lo que lo probó no fue el muerto, fue el SUPERVIVIENTE**: los tres tokens que aguantaron
|
||
eran `orphan: true` y el que murió era el único que no. *El diferencial es la prueba*, y no exigió
|
||
el privilegio que ya no se tenía — que es justo cuando hace falta un método, porque investigar un
|
||
incidente de credenciales suele empezar sin credenciales.
|
||
⇒ Antes de revocar: **enumera qué cuelga y comprueba su `orphan`**. Y al acuñar cualquier
|
||
credencial de servicio, **hazla huérfana desde el principio** — una credencial que hereda el ciclo
|
||
de vida de quien la creó es un rehén. La solución de fondo no es la orfandad, es **no usar tokens
|
||
estáticos** (auth method), pero la orfandad es lo que se puede hacer hoy.
|
||
|
||
⛔ **No concedas un privilegio para callar un error: pregunta si el proceso debería estar haciendo
|
||
eso.** *(F11.14: chronyd moría con `CAP_SYS_TIME not present`. Añadir la capacidad era el reflejo —y
|
||
le habría dado a un servicio en red la potestad de mover el reloj del NODO, y con ella TLS,
|
||
certificados y etcd. La respuesta era `-x`: **un contenedor que SIRVE hora no es uno que PONE
|
||
hora**.)*
|
||
|
||
⭐ **Y su forma positiva, que es de DISEÑO y no de permisos: la AUSENCIA de una capacidad es un
|
||
guardarraíl.** El poder peligroso se **retiene estructuralmente** —no se expone porque **no se
|
||
construye**—: vocabulario cerrado, sin `exec`, sin SQL libre, sin comando arbitrario. Lo que no
|
||
existe no hay que acordarse de no usarlo, ni protegerlo con un permiso que alguien acabará
|
||
concediendo un martes para callar un error.
|
||
|
||
⛔ **Una PROYECCIÓN construida para otra cosa no sirve de entrada a una decisión: los campos que no
|
||
lleva se leen como valores, no como ausencias.** *(Cicatriz `E6`, 2026-08-01: el reconciliador
|
||
decidía la política de resolución a partir de `lease_rows()` —la proyección **para publicar**, que no
|
||
lleva `nat`—. «No viene `nat`» se leyó como «ninguna LAN tiene salida», así que la negativa se
|
||
encendía **en cada vuelta, con el NAT puesto**. Y el test propio lo consagraba.)*
|
||
⇒ Dos arreglos, y hacen falta **los dos**: que el dato salga de **su** fuente (la configuración de las
|
||
LAN, no la proyección), y que **una fila que no declara el campo devuelva `dato_incompleto` y no
|
||
decida nada**. Una ausencia nunca puede ser interpretable como un valor.
|
||
|
||
⛔ **Un valor de configuración que sale VACÍO no falla: cae al default, y el default suele ser el
|
||
menos seguro.** *(Cicatriz F11.14, 2026-07-27, cazada **en el render, antes de existir**: el
|
||
`bindaddress` de chrony salía vacío porque `serviceLoopback` lo inyecta el ApplicationSet y no está
|
||
en ningún `values`. Un `bindaddress` a secas no da error — chronyd vuelve a su default y escucha en
|
||
**todas** las direcciones, IP pública incluida: un **reflector de amplificación NTP** abierto a
|
||
internet.)* ⇒ En las plantillas, `required` en todo valor cuyo vacío sea peligroso, para que
|
||
**reviente al renderizar** en vez de desplegarse abierto.
|
||
|
||
⇒ ⛔ **Y el corte que decide cuándo un default es admisible: PREFERENCIA vs IDENTIDAD.** Un default
|
||
de **preferencia** —qué valor usar entre varios válidos— es legítimo. Un default de **identidad**
|
||
—qué ES esto— **no existe**: si el dato que identifica al artefacto no está declarado, el artefacto
|
||
**no se puede construir**, y la respuesta es un fallo explícito **con su motivo**, nunca el valor más
|
||
común. *(Cicatriz infraserver, cosecha 2026-08-15: un hueco de identidad rellenado con «lo de
|
||
siempre» sirvió un **kickstart destructivo** con un **HTTP 200** impecable.)*
|
||
|
||
⛔ **Y el corolario que este raíl NO llevaba, medido seis veces el 2026-08-19: un fallo benigno
|
||
explícito sólo protege si alguien LEE la línea.** Un banco que se salta algo **anunciándolo**, en
|
||
una salida que nadie mira, produce el mismo resultado que el silencio — y encima con la conciencia
|
||
tranquila de haberlo declarado. *(Cicatriz `V11b`: el careo `routes-*.txt` ↔ `superficie.md` del
|
||
chart del hub buscaba `repos/infrarouter/…`, carpeta renombrada a `repos/hermes-hub` el
|
||
2026-08-15. Se saltó en **todas** las corridas de cuatro días, y no en silencio: llevaba un
|
||
`warnings.warn` **además** del `pytest.skip`, puesto a propósito citando este §8, con el comentario
|
||
«*esto no se salta en silencio — se salta diciéndolo*». **Y lo decía. Y nadie lo leyó.** Mientras
|
||
tanto las cuatro capturas se quedaron atrás de una capa entera —la CRL— sin que el gate pudiera
|
||
enrojecer.)*
|
||
⇒ ⭐ **Lo único que convierte un aviso en un gate es un NÚMERO QUE TIENE QUE CUADRAR**, y que ese
|
||
número mueva el **código de salida**, no la pantalla. Las tres formas que ya existen en el árbol, y
|
||
se copian en vez de inventarse:
|
||
- **suelo de comprobaciones EJECUTADAS** (`guards.sh`: `SUELO_CHECKS`) — un `command not found` no
|
||
para el script y la comprobación *simplemente no ocurre*;
|
||
- **suelo de mutaciones EJECUTADAS** (los bancos de `hermes-edge`/`hermes-hub`) — una mutación que
|
||
deja de casar **no se queja**;
|
||
- **un `skip`/una dormancia CONTADOS** (`helm-charts/hermes-hub/tests/conftest.py`) — saltarse un
|
||
careo es rojo, y las ausencias legítimas se declaran **por nombre** y se carean **por igualdad**,
|
||
para que la lista no se pudra afirmando una ausencia que ya no existe.
|
||
|
||
⛔ **Y su hermano gemelo, que es el que no se ve mirando el código: un gate que NADIE LANZA da
|
||
igual de bien escrito que esté.** *(Mismo censo: `probe-guard-hub.js` llevaba los mismos cuatro
|
||
días roto —falla ruidoso, `MODULE_NOT_FOUND`— y nadie vio el rojo porque no lo corre ningún gate;
|
||
y `pq-sync.sh` lleva escrito de su puño que **su línea base estuvo ocho días en rojo** «porque
|
||
este banco no lo corre ningún gate». Un banco mudo y un banco muerto se leen igual desde fuera.)*
|
||
⇒ Al escribir una comprobación, la pregunta no acaba en *¿puede ponerse roja?* — sigue en **¿quién
|
||
la lanza, y qué pasa si ese lanzador deja de existir?** Un suelo dentro de un banco que nadie
|
||
lanza sigue sin ser una puerta.
|
||
|
||
⛔ **Y el rastrillo que hay que pasar SIEMPRE que un testigo salga verde: ¿ese testigo puede decir
|
||
que no?** Tres veces el mismo día 2026-08-19, las tres midiendo el arreglo del censo `V12` y las
|
||
tres a punto de firmar un verde falso:
|
||
- `git status --porcelain 2>/dev/null` **desde WSL** aborta por *«dubious ownership»* ⇒ **no imprime
|
||
nada porque FALLA**. Ese vacío se leyó como «el árbol está limpio»; medido desde Git Bash, había
|
||
**tres ficheros mutados**.
|
||
- `bash -n` dio **verde** sobre una llamada cuyos argumentos estaban corridos un puesto (unos
|
||
comentarios metidos dentro de una continuación `\`). Sintaxis válida, semántica rota: lo cazó
|
||
**correr** el banco, con `$2: unbound variable` a mitad de vuelta.
|
||
- Un `tail` sobre `/tmp/a.out` devolvió la salida **de otra sesión** —fichero ajeno, de doce horas
|
||
antes, con el redirect propio fallando por permisos— y estuvo a punto de reportarse como medida
|
||
propia.
|
||
⇒ Tres instrumentos distintos, un solo modo de fallo: **el silencio de la herramienta se leyó como
|
||
la respuesta buena**. Antes de creerte un verde, **hazle decir que no una vez** — rompe algo a
|
||
propósito y comprueba que el testigo lo acusa. Y no midas en un sitio compartido (`/tmp`) lo que
|
||
tiene que ser tuyo.
|
||
|
||
## 9. Sesiones: fresca para su stack; la continuidad la lleva el orquestador
|
||
|
||
Cada sesión de ejecución arranca limpia sobre el stack que toca (edge ≠ hub ≠ MCP). No se
|
||
reutiliza una sesión de otro stack "por aprovechar contexto" — arrastra ruido. El hilo entre
|
||
sesiones lo mantiene la orquestadora (backlog + memoria).
|
||
|
||
**TODO PROMPT DICE SI ES EN FRÍO O EN CALIENTE.** Es la primera línea, no una nota al pie: quien
|
||
lo recibe tiene que saber si abre sesión nueva o lo pega en la que ya corre. *(Cicatriz 2026-07-26:
|
||
la orquestadora de redes escribió toda una racha de prompts sin decirlo — se asumían fríos porque
|
||
empezaban con el ritual de lecturas, y nadie lo había hecho explícito.)*
|
||
|
||
- **CALIENTE** — mismo stack, mismo repo, **continuación directa**, y el contexto de la sesión
|
||
anterior es un **activo**: acaba de construir justo eso que vas a tocar. Típico de los remates
|
||
cortos (cerrar un endpoint que ella misma dejó anotado). Re-arrancar en frío para cambiar un
|
||
decorador significa releer cinco documentos para nada.
|
||
- **FRÍA** — cambio de stack; **o** la tarea es *verificar / cuestionar* lo que esa sesión acaba de
|
||
hacer; **o** ha pasado tiempo y el estado ha cambiado; **o** su contexto ya es ruido (sesión
|
||
larguísima, tema agotado).
|
||
|
||
⛔ **La sesión que acaba de construir algo es la PEOR para verificar que está bien.** Ya cree sus
|
||
propias premisas, y el raíl 6 dice que de lo heredado hay que re-verificar **más**, no menos. Si la
|
||
tarea es comprobar, medir o poner en duda → **fría, siempre**. Construir encima → caliente.
|
||
|
||
## 10. Lo que "funciona por casualidad" está roto
|
||
|
||
Construir bien la capa de encima destapa lo de debajo. Al hacerlo, **arréglalo y anótalo**, no
|
||
lo dejes pasar.
|
||
|
||
- CSS que teñía por selectores de ancestro que no cruzan el Shadow DOM (llevaba tiempo sin
|
||
teñir nada). `dnsmasq interface=wg0` sin dirección → el hub no servía DNS a nadie desde hacía
|
||
meses. El split-DNS escribiendo en `/etc/dnsmasq.d` cuando OpenWrt lee `/tmp/dnsmasq.d`.
|
||
|
||
**Cuando destapes algo muerto, mira las DOS mitades: que nadie lo consuma puede estar escondiendo
|
||
que tampoco se podía escribir.** *(Cicatriz F11.9, 2026-07-26: la Global Config del hub estaba
|
||
muerta río abajo —su payload alimentaba a un agente borrado tres semanas antes— y al ir a
|
||
arreglarla apareció que también lo estaba río arriba: el handler `POST` estaba **registrado como
|
||
`GET`**, así que el formulario devolvía **405** y «Guardar» no había guardado **nunca**. Cada mitad
|
||
tapaba a la otra: nadie echaba en falta un dato que nadie podía introducir.)* **Señal barata y
|
||
fiable**: un contador de versión de config que sigue en `1` con todos los campos vacíos no es «no
|
||
lo han usado» — es «no se puede usar». Búscala.
|
||
- Si es de **producción** (p. ej. el `gre-sync` de c2et), no lo arregles a lo loco: anótalo como
|
||
bomba latente y encájalo en el plan de release (ver `incidente-produccion.md`).
|
||
|
||
## 11. El orden de lo irreversible: añadir antes de quitar · *C14 (4 fuentes), promovido 2026-08-08*
|
||
|
||
Todo lo verificable ocurre **antes** del paso que no se deshace. Se **añade antes de quitar**, se
|
||
crea antes de borrar; cada paso de una release se revierte por separado y ninguno depende de dos
|
||
cosas nuevas a la vez.
|
||
|
||
- Un tercer nodo de etcd entra **antes** de que salga el que se retira — al revés, el quórum se
|
||
queda en uno.
|
||
- "Nunca borres el `IPAddressPool` viejo hasta tener el nuevo sirviendo" (MetalLB).
|
||
- El cutover de un servicio con nombre: primero responde el nuevo, luego se retira el viejo.
|
||
|
||
## 12. Exploración solo-lectura, sin autoengaño · *C35 (Solidaria), promovido 2026-08-08*
|
||
|
||
Explorar es **medir sin tocar, y sin creerte tu propia medida**.
|
||
|
||
- `curl` **con control**: mira `/api/health`/`http_code`, no "parece que responde".
|
||
- **"Aquí no hay nada" es un RESULTADO**, no un fracaso — se anota igual que un hallazgo.
|
||
- Los **falsos positivos de `grep` son el riesgo nº1**: una lista larga de "roto" delata TU método
|
||
(un patrón demasiado amplio), no el sistema. Verifica cada hit contra la fuente antes de declararlo.
|
||
- Sobre **texto generado**, el testigo se define sobre las líneas **EJECUTABLES**, no sobre el
|
||
fichero: si el generador emite comentarios que documentan las ramas NO tomadas, el grep ingenuo
|
||
mide el comentario — verde indistinguible de rojo. *(Cicatriz: banco de render de infraserver,
|
||
2026-08-12.)*
|
||
- Medir, no opinar. Sondear una escritura **ES** escribir (no es exploración).
|
||
|
||
## Entrega — la parte de "sacar el trabajo" *(promovido 2026-08-08)*
|
||
|
||
El método nació de verificación/UI; la cicatriz de **entrega** (git, secretos, naming, deploy) vivía
|
||
fuera, repetida en varios repos. Aquí se consolida.
|
||
|
||
### Naming · *C17, C32*
|
||
- **Ids con prefijo por ámbito**, **no se renumeran** (romperían citas cruzadas), y **antes de
|
||
añadir uno se corre el grep de colisión** — no basta con mirar.
|
||
- El nombre nombra **DESTINOS, no rutas**: ni el host, ni la IP, ni activo/reserva entran en un FQDN.
|
||
Se escribe como **regla**, no como observación.
|
||
- **Renombrar sin CNAME de cortesía**: que lo viejo rompa a la vista, no en silencio.
|
||
|
||
### Secretos · *C20, C21, C22*
|
||
- **Una frontera de credenciales por dominio**; aislamiento por **ServiceAccount** (ninguno con
|
||
acceso a secretos ajenos).
|
||
- **Capacidad > credencial**: se **actúa** con el secreto, no se **revela** (revelar lo deja en el
|
||
transcript); el estado raro se dice con la URL, nunca con el secreto.
|
||
- **Nunca en git**: imperativos / GPG / PushSecret, forzado por `.gitignore`; se documenta **cómo
|
||
recrearlos**, no el valor.
|
||
|
||
### Git · *C24, C25, C38*
|
||
- **Git canónico único y MEDIDO**: se empuja a un remoto y solo ahí; un fallo se **mide**
|
||
(dry-run/reintento) antes de concluir "no hay credenciales"; el remoto muerto se elimina localmente.
|
||
- **Commits Conventional** (`feat/fix/docs/ci` + scope); el `doc:` registra el **porqué**.
|
||
- **Working-tree compartido**: `git add` con ruta explícita (**nunca `-A`**), `git pull` antes de
|
||
deploy y antes de commitear docs, una tarea = una sesión/worktree.
|
||
- La **representación en disco es parte del artefacto**: cuando un hash o un runtime consume el
|
||
checkout, se fija con el repo — **eol** por `.gitattributes` y **bit de ejecución** — o el testigo
|
||
mide el checkout, no el contenido (una DERIVA falsa en un clone sin tocar entrena a ignorar el
|
||
check). *(Cicatrices: copia vendorizada en CRLF, 2026-08-11; `git archive` sale en CRLF.)*
|
||
|
||
### Deploy · *C12, C13*
|
||
- **Un solo flujo de build**, disparado **deliberadamente**, no por `git push`; el legacy es
|
||
**lápida** (código muerto conservado hasta MEDIR qué lo ejecuta, no borrado a ciegas).
|
||
- Todo cambio **por git → deploy declarativo**; imagen pineada por **DIGEST inmutable commiteado**;
|
||
el historial de commits ES el registro de auditoría.
|
||
- El testigo de «**puedo entregar**» no es el push a git: es el **artefacto en el registro del que
|
||
tira el destino**. Una cadena de entrega tiene tantas credenciales como saltos, y la que no se
|
||
ejercita se pudre en silencio — un componente que lleva semanas sin reconstruirse no se sabe si
|
||
puede entregar. *(Cicatriz: robot de registro con 401 en tres repos, mudo durante semanas porque
|
||
nadie construía, 2026-08-12.)*
|
||
- El **orden de lo irreversible** aplica aquí → **raíl 11**.
|
||
|
||
## La estructura de sesión no vive aquí
|
||
|
||
Los dos ejes **fase × persona** y el **handoff automático** entre fases son **procedimiento**, no
|
||
principio → viven en la capa `workflows/`, con la capa **persona opcional** (se activa solo con
|
||
interlocutor no-técnico; en infra en solitario, inactiva pero documentada).
|