Commit Graph
1850 Commits
Author SHA1 Message Date
Sergio 2ce8addfab estado: cosecha granja 2026-09-03T18:31:58Z — avance del árbol KDE 2026-09-03 18:31:58 +00:00
SergioandClaude Opus 5 bcd026f3b4 plan: el X11 de Firefox no era un muro — está detrás de CONFIG[MOZ_X11]
La tabla lo listaba como muro duro por los libxt/libxcomposite del APKBUILD. En el árbol
de Gecko todo el código X11 cuelga de CONFIG["MOZ_X11"] (widget/gtk/moz.build:131): es
una perilla de build y existen builds Wayland-only. El muro real es GTK3 + el toolchain
wasi. Cuarta corrección a esta tabla en el día; el método viejo contaba nombres de
paquete de Alpine y no miraba el árbol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 18:19:47 +00:00
Sergio e2ef0df29f estado: cosecha granja 2026-09-03T18:02:49Z — avance del árbol KDE 2026-09-03 18:02:49 +00:00
Sergio 70c6690fa1 estado: cosecha granja 2026-09-03T17:32:04Z — avance del árbol KDE 2026-09-03 17:32:04 +00:00
SergioandClaude Opus 5 6c37df6fa4 ncurses-shared: cierra la fuga de pw-top al lab, que llevaba desde agosto
recipes/pipewire.toml:85-88 afirma que quitando la dep de ncurses «meson saltea pw-top
y el resto construye». ES FALSO, y llevaba así desde al menos el 2026-08-29. Quitar la
dep no desactivó nada: el guardián de upstream es `if ncurses_dep.found()` y meson lo
encontró igual, en el sysroot Alpine DEL LAB. pw-top se construye, se sella y sale con
NEEDED libncursesw.so.6, que ningún artefacto del cierre publica porque el ncurses del
catálogo es --without-shared. Una herramienta rota dentro de las CUATRO imágenes,
invisible para el store porque el lab no entra en hash_inputs.

LA LECCIÓN, que es más grande que pw-top: QUITAR UNA DEP DE [deps] NO APAGA LA FUNCIÓN.
Cambia de dónde sale. Para apagarla de verdad hay que decírselo a la perilla del
proyecto; si no la hay, la dep tiene que estar declarada y satisfecha desde el store.

POR QUÉ UNA RECETA NUEVA Y NO RE-SELLAR PIPEWIRE. El arreglo limpio es declarar la dep y
reconstruir; cuesta 10 dependientes directos y 7 sellados que caen a deuda, entre ellos
xdg-desktop-portal-cosmic y cosmic-settings-daemon, que son Rust —el portal murió por
OOM tres veces y el que selló tardó 48 min con pico de 5,4 GiB de swap—. No se paga eso
hoy por una herramienta de diagnóstico, y menos con otro agente compilando en la misma
máquina. Lo que cruza acá es un SONAME, y para eso el repo ya tiene regla escrita:
enlazar contra una variante y CORRER contra otra es legítimo cuando lo que cruza es un
SONAME. Cero rebuilds.

⚠ QUEDA DEUDA ANOTADA: pipewire sigue enlazando contra el lab en BUILD. El día que se
re-selle por cualquier otro motivo hay que declarar ncurses-shared en sus [deps] y
corregir el comentario mentiroso. Esto tapa el síntoma en runtime, no la causa.

DOS COSAS QUE COSTARON, y están escritas en la receta:
- `-stats` es un flag de GNU ld que lld rechaza, y viene DENTRO del token
  `-Wl,-soname,...,-stats,-lc` que arma el configure de ncurses. No se puede pasar por
  LDFLAGS ni hay perilla MK_SHARED_LIB, y el patrón .zwrap de poppler tampoco sirve tal
  cual porque filtra argumentos completos y acá hay que reescribir uno.
- Quitando `-stats`, el siguiente en caer es el `-lc` del mismo token. El driver de zig
  valida lo que va dentro de `-Wl,` y no deja pasar ninguno de los dos; libc la liga él
  solo, así que la cola `,-stats,-lc` se va entera. Se quita del Makefile GENERADO, con
  un grep previo que FALLA si el configure deja de emitirlo (para no seguir en silencio).

EVIDENCIA, con control discriminante:
  sin ncurses-shared -> Error relocating /lib/libncursesw.so.6: __vfprintf_chk: symbol
                        not found   (símbolo de _FORTIFY_SOURCE de glibc que musl no
                        tiene: la fuga al lab, reproducida)
  con ncurses-shared -> carga y corre; sólo falla en el runtime de PipeWire por no haber
                        demonio ni plugins SPA en la raíz de prueba

vigia-sonames en los cuatro perfiles: libncursesw.so.6 Y libpanelw.so.6 desaparecen.
Lo que queda son herramientas de build (go, python3, perl) más tres de RUNTIME que NO
son de este cambio y quedan anotadas: spidermonkey pide libgcc_s/libstdc++ en gnome,
libadwaita pide liblzma, y sqlite-shared pide libreadline.

Grafos: 820/820, 999/999, 883/883, 856/856, 833/833.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 17:29:24 +00:00
SergioandClaude Opus 5 68274d6c2a kde: declarar las 22 apps que ya estaban selladas y en ninguna imagen
CERO builds: las 22 llevaban tiempo selladas en incoming-kde y no las declaraba nadie,
así que sólo entran a la clausura. El perfil pasa de 200 a 259 nodos y sigue 998/998.

POR QUÉ FALTABAN, que no es lo mismo que «se decidió que no fueran». Las 7 raíces del
perfil son el set MÍNIMO del arranque en metal, tomado de
docs/HANDOFF-noche-kde-2026-07-14.md:135. Ese mismo documento nombra lo que faltaba como
el trabajo siguiente: «Capa 2 KF6 / apps núcleo: importar+construir konsole/dolphin/kate
vía granja (extiende el escritorio usable)». Se construyeron y se sellaron; lo que nunca
ocurrió fue DECLARARLAS. La métrica no lo podía ver porque mide lo declarado — la misma
figura que dejó a sway en 121/121 sin emulador de terminal.

Sin esto la imagen era compositor + shell + tema y nada más: sin terminal no se puede
salir de un fallo, y sin plasma-nm/plasma-pa/powerdevil no hay red, ni audio, ni gestión
de energía.

Entran en tres grupos:
- plomería de Plasma: plasma-desktop, plasma-nm, plasma-pa, powerdevil, kscreen,
  systemsettings, kinfocenter, kmenuedit, kwalletmanager
- núcleo usable: konsole, dolphin, kate, okular, ark, spectacle, gwenview
- accesorios ya construidos: kcalc, kfind, filelight, kcharselect, kruler, kdf

Cómo se encontró, para repetirlo: listar los (cola,nombre) cuyo artefacto instala un
usr/share/applications/*.desktop y que yupana.membresia() no asigna a ningún perfil.
Salieron 23; 22 eran de acá.

VERIFICADO con scripts/vigia-sonames.py, que es donde 22 componentes de RUNTIME nuevos
podrían filtrar al lab: escritorio-kde queda con 1254 sonames provistos y 5 sin
proveedor, y los 5 son ruido conocido —cmake, python3 y perl son herramientas de build
que la imagen no instala, más el pw-top de pipewire que ya estaba anotado—. Ninguna app
nueva aporta un NEEDED huérfano.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 17:22:02 +00:00
Sergio 32719dc89d estado: cosecha granja 2026-09-03T17:01:31Z — avance del árbol KDE 2026-09-03 17:01:31 +00:00
Sergio b5cb8fb2cc estado: cosecha granja 2026-09-03T16:32:05Z — avance del árbol KDE 2026-09-03 16:32:05 +00:00
SergioandClaude Opus 5 ce76796b9b qorpa D11: el proxy de Wayland — el socket deja de ser un cheque en blanco
Paso 8 del ADR 0015, el que el propio ADR llamaba el más valioso, y cierra su
§NO-resuelve 1.

Pasar el socket de Wayland crudo NO es abrir un caño: es un borde de privilegio.
El compositor anuncia TODOS sus globals, y entre ellos hay protocolos que hacen
cosas que nadie concedió — screencopy captura la pantalla, virtual_keyboard
sintetiza teclas, data_control lee el portapapeles sin que nadie copie, y
layer_shell dibuja encima de todo. Una instancia ajena con el socket crudo podía
las cuatro, y lo único que teníamos era un aviso.

El proxy relaya todo salvo dos mensajes:
- `wl_registry.global` del compositor: si el interface no está en la lista, no se
  reenvía y el cliente nunca se entera de que existe.
- `wl_registry.bind` del cliente: si el `name` es uno de los ocultos, se manda un
  wl_display.error y se corta. Sin esto el filtro sería decorativo — el `name` es
  un número y se puede adivinar.

LISTA BLANCA, al revés que la denylist de syscalls de harkaq, y la asimetría
tiene razón: el conjunto PELIGROSO de Wayland crece —cada compositor inventa
protocolos privilegiados— mientras que el NECESARIO para dibujar una ventana es
corto y estable. Una denylist estaría desactualizada el día que alguien
actualice el compositor, y sin un solo error visible. La lista incluye a
propósito lo que los juegos piden y suele olvidarse: pointer_constraints,
relative_pointer, tearing_control, idle_inhibit.

Lo que hace esto viable en un fichero: ningún mensaje que se descarta lleva
descriptores, así que los fds —que Wayland pasa por SCM_RIGHTS para wl_shm y
dmabuf, y sin los cuales no se dibuja nada— se relayan en orden de llegada sin
tener que asociarlos a su mensaje.

PROBADO SIN COMPOSITOR, con uno falso que anuncia siete globals (tres permitidos
y cuatro peligrosos) y un cliente que cuenta lo que ve: pasan exactamente los
tres, y el intento de bindear por número el name que nunca se anunció corta la
conexión. Es la clase de prueba que no necesita GPU y responde la pregunta
entera. Un test más cubre el `size` menor que la cabecera, que haría avanzar 0
bytes y colgar el proxy en un bucle.

El socket crudo sigue disponible con `wayland_raw`, apagado por defecto y
avisando a gritos. Y si no hay compositor, `run` FALLA en vez de degradar.

Las features `socket`/`uio` de nix son aditivas: no cambian lo que compilan los
demás crates. 54/54.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 16:24:05 +00:00
SergioandClaude Opus 5 1f415802f1 apps: GNOME tenía imagen sin terminal ni editor — arreglado con CERO recetas; y el montón A queda agotado
DOS COSAS, y la segunda no estaba en el plan.

1. EL MONTÓN A SE AGOTÓ DE COSAS SIN MURO. Re-triadas con scripts/provee.py las tres
   candidatas que la tabla daba como baratas o dudosas; las tres son muro, y ninguna por
   el motivo que decía la tabla:

   - imv     → no hay proveedor de OpenGL de escritorio (ya documentado; la sustituyó swayimg)
   - mupdf   → la nota decía «el X11 es del visor mupdf-x11; mupdf-gl no». Falso: platform/gl/
               va sobre GLUT + OpenGL de escritorio, el mismo muro que imv. mupdf NO TIENE
               visor Wayland: sus dos UIs son X11 o GL. El motor no tiene muros pero no hace
               falta, el PDF ya lo da poppler.
   - inkscape→ era GTK3 encubierto, como la tabla sospechaba: INKSCAPE_1_4_4 pide
               gtkmm-3.0>=3.24 y gtk+-3.0>=3.24. master SÍ migró a GTK4 (gtk4>=4.14) pero
               está sin publicar, y además el catálogo no tiene NI UN binding C++:
               provee.py da ✗ en sigc++-2.0, glibmm-2.4, cairomm-1.0, pangomm-1.4 y gtkmm.

   ⇒ todo lo que queda está detrás de UNA de cuatro decisiones (GTK3 / Qt6-en-el-corpus /
   GL de escritorio / X11), no de trabajo de recetas.

2. LO QUE SÍ ESTABA ROTO, Y NO ERA FALTA DE RECETAS. Cruzando la membresía de los cuatro
   perfiles contra emuladores de terminal, editores y gestores de ficheros:

       escritorio-cosmic  term=cosmic-term  editor=cosmic-edit  archivos=cosmic-files
       escritorio-sway    term=foot         editor=vim          archivos=—
       escritorio-gnome   term=—            editor=—            archivos=—
       escritorio-kde     term=—            editor=—            archivos=—

   Es la lección de foot otra vez y a mayor escala: sway llegó a 121/121 SIN emulador de
   terminal porque nadie lo declaraba, y la métrica de clausura no lo ve porque mide lo
   DECLARADO. Un escritorio sin terminal no es uno incompleto: es uno del que no se puede
   salir cuando algo falla.

   GNOME arreglado con CERO recetas nuevas: foot y helix viven en el CORPUS, ya estaban
   selladas y no las declaraba NINGÚN perfil. foot es Wayland puro sobre xdg-shell (no
   pide protocolos de wlroots) así que corre bajo mutter igual que bajo sway; su único
   NEEDED es libc.so. helix es TUI y corre dentro de la terminal.

   ⚠ KDE NO SE TOCA, y es un hallazgo aparte que hay que acordar con ese frente: tiene 22
   apps con .desktop selladas en su cola y sin declarar (konsole, dolphin, kate, okular,
   spectacle, ark, gwenview, systemsettings, kcalc…) y además le faltan plasma-desktop,
   plasma-nm, plasma-pa, powerdevil y kscreen — o sea red, audio, energía y pantalla. Su
   perfil son 13 raíces de ARRANQUE EN METAL, no la imagen de escritorio completa (ésa se
   hidrató aparte, por hash). No cuesta un build; cuesta acordarlo.

Nota de método: build-state.py invoca `hammer hash` ~1000 veces y el otro agente
recompiló el binario a mitad del barrido ⇒ kjobwidgets salió `unhashable` y KDE reportó
997/998. No era la receta: era la carrera. Re-corrido da 998/998.

Los cinco grafos: 819/819, 998/998, 882/882, 855/855, 832/832, cero nodos `wanted`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 16:18:44 +00:00
SergioandClaude Opus 5 cab9439644 fuentes: dejavu al corpus (arregla un wanted que dejé), KDE con fuente, y la nerd de monospace
TRES COSAS, y la primera es un bug mío de hace un rato.

1. dejavu-fonts PROMOVIDA AL CORPUS — arregla un nodo `wanted` que introduje.
   Al añadirla de raíz a escritorio-gnome quedó como `wanted`: la receta existía en
   incoming-{kde,wlr,cosmic} y NO en incoming-gnome ni en el corpus, y una receta no
   alcanza una cola hermana. O sea que la raíz no resolvía a nada y GNOME seguía sin
   fuentes. `wanted` NO es `debt`, así que drenaje.json seguía diciendo deuda=0 y el
   hueco no salía por ninguna métrica — se vio mirando el nodo en build-state-gnome.json.
   Promoción gratis: las tres copias sellaban el mismo hash (b3:5b3a5df3) y sus deps
   están vacías. Las tres se jubilan. Ahora los cuatro perfiles resuelven a corpus y los
   cinco grafos quedan con CERO nodos `wanted`.

2. dejavu-fonts en escritorio-kde, como se pidió. Tenía UNA fuente y de rebote, dentro
   de qtbase — apoyarse en un detalle de empaquetado de Qt deja sin nada a todo lo que
   no es Qt.

3. dejavu-fonts-nerd 3.5.1 en los CUATRO perfiles: DejaVu Sans Mono parcheada con Nerd
   Fonts. Lo que se midió antes de escribirla, porque cambia la forma de hacerla:

   - SE AÑADE, NO REEMPLAZA, y no es prudencia: la licencia de Bitstream Vera exige que
     una fuente modificada se RENOMBRE. Por eso upstream la llama «DejaVuSansM Nerd Font
     Mono» — leído del name de la TTF, no supuesto. Una fuente nerdeada NO puede
     responder al nombre de la original; sustituirla rompería a todo lo que pide «DejaVu
     Sans Mono», empezando por foot, que ya murió una vez así.
   - SÓLO LA MONOESPACIADA: upstream no publica DejaVu Sans ni Serif parcheadas, y en una
     fuente de interfaz proporcional esos glifos no los pide nadie. «Todas nerdeadas» no
     se puede cumplir al pie de la letra.
   - SÓLO UNA DE LAS TRES VARIANTES: NerdFont / NerdFontMono / NerdFontPropo son los
     mismos glifos con distinto avance. Las doce pesan 32 MB; las cuatro de Mono, 11 MB.
     Se quedan las Mono, que son las correctas para una rejilla de terminal. Referencia:
     la familia DejaVu ENTERA son 9,8 MB.

   ⚠ EL NÚMERO DEL .conf ESTÁ MEDIDO, NO ELEGIDO. fontconfig carga conf.d en orden
   alfabético y el <prefer> que llega antes gana. Con 60-nerd-monospace.conf —que ordena
   después de 60-latin.conf, que ya prefiere Noto/DejaVu/Inconsolata— `fc-match
   monospace` seguía dando DejaVu Sans Mono: la conf estaba INSTALADA Y ERA INERTE, el
   mismo modo de fallar que el plugin ALSA de PipeWire. A 59- gana. 45-latin.conf también
   menciona monospace pero usa <default>, no <prefer>, así que no compite.

   ⚠ Y strip_components = 0, que tampoco es cosmético: el tarball de nerd-fonts es PLANO
   y con el default de 1 el árbol de fuentes queda VACÍO — el build no se detiene ahí,
   sigue y falla más tarde en el cp, con un mensaje que no menciona la extracción.

   ⚠ LA LICENCIA NECESITA UNA PASADA HUMANA antes de `hammer pack`: el LICENSE.txt del
   tarball cubre sólo DejaVu/Bitstream Vera y no menciona los ~10 conjuntos de iconos
   importados, que tienen licencias propias (Font Awesome CC-BY-4.0, Octicons MIT,
   Material OFL, Powerline MIT…). El campo dice lo DOCUMENTADO, no el conjunto real; no
   se inventa una cadena SPDX. Anotado en la receta.

EVIDENCIA, contra el conf.d REAL del artefacto de fontconfig:
  monospace          -> DejaVuSansM Nerd Font Mono   (los iconos llegan)
  "DejaVu Sans Mono" -> DejaVu Sans Mono             (intacta)
  sans-serif         -> DejaVu Sans                  (sin tocar)
  serif              -> DejaVu Serif                 (sin tocar)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 16:09:24 +00:00
Sergio be7aa0ef35 estado: cosecha granja 2026-09-03T16:01:31Z — avance del árbol KDE 2026-09-03 16:01:31 +00:00
Sergio 9028274c68 estado: cosecha granja 2026-09-03T15:31:18Z — avance del árbol KDE 2026-09-03 15:31:18 +00:00
Sergio 8667fd43b7 estado: cosecha granja 2026-09-03T15:01:21Z — avance del árbol KDE 2026-09-03 15:01:21 +00:00
Sergio 4f1f10d50b estado: cosecha granja 2026-09-03T14:31:28Z — avance del árbol KDE 2026-09-03 14:31:28 +00:00
Sergio 3bd702e7e2 estado: cosecha granja 2026-09-03T14:01:12Z — avance del árbol KDE 2026-09-03 14:01:12 +00:00
Sergio 1870bc9d5f estado: cosecha granja 2026-09-03T13:31:13Z — avance del árbol KDE 2026-09-03 13:31:13 +00:00
Sergio ae2f97e4ff estado: cosecha granja 2026-09-03T13:01:28Z — avance del árbol KDE 2026-09-03 13:01:28 +00:00
Sergio 0bf92590fc estado: cosecha granja 2026-09-03T12:31:10Z — avance del árbol KDE 2026-09-03 12:31:10 +00:00
SergioandClaude Opus 5 d77ad15d01 qorpa prune: la poda nace con el subsistema, no después
Paso 7 del ADR 0015, sus dos mitades.

PODA. Un rootfs son cientos de MB o varios GB y no lo alcanzan ni store-gc ni
la caché .dmerge: es un tercer montón sin dueño, como ya lo fue work/sources.
`hammer qorpa prune` barre restos de pulls a medias, imágenes que ninguna
instancia usa —se re-traen por digest, que es justo la propiedad que da el pin—
y, con --upper, las capas mutables, que por D3 siempre se pueden tirar.

Con las dos cicatrices del repo cableadas:
- SIN --yes es un simulacro. Y tras borrar COMPRUEBA que el directorio se fue,
  porque store-gc reportaba borrados que no ocurrían y eso se descubrió tarde.
  Si dice que sí y sigue ahí, sale ≠0 diciendo que el número de arriba no es lo
  que se liberó.
- El tamaño de un árbol con directorios ilegibles sale MENOR de lo que es: un
  upper con ficheros de los subuid no se puede recorrer entero. Ahora se cuentan
  los directorios ciegos y la cifra se marca con `≥`. Un número silenciosamente
  bajo es peor que ninguno cuando con él se decide borrar.

Y --upper deja la instancia USABLE: sin upper/ y work/ no vuelve a arrancar.

LICENCIAS (SDD 20). Las imágenes ajenas quedan fuera del catálogo publicable y
del reporte de licencias por escrito, y la razón no es pereza: no podemos
enumerarlas — un `pacman -S` dentro de una instancia trae paquetes que nadie
declaró acá, y afirmar una licencia sobre eso sería inventarla. Lo que sí se
hace: contarlas aparte en clase `ajeno` (el riesgo real del ADR es que en seis
meses alguien las cuente como corpus), y dejar dicho que si algún día se espejan
hay que mirar licencia Y MARCA antes, igual que con Firefox. Más el corolario
que faltaba escribir donde se lea: el claim «hammer reproduce bit a bit» hay que
acotarlo desde el día que exista una instancia.

2 tests nuevos (la poda no toca una imagen en uso y sí los restos; con --upper
la capa se va pero la instancia queda usable). 50/50.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 12:18:10 +00:00
SergioandClaude Opus 5 da0186d250 apps: zathura + backend de PDF, en gnome/cosmic/sway — y tres defectos de imagen que salieron
La cadena: 5 recetas nuevas (girara, xxhash, poppler-glib, zathura,
zathura-pdf-poppler) y 5 promociones GRATIS al corpus (json-glib, lcms2, glib-shared,
pcre2-shared, bzip2-shared — todas con hash idéntico desde la cola y desde el corpus,
medido con hammer hash antes de mover nada).

NO va en escritorio-kde, y está escrito en tres sitios para que no se liste por
descuido: su backend necesita una poppler con ENABLE_GLIB=ON, y esa imagen ya trae la
de Qt6 vía okular. Las dos instalan /usr/lib/libpoppler.so.146 y son artefactos
DISTINTOS ⇒ hidratar las dos pone dos ficheros en la misma ruta. La salida no sería
listarla igual, sería jubilar una de las dos popplers.

LAS TRES COSAS QUE SE ROMPIERON, que valen más que las recetas:

1. -DENABLE_GLIB=ON SE APAGA SOLO Y EL BUILD SALE OK. CMakeLists:260 hace
   `if(NOT CAIRO_FOUND) set(ENABLE_GLIB OFF)`: no falla, obedece distinto. El primer
   artefacto selló sin nada de glib, exit 0, sin aviso. La causa era una línea que este
   repo ya tiene como patrón: `.pc Requires` → `[deps].build`. cairo.pc pide pixman-1 y
   pixman no estaba declarado ⇒ `pkg-config --exists cairo` falso ⇒ CAIRO_FOUND falso.
   Un .pc que falta a tres saltos apaga una FUNCIÓN, no una librería. La receta ahora
   comprueba en install que poppler-glib.pc y libpoppler-glib.so existan.

2. DOS COPIAS ESTÁTICAS DE GObject EN UN PROCESO. Con la glib estática del corpus todo
   sella y zathura arranca — y al dlopen del plugin escupe «cannot register existing
   type 'gchar'» y no abre nada. No son dos ficheros en una ruta: son dos copias del
   sistema de tipos dentro del mismo proceso, una en el ejecutable y otra dentro de
   libpoppler-glib.so. Un .a NO duplica (sólo tiene símbolos sin definir, se resuelven
   al ligar); el problema aparece sólo cuando dos objetos enlazados por separado meten
   cada uno la suya. Arreglo: las TRES recetas de la cadena declaran glib-shared.

3. FUGA AL LAB, cazada por scripts/vigia-sonames.py: zathura selló con
   NEEDED libsqlite3.so.0, un SONAME que ningún artefacto del cierre publica — meson
   había resuelto dependency('sqlite3') contra el sysroot Alpine DEL LAB. Y declarar la
   dep NO alcanzó: el .pc da un -lsqlite3 pelado y el linker prefiere la .so del lab
   sobre la .a del store. Lo arregla -Dprefer_static=true. Medido: los NEEDED bajaron de
   DIEZ a CUATRO y los cuatro los publica el cierre.

DOS DEFECTOS DE IMAGEN PREEXISTENTES que salieron de paso:

- libbz2.so.1 no lo publicaba nadie y freetype-shared lo pide, en los TRES perfiles.
  Se vio de verdad al probar el visor (el plugin no hacía dlopen). bzip2-shared existía
  sólo en incoming-kde; promovida y puesta de raíz junto a expat-shared/libffi-shared.

- escritorio-gnome NO TRAÍA NI UN FICHERO DE FUENTE. Contado sobre los artefactos del
  cierre: gnome=0, sway=22, kde=1 (una de rebote dentro de qtbase). Tenía fontconfig,
  que es el MOTOR que busca fuentes, no una fuente. Un PDF con base-14 se ve VACÍO y sin
  error. Añadida dejavu-fonts, la única receta de fuentes del catálogo entero.
  ⚠ KDE queda igual y NO se toca acá: no lleva zathura y su árbol lo trabaja otro frente.

EVIDENCIA DE QUE ANDA, no de que sella:
- `zathura --version` con el plugin lista «(plugin) pdf-poppler (2026.07.18)» sin un
  solo GObject-CRITICAL.
- poppler-render-check, receta-TESTIGO en el espíritu de gtk4-hello: arma un PDF a mano,
  lo abre con poppler-glib, lo rasteriza sobre cairo y CUENTA PÍXELES — 9600 negros,
  exactamente el rectángulo de 160x60. Un lienzo blanco no pasa. Su primera versión
  medía el TEXTO y falló: el sandbox no tiene fuentes, que es cómo se descubrió el
  hueco de arriba. El assert quedó sobre el rectángulo, que no depende de tipografía, y
  el texto se informa aparte.

Los cinco grafos quedan en N/N con cero deuda.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 12:14:07 +00:00
Sergio 4e1fed04d7 estado: cosecha granja 2026-09-03T12:01:45Z — avance del árbol KDE 2026-09-03 12:01:45 +00:00
SergioandClaude Opus 5 1d9ddcee37 qorpa D10: Steam en la mano, y los tres muros que sólo se ven así
Paso 6 del ADR 0015, PARCIAL y dicho como parcial. Steam 1.0.0.87 instalado de
verdad desde el multilib de Arch —las libs de 32 bits que la F2 del plan de
juegos daba por «una campaña entera»—, su cliente i386 bajado y desempacado por
el bootstrap de Valve junto con el Steam Runtime, y el BWRAP ANIDADO VERIFICADO
EXPLÍCITAMENTE (24 montajes propios dentro de la instancia), que es lo que este
paso pedía. Lo que NO se pudo: la máquina no tiene sesión gráfica, y el runtime
sniper sólo se baja al instalar un juego, que exige credenciales ⇒
pressure-vessel con un juego real sigue sin ejercitarse. Se dice, no se insinúa.

Tres muros, ninguno en el ADR:

1. LA JAULA MATABA TODOS LOS BINARIOS DE 32 BITS. El filtro seccomp comprueba
   arch == x86_64 y MATA lo que no lo sea; para un build es la defensa clásica y
   correcta, para el montón B es fatal porque el cliente de Steam es un ELF i386.
   El síntoma fue `ldd: exited with unknown exit code (159)` = 128+31 = SIGSYS,
   que no se parece en nada a la causa. La salida no es aflojar el check sino
   darle a i386 su propia tabla con la MISMA política. Los 25 números se
   verificaron uno por uno contra /usr/include/asm/unistd_32.h: 24 bien y UNO
   MAL — kexec_file_load no existe en i386 y su número de x86_64 (320) es ahí
   `utimensat`, o sea que habríamos denegado algo que usa cualquier cosa que
   toque una marca de tiempo. Es la diferencia entre una tabla de memoria y una
   verificada.

2. STEAM SE NIEGA A CORRER COMO ROOT, y cambiar el mapa para evitarlo CORROMPE
   la instancia: un fichero creado bajo un mapa aparece con otro uid bajo el
   otro, así que el useradd de la preparación deja un /home que su propio dueño
   no puede escribir. ⇒ el mapa es parte de la IDENTIDAD de la instancia. La vía
   correcta es la de cualquier runtime de contenedores: un solo mapa y se BAJA de
   privilegio adentro — campo `run_as`, setpriv, con el CAP_SETUID que ya
   tenemos en el namespace.

3. EL XDG_RUNTIME_DIR ES DEL USUARIO QUE CORRE, no del uid del mapa: con
   `run_as`, apuntarlo al de root deja al Steam Runtime sin poder crear su
   temporal. Se lee como un aviso menor hasta que algo deja de andar sin decir
   por qué.

Lo que sí quedó probado, y es el corazón del ADR: un userland glibc ajeno con su
cadena de 32 bits completa corre enjaulado sobre nuestro kernel, con seccomp y
no_new_privs puestos, y un contenedor anidado funciona adentro — la forma exacta
en que Valve prueba Proton.

1 test nuevo (run_as resuelve uid/gid/home del passwd de la imagen, y un run_as
inexistente NO cae a root). 48/48.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
2026-09-03 12:01:32 +00:00
SergioandClaude Opus 5 058e3e9206 recetas: girara, xxhash y json-glib al corpus — la cadena de zathura, medida
⚠ CORRIGE UNA CREENCIA DEL REPO: zathura NO está bloqueada por GTK3. Upstream VACIÓ
girara — en 2026.07.18 su árbol son siete .c (datastructures, input-history, log,
template, utils) y su meson.build no menciona GTK ni una vez. Toda la UI se mudó
adentro de zathura, que hoy pide GTK4 >= 4.12, y gtk4 4.18.6 está en el corpus.
El plan de apps daba a zathura por «verificar si su cadena es GTK4 y no GTK3»: lo es.

Las tres piezas que faltaban, medidas con scripts/provee.py --desde corpus:

- girara: sella al primer intento; sus tres deps (glib/gobject/gio) salen del mismo
  artefacto glib. -Ddocs=disabled porque ese target es doxygen y produce HTML de API.

- xxhash 0.8.3 (zathura lo usa para la caché de páginas). DISPATCH=0 no es preferencia,
  el build se cae sin eso: Makefile:58-62 hace `$(CC) -dumpmachine | grep x86_64` y
  enciende DISPATCH solo por detección de arquitectura — una perilla auto que hace
  depender el artefacto de dónde se construyó. Con ella, xxh_x86dispatch.c muere porque
  pide AVX-512 con -mavx512f a secas y el clang de zig ya exige además -mevex512
  («AVX vector argument of type __m512i without evex512 enabled changes the ABI»).
  No se arregla subiendo flags: esa variante sólo la usa el despacho en runtime, que es
  justo lo que no queremos. Apaga las dos cosas de una.

- json-glib PROMOVIDA al corpus, y salió GRATIS por la misma razón que pipewire: la
  variante de COSMIC resuelve sus deps enteras contra el catálogo padre ⇒ mismo
  ArtifactHash desde las dos partes (b3:62011e81, medido antes de mover nada). La de
  GNOME no podía: arrastra gobject-introspection, glib-introspected, gi-foreign-girs y
  py3-setuptools. La copia de incoming-cosmic se jubila, verificado como manda el repo:
  su único consumidor (xdg-desktop-portal) hashea b3:749fbffe ANTES y DESPUÉS ⇒ cero
  rebuilds. La de incoming-gnome SE QUEDA: variante deliberada, artefacto distinto.

Falta todavía el backend de PDF, que es el hueco real: incoming-kde/poppler va con
-DENABLE_GLIB=OFF ⇒ publica libpoppler.so pero NO poppler-glib, y encima sólo se
alcanza desde su cola.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 11:49:44 +00:00
SergioandClaude Opus 5 e6cfb470f1 vigía: provee.py — qué publica de verdad el catálogo, y desde dónde se alcanza
El triaje de apps falló TRES veces, cada vez por una pregunta distinta y cada vez la
anterior daba verde:

  1. ¿existe la receta en el disco?       — el método original
  2. ¿la ALCANZA el consumidor?           — wf-recorder grababa mudo: sus backends de
     audio existían, pero en colas hermanas, que una receta del corpus no ve
  3. ¿la VARIANTE sellada publica la ABI? — imv: mesa existe, es alcanzable, y aun así
     no sirve; las tres variantes no publican libGL.so ni gl.pc

Las tres son la misma equivocación disfrazada: preguntarle al CATÁLOGO lo que sólo
sabe el ARTEFACTO. Este vigía indexa los .pc y las librerías (.a y .so, porque
find_library no mira pkg-config) de todos los artefactos sellados y contesta quién
publica cada nombre y desde qué colas es pedible, aplicando sibling-first: lo del
corpus lo ve todo el mundo, lo de una incoming-* sólo esa cola.

Es el hermano de BUILD de vigia-sonames.py, que cubre la mitad de RUNTIME. La de build
se paga antes: es la que decide si el configure de una receta nueva va a morir.

Tres decisiones de forma que salieron de usarlo y verlo fallar:
- el nombre se busca flojo: gl, gl.pc, libGL.so.1 y librsvg (que tiene que encontrar
  librsvg-2.0.pc) dan lo mismo. El nombre que trae un APKBUILD casi nunca lleva la
  versión, y comparar a lo bruto daba falsos «nadie lo publica».
- agrupado por receta, no por fichero: mesa publica cuatro ficheros de EGL y repetirla
  cuatro veces convierte el informe en ruido justo cuando hay que leerlo rápido.
- --desde <cola> sale con código 1 si algo no se alcanza ⇒ sirve de puerta en cron/CI.

Verificado contra los dos casos conocidos: --desde corpus con lo que swayimg pide da
0, y con lo que imv pedía (gl, opengl) da 1. Barrido completo ~9 s con caché por
ArtifactHash en work/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 11:40:40 +00:00
SergioandClaude Opus 5 e2bcd96feb apps: swayimg sube a 5.5, que es lo que justificaba escribir luajit
Entró como 4.7 —la última sin Lua— sólo porque no había receta de luajit. Con luajit
sellada, sube a la actual. Lo que se gana: el motor de configuración pasó a ser Lua
(src/luaengine.cpp), y con él los perfiles de teclas y el .lua de ejemplo. Formatos:
suma qoi, ttf y xbm a los de 4.7.

Dos cambios de forma que sacan dos deps: `compositor` ya no pide json-c (la
integración con sway se rehizo sin JSON) y la man page viene PRE-GENERADA en
extra/swayimg.1 en vez de armarse con scdoc. -Ddoc=false porque ese target regenera
markdown con dos scripts de Python y lo que instala son .md, no páginas de manual.

Corregido de paso el comentario de las opciones apagadas: son DOS por cola, no una.
`svg` pide librsvg-2.0 (sólo en incoming-gnome) y `exif` pide exiv2 (sólo en
incoming-kde). Las dos existen en el disco y ninguna es alcanzable desde el corpus.
Por eso este visor no muestra metadatos EXIF, y queda escrito dónde está el hueco.

Evidencia de que la cadena cierra, no sólo de que sella: `swayimg -e` ejecuta Lua
dentro del visor y reporta «Lua 5.1 · jit=true · LuaJIT 2.1.1787165859» — que es
exactamente el relver de nuestra receta de luajit, o sea que el intérprete que corre
es el que construimos. Un script con error de sintaxis se reporta como error de Lua.
NEEDED sigue siendo sólo libc.so.

Los cinco grafos quedan en N/N con cero deuda.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 11:37:02 +00:00
SergioandClaude Opus 5 2d68e70a07 recetas: luajit 2.1 (rolling) — el intérprete que swayimg 5.x exige y mpv prefiere
Tercer Lua del catálogo y hay razón para cada uno: `lua` 5.4 para wireplumber,
`lua5.2` para el OSC de mpv, y ésta porque hay consumidores que piden LuaJIT POR
NOMBRE — swayimg >=5.0 hace dependency('luajit') sin alternativa. No es la misma ABI:
LuaJIT es Lua 5.1 + FFI + JIT. Convive sin pisar: todas sus rutas ya vienen
versionadas (include/luajit-2.1, libluajit-5.1.so.2, /usr/bin/luajit-2.1.<relver>).

⚠ EL PIN DE COMMIT NO ALCANZA. Desde que 2.1 es rolling no hay tags: la versión sale
de `git show -s --format=%ct` sobre el árbol, y NO es cosmética — entra en un SÍMBOLO
EXPORTADO (luaJIT_version_2_1_ROLLING). Dos builds del mismo commit con distinto
estado de .git publican símbolos distintos y el consumidor muere con símbolo
indefinido. Se escribe .relver a mano Y se borra .git, porque la rama que consulta git
gana si el directorio existe. Verificado: nm -D da luaJIT_version_2_1_1787165859.

LIBS=-lunwind: la misma deuda que ya paga librsvg. Sin esto, nueve _Unwind_*
indefinidos desde lj_err.o — LuaJIT usa unwinding DWARF de verdad y en glibc ese
runtime vive en libgcc_s, que musl no tiene. Va por LIBS= y no LDFLAGS= porque
TARGET_ALIBS se expande DESPUÉS de los objetos, que es donde el linker resuelve.

TARGET_STRIP=true: su Makefile strippea como parte de la compilación, lo que pediría
binutils y borraría el .debug_* antes de que hammer lo parta (SDD 23).

Evidencia de que anda: NEEDED = libc.so y nada más; jit.status() da true con todas las
optimizaciones; FFI llama a C; y los cuatro caminos de unwinding responden — pcall,
error DENTRO de un trace compilado (err_unwind_jit, el que pedía _Unwind_GetIP),
metamétodo y xpcall+traceback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 11:33:08 +00:00
Sergio 26ecb1fa0c estado: cosecha granja 2026-09-03T11:31:19Z — avance del árbol KDE 2026-09-03 11:31:19 +00:00
SergioandClaude Opus 5 d8dd779812 apps: swayimg 4.7 sellada, visor de imágenes en las cuatro imágenes
El plan proponía `imv` como paso 4. Al medirla apareció un muro que el método de
triaje no podía ver: imv dibuja con OpenGL de función fija (glBegin/glOrtho) y esta
distro NO tiene proveedor de GL de escritorio — las tres variantes de mesa van
-Dglx=disabled -Dglvnd=false y no publican libGL.so ni gl.pc ni opengl.pc, y no hay
receta libglvnd. La trampa fina: mesa SÍ instala GL/gl.h, así que compila entero y
muere al ligar. Darlo vuelta = autorar libglvnd + rehacer mesa (radio 139).

swayimg cubre el mismo caso de uso sin tocar GL (rasteriza a wl_shm) y con CERO
recetas nuevas: sus deps requeridas ya estaban en el corpus. De yapa trae backend DRM
(imágenes en TTY pelada, sin compositor).

Pineada 4.7 y no 5.5 a propósito: desde la 5.0 luajit es obligatoria y no hay receta
(la lua5.2 del OSC de mpv es otra ABI). Escribir luajit es el próximo paso barato.

-Dversion=4.7 no es cosmético: su default 0.0.0 dispara un `git describe` sobre el
árbol ⇒ la versión estampada en el binario dependería del checkout. Misma familia que
el -Dbuild-date de mpv.

Evidencia de que anda, no sólo de que sella: NEEDED = libc.so y nada más (cero glibc,
cero X11, cero GL), `--version` dice 4.7 con jpeg/png/gif/webp/tiff, prueba las dos
UIs (Wayland → DRM) y el bucle de decodificación rechaza lo que no es imagen.

Añadida a las raíces de los cuatro perfiles (sellado ≠ instalado). Los cinco grafos
quedan en N/N con cero deuda.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XB2iEmxeLZgzqNfhWChrLo
2026-09-03 11:28:19 +00:00
Sergio d54222e7f9 estado: cosecha granja 2026-09-03T11:01:23Z — avance del árbol KDE 2026-09-03 11:01:23 +00:00
Sergio 6f9fcc9e34 estado: cosecha granja 2026-09-03T10:31:32Z — avance del árbol KDE 2026-09-03 10:31:32 +00:00
Sergio b1eb2c4415 estado: cosecha granja 2026-09-03T10:01:15Z — avance del árbol KDE 2026-09-03 10:01:15 +00:00
Sergio b8c45514f8 estado: cosecha granja 2026-09-03T09:31:10Z — avance del árbol KDE 2026-09-03 09:31:10 +00:00
Sergio ae4af616ee estado: cosecha granja 2026-09-03T09:01:09Z — avance del árbol KDE 2026-09-03 09:01:09 +00:00
Sergio b37684b764 estado: cosecha granja 2026-09-03T08:31:15Z — avance del árbol KDE 2026-09-03 08:31:15 +00:00
Sergio 7a4967d021 estado: cosecha granja 2026-09-03T08:01:09Z — avance del árbol KDE 2026-09-03 08:01:09 +00:00
Sergio d3fa729698 estado: COSMIC vuelve a 106/106 — el portal selló al 4º intento, con el zram en 12 G
`xdg-desktop-portal-cosmic` b3:9e6d432b, 9 ficheros / 38 M. Los cuatro perfiles cierran otra vez:
KDE 188/188, GNOME 132/132, COSMIC 106/106, sway 146/146. CERO deuda.

No era la receta, era la máquina, y ahora hay el número que lo prueba. Los tres intentos anteriores
murieron por OOM entre los 24 y los 25 minutos con el zram de 4 GiB lleno al 90%. Éste tardó **48
minutos** y su pico de swap fue **5,4 GiB** — o sea que necesitaba 1,4 GiB MÁS de los que el swap
entero podía darle. Con 4 GiB no había paciencia que alcanzara.

CÓMO SE PASÓ DE 4 A 12 GiB, y por qué no fue con `swap-zram 12`. Su freno se negaba con razón:
había 3.694 M adentro contra 3.283 M de RAM disponible, y ese guardián existe porque un `swapoff`
sin destino es como se volteó la máquina el 2026-08-17. Esperar tampoco servía: el zram no se drena
solo, sus páginas son de procesos vivos. Se hizo AÑADIENDO ANTES DE QUITAR, que es la forma estándar
de redimensionar swap sin el pico:

  1. zramctl --find --size 12G --algorithm zstd            → /dev/zram1
  2. verificar `backing_dev: none`  ← el único agujero de zram contra T9 de qullqa; con un
     backing_dev las páginas salen EN CLARO a disco. Se comprobó a mano por no pasar por el script.
  3. swapon --priority 110 /dev/zram1                       ← destino disponible antes de quitar
  4. swapoff /dev/zram0                                     ← migra, no choca contra la pared
  5. zramctl --reset /dev/zram0

El 12 no es a ojo: sale de la compresión MEDIDA acá (`mm_stat` daba orig=3,7G compr=1,3G ⇒ 3,0x con
zstd), así que 12 GiB de páginas viven en ~4 GiB de RAM. 12 sobre 7,6 de RAM es 1,6x.

⚠ NO se puso un swapfile en disco, ni en claro ni cifrado, y no es prudencia: T9 del SDD de qullqa
dice que «un `swapon` sin cifrar anula todo el documento», y la variante cifrada (dm-crypt sobre
loop) congeló la máquina 70 minutos el 2026-08-17. Queda escrito en `/etc/conf.d/zram-swap`, que
además hace persistente el tamaño (`ZRAM_TAM_GIB=12`) por el override que el propio init prevé.
2026-09-03 07:51:46 +00:00
Sergio 3d1506712d estado: cosecha granja 2026-09-03T07:32:38Z — avance del árbol KDE 2026-09-03 07:32:39 +00:00
Sergio 5bad7d684d estado: cosecha granja 2026-09-03T07:01:18Z — avance del árbol KDE 2026-09-03 07:01:18 +00:00
Sergio 5d86c9de64 estado: cosecha granja 2026-09-03T06:31:13Z — avance del árbol KDE 2026-09-03 06:31:13 +00:00
Sergio 3b305286d5 plan de apps: la decisión de audio queda tomada — opción 1, con la salvedad de pulseaudio
Se cierra la pregunta abierta de anoche. Ganó promover UNA pipewire al corpus, y el documento pasa
a registrar POR QUÉ salió barata (la variante de cosmic ya resolvía todo contra el padre) y por qué
pulseaudio quedó afuera (45.712 vs 2.455.600 bytes de `libpulse-mainloop-glib`: la del corpus se
traga la glib estática).

Queda escrita la regla que decide la próxima promoción: enlazar contra una variante y correr contra
otra es legítimo cuando lo que cruza es un SONAME; lo que no se puede es hidratar dos artefactos
distintos en la misma ruta.
2026-09-03 06:18:22 +00:00
Sergio 39f5007cff wf-recorder 0.6.0: la primera app que cobra la promoción de pipewire
Sellada b3:25caa359 al primer intento. `--help` corre y los NEEDED traen `libpipewire-0.3.so.0`:
graba CON audio, que era exactamente lo que anoche no se podía.

Es la app que estaba bloqueada por el defecto del triaje —preguntaba «¿existe la receta?» en vez de
«¿la alcanza quien la usa?»—: sus dos backends de captura de sonido vivían sólo en colas y una
receta del corpus no alcanza una cola hermana. Con pipewire en el corpus, cae sola.

`-Ddefault_audio_backend=pipewire` explícito y no `auto`: su meson resuelve `auto` mirando qué
encontró en el sandbox (meson.build:95-101), o sea que el artefacto dependería de lo que quedó
montado en el lab. Misma disciplina que en mpv y ffmpeg.

⚠ SE LISTA SÓLO EN `escritorio-sway`, y la razón es de PROTOCOLO: captura por `wlr-screencopy`, que
implementan los compositores wlroots. KWin y mutter NO lo implementan —usan el portal/ScreenCast—,
así que meterlo en esas dos imágenes sería enviar una herramienta que no puede funcionar ahí: deuda
fantasma con forma de app. En COSMIC hay que verificar si cosmic-comp expone el protocolo antes de
listarlo; no se listó a ciegas.

Cubre el caso de uso principal por el que uno instala OBS, con UNA receta en vez de las 20 + Qt6 +
X11 que OBS pedía. escritorio-sway 146/146.
2026-09-03 06:17:57 +00:00
Sergio e08add24be pipewire al corpus: mpv habla PipeWire nativo, y pulseaudio NO se promueve (medido)
Cierra la decisión que quedó abierta anoche en `docs/plan-apps-usuario-final.md`: el corpus no
tenía ningún cliente de audio salvo ALSA, y eso bloqueaba a toda app multimedia.

GANA LA VARIANTE DE COSMIC y la promoción es GRATIS: era la única cuyo cierre ya resolvía entero
contra el catálogo padre, así que `recipes/pipewire.toml` sella b3:e240653a — el mismo hash que
tenía en la cola. Con ella suben `libsndfile` (las tres copias sellaban idéntico) y `dbus-shared`
(gnome y cosmic, idénticas). Se jubilan 9 copias de cola.

POR QUÉ ESTO NO REPITE EL EPISODIO DE LAS DOS GLIB — leído de los artefactos sellados, no razonado:
**ningún pipewire de los tres enlaza glib.** Los NEEDED de las tres variantes son exactamente
`libdbus-1.so.3`, `libpulse.so.0` y `libsndfile.so.1`; glib entraba sólo como Requires transitivo
de pkg-config, en tiempo de build. ⇒ `libpipewire-0.3.so` es glib-free y se comparte entre las
cuatro imágenes sin riesgo.

⚠ Y POR ESO `pulseaudio` NO SE PROMUEVE, aunque sea la dep de al lado. Ahí la objeción SÍ aplica y
el número lo dice solo: `libpulse-mainloop-glib.so.0.0.6` mide **45.712 bytes** en la variante de
GNOME (NEEDED `libglib-2.0.so.0`) y **2.455.600** en la del corpus, que se traga la glib ESTÁTICA
del catálogo. Meter ésa en el proceso de gnome-shell —que ya carga la glib sombra dinámica— es
literalmente el cuadro de colord. `incoming-kde/pulseaudio` e `incoming-gnome/pulseaudio` se quedan
donde están: son variantes deliberadas, no duplicados. La copia del corpus existe sólo para que
pipewire tenga contra qué enlazar `libpulse.so.0`, y ese enlace es por SONAME — en runtime lo sirve
la libpulse de cada imagen, ABI-compatible (misma 0.24.3).

Enlazar contra una variante y correr contra otra es legítimo cuando lo que cruza es un SONAME; lo
que NO se puede es hidratar dos artefactos distintos en la misma ruta. Esa distinción es la que
decide qué se promueve y qué no.

mpv (b3:392e9690) pasa a `-Dpipewire=enabled`, con ALSA encendida detrás: prueba pipewire primero y
cae a alsa si no hay demonio —KDE todavía no lista PipeWire entre sus raíces—. Evidencia:
  NEEDED … libasound.so.2 **libpipewire-0.3.so.0** libEGL.so.1 libc.so
  --ao=help → pipewire (PipeWire audio output), alsa, null

Impacto medido antes de tocar, hasheando las 336 recetas de cola y comparando: 9 retiradas, **5
rebuilds** (wireplumber, xdg-desktop-portal ×2, kpipewire, spectacle) — los 5 sellados. Sin la
salvedad de pulseaudio habrían sido 8, incluido gnome-shell.

Perfiles: KDE 188/188, GNOME 132/132, sway 145/145.
⚠ COSMIC 105/106: `xdg-desktop-portal-cosmic` murió por OOM por TERCERA vez (7 G de RAM sin cgroups,
con otro agente compilando Rust). Sigue siendo la máquina, no la receta.
2026-09-03 06:16:05 +00:00
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