Commit Graph
24 Commits
Author SHA1 Message Date
Sergio d4d120a079 brotli y json-c: selladas y rotas a la vez — las dos baratas del censo, arregladas
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.
2026-09-12 11:29:01 +00:00
Sergio c0a25cef22 protobuf/abseil vuelven a zig: la causa raíz quita el escape a gcc — y lo valida en un segundo proyecto
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.
2026-09-12 10:46:19 +00:00
Sergio ff4c23a858 estado: firefox-pgo-profile verificada — el arreglo del mirror destraba las tres recetas de URL .invalid 2026-09-12 05:05:23 +00:00
Sergio 76fc142244 verificar-repro: activar el mirror — el veredicto dependía de la suerte del caché
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.
2026-09-12 05:04:43 +00:00
Sergio 0b9055f52a estado: ia-modelo-embeddings verificada — REPRODUCE (cuantización y copia deterministas) 2026-09-12 05:02:06 +00:00
Sergio 097579546f dwarves: el linker de zig se caía con --dependency-file — una línea, y sin escapar a gcc
`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.
2026-09-12 04:13:53 +00:00
Sergio e1413d5e8d qdrant: promovida al corpus — limpia, reproducible, y la familia «base vectorial» deja de estar vacía
El worker selló la versión con el `install` que limpia `/out/src`: **70 M en vez de 139**, sólo
`usr/bin/qdrant`, sin NEEDED. Y REPRODUCE bit a bit (verificado allá, donde vive el artefacto).

    corpus 890/890 sellado · deuda 0 · el grafo CIERRA

Control de la promoción, en los dos sentidos: el hash antes y después del `git mv` es el mismo
(`4bd8feca…`) — la ruta no entra en `hash_inputs`, así que mover de cola al corpus no re-hashea nada.

De las cuatro familias que la tabla de `planear.py` daba vacías al empezar la noche quedan sólo
«contenedores», y eso es HONESTO aunque `crun` ya esté sellado: crun es el runtime OCI, la capa de
abajo — no sustituye a docker/podman/containerd, los ejecuta. Meterlo en esa casilla sería declarar
resuelta una decisión que sigue abierta.

El artefacto vive en el store del worker y el hub lo cuenta por manifiesto, que es como está
diseñado desde que el store se mudó al volumen.
2026-09-11 23:31:11 +00:00
Sergio 2ecce583e5 crun: un contenedor ARRANCÓ de verdad — la hoja de la última familia vacía
`crun` 1.29.1, y la prueba no es `--version`: con el `busybox` del propio corpus como rootfs y un
bundle OCI mínimo, rootless,

    $ crun run prueba-takana
    HOLA-DESDE-EL-CONTENEDOR
    Linux
    0
    93

Entra como HOJA a propósito de la familia «contenedores», la última que la tabla de `planear.py`
daba vacía. Un runtime OCI es lo que cualquiera de los tres candidatos (docker, podman, containerd)
acaba ejecutando por debajo, así que esto **no presupone cuál se elige arriba** — esa decisión sigue
abierta y no la toma una receta.

crun y no runc: C en vez de Go, ~300 KB contra ~10 MB, y es el runtime por defecto de Alpine ⇒ musl
es objetivo probado. No son excluyentes.

Tres deps que NO se deducen del proyecto, y las tres abortaban el `configure`:

· `--disable-systemd` no es preferencia: `AC_CHECK_HEADERS([systemd/sd-bus.h], [], [AC_MSG_ERROR…])`
  lo hace OBLIGATORIO salvo que se apague. Esta distro no lleva systemd ⇒ el cgroup manager será
  `cgroupfs`. Coherente con el resto del sistema, y mejor decidido acá que descubierto después.
· **`argp-standalone`**: crun parsea flags con `argp_parse(3)`, extensión de glibc que musl no tiene.
  La receta YA estaba en el corpus y sellada — sólo faltaba que alguien la pidiera.
· **`python3`** aunque crun sea C puro: su `AM_PATH_PYTHON` no está marcado opcional. Misma figura
  que meson.

**Y el vigía de enlace estático se ganó el sueldo.** El primer sellado pasó todo —corría, 0 deuda—
pero `static-audit.sh` cantó: `✗ crun dice static, es DINÁMICO → libc.so`. crun enlaza con libtool,
que lee el `-static` del lab como «preferí los `.a` de libtool» y no como flag al linker. El arreglo
es `LDFLAGS="-all-static -no-pie"` en compile **y en install** — libtool RELINKEA al instalar, así
que sólo en compile deja bueno el árbol de build y dinámico lo que se sella. Ahora: 0 NEEDED, y
`+CAP +SECCOMP +EBPF +JSON_C` intactos.

libcap y libseccomp se declaran y NO se apagan, que es la decisión contraria a los `--disable-*` de
arriba y es deliberada: son las dos piezas con las que un runtime OCI acota de verdad al contenedor.
Un crun sin ellas compila igual, aísla mucho menos, y el binario no lo diría.

**Promoción**: `libseccomp` pasa de `recipes/incoming-gnome/` al corpus, porque ya tiene dos
consumidores independientes (el stack de GNOME y crun) y no hay variante homónima con la que colisionar.
Control en los dos sentidos antes de moverla: su hash y los de `gnome-shell`/`mutter` son idénticos
antes y después, y coinciden con los que el grafo ya tenía registrados ⇒ **cero re-hasheo**. La
resolución sibling→padre de `resolve_dep_path` hace que los consumidores de la cola la sigan viendo.
2026-09-11 21:23:57 +00:00
Sergio f52205af31 opensmtpd: la familia «correo» estaba vacía — y la colisión de símbolos se CONTÓ antes de arreglarla
Tercera de las cuatro familias que la tabla de `planear.py` daba SIN NADA en el catálogo. Quedan dos:
contenedores y base vectorial (qdrant está moliendo en el worker).

Cuál de los tres servidores es una decisión, y va con el motivo para poder discutirla: **postfix**
asume glibc en varios sitios (NIS, nsswitch, su `dict_nis`) y son ~250 kLOC — portarlo a musl es un
frente, no una receta; **exim** tiene una configuración que es literalmente un lenguaje de
programación, y esa superficie ha sido su fuente histórica de CVEs; **OpenSMTPD** viene de OpenBSD
con separación de privilegios POR DISEÑO, ~40 kLOC, ISC, y su rama «portable» existe justo para
construirse fuera de OpenBSD ⇒ musl es un objetivo previsto. Si alguien necesita las tablas
MySQL/LDAP de postfix esto no sirve — pero se discute con la razón delante en vez de descubrir la
ausencia durante una mudanza.

Verificado: los 10 binarios estáticos, CERO NEEDED, y `smtpd -n -f` sobre una `smtpd.conf` real
contesta `configuration OK`. REPRODUCE bit a bit.

**El muro fue una colisión de símbolos, y lo que importa es que se MIDIÓ antes de elegir el arreglo.**
OpenSMTPD-portable trae su propia **libtls** (la envoltura de LibreSSL) porque la necesita cuando se
construye contra OpenSSL; y OpenSSL 3 define en su capa de récords una función INTERNA con el mismo
nombre. Estático, las dos caen en el mismo binario:

    ld.lld: error: duplicate symbol: tls_free
      defined at ../../openbsd-compat/libtls/tls.c:708
      defined at ssl/record/methods/tls_common.c:1473  in archive /usr/lib/libssl.a

En vez de suponer el tamaño del problema, se contó:

    nm -g --defined-only openbsd-compat/libtls/*.o | sort -u   →  158 símbolos
    comm -12 <esos> <los globales de libssl.a>                 →  **1**:  tls_free

Uno solo ⇒ el arreglo es renombrar ése y nada más, con `--with-cppflags=-Dtls_free=…` (la perilla que
upstream ya expone). El `-D` alcanza toda la compilación de OpenSMTPD, así que renombra a la vez la
definición, la declaración de `tls.h` y los usos — consistente por construcción; la de OpenSSL vive
en un `.a` ya compilado y no se toca. Es seguro porque esa libtls es interna a este build.

Si hubieran sido docenas de símbolos, el arreglo correcto era otro —traer LibreSSL al corpus, que es
contra lo que upstream construye— y por eso se midió primero. Queda escrito en la receta para que el
día que OpenSSL sume otro choque se vuelva a contar en vez de apilar `-D`s.

Dos cosas más anotadas y no tapadas: `--with-libfts=/usr` es obligatorio porque `fts(3)` está en
glibc y NO en musl (el corpus tiene `musl-fts` justo para eso) y sin pasarlo `configure` lo buscaría
en el LAB, que no entra en `hash_inputs`; y los usuarios `_smtpd`/`_smtpq` todavía no existen en la
distro — se dejan en su nombre canónico a propósito, porque cambiarlos por `root` tiraría la
separación de privilegios, que es la razón principal para elegir este servidor.
2026-09-11 20:43:35 +00:00
Sergio 2546f5cdf8 kubectl: el vigía nuevo señaló dos inertes y esto los enciende — 46 → 44
`kubectl-neat` y `kubectl-tree` llevaban meses sellados y **no se podían invocar**: los dos son
subcomandos que despacha `kubectl`, y `kubectl` no estaba en el catálogo. Probado, no supuesto —
`kubectl plugin list` con los dos artefactos en el PATH los lista a los dos.

`kubectl version --client -o json` contesta con todos los campos poblados (gitVersion v1.37.0,
gitCommit, gitTreeState clean, goVersion go1.26.4). REPRODUCE bit a bit.

Los `-X` del ldflags no son adorno: sin ellos `kubectl version` dice `v0.0.0-master+$Format:%H$`, y
un cliente de k8s NEGOCIA por versión — varios comandos avisan de desfase cliente/servidor
comparando exactamente esos campos. Los nombres son los de `hack/lib/version.sh` de upstream, para
no inventar un esquema paralelo.

**Uno de esos campos era una trampa de reproducibilidad**: `buildDate`, que upstream llena con
`date -u` ⇒ dos builds del mismo fuente darían binarios distintos (la familia del sello de nftables
y del `BUILD_ID` de valkey). Se resuelve como lo resuelve upstream, leyendo `SOURCE_DATE_EPOCH`, que
el lab exporta con valor FIJO. Sale `1970-01-01T00:00:01Z` — feo y CONSTANTE, que es lo que importa.

`gitCommit` va pineado a mano porque el tarball no trae `.git`. ⚠ El tag `v1.37.0` es ANOTADO: el
`git/ref` devuelve el sha del OBJETO TAG, no el del commit, y quedarse con ése habría horneado un
sha que no es ningún commit. Hay que resolver tag → commit.

⚠ **Y un hallazgo lateral que dejo anotado en la receta porque no es de esta receta:** el árbol de
k8s trae su propio `vendor/` curado, pero el lab corre `go mod vendor` en el FETCH siempre que haya
`go.mod`, sin mirar si el proyecto ya traía uno (`vendor_go_deps`, incondicional — se ve en el log:
`go: downloading …` ANTES de `configure`). O sea que se compila el vendor REGENERADO, no el de
upstream. Acá salió bien y reproduce, pero es la misma figura que el clobber del `vendor/` de Cargo,
que sí necesitó un campo de receta para resolverse.
2026-09-11 20:37:01 +00:00
Sergio 78e0c126cb protoc: el catálogo tenía el PLUGIN y no el compilador — y abseil y protobuf tienen que compartir runtime de C++
`recipes/protoc-gen-go.toml` estaba SELLADO y era INERTE: un plugin de protoc no hace nada sin
`protoc`, y `protoc` no estaba en el catálogo. Cualquier servicio gRPC lo necesita en tiempo de
build — empezando por `qdrant`, que el censo del servidor de origen encontró corriendo como binario
suelto, de los que se pierden al apagar la máquina vieja. Entran dos recetas: `abseil-cpp` (que
protobuf exige) y `protobuf`.

Verificado corriéndolo, no por el código de salida: `protoc --version` contesta `libprotoc 36.1`, y
un `.proto` de prueba se compila a `.pb.h`/`.pb.cc` reales. Único NEEDED `libc.so`, que provee
`musl-shared`. Las dos REPRODUCEN bit a bit.

**Dos muros, y el segundo escondía al primero.**

1. **`zig c++` se cae al enlazar los tres plugins `protoc-gen-upb*`**: `Error running link command:
   Segmentation fault` — un crash del linker, no un error de símbolos. Se aisló FUERA de takana y
   fuera del sandbox, rehaciendo el link a mano sobre los mismos `.o` y las mismas `.a`: `zig c++`
   vuelve a segfaultear. No es presión de memoria (corrió solo) y no es el `-Wl,-rpath,::::::::::::::`
   que CMake emite — se probó sin él y cae igual. `protobuf_BUILD_LIBUPB=OFF` tampoco es escapatoria:
   queda `OFF` en el `CMakeCache` y los plugins se construyen igual. Con `g++` y el `ld` de GNU los
   cuatro binarios enlazan.

2. Al pasar **sólo** protobuf a `gcc`, con abseil todavía construida por `zig-cc`, el link murió con
   `undefined reference to std::__1::basic_string<…>::assign(char const*, unsigned long)`. **El `__1`
   es el namespace inline de libc++** —la libstdc++ que trae zig— y g++ usa la de GNU, con otro
   mangling para los mismos tipos. O sea: **abseil y protobuf tienen que compartir runtime de C++ o
   no enlazan**, y el error habla de `std::string` sin nombrar a abseil ni una vez. Es la familia de
   las dos glib estáticas en un proceso, pero en C++ y en tiempo de link.

⇒ Las dos recetas llevan `compiler = "gcc"` y está escrito en ambas que cambiar una sin la otra
vuelve a romper. `-static-libstdc++ -static-libgcc` para que `protoc` no salga pidiendo una soname
que sólo vive en el lab.

Y una decisión que NO es «la última versión»: abseil va pineada a `20250512.1` porque es exactamente
la que protobuf 36.1 declara en `cmake/dependencies.cmake`. Abseil no promete ABI estable entre
releases; el disparador para subirla es que protobuf suba la suya, no que abseil publique.
2026-09-11 18:59:10 +00:00
Sergio a90b4ef626 valkey: la familia «caché en memoria» estaba VACÍA — y el artefacto salió vacío sin que nada fallara
`scripts/mudanza/planear.py` agrupa el software del origen en familias funcionales y contesta, para
cada una, con qué la reemplaza takana. Cruzando esa tabla contra el catálogo, cuatro familias salen
SIN NADA sellado: caché en memoria, contenedores, correo y base vectorial. Ésta cierra la primera.

Valkey y no redis a propósito: redis dejó de ser software libre en 2024 (RSALv2/SSPLv1, que no pasan
la OSI); valkey es el fork de la Linux Foundation desde el último commit BSD, mismo protocolo y mismo
RDB/AOF. Y upstream instala los seis alias `redis-*`, así que un `redis-server` se reemplaza sin
tocar datos ni clientes. Para una distro que publica el catálogo con `license` poblado receta a
receta, meter SSPL sería meter algo que después hay que sacar.

**El hallazgo caro de esta receta no fue compilar: fue que el PRIMER artefacto se selló VACÍO.**
`make install PREFIX=/usr DESTDIR=/out` imprimió sus `INSTALL valkey-server` en verde y salió 0 — y
`/out` quedó sin un solo fichero. La causa está en la línea 65 de `src/Makefile`: valkey define
`INSTALL_BIN=$(PREFIX)/bin` **sin prefijar `$(DESTDIR)`**, o sea que IGNORA `DESTDIR` y copió los
binarios a `/usr/bin` dentro del sandbox, que se tira al terminar. Lo sellado fue un directorio con
`.hammer/recipe.toml` y nada más: un cache-hit permanente que habría contestado «valkey ya está»
para siempre. Es la regla 3 del repo en vivo — *un ausente falla ruidosamente; un vacío llega hasta
el final diciendo que todo fue bien*. Se vio mirando el árbol del artefacto, NO el código de salida.
El arreglo es `PREFIX=/out/usr`.

El otro muro, medido con un control mínimo fuera de valkey: con `zig cc` el link muere con
`undefined symbol: __cpu_model` en los tres binarios. No es valkey — es que `__builtin_cpu_supports()`
(que valkey usa para elegir en runtime las rutas AVX2/AVX-512) emite esa referencia y el
`compiler_rt` de zig 0.16.0 no la trae:

    printf '#include <stdio.h>\nint main(void){__builtin_cpu_init();return printf("%d",__builtin_cpu_supports("avx2"));}\n' > t.c
    zig cc -target x86_64-linux-musl -O2 -o t t.c   ⇒  ld.lld: error: undefined symbol: __cpu_model

De ahí `compiler = "gcc"`, que es la palanca declarativa que el lab expone justo para esto. Arrancarle
el multiversioning a parches habría costado las rutas SIMD de `BITCOUNT`/`PFCOUNT` y habría que
rehacer el parche en cada versión.

Verificado: REPRODUCE bit a bit. Único NEEDED `libc.so`, que provee `musl-shared` — raíz de `base`
desde hoy ⇒ resuelve en cualquier imagen sin declarar nada en la receta.
2026-09-11 17:29:41 +00:00
Sergio 9a1139ce5f granja: las cuatro recetas nuevas REPRODUCEN bit a bit
`verificar-repro.sh` sobre popt, logrotate, chrony y cronie: 4 REPRODUCEN, 0 deriva,
0 no-determinismo. Importaba comprobarlo el mismo día y no dentro de tres meses: recién selladas,
el artefacto guardado y el lab son el MISMO, así que una diferencia acá sólo podía ser
no-determinismo — no deriva. Verificarlas más tarde mezcla las dos causas y obliga a la segunda
reconstrucción para desempatar.

Vale la pena decir cuál era el riesgo concreto, porque no era genérico: chrony mete en el binario un
`NTP_ERA_SPLIT` que por default calcula con `date` en tiempo de configure («hace 50 años»). Es
exactamente la familia del sello de tiempo de nftables y del BuildID de waterfox. No hizo falta
parchearlo: upstream ya respeta `SOURCE_DATE_EPOCH`, que el lab exporta — y esto lo confirma.
2026-09-11 17:17:03 +00:00
Sergio 2f930322cc estado: nftables verificada — REPRODUCE (el sello de tiempo lo neutraliza el parche) 2026-09-10 23:19:08 +00:00
SergioandClaude Opus 5 3c628deeb6 purpose: el último hueco de la frontera, y enganchado a las tres apps
purpose 6.27.0 — el marco de «compartir» de KDE. 23 plugins (código de barras, bluetooth,
portapapeles, correo, imgur…) más sharefileitemaction.so, que añade «Compartir» al menú de
Dolphin.

Era el último hueco. La receta de okular lo degradaba con el flag oficial
FORCE_NOT_REQUIRED_DEPENDENCIES y su cabecera decía «los que no tenemos sellados» — un ESTADO,
no una preferencia. Ahora sale de esa lista.

⚠ Y va ENGANCHADO, no sólo autorado: una receta que nadie alcanza no arregla nada. okular sale
de la lista de degradados; gwenview y spectacle lo declaran (ninguna lo mencionaba: construían
sin él y el CMake se lo saltaba en silencio). Coste medido antes de tocar: las tres son hojas
con 0 dependientes transitivos ⇒ 3 builds, sin cascada.

La prueba es el enlace, no la intención: las tres referencian libKF6Purpose (2 cada una).

Un tropiezo con su lección: el CMake abortó pidiendo tres MÓDULOS QML —org.kde.prison,
org.kde.kitemmodels, org.kde.kcmutils— que no salen de ningún find_package. Los pide con
ecm_find_qmlmodule, o sea que comprueba que el IMPORT exista, no que la librería esté enlazada.
Las tres recetas ya existían y publicaban su qmldir; sólo faltaba pedirlas. Un requisito que no
se ve leyendo los find_package.

KAccounts6 NO se pide: su find_package va sin REQUIRED y arrastraría toda la torre de
kaccounts/signond que esta distro no tiene.

Verificado: REPRODUCE bit a bit · tarball en el mirror · escritorio-kde 304/304 sellado, deuda 0.

FRONTERA: 0 HUECOS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-09 14:21:02 +00:00
SergioandClaude Opus 5 248fa8e991 desktop-file-utils: cerrar el «abrir con»
update-desktop-database construye la caché MIME→APLICACIÓN (mimeinfo.cache). Comprobado antes
de escribir la receta: NINGÚN artefacto del store lo traía, así que esa caché no se construía
nunca. Con shared-mime-info (cerrado ayer) el escritorio sabe QUÉ TIPO es un fichero; sin ésta
seguía sin saber CON QUÉ abrirlo — el menú «abrir con» vacío y sin aplicación por defecto.

Misma familia que aquel hueco: falla en RUNTIME, no en build. gnome-shell, libadwaita y mutter
lo piden en nixpkgs y acá construyen tan contentos sin él, así que ninguna métrica de deuda
puede verlo.

Prueba del consumidor con 14 .desktop REALES de okular/dolphin/kate/konsole/spectacle:
  mimeinfo.cache de 2.368 bytes · 47 asociaciones
  application/pdf=okularApplication_pdf.desktop
  text/plain=okularApplication_txt.desktop;org.kde.kate.desktop;org.kde.kwrite.desktop
Los 4 binarios estáticos con 0 NEEDED (--prefer-static desde el principio, no tras el error).
REPRODUCE bit a bit · tarball en el mirror.

Va como RAÍZ en los CUATRO escritorios y no en , y la razón importa: escritorio-kde y
escritorio-gnome comparten CERO raíces con base y bash NO está en su clausura. escritorio-mirada
documenta que no hereda «a propósito, es slim»; esos dos no dicen nada, y sway sí hereda cli.
Declararla en los cuatro funciona con cualquiera de las dos lecturas. La asimetría en sí queda
para decidir aparte: hoy «KDE 302/302» significa «la capa de escritorio está completa», no «la
imagen está completa».

Triaje: huecos 2 → 1 (queda ). Clausuras 303/182/160/205, todas selladas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-09 13:40:46 +00:00
SergioandClaude Opus 5 1da6851775 bash-completion: cerrar el hueco del tabulador
bash es RAÍZ del perfil base —el shell de la distro— y cinco recetas repartidas en cuatro colas
(appstream, colord, gnome-shell, modemmanager, swayimg) instalan completados en el directorio
que este paquete define. Sin él NO los instalan y siguen adelante: nada falla, nadie se entera,
y el usuario se queda sin tabulador en las herramientas del sistema. Hueco de RUNTIME puro, de
los que ninguna métrica de deuda puede ver — por eso hizo falta la frontera para encontrarlo.

Va al CORPUS y como raíz de , no a una cola: lo alcanzan las cuatro imágenes de escritorio
y la CLI, y una receta resuelve sibling-first y después el catálogo padre, nunca una cola
hermana. Y entra como RAÍZ porque nadie lo alcanza por deps de build: los consumidores sólo
preguntan por su .pc.

Verificado como consumidor, las dos mitades:
  pkgconf --variable=completionsdir bash-completion → /usr/share/bash-completion/completions
  el marco carga 131 completados y complete  devuelve regla · 522 ficheros instalados
  REPRODUCE bit a bit · tarball en el mirror

Triaje: los huecos vuelven a CERO. base pasa de 26 a 27 raíces, clausura 58/58 sellada.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-09 02:48:20 +00:00
SergioandClaude Opus 5 3d8c6fc6c7 KDE: cerrar los 3 huecos que quedaban — polkit-qt-1, qqc2-breeze-style y xwayland
La frontera queda en CERO huecos y cero pendientes, y el perfil escritorio-kde en 302/302
sellados. Los tres eran huecos de RUNTIME: KDE sellaba 1037/1037 con la autorización muerta,
los controles QML en estilo genérico y ninguna app X11 capaz de arrancar.

polkit-qt-1 0.201.1 — al conectarlo se vio lo que ninguna métrica decía: kauth NO traía plugin
de backend, ni siquiera el directorio. Ahora trae kf6/kauth/backend/kauth_backend_plugin.so con
el símbolo KAuth::Polkit1Backend. Cascada de 26 dependientes reconstruida, 26/26 sin fallos.
Añadirlo cambió la rama del CMake y pidió kwindowsystem y tras él las X11 — cada cosa dicha por
el configure al fallar, no supuesta.

qqc2-breeze-style 6.7.2 (la del stack Plasma, NO la 6.7.4 de nixpkgs: mezclar versiones de un
mismo release es pedir desajuste de ABI en los plugins QML, que se ve en runtime y no al
construir). Enganchado a plasma-workspace como deps.runtime, no build: es un plugin que Qt carga
por la ruta de imports. Verificado que runtime no re-hashea ⇒ cero rebuilds.

xwayland 24.1.13 — resultó ser una CADENA de seis: libfontenc, libXfont2, libxkbfile,
libxshmfence, xkbcomp y el servidor. Ninguna existía. Tres tropiezos con su lección:

  · libxkbfile 1.2.0 ya no trae ./configure: es sólo meson. El resto de las libs X11 de la cola
    son autotools, así que copiar su plantilla era lo natural y estaba mal.
  · «checking for freetype2... no» culpaba a freetype, que estaba perfecta: freetype2.pc declara
    Requires: zlib, libpng y pkg-config resuelve transitivamente. El mensaje señala al paquete
    equivocado.
  · libXfont2 construye una .so y moría en «recompile with -fPIC» contra la libfreetype.a
    estática. Van las variantes -shared.

  Y xkbcomp es el eslabón que NINGÚN build habría delatado: xwayland lo lanza como subproceso en
  runtime (xkb/ddxLoad.c) y su meson lo pide con required:false, así que la receta compila igual
  y el servidor arranca sin teclado. Se encontró leyendo la fuente.

⚠ Sin GLX, y no por preferencia: glx/meson.build:43 pide dependency('gl') y ninguna cola publica
gl.pc — las tres recetas de mesa van con glx y glvnd apagados por decisión anterior. Las apps
X11 corren en 2D. ⚠ Y el meson.build RAÍZ engaña: ahí build_glx sólo enciende una tabla hash y
parece gratis; yo mismo di la preocupación por infundada mirando el fichero equivocado.

Verificado: Xwayland arranca y se identifica («The X.Org Foundation Xwayland Version 24.1.13»),
las 6 REPRODUCEN bit a bit, los 5 grafos con deuda 0, y los 6 tarballs en el mirror.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-08 22:26:17 +00:00
SergioandClaude Opus 5 71bdb5ec5b shared-mime-info: cerrar el primer hueco de la frontera
Salió de 'yupana frontera' en cuanto nix volvió a correr: kcoreaddons la pide y no había receta.
Es un hueco de RUNTIME, no de build — KDE sella 1037/1037 sin ella, y por eso NINGUNA métrica de
deuda la veía. Sin la base MIME el escritorio no resuelve asociaciones, iconos por tipo ni
'abrir con', y GIO cae a adivinar por extensión.

Va al CORPUS, no a incoming-kde: la necesitan las cuatro imágenes de escritorio, y una receta
resuelve sibling-first y después el catálogo padre, nunca una cola hermana.

Dos cosas que se comprobaron ANTES de construir, no después de que fallaran:

  · El tarball de GitLab no trae el submódulo 'xdgmime' — .gitmodules lo declara y el directorio
    llega vacío. Leyendo meson.build:26-47 se ve que sólo lo usa la suite de tests y con
    required:false, así que con build-tests=false lo único que pasa es un warning.
  · 'i18n.merge_file' corre SIEMPRE, no lo gobierna build-translations: no traduce UI, arma el
    fichero de datos desde el .in. De ahí el msgfmt, que gettext-tiny sí provee.

'--prefer-static' desde el principio, que es la lección de json-glib de hace un rato: el binario
sale ESTÁTICO con 0 NEEDED y corre en el host.

Y el artefacto trae la mime.cache ya compilada (update-mimedb=true, que upstream trae en false):
sin ella el paquete son datos crudos y alguien tendría que acordarse de correr
update-mime-database al armar cada imagen.

Verificado:
  prueba del consumidor  update-mime-database genera 157.560 bytes de cache y 26 media-types
  binario                estático, 0 NEEDED, corre fuera del sandbox
  repro                  REPRODUCE bit a bit (la caché se genera en el build: era candidata)
  mirror                 el tarball subido al Storage Box (ADR 0013)
  frontera               huecos 4 → 3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-08 20:15:06 +00:00
SergioandClaude Opus 5 ca2c164512 sombras: CERO — promover las 6 compartidas al corpus y barrer sus 16 copias
Las 6 que quedaban (fuse3, hwdata, icu4c, libdisplay-info, libelogind, lua) no tenían copia en
el corpus, así que borrar una habría roto su cola: la resolución es sibling-first y después el
catálogo PADRE, nunca una cola hermana. Se promueve una y se barren las 16.

Las 16 eran byte a byte IDÉNTICAS entre sí, con el mismo ArtifactHash — medido con 'hammer hash',
no deducido del nombre. Cada una la comparten entre 2 y 4 imágenes, que es exactamente el
criterio que dejó escrito xkeyboard-config al promoverse: lo que comparten varias imágenes tiene
que vivir en el corpus o no lo alcanzan.

A diferencia de aquella, acá las copias SÍ se barren en el mismo movimiento: sin copia en el
corpus no había dónde caer, y dejarlas sería mantener hasta 4 ficheros que son el mismo hash.

Verificado con la huella de las 1167 recetas antes y después:
  ficheros de receta   1167 → 1157   (16 borradas, 6 promovidas)
  hashes supervivientes  ninguno se movió ⇒ CERO rebuilds
  los cinco grafos       deuda 0, huérfanas [], sellados == recetas
  static-audit           MIENTEN 0 de 690
  duplicados             SOMBRAS REDUNDANTES: 0   ← de 14

⚠ Y una lectura que casi publico mal: mi primera comparación de hashes dijo 'CAMBIÓ ✗' en las
seis. Era el grep de la comparación, que no casaba y dejaba el valor 'antes' vacío — los hashes
nuevos eran idénticos a los medidos minutos antes. Un instrumento roto que grita es tan malo
como uno mudo.

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

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

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

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

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

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

La regla, ya por triplicado hoy: cuando un guardián no puede establecer algo, el estado es *sin
veredicto*, y eso no se escribe. Un libro que anota lo que no midió se cree igual que uno que sí.
2026-09-05 21:44:50 +00:00
Sergio 6c1f80204a repro: cobertura 92 de 1156 — la tanda sólo-C es la sostenible
18 recetas C verificadas de una, en unos nueve minutos, 15 REPRODUCEN y 3 eran DERIVA (ya al día).
Cero no-determinismos. Y `work/sources` se quedó en 4 KB toda la tanda gracias a la poda por
veredicto: antes una tanda así llevaba el disco del 95% al 98%.

Lo que hace sostenible a esta y no a la anterior es la SELECCIÓN. El filtro `MAX_SECONDS` sobre los
tiempos del worker no transfiere a Rust —`zola` se comió 40 minutos y no dio un solo veredicto— pero
en C sí: sin caché de cargo de por medio, un build de 40 s allá son 40 s acá. Lista explícita de
clase `c`, no muestreo ciego.
2026-09-05 21:41:41 +00:00
Sergio d7d9cf8fd2 repro: «la 2ª reconstrucción no construyó» NO es no-determinismo — mi propio bug, y lo pagó el libro
Corriendo una tanda con `MAX_SECONDS=45` se llenó el disco a mitad, los rebuilds empezaron a morir
por ENOSPC, y la rama que yo mismo había escrito para distinguir DERIVA de NO-DETERMINISMO metió
«falló» y «difiere» en la misma condición ⇒ **21 recetas sanas quedaron anotadas como
NO-DETERMINISMO** en un libro que pretende ser autoritativo. Un registro falso y persistente es peor
que no tener registro: el propósito del libro es que nadie tenga que volver a mirar, así que una
entrada equivocada se cree para siempre.

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

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

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

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

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

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

⚠ Y una trampa que casi me come, anotada acá porque el próximo la va a pisar: elegí candidatos «por
artefacto chico» y la lista empezó con **nodejs**, cuyo artefacto pesa 0,2 MB y cuyo build son horas
de V8. El tamaño del artefacto NO dice nada del costo de construirlo. Para elegir muestra barata hay
que mirar tiempos de build, no bytes de salida.
2026-09-05 19:31:33 +00:00