Commit Graph
552 Commits
Author SHA1 Message Date
Sergio 8c1378cb1f cazador: evidencia a favor de la hipótesis de presión de memoria
Al montar otro banco, la carga estaba en 3,34 (contra 0,13 de las cuatro cazas)
porque otro agente arrancó un cargo, y kswapd0 consumía CPU con CERO memoria
libre: recuperación de páginas bajo presión. Cada corrida mapea un libxul de
227 MB.

Es exactamente la condición que las cazas NO tenían y que los dos bancos con
cuelgues SÍ. No es prueba —el estado de aquellas ventanas no se reconstruye—
pero es la primera pista concreta, y cambia dónde buscar: cazar BAJO PRESIÓN de
memoria, no con la máquina ociosa.
2026-09-08 14:17:21 +00:00
Sergio 11e2c5f3a2 cazador del cuelgue en headless: NO reproducido en 202 corridas, y qué descarta
Dos corridas del banco de PGO se colgaron en 120.034 y 120.030 ms — exactamente
el timeout del arnés, o sea bloqueos y no lentitud. Dos de 56 (~3,5%).

NO SE REPRODUJO. 202 corridas en cuatro condiciones, cero cuelgues:

  un binario, un sandbox compartido ......... 30 · 0
  4 binarios alternando, un sandbox ......... 60 · 0  (descarta churn de caché)
  4 binarios, UN SANDBOX NUEVO POR CORRIDA .. 56 · 0  (descarta los namespaces)
  ídem con LAS PÁGINAS EXACTAS del banco .... 56 · 0  (descarta la página)

Si la tasa fuera 3,5%, ver cero en 202 tendría probabilidad ~0,06%. La tasa real
bajo estas condiciones no es ésa, y lo que falta está fuera de ellas.

UN ERROR DE MÉTODO QUE COSTÓ 146 CORRIDAS, anotado en el script para no
repetirlo: las tres primeras cazas usaron flex.html y tablas.html porque las
tenía a mano, cuando las colgadas habían sido en fuera-del-corpus.html y
del-corpus.html. Estuve probando una condición que NO era la observada,
creyendo que sí. Antes de concluir «no se reproduce», comprobar que el
instrumento reproduce lo que dice reproducir.

Lo que sí se aprendió, y acota: es TODO O NADA. En 202 corridas ninguna pasó
siquiera de 45 s — o terminan en ~20 s o se bloquean hasta el timeout. No es
degradación, es bloqueo. Sobrevive la hipótesis que no se puede probar
retroactivamente: contención transitoria de otra cosa en la máquina —que es
compartida con otros agentes— durante esas dos ventanas.

El script queda para que la PRÓXIMA vez se capture en el acto (wchan, syscall,
state, memoria y carga del host, y el log del navegador de esa corrida) en vez
de empezar de cero.
2026-09-08 10:33:13 +00:00
SergioandClaude Opus 5 639948f2cd atuq: la página de inicio y la pestaña nueva, verificadas contra el propio motor
La v0.3 las puso por extensión de sistema y quedó como afirmación. Que el XPI
esté en el artefacto no dice nada: el override lo puede rechazar el gestor de
extensiones, lo puede pisar una política, o el navegador puede arrancar con el
chrome viejo cacheado. Ninguna de las tres falla ruidosamente — se ve una pestaña
nueva perfectamente normal, que es de otro.

Se mide preguntándole AL MOTOR, no mirando el disco: una sonda con permiso
`browserSettings` lee `homepageOverride` y `newTabPageOverride`.

    positivo  moz-extension://…/inicio.html  (las dos)
    control   about:home · about:newtab

El control negativo borra el XPI de `inicio` dentro del overlay temporal —el
rootfs real no se toca— y exige el resultado contrario.

Y una escotilla `ATUQ_DIR` en las dos sondas nuevas, con su aviso a gritos: el
corpus es compartido y hoy mismo el perfil PGO v2 de otro frente re-hasheó
`firefox` y con él `atuq`, así que el artefacto VIGENTE no existe en ningún store
hasta que alguien pague un build de horas. Sin escotilla no se puede correr una
sola prueba de atuq en esa ventana; con ella se corre contra un artefacto viejo A
SABIENDAS, y por eso el resultado no se puede citar como «atuq de hoy pasa».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 21:03:25 +00:00
Sergio e74d449655 perfilar.sh: la corrida de entrenamiento PGO, con las tres defensas puestas
Estaba en un scratchpad y se perdía; ahora vive en el repo con el porqué de cada
guarda, porque las tres se pagaron durante la primera corrida:

1. --unshare-pid en el bwrap. Sin él los hijos SOBREVIVEN al sandbox: un
   http.server quedó vivo 19 HORAS, retuvo el puerto y envenenó las corridas
   siguientes con «Address in use».

2. Puerto aleatorio por corrida. Elimina la colisión de raíz en vez de
   detectarla: si otro proceso tiene un puerto, este arranque usa otro.

3. Marcador único por corrida, servido desde una copia ESCRIBIBLE del corpus.
   La versión anterior horneaba el marcador como constante del script, y eso
   rompía justo lo que el chequeo existe para probar: el servidor huérfano
   servía el MISMO fichero de marca y el chequeo lo daba por bueno. Verificaba
   «alguien sirve este contenido», no «este servidor es el mío». Con el marcador
   por corrida, un servidor ajeno devuelve otra cosa y se aborta.

Y queda escrito por qué los sandboxes de Firefox van apagados en el
entrenamiento: con ellos los hijos mueren con signal 11, el proceso que renderiza
no nace, el servidor registra 0 GET y el único .profraw es el del padre
arrancando — un perfil de nada, con todo en verde. Es lo que hace el
profileserver.py de Mozilla. Sólo aplica al entrenamiento.

La espera al servidor va con reintento y no con un sleep fijo: 2 s concluían
«no responde» sobre uno que sí iba a responder.
2026-09-07 20:45:56 +00:00
SergioandClaude Opus 5 c13be792ab atuq: el camino de native messaging EXISTE — sonda, y tres cosas que no eran obvias
Todo el §6 del SDD 26 —sct, descargas al CAS, archivo con RAG, torrent— pasa por
un solo mecanismo: un proceso Rust hablando native messaging con la extensión.
Antes de escribir ese crate en tawasuyu conviene saber si el camino existe en
NUESTRO build, que es propio, rebrandeado y con MOZ_REQUIRE_SIGNING vacío.

Existe. Una extensión de diez líneas y un host de tres:

    positivo  MENSAJE {"ok":true}
    control   DESCONECTADO No such native application puente_atuq

El control no sólo falla: NOMBRA la causa, que es lo que confirma que la ruta del
manifiesto es la que se probó.

TRES COSAS MEDIDAS QUE NO ERAN OBVIAS:

1. El manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/`, NO en el
   appdir. Gecko lo busca por `XRESysNativeManifests`, que en Linux sale de un
   `/usr/lib/mozilla` compilado, y el rebranding a atuq no lo mueve.

2. Un host que escribe y SALE pierde el mensaje. La primera sonda hacía printf y
   terminaba: el puerto llegaba a onDisconnect «sin error y sin mensaje», o sea
   el peor informe posible — parece que el camino no existe. Con un sleep detrás
   del printf, el mensaje aparece. El host de verdad es un proceso largo, así que
   en producción no se nota; en una prueba, sí.

3. `ExtensionSettings.install_url` con `file://` NO instala nada, y sin una línea
   de log. Probado con normal_installed y con force_installed, y con el XPI dentro
   y fuera del appdir: ninguna instala. Lo que instala las extensiones de atuq es
   el ESCANEO de `distribution/extensions/`. La política sirve para fijarlas y
   configurarlas; leerla como «esto es lo que las instala» es un error fácil,
   porque los dos mecanismos apuntan a los mismos ficheros y se tapan uno al otro.

`sendNativeMessage` no sirve para diagnosticar: devuelve «An unexpected error
occurred» para todo. El error del puerto de `connectNative` es el único que
nombra la causa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 20:27:07 +00:00
Sergio 8a0496dbb3 corpus de maquetación para el PGO: 10 páginas nuestras
Medida la ganancia del PGO ayer: -9,4% en una página de DOM/maquetación y -1,0%
en 3d-raytrace de SunSpider. La razón de la diferencia es que un benchmark
JIT-bound es casi CIEGO al PGO: el bucle caliente no lo ejecuta el C++ de
SpiderMonkey sino el código máquina que el JIT emite en runtime, y el PGO
optimiza el intérprete, el GC y el propio JIT — no lo que el JIT produce.

El corpus de Mozilla está dominado en número de páginas por SunSpider, o sea que
entrenaba mucho justo donde el PGO menos rinde. Estas diez ejercitan el camino
que sí es C++ de punta a punta:

  flex          flexbox anidado, wrap, alineaciones
  rejilla       CSS Grid: pistas, áreas nombradas, auto-fit
  tablas        tablas grandes con table-layout FIJO y AUTO (dos algoritmos)
  texto         columnas, shaping, reflujo por cambio de ancho
  selectores    DOM profundo contra 600 reglas, muchas que NO casan
  pintado       degradados, sombras, opacidad, mix-blend-mode
  transformar   transforms y contextos de apilamiento
  svg           paths, degradados, clip
  desbordes     overflow anidado, position:sticky, scroll programático
  reflujo       layout thrashing: leer y escribir geometría alternadamente

Reglas que cumplen todas, y no son de estilo:
 · DETERMINISTAS (LCG propio, ni Math.random ni Date ni red) — dos corridas hacen
   lo mismo, así que dos perfiles difieren por el timing de los contadores y no
   por haber visitado caminos distintos.
 · AUTOCONTENIDAS — el perfilado corre sin red.
 · NUESTRAS — nada de páginas ajenas capturadas, que traerían licencia y
   fragilidad.
 · ACOTADAS — 16 a 32 s cada una; el perfilado arranca un navegador POR PÁGINA.

Validadas las diez contra el firefox sellado: todas renderizan y producen
captura. `flex` hubo que acortarla de 40 a 12 cajas raíz: con 40 la página medía
68.241 px de alto y la captura moría con «Failed to allocate a surface due to
invalid size». El layout ocurría igual, pero sin captura no se ejercita el
pintado, que es justo la mitad que se venía a entrenar.
2026-09-07 20:24:52 +00:00
SergioandClaude Opus 5 7d0dc72de0 test-atuq-ruteo: los puertos fijos daban un fallo del producto que no existía
Esta prueba dio «NO concluyente — la pestaña sin contenedor no salió directa»
dos veces seguidas, y la causa no tenía nada que ver con atuq: **otro frente de
esta misma máquina tenía levantado un `python3 -m http.server 8099`** desde hacía
dos horas. La oreja del destino no podía atarse, no llegaba nada, y el informe
publicaba un fallo inexistente. En un repo que comparten varios agentes, un
puerto fijo es estado compartido sin dueño.

Y había un segundo bug que es el que lo hizo dañino: el `bind` vivía DENTRO del
hilo de la oreja, donde `fatal()` no puede matar el proceso — `SystemExit` en un
hilo secundario sólo termina ese hilo. Así que la prueba imprimía «el puerto está
ocupado, la medición no valdría nada» y **seguía adelante hasta publicar un
veredicto**. Un guardián que avisa de que no puede medir y mide igual es peor que
uno que no mide.

Ahora las dos orejas se atan en el hilo principal, con el puerto 0: lo elige el
kernel. Si no hay puertos, no hay prueba.

Con eso, y contra el atuq de hoy (d36ae188, el del arreglo del LD_LIBRARY_PATH):

    positivo  destino 40843 · proxy 38503 → GET /directo al destino, SOCKS5 al proxy
    control   destino 40917 · proxy 37113 → las dos directas, cero al proxy

O sea que el ruteo por contenedor del §6.8 sigue en pie y no lo rompió nada de
hoy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 20:12:20 +00:00
SergioandClaude Opus 5 ec45c3fa97 dunst-headless: --via-atuq — la cadena entera, empezando en una página web
El modo por defecto prueba la cadena del SISTEMA (`notify-send` → bus → dunst).
Éste prueba la que motivó todo el hilo del §6.10: una página llama a
`new Notification(...)`, `libxul` hace `dlopen("libnotify.so.4")` —que no es
NEEDED de ningún ELF, o sea invisible para cualquier auditor—, eso habla D-Bus,
el bus ACTIVA dunst y dunst dibuja. Cinco piezas, y la única forma de saber que
están las cinco es verlo.

El veredicto acá no puede ser el diff de píxeles solo: el navegador ocupa la
pantalla y repinta por su cuenta. Lo decisivo es el `onshow` del objeto
Notification, que es el motor diciendo que el sistema ACEPTÓ la notificación; el
diff queda como corroboración y la captura «antes» se toma con atuq ya pintado.

    --via-atuq                     MOSTRADA #33 · 151.174 píxeles
    --via-atuq --negative-control  «ERROR al mostrar» · 0 píxeles

