Commit Graph
260 Commits
Author SHA1 Message Date
sergioandClaude Opus 5 98608524dd cosmic: COSMIC_MODE viaja como fichero, no como variable
El getty ejecuta cosmic-start sin ambiente heredable, así que probar el modo
session exigía editar el script. Ahora la imagen hornea /etc/cosmic-mode, mismo
truco que /etc/gnome-mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 13:01:06 -04:00
sergioandClaude Opus 5 78899549fe cosmic: el wallpaper por defecto vive en git-lfs — fondo de color liso en la imagen de prueba
El default de cosmic-bg apunta a una imagen del repo cosmic-wallpapers, cuyo
tarball de GitHub pesa 20 KB: son punteros LFS, no imágenes. Una receta escrita
del modo normal produciría un paquete de ficheros de texto que además PARECERÍA
correcto — nombre bueno, ruta buena.

Mientras tanto cosmic-start escribe la config de usuario con una fuente Color,
que cosmic-bg soporta de fábrica. Va en el lanzador y no en el artefacto porque
es política de esta imagen de prueba, no algo que el paquete prometa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:50:50 -04:00
sergioandClaude Opus 5 ef4b18bc88 🖥 cosmic: COMPONE EN QEMU — cosmic-comp presenta frames sobre el kernel de hammer
Pantalla completa en el gris de COSMIC (39,41,42) con el cursor dibujado, 134
colores distintos, validado CON PANTALLA (DISP=gtk + screendump).

Entra el andamiaje completo: hydrate-cosmic.sh (44 recetas, 0 faltantes),
qemu-desktop-image.sh y cosmic-start-qemu.sh.

Tres causas medidas en tres arranques, ninguna donde uno la busca:

1. cosmic-settings-daemon NO es opcional: cosmic-session lo lanza con .expect()
   (main.rs:255) y panickea si falta. Corrige lo que este runbook afirmaba. De
   ahi el modo bare del lanzador, que separa como compone de como arranca la
   sesion.
2. La pantalla negra era libz.so.1: kms_swrast_dri.so lo NEEDea y el corpus solo
   traia zlib estatica. Sin driver de software no hay GBM y el compositor arranca
   igual, negro. Causa a tres capas del sintoma.
3. Los clientes no pueden ser estaticos: wayland-client entra con la feature
   dlopen y un musl estatico no tiene dlopen funcional — moria 'The wayland
   library could not be loaded' CON el .so presente.

Y dos carreras con /run (dbus y XDG_RUNTIME_DIR) de la misma forma que el bug de
PolicyKit1 de hoy en GNOME: comprobar al principio y usar al final. Se crea justo
antes de usar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:48:44 -04:00
sergioandClaude Opus 5 98a8152f49 cosmic: hidratador del cierre de runtime — 35 recetas, 0 faltantes
Hermano de hydrate-gnome.sh y con su misma propiedad: el cierre sale del GRAFO
REAL de recetas y el artefacto se elige por 'hammer hash' (la receta de HOY), no
por el más reciente del store, que con dos artefactos del mismo paquete miente.

Las raíces se nombran a mano porque en COSMIC la brecha entre cierre de build y
cierre de runtime es enorme: cosmic-session lanza a sus componentes por PATH y
por nombre, así que ninguno es dep de build de otro y el grafo no los ve. La
lista autoritativa es cosmic-session/src/main.rs.

Resultado: 207 binarios, 109 .so, y los siete NEEDED de cosmic-comp resueltos
dentro del propio cierre. Falta sólo el loader musl, que lo pone la base metal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:25:25 -04:00
sergioandClaude Opus 5 c0055331c7 gnome: esperar el NOMBRE de PolicyKit1 antes de lanzar a sus clientes
accounts-daemon y upowerd morían al arrancar: el shim de polkit se lanza en
background y tarda ~2s en adquirir org.freedesktop.PolicyKit1, ellos llamaban a
polkit_authority_get_sync() en ese hueco, D-Bus intentaba ACTIVAR el servicio y
fallaba con 'Failed to execute program ... Permission denied'.

Lo peor era el informe: los tres esperar_nombre estaban al final, así que para
cuando corrían el shim ya tenía el nombre y el resumen daba 'PolicyKit1 OK'
sobre dos clientes ya muertos. Un chequeo posterior a la carrera no la mide.

esperar_nombre pasa a definirse antes del primer daemon y el bloque de polkit
espera su propio nombre antes de que arranque ningún cliente suyo. Lanzar en
orden no es estar listo en orden.

De paso queda cerrado el ítem 4 del runbook: validación CON PANTALLA (DISP=gtk)
del Overview con el PID1 sellado nuevo — 689 colores distintos, con evidencia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 10:05:29 -04:00
sergioandClaude Opus 5 84e088fd13 imágenes: PID1 sale del artefacto sellado, no del directorio base congelado
Las 4 imágenes de escritorio (GNOME + los 3 de KDE: qemu, metal, dual) fundían `work/metal-rootfs`
por hardlinks y se llevaban SU arje-zero, que era del 2026-06-19: ese directorio se armó a mano en
la campaña de metal y nadie lo regeneraba al re-sellar la receta. Resultado: todas arrancaban con un
PID1 de hace mes y medio, en silencio.

No es hipotético — se comió un fix real. El `identity mismatch` del bus del fractal estaba arreglado
río arriba y la VM seguía imprimiendo el mensaje viejo porque el binario de la imagen no venía de la
receta (ver el comentario de recipes/arje-zero.toml).

El síntoma engaña porque el directorio base es LEGÍTIMO para todo lo demás —busybox, firmware, el
kernel EFI-stub, la estructura de /etc—: sólo la pieza que TAMBIÉN es receta se queda atrás, y justo
esa es PID1. La regla que queda escrita en el helper: si algo del rootfs tiene receta, la imagen lo
toma del artefacto sellado, no de la copia congelada.

El helper va en scripts/lib/ y no inline ×4 a propósito: el porqué es largo y vale una sola copia.
Elige el artefacto por `hammer hash` (el de la receta de HOY, no el más nuevo por fecha, que miente
en cuanto conviven dos) y ABORTA si falta, porque seguir con el PID1 congelado es exactamente el
modo de falla que cierra.

Probado: las 4 pasan `bash -n` y la imagen GNOME lo ejecuta bien sourceado (`✓ e570c1482432 (el de
work/metal-rootfs era de 2026-06-19)`), con la VM ya validada booteando ese PID1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 19:52:00 -04:00
sergio 7190cc48a3 estado: cosecha granja 2026-07-30T23:20:59Z — avance del árbol KDE 2026-07-30 19:20:59 -04:00
sergioandClaude Opus 5 2430528c16 dbus: las políticas de login1 y PolicyKit1 viajan en su ARTEFACTO, no en el script de imagen
Cierra un TODO que el propio script tenía escrito («esto pertenece al artefacto de
arje-logind-compat, no a la imagen») y que estaba bien puesto: **un servicio que no puede
adueñarse de su nombre no es un servicio**, así que su política D-Bus es parte de lo que el
paquete promete, igual que el binario. Con la política en el script de UNA imagen,
cualquier otra —metal, KDE, mirada— se llevaba el daemon y no podía usarlo.

arje-logind-compat b3:b4795829 · arje-polkit-compat b3:6c82f44b. Radio de las dos: 0.

**Y la comprobación que reemplaza a escribirlas atrapó un bug de verdad en el primer
intento.** Mover algo a un artefacto sólo es una mejora si se NOTA cuando falta, así que el
script pasó de ESCRIBIR los .conf a EXIGIRLOS. Falló al toque: `✗ falta
org.freedesktop.login1.conf`. Causa — el script inyectaba **sólo el binario** del artefacto
(`install -Dm755 .../usr/bin/...`), así que el .conf existía en el store y nunca llegaba a
la imagen. Sin esa comprobación habría sido el fallo tardío de siempre: daemon que arranca,
no adquiere el nombre, y 25s de espera por servicio sin decir por qué.

Verificado de punta a punta con las políticas viniendo del artefacto: los cuatro nombres
del bus arriba (login1, PolicyKit1, Accounts, UPower) y el audio intacto.

El `zz-` de la de polkit no es decorativo y queda explicado en su receta: a diferencia de
login1, ese nombre YA tiene política —la trae el artefacto de polkit— y permite `own` sólo
al usuario polkitd; dbus lee system.d en orden alfabético y las reglas posteriores ganan, así
que un fichero que ordene después AÑADE el permiso sin descartar el resto de upstream.

ColorManager sigue sin aparecer: es lo de colord, ya diagnosticado hasta dónde llega y
fuera del alcance de este cambio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 17:18:42 -04:00
sergioandClaude Opus 5 beb2851d99 colord: el demonio ya se construye (b3:8100fa39) — arranca, abre sus DB y NO adquiere el nombre
La receta iba con `-Ddaemon=false` y el razonamiento era «mutter enlaza libcolord, no
necesita el demonio». Cierto para COMPILAR mutter y falso para el escritorio andando: el
artefacto INSTALA el `.service` de activación (`Exec=/usr/libexec/colord`) y ese binario no
existía, así que cada arranque se comía 25s de timeout. **No era «colord apagado» sino
colord roto de forma lenta** — el peor de los dos, porque no falla, tarda.

Su propio comentario marcaba la condición de vuelta: «si alguna vez hace falta el demonio
de verdad, vuelve con polkit encima». Polkit ya está (arje-polkit-compat). Y medido: prender
el demonio **no agrega una sola dep nueva** salvo polkit-gobject-1, ya sellada — el bloque
de dependency() de colord 1.4.7 es de nivel superior y pedía gusb/gudev/libudev igual con
daemon=false. El coste estaba pagado desde el 2026-07-27 sin que nadie lo cobrara.

**PERO EL TIMEOUT SIGUE**, y lo que aprendí es dónde NO está:
- El binario existe y arranca: `/usr/libexec/colord` crea sus tres bases en
  /var/lib/colord (mapping.db, storage.db) y lo dice en el log.
- Con `--verbose` NO hay una línea más después de abrir la tercera base.
- La política D-Bus SÍ permite `own` a root, y el `.service` corre como root: no es el caso
  de polkit (donde `own` estaba restringido al usuario `polkitd`).
- Los cuatro `cd_main_load_introspection` que van entre las DB y `g_bus_own_name`
  (cd-main.c:2434-2460) leen de un **GResource compilado en el binario**, no de disco, así
  que no pueden faltar. Los XML instalados en /usr/share/dbus-1/interfaces son para otros.

⇒ Queda entre `cd_main_load_introspection` y `g_main_loop_run`, y el siguiente dato es si el
proceso sigue vivo en ese momento. **Esa comprobación me faltaba en el script** — reportaba
«colord lanzado (pid N)» sin verificar nada, que es exactamente el error que yo mismo había
señalado para upowerd («un pid no es un servicio») y no apliqué acá. Ya está puesta: sin
ella, «no apareció el nombre» no distingue MURIÓ de SE COLGÓ, y son dos investigaciones
distintas.

