Commit Graph
694 Commits
Author SHA1 Message Date
Sergio 377eecc57a argp-standalone: nueva receta — la argp de glibc que musl no trae
Segunda pata del frente libdw/musl: `dwarves` hace `find_package(argp REQUIRED)`
y musl no trae argp. Es lo que Alpine empaqueta como `argp-standalone`.

Se compila a mano (sin autotools, como musl-obstack) y SÓLO los siete
`argp-*.c` que el propio Makefile.am pone en `libargp_a_SOURCES`. Los otros .c
del tarball (mempcpy/strchrnul/strndup/strcasecmp) son LIBOBJS de autoconf —
reemplazos para sistemas que NO los tienen— y musl SÍ los tiene, así que
incluirlos duplicaría símbolos de la libc.

Gotcha: `UNUSED` no es del código, lo define el `acinclude.m4` de autoconf. Sin
él las firmas `char* arg UNUSED` no parsean y salen `expected ')'` y varios
`undeclared identifier 'state'` — errores que parecen del código y son de
config. Va en el config.h a mano.
2026-08-27 20:30:03 +00:00
Sergio 66048af2a0 musl-obstack: nueva receta — la obstack de glibc que musl no trae
Primera pieza del frente libdw/musl (para `dwarves`). La necesitan DOS cosas:
`libdw` de elfutils (su libdwP.h mete `struct obstack` en el Dwarf) y el propio
`dwarves`, que hace `find_package(obstack REQUIRED)`. Es lo que Alpine
empaqueta como `musl-obstack`.

Se compila A MANO: el tarball trae Makefile.am pero no un `configure` generado,
y el corpus no tiene autoconf/automake/libtool. Son dos .c.

Dos gotchas resueltos:
· `obstack.c` hace `#include <config.h>` SIN guarda (fuera de _LIBC) ⇒ hay que
  fabricarle uno. Lo único que mira es HAVE_LIBINTL_H, que musl no tiene, así
  que el config.h mínimo vale.
· Es `<config.h>` con ÁNGULOS, no comillas ⇒ no basta con dejarlo en el cwd,
  hace falta `-I.` en el compile.

El nombre importa: dwarves lo busca con `find_library(NAMES obstack)` en
/usr/lib ⇒ instala /usr/lib/libobstack.a y obstack.h.
2026-08-27 19:56:11 +00:00
Sergio 75609c215b strace: quitar --enable-bundled=yes y el pin de zig — NINGUNO hacía falta
Los añadí durante el diagnóstico y luego los verifiqué uno a uno quitándolos:
la receta construye igual sin ellos. Dejarlos era cargo-cult, y encima el
comentario afirmaba cosas que mis propias pruebas ya habían desmentido.

La causa real es UNA y las dos únicas piezas necesarias siguen ahí: quitar la
dep `linux-headers` (Linux 6.16 tapando los headers 7.1 del rootfs) y el
cherry-pick de upstream para el `io_uring_buf_reg` de 7.1.

Ambos descartes quedan escritos en la receta para que nadie los re-intente:
`--enable-bundled=yes` no puede ganar porque zig-cc impone sus headers por
delante de cualquier `-isystem`, y la versión de zig es irrelevante porque los
headers que mandan son los del ROOTFS.

Verificado tras la limpieza: sella, y `strace -c /bin/true` traza 30 syscalls.
2026-08-27 19:15:41 +00:00
Sergio 1cbcac6600 strace: construye — headers del rootfs (Linux 7.1) contra strace 6.19
Dos cambios, una causa: el sandbox sirve headers UAPI MÁS NUEVOS de lo que
strace 6.19 espera, y la receta traía una dep que los ensombrecía con otros
más VIEJOS.

1) Fuera `linux-headers` de [deps]. Esa receta instala Linux 6.16 en
   /usr/include y TAPA los headers del rootfs (7.1). Con ella faltaban 35
   constantes —18 de btrfs.h, 17 de input-event-codes.h— y como los xlat de
   strace son `#unconditional` (sin guardas #ifdef), una constante ausente NO
   degrada la traza: rompe la compilación. Comprobado que no aporta NI UN
   header que el rootfs/zig no traigan ya (0 ficheros exclusivos).

2) `strace-io-uring-7.1.patch`: Linux 7.1 añadió `min_left` a
   `struct io_uring_buf_reg` y cambió `__u64 resv[3]` por `__u32 resv[5]`, así
   que `CHECK_TYPE_SIZE(arg.resv, sizeof(uint64_t)*3)` saltaba como
   `static assertion failed`. El cambio es el de UPSTREAM (strace master ya lo
   trae), mismo criterio que `linux-headers-7.0.patch`, que también es un
   cherry-pick.

⚠ El fallo se destapa POR CAPAS: make abortaba en btrfs.o, así que las 17
constantes de evdev ni se compilaban, y al arreglar btrfs "aparecían" las de
evdev como si el arreglo no hubiera servido. Y sólo tras resolver las 35 se
llegó a compilar io_uring.c y salió el static_assert. Una causa, tres caras.

⚠ `--enable-bundled=yes` NO sirve acá aunque el compilador lo sugiera: zig-cc
impone sus headers por delante de cualquier `-isystem`, así que los bundled de
strace no pueden ganar. Se probó: la opción se aplica (`checking whether to use
bundled linux kernel headers... yes`) y el assert sigue.

Verificado: sella, 0 undeclared, 0 asserts, y el binario TRAZA de verdad —
`strace -c /bin/true` cuenta 30 syscalls.
2026-08-27 19:10:54 +00:00
Sergio a09cc9fd60 gobetween: renombrar el binario de main a gobetween
El módulo es `github.com/yyyar/gobetween/main` y el paquete main está en la
raíz, así que `go install` —que nombra el binario por el último elemento de la
ruta del módulo— lo dejaba como `/usr/bin/main`. Un `/usr/bin/main` en una
distro choca con cualquier cosa y no dice qué es.

La fase `install` que genera hammer para Go es `true` (el binario ya lo puso
`go install` en GOBIN=/out/usr/bin), así que definirla sólo añade el renombrado.

Los `test -x` no son adorno: si `go install` cambiara el nombre, sin ellos el
`mv` fallaría y se sellaría un artefacto SIN binario — regla 3 del repo, que
falle ruidosamente.

Verificado: /usr/bin/gobetween (33 M), arranca e imprime su versión, y no queda
ningún fichero `main` en el artefacto.
2026-08-27 17:36:44 +00:00
Sergio 23bb79f2ea gobetween: replace a illarion/udpfacade — la dep transitiva desapareció de internet
`github.com/eric-lindau/udpfacade` (que importa src/server/udp) ya no existe:
GitHub da «Repository not found» y proxy.golang.org 404 para esa versión, así
que `go mod vendor` moría y la receta era INCONSTRUIBLE. No se arregla subiendo
de versión: master de gobetween pinea el mismo módulo.

Se redirige con `replace` al único superviviente, illarion/udpfacade.

⚠ Apunta a MASTER (031998cc71fa), no al commit pineado. El original
(d8c1c27add169599654f71a6c62d13cde1c51e50) SÍ existe como objeto en illarion
—los forks comparten almacén y la API de GitHub lo muestra— pero NO es
alcanzable desde ninguna ref, y `go` sólo resuelve pseudo-versiones alcanzables
desde una ref. Comprobado: `unknown revision d8c1c27add16`. (`git fetch <sha>`
sí lo trae; `go` no usa esa vía.)

⚠ SEGURO y comprobado, no supuesto: entre master y el commit pineado hay TRES
commits y el diff entero es `udp.go` +2 y `udp_test.go` +2 —sólo la línea
`// +build !windows`— más `udp_windows.go` nuevo con `// +build windows`. Para
GOOS=linux el código compilado es IDÉNTICO, y esto construye x86_64-linux-musl.

Va como PATCH y no como fase porque `vendor_go_deps` corre en el fetch
(lib.rs:277), antes de las fases; los patches se aplican antes (lib.rs:224).

Verificado: el patch aplica limpio sobre un checkout pristino, hammer sella, y
el binario arranca e imprime su versión.
2026-08-27 16:30:56 +00:00
Sergio c1466de9d6 rsync: subir 3.4.4 → 3.5.0 — upstream podó el tarball viejo
samba.org borró `rsync-3.4.4.tar.gz` (404) y en ese directorio ya sólo queda
3.5.0. Tampoco estaba en nuestro mirror, así que la receta era INCONSTRUIBLE:
la única fuente MUERTA del corpus según docs/state/fuentes-vigia.json (1164
vivas / 3 muertas; las otras dos son un 502 transitorio de savannah).

⚠ NO es «cambiar el sha256 para tapar un 404» (lo prohibido por ADR 0013): el
tarball es OTRO, con su versión y su hash propios. Provenance verificada de
verdad, no asumida:

    gpg --verify rsync-3.5.0.tar.gz.asc rsync-3.5.0.tar.gz
    → Good signature from "Andrew Tridgell <andrew@tridgell.net>"
      RSA key 9FEF112DCE19A0DC7E882CB81BB24997A8535F6F

Clave traída de keys.openpgp.org; su fingerprint coincide con el `issuer` del
.asc. El aviso «User ID not certified» es esperable (no hay web-of-trust acá);
la firma en sí es válida.

Comprobado ANTES de subir: las diez opciones de configure siguen existiendo en
3.5.0, `rrsync.1` sigue en la raíz, y dont-use-nobody.patch aplica limpio
(`patch -p1 --dry-run` sin rechazos).

Verificado: sella, artefacto de 3,3 M con rsync/rrsync/rsync-ssl, y el binario
responde `rsync version 3.5.0 protocol version 32`.
2026-08-27 16:07:21 +00:00
Sergio 0a2f8916e6 python3: TRAE ssl — la causa era link=static, no openssl
CPython abortaba `_ssl` con «OPENSSL_THREADS is not defined, Python requires
thread-safe OpenSSL», y la receta lo daba por imposible sin rehacer openssl.

CAUSA RAÍZ, probada: no es openssl, es `link = "static"`.
`hammer-build/src/lib.rs:285` inyecta `LDFLAGS=-static`, y el Configure de
openssl (línea 1514) hace:

    if (grep { $_ =~ /(?:^|\s)-static(?:\s|$)/ } @{$config{LDFLAGS}}) {
        disable('static', 'pic', 'threads');
    }

⇒ el propio `-static` apaga los threads en silencio. Añadir `threads` explícito
NO sirve (el disable() va después y gana): el configuration.h salía byte a byte
idéntico. Dentro del sandbox `configdata.pm` decía `thread_scheme => pthreads`
pero `THREADS_DEFINE=0`; fuera del sandbox, los mismos flags —y hasta con el
perl del corpus— SÍ definían el macro. La diferencia era el entorno.

`openssl-threads`: misma receta con `link = "dynamic"` (mantiene `no-shared`,
sigue emitiendo .a). Verificado: OPENSSL_THREADS definido y 36 símbolos
pthread_ reales en libcrypto.a.

VARIANTE y no tocar `recipes/openssl.toml`: de la canónica cuelgan los CUATRO
kernels (linux, linux-generic, linux-metal, linux-metal-dual) y la reproducción
bit a bit del 6.16.12 es un resultado cerrado; moverle el hash lo invalidaría,
más 125 sellados.

Además `ssl` entra en la asserción del install. Sin ese guardián se volvería a
sellar un intérprete mudo — que es justo como esto estuvo escondido meses.

Verificado: `checking for stdlib extension module _ssl... yes`,
_ssl.cpython-312-x86_64-linux-musl.so instalada, y el propio install imprime
`modulos opcionales: OK` importando ssl.

⚠ Mueve el hash de python3: 251 dependientes directos, ~369 sellados en los
cinco grafos (161 en KDE). Es el coste aceptado de la decisión.
2026-08-27 15:26:38 +00:00
Sergio 5f6c062cd9 adwaita-hello, sourceview-hello, hammer-edit: añadir xz a [deps]
Con gtk4 ya sin objetivos compartidos, estas tres dejaron de fallar por PIC
(0 errores) y pasaron a morir en el link con `unable to find static system
library 'lzma'`.