El control negativo también se lee distinto según el modo, y no por comodidad:
sin el .service, lo que tiene que faltar en el modo navegador es el onshow.

Captura en docs/evidencia/atuq-notificacion-web-sway-2026-09-07.png: dos globos
de dunst sobre la ventana de atuq, en sway headless, sin que nadie haya lanzado
el daemon a mano.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 18:37:24 +00:00
SergioandClaude Opus 5 4f9ee5430f dunst: sway ya atiende las notificaciones, y hay un píxel que lo prueba
Era la única mitad que quedaba abierta del mapa del §6.10: KDE atiende
`org.freedesktop.Notifications` con plasma-workspace, GNOME con gnome-shell y
COSMIC con cosmic-notifications; sway no tenía a NADIE, así que una página que
pedía notificar mandaba el mensaje al bus y se lo comía el silencio.

`dunst` 1.12.2 y no `mako` —que es el de la casa wlroots— por dos razones
medidas: el nombre `mako` YA ESTÁ OCUPADO en el corpus por el motor de plantillas
de mesa, y el cierre del perfil incluye las herramientas de build, así que la
imagen terminaría con dos artefactos homónimos peleando por las mismas rutas; y
mako habla D-Bus por sd-bus, que en musl es una receta nueva (basu), mientras
dunst habla por GDBus, que ya está sellado. Cero dependencias nuevas.

DOS GUARDIANES EN LA PROPIA RECETA, porque las dos cosas que importan no se ven
en que compile:

- `X11=0` es una intención; el hecho se lee en el ELF. dunst compila los dos
  backends por defecto y su config.mk avisa: sin wayland «forzará xwayland». En
  esta distro eso sella un binario que arranca, toma el bus y NO PINTA NUNCA. La
  fase exige `libwayland-client` entre los NEEDED y prohíbe `libX11`.
- El `.service` de D-Bus es la mitad que importa: sin systemd, al daemon lo
  levanta EL BUS. Se comprueba que reclame `org.freedesktop.Notifications` y se
  le saca la línea `SystemdService=`, que acá no puede significar nada.

Y `DUNSTIFY=0`: entró primero en 1 y el binario **segfaultea hasta en
`--help`** (rc=139 en las tres pruebas). La causa es el cuadro que este repo ya
tiene escrito: nuestro `libnotify.so.4` es compartido y arrastra la cadena glib
`.so`, mientras esta cola enlaza la GLib estática ⇒ dos copias de GObject en un
proceso. No se pierde nada: el emisor de la casa es `notify-send`, que viene
DENTRO del artefacto de libnotify y ya está en las cuatro imágenes. Hay un
guardián que frena el día que alguien vuelva a poner DUNSTIFY=1 sin leer.

LA PRUEBA, en `scripts/wlr/dunst-headless.sh`: sway headless + bus de sesión +
`notify-send`, y NADIE lanza dunst a mano — lo activa el bus por el .service,
que es el mismo camino de una página web en atuq. La evidencia es un diff de
píxeles antes/después acotado al cuadrante donde dunst dibuja, porque un log en
verde es compatible con una pantalla vacía y el color no distingue (swaynag pinta
su barra de error en el mismo rojo).

    positivo  14832 píxeles cambiados · servidor: dunst knopwob 1.12.2 1.2
    control   0 píxeles · ServiceUnknown · notify-send rc=1

El control negativo borra el .service dentro del overlay temporal y EXIGE que no
se dibuje nada: probado en los dos sentidos.

Captura en docs/evidencia/dunst-sway-notificacion-2026-09-07.png.
Vigía de sonames: escritorio-sway 203 → 204 nodos, 512 sonames, 0 sin proveedor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 18:15:29 +00:00
SergioandClaude Opus 5 f3aaab75c6 test-atuq-rootfs: el triaje decía «sin receta de ffmpeg» y era falso
La familia `libavcodec.so.*` estaba clasificada como hueco con el motivo «sin
receta de ffmpeg en el corpus». `recipes/ffmpeg.toml` existe, está sellada,
publica `libavcodec.so.61` —uno de los once sonames que sondea libxul— y ya
viajaba en la clausura de los cuatro escritorios arrastrada por `mpv`. Pasa a
ruido: lo que falta son las OTRAS versiones del soname, y Firefox recorre la
lista hasta que una carga.

`libva` igual: la receta está y ahora también en el rootfs del runner. Sigue
siendo hueco, pero por la otra mitad —las tres mesa van con `-Dgallium-va=disabled`
y `-Dvideo-codecs=` vacío, así que no hay un solo `*_drv_video.so` que cargar—,
y el motivo ahora lo dice.

Con eso el mapa pasa de 47 cadenas sin proveedor a 7 huecos reales.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 15:39:36 +00:00
SergioandClaude Opus 5 a44fc4f06a atuq: AV1 no reproducía, y la causa era UN elemento de LD_LIBRARY_PATH
Gecko SE COME EL PRIMER ELEMENTO de `LD_LIBRARY_PATH` al lanzar el proceso RDD
—el que decodifica vídeo—. El lanzador ponía `/usr/lib/atuq` una sola vez, o sea
primero, así que el RDD arrancaba sin él, no encontraba `libmozavcodec.so` /
`libmozavutil.so` (el ffvpx bundleado, donde vive dav1d) y anotaba:

    PlatformDecoderModule  FFVPX: Link result: NoProvidedLib

El navegador NO falla ahí: se cae al ffmpeg del sistema, que cubre
H.264/AAC/VP8/VP9/MP3/FLAC/Opus. Pero AV1 por software NO lo cubre —el
decodificador `av1` de ffmpeg es sólo hwaccel y nuestra receta va sin dav1d—, así
que un vídeo AV1 se quedaba en `readyState=1` para siempre, sin un error, sin un
NEEDED faltante y sin una cadena ausente. Media función apagada en silencio, que
es la forma de fallo de esta casa.

Tres corridas que sólo cambian esa variable, con el mismo artefacto:

    LD=/usr/lib/atuq:/usr/lib:/lib            → RDD NoProvidedLib   AV1 ✗
    LD=/usr/lib/atuq:/usr/lib/atuq:/usr/lib   → RDD Success         AV1 ✓
    LD=/relleno:/usr/lib/atuq:/usr/lib        → RDD Success         AV1 ✓

El arreglo es repetir el appdir. Feo y correcto mientras no haya `patchelf` en el
corpus para grabar `RUNPATH=$ORIGIN`, que borraría la variable entera.

Y el guardián que sale de este punto ciego, `scripts/test-atuq-codecs.sh`: abre
cinco muestras versionadas en `scripts/fixtures/codecs/` y mira si `currentTime`
AVANZA — «se creó el decodificador» no es «decodifica». El veredicto sale por
`dump()` al stdout, así que no necesita ni red ni servidor, y corre headless para
que sirva en el worker. Con `--negative-control` se saltea el lanzador y EXIGE
que AV1 falle: probado en los dos sentidos, 5/5 y control ✓.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 15:38:02 +00:00
SergioandClaude Opus 5 b6d000eca3 atuq-nested: el runner medía un rootfs MÁS POBRE que la imagen, y su caché no cacheaba
Tres cosas en el mismo fichero, las tres medidas hoy:

1. `ffmpeg` y `libva` entran a las raíces. NO son recetas nuevas: las dos están
   selladas en el corpus y ya viven en la clausura de los cuatro escritorios,
   arrastradas por `mpv` (verificado con `yupana.membresia`, no leyendo el TOML).
   Faltaban acá, o sea que el runner abría un rootfs sin libavcodec y desde ahí
   se concluía que la imagen no tiene códecs. El instrumento otra vez, no el
   artefacto.

2. El bucle de hidratación salía 1 SIEMPRE: `$VIGENTES` termina en `\n`, `echo`
   agrega otro, la última vuelta lee la línea vacía, `[ -n "$d" ]` da 1 y con
   `set -e` el script moría sin imprimir una sola línea, justo después de
   hidratar bien las 41 raíces. Ahora `continue` en la vacía y un `hydrate` que
   falla grita.

3. La comparación del sello NUNCA daba igual: el lado izquierdo lleva su `\n`
   final y `$(cat …)` lo recorta ⇒ «DESACTUALIZADO» en cada corrida y 3013
   ficheros rehidratados de más. Los dos lados pasan ahora por la misma
   sustitución de comandos.

Y `ROOTFS_ONLY=1`, que corta después de hidratar: lo necesita el guardián de
códecs, que corre headless y tiene que usar ESTA lista de raíces y no una copia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 15:37:46 +00:00
SergioandClaude Opus 5 6886b1541c libnotify: el primer hueco que el mapa del §6.10 encontró Y cerró
`libxul.so` lleva la cadena `libnotify.so.4` adentro y la abre por dlopen cuando
una página pide permiso para notificar. Nadie en el corpus la proveía, así que la
función quedaba apagada SIN UN SOLO MENSAJE: el navegador arranca, la web pide
notificaciones, y no pasa nada. No lo veía ninguna herramienta porque no hay
NEEDED en ningún ELF — es el tercer escalón, y sobrevivió a vigia-sonames con los
cinco perfiles en CERO.

Receta nueva, 0.8.8, SÓLO en variante compartida y eso no es un olvido: a un
`dlopen("libnotify.so.4")` una `.a` no le sirve de nada, así que una receta
estática sería un artefacto que nadie puede consumir.

Dos cosas que la receta se comió y quedan escritas:

1. `libpng-shared` hace falta porque las deps de hammer NO son transitivas: el
   `.pc` de gdk-pixbuf-2.0 declara `Requires: libpng` y el meson muere con un
   mensaje que nombra a gdk-pixbuf —que sí está— en vez de a lo que falta.
2. El guardián informaba «18 bytes» y parecía una librería vacía: `stat -c%s` no
   sigue el symlink, y `libnotify.so.4` apunta a `libnotify.so.4.0.0`. El
   artefacto estaba bien y el MENSAJE mentía. Con `-L` son 162.928 bytes. Se
   arregla el mensaje porque es lo que alguien va a leer a las tres de la mañana,
   y de paso el guardián exige el fichero real, no sólo el nombre.

⚠ Y lo que NO arregla, escrito en la receta para que nadie lea de más: libnotify
no trae daemon, manda org.freedesktop.Notifications por D-Bus. Tenerla resuelve
la mitad —que firefox la encuentre—; la otra mitad es que en la imagen haya
alguien escuchando ese nombre.

Verificado con el propio mapa: `--dlopen` pasa de 25 huecos a 24 y libnotify.so.4
desaparece; el soname viejo `.so.1` se reclasifica de «hueco» a «ruido», que es lo
que ahora es. El guardián normal sigue en CERO con un soname más pedido (66).

Queda pendiente la membresía de perfil en `docs/state/targets.toml`, que es
catálogo compartido y no lo toco sin decidirlo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-07 00:51:56 +00:00
SergioandClaude Opus 5 7a81b6480c atuq §6.10: el tercer escalón — qué NO puede hacer el navegador, y por qué
hammer-9f nombró el punto ciego que ni su vigía ni mi guardián podían cubrir:
una librería que sólo aparece como CADENA LITERAL dentro de un dlopen(). No hay
NEEDED en ningún ELF, así que ningún auditor de readelf la encuentra. La
jerarquía queda:

    NEEDED del ejecutable          -> lo vemos los dos
    NEEDED de un .so dlopeado      -> lo vemos los dos
    dlopen("libfoo.so.1") literal  -> NO LO VE NINGUNO

Y un navegador vive de eso. Firefox sondea ffmpeg, VA-API, vulkan y libnotify
por nombre, y cuando no están NO FALLA: apaga la función y sigue. No hay línea
roja; hay una función que nadie ofrece y nadie reclama. Es la forma que ya costó
caro con OBS y su dlopen("libGL.so.1").

`--dlopen` busca esas cadenas y las cruza contra el rootfs. Es un HEURÍSTICO y se
declara como tal —una cadena no prueba un dlopen y su ausencia no prueba que no
lo haya—, así que no falla nunca: imprime un mapa triado. Lo afirmable es lo
contrario, que es lo útil: si la cadena está y el fichero no, esa función no
existe en esta imagen.

De 85 cadenas, 47 sin proveedor. Siete son huecos de verdad:

    códecs del sistema (H.264/AAC)  sin receta de ffmpeg — el más caro
    notificaciones web              sin receta
    llavero (libsecret)             RECETA YA EXISTE en incoming-gnome
    sonidos (libcanberra)           receta en incoming-kde
    WebGPU (vulkan-loader)          receta en incoming-kde
    vídeo por hardware (VA-API)     sin receta
    lectura en voz alta             sin receta