Verificado que no hay regresión en lo que sí funciona: audio (48. HDA Intel, sink y source
reales) y vídeo (0 page-flips fallidos) siguen bien con el demonio en la imagen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 04:13:40 -04:00
sergioandClaude Opus 5 888fdc31c7 firmware: SOF para TigerLake — el DSP de audio, pineado como el de i915
El kernel ya traía el driver (SND_SOC_SOF_TIGERLAKE); faltaban los blobs, y sin ellos SOF
carga y falla: en el laptop no hay sonido aunque ALSA esté. Mismo patrón que i915 con su
DMC/GuC — driver desde fuente, firmware pineado por contenido, porque Intel no libera el
código y la soberanía build-from-source no aplica a datos fijos.

Tres piezas, cada una por un motivo distinto:
- `sof-tgl.ri` + `sof-tgl-h.ri`: el firmware del DSP. Se copian LOS DOS porque el driver
  elige según el SKU y no quiero hornear la suposición. **`cp -L` es obligatorio**: en
  linux-firmware son SYMLINKS a `intel-signed/`, y un symlink relativo copiado a otro
  árbol apunta a la nada. Verificado: los tres quedan ficheros reales (525K/447K/95K).
- `sof-tgl.ldc`: el diccionario de logs del DSP. No es opcional en la práctica — el
  driver lo pide al inicializar y sin él el firmware arranca SIN TRAZA, o sea que el día
  que algo falle no hay por dónde mirar.
- 47 topologías (`sof-hda-generic-*`, `sof-tgl-*`): describen el grafo de audio. El driver
  pide UNA por nombre derivado de la máquina, y no se puede saber cuál sin el hardware
  delante. Copiar de más cuesta megas; copiar de menos cuesta un viaje físico al laptop
  para descubrir qué nombre pidió.

~3 MB en total. Si no hay SOF en el origen, AVISA y NO falla: el audio del laptop se
pierde pero la imagen sigue booteando y en QEMU suena por HDA legacy — fallar bloquearía
builds que no necesitan SOF.

Verificado que no hay regresión: con los blobs en la imagen, el audio de la VM sigue
funcionando por la ruta HDA (48. HDA Intel [alsa], sink y source reales) y cero page-flips
fallidos. **El camino SOF en sí queda SIN VALIDAR hasta el próximo viaje al metal** — en
QEMU no se ejerce, y eso está escrito en la receta del kernel en vez de dado por bueno.

De paso, la cabecera del script deja de mentir: se llama «firmware» y no «wifi» porque
copia las TRES familias de blobs que el metal necesita (iwlwifi, i915, SOF).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 03:04:03 -04:00
sergioandClaude Opus 5 8c3bd323d5 audio: ALSA encendido en el kernel — y el muro real es libudev-zero, no el stack de audio
ALSA =y en linux-metal (b3:613bca15): HDA legacy + codecs (lo que emula QEMU y usan las
máquinas pre-SOF), SOF para TigerLake (el laptop del target NO suena por la ruta legacy:
Intel movió el audio al DSP) y SND_USB_AUDIO. Todo =y porque el producto es monolítico.
⚠ Falta inyectar el FIRMWARE de SOF en la imagen (intel/sof/*.ri, sof-tplg/*.tplg), igual
que metal-firmware.sh hace con los tgl_* de i915: sin blobs el driver carga y falla.

FUNCIONÓ LA MITAD: en la VM `/dev/snd` ya trae controlC0 + pcmC0D0p/c y /sys/class/sound
lista card0. El kernel detecta la tarjeta. Pero wpctl sigue con `Devices:` vacío.

**Y LA CAUSA ES ESTRUCTURAL, no una propiedad que falte.** `get_card_nr` de SPA
(alsa-udev.c:178-183) sólo acepta el device CARD —`/sys/.../sound/card0`—, que es un
device de CLASE: sin major:minor, sin nodo en /dev. Y `udev_enumerate_scan_devices` de
libudev-zero recorre EXCLUSIVAMENTE /sys/dev/block y /sys/dev/char (udev_enumerate.c:281),
o sea sólo devices CON nodo; el udev real enumera /sys/class entero. **El device que SPA
necesita nunca entra en la enumeración**: no hay guarda que sacar, falta la mitad del
espacio de búsqueda. Salidas: (a) enseñarle a libudev-zero a recorrer /sys/class —radio 51
sellados— o (b) empaquetar eudev. Es decisión de arquitectura, no parche.

CORRIJO ALGO QUE ESCRIBÍ ANTES: el parche `pipewire-sound-initialized.patch` se hizo
creyendo que SOUND_INITIALIZED era el bloqueo, y NO lo era — con el parche aplicado el
resultado no cambió. Se conserva porque la incompatibilidad que describe es real (sin
udevd nadie pone esa propiedad y SPA descartaría toda tarjeta en cuanto la enumeración
funcione), pero su prosa ahora empieza avisando que no es la solución. Sacarlo del camino
importa: dejarlo presentado como el arreglo mandaría al próximo a buscar en el lugar
equivocado.

Dos arreglos de andamiaje que costaron un ciclo cada uno:
- **`-nic none` por defecto.** Agregar la tarjeta de sonido CORRIÓ LAS RUTAS PCI, la entrada
  de disco cacheada en las VARS de OVMF dejó de coincidir, y la firmware se fue al orden por
  defecto: PXE v4, PXE v6, HTTP… minutos de timeouts. Con un TIMEOUT corto eso se lee como
  «la VM no arrancó» y el serial sólo dice `PXE-E16: No valid offer received`.
- El filtro del log de wireplumber era `alsa|udev|sound|card|monitor` con `tail -40`, y se
  llenó de bluez/v4l2/libcamera —tres monitores que avisan que su plugin no está— dejando
  fuera justo las líneas de ALSA. **Un filtro ancho con cola corta es peor que ninguno.**
  Ahora filtra `alsa` y además copia el log entero a la raíz ext4 (tmpfs muere con la VM).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:59:18 -04:00
sergioandClaude Opus 5 51c6830f58 audio: wireplumber (b3:39a9a06c) + lua (b3:e6c35f98) — y el Devices vacío es del KERNEL
Cierra el gestor de sesión de PipeWire, que es lo que separa «PipeWire acepta clientes»
de «PipeWire tiene dispositivos». Su política está escrita en Lua, así que arrastró una
receta de Lua que hubo que autorar entera.

LO QUE LA RECETA DE LUA TIENE QUE INVENTAR: el Makefile de upstream sólo produce
liblua.a y los binarios — **no hay regla de .so ni fichero pkg-config**, los agrega cada
distro. Acá la compartida se enlaza a mano desde la .a con --whole-archive (una estática
sólo aporta lo referenciado y hay que llevarse todo) y el .pc se escribe con los TRES
nombres que se usan por ahí, porque wireplumber prueba lua-5.4, lua5.4 y lua54 en orden.
`-fPIC` no es opcional: libwplua es un objeto compartido.

Dos símbolos más en libelogind (tawasuyu 087230054 → re-pineado 749edfe41):
sd_uid_get_seats y sd_uid_get_state, que module-logind.c de wireplumber usa para saber si
el usuario está en un asiento antes de tomar los dispositivos. Van 28 símbolos sd-*.

**EL `Devices:` VACÍO DE wpctl NO ES UN FALLO DE wireplumber: el kernel no tiene ALSA.**
recipes/linux-metal.toml:111 lo apaga explícito (`-d SOUND -d SND`), así que no hay
/dev/snd que enumerar. Y esto NO se dio por supuesto: se agregó una ich9-intel-hda
emulada a QEMU (AUDIO=1, ahora el default de run-qemu-desktop.sh) justo porque un
«Devices: vacío» se lee IGUAL si wireplumber funciona sobre una VM sin hardware que si no
funciona. Con tarjeta y sin ALSA en el kernel, el resultado no cambia — la ambigüedad
queda resuelta. Encender el sonido en la distro es una decisión con costo (re-sellar el
kernel, rehacer la imagen metal) y queda a la vista en vez de escondida en un default.

Bug propio destapado en el camino: el bloque de wireplumber quedó DUPLICADO en
gnome-start y había DOS wireplumber peleándose el grafo. El síntoma no era un error sino
una lista de clientes que se lee normal si no se cuenta: dos «WirePlumber» con pids
distintos en wpctl status.

Y una nota de ADR 0012 que costó un rato: la cascada de libelogind cortó a mitad y dejó
el `output/` de mutter a medio hacer; el reintento moría con «error opening
'...c.o.d': No such file or directory». Un `rm -rf output` en el configure NO alcanzó
—el árbol tenía estado viejo más allá del build dir—; lo que lo arregló fue BORRAR
work/sources/mutter-* para que el fetch lo re-extraiga limpio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 20:40:12 -04:00
sergioandClaude Opus 5 5faa259603 audio: PipeWire es el servidor de la distro (decisión del usuario) — gvc conecta
Cierra una pregunta que estaba explícitamente abierta en varias recetas, y que es la
razón por la que `pulseaudio` se había construido con `-Ddaemon=false`: sólo el cliente,
para que gvc hablara el protocolo sin cerrar la decisión de prestado.

**Los dos conviven y por eso la decisión no rompe nada**: pipewire va con
`-Dlibpulse=enabled` ⇒ `pipewire-pulse`, un servidor que habla el protocolo de
PulseAudio. gnome-shell usa libpulse sin enterarse de qué hay del otro lado. No hubo que
tocar una línea del cliente.

  == gnome-qemu :: socket pipewire-0 OK
  == gnome-qemu :: socket pulse/native OK — gvc va a poder conectar
  Gvc-DEBUG: Updating client: index=32 name='pipewire'
  Gvc-DEBUG: Updating sink: index=33 name='auto_null' description='Dummy Output'

El `Failed to connect context: Connection refused` desapareció. El sink es auto_null, que
es la respuesta honesta: la VM no tiene tarjeta de sonido.

**⚠ Falta wireplumber** (gestor de sesión, Lua + su cierre). Sin él PipeWire acepta
clientes pero NO enumera ni enruta dispositivos. «El shell conecta» ≠ «hay sonido», y el
log de arriba es justo el caso donde confundirlos sería fácil. Queda escrito en la receta
y en el runbook.

GOTCHA MEDIDO: ya existía incoming-kde/pipewire.toml sellada y NO se puede reusar
hidratándola acá — los dos cierres traen glib y **no son la misma glib**:
incoming-kde/glib-shared y incoming-gnome/glib producen libglib-2.0.so.0.8800.1 con bytes
distintos (verificado con cmp) y comparten 370 rutas. Proyectar las dos deja que una gane
por orden de proyección ⇒ dos registros de GType en un proceso, el cuadro que costó el
episodio de colord. De ahí la copia en la cola GNOME (glib-shared→glib, dbus→dbus-shared,
pcre2-shared→pcre2), que sí re-construye. Lo que FUE gratis: alsa-lib, cuya única dep es
pkgconf y resuelve al catálogo PADRE ⇒ hash idéntico y cache hit (b3:93cae411).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 19:59:30 -04:00
sergioandClaude Opus 5 3d01d5cb04 gnome: el bus de sistema COMPLETO — faltaba polkit, y lo dijo la lista de nombres
bus: org.freedesktop.login1      OK
  bus: org.freedesktop.PolicyKit1  OK
  bus: org.freedesktop.Accounts    OK
  bus: org.freedesktop.UPower      OK

accounts-daemon y upowerd arrancaban, seguían vivos, y NUNCA adquirían su nombre: los
dos se bloquean en `polkit_authority_get_sync()` al iniciar, y `org.freedesktop.PolicyKit1`
no lo servía nadie —nuestra receta polkit es libs-only a propósito, porque el demonio lo
pone arje—. gnome-shell esperaba después 25s por cada uno.

**El dato que lo resolvió no salió del log de los daemons sino de la LISTA DE NOMBRES DEL
BUS**: ahí estaban como conexiones anónimas `:1.0`, `:1.1`, sin nombre bien conocido. Eso
distingue tres cosas que en el log del cliente se ven igual — «no arrancó», «arrancó y no
llegó a pedir el nombre» y «lo pidió y se lo negaron». Era la segunda. `gnome-start` ahora
espera cada nombre con NameHasOwner y, si no aparece, vuelca el log del daemon y la lista.

Receta nueva `arje-polkit-compat` (b3:03e0a86f), tercer shim de este tipo tras
arje-logind-compat y arje-sdlogin-compat. Su propio autor ya había escrito el diagnóstico
en el crate: «apps que usan polkit bloquean en CheckAuthorization si no responde nadie».
⚠ AUTORIZA TODO — es la postura de sistema confiado que arje ya tenía tomada, no algo que
esta receta introduzca; queda explícito en su comentario.

Dos gotchas del empaquetado:
- **La política de polkit ya existe y NO alcanza**: permite `own` sólo al usuario polkitd
  y el shim corre como root. Se agrega un `zz-arje-polkit-compat.conf` en vez de reescribir
  la de upstream — dbus lee system.d en orden alfabético y las posteriores ganan.
- **Los ficheros del rootfs fundido son hardlinks de SÓLO LECTURA del store** (0444). Un
  `cat >` encima falla con Permission denied y rompió un build. Regla: nunca sobrescribir
  un fichero que venga de un artefacto; agregar al lado.

Queda escrito en el runbook lo que sigue faltando (colord, y que el shell no tiene servidor
de sonido porque pulseaudio se construyó sólo-cliente: el demonio de audio de la distro
sigue sin decidirse) y las dos lecciones de método que costaron una iteración cada una.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 19:19:23 -04:00
sergioandClaude Opus 5 3999b421da gnome: cursor visible, iconos y el setuid del launch-helper de D-Bus
adwaita-icon-theme b3:429aef43 + hicolor-icon-theme. No es cosmética: con el cursor por
SOFTWARE —obligatorio en virtio-gpu— mutter dibuja la imagen que le da el tema, y sin
tema el puntero se movía INVISIBLE. Ahora se ve en la captura, y la notificación del
shell tiene su icono. Colores distintos 582 → 618.

hicolor arrastrada por `Inherits=hicolor` del index.theme de Adwaita: la cadena de
fallback tiene que terminar en algo que sea un TEMA (con su index.theme), no sólo un
directorio. No trae un solo icono propio — es el contrato, no el contenido.

Dos gotchas medidos:
- **hicolor 0.18 pasó de autotools a meson**: su tarball ya no trae `configure` (127).
- adwaita declara `gtk-update-icon-cache` como `required: true` para un
  `add_install_script` que **su propio autor marcó `skip_if_destdir: true`** — exige un
  binario que en un build empaquetado no puede ejecutar. Se borran los dos bloques.
  `required: false` NO alcanza: meson no propaga el disabler dentro de
  add_install_script y revienta con «Unhandled python exception». Se descartó declarar
  gtk4 como dep: acoplaría el hash de un paquete de DATOS al del toolkit.

**El setuid del launch-helper es la causa raíz de dos síntomas, no de uno.** El
`dbus-daemon-launch-helper` comprueba sus propios permisos antes de activar nada y se
niega si no es setuid root — de ahí el «The permission of the setuid helper is not
correct» de colord. Sin eso NINGUNA activación por bus de sistema funciona, que es
también por qué UPower timeouteaba a los 25s. La imagen ahora lo deja root:messagebus 4750.

Y `gnome-start` lanza upowerd, pero **verificando**: la primera versión decía «lanzado»
y el shell seguía esperando 25s. Un pid no es un servicio. Ahora comprueba que siga vivo
y, si murió, vuelca su log. Además crea los directorios de estado que upower deriva de
--prefix y sin los cuales se muere en silencio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 18:59:46 -04:00
sergioandClaude Opus 5 723f3ead33 gnome: 🏔🏔 EL ESCRITORIO PINTA — Overview de GNOME desde fuente, con captura
docs/evidencia/gnome-shell-qemu-2026-07-29.png: el conmutador de espacios de trabajo,
el reloj en el panel, la miniatura del escritorio, el dash, y la notificación estándar
de GNOME por sesión de root. Todo construido desde fuente sobre el kernel de hammer,
arje-zero como PID1 y musl.

**EL MURO NO ERA DE KMS.** El diagnóstico anterior culpaba al plano primario de
virtio-gpu («no advertised formats», sólo `Queue mode set`, ningún page flip). Era
correlación. Se probó quitando MUTTER_DEBUG_FORCE_KMS_MODE=simple —el log pasó a decir
`using atomic mode setting`, o sea que el cambio SÍ tomó efecto— y la pantalla siguió
con exactamente 2 colores. Refutada. Mutter no tenía un frame que presentar; no es que
no supiera presentarlo.

La causa estaba en el stack trace, en el log, desde el principio: la excepción por el
gschema `org.gnome.settings-daemon.peripherals.touchscreen` ocurre DENTRO de
`Main.start()`, construyendo los quick settings ⇒ el panel nunca se arma. Proceso vivo,
compositor con DRM master, nada que pintar.

Receta `gsd-schemas`: los gschemas de gnome-settings-daemon SIN g-s-d, que sigue
aparcada por GTK3/X11. **Tercera vez que aparece la misma lección** (tras la política
D-Bus de login1 y el `org.gnome.login-screen` de libgdm): un gschema no es un fichero de
datos del demonio, es una interfaz publicada que consume otro programa.

Dos trampas de diagnóstico, las dos ahora blindadas:
- **`glib-compile-schemas` sale con 0 aunque RECHACE un esquema.** Instalé los once XML,
  el log dijo `esquemas OK` y el shell seguía diciendo `not found`: faltaba
  `org.gnome.settings-daemon.enums.xml`, que meson genera con glib-mkenums y no es uno
  de los `.in`. Sin los `<enum>`, glib descarta el esquema entero, avisa por stderr y
  sigue. `gnome-start` ahora vuelca ese stderr SIEMPRE, no sólo al fallar.
- El log serial persiste entre arranques y me hizo leer dos veces un `STATUS +30s` de una
  VM ya muerta. Truncarlo antes de arrancar; `Failed to get "write" lock` avisa de que
  hay otro QEMU con el disco tomado.

Y queda escrito cómo validar CON PANTALLA sin humano delante: screendump por el monitor
de QEMU, con una métrica barata previa a mirar — contar colores distintos. Negro = 2;
pintando = 582, con el azul GNOME (2,60,136) al frente.

Lo que falta son detalles, ninguno impide el escritorio: lanzar upowerd desde gnome-start,
un tema de cursor, el setuid de colord y gnome-control-center.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 18:05:20 -04:00
sergioandClaude Opus 5 8d1efca26e gnome: GIRepository-2.0 — el typelib que no pide el shell sino gjs
Con los siete de `dependencies.js` cerrados, el arranque avanzó y murió en uno que NO
está en esa lista:

  JS ERROR: Requiring GIRepository, version 2.0: Typelib file ... not found

El que lo pide es **gjs**, desde su propio JavaScript embebido: su GResource trae
literalmente `imports.gi.versions.GIRepository = '2.0';` (verificado con strings sobre
libgjs.so.0). Dep de runtime del INTÉRPRETE — invisible al grafo de build Y a la lista
del shell. O sea que `dependencies.js` es necesario pero no suficiente: hay una capa
más abajo.

HAY DOS GIRepository Y NO SON LO MISMO, y los números confunden a propósito:
  · GIRepository-3.0 = la API NUEVA, la que GLib absorbió (libgirepository-2.0). La
    produce glib-introspected y ya estaba.
  · GIRepository-2.0 = la API VIEJA, la de libgirepository-1.0.so.1, contra la que gjs
    está enlazado. La produce gobject-introspection, pero sólo con
    -Dbuild_introspection_data=true, y el nuestro va con false.
La 2.0 es la vieja porque nombra la librería 1.0; la 3.0 es la nueva porque nombra la 2.0.

Receta aparte en vez de prenderle la opción a g-i: su radio alcanza toda la cadena GNOME
(catorce recetas la declaran para escanear) y además traería de vuelta los typelibs de
X11/cairo que motivaron apagarla. Acá el radio es cero.

Replica el custom_target('gir-girepository') de upstream (gir/meson.build:494) pero
escaneando contra la libgirepository-1.0 ya instalada. **La lista de fuentes va enumerada
a mano y no como glob**, y no es prolijidad: `girepository/*.h` barre también
`gitypelib-internal.h`, que declara G_TYPELIB_ERROR; el scanner emite entonces el stanza
del error-quark y su binario temporal no linkea, porque `g_typelib_error_quark` es LOCAL
en la .so instalada. Upstream nunca lo escanea — el glob era el error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 14:54:34 -04:00
sergioandClaude Opus 5 bc4d0c8aea gnome: los 7 typelibs CERRADOS — librsvg y ibus, y dos deudas viejas pagadas de paso
Rsvg-2.0   librsvg b3:351f4658
  IBus-1.0   ibus    b3:d4d3750a

**1. PERILLA NUEVA DEL LAB: `[source] cargo_vendor`.** `detect_build_system` asume UN
sistema de build por árbol, y librsvg genuinamente tiene dos: `configure.ac` gana por
precedencia pero su Makefile llama a `cargo build`, que dentro del sandbox hermético
no tiene red. La perilla fuerza el vendoreo (que ocurre en el fetch, donde sí hay red)
sin tocar la detección. **No entra al ArtifactHash** —decide de dónde salen las deps,
no cuáles: eso lo fija el Cargo.lock, ya bajo el sha256 de la fuente— así que se puede
prender en una receta ya sellada sin re-hashear nada. Con test, y verificado en vivo:
libelogind no movió su hash tras el cambio.

**2. LA DEUDA DEL UNWINDER, PAGADA.** librsvg moría en
`undefined reference: _Unwind_DeleteException`. No es de librsvg: es de la `std` de
rustc, que trae landing pads y espera el runtime que en glibc vive en libgcc_s. Es la
deuda que el corpus arrastra desde matar-gcc —«las 12 recetas Rust son UN problema, no
12»— y el remedio estaba a mano: **zig empaqueta la libunwind de LLVM** y exporta los
`_Unwind_*` (verificado con nm). Alcanza `LIBS=-lunwind`. Va por LIBS y no por LDFLAGS
porque autotools pone LIBS al FINAL de la línea de enlace, que es donde tiene que ir
una librería que resuelve símbolos indefinidos.

librsvg va en 2.58.5 y no 2.59+: en 2.59 cambió a meson + cargo-c, y cargo-cbuild
enlaza el crate `cargo` entero (libgit2, libssh2, libcurl, openssl) — una campaña
propia por un binario que sólo corre en el constructor. 2.58.5 produce el mismo
Rsvg-2.0. Mismo criterio que gnome-desktop 44.5.

**3. `x11-compose-data`, y es una tensión que vale la pena tener escrita.** ibus COMPILA
la tabla de teclas muertas de X11 dentro de libibus (Makefile.am:310, incondicional, sin
`--disable-`). Sin datos no construye. Los datos viven en el tarball de libX11 por
historia, no por necesidad técnica: son 5192 líneas de reglas. La receta extrae SÓLO los
ficheros de locale reproduciendo la regla de upstream (`cpprules.in`: cpp crudo +
CPP_SED_MAGIC literal) y no compila una línea de X11. Un escritorio Wayland-only sigue
necesitando la tabla de composición del mundo Unix.

Gotchas de ibus, los tres medidos: su ayuda MIENTE (`--enable-gtk4`/`--enable-wayland`
sugieren default apagado; el default es `yes`); `--disable-emoji-dict` NO apaga
`--disable-unicode-dict`; y con `--disable-wayland` el build muere igual porque
`tools/main.c` llama wl_display_* sin `#ifdef` mientras WAYLAND_LIBS sólo se agrega si
la opción está prendida — bug de upstream en su propia configuración sin Wayland.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 14:28:50 -04:00
sergioandClaude Opus 5 fd9f9dff6d gnome: libgdm (b3:46fac484) + cairo-1.0.typelib — la capa JS avanza dos muros más
Dos rondas del mismo patrón, las dos destapadas ARRANCANDO:

1. `Requiring Gdk 4.0: Typelib 'cairo' 1.0 not found` ⇒ cairo-1.0 sumado a
   gi-foreign-typelibs. Necesita el mismo sed que gi-foreign-girs (viene como .gir.in
   con dos placeholders), para compilar EXACTAMENTE el XML que está instalado: si
   divergieran, el typelib describiría otra librería. NO segfaultea g-ir-compiler — el
   crash que motivó -Dbuild_introspection_data=false era del g-i de antes de la isla
   dinámica. La lista de cinco no se adivinó: sale de leer los <include> de TODOS los
   .gir del cierre y cruzarlos contra los typelibs existentes.

2. `Requiring Gdm 1.0` ⇒ receta `libgdm`, SÓLO la librería cliente. gnome-shell la
   importa sin condicional (js/misc/dependencies.js:12), o sea que **la librería
   cliente de GDM es dep de runtime del SHELL, no del greeter** — y eso no se ve en
   ningún meson.build. `gdm.toml` (el demonio entero) sigue aparcada por linux-pam,
   pero PAM es del DEMONIO: libgdm/ no lo toca. La receta corta por ahí; cuando exista
   linux-pam las dos conviven.

Para que libgdm compilara hicieron falta tres símbolos más en libelogind
(tawasuyu 9bf977a52, re-pineado a 98db584fd, re-sellado b3:c1fc4bd8):
sd_seat_get_sessions, sd_session_get_service y **sd_booted**, éste en una cabecera
nueva systemd/sd-daemon.h. sd_booted NO es un stub que devuelve 0: comprueba lo mismo
que systemd —que exista /run/systemd/system/— y bajo arje no está, así que responde 0
porque ES 0.

gnome-shell re-sellado b3:b2d5919b por la cascada.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:50:14 -04:00
sergioandClaude Opus 5 a98267468c gnome: gi-foreign-typelibs (b3:a159373c) — un .gir NO es un typelib
Con accountsservice adentro, la capa JS avanzó y murió un paso después:

  JS ERROR: Requiring Atspi, version 2.0: Typelib file for namespace 'DBus', version '1.0' not found

`Atspi-2.0.gir` declara `<include name="DBus" version="1.0"/>`. El `DBus-1.0.gir` YA estaba en
/usr/share/gir-1.0 (lo pone gi-foreign-girs), pero gjs resuelve en runtime contra el TYPELIB
compilado, no contra el XML — el XML le alcanza al scanner, no al cargador.

Receta aparte y no un compile agregado a gi-foreign-girs porque `yupana radio` da **17 sellados
cayendo a deuda** ahí (catorce recetas la declaran para escanear, mutter y gnome-shell entre ellas).
Compilar cuatro typelibs no justifica reconstruir la cima. No se pisan: una instala sólo en gir-1.0
y la otra sólo en girepository-1.0. Mismo criterio que separó udev-pc de libudev-zero.

Se compilan CUATRO nombrados explícitamente —DBus-1.0, DBusGLib-1.0, fontconfig-2.0, freetype2-2.0—
y no un glob: los .gir de X11 y cairo arrastran includes que no tenemos y son justamente los que
hacían SIGSEGV a g-ir-compiler (la razón de nuestro -Dbuild_introspection_data=false).

Además: gnome-start lanza accounts-daemon explícito. Su .service de activación está instalado, pero
la activación por bus de sistema pasa por dbus-daemon-launch-helper, que acá no es setuid root;
correr como root probablemente alcanzaría, pero depender de eso es depender de un accidente.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:10:05 -04:00
sergioandClaude Opus 5 08fc1d4b92 gnome: accountsservice SELLA (b3:0328b0d0) — cae la frontera de la C-ABI de logind
Los dos huecos que bloqueaban esta receta estaban medidos desde ayer símbolo por
símbolo, y ninguno era de accountsservice. Los dos cerrados:

1. libelogind pasa de 14 a 22 símbolos (tawasuyu 8d892151b, re-pineado a b56aeff46,
   re-sellado b3:9650ee06). Los ocho nuevos son los que usa accountsservice, con
   `sd_login_monitor_*` implementado sobre inotify en /run/systemd/{sessions,seats,users}.

2. `fgetspent_r` no existe en musl ⇒ accountsservice-fgetspent_r-musl.patch. NO es
   sustituir por `fgetspent()` a secas: daemon.c guarda los buffers en un GHashTable
   y `fgetspent()` devuelve un struct estático que se reescribe en cada llamada —
   todas las entradas de la tabla apuntarían al último usuario leído. El parche copia
   el registro al buffer del llamador, y sigue el patrón que el propio accountsservice
   ya usa para /etc/passwd (`src/fgetpwent.c` bajo `#ifndef HAVE_FGETPWENT`).

Produce `AccountsService-1.0.typelib`, que es exactamente lo que la capa JS pedía.

De paso, dos cosas que este frente enseñó y quedan horneadas:
- libelogind necesitaba `cargo_vendor_dir` (el `vendor/` de smithay entró a la rama
  selfhost con el merge de main; el mismo choque que ya tenía arje-logind-compat).
- `hydrate-gnome.sh` ahora hidrata DOS raíces por defecto. accountsservice no es dep
  de build de nadie: es dep de RUNTIME, resuelta por gjs al arrancar. **El cierre de
  build no es el cierre de runtime**, y lo que el shell carga por `imports.gi.*` hay
  que nombrarlo a mano o no entra al rootfs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:54:52 -04:00
sergioandClaude Opus 5 03bdd41b4a gnome: el muro de input NO era libudev-zero — el fd de TakeDevice era bloqueante
Tres cambios de diagnóstico en `gnome-start` que, juntos, dieron el veredicto:

1. SONDA DE INPUT antes de lanzar el shell. `libinput list-devices` hace lo mismo
   que `init_libinput()` de mutter (udev_new + create_context + assign_seat) pero
   abriendo el devnode por su cuenta. Volvió con rc=0 y los tres dispositivos
   listados ⇒ **la hipótesis libudev-zero del runbook queda REFUTADA**: la
   enumeración por /sys funciona.

2. El volcado POR HILO corría DESPUÉS del `kill -ABRT`, o sea sobre un
   /proc/<pid>/task que ya no existía: salía vacío. Movido antes. Ahí apareció el
   dato: `tid 201 [Mutter Input Th] wchan=evdev_read syscall=0`. El hilo de input
   dormido en un `read()` de evdev dentro del kernel. (El core no servía: gdb no
   desenrolla a través de musl y devuelve `?? ()` para los 11 hilos no principales.)

3. `MUTTER_DEBUG` es una LISTA DE TÓPICOS (g_parse_debug_string sobre
   meta_debug_keys), no un booleano: con `1` no encendía nada. Ahora
   `backend,input,kms`, y el tópico `backend` imprime la última línea antes del
   cuelgue: «Opening and taking control of device file '/dev/input/event0'».

Más: ping D-Bus a login1 en el diagnóstico (el daemon respondía: no estaba
tildado) y copia de los logs de /tmp —que es tmpfs— a la raíz ext4 para poder
sacarlos con debugfs junto al core.

La causa está arreglada en tawasuyu (3dd88f582): `TakeDevice` abría sin
`O_NONBLOCK`. Receta re-pineada a 2445fa31c y re-sellada b3:7ffa9256.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:09:54 -04:00
sergioandClaude Opus 5 8cf9a87dd2 arje-logind-compat: repineado al fix (b3:5e4b8e04) — la cadena vuelve a ser reproducible
Los dos arreglos de arje-logind-compat (sesión eager + object path de Session
escapado como systemd) están commiteados y pusheados en tawasuyu, así que la
imagen ya NO corre un binario compilado a mano: corre el artefacto sellado, y se
verificó que da EXACTAMENTE el mismo resultado (mismo `Added virtual monitor
Meta-0` → `Using Wayland display name 'wayland-0'` → mismo muro en el typelib de
AccountsService).

El pin va a la rama selfhost, no a main: esa rama existe para que hammer pueda
construir con --locked de forma hermética (el monorepo gitignora el Cargo.lock).
Estaba MUY atrás — el artefacto viejo ni siquiera tenía el objeto Session que
mutter necesita —, así que se le mergeó main y se regeneró el Cargo.lock, que con
main al día había quedado viejo y habría hecho fallar el --locked.

Y ahí apareció una colisión ya conocida pero no aplicada acá: el monorepo COMMITEA
su propio `vendor/` (`[patch.crates-io] smithay = { path = "vendor/smithay" }`, la
copia parcheada que mirada usa para el tearing) y `cargo vendor` de hammer escribe
en `vendor/` por defecto, pisándolo. El build moría con
`failed to read /src/vendor/smithay/.cargo-checksum.json`. Se resuelve con el
campo que ya existía para esto: cargo_vendor_dir = ".hammer-cargo-vendor".

De paso, el script de imagen elige el artefacto por `hammer hash` sobre la receta
—el que corresponde a la receta de HOY— en vez de por el más reciente del store:
con dos artefactos del mismo paquete conviviendo, la fecha no dice cuál es el
vigente. Mismo criterio que hydrate-gnome.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 06:52:15 -04:00
sergioandClaude Opus 5 f56bab72f5 gnome: el compositor SUBE ENTERO en headless — el muro DRM es libinput, y aparece accountsservice
Experimento decisivo: `gnome-shell --headless --virtual-monitor 1280x800`. El
backend headless crea el seat con META_SEAT_NATIVE_FLAG_NO_LIBINPUT, o sea que
saltea init_libinput() — justo el último paso del hilo de input antes de señalar
`input_thread_initialized` (meta-seat-impl.c:3098). Resultado:

  libmutter-Message: Added virtual monitor Meta-0
  libmutter-Message: Using Wayland display name 'wayland-0'
  == compositor OK (wayland-0) — el shell ES el display server

⇒ **CONFIRMADO: el bloqueo del camino DRM está en libinput/udev, no en el resto
del arranque.** Todo lo demás de mutter funciona. Primer sospechoso libudev-zero.

Y con el compositor arriba aparece el muro siguiente, que es de otra naturaleza:

  Gjs-CRITICAL: JS ERROR: Requiring AccountsService, version 1.0:
                Typelib file for namespace 'AccountsService' not found

LA LECCIÓN DE MÉTODO: gnome-shell selló con su cierre de build COMPLETO (108/108)
y aun así la sesión muere pidiendo este typelib. **El cierre de build no es el
cierre de runtime**: todo lo que el shell carga por `imports.gi.*` desde
JavaScript es invisible al grafo de deps. Se encuentra ARRANCANDO, no compilando.

Se autoró recipes/incoming-gnome/accountsservice.toml. El configure pasa entero
—tres seds verificados contra el build real: generate-version.sh (deriva la
versión del nombre del DIRECTORIO, que en el sandbox es /src, y la rama git usa
`date`, o sea no-determinista), la aserción de wtmp (musl no define WTMPX_FILENAME
ni _PATH_WTMPX) y subdir('tests') (arrastra mocklibc, que llama fgetgrent, ausente
en musl)—. Ojo: -Dsystemdsystemunitdir tiene que ser literalmente `no`; con la
cadena vacía el meson interpreta "averiguá el directorio" y aserta pidiendo
systemd.pc.

NO SELLA todavía, y la frontera está contada símbolo por símbolo, no estimada:

  · libelogind exporta 14 símbolos (los que pedía mutter) y accountsservice usa
    OCHO que faltan: sd_get_sessions, sd_seat_can_multi_session,
    sd_session_get_display y los cinco de sd_login_monitor_*. Estos últimos son
    la parte con enjundia: no son getters sino una API de NOTIFICACIÓN (un fd
    poll-able). Sobre el diseño actual el camino natural es inotify sobre
    /run/systemd/{sessions,seats,users}.
  · fgetspent_r no existe en musl (extensión glibc de /etc/shadow, usada en
    src/daemon.c:265): necesita shim o parche a la variante no-reentrante.

Ninguno de los dos es de accountsservice: son huecos de NUESTRA capa de compat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 06:07:42 -04:00
sergioandClaude Opus 5 34c7962b29 gnome: cae el SIGSEGV — el shell toma DRM master e input; muro nuevo en la hebra de input
Dos bugs REALES de arje-logind-compat, encontrados arrancando la imagen y
arreglados (con test) en el árbol de tawasuyu — todavía LOCALES, sin pushear:

  1. La sesión se creaba PEREZOSAMENTE. `ensure_user` sólo corría dentro de un
     método del Manager, y `write_login_state` vive adentro. Pero mutter no
     empieza por D-Bus: lo primero que hace es sd_pid_get_session(), que
     arje-sdlogin-compat resuelve LEYENDO /run/systemd/sessions. Huevo y gallina:
     el estado en disco sólo se escribía después de un pedido que sólo ocurre si
     el estado ya existe. Ahora se crea al arrancar (eager_session).

  2. El object path de la Session estaba MAL ESCAPADO: era `/session/_1`. La
     convención de systemd (bus_label_escape) codifica `_<hex>` todo lo que no sea
     [A-Za-z0-9] **y también el primer carácter si es dígito** ⇒ el id "1" da
     `_31`. Importa porque el cliente calcula el path por su cuenta y NO pregunta:
     mutter reimplementa la misma regla en meta-dbus-utils.c. Con `_1` no había
     nadie sirviendo ahí, la propiedad `Seat` volvía NULL y mutter —que no
     chequea— moría de SIGSEGV en get_seat_proxy. Los objetos User NO usan este
     escapado (systemd hardcodea `_<uid>`), así que user_path() queda igual.

Y ARJE_LOGIN_STATE=1 YA EXISTÍA: el comentario del daemon dice literalmente "en
arje (sin systemd) el launcher de sesión lo prende. Default off" — y el launcher
es gnome-start. El puente que escribía /run/systemd/ a mano era reinventar esa
perilla; queda de fallback inerte.

Resultado: el shell ya no crashea. Corre con 12 hilos, /dev/dri/card0 abierto
tres veces y /dev/input/event0 abierto — el TakeDevice de logind funciona y el
compositor tiene DRM master e input. Queda bloqueado en
meta_seat_impl_initable_init (meta-seat-impl.c:3154): espera en un condvar a que
la "Mutter Input Thread" avise que inicializó, y nunca avisa. Hipótesis principal
libudev-zero, que ya dio un episodio idéntico en el frente de la USB nvidia.

El gnome-start ahora le fuerza un core con SIGABRT al proceso colgado: sin gdb en
la imagen es la única forma de ver dónde está parado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:49:46 -04:00
sergioandClaude Opus 5 5295aa35c6 gnome: la cima ARRANCA en QEMU — muere en meta_launcher_new, causa localizada
gnome-shell bootea, compila sus esquemas, toma el DRM de virtio-gpu y se declara
Wayland display server. Después SIGSEGV. El backtrace del core no deja dudas:

  #4 g_variant_get (value=0x0, "(s&o)")     ← GVariant NULO
  #5 get_seat_proxy   src/backends/meta-launcher.c:407
  #6 meta_launcher_new (META_LAUNCHER_FLAG_TAKE_CONTROL)

Mutter pide la propiedad `Seat` del objeto **Session** de logind — `(s&o)` es su
firma estándar (nombre_del_seat, object_path). arje-logind-compat adquiere
org.freedesktop.login1 y sirve el Manager, pero NO expone un objeto Session con
esa propiedad ⇒ la lectura devuelve NULL y mutter desreferencia sin chequear. Que
mutter no valide es fragilidad suya; el hueco es nuestro.

Cinco eslabones hubo que armar antes de llegar a ese muro, ninguno anotado:
  1. gschemas.compiled NO se genera con DESTDIR seteado (meson lo dice en el log
     del build) ⇒ GSettings abortaba en el primer g_settings_new().
  2. GI_TYPELIB_PATH tiene que incluir /usr/lib/gnome-shell: St/Shell/Gvc/Shew se
     instalan aparte por ser privados del shell. Es el env sin análogo en KDE.
  3. /var/run no existía en la base metal; arje-logind-compat busca el bus en la
     ruta legacy y sin el symlink se iba a "modo idle".
  4. La política D-Bus de login1 faltaba (system.conf trae <deny own="*"/> y
     normalmente la instala systemd). PERTENECE al artefacto de arje: está en el
     script de imagen sólo para dejar visible qué falta empaquetar.
  5. /run/systemd/{sessions,seats,users} vacíos — libelogind resuelve la C-ABI
     sd-login LEYÉNDOLOS y nadie los escribe (el Announce de arje-logind-compat al
     bus del fractal falla con "identity mismatch"). gnome-start los escribe A
     MANO y está MARCADO COMO ANDAMIO. Con ese puente desaparece el "Failed to
     find any matching session".

Gotcha que costó una iteración: `kill -0` TIENE ÉXITO sobre un zombi (el padre no
lo cosechó todavía), así que el script reportaba "sin wayland-0 tras 45s" cuando
el shell había muerto en el primer segundo. Mirando State: de /proc/<pid>/status
sale el código real, 139.

Todo el ciclo (hidratar → imagen → bootear → sacar el core con debugfs → gdb) y
la lista de lo que falta quedan en docs/runbooks/gnome-qemu-desktop.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 21:47:11 -04:00
sergioandClaude Opus 5 f6734d6ce6 gnome: hydrate-gnome.sh — el cierre de runtime de la cima CIERRA 108/108
Proyecta al FHS el cierre de una receta GNOME desde artefactos SELLADOS, sin
rebuild. Resultado sobre gnome-shell: **108 recetas, 0 faltantes** — 614
binarios, 455 .so, 49 typelibs.

Diferencia con scripts/kde/hydrate-from-store.sh, y por qué importa: aquél
resuelve el cierre desde un index.json de repo y elige el artefacto por MTIME con
un CUTOFF. Eso es una heurística — si dos artefactos del mismo paquete conviven
en el store, la fecha no dice cuál corresponde a la receta VIGENTE. Acá el cierre
sale del GRAFO REAL de recetas (deps.build, resolución hermano→padre, la misma
que usa hammer) y el artefacto se elige por `hammer hash`. Cero ambigüedad, y si
falta algo el reporte dice qué receta y con qué hash lo buscaba.

Auditado además el cierre DINÁMICO del rootfs (452 ELF, 104 librerías NEEDED).
Sin resolver quedan cuatro, y sólo dos son hallazgos:

  libc.so                 359 consumidores — es el propio musl (en musl el loader
                          ES libc); lo aporta la base metal, no es hueco.
  libc.musl-x86_64.so.1   17 artefactos (nss + spidermonkey) piden ESTE soname en
                          vez de libc.so. Es la MISMA libc: los construidos con el
                          gcc/clang de Alpine emiten un soname distinto al de
                          zig-cc. No rompe si el rootfs trae los dos nombres, pero
                          es una fisura de consistencia a documentar.
  libstdc++.so.6 +        SÓLO libmozjs-128.so y js128 ⇒ **la sesión GNOME arrastra
  libgcc_s.so.1           el runtime C++ de Alpine por la cadena
                          gnome-shell → libgjs → libmozjs**. Es exactamente la
                          "última milla" de matar-gcc que se cerró para cmake con
                          -static-libstdc++ y que spidermonkey no cubrió. Radio
                          medido: 2 sellados (gjs, gnome-shell). Pendiente, y
                          conviene en el worker: el compile de mozjs come ~8GB+.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 21:06:55 -04:00
sergioandClaude Opus 5 5aec112629 gnome onda 3: cierra las 3 fronteras de gnome-desktop + el latido regenera el grafo GNOME
Las 3 fronteras que el propio comentario de gnome-desktop declaraba:

  iso-codes         receta nueva (datos + .pc). Ya no está en download.gnome.org (404);
                    Salsa es GitLab y su archive no es determinista (lección de cairo-shared),
                    así que la fuente es el .orig.tar.xz inmutable del pool de Debian.
                    Sin traducciones: i18n.gettext exige msgfmt completo y sólo hay
                    gettext-tiny — se vacían los 8 meson.build de dominio.
  libseccomp        receta nueva (autotools estático, gperf de build-dep real).
  xkeyboard-config  duplicada desde incoming-kde: fichero idéntico ⇒ MISMO ArtifactHash
                    (b3:bcf9b766) ⇒ ya está SELLADO. Frontera cerrada con cero rebuild.

Y un punto ciego del latido: cosecha-cron regeneraba build-state.json y el de KDE, pero
NUNCA el de GNOME. El grafo llevaba días mintiendo `never` sobre gobject-introspection y
toda la onda 2, que están selladas. Un grafo viejo miente con la misma cara que uno fresco.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:14:59 -04:00
sergioandClaude Opus 4.8 da26b3a280 gnome onda-1: cola aislada incoming-gnome-onda1 para el worker
Las 3 raíces de la onda 1 (gsettings-desktop-schemas, gobject-introspection,
gnome-desktop) copiadas a una cola propia + añadida al QUEUES del worker-loop.
Aislar evita que el worker dispare rebuilds de spidermonkey/mutter (onda 2/3).

El cierre real de RECETAS de las 3 es 39 nodos, 0 en incoming-kde: el "gap del
resolver" que temía era de seed-edges (.pc cairo→libX11), NO de deps declaradas.
El espinazo corpus (38 nodos: gtk4/cairo/pango/gdk-pixbuf en recipes/) está 100%
sellado ⇒ el worker cachea todo e sólo construye las 3. Hashes byte-idénticos a
los sellos de incoming-gnome (base_dir no entra al hash).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 23:31:27 -04:00
sergioandClaude Opus 4.8 afd867ea51 yupana: drenar reckona honesto un perfil sin cierre (guardián GNOME)
drenar --perfil escritorio-gnome erraba 'no encontré un grafo (¿corriste
build-state.py?)' — mentira: el grafo GNOME existe, pero NINGUNA raíz del perfil
(mutter/gnome-shell/gdm) está autorada ⇒ 0 nodos etiquetados con el perfil, y la
comprobación por etiqueta en cargar() fallaba. Dos correcciones:
  · cargar(): una cola NO-corpus identifica su grafo unívocamente ⇒ matchear por
    cola; la etiqueta sólo desambigua dentro de corpus (perfiles solapados).
  · main(): ámbito vacío con perfil ≠ 'imagen completa' — es 'SIN CIERRE todavía:
    0 raíces autoradas', y apunta a autorar top-down.
KDE (162 sellados) y corpus (769/4-deuda) sin regresión. El yupana-gnome queda
armado en las 4 capas: objetivo, radio/grafo, keystones y drenar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 14:23:12 -04:00
sergioandClaude Opus 4.8 9a028ba04f gnome: SPIDERMONKEY (mozjs 128) SELLA en el hub — keystone caído
El motor JS de Mozilla, keystone del frente GNOME (gjs→gnome-shell cuelgan de él),
sella b3:c48dbd10: libmozjs-128.so + libjs_static.a + shell js128 + mozjs-128.pc +
550 headers. Compiló el C++ entero a -O3 -j8 sin OOM en el hub (31GB/0 swap, al filo).

Seis capas iteradas desde el borrador:
  1-2. FIX DE INFRA: el rootfs builder tenía clang pero NO el paquete  (Alpine
       separa los binutils LLVM del compilador). Sin llvm-ar/llvm-objdump/llvm-profdata
       configure aborta. bootstrap-devfs.sh: +llvm22 en NEEDED + paso 3a-ter que
       symlinkea la suite llvm-* a /usr/bin (Alpine la deja en /usr/lib/llvm22/bin sin
       exponerla). Aplicado ya al rootfs real (.dev-fs/alpine).
  3. triple: mozjs no traduce vendor ; 4. debe COINCIDIR con el host de rustc
     (); fijado ese exacto.
  5. venv de mach: cada fase es un bwrap con /tmp fresco ⇒ el venv no sobrevivía de
     configure a compile. Anclado a /src/.mozbuild (persiste, es scratch).
No re-hashea artefactos (el rootfs no entra al hash). OJO: el worker golden también
necesita llvm22 (ya cubierto por bootstrap-devfs.sh para la próxima provisión).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 14:17:13 -04:00
sergioandClaude Opus 4.8 d33fc94fc5 gnome: enciende la CAPA GRAFO del yupana-gnome
build-state.py gana --gnome (análogo a --kde): carga recipes/incoming-gnome/ y sale
a build-state-gnome.json. drenar/yupana/seed-graph aprenden ese grafo (keystones,
drenar --perfil escritorio-gnome, radio de nodos incoming-gnome, frontera). Hoy la
cola está vacía ⇒ el grafo trae 0 nodos gnome; se poblará al aterrizar recetas y ahí
drenar/keystones reckonan solos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:05:40 -04:00
sergioandClaude Opus 4.8 0b333b4c9e gnome: integra el frente a yupana — perfil escritorio-gnome + reckoning honesto
1. targets.toml: [perfil.escritorio-gnome] (9 raíces de sesión, cola incoming-gnome)
   ⇒ `yupana objetivo` ya lo ve; fluye por targets.py. build-state lo SALTA hasta
   que la cola se cargue (--gnome), así declararlo no reporta raíces fantasma.
2. seed-gnome.py OPTIMIZADO: hornea la triage (sustitución/tooling/opcional/espinazo)
   como HIPÓTESIS con el caveat "el cierre de nixpkgs sobreestima ~5-10×", marca el
   keystone (spidermonkey→gjs→gnome-shell), y deja de mentir con el 465 crudo. El
   espinazo (353) se reporta como COTA ALTA, no como lista de build.

Falta (se activa al autorar): grafo build-state-gnome + flag --gnome ⇒ keystones/
drenar nativos sobre GNOME. Hoy la capa OBJETIVO del yupana-gnome está; la GRAFO no.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:58:23 -04:00
sergioandClaude Opus 4.8 224b15f231 gnome: añade seed-gnome.py + los mapas del subtree (faltaron en 72de19c)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:50:17 -04:00
sergioandClaude Opus 4.8 b98995b7b4 deadman: NO contar farm-worker-loop como trabajo (3er bug del dead-man)
El loop del worker está SIEMPRE vivo (idle-loopea con la cola vacía). Contarlo
en hay_trabajo() reseteaba los ticks a 0 cada 10 min ⇒ el worker NUNCA acumulaba
idle ⇒ NUNCA se auto-mataba. Quedó 2h17m idle quemando € (2ª vez).

El loop construyendo YA se detecta por su hijo `hammer build`; el loop vacío NO
es trabajo. Se afina el match a `release/hammer.* build ` y se suma `campana-deuda`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:14:16 -04:00
sergioandClaude Opus 4.8 2bd1944173 deadman: --puede-borrar sourcea /etc/hammer-deadman.env (o daba falso-negativo)
Cazado con el 1er worker real: el token vive en /etc/hammer-deadman.env (lo carga
systemd como EnvironmentFile), pero corrido A MANO (farm-up --puede-borrar) ese
fichero no se lee solo ⇒ mi cadena de fallback no lo veía ⇒ decía 'sin token' y
farm-up habría DESTRUIDO un worker que sí puede matarse. Fix: sourcear el
EnvironmentFile antes de la cadena de tokens desnudos. Verificado en worker real:
'SÍ: puedo borrar mi id=154276696'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 03:48:41 -04:00
sergioandClaude Opus 4.8 3de75a5229 granja: el dead-man del worker se mata SOLO (sin hub) — 3 bugs + gioser doble-blindado
El worker DEBE matarse solo, sin depender de la laptop (es la razón de ser del
dead-man: vive en el worker; el volumen hace que morir no pierda nada). El reaper
del hub queda sólo como último recurso. El dead-man estaba TRIPLEMENTE roto:

 1. TOKEN EN EL FICHERO EQUIVOCADO: hay 2 caminos de creación con el token en
    lugares distintos (farm-up → /etc/hammer-deadman.env; harkaq-vol → /root/
    .hcloud-token). La service lee sólo el primero ⇒ un worker del otro camino
    quedaba sin token, disparaba pero salía 1 "sin HCLOUD_TOKEN". Fix: deadman.sh
    busca el token EN CADENA (volumen primero, que es lo más persistente).
 2. REGEX DEL ID ROTO: la API devuelve JSON con espacio ("id": 126, no "id":126)
    y el dead-man usaba '"id":[0-9]+' ⇒ NUNCA resolvía su id, aun con token
    válido. Fix: '"id":[[:space:]]*[0-9]+'. (Doblemente roto: por eso NUNCA murió.)
 3. FIRING ≠ CAN-DELETE: farm-up sólo verificaba que el timer dispara. Ahora
    corre `deadman.sh --puede-borrar` (token en cadena + ve su id en la API) y si
    NO puede matarse, DESTRUYE el worker ahí mismo. Un worker que no se autodestruye
    es inadmisible ⇒ jamás debe existir.

gioser DOBLE-BLINDADO en TODOS los sitios de borrado (lista negra por nombre +
label role=hammer-worker), igual que farm-down ya tenía: reaper, farm-up-destroy,
deadman, y el helper de harkaq-vol. "Ahí está nuestra vida." Verificado vivo (104d).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 03:23:42 -04:00
sergioandClaude Opus 4.8 be06314336 granja: el reaper dropea de .fleet los workers que ya no existen en hcloud
Sin esto un server borrado se quedaba en .fleet para siempre (describe falla ⇒
'sin label' ⇒ nunca se dropeaba). Ahora si no existe en hcloud, se saca.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 03:08:13 -04:00
sergioandClaude Opus 4.8 1203f86cda granja: reaper HUB-SIDE — la garantía "vps idle inadmisible" deja de depender del worker
INCIDENTE (2026-07-23): un worker quedó 3.5h idle quemando € mientras el usuario
dormía. El dead-man del worker estaba "activo" (timer disparando cada 10 min)
pero FALLABA con exit 1 en cada tick: llegaba a matarse pero su
/etc/hammer-deadman.env no tenía token válido ⇒ salía 1 "sin HCLOUD_TOKEN" y
nunca se borraba. Verificaron que el timer DISPARA, no que puede BORRAR —
firing ≠ can-delete.

Causa de fondo: la garantía dependía de que CADA worker se auto-provisione bien
un token, y eso falla EN SILENCIO. Un invariante que depende de una provisión
frágil no es un invariante.

FIX en dos capas (defensa en profundidad):
 1. REAPER HUB-SIDE (cosecha-cron.sh): el hub tiene un token que funciona y el
    latido corre cada 30 min aunque no haya sesión (setsid). Un worker sin trabajo
    activo REAPER_MAX=2 ciclos (~1h) se BORRA desde el hub, con el MISMO blindaje
    de label role=hammer-worker (gioser jamás pasa). Cubre "dead-man del worker
    roto". El dead-man del worker sigue cubriendo "hub caído".
 2. farm-up verifica CAN-DELETE, no sólo firing: que el token del worker vea su
    propio id en la API (lo mismo que hace al morir). Si no puede, LO GRITA al
    provisionar en vez de descubrirlo 3.5h tarde.

Acción inmediata tomada: worker borrado a mano (label verificado), €0 baseline
restaurado — sólo queda gioser (protegido, fijo). Todo estaba cosechado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 03:05:12 -04:00
sergioandClaude Opus 4.8 8c10c0a908 yupana: modelo EXTRA-GRAFO — preflight aprende los patrones de hueco de entorno
El grafo modela deps (R1: Receta×Receta). El entorno es OTRA capa: R2 (Receta×
Capacidad, qué exige un build) y R3 (Host×Capacidad, qué ofrece la máquina).
Construible = clausura(R1) ∧ R2⊆R3. Esos huecos viven FUERA del grafo (infra del
sandbox, toolchain, layout de fs) y una matemática de grafos sola no los ve —
pero SÍ se aprenden, como patrones con predictor, calibrados contra fallos reales.

`yupana preflight <receta>` corre los patrones ANTES de construir. Primer patrón:
overlay-lowerdir, la cicatriz de kio. Habría marcado kio antes de quemar la
campaña descubriéndolo a los golpes:

  kio: 52 deps ⇒ lowerdir ~5346B > 4096B (una página) ⇒ depende del merge

CALIBRADO, no adivinado: el estimador daba corto (2974B "ok") hasta usar el
prefijo que bwrap VE en runtime (/oldroot/…, ruta del worker, no la del hub).
Con eso: kio estimado 5346B vs real 5315B — 31B de error, sin fudge. El primer
predictor "obvio" mentía con confianza; la lección de siempre.

preflight --perfil escritorio-kde destapa lo sistémico: las 40 recetas KDE
profundas CUELGAN del merge (plasma-desktop: 114 deps → 11708B, casi 3× el
límite). Todo el escritorio dependía del fix 6a0dc40. El pre-flight vuelve
VISIBLE y contable una fragilidad que antes sólo se manifestaba fallando.

Registro extensible (PATRONES_ENTORNO): futuros = capacidad-de-host (rootfs
laptop≠worker, techo MSRV — acumular observaciones), skew de toolchain (zig-skew
— fingerprint por artefacto). harkaq ya MIDE R2 (evidencia negativa); esto lo
vuelve consultable + pre-vuela. Guardián: kio marcado + estimador calibrado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:42:42 -04:00
sergioandClaude Opus 4.8 6a0dc40a05 sandbox: el merge de deps cae en el store (mismo fs), no en la raíz — desbloquea kio
merge_deps_layer funde las deps en UNA capa overlay por hardlinks (cp -al) para
no desbordar el lowerdir de overlayfs (tope ~4096B por página). Pero calculaba
la ruta del merge en deps.parent().parent() = <store>/../.dmerge, asumiendo que
el store está en <base>/store del MISMO fs. En el worker el store es un VOLUMEN
bind-montado (/opt/hammer/store en /dev/sdb, /opt/hammer en /dev/sda1) ⇒ el merge
caía en la raíz, cp -al fallaba cross-device, y el fallback apilaba las ~50 deps
de kio directo → lowerdir 5315B > 4096B → "bwrap: Can't make overlay mount" →
kio (EL keystone, gatea 36) imposible de construir. Por eso estaba clavado.

Fix de una línea: el merge va en el padre INMEDIATO de las deps (= el store
mismo), garantizado mismo fs. Confirmado en el worker: cp -al a /opt/hammer/.dmerge
falla "Invalid cross-device link"; a /opt/hammer/store/.dmerge funciona.
En el laptop andaba por casualidad (store y su padre en el mismo fs).

`.dmerge` (dot-prefix) es invisible para el store lookup/build-state como .times.
cosecha excluye /.dmerge del rsync (es scratch de hardlinks; sin -H rsync lo
expandiría a copias reales).

Descubierto levantando el worker para poblar tiempos: yupana keystones apuntó a
kio, la campaña falló ahí, y el radio de lowerdirs (5315B) destapó la causa. Una
"sorpresa de entorno" (infra del sandbox, fuera del grafo) que el frente de
timing sacó a la luz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:06:10 -04:00
sergioandClaude Opus 4.8 85b5a9bc46 yupana: instrumentar duración de build en el worker → camino crítico PESADO
El peso que le faltaba a las ondas: sin él, critical-path = nº de pasos; con él,
ETA real en segundos.

INSTRUMENTACIÓN (worker):
  scripts/farm/build-timed.sh envuelve `hammer build` midiendo la pared y la
  registra en $STORE/.times/<hash>-<host>.json — un SIDECAR del store, NUNCA
  dentro del artefacto. La duración es no determinista (varía por máquina/carga)
  ⇒ no puede tocar el ArtifactHash. Keyed por hash (CAS-safe: dos workers no se
  pisan) + host (varias muestras por receta). Viaja al hub con el rsync de store
  que ya hace la cosecha; ninguna herramienta del store lo confunde con artefacto.
  Sólo builds REALES (pared ≥ UMBRAL 3s) — los cache-hits no envenenan la mediana.
  campana-deuda.sh ahora construye vía build-timed.sh (transparente, mismo exit).

CONSUMO (hub): yupana._tiempos() carga name→mediana de segundos; keystones
computa el CAMINO CRÍTICO pesado = longest weighted path del subgrafo de deuda
(peso = segundos de build). La cadena más larga hay que construirla en SERIE
aunque haya ∞ workers ⇒ es la ETA con paralelismo infinito. Degrada con gracia:
sin datos, peso=1 y el camino crítico = nº de pasos (lo que ya daban las ondas).

Verificado: con muestras sintéticas da "ETA 19 min sobre 5 pasos, kio → kparts →
frameworkintegration → breeze → plasma-integration"; sin datos degrada a "5 pasos
(SIN datos)". store/.times está git-ignorado (metadata de máquina, no se commitea).

LÍMITE honesto (en la cabecera de build-timed.sh): `hammer build` arrastra deps ⇒
la pared incluye deps no selladas. En orden topológico (drenar) las deps ya están
selladas y la pared mide sobre todo ESTA receta — cota superior buena para pesar.
La precisión exacta pediría instrumentar el sellado dentro de hammer-build (Rust).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:25:09 -04:00
sergioandClaude Opus 4.8 4f609080df yupana: los 2 "bugs" de duplicados eran falsos positivos MÍOS — detector afinado, 0 reales
El usuario pidió arreglar itstool y la sombra de dbus. Apliqué el reflejo
(`yupana radio` + diff completo antes de tocar) y NINGUNO era un bug: ambos eran
patrones deliberados y documentados que mi detector, demasiado superficial, leyó
mal. Si los "arreglaba" a ciegas rompía dos builds.

  · dbus: corpus/dbus (estático) e incoming-kde/dbus (dinámico) NO son redundantes
    — qtbase linkea libdbus-1.so dinámicamente para Qt6DBus. El detector comparó
    sólo versión+sha+deps, no el BUILD. Borrar la sombra rompía KDE.
  · itstool: es un STUB documentado (`[source]=carrier`). El itstool real es
    Python con libxml2-bindings ausentes en el lab; este genera un script inline y
    sólo PRESTA el tarball de gettext-tiny. Apuntarlo al itstool real rompía
    appstream (radio 5) con un source que ni compila acá.
    Los 3 *-hello son el mismo patrón carrier; prison/prison-scanner una variante
    cross-nombre (misma fuente, WITH_ZXING distinto).

FIX = el detector, no las recetas. Tres señales que le faltaban:
  1. huella de BUILD en la firma de sombra (estático≠dinámico ⇒ no redundante).
  2. CARRIER: ≤1 receta del grupo construye la fuente ⇒ el resto presta el tarball.
     Señal: ¿invoca make/meson/cmake/ninja/cargo? (comentarios strippeados — la
     prosa "invocación de meson" de un stub daba falso builder, lo cazó el guardián).
  3. variante cross-nombre: ≥2 builders con BUILD distinto = deliberado, no bug.
Resultado: 0 colisiones reales (invariante c se cumple), 4 carrier + 14 variantes
+ 12 sombras justificadas, todas benignas.

Guardián extendido: ancla que el detector agrupa por FUENTE (itstool↔gettext-tiny)
y NO cría lobos (itstool=carrier, dbus=variante deliberada).

LECCIÓN: el reflejo del radio/diff antes de tocar evitó dos borrados destructivos
guiados por un falso positivo de mi propia herramienta. Medir antes de creer, aun
a la propia yupana.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:14:10 -04:00
sergioandClaude Opus 4.8 619a30187c yupana: keystones — el camino crítico real, con la matemática correcta (AND, no OR)
`yupana keystones` rankea los nudos EN DEUDA por cuánto trabajo BLOQUEADO libera
su sellado (cierre inverso transitivo restringido a la deuda). Sellá de arriba
hacia abajo y la cascada cae lo antes posible.

CORRIGE UN ERROR MÍO: dije "árbol de dominadores = keystones". Lo medí y es
FALSO. El dominador ingenuo asume alcanzabilidad OR (basta un camino), pero
construir es AND (hacen falta TODAS las deps): kcoreaddons depende de dbus/libdrm
/mesa además de qtbase ⇒ hay un camino de desbloqueo que evita qtbase y el
dominador-OR lo pierde. La métrica correcta bajo AND es el cierre inverso
transitivo. Y el crudo lo gana el toolchain (make gatea 313, inútil) ⇒ el
keystone accionable es el cierre restringido a la deuda, y sólo cuenta si el
nudo mismo está en deuda (un sellado ya está disponible, no gatea nada).

Estado actual (qtbase ya sellado, era EL keystone con 117 gateados):
   kio        36   ← sellarlo libera 36 recetas KDE bloqueadas
   kparts     19
   kcmutils   17
   ksvg       14
   libplasma  13
Ése es el camino crítico ahora.

El dominador SÍ entra, en su uso legítimo: la columna `exclusivo` = retención
estilo GC (nodos que sólo dependen de mí). kwin=9 (sus plugins privados),
libksysguard=3. Responde "si suelto esta feature, cuánto más puedo soltar".

Sin datos nuevos (no necesita duración de build). Cuando instrumentemos tiempos,
el mismo cierre se vuelve camino crítico PESADO (ETA real).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:03:56 -04:00
sergioandClaude Opus 4.8 0f3a1dc394 yupana: invariante (c) CANÓNICO — detector de duplicados por evidencia de fuente
`yupana duplicados` caza nudos que son el MISMO paquete bajo nombres/colas
distintos, agrupando por lo que BAJA (sha/url), no por el nombre. Clasifica:

  colisión-fuente — mismo sha, familias de nombre DISTINTAS ⇒ casi seguro un
                    sha copiado por error. ACCIONABLE.
  variante        — mismo sha, misma familia (zlib/zlib-shared, mesa-*) ⇒
                    diversificación deliberada estático/shared, OK (14 vistas).
  sombra          — mismo nombre en >1 cola; redundante si versión+sha+deps
                    coinciden (una sobra), justificada si difieren.

HALLAZGO INMEDIATO — el detector encontró un bug real en su primera corrida:
  itstool.toml dice versión 2.0.7 pero su url+sha apuntan al tarball de
  gettext-tiny (29cc165e…). Construiría la fuente equivocada. radio itstool=1,
  gettext-tiny=78 ⇒ el sha copiado es el de itstool.
Otras 3 colisiones son demos (adwaita-hello/libadwaita, sourceview-hello/
gtksourceview, radio 0) o subcomponentes (prison/prison-scanner) — a revisar,
no urgentes. Y 1 sombra redundante: incoming-kde/dbus es byte-idéntica a
corpus/dbus (mismo hash de artefacto) ⇒ los 114 consumidores KDE podrían
apuntar a corpus/dbus sin rebuild.

NO funde nada solo: reporta a docs/state/duplicados.json y deja el juicio al
humano (como el triaje). El radio de cada nudo dice cuál es el canónico (el de
mayor radio). Guardián extendido: test-yupana-radio.py falla si el detector
deja de ver colisiones de fuente (agrupar por nombre en vez de sha = invariante
c ciego).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:59:20 -04:00
sergioandClaude Opus 4.8 e4a0237b6d yupana: rename khipu→yupana — el registro es el khipu, la yupana es el ábaco que reckona sobre él
`khipu` colisionaba con una app de tawasuyu. En vez de un nombre a dedo, el
significado más profundo RESUELVE la colisión: en los Andes el khipu GUARDABA
(el registro de nudos) y la yupana CALCULABA sobre él (el ábaco). Acá igual, y
la distinción es arquitectura:

  docs/state/build-state.json = el KHIPU  (el registro firme: nudos y cuerdas)
  scripts/yupana.py           = la YUPANA (el motor que reckona: radio, ondas, clausura)

Nunca fue del todo un khipu lo que construimos; era la yupana. La colisión
empujó al nombre más exacto.

git mv preserva historia. Verificado tras el rename: `yupana radio libdrm` da
126/132/[kde,mirada]; el guardián de regresión pasa; build-state importa yupana
y --check sigue en 0. Cero referencias `khipu` colgadas en código.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:53:53 -04:00
sergioandClaude Opus 4.8 4d0a909bbd khipu: guardián de regresión del radio cruzado (la cicatriz de libdrm)
test-khipu-radio.py falla si el radio de un paquete compartido entre colas
vuelve a medirse recortado: verifica que libdrm lo consuman >1 cola y que su
membresía incluya escritorio-kde (la mentira original decía sólo mirada).

Regla que blinda: cuando khipu se equivoca, no sólo se arregla — queda el test
que impide repetirlo. Es la automejora arquitectónica hecha práctica.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:45:37 -04:00
sergioandClaude Opus 4.8 8da6ef6122 khipu: el motor de dependencias inversas que cruza TODAS las colas (y arregla la mentira de perfiles)
El 2026-07-22 un cambio a libdrm (default_library=both, para gtk4) invalidó 118
recetas KDE. El radio se midió contra recipes/*.toml y no contra
recipes/incoming-kde/ ⇒ 8 en vez de 118. Y build-state.py sin --kde cometía el
MISMO error: cargaba sólo el corpus, y su campo `perfiles` AFIRMABA que tocar
libdrm sólo afectaba a escritorio-mirada. Una mentira con confianza.

Causa estructural: las dependencias INVERSAS son un hecho del disco entero, no
de la vista que uno cargó. khipu.py las calcula SIEMPRE sobre todas las colas,
con resolución sibling-first (el nodo se identifica por (cola, nombre), porque
corpus/fontconfig y incoming-kde/fontconfig son nudos distintos).

  QUÉ NODOS PUNTÚO es decisión de vista. QUIÉN ME CONSUME es un hecho.

khipu es la PUERTA ÚNICA de la metodología: radio/perfiles nativos (cruzan todo
el repo) + delega estado/drenar/triaje/frontera/objetivo a sus órganos.

  khipu radio libdrm → 126 directos, 132 transitivos, [escritorio-kde,
                        escritorio-mirada], 11 sellados que caen. El número
                        honesto que habría frenado el cambio.

build-state.py ahora saca `perfiles` y el nuevo `dependientes_total` de khipu
(grafo entero), no de los nodos que cargó. Verificado: el grafo por defecto ya
atribuye libdrm a escritorio-kde; sin_perfil bajó 674→666 (8 recetas del corpus
que sólo KDE consume, ahora bien atribuidas). --check sigue en 0, grafo cierra.

Repo-wide por diseño, no KDE-céntrico: zlib toca las 4 imágenes (233 transitivos),
make 313.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:43:40 -04:00
sergioandClaude Opus 4.8 9b67268a65 triaje: los 170 candidatos de frontera clasificados, y la cadena se ejerce entera
Evidencia dura primero: NINGUNO de los 170 bloquea un build. Toda receta que
pide uno de estos ya está sellada al menos una vez ⇒ hammer construye sin
ninguno. Eso descarta empíricamente "hueco de build" para los 170 y deja sólo
la pregunta de runtime.

VEREDICTOS
  provisto  11 — no faltan: ya los da otra receta con otro nombre. nixpkgs parte
                 en varios paquetes lo que acá es uno solo. Verificado contra el
                 store: wayland-scanner→wayland, mesa-libgbm→mesa (nuestros tres
                 mesa producen libgbm.so.1), libxcb-*→xcb-util-*, poppler-qt6→
                 poppler, gmp-with-cxx→gmp, uname→coreutils.
  nix-ismo   5 — andamiaje de nixpkgs (env wrappers, helpers del stdenv, glibc
                 asomando por su libc).
  opcional 150 — software real que nixpkgs habilita y hammer no necesita, con el
                 porqué agrupado: systemd (usamos arje-zero), X11 heredado (el
                 escritorio es Wayland), Vulkan/shaders, audio/multimedia,
                 conectores de BD, tooling de docs/tests, bindings Python de Qt,
                 paquetería ajena (tenemos .swm).
  hueco      4 — gaps de RUNTIME, no de build: shared-mime-info (sin base MIME
                 no hay tipos de fichero), xwayland (ninguna app X11 corre),
                 polkit-qt-1 (sin diálogos de autorización), qqc2-breeze-style
                 (los controles QML caen a un estilo genérico).

CORRIGE UN ERROR MÍO DE P3: dije que mesa-libgbm/libglvnd eran "el muro de
GBM/EGL". Falso — libgbm ya lo produce nuestro mesa. El muro era softpipe vs
llvmpipe, no un paquete ausente.

CUARTO VEREDICTO NUEVO (`provisto`) con su propio lazo: sale a alias-triaje.txt
y seed-graph.py lo carga en MAPA ⇒ esos 11 nombres dejan de contarse como hueco
para siempre. Junto con nixismos-triaje.txt, el sembrador aprende de su triaje.

Y LA CADENA SE EJERCE ENTERA POR PRIMERA VEZ: los 4 huecos entraron como raíces
de escritorio-kde → nacieron 4 nodos `wanted` (raíces 11, sin receta 4; el grafo
sigue cerrando, --check exit 0) → seed-graph los sembró (42 aristas conocidas,
21 candidatos nuevos de frontera) → drenar.py los ordena marcándolos [semilla],
que es la regla de la fuente única a la vista. qqc2-breeze-style no cae en la
onda 1 porque sus deps SEMBRADAS la traban (kcodecs, kirigami…): el andamio
funcionando como se diseñó.

De paso: qtbase se selló mientras corría esto ⇒ la onda 1 de KDE se abrió de 1 a
13 recetas. El cuello de botella que reportó P4 ya está destrabado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:30:19 -04:00
sergioandClaude Opus 4.8 ec28a9b394 catálogo objetivo P5: tandas a archivo, y el triaje que cierra el lazo
tandas/README.md declara los 55 ficheros ARCHIVO de campañas pasadas (no se
borran: su git log es la crónica de cada frente) y documenta de dónde sale el
trabajo ahora. import-batch.sh -f sigue vivo como vía de escape, no como camino.

Lo que faltaba para que la cola fuera derivada de verdad era el paso humano, y
estaba sin forma: 137 candidatos clasificables sólo en la cabeza de alguien.
triaje.py les da forma durable — docs/state/frontera-triaje.toml, 170 candidatos
unificados de los 3 perfiles. Cada veredicto cierra un lazo distinto:

  hueco    → raíz en targets.toml ⇒ nace un nodo `wanted`, el sembrador lo
             siembra y el drenaje lo ordena (= trabajo nuevo para el worker)
  opcional → registrado, deja de reaparecer como pregunta
  nix-ismo → vuelve a seed-graph.py como descarte ⇒ el sembrador APRENDE de su
             propio triaje y cada corrida sale más limpia

Sincronizar NUNCA pisa un juicio humano: agrega los nuevos y marca ausente=true
los que dejaron de aparecer (progreso, o cambio de nixpkgs — se ve en el diff).
`sugerencia` propone con evidencia mecánica pero `veredicto` arranca en
`pendiente`: la heurística abarata el juicio, no lo cierra.

Lazo verificado punta a punta: python3-3.14.6-env marcado nix-ismo → --aplicar
lo escribe al descarte → seed-graph.normalizar() lo devuelve clasificado y deja
de contarlo como hueco. Probado y revertido; los 170 siguen pendientes.

Cadena cerrada y corriendo: targets.toml → build-state.json → drenaje.json →
worker, regenerada por el latido cada 30min. El único paso que pide una persona
es el triaje.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:20:30 -04:00