Misma causa que appstream: declaran `appstream` y `libxmlb` pero no `xz`, y
`libxmlb.pc` trae `Requires: gio-2.0, liblzma, libzstd`. Al enlazar appstream
estática necesitan la liblzma igual. Patrón `.pc Requires`→[deps]; `xz` va
junto a `zstd`, que viene del mismo .pc.

Con esto cierra la clausura ENTERA de gtk4 en el grafo del corpus, las 6 que
colgaban de él: gtk4-hello, libadwaita, gtksourceview, adwaita-hello,
sourceview-hello y hammer-edit — todas SELLADAS y con binario real.
2026-08-27 14:50:10 +00:00
Sergio 0d6796e319 gtk4: no construir NINGÚN objetivo compartido — la .so era el muro PIC
`gtk/meson.build` declara dos hermanos con llamadas explícitas —`static_library('gtk')`
y `shared_library('gtk-4')`— y por ser explícita, `-Ddefault_library=static`
NO la suprime: no hay flag. Esa .so era el único objetivo compartido que se
enlazaba, arrastraba CINCO archives sin PIC (libtiff, libfreetype,
libfontconfig, libjpeg, libpng16) y moría con 13930 `relocation … recompile
with -fPIC`. Un ejecutable puede enlazar una .a no-PIC; una .so no.

Se apagan con `build_by_default: false` + `install: false` vía dos seds
acotados por rango (tocan 1 de 10 y 1 de 3 `install: true` respectivamente).

⚠ `build_by_default: false` NO BASTA SOLO: ninja construye igual lo que algo
NECESITA. El primer intento falló exactamente así — `shared_module('printbackend-file')`
depende de `libgtk_dep` y el fuente avisa «The 'file' print backend cannot be
disabled» (está fuera de los `if` de cpdb/cups), así que devolvía la .so al
grafo y re-aparecían los 13930 errores. Por eso se apagan LOS DOS.

En todo el árbol hay 5 objetivos compartidos: libgtk-4.so, printbackend-{cpdb,
cups,file} y media-gstreamer; las flags ya quitaban tres, estos seds los otros dos.

No se borra la declaración de libgtk: `pkg_config.generate(libgtk)` la necesita
para emitir el .pc con `-lgtk-4`, que es el nombre del archive GORDO que fabrica
la fase install. Nada la enlaza: los usuarios de `libgtk_dep` son demos, tests,
testsuite, examples y los backends de impresión/media, todos desactivados.

Verificado: sella, 0 errores PIC, 0 .so enlazadas, targets 997→993. Artefacto
con gtk4.pc `Libs: -lgtk-4`, libgtk-4.a de 808 objetos y `gtk_window_new` como
símbolo T definido. Las colas wlr/gnome tienen sus PROPIAS recetas de gtk4.
2026-08-27 14:46:55 +00:00
Sergio b6bcdee006 dufs: añadir xz a [deps] — sys-crate de compresión exige liblzma
Quinta de la familia (appstream, cargo-deb, cargo-make, dprint). Sin `[deps]`
el link muere con `unable to find static system library 'lzma'`. Pendiente de
verificar en la próxima parada; el patrón está verificado tres veces.
2026-08-27 11:51:18 +00:00
Sergio f14322bd6b dprint: añadir xz y bzip2 a [deps] — sys-crates de compresión
Falla con las DOS a la vez: `unable to find static system library 'lzma'` y
`… 'bz2'`. La receta no tenía `[deps]`.

Cuarta de la misma familia esta noche (appstream, cargo-deb, cargo-make): el
lab trae el toolchain de Rust pero NO las libs C que los crates `*-sys`
esperan del sistema. El arreglo va en [deps], NUNCA engordando el rootfs
(regla de rootfs-laptop-worker-divergen).

NO se añade en bloque a las 220 recetas Cargo sin [deps]: la mayoría construye
bien y re-hashearlas invalidaría las ya selladas. Se arregla la que falla.

⚠ Pendiente de verificar como cargo-make en su momento; el patrón está
verificado tres veces (appstream, cargo-deb, cargo-make selladas tras el mismo
arreglo). Se verifica en la próxima parada.
2026-08-27 11:38:03 +00:00
Sergio 6a0f3a1afe cargo-make: añadir bzip2 a [deps] — un sys-crate exige libbz2 del sistema
`unable to find static system library 'bz2' using strategy 'no_fallback'`. La
receta no tenía `[deps]`: el lab trae el toolchain de Rust pero no las libs C
que los crates `*-sys` esperan del sistema. bzip2 está sellado y aporta
/usr/lib/libbz2.a.

Tercera de la misma familia esta noche: appstream (lzma vía el .pc Requires de
libxmlb), cargo-deb (lzma vía lzma-sys) y ésta (bz2). El síntoma del linker es
idéntico en las tres y el arreglo también.

⚠ PENDIENTE DE VERIFICAR: las otras dos se construyeron y sellaron antes de
commitear; ésta no, para no parar el driver una vez por receta. Se verifica en
la próxima tanda de reanudación. Si fallara, el patrón —no la receta— es lo que
habría que revisar.
2026-08-27 08:42:47 +00:00
Sergio 7934a82e36 cargo-deb: añadir xz a [deps] — lzma-sys exige la liblzma del sistema
La receta NO tenía `[deps]` en absoluto. El lab trae el toolchain de Rust pero
no las libs C que los crates `*-sys` esperan del sistema, así que el link moría
con `unable to find static system library 'lzma' using strategy 'no_fallback'`.
El log lo confirma: vendoriza y compila `lzma-sys v0.1.20`.

Mismo fallo y mismo arreglo que `appstream.toml` de hace un rato, por vías
distintas: allá liblzma entraba por el `.pc Requires` de libxmlb, acá por un
sys-crate. El síntoma del linker es idéntico.

Verificado: sella en 3m02s, artefacto de 4,6 M con un ELF real en
/usr/bin/cargo-deb. Blast radius nulo: en deuda en los cinco grafos con 0
dependientes sellados.
2026-08-27 07:52:20 +00:00
Sergio 93e8fd9848 appstream: falta xz en [deps] — liblzma entra por el .pc Requires de libxmlb
libxmlb.pc → Requires: gio-2.0 >= 2.45.8, liblzma, libzstd

appstream declaraba libxmlb y zstd pero NO xz, así que el link moría con
`unable to find static system library 'lzma'`. Patrón `.pc Requires`→[deps].

Se veía primero en los binarios de tests/ (as-test_xml, as-test_validate…) y
appstream 1.0.5 NO tiene opción `tests` en meson_options.txt. Sedear
`subdir('tests')` —como ya hace la receta con docs/— habría tapado el síntoma
y dejado el fallo para los 4 dependientes, que enlazan appstream estática y
necesitan la liblzma igual.

Verificado: sella en 23 s, 0 errores de lzma, artefacto de 18 M / 97 ficheros.
Blast radius nulo: en deuda en los cinco grafos con 0 dependientes sellados.

El dependiente adwaita-hello pasa de fallar en 23 s a 3m33s y ahora muere en
gtk4 (libgtk-4.so contra libtiff/libfreetype/libfontconfig/libjpeg/libpng16
no-PIC), que es una decisión de arquitectura aparte y sigue abierta.
2026-08-27 04:53:55 +00:00
Sergio 5044c9f1ca gdk-pixbuf: -Dgif=disabled — el único .so de la receta no podía enlazar libpng no-PIC
En 2.44 `gif` es opción propia con default `auto` y ya no cae bajo `others`.
Sin declararla, el loader de GIF se construía como MÓDULO COMPARTIDO
(`libpixbufloader-gif.so`, el único .so de los 14 targets; el resto son
ejecutables), y una .so no puede enlazar una .a sin PIC: 1978 errores
`relocation R_X86_64_32 ... recompile with -fPIC`, todos de /usr/lib/libpng16.a
(que es --disable-shared).

Coherente con la intención declarada de la receta —«solo PNG (leaf mínimo)»:
jpeg y tiff ya se desactivaban; gif se coló por DERIVA DE VERSIÓN. Por eso el
arreglo es la flag y NO `[deps]`: cambiar libpng→libpng-pic es la decisión de
arquitectura que se descartó para el canónico el 2026-08-21, y que las colas
wlr/gnome resuelven con SOMBRAS propias.

Regresión CONGELADA, no fallo nuevo: se servía por cache-hit y sólo aparece al
reconstruir. Tumbaba en cascada xdg-desktop-portal, gnome-desktop, gjs, gdm,
gnome-session, gnome-settings-daemon y gnome-shell — todas fallando en 2-45 s
por su dep, no por sí mismas.

Verificado: sella en 12 s, 0 errores PIC, artefacto de 58 M con
libgdk_pixbuf-2.0.a y ningún .so. Blast radius nulo: el canónico estaba en
deuda con 0 dependientes sellados en los cinco grafos.
2026-08-27 03:31:09 +00:00
SergioandClaude Opus 5 f8f679d038 corpus: expat-shared y libffi-shared cierran la fuga al Alpine del lab
sway es `link = "dynamic"` A PROPÓSITO —un compositor dlopea los drivers DRI de mesa en runtime— y
sale con doce NEEDED. Nueve los cubría la clausura (pixman, drm, evdev, input, udev, wayland-server,
wlroots, xkbcommon, libc), porque esas recetas ya son dinámicas. Los otros tres —libz.so.1,
libexpat.so.1, libffi.so.8— venían de recetas `--disable-shared`, así que NINGÚN artefacto sellado
producía ese `.so` y el rootfs los resolvía contra el sysroot Alpine DEL LAB.

POR QUÉ ERA PEOR QUE UNA DEP FALTANTE. Una dep ausente falla ruidosamente. Ésta no: el lab NO entra
en `hash_inputs`, así que el store daba el artefacto por bueno mientras el binario sólo arrancaba en
una máquina que tuviera Alpine debajo. La fuga era invisible para todo el sistema de medición.

`zlib-shared` ya existía en el corpus y sólo faltaba declararla. `expat-shared` y `libffi-shared` son
nuevas, calcadas del patrón: autotools con --enable-shared, sin el truco de --whole-archive que
zlib-shared necesita porque SU configure aborta bajo zig cc. SONAMEs verificados contra lo que pide
el ELF: libexpat.so.1, libffi.so.8, libz.so.1. Exactos.

NO se tocó `link` en las canónicas: entra en `hash_inputs` y habría re-hasheado expat, libffi y todo
lo que los lista en deps —fontconfig, dbus, mesa, glib, python3, los crates `-sys`— cientos de
recetas selladas por libs que ya están bien. El nombre `*-shared` es distinto del canónico, así que
conviven. Verificado antes de construir: recalculadas las 794 recetas, **0 hashes cambiados**, sólo
las 2 nuevas.

Van al CORPUS y no a incoming-wlr/ porque la resolución de deps es hermano→padre: una receta del
corpus no ve una cola. Es donde ya viven zlib-shared, fontconfig-shared, freetype-shared…

VERIFICADO CON PÍXELES, no con el log. Rootfs rehidratado desde cero (126/126), CERO ficheros de
`.dev-fs/alpine`. Cero «Error relocating». La captura tiene el MISMO sha256 que la de las libs de
Alpine: 177fea396caa9dca, 1280x720, 383 colores. Tercera vez que la evidencia sale bit a bit igual
al cambiar la procedencia — la soberanía no costó ni un píxel.

Perfil: escritorio-sway 126/128. Faltan strace (linux-headers del lab) y rsync (404 de upstream).

QUEDA UN RESTO: el loader `/lib/ld-musl-x86_64.so.1` todavía se toma del host. `recipes/musl.toml`
existe en el corpus pero está EN DEUDA y fuera de todo perfil — mismo agujero de declaración, un
nivel más abajo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
2026-08-26 18:42:11 +00:00
SergioandClaude Opus 5 428ad80b24 wlr: fuentes y XKB DECLARADAS en el perfil — la captura sale bit a bit idéntica
`dejavu-fonts` y `xkeyboard-config` entran en `recipes/incoming-wlr/` y en las raíces de
`escritorio-sway`. La clausura pasa de 123 a 125 y ambas ya estaban selladas: cero builds.

