Commit Graph
875 Commits
Author SHA1 Message Date
Sergio af20d90648 bzip2: cuatro comandos rotos desde siempre — /usr/bin/bzcmp -> /out/usr/bin/bzdiff
`bzcmp`, `bzegrep`, `bzfgrep` y `bzless` son symlinks con **la ruta del SANDBOX horneada dentro**.
El Makefile de bzip2 los crea con `ln -s $(PREFIX)/bin/bzdiff bzcmp` —con `$(PREFIX)` DENTRO del
destino— y como bzip2 **no soporta `DESTDIR`**, la receta pasa `PREFIX=/out/usr` (el mismo truco que
valkey) ⇒ el destino que queda es `/out/usr/bin/bzdiff`. Al hidratar, esos cuatro apuntan a un
directorio que en el sistema real no existe.

⚠ **Cinco indicadores en verde sobre cuatro comandos que no funcionan**: el artefacto tiene
contenido, `bzip2`/`bzgrep`/`bzdiff` corren, `static-audit` pasa, el grafo lo cuenta y la receta
REPRODUCE. Ninguna métrica existente mira a dónde apunta un enlace.

Arreglo: rehacerlos RELATIVOS después del install, que es lo correcto para un artefacto direccionado
por hash — un enlace relativo sigue valiendo esté el árbol montado donde esté. Verificado: los cuatro
resuelven y `bzip2 -c | bzcat` sigue dando `hola`.

## El guardián, en el mismo sitio y por la misma pregunta

`--auditar-raices` ya contestaba «¿esta receta dejó ficheros donde no van?». Ahora contesta también
la otra mitad: «¿dejó ENLACES a una raíz que no es del FHS?». Son la misma familia —un `install` que
se equivocó de destino— y comparten la definición de `FHS_RAIZ`, que es lo que evita dos listas que
se desincronizan.

La regla es independiente del perfil, y por eso vale sobre el store entero: un enlace a OTRO
artefacto es normal (`kinfocenter -> /usr/bin/systemsettings`, los dos en `escritorio-kde`, resuelve
en el rootfs fundido). Lo que nunca puede estar bien es un destino absoluto cuya primera componente
no sea del FHS: `/out`, `/src`, `/tmp` son rutas del lab.

Dos correcciones al propio guardián, las dos aprendidas midiendo:

· **Sólo audita el artefacto VIGENTE de cada receta.** El store guarda todos los sellados históricos,
  así que la primera versión acusaba a bzip2 por el artefacto YA SUPERADO — el mismo sobre-reporte
  que `static-audit.sh` tuvo que quitarse de encima. El hash vigente sale de los grafos de estado,
  no de 1150 llamadas a `takana hash`. ⚠ Y hay que regenerar **los cinco** grafos: con sólo el
  principal regenerado, el de KDE seguía apuntando al bzip2 viejo y el filtro lo dejaba pasar.
· **Un barrido que no miró NADA lo dice y sale 2.** Filtrar por hash vigente hace que un `--store`
  que no case con los grafos deje cero artefactos auditados, y sin la guarda eso se imprimía como
  «✓ 0 ofensores»: una respuesta falsa con forma de respuesta. Probado: `--store /tmp` → exit 2.

El control del hallazgo es el propio arreglo, sobre datos reales y no sintéticos: antes del fix el
barrido nombra los cuatro enlaces de bzip2; después, `✓ ninguno`.
2026-09-12 00:46:10 +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 54bf3e810e ia: el modelo de chat, pineado — Qwen2.5-1.5B-Instruct Q4_K_M (Apache-2.0)
Elegido por tres cosas, y las tres medidas antes de pinearlo: licencia Apache-2.0 (lo que una distro
puede shipear sin letra chica, al revés que Llama-3.2 o Gemma), habla español —se le preguntó qué es
una distribución de GNU/Linux y contestó dos frases correctas— y corre en CPU: 20,1 tokens/s en el
hub sin GPU, 1,04 GiB.

El objeto pineado es un tar que envuelve el .gguf, publicado en el mirror y servido por sha256:
takana extrae toda fuente con `tar` y un GGUF pelado no lo es. Es el camino de firefox-pgo-profile.
La receta anota el sha256 del GGUF DE UPSTREAM (el lfs.oid de HuggingFace, verificado al bajarlo) y
no sólo el del tar nuestro, para que nadie tenga que confiar en nuestro tar. Round-trip verificado:
apartados el caché y el artefacto, el build lo bajó del mirror y selló el mismo ArtifactHash.

Guardián en la receta, porque el fallo es callado: un fichero truncado o un HTML de error renombrado
a .gguf se instala igual y sella en verde. Se comprueban el mágico GGUF y el tamaño.

⚠ Y el modelo de verdad destapó una CARRERA en el guardián del §6.7: el censo de motores contaba en
el instante del cierre — con el de juguete daba 0 y con el de la imagen daba 1, que se lee como fuga
cuando en realidad matar un proceso con un giga mapeado tarda ~1 s. Ahora espera hasta 15 s y anota
cuánto tardó. La rotura a propósito sigue fallando, ahora con el tiempo a la vista.

Falta decidir en qué imágenes se declara (con el motor son ~1,25 GiB por perfil) y pinear el de
embeddings (multilingual-e5-small) para la mitad semántica del §6.3.
2026-09-11 22:41:46 +00:00
Sergio 733065b1c4 qdrant: CONSTRUYE y ARRANCA — pero el artefacto venía con 69 M de basura, y la causa es del lab
Selló en el worker con el arreglo del `compiler = "gcc"`. Verificado allá, corriéndolo y no por el
código de salida: binario estático de 70 M sin NEEDED, `qdrant --version` → `qdrant 1.19.1`, y
levantándolo de verdad:

    Qdrant HTTP listening on 6399
    Qdrant gRPC listening on 6334
    Access web UI at http://localhost:6399/dashboard

⚠ **Y al mirar el ÁRBOL del artefacto antes de promoverlo, pesaba 139 M — la mitad, basura.** 140
ficheros bajo `src/target/release/build/protobuf-src-*/out/install/include/google/protobuf/…`: las
cabeceras y libs del protobuf que `protobuf-src` compila para su uso interno.

**La causa no es de esta receta, y por eso vale escribirla.** El sandbox exporta **`DESTDIR=/out` de
forma GLOBAL** (`sandbox.rs`), para que el `make install` de las recetas autotools funcione.
`protobuf-src` hace su propio `make install` DENTRO de la fase compile, con
`--prefix=/src/target/release/build/…/out/install`, y ese install anidado **hereda el DESTDIR** ⇒ su
prefijo aterriza en `/out/src/target/…`. Nadie lo pidió, nada falla, y el artefacto sella con el
doble de tamaño y un `/src` en la raíz que al hidratar se proyectaría sobre el FHS de la imagen.

Le puede pasar a cualquier receta cuyo build ejecute un `make install` anidado — los crates `*-src`
son la familia entera. Se limpia en la receta y NO en el lab: quitar el `DESTDIR` global cambiaría el
comportamiento de todas las recetas autotools del corpus, que es una campaña con su propia
verificación. `/out/src` nunca es salida legítima — la raíz del artefacto es un FHS y `/src` es el
nombre del bind del lab.

Queda en `incoming/` hasta que el worker selle la versión limpia; promover a `recipes/` con el
artefacto sucio sería meter los 69 M al grafo.
2026-09-11 21:36:20 +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 07cc386580 qdrant: murió a los 408 crates por el NOMBRE del wrapper del lab, no por qdrant
Primer intento en el worker: 1,5 h, 408 crates compilados, y esto:

    "/src/vendor/protobuf-src/protobuf/configure" … "--host=/src/.hammer-zig"
    Invalid configuration `/src/.hammer-zig': machine `/src/.hammer-unknown' not recognized

La cadena es qdrant → `raft-proto` (tikv/raft-rs) → `protobuf-build` → **`protobuf-src`**, que
compila protobuf 21.5 DESDE FUENTE con el crate `autotools` — o sea que ni siquiera usa el `protoc`
que acabo de meter en el catálogo. Y `autotools` adivina el triple `--host` **recortándole el sufijo
al nombre del compilador**. Leído en `vendor/autotools/src/lib.rs` del árbol de post-mortem, no
supuesto:

    let host = cc_path.strip_suffix("-cc").or_else(|| cc_path.strip_suffix("-gcc"));
    if let Some(host) = host { args.push(format!("--host={}", host)); }

El lab exporta `CC="$PWD/.hammer-zig-cc"` ⇒ recortar `-cc` deja `/src/.hammer-zig`, que viaja como
triple. **El fallo no tiene nada que ver con qdrant ni con protobuf: lo causa cómo se llama nuestro
wrapper.** Cualquier receta Cargo que arrastre el crate `autotools` va a chocar igual.

Con `compiler = "gcc"` el lab exporta `CC="gcc"`, al que no se le puede recortar `-cc` ni `-gcc` ⇒ el
crate **no añade `--host`** y configure corre nativo. El propio crate ya tiene un caso especial
`cc_path != "musl-gcc"`, señal de que la heurística es frágil y upstream lo sabe.

⚠ **Y NO se arregla renombrando el wrapper del lab, aunque sea lo obvio:** ese nombre vive dentro de
la cadena de la fase `compile`, que entra en `hash_inputs` ⇒ tocarlo re-hashea las **234 recetas Rust
del corpus**. Es una campaña con su propia verificación, no un arreglo de paso. La palanca por receta
es la correcta, y queda escrito en la receta para que el próximo que lo vea no reabra la discusión.
2026-09-11 20:46:05 +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 a14b62436c atuq §6.7.bis: la barra lateral pregunta, y contesta un modelo de ESTA máquina
Inferencia REAL de punta a punta, en una jaula sin red: panel → fondo → host → `llama-server` (el
llama-cpp del corpus) → respuesta. El servidor lo levanta el host en la primera pregunta —no hay
servicio de IA en la imagen— y **se muere con el navegador**, al revés que el daemon del torrent
(§6.9): un torrent tiene trabajo que sobrevive; medio giga de modelo en RAM, no.

El puerto nativo vive en el FONDO y no en la página: si viviera en la página, cerrar la barra lateral
se llevaría el host y el modelo cargado con él.

`scripts/test-atuq-ia.py` mide tres cosas: que la respuesta salga del modelo de la imagen, que no
venga VACÍA, y que al cerrarse el navegador queden cero `llama-server`. Control negativo: sin modelo
aparece la causa y NO hay respuesta inventada.

⚠ Ese censo nació roto y del género que este repo colecciona: `pgrep -c` no existe en busybox y el
`|| echo 0` convertía el error en un cero — o sea en un verde. Con un motor vivo a propósito el
guardián pasaba igual. Ahora cuenta leyendo /proc y la rotura falla como debe. Y el PRIMER intento de
romperlo tampoco rompía nada: el motor de mentira moría al hacer bind porque el socket estaba en
/salida, compartido entre las dos corridas.

Queda UNA decisión, no trabajo: qué modelo se pinea (tamaño de imagen y licencia). Hasta entonces la
imagen no trae modelo y el panel lo dice con todas las letras.
2026-09-11 19:24:33 +00:00
Sergio 37bf7077e7 qdrant: a la cola del worker — y dos comprobaciones previas que podían haberla matado
La familia «base vectorial» de la tabla de `planear.py` sale SIN NADA en el catálogo (ni qdrant, ni
milvus, ni weaviate), y el censo del servidor de origen encontró `qdrant` CORRIENDO como binario
suelto en `/usr/local/bin` — de los que nadie provee y se pierden al apagar la máquina vieja. Si la
mudanza llega a ese servicio sin receta, se para.

Va a `recipes/incoming/` y no a `recipes/`: son 912 crates, no está construida todavía, y el hub
clasifica DESPUÉS de que el worker selle. El worker está ocioso y es gratis — ése es su trabajo.

Lo que la destrabó fue el `protoc` del commit anterior: su `build.rs` genera los stubs gRPC con
tonic/prost, que lo invocan. Es dep declarada, no accidente del lab.

Dos cosas comprobadas ANTES de escribir la receta, cada una capaz de matarla:

1. **MSRV.** Pide `rust-version = "1.97"` y el lab trae **exactamente** `rust-1.97.0-r0`. Margen
   CERO — y el toolchain del lab sale de Alpine edge, que es rodante: esto entra hoy porque edge ya
   movió. Queda escrito en la receta para que el día que falle no se busque en otro lado.
   (De paso: la nota que yo tenía de «techo MSRV 1.96» está vencida; el lock dice 1.97.0.)

2. **Qué `*-sys` arrastra**, que es la frontera conocida de las recetas Cargo. Grep sobre el
   `Cargo.lock`: **no hay rocksdb, ni librocksdb-sys, ni openssl-sys, ni bindgen** — qdrant migró a
   Gridstore y se sacó RocksDB de encima. Queda `tikv-jemalloc-sys`, que compila jemalloc desde
   fuente (de ahí `make`). Sin ese grep, la suposición razonable era «qdrant = rocksdb = C++ +
   libclang», media tarde de trabajo que no hacía falta.
2026-09-11 19:01:32 +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
SergioandClaude Opus 5 d6734cc0a2 caddy: receta TERMINADA — era un import de nix con un tag flotante
Su propia cabecera decía «PUNTO DE PARTIDA, no final», y el `commit` era el TAG `v2.11.4`. Eso
contradice el ADR 0006: un tag se puede mover, y entonces la misma receta construye otra cosa sin que
el hash lo note. Anclado con `takana pin` al SHA inmutable `e2eee6a7…`; el hash se movió a propósito
(`b3:d38acaa0…` → `b3:c915987d…`), que es exactamente lo que un anclaje debe hacer.

**Por qué importaba terminarla**: el `caddy` de gioser NO TIENE DUEÑO — es un binario puesto a mano
en `/usr/bin`, sin paquete y sin receta (lo midió `scripts/mudanza/censar.py` preguntándole a
pacman). Se pierde con la máquina. Con la receta anclada, el servidor nuevo lo declara en su perfil y
lo reconstruye: la diferencia entre mudar un servidor y poder mudarlo otra vez.

Verificado que sirve, no sólo que compila: 77 MB estáticos, **0 intérpretes requeridos** (Go estático
no arrastra sonames, así que no repite la fuga del `NEEDED` colgante), y un `file-server` de prueba
contesta **200**.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 17:43:25 +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 cc5aebd8c4 granja: las tres wanted del perfil servidor, construidas — y popt, que faltaba debajo
`targets.toml` declaraba `chrony`, `cronie` y `logrotate` como raíces del perfil `servidor` con el
motivo escrito al lado, y ninguna tenía receta: el grafo las contaba como `wanted` y ésa era toda la
deuda que le quedaba al perfil. Ahora sellan las cuatro (popt es la hoja que `logrotate` exige:
incluye `<popt.h>` en la primera pantalla y su configure aborta sin él).

    servidor  97/97 → 101/101 listo, falta 0.  `wanted` desaparece de los totales.

Lo que se midió, no se supuso:
  · logrotate  estático, CERO NEEDED, corre, y las rutas de gzip quedan en `/bin` — que es donde
    busybox las deja de verdad; el default de upstream en Linux es `/usr/bin/gzip`, que en esta
    distro NO EXISTE y habría fallado en runtime diciendo «no se pudo comprimir».
  · chrony     `+CMDMON +REFCLOCK +RTC +PRIVDROP +IPV6`; `cap_set_proc` está en el ELF, o sea que
    libcap entró de verdad y chronyd suelta privilegios. Sale `-NTS -SECHASH` a propósito: NTS
    necesita nettle o gnutls y ninguna está en el corpus.
  · cronie     los cuatro binarios estáticos sin NEEDED, con inotify dentro, y `/bin/vi` pineado.

**El hilo que recorre las tres recetas es el mismo, y es el que valía la pena escribir:** los tres
`configure` DECIDEN MIRANDO EL LAB. `logrotate` trae `--with-selinux/--with-acl` en `[default=check]`;
`chrony` prueba nettle, gnutls, libcap, seccomp y editline; `cronie` resuelve el editor de
`crontab -e` con `AC_PATH_PROG([vi])` y lo graba en el binario. El lab NO entra en `hash_inputs` ⇒
dos labs distintos sellarían bytes distintos en la MISMA dirección del store y nada lo notaría.
Cada palanca va fijada en la receta —que sí entra en el hash— para que sea una decisión y no un
accidente del entorno.

Y una anotada en vez de tapada: cronie no reemplaza al `crond` de busybox por reflejo (busybox ya lo
pone en `/sbin`); agrega `@reboot`, crontabs por usuario, `/etc/cron.d` y anacron. Cuál va en cada
imagen es decisión de perfil — lo que faltaba era que la opción existiera en el corpus.
2026-09-11 17:13:42 +00:00
SergioandClaude Opus 5 426093cd20 SDD 28 §6.13: takana upgrade probado de verdad — apply, rollback y un corte duro
Probado con un paquete que la caja necesitaba (`zstd-cli`): apply deja generación, la caja pasa a
desempacar su propio lab, `rollback` devuelve el estado anterior fichero a fichero (9 restaurados,
1 borrado), y tras el arreglo de durabilidad **sobrevive a un `hcloud server reset`, que es un corte
DURO y no un apagado limpio**.

Y cruza la frontera que `hydrate` no puede: store en el volumen, `/` en el disco local. Es el único
camino de actualización que funciona en una caja instalada.

El bug que destapó está en el commit anterior: `write_atomic` era atómico frente a otros procesos y
no frente a un corte, así que el primer upgrade se perdió al reiniciar y `recover` abortaba con la
huella misma del corte. Verificado en la máquina en los dos sentidos: binario viejo ⇒ estado en 0
bytes; binario nuevo ⇒ estado íntegro.

`recipes/takana.toml` re-pineado al commit del arreglo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 13:31:56 +00:00
SergioandClaude Opus 5 d3b42f892d zstd-cli: la distro comprime todo con zstd y no traía con qué descomprimirlo
`recipes/zstd.toml` construye **sólo `lib/`** —lo dice su propia cabecera, «build de lib/ (no CLI)»—
porque lo que necesitan sus 13 consumidores (mesa, libadwaita, appstream, rsync, los `zstd-sys`) es
`libzstd.a`. El binario nunca se construyó, y el agujero sólo se ve al instalar la distro en una
máquina de verdad: **un takana recién instalado no puede desempacar su propio laboratorio**, porque
el `tar` del rootfs es el de busybox (`tar: unrecognized option: zstd`) y `zstd` no existe. Hubo que
extraer por tubería desde otro hub — y un hub que se instala solo no puede depender de eso.

⚠ **Y mi arreglo anterior estaba mal**: declaré `zstd` en `perfil.base` dando por hecho que traía el
binario. Se destapó aplicándolo con `takana upgrade` en la caja: «✓ generación 1 aplicada, + 8
añadidos» y `zstd` seguía ausente, porque los 8 ficheros eran headers y `libzstd.a`. **El upgrade
hizo exactamente lo que debía; lo que estaba mal era lo que le pedí que aplicara.** Corregido a
`zstd-cli`.

Variante y no ampliación de la canónica: `zstd` es dep de 13 recetas y `mesa` está entre ellas;
re-hashearla arrastraría esa torre por un binario de 1 M que ninguna usa. Mismo criterio que las 23
variantes `*-shared`, con el eje en librería/herramienta en vez de estático/dinámico. Radio cero.

`HAVE_ZLIB/LZMA/LZ4=0` a propósito: sin eso el Makefile las detecta del sysroot del LAB y el binario
sale pidiendo sonames que el artefacto no publica. Verificado: estático, 0 intérpretes requeridos,
`Zstandard CLI v1.5.7`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 13:18:41 +00:00
SergioandClaude Opus 5 ae1b0c9fe8 git: la distro traía un git que NO PODÍA CLONAR — sha1dc leía desalineado y ubsan lo abortaba
Encontrado intentando que la caja de producción clonara el repo para ser un hub de verdad
(SDD 28 §6.11). `git ls-remote` funciona; `git clone` —de cualquier repo, incluso `--depth 1`— muere:

    fatal: fetch-pack: invalid index-pack output

El informe completo, que la traza corta escondía:

    panic: load of misaligned address 0x… for type 'const uint32_t',
           which requires 4 byte alignment
      in sha1_compression_states → sha1_process → SHA1DCUpdate → git_SHA1DCUpdate
      → git_hash_update → unpack_entry_data → cmd_index_pack

Dos cosas, y las dos son de fondo:

1. **`-fsanitize=undefined` estaba en CFLAGS**, no sólo en LDFLAGS. El motivo original era legítimo
   —las `libz.a` etc. materializadas traen referencias `__ubsan_handle_*` que bajo `-static` no se
   resuelven solas, y el flag AL ENLAZAR trae el runtime de zig— pero en CFLAGS **instrumenta el
   código de git**. Un arreglo de ENLACE se había vuelto una mina en RUNTIME, y justo en la ruta de
   hash, que es por donde pasa todo lo que git recibe. Ahora va sólo en LDFLAGS.

2. **`-DSHA1DC_FORCE_ALIGNED_ACCESS`**, que es la causa real. `sha1collisiondetection` —el backend
   SHA1 por defecto, el que detecta SHAttered— lee palabras de 32 bits SIN alinear. En x86 eso
   funciona y por eso nadie lo nota nunca; según el estándar es UB, y basta con que el runtime ubsan
   esté enlazado para que aborte. El define hace que lea byte a byte: quita el UB en la FUENTE en vez
   de esconderlo. Sin él, cualquier build que arrastre el runtime vuelve a romper `clone`.

Probado: `git clone --depth 1` del propio repo desde la caja ⇒ **877 recetas, commit 543676e1**.
Antes fallaba con y sin `--depth`.

Radio cero: `git` no es dep de ninguna receta (medido). Pero SÍ es raíz de `perfil.base`, así que
esto arregla la distro entera, no sólo el hub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 12:49:30 +00:00
SergioandClaude Opus 5 4190c7b43e atuq §6.7: entra el motor de inferencia local — y SOURCE_DATE_EPOCH le había apagado el AVX2
La IA local de la barra lateral (§6.7) y la mitad semántica del archivo personal (§6.3) figuraban
como dos pendientes distintos. Son uno: el corpus no tenía con qué correr un modelo. `pluma-llm`
sólo trae backends de NUBE y `rimay-verbo-fastembed` DESCARGA onnxruntime (glibc) y el modelo de
HuggingFace en el primer arranque. `recipes/llama-cpp.toml` (b10901, estática, 199 M) derriba el
muro entero: el mismo binario sirve `/v1/chat/completions` y `/v1/embeddings`.

⚠ Lo que este commit deja medido, y es lo que no se podía deducir: el PRIMER sello llevaba sólo
`-DGGML_NATIVE=OFF` —leído en `ggml/CMakeLists.txt:141`, que con NATIVE=OFF debería ENCENDER las
perillas explícitas de ISA— y salió con CERO instrucciones vectoriales. La causa está 36 líneas
más arriba: `if (CMAKE_CROSSCOMPILING OR DEFINED ENV{SOURCE_DATE_EPOCH})` apaga
`GGML_NATIVE_DEFAULT`, y el sandbox de takana exporta `SOURCE_DATE_EPOCH=1` (`sandbox.rs:453`)
justamente para que los builds REPRODUZCAN. O sea: la variable que nos da reproducibilidad apagaba
todas las instrucciones vectoriales del motor de inferencia — y no falló nada. El artefacto selló,
`--version` contestaba, y el binario era x86-64 pelado: 0 `%ymm`, 0 `vfmadd`, 0 `roundps` en
1.634.770 líneas de `objdump -d`. Ahora las seis van declaradas una por una y el `install` LAS
COMPRUEBA en el binario: 42.356 `%ymm` / 1.298 `vfmadd` / 86 `roundps`.

`strip_debug = true` porque zig cc emite debug_info por defecto y son 16 binarios estáticos:
1,8 G → 199 M.

Guardián `scripts/test-llama-cpp.py`, sobre el artefacto VIGENTE (resuelto por `takana hash`, no
por `ls store/*`) y con control negativo vivo (`--sin-modelo`, que TIENE que fallar): ISA,
hermético (0 NEEDED), una pasada de inferencia REAL con `stories260K` pineado por sha256, y el
endpoint de embeddings — vector de verdad, el mismo texto da el mismo vector, dos textos distintos
dan vectores distintos, y no son ceros. Y `verificar-repro.sh`: REPRODUCE, 0 no-determinismos.

⚠ Esto es el MOTOR, no la función. Un motor sin modelo no contesta nada: el modelo de producción
es fuente pineada aparte, como el perfil de PGO de firefox, y es su propia unidad de trabajo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqBhowvFe3aiieGCKgvxwa
2026-09-11 03:03:31 +00:00
SergioandClaude Opus 5 01b1dcfefe respaldo: el rsync del propio corpus no podía correr el respaldo del propio corpus
Corriendo el respaldo desde la caja de producción (SDD 28, puerta 7):

    ==> [1/3] estado (el grafo: qué había construido y con qué hash)
    unknown compress name: zstd
    !!  estado: error 4 que NO es de red — no reintento

El script exige `--compress-choice=zstd` (medido: el doble de rendimiento efectivo, porque el 79 %
del store son secciones `.debug_*` y comprimen como texto) y **`recipes/rsync.toml` lo construía con
`--disable-zstd`**. El comentario de la receta decía por qué: «deps externas quitadas (no en
catálogo)». Ya no es cierto — `zstd` tiene receta y está sellada.

Dos arreglos, y los dos hacen falta:

1. **La receta enciende zstd** (dep `zstd`, fuera el `--disable-zstd`). Control en los dos sentidos:
   el rsync nuevo lista `zstd zlibx zlib none`, el anterior `zlibx zlib none`. Radio cero: `rsync` no
   es dep de ninguna receta (medido), así que no re-hashea nada más.
2. **El script DEGRADA en vez de morir.** Detecta el soporte (`rsync --version` → «Compress list»)
   y cae a zlib avisando. Un respaldo que no corre por un algoritmo de compresión es peor que un
   respaldo lento — y el fallo era especialmente malo porque el error 4 se clasifica como "no de red"
   y el script no reintenta: el respaldo simplemente no se hace.

Y de paso queda cubierto el caso de un hub que todavía no reconstruyó su rsync.

**También la raíz cableada**: el script hacía `cd /mnt/vvv/takana` por defecto —la ruta de gioser—
así que desde cualquier otro hub moría con `No such file or directory`. Ahora se deriva de la
ubicación del script, como el resto de `scripts/`; el override por `RAIZ` se conserva. Control: en
gioser sigue resolviendo a `/mnt/vvv/takana`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 02:59:13 +00:00
SergioandClaude Opus 5 7e86459738 musl-shared: el corpus publica su propio CARGADOR — 23 binarios dejan de ser inertes
La imagen del perfil servidor traía 23 binarios de 726 que no podían ejecutarse:

    $ python3 -c "print(1)"
    sh: python3: not found        <- y `command -v python3` decía /usr/bin/python3

No faltaban: eran ELF dinámicos pidiendo `/lib/ld-musl-x86_64.so.1`, y **ningún artefacto del corpus
publicaba ese fichero**. Funcionaban en el hub sólo porque el rootfs Alpine del LAB lo presta — la
misma fuga que documentan `zlib-shared` y `expat-shared`, un piso más abajo, y peor: el lab no entra
en `hash_inputs`, así que la dependencia era invisible para el store.

Los 23 son la suite binutils entera + perl, python3, sqlite3, flex y nft. Entre ellos, TODO el
instrumental del proyecto, que es Python.

**Variante y no `--enable-shared` en la canónica**: `musl` es componente de Stage 1 y su `of_tree` es
el baseline de `selfhost-verify`. Re-hashearlo obliga a rehacer ese baseline a propósito, y eso es su
propia unidad de trabajo. Con el patrón `*-shared` (ya hay 23 en el corpus) la canónica no se mueve —
control: `takana hash recipes/musl.toml` sigue dando `b3:ce952f72…`, el mismo sellado de Stage 1. Y
además `musl` no es dep de NADIE (medido: sólo de sí misma; el enlace estático lo resuelve el musl de
zig), así que esto suma sin mover un solo ArtifactHash del corpus.

**⚠ `compiler = "gcc"` y no `zig-cc`, medido.** Con zig cc el `libc.so` sale con 1586 símbolos
dinámicos contra los 1654 de Alpine, y los 68 que faltan son EXACTAMENTE los que musl implementa en
ensamblador x86_64 (`memset memcpy memmove memcmp strlen` y toda la familia matemática) más los
`__stack_chk_*`. No es que no se compilen —en el `libc.a` canónico están—: zig los resuelve con su
propio musl y los deja `FUNC LOCAL HIDDEN` de tamaño 0, fuera de la tabla dinámica. El síntoma es un
cargador que arranca, reloca y muere con `memset: symbol not found`, que se lee como "el binario está
roto" y no como "a la libc le faltan símbolos". Con gcc: **1651 símbolos**, `memset` y `ceil`
presentes, y `python3 3.12.10` CORRE con el cargador del corpus.

De paso: el comentario de `musl.toml` decía que el artefacto aportaba «el loader, para uso dinámico
futuro». Era falso —`--disable-shared` no construye ninguno— y es justo la etiqueta que hizo que
nadie buscara el agujero. Corregido (los comentarios no entran en `hash_inputs`; verificado).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-11 01:47:54 +00:00
Sergio 10c499777e atuq §6.5.bis: la extensión foco — muestra el foco del sistema y no tiene con qué apagarlo
La mitad del navegador del §6.5, deliberadamente asimétrica: sólo lee. Pregunta `focus.state` (nuevo
en puriy-costura, commit 38815b5f3) y pinta tres estados — `foco`, nada, y `?` cuando nadie escribió
el estado. Ese tercero es el que importa: si «no sé» se redondeara a «apagado», la insignia afirmaría
que no hay foco sin haberlo mirado.

No hay verbo para apagarlo, y es la propiedad y no un pendiente: si el navegador pudiera levantar el
foco, valdría lo que vale un bloqueador de extensión. En tawasuyu hay un test que lo fija; acá el
guardián mide el EFECTO — tras una sesión entera, el fichero de estado quedó igual.

`scripts/test-atuq-foco.py` corre los tres estados sobre el path de PRODUCCIÓN (`/etc/takana/focus`),
no la escotilla de pruebas: una escotilla mide el código, no el contrato con la imagen. Verde sobre
el artefacto vigente (atuq 5d1afc50, puriy-costura 0de6b4ca), y verificado rompiéndolo — con una
extensión parcheada que pinta «foco» siempre (en una COPIA del artefacto, vía ATUQ_DIR), falla
nombrando la insignia.
2026-09-10 23:17:32 +00:00
Sergio 817cd65d41 corpus: la cadena nftables (libmnl + libnftnl + nft 1.1.6), que el grafo pedía y nadie había puesto
Entra por el §6.5 del SDD 26 —el «modo foco» de atuq lo sostiene el cortafuegos del sistema y no
una extensión que el navegador puede apagar— pero `nftables` estaba `wanted` en el grafo desde
antes: la cadena sirve igual al servidor de producción del SDD 28.

Selladas y MEDIDAS con el consumidor, no por presencia:
  nft --version → nftables v1.1.6, corriendo desde nuestro artefacto
  nft -c -f / nft -f  → aceptan Y APLICAN en un netns privado (bwrap --unshare-net + CAP_NET_ADMIN)
  socket cgroupv2 level 1 "<cgroup>" → la expresión EXACTA que genera cortafuegos-core: aplica

Dos hallazgos del camino, los dos en los comentarios de la receta:

1. nftables NO reproduce de fábrica: `MAKE_STAMP` es `$(shell date +%s)` y `nftversion.h` lo mete
   byte a byte en el binario — la familia del BuildID de waterfox. No mira SOURCE_DATE_EPOCH. Se
   fija en 0, que además es lo correcto: el sello sólo sirve para comparar qué nft creó una tabla,
   y dos builds de la misma versión SON el mismo nft.
2. su `config.status` trae un bashismo (`for ((i = 56; ...))`) que busybox rechaza con
   «bad for loop variable». Se arregla en el parche y no con CONFIG_SHELL=bash: el lab no entra en
   `hash_inputs`, así que apoyarse en su bash sería una dependencia invisible.

Y una medición que condiciona al guardián que viene: `nft -c` NO es un chequeo de sintaxis — resuelve
el path del cgroup contra la máquina viva y falla con «cgroupv2 path fails» si no existe. O sea que
verificar una política del cortafuegos exige crear los cgroups, y eso pide root.
2026-09-10 22:30:18 +00:00
SergioandClaude Opus 5 6eb7960bef recetas: re-pinear netup y takana al commit con el loopback y el strip_debug del .swm
netup `b3:5d4eebb6…` (trae el `lo` levantado), takana `b3:e1f79ff3…` (trae `strip_debug` viajando en
el paquete). Los dos pineados a c0545ea4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-10 21:40:46 +00:00
SergioandClaude Opus 5 adeda7e499 recetas: takana se empaqueta a sí mismo — y el alias del ADR 0016 volvía al crate INEMPAQUETABLE
El corpus construía 869 recetas y no la suya. `takana` sólo existía como `cargo build --release`
sobre un clon del repo, y eso bloqueaba lo de arriba de todo del SDD 28: que un servidor takana se
sirva sus propios paquetes **a su propio host**. El host necesita `takana` instalado para consumir
el repo, y no podía instalarlo DESDE el repo porque no estaba en el repo.

**El muro, que nadie había tocado porque takana nunca se había empaquetado:** `takana-cli` declara
DOS `[[bin]]` sobre el mismo `main.rs` (`takana` y `hammer`, la compatibilidad de la etapa 2 del
ADR 0016). El camino Cargo del corpus invoca `cargo rustc … -- -C target-feature=+crt-static`, y
`cargo rustc` con argumentos extra **sólo admite UN target**:

    error: extra arguments to `rustc` can only be passed to one target, consider filtering
           the package by passing, e.g., `--lib` or `--bin NAME` to specify a single target

O sea: la decisión de emitir dos binarios —tomada para que el alias sobreviviera a `cargo clean` y a
la siembra de la granja, que excluye `/target`— hacía que el crate no se pudiera empaquetar. Se
resuelve con `--bin takana` y reponiendo `hammer` como ENLACE en la fase de instalación: en el árbol
de build un symlink no sobrevive, pero en el ARTEFACTO sellado es permanente y no duplica los megas.

Y ese alias tapa un agujero real: **`arje-zero` todavía invoca el binario por el nombre viejo** — en
el primer arranque de la caja de producción se leyó `hammer no corre, sin menú de arranque`. Viene
pineado desde tawasuyu, así que no se arregla desde este repo; mientras `hammer` esté en el
artefacto, el menú de arranque por grafo del ADR 0010 tiene qué ejecutar. Cuando la etapa 6 retire
el alias, hay que subir arje-zero ANTES.

Sellado `b3:941d1857…`, 3,3 M con el split de debug. Verificado: ELF estático, `takana --version` y
`hammer --version` dan los dos `takana 0.0.1`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-10 21:13:30 +00:00
Sergio 78f1c6a771 atuq: re-pin del daemon con el arreglo del inmortal, y la lección en el SDD
El §6.9 apunta ahora a tawasuyu `3bd1440d` (`b3:27c7b73a`), que arregla un daemon que
quedaba VIVO PARA SIEMPRE cuando su sesión de torrent no arrancaba: el bucle hacía
`let Ok(g) = gestor() else { continue }` y el `continue` saltaba la evaluación de
«¿sobro?».

Y lo que queda escrito en el §6.9 es CÓMO se encontró, porque es lo reutilizable: no lo
encontró ningún test sino mirar `ps` después de las pruebas —un daemon con 23 minutos y
`idle_seconds = 300`—. Los tests cubrían la decisión como función PURA, y esa función
estaba bien: el que no llegaba a llamarla era el bucle. **Una decisión correcta que
nadie toma se ve igual que una que no existe.**

Los dos guardianes vuelven a pasar sobre los artefactos vigentes (`atuq b3:d1a444ad`,
`puriy-costura b3:cad5c855`, `puriy-costura-torrent b3:27c7b73a`): el positivo con
`TOMADO prueba-atuq.bin` y el socket que nadie arrancó, y el control negativo nombrando
lo que falta.
2026-09-10 21:07:30 +00:00
Sergio c214438f00 atuq: el torrent lo toma un daemon propio, perezoso, que sobrevive al navegador
El 6.9 del SDD 26, con la arquitectura que pidió el operador: daemon PROPIO,
CONFIGURABLE y PEREZOSO. Vivaldi ya trae torrent y termina en una carpeta; acá lo que
baja entra al CAS —un objeto BLAKE3 con la misma identidad que una descarga del
navegador (§6.2) o un `.swm`—, y ésa es la razón por la que esto vale.

    página de la extensión → fondo → connectNative → puriy-costura
      → /usr/bin/puriy-costura-torrent add   (que LEVANTA el daemon si no está)
      → daemon: librqbit, y al completarse, el CAS

POR QUÉ NO ES UN VERBO MÁS DEL HOST — dos razones, y la primera decide:
1. una descarga tiene que sobrevivir al navegador, y Gecko mata al host cuando se
   cierra el puerto (medido hoy);
2. librqbit + tokio + rustls son 226 crates: dentro del host, ese binario —952 K,
   compartido por CINCO extensiones— pasaría a ~20 MB y cada iteración de cualquier
   función del navegador a un cuarto de hora.

⚠ Que esa pila COMPILA para musl con zig-cc se midió ANTES de decidir, con una receta
desechable: 226 crates y sella. Era una incógnita real —ninguna receta del corpus
había construido tokio+rustls desde tawasuyu— así que la decisión no fue «no se
puede», fue «no ahí». El artefacto del daemon pesa 7,7 M.

PEREZOSO EN LOS DOS SENTIDOS: no hay servicio en la imagen (el binario está y no corre
hasta que hay un torrent; lo levanta su cliente con `setsid`, sin lo cual moriría con
el navegador) y se va solo tras `inactividad_seg` sin nada activo — sembrar cuenta como
actividad.

CONFIGURABLE: TOML opcional; sin fichero anda, y uno ROTO es error. El CAS por defecto
es el del host, con un test que falla si esas raíces se separan.

LO QUE EL GUARDIÁN NO HACE, Y POR QUÉ (está en su cabecera y en el §6.9):
· no usa un `magnet:` sino un `.torrent` servido por HTTP —dar de alta un magnet
  BLOQUEA esperando metadata que sin peers no llega: se mediría un timeout y no la
  cadena—. El `.torrent` se arma en el propio guardián, en bencode a mano, porque un
  binario de prueba en el repo es una dependencia que nadie revisa;
· no pasa por el despacho de protocolos de Gecko: un clic en `magnet:` abre el diálogo
  de «¿con qué lo abro?», que en headless no contesta nadie, y eso es una elección del
  usuario y no código nuestro. Se abre la página de la extensión —la misma que el
  handler abriría— descubriendo su URL base del `dump` de una primera corrida, porque
  el UUID lo asigna Gecko por perfil y adivinarlo sería inventar.

El control negativo saca el cliente del daemon de la imagen y exige que el host lo diga
NOMBRANDO lo que falta, en vez de fingir que lo tomó.

⚠ Y sigue sin haber test automático de un transfer REAL entre peers: haría falta un
sembrador y una espera que volverían la suite una que nadie corre. Dicho en el test del
daemon y en el §6.9, para que nadie lo lea como «probado de punta a punta».

La página de la extensión NO valida la forma del origen: la valida el host, que es
quien decide qué le pasa al daemon. Tener esa regla escrita dos veces es tenerla
mintiendo el día que una cambie.

Del §6 quedan el foco (6.5) y la IA local (6.7), ésta bloqueada por algo medido: el
corpus no tiene ninguna receta de LLM ni de embeddings.

MEDIDO sobre `atuq b3:d1a444ad`, `puriy-costura b3:cad5c855` y
`puriy-costura-torrent b3:76afb7a8` (7,8 M):

    MAGNET http://…/prueba.torrent
    TOMADO prueba-atuq.bin · nuevo · en /salida/hogar/Downloads · id=0
    socket del daemon: run/puriy-costura-torrent.sock      ← nadie lo arrancó

⚠ Y DOS COSAS QUE EL GUARDIÁN DESTAPÓ, las dos por comprobar el NOMBRE y no sólo que
la respuesta llegara:

1. **la extensión leía claves que el daemon no manda** (`name`/`files`/`bytes` contra
   `nombre`/`carpeta`), y el síntoma era «TOMADO — 0 fichero(s), 0 bytes»: parecía un
   torrent vacío y era un vocabulario inventado del lado del lector — la misma trampa
   que había evitado en el host y no acá;
2. mirándolo apareció que **el socket y la config del daemon estaban en castellano**, y
   la regla 7 de tawasuyu nombra explícitamente las claves de configuración entre lo
   que se tipea. Corregido allá antes de que llegara a ninguna imagen: un protocolo y
   un fichero de config son contrato, y renombrarlos después rompe lo que alguien ya
   escribió.

tawasuyu: 4f36f057 (el crate) · 3f69aab2 (lock) · 3b56db26 (el verbo del host) ·
066b3761 (el log del daemon, que faltaba y hacía invisible su único fallo de arranque)
· ca9947b9 (las claves en inglés).
2026-09-10 20:45:53 +00:00
SergioandClaude Opus 5 2613fe3951 servidor-image.sh: la imagen del perfil servidor, armada por script — y netup pasa a ser raíz
El ensamblado dejaba de ser reproducible en cuanto se cerraba la terminal: cuatro pasos, tres de
ellos con una trampa que no se ve. Ahora es un script, con las trampas escritas en su cabecera:

1. **El kernel.** `linux.toml` NO sirve para hcloud (apaga SCSI y el disco de Hetzner es virtio-SCSI).
   Va `linux-generic`, que sí trae `CONFIG_SCSI_VIRTIO=y` — dato que no está en la receta, lo pone
   `make defconfig`, y sólo se ve en el `.config` que el artefacto publica.
2. **El init se pisa.** La clausura del perfil trae busybox y su `/sbin/init` gana por hidratarse
   después. El script lo restaura e IMPRIME la reparación (`../bin/busybox → /usr/bin/arje-zero`),
   que es la diferencia entre arreglarlo y creer que estaba bien.
3. **EXDEV.** Comprueba con `findmnt` que STORE y WORKDIR estén en el MISMO montaje y aborta con un
   mensaje que dice qué hacer, en vez de fallar fichero por fichero a mitad de un `cp -al`.
4. **La clave es obligatoria.** Sin `AUTHKEYS` no arma nada: una instalación remota sin
   `authorized_keys` deja una máquina viva e INALCANZABLE, que es peor que una que no arrancó.

Y el cmdline va SIN `init=`, a propósito: así corre el wrapper que escribe `install-image.sh`, que
es quien monta `/store` y `/var/lib/hammer`.

**`netup` entra como raíz de `perfil.servidor`** aunque su binario ya venga dentro del
`product-rootfs`. La razón es concreta y se pagó hoy: el del producto está congelado en el artefacto
sellado del bootstrap, así que el arreglo de la ruta on-link NO llegaba a la imagen. Declarado como
raíz, la hidratación lo proyecta encima y la imagen lleva el vigente — verificado por sha256: la
imagen trae `a23df20e…` y el product-rootfs `a84b0a0d…`.

`recipes/netup.toml` re-pineado a `be383ea4` (el commit del arreglo).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-10 20:15:39 +00:00
Sergio 928d5eb119 atuq: un vídeo se abre en el reproductor de la distro, no en una pestaña
El 6.6 de la unidad 9 del SDD 26. Una navegación de PRIMER NIVEL a un medio
(`Content-Type: video/*` o `audio/*`) se cancela y la URL se la lleva `mpv`, que ya viaja en las
cuatro imágenes de escritorio: esto no agrega ni una receta, es cablear lo que ya estaba.

    MEDIO http://…/audio.wav
    CANCELADA la navegación http://…/audio.wav
    ABIERTO /usr/bin/mpv pid=266 http://…/audio.wav
    mpv: AO: [null] 8000Hz mono 1ch u8        ← su propio log: DECODIFICÓ, no sólo arrancó

LA REGLA ES ESTRECHA A PROPÓSITO: sólo el primer nivel. Un `<video>` embebido es parte de la página y
sacarlo de ahí rompería el sitio que lo puso. Las reglas anchas en el camino de cada petición son las
que terminan rompiendo la web de alguien.

FAIL-OPEN, y es lo que prueba el control negativo: cancelar es SÍNCRONO y la respuesta del host llega
después, así que no se puede saber en el momento si el reproductor arrancó. Se cancela sólo con el
puerto vivo y, si el host contesta que no pudo, la URL VUELVE al navegador con una marca para no
entrar en bucle:

    SIN REPRODUCTOR no hay reproductor en /usr/bin/mpv: esta imagen no lo trae
    VUELVE AL NAVEGADOR http://…/audio.wav?atuq-medios=no

Quedarse sin vídeo Y sin pestaña es el único resultado inaceptable, y es justo el que se consigue si
uno confía en que salió bien.

⚠ EL BUG QUE ESTE GUARDIÁN DESTAPÓ VALE MÁS QUE LA FUNCIÓN, y estaba DESPUÉS del éxito aparente: la
primera corrida detectó el medio, canceló y recibió el pid… y después el puerto murió con «Native
application tried to send a message of 546281442 bytes». **Un hijo hereda los descriptores del padre,
y los del padre SON la tubería de native messaging**: `mpv` escribía su salida ahí y el navegador la
leía como un marco. Arreglado en tawasuyu (`2e99d216`, pineado acá) con tres redirecciones, cada una
contra un fallo distinto — `stdin` a null porque mpv LEE stdin y se comería los mensajes del
navegador; `stdout` al stderr del host y no a `/dev/null`, porque apagarlo arreglaría el bug y se
llevaría puesto el diagnóstico; `stderr` heredado por lo mismo.

Y dos cosas del ARNÉS, las dos ya vistas antes en esta misma sesión:
· la URL NO se pasa por la línea de comandos: una petición del arranque compite con la
  inicialización de la extensión (la misma carrera del guardián de `sct`). El guardián navega a una
  página que redirige a los 3 s, que además es lo que hace una persona: seguir un enlace;
· la evidencia de que el reproductor CORRIÓ se le pide a su propio `log-file`, porque su salida ya no
  va al stdout del host y Gecko no vuelca el stderr del host al del navegador. Sin eso sólo se sabría
  que hubo un `spawn`.

Del §6 quedan el foco (6.5), la IA local (6.7) —que además es la que traería el motor que le falta al
archivo del §6.3— y el torrent (6.9). Medido de paso: **el corpus no tiene ninguna receta de LLM ni de
embeddings**, así que 6.7 hoy no se puede pagar.

Medido sobre `atuq b3:a7080058` y `puriy-costura b3:060314e6`.
2026-09-10 15:45:08 +00:00
Sergio 7f6459c132 atuq: el archivo personal — lo que leés queda congelado, y se encuentra
La unidad 8 del SDD 26 (§6.3), en su mitad pagable. Cada página que se lee se congela: el HTML **ya
ejecutado** —lo que la persona vio, no lo que mandó el servidor— va al CAS con su BLAKE3, y la url,
el título y el texto visible al índice. Después se busca, sin el navegador.

    visita 1 (página A)  ⇒ archivada, visitas=1, total=1
    visita 2 (página A)  ⇒ dedup, visitas=2, total=1     ← volver NO duplica
    visita 3 (página B)  ⇒ total=2
    «masa» → 1 de 2 · «tobera» → 1 de 2 · «masa tobera» → 0 · «helicóptero» → 0

LA IDENTIDAD ES EL BLAKE3 DEL HTML, NO LA URL, y de ahí sale lo que hace que esto valga: la misma URL
con otro contenido ES otra página, así que el archivo tiene VERSIONES de lo que leíste. Un historial
de direcciones guarda punteros, y los punteros se pudren.

⚠ LA MITAD QUE NO SE PAGA HOY, dicha en el código, en el LEEME de allá y en el §6.3.bis: preguntarle
al historial en LENGUAJE NATURAL. Eso es el registro semántico y en la suite ya tiene forma —un motor
`rag-motor::RagMotor`, como `willay-rag`—, que necesita un daemon de embeddings y un backend LLM real
y devuelve `None` cuando no están. `archive.search` es el registro LITERAL: todas las palabras tienen
que aparecer. Enseñar lo uno diciendo que es lo otro sería el peor cambio posible.

LO QUE NO SE ARCHIVA ES LA PARTE QUE HAY QUE MIRAR: nada de una ventana privada, nada fuera del marco
principal (un `<iframe>` de publicidad no es una página que alguien leyó) y nada sin texto visible (el
HTML de un visor de PDF llenaría el archivo de cosas que no se encuentran). Y si no se puede saber si
la pestaña es privada, NO se archiva.

