097579546f4dce4b7dd8523c967abb4ab8af280a
880
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
097579546f |
dwarves: el linker de zig se caía con --dependency-file — una línea, y sin escapar a gcc
`dwarves` estaba SELLADA y llevaba tiempo sin construir. Se destapó al arreglar los symlinks de
`bzip2`: su hash se movió, dwarves cayó a deuda, y al reconstruirla el link murió con
`Error running link command: Segmentation fault` — crash del linker, no error de símbolos.
**No lo rompió el cambio de bzip2, y se puede probar**: el `libbz2.a` y el `bzlib.h` nuevos son
byte-idénticos a los viejos (`cmp -s`); lo único que cambió fueron cuatro destinos de symlink. El
artefacto sellado tapaba una rotura que ya existía — el lab rueda desde Alpine edge y zig subió. El
rehash no causó la rotura: la DESTAPÓ.
## La causa, bisecada sobre la línea de enlace real
CMake deja la línea literal en `build/CMakeFiles/<target>.dir/link.txt`, y el árbol de post-mortem la
conserva. Rehecha a mano FUERA de takana y del sandbox, sustituyendo el wrapper por el `zig` del lab
y las rutas `/usr/lib/*` por las del store:
tal cual → exit 139 (SIGSEGV)
quitando `-static` → exit 139 ⇒ no es el enlace estático
quitando `--dependency-file` → **exit 0** ⇒ ES ESO
`-Xlinker --dependency-file=…` lo emite CMake ≥3.27 para que el LINKER calcule las dependencias de
enlace, y el `lld` de zig 0.16.0 segfaultea procesándolo. `-DCMAKE_LINK_DEPENDS_USE_LINKER=OFF` es la
palanca de upstream (cmake del corpus: 3.31.6).
**Es mejor que `compiler = "gcc"`**, que es como esquivé ayer el MISMO crash en los tres
`protoc-gen-upb*` de protobuf sin conocer la causa: deja la receta en el toolchain por defecto del
proyecto en vez de escapar de él.
⚠ **Y el alcance no son dos recetas: 153 del corpus usan CMake con zig.** Todas selladas, así que hoy
nadie lo ve — pero el crash depende de los inputs (dentro de protobuf caían 3 de ~10 ejecutables), o
sea que no se sabe cuáles fallan hasta que su hash se mueva. Deuda latente pura.
No se arregla poniendo la perilla en el lab: la mayoría de esas 153 traen su `cmake …` EXPLÍCITO en
la receta, así que tocar la fase por defecto no las tocaría **y** re-hashearía a las que sí usan la
heurística. Incompleto y disruptivo a la vez.
Verificado: los 10 ejecutables estáticos con 0 NEEDED —incluidos `codiff` y `dtagnames`, los dos que
segfaulteaban—, `pahole --version` → v1.30, y REPRODUCE bit a bit.
|
||
|
|
34f30bfe2b |
ia-modelo-embeddings: el guardián probado EN EL HUB antes de gastar un turno de build — tres fallos
El guardián nuevo (tokeniza español + el espacio distingue) se corrió a mano contra el modelo antes de esperar el lock, y estaba mal de tres formas distintas: 1. **`cos` es una función interna de awk**, así que `cos[2]` muere con un `syntax error` que no menciona el nombre. Se llama `cs`. 2. el `sed` que partía por corchete de apertura **se comía el último vector** (salían 2 de 3, y el `test -ge 3` lo habría cazado, pero fallando por el motivo equivocado). Ahora `grep -o` por el corchete completo. 3. y los backslashes iban DOBLES: en un literal TOML de comilla triple no se escapan, así que la shell recibía `\\n` y `tr` se ponía a borrar las letras «n». Y una cuarta, de mi propio comentario: al explicar el punto 3 escribí la comilla triple **dentro** de la cadena que empieza con comilla triple. Cerró el literal y el TOML dejó de parsear, con el error apuntando a la línea del comentario. Queda avisado ahí mismo. Verificado en el hub: guardián 2 → 11 y 14 tokens distintos, 0 comunes; guardián 3 → gato~perro=0,8436 contra gato~cortafuegos=0,6292. Los dos PASAN con el modelo bueno. |
||
|
|
c78c5ff91f |
ia: el modelo de embeddings elegido NO servía — Qwen3-Embedding-0.6B en su lugar, y un guardián que lo habría cazado
multilingual-e5-small sellaba, cargaba, contestaba 384 dimensiones y 45 tests en verde. Y con el modelo de verdad, de punta a punta, el ranking devolvía SIEMPRE la misma página. Seis pasos descartando hipótesis —batching, posición, nuestro código, la cuantización, la conversión— hasta que `/tokenize` lo dijo en una línea: «cortafuegos», «minino», «duerme» y «tejado» van todos al id 100 = `<unk>`. Un vocabulario XLM-RoBERTa por esta ruta deja casi todo en desconocido, y un texto que es todo `<unk>` embebe igual que cualquier otro. La pista estaba a la vista desde el principio: `gato~perro` y `gato~cortafuegos` daban el mismo número a CUATRO DECIMALES. En su lugar, Qwen3-Embedding-0.6B Q8_0, GGUF oficial de Qwen (Apache-2.0): tokeniza español de verdad (`cort|af|uegos`), acierta 3/3 con márgenes anchos (0,649 contra 0,237), y es de la misma familia que el modelo de chat. Cuesta 610 MiB en vez de 126: es el precio de que funcione, y sube la cuenta de la imagen a ~1,85 GiB si se declara. ⚠ Y LA PARTE QUE IMPORTA PARA LA PRÓXIMA VEZ: el guardián del `install` ya no mira sólo el mágico y el tamaño —«existe» no es «sirve»—. Ahora tokeniza dos frases en español sin palabras en común y exige que no compartan tokens (con el e5 roto compartían la mitad, todos `<unk>`), y después levanta el servidor de verdad y exige que un gato se parezca más a un perro que a un cortafuegos. Con esos dos chequeos el e5 no habría sellado nunca. El tar del modelo roto se borró del mirror (476 MB) y del disco. ⚠ El build está en la cola del flock detrás de otro agente (pixi, 22 min y contando), así que el artefacto todavía no está sellado y los guardianes del navegador no corrieron. Lo medido hasta acá: el camino completo host→motor→índice con el modelo nuevo, 3/3 (`archive.ask` de punta a punta, fuera de la jaula), más 45 tests en tawasuyu. |
||
|
|
0ca88fdcd5 |
atuq §6.3.ter: preguntarle al archivo por lo que DECÍA, no por las palabras exactas
El muro no era un daemon ni un LLM — y el propio comentario de la extensión lo decía mal, copiando lo que hace willay-rag. Medido: para ordenar por parecido no hace falta ningún LLM (eso es un coseno) y el «daemon de embeddings» resultó ser el mismo llama-server que ya levanta el chat, con otro modelo. La corrección quedó escrita donde estaba la afirmación. Del lado de la suite (tawasuyu 22527f7d1 y b4ecffea8): el protocolo de llama-server salió a `shared/foreign-llama` (regla 4 — vivía dentro de puriy-costura y ya tenía dos consumidores), el `Provider` es `rimay-verbo-llama` (regla 10 — la familia verbo YA es la abstracción de embeddings, y éste es su primer backend sin nube ni descargas), y el índice es `rimay-verbo-index::VectorIndex`. Acá: la receta del modelo (multilingual-e5-small, MIT), que se pinea en fp32 y la receta CUANTIZA a Q8_0 con nuestro llama-quantize — así la procedencia es de quien declara la licencia, la imagen se lleva 126 MB en vez de 476, y la transformación es nuestra y verificable. Más el guardián y la extensión, que ahora expone `archive.ask` al lado del `archive.search` literal: son dos preguntas distintas y conviven. ⚠ El guardián está escrito pero NO corrió todavía: `ia-modelo-embeddings` quedó en la cola del flock detrás del build de otro agente. Lo medido hasta acá es el modelo a mano (384 dimensiones, el pasaje correcto gana con y sin los prefijos de e5) y 45 tests en tawasuyu. La línea del SDD que lo dice se borra cuando dé verde. |
||
|
|
0a9622c7b0 |
kernel kexec: el reboot suave por kexec_file_load, sin kexec-tools
ADR 0017 §1. Se usa kexec_file_load, no el kexec_load viejo, y la diferencia es de orden de magnitud: kexec_load recibe los segmentos YA armados y el purgatory —el código que corre entre los dos kernels—, o sea que usarlo obliga a reimplementar kexec-tools entero. kexec_file_load recibe los DESCRIPTORES del kernel y del initrd y hace el trabajo adentro. Son ~50 líneas. El syscall va con asm! en vez de agregar la crate libc al workspace: es UN syscall, su número y su ABI son contrato estable de Linux, y la alternativa era arrastrar una dependencia a un workspace que comparten dos frentes. El cmdline viaja CON su NUL: el kernel cuenta cmdline_len incluyendo el terminador, y sin él lee un byte de más. Es el error clásico de esta llamada. recipes/linux-metal-kexec.toml — variante cuya ÚNICA diferencia es CONFIG_KEXEC_FILE=y. Variante y no flag en la canónica porque el .config ES la identidad del artefacto (SDD 22 §1): tocar linux-metal re-hashea el kernel que arranca las imágenes y el que reproduce bit a bit, y el ADR deja esa decisión al usuario. Comprobado que la canónica no se mueve: sigue en b3:2ed8f54a…, el que ya está sellado. Y un diagnóstico que corregí a los dos minutos de escribirlo, porque mandaba al lugar OPUESTO: decía "falta CONFIG_KEXEC_FILE" para EPERM. Medido en este hub — el kernel de Artix trae CONFIG_KEXEC_FILE=y y aun así devolvió EPERM, por no ser root. EPERM es falta de CAP_SYS_BOOT (o lockdown si ya sos root); ENOSYS es el flag que falta. Confundirlos manda a recompilar un kernel que estaba bien. La ayuda del comando dice con todas las letras que esto NO es "actualizar sin rebootear": el userspace muere igual. Ahorra los 20-30 s de POST/UEFI, que es la mitad del downtime en un servidor remoto y nada en un portátil. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj |
||
|
|
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`. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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
|
||
|
|
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
|
||
|
|
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.
|
||
|
|
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).
|
||
|
|
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 |
||
|
|
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`.
|
||
|
|
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`.
|
||
|
|
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. |
||
|
|
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`.
|
||
|
|
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á.
|
||
|
|
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 |
||
|
|
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). |
||
|
|
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 |
||
|
|
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 ->
|
||
|
|
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
|
||
|
|
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 / |
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|