cmus b3:9a540d4270674ec74679dffbeed11d98796c51801663bde88e87affc041213ab
La primera versión decía `static` y era MENTIRA MEDIBLE: el audit del cron
(docs/state/static-audit.txt, medido 2026-09-12T21:04Z) la marcó — «dice static, es DINÁMICO →
libncursesw.so.6 libc.so» — y dejó el veredicto global en rojo. 715 honestas y las dos nuevas de
ayer mintiendo.
Pero el arreglo NO es hacerlo estático: **cmus no puede serlo**. El artefacto trae cinco plugins
—usr/lib/cmus/ip/{wav,cue,ffmpeg}.so y usr/lib/cmus/op/{alsa,oss}.so— y los abre por dlopen. Un
binario estático no puede abrirlos: quedaría un reproductor que no reproduce nada. Es la misma
razón por la que `sway` va dinámico (dlopea los drivers DRI de mesa).
⇒ `dynamic` es la declaración CORRECTA, no una concesión. Una declaración que el build ignora es
peor que no declarar, porque las herramientas río abajo la creen.
La prueba que decide, y cuesta dos segundos:
find store/<hash>-<nombre> -name '*.so' | wc -l
0 plugins → se puede y se debe ser estático. N plugins → dynamic es lo honesto.
Comprobadas las NEEDED de los plugins contra el corpus con scripts/provee.py, que es la otra mitad:
libasound.so.2 (alsa-lib), libavformat/libavcodec/libswresample .so (ffmpeg) y libncursesw.so.6
(ncurses-shared) los publica el corpus ⇒ no hay NEEDED colgante. Las tres primeras llegan por
[deps] build; ncurses-shared va declarada en runtime desde el commit anterior.
Tres preguntas del usuario, contestadas midiendo, y dos de las mediciones CORRIGEN cosas que este
mismo fichero afirmaba.
1. CENSO: de ~50 apps muy usadas el catálogo tiene 9, y CINCO de las nueve son de KDE (o sea que
llegan a una de las cuatro imágenes).
2. ⚠ CORRECCIÓN al «tercer defecto del método». Este documento dice, con fecha 2026-09-03, que
ninguna cola publica libGL.so ni gl.pc. Dejó de ser cierto AL DÍA SIGUIENTE: recipes/libglvnd.toml
es del 2026-09-04 y `provee.py --desde corpus opengl` da ✓ (libOpenGL.so.0 + opengl.pc). Y la
corrección trae su propia corrección: la campaña quedó A MEDIAS. Las tres mesa siguen con
-Dglvnd=false, no hay libEGL_mesa.so.0 ni egl_vendor.d, y en el rootfs KDE hidratado conviven DOS
libEGL.so.1 distintos (mesa 1.440.624 B gana el symlink; el de glvnd, 323.144 B, queda huérfano).
OBS —la razón de escribir libglvnd— NO la usa en runtime: libobs-opengl.so sale con NEEDED
libEGL.so.1 y ni un gl[A-Z] indefinido, o sea que su glad resuelve por eglGetProcAddress.
⇒ el `opengl ✓` es verdad DE ENLACE. El muro pasó de «no enlaza» a «enlaza y no corre».
Corolario: darktable estaba MAL clasificada como caso de GL. Su UI es GTK3+cairo; su muro es la
cola de deps (lensfun, libgphoto2, openexr, imath, libraw, osm-gps-map, portmidi + cuatro que
sólo viven en colas). Blender sí es el caso de GL.
3. ELECTRON: `grep -rni electron docs/ recipes/` da CERO. Nunca estuvo planeado. Y no existe un
«Electron con base Firefox» porque Gecko no tiene API de embebido desde XULRunner (SDD 26 §1) —
pero eso es exactamente lo que es atuq, y ya está: artefacto derivado + chrome propio +
extensiones + host de native messaging en Rust. Falta un modo SSB, que es más barato que UN port.
GenOffice concretamente: Apache-2.0 (salvo ee/), acepta endpoints OpenAI-compatible locales ⇒ la
mitad de IA es una línea de config contra el llama-cpp que YA viaja en los cuatro escritorios. La
mitad de runtime es qorpa. ⚠ Y el muro real es el MODELO: Qwen2.5-1.5B no sostiene IA agéntica
sobre un .xlsx.
4. QUÉ REHACER, con criterio escrito: sólo cuando el bloqueo es estructural Y el valor vive en un
protocolo o formato abierto. Pasan tres: el shell de apps sobre atuq, una bóveda de contraseñas
(la mitad cara —la integración con el navegador— ya está construida), y un cliente Matrix si
fractal no construye. Thunderbird es el más desaprovechado de la lista de EMPAQUETAR: es Gecko.
Deja escrito lo que el barrido midió y, sobre todo, CÓMO se midió, para que el recuento se pueda
repetir en vez de re-derivar.
Lo que aporta que no estaba en ningún lado:
1. **La política «catálogo ≠ imagen».** El repo ya la aplicaba de hecho y no la había escrito, y sin
ella «620 hojas sin perfil» se lee como deuda. No lo es: 529 son volcado crudo del importador y
355 son herramientas Go de nube. Una receta sellada y no declarada es CATÁLOGO — se instala por
nombre. Se vuelve deuda sólo cuando lo que falta es una CAPACIDAD.
2. **El método en tres preguntas encadenadas**, y por qué el orden importa: ¿está en algún perfil?
→ ¿podría llegar por clausura ajena (`dependientes_total>0`)? → ¿fue curada alguna vez (la
cabecera «PUNTO DE PARTIDA» del importador)? Las hojas son el hueco; las librerías no.
3. **El triaje completo de las 91 hojas trabajadas a mano**, en seis clases que suman 91 exactas.
Incluye las que NO se declaran de oficio y por qué: `openrc` compite con arje-zero, `uutils` y
coreutils/grep/findutils compiten con los applets de busybox, `waterfox` es un segundo Gecko,
y valkey/opensmtpd/step-ca/qdrant definen QUÉ CLASE de servidor es el perfil `servidor`.
4. **Dos hallazgos que no eran el objetivo del barrido:**
- `networkmanager` existe SÓLO en incoming-kde ⇒ GNOME, COSMIC y sway no lo alcanzan. Con
wpa_supplicant la WiFi se destraba, pero se configura a mano y el indicador de red del
escritorio no tiene con qué hablar.
- `ia-modelo-embeddings` no está en ningún perfil: hay motor (llama-cpp) y hay chat
(ia-modelo-chat) en los cuatro escritorios, y NO hay búsqueda por significado. La mitad
semántica del §6.3 de atuq no viaja.
5. **Una trampa del instrumento**, que casi hace escribir mal las listas de servicios:
`scripts/targets.py <perfil>` imprime las RAÍCES, no la clausura. Cruzar los [[service]] contra
esa salida dejaba fuera a pipewire y colord en GNOME, que sí están en la imagen. La clausura la
da el campo `perfiles` de los build-state*.json.
── BARRIDO 1: las hojas selladas sin perfil ────────────────────────────────────────────────────
Método, para que se pueda repetir: cruzar el campo `perfiles` de los cinco build-state*.json con
`dependientes_total`. De 905 nodos, 631 sin perfil; de ésos, **620 son HOJAS** (nadie depende de
ellas, o sea que ninguna clausura las puede alcanzar) y 11 llegan por la clausura de otra.
Las 620 se parten en dos por una marca objetiva, la cabecera del importador:
· **529** dicen «PUNTO DE PARTIDA, no final» — volcado crudo de import-nix/import-alpine
(328 Go + 195 Rust + 6 C). Nadie las curó nunca para una imagen.
· **91** están trabajadas a mano. Ésas son las que se triaron una por una.
Lo que salió de ese triaje, y lo que se declara:
**`base` += `wpa_supplicant` — la imagen NO PODÍA ASOCIARSE A UN WiFi.** dhcpcd resuelve la IP de
un cable; el handshake WPA2 de 4 vías lo hace un supplicant en userspace y busybox no lo trae. La
receta existía desde el frente de metal —su cabecera dice «para asociar el WiFi del medio live al
AP»— y estaba en CERO perfiles. Es la figura de `foot` y esta vez el precio era quedarse sin red.
**`cli` += nano, tig, patch, socat, pigz, miller, dwarves.** El criterio va escrito en el fichero
porque meter las 620 habría sido peor que no mirar: entra lo que un usuario de esta capa ESPERA
encontrar y hoy no está. El resto queda en el catálogo, que NO es lo mismo que la imagen.
**`escritorio-sway` += wlr-randr**: no había con qué cambiar la resolución ni rotar una pantalla.
── BARRIDO 2: el «enable» ──────────────────────────────────────────────────────────────────────
De los cuatro escritorios **sólo GNOME tenía lista `servicios`**. Los otros tres salían con sus
demonios instalados y NINGUNO arrancado, y la métrica de clausura daba N/N igual — `paquetes` dice
qué se instala, `servicios` dice qué se levanta, y sin la segunda la imagen está a medias sin que
nada falle.
Escritas las tres que faltaban, computando la lista (no a ojo): se cruzaron los `[[service]]` de
las 11 recetas que los declaran contra el campo `perfiles` de los cinco grafos — la CLAUSURA, no
las raíces, porque la mitad de los demonios llegan como dep. Y GNOME sumó `cupsd`+`bluetoothd`,
que nacieron ayer.
⚠ El cruce destapó tres ausencias que NO se arreglan acá y quedan escritas en el fichero:
· KDE y sway tienen `pipewire` y **no tienen `wireplumber`**: sin gestor de sesión los nodos
existen y nadie los conecta.
· COSMIC **no tiene `upowerd`** y dibuja un indicador de batería.
· KDE y sway no tienen `logind-compat`.
Comprobado: tomllib parsea, y los ocho perfiles resuelven con scripts/targets.py.
El grafo regenerado por el cron delató lo que faltaba: `mold` y `cliphist` salían con
`perfiles: []`. O sea que en el mismo día en que se midió que 649 recetas selladas no están en
ninguna imagen, dos de las recetas nuevas se iban a sumar a la lista. La métrica lo vio porque
existe; la intención no alcanza.
- `cli` += `mold` y `sccache`. Las dos son herramientas de quien CONSTRUYE con la distro, no de
quien la usa. `sccache` estaba sellada hace meses y sin perfil; `mold` es de hoy.
- `escritorio-sway` y `escritorio-cosmic` += `cliphist`. SÓLO ahí: habla `wlr-data-control`, que
KWin y mutter no implementan — mismo muro que `wf-recorder`. Declararlo en KDE o GNOME daría un
binario que arranca y no hace nada, que es peor que no tenerlo.
Comprobado con scripts/targets.py: cli resuelve mold+sccache, y sway/cosmic los tres.
mold b3:9ce9fe3194982cc2b28d0fa547b037d7d36dae773fbd033ad252d7f11cb92dd2
wireguard-tools b3:086d0837e30e8a7a5e05c61c2df47987c2574645c03226e0020f35635ba8ba41
Los dos sellaban, reproducían y NO CORRÍAN. Se descubrió ejecutando el binario; ni el sellado, ni
`takana hash`, ni la reproducibilidad lo veían.
mold — dos causas independientes, las dos leídas en su CMakeLists.txt:
1. `:149` — `if(ZLIB_FOUND AND NOT MOLD_MOSTLY_STATIC)` enlaza la zlib COMPARTIDA del sistema
(ídem zstd `:179` y blake3 `:161`). El binario salía pidiendo `libz.so.1`/`libzstd.so.1`, que
el corpus no publica (`zlib` canónica es `.a`), y al correrlo tomaba la del anfitrión:
`Error relocating /lib/libz.so.1: __snprintf_chk: symbol not found` — símbolo de
_FORTIFY_SOURCE de glibc que musl no tiene. `-DMOLD_MOSTLY_STATIC=ON` usa las que trae en tree.
2. El `LDFLAGS=-static` que el lab exporta para `link = "static"` NO llegaba a la línea de enlace
de CMake ⇒ hace falta `-DCMAKE_EXE_LINKER_FLAGS=-static` explícito.
Ahora: `statically linked`, y `mold --version` contesta 2.42.1.
Y antes de eso, otro defecto que tampoco fallaba: el primer sello pesaba 629 MB, de los cuales
628 eran UN SOLO FICHERO — /usr/bin/mold con el DWARF adentro, que CMAKE_BUILD_TYPE=Release no
quita. Con `strip_debug = true` (SDD 23) quedó en 41 MB. Un artefacto obeso no rompe nada: entra
en el store, en la imagen y en el respaldo, y nadie mira el tamaño.
wireguard-tools — acá el error era del razonamiento, y el artefacto lo desmintió. La receta iba
`link = "dynamic"` con [deps] build=["libmnl"] argumentando «wg enlaza libmnl y no hay libmnl.a en
el store». `readelf -d wg` da UN solo NEEDED, `libc.so`, y los `mnl_*` están definidos adentro.
La razón está en la fuente, `src/netlink.h:1`:
/* This is a minimized version of libmnl meant to be #include'd */
Upstream embebe el subconjunto que usa para no arrastrar la dependencia. Pasa a `link = "static"`
y pierde la dep: declarar una que no se usa ata esta receta al hash de otra y haría que un rebuild
de libmnl la re-selle para nada.
REGLA, escrita en las dos recetas: `link = "static"` es una DECLARACIÓN. Lo único que la comprueba
es `readelf -d` sobre el artefacto, o correrlo.
Se descubrió CORRIENDO los binarios, no auditando las recetas, y el hash NO se mueve (las deps de
runtime no entran en hash_inputs): los artefactos sellados siguen valiendo.
cmus/cmus NEEDED libncursesw.so.6 ⇒ runtime = ["ncurses-shared"]
bluez/bluetoothctl NEEDED libreadline.so.8 ⇒ runtime = ["readline-shared"]
Las canónicas `ncurses` y `readline` del corpus son `--without-shared`/sólo `.a`, así que sin
declarar la variante `-shared` esos `.so` no viajan en la imagen y el binario muere en el loader.
Es la MISMA fuga que el perfil de sway ya pagó con zlib/expat/libffi, y no la ve ningún auditor de
recetas: sólo `readelf -d` sobre el artefacto, o ejecutarlo.
El reparto de bluez era el peor posible: `bluetoothd` sale con `libc.so` a secas y habría
arrancado, y la herramienta con la que se emparejan los dispositivos habría sido la que no.
Comprobado con scripts/provee.py: los dos sonames SÍ los publica el corpus
(corpus/ncurses-shared, corpus/readline-shared) ⇒ no hay NEEDED colgante, faltaba la declaración.
MEDIDO al ir a declarar las recetas nuevas, y es más grande que ellas: de las 892 recetas selladas
del corpus, **649 no están en NINGÚN perfil**. El 73% del catálogo está compilado y no viaja en
ninguna imagen. No es deuda de build (drenaje.json dice 0) y la métrica de perfil NO lo puede ver:
mide la clausura de lo DECLARADO. Es la lección de foot a escala de catálogo.
Concreto: yazi, atuin, zellij, jujutsu, just, direnv, watchexec, mise, xh, age, rclone, restic,
lazygit, difftastic, bottom y duf estaban selladas hace meses y en cero imágenes; y `helix` estaba
declarada sólo en escritorio-gnome, que es donde menos sentido tiene.
perfil.cli += esas 16 + las nuevas de hoy (btop, ncdu, nushell, aerc, cmus, syncthing).
Cero recetas nuevas y cero builds: sólo dejan de ser invisibles. Como los CUATRO escritorios y
`servidor` heredan `cli`, una sola edición los alcanza a todos.
perfil.servidor += wireguard-tools: el kernel ya trae WireGuard (SDD 22) y no había con qué
configurarlo — misma figura que el cortafuegos y el reloj que este perfil ya declaraba.
Los cuatro escritorios += cups, bluez (punto 8 de PUBLICABLE, docs/20). Viven en el CORPUS y no en
las colas por lo mismo que mpv y atuq: se alcanzan sibling-first→padre, una copia para los cuatro.
⚠ DEUDA DECLARADA, NO OLVIDO: las dos recetas traen su [[service]] y los perfiles NO los arrancan
— de los cuatro escritorios sólo escritorio-gnome tiene lista `servicios`. Los otros tres no
arrancan NADA, y eso es más grande que cups y bluez. Va escrito en el fichero para que se vea.
El barrido de las 649 queda pendiente y es su propia unidad de trabajo: 363 son recetas Go
importadas en tanda (herramientas de desarrollo) y NO todas deben entrar en una imagen.
Comprobado con scripts/targets.py: los ocho perfiles resuelven, sin nombres desconocidos.
bluez b3:21bee949122f770a319b806b83d23fa2bddc958252f6de7e27fffa5a75a3315a (28 MB, 31 ficheros)
Con esto queda cerrado el punto 8 de PUBLICABLE (docs/20): «cups y bluez».
El fallo del primer intento vale más que la receta: con [deps] build=["glib", ...] el configure
corta con «Package 'libpcre2-8', required by 'glib-2.0', not found». No falta una dep de bluez:
faltan las TRANSITIVAS del .pc de glib. Un enlace estático resuelve la cadena entera en el punto de
enlace, así que pcre2/libffi/zlib hay que materializarlas aunque bluez no las nombre — es el mismo
juego de cuatro que ya declaran cairo, appstream y adwaita-hello, y que nadie había escrito por qué.
Perillas leídas del «configure --help» del tarball pineado:
- --disable-obex: obexd pide libical, y libical existe SÓLO en incoming-gnome. Una receta del
corpus resuelve sibling-first y después el catálogo PADRE, nunca una cola hermana ⇒ desde acá no
se alcanza. Encenderlo exige promover libical, que es otra unidad de trabajo.
- --disable-manpages: las genera rst2man (docutils); no es receta.
- --disable-cups: el backend de impresión por Bluetooth. cups YA es receta desde hoy, o sea que
esto se puede encender — queda anotado como lo próximo, no como olvido.
Declara [[service]] bluetoothd; el exec VERIFICADO contra el artefacto: /usr/lib/bluetooth/bluetoothd.
Y va escrito que esto no enciende el bluetooth de nadie: falta dbus vivo y que el perfil lo habilite.
nushell b3:742c008b462930c6ee9c2af0646fe842e9e1bad7fe35277eddabdb15aa4fdae6 (41m17s de cargo)
cmus b3:cae8d90b5c1c7e5864c708371b2bdefbbccaa3ef1248f165a20f2dc2ce481b8b
- nushell entra como shell INTERACTIVA y va escrito en la receta que no es ni va a ser /bin/sh: su
lenguaje no es POSIX y un script del sistema no corre ahí. El catálogo tenía tres shells (bash,
busybox/ash, elvish) y ninguna era una alternativa moderna. compiler="gcc" por el patrón
zellij/gitui/delta: hay crates *-sys con build-script en C y cc-rs invoca gcc; el linker sigue
siendo zig-cc.
- cmus cierra el hueco de audio: el catálogo había cerrado el vídeo con mpv y no tenía con qué
escuchar música. Su configure NO es autotools — toma asignaciones clave=valor, no --flags — y
eso importa: pasarle --prefix=/usr no da error, lo IGNORA, y el paquete acabaría en /usr/local
sin que nada falle. CONFIG_PULSE=n a propósito: la salida ALSA desemboca en PipeWire por
-Dpipewire-alsa, igual que hace mpv, y la libpulse del corpus es la variante con la glib
ESTÁTICA adentro, que no se quiere en un cliente.
cups b3:528b38b20d3cecf7e06fbc51f271603e3c71fbec6ef8047c86ad421edb7a1a45
No es una app más: es uno de los NUEVE pendientes de PUBLICABLE (docs/20, punto 8). Una distro de
escritorio que no imprime no es una distro de escritorio, y ese hueco no se ve contando recetas
— es de CAPACIDAD, la familia de foot y las fuentes.
El muro real fue el PIE, y el diagnóstico vale más que el arreglo: el configure detecta que el
compilador acepta -fPIE y mete «-fPIE -pie» en ALL_LDFLAGS *y sólo ahí* — los objetos de
libcups.a se compilan sin PIC — así que lld corta con «relocation R_X86_64_64 cannot be used
against local symbol». No se puede apagar con una flag: @PIEFLAGS@ se sustituye DENTRO de
Makedefs, no es una variable de make que «make PIEFLAGS=» pueda pisar. Y un ejecutable PIE con
-static exigiría -static-pie, que no es lo que esta distro construye ⇒ el sed que lo quita es lo
correcto acá, no un atajo.
Todas las perillas salen del «configure --help» del TARBALL PINEADO, no de un APKBUILD ajeno.
--with-dnssd=no y --disable-acl se apagan POR DECLARACIÓN aunque no haya avahi ni acl en el
catálogo: si el configure los busca y los encuentra en el sysroot del lab, el lab NO entra en
hash_inputs y el artefacto deja de reproducir sin que nadie toque la receta.
⚠ Queda escrito en la receta un segundo tramo que ESTA receta no paga: gtk3 del corpus está
sellada con -Dprint_backends=file, así que una app GTK3 seguirá viendo un solo destino hasta que
gtk3 se re-selle. La pieza está, el sistema la ignora, y ninguna métrica lo dice.
Declara [[service]] cupsd (SDD 30). Declararlo NO lo enciende: falta que el perfil lo habilite.
btop b3:d1e3d3111d306e158f2269ba3ecb4852d7d095f688e707db54381dbb3b26e34f
Dos cosas que valían el viaje:
- `configure = 'true'` NO es relleno. btop 1.4.x trae CMakeLists.txt *y* Makefile; el autodetector
del lab ve el primero y generaba `cmake -S . -B _build`, que muere con `cmake: not found`
(cmake es una receta del corpus, no una herramienta del lab). Apagar la fase de configure es lo
que elige la vía soportada por upstream.
- C++20 (<ranges>, <format>) compila y ENLAZA ESTÁTICO con la libc++ de zig, sin gueto gcc. Se
puede porque btop no enlaza ninguna librería C++ del corpus: todo su C++ es suyo. El binario sale
con -D_LIBCPP_HARDENING_MODE, o sea libc++ de verdad y no libstdc++.
aerc b3:a33dbafdcc302154888095b7b5b14278461e60a69c4c8f310e388ba85f544d0a (43 ficheros)
syncthing b3:544ebf125c682c720b8555faf4259cd48bee5b4a302c4a4cc29a7311691a6e34
Las dos necesitaron salirse del driver Go genérico, y por motivos distintos:
- aerc se construye con SU GNUmakefile. El driver hace `go install` y deja el binario en
/out/usr/bin; eso habría dado un paquete que ARRANCA Y NO SIRVE — aerc busca plantillas,
stylesets y filtros en /usr/share/aerc, y dos filtros (colorize, wrap) son fuente C que hay que
compilar. Con el Makefile el artefacto trae los 43 ficheros. VERSION y GOFLAGS van fijados a
mano: el Makefile los saca de `git describe` y de contrib/goflags.sh, y el árbol llega al
sandbox SIN .git ⇒ el binario dependería de si hay un .git al lado.
- syncthing falla con `go install` a secas y el error no dice por qué:
`lib/api/api_statics.go:45:25: undefined: auto.Assets`. La GUI web se sirve desde un fichero Go
GENERADO que empaqueta gui/ y el repo no lo versiona: lo produce `go run build.go assets`. No es
una dependencia que falte, es un CODEGEN. Todo local, sin red.
commit = el tag PELADO en los dos casos (ambos son tags anotados; la trampa de foot).
Primera tanda del barrido de huecos del catálogo moderno. Las tres SELLAN en el hub:
ncdu b3:455589610a4d0378299a2a428f85d77c4d1998f71dbdb68bb704bd64a4ef40c5
wireguard-tools b3:9283f272e7833d55ec50ac854c98a5abdee81ad5ca01ce90de4ce6f241744bac
cliphist b3:86597f25894ab7fce9df1af6e7a6942de5e677546fcc40d91fc087778c059343
- ncdu 1.22 y NO la 2.x: la 2 esta reescrita en Zig y exige una version concreta del compilador;
84 recetas ya pinean zig_version=0.13.0 y el zig del lab es rodante. La 1.22 es la ultima de la
rama C y hace lo mismo.
- wireguard-tools va link="dynamic" a proposito: libmnl canonica es --disable-static, o sea que en
el store NO hay libmnl.a. Forzar static no daria un estatico sino un enlace contra el .a del
sysroot Alpine DEL LAB — la misma fuga que documenta el perfil de sway con zlib/expat/libffi.
- cliphist habla wlr-data-control: sirve en sway y COSMIC, NO en KDE ni GNOME (mismo muro que
wf-recorder). Declararlo donde no funciona seria peor que no tenerlo.
Las tres licencias VERIFICADAS contra el LICENSE/COPYING del arbol, no adivinadas.
Ninguna esta declarada todavia en targets.toml: sellado != instalado (la leccion de foot).
Lo encontró la instalación del modelo opcional del §6.3: `takana install --prefix` murió con
`Invalid cross-device link (os error 18)` justo en el caso que su propia ayuda promete por escrito
(«útil para tests offline o staging en otro filesystem»).
⚠ Y no hace falta tener dos discos para pegarse con esto: `linkat` da EXDEV **entre dos MOUNTS del
mismo dispositivo**, y el store de esta máquina es un bind-mount del volumen. O sea que el caso es
normal, no exótico.
Ahora, si el hardlink da EXDEV —y sólo si da EXDEV; cualquier otro error sigue siendo un error— se
copia. El precio es el espacio: el fichero deja de compartir inode con el store. Está aceptado,
porque la alternativa es no poder instalar. El encabezado del módulo, que prometía «cero copia», dice
ahora la excepción.
Test con el caso real (tmpfs de /dev/shm contra el temporal, que son mounts distintos) y comprobado
en los dos sentidos: quitando la excepción de EXDEV, el test cae con el mismo error que reportaba el
instalador. Si en alguna máquina esos dos caminos fueran el mismo mount, el test lo DICE en vez de
saltarse en silencio.
De paso: `swm_bridge.rs` tenía un constructor de test sin el campo `strip_debug` que agregó 9bc51889,
y eso dejaba al crate ENTERO sin compilar sus tests. Completado.
Decisión del usuario. El motor (199 M) y el modelo de chat (1,04 GiB) van en las cuatro imágenes
porque la barra lateral está en las cuatro; el de embeddings (610 MiB) no, porque es para una función
—preguntarle al archivo por significado— que no todo el mundo usa.
Y «opcional» no significa «lo armás vos»: el modelo ya es receta del corpus, así que la maquinaria de
la Etapa F lo vuelve paquete sin trabajo extra. Publicado en `dist/repo` con su `expected_hash`
anclado, junto con las deps que `install` necesita para reproducirlo — `llama-cpp` faltaba en el
catálogo, y sin ella el paquete no se puede instalar aunque exista. De paso entraron nftables, libmnl
y libnftnl, que tampoco estaban.
⚠ Tres de esos paquetes se habían publicado con el ancla de sanidad por default (`/usr/bin/<nombre>`),
que en una librería o en un modelo NO EXISTE: `install` habría fallado al verificar. Re-empaquetadas
con su ancla de verdad (`/usr/bin/llama-server`, `/usr/sbin/nft`, `/usr/lib/libmnl.so.0`…).
Lo que hace que esto sea opcional de verdad es una línea: la receta NO está declarada en ningún
perfil. Está escrito en el SDD para que nadie lo «arregle» agregándola.
Y la mitad que separa «opcional» de «invisible»: con el modelo ausente el host ya no dice sólo «no
está» sino cómo conseguirlo — «es una descarga opcional — instalalo con `takana install
ia-modelo-embeddings`». Medido en el control negativo del guardián.
El barrido con `verificar-repro.sh` sobre las 15 recetas CMake baratas del corpus dio
**5 que no construyen hoy**. Éstas son las dos cuyo arreglo no cuesta nada (radio 0 y 3 rebuilds).
Diagnosticada `json-c` en vez de suponerla: apartando su artefacto y reconstruyendo con la salida
capturada, muere con el mismo `Error running link command: Segmentation fault` que `dwarves` y que
los `protoc-gen-upb*` de protobuf. Un `-DCMAKE_LINK_DEPENDS_USE_LINKER=OFF` en cada una y listo — el
`lld` de zig 0.16.0 segfaultea con el `--dependency-file` que CMake ≥3.27 emite.
⚠ **Lo que importa de este commit no son las dos recetas: es que el fallo no era un caso aislado.**
Cuando apareció en protobuf lo esquivé con `compiler = "gcc"` creyendo que era cosa de esos plugins.
Ya van CINCO proyectos sin relación entre sí con el mismo crash, y otros tres medidos y pendientes.
`crun` cayó a deuda (json-c es dep suya) y se reconstruyó: sigue siendo estático con 0 NEEDED y
**vuelve a arrancar un contenedor de verdad** con el busybox del corpus como rootfs. Las tres
REPRODUCEN bit a bit.
Queda planteado con el número delante, sin arrancarlo: los otros tres del censo son
`libtiff-shared` (12 rebuilds), `libjpeg-turbo` (17) y `libjpeg-turbo-shared` (23) — ~52 en total y
tocan los stacks gráficos de los escritorios. Y el censo cubrió 15 de las 23 CMake del corpus y
NINGUNA de las ~130 de las colas: el número real de rotas es mayor que 5.
Verificado en OVMF con el kernel linux-metal-kexec recién construido
(b3:965acc37…, CONFIG_KEXEC_FILE=y confirmado en el .config sellado):
BdsDxe: starting Boot0002 "UEFI Misc Device" ← el firmware, UNA vez
KEXEC-PRUEBA: kernel = 6.16.12
✓ kernel cargado en memoria
⚡ saltando — el userspace muere ACÁ. Esto no vuelve.
KEXEC-PRUEBA: kernel = 7.1.2
KEXEC-PRUEBA-LLEGADA: este kernel NO lo arrancó el firmware
La prueba no es que aparezca el 7.1.2: es que "BdsDxe: starting" aparece
UNA SOLA VEZ. Dos kernels distintos corrieron y el firmware sólo
intervino en el primero. Y el 7.1.2 vive dentro del initramfs, sin
entrada de arranque, así que el firmware no podría haberlo lanzado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
`no construyó` no es ruido del verificador: es EL hallazgo. Un `sealed` en el grafo dice que alguien
construyó eso alguna vez con algún lab, no que se construya hoy — y el lab rueda desde Alpine edge.
Este script es lo único que convierte esa sospecha en un número SIN RIESGO, porque aparta en vez de
borrar y restaura si el build falla.
Barriendo las 15 recetas CMake baratas del corpus: REPRODUCEN 9 · DERIVA 1 · no construyeron 5.
Cinco selladas y rotas a la vez, todas por el mismo crash del lld de zig con `--dependency-file`.
Y la nota de método que hace legible el censo: barrer por FAMILIA de sistema de build. Si el fallo es
del toolchain se concentra en una familia y el patrón salta; barrer al azar lo diluye.
Contesta la pregunta que ni el grafo ni `vigia-sonames` contestan: no «¿está
sellado?» ni «¿arranca el binario?» sino **«¿hay alguien que lo LEVANTE?»**. Un
demonio puede estar sellado, con contenido, con todos sus sonames resueltos, y
que ninguna imagen lo arranque nunca.
Va al latido por la lección que `vigia-sonames` ya dejó escrita doce líneas más
arriba —un vigía que hay que acordarse de invocar no se distingue de no tenerlo—
y con más motivo: su entrada son DOS ficheros que cambian por separado (las
recetas y `targets.toml`), así que la divergencia entre «declarado» y
«habilitado» aparece sola, sin que nadie toque el vigía.
Corre PRIMERO su propio `--selftest`: si el guardián está roto, su silencio no
es una respuesta.
Primera lectura, ya en el repo: 0 errores y 18 avisos. El más ruidoso es
`dbus-system`, que viaja en SEIS perfiles y sólo GNOME lo arranca.
Lo que el script de sesión lanza con `&` ahora está declarado en las recetas y
habilitado en el perfil. Ninguna receta movió su hash: 9/9 idénticos a los que
los grafos ya registraban.
EL HALLAZGO, y no lo buscaba: la comprobación inversa del resolutor rechazó
`arje-logind-compat` y `arje-polkit-compat` porque están en CERO perfiles — y
sin embargo qemu-desktop-image.sh los copia al rootfs a mano y el de COSMIC hace
`exit 1` si falta logind-compat. Dos binarios imprescindibles, presentes en la
imagen y ausentes del destino declarado: la misma forma del agujero de `foot`,
encontrada por una comprobación en vez de por una imagen inusable. Son raíces de
escritorio-gnome (los dos) y de escritorio-cosmic (sólo logind, verificado que
sus scripts no nombran polkit).
DOS COSAS QUE NO SON TRANSCRIPCIÓN:
- `dbus-daemon --fork` no se traduce tal cual: arje supervisa al HIJO DIRECTO y
Type=forking no existe, así que un daemon que forkea y sale deja a arje viendo
morir al padre con éxito y reencarnándolo para siempre. La card usa --nofork.
- `scope = system|session` decide DÓNDE va la card. Las de sesión necesitan
XDG_RUNTIME_DIR y usuario logueado; en el genesis arrancarían antes de que
exista ninguno. Y fuera de mirada NADIE entrega cards de sesión todavía, así
que salen con AVISO: el hueco queda contado, no omitido.
Correcciones propias: la unicidad del label es DENTRO del perfil, no del corpus
(upower vive legítimamente en dos colas); la membresía se lee de los CINCO
grafos, no sólo el del corpus; una RAÍZ manda sobre el grafo, que es derivado y
lo regenera el cron; y la flag nace en inglés (`--services`) como manda la
regla 4, aunque `--lista` sea deuda vieja del mismo fichero.
`--selftest`: 7 casos, el primero es el CONTROL que tiene que pasar en verde.
Buscando cómo escribir la receta de `tawasuyu` apareció que su ÚNICO remoto es el gitea de gioser
(`ssh://gitea@git.tawasuyu.net:2345/…`, y ese nombre resuelve a 204.168.193.248 = gioser). Al mirar
el resto: **28 repositorios con remoto en esta máquina, 26 SIN NINGUNA copia fuera**. Sólo `takana` y
`llimphi-standalone` tienen espejo externo.
Apagar el origen no borra unos servicios: borra EL CÓDIGO CON EL QUE SE VOLVERÍAN A CONSTRUIR, y los
clones de trabajo están en el mismo disco que también muere. Entre los 26 está `tawasuyu`, que
produce 10 de los 13 binarios que nadie más provee — la dependencia circular completa.
No se ve desde ninguna otra parte del censo: un repo no es un proceso, ni un puerto, ni un dominio.
Se descubre cuando ya no hay de dónde sacarlo.
· `censar.py` lo mira ahora (`[[repo]]`), y reporta NO «tiene remoto» sino si alguno de sus remotos
NO es esta máquina: un remoto que apunta afuera es la prueba de que el código sobrevive. La lista
de «esta máquina» sale del censo mismo (sus IPs + los dominios que sirve), no de nombres cableados.
· `planear.py` lo emite como PASO 1, por delante del rescate de binarios: aquello pierde un servicio,
esto pierde la posibilidad de reconstruirlo. El arreglo ya está escrito en el repo —takana usa
`pushurl` doble por `scripts/espejo-setup.sh`.
Y una corrección de algo que dije antes: la perilla por receta que vi en `recipe.rs` es `strip_debug`,
no una versión de rust. NO existe `rust_version`; sólo `zig_version`. Pinear rustc por receta para
honrar el `rust-toolchain.toml` de tawasuyu (1.96.0, contra 1.97.0 del lab) sería código nuevo en
takana-core, no una opción ya disponible.
De paso, medido el subárbol de los diez binarios por separado: cuatro (`willay-daemon`,
`sandokan-seguridad-core`, `pacha-secretos`, `tupu-cli`, entre 84 y 153 deps) NO arrastran
criptografía en C; cuatro traen `ring` y dos `aws-lc-sys`. O sea que hay un escalón por donde
empezar sin pelear con cmake.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
`paquetes` decía qué se INSTALA; no había dónde decir qué se LEVANTA. El sshd
del producto arrancaba porque su Card estaba escrita a mano en una constante de
Rust, no porque nadie lo hubiera declarado.
`servicios = [...]` por perfil (se hereda como `paquetes`, mismo orden y dedup)
+ `targets.py --servicios <perfil>`, que cruza label → receta → exec contra los
`[[service]]` del corpus y la membresía de perfil de build-state.json.
Lo que vale más es la comprobación INVERSA: avisa de los paquetes que están en
la imagen, TRAEN un demonio y el perfil no arranca. Es la versión servicios de
la lección de `foot` —la métrica mide la clausura de lo DECLARADO y no ve lo que
falta en la declaración— y no es hipotética: antes de escribir `servicios =
["sshd"]` el resolutor ya avisaba «openssh está en la imagen y TRAE este
servicio, pero el perfil no lo arranca».
Probado con roturas A PROPÓSITO, 5/5, y con un control que TIENE que pasar:
habilitado sin declarar (ERROR) · dos recetas con el mismo label (ERROR) · lo
declara una receta que no está en el perfil (ERROR: el card apuntaría a un
binario ausente y arje lo encarnaría con ENOENT en cada backoff) · la inversa
(AVISO, no rompe el cron) · el caso bueno (rc=0). Los 8 perfiles siguen
expandiendo igual y los llamadores de shell no cambian.
De los 13 servicios cuyo binario NADIE provee —los que se pierden al apagar gioser— DIEZ salen del
mismo repositorio (`tawasuyu`, hoy `bad13117c`): matilda, pacha (paquete `pacha-cli`),
pacha-secretos, sandokan-watch (`sandokan-seguridad-core`), shuma-daemon, shuma-gateway, tejido, tupu
(`tupu-cli`), willay-crosscheck (`willay-cruce`) y willay-daemon. Los otros tres son ajenos
(act_runner, adb, y el `puerta-…` de target/debug). Una receta cubre diez servicios.
**El tamaño real es una quinta parte del aparente.** El Cargo.lock del workspace tiene 2823 crates,
pero eso incluye su stack gráfico, audio y Android; el subárbol que esos diez binarios necesitan son
524. Y sólo CINCO traen C: `aws-lc-sys` y `ring` (criptografía, compilan C/asm y piden cmake) más
`dirs-sys`, `inotify-sys` y `netlink-sys`, que son bindings puros sin librería externa.
⚠ **Lo que hay que decidir antes de escribirla.** `tawasuyu/rust-toolchain.toml` fija 1.96.0 y lo
argumenta en el propio fichero: una distro que promete builds deterministas y atestación firmada no
puede tener el compilador flotando. El lab de takana trae 1.97.0 y es RODANTE por diseño —
`lab-toolchain.lock` detecta la deriva, no la evita. Una receta takana compilaría con un rustc
distinto del que tawasuyu exige: el binario NO sería el mismo que corre, y rompería el contrato de
atestación del otro frente. Tres salidas, y es decisión, no trámite: pinear 1.96.0 para esta receta
(como las 84 que pinean zig 0.13.0), mover el pin de tawasuyu, o llevar los binarios tal cual
sabiendo que no reproducen.
Y no se construye en el origen: 4 núcleos y ~2 G disponibles. Eso va al worker dev.gioser.net.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
La barrida de regresión del frente (14 guardianes tras rehacer host y navegador) encontró uno en
rojo: `test-atuq-descargas.py` buscaba el CAS en `<estado>/descargas-cas` y el host lo escribe en
`<estado>/cas` desde el commit del archivo personal (357791a85, 2026-09-10) — el §6.3 UNIFICÓ los dos
CAS, que es justo lo que hace que una página archivada y un fichero bajado con el mismo contenido
sean un solo objeto. Lo renombré yo y no actualicé este guardián.
Lo que importa no es el renombre: es que el guardián estuvo rojo dos días sin que nadie se enterara,
porque **un guardián que no se ejecuta no protege de nada** — la misma familia que el cache-hit que
congela regresiones. Los otros 13 pasan.
Arreglado el path, y el README dice ahora por qué el directorio se llama `cas` y no `descargas-cas`.
Ayer estas dos huyeron a `compiler = "gcc"` para esquivar un crash del linker en los tres
`protoc-gen-upb*`. **Esquivé el fallo sin conocer su causa, y salió caro:** al mover sólo protobuf, el
link murió con `undefined reference to std::__1::basic_string<…>` —el `__1` de libc++— porque abseil
seguía en zig, así que hubo que arrastrar las dos fuera del toolchain por defecto.
La causa apareció al día siguiente en `dwarves`, bisecando la línea de enlace real:
tal cual → exit 139 (SIGSEGV)
quitando `-static` → exit 139 ⇒ no es el enlace estático
quitando `--dependency-file` → **exit 0** ⇒ ES ESO
El `lld` de zig 0.16.0 segfaultea con el `--dependency-file` que CMake ≥3.27 mete en la línea de
enlace. Con `-DCMAKE_LINK_DEPENDS_USE_LINKER=OFF` **los cuatro binarios de protobuf enlazan con zig**,
incluidos los tres plugins que se caían.
Esto es, además, la validación de la causa raíz en un proyecto INDEPENDIENTE de dwarves: dos
proyectos sin relación, mismo síntoma, misma perilla, los dos arreglados.
Con el escape se va también el `-static-libstdc++`, que sólo hacía falta porque g++ enlaza contra la
libstdc++ de GNU. Comprobado sobre el artefacto: el único NEEDED de `protoc` es `libc.so`, que provee
`musl-shared`.
Verificado de punta a punta, no por el código de salida: `protoc --version` → `libprotoc 36.1`, y un
`.proto` compila a `m.pb.h`/`m.pb.cc` **y** a `p/m.pb.go` pasando por `protoc-gen-go` — o sea que la
cadena plugin↔driver que abrí anteayer sigue entera. Las dos REPRODUCEN bit a bit.
Se deja escrito el camino completo en las recetas, diagnóstico corto incluido, porque la lección no
es la perilla: es que **un escape que funciona sin explicar el fallo se paga después**, y acá se pagó
con dos recetas fuera del toolchain por defecto y un choque de runtimes de C++ que sólo apareció
porque el escape era parcial.
El diseño completo del hueco: qué declara el PAQUETE (`[[service]]`, hecho) y
qué decide el PERFIL (si arranca, pendiente), que es la misma partición que
systemd hace entre [Service] e [Install] y que `arje-absorb` respeta al absorber
sólo lo habilitado.
Deja escritas las dos cosas que cuestan caro si se descubren después:
1. El SDD 06 decía «el init (arje) lee ese árbol» de /etc/hammer/init.d/*.rule.
Es FALSO: el único uso de INIT_RULES_DIR en el repo es escribirlo. Había TRES
convenciones de dónde vive un servicio y ninguna se tocaba con las otras
(más una cuarta en `query service:`). Canónicos son los de arje —genesis de
la seed y cards.d/—; la mutación del .swm queda derogada o reapuntada, y la
afirmación falsa queda marcada en su propio doc.
2. La trampa para el emisor: el sidecar .hammer/recipe.toml NO entra al
ArtifactHash, así que el openssh ya sellado no lleva el bloque y, con el hash
sin mover, NUNCA se reconstruye solo. Derivar la seed del sidecar hoy daría
un producto SIN sshd, en silencio. Por eso product_seed_card() sigue usando
la constante a propósito, y re-sellar es una unidad aparte CON control de
reproducibilidad — openssh no está certificado como reproducible.
Un paquete con servicio no tenía dónde decirlo: los dos del producto (hammerd,
sshd) vivían en constantes de Rust y los ocho de una sesión GNOME se lanzaban
con `&` desde un script, sin supervisión ni backoff ni el CRASHED real — o sea
sin nada de lo que arje es PID 1 para dar.
`[[service]]` en la receta, fuera de `hash_inputs` como license/slots/evidence:
declarar el servicio de openssh NO movió su hash, medido con el binario viejo
(que ignora el bloque) contra el nuevo, b3:938835e6… en los dos.
La prueba que autoriza el cambio no es que "parezca bien": el card generado
desde recipes/openssh.toml se compara ENTERO contra SSHD_SERVICE_CARD, que es
el que ya bootea en QEMU. Empatan ⇒ mover el servicio a la receta no cambiaría
un byte de la seed ni del product_rootfs_hash.
Por qué no alcanzaba `arje-absorb` (que existe y traduce systemd/openrc/runit/
dinit/sysvinit): sólo absorbe lo HABILITADO, los symlinks de <target>.wants/.
Un rootfs nuestro no tiene ese estado — medido: 62 .service sellados en el
store y CERO directorios .wants. Absorber devuelve vacío, y es la respuesta
correcta a la pregunta que absorb contesta. El enable de una distro construida
desde fuente no se lee del árbol: se declara.
Un demonio de syslog no se muda a un sistema que ya tiene journal propio: la semilla de arje declara
`provides: ["Spawn", "Journal"]` (crates/takana-bootstrap/src/lib.rs:185) y existe el crate
`takana-journal`. Mismo razonamiento que dbus/udev/elogind, que ya estaban en NO_APLICA: no es
gusto, es que el destino provee la función.
Vale para metalog, syslogd, syslog-ng, rsyslogd, socklog y busybox-syslogd.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
Medido hoy: `ia-modelo-embeddings` dio REPRODUCE y `ia-modelo-chat` «no construyó», y la ÚNICA
diferencia entre las dos era que el tar de la primera seguía en `work/tarballs` y el de la segunda lo
había borrado yo liberando disco. Sin caché, el build sale a buscar la fuente a una URL `.invalid`
—que es lo que el ADR 0013 pone a propósito cuando el objeto vive sólo en nuestro mirror— y muere con
`Could not resolve host`.
O sea que el gate informaba «no construyó» para `firefox-pgo-profile` y los dos modelos de IA según
qué hubiera en el caché local. Un guardián cuyo veredicto depende de eso no es un guardián.
Ahora hace `source` de `scripts/fuentes/mirror-env.sh` si existe. Aditivo, como manda el ADR 0013: si
el fichero no está, todo sigue igual que antes.
Con eso, ia-modelo-chat: REPRODUCE.
Los cuatro guardianes pasan sobre los artefactos sellados (atuq bfc14c92, puriy-costura 3f31233e,
ia-modelo-embeddings 2c0c4258):
semántico 0.6252 gato / 0.2471 red / 0.0973 pan → y la otra pregunta gana la otra página
control «no hay modelo de embeddings en …» y NINGÚN orden inventado
ia el modelo contesta y el motor se va con el navegador (0 vivos)
foco los tres estados, y el estado intacto tras la sesión
La barra lateral ahora tiene dos botones: «Al modelo» (genera texto) y «A mis páginas» (ordena lo que
ya leíste). No se mezclan a propósito — una inventa y la otra recuerda, y juntas sería imposible
saber cuál contestó. Los resultados van EN ORDEN y sin porcentaje: el puntaje es un coseno y leerlo
como «85 % de acierto» sería inventarle un significado.
⚠ Y el guardián nació midiendo NADA: metía las tres páginas en `<iframe>` y archivó cero, porque el
§6.3 ignora lo que no es marco principal. Encadenadas como navegación de verdad entran las tres; y
las páginas de tránsito van sin texto visible para que el archivo las descarte y la evidencia no
liste coincidencias sin título.
Dos cosas más del camino, las dos medidas: el `cp` del install buscaba el nombre de upstream y el tar
—nuestro— lleva el fichero con nombre corto; y el build murió dos veces por DISCO LLENO (0 bytes en
/mnt/vvv), no por el lock. `scripts/poda-fuentes.sh --horas 6` liberó 4,6 G, que es exactamente para
lo que existe.
`dwarves` estaba SELLADA y llevaba tiempo sin construir. Se destapó al arreglar los symlinks de
`bzip2`: su hash se movió, dwarves cayó a deuda, y al reconstruirla el link murió con
`Error running link command: Segmentation fault` — crash del linker, no error de símbolos.
**No lo rompió el cambio de bzip2, y se puede probar**: el `libbz2.a` y el `bzlib.h` nuevos son
byte-idénticos a los viejos (`cmp -s`); lo único que cambió fueron cuatro destinos de symlink. El
artefacto sellado tapaba una rotura que ya existía — el lab rueda desde Alpine edge y zig subió. El
rehash no causó la rotura: la DESTAPÓ.
## La causa, bisecada sobre la línea de enlace real
CMake deja la línea literal en `build/CMakeFiles/<target>.dir/link.txt`, y el árbol de post-mortem la
conserva. Rehecha a mano FUERA de takana y del sandbox, sustituyendo el wrapper por el `zig` del lab
y las rutas `/usr/lib/*` por las del store:
tal cual → exit 139 (SIGSEGV)
quitando `-static` → exit 139 ⇒ no es el enlace estático
quitando `--dependency-file` → **exit 0** ⇒ ES ESO
`-Xlinker --dependency-file=…` lo emite CMake ≥3.27 para que el LINKER calcule las dependencias de
enlace, y el `lld` de zig 0.16.0 segfaultea procesándolo. `-DCMAKE_LINK_DEPENDS_USE_LINKER=OFF` es la
palanca de upstream (cmake del corpus: 3.31.6).
**Es mejor que `compiler = "gcc"`**, que es como esquivé ayer el MISMO crash en los tres
`protoc-gen-upb*` de protobuf sin conocer la causa: deja la receta en el toolchain por defecto del
proyecto en vez de escapar de él.
⚠ **Y el alcance no son dos recetas: 153 del corpus usan CMake con zig.** Todas selladas, así que hoy
nadie lo ve — pero el crash depende de los inputs (dentro de protobuf caían 3 de ~10 ejecutables), o
sea que no se sabe cuáles fallan hasta que su hash se mueva. Deuda latente pura.
No se arregla poniendo la perilla en el lab: la mayoría de esas 153 traen su `cmake …` EXPLÍCITO en
la receta, así que tocar la fase por defecto no las tocaría **y** re-hashearía a las que sí usan la
heurística. Incompleto y disruptivo a la vez.
Verificado: los 10 ejecutables estáticos con 0 NEEDED —incluidos `codiff` y `dtagnames`, los dos que
segfaulteaban—, `pahole --version` → v1.30, y REPRODUCE bit a bit.
La config del servidor declara de qué servicios depende: cada `reverse_proxy` y cada `php_fastcgi`
apuntan a algo. Cruzarlo contra la decisión tomada sobre cada servicio caza un fallo que ninguna otra
parte ve: **el dominio se MUDA y el servicio que lo sirve está marcado MUERE**. Las dos decisiones
son razonables por separado y juntas dejan el sitio nuevo devolviendo 502 — no lo ve el DNS, no lo
ven los procesos, y no lo ve quien decide de a una entrada por vez, que es como se decide.
En gioser apareció el revés: dos sitios proxean a un puerto que NINGÚN servicio censado sirve, o sea
que ya devuelven 502 hoy, en el origen — `mail.sigma.gioser.net` → :9000 y `api.gioser.net` → :8000.
Confirmado aparte con `ss -lntp`: no hay nada escuchando en ninguno de los dos.
**Y un falso positivo que hubo que matar primero.** La primera corrida acusaba a `sergio.gioser.net`
de mudarse dejando atrás a `shuma`. Falso, y la causa estaba en el CENSO: los puertos se adjudicaban
por prefijo de nombre (`proc.startswith(name[:15])`), así que un `shuma` DECLARADO-MUERTO se quedaba
con el 7378 — que lo escucha `shuma-gateway` (pid 294, verificado con `ss -lntp`). Un
declarado-muerto por definición no puede estar escuchando. Ahora se atan por PID, que es lo único sin
ambigüedad, con caída al nombre sólo para servicios VIVOS cuando `ss` no da el pid.
El puerto es la señal más fuerte de que algo sirve, así que colgárselo al servicio equivocado
envenena todo lo que se derive de él — acá se derivó un guardián acusando al inocente, que es la
forma más rápida de que un guardián se deje de leer.
De paso, el lector de Caddy entiende `php_fastcgi` (antes caía en «directiva no reconocida») y
registra a dónde proxea cada sitio, incluidos los bloques anidados.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
`origen_binario` buscaba la receta por el nombre del SERVICIO, y ése casi nunca es el nombre del
paquete: `sshd` lo trae `openssh`, `crond` lo trae `cronie`. El censo ya le preguntó al gestor de
paquetes quién posee cada binario, así que ese nombre también se prueba. El perfil pasó de 4 recetas
a 7, y los 27 servicios quedan: receta-takana 9 · suelto 9 · paquete-ajeno 13 · interprete 4 ·
borrado 4.
**La trampa, que es la que haría mentir al perfil.** `openclaw` lo posee el paquete `nodejs`;
`fail2ban-server`, `glances` y `uvicorn` los posee `python`. Contarlos como cubiertos porque existe
`recipes/nodejs.toml` haría salir el perfil N/N describiendo un servidor al que le faltan CUATRO
programas. Tienen clase propia (`interprete`) y cuentan las DOS cosas a la vez, porque las dos son
ciertas: su runtime entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo que pase— y el
servicio sigue listado como NO cubierto. Meterlo sólo en las raíces miente; dejarlo sólo en los
faltantes arma una imagen sin runtime y el programa, cuando llegue, no arranca.
Y el paquete del origen no se llama igual que la receta ni para el mismo intérprete: Artix empaqueta
`python` y el catálogo tiene `python3.toml`. Sin ese alias tres servicios decían «hace falta una
receta takana» teniendo el runtime sellado — dos trabajos muy distintos.
**Sellada y sin sellar tampoco son lo mismo**, y el perfil las listaba igual: una sellada se INSTALA
del repo firmado, una sin sellar hay que CONSTRUIRLA. Salió con un caso real — `recipes/qdrant.toml`
entró al catálogo desde otro frente mientras se escribía esto y no tiene artefacto. Ahora se marca en
la línea y se resume al pie.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
`vigia-subcomandos.py` y `hydrate-profile.py --auditar-raices` nacieron ayer encontrando cosas
reales: 46 binarios sellados que no se pueden invocar por falta de driver, y 4 comandos de `bzip2`
apuntando a `/out/usr/bin/…`. **Las dos las encontré a mano, y eso no se repite solo.**
El argumento ya estaba escrito tres líneas más abajo en este mismo fichero, al lado de
`vigia-sonames`, y costó caro: nadie lo corría, así que `libstdc++.so.6` —que rompía el navegador en
los CUATRO perfiles— estuvo en su salida meses sin que nadie la leyera.
Van FUERA de la puerta diaria porque son baratos: **4 s y 22 s** medidos. Dentro del `if` del sello
correrían una vez al día sin motivo.
⚠ **Y la primera versión de este commit los metió DENTRO de la puerta**, justo lo contrario de lo que
decía su propio comentario — el ciclo de prueba no imprimió ni una línea de ellos y así se vio. Por
eso se corre el ciclo de verdad antes de dar por bueno un cablazo al cron: un bloque mal colocado en
un script desatendido no avisa, simplemente no pasa nada.
Dejan fichero en `docs/state/` con la FECHA DE MEDICIÓN por delante, por la misma razón que el
static-audit: con el frente en verde el texto es constante, `git diff --cached --quiet` no vería
cambio, y dentro de tres meses el fichero sería indistinguible de uno rancio. Con la fecha, cada
ciclo deja huella en el `git log` — se ve que el vigía sigue VIVO, no sólo que el último veredicto
fue bueno.
Verificado corriendo el ciclo completo:
subcomandos.txt ✓ TOTAL: 44 herramientas selladas que no se pueden invocar.
raices.txt ✓ ✓ 0 ofensores NUEVOS sobre 1 artefactos.
==> estado commiteado+pusheado
El guardián nuevo (tokeniza español + el espacio distingue) se corrió a mano contra el modelo antes
de esperar el lock, y estaba mal de tres formas distintas:
1. **`cos` es una función interna de awk**, así que `cos[2]` muere con un `syntax error` que no
menciona el nombre. Se llama `cs`.
2. el `sed` que partía por corchete de apertura **se comía el último vector** (salían 2 de 3, y el
`test -ge 3` lo habría cazado, pero fallando por el motivo equivocado). Ahora `grep -o` por el
corchete completo.
3. y los backslashes iban DOBLES: en un literal TOML de comilla triple no se escapan, así que la
shell recibía `\\n` y `tr` se ponía a borrar las letras «n».
Y una cuarta, de mi propio comentario: al explicar el punto 3 escribí la comilla triple **dentro**
de la cadena que empieza con comilla triple. Cerró el literal y el TOML dejó de parsear, con el error
apuntando a la línea del comentario. Queda avisado ahí mismo.
Verificado en el hub: guardián 2 → 11 y 14 tokens distintos, 0 comunes; guardián 3 →
gato~perro=0,8436 contra gato~cortafuegos=0,6292. Los dos PASAN con el modelo bueno.
`/proc/<pid>/exe` termina en « (deleted)» para cuatro servicios de gioser: el fichero ya no está en
disco, sólo vive el inodo que sostiene su proceso. Y en tres de los cuatro hay AHORA otro fichero en
la misma ruta, de distinto tamaño:
tejido corre 12750368 B · en su ruta hay 13815864 B
shuma-gateway corre 8431896 B · en su ruta hay 10363504 B
pacha-secretos corre 8634240 B · en su ruta hay 8647456 B
puerta-f6e393ff corre 197262440 B · en su ruta NO HAY NADA
Copiar la ruta NO FALLA: muda otra cosa, y el servicio nuevo no es el que estaba andando. Todo verde,
todo distinto — el modo de fallo más caro que hay en este frente.
Se recuperan leyendo `/proc/<pid>/exe`, y SÓLO mientras el proceso viva. En una mudanza que termina
BORRANDO el origen, un reinicio de gioser antes de este paso los pierde para siempre. Por eso:
· clase propia en `origen_binario` (`borrado`), no una coletilla dentro del texto de `suelto`: no es
«hay que llevarlo», es «se pierde en el próximo reinicio y el que está en su ruta no es el mismo».
El motivo trae el comando literal de rescate con su pid.
· paso `rescate` en el plan, ANTES del preflight, porque es el único paso que puede volverse
IMPOSIBLE mientras se piensa el resto.
· su verificación COMPARA TAMAÑOS contra el que corre. No es celo: un `cat` de un `/proc` que ya no
existe crea un fichero VACÍO y devuelve 0, así que sin comparar el rescate «pasa». Probado en los
dos sentidos — el paso real sale 0, y con un fichero vacío a propósito dice `FALTA tejido` y sale 1.
Los cuatro ya están rescatados en `work/mudanza/rescate/` (gitignored), byte a byte iguales a los que
corren, con su SHA256SUMS.
Y el empalme se hizo comprobando que el marcador fuera ÚNICO antes de cortar, que es la lección del
`def pasos` duplicado de ayer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
multilingual-e5-small sellaba, cargaba, contestaba 384 dimensiones y 45 tests en verde. Y con el
modelo de verdad, de punta a punta, el ranking devolvía SIEMPRE la misma página.
Seis pasos descartando hipótesis —batching, posición, nuestro código, la cuantización, la conversión—
hasta que `/tokenize` lo dijo en una línea: «cortafuegos», «minino», «duerme» y «tejado» van todos al
id 100 = `<unk>`. Un vocabulario XLM-RoBERTa por esta ruta deja casi todo en desconocido, y un texto
que es todo `<unk>` embebe igual que cualquier otro. La pista estaba a la vista desde el principio:
`gato~perro` y `gato~cortafuegos` daban el mismo número a CUATRO DECIMALES.
En su lugar, Qwen3-Embedding-0.6B Q8_0, GGUF oficial de Qwen (Apache-2.0): tokeniza español de verdad
(`cort|af|uegos`), acierta 3/3 con márgenes anchos (0,649 contra 0,237), y es de la misma familia que
el modelo de chat. Cuesta 610 MiB en vez de 126: es el precio de que funcione, y sube la cuenta de la
imagen a ~1,85 GiB si se declara.
⚠ Y LA PARTE QUE IMPORTA PARA LA PRÓXIMA VEZ: el guardián del `install` ya no mira sólo el mágico y
el tamaño —«existe» no es «sirve»—. Ahora tokeniza dos frases en español sin palabras en común y
exige que no compartan tokens (con el e5 roto compartían la mitad, todos `<unk>`), y después levanta
el servidor de verdad y exige que un gato se parezca más a un perro que a un cortafuegos. Con esos
dos chequeos el e5 no habría sellado nunca.
El tar del modelo roto se borró del mirror (476 MB) y del disco.
⚠ El build está en la cola del flock detrás de otro agente (pixi, 22 min y contando), así que el
artefacto todavía no está sellado y los guardianes del navegador no corrieron. Lo medido hasta acá:
el camino completo host→motor→índice con el modelo nuevo, 3/3 (`archive.ask` de punta a punta, fuera
de la jaula), más 45 tests en tawasuyu.
La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Eso lo dice el
FICHERO DE CONFIGURACIÓN, y leerlo no toca al origen: ni una petición, ni riesgo de fail2ban.
Para leerlo se agregó un LECTOR de Caddy al centro (`formatos/caddy.py` ya tenía el escritor), y el
censo lo usa en vez de tener su propio parser a medias — los de nginx y apache ya existían, y dos
parsers del mismo formato es cómo se separan sin que nadie lo note. Cuarto par de la familia web.
Sobre el Caddyfile real de gioser, cinco sitios apuntan a un `root` que no existe — y son justo los
que devolvían los 502 que en su día hicieron que el censo SE BANEARA A SÍ MISMO al sondearlos:
aura.gioser.net → /var/www/aura_frontend · sigma → /var/www/sigma/frontend
summa → /var/www/summa/frontend · kosmofono → … · dev.summa → …
Es evidencia MÁS FUERTE que el DNS: no hay nada que servir, devuelve 502 resuelva donde resuelva. Va
como recomendación `muere` con la ruta y el fichero donde está el bloque.
**Y sirve para lo contrario, que es donde el aviso hacía daño.** Un directorio que la config SÍ
referencia no es huérfano: el plan marcaba `/var/www/git-tawasuyu` como «nadie lo recuerda» estando
servido, y ese aviso aplicado tira `git.tawasuyu.net`.
Dos bugs propios, los dos encontrados contra el fichero real y no sobre un ejemplo mío:
· El `root` de ese sitio vive DENTRO de un `handle`, y yo saltaba los bloques anidados enteros por no
saber modelarlos. Que el pivote no sepa MODELAR algo no es razón para no VERLO: el bloque sigue
marcado SIN-TRADUCIR pero sus `root`/`reverse_proxy` se leen. Con eso aparecieron dos raíces
ausentes más, también anidadas.
· Detectar la cabecera de sitio con una lista negra de directivas dejaba pasar `log { output file … {`
y el snippet `(acceso) {`: DOS dominios inventados que el censo habría puesto a decidir. La regla
que aguanta es positiva — todos los tokens de la cabecera tienen que PARECER una dirección.
Sin regresión: `nginx → caddy` y `apache → caddy` siguen dando `Valid configuration` con el caddy del
corpus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
El muro no era un daemon ni un LLM — y el propio comentario de la extensión lo decía mal, copiando
lo que hace willay-rag. Medido: para ordenar por parecido no hace falta ningún LLM (eso es un
coseno) y el «daemon de embeddings» resultó ser el mismo llama-server que ya levanta el chat, con
otro modelo. La corrección quedó escrita donde estaba la afirmación.
Del lado de la suite (tawasuyu 22527f7d1 y b4ecffea8): el protocolo de llama-server salió a
`shared/foreign-llama` (regla 4 — vivía dentro de puriy-costura y ya tenía dos consumidores), el
`Provider` es `rimay-verbo-llama` (regla 10 — la familia verbo YA es la abstracción de embeddings, y
éste es su primer backend sin nube ni descargas), y el índice es `rimay-verbo-index::VectorIndex`.
Acá: la receta del modelo (multilingual-e5-small, MIT), que se pinea en fp32 y la receta CUANTIZA a
Q8_0 con nuestro llama-quantize — así la procedencia es de quien declara la licencia, la imagen se
lleva 126 MB en vez de 476, y la transformación es nuestra y verificable. Más el guardián y la
extensión, que ahora expone `archive.ask` al lado del `archive.search` literal: son dos preguntas
distintas y conviven.
⚠ El guardián está escrito pero NO corrió todavía: `ia-modelo-embeddings` quedó en la cola del flock
detrás del build de otro agente. Lo medido hasta acá es el modelo a mano (384 dimensiones, el pasaje
correcto gana con y sin los prefijos de e5) y 45 tests en tawasuyu. La línea del SDD que lo dice se
borra cuando dé verde.
La diferencia entre los dos syscalls decide el frente: kexec_load pide
los segmentos armados y el purgatory (= kexec-tools entero);
kexec_file_load recibe los descriptores y hace el trabajo adentro, ~50
líneas sin dependencias.
Con recipes/linux-metal-kexec.toml (única diferencia: CONFIG_KEXEC_FILE)
queda respondida media decisión abierta nº2: kexec y Secure Boot SÍ
pueden coexistir, porque lockdown prohíbe kexec_load y acepta
kexec_file_load con imagen firmada. Lo que queda es si se firma y quién
paga el artefacto extra — decisión de coste, no de viabilidad.
Y queda anotado el diagnóstico que mandaba al lugar opuesto: EPERM no es
"falta CONFIG_KEXEC_FILE" sino falta de CAP_SYS_BOOT. Medido en el hub,
cuyo kernel trae el flag y aun así dio EPERM por no ser root. Tercera
vez en dos días que el error caro no es el mecanismo sino el mensaje que
lo explica.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
ADR 0017 §1. Se usa kexec_file_load, no el kexec_load viejo, y la
diferencia es de orden de magnitud: kexec_load recibe los segmentos YA
armados y el purgatory —el código que corre entre los dos kernels—, o
sea que usarlo obliga a reimplementar kexec-tools entero.
kexec_file_load recibe los DESCRIPTORES del kernel y del initrd y hace
el trabajo adentro. Son ~50 líneas.
El syscall va con asm! en vez de agregar la crate libc al workspace: es
UN syscall, su número y su ABI son contrato estable de Linux, y la
alternativa era arrastrar una dependencia a un workspace que comparten
dos frentes.
El cmdline viaja CON su NUL: el kernel cuenta cmdline_len incluyendo el
terminador, y sin él lee un byte de más. Es el error clásico de esta
llamada.
recipes/linux-metal-kexec.toml — variante cuya ÚNICA diferencia es
CONFIG_KEXEC_FILE=y. Variante y no flag en la canónica porque el .config
ES la identidad del artefacto (SDD 22 §1): tocar linux-metal re-hashea
el kernel que arranca las imágenes y el que reproduce bit a bit, y el
ADR deja esa decisión al usuario. Comprobado que la canónica no se
mueve: sigue en b3:2ed8f54a…, el que ya está sellado.
Y un diagnóstico que corregí a los dos minutos de escribirlo, porque
mandaba al lugar OPUESTO: decía "falta CONFIG_KEXEC_FILE" para EPERM.
Medido en este hub — el kernel de Artix trae CONFIG_KEXEC_FILE=y y aun
así devolvió EPERM, por no ser root. EPERM es falta de CAP_SYS_BOOT (o
lockdown si ya sos root); ENOSYS es el flag que falta. Confundirlos
manda a recompilar un kernel que estaba bien.
La ayuda del comando dice con todas las letras que esto NO es
"actualizar sin rebootear": el userspace muere igual. Ahorra los 20-30 s
de POST/UEFI, que es la mitad del downtime en un servidor remoto y nada
en un portátil.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj