Commit Graph
1807 Commits
Author SHA1 Message Date
SergioandClaude Opus 5 f64859bade qorpa export: shims generados, y la clase ajeno para que nadie los cuente mal
Paso 5 del ADR 0015, sus dos mitades.

SHIMS. `hammer qorpa export <id>` genera lanzadores finos y `.desktop` en el
espacio del host, desde lo DECLARADO en `[export]` — nunca todo: exportar todo
haría que el `ls` de la imagen compita con el nuestro, que es la falla de
Bedrock (arbitra en tiempo de exec, por heurística). Gana en tres cosas contra
un FUSE: cero costo en runtime, `cat` al shim y ves qué hace, y se revoca
borrándolos.

Se GENERAN, no se copian. El `.desktop` se arma con lista BLANCA de claves, así
que `Exec`, `TryExec`, `Path` y `DBusActivatable` quedan fuera por definición y
no por enumeración — una lista negra dejaría entrar la próxima clave ejecutable
que invente el estándar. El Exec original se cita en un comentario del fichero
generado, para que se vea qué decía y qué no se copió. El icono se busca en la
vista merged (upper primero, imagen después: si no, se perdería lo que instaló
el gestor de paquetes) y se copia al host, porque un icono que el host no
resuelve se ve como un cuadrito gris. Y `exported.json` registra cada fichero
escrito, para que `--remove` borre EXACTAMENTE eso y no por patrón sobre el
~/.local/bin de alguien.

Probado de punta a punta con un .desktop ajeno real de la imagen de Arch: el
shim corre `pacman -Q` del huésped desde el host, el X-KDE-Wayland-Interfaces
quedó fuera, el icono viajó, y --remove dejó 0 ficheros con la instancia intacta.

CLASE `ajeno`. build-state.py inyecta los nodos declarados en el nuevo
docs/state/qorpa-ajenos.toml ANTES que los `wanted`, y ese orden es la mitad del
punto: un nodo que provee una imagen ajena no es una receta por escribir. Con
eso el `xwayland` de escritorio-kde deja de ser deuda y pasa a contarse aparte:

  escritorio-kde       187/188 listo   falta   1  (raíces 14, + 1 ajenas)

Dos decisiones que sostienen esa cifra: los ajenos se RESTAN del denominador (si
entraran, el número que se lee como "cuánto construimos" crecería solo cada vez
que alguien enjaula una app), y la declaración vive en el REPO y no se lee de
/var/lib/hammer — build-state.json se commitea y lo regenera el cron en dos
máquinas; si la clase saliera de las instancias instaladas, cada una diría algo
distinto y se pisarían en cada cosecha. Es el error que ya se cometió con
sealed_remoto. Qué provee una imagen ajena es diseño; qué tenés instalado, no.

Un ajeno tampoco se hashea, y no por comodidad: no tiene procedencia de fuente,
así que un hash afirmaría que lo reproducimos.

2 tests nuevos (que del .desktop ajeno no sobreviva nada ejecutable; que el shim
no se rompa con rutas raras). 47/47.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 06:15:19 +00:00
Sergio 3448ca4678 estado: cosecha granja 2026-09-03T06:02:15Z — avance del árbol KDE 2026-09-03 06:02:15 +00:00
Sergio f04364fc06 pipewire al corpus: falta el parche, que quedó fuera del commit ajeno y dejó la receta rota
⚠ INCIDENTE, y queda escrito porque es la regla 2 del CLAUDE.md en vivo: los cuatro `git mv` de
esta promoción (pipewire, pulseaudio, libsndfile, dbus-shared) estaban STAGED y sin commitear
cuando `89da9c8` («qorpa: el mapeo por rango») se los llevó puestos. El commit ajeno barrió el
índice de otro agente — exactamente lo que la regla prohíbe— y encima lo hizo A MEDIAS: se llevó
los cuatro `.toml` pero no el `.patch`, porque el `git mv` del parche se cortó cuando el disco se
llenó al 100%.

Resultado: HEAD quedó con `recipes/pipewire.toml` declarando `patches = ["pipewire-sound-
initialized.patch"]` y ese fichero **inexistente en `recipes/`**. Cualquiera que clonara veía
`Error: receta inválida: no pude leer patch`. Esto lo repara.

El hash confirma que la promoción es gratis: `recipes/pipewire.toml` sella
b3:e240653adae656318d5c3445b36ffb37aa491589b5c34c8688515bc3806944ab, EXACTAMENTE el mismo que tenía
`incoming-cosmic/pipewire`. Era la variante que ya resolvía todo su cierre contra el catálogo padre.
2026-09-03 05:34:32 +00:00
Sergio b1713dd221 estado: cosecha granja 2026-09-03T05:31:09Z — avance del árbol KDE 2026-09-03 05:31:09 +00:00
SergioandClaude Opus 5 89da9c822f qorpa: el mapeo por rango — subuid + --userns FD, y el impuesto se paga
Resuelve §NO-resuelve 2 del ADR 0015, que era el ticket que más desbloqueaba.
Tres síntomas que parecían distintos —apt sin poder bajar a `_apt`, pacman sin
poder chownear a `alpm`, pressure-vessel sin poder escribir su uid_map— eran la
misma causa: bwrap crea el userns con UN SOLO id.

La cura resultó tener TRES partes, y ninguna sobra:

1. `setcap cap_setuid+ep newuidmap` (+ cap_setgid en newgidmap). shadow.toml los
   instala pero no los provisiona; sin la capability no escriben el mapa.
2. Crear el userns nosotros, mapear el rango de /etc/subuid con newuidmap y
   pasárselo a bwrap con `--userns FD`. bwrap crea el suyo con un solo id A
   PROPÓSITO y nunca llama a newuidmap: el trabajo es de quien lo invoca. El fd
   lo abre la shell (`exec 3<…`), porque un fd sólo cruza el exec si no es
   CLOEXEC y no valía la pena una dep de C para un fcntl.
3. Devolver las capabilities DENTRO del namespace. Ésta no estaba en el plan y
   es la que costó: bwrap las tira todas, y en Linux ser root es tener
   CAP_SETUID, no tener uid 0. Sin ella apt seguía sin poder seteuid(42) — un
   síntoma que parecía de subuid y no lo era. Son seguras por construcción:
   dentro de un userns sólo alcanzan lo que ese namespace posee, o sea nuestros
   propios subuid. CAP_SYS_ADMIN queda fuera y sigue colgando de `nesting`.

MEDIDO después: uid_map de 65537 ids, setgroups: allow, apt instala SIN el
APT::Sandbox::User=root, pacman sincroniza con DownloadUser=alpm INTACTO, y el
userns anidado monta con root=true ⇒ el conflicto 2 de D9 se disuelve solo.

Dos cosas más que salieron por medir, no por pensar:

- El guardián MENTÍA. qorpa-preflight envolvía al hijo en `timeout`, que forkea,
  así que newuidmap apuntaba al PID equivocado y el kernel respondía "Operation
  not permitted" — un falso negativo idéntico a un fallo real. Decía que subuid
  no andaba cuando a mano andaba. Ahora sale exit 0.
- Quitar el impuesto MUEVE el problema: el upper pasa a contener ficheros de los
  subuid (apt deja los suyos con uid 165577) que nuestro uid no puede borrar ⇒
  recreate entra a un userns mapeado para limpiar. Y cuando no hay rango, se
  degrada diciendo la causa exacta en vez de quedar en misterio.

Y un detalle que no es cosmético: `--perms 1777` antes del `--tmpfs /tmp`, o el
_apt al que apt baja no puede escribir su fichero temporal. Un /tmp que no es
1777 no es /tmp.

2 tests nuevos (el rango se lee por usuario; CAP_SYS_ADMIN NO está en las caps
de root, o `nesting` dejaría de ser una decisión). 45/45.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 05:27:03 +00:00
Sergio f19b8b3e4a jubilar 13 copias de cola que sellan IDÉNTICO al corpus — y de paso el grafo deja de mentir
Continúa lo que ya se hizo el 2026-09-02 con las 8 copias de gnome. Medido receta por receta con
`hammer hash`: de los 30 nombres que viven en el corpus Y en alguna cola, 13 dan el MISMO
ArtifactHash que su gemela del corpus y 22 son variantes deliberadas (glib/gtk4/pango de gnome,
dbus/openssl/libxml2 de kde…) que NO se tocan.

Se van las 13 redundantes: alsa-lib ×3, nasm ×2, zlib-shared ×2, y de incoming-kde ffmpeg,
fontconfig-shared, freetype-shared, libjpeg-turbo-shared, libpng-shared y vulkan-headers. Cada
consumidor las resuelve ahora en el catálogo PADRE, con el mismo hash.

VERIFICADO ANTES Y DESPUÉS, no argumentado: se snapshotearon los 47 consumidores directos, se
borraron las 13, y se re-hashearon. **0 consumidores vivos con hash distinto** (las 4 diferencias
son las propias recetas borradas, que se consumían entre sí). Cero rebuild.

EL EFECTO QUE NO SE BUSCABA Y ES EL MÁS VALIOSO: `build-state.json` guarda sus nodos POR NOMBRE, y
cuando un nombre vive en dos colas la membresía se consulta por el par `(cola, nombre)` de UNA sola
de ellas. Resultado: `ffmpeg`, `alsa-lib`, `vulkan-headers` y `zlib-shared` reportaban
`perfiles: []` —o sea "no está en ninguna imagen"— **mientras estaban en las cuatro**. Ahora dicen
la verdad, y `escritorio-kde` pasa de 183 a 185 porque aparecen dos nodos que el grafo no contaba.

La lección para el próximo análisis, que ya quedó en `scripts/vigia-sonames.py`: sobre el grafo se
pregunta con `yupana.membresia()`, que llavea por `(cola,nombre)`; el campo `perfiles` del JSON es
fiable sólo mientras el nombre viva en una sola cola.

Sin cambios en la deuda: cosmic sigue 105/106 por el OOM de xdg-desktop-portal-cosmic.
2026-09-03 05:13:21 +00:00
Sergio 5e6b3631e2 pipewire ×3: ALSA pasa a ser la ABI de audio de la distro — el flag no alcanzaba
Las tres recetas pasan a `-Dpipewire-alsa=enabled`. Sirve a TODA app ALSA del corpus, no sólo a mpv:
un cliente que abre "default" desemboca en PipeWire por `libasound_module_pcm_pipewire.so` en vez de
pelearle la tarjeta al servidor. Es protocolo en vez de ABI compartida — la misma figura con la que
el ADR 0015 cruza el borde de la jaula.

EL FLAG SOLO NO HACE NADA, y ésta es la parte que no se ve venir: meson deja los plugins en
`/usr/lib/alsa-lib` (bien, ahí los busca alsa-lib) pero la CONFIGURACIÓN en
`/usr/share/alsa/alsa.conf.d`, **un directorio que alsa-lib no lee**. Sus `@hooks` cargan
`/var/lib/alsa/conf.d`, `/usr/etc/alsa/conf.d` y `/etc/alsa/conf.d` — leído del `alsa.conf` del
artefacto sellado, no supuesto. Sin el enlace que agrega la fase install, el plugin queda instalado
y no lo usa nadie: sellado e inerte, que es la familia de fallo del artefacto vacío. Se enlazan los
DOS ficheros: `50-pipewire.conf` hace que el destino EXISTA, `99-pipewire-default.conf` hace que sea
el destino POR DEFECTO — sin el segundo, mpv abriendo "default" no llega igual.

EVIDENCIA (test discriminante, porque "no falla" no prueba nada acá):
  mpv --audio-device=alsa/pipewire  → abre el device, falla al conectar (no hay demonio) ⇒ el PCM
                                      ESTÁ DEFINIDO
  mpv --audio-device=alsa/noexiste  → "ALSA lib: Unknown PCM noexiste"  ⇒ el control
El camino de verdad —que suene— sólo se puede ejercer con un PipeWire vivo, o sea en la sesión; eso
no se probó y no se afirma.

⚠ PRECIO, escrito para que no se diagnostique mal: con `99-pipewire-default.conf` puesto,
`pcm.!default` ES PipeWire. En una máquina donde PipeWire no esté corriendo, un cliente ALSA ya no
cae a la tarjeta: no suena. Es el trato que hace toda distro con pipewire-alsa.

Radio medido antes de tocar (`yupana radio pipewire`): 4 cosmic / 3 gnome / 2 kde. Reconstruidos 10
de 11 dependientes.

⚠ DEUDA DECLARADA, 1: `incoming-cosmic/xdg-desktop-portal-cosmic` murió DOS VECES por OOM (7 G de
RAM, sin cgroups, y otro agente compilando Rust a la vez — `dmesg` confirma «Out of memory: Killed
process (cargo)»). No es un fallo de la receta ni del cambio: es la máquina. Queda visible en el
grafo (escritorio-cosmic 105/106) en vez de escondido, y lo levanta el worker o un reintento con la
máquina libre.

Los otros tres perfiles cierran: KDE 183/183, GNOME 132/132, sway 140/140.
2026-09-03 05:11:47 +00:00
Sergio 032aa02582 lua5.2 5.2.4: mpv gana el OSC — la barra, las estadísticas y la consola
El OSC de mpv es `player/lua/osc.lua`: sin Lua el reproductor anda y responde al teclado pero NO
DIBUJA NADA de interfaz. La `lua` del corpus no sirve — `meson.build:701` acepta 5.1, 5.2 o LuaJIT
y el `lua.pc` genérico va acotado a `>=5.1.0, <5.3.0`; la nuestra es 5.4.8 y entró para wireplumber.

CÓMO CONVIVEN LAS DOS SIN PISARSE, que es lo que hace legítimo tener dos Lua: esta receta instala
**sólo la librería**. Sin `/usr/bin/lua`, sin `luac` y sin `lua.pc` genérico — que son exactamente
los tres ficheros que colisionarían si una imagen hidratara las dos. Las `.so` no colisionan porque
el SONAME lleva la versión, y los headers van a `/usr/include/lua5.2/` porque los de 5.2 y 5.4 se
llaman igual y dicen cosas distintas.

La diferencia con 5.4 que muerde: en 5.2.4 `luaconf.h:43-49` hace que `LUA_USE_LINUX` implique
`LUA_USE_READLINE` (en 5.4 eso vive en el target `linux-readline` del Makefile). Se esquiva
construyendo el target `a` —sólo `liblua.a`— en vez de `all`: `LUA_USE_READLINE` lo consume nada
más que `lua.c`, el binario del REPL, que acá no se compila. Sale gratis y de paso es lo que hace
que no haya `/usr/bin/lua` que colisione.

`-Dlua=lua5.2` y no `auto`: auto recorre ocho nombres de `.pc` y se lleva el primero que encuentre
en el sandbox ⇒ el artefacto dependería de qué otra cosa quedó montada en el lab.

EVIDENCIA, del mpv re-sellado (b3:d9072a10):
  List of enabled features: … libass libplacebo **lua5.2** … wayland
  [cplayer] Set property: user-data/osc/visibility="auto" -> 1
  [cplayer] Done loading scripts.
o sea que el script del OSC no sólo compiló: se carga y corre.

alsa-lib: se corrige el comentario, que decía que las pipewire iban con `-Dpipewire-alsa=disabled`.
Ya no. Y queda escrito el gotcha que costó encontrarlo: el `alsa.conf` que instala carga
`/var/lib/alsa/conf.d`, `/usr/etc/alsa/conf.d` y `/etc/alsa/conf.d`, NO `/usr/share/alsa/alsa.conf.d`,
que es donde meson deja la config del plugin.
2026-09-03 05:07:12 +00:00
SergioandClaude Opus 5 5b82474079 qorpa D9: harkaq enjaula la instancia, y midiendo salieron dos conflictos
Paso 4 del ADR 0015. harkaq-exec entra como último eslabón dentro de bwrap,
igual que en el sandbox del build. Cruza el borde un binario ESTÁTICO, no una
librería, así que D2 sigue en pie: lo único compartido es la ABI del kernel.

Honestidad primero, y está escrita en el código: en el eje del sistema de
ficheros harkaq casi no agrega nada, porque el namespace de montaje de bwrap ya
es una lista blanca. Escribir reglas `ro` que repiten eso sería un sello de
goma, así que la política de una instancia no sellada es UNA línea (`rw /`) y no
finge. Lo que sí aporta: seccomp (bwrap no instala filtro alguno — hoy una
instancia podía io_uring, bpf, ptrace, userfaultfd, keyctl, perf_event_open),
no_new_privs, el canal de evidencia, y `seal_image`, que congela /usr /bin /lib
/opt aunque adentro seas root.

Verificado contra el kernel, no contra el log: Landlock ABI 9, logging
post-exec ON, NoNewPrivs 1, Seccomp 2. Y sellando, `/usr/bin` y `/bin` denegados
mientras /etc y /var siguen escribibles.

DOS CONFLICTOS que sólo se ven midiendo, y ninguno estaba en el ADR:

1. Landlock y los contenedores anidados son INCOMPATIBLES hoy: con un dominio
   activo, `mount` falla con EACCES aunque seccomp lo permita — el kernel no
   admite montajes nuevos bajo un dominio porque escaparían de sus reglas
   por-ruta. ⇒ pressure-vessel no arranca bajo Landlock. Por eso `nesting` pasa
   `--allow-nesting --no-landlock` y lo dice a gritos; seccomp y no_new_privs
   siguen puestos, que es lo que más pesa con un binario ajeno.
2. `root` adentro y anidar se pelean: con --uid 0, un userns anidado no puede
   escribir su uid_map. Sin remapear anida, pero el gestor de paquetes se queja.
   La instancia de juegos y la de paquetes quieren mapeos OPUESTOS, y ahora el
   manifiesto lo declara (`root`, encendido por defecto).

Las dos mitades se curan con lo mismo que el impuesto de apt: un rango real de
subuid con newuidmap + --userns FD. Ése es el ticket que más desbloquea.

En harkaq-exec, dos flags ADITIVOS y apagados por defecto (--allow-nesting,
--no-landlock): el camino del build no cambia ni un byte, que es requisito duro
con 700+ artefactos sellados. La lista de syscalls del anidamiento se separó de
la base y el _Static_assert del techo de salto BPF ahora suma las dos.

3 tests nuevos: que la política sin sellar no finja, que sellando el `rw /` no
sobreviva (uniría derechos por ancestro y anularía el sellado), y que `root` sea
lo único que nace encendido. 43/43.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 05:02:56 +00:00
SergioandClaude Opus 5 ba09fdb403 qorpa create/recreate/run: la instancia nace sin ver nada
Paso 3 del ADR 0015. El overlay lo monta bwrap dentro de su propio namespace
(--overlay-src + --overlay), así que no hace falta root ni se monta nada en el
host. `run` entra con --clearenv y con --unshare-net salvo que se declare
`network`: el entorno del host TAMBIÉN es una concesión, y lo que no se declara
no entra (D2/D7). `--dry-run` imprime el bwrap entero, una línea por concesión,
porque una jaula que no se puede leer no se puede auditar. `list` ahora enumera
también las instancias con lo que abre cada una — una instancia sin política y
una con la pantalla abierta se ven IGUAL desde fuera y no son lo mismo.

Los campos del manifiesto van en inglés (regla 4); el ADR los tenía en
castellano y quedan corregidos, igual que las rutas images/ e instances/.

D3 VALIDADO en la mano, no en el papel: la escritura va al upper, la imagen base
no se toca, y `recreate` tira la capa y la instancia sigue siendo la misma.

Y se midió la otra mitad del paso 1, que el ADR daba por «lo primero que va a
fallar». Falla, sí, pero el veredicto es MEJOR de lo que decía:

  uid_map: 0 1001 1 · setgroups: deny

- apt: el método http hace setgroups para bajar a _apt ⇒ para. Salteándolo con
  -o APT::Sandbox::User=root baja 34 MB, instala y corre los triggers de dpkg
  enteros; el único residuo es un AVISO de chown a root:adm.
- pacman: chownea el directorio de descarga a `alpm` ⇒ para en duro. Con
  DownloadUser comentado sincroniza, y tras pacman-key --init/--populate
  instala y el binario corre.

⇒ subuid no es un muro, es un IMPUESTO: un solo id alcanza para instalar
paquetes reales en los dos gestores, y lo que rompe es el chown/setgroups a
OTRO id, que cada gestor hace en un sitio distinto. Y quitarlo pide algo que el
ADR no decía: bwrap crea el userns con un solo id A PROPÓSITO y no llama a
newuidmap, así que además del setcap hay que crear el namespace aparte, mapear
el rango y pasárselo con --userns FD.

5 tests nuevos (nace sin concesiones, rechaza imagen vacía, el upper es caché,
sin grants la red queda fuera, y que el aviso de wayland no sea tibio). 40/40.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 04:38:25 +00:00
SergioandClaude Opus 5 fe155cf20d qorpa pull/list: la imagen ajena entra verificada, o no entra
Paso 2 del §Orden de trabajo del ADR 0015. Verbos en inglés (regla 4); el ADR
decía traer/crear/correr y queda corregido, con una línea que dice por qué para
que no se vuelva a proponer.

`hammer qorpa pull <url> --sha256 <sha>` baja, VERIFICA y recién entonces
desempaca — nunca al revés: un tar ajeno sin verificar es código ajeno que ya
escribió en tu disco. Veredicto de ADR 0014: contenido distinto ⇒ ABORTAR, y no
queda nada a medias. La identidad es el sha256 del ARCHIVO, no del árbol, así
que la URL es informativa y espejar sale gratis (ADR 0013). `list` marca a
gritos las imágenes vacías y sale ≠0 (regla 3). Los pasos 3-7 están declarados
en la superficie y fallan diciendo a qué paso del ADR pertenecen.

Nada de esto toca el store: es el espacio paralelo /var/lib/hammer/qorpa (D1).

PROBADO de punta a punta contra las dos imágenes curadas — Ubuntu base 24.04.3
(2760 ficheros, 78 M) y Arch bootstrap 2026.09.01 (31748, 534 M), las dos con
su glibc adentro, que es el montón B entero. Y probarlo de verdad destapó tres
cosas que en verde no se ven:

1. `-p` sin `--delay-directory-restore` NO extrae un rootfs real sin ser root:
   /etc/ca-certificates/extracted/cadir es 0555 y tar lo crea con su modo final
   ANTES de llenarlo.
2. Mi limpieza mentía: `remove_dir_all().ok()` no puede con un árbol que trae
   directorios de sólo-lectura, así que el staging de un pull roto SOBREVIVÍA y
   el siguiente pull extraía encima. El síntoma («Permission denied» en un
   directorio recién creado) no se parece en nada a la causa.
3. Renombrar un DIRECTORIO exige escritura sobre el directorio mismo, y el
   root.x86_64 de Arch viene dr-xr-xr-x. Se abre, se mueve y se le devuelve su
   modo exacto.

Y una regla que sonaba razonable y era falsa: «si hay un solo directorio arriba,
ése es el rootfs». El bootstrap de Arch trae TRES entradas arriba (root.x86_64,
version, pkglist) ⇒ no disparaba y el rootfs quedaba un nivel abajo, con todo
verde y sin un error. Ahora se ancla por ESTRUCTURA (tiene etc/ y usr|bin), con
--subdir como escape, y si no acierta FALLA en vez de adivinar: un rootfs mal
anclado no rompe acá, rompe cuando la instancia no encuentra su loader. Los
hermanos descartados quedan escritos en el manifiesto, no tirados en silencio.

5 tests nuevos, incluida la cicatriz de Arch. 35/35 en hammer-cli.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 04:02:46 +00:00
Sergio 9a2739f856 estado: cosecha granja 2026-09-03T04:02:45Z — avance del árbol KDE 2026-09-03 04:02:45 +00:00
Sergio 524d18aba1 plan de apps: el triaje preguntaba «¿existe la receta?» y la pregunta es «¿la alcanza quién la usa?»
Se descubrió usándolo. `wf-recorder` daba 0 faltantes / 0 muros, y al ir a escribirlo resultó que
sus dos backends de audio —pipewire y pulse— existen en las TRES colas de escritorio y en NINGUNA
en el corpus. Una receta del corpus no ve una cola hermana, así que wf-recorder construye pero
graba MUDO, y no tiene backend ALSA con el que caerse.

Lo que eso destapa es más grande que wf-recorder y por eso queda escrito como decisión abierta:
**el corpus no tiene ningún cliente de audio salvo ALSA**. Ya chocó dos veces en un día — mpv
terminó con `--ao=alsa` en vez de su salida nativa, y ahora esto. Las tres salidas posibles
(promover UNA pipewire al corpus / aceptar ALSA como la ABI única / que las apps multimedia vivan
en las colas) quedan planteadas con su precio; la primera choca con la enfermedad de las dos glib
y no se decide de madrugada.

wf-recorder NO se escribe hasta entonces: un grabador de pantalla mudo es la clase de media-cosa
que conviene no sellar sin que alguien la haya elegido.
2026-09-03 03:47:17 +00:00
Sergio 04e2c89014 vigia-sonames: el flag va en inglés (--fail), regla 4 del CLAUDE.md
Código nuevo nace con la superficie de CLI en inglés. Nació con `--fallar` unas horas antes de
que la regla quedara escrita; se corrige ahora que no lo llama nadie todavía.
2026-09-03 03:45:50 +00:00
Sergio 223dc47c64 plan de apps: el orden mpv → OBS → Firefox del ADR 0015 no sobrevive a la medición
Al ir por la segunda app del montón A salió que la premisa del ADR —«no lo bloquea nada
estructural, falta escribirlas»— vale para mpv y no para el resto. Medido contra el catálogo real
(1079 recetas) cruzando los makedepends del APKBUILD de Alpine de cada candidata:

  · firefox   → pide gtk+3.0-dev. Firefox NO tiene backend GTK4. Más X11 y un toolchain wasi que
                no existe acá. La memoria decía «le falta subir el techo MSRV»: eso es cierto y es
                LO MENOR. ⇒ NO es montón A.
  · obs       → pide qt6-qtbase/qtsvg, y Qt6 vive SÓLO en incoming-kde. Una receta del corpus no
                alcanza una cola hermana ⇒ OBS hoy sólo puede ser una app DE KDE, no de las cuatro
                imágenes. Más X11 y 20 recetas.
  · chromium, libreoffice, gimp → peores, y los tres con GTK3 encima.

La consecuencia estratégica, que es lo que hay que decidir despierto: TODOS los navegadores Linux
son GTK3 (Firefox y derivados) o Chromium (que arrastra GTK3 y Qt6). «Tener navegador» no es un
ticket de recetas: es elegir entre autorar GTK3 o darle el navegador a qorpa (ADR 0015). La segunda
es coherente con lo ya decidido, y el navegador es justamente el proceso al que menos ganas dan de
darle el sistema entero.

Lo que SÍ está a mano, y es el hallazgo útil: `wf-recorder` sale con CERO recetas faltantes y cero
muros —su cierre quedó completo cuando entró mpv— y cubre el caso de uso principal por el que uno
instala OBS. Después `imv` (7 faltantes, todos cargadores de formato opcionales).

El método incluye su propio control: mpv, que YA ESTÁ SELLADA, aparece con 16 faltantes, que son
exactamente las perillas que su receta apaga a propósito. La columna que decide no es «cuántas
faltan» sino «cuántos MUROS», porque un muro no se paga escribiendo una receta sino cambiando una
decisión.
2026-09-03 03:45:25 +00:00
SergioandClaude Opus 5 370c4a7443 regla 4: la superficie de la CLI va en inglés, los mensajes en castellano
La regla existía —kernel_cmd.rs la cita como «Regla 7.bis»— pero no estaba
escrita en ningún sitio que un agente lea, así que nadie la respetó: el ADR
0015 nació proponiendo traer/crear/correr. Queda en CLAUDE.md, que es lo que
se carga en cada sesión.

Con la deuda declarada en vez de tapada: varios scripts/ exponen flags en
castellano y el barrido es su propia unidad de trabajo, porque tocarlos de
paso rompe cron y la granja. Código nuevo nace en inglés desde hoy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 03:44:33 +00:00
Sergio f9da495b8c vigía de sonames: KDE, GNOME y COSMIC tenían la MISMA fuga al lab que sway ya había documentado
Verificando que mpv anduviera en las cuatro imágenes salió esto: `libEGL.so.1` (mesa) pide
`libexpat.so.1` y `libwayland-client.so.0` pide `libffi.so.8`, las recetas canónicas de expat y
libffi son `--disable-shared`, y ningún artefacto del cierre publica esos SONAME ⇒ los tres rootfs
los resolvían contra el **sysroot Alpine DEL LAB**.

Es peor que una dep faltante: el lab NO entra en `hash_inputs`, así que el store no puede notarlo —
el artefacto se sella, el perfil reporta 100%, y la imagen sólo arranca en una máquina con Alpine
debajo. El perfil de sway ya tenía escrito exactamente este párrafo desde 2026-08-26; lo que faltaba
era el instrumento para ver que las otras tres estaban igual.

Arreglo (gratis, sin rebuild: las dos recetas ya estaban selladas por sway): `expat-shared` y
`libffi-shared` pasan a raíces de los tres perfiles. Van de raíces y no de `[deps]` por la misma
razón que las fuentes y el XKB — son data de RUNTIME y ninguna arista de build las alcanza.

`scripts/vigia-sonames.py` es el guardián que sale del punto ciego: recorre los NEEDED de todo el
cierre de cada imagen contra los SONAME que ese mismo cierre publica. Dos decisiones de diseño:
  · keying por PAR `(cola, nombre)` vía yupana, NO por nombre — `build-state.json` colapsa los
    nombres que viven en dos colas y su campo `perfiles` puede quedar vacío para una receta que sí
    está en la imagen.
  · imprime SIEMPRE quién pide cada soname, porque el cierre incluye herramientas de build
    (python3, cmake, perl, go) que la hidratación no instala: sin esa columna el informe no se tría.

Después del arreglo, `libexpat.so.1` y `libffi.so.8` desaparecen de los tres. Lo que queda son
hallazgos REALES que no son de este commit y quedan anotados:
  · kde:    karchive pide libbz2.so.1 y liblzma.so.5
  · gnome:  spidermonkey pide libstdc++.so.6 y libgcc_s.so.1; libadwaita pide liblzma.so.5;
            freetype-shared pide libbz2.so.1; sqlite-shared pide libreadline.so.8
  · cosmic: llvm18 pide libgcc_s.so.1
  · los tres: `pw-top` de pipewire enlaza `libncursesw.so.6` DEL LAB — y la receta afirma en un
    comentario que sin la dep «meson saltea pw-top». Es falso desde al menos el 2026-08-29:
    `dependency('ncursesw')` lo encuentra igual en el sysroot del lab. El comentario dice una cosa
    y el binario otra.
2026-09-03 03:42:31 +00:00
SergioandClaude Opus 5 d8778e4dd9 qorpa D8: el pin fija el suelo, no la instancia; y la terna curada
Faltaba decir en el ADR lo que se confunde solo: "pinear" no significa que la
imagen no se actualice. El digest es la IDENTIDAD, igual que en el rootfs del
lab. Lo que se pinea es el suelo; lo que instalás adentro con dnf/pacman ni
está pineado ni puede estarlo, y se actualiza normal.

Subir la base de versión es barato PRECISAMENTE por D3: manifiesto = verdad,
upper = caché ⇒ cambiar el digest y recrear. Y el riesgo del pin (que upstream
borre el tarball) ya lo resolvió ADR 0013: la URL no entra en la identidad,
sólo el sha256, así que espejar es gratis.

Curaduría decidida con el usuario — TRES, cada una por un trabajo distinto:
- Arch bootstrap (juegos: multilib 32-bit y SteamOS es Arch ⇒ extiende D6),
- Ubuntu base LTS (binarios comerciales: es contra lo que se compilan),
- Steam Runtime sniper (la única SELLABLE: inmutable ⇒ file_drop al store).
Fedora queda BYO: hace el mismo trabajo que Arch en el slot "fresco" y una
tercera cadena mutable es la normalización que §NO-resuelve 3 quiere evitar.

Los pines de las dos primeras están VERIFICADOS contra upstream hoy (Arch
2026.09.01 sha 895661bd…, Ubuntu 24.04.3 sha 6bc2cde3…); el de sniper NO, y se
dice que no en vez de suponerlo.

Y queda anotado que hacerlas compartibles más adelante no pide diseño nuevo:
una imagen pineada es cuerpo inmutable direccionado por contenido ⇒ ADR 0014
se le aplica tal cual. Lo único que hay que mirar antes de publicarlas a
terceros es licencia y marca, que no es una pregunta técnica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 03:39:36 +00:00
Sergio 9a7e7bc17f estado: cosecha granja 2026-09-03T03:32:28Z — avance del árbol KDE 2026-09-03 03:32:28 +00:00
Sergio 01b86fd0fb targets: mpv entra como raíz de los cuatro escritorios — sellado no es instalado
Sin esto mpv quedaba sellado y en NINGUNA imagen. Es exactamente la lección que este fichero ya
tiene escrita en el perfil de sway: `foot` estaba sellado en el corpus, ningún perfil lo listaba,
y el escritorio daba 121/121 SIN EMULADOR DE TERMINAL. La métrica de clausura mide las raíces
declaradas y no puede ver lo que falta en la declaración.

Se lista en los cuatro porque mpv vive en el corpus: una receta resuelve sibling-first y después
el catálogo padre, así que desde cualquiera de las cuatro colas se alcanza.

Los cuatro perfiles siguen en 0 deuda y cierran con la clausura nueva ya sellada:
  escritorio-kde     171 → 180/180
  escritorio-gnome   119 → 129/129
  escritorio-cosmic   90 → 103/103
  escritorio-sway    129 → 139/139
Corpus 788 → 798 recetas, 798 selladas. grafo: CIERRA | topo-sort: OK en los cinco.
2026-09-03 03:21:38 +00:00
Sergio 16e2be84d4 mpv 0.41.0: la primera app gráfica de usuario final del corpus
Cuatro escritorios que cierran y ninguna forma de abrir un video. El ADR 0015 ordenó los montones
y puso a mpv primero del A —lo que no tiene muro estructural, sólo falta escribirlo—. Éste es.

Sellada b3:8e6ab4e7. Evidencia de que ANDA, no sólo de que sella:
  · `mpv --version` corre con el loader musl → v0.41.0, libplacebo v7.360.1, FFmpeg 7.1
  · decodifica un h264 640x480 + AAC de prueba, 75 frames, `Exiting... (End of file)`
  · `--vo=help` → `gpu-next` (libplacebo) y `gpu`; `--ao=help` → `alsa`
  · `--gpu-context=help` → SÓLO `wayland` (Wayland/EGL). Ni un contexto X11 en el binario.
  · NEEDED: libav*, libplacebo, libasound, libEGL, libc. Cero glibc, cero X11.

En el corpus y no en una cola: un reproductor lo quieren las cuatro imágenes, y una receta resuelve
sibling-first + catálogo padre, nunca una cola hermana.

`-Dbuild-date=false` no es cosmético: su default estampa la fecha de compilación DENTRO del binario
⇒ dos builds del mismo commit darían bytes distintos y el artefacto dejaría de reproducir.

`-Dprefer_static=true` (como foot) porque sin él meson resuelve los `.pc` sin `Libs.private` y el
link muere con símbolos `XML_*` sin definir «en libfontconfig.a» — culpando a fontconfig cuando lo
que falta es propagar expat.

Las ~90 opciones van EXPLÍCITAS: casi todas nacen `auto`, o sea que miran el sandbox y se prenden
con lo que encuentren. Misma trampa que ffmpeg cierra con `--disable-autodetect`.

TRES HUECOS CONOCIDOS, escritos en la receta para que no se descubran en la mano del usuario:
  1. audio ALSA, y las tres pipewire llevan `-Dpipewire-alsa=disabled` ⇒ hoy no suena en un
     escritorio con PipeWire vivo. Radio de darlo vuelta: 4/3/2. Es el próximo paso.
  2. sin Lua ⇒ sin OSC. mpv 0.41 sólo acepta 5.1/5.2/LuaJIT y la del corpus es 5.4.8 (wireplumber).
  3. sin vaapi ⇒ sin decodificación por hardware (libva está sólo en incoming-kde), y el ffmpeg
     heredado va `--disable-x86asm` ⇒ tampoco SIMD.
2026-09-03 03:20:24 +00:00
Sergio 2a84b8252a libplacebo 7.360.1: la librería de render de mpv, con los submódulos resueltos como recetas
`meson.build:29` de mpv la pide sin `required:` ⇒ es la segunda dep dura, junto con libass.

EL PROBLEMA REAL NO ERA COMPILARLA, era que upstream le entrega CINCO 3rdparty por submódulo git y
`[source]` de hammer no clona submódulos (sólo repo+commit o tarball+sha256). Resolución uno por uno:
  · glad + jinja + markupsafe → recetas del corpus (commit anterior), como el `py3-glad` de Alpine
  · Vulkan-Headers → sube al corpus, headers-only, hash idéntico (b3:7731a012)
  · fast_float → upstream lo guarda con `fs.is_dir` ⇒ su ausencia es camino soportado

La sorpresa fue Vulkan-Headers: hace falta AUNQUE Vulkan vaya apagado. `src/vulkan/stubs.c` se
compila siempre y hace `#include <vulkan/vulkan.h>` — los stubs de «Vulkan no disponible» también
necesitan saber contra qué API no están.

Dinámica y no estática: `src/convert.cc` compila siempre ⇒ el artefacto lleva C++ y arrastra su
runtime. La `.so` se lo lleva puesto en vez de obligar a cada consumidor a pedir `-lc++`. Además es
lo coherente con mesa y ffmpeg, sus dos vecinos en el cierre de mpv.

Todas las perillas explícitas aunque el default sea `auto`: `auto` escanea el sandbox y activa lo
que encuentre — el mismo no-determinismo que ffmpeg cierra con `--disable-autodetect`.

Sellada b3:4346feca, con `pl_has_opengl=1` / `pl_has_vulkan=0` en el .pc y 8 símbolos `pl_opengl`
exportados. Vulkan queda pendiente a propósito: el loader arrastra el stack X11 entero por los WSI
y esta distro es Wayland-only ⇒ subirlo es una decisión con radio propio.
2026-09-03 03:14:22 +00:00
Sergio bb3fdc2ca0 libass 0.17.5 + libunibreak 6.1: el renderizador de subtítulos que mpv exige sin opción
`meson.build:32` de mpv pide libass sin `required:` ⇒ no hay build de mpv sin esto.

libunibreak es "opcional" para upstream (`default=check`) y obligatorio acá: sin él libass parte
los renglones por espacios, que es justo lo que no sirve en japonés, chino o tailandés. Cuesta una
receta leaf de C plano.

`--enable-asm`: 234 símbolos SSE2/AVX2 en la `.a` sellada. Es la razón por la que `nasm` subió al
corpus en el commit anterior.

La lección de las deps, para la próxima receta que resuelva `.pc`: más de la mitad de `[deps]` no
aparece en el `./configure`. La harfbuzz del corpus va `-Dglib=enabled` (por GTK4) ⇒ su `.pc` pide
`glib-2.0`, que arrastra pcre2 y libffi. Y el fallo NO dice que falte glib: dice «Package
requirements (harfbuzz >= 1.2.3) were not met», culpando a la lib que sí estaba.

Selladas: libunibreak b3:a01fb6bd, libass b3:213df6b1.
2026-09-03 03:12:12 +00:00
Sergio ecc6166dfe jinja2 3.1.6 + glad 2.0.8: el generador del loader GL que libplacebo trae como submódulo
libplacebo genera su `gl.h` con `python -m glad`, y glad le llega a upstream como submódulo git
(`3rdparty/glad`). El `[source]` de hammer no clona submódulos —sólo repo+commit o
tarball+sha256— así que glad entra como receta propia desde PyPI, que además pinea por sha256.
Es lo mismo que hace Alpine con `py3-glad` como makedepend.

Python puro las dos, mismo molde que `mako`/`markupsafe`: se copia el paquete a site-packages,
sin pip ni wheel. jinja2 es dep de runtime de glad; markupsafe ya estaba en el corpus.

El sdist de glad trae los XML de Khronos adentro ⇒ el generador no sale a la red, que es lo que
lo hace usable dentro del sandbox hermético.

Selladas: jinja2 b3:74a3aa9f (27 ficheros), glad b3:c1d773f2 (75 ficheros).
2026-09-03 03:10:00 +00:00
Sergio 681bb642c2 corpus: subir nasm, alsa-lib y ffmpeg desde las colas — los tres con hash idéntico
Primer paso del frente de apps (ADR 0015, montón A: mpv → OBS → Firefox). `mpv` va al corpus
porque lo quieren las cuatro imágenes, y una receta del corpus resuelve sibling-first en
`recipes/`: desde ahí NO alcanza `incoming-kde/` ni `incoming-cosmic/`. Sus tres deps de sistema
vivían sólo en colas.

Lo que hace que esto no sea duplicar: `hammer hash` da el MISMO ArtifactHash a cada par
(nasm b3:3624bdd8, alsa-lib b3:73fb200a, ffmpeg b3:bfef7bf3), porque las deps que las colas les
resolvían ya caían al catálogo padre. Cache hit, cero rebuild, y una imagen que arrastre las dos
recetas hidrata UN artefacto en vez de dos peleando por la misma ruta.

Verificado también que comentar la receta es gratis: las cabeceras nuevas no mueven el hash.
2026-09-03 03:09:01 +00:00
SergioandClaude Opus 5 aa5bb67fd7 qorpa: nombre adoptado y el preflight que mide si una máquina puede alojar
ADR 0015 pasa de "propuesta de nombre" a `qorpa` adoptado: la frontera es
`hammer qorpa {…}` y el espacio de nombres se unifica en
/var/lib/hammer/qorpa/{imagenes,instancias}/ — un solo árbol, para que la poda
de §NO-resuelve 5 tenga un único sitio que barrer. La clase de nodo del grafo
sigue siendo `ajeno`: describe la procedencia, no el subsistema.

Y arranca el §Orden de trabajo 1 (subuid) como GUARDIÁN en vez de a mano:
scripts/qorpa/qorpa-preflight.sh mide las cinco capacidades de entorno que un
huésped necesita y que no están en ningún grafo — userns sin privilegios (+
anidado), mapeo multi-id, overlayfs sin root, los nodos del borde y disco.
Tres niveles (BLOQUEA/LIMITA/NOTA) y salida 0/1/2, porque "arranca pero sin
dnf" es una respuesta legítima, no un error.

Medido en `momento` (exit 2, 2 limitaciones):
- userns ANIDADO funciona ⇒ el "verificar, no asumir" de D6 (pressure-vessel
  creando su userns dentro del nuestro) queda verificado a nivel de primitiva.
- subuid es papel mojado acá: el rango está declarado en /etc/subuid y las
  herramientas están, pero newuidmap/newgidmap vienen sin setuid y sin
  capability ⇒ no pueden escribir el uid_map. Es exactamente la "primera cosa
  que va a fallar" del ADR, y resulta ser de PROVISIÓN, no de kernel.

El guardián ya se corrigió a sí mismo una vez: marcaba LIMITA por
CONFIG_OVERLAY_FS=m mientras tres secciones más abajo el overlay montaba de
verdad. Manda la prueba funcional, no la declarada.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 03:05:12 +00:00
Sergio 2fc30c4df2 estado: cosecha granja 2026-09-03T03:01:13Z — avance del árbol KDE 2026-09-03 03:01:13 +00:00
Sergio 776692d01a ADR 0015 (propuesto): imágenes ajenas — el mundo glibc entra enjaulado y no entra al store
Decide la frontera antes de escribir código. Sale de una medición incómoda: el
corpus tiene cuatro escritorios que cierran y CERO navegador, ofimática,
reproductor o editor de imagen. Al partir el «qué falta» por causa, la
intersección de «sólo X11» con «compilable desde fuente en musl» es casi vacía:
casi todo lo que se pierde por Wayland ya estaba perdido por la libc. El montón
que duele son binarios ajenos que nadie va a recompilar.

Las siete decisiones:

D1 — Una imagen ajena NO es un artefacto y no vive en el store. El store promete
     reconstrucción bit a bit desde fuente; un rootfs de Fedora no. Meterlo ahí
     sería la misma clase de error que el artefacto vacío: algo que se lee como
     garantía y no lo es. Namespace paralelo, por digest, fuera de hash_inputs.
D2 — Se cruza el borde con protocolos y nodos de dispositivo, NUNCA con
     librerías. Wayland/PipeWire son protocolos; /dev/dri y /dev/ntsync son ABI
     de kernel. Mesa va adentro de la imagen. Corolario: la jaula no sabe qué
     libc hay adentro, y por eso resuelve el montón entero de una vez.
D3 — El manifiesto es la verdad; el `upper` del overlay es CACHÉ. Misma relación
     que receta↔artefacto. De ahí se caen solas la actualización de base (se
     recrea, no se rebasea), el respaldo (KB, no GB) y la poda.
D4 — Cuatro granularidades, no una. El runtime curado inmutable (tipo 2) sigue
     siendo el preferido cuando alcanza: se sella. El rootfs con dnf existe
     porque es justo lo que el tipo 2 no permite.
D5 — Transparencia por shims GENERADOS, no por un FUSE global. Es el poder de
     Bedrock sin sus formas: cero costo en runtime, inspeccionable, revocable, y
     se exporta lo declarado (Bedrock arbitra en tiempo de exec, con heurísticas).
     Los nodos exportados entran al grafo con clase `ajeno` ⇒ no se pueden contar
     como corpus. Bedrock no puede decirte qué tenés.
D6 — Steam ya ES un contenedor: se anida pressure-vessel adentro, que es la
     configuración que Valve prueba. El bwrap anidado hay que VERIFICARLO.
D7 — Es el único lugar del sistema donde la política se escribe en vez de
     derivarse. harkaq deriva `política = clausura(deps)`; una imagen ajena no
     tiene clausura declarada. Excepción nombrada y acotada, por defecto vacía.

Y lo que el ADR admite que NO resuelve, escrito para no descubrirlo en producción:
el socket de Wayland es un borde de privilegio y lo pasamos crudo (screencopy y
virtual-keyboard incluidos — Flatpak pasa un proxy filtrante, nosotros no lo
tenemos); el UID mapping va a fallar primero y las piezas ya están en el corpus
(shadow instala newuidmap/newgidmap y crea /etc/subuid vacío, falta
provisionarlo); es una segunda cadena de suministro sin garantías; y hay que
acotar por escrito el claim de bit-repro o la cultura de números honestos se
erosiona sola.

Se cae gratis: `xwayland` deja de ser deuda del corpus (va DENTRO de la imagen,
que ya lo trae, y se cuelga de kwin por el socket) ⇒ el wanted de KDE se
disolvería sin escribir la receta y sin tocar Wayland-only. GIMP e Inkscape dejan
de reabrir la deuda GTK3. Y del plan de juegos: F2 (glibc+multilib desde fuente,
«una campaña entera») queda CANCELADA y F0 (flatpak+ostree al catálogo)
innecesaria.

Lo nativo no se afloja: el montón A se sigue construyendo, en orden mpv → OBS →
Firefox.

Toca sólo documentación: el ADR nuevo, la nota de generalización en
plan-jaula-juegos.md §Capa 3, y el comentario en targets.toml que evita que
alguien escriba la receta de xwayland sin ver la decisión pendiente. Cero recetas
tocadas, cero re-hasheo: --kde sigue en 978 sealed / 1 wanted / 171-171, CIERRA.
2026-09-03 02:49:40 +00:00
Sergio 3b275f482e estado: cosecha granja 2026-09-03T02:31:12Z — avance del árbol KDE 2026-09-03 02:31:12 +00:00
Sergio f4268acfdc estado: cosecha granja 2026-09-03T01:31:07Z — avance del árbol KDE 2026-09-03 01:31:07 +00:00
Sergio 9fb0229f19 estado: cosecha granja 2026-09-03T01:01:13Z — avance del árbol KDE 2026-09-03 01:01:13 +00:00
Sergio 520f1d5279 polkit (variante KDE) + polkit-qt-1 0.201.1: Plasma ya puede pedir autorización
Tercer y cuarto hueco de RUNTIME del triaje. Sin polkit-qt-1, Plasma no puede
elevar privilegios: ni montar un disco, ni cambiar ajustes del sistema.

Son DOS recetas porque hacía falta una segunda polkit. Hammer resuelve las deps
hermano→padre —la cola propia, después recipes/— y nunca cruza a una cola hermana,
así que la polkit de incoming-gnome es invisible desde incoming-kde. Y aunque se
viera no serviría: aquélla va con -Dintrospection=true contra glib-introspected,
que es la ISLA DINÁMICA del shell de GNOME (gjs importa gi://Polkit y necesita el
typelib). KDE enlaza las libs desde C++ y el typelib le sobra; traerlo obligaría a
meter gobject-introspection entero en la cola.

La nueva es homónima a propósito, mismo patrón deliberado que dbus (incoming-kde vs
raíz) y gmp (incoming-kde vs incoming-cosmic), con el mismo coste conocido:
store-gc no puede decidir por NOMBRE cuál de los dos sellados es el vigente. No la
renombré a polkit-kde porque el .pc que busca polkit-qt-1 es polkit-gobject-1, no
el nombre del paquete: renombrar sólo movería el problema al lector.

Radio medido antes de escribirla (yupana radio polkit): la de incoming-gnome tiene
6 dependientes transitivos y no se toca; la nueva, 0. Cero daño colateral.

Nada de esto necesitó descubrirse: glib-shared ya existía en la cola KDE y su
propio encabezado nombraba a polkit como uno de los motivos por los que se escribió.

── El guardián que sí hizo falta ────────────────────────────────────────────────
polkit-qt-1 sondea polkit con check_function_exists, que COMPILA Y ENLAZA. Si ese
enlace falla por algo del sandbox —y no porque la función falte—, cmake NO da
error: imprime «You have an older polkit-1 version» y define
POLKIT_QT_1_COMPATIBILITY_MODE. El build sella, el artefacto pesa lo esperado y la
autorización queda recortada sin que nadie se entere. La fase configure ahora
aborta si HAVE_POLKIT_SYSTEM_BUS_NAME_GET_USER_SYNC no llegó a 1 en el header
generado: polkit 127 tiene esa función, así que un 0 significa «el sondeo no pudo
enlazar», no «polkit es viejo». Pasó: no hay modo compatibilidad.

También va -DQT_MAJOR_VERSION=6 explícito. El default del CMakeLists es "5", y sin
la flag construiría bindings con otros nombres (polkit-qt5-1) que el dependiente
no encontraría hasta mucho después.

── Lo que esto NO enciende, a propósito ─────────────────────────────────────────
Los dos consumidores siguen esquivando polkit igual que antes:
  · kauth construye con el backend polkit OPCIONAL apagado (cae al backend fake);
  · plasma-workspace apaga el helper de Región&Idioma con -DGLIBC_LOCALE_GEN=OFF
    -DGLIBC_LOCALE_PREGENERATED=ON, que es justo lo que evita el PolkitQt6-1
    REQUIRED.
Encenderlos es otra decisión con su propio radio (plasma-workspace arrastra medio
escritorio). La pieza está puesta; la palanca no se toca sin pedirlo.

⚠ Y con las librerías viaja la postura de seguridad que ya declaraba
arje-polkit-compat: el demonio responde is_authorized=true a TODO. Estas libs son
la INTERFAZ que el escritorio espera, no la política. Queda repetido en las dos
recetas nuevas porque son las que ponen la pieza en manos del escritorio.

Las dos REPRODUCEN bit a bit (verificar-repro.sh 2/2). Los NEEDED de
libpolkit-qt6-core-1 son exactamente los esperados: Qt6DBus, Qt6Core,
libpolkit-gobject-1, gio/gobject/glib y libc.

corpus 788/788 · escritorio-kde 171/171 · grafo CIERRA · gate --check OK
Queda 1 wanted en KDE: xwayland — y ése no es deuda técnica sino una decisión
(X11 al tacho, Xwayland = compat opcional por-imagen, no en el core).
2026-09-03 00:38:55 +00:00
Sergio 3b1d69f2a6 estado: cosecha granja 2026-09-03T00:31:06Z — avance del árbol KDE 2026-09-03 00:31:06 +00:00
Sergio f3d33b5a6e estado: cosecha granja 2026-09-03T00:01:10Z — avance del árbol KDE 2026-09-03 00:01:10 +00:00
Sergio de660fc10f qqc2-breeze-style 6.7.2: el estilo QML de Plasma sellado — KDE pasa de 3 wanted a 2
Segundo hueco de RUNTIME del triaje de la frontera. Nadie lo pide para construir
—por eso el escritorio sellaba completo sin él— pero sin este módulo los controles
QML de Plasma (botones, sliders, combos, los menús de los applets y de los KCM) se
dibujan con el estilo genérico de Qt. No lo reemplaza qqc2-desktop-style: aquél es
el estilo de escritorio integrado con KStyle; éste es la implementación QML nativa
de Breeze, la que Plasma 6 usa por defecto.

Dos cosas que no eran obvias:

1. Se llama casi igual que el vecino y viene de otro tarball. qqc2-DESKTOP-style
   sale de Frameworks 6.27.0; qqc2-BREEZE-style sale del release de Plasma 6.7.2,
   el mismo de breeze/kwin/plasma-workspace. Copiar la URL del vecino da 404.
   El sha256 va contrastado contra el .sha256 que publica KDE al lado del tarball,
   no sólo contra lo que bajó acá: la primera descarga volvió con 0 bytes y su
   sha256 era el de la cadena vacía — un vacío que se lee como éxito.

2. La lista de deps no es la del vecino copiada: sale de la clausura de
   find_dependency de los KF6*Config.cmake que ya están en el store. Ese recorrido
   enseñó algo reutilizable: casi todo lo que esos configs piden (X11, XCB,
   Wayland, OpenSSL, BZip2, LibLZMA) vive dentro de un `if (NOT TRUE)` — la rama
   de build ESTÁTICO — y por lo tanto es código muerto en nuestro corpus, que
   compila KF6 dinámico. Por eso acá no hay libX11 ni xorgproto, coherente con
   Wayland-only. El find_package(X11) del CMakeLists raíz no es REQUIRED, así que
   falla en silencio sin romper el feature_summary(FATAL_ON_MISSING_REQUIRED).
   Confirmado a posteriori: los NEEDED del plugin son exactamente los 5 KF6 que la
   clausura predijo (KirigamiPlatform, IconThemes, ColorScheme, GuiAddons,
   ConfigCore) más Qt6 Quick/Gui/DBus/Core.

La fase install verifica por CONTENIDO, no por presencia: qmldir de org.kde.breeze
y de org.kde.breeze.impl no vacíos, Button.qml presente y el plugin de plataforma
de kirigami instalado. Un módulo QML sin su qmldir ocupa disco y arranca con el
estilo genérico sin decir nada. Salieron 83 controles y 3 .so.

Selló al primer intento y REPRODUCE bit a bit (verificar-repro.sh 1/1).

corpus 788/788 · escritorio-kde 166/166 · grafo CIERRA · gate --check OK
Quedan 2 wanted en KDE: xwayland y polkit-qt-1.

Nota sobre el diff de los cinco grafos: cambian los `dependientes_total` de las
deps de esta receta en TODAS las vistas, no sólo en --kde. Es correcto y está
documentado en build-state.py: ese campo se calcula sobre el grafo entero (todas
las colas del disco), que es la corrección del bug de libdrm.
2026-09-02 23:40:21 +00:00
Sergio 06aae8f2a8 estado: cosecha granja 2026-09-02T23:01:05Z — avance del árbol KDE 2026-09-02 23:01:05 +00:00
Sergio ed603f0749 estado: cosecha granja 2026-09-02T22:31:16Z — avance del árbol KDE 2026-09-02 22:31:16 +00:00
Sergio a6beb3b28d shared-mime-info 2.5.1: la base MIME sellada — KDE pasa de 4 wanted a 3
Primero de los cuatro huecos de RUNTIME del triaje de la frontera. Ninguna receta
lo pedía para construir (por eso escritorio-kde cerraba 163/163 sin él), pero sin
la base el escritorio no sabe qué es un fichero: ni asociaciones, ni iconos, ni
abrir-con. Qt6 y GLib lo leen los dos de /usr/share/mime.

Vive en recipes/ y no en incoming-kde: es freedesktop puro, y en la raíz lo ven
los cinco grafos (GNOME y COSMIC lo quieren igual — gdk-pixbuf hoy lo ESQUIVA con
-Dgio_sniffing=false).

Tres cosas que no eran obvias:

1. Fuente por GIT, no tarball. Freedesktop no publica downloads de release para
   este proyecto: la API sólo ofrece los -/archive/ autogenerados de GitLab, que
   no son estables byte a byte (mismo criterio ya escrito en wlr-randr). El pin
   es el tag 2.5.1 PELADO — acá el tag es objeto tag, la trampa de las 4 del
   mirror (ADR 0013).

2. i18n.merge_file de data/meson.build es INCONDICIONAL: no lo apaga
   -Dbuild-translations=false, porque es el paso que PRODUCE freedesktop.org.xml,
   el payload entero. Eso exige un msgfmt que entienda --xml. El de gettext-tiny
   sirve y no degrada nada: desde 2.x el template ya es XML válido (las
   traducciones se marcan con reglas ITS externas, cero ocurrencias de "<_"), así
   que sin catálogos la salida de un msgfmt real ES el template. Verificado a mano:
   copia byte a byte de los 384036 del template.

3. meson install deja SÓLO el XML fuente. Los consumidores no leen ese XML: leen
   los índices que genera update-mime-database (mime.cache, globs2, magic,
   aliases, subclasses). Sin ese paso el artefacto pasa toda verificación de
   presencia y el escritorio sigue sin saber qué es un .png — la regla 3 del repo
   en su forma exacta. La fase install lo corre contra /out y verifica por
   CONTENIDO: mime.cache y globs2 no vacíos, image/png en types. Salieron 1038
   tipos y 1448 globs.

Nace con strip_debug = true y REPRODUCE bit a bit (verificar-repro.sh: 1/1, cero
divergencias), caché binaria incluida. Estático, 0 NEEDED.

corpus 788/788 sealed · escritorio-kde 165/165 · grafo CIERRA · gate --check OK
Quedan 3 wanted en KDE: xwayland, polkit-qt-1, qqc2-breeze-style.
2026-09-02 22:28:54 +00:00
Sergio 48ce856246 estado: cosecha granja 2026-09-02T20:01:07Z — avance del árbol KDE 2026-09-02 20:01:07 +00:00
Sergio 17604b9d2c estado: cosecha granja 2026-09-02T19:31:16Z — avance del árbol KDE 2026-09-02 19:31:16 +00:00
Sergio a46236a405 estado: cosecha granja 2026-09-02T17:32:06Z — avance del árbol KDE 2026-09-02 17:32:06 +00:00
Sergio 612260007a estado: cosecha granja 2026-09-02T16:01:27Z — avance del árbol KDE 2026-09-02 16:01:28 +00:00
SergioandClaude Opus 5 390c6a5e5e llimphi-counter: pinear a v0.2.0 y arreglar el install — el corpus queda 787/787
Deja de ser una plantilla con `commit = 000…0`. Tres cosas, y las dos últimas
son el valor del commit:

1. PIN. `v0.1.0` era el tag que la versión declaraba y NO SIRVE: su `Cargo.lock`
   está desincronizado con sus manifiestos en el propio repo, y `cargo vendor
   --locked` se niega ("cannot update the lock file … because --locked"). No es
   de hammer: el vendoreo del fetch arranca bien y muere DENTRO de cargo.
   Comprobado con `cargo metadata --locked` en los tres refs — v0.1.0
   incoherente, v0.2.0 y main coherentes. Se pinea v0.2.0, PELADO con ^{commit}
   porque es objeto tag (93253cc2, no 0beb83b7).
   => Un tag no sirve como pin sólo por existir: hay que verificar que su
   lockfile cierre.

2. El requisito de `vendor/` en la fuente que pedía el comentario quedó VIEJO:
   hammer vendorea en el FETCH a partir del Cargo.lock, host-side, y por eso el
   `--offline --locked` del sandbox se cumple. El repo no trae vendor/ en ningún
   tag y aun así construye.

3. ⚠ FASE `install` PROPIA, obligatoria con `--example`. El default de Cargo hace
   `find target/release -maxdepth 1 -type f -perm -100`, y cargo deja los
   ejemplos en `target/release/examples/`. El find no encontraba nada, salía 0 y
   **hammer selló un artefacto SIN BINARIO**: 22 min de compilación y un `sealed`
   con sólo `.hammer/recipe.toml` dentro, 20K. `hash --check` decía SELLADO y el
   grafo lo habría contado como al día.
   No lo atrapa `Store::has` ni el guardián de vacíos de build-state, porque el
   directorio NO está vacío — tiene el manifiesto. Es la regla 3 del CLAUDE.md en
   su peor forma: un ausente falla a gritos, esto llegó al final diciendo que
   todo fue bien. Por eso la fase lleva un `test -x` que hace ruidosa la
   ausencia.

Verificado por contenido, no por el `sealed`: 17 M, usr/bin/llimphi-counter, ELF
pie, NEEDED = libc.so (dinámico, como la receta promete para un binario que
dlopea Vulkan/Wayland). El artefacto falso se podó y quedó en el ledger.

`version` pasa de 0.1.0 a 0.2.0 y NO mueve el hash: no está en hash_inputs (como
`license`). La identidad la da el commit.

Corpus: 787/787 SELLADAS, 0 deuda, 0 never. Gate --check OK, grafo CIERRA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-09-02 15:51:21 +00:00
Sergio 94faf1289c estado: cosecha granja 2026-09-02T15:32:58Z — avance del árbol KDE 2026-09-02 15:32:58 +00:00
Sergio 2c8a82859f estado: cosecha granja 2026-09-02T15:02:27Z — avance del árbol KDE 2026-09-02 15:02:27 +00:00
SergioandClaude Opus 5 12c30ec9f9 gnome: la terna no es deuda, es alcance — a .deferred/ con el mapa corregido
gdm, gnome-session y gnome-settings-daemon salen de la cola y quedan aparcadas
en `recipes/incoming-gnome/.deferred/`, que es el mecanismo que el repo ya usa
(precedente: incoming-kde/.deferred/libXft.toml). El glob del worker es
TOP-LEVEL y el del grafo también, así que dejan de molerse cada ciclo y dejan de
contarse como deuda que nadie va a pagar. `targets.toml` ya no las lista desde
el 2026-08-07 («APARCADAS POR DISEÑO»); esto alinea el árbol con esa decisión.

Y el mapa que llevaban estaba mal en las dos direcciones:

1. «La terna GTK3» es un nombre engañoso: **GTK3 no es el muro**. GTK3 tiene
   backend Wayland y se construye con -Dx11_backend=false. Autorarlo no habría
   destrabado ninguna de las tres. Lo que bloquea de verdad:
     - g-s-d 48.1: gtk+-x11-3.0 / x11 / xfixes INCONDICIONALES => imposible en
       Wayland-only, no «pendiente».
     - gnome-session 48.0: dependency('libsystemd', required: true) en meson:124
       es una comprobación de pkg-config EN BUILD. arje-logind-compat no la
       satisface y es a propósito — su receta explica que GNOME consulta login1
       en RUNTIME por D-Bus y que no hace falta la C-ABI sd-login. Único camino:
       parchear ese required a false, que es una decisión, no un arreglo.
     - gdm: cuelga de gnome-session, y con mirada-greeter probablemente sobra.

2. La lista «FRONTERA» estaba VIEJA: ya existen libX11, libXfixes, xorgproto,
   libXau/Xcursor/Xdmcp/Xext/Xi/Xrender/Xtst, libxcb (casi todas por KDE) y
   polkit, upower, geocode-glib, libgweather. Cuatro de las ocho líneas de g-s-d
   habían dejado de ser ciertas. Faltan de verdad: gtk3, libnotify y una
   variante libcanberra-gtk3.

Verificado contra el TARBALL (sha256 = el del pin), no contra el comentario.

incoming-gnome queda en 79 recetas, sellado=79 deuda=0 nunca=0.
Gate --check OK, grafo CIERRA, escritorio-gnome sigue 119/119.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-09-02 14:46:37 +00:00
Sergio 28611dabf0 estado: cosecha granja 2026-09-02T14:32:36Z — avance del árbol KDE 2026-09-02 14:32:36 +00:00
SergioandClaude Opus 5 c29a4fcb9e gnome: retirar 8 copias de cola que sellan igual que el corpus
`fontconfig-shared`, `freetype-shared`, `libjpeg-turbo-shared`, `libpng-shared`,
`libtiff-shared`, `libxml2-shared`, `libyaml-shared` y `zlib-shared` existían a
la vez en `incoming-gnome/` y en `recipes/`, y las ocho daban el MISMO
ArtifactHash que su gemela del corpus. Como la resolución de deps cae al padre
cuando no hay hermano, quitarlas deja a las consumidoras resolviendo contra
`recipes/` y con el mismo hash.

Medido, no supuesto: se hashearon las 1144 recetas antes y las 1136 después, y
de las 1136 supervivientes **0 cambiaron de hash**. No hay rebuild.

Lo que NO se toca, y conviene que quede dicho porque se parece:

- Las variantes de la ISLA DINÁMICA (glib, gtk4, gdk-pixbuf, pango, harfbuzz,
  graphene, libadwaita, json-glib, libusb, libxcvt, pipewire, pulseaudio,
  wireplumber, xdg-desktop-portal): mismo nombre, hash DISTINTO. Son sombras a
  propósito — la introspección de GNOME exige .so reales.
- Las copias entre COLAS (alsa-lib, hwdata, xkeyboard-config, libdisplay-info,
  lcms2, icu4c, lua, nasm, fuse3, libsndfile, libelogind, hicolor-icon-theme,
  dbus-shared): también dan el mismo hash, pero NO sobran. Cada cola necesita su
  propio hermano para cerrar su clausura; borrar la de una rompe esa cola. No es
  el caso de onda-2, donde la cola entera duplicaba a otra.

Y el criterio, otra vez: `pipewire` y `pulseaudio` son TEXTUALMENTE idénticas a
las de COSMIC y sellan distinto, porque sus deps resuelven distinto según la
cola. El fichero no dice la verdad; el hash sí.

Gate --check OK, grafo CIERRA, escritorio-gnome sigue 119/119. La cola queda en
82 recetas (79 selladas + la terna GTK3) y sus 5 parches, todos en uso.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
2026-09-02 14:10:56 +00:00
Sergio 9ab5464a6b estado: cosecha granja 2026-09-02T14:03:32Z — avance del árbol KDE 2026-09-02 14:03:32 +00:00
Sergio 62899432f6 estado: cosecha granja 2026-09-02T13:33:10Z — avance del árbol KDE 2026-09-02 13:33:10 +00:00