Tres de los siete son promoción, no autoría. Ocho son decisiones ya tomadas
(libGL por Wayland-only sin GLX; libcurl porque sólo lo usa el pingsender de
telemetría) y siete son ruido de musl o sonames viejos. El triaje va en una tabla
del propio script, no escondido en un `if`, porque es criterio y no medición: ahí
se puede discutir.

Sin triar: 0. Si aparece una cadena nueva, el informe la marca «SIN TRIAR» en vez
de tragársela.

Lo que esto cambia de fondo: hasta hoy la pregunta era «¿arranca?» y la respuesta
era sí. La que faltaba era «¿y qué NO puede hacer?», que ninguna métrica del repo
respondía porque todas miran presencia y ésta mira ausencia declarada por el
propio binario.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-07 00:43:29 +00:00
SergioandClaude Opus 5 a20d1b7453 atuq: un guardián para el rootfs del runner — y encontró SIETE sonames más cayendo al lab
La lista de raíces de `atuq-nested.sh` se mantiene A MANO y la jaula monta
`.dev-fs/alpine` como capa de abajo. Esas dos cosas juntas hacen que una raíz que
falte NO se note: el navegador arranca igual, resolviendo contra el lab. Ayer eso
costó `gcc-libs`. La regla del repo es que cada punto ciego se convierte en un
guardián, así que acá está el guardián en vez del parche.

`scripts/test-atuq-rootfs.py` mira el objeto que la jaula monta de verdad —el
directorio HIDRATADO— y no el grafo de recetas, que es lo que ya cubre
`vigia-sonames.py`. Son preguntas distintas: la lista del runner no sale del
grafo, así que el grafo no puede auditarla.

Lo primero que hizo fue encontrar SIETE sonames más que se estaban resolviendo
contra el lab, y no son cosmética:

    libexpat.so.1, libzstd.so.1   <- los pide mesa (iris_dri, libEGL, libgbm)
    libdbus-1.so.3                <- lo pide pipewire; lo trae `dbus-shared`, no `dbus`
    libbz2.so.1                   <- freetype
    libudev.so.1                  <- libspa-alsa
    libsndfile.so.1, libncursesw.so.6

Los siete tienen proveedor en el corpus. Agregados a las raíces: el rootfs pasa
de 7 huecos a CERO, y la única excepción que queda es `libc.so`, que va en una
lista explícita porque ningún artefacto lo provee — las imágenes lo copian del
devfs. Si algún día hay receta que lo provea, esa lista se achica y el guardián
se vuelve más estricto solo.

CONTROL NEGATIVO incluido, que sin él esto no probaría nada:
`--negative-control` esconde libstdc++.so.6 y exige que el guardián lo cace.
Corrido: lo caza, y nombra a quién lo pide (atuq, atuq-bin).

Y `scripts/test-atuq-ruteo.py` vuelve a pasar entero contra el rootfs completo,
o sea que el ruteo por contenedor está probado ahora sobre un rootfs que no le
pide nada al lab salvo el intérprete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-07 00:38:16 +00:00
SergioandClaude Opus 5 7389d44569 atuq-nested: faltaba gcc-libs, y su ausencia hacía MENTIR a la prueba entera
`atuq` declara `NEEDED libstdc++.so.6` y `libgcc_s.so.1` —lo hereda de firefox,
que va con clang++ y la libstdc++ COMPARTIDA— y su clausura ya estaba bien:
`recipes/atuq.toml` declara `runtime = ["gcc-libs"]` desde 5b9777bd, yupana lee
`deps.runtime` desde 02facebe, y el vigía de sonames da CERO en los cinco
perfiles (rehecho hoy: 41/306/190/161/201 nodos, 0 sin proveedor).

Lo que estaba mal era el INSTRUMENTO. `atuq-nested.sh` no hidrataba `gcc-libs`, y
como la jaula monta `.dev-fs/alpine` como capa de abajo, cada «atuq corre» de
estos días resolvió libstdc++ contra el LAB. Corría, sí: en una máquina con
Alpine debajo, que es exactamente lo que la distro no es. El artefacto estaba
sano y la prueba era ciega — que es peor, porque una prueba ciega dice que sí.

Los dos ficheros ni se parecen, así que la duda se resuelve mirando:

    corpus  20.850.968 bytes
    lab      2.804.104 bytes

Con `gcc-libs` en las raíces, dentro de la jaula se ve el de 20 MB, y
`scripts/test-atuq-ruteo.py` vuelve a pasar entero — o sea que el ruteo por
contenedor está probado ahora contra la libstdc++ del corpus y no contra la del
lab. Queda una dependencia del lab que NO es de esta prueba y está por diseño: el
intérprete `/lib/ld-musl-x86_64.so.1`, que ningún artefacto provee y que las
imágenes copian del devfs (`scripts/mirada-usb.sh`).

De paso, un puntero muerto: las recetas dicen «lo vigila scripts/audit-needed.sh»
y ese script lo BORRÓ 02facebe al reemplazarlo por el vigía de sonames. Corregido
en atuq.toml. Quedan otras siete recetas apuntándole (firefox, waterfox,
gcc-libs, mesa-llvmpipe, librsvg, spidermonkey, firefox-instrumentado) y no se
tocan desde acá porque son de otro frente.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-07 00:28:52 +00:00
SergioandClaude Opus 5 2e093f9cab atuq §6.8: probado que el paquete sale por el proxy del contenedor — y con control negativo
Era lo único que la v0.5 dejó sin probar, y las tres puertas que había
encontrado siguen cerradas: no hay flag de línea de comandos para el
userContextId, Marionette no toma `-remote-allow-system-access` en la jaula, y
`tabs.create({cookieStoreId})` exige el permiso `cookies` — que no se le da a la
extensión del proxy sólo para que pueda probarse a sí misma.

La cuarta puerta estaba abierta: el fichero de SESIÓN guarda el userContextId de
cada pestaña y Gecko lo restaura. Se fabrica a mano — `mozLz40\0` + tamaño + un
bloque LZ4, y un bloque de sólo literales es LZ4 válido: veinte líneas, sin
librería.

Y la medición no le pregunta nada al navegador: le pone dos oídos en la red y
mira a cuál llama. Dos pestañas piden la MISMA url, una sin contenedor y otra en
«Banco»:

    al destino (8099)  GET /directo   <- la de sin contenedor, directa
    al proxy   (9099)  \x05\x01\x00   <- saludo SOCKS5 de la de «Banco»

Ese saludo sólo aparece si Gecko decidió hablar con un proxy para esa petición, y
el único que se lo pudo indicar es proxy.onRequest mirando el cookieStoreId. La
pestaña sin contenedor no es decorativa: sin ella, «todo fue por el proxy» y «el
ruteo anda» se verían iguales.

CONTROL NEGATIVO, que es lo que este repo se exige desde hoy: con
`--negative-control` no se configura el proxy y se exige lo contrario — las dos
pestañas directas y nadie llamando al proxy. Corrido: pasa. La única diferencia
entre las dos corridas es una línea de configuración y el observable se da vuelta
entero. Sin ese modo, una prueba que se hubiera vuelto ciega se vería idéntica a
una que funciona.

Tres detalles sin los cuales la prueba mide un silencio y se lee como fallo:
restore_on_demand=false, allow_hijacking_localhost=true, y el
triggeringPrincipal_base64 en cada entrada de sesión.

El artefacto se resuelve por `hammer hash` y no por glob, que es la lección de
hammer-03 de esta madrugada: con dos artefactos de la misma receta en el store,
`ls | head -1` es una ruleta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-06 23:47:08 +00:00
Sergio 44b6032522 vigia-sonames: dos falsos positivos menos — los cinco perfiles en CERO
NO hacía falta una receta de perl. `libperl.so` está en el artefacto, son 9,6 MB
en usr/lib/perl5/5.40.2/x86_64-linux/CORE/, y el binario la encuentra por su
RUNPATH, que apunta justo ahí. El vigía no la veía porque perl la construye con
-Duseshrplib y NO le pone SONAME: el código sólo registraba el nombre del fichero
`if son:`. Un ELF compartido sin SONAME se resuelve POR NOMBRE DE FICHERO, y eso
es exactamente lo que había que indexar.

El otro falso positivo era el último «hueco» del informe:
usr/lib/go/src/debug/elf/testdata/libtiffxx.so_ — un ELF de MUESTRA que Go
shipea para los tests de su propio paquete debug/elf, pidiendo libc.so.6 (glibc)
en una distro musl. Un binario que nadie ejecuta. `testdata/` no es cierre.

Importa arreglar un falso positivo aunque «sólo» sea ruido: un vigía que grita
en falso enseña a ignorarlo, y así es como libstdc++.so.6 —que SÍ rompía el
navegador en los cuatro escritorios— estuvo en esta misma salida sin que nadie
la mirara.

    escritorio-mirada    41 nodos ·  231 sonames ·  0 sin proveedor
    escritorio-kde      306 nodos · 2083 sonames ·  0 sin proveedor
    escritorio-gnome    190 nodos ·  657 sonames ·  0 sin proveedor
    escritorio-cosmic   161 nodos ·  498 sonames ·  0 sin proveedor
    escritorio-sway     201 nodos ·  509 sonames ·  0 sin proveedor

PROBADO CON UNA ROTURA A PROPÓSITO, porque un vigía todo-verde se ve igual que
uno roto. Quitando `runtime = ["gcc-libs"]` de firefox SOLO no pasa nada —atuq
lo declara también y es raíz de los mismos perfiles, así que la librería entra
igual; el primer control estaba mal montado—. Quitándolo de las DOS, kde y
cosmic vuelven a reportar libstdc++.so.6 y libgcc_s.so.1 exactamente. Restaurado
después.
2026-09-06 23:00:59 +00:00
Sergio 02facebe28 deps.runtime: que ALGUIEN las lea — de 29 huecos de soname a 6
Tres arreglos que son el mismo: un campo que nadie lee es un campo que miente.

1. yupana._deps() leía SÓLO `deps.build`. Como yupana es la base de todas las
   herramientas de grafo, una dep de EJECUCIÓN no existía para ninguna: ni el
   vigía de sonames, ni la membresía de perfiles, ni el rootfs hidratado.
   Medido: firefox declaraba `runtime = ["gcc-libs"]` y el cierre de las cuatro
   imágenes seguía sin libstdc++.so.6, así que el navegador no arrancaba y el
   vigía lo seguía reportando como hueco DESPUÉS de haberlo arreglado.
   `deps.runtime` está en el esquema de hammer desde siempre. La unión es la
   definición de cierre: para CORRER hacen falta las dos.

2. build-state.py, lo mismo y por lo mismo.

3. El vigía entra en el LATIDO y deja docs/state/sonames.txt. Existía desde
   antes y contesta la pregunta que el grafo no contesta —no «¿está sellado?»
   sino «¿arranca?»— pero NADIE LO CORRÍA: no estaba en cosecha-cron y no dejaba
   fichero de estado. Por eso libstdc++.so.6, que rompía el navegador en los
   CUATRO perfiles, estuvo en su salida sin que nadie lo leyera, y se
   redescubrió arrancando atuq a mano. Un vigía que hay que acordarse de invocar
   no se distingue de no tenerlo.

Y se BORRA scripts/audit-needed.sh, que escribí ayer sin ver que vigia-sonames.py
ya hacía exactamente esto, con la misma frase en la cabecera. Dos herramientas
que miden lo mismo divergen y la que nadie mira es la que miente; la que se
queda es la que ya existía, que además reporta POR PERFIL y encontró más cosas.

python3 declara sus cuatro deps de ejecución (readline/sqlite/lzma/bz2): son
módulos de la stdlib que se cargan por dlopen, así que no rompen el arranque
sino un `import` — un fallo que aparece lejos y no menciona a python.

Resultado, con todo aplicado: 29 huecos -> 6, y los que quedan son otra clase.
`libperl.so` es empaquetado de la receta perl; `libc.so.6` lo pide el `go`
prebuilt y es un soname de GLIBC en una distro musl, que es un síntoma distinto.
Los dos quedan anotados en docs/state/sonames.txt, que ahora se regenera solo.
2026-09-06 22:28:17 +00:00
Sergio 5b9777bdbd declarar gcc-libs como dep de EJECUCIÓN en las seis — y el navegador arranca
Las seis recetas que el audit daba por colgantes declaran ahora
`runtime = ["gcc-libs"]`. `deps.runtime` NO entra en el ArtifactHash —medido:
firefox sigue en b3:8116bdec, atuq en b3:f2960991, waterfox en b3:88b5a762—
así que esto NO reconstruye nada. Lo que cambia es la CLAUSURA, que es lo que se
hidrata en la imagen.