⚠ Y EL CONTROL NEGATIVO NO SÓLO COMPRUEBA QUE NO SE ARCHIVE: DICE QUIÉN LO IMPIDIÓ. La respuesta
medida no es la que uno supondría — hoy lo impide **el navegador**, porque las extensiones no corren
en ventanas privadas salvo que se las habilite, así que nuestro `if (incognito) return` no se ejecuta
nunca. No es código muerto: es lo que haría seguro habilitar `private_browsing` el día que haga
falta. Lo que sí habría sido un error es escribir «la extensión no archiva lo privado» sin saber cuál
de las dos cosas estaba pasando.

DOS TECHOS CON NOMBRE (en el host): 4 KiB de texto por página en el índice —es JSON para poder leerse
con `cat`, y uno sin techo deja de poder— y el corte por CARÁCTER y no por byte, porque `truncate`
sobre medio multibyte entra en pánico y en un archivo personal el texto con acentos es el caso normal.

Del lado de tawasuyu (`357791a8`, pineado acá): `archive.add`/`archive.search`, 6 tests nuevos (20 en
total) y el CAS renombrado de `descargas-cas` a `cas` — el mismo almacén guarda ahora lo bajado y lo
leído, y un nombre que describe la mitad de su contenido es el que se lee mal dentro de seis meses.

Medido sobre `atuq b3:5cfe3221` y `puriy-costura b3:c06f55aa`.
2026-09-10 14:58:50 +00:00
Sergio 897fad178a atuq: una descarga deja de ser un archivo con nombre y pasa a ser un objeto con identidad
La unidad 7 del SDD 26 (§6.2). Cuando una descarga TERMINA, la extensión
`descargas@atuq.tawasuyu` le pasa al host la ruta, el nombre y de dónde salió; el host la ingiere a
un CAS BLAKE3 y contesta el hash, el tamaño y **cuántas veces se bajó ese mismo contenido**. El mismo
contenido con otro nombre es UN objeto y dos nombres, y el navegador lo dice — que es exactamente lo
que ningún navegador sabe hacer: para todos una descarga es un blob con nombre, y por eso la bajás
dos veces y no te enterás.

No es un almacén nuevo: es `arje-cas`, el mismo formato y el mismo hash que usan arje, takana y
tejido. El `<hex>` del CAS ES el `expected_hash` de un `.swm`.

⚠ TRES DECISIONES QUE SALIERON DE MEDIR, NO DE DISEÑAR:

1. **La raíz del CAS de descargas no es la del sistema.** `arje_cas::gc` borra todo blob que no esté
   en el set `reachable` de su llamador, y el único que existe (`arje-brain`, `GcCas`) lo arma con la
   cadena de audit y las raíces vivas del grafo. Una descarga del usuario no está en ninguno: el
   primer GC se la llevaría, en silencio. Van a `<estado>/descargas-cas` — mismo formato (mover un
   objeto al CAS del sistema es un `rename`), fuera del alcance del GC de otro. El precio queda
   escrito: tejido sirve el CAS por defecto, así que compartir una descarga hoy exige apuntarlo ahí.
2. **El fichero del usuario no se toca**: se copia y se deja donde estaba. El guardián lo comprueba
   byte a byte. Borrar o mover lo que alguien acaba de bajar no es decisión de un navegador.
3. **La extensión observa, no intercepta.** Se entera cuando la descarga terminó. Meterse en el medio
   obligaría a decidir qué pasa si el CAS falla, y la respuesta correcta —que la descarga siga igual—
   es lo que se consigue no metiéndose.

Y EL HALLAZGO QUE COSTÓ LA TARDE, que no es de esta unidad: **en `--headless` el navegador se cae con
SIGSEGV en cuanto una descarga termina.** Se atribuyó con dos controles antes de tocar nada:

    --headless, con la extensión      → baja el fichero, TERMINADA, Segmentation fault (139)
    --headless, SIN la extensión      → baja el fichero, Segmentation fault (139)   ← no es nuestro
    --headless, panel de descargas apagado → Segmentation fault (139)
    sway headless (compositor REAL)   → baja, INGIERE al CAS, y no se cae          ← no es del producto

Sin el primer control esto se leía como «la extensión de descargas rompe el navegador» (perseguir un
bug que no existe); sin el último, como «atuq no puede descargar» (reportar un bug de producto que
tampoco existe). Por eso `scripts/test-atuq-descargas.py` corre sobre sway y no sobre `--headless`, y
por eso lo dice en su cabecera: un arnés distinto al de los demás guardianes, sin explicación, es una
invitación a «simplificarlo» de vuelta al que se cae. Queda en el §6.10.bis del SDD, que es donde vive
lo que atuq NO puede hacer.

El guardián baja de verdad (`Content-Disposition: attachment`, no una llamada a la API) dos veces
dentro de un mismo compositor, y trae control negativo: con OTRO contenido la segunda vez exige
`dedup=false` y dos objetos. Sin él, una sonda que dijera «ya lo tenías» siempre se vería idéntica a
una que funciona.

Del lado de tawasuyu (`ffa939c6`, pineado acá): `cas.ingest`/`cas.list` en el host y
`arje-cas::almacenar_fichero_en` — ingesta en streaming, porque `store` toma `&[u8]` y una ISO de
4 GiB serían 4 GiB de `Vec`. 5 tests nuevos en arje-cas y 5 en puriy-costura.

Y EL BUG QUE EL GUARDIÁN DESTAPÓ, que vale más que la función: **hay un host por PUERTO, no uno por
perfil.** Con `sct` y `descargas` hablando hay dos procesos vivos a la vez, cada uno con su copia en
memoria del estado, y cada uno escribía el fichero ENTERO al guardar: el de descargas borraba el
registro TOFU de `sct` al salir, y el de `sct` revertía el índice de descargas. Los dos contestaban
bien — el daño estaba sólo en el disco. Se vio porque el guardián lee el índice EN DISCO en vez de
creerle a la respuesta: decía `veces=2` y el fichero decía 1. Arreglado en tawasuyu (`55b918e8`,
pineado acá): cada proceso escribe sólo la parte que tocó, y el test que lo fija se comprobó
ROMPIENDO el arreglo a propósito — porque la primera versión de ese test abría los hosts en secuencia
y pasaba con el bug puesto.
2026-09-10 14:16:32 +00:00
Sergio 02513b861f atuq: sct v1 — el navegador avisa cuando un sitio ya estable ejecuta código que nadie vio nunca
La unidad 6 del SDD 26, que es el diferenciador del §6 que no tiene ningún navegador. Cadena entera,
medida de punta a punta con un servidor HTTP real y seis cargas de página:

    servidor HTTP → filterResponseData → connectNative → /usr/lib/mozilla/native-messaging-hosts/
                  → puriy-costura --state → puriy-sct (TOFU + bitácora)

    carga 1-3 (mismo script)          fase=learning  eventos=0   insignia vacía
    carga 4   (mismo script)          fase=stable    eventos=0   insignia vacía
    carga 5   (mismo script, estable) fase=stable    eventos=0   insignia vacía   ← control
    carga 6   (script CAMBIADO)       fase=stable    eventos=1   insignia "1"
              EVENTO ext:…/app.js 0ff4771fb797→f7ccb9fedfbd +27B

La extensión NO hashea ni guarda nada: ve bytes y pregunta. El registro es `puriy-sct`, del otro lado
del cable — duplicarlo en JS habría sido un segundo registro que se desalinea del primero, y el
primero es el que está certificado sin red.

CINCO COSAS QUE SE MIDIERON EN VEZ DE SUPONERSE, y las cinco fallan calladas:

1. el manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/` y NO en el appdir: la ruta sale de
   `XRESysNativeManifests`, un `/usr/lib/mozilla` COMPILADO dentro de Gecko;
2. **el manifiesto no puede llevar argumentos** — `NativeMessaging.sys.mjs` hace
   `command = manifest.path` y los únicos argumentos son `[ruta-del-manifiesto, id]`. Y sin `--state`
   el host corre en MEMORIA: cada arranque volvería a «aprendiendo» y nada alertaría nunca. De ahí el
   lanzador `bin/puriy-costura-host`, que además decide la ruta del estado — dónde vive el estado de
   un usuario es layout del FHS, o sea asunto de la distro y no del crate;
3. `filterResponseData` y el permiso `webRequestFilterResponse` SÍ están en nuestro `omni.ja`
   (se le preguntó al artefacto, no a la documentación de Mozilla);
4. los scripts `inline` NO se ven por esta vía —`filterResponseData` entrega el cuerpo de una
   PETICIÓN— y para v1 alcanza: el ataque que sct nombra es la sustitución en el CDN;
5. ⚠ **una carga de página puede producir dos peticiones del mismo documento, y una llega con
   `tabId = -1`.** La primera versión agrupaba por `(tabId, documento)` y contaba esa carga como DOS
   visitas. No es cosmético: inflar las visitas estabiliza el origen ANTES de conocer su código real,
   y entonces alerta por churn legítimo — el falso positivo que la spec de puriy-sct pide evitar por
   encima de todo. Ahora agrupa por documento (dos pestañas con la misma url cuentan UNA: es el error
   seguro, tarda más en proteger y no alerta de más) y el guardián VIGILA el invariante «una carga,
   una visita», así que si vuelve, falla ruidoso.

DOS CORRECCIONES DEL PROPIO §6.1, que decía «consulta al testigo antes de dejarla pasar»: el cable
del testigo es un POST con postcard, así que un JS no puede ser su cliente; y v1 OBSERVA Y AVISA, no
bloquea — es lo que puriy-sct dice de su propia v1, y poner un viaje entre procesos en el camino
crítico de cada script de cada página no es «más seguro», es un navegador que nadie usa.

EL AVISO SE MIDE, NO SE SUPONE: la extensión relee la insignia con `getBadgeText` después de ponerla,
y el guardián exige vacía en las cinco cargas sin novedad y "1" en la del script cambiado. Es la única
parte de la cadena que el usuario ve; dejarla en «se llamó a la API» era dejar sin medir el final.

CONTROL NEGATIVO: `--negative-control` borra el manifiesto y exige que NO haya veredicto — o sea que
el veredicto de la corrida positiva viene del host y no de la extensión inventándolo.

Y el aviso pasivo es decisión, no falta de tiempo: insignia y tooltip, no modal. Un modal por cada
despliegue de un sitio entrena a la gente a cerrarlo sin leer, y entonces el que importa también se
cierra.

Además: `rebrand.py` instala y CRUZA los manifiestos nativos (que el `path` exista y sea ejecutable
dentro del artefacto, y que sus `allowed_extensions` sean extensiones que de verdad empaquetamos), y
de paso se corrige el comentario del §4.ter que repetía la afirmación falsa sobre quién instala las
extensiones — lo mide `scripts/test-atuq-instalacion.py`: instala el escaneo de la carpeta.

`runtime = [..., "puriy-costura"]` en atuq.toml: sin eso la imagen llevaría manifiesto y extensión y
el host NO estaría, y la función se apagaría sola sin una línea de error. `yupana radio` confirma que
llega a las cuatro imágenes de escritorio.

⚠ Límite escrito en `fondo.js`, en el `lib.rs` del host y en los dos LEEME: se hashea el TEXTO ya
decodificado que entrega la extensión, no los bytes que sirvió el servidor. Vale para comparar dos
cargas nuestras; NO es comparable con el hash que publique un tercero sobre los bytes servidos, ni
con el de la v2, que engancha el script loader y ve los bytes reales.

Y una segunda cosa que el guardián encontró y que es del PRODUCTO, no del test: **la página que abre
el navegador al lanzarse puede no ser observada** — compite con la inicialización de la extensión, y
la carrera se gana o se pierde según la corrida. En uso real sólo afecta a esa primera página (después
la extensión ya está escuchando). Por eso las aserciones van sobre la SECUENCIA OBSERVADA y no sobre
un calendario: se exige que ninguna carga se observe dos veces, que la única que puede faltar sea la
del arranque, y que la secuencia aprender→estabilizar→no-alertar→alertar sea la correcta.

Sin regresiones: `test-atuq-politica.py` («guardianes: todos correctos», y sus cinco roturas siguen
matando el build con tres extensiones) y `test-atuq-inicio.py` (la home y la pestaña nueva siguen
siendo las nuestras) pasan sobre el artefacto final `b3:d3ced586`.
2026-09-10 01:57:07 +00:00
Sergio 60703e3975 puriy-costura: la receta del host de native messaging, y las dos trampas del monorepo
El binario de «la costura» (SDD 26 §7) entra al corpus: `b3:3b633431`, ELF estático musl de 952 K
pineado a tawasuyu `ebe41e96`. Verificado como artefacto y no sólo como build — un marco de 4 bytes
por stdin y contesta `{"id":1,"ok":true,"verb":"ping","version":"0.1.0"}`; y la promesa del §6.1
entera contra el binario SELLADO: cuatro visitas aprendiendo, a la cuarta `stable`, y en la quinta un
script cambiado ⇒ un evento que nombra el recurso y el delta de tamaño (+6 bytes). Deja
`registro.postcard` y `bitacora.postcard` en el `--state`.

Las dos cosas que costaron, las dos con el mismo patrón (el error no nombra la causa):

1. `cargo vendor --locked` murió con «cannot update the lock file», que NO dice qué crate falta. El
   lock de tawasuyu estaba desalineado con su propio `main`, y —esto es lo reutilizable— **un
   `Cargo.lock` regenerado en ese clon compartido NO describe su `main`**: ese árbol tiene cientos de
   ficheros en vuelo de otras sesiones. El nombre del crate culpable salió de diffear el lock
   publicado contra el que resuelve un árbol LIMPIO del commit (`git archive <sha> | tar -x`, que
   además no toca el `.git` compartido). Arreglado allá en dos commits; acá se pinea el que cierra.

2. Después murió por el `vendor/` propio del monorepo: tawasuyu parchea `smithay` a mano y lo enchufa
   con `[patch.crates-io] path = "vendor/smithay"`. Un patch por RUTA no necesita
   `.cargo-checksum.json` —y upstream no lo commitea—, pero `cargo vendor` convierte su directorio de
   salida en un directorio de REEMPLAZO DE FUENTE, donde cada crate sí tiene que traerlo. El síntoma
   fue 1 de 1699 crates sin checksum y un error que hablaba de `taffy`. La solución ya estaba en el
   corpus: `cargo_vendor_dir`, que las otras cinco recetas de este monorepo ya llevan.
   ⚠ Y la explicación FÁCIL era falsa: «cargo vendor borró el checksum» — `git ls-tree` dice que ese
   fichero nunca existió. Queda escrito en la receta para que nadie lo vuelva a deducir.

El precio, medido y anotado: el vendoreo es del workspace ENTERO (2,4 G, 1699 crates, `axum` y
`aws-lc-sys` incluidos) para un host que usa quince deps. Mismo precio que ya pagan las otras cinco.

NO se declara en ningún perfil de `targets.toml` todavía, y NO se agrega el manifiesto de native
messaging a atuq: sin la extensión que lo llame (unidad 6) sería un binario que nadie invoca y un
manifiesto apuntando a una extensión inexistente, que es un fallo silencioso — el navegador arranca
igual y la función no está.
2026-09-10 00:05:53 +00:00
SergioandClaude Opus 5 7b38e2ef16 kde: los portales — segundo hueco cerrado, y OBS ya tiene con quién hablar
Medido recorriendo los `.service` de D-Bus de TODO el store: los únicos backends de portal sellados
eran los de COSMIC y GNOME. Ninguno de KDE, y la cola `incoming-kde` no tenía ni el frontend.
Consecuencia concreta: la imagen KDE trae `obs-studio` con su plugin linux-pipewire, que captura
pantalla POR EL PORTAL — o sea que en Plasma no tenía con quién hablar mientras en los otros dos
escritorios ese frente estaba cerrado. Sin portal tampoco hay diálogo de fichero ni compartir
pantalla para nada que los use.

Van los DOS paquetes porque la cadena es de tres:
  cliente → org.freedesktop.portal.Desktop [frontend] → org.freedesktop.impl.portal.* [backend]
El frontend se copia de la cola de COSMIC y **comparte artefacto**: hash idéntico (ecbe13f68eca6)
desde las dos colas ⇒ un solo directorio en el store, cero builds.

El backend es receta NUEVA, 6.7.2 — la MISMA versión que plasma-workspace, kwin y kscreenlocker de
esta cola, porque habla interfaces privadas de kwin para el ScreenCast y desalinear ahí es pedir que
dos mitades del mismo release se entiendan por casualidad. **Cero frontera nueva**: sus 22
dependencias (Qt6, KF6, KWayland, protocolos de Wayland, xkbcommon) ya estaban todas, verificadas
una por una antes de escribir la receta.

⚠ SIN EL PORTAL DE IMPRESIÓN, y la causa es más honda que el error. El build moría en
`src/print.cpp:27` con `'QtPrintSupport/private/qcups_p.h' file not found`, y la cadena está medida
entera: no hay NINGUNA receta de cups en el catálogo ⇒ qtbase 6.11.1 se construyó sin CUPS ⇒ su
`QtPrintSupport/private/` trae qpaintengine_alpha_p.h y qprintengine_pdf_p.h pero NO qcups_p.h.
No falta una cabecera: falta el subsistema. Un portal de impresión sin con qué imprimir no es
funcionalidad que se pierde, es código muerto que no enlaza.

Upstream no expone `option()` para portales —se compilan todos—, así que el parche va en TRES
sitios y no en uno: la fuente, la instanciación (desktopportal.cpp/.h) y **la lista de interfaces
que el backend ANUNCIA**. El tercero es el que se olvidaría y el que importa: anunciar
`impl.portal.Print` sin implementarlo hace que el frontend enrute a un portal que no contesta, que
es peor que no tenerlo. Cada `sed` lleva su `grep` de verificación al lado, para que el día que
upstream mueva esas líneas el parche falle RUIDOSO en vez de volverse inerte.

VERIFICADO sobre el artefacto sellado: publica `org.freedesktop.impl.portal.desktop.kde.service`,
y su `kde.portal` anuncia 17 interfaces con **0 apariciones de Print** e incluyendo ScreenCast,
Screenshot y RemoteDesktop, que es exactamente lo que OBS necesita. Y sobre el rootfs hidratado:
están el frontend (`org.freedesktop.portal.Desktop.service`), el backend y el `kde.portal`.

⚠ Y la licencia casi la firmo mal: leí la cabecera de UN fichero y generalicé. Contadas las
etiquetas SPDX del árbol entero: 88 `LGPL-2.0-or-later`, 23 de la fórmula de KDE e.V. y 1
`GPL-2.0-or-later`. El binario las enlaza a todas ⇒ la expresión es la CONJUNCIÓN.

El perfil pasa a 98 raíces, 361/361 listo, deuda 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
2026-09-09 23:17:42 +00:00
Sergio 97c35d67b3 atuq: dos cosas que el README daba por ciertas eran falsas, y las dos se midieron
El README de la receta afirmaba (a) que las extensiones las instala la POLÍTICA porque el
sideloading desde `distribution/extensions/` «ya no funciona», y (b) que el chrome de verdad
—split view— exige entrar a `omni.ja` y escribirlo nosotros. Las dos son falsas, y ninguna
observación del artefacto las distinguía: hubo que romper un mecanismo por vez.

scripts/test-atuq-instalacion.py — tres escenarios:
  as-is        nada roto                             ⇒ las dos extensiones puestas
  no-policy    `policies.json` sin ExtensionSettings ⇒ SIGUEN puestas
  policy-only  los XPI fuera de la carpeta           ⇒ NINGUNA, y la home vuelve a about:home
⇒ instala el ESCANEO de la carpeta; `install_url` con `file://` no instala nada. El §7.bis del
SDD 26 tenía razón. El control es por construcción: la sonda vive en la carpeta y ninguna política
la nombra, así que una corrida muda se declara ROTA en vez de leerse como «no instaló». Y la
segunda señal que probé NO discrimina, queda dicho: el `location` de `extensions.json` sale
`app-profile` también cuando instala la carpeta.

scripts/test-atuq-chrome.py — el chrome se programa desde `atuq.cfg`, sin abrir el zip: observando
`browser-delayed-startup-finished` se toca el `gBrowser` de cada ventana. Y la vista dividida ya la
trae el motor (fx 154) PRENDIDA de fábrica, ejercitada de verdad —`addTabSplitView` ⇒
`activeSplitView` + 2 navegadores—, no «el fichero está». Dos pendientes que eran de upstream.
Control negativo del propio motor: con las pestañas fijadas devuelve null (WRAPPER null, ACTIVA no).

Corolario para lo que venga: cada función del chrome que NO entre a `omni.ja` es una que no pelea
con el orden del `jarlog` del PGO — que es justo lo que tiene al jarlog aparcado.

Queda escrito lo que NO está: ninguno de los `test-atuq-*` corre en el latido (sólo lo hace
`vigia-sonames.py`), y meterlos cuesta ~3 min por ciclo más el rootfs hidratado en el hub.

Medido sobre atuq b3:fab2fbfb → b3:8f6d09c2 (el README entra en el hash: documentar la medición
re-hashea el artefacto medido, y los guardianes se niegan a medir uno que no sea el vigente).
2026-09-09 23:15:09 +00:00
SergioandClaude Opus 5 542d9875fb kde: upower — la imagen declaraba powerdevil y no tenía batería
Primero de los dos huecos que dejaban a KDE POR DEBAJO de una instalación estándar. El perfil
declaraba `powerdevil` —el gestor de energía de Plasma— pero no a `upower`, que es con quien
powerdevil habla para leer la batería y aplicar perfiles. Medido: upower sólo existía en la cola de
GNOME, y las colas no se ven entre sí (HERMANO→corpus, nunca de reojo). En un portátil eso es un
escritorio sin indicador de batería, y el arranque no falla ⇒ no se ve hasta usarlo.

Cuesta TRES recetas y no siete, y la diferencia es una perilla: `-Dintrospection=disabled`.
GNOME la necesita ENCENDIDA porque su shell hace `imports.gi.UPowerGlib` desde JavaScript, o sea que
carga el typelib. powerdevil NO: habla D-Bus con `org.freedesktop.UPower`. Apagarla saca de la
frontera gobject-introspection, gi-foreign-girs, glib-introspected y py3-setuptools, que sólo viven
en la cola de GNOME.

⚠ Y NO SE PROMUEVEN AL CORPUS, aunque era lo primero que intenté. Al mover `libgudev` allá su
ArtifactHash CAMBIÓ (7404bc8d → 87ca5e73) y eso no es una mudanza: `glib` existe en las DOS colas y
son recetas distintas, así que desde `incoming-gnome/` se construye contra la glib de GNOME y desde
el corpus contra la del corpus. Promoverla habría arrastrado a GNOME a la glib del corpus — el
cuadro de las «dos glib» que este repo ya pagó caro. Revertido y verificado que vuelve al hash de
antes (7404bc8d), o sea GNOME intacto. Se COPIAN a la cola de KDE.
`udev-pc` sí comparte artefacto (sus deps son sólo pkgconf ⇒ mismo hash desde las dos colas, un
solo directorio en el store). `libgudev` es otro a propósito, por la glib.

VERIFICADO sobre el artefacto sellado, no supuesto: trae `usr/libexec/upowerd`, el
`org.freedesktop.UPower.service` de system-services y su política en `system.d` — que es exactamente
lo que powerdevil necesita para que el demonio arranque por activación D-Bus.

El perfil pasa a 49 raíces, 301/301 listo, deuda 0.

Queda el segundo hueco: KDE no tiene backend de portal (ni `xdg-desktop-portal` en su cola), o sea
que no hay compartir pantalla — y en particular la captura de obs-studio, que la imagen ya trae, no
tiene con quién hablar en Plasma. Eso es más caro: pide traer el portal a la cola y ESCRIBIR
`xdg-desktop-portal-kde`, que no existe como receta en ninguna cola.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
2026-09-09 22:02:49 +00:00
Sergio 9e5cfa1649 takana: las recetas que clonan el propio repo apuntaban a la URL renombrada
Rotura que introduje yo al renombrar el repo en gitea, y que no fallaba todavia
porque las tres tienen su artefacto sellado en cache:

  git ls-remote sergio/hammer.git  -> no responde
  git ls-remote sergio/takana.git  -> b393687d

hammerd, netup y portal-probe clonan el propio repo por ssh. Cualquier rebuild
de esas tres —o un hub nuevo sin store— habria muerto en el fetch.