POR QUÉ TENÍAN QUE SER RAÍCES Y NO PODÍAN LLEGAR POR CLAUSURA. Son DATOS, no binarios: ninguna
receta depende de una fuente ni de un mapa de teclado para COMPILAR. El grafo puede seguir todas las
aristas que quiera y no va a alcanzarlas nunca. Si no se declaran, no están — y el perfil sigue
dando 100%, porque mide la clausura de las raíces declaradas.

Copias BYTE-IDÉNTICAS a las de incoming-kde, a propósito. Ya había tres copias iguales de
xkeyboard-config (kde, gnome, cosmic) y dos de dejavu-fonts (kde, cosmic); ésta es la cuarta. No se
consolidan al corpus ahora porque eso toca las colas de otros tres frentes que otros agentes
trabajan. Mantenerlas idénticas deja esa consolidación como un `git mv` trivial en vez de un merge:
cuatro ficheros iguales se unifican de un tirón, cuatro casi-iguales exigen revisión.

VERIFICADO de punta a punta, no por el log: rootfs rehidratado DESDE CERO con sólo lo que la
clausura declara (123/123), y `sway-headless.sh` sin una línea de inyección manual de fuentes ni
XKB. Cero errores de xkbcommon, cero de fcft. La captura resultante tiene el MISMO sha256 que la de
ayer con todo inyectado a mano — 1280x720, 383 colores. La declaración es exactamente equivalente a
la inyección, y el pipeline reproduce.

Queda como deuda real (no de declaración): libz.so.1 / libexpat.so.1 / libffi.so.8, que sway pide
por estar enlazado dinámico y el corpus sólo produce como `.a`. Y `fonts.conf` NO era deuda: lo trae
el propio artefacto de fontconfig.

Perfil: escritorio-sway 123/125. Los 2 que faltan son strace (choca con los linux-headers del lab) y
rsync (404 de upstream), ninguno del escritorio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
2026-08-26 18:03:46 +00:00
SergioandClaude Opus 5 2b4999f682 libpng-pic: la variante que faltaba entre la estatica y la .so
gdk-pixbuf no sellaba, y las dos libpng del corpus fallaban por extremos
OPUESTOS — probadas las dos hoy, una detras de otra:

  libpng         .a sin PIC   → «relocation R_X86_64_64 cannot be used against
                                local symbol; recompile with -fPIC», porque ese
                                link acaba siendo dinamico (aparece libc.so)
                                pese al `link = "static"` de la receta.
  libpng-shared  solo la .so  → «unable to find static system library 'png16'»,
                                porque la receta pide --prefer-static.

Es la misma tenaza que el `bz2` de freetype: los -dev traen .so pero no .a. La
salida no es elegir un extremo sino una .a que TAMBIEN sea PIC. `libpng-pic` es
copia literal del canonico con `--with-pic` en el configure; nada mas.

EVIDENCIA de que es PIC, y hay que medirla bien: 0 relocs R_X86_64_32/32S
contra 706 del canonico — EXCLUYENDO las secciones .debug_*. Sin ese filtro
ambas dan >13000 y parecen identicas; es la correccion de la regla del .a
no-PIC que ya nos mordio en GNOME.

El canonico NO se toca (decision del usuario): 12 dependientes en cuatro colas
y es estatico por diseño. Se añade una SOMBRA en incoming-wlr que cambia una
sola linea de deps. libpng-pic si va al corpus, porque la resolucion es
HERMANO→PADRE y una receta del corpus no veria una variante en incoming-*.

⚠ ALCANCE: la sombra la ve su cola. gtk4, libadwaita, gtksourceview y otras 4
son del CORPUS y siguen resolviendo el gdk-pixbuf canonico roto.

⚠ NO ERA UN FALLO NUEVO: gdk-pixbuf tenia artefacto sellado y se servia por
cache-hit. La reconstruccion no lo rompio, lo DESTAPO.

Verificado: sombra SELLADA, 71 MB, con .a, .pc y los loaders .so reales — no un
directorio vacio haciendose pasar por artefacto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xd5DComN9Nu47TBpYMBHBn
2026-08-21 13:08:49 +00:00
SergioandClaude Opus 5 dba9ec47c5 licencias: el detector borraba la evidencia que decia registrar
Consulta solo las recetas que HOY no tienen licencia, asi que su cosecha MENGUA
a cada pasada: lo sembrado ayer ya no sale en --faltan. Y reescribia el fichero
entero con la cosecha del dia. Hoy una pasada con CERO detecciones dejo
docs/licencias-detectadas.tsv en 0 filas y se llevo las 610 que documentaban de
donde salia cada licencia ya sembrada.

Salio con exit 0 diciendo «escrito» y «sembrar con». Un vacio que llega hasta el
final afirmando que todo fue bien — la regla 3 de CLAUDE.md, esta vez sobre el
registro de procedencia en vez de sobre un artefacto.

Ahora FUSIONA con lo registrado (la fila nueva gana) y se niega a sobrescribir
si el resultado pierde filas: un registro de evidencia que encoge es un fallo,
no un resultado. Con CON=0 lo dice y no invita a sembrar nada.

Y las cuatro de HashiCorp quedan declaradas: BUSL-1.1.

No es adivinado ni sale de la API —que devuelve NOASSERTION en las cuatro, y por
eso llevaban meses sin licencia—: es el texto del LICENSE del TAG QUE LA RECETA
PINEA, no el de la rama por defecto. consul v1.22.7, vault v1.21.4, nomad
v1.11.3 y packer v1.15.4 abren con «Business Source License 1.1».

⚠ BUSL-1.1 NO es una licencia libre: prohibe el uso en produccion que compita
con el producto de pago, y solo pasa a MPL-2.0 cuatro años despues de cada
version. Cuatro paquetes del catalogo publicable no son redistribuibles como si
fueran libres.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xd5DComN9Nu47TBpYMBHBn
2026-08-20 17:00:15 +00:00
SergioandClaude Opus 5 683dd9cbee freetype: --with-bzip2=no explicito — su autodeteccion tumbo el escritorio entero
Cadena probada de punta a punta (2026-08-12):

1. Se añadio `bzip2-dev` al lab para que CPython tuviera sus modulos.
2. freetype AUTODETECTA bzip2 —la receta desactivaba harfbuzz y brotli de
   forma explicita, pero no este— y lo grabo en su freetype2.pc:
   `Requires: zlib, bzip2, libpng`.
3. Toda receta que enlaza freetype con `-all-static` resuelve ese Requires
   via pkg-config y pide `-lbz2`.
4. Los `-dev` de Alpine traen `.so` pero NO `.a` ⇒ «unable to find static
   system library 'bz2'».
5. Cayo fontconfig y con el 27 recetas: KDE, GNOME y COSMIC dependen de el.

Verificado: el freetype nuevo vuelve a `Requires: zlib, libpng` y fontconfig
SELLA (8290b402...).

Se arregla en la receta y NO quitando bzip2-dev del lab: esto mueve el hash
de freetype y sus dependientes; lo otro re-hashearia el corpus ENTERO por
tercera vez en un dia. freetype solo usa bz2 para fuentes PCF comprimidas.

⚠ Metodo, porque me costo tres hipotesis fallidas: hay CINCO artefactos de
freetype en el store y mirar uno al azar dio el `.pc` equivocado. Y al
apartar bzip2.pc el error cambio a «freetype2 not met» — eso era evidencia A
FAVOR (pkg-config no podia resolver el Requires) y lo lei como si fuera en
contra. Comprobar el hash VIGENTE, no el primero que lista `ls`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:48:55 +00:00
SergioandClaude Opus 5 1940a94a11 lab: los -dev al rootfs — es el unico sitio donde _curses se puede resolver
Decision del usuario. CPython construye sus modulos opcionales como .so
COMPARTIDOS y las .a del corpus NO son PIC («relocation R_X86_64_PC32 against
symbol 'stdscr'»), asi que declararlas en [deps] NO funciona: se probo una
por una. Las libs del rootfs son compartidas y si sirven.

Añadidos: libffi-dev ncurses-dev readline-dev sqlite-dev bzip2-dev xz-dev
expat-dev. Resultado VERIFICADO: el guardian de python3 importa los OCHO
modulos (_ctypes _curses readline sqlite3 bz2 lzma zlib pyexpat) y pasa. De
paso `libffi` sale de [deps]: ya lo aporta el lab.

openssl-dev NO va, deliberado: se de-Alpinizo porque el host-tool del kernel
enlaza el openssl del corpus desde el overlay; devolverlo podria cambiar el
artefacto del kernel. python3 sigue sin `ssl`.

Y SE CIERRA EL AGUJERO QUE ESTO ABRIA. Esas libs del rootfs ahora entran en
el CONTENIDO de un artefacto, asi que su version es parte de su identidad ⇒
van a TOOLCHAIN_PREFIXES. Sin eso habriamos reabierto en pequeño el mismo
fallo que 58d3161 cerro: instalar los -dev subio ncurses 6.5→6.6 y readline
8.3.1→8.3.3 —sin tocar gcc/rust/musl, lo confirmo el lock— y ese salto habria
cambiado el python3 producido SIN mover su direccion.

Coste: la huella del lab cambia ⇒ el corpus se re-hashea otra vez. Se hace
AHORA a proposito, con 194 artefactos, no con el corpus a medio construir.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:55:01 +00:00
SergioandClaude Opus 5 6ab29b1e78 python3: libffi arregla _ctypes; el resto choca con dos fronteras reales
Estado VERIFICADO: con libffi el build sella y el guardian de la fase install
imprime «modulos opcionales: OK» ⇒ _ctypes y zlib importan de verdad.

El guardian es la pieza que mas vale de este commit: `configure` de CPython
OMITE en silencio cada modulo opcional cuyos headers no encuentra, `make`
termina 0 y el artefacto se sella mutilado. El error aparece dias despues en
OTRA receta (gjs, gdm) como ModuleNotFoundError y no se parece a su causa.
Importarlos en install lo convierte en un fallo en la receta correcta.

Lo que NO se pudo añadir, probado uno por uno y documentado en la receta:

  openssl  → el del corpus es libcrypto ESTATICA sin threads; CPython aborta
             con «OPENSSL_THREADS is not defined». Sin modulo `ssl`.
  ncurses  → `.a` no-PIC: _curses_panel.so no enlaza
             («relocation R_X86_64_PC32 ... against symbol 'stdscr'»).
  sqlite/bzip2/xz/readline/expat → mismo muro no-PIC (_lzma lo destapo).

CPython construye sus modulos opcionales como .so COMPARTIDOS, y las .a del
corpus no son PIC. El python3 viejo tenia _ctypes Y _curses porque se
construyo contra las libs COMPARTIDAS del rootfs de entonces; el lab anclado
trae los runtime pero no los headers.

⇒ _curses sigue roto y con el la familia GNOME. Arreglarlo pide DECIDIR:
meter los -dev en la imagen del lab (re-hashea el corpus otra vez, barato
AHORA que van 48 de 1186) o hacer recetas PIC de esas libs (frente nuevo).
Queda para el usuario.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 23:08:58 +00:00
SergioandClaude Opus 5 2f0ff5d0d3 python3: declarar libffi — sin el, CPython se construye SIN _ctypes en silencio
Destapado reconstruyendo el corpus: gjs y el build de mozjs morian con
`ModuleNotFoundError: No module named '_ctypes'`. La causa no estaba ahi: el
artefacto de python3 se sella SIN ese modulo y arranca perfectamente; el
fallo aparece mucho despues, en otra receta. 310 recetas dependen de python3.

El rootfs del lab trae libffi (runtime) pero NO sus headers — no hay
libffi-dev en el lock. El configure de CPython no encuentra ffi y omite
_ctypes sin fallar. recipes/libffi.toml si instala headers y .pc, y pkgconf
—ya declarado— los ve en la capa overlay.

Por que aparece AHORA y no antes: el artefacto viejo de python3 tenia _ctypes
porque el rootfs de entonces traia los headers, y el cache-hit lo mantuvo
congelado hasta que el corpus se re-hasheo. Mismo patron que
make/--disable-load: reconstruir destapa lo que la cache sostenia.