EVIDENCIA DE QUE ARREGLA EL FALLO REAL, no de que compila: `atuq` bajo sway
headless, sin GPU, renderizando about:buildconfig con sus tablas y la barra de
contenedores de la v0.5. Antes moría con decenas de «Error relocating:
_ZNKSt5ctypeIcE13_M_widen_initEv: symbol not found».
docs/evidencia/atuq-arranca-con-gcc-libs-2026-09-06.png

Y el audit se corrige, porque SUB-REPORTABA en dos formas:

  · Miraba sólo ejecutables y `head -12` por artefacto. El siguiente NEEDED que
    faltaba estaba en una LIBRERÍA (libmozsandbox.so pedía libnspr4.so), así que
    daba «6 recetas» cuando el artefacto de firefox declara 30 sonames. Ahora
    recorre TODOS los ELF.
  · No contaba lo que el propio artefacto TRAE. Firefox bundlea su nspr/nss y
    las encuentra por el LD_LIBRARY_PATH que fija su lanzador; contarlas como
    colgantes era un falso positivo.

Con las dos correcciones y gcc-libs en el store, el corpus baja de 25 artefactos
colgantes a TRES, y son otra cosa:

    sqlite-shared   libreadline.so.8
    python3         libreadline.so.8
    naabu           libdl.so.2 libpthread.so.0 libc.so.6

Los dos primeros piden una receta que falta (readline). El tercero pide sonames
de GLIBC, que en una distro musl es un síntoma distinto y merece su propia
mirada. Quedan anotados, no arreglados.
2026-09-06 20:09:10 +00:00
Sergio 2be672415c audit-needed: cazar deps de ejecución colgantes — firefox no arranca en la distro
Encontrado corriendo el navegador, no leyendo. Lancé `atuq` bajo el sway
headless y murió con decenas de «Error relocating: _ZNKSt5ctypeIcE...: symbol
not found». El binario SELLADO de firefox declara:

    NEEDED libstdc++.so.6      NEEDED libgcc_s.so.1      NEEDED libc.so

y las dos primeras NO EXISTEN en el store ni en ningún cierre hidratado: sólo en
.dev-fs/alpine, o sea en el LAB. Es exactamente la lección que la memoria
`quitar-dep-no-apaga-funcion` describe: el proyecto la encuentra en el sysroot
del lab y el artefacto sella con un NEEDED que nadie provee.

LO QUE LO HACE CARO ES QUE NINGUNA MÉTRICA LO VE. build-state.json dice
`sealed`; verificar-repro.sh dice que REPRODUCE bit a bit; los tres guardianes de
la receta pasan (RLBox en el binario, MOZ_REQUIRE_SIGNING, BuildID determinista)
— y el binario no arranca fuera del lab. El verde responde «¿construye?», nunca
«¿corre?».

El audit recorre el store, indexa qué sonames PROVEE el corpus y resta los NEEDED
de cada ELF. No mira el lab a propósito: mirarlo diría que todo está bien, que es
justo la ilusión que causó el problema.

Resultado de hoy: 25 artefactos colgantes, 6 recetas distintas —atuq, firefox,
waterfox, librsvg, spidermonkey, mesa-llvmpipe— y sólo DOS sonames faltan en todo
el corpus. O sea que falta UNA receta que provea las libs de runtime de gcc.

Gravedad: firefox y atuq son raíces de los CUATRO perfiles de escritorio
(cosmic, gnome, kde, sway), así que hoy las cuatro imágenes llevan un navegador
que no puede arrancar. La receta de firefox razona sobre esto en su comentario
—«clang++ de Alpine usa la libstdc++ COMPARTIDA ⇒ el NEEDED existe»— pero cierra
el requisito de BUILD y deja abierto el de EJECUCIÓN. hammer tiene
`[deps] runtime` y ninguna de las seis lo declara.
2026-09-06 19:23:02 +00:00
Sergio dc1391e611 siembra: anclar la exclusión de PNG — sin anclar rompe las recetas que traen PNG
La siembra excluía `*.png` sin anclar, o sea PNG en CUALQUIER sitio, incluido
recipes/. Dos consecuencias y las dos malas:

  a) rsync PROTEGE DE BORRADO lo que excluye. Un directorio cuyo único resto es
     un .png no se puede vaciar y por lo tanto no se puede borrar nunca, aunque
     el hub lo haya eliminado. Medido: recipes/atuq/extension/atuq128.png
     sobrevivió a un rename hecho en el hub, y el log del latido venía repitiendo
     `cannot delete non-empty directory: recipes/atuq/extension` sin que nadie lo
     leyera como un error.
  b) Un PNG NUEVO de una receta tampoco viaja. Y hay recetas cuyo `[source] dir`
     lleva PNG dentro: los 9 iconos de recipes/atuq/branding/icons/.

El efecto medido: `atuq` daba b3:f2960991 en el hub y b3:b33e81a5 en el worker,
por ESE único fichero de más. El worker no podía coincidir con el hub ni
construyendo bien.

Lo bueno es que no era silencioso, y por diseño: hammer SÍ hashea el árbol de un
`[source] dir` (recipe.rs, `dir:<hash>`), así que la divergencia sale como otro
ArtifactHash en vez de como dos bytes distintos en la misma dirección. Es
exactamente la propiedad que le falta al lab y a la versión de hammer.

Lo que se quería ahorrar eran las capturas: docs/evidencia/ (4,4 M) y el PNG
suelto de la raíz (1 M). Eso ahora se excluye POR RUTA. Los PNG de recipes/
viajan, que es lo correcto: ahí son fuente, no adorno.