El ArtifactHash NO se mueve, y esta comprobado receta por receta antes y despues
del cambio (48bbbe52, 8d093d74, 13ebb33d): la URL es locator y no entra en
hash_inputs, solo el commit (ADR 0013). Por eso mismo el arreglo es gratis.

Tambien el default de GITEA en espejo-setup.sh. git.tawasuyu.net y
git.gioser.net son la MISMA maquina (204.168.193.248), asi que el renombre le
aplica igual.

El espejo de GitHub sigue siendo sergiovelasquezzeballos/hammer: alla el repo no
se renombro. No rompe nada porque el push va por URL explicita, pero queda dicho.
2026-09-09 20:29:21 +00:00
SergioandClaude Opus 5 4e4a2c8d2a gnome: dash-to-panel, appindicator y gnome-tweaks — y la lista de activas sale de las recetas
GNOME deja de ser la cáscara: tres extensiones y la app que el proyecto no instala y todo el mundo
termina instalando. El perfil queda 190/190 con 28 raíces, deuda 0.

  dash-to-panel v73   junta dash, ventanas y bandeja en una sola barra (lo que más cambia el uso)
  appindicator v64    devuelve los ICONOS DE BANDEJA que GNOME quitó; sin ella las apps que usan
                      StatusNotifierItem corren y no tienen dónde mostrarse
  blur-my-shell v72   (ya estaba; se le saca el override, ver abajo)
  gnome-tweaks 46.1   tema, iconos, cursor, tipografías, los botones de minimizar/maximizar que
                      GNOME esconde, y el interruptor de las extensiones

⚠ LA LISTA DE ACTIVAS SE MUEVE A UN SOLO SITIO, y esto era una bomba de relojería. `enabled-extensions`
es un ARRAY: un override de GSettings lo escribe ENTERO. Con la primera extensión el override vivía
en su receta y funcionaba; con la segunda NO se habrían sumado — ganaría la que compile última
(orden alfabético) y las otras quedarían instaladas y MUERTAS, sin un solo error. Ahora la arma
`scripts/gnome/gnome-start-qemu.sh`, que ya compilaba los esquemas al arrancar, enumerando los uuid
presentes en /usr/share/gnome-shell/extensions. La política es «lo que la imagen instala, se
activa»; el dconf del usuario gana sobre el default.
Una receta NO puede ser ese sitio: hammer exige `[source]` en toda receta («missing field `source`»),
así que no hay forma de escribir una receta de pura política. Se probó el bloque contra un árbol de
juguete: lista las dos extensiones, salta un directorio sin metadata.json, el override compila con
stderr vacío y `gsettings get` devuelve las dos. Y si la lista cambia borra `gschemas.compiled` para
forzar recompilación — sin eso, una imagen ya arrancada ignoraría la extensión nueva en silencio.

⚠ EL HALLAZGO CARO: `msgfmt` de gettext-tiny REVIENTA con las formas plurales del árabe. SIGILL,
exit 132, «index 6 out of bounds for type 'size_t[6]'», y deja un .mo de 0 bytes. Reproducido FUERA
de la receta con el binario sellado. No es el idioma sino el mensaje: blur-my-shell compila su
ar.po sin problema porque no tiene NINGÚN plural (msgstr[5] = 0); el de tweaks tiene exactamente
uno. Acá se saca `ar` del LINGUAS y se pierde la traducción de UNA app, con el diagnóstico escrito
en la receta. ARREGLARLO ES OTRA UNIDAD DE TRABAJO: gettext-tiny la usan 108 recetas.

Cada receta usa el mecanismo de SU upstream, que no es el mismo en las tres:
  blur-my-shell   `make build` llama a `gnome-extensions pack` —el CLI que nuestro shell apaga— así
                  que se copia el árbol a mano, con el layout LEÍDO del zip de EGO y comparado
                  contra él (idéntico, sin sobras ni faltantes)
  dash-to-panel   su Makefile tiene un `install` que honra DESTDIR y hace lo de un paquete de
                  distro: gschema y locale a los directorios GLOBALES. ⚠ `VERSION=73` se pasa a
                  mano porque sin ella el Makefile hace `git describe` y el árbol viene por
                  `git archive`, sin `.git` ⇒ metadata quedaría con el marcador "version": 9999
  appindicator    meson. ⚠ `-Dlocal_install=disabled` explícito: en `auto` decide mirando si el uid
                  es 0, o sea que hoy acierta POR ACCIDENTE — y si el lab dejara de correr como
                  root instalaría en $HOME/.local y el artefacto sellaría vacío. Y `jq` va en deps
                  aunque sea JavaScript puro: su meson saca el uuid del metadata.json con jq

⚠ gnome-tweaks NO costó ninguna receta nueva, y eso hubo que medirlo: su meson declara siete deps
mínimas de runtime y las siete ya estaban selladas. La que parecía traer una cadena de Python
entera —`pygobject-3.0`— la provee `py3-gobject`, que YA ESTABA en el corpus con otro nombre y
publica `pygobject-3.0.pc`. El nombre no es el hecho, otra vez.
Sí costó dos deps que el configure reclamó y que son el patrón `.pc Requires` → `[deps].build`:
`appstream` (la pide `libadwaita-1.pc` y el error culpa a libadwaita, que está bien instalada) y
`desktop-file-utils` (meson la busca como PROGRAMA para `update-desktop-database`).
Sus deps de RUNTIME —python3 y py3-gobject— van declaradas: es una app de Python, si no están en la
imagen se instala y no arranca, y ningún build lo delata.

Las cuatro licencias, verificadas en la fuente pineada y NINGUNA leída del COPYING —cuyo apéndice
«How to Apply» trae siempre la frase «or any later version»—: blur-my-shell del README del commit,
dash-to-panel y appindicator de la cabecera de sus fuentes, gnome-tweaks de su etiqueta SPDX.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
2026-09-09 19:41:52 +00:00
Sergio e852f48491 takana etapa 5a: los comentarios de las 741 recetas
Barrido de TEXTO puro: 1427 líneas de comentario TOML. Cero ArtifactHash
movidos, y eso está MEDIDO, no deducido: hasheé las 741 antes y después y los
ficheros de hashes son idénticos byte a byte (723 con hash real + 18 que ya no
hasheaban de antes).

El guardián valió la pena: el primer barrido, filtrando por 'la línea empieza
con #', movió el hash de helix, lsof y steam-runtime-sniper. La causa es que una
fase se escribe como compile = <triple> ... <triple> y sus comentarios de SHELL
también empiezan con #, pero viven dentro del VALOR — y las fases sí entran en
hash_inputs. El barrido ahora calcula los rangos de las cadenas multilínea de
TOML y no entra ahí.

Quedan intactos a propósito: .hammer-zig-cc y .hammer-cargo-vendor (literales
dentro de fases), hammerd, hammer-recover y toda ruta que empiece por /
2026-09-09 19:23:26 +00:00
SergioandClaude Opus 5 8c2cb10414 gnome: la primera extensión del catálogo — blur-my-shell v72, instalada Y activa
La imagen de GNOME traía la CÁSCARA: el shell sabe cargar extensiones —`ui/extensionSystem.js` va
en el gresource sin opción que lo apague y su gschema sellado trae `enabled-extensions`— pero no
había ninguna que cargar. Esta es la primera, y fija el patrón para las otras ~1400.

⚠ NO HIZO FALTA TLS, que era la conclusión fácil del análisis de ayer. glib-networking sólo lo pide
BAJARLAS desde extensions.gnome.org con el descargador del shell. Empaquetada, una extensión se
instala como cualquier receta y no toca la red.

LA FUENTE ES EL REPO DEL AUTOR, NO EL ZIP DE EGO, por dos razones y las dos medidas:
  1. hammer no sabe abrir un zip: el fetch extrae con `tar -x` y GNU tar contesta «This does not
     look like a tar archive». Ninguna receta del corpus usa .zip.
  2. Y aunque lo abriera, el zip de EGO es un ARTEFACTO que construye EGO desde este mismo repo.
     Pinear el zip es pinear el binario de un tercero; pinear el commit es pinear el código.
El tag v72 es el que EGO sirve para shell 48 —preguntado a su API, no supuesto— y el metadata.json
declara ['46','47','48','49','50']. ⚠ Eso hay que preguntarlo por extensión: nixpkgs va por GNOME
50 y hay extensiones que ya sólo declaran el major nuevo.

SIN `gnome-extensions pack`, que es lo que usa el Makefile de upstream y es el CLI que nuestra
receta del shell apaga (-Dextensions_tool=false; encenderlo costaría gnome-autoar y re-hashear
gnome-shell entero). No hace falta: `pack` sólo arma un zip que después alguien descomprime, y lo
que el shell lee es un DIRECTORIO. El layout no se inventó — se leyó del zip de EGO v72, que es la
salida de ese mismo `pack`, y el artefacto sellado se comparó contra él: **entradas idénticas, ni
sobras ni faltantes**, 45 .mo compilados.

⚠ INSTALADA ≠ ACTIVA, y esto es lo que hace que la receta sirva. El shell sólo carga lo que esté en
`org.gnome.shell enabled-extensions`: sin eso el artefacto sella, el fichero está en la imagen y NO
PASA NADA al arrancar — el mismo cuadro de «sellado ≠ instalado» que este repo ya se comió con
foot, mpv y libnotify, un escalón más abajo. Se resuelve con un override de GSettings, que cambia
el DEFAULT y deja ganar al dconf del usuario que no la quiera.
PROBADO DE VERDAD, no deducido: se compiló el directorio de esquemas del shell con el override
puesto (`glib-compile-schemas`, exit 0 y stderr vacío — que hay que mirar, porque sale 0 aunque
RECHACE un esquema) y después `gsettings get org.gnome.shell enabled-extensions` devolvió
['blur-my-shell@aunetx'].

⚠ TRAMPA ANOTADA PARA LA SEGUNDA: `enabled-extensions` es un ARRAY y el override lo escribe ENTERO.
Dos recetas con su propio override no se suman — gana la que compile última y la otra queda
instalada y muerta, sin error. Cuando llegue la segunda, la lista pasa a un solo sitio.

Y una corrección sobre la licencia, que casi firmo mal: el LICENSE es el texto del GPLv3, y su
apéndice «How to Apply» CONTIENE la frase «either version 3 … or any later version» siempre, así
que grepearla ahí no distingue -only de -or-later (la trampa que la memoria del repo ya tenía
escrita). La evidencia buena es el README del commit pineado: «This program is distributed under
the terms of the GNU General Public License, version 3 or later». El árbol JS no lleva cabecera de
licencia en ningún fichero.

Va de RAÍZ en escritorio-gnome por la lección de `foot` que targets.toml ya aprendió tres veces.
El perfil queda 183/183 listo con 25 raíces, deuda 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
2026-09-09 19:14:08 +00:00
SergioandClaude Opus 5 71904ebbde xwayland: retirar la receta — la decisión ya estaba escrita y decía que no
Decisión del usuario (2026-09-09): X11 se erradica, y eso incluye no construir el servidor que lo
mantiene vivo. La receta se retira; `xwayland` VUELVE a ser lo que sus propios documentos decían.

⚠ LO QUE APARECIÓ AL MIRAR: esto ya estaba decidido, por escrito y DOS VECES, el 2026-09-03
(commit f64859ba):
  docs/state/targets.toml:143   «`xwayland` sigue acá como RAÍZ pero su receta NO se va a escribir:
                                 lo provee una imagen ajena enjaulada … el hueco se disuelve sin
                                 receta y sin tocar la postura Wayland-only»
  docs/state/qorpa-ajenos.toml  «X11 está fuera de alcance para toda la distro … el Xwayland vive
                                 DENTRO de la imagen —Arch y Fedora ya lo traen— y se cuelga de
                                 nuestro kwin por el socket. La receta no se escribe.»
El 2026-09-08 se escribió igual (3d8c6fc6, «cerrar los 3 huecos que quedaban»), y los dos
documentos siguieron diciendo lo contrario durante toda la jornada de hoy. No hubo que cambiar
ninguno de los dos: alcanzó con borrar la receta para que el repo volviera a coincidir con ellos.

MEDIDO ANTES DE SACARLA, no supuesto:
  · Sólo kwin podía lanzarlo. mutter va con -Dxwayland=false y wlroots con -Dxwayland=disabled ⇒
    GNOME, sway y COSMIC no podían usarlo aunque el binario existiera. Nunca sirvió a más de un
    perfil de los cuatro.
  · NINGUNA app del catálogo necesita servidor X: todo lo gráfico es Wayland nativo (Qt6 con
    qtwayland, GTK3 Wayland-only, mpv, OBS con ENABLE_WAYLAND=ON, foot, fuzzel, swayimg). El
    consumidor que lo justificaba está nombrado en qorpa-ajenos.toml y es Steam, que corre
    enjaulado y TRAE EL SUYO ADENTRO.
  · Y la imagen KDE ya arrancaba sin él: la propia receta contaba que KDE sellaba 1037/1037 con las
    apps X11 muertas. Esto no estrena una configuración, vuelve a una ya probada.

EFECTO EN EL GRAFO, verificado regenerando: `xwayland` deja de ser una receta y pasa a clase
`ajeno` —que es como targets.toml decía que había que contarlo— y el perfil escritorio-kde baja de
304 a 299 nodos con deuda 0. Los cinco que se van son la cadena que sólo él usaba.

⚠ QUEDAN CINCO RECETAS HUÉRFANAS, y no las borro de paso: `libfontenc`, `libXfont2`, `libxkbfile`,
`libxshmfence` y `xkbcomp`. Medido: NADA más en el catálogo las declara, y ninguna es raíz de
ningún perfil ⇒ siguen selladas pero no entran en ninguna imagen. Sacarlas es su propia unidad de
trabajo y su propia decisión.

El triaje pasa de `provisto` a `opcional` con la historia entera en su `porque`, y con eso se cae
el alias `xwayland xwayland` de alias-triaje.txt, que habría afirmado que tenemos una receta con
ese nombre.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
2026-09-09 18:55:20 +00:00
SergioandClaude Opus 5 3379a1f170 licencias: las 26 que faltaban, y el guardián que las contaba mal
CAMPAÑA CERRADA: 1166/1166 recetas declaran `license`. Ninguna adivinada — cada una sale del
fichero de licencia de su fuente PINEADA (tarball del sha256 de la receta, o el commit exacto en
la forja), y la cita queda como comentario en la propia receta.

⚠ Y EL GUARDIÁN ESTABA MAL, que es el hallazgo que vale más que las 26. `licencias-rootfs.sh`
resolvía la receta por NOMBRE DE FICHERO (`ls recipes/$pkg.toml`), y el paquete se llama por su
campo `name`, que en 34 recetas NO coincide: nu.toml→`nushell`, dust.toml→`du-dust`,
incoming-kde/qtbase.toml→`qt6-qtbase`… Medido: **14 paquetes que SÍ declaran licencia salían como
«licencia desconocida»** y el guardián vetaba una imagen perfectamente publicable.
El falso veto se nota; el hermano silencioso NO: si existe un `<pkg>.toml` que pertenece a OTRO
paquete, la versión vieja reportaba la licencia EQUIVOCADA sin decir nada. Hoy no pasa —medido,
0 casos, y de los 34 nombres duplicados CERO declaran licencias distintas—, pero ahora es
imposible en vez de improbable.

El arreglo tuvo que ser por LOS DOS lados, y el primer intento rompió el otro: el grafo de estado
nombra sus nodos por el fichero (`dust`) y el artefacto del store por `name` (`du-dust`), así que
resolver sólo por `name` dejaba a `dust` sin licencia. Ahora busca por `name` y cae al fichero.
Medido en los dos sentidos: corpus entero 1128/1128, perfil base+cli 81/81, cero sin licencia.

Y probado CON ROTURA A PROPÓSITO además del control, que es lo único que distingue a un guardián
que sirve de uno que nunca salta:
  paquete inexistente en la lista               → exit 1 y «ESTA IMAGEN NO SE PUEDE PUBLICAR»
  control (zlib nushell qt6-qtbase lsof tzdata) → exit 0, y escribe los textos
(⚠ ojo al medir: `script | tail` devuelve el exit de `tail`. La primera corrida dijo exit=0 sobre
la rotura y no era el guardián, era el pipe.)

SE LEVANTA EL VETO QUE SDD 20 DEJÓ ESCRITO. Decía que `base` y `cli` iban con 2 paquetes cada una
con binarios y licencia desconocida: `lsof` y `tzdata`, «que necesitan la vía LicenseRef- y siguen
vetando a propósito». Hechos los dos, con su texto real en licenses/:
  lsof    → LicenseRef-lsof (licencia propia de Purdue, sin identificador SPDX)
  tzdata  → LicenseRef-tz-public-domain (su LICENSE: «all files in the tz code and data … are in
            the public domain»; los tres ficheros BSD-3-Clause que menciona NO se instalan — la
            receta sólo compila zic y deja /usr/share/zoneinfo)

⚠ DOS QUE NO SE PUEDEN REDISTRIBUIR, y ahora el veto los ve:
  duplicacy    NO ES LIBRE. Su LICENSE.md: «Free for personal use or commercial trial; non-trial
               commercial use requires per-computer CLI licenses … $50 per year»
  waybackurls  NO DECLARA LICENCIA: en el commit pineado la raíz es .gitignore, README.mkd,
               go.mod, main.go y script/ — sin LICENSE ni COPYING, y el README no la menciona. Sin
               concesión expresa, el defecto es «todos los derechos reservados»
Los dos con LicenseRef y un texto en licenses/ que explica qué hay, en vez de dejar el campo vacío,
que se lee como «todavía no lo poblamos». Ninguno está hoy en un perfil de imagen; si alguien los
mete, el guardián corta.

De paso queda escrito el texto de `LicenseRef-qorpa-ajena-no-enumerable`, que ya se usaba en
steam-runtime-sniper y no tenía fichero; y `licencias-textos.sh` bajó los canónicos nuevos
(BSL-1.0 para boost, GCC-exception-3.1 que ya hacía falta).

Hueco conocido y anotado en la receta: `XFree86-1.0` (rama del OR de hwdata) se queda sin texto —
SPDX no publica ese identificador, sólo XFree86-1.1, que es otra licencia, y el tarball lo nombra
sin incluirlo. El guardián avisa y no veta, que es correcto: la otra rama del OR es la GPL y su
texto sí está.

NADA SE RE-HASHEA: `license` está fuera de `hash_inputs`. Verificado, no supuesto — `hammer hash`
sobre pigz, lsof y boost después de editarlas devuelve el hash cuyo artefacto YA está en el store.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
2026-09-09 17:47:16 +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
SergioandClaude Opus 5 a4a462b751 sombras: barrer 14 recetas redundantes que ya tenían copia en el corpus
Las 14 sombras marcadas 'redundante' dan el MISMO ArtifactHash en todas sus colas — no son
variantes, son el mismo artefacto declarado dos, tres, cuatro y hasta cinco veces. Medido con
'hammer hash' receta por receta, no deducido del nombre.

Se barren sólo las que YA tenían copia en el corpus, porque la resolución es sibling-first: una
dep desde incoming-kde busca incoming-kde/x y, si no está, CAE al corpus/x. Son 8 nombres, 14
ficheros: bzip2-shared, glib-shared, lcms2, mpc, pcre2-shared, sqlite-shared, xkeyboard-config
y xz-shared.

13 de los 14 eran byte a byte idénticos al del corpus. El decimocuarto, xkeyboard-config,
difiere en 14 líneas de COMENTARIO (mismo hash), y ese comentario es el permiso explícito para
esto: quien la promovió el 2026-09-05 escribió que dejaba las copias a propósito y que
'barrerlas es otra unidad de trabajo, no un efecto colateral de ésta'. Ésta es esa unidad.

Verificado con la huella de las 1181 recetas antes y después: NINGÚN hash superviviente se
movió ⇒ cero rebuilds. Y tras regenerar los cinco grafos: deuda 0, huérfanas [], y todas las
clausuras de perfil siguen completas.

⚠ Nota sobre una lectura que casi me confunde: los conteos de 'recipes' NO bajan (kde sigue en
1034) y eso es lo correcto — cuentan nodos alcanzables, no ficheros. Antes incoming-kde/x tapaba
a corpus/x; ahora el del corpus ocupa su lugar. Un nodo por otro.

Quedan 6 nombres (fuse3, hwdata, icu4c, libdisplay-info, libelogind, lua) sin copia en el
corpus: ésos exigen promover una primero, y van aparte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-08 19:44:28 +00:00
SergioandClaude Opus 5 7892d50842 json-glib: --prefer-static — declaraba static y sus binarios no corrían fuera del sandbox
'-Ddefault_library=static' gobierna cómo se construye la librería PROPIA, y eso funcionaba: el
artefacto trae libjson-glib-1.0.a. Pero no dice nada de con qué se enlazan los EJECUTABLES, así
que json-glib-format y json-glib-validate salían dinámicos contra libffi.so.8, libz.so.1 y
libc.so — y ese libc.so es la musl de zig, 255 bytes de linker script en el host. O sea binarios
que NO CORREN fuera del sandbox, en una receta que declara link=static. Mismo remedio que
appstream, que ya llevaba --prefer-static.

Medido antes y después:
  json-glib-format    dinámico, 3 NEEDED  →  ESTÁTICO, 0 NEEDED
  json-glib-validate  dinámico, 3 NEEDED  →  ESTÁTICO, 0 NEEDED
  libjson-glib-1.0.a  igual (los consumidores no cambian de forma)

Y la prueba del consumidor, que es la que vale: json-glib-format sobre un JSON real corre EN EL
HOST y devuelve {"a":1}. (El ruido de libgvfsdbus.so en stderr es glib intentando dlopen
módulos GIO desde un binario estático: inocuo, la salida sale bien.)

Cascada reconstruida en orden de onda, con el cierre transitivo sacado de yupana y no de un grep
—que no veía a zathura ni a zathura-pdf-poppler por tener el array de deps multilínea, y la regla
del repo es justamente ésa—: zathura, incoming-cosmic/xdg-desktop-portal, zathura-pdf-poppler.

Verificado después, no antes:
  static-audit   MIENTEN 0 de 690    toda receta que declara link=static lo cumple
  los 5 grafos   deuda 0
  repro          REPRODUCEN 3 · DERIVA 0 · NO-DETERMINISMO 0

⚠ incoming-gnome/json-glib NO se toca: declara link="dynamic" a propósito porque el shell de
GNOME dlopea .so reales para introspección. No miente — es honesta, y su radio de 10 no se paga.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-08 19:26:12 +00:00
SergioandClaude Opus 5 381780aab4 fuentes: tapar el agujero de libtool y que el vigía distinga noticia de emergencia
El vigía decía 5 fuentes muertas y sólo UNA estaba en riesgo de verdad: busybox y freetype
tenían el tarball en la caché local Y en el mirror, y firefox-pgo-profile apunta a .invalid a
propósito. La que no tenía copia en ningún lado era libtool (incoming-kde), con ftpmirror.gnu.org
devolviendo 502 — o sea una receta que ya no se podía reconstruir y nadie lo sabía.

Arreglado: el tarball se bajó de ftp.gnu.org, se verificó contra el sha256 PINEADO (coincide
byte a byte), y está en la caché local y en el mirror. La URL de la receta pasa al espejo que
responde, y se midió que eso no re-hashea nada — ANTES y DESPUÉS dan el mismo
b3:5c09bff5..., que es el ADR 0013 comprobado en vez de citado.

Y el vigía ahora cruza cada fuente muerta con la caché local y el mirror, porque mezclar
'upstream caído con el blob guardado' (noticia) con 'caído y sin copia' (emergencia) hace que
el número suba, nadie lo mire, y el día que uno importe esté enterrado entre cuatro que no. A
un guardián lo mata el ruido. Ahora informa EN RIESGO aparte:

  == vivas 1175 / 1179   muertas arriba 4   EN RIESGO 0

⚠ Y si el mirror no se puede consultar —el worker no tiene la clave del Storage Box— dice
'indeterminado', NUNCA 'EN RIESGO': un instrumento que no pudo correr no puede parecer un
hallazgo.

Probado con rotura a propósito y con los dos controles, que es lo que separa un guardián que
sirve de uno que nunca saltó:
  sha inexistente + upstream muerto → EN RIESGO        ✓ salta
  blob en caché + upstream muerto   → cache-local      ✓ no salta
  mirror inalcanzable               → indeterminado    ✓ no alarma en falso

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-08 19:14:21 +00:00