Se arregla ahora y no mas tarde a proposito: mover el hash de python3 mueve
el de sus 310 dependientes, y con solo ~48 recetas selladas eso es barato.
Dentro de dos dias no lo habria sido.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:41:21 +00:00
SergioandClaude Opus 5 98a958ec48 make: --disable-load — sin esto el binario sale DINAMICO Y ROTO y tumba el corpus
Sintoma: al reconstruir el corpus, bash/binutils/bison/busybox/zlib morian
con exit 139. dmesg lo nombraba: `traps: make general protection fault in
ld-musl-x86_64.so.1`.

Causa exacta, leida de la linea de link real:

  hammer-zig-cc -g -O2 -Wl,--export-dynamic -static -o make src/*.o

GNU make 4.4.1 soporta cargar objetos en runtime (directiva `load`), asi que
su configure anade -Wl,--export-dynamic. El wrapper hammer-zig-cc tiene la
regla —correcta— de que `-static` CEDE ante cualquier link que exija dinamico,
y --export-dynamic es uno de esos marcadores ⇒ le quitaba el -static y se
sellaba un make dinamico que segfaultea. Como toda receta autotools invoca
make, se propagaba a casi todo el corpus.

El arreglo va en la receta y NO en el wrapper: el conflicto es de origen
—pedimos estatico de un proyecto que pide symtab dinamica para una funcion
que no usamos—. Ensenarle al wrapper a ignorar --export-dynamic lo romperia
para gobject-introspection, que si lo necesita.

POR QUE NO SE VIO ANTES, que es la leccion: el artefacto viejo de make era
estatico y estaba congelado por cache-hit, probablemente desde antes de que
existiera esa regla del wrapper. Nadie lo reconstruyo hasta que el corpus se
re-hasheo entero. Un cambio que "no re-hashea nada sellado" igual cambia lo
que producen los builds FUTUROS, y eso queda invisible hasta que algo fuerza
la reconstruccion.

Verificado: make ESTATICO, corre (GNU Make 4.4.1), y zlib —que fallaba con
139— sella.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:37:49 +00:00
SergioandClaude Opus 5 2602218332 kernel: THUNDERBOLT->USB4 y fuera REISERFS_FS en las cuatro recetas
Los dos simbolos ya no existen en 6.16.12, asi que `-d THUNDERBOLT` y
`-d REISERFS_FS` eran no-ops silenciosos: scripts/config los escribia y el
olddefconfig siguiente los tiraba por desconocidos.

No son el mismo caso y por eso no reciben el mismo trato:

- THUNDERBOLT se FUNDIO, no se fue. drivers/thunderbolt/Kconfig declara
  `menuconfig USB4` = "Unified support for USB4 and Thunderbolt". El simbolo
  cambio de nombre, la intencion de apagarlo sigue viva ⇒ se renombra a
  `-d USB4`, que ademas pasa a ser un guardian de verdad: si un dia la base
  o un `select` lo encienden, ahora si lo apaga.
- REISERFS_FS se RETIRO del kernel. No hay nada que apagar ⇒ se quita.

El .config resultante NO cambia hoy: USB4 no aparece en x86_64_defconfig y su
Kconfig no trae `default`, asi que ya estaba en n. Lo que cambia es el
ArtifactHash de los cuatro, porque el texto de la fase configure entra en
hash_inputs. Los cuatro artefactos viejos siguen en el respaldo como
superados; no se pierde nada.

Verificado contra el arbol real (work/kconfig-6.16.12/), no de memoria.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 14:08:06 +00:00
sergioandClaude Opus 5 0ac421b516 libsndfile: su configure sólo busca python2.x — tumbaba CUATRO recetas y parecía fallo de cada una
COSMIC cerró 25 de 30 raíces. De los 5 fallos, CUATRO daban el mismo mensaje —
«error: no suitable Python interpreter found»— en wireplumber, xdg-desktop-portal,
xdg-desktop-portal-cosmic y cosmic-settings-daemon.

NO ERA DE ELLAS, Y NI SIQUIERA ERA DE MESON. El error viene de un `configure` de AUTOTOOLS que
prueba `python2.2`, `python2.1`, `python2.0` —literalmente— y aborta al no hallarlos. Se
identificó por las flags del comando en el log (`--disable-mpeg --disable-full-suite`), que son
inconfundibles de **libsndfile**: una dep común a las cuatro.

Y declarar `python3` en `[deps]` NO ayuda —dos de las cuatro ya lo tenían—: el problema no es
que falte el intérprete, es que el script no reconoce ese NOMBRE. Autoconf respeta la variable
de entorno `PYTHON`, así que se le pasa `PYTHON=python3` y sella.

LA LECCIÓN, que hoy ya se repitió con `dbus-shared`: cuando varias recetas fallan con el MISMO
mensaje raro, el fallo está en una dep común — y **la forma de identificarla es mirar QUÉ
configure/meson.build lo emite**, no la receta que se estaba construyendo. Las flags del comando
y el número de línea del meson.build son la firma del paquete que realmente muere.

Aplicado a las tres copias (incoming-gnome, -cosmic, -kde), que dan el mismo hash. Y de paso
`cosmic-settings-daemon` sí necesitaba `python3` en deps, que no lo declaraba: dos causas
distintas bajo un mensaje idéntico.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:17:01 -04:00
sergioandClaude Opus 5 0f1f2e9ef6 dbus-shared: la misma flag ausente tumbaba GNOME y COSMIC enteros
`mutter`, `gnome-shell` y `xdg-desktop-portal-gnome` fallaban las tres con «ERROR: Unhandled
python exception». Las tres YA tenían `--wrap-mode=nodownload` en su propia receta, así que no
era eso — hasta que el log mostró la línea culpable:

    meson.build:372:11: ERROR: Unhandled python exception
    <urlopen error unknown url type: https>
    WARNING: failed to download with error: name 'ssl' is not defined

La misma línea 372 que el fallo de `dbus` de hace unas horas. No fallaban ellas: fallaba una
DEP que arrastran, `dbus-shared`, cuya sombra en incoming-gnome e incoming-cosmic no tenía la
flag. La canónica del corpus y la sombra de KDE sí la tenían; estas dos no.

⇒ UNA flag ausente en una sombra tumbaba TRES raíces de perfil en GNOME y bloqueaba COSMIC de
paso. Y el mensaje nunca nombra la red: «Unhandled python exception» se lee como un bug de
meson y es que el sandbox es hermético a propósito.

Lección de método: cuando tres recetas fallan con el MISMO error y las tres ya tienen el
arreglo obvio, el fallo está en una dep común, no en ellas. La línea de meson.build en el
mensaje es lo que lo identifica — es la firma del paquete que realmente muere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 16:46:40 -04:00
sergioandClaude Opus 5 c56fd373a1 etapa 4: tanda de 30 HOJAS — cero daño colateral, que era el punto
Primera tanda con el orden nuevo (por impacto en el grafo, no alfabético). 30 recetas C de
clase HOJA: bash, coreutils, git, gnupg, grep, gzip, jq, e2fsprogs, libarchive, htop…

LA MEDIDA QUE JUSTIFICA EL CAMBIO DE ORDEN: activar estas 30 dejó 41 recetas sin artefacto
vigente = las 12 que ya estaban en deuda + las 30 tocadas (una ya estaba entre las 12). **Cero
colateral.** Contra la tanda anterior, donde tocar TRES bibliotecas base (expat, zstd, ncurses)
dejó 54 sin artefacto y tumbó la cadena wlroots/sway entera.

Mismo esfuerzo de build, diez veces menos destrozo. El orden no era un detalle de comodidad.

Selección: clase `c` + `dependientes_total == 0` + estado sellado, excluyendo las de tawasuyu
(git privado ⇒ no construyen en el worker, que es sin secretos por diseño) y las Go/Rust, que
necesitan sus módulos y fallan ahí. Quedan 73 hojas C elegibles; van 30.

La tanda corre DESASIDA con nohup en el worker — la lección de ayer, cuando un drenaje murió a
las 14 de 54 al caerse la sesión ssh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 11:14:37 -04:00
sergioandClaude Opus 5 15e34cb257 dbus: una flag ausente bloqueaba TRES perfiles — --wrap-mode=nodownload
`base`, `cli` y `escritorio-sway` bajaron a 50/51, 73/74 y 120/121 después de la cascada del
split, y los tres fallaban por LA MISMA receta: dbus.

EL ERROR NO MENCIONABA LA RED POR NINGÚN LADO:
    meson.build:372:11: ERROR: Unhandled python exception
Y arriba, enterrado entre reintentos:
    <urlopen error unknown url type: https>
    WARNING: failed to download with error: name 'ssl' is not defined
dbus declara subproyectos con `.wrap` y meson intenta DESCARGARLOS. En el sandbox no hay red y
python no trae ssl ⇒ «Unhandled python exception», que se lee como un bug de meson y es
simplemente que no hay salida a internet. Y no debe haberla: el build es hermético a propósito.

`--wrap-mode=nodownload` obliga a usar las deps del sistema (nuestros artefactos, vía
pkg-config) y deja los wraps inertes. Mismo caso que `wl-clipboard` anoche.

⇒ base 51/51 · cli 74/74 · escritorio-sway 121/121. Los tres cierran otra vez.

ESTADO DE LA REPARACIÓN DE LA CASCADA: de las 54 que quedaron sin artefacto, 42 reconstruidas.
Las 12 restantes son EXACTAMENTE las que ya estaban en deuda antes de la campaña:
  · 6 el muro del PIC (gtk4 y su cadena estática — duplicado superado de la dinámica de GNOME);
  · 3 HUB-ONLY (mirada-*, llimphi-counter: git privado / commit en ceros);
  · dwarves (libdw/musl), y adwaita-hello (error 39 = la carrera del ADR 0012).
O sea que la campaña no dejó deuda nueva.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 10:58:05 -04:00
sergioandClaude Opus 5 b344c5803b etapa 4: primera tanda desplegada y verificada (3/3) + el desplegador por tandas
zstd 12M→2M (−83%) · ncurses 6M→2M (−67%) · expat 2M→1M (−50%). Cero ficheros vacíos y
**las tres REPRODUCEN**. La maquinaria de la etapa 4 queda validada de punta a punta:
activar → construir en la granja → verificar con why-differs.

LA DEP DE binutils VA EXPLÍCITA, y es la decisión de diseño de esta etapa. El paso de strip usa
`strip --strip-debug -D` de binutils. Se podría hacer que el lab lo materialice solo, sin tocar
las recetas — pero entonces la VERSIÓN de binutils sería un input INVISIBLE: dos corridas con
binutils distintos darían artefactos distintos con el mismo hash. Los `deps` sí entran en
`hash_inputs`, así que declararlo es lo único que mantiene el invariante. Cuesta una edición
mecánica por receta; el invariante no se negocia por comodidad.

EXCLUSIONES, y son exactamente dos: `binutils` y `make`. binutils provee el strip y depende de
make ⇒ activarles el split los haría necesitarse a sí mismos para construirse. No es preferencia,
es la circularidad. (Las recetas CERRADAS POR DECISIÓN tampoco se tocan.)

POR TANDAS Y NO DE GOLPE: activar re-hashea la receta a propósito y en cascada todo lo que
dependa de ella. Hacerlo sobre las 775 candidatas a la vez dejaría el corpus entero sin sellar
al mismo tiempo — días de granja antes de poder verificar NADA, y el disco aguantando artefactos
viejos y nuevos a la vez. Por tandas se mide, se verifica y se poda entre medias, que es lo que
hace la campaña reversible.

La primera tanda NO se tomó del orden alfabético que propone el script: ésas son CLIs Go/Rust
que ya vimos que no reconstruyen en el worker (necesitan red para sus módulos), y validar la
maquinaria con recetas que fallan por otro motivo no habría probado nada. Se eligieron tres
paquetes C con artefacto presente. Para las tandas grandes hay que resolver antes el acceso a
red de los módulos Go/Rust, o restringirse a lo que reconstruye.

Estado: 775 candidatas, 5 desplegadas (bison y appstream del piloto + estas 3).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 00:53:26 -04:00
sergioandClaude Opus 5 fbd586f9b2 etapa 2: el split de debug FUNCIONA — y el piloto destruyó los artefactos antes de lograrlo
RESULTADO, con los tres criterios medidos a la vez sobre las dos recetas que divergían:
  bison      6M → 3M  · 0 ficheros vacíos · 0 secciones .debug_ · «bison (GNU Bison) 3.8.2»
  appstream 56M → 18M · 0 ficheros vacíos · 0 secciones .debug_ · «AppStream version: 1.0.5»
  y las DOS pasan de DIVERGIR a REPRODUCIR.

O sea que un solo cambio recupera espacio Y cierra la fuga de reproducibilidad, como predijo el
§1.bis. Pero se llegó ahí después de tres errores que conviene dejar escritos.

🧨 1. `zig objcopy --strip-debug X X` (mismo fichero de entrada y salida) TRUNCA EL FICHERO A 0
BYTES. Destruyó los artefactos del piloto — y lo grave es que LOS TRES INDICADORES DECÍAN QUE
IBA BIEN: el tamaño cayó 84% (porque los ficheros quedaron vacíos), `why-differs` dijo REPRODUCE
(porque dos árboles vacíos son idénticos) y no quedaban secciones .debug_ (porque no quedaba
ninguna sección). Se cazó al EJECUTAR el binario: 0 bytes.
⇒ La verificación de un artefacto tiene que incluir que SIGA FUNCIONANDO, no sólo que pese menos
y reproduzca. Un artefacto vacío cumple las dos y no sirve para nada. Es la lección de esta
campaña aplicada a la campaña misma: una métrica que parece éxito.

2. Al arreglarlo con fichero temporal, el strip pasó a ser un NO-OP SILENCIOSO: los binarios
quedaban intactos y el tamaño no bajaba, porque no se pudo confirmar que `zig objcopy` acepte
`--strip-debug`. Cambiado al `strip` de binutils, que sí funciona, a costa de declarar la dep.
⇒ Preferible una dep explícita que funciona a una comodidad que no se sabe si hace algo.

3. Con el strip real, apareció una fuga NUEVA: los artefactos seguían divergiendo, ahora por la
CABECERA `ar` de los `.a` — `strip` los reescribe con los timestamps de cada corrida. Lo nombró
`why-differs` exacto («archivar en modo determinista»). Arreglado con `strip -D`
(= --enable-deterministic-archives). ⇒ Arreglar media causa deja el invariante igual de roto: el
debug ya no divergía y el archivo sí.

DISEÑO: `strip_debug` es un campo de la receta que ENTRA en `hash_inputs` y sólo si está fijado
(mismo patrón que `zig_version`). Las dos mitades importan y están clavadas en un test: si no
entrara, el lab cambiaría el contenido del artefacto sin mover el hash y el store MENTIRÍA; y al
entrar sólo si está fijado, se despliega receta a receta sin re-hashear las 1161 de golpe —
verificado: con el campo añadido al código, los 1161 hashes existentes NO se movieron.

Va como paso del lab y no en la fase install de cada receta porque 383 de las 1161 no tienen
install explícita: meterlo receta a receta obligaría a escribir a mano ese install por defecto en
las 383, con riesgo de no clavarlo exacto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 00:31:35 -04:00
sergioandClaude Opus 5 6e27721098 sombras -shared: 8 promovidas al corpus, 0 hashes movidos — y cairo se para en un muro real
Decisión del usuario: promover una por una, verificando. Hecho así, y valió la pena.

EL RIESGO QUE SE TEMÍA NO APLICABA. La nota de `granja-promote-colisiones` advierte que
promover a ciegas hace que variantes homónimas pisen recetas canónicas. Acá no: los nombres
`*-shared` son DISTINTOS de los canónicos —`zlib-shared` no pisa a `zlib`— y se comprobó que
ninguna de las 19 sombras del catálogo choca con un nombre del corpus. Decirlo importa: repetir
una advertencia donde no aplica es tan malo como ignorarla donde sí.

VERIFICADO EMPÍRICAMENTE, no razonado: se calcularon los hashes de LAS 1153 recetas antes y
después de promover. **Ninguna cambió**, y las 8 nuevas tienen hash idéntico a su original ⇒ sus
artefactos ya están sellados y no hay que reconstruir nada. Promovidas: zlib, libpng, freetype,
fontconfig, libjpeg-turbo, libtiff, libxml2 y libyaml (todas en su variante -shared).

 `cairo-shared` NO SE PROMOVIÓ, y el porqué es el hallazgo: su hash SÍ cambia al moverla,
porque una de sus deps —`glib`— resuelve distinto desde el corpus. En `incoming-gnome-onda2`
hay una SOMBRA DE GLIB CON EL MISMO NOMBRE que la canónica (`link=dynamic`,
`-Ddefault_library=both` en vez de static), y `cairo-shared` está construida contra ella.

⇒ O sea que el riesgo de homónimos SÍ existe, pero un nivel más abajo del que se miraba: no en
las `-shared`, sino en el `glib` sombra. Promoverlo pisaría la receta canónica de glib y
re-hashearía en silencio todo lo que cuelga de ella, que es medio catálogo. El camino limpio es
renombrarla a `glib-shared` —nombre que YA existe en incoming-cosmic— y reapuntar cairo-shared;
eso cambia hashes de la cadena GNOME y merece su propia decisión, no colarla acá de madrugada.

Con esto la cadena estática del corpus (gtk4 y las 6 que cuelgan) sigue bloqueada, pero ahora se
sabe exactamente por qué y cuál es el siguiente paso concreto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 23:33:32 -04:00
sergioandClaude Opus 5 d70098ade5 GTK3: decisión del usuario — NO entra; gnome-session, gsd y gdm quedan CERRADAS por escrito
Decidido el 2026-08-08. Las tres recetas dependen de GTK3 (gnome-settings-daemon además de
gtk+-x11-3.0) y quedan cerradas, **no «en deuda»**. La diferencia no es semántica: una deuda
invita a reintentarla en cada informe y en cada ciclo de granja; una decisión se respeta.

POR QUÉ ES RAZONABLE Y NO UNA RENDICIÓN:
· GNOME funciona sin esto — el camino vivo es mutter → gnome-shell arrancado por arje, y
  ninguno de los dos depende de gnome-session (su cierre es mutter+gjs+gobject-introspection+
  gnome-desktop).
· Traer GTK3 sería una torre de C muerta —y para waybar además gtkmm, sus bindings de C++—
  por un gestor de sesión y un login manager que este escritorio no necesita.
· La función está cubierta: la sesión la levanta arje y la barra de estado es `yambar`, C puro,
  sellado anoche en el frente wlr.
Se reabre si alguien trae GTK3 por otro motivo con peso propio (una app gráfica que lo exija).

EL MARCADOR VIVE EN LA RECETA, NO EN UNA LISTA APARTE. `build-farm.sh` salta las recetas cuya
cabecera dice «CERRADA POR DECISIÓN». Podría haber hecho un fichero de exclusiones, pero una
lista y una cabecera se desincronizan solas: así, quien lee el porqué y quien decide saltarla
miran EL MISMO TEXTO. Y el drenador lo dice en su salida, para que la decisión sea visible en
vez de silenciosa.

Con esto la granja deja de quemar CPU en tres recetas que nadie va a arreglar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 23:31:08 -04:00
sergioandClaude Opus 5 427ca1dd01 wl-clipboard 2.2.1 SELLADA — cierra el entorno mínimo del frente wlr
`wl-copy` y `wl-paste`. En Wayland no hay forma de copiar o pegar desde un script sin esto, y
varios flujos que ya sellamos lo necesitan para ser útiles: capturar con grim, seleccionar
región con slurp, y poder pegar el resultado en algún lado.

UNA DIFERENCIA CON LAS ANTERIORES QUE CONVIENE NOTAR. Va por commit (ADR 0006, GitHub genera
sus archive/refs/tags al vuelo igual que codeberg), pero este tag es **LIGERO**: `git ls-remote`
devuelve UN solo sha y ése ES el commit. En fuzzel, yambar y foot los tags son ANOTADOS y
aparecen dos, donde el bueno es el `^{}`. Comprobar el tipo de tag antes de copiar evita pinear
el objeto del tag en vez del commit — un error silencioso, porque la receta valida y sólo falla
después, al no encontrar el árbol.

TRAE `subprojects/` CON WRAPS QUE BAJAN DE LA RED (expat, libffi, wayland, wayland-protocols).
`--wrap-mode=nodownload` es lo que lo impide y lo obliga a usar las del sistema, o sea nuestros
artefactos vía pkg-config. Sin esa flag el build intentaría salir a internet DENTRO del sandbox
hermético y moriría; con ella los wraps quedan inertes. Por eso las cuatro van en deps.

Estado del frente wlr: 10 recetas, 12 binarios — wlroots, sway (+swaybar/swaymsg/swaynag),
swaybg, swayidle, swaylock, grim, slurp, fuzzel, yambar, wl-copy/wl-paste. Con `foot` (ya en el
corpus) como terminal, el perfil «escritorio-sway» es armable: compositor, barra, lanzador,
fondo, bloqueo, inactividad, captura y portapapeles. Todo sin X11 y sin systemd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 21:54:53 -04:00
sergioandClaude Opus 5 3545ea77fa fuzzel + yambar SELLADAS — y waybar queda fuera POR MEDICIÓN, no por gusto
Lanzador y barra de estado: las dos piezas que faltaban para que un WM ligero se use de
verdad y no sólo arranque. El frente wlr suma 9 recetas y 11 binarios.

WAYBAR NO ENTRA, Y CONVIENE QUE CONSTE POR QUÉ. Medidas sus dependencias reales (0.12.0):
pide `gtkmm-3.0` — o sea GTK**3** *y además* sus bindings de C++ — más gtk-layer-shell,
jsoncpp, fmt, spdlog, sigc++, date y libnl. Es el mismo muro de GTK3 que el frente GNOME
aparcó a propósito, con ocho recetas nuevas encima. `yambar` es C puro, del mismo autor que
foot/fcft/fuzzel, y **sus dependencias ya estaban selladas**: es el bar estándar de esa
familia, no un sustituto pobre. Si algún día entra GTK3, waybar vuelve a estar sobre la mesa.

FUENTE GIT POR COMMIT, no el archive de codeberg (ADR 0006). Es el criterio que ya usan foot y
fcft, con la razón medida allí: el `/archive/<tag>.tar.gz` lo genera la forge AL VUELO, así que
sus bytes dependen de la versión de git/gzip del servidor y el sha256 pineado se muere solo
cuando codeberg actualiza su tooling. ⚠ Y los tags son ANOTADOS: el sha de `refs/tags/1.12.0`
NO es el commit; hay que pinear el `^{}` desreferenciado. Lo verifiqué con `git ls-remote`
antes de escribir, porque los dos shas aparecen juntos y es fácil copiar el equivocado.

DOS DEPS DE BUILD-TIME QUE NO SE VEN EN EL BINARIO Y MATAN EL BUILD:
· `scdoc` (las dos) — generan sus man pages con él y, a diferencia de sway/swaybg/grim, NO
  ofrecen `-Dman-pages` para saltárselo. Estaba en el corpus: la vía correcta es declararlo,
  no buscar cómo evitarlo.
· `flex` + `bison` (yambar) — generan el parser de su lenguaje de partículas. Invisibles en el
  resultado, y del tipo que se olvida hasta que el build muere con «Program 'flex' not found».

`-Dbackend-x11=disabled` en yambar evita SIETE dependencias de X11 (xcb-aux, xcb-cursor,
xcb-errors, xcb-event, xcb-ewmh, xcb-randr, xcb-render) para un backend que en una distro
Wayland pura no se usa nunca. Y en fuzzel, `-Dsvg-backend=nanosvg` (el default, vendorizado)
evita librsvg, que arrastraría la cadena de GNOME/Rust por unos iconos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 21:39:24 -04:00
sergioandClaude Opus 5 50b080d258 accesorios wlroots: swaybg, swayidle, swaylock, grim y slurp — 5 selladas
Con esto el frente de WMs ligeros pasa de «hay un compositor» a «se puede usar»: fondo de
escritorio, apagado por inactividad, bloqueo de pantalla y captura por región. Sumado a sway
y wlroots, el frente tiene hoy 7 recetas y 9 binarios (sway swaybar swaymsg swaynag swaybg
swayidle swaylock grim slurp), todos sin X11 y sin systemd.

CADA FALLO FUE DE UNA CLASE DISTINTA, y ninguno del paquete que estaba escribiendo:

· swayidle — pasé `-Dsd-bus-provider=none` porque es la opción que tiene SWAY. En swayidle
  1.9.0 no existe: la suya se llama `logind`. **Las perillas no se heredan entre proyectos
  hermanos aunque resuelvan lo mismo**; hay que leer el meson_options.txt de CADA uno. Tercera
  vez hoy que una opción inventada mata un build (gnome-session, y ahora ésta).

· grim — `undefined symbol: crc32`, o sea zlib. No lo usa grim: lo usa `libpng`, cuyo `.pc`
  declara zlib en `Requires.private`, la línea que pkg-config sólo expande en resolución
  estática. Es la MISMA causa que el `-lexpat` de sway (allí vía fontconfig), sobre otra
  biblioteca. Ya van dos casos ⇒ es un patrón del corpus, no un accidente: enlazar contra una
  lib estática cuyo .pc tiene Requires.private exige añadir esas libs a mano.

· slurp — le faltaba `libxkbcommon` en deps, sin más.

DOS COSAS QUE VALE GUARDAR:

1. EL TARBALL DE slurp CASI ENVENENA LA RECETA. La URL de releases de GitLab devolvió **HTTP
   200 con una página HTML** en vez del archivo, así que `curl -f` no falló y el sha256sum que
   saqué era el de un documento HTML. Pinearlo habría dado una receta que descarga «algo» con
   hash correcto y no construye jamás, con un error incomprensible. Se cazó verificando el
   FORMATO (`tar tzf`), no el código de salida — misma lección que el `rm` que devuelve 0 sin
   borrar y el proceso vivo que no avanza. El sha256 correcto es el del tarball de GitHub.

2. PAM NO ESTABA BLOQUEADO. «PAM» figura entre las tres deudas aparcadas del frente GNOME, así
   que lo natural era dar swaylock por imposible. Pero en swaylock PAM es una OPCIÓN (cae a
   `crypt` si falta) y sobre todo **`linux-pam` SÍ está en el corpus**: lo aparcado era la
   integración de PAM del stack GNOME, no la biblioteca. ⇒ Una deuda «aparcada» nombra un
   frente concreto, no una palabra; conviene verificar antes de heredar el veto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 21:12:24 -04:00
sergioandClaude Opus 5 f7aafc9d26 sway 1.10 SELLADA — primer compositor Wayland ligero del catálogo
b3:ae10753a5467f3642464aaad0b1b41135b8c51972091042baf0d09134af46c7e · sway, swaybar,
swaymsg y swaynag. Construido en el worker sobre el wlroots 0.18.2 sellado hace un rato
(sway 1.10 pide `wlroots-0.18` explícitamente: no es una versión cualquiera, es LA que casa).

XWAYLAND SE HEREDA, NO SE PASA. sway no tiene perilla propia: lee las variables del .pc de
wlroots (`xcb = wlroots_features['xwayland'] ? dependency('xcb') : null_dep`). Como nuestro
wlroots se selló con have_xwayland=false, sway se salta xcb y xcb-icccm solo. Para eso servía
haber inspeccionado esas variables al verificar el artefacto anterior. Si algún día se quiere
Xwayland, se cambia en wlroots, no acá.

TRES COSAS QUE SALIERON AL CONSTRUIR, y ninguna estaba en sway:

1. `json-c` NO DECLARABA NADA y sellaba igual — porque el rootfs del laptop trae cmake y make
   y el sandbox los tomaba «de prestado». En el worker muere con `cmake: not found` (exit 127,
   el mismo código que engaña con meson). Una receta que sólo construye en la máquina del autor
   es una bomba de relojería. Declarados cmake+make+pkgconf; el arreglo es declarar, NUNCA
   engordar el rootfs del worker. Re-hashea json-c y su único dependiente, que es sway: gratis.
   Salió a la luz construyendo sway, o sea que el fallo estaba a tres niveles de donde miraba.

2. 45 SÍMBOLOS INDEFINIDOS AL ENLAZAR, todos `XML_*` — expat, y sólo expat (clasificada la
   lista completa, no adivinado por el primero). sway no usa expat: lo usa `fontconfig`, que en
   el corpus es estático, y su `.a` trae las llamadas sin resolver. `fontconfig.pc` lo declara
   en `Requires.private`, que es la línea que pkg-config sólo expande en resolución ESTÁTICA, y
   meson llama a dependency('cairo') en modo normal. No es un fallo de fontconfig ni de cairo:
   es donde el modelo de pkg-config y el enlace estático no encajan. Resuelto con
   `-Dc_link_args=-lexpat`; como los indefinidos eran de UN solo prefijo, se sabe que no falta
   nada más.

3. `-Dtray=disabled` esquiva la única dep que nos falta de verdad. La bandeja habla sd-bus y
   sway ofrece tres proveedores (libsystemd | libelogind | basu): no hay systemd por decisión,
   `basu` no está en el catálogo, y el `libelogind` que sí tenemos NO sirve — es la
   reimplementación propia de la C-ABI sd-login de arje-compat, no un sd-bus. Misma trampa del
   nombre que ya mordió con `knighttime`. Traer basu es un ticket aparte y pequeño.

Con esto el frente de WMs ligeros deja de ser un hueco: había 0 compositores y ahora hay uno
que arranca desde wlroots propio, sin X11 y sin systemd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:48:55 -04:00
sergioandClaude Opus 5 c13d25f02b wlroots 0.18.2 SELLADA — el desbloqueo del frente de WMs Wayland ligeros
Primera receta del frente nuevo. wlroots es la biblioteca de la que cuelgan casi todos los
compositores ligeros (sway, labwc, river, hyprland): sin ella no hay ninguno, y con ella los
compositores salen baratos porque no traen torre de C debajo. Hasta hoy el catálogo tenía de
toda esa familia sólo `foot` y `mako`.

b3:9b56bac0d00977524072cf393ecc9f0b847fcc1a0bfba9d9c4d57fd9a4a74b44 · 13 MB · construida en
el worker (cadena GUI: no se rebuildea en el laptop por el zig-skew).

COLA AISLADA `incoming-wlr/`, porque otro agente comparte el repo. Como la resolución de deps
es hermano→padre, una receta de acá ve el corpus pero NO ve `incoming-gnome/`, donde viven
`libdisplay-info` y `hwdata` ⇒ se copian como hermanas. No duplica trabajo: los ficheros son
idénticos, el ArtifactHash es el mismo y el build hace cache-hit. Verificado antes de
construir: ambas dan el mismo b3: que su original.

LAS OPCIONES, VERIFICADAS CONTRA EL meson_options.txt DEL TARBALL, no de memoria — porque una
opción `-D` inventada NO se ignora: meson aborta en la primera desconocida. Es exactamente lo
que tenía muerta a gnome-session con su `-Dsystemd=false`. El `.pc` resultante confirma que
salió lo pedido: have_drm_backend=true, have_libinput_backend=true, have_gles2_renderer=true,
have_gbm_allocator=true, have_session=true, y have_x11_backend / have_xwayland en FALSE —
la distro es Wayland pura y X11 es deuda aparcada por diseño; no entra por la puerta de atrás.

DOS COSAS QUE COSTARON UNA ITERACIÓN CADA UNA:
· `-Dwerror=false` no es pereza, es DIFERENCIA DE TOOLCHAIN. wlroots compila con -Werror y
  upstream lo valida con gcc; nuestro `zig cc` es clang y avisa de lo que gcc calla. Murió en
  `types/wlr_shm.c:503` por `-Wunused-but-set-variable` sobre has_argb8888/has_xrgb8888 —
  código correcto que sólo clang señala. Tratar los avisos de OTRO compilador como errores del
  proyecto es un fallo de método, no un hallazgo de calidad.
· `libffi`, `expat` y `zlib` en deps aunque wlroots no los use: los exigen los `.pc` de wayland
  y mesa en su `Requires`, y pkgconf resuelve el grafo de .pc completo antes de dar un cflag.
  Misma lección que wlr-randr esta mañana.

El artefacto trae `libwlroots-0.18.{a,so}`, su .pc y 111 cabeceras bajo
`usr/include/wlroots-0.18/wlr/`. Listo para que sway enlace contra él.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:24:34 -04:00
sergioandClaude Opus 5 129aff9dd9 granja: el worker rehacía 87 de 90 recetas por ciclo — el sync del store es de UN SOLO SENTIDO
Fui a mirar los fallos de la granja y el número no cuadraba: de 20 «fallos» de incoming-gnome,
17 correspondían a recetas CON ARTEFACTO VIGENTE en el hub. Primero pensé que eran marcadores
`.fail` rancios, y me equivoqué: `build-farm.sh` hace `rm -rf "$FARM"` al empezar cada ciclo,
así que el status es siempre del ciclo actual. Después pensé que el worker corría recetas
viejas, y también me equivoqué: comparados los md5, hub y worker tienen copias IDÉNTICAS.

LA CAUSA REAL está en `farm-sync.sh`: sube el código EXCLUYENDO /store y baja el store del
worker. El store viaja worker→hub y nunca hub→worker. O sea que **el worker no se entera de
nada de lo que se sella en el hub**, y cada ciclo reintenta —y vuelve a fallar— trabajo ya
hecho. Medido: de las 90 recetas de incoming-gnome, **87 ya están selladas en el hub**. El
worker estaba quemando el 97% de esa cola en repetir lo hecho.

EL ARREGLO no es mandarle los artefactos (gigabytes por un enlace de 8 Mbps) sino la LISTA:
`work/farm-sellados.txt` son los nombres `<hash>-<paquete>` del store del hub, unos kilobytes.
`farm-sync.sh` la regenera en cada sync (para que no envejezca sola) y `build-farm.sh` salta
la receta cuyo hash vigente ya esté ahí. Ojo al detalle de rsync: la subida excluye /work, así
que el manifiesto necesita un `--include` ANTES del `--exclude` — rsync aplica la primera regla
que casa, y sin ese orden el fichero no viajaba y el arreglo no habría hecho nada en silencio.

Y LAS TRES DEUDAS REALES DE LA GRANJA SON UNA. gnome-session, gnome-settings-daemon y gdm
mueren todas en GTK3, que el frente GNOME aparcó a propósito. Verificado contra el meson.build
de cada tag: gnome-session 48.0 lo pide incondicional; gnome-settings-daemon 48.1 pide gtk+-3.0
Y gtk+-x11-3.0 (dos de las tres deudas aparcadas, no una); gdm 48.0 lo tiene condicionado a
`if have_xdmcp` pero da igual, porque muere construyendo gnome-session.

De paso, gnome-session pasaba `-Dsystemd=false -Dsystemd_journal=false`, dos opciones que NO
EXISTEN en 48.0 (sus opciones reales son seis: deprecation_flags, session_selector,
systemduserunitdir, docbook, man, x11). Meson aborta en la primera opción desconocida, así que
la receta ni llegaba a configurar y el fallo real quedaba tapado. Corregido, más -Dx11=false.
El diagnóstico de fondo YA ESTABA en la receta desde el 2026-07-27 y era más completo que el
mío (dice GTK3 **y** libsystemd); quité la nota duplicada que había añadido.

Y en el respaldo: la comprobación inicial de SSH era de un solo intento y tiró la corrida al
relanzarlo llegando a casa, con el wifi aún sin levantar. Es el peor momento para rendirse —
quien relanza un respaldo interrumpido acaba de cambiar de red. Ahora reintenta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:27:29 -04:00
sergioandClaude Opus 5 2c2ee951d6 licencias: resolver -only vs -or-later CON LA CITA — y tres «pruebas» que no probaban nada
Los identificadores obsoletos (`GPL-2.0`, `LGPL-2.1`, `AGPL-3.0`) no dicen si el proyecto
concede «sólo esta versión» o «ésta o cualquier posterior», y eso decide con qué se puede
combinar el paquete y bajo qué términos puede redistribuirlo quien lo reciba.

NO SE PUEDE MIRAR EL COPYING, que es la trampa evidente: el texto de la GPL es IDÉNTICO en
los dos casos —es la licencia, no la concesión— y encima su apéndice «cómo aplicar la
licencia» contiene literalmente «or (at your option) any later version», así que buscarla
ahí da SIEMPRE positivo y parece evidencia siendo plantilla. Misma trampa que la regla del
`.a` no-PIC, donde grep contaba reubicaciones de `.debug_*`. La concesión vive en las
cabeceras de los fuentes y en el README; ahí se busca, excluyendo los ficheros de licencia.

SE GUARDA LA CITA, no sólo el veredicto: fichero + frase exacta que decidió. Una licencia es
una afirmación legal y quien la revise tiene que poder ver POR QUÉ dice lo que dice sin
repetir el trabajo.

Y eso pagó de inmediato: de las 12 primeras resoluciones, TRES eran evidencia inválida y
sólo se vieron porque estaba la cita. Cada una de una clase distinta:
 · caligula   — la frase salía de `checks/headless/expected.iso`, un FIXTURE DE TEST;
 · libnl      — de `include/linux-private/linux/seg6.h`, una cabecera del KERNEL vendorizada.
                La licencia del kernel no es la de libnl;
 · lm-sensors — declarado GPL-2.0 y la cita decía «version 2.1 of the License», que es la
                LGPL. La frase era real pero no sostenía lo que se le atribuía.

Las tres clases quedan filtradas en el script: ficheros de licencia, rutas de test/fixture, y
código vendorizado; más una comprobación nueva de que **la cita hable de la misma versión que
la licencia declarada**.

Sembradas las 9 auditadas (13 ficheros; algunas viven en varias colas). Ambiguas: 55 → 41.
Las 40 sin evidencia quedan listadas y siguen contadas — la mayoría son Go y la familia
cosmic, donde la frase no aparece fuera del COPYING.

Hashes verificados: 0 movidos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:15:20 -04:00
sergioandClaude Opus 5 be4e91086c licencias: el texto dentro de la imagen, con veto — y el SDD 19/20 se equivocaba de sitio
═══ LA CORRECCIÓN DE FONDO: no es `pack`, son las IMÁGENES ═══
El SDD 19 y el 20 decían «inyectar el texto de la licencia en `hammer pack`, que es aguas
abajo del ArtifactHash y sale gratis». La conclusión sobre el coste era correcta pero el
SITIO estaba mal, y por una razón que sólo se ve leyendo el código: `pack` produce un
`.swm`, que es una RECETA DE TRANSFORMACIÓN sobre fuente pública y por diseño explícito
«NUNCA transporta binarios cocidos»; e `install` REPRODUCE construyendo, con cache-hit del
corpus, en vez de bajar binarios. O sea que **el canal de paquetes de hammer no distribuye
binarios** y la obligación de acompañar-el-binario ahí casi no aplica.

Donde sí aplica, con toda su fuerza, es en la IMAGEN INSTALABLE: su rootfs se puebla con
`hammer install` sobre un prefix y de ahí sale un USB/ISO lleno de binarios que se le da a
alguien. Ése es el punto de entrega. (La obligación del espejo de FUENTES es otra y sigue
entera: las recetas apuntan a URLs de terceros que se caen.)

═══ LO QUE ENTRA ═══
· `scripts/licencias-textos.sh` + `licenses/`: los 44 textos CANÓNICOS de SPDX
  (spdx/license-list-data), uno por identificador realmente en uso — la lista se saca de las
  recetas, no de un fichero escrito a mano que se desincroniza. 764K.
· `scripts/licencias-rootfs.sh`: escribe /usr/share/licenses/<pkg>/ con el texto de CADA
  licencia de la expresión (`Unlicense OR MIT` ⇒ los DOS textos: en un OR el destinatario
  elige, y entregar uno solo le quita una opción que el autor le concedió), más un
  MANIFEST.tsv. Medido: 1,0 MB para el perfil `cli` de 44 paquetes.

═══ Y SOBRE TODO: ES UN VETO ═══
Sale ≠0 si algún paquete del rootfs no declara licencia. Un informe que no puede bloquear no
cambia lo que ocurre; esto corta el paso. Y se ganó el sueldo al primer intento: reveló que
los perfiles `base` y `cli` —los dos más simples, los que primero se publicarían— llevaban
10 y 12 paquetes con binarios y licencia desconocida. Curados los 10 que se podían afirmar
(doas, iputils, less, mandoc, procps-ng, rsync, strace, sudo, tree, usbutils); ambos perfiles
bajan a 2.

Esos 2 NO se rellenan a propósito, y quedan vetando:
 · lsof   — licencia propia del proyecto, sin identificador SPDX. Toca `LicenseRef-lsof` con
            su texto, que ya viaja en el tarball (la receta instala su COPYING).
 · tzdata — dominio público por declaración de sus autores, y SPDX no tiene identificador
            para eso (es una AUSENCIA de licencia, no una licencia). Toca
            `LicenseRef-PublicDomain` documentado, no forzar uno de la lista.

De paso: normalizado `GPL-3.0+` (sufijo antiguo) a `GPL-3.0-or-later`, misma equivalencia
documentada que la barra de Cargo. Y el caso especial que puse para `Linux-syscall-note`
(«las excepciones viven en exceptions/») era una suposición razonable y FALSA: SPDX las
publica en el mismo `text/`; ese directorio no existe.

1063 de 1141 (93%). Hashes verificados: 0 movidos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:53:20 -04:00
sergioandClaude Opus 5 487e155f74 licencias: leer el LICENSE de verdad cuando GitHub dice «no sé» — 1053 de 1141 (92%)
Último recurso mecánico, `scripts/licencias-texto.sh`: para las 60 recetas donde la API de
GitHub devuelve NOASSERTION (el repo TIENE un LICENSE pero licensee no lo reconoce: texto
retocado, encabezado propio, dos licencias en un fichero), baja el texto y lo clasifica acá.
34 identificadas — 16 MIT, 10 Apache-2.0, 7 BSD-2-Clause, 1 BSD-3-Clause.

SÓLO PERMISIVAS, Y NO ES PEREZA. MIT, Apache-2.0, BSD, ISC, MPL-2.0 y Unlicense se
reconocen por frases inconfundibles y NO tienen variantes -only/-or-later ⇒ reconocer el
texto da el SPDX completo.

La familia GPL se deja al humano A PROPÓSITO, y la razón es sutil: el COPYING de la GPL es
IDÉNTICO tanto si el proyecto es «sólo v3» como «v3 o posterior» — lo que las distingue vive
en las cabeceras de los fuentes. Y encima el propio COPYING incluye, en su apéndice «cómo
aplicar la licencia», la frase «or (at your option) any later version», así que buscarla da
SIEMPRE positivo y parecería evidencia siendo texto de plantilla. Es exactamente la trampa
de la regla del `.a` no-PIC, donde grep contaba reubicaciones de .debug_* y mentía.

Comprobado a mano un caso que daba mala espina: el LICENSE de `conftest` empieza
«Conftest — Write tests against your config files / Copyright (C) 2019 …», que es el formato
típico de una cabecera GPL. Leído entero, dice «Licensed under the Apache License, Version
2.0». La clasificación era correcta; la sospecha, barata.

Hashes verificados sobre las 36 tocadas: idénticos.

Quedan 88, ya sin vía mecánica: los repos donde ni la API ni el texto deciden, la familia
GPL sin desambiguar, y tarballs de sitios propios (gmplib, xiph, sr.ht, codeberg…).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:06:59 -04:00
sergioandClaude Opus 5 0225aa4e08 licencias: el código propio (tawasuyu) queda en MIT — 1017 de 1141 (89%)
Decisión del usuario: «tawasuyu está en MIT». Es la misma licencia que hammer (LICENSE en
la raíz + Cargo.toml), así que todo lo nuestro queda coherente bajo un solo término. Las 20
recetas con fuente en git.tawasuyu.net: arje-* (el init y sus compat), mirada-* (el
compositor, el greeter y mirada-ctl), netup, agora-cli, cosmos-cli, dominium-cli,
tinkuy-sim, llimphi-counter y libelogind.

⚠ ANOTADO EN LA TABLA porque es una trampa que casi pisé: `libelogind` NO es el elogind de
upstream (que sería LGPL-2.1+). Por el nombre lo parece; su cabecera dice que es una
reimplementación PROPIA de la C-ABI sd-login dentro de arje-compat. Declararla LGPL «porque
se llama así» habría sido exactamente el error que este fichero prohíbe — el tercero del
mismo tipo hoy, después de `knighttime` (parecía Go y es KDE) y de la familia `kube*`
(parecen KDE y son Go). El nombre nunca es evidencia; la fuente sí.

Hashes verificados sobre las 20: idénticos.

Quedan 124 sin declarar, ya todas de terceros.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:01:15 -04:00
sergioandClaude Opus 5 c8d1357f06 licencias: la declaración del autor manda — y arreglo 5 recetas que ROMPÍ y commiteé
Dos cosas: una mejora de calidad y un fallo mío que hay que contar entero.

═══ EL FALLO: escribí un error JSON dentro de 5 recetas y las commiteé rotas ═══
En `a6a9d59`, cinco recetas quedaron con
    license = "{"message":"Not Found","documentation_url":"...","status":"404"}"
Causa: cuando el repo da 404 (renombrado, borrado, privado), `gh api` escribe el CUERPO DEL
ERROR en stdout y el filtro `--jq` falla, así que la variable queda valiendo el JSON. Mi
`case` sólo descartaba ""/null/NOASSERTION/other ⇒ el JSON pasó como si fuera una licencia.
Las comillas ROMPEN el TOML, y esas 5 recetas dejaron de parsear y de tener ArtifactHash.

No lo vio nadie leyendo: lo cazó la comparación de hashes, al detectar que `cargo-sort`
había cambiado. Afectadas: cargo-sort, cargo-audit, cargo-binstall, waybackurls, usbutils.

La lección no es «se me pasó un caso», es que **validé los «no sé» conocidos en vez de la
FORMA del dato**. Ahora se acepta sólo lo que tiene forma de expresión SPDX, y la barrera
está en los DOS sitios que escriben (detectar y sembrar), más un GUARDIÁN en el informe que
sale ≠0 si alguna licencia tiene forma imposible. Un fallo aguas arriba disfrazado de dato
hay que verlo sin buscarlo.

Y de paso normalizadas 11 licencias con la barra antigua de Cargo (`MIT/Apache-2.0`), que
no es SPDX válido. Traducir la barra a OR no es inferir: Cargo documentó esa equivalencia.

═══ LA MEJORA: `scripts/licencias-cargo.sh` ═══
Lee la licencia que el AUTOR declara en su Cargo.toml, pidiéndola AL TAG QUE LA RECETA
PINEA (`?ref=v<version>`, con HEAD como último recurso y anotando cuál se usó). 176
obtenidas, 167 al tag exacto. Es la fuente más autoritativa de las que usamos y corrige los
dos defectos de la detección por API de una vez:
 · las licencias DOBLES dejan de colapsar: `fd` pasa de `Apache-2.0` a `MIT OR Apache-2.0`,
   `ripgrep` a `Unlicense OR MIT`, `bat` a `MIT OR Apache-2.0`;
 · los SPDX AMBIGUOS caen de 71 a 55, porque el autor sí escribe -only/-or-later.
66 corregidas, 7 nuevas. Y aparecieron cosas que la detección había perdido: `eza` es
EUPL-1.2, una copyleft europea que se habría empaquetado creyendo otra cosa.

Jerarquía de evidencia, escrita en la cabecera del script: nombre (prohibido) < familia/URL
de fuente < API de licencias de GitHub < Cargo.toml del autor.

997 de 1141 (87%). Verificación COMPLETA sobre las 77 recetas tocadas —no una muestra—:
0 hashes cambiados, y 5 que ahora parsean y en HEAD no.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:51:09 -04:00
sergioandClaude Opus 5 fe474dab63 licencias: 994 de 1141 (87%) — cerradas las familias Qt, freedesktop, kernel.org y PyPI
Tercera tanda curada, agrupada por la URL DE FUENTE de cada receta (que es la evidencia que
el nombre no da): los once módulos Qt sin prefijo, freedesktop (dbus, libinput, libevdev,
libdisplay-info, poppler, pulseaudio, upower, NetworkManager, ModemManager, polkit),
kernel.org (los tres kernels, linux-headers, git, iproute2, libuuid) y PyPI.

Los kernels llevan `GPL-2.0-only WITH Linux-syscall-note` explícita. No es adorno: sin esa
excepción, todo binario de espacio de usuario que hace un syscall sería obra derivada del
kernel. Es exactamente el tipo de dato que un campo de licencia existe para no perder.

Quedan 147 sin declarar: 69 de GitHub donde la propia API dice NOASSERTION (hay que abrir
el fuente), 20 de tawasuyu —que son nuestras o del otro agente, así que la licencia la
decide el usuario, no yo— y el resto repartido en GNOME, gitlab.freedesktop y sueltos.
Más 71 con SPDX ambiguo pendientes de desambiguar (`licencias.sh --revisar`).

Hashes verificados otra vez sobre 20 recetas tocadas: 20 idénticos, 0 cambiados.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:29:31 -04:00
sergioandClaude Opus 5 a6a9d59e4f licencias: 936 de 1141 (82%) — detección por evidencia, con sus dos imprecisiones declaradas
Esta mañana eran 0. Tras la tabla curada (228) y este paso, 936. Ninguna receta se
re-hasheó: verificado en 30 de las 708 tocadas, 30 hashes idénticos, 0 cambiados.

DE DÓNDE SALE EL DATO, Y POR QUÉ NO ES ADIVINAR. `scripts/licencias-detectar.sh` consulta
la API /repos/{o}/{r}/license de GitHub para las 673 recetas cuya fuente vive allí. Eso
devuelve el resultado de DETECTAR el fichero LICENSE que el repo tiene de verdad
(licensee), no una etiqueta escrita a mano en una web: es la misma evidencia que veríamos
abriendo el tarball, obtenida sin bajar 673 tarballs por un enlace de 8 Mbps. 610 con SPDX
definido; las 63 que GitHub marca NOASSERTION/other se DESCARTAN — un «no sé» de la fuente
se propaga como hueco, no se redondea a una licencia plausible.

Las familias no-GitHub van curadas por la URL DE FUENTE, que es la evidencia que el nombre
no da. Y ahí cometí el error simétrico al que este mismo fichero advertía: había puesto
`knighttime` entre las «herramientas Go» POR SU NOMBRE, y su tarball sale de
download.kde.org — es un Framework de KDE, LGPL. Descartar por nombre falla igual que
aceptar por nombre. Corregido en la cabecera.

LAS DOS IMPRECISIONES, contables con `licencias.sh --revisar` en vez de escondidas:

 1. SPDX OBSOLETOS Y AMBIGUOS (71 recetas). GitHub devuelve `GPL-3.0`, `LGPL-2.1`,
    `AGPL-3.0`… identificadores que SPDX declaró obsoletos PRECISAMENTE porque no
    distinguen `-only` de `-or-later`, y esa diferencia decide con qué se puede combinar el
    paquete. NO se normalizan a ciegas: mapear GPL-3.0 → GPL-3.0-or-later sería inventar el
    dato que falta. Quedan marcadas para resolver mirando el fuente.

 2. LICENCIAS DOBLES COLAPSADAS. La API devuelve UNA sola licencia y muchos proyectos Rust
    son «MIT OR Apache-2.0» (p.ej. `fd` quedó como Apache-2.0). No es falso —cumplir una de
    las opciones concedidas basta— pero es incompleto. Lo resuelve el cierre estructural:
    leer el campo `license` del Cargo.toml en la fase de fetch.

Y un fallo que habría escrito basura en silencio: `licencias-detectadas.tsv` tiene TRES
columnas (añade el owner/repo consultado, para poder auditar) y el sembrador leía dos, así
que la licencia se habría llevado pegado el slug — `license = "MIT<TAB>owner/repo"`, sin
que nada lo validara. Arreglado antes de sembrar.

Quedan 205 sin licencia y 71 por desambiguar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:26:22 -04:00
sergioandClaude Opus 5 7b2489a03c deuda del corpus: las 12 no eran un problema, eran cuatro — medido en el worker
Fui a destrabar «las 12 recetas en deuda» con el diagnóstico heredado (fallan por el rootfs
del worker: python OSError en meson, find_package en cmake). Lo probé construyéndolas de
verdad y **el diagnóstico es falso**: cmake y meson corren perfectamente ahí. Son cuatro
situaciones distintas y ninguna es el rootfs.

(a) SEIS ESTÁN SUPERADAS, NO EN DEUDA. `gtk4` del corpus es link=static y enlaza
    libfontconfig.a, que tiene 1122 reubicaciones no-PIC ⇒ 13.032 errores
    `R_X86_64_64 cannot be used against local symbol`. No es una receta rota: es imposible.
    Y mientras tanto recipes/incoming-gnome/gtk4.toml (link=dynamic, deps -shared) YA ESTÁ
    SELLADA, con libadwaita y fontconfig-shared. O sea que la cadena estática del corpus
    —gtk4, libadwaita, gtksourceview y los tres hello/edit que cuelgan— es un DUPLICADO
    superado de la cadena dinámica de GNOME.
    Cerrarla no es construirla: es decidir si se promueven las sombras -shared al corpus,
    porque la resolución de deps es hermano→padre. Es una decisión de arquitectura, y hay
    aviso registrado de que promover a ciegas hace que variantes homónimas pisen recetas
    canónicas. NO la tomo yo.

(b) wlr-randr: cerrada en el commit anterior.

(c) dwarves: frente propio con muro identificado, no deuda. Nuestro elfutils entrega SÓLO
    libelf a propósito (libdw arrastra argp/obstack/fts, lo difícil en musl). No se arregla
    ampliando elfutils: de él cuelga el kernel que ya reproduce bit a bit. El camino es una
    receta aparte `elfutils-libdw`, con el patrón de las sombras -shared. Sin urgencia:
    ningún perfil pide dwarves y el kernel desactiva DEBUG_INFO_BTF justamente por su
    ausencia. Queda escrito en la cabecera de la receta, que es donde se va a leer.

(d) mirada-compositor, mirada-greeter, llimphi-counter: source por SSH a
    git.tawasuyu.net y el worker es SIN SECRETOS por diseño ⇒ HUB-ONLY estructural, no
    fallo. llimphi-counter ni siquiera llega a construir: su commit son ceros con el
    comentario «fijar al commit real». Receta sin terminar, y es de tawasuyu.

EL HALLAZGO DE FONDO: el bucle del worker sólo recorre las colas incoming-*; el corpus
(recipes/) NO está en QUEUES. Las 12 nunca se habían intentado allí. Buena parte de «la
deuda» era de ENCOLADO, no técnica — y por eso el diagnóstico heredado nunca se verificó.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:27:03 -04:00
sergioandClaude Opus 5 f24b52df18 wlr-randr: SELLADA — eran dos deps ausentes, no el rootfs del worker
Primera de las 12 recetas del corpus en deuda, cerrada. Y de paso desmiente el diagnóstico
heredado («las 12 fallan por el rootfs del worker: python OSError en meson, find_package en
cmake»). Medido hoy contra el worker, las causas son CUATRO distintas y ninguna es el
rootfs:

 · gtk4 construye ahí SIN TOCAR NADA. Nunca había fallado: el bucle del worker sólo recorre
   las colas `incoming-*` y NO incluye el corpus, así que las 12 jamás se intentaron. La
   deuda no era técnica, era de encolado.
 · wlr-randr (ésta): dos deps que faltaban en la receta.
 · dwarves: FindDWARF no halla las libs ELF/DWARF pese a declarar elfutils. Pendiente.
 · mirada-compositor, mirada-greeter, llimphi-counter: `repo = gitea@git.tawasuyu.net:...`
   por SSH. El worker es SIN SECRETOS por diseño ⇒ son HUB-ONLY estructuralmente, no un
   fallo. Y llimphi-counter ni siquiera es un fallo de build: su commit es literalmente
   ceros, con un comentario «fijar al commit real» — es una receta sin terminar, y es de
   tawasuyu.

LAS DOS DEPS DE ESTA RECETA, que son dos lecciones distintas:

 1. `python3` — porque **meson es un script de Python, no un binario**. Faltando el
    intérprete la fase muere con exit 127, que se lee como «meson no está» cuando meson SÍ
    está y se hidrata bien. gtk4/libadwaita/gtksourceview construyen en el worker
    precisamente porque sí lo declaran. Auditadas las 1141 recetas: wlr-randr era **la
    única** con meson y sin python3. Regla: meson en deps ⇒ python3 al lado.

 2. `libffi` — que wlr-randr no usa. Lo exige `wayland-client.pc` en su línea `Requires`, y
    pkgconf resuelve el grafo de .pc completo antes de emitir un cflag. Patrón ya
    documentado `.pc Requires` → `[deps].build`; el síntoma («Package 'libffi', required by
    'wayland-client', not found») culpa a wayland, que es inocente.

En ambos casos el arreglo es hidratar la dep, NO instalar nada en el rootfs del worker:
engordarlo haría que el build dependa de qué hay en una máquina concreta, que es justo lo
que rompe la reproducibilidad.

Re-hashear sale gratis: la receta estaba en deuda, nunca se había sellado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:23:28 -04:00
sergioandClaude Opus 5 346cd59706 licencias: campo license en la receta — de 0 a 228 de 1141, sin re-hashear nada
La deuda legal bloqueante del SDD 19 §2.1. Medido hoy: **0 de 1141 recetas** declaraban
licencia, no «5 de 771» como decía el informe anterior. Los dos números estaban mal: los
«5» eran falsos positivos de `grep license` (el paquete `addlicense`, el paquete
`cargo-bundle-licenses`, una línea `install .../share/licenses/` y un comentario), y las
recetas son 1141. Contar con `grep -l <palabra>` sobre TOML cuenta comentarios y nombres,
no campos; `scripts/licencias.sh` cuenta el campo de verdad (clave en la raíz, antes del
primer `[table]`).

LO QUE HACE LA DEUDA PAGABLE: `Recipe::hash_inputs` es una LISTA BLANCA — sólo entran
source, compiler, target, link, patches, flags, phases y deps. `license` no entra, igual
que `evidence` y `slots`. Por eso se puede poblar en las recetas YA SELLADAS sin mover un
solo ArtifactHash. Verificado, no supuesto: en 40 recetas modificadas se comparó el hash
con y sin la línea — 40 idénticos, 0 cambiados. Si el campo entrara al hash, declarar la
licencia costaría reconstruir el corpus entero y no se haría nunca. Clavado con el test
`licencia_round_trip_y_no_afecta_el_hash`.

TRAMPA DE TOML: una clave suelta después de un `[table]` pertenece a esa tabla. Puesta al
final del fichero, `license` acaba dentro de `[deps]` y se pierde EN SILENCIO, porque serde
ignora los campos que no conoce — no hay error, simplemente no está. Va arriba, junto a
`name` y `version`; el sembrador la inserta tras `version`.

NO SE ADIVINA. Declarar mal una licencia es peor que dejarla vacía: convierte un hueco
visible en una afirmación falsa. Sólo se puebla desde una tabla curada entrada por entrada
(`docs/licencias-conocidas.tsv`); lo que no tiene evidencia queda vacío y se CUENTA.
Concretamente se descartó el atajo «k* = KDE ⇒ LGPL»: en este catálogo `kail`, `kind`,
`ko`, `kopia`, `krew`, `kustomize`, `kyverno`, `katana`, `kibi`, `kmon` y toda la familia
`kube*` son herramientas Go sin relación con KDE. El nombre no es evidencia.

Quedan 913, casi todas CLIs Go/Rust importados en masa — y ésas sí son automatizables con
evidencia real: Cargo.toml trae el campo `license` y los módulos Go traen su LICENSE en el
árbol. El cierre estructural es capturarlo en la fase de fetch, que ya descarga y extrae
cada tarball, y inyectar el texto en `hammer pack` (aguas abajo del ArtifactHash) en vez de
en la fase install (que sí re-hashearía).

De paso, respaldo-storagebox.sh reordenado por valor irreemplazable y con zstd: medido en
la oficina, el uplink da 8 Mbps iguales por cable y por wifi ⇒ 128 G no caben en una
sentada, así que sube primero el cerebro (estado + repo) y `--partial-dir` hace que cortar
a mitad de un artefacto no tire lo ya subido.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 10:24:11 -04:00