Borrado el fichero rancio del worker; hub y worker vuelven a coincidir en
b3:f2960991.
2026-09-06 04:29:43 +00:00
Sergio b6a35bc2bc worker-loop: que el watchdog no se lleve el post-mortem
El watchdog barre work/sources/* cada 180 s con un suelo de 2 minutos, así que
el árbol de una receta que ACABA DE FALLAR se destruye antes de que nadie lo
mire. Medido hoy: waterfox murió a las 03:20:15 tras 36 minutos y 23.980 pasos,
enlazando libxul.so con un `collect2: error: ld returned 1 exit status` SIN
mensaje del linker delante — o sea justo el caso en que hace falta el objdir. A
las 03:22 work/sources/ estaba vacío. Reproducirlo cuesta otros 36 minutos.

Lo que más molesta es que la doctrina ya estaba escrita en el repo, en dos
sitios, y este watchdog la contradecía sin que nadie lo notara:

  campana-deuda.sh:138  «NO se poda tras un ✗: el árbol de una receta que falló
                         es el post-mortem»
  poda-fuentes.sh       suelo de 24 h, «deja el post-mortem del día»

No se arregla subiendo el suelo a secas: este watchdog existe para que una tanda
Go no llene el disco vendoreando, y ahí los minutos importan de verdad (80 G en
una tanda, con el I/O-wait disparando el load). Se arregla siendo agresivo SÓLO
cuando el recurso escasea: con el disco por debajo de DISK_HIGH el suelo pasa a
SUELO_FRIO_MIN (120 min por defecto, configurable); en cuanto el disco aprieta
vuelve a los 2 minutos de siempre. Un árbol frío con disco al 37% no le hace
daño a nadie.

Probado en los dos sentidos: árbol de 5 min -> se borra con suelo 2, protegido
con suelo 120; árbol de 3 h -> se borra con los dos (ya no es post-mortem).

Ojo al leerlo: `df --output=pcent` da el porcentaje USADO, no el libre. La
primera versión de este parche llamaba `libre` a esa variable.
2026-09-06 03:30:25 +00:00
Sergio 7638e4e2be worker-loop: recompilar hammer POR CICLO, no sólo al arrancar
El bloque de rebuild que ya existía corre una sola vez, al arrancar el loop, y
este loop vive días — hoy el worker llevaba 8 de uptime. El source SÍ sigue
llegando fresco cada 30 min (la siembra rsync-ea el repo), así que el worker
acaba con FUENTE NUEVO y BINARIO VIEJO.

Eso es peor que el fósil de la golden que motivó el bloque original, porque nada
lo delata: la versión de hammer NO entra en el ArtifactHash, así que el build se
comporta distinto y build-state.json sigue en verde. Misma familia que el lab
fuera de hash_inputs, sin siquiera el aviso del lock.

Caso real que lo motiva, de hoy: el worker corría un hammer de las 04:54 y el
arreglo que materializa los submódulos git había entrado a las 15:11 (114aeba).
`waterfox` moría en CUATRO SEGUNDOS por waterfox/browser/locales/moz.build
inexistente, y el diagnóstico apuntaba a la receta, a los once parches de musl y
al --filter=blob:none: a todo menos al binario. Lo resolvió `strings` sobre los
dos binarios en un minuto — hub 2 cadenas de .gitmodules/gitlink, worker 0.
Recompilado el worker (23 s) y borrado el árbol de fuentes viejo, waterfox pasa
el punto donde moría y el submódulo se materializa.

Se comprueba por mtime y no se recompila a ciegas: cargo cacheado son ~24 s,
pero correrlo cada IDLE_SLEEP sin motivo es ruido en el log y CPU que le estamos
quitando a un build. Si nada cambió cuesta un `find`. Probado en los dos
sentidos (fuente más nuevo -> detecta; binario más nuevo -> no hace nada).
2026-09-06 02:46:28 +00:00
Sergio 4fae3ee1ff prueba que los guardianes de la cadena wasm sirven — con CONTROL
Un guardián que nunca falló no se sabe si sirve, y uno que falla SIEMPRE se ve
idéntico a uno que funciona. Por eso el control con el sysroot intacto —que
tiene que PASAR— no es un extra: es la mitad de la prueba. El método lo aportó
la sesión hammer-f8 (scripts/test-atuq-politica.py hace lo mismo con el cruce
política<->XPI).

Le pone delante a la cadena las cuatro formas conocidas de romperla —sin la
sonda include/c++/v1, sin cstring, sin libc++.a, sin libc.a— y exige que las
cuatro maten, usando la invocación EXACTA del configure de firefox dentro del
lab. No construye nada, no toma work/.farm-build.lock y no escribe en el store:
trabaja sobre copias temporales de artefactos ya sellados, así que corre con la
granja a pleno.

Y el control se ganó el sueldo en su primera corrida: salió en ROJO. La causa no
era un guardián flojo sino este script, que resolvía el artefacto con
 y agarraba el VIEJO —el de antes de que la
receta creara el directorio-sonda—. Misma trampa que el cache-hit que congela
regresiones: un artefacto anterior se hace pasar por el actual y la prueba mide
otra cosa. Sin el control se habría leído como «4 en verde, guardianes ok».

Ahora el artefacto se resuelve por  de la receta, nunca por glob.
5 en verde, 0 en rojo.
2026-09-06 02:02:34 +00:00
SergioandClaude Opus 5 c5ebbda933 atuq v0.5: proxy por contenedor — el primer diferenciador del §6 que se paga entero
Es el único de la lista del SDD 26 §6 que no pasa por el host de native
messaging del §7: es API de Firefox y nada más. Cada contenedor —Personal,
Trabajo, Banco, Compras— puede salir por su propio proxy.

Tres piezas y ninguna alcanza sola: `atuq.cfg` prende los contenedores (vienen
apagados), `policies.json` los CREA con `Containers.Default` —el único
mecanismo que los pone en un perfil NUEVO— y deja la config de fábrica en
`3rdparty.Extensions`, y `extensions/proxy/` los enruta con `proxy.onRequest`,
que es lo único que ve el `cookieStoreId` de la petición.

Las cuatro se comprobaron DENTRO del artefacto de firefox antes de escribir una
línea, no en la documentación de Mozilla: `Containers` y `3rdparty` en el
`policies-schema.json` de browser/omni.ja, `cookieStoreId` en el
`schemas/proxy.json` de omni.ja, y `storage.managed` leyendo
`getExtensionPolicy(id)` en ext-storage.js. Es la regla del §2.sexies: la
pregunta no es si Mozilla lo tiene, es si NUESTRO build lo tiene.

Tres decisiones que valen más que el código:

1. La config se indexa por NOMBRE de contenedor y no por `cookieStoreId`: el id
   depende del orden en que se crearon, así que la misma configuración aplicada
   a otro perfil apuntaría a otro contenedor.
2. FAIL CLOSED. Un contenedor con proxy configurado que no se pudo honrar no
   sale directo: va a un destino cerrado y el navegador muestra el error. Salir
   directo sería una fuga silenciosa — la misma familia que el artefacto vacío
   de la regla 3, el fallo que llega hasta el final diciendo que todo fue bien.
3. `proxyDNS` PRENDIDO por defecto: sin él la consulta DNS sale por la línea que
   se quería evitar. Es la fuga clásica de esta configuración.

Y lo que no promete está arriba de todo en la página de opciones, no en un pie:
separación de tráfico, NO anonimato; para anonimato, Tor Browser. El §4 cumplido
donde el usuario lo lee.

De paso, una verdad que estaba escrita en dos sitios pasa a tener un dueño: el
id de cada extensión sale ahora del `manifest.json` y no de una constante de
rebrand.py, el nombre del XPI se deriva de él, y el `install_url` de la política
se cruza contra la ruta donde el fichero quedó escrito de verdad. El icono se
inyecta desde branding/icons/ en vez de estar copiado byte a byte dentro de cada
extensión. Agregar una tercera extensión es ahora un directorio.

PROBADO, corriendo el árbol en la misma jaula que atuq-nested.sh:
  · captura de about:preferences#containers con los cuatro contenedores y sus
    iconos, más el containers.json del perfil;
  · extensions.json del perfil nombra las dos extensiones;
  · `console.info: "atuq/proxy: 4 contenedor(es) enrutado(s)"` — leyó la config
    de fábrica por storage.managed Y la casó con los contenedores de la política;
  · scripts/test-atuq-politica.py: cinco formas de desincronizar política y XPI,
    las cinco matan el build, y el control con la política intacta pasa.

NO probado y dicho por su nombre: que una petición hecha en «Banco» salga por el
proxy de «Banco». Pide automatizar la UI y queda pendiente.

Dos obstáculos del método, que valen para la próxima. La consola de una
extensión es CONTENIDO: `devtools.console.stdout.chrome` (que viene en true) no
la incluye, hace falta `...stdout.content`. Y un `moz-extension://` NO se abre
desde la línea de comandos —muere con `NS_NOINTERFACE [nsIFileURL.file]` y abre
la home en su lugar—, además de que `--screenshot` dispara al `load`, que puede
ocurrir antes de que arranquen las extensiones. Eso último destapó un fallo real
y arreglado: la página de opciones confundía «no hay contenedores» con «la API
no está» y mostraba un mensaje FALSO.

Verificado contra firefox b3:352d7880; se reconstruye contra el firefox con
RLBox cuando selle. La otra mitad de esta unidad —el rename de
recipes/atuq/extension/ a extensions/inicio/— entró sin querer en ff0b556, que
es de otro frente.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-06 01:57:44 +00:00
Sergio 8a2c6fad39 repro: «no pude comparar» tampoco es no-determinismo — segundo sitio, misma lección
Una tanda volvió a llenar el libro de veredictos falsos: 47 recetas sanas anotadas como
NO-DETERMINISMO. Esta vez la cadena empezó antes de donde yo había mirado.

El `mv` que APARTA el artefacto falló (el directorio de apartado no estaba) y yo nunca comprobaba que
hubiera funcionado. A partir de ahí todo lo que sigue es basura: el artefacto se queda en su sitio,
`hammer why-differs` compara contra una ruta que no existe y devuelve `Error: store: no existe…`, y
mi código leía cualquier salida no-cero como «difieren». Un error de comparación disfrazado de
veredicto, 47 veces.

Es la MISMA lección que arreglé hace un rato para el build, en un segundo sitio que no miré:
**no poder medir no es un resultado negativo**. Dos guardas nuevas:

  · el apartado se comprueba (`mv || SIN VEREDICTO`) y la receta se salta ruidosamente;
  · `why-differs` se lee, no sólo su exit code: si dice `Error:`/`no existe`, es SIN VEREDICTO, se
    restaura el artefacto y NO se anota nada.

Probadas las dos: con el apartadero sin permiso de escritura sale «no pude apartar el artefacto ⇒
SIN VEREDICTO», el artefacto queda intacto y el libro no crece.

Comprobado además que la tanda mala no perdió NADA: como el `mv` fallaba, los 47 artefactos nunca
salieron del store (`faltan: 0`). El daño fue sólo el registro, y está purgado — el libro queda con
51 verificaciones buenas.

La regla, ya por triplicado hoy: cuando un guardián no puede establecer algo, el estado es *sin
veredicto*, y eso no se escribe. Un libro que anota lo que no midió se cree igual que uno que sí.
2026-09-05 21:44:50 +00:00
Sergio a48885e143 repro: podar el árbol de fuentes tras cada veredicto sano — el barrido ya no llena el disco
Un barrido dejaba un árbol extraído por receta en `work/sources` y no limpiaba hasta el final. En el
hub eso llevó el disco del 95% al 98% DOS VECES hoy, y un disco lleno no se lee como disco lleno: se
lee como recetas rotas. Con la poda por veredicto, `work/sources` se queda en 3 MB durante toda la
tanda en vez de crecer hasta 7,7 G.

No cuesta nada porque `fetch.rs` borra y re-extrae el árbol en CADA build por diseño: guardarlo entre
recetas de un barrido no ahorra un segundo.

Dos límites deliberados:
  · Sólo se poda cuando el veredicto es SANO. Si algo falló o divergió, el árbol es justamente lo que
    hace falta para mirarlo — un verificador que limpia la escena del problema que acaba de encontrar
    sirve para poco. Es la misma razón por la que los ejemplares divergentes se conservan.
  · Sólo el árbol de ESA receta. Las deps extraídas se quedan: son compartidas, y borrarlas mientras
    otra cosa las usa es exactamente el ADR 0012.

Con esto la cobertura se puede levantar en el HUB, que es donde están los 1157 artefactos. El worker
sólo tiene los que construyó —el último barrido allá reportó 47 de 60 «sin artefacto»— así que como
máquina de cobertura no sirve por más disco que tenga.
2026-09-05 21:05:12 +00:00
Sergio 058162c6d3 licencias: la cola de pendientes también se escribe — «lo que falta y por qué» es dato
Las 33 que quedan sólo existían en la salida del script, o sea que se perdían al cerrar la terminal.
Para quien tiene que decidirlas —y son decisiones legales, no mecánicas— «qué falta y POR QUÉ no se
puede afirmar» es tan dato como los veredictos. `docs/licencias-pendientes.tsv` las deja listadas con
su motivo: cuáles traen varios ficheros de licencia que dicen cosas distintas (gmp, ffmpeg, clang18,
llvm18, los `gi-*`), cuáles tienen un LICENSE que no reconozco y con qué frase empieza (fuse3, lsof),
y cuáles no traen nada en el árbol (boost, pigz, sqlite-shared).

Al revés que `licencias-evidencia.tsv`, ésta se REGENERA entera en cada corrida. No es un registro de
lo que se probó sino una foto de lo que queda, y una pendiente resuelta tiene que DESAPARECER de acá
—si se acumulara, la lista de trabajo mentiría hacia arriba para siempre.
2026-09-05 20:18:21 +00:00
Sergio d7d9cf8fd2 repro: «la 2ª reconstrucción no construyó» NO es no-determinismo — mi propio bug, y lo pagó el libro
Corriendo una tanda con `MAX_SECONDS=45` se llenó el disco a mitad, los rebuilds empezaron a morir
por ENOSPC, y la rama que yo mismo había escrito para distinguir DERIVA de NO-DETERMINISMO metió
«falló» y «difiere» en la misma condición ⇒ **21 recetas sanas quedaron anotadas como
NO-DETERMINISMO** en un libro que pretende ser autoritativo. Un registro falso y persistente es peor
que no tener registro: el propósito del libro es que nadie tenga que volver a mirar, así que una
entrada equivocada se cree para siempre.

Es la trampa de siempre —un disco lleno se lee como receta rota, y la pista es que mueren varias
seguidas— y el script ORIGINAL ya la evitaba: «no es lo mismo «no reproduce» que «no construye
acá». Distinguirlos importa: mezclarlos inventaría un problema de determinismo». Lo rompí al añadir
la segunda reconstrucción. Restituido: si la 2ª no construye, se restaura el artefacto, se cuenta
como fallo y **no se anota nada** — sin veredicto es un estado legítimo.

Reparado además el daño: las 21 entradas falsas purgadas del libro (quedan 27 verificaciones
buenas), y `angle-grinder` repuesto desde `store/.divergen/`, donde lo había dejado la clasificación
errónea. Eso sí funcionó como debía: el ejemplar estaba guardado y entero, no perdido.

Y queda anotado en el propio script el límite del filtro por tiempo que provocó todo: el sidecar de
`store/.times/` lo escribe el WORKER, así que acota el build de esa receta EN LA MÁQUINA QUE LO
MIDIÓ. Una receta Rust de 40 s allá con la caché de cargo caliente son muchos minutos acá en frío
—`angle-grinder` y `amp` entraron por el filtro y se comieron el disco— y encima el número no cuenta
la CASCADA de deps que falten en el store local, que es la misma sorpresa que dio `gjs`. Con disco
justo conviene pasar las recetas a mano.
2026-09-05 20:05:33 +00:00
Sergio efe6e001d2 repro: un libro de lo verificado, y por fin un número de cobertura — era 2 de 1157
`verificar-repro.sh` sorteaba una muestra y se olvidaba. Sin registro no había forma de contestar la
pregunta que importa —¿qué fracción del corpus se comprobó alguna vez que reproduce?— y encima las
mismas recetas salían sorteadas una y otra vez mientras otras no se miraban jamás. La primera medida
con el libro puesto: **2 de 1157**. El invariante central del proyecto no se había verificado de
forma acumulativa prácticamente en nada, y eso no se veía porque cada corrida daba « PUERTA
SUPERADA» sobre su propia muestra.

`docs/state/repro-verificado.tsv` va con la misma clave auto-invalidante que el libro de fuzz:
(receta, **ArtifactHash**). El hash resume la receta MÁS su cierre de deps, así que una entrada deja
de casar en cuanto algo aguas arriba cambia. Un «ya lo verifiqué» que sobreviviera a un re-hash sería
una mentira; éste no puede serlo.

Con eso el muestreo pasa a sortear entre lo NO verificado, así que corridas sucesivas ACUMULAN
cobertura en vez de repetir (con `TODO=1` se sortea entre todas, para re-verificar a propósito), y
`--coverage` da el número sin construir nada.

Verificadas en esta tanda: itstool, markupsafe, musl-obstack, hicolor-icon-theme, musl-fts, npth,
poppler-render-check, doas, libffi y packaging REPRODUCEN; `when` derivaba y quedó al día. Cero
no-determinismos. Van 11 de 1157.

⚠ Y una trampa que casi me come, anotada acá porque el próximo la va a pisar: elegí candidatos «por
artefacto chico» y la lista empezó con **nodejs**, cuyo artefacto pesa 0,2 MB y cuyo build son horas
de V8. El tamaño del artefacto NO dice nada del costo de construirlo. Para elegir muestra barata hay
que mirar tiempos de build, no bytes de salida.
2026-09-05 19:31:33 +00:00
Sergio d5863ae554 verificar-repro: el apartadero iba a tmpfs, no tomaba el lock, y dejaba el veredicto a medias
Tres defectos, encontrados corriéndolo y pagando uno de ellos: perdí el artefacto de `aichat` al
interrumpir una corrida. Está reconstruyéndose en el worker.

**1. El apartadero estaba en `/tmp`.** Para verificar hay que sacar el artefacto de su sitio y dejar
que hammer lo reconstruya; eso iba a un `mktemp -d`. La cabecera prometía «un verificador que
destruye lo que verifica es peor que no tenerlo», y esa ubicación rompía la promesa en tres sitios a
la vez: `/tmp` es **tmpfs**, así que apartar copiaba el artefacto a RAM en una caja de 7,6 GiB sin
swap; al ser otro sistema de ficheros el `mv` no era un rename atómico sino copiar-y-borrar, o sea
que una interrupción a mitad lo dejaba en ninguna parte; y lo que sobreviviera moría al reiniciar.
Ahora va a `store/.verificar-repro`: el `mv` es instantáneo, no cuesta RAM ni disco, y lo apartado
SOBREVIVE a un kill — la corrida siguiente lo repone sola antes de empezar.

**2. Llamaba a `hammer build` sin tomar el `flock`.** Es la regla 1 del repo, y saltársela arriesga
el árbol compartido de `work/sources` de forma irreversible (ADR 0012). Lo toma el script y no quien
lo llama, por lo mismo que `poda-fuentes.sh`: así no hay forma de correrlo mal — y correrlo mal fue
exactamente lo que hice. Con `-E 3`, porque sin eso «no conseguí el lock» sale como exit 1, que es lo
que este script usa para «hay divergencias»: informar «el corpus no reproduce» cuando lo que pasaba
era que había otro build corriendo es peor que no correr.

**3. Confundía DERIVA con NO-DETERMINISMO y lo delegaba en la memoria de quien leía.** Que el
guardado difiera de una reconstrucción de hoy puede ser que el lab se movió (deriva, sano) o que las
mismas entradas dan salidas distintas (no-determinismo, bug). La cabecera decía «corré el script dos
veces y mirá la segunda» — con lo que la primera corrida informaba «DIVERGE» sobre cosas sanas y
entrenaba a no creerle. Ahora, ante una diferencia, reconstruye una segunda vez y compara las dos
reconstrucciones ENTRE SÍ: si coinciden es DERIVA, si no, NO-DETERMINISMO. El rebuild extra sólo se
paga cuando ya hubo una diferencia. Y cuando es no-determinismo de verdad, los dos ejemplares se
conservan en `store/.divergen/` en vez de borrarse: antes el guardián tiraba la única evidencia justo
en el caso que existe para encontrar.

Medido de paso, y es la parte buena: `libassuan`, `libksba`, `libffi` y `packaging` —artefactos del
21 de agosto— reproducen BIT A BIT con el lab de hoy. `scdoc` derivaba y ya está al día; `gron`,
`age` y `anew` reproducen. La rama de DERIVA se probó ensuciando a propósito una copia guardada:
clasifica bien y el store queda con la reconstrucción limpia.
2026-09-05 19:03:48 +00:00
Sergio 59a0e56a16 licencias 97% (1132/1165): «no hay fichero» y «no reconozco el texto» no son lo mismo
El script decía «sin COPYING ni declaración en el árbol» para `lsof`, `when`, `wpa_supplicant`,
`perl-xml-parser` y `fuse3` — y las cinco SÍ tienen un COPYING o un LICENSE en la raíz. Lo que
pasaba es que ninguna de mis huellas reconocía su texto, que es un problema distinto y se acciona
distinto: uno pide buscar la licencia en otro sitio, el otro pide una huella nueva o leerla. Decir lo
mismo de los dos casos mandaba a mirar donde no era. Ahora el mensaje nombra el fichero y cita su
primera línea, y con eso tres se resolvieron leyéndolas:

  · `wpa_supplicant` — su COPYING no es una licencia sino un aviso: «the project has chosen to use
    only the BSD license option for future distribution», y remite al README. El README trae las tres
    cláusulas clásicas ⇒ BSD-3-Clause. Sin seguir esa remisión no había veredicto posible.
  · `when` — «under the same terms as Perl, or, at your option, under version 2 of the GPL». «Los
    mismos términos que Perl» ES la disyunción Artistic-1.0-Perl OR GPL-1.0-or-later, y la opción de
    GPL-2 ya queda cubierta por el `-or-later`.
  · `perl-xml-parser` — su LICENSE es el texto íntegro de la Artistic License 2.0.

Ese último obligó a cerrar un lazo que yo mismo había dejado abierto: el script se niega a declarar
un SPDX cuyo TEXTO no tengamos en `licenses/`, porque la obligación legal es entregar el texto y no
el nombre. Pero `licencias-textos.sh` deriva qué textos bajar DE las licencias ya declaradas ⇒ una
licencia genuinamente nueva no podía entrar por ningún lado. El orden que lo resuelve es: declarar
con evidencia, bajar el texto, comprobar que llegó. `licenses/` pasa de 44 a 48 textos y el único que
queda sin texto canónico es `LicenseRef-qorpa-ajena-no-enumerable`, que es un identificador nuestro y
SPDX no publica.

Quedan 33. `lsof` (licencia propia de Purdue) y `fuse3` (aviso compuesto: LGPL para la librería, GPL
para las utilidades) siguen pendientes a propósito, y el resto son expresiones que decide una
persona: gmp, ffmpeg, clang18, llvm18 y los `gi-*` traen VARIOS ficheros de licencia que dicen cosas
distintas, y en ffmpeg la respuesta ni siquiera está en el árbol —la deciden los flags de la receta.

Los 3 ArtifactHash, idénticos antes y después.
2026-09-05 18:10:52 +00:00
Sergio b87835f719 licencias 96% (1129/1165): la fuente git también es evidencia, y el script deja de llamarse «tarball»
Faltaban tres recetas cuya fuente es un repo, no un tarball: `fcft`, `foot` y `glab`. El commit
pineado cumple exactamente el mismo papel que el sha256 —es el árbol EXACTO que construimos— así que
la evidencia estaba igual de disponible, sólo que por otra puerta.

`miembros_de_git()` la abre sin clonar el árbol entero: `--depth 1 --filter=blob:limit=64k` sobre el
commit. Un COPYING nunca pasa de 64k y los fuentes donde vive la concesión tampoco; los que sí pasan
—binarios, assets— son justo los que no interesan. Un repo de cientos de megas se resuelve con unos
pocos. Las tres salieron del `license:` que el propio autor escribe en su `meson.build` (fcft, foot)
y del texto del LICENSE (glab).

Los repos por SSH se saltan diciéndolo: este script hace fetch anónimo y no tiene claves. Son
`hammerd` y `portal-probe`, y ya están resueltas por otra vía (su repo es este mismo).

`analizar()` pasa a consumir un iterable de `(ruta, bytes)` en vez de abrir el tar ella misma, así
que las dos fuentes comparten TODA la lógica de veredicto: la jerarquía de evidencia, la regla de
sólo-la-raíz, la unión de identificaciones y la exigencia de que la concesión hable de la misma
versión que el COPYING. Un criterio, dos puertas.

Y el script se renombra `licencias-tarball.py` → `licencias-fuente.py`, porque el nombre viejo pasó a
ser mentira en el momento en que aprendió a leer git. Un nombre que describe la implementación de
ayer manda a la gente a buscar en el sitio equivocado.

Los 3 ArtifactHash, idénticos antes y después.
2026-09-05 18:07:17 +00:00
Sergio db7fd81236 parches: los 8 FUZZ del catálogo, mirados uno por uno — y un libro que se auto-invalida
`vigia-parches.py --all` sobre las 99 aplicaciones de parche del catálogo: **0 FALLA**, y 14 líneas
FUZZ (8 pares parche/fuente distintos). FUZZ significa que `patch` metió el cambio ADIVINANDO dónde
porque el contexto no casaba, y si cayó en el sitio correcto sólo lo dice el diff. Nadie los había
mirado. Los miré todos:

  · **libxml2 / CVE-2026-6732** — el que más importaba, un parche de seguridad con fuzz 2 en sus dos
    hunks. Cayó bien: los dos dentro de `xmlParseReference()`, las 4 llamadas pasan `ctxt->userData`
    y no sobrevive ningún `sax->characters(ctxt,` en el fichero. El fuzz era por un offset de 226
    líneas, no por el sitio.
  · **doas / rowhammer** — el otro sensible, toca la decisión de privilegio. Cae dentro de
    `checkconfig()` y queda `rv=permit(...); if(rv==0)→permit`, coherente con el `if(rv!=0)→EPERM`
    de `main()`. Y el fuzz lo causa un parche ANTERIOR de la propia cadena (el `#ifdef DOAS_CONFDIR`
    que inserta `configuration-directory.patch`), no un cambio de upstream — que es una causa que no
    se me habría ocurrido sin abrirlo.
  · **wayland**, **mesa** (×3), **cairo** (×2), **firefox** (time64 y fix-rust-target): todos en su
    sitio, cada uno comprobado contra lo que el propio parche declara querer.
  · **gnupg / 0001-include-unistd** — hallazgo: el parche está OBSOLETO. Añade `#include <unistd.h>`
    y upstream YA lo trae dos líneas más abajo, así que sólo lo duplica. Inocuo (el header tiene
    guardas) y por eso el fuzz 2: cambió el contexto porque upstream lo incorporó. Quitarlo re-hashea
    gnupg, así que se paga cuando se re-selle por otro motivo.

Y para que esto no se repregunte en cada corrida —lo que vuelve ruido al vigía, y así es como se
pierde el FALLA del día que aparezca— los veredictos van a `docs/state/fuzz-verificado.tsv` con un
cuarto estado, FUZZ✓.

La clave del libro NO es (receta, parche) sino (receta, parche, HUELLA), donde la huella resume el
texto del parche MÁS el pin de la fuente. Tocá el parche o subí la versión y la huella cambia, la
entrada deja de casar y el vigía vuelve a preguntar. Es lo contrario de una lista de excepciones: no
hay forma de silenciar algo y que siga silenciado cuando cambió. Y el vigía imprime la línea lista
para pegar debajo de cada FUZZ sin verificar, porque calcular la huella a mano es justo la fricción
que hace que nadie lo anote.

Los dos FUZZ de waterfox quedan FUERA del libro a propósito: no los verifiqué en esta ronda y
waterfox está fuera de alcance. Van a seguir saliendo como FUZZ, que es lo honesto — el libro dice
lo que se miró, no lo que se supone.
2026-09-05 17:34:28 +00:00
Sergio f6e690d994 vigia-imagen: un módulo QML registrado en C++ no está «sin instalar» — se comprueba, no se recuerda
Los DOS únicos ✗ que reportaba este vigía eran falsos, y de la misma clase: un `import` no siempre se
satisface con un directorio y su `qmldir`. El greeter de la pantalla de bloqueo hace
`qmlRegisterUncreatableType<PamAuthenticator>("org.kde.kscreenlocker", …)` — el módulo existe SÓLO
dentro del proceso que carga ese `.qml` y en disco no hay nada que instalar. Buscarlo en el sistema
de ficheros da ✗ con la imagen perfecta.

Los dos, verificados uno por uno antes de tocar el script:
  · `org.kde.kscreenlocker` ← `usr/lib64/libexec/kscreenlocker_greet`. Y valía la pena mirarlo: los
    que lo importan son `lockscreen/LockScreenUi.qml` y `MainBlock.qml` de plasma-desktop, o sea la
    pantalla de bloqueo de verdad. Si hubiera faltado no era un diálogo de ajustes.
  · `org.kde.newstuff.core` ← `usr/lib/qt6/qml/org/kde/newstuff/libnewstuffqmlplugin.so`.

Antes esto se tapaba anotando el módulo a mano en `QML_EN_RUNTIME`. `registrado_en_binario()` lo
contesta ahora con evidencia —el URI queda como literal en `.rodata`, así que se busca en los ELF del
cierre— y además NOMBRA al registrador, que es lo que permite auditar el veredicto sin repetirlo.
Mira primero los artefactos cuyo nombre comparte una palabra con el URI, que es donde está casi
siempre, y sólo si no aparece recorre el resto.

⇒ la lista escrita a mano SE BORRA ENTERA. Al vaciarla, los cuatro módulos que tenía se resolvieron
solos, cada uno nombrando su binario (plasmashell, kwin_wayland). Y salió gratis un hallazgo que
ninguna lista podía dar: `org.kde.kwin.effect` no se importa en NINGÚN cierre — era una entrada
muerta, y nadie tenía cómo saberlo.

Los cinco perfiles quedan en ✓ de qml, sin un solo ✗ en todo el informe.

La regla detrás: un guardián que grita en falso no se endurece, se ignora. Cuando el 100% de sus
hallazgos son falsos positivos, el trabajo no es anotarlos — es enseñarle a preguntar bien.
2026-09-05 17:11:23 +00:00
Sergio 52d3bbfeb3 atuq-nested: CAPTURA=<png> — la prueba en el píxel, en una variable
Durante horas dije «mirá tu pantalla» y reporté «el proceso vive» como si fuera lo mismo. No lo es:
la ventana estaba ahí desde el principio y lo que fallaba era el branding DENTRO del zip, que
ningún log iba a contar.

`grim` ya estaba en el corpus (`incoming-wlr`), se hidrata en el mismo rootfs y captura la pantalla
real contra el mismo socket wayland desde dentro de la jaula. Ahora es parte del runner: con
`CAPTURA=/ruta/foto.png` se lanza atuq, se espera a que pinte y se deja el PNG afuera para mirarlo.

De paso, la resolución de raíces prueba el corpus Y `incoming-wlr`, en ese orden — el mismo que usa
la resolución de deps (sibling-first, luego el catálogo padre). Sin eso, `grim` no se encontraba y
el script habría dicho «falta un artefacto» sobre una receta que existe.
2026-09-05 17:06:17 +00:00
Sergio 748b2f604c yupana: el khipu se descubre, no se escribe a mano — cosmic y wlr eran invisibles
Dos sitios llevaban la lista de grafos codificada como `(kde, gnome, corpus)` y **omitían
`build-state-cosmic.json` y `build-state-wlr.json`**, que son dos de los cuatro escritorios. Es
exactamente la forma del fallo que motivó este fichero y que su cabecera describe: medir contra un
subconjunto de las colas y que el número salga chico sin que nada avise.

Medido: `_estado()` devolvía `?` para los 36 nudos propios de cosmic y para 12 de los 14 de wlr.

Pero el daño peor no era el `?`. `_estado()` elegía el fichero por la cola —gnome si
`incoming-gnome`, corpus si `corpus`, **KDE para todo lo demás**— así que un nudo de `incoming-wlr`
se buscaba en el grafo de KDE. `gdk-pixbuf` existe en `corpus` Y en `incoming-wlr` con recetas y
hashes distintos, y la consulta por el de wlr devolvía el estado del de corpus, con toda confianza.
Coincidía en `sealed`, así que nunca se notó. Un `?` se ve; una respuesta de la receta equivocada,
no.

Ahora `_khipus()` los descubre por glob (un grafo nuevo entra solo; ninguno se puede olvidar) y
`_estados_por_cola()` indexa por **(cola, nombre)**, que es la clave real: el mismo nombre vive en
varias colas siendo recetas distintas. Si la cola no tiene ese nudo, la respuesta es `?` — que es la
verdad, y no el estado de su homónimo.

`keystones` compartía el mismo índice por nombre suelto: pasa a `(cola, nombre)` en las tres
consultas.

Los tres invariantes de `test-yupana-radio.py` siguen verdes, y las 50 discrepancias de cosmic+wlr
bajan a 0.
2026-09-05 16:31:31 +00:00
Sergio 96855001c6 licencias: 96% (1124/1164) — --fetch para los tarballs que no estaban en caché
Ocho paquetes más (giflib, pcre2, nghttp2, libogg, libvorbis, speexdsp, libuv, tea) con la misma
regla de evidencia. Su tarball no estaba en `work/tarballs/`, así que el modo `--fetch` lo baja a un
temporal, lo verifica contra el sha256 QUE LA RECETA PINEA —o es el árbol exacto o no se mira— y lo
borra. No se escribe en la caché de hammer a propósito: es suya, y un fichero puesto ahí por otro
camino es una vía de envenenamiento que nadie audita.

Dos correcciones al criterio, las dos porque produjo una afirmación falsa:

  · `gmp` salía GPL-3.0-or-later. Su `COPYING` es la GPLv3, pero al lado trae `COPYING.LESSERv3` y
    `COPYINGv2` porque la biblioteca es LGPL. La regla anterior —«el `COPYING` a secas gana al
    sufijado»— arregla socat, cuyo `COPYING.OpenSSL` es una excepción, y rompe gmp, donde el extra
    nombra otra VERSIÓN y no una dep. Esa diferencia es demasiado fina para codificarla sin
    equivocarse, así que ahora se identifican TODOS los ficheros de la raíz y sólo hay veredicto si
    dicen lo mismo. gmp, ffmpeg y los tres `gi-*` quedan pendientes, que es lo correcto: en ffmpeg
    la respuesta ni siquiera está en el árbol, la deciden los flags de la receta.

  · Pero «varios ficheros» no es «ambigüedad»: el `COPYING` de pcre2 son dos líneas apuntando a
    `LICENCE.md`, y los dos dicen BSD-3. Por eso se unen las identificaciones en vez de contar
    ficheros — ambiguo es que digan cosas DISTINTAS.

Y el TSV pasa a ser ACUMULATIVO. Una receta ya sembrada sale del conjunto de entrada porque ya tiene
`license`, así que regenerar el fichero entero borraba su cita — y la cita es el rastro de
auditoría, lo único que deja revisar un veredicto sin repetir el trabajo. Un fichero de evidencia
que se olvida de lo que ya probó no es evidencia.

Los 10 ArtifactHash afectados, idénticos antes y después.
2026-09-05 15:43:51 +00:00
Sergio ba1fb2d3a8 licencias: 95% (1114/1164) — la evidencia sale del tarball pineado, offline
`licencias.sh --sembrar` escribe desde una tabla curada a mano y `licencias-desambiguar.sh` resuelve
`-only` vs `-or-later` preguntándole a la búsqueda de código de GitHub. Los dos dejan fuera lo mismo:
lo que nadie curó y lo que no vive en GitHub.

Pero `work/tarballs/` indexa cada tarball por su sha256, así que el árbol EXACTO que la receta pinea
ya está en disco: la declaración del propio autor, en el commit que construimos, sin red. De las 76
sin licencia, 55 tenían su tarball cacheado y 25 salieron con evidencia citable.

`scripts/licencias-tarball.py` la busca en tres niveles: la declaración del autor (`Cargo.toml`,
`meson.build`), un único fichero en `LICENSES/` (REUSE, que usa KDE), y el texto del COPYING más la
CONCESIÓN buscada en las cabeceras de los fuentes — excluyendo COPYING/LICENSE, porque el apéndice
de la GPL trae literalmente «or (at your option) any later version» y buscarla ahí da siempre
positivo siendo plantilla. Es la regla que ya fijó `licencias-desambiguar.sh`, aplicada al árbol
pineado en vez de a GitHub.

La primera versión resolvía 40, y CUATRO estaban mal. Las dejo escritas porque son la forma del
error, no accidentes:

  · `socat` salía BSD-2 por su `COPYING.OpenSSL`, que es la excepción, no la licencia. Un `COPYING`
    a secas gana ahora a cualquier sufijado.
  · `pigz` salía Apache-2.0 por `zopfli/COPYING`: la licencia de una pieza VENDORIZADA leída como la
    del contenedor. Sólo se mira la raíz.
  · `openssh` salía MIT porque su `LICENCE` es un compendio de cuatro y me quedaba con la primera
    que pegara. Si el texto trae varias, no hay veredicto: lo compone un humano.
  · `nano` salía GPL-3.0-**or-later** citando el «either version 2» de su `aclocal.m4` — plantilla de
    autotools, no del proyecto, y encima de otra versión. Ahora la cita tiene que venir de un fichero
    del autor Y hablar de la misma versión mayor que el COPYING.

Y dos bugs míos que producían el mismo daño en silencio: la marca de BSD-3 era «Neither the name of»,
que libpcap y libzip no usan («The names of the authors may not be used to endorse») ⇒ se declaraban
BSD-2; y el mayor de versión lo sacaba de `"GPL-3.0".rsplit("-",1)[0][-1]`, que da «L», así que la
comprobación de coherencia rechazaba TODA cita válida.

La validación final no es una regex: un identificador vale si tenemos su texto en `licenses/`. La
obligación legal es acompañar el binario del TEXTO, así que un SPDX que no podemos entregar no
adelanta nada y sí crea una afirmación que no se sostiene. Eso es lo que atrapa el «GPL2+» que meson
deja escribir en `gsd-schemas` y `libgdm`, que no es un SPDX sino taquigrafía.

Nada se sembró a ojo: cada línea de `docs/licencias-evidencia.tsv` lleva la cita que la decidió, y
las 50 que quedan salen listadas con lo que SÍ se encontró en vez de rellenarse.

Comprobado que no se invalida nada: los 26 ArtifactHash afectados son idénticos antes y después
(`license` no está en `hash_inputs`) — medido, no supuesto.
2026-09-05 15:34:35 +00:00
Sergio 5d06a3b83d atuq ABRE: el app_id estaba compilado en el binario, y el árbol copiado llegaba de sólo lectura
Con la cadena GTK3 arreglada, atuq arrancó de verdad: parent vivo, CERO procesos de contenido
muertos, y pintando una página local. Quedaban dos cosas.

1. SE ANUNCIABA COMO `firefox-default`. El application.ini decía RemotingName=atuq y aun así el
   proceso se presentaba así — porque esa cadena es un MOZ_APP_REMOTINGNAME horneado en el ELF, y es
   la que Gecko pasa a g_set_prgname(), o sea el **app_id de Wayland**. Consecuencia visible: el
   escritorio no casaba la ventana con nuestro .desktop (StartupWMClass=atuq) ⇒ icono genérico y sin
   agrupar.

   Se parchea EN SITIO, no recompilando: cambiarlo en recipes/firefox.toml brandearía la BASE como
   atuq, y firefox tiene que seguir siendo firefox para los otros forks. Es seguro porque es una
   cadena C terminada en NUL en el pool de .rodata —`firefox-default\0` son 16 bytes exactos y se
   escriben 16—, y se EXIGE una única ocurrencia: si upstream la duplica o la renombra, falla en vez
   de parchear el sitio equivocado. Verificado después: 0 ocurrencias de la vieja, y el log dice
   `(atuq:26423)`.

2. UN BUG QUE SÓLO SE VE FUERA DE ROOT. `cp -a` preserva los modos y los ficheros del store están
   sellados sin permiso de escritura; todo lo que viene después los modifica. En el worker pasaba
   inadvertido porque corre como root, que ignora los bits. El primer build local murió con
   `PermissionError: /out/usr/lib/atuq/application.ini`. Se arregla con `chmod -R u+w` en la receta y
   no en el entorno: un artefacto no debe depender de con qué uid lo construiste.

Y el runner pasa a hidratar las variantes `-shared`: en runtime una `.a` no sirve de nada, y son las
que gtk3 declara NEEDED desde el arreglo del cuadro de las dos Pango.

Este atuq se construyó EN LOCAL en segundos, que era la promesa entera del diseño derivado del
SDD 26: iterar el envoltorio sin volver a pagar un build de Gecko.
2026-09-05 14:31:35 +00:00
Sergio a828e74c40 la cadena GTK3 se enlazaba estática dentro de dos .so: cuatro variantes -shared y xkeyboard-config al corpus
Con `atk` arreglado, atuq llegó más lejos y murió igual, ahora con Pango:

    GLib-GObject-CRITICAL: cannot register existing type 'PangoFontMap'
    … decenas de líneas … tipo '<invalid>'

MISMO CUADRO, CULPABLE DISTINTO Y MÁS GRANDE. `gtk3` produce DOS objetos compartidos —libgtk-3.so y
libgdk-3.so— y libxul declara NEEDED las dos, así que se cargan siempre juntas. Con las variantes
ESTÁTICAS de sus deps, cada una se llevaba adentro su propia copia. Medido con el mismo instrumento
que cazó a atk, `nm -D --defined-only`, preguntando quién DEFINE cada símbolo:

    pango_font_map_get_type → libgdk-3.so, libgtk-3.so, libgailutil-3.so
    cairo_create            → libgdk-3.so, libgtk-3.so
    gdk_pixbuf_get_type     → libgdk-3.so, libgtk-3.so
    hb_buffer_create        → libgdk-3.so, libgtk-3.so, libgailutil-3.so

Y se comprobó el otro lado: libxul NO embebe ninguna —enlaza libgtk-3/libgdk-3 como debe—, así que
la duplicación es toda interna del par GTK3.

Cuatro variantes nuevas: harfbuzz-shared, cairo-shared, gdk-pixbuf-shared, pango-shared. Sólo cambia
el modo de librería; se conservan todos los switches del canónico para no arrastrar deps nuevas.

DOS DEUDAS ANOTADAS, NO OLVIDADAS: `fribidi` y `pixman` siguen estáticos —no existe variante— pero
quedan embebidos en UNA sola .so cada uno, así que no hay copia que colisione; y `libepoxy` sí queda
en las dos, y se deja porque no registra tipos de GObject ni mantiene estado global. Si algún día
otra .so del mismo proceso los embebe, vuelve el cuadro.

`firefox` swapea las mismas cuatro: declarar las ESTÁTICAS junto a un gtk3 que trae las compartidas
son dos artefactos peleando por el mismo `pango.pc`, que es la otra forma conocida de este fallo.

Y `xkeyboard-config` SUBE AL CORPUS. Existía idéntica en las cuatro colas de escritorio (un solo md5
entre las cuatro, verificado antes de mover) y atuq, que vive en el corpus, no podía alcanzar
ninguna: sibling-first y después el catálogo padre, nunca una cola hermana. Sin sus datos el
navegador ni pinta («xkbcommon: failed to add default include path /usr/share/X11/xkb»). Mismo hash
en el corpus que en las colas ⇒ cero rebuilds, y las copias de las colas se quedan donde están.
2026-09-05 12:02:24 +00:00
Sergio d2527284b9 atuq-nested: el rootfs se revalida por HASH, no por existencia
El script daba por bueno el rootfs si el directorio estaba: «¿ya está hidratado?». Es la pregunta
equivocada. Tras el re-hash de la cadena atk→gtk3→firefox→atuq el directorio seguía ahí con los
artefactos VIEJOS, así que habría abierto contento la versión que acabábamos de arreglar y el
diagnóstico habría sido buscar en el sitio equivocado. Misma forma del cache-hit que congela
regresiones.

Ahora resuelve los hashes vigentes, los anota en `.raices` y rehidrata cuando difieren.

De paso: nada de `diff <(...)`, que es de bash. El shebang dice /bin/sh y un script que sólo anda
cuando /bin/sh resulta ser bash es una trampa que salta en otra máquina.
2026-09-05 11:52:18 +00:00
Sergio 0f7d19232e atuq: el lanzador deja de ser un symlink, y un runner para abrirlo en la pantalla que ya tenés
Dos cosas que salieron de intentar ABRIRLO, que es lo único que distingue «sellado» de «usable».

1. EL LANZADOR. Con `/usr/bin/atuq` como symlink, el navegador moría antes de pintar:

     XPCOMGlueLoad error for file /usr/lib/atuq/libmozsandbox.so:
     Error loading shared library libnspr4.so: No such file or directory

   El motor carga sus propias librerías desde `/usr/lib/atuq` y ni el binario ni esas `.so` traen
   RPATH/RUNPATH — comprobado con `readelf -d`, no supuesto. Pasa a ser un script que exporta
   `LD_LIBRARY_PATH` (lo mismo que hacen Debian y Fedora) y hace `exec`, para que el proceso que
   queda sea el motor y `/proc/self/exe` siga resolviendo el appdir. El `LD_LIBRARY_PATH` además
   tiene que HEREDARSE: Firefox lanza un proceso por pestaña y todos cargan las mismas librerías.
   La alternativa limpia es grabar RPATH=$ORIGIN con `patchelf` —es lo que hace Alpine— pero
   `patchelf` todavía no existe como receta del corpus; cuando exista, esto vuelve a ser un symlink.

2. `scripts/atuq-nested.sh` — hidrata el cierre de runtime y abre atuq como ventana anidada en el
   compositor que ya está delante (waypipe, mirada, sway). Trae dos cosas aprendidas a golpes:

   · EL ROOTFS VA EN EL MISMO MOUNT QUE EL STORE. `hydrate` proyecta con hardlinks y `linkat()`
     rechaza cruzar un punto de montaje aunque sea el mismo filesystem. Acá el store es /dev/sdb
     bind-monteado y `work/` vive en /dev/sdc: hidratar a `work/…` muere con «Invalid cross-device
     link (os error 18)». El volumen entero está en /mnt/cosecha, así que el rootfs va ahí. Se
     comprueba con `findmnt -T`, nunca con `stat -c %d`.
   · `dejavu-fonts` va en las raíces por necesidad, no por completismo: un navegador sin una sola
     fuente arranca, pinta y muestra cuadraditos. Ya nos costó una tarde en GNOME.

   Y no saltea en silencio: si falta un artefacto de la lista, sale con error en vez de armar un
   rootfs al que le faltan tres paquetes y falla tres capas más abajo.
2026-09-05 09:53:33 +00:00
Sergio 442a319499 lab re-pineado con lld (d1e341d5) — y el sha «determinista» lo ensuciaba un log
Al ir a re-pinear la imagen para propagar el `lld` a la granja, hub y worker empaquetaron a shas
DISTINTOS. La imagen se creó para que el sha VERIFIQUE, así que un desacuerdo ahí no se pinea: se
mide.

Los dos rootfs son idénticos: 9550 entradas con la misma lista de ficheros, los mismos tamaños, y
CERO diferencias de metadato (modo, nlink, destino de symlink) en las 8778 entradas de fichero y
symlink. La única diferencia de contenido en todo el rootfs era `var/log/apk.log`, donde apk anota
la FECHA de cada operación — o sea que dos labs equivalentes empaquetaban distinto sólo porque las
instalaciones ocurrieron en momentos distintos. Un sha que no se puede reproducir desde un rootfs
equivalente no verifica nada, que es justo lo que este fichero existe para dar.

Se excluye ese log del tar (no todo `var/log`: cambio mínimo; apk lo recrea solo). Con la exclusión,
hub y worker dan el MISMO tar: 179d04f054f28d8dafe7c626d6b8c686a33a00a96715ee544a9bcf6f62cf586d.

⇒ LA GRANJA YA ESTABA SINCRONIZADA Y AHORA SE PUEDE DEMOSTRAR, sin correr `--traer` en un worker que
está construyendo firefox — reemplazarle el rootfs a mitad de build habría sido la forma cara de
descubrir lo mismo.

Nueva imagen publicada: hammer/lab/lab-d1e341d5….tar.zst (310 M), pin actualizado en
bootstrap-devfs.sh. Queda en el comentario cómo comparar dos máquinas: se compara el TAR y no el
`.tar.zst`, porque el sha comprimido depende de la versión de zstd de cada máquina y dos labs
idénticos con zstd distintos darían un falso desacuerdo.
2026-09-05 03:49:56 +00:00
Sergio cb3ecd5a00 firefox pasa a clang+lld con LTO: la puerta de las optimizaciones costaba apk add lld, no una receta de LLVM
El SDD 26 estimó que habilitar PGO/LTO exigía una receta `llvm-toolchain` (clang+lld+libc++ desde
fuente). ERA CARO DE MÁS. Al mirar el lab en vez de suponerlo:

- `.dev-fs/alpine` YA TRAE clang22 + llvm22 22.1.8 — la MISMA major que usa el APKBUILD de Alpine
  para este mismo Firefox (`_llvmver=22`).
- `Compiler::Clang` YA EXISTE en hammer, cableado de punta a punta: `parse_compiler` lo acepta y
  `hammer-build/src/lib.rs` pone CC=clang, CXX=clang++ y AR=llvm-ar. NINGUNA receta lo usaba.
- Lo único que faltaba era `ld.lld`. `apk add lld` ⇒ lld22 22.1.8, dos paquetes, cero upgrades.

CON ESO CAEN LOS TRES MUROS QUE OBLIGABAN A gcc, sin perder lo que gcc daba: el sondeo de linker se
satisface con `--enable-linker=lld`, el `ar` lo pone hammer solo, y el `NEEDED` de la stdlib de C++
existe porque clang++ de Alpine usa la libstdc++ COMPARTIDA — la prueba no es teórica, Alpine
construye este Firefox con clang22 y sin libcxx en sus makedepends.

LA HUELLA DEL LAB NO SE MOVIÓ, Y SE MIDIÓ ANTES DE TOCAR NADA. `lld` no casa ningún prefijo de
TOOLCHAIN_PREFIXES (hammer-core/src/lab.rs), así que los 43 paquetes que entran en `hash_inputs`
salieron idénticos ⇒ los 837 artefactos sellados quedan intactos. Eso es lo que hace barato el
cambio HOY, y a la vez es un agujero escrito en los dos sitios: la versión de lld no es parte de la
identidad del artefacto, y sólo expone a las recetas `compiler="clang"`, que hoy es una. Meter
"lld" en la lista de prefijos es lo correcto y cuesta re-hashear el corpus entero: próxima campaña.

En el mozconfig entran, además de lld: `--enable-lto=cross`, `--enable-packed-relative-relocs` y
`--with-unsigned-addon-scopes=app,system`. El último no es cosmético: sin él un Firefox de release
rechaza las extensiones que la distro deja en distribution/extensions/, así que la capacidad de atuq
de shipear su propio `sct` se decide ACÁ, en la base, y no en el overlay del derivado.

PGO no entra en esta pasada y el porqué queda escrito en la receta: el perfil se junta corriendo el
navegador (Alpine y Arch usan xvfb-run; nosotros no tenemos X11 ⇒ sway headless) y el profdata NO es
determinista, así que tiene que sellarse como artefacto propio y consumirse por hash.

ThinLTO se capa con la misma cuenta que -j y por la misma razón que ella no es un literal: un número
fijo ataría el ArtifactHash a la RAM de quien escribió la receta.

Nuevo ArtifactHash: b3:6f2a3b2f6db4452ed0d2d3f4e2ff7cd6562a86878d4360653859720be1c3d94d
(el firefox 154.0 sellado con gcc queda SUPERADO, no perdido).
2026-09-05 03:13:38 +00:00
Sergio aa200a4e7e vigía de parches: probar que agarran ANTES de las cuatro horas de build
`hammer build` aplica los parches DESPUÉS de materializar las dependencias, así que en una receta
hoja de la plataforma Gecko el `patch` que no agarra se descubre detrás de horas de compilar OTRA
COSA: waterfox estrena su primer build reconstruyendo nodejs entero (4414 objetivos de V8, a -j2
porque la propia receta capa por RAM). La receta de waterfox dice, con todas las letras, que si
patch falla «falla TEMPRANO, antes de compilar nada». Con el orden real de hammer eso no era
cierto. Este vigía es lo que lo vuelve cierto: saca de los propios parches los ficheros que tocan
(21 en waterfox), los trae por sparse-checkout blob:none, y aplica los once acumulativos y en
orden como hace fetch.rs. Un minuto en vez de cuatro horas.

LO QUE MIDE NO ES SÍ/NO, SON TRES ESTADOS. `patch` también responde «sí, adivinando»: cuando el
contexto no casa aplica igual con FUZZ, y con el `--silent` de fetch.rs eso sale por exit 0 sin que
nadie se entere. Un parche con fuzz puede haber editado el sitio correcto o cualquier otro, y el
exit code no distingue. Por eso `ok` / `FUZZ` / `FALLA`, y el del medio existe para no perderse.
El desplazamiento no se marca: sólo dice que el fichero creció por arriba.

VEREDICTO SOBRE LA APUESTA DE WATERFOX (parches de 154 sobre base 153.1.0): SE SOSTIENE. Los once
entran; los dos que entran con fuzz —time64 y fix-rust-target— se verificaron a mano y los dos
editan el sitio correcto. El contexto que no casa es cosmético: comillas simples vs dobles de un
reformateo con black, y un brazo `cfg` de más.

Y UN HALLAZGO DE PASO: `firefox-patches/time64.patch` tiene la cabecera MENTIROSA. Su diffstat
anuncia tres ficheros y el cuerpo entrega dos — falta el hunk de `wgpu-hal/src/vulkan/adapter.rs`.
Como firefox 154 sella igual, nadie lo había notado. Por eso los ficheros se leen del cuerpo y
nunca del diffstat.

El vigía nació mintiendo en la dirección cara y por eso se probó contra recetas selladas antes de
commitear: suponía el prefijo `a/`+`b/` de git y marcaba FALLA sobre gawk, que sella perfecto — su
parche es un `diff -upr` a secas con caminos `gawk-5.1.0.orig/`. Se descarta el primer componente
se llame como se llame, que es lo que significa el -p1 con el que se aplican.

Flags en inglés (--all, --git-only, --keep, --strict) por la regla 4 del repo.
2026-09-05 02:19:04 +00:00
Sergio 9834f57052 yupana: --help imprimía literalmente None
Todo el encabezado del fichero era comentario `#`, así que __doc__ era None y
`main()` hace `print(__doc__)` en dos caminos: `--help` y verbo desconocido. La
puerta única de la metodología no sabía explicarse, y el error de verbo tampoco
decía cuáles son los verbos válidos.

La ayuda operativa pasa a docstring; el diseño y el porqué se quedan arriba como
comentario. Cotejada contra el despacho real: los 11 verbos de la ayuda existen y
los 11 que existen están en la ayuda (faltaba `sembrar`).
2026-09-04 14:27:34 +00:00
Sergio 28d98dc5f9 drenaje: medía 5 de 7 imágenes y las otras 2 desaparecían en silencio
targets.toml declara 7 perfiles; drenaje.json listaba 5. escritorio-cosmic (cola
incoming-cosmic) y escritorio-sway (cola incoming-wlr) declaraban una cola que
ningún grafo de GRAFOS contenía, así que cargar() devolvía None y --todos hacía
`continue` pelado. Dos imágenes enteras fuera del artefacto sin dejar rastro, y
los dos números —5 medidos, 7 declarados— no se cruzaban en ningún lado.

Mismo olvido que ya costó 17 días de build-state-wlr.json congelado: la lista de
grafos crece a mano y se queda atrás cuando se abre una cola.

Tres cambios:
- GRAFOS suma los grafos de cosmic y wlr.
- Un perfil sin medir se anota en el artefacto (`sin_medir`), se grita por stdout
  y drenar.py sale 1. Regla 3 del repo: un ausente falla ruidosamente.
- cosecha-cron deja de tragarse la salida; filtra las líneas ⚠ al log, que si no
  llegaban como un "falló" mudo que no dice cuál imagen falta.

Medido: las 2 imágenes que nadie miraba estaban limpias, 0 en deuda. Ahora son
7/7 verificadas en vez de 5 verificadas y 2 supuestas.
2026-09-04 14:26:04 +00:00
Sergio 97023c3306 granja: PODA_FUENTES nace encendida y respeta el árbol de la receta que falló
Causa raíz de la deuda KDE de hoy: la cascada del 2026-09-03 corrió campana-deuda
SIN PODA_FUENTES=1 (cero menciones en su log). Los 71 árboles de fuentes se
acumularon en /dev/sdc a ~300 MB cada uno y lo llenaron en la receta 52; las 8
últimas murieron por disco, no por receta. Un default que hay que acordarse de
encender no es una defensa.

No poda tras un ✗: el árbol de la receta fallida es el post-mortem, y es justo el
caso en que alguien va a mirarlo. Mismo criterio que el suelo de 24 h de
poda-fuentes.sh, aplicado por resultado en vez de por edad.

Probado: sobrevive tras ✗, poda tras ✓, y PODA_FUENTES=0 sigue apagándola.
2026-09-04 13:11:47 +00:00