El 6.6 de la unidad 9 del SDD 26. Una navegación de PRIMER NIVEL a un medio
(`Content-Type: video/*` o `audio/*`) se cancela y la URL se la lleva `mpv`, que ya viaja en las
cuatro imágenes de escritorio: esto no agrega ni una receta, es cablear lo que ya estaba.
MEDIO http://…/audio.wav
CANCELADA la navegación http://…/audio.wav
ABIERTO /usr/bin/mpv pid=266 http://…/audio.wav
mpv: AO: [null] 8000Hz mono 1ch u8 ← su propio log: DECODIFICÓ, no sólo arrancó
LA REGLA ES ESTRECHA A PROPÓSITO: sólo el primer nivel. Un `<video>` embebido es parte de la página y
sacarlo de ahí rompería el sitio que lo puso. Las reglas anchas en el camino de cada petición son las
que terminan rompiendo la web de alguien.
FAIL-OPEN, y es lo que prueba el control negativo: cancelar es SÍNCRONO y la respuesta del host llega
después, así que no se puede saber en el momento si el reproductor arrancó. Se cancela sólo con el
puerto vivo y, si el host contesta que no pudo, la URL VUELVE al navegador con una marca para no
entrar en bucle:
SIN REPRODUCTOR no hay reproductor en /usr/bin/mpv: esta imagen no lo trae
VUELVE AL NAVEGADOR http://…/audio.wav?atuq-medios=no
Quedarse sin vídeo Y sin pestaña es el único resultado inaceptable, y es justo el que se consigue si
uno confía en que salió bien.
⚠ EL BUG QUE ESTE GUARDIÁN DESTAPÓ VALE MÁS QUE LA FUNCIÓN, y estaba DESPUÉS del éxito aparente: la
primera corrida detectó el medio, canceló y recibió el pid… y después el puerto murió con «Native
application tried to send a message of 546281442 bytes». **Un hijo hereda los descriptores del padre,
y los del padre SON la tubería de native messaging**: `mpv` escribía su salida ahí y el navegador la
leía como un marco. Arreglado en tawasuyu (`2e99d216`, pineado acá) con tres redirecciones, cada una
contra un fallo distinto — `stdin` a null porque mpv LEE stdin y se comería los mensajes del
navegador; `stdout` al stderr del host y no a `/dev/null`, porque apagarlo arreglaría el bug y se
llevaría puesto el diagnóstico; `stderr` heredado por lo mismo.
Y dos cosas del ARNÉS, las dos ya vistas antes en esta misma sesión:
· la URL NO se pasa por la línea de comandos: una petición del arranque compite con la
inicialización de la extensión (la misma carrera del guardián de `sct`). El guardián navega a una
página que redirige a los 3 s, que además es lo que hace una persona: seguir un enlace;
· la evidencia de que el reproductor CORRIÓ se le pide a su propio `log-file`, porque su salida ya no
va al stdout del host y Gecko no vuelca el stderr del host al del navegador. Sin eso sólo se sabría
que hubo un `spawn`.
Del §6 quedan el foco (6.5), la IA local (6.7) —que además es la que traería el motor que le falta al
archivo del §6.3— y el torrent (6.9). Medido de paso: **el corpus no tiene ninguna receta de LLM ni de
embeddings**, así que 6.7 hoy no se puede pagar.
Medido sobre `atuq b3:a7080058` y `puriy-costura b3:060314e6`.
La unidad 8 del SDD 26 (§6.3), en su mitad pagable. Cada página que se lee se congela: el HTML **ya
ejecutado** —lo que la persona vio, no lo que mandó el servidor— va al CAS con su BLAKE3, y la url,
el título y el texto visible al índice. Después se busca, sin el navegador.
visita 1 (página A) ⇒ archivada, visitas=1, total=1
visita 2 (página A) ⇒ dedup, visitas=2, total=1 ← volver NO duplica
visita 3 (página B) ⇒ total=2
«masa» → 1 de 2 · «tobera» → 1 de 2 · «masa tobera» → 0 · «helicóptero» → 0
LA IDENTIDAD ES EL BLAKE3 DEL HTML, NO LA URL, y de ahí sale lo que hace que esto valga: la misma URL
con otro contenido ES otra página, así que el archivo tiene VERSIONES de lo que leíste. Un historial
de direcciones guarda punteros, y los punteros se pudren.
⚠ LA MITAD QUE NO SE PAGA HOY, dicha en el código, en el LEEME de allá y en el §6.3.bis: preguntarle
al historial en LENGUAJE NATURAL. Eso es el registro semántico y en la suite ya tiene forma —un motor
`rag-motor::RagMotor`, como `willay-rag`—, que necesita un daemon de embeddings y un backend LLM real
y devuelve `None` cuando no están. `archive.search` es el registro LITERAL: todas las palabras tienen
que aparecer. Enseñar lo uno diciendo que es lo otro sería el peor cambio posible.
LO QUE NO SE ARCHIVA ES LA PARTE QUE HAY QUE MIRAR: nada de una ventana privada, nada fuera del marco
principal (un `<iframe>` de publicidad no es una página que alguien leyó) y nada sin texto visible (el
HTML de un visor de PDF llenaría el archivo de cosas que no se encuentran). Y si no se puede saber si
la pestaña es privada, NO se archiva.
⚠ Y EL CONTROL NEGATIVO NO SÓLO COMPRUEBA QUE NO SE ARCHIVE: DICE QUIÉN LO IMPIDIÓ. La respuesta
medida no es la que uno supondría — hoy lo impide **el navegador**, porque las extensiones no corren
en ventanas privadas salvo que se las habilite, así que nuestro `if (incognito) return` no se ejecuta
nunca. No es código muerto: es lo que haría seguro habilitar `private_browsing` el día que haga
falta. Lo que sí habría sido un error es escribir «la extensión no archiva lo privado» sin saber cuál
de las dos cosas estaba pasando.
DOS TECHOS CON NOMBRE (en el host): 4 KiB de texto por página en el índice —es JSON para poder leerse
con `cat`, y uno sin techo deja de poder— y el corte por CARÁCTER y no por byte, porque `truncate`
sobre medio multibyte entra en pánico y en un archivo personal el texto con acentos es el caso normal.
Del lado de tawasuyu (`357791a8`, pineado acá): `archive.add`/`archive.search`, 6 tests nuevos (20 en
total) y el CAS renombrado de `descargas-cas` a `cas` — el mismo almacén guarda ahora lo bajado y lo
leído, y un nombre que describe la mitad de su contenido es el que se lee mal dentro de seis meses.
Medido sobre `atuq b3:5cfe3221` y `puriy-costura b3:c06f55aa`.
La unidad 7 del SDD 26 (§6.2). Cuando una descarga TERMINA, la extensión
`descargas@atuq.tawasuyu` le pasa al host la ruta, el nombre y de dónde salió; el host la ingiere a
un CAS BLAKE3 y contesta el hash, el tamaño y **cuántas veces se bajó ese mismo contenido**. El mismo
contenido con otro nombre es UN objeto y dos nombres, y el navegador lo dice — que es exactamente lo
que ningún navegador sabe hacer: para todos una descarga es un blob con nombre, y por eso la bajás
dos veces y no te enterás.
No es un almacén nuevo: es `arje-cas`, el mismo formato y el mismo hash que usan arje, takana y
tejido. El `<hex>` del CAS ES el `expected_hash` de un `.swm`.
⚠ TRES DECISIONES QUE SALIERON DE MEDIR, NO DE DISEÑAR:
1. **La raíz del CAS de descargas no es la del sistema.** `arje_cas::gc` borra todo blob que no esté
en el set `reachable` de su llamador, y el único que existe (`arje-brain`, `GcCas`) lo arma con la
cadena de audit y las raíces vivas del grafo. Una descarga del usuario no está en ninguno: el
primer GC se la llevaría, en silencio. Van a `<estado>/descargas-cas` — mismo formato (mover un
objeto al CAS del sistema es un `rename`), fuera del alcance del GC de otro. El precio queda
escrito: tejido sirve el CAS por defecto, así que compartir una descarga hoy exige apuntarlo ahí.
2. **El fichero del usuario no se toca**: se copia y se deja donde estaba. El guardián lo comprueba
byte a byte. Borrar o mover lo que alguien acaba de bajar no es decisión de un navegador.
3. **La extensión observa, no intercepta.** Se entera cuando la descarga terminó. Meterse en el medio
obligaría a decidir qué pasa si el CAS falla, y la respuesta correcta —que la descarga siga igual—
es lo que se consigue no metiéndose.
Y EL HALLAZGO QUE COSTÓ LA TARDE, que no es de esta unidad: **en `--headless` el navegador se cae con
SIGSEGV en cuanto una descarga termina.** Se atribuyó con dos controles antes de tocar nada:
--headless, con la extensión → baja el fichero, TERMINADA, Segmentation fault (139)
--headless, SIN la extensión → baja el fichero, Segmentation fault (139) ← no es nuestro
--headless, panel de descargas apagado → Segmentation fault (139)
sway headless (compositor REAL) → baja, INGIERE al CAS, y no se cae ← no es del producto
Sin el primer control esto se leía como «la extensión de descargas rompe el navegador» (perseguir un
bug que no existe); sin el último, como «atuq no puede descargar» (reportar un bug de producto que
tampoco existe). Por eso `scripts/test-atuq-descargas.py` corre sobre sway y no sobre `--headless`, y
por eso lo dice en su cabecera: un arnés distinto al de los demás guardianes, sin explicación, es una
invitación a «simplificarlo» de vuelta al que se cae. Queda en el §6.10.bis del SDD, que es donde vive
lo que atuq NO puede hacer.
El guardián baja de verdad (`Content-Disposition: attachment`, no una llamada a la API) dos veces
dentro de un mismo compositor, y trae control negativo: con OTRO contenido la segunda vez exige
`dedup=false` y dos objetos. Sin él, una sonda que dijera «ya lo tenías» siempre se vería idéntica a
una que funciona.
Del lado de tawasuyu (`ffa939c6`, pineado acá): `cas.ingest`/`cas.list` en el host y
`arje-cas::almacenar_fichero_en` — ingesta en streaming, porque `store` toma `&[u8]` y una ISO de
4 GiB serían 4 GiB de `Vec`. 5 tests nuevos en arje-cas y 5 en puriy-costura.
Y EL BUG QUE EL GUARDIÁN DESTAPÓ, que vale más que la función: **hay un host por PUERTO, no uno por
perfil.** Con `sct` y `descargas` hablando hay dos procesos vivos a la vez, cada uno con su copia en
memoria del estado, y cada uno escribía el fichero ENTERO al guardar: el de descargas borraba el
registro TOFU de `sct` al salir, y el de `sct` revertía el índice de descargas. Los dos contestaban
bien — el daño estaba sólo en el disco. Se vio porque el guardián lee el índice EN DISCO en vez de
creerle a la respuesta: decía `veces=2` y el fichero decía 1. Arreglado en tawasuyu (`55b918e8`,
pineado acá): cada proceso escribe sólo la parte que tocó, y el test que lo fija se comprobó
ROMPIENDO el arreglo a propósito — porque la primera versión de ese test abría los hosts en secuencia
y pasaba con el bug puesto.
La unidad 6 del SDD 26, que es el diferenciador del §6 que no tiene ningún navegador. Cadena entera,
medida de punta a punta con un servidor HTTP real y seis cargas de página:
servidor HTTP → filterResponseData → connectNative → /usr/lib/mozilla/native-messaging-hosts/
→ puriy-costura --state → puriy-sct (TOFU + bitácora)
carga 1-3 (mismo script) fase=learning eventos=0 insignia vacía
carga 4 (mismo script) fase=stable eventos=0 insignia vacía
carga 5 (mismo script, estable) fase=stable eventos=0 insignia vacía ← control
carga 6 (script CAMBIADO) fase=stable eventos=1 insignia "1"
EVENTO ext:…/app.js 0ff4771fb797→f7ccb9fedfbd +27B
La extensión NO hashea ni guarda nada: ve bytes y pregunta. El registro es `puriy-sct`, del otro lado
del cable — duplicarlo en JS habría sido un segundo registro que se desalinea del primero, y el
primero es el que está certificado sin red.
CINCO COSAS QUE SE MIDIERON EN VEZ DE SUPONERSE, y las cinco fallan calladas:
1. el manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/` y NO en el appdir: la ruta sale de
`XRESysNativeManifests`, un `/usr/lib/mozilla` COMPILADO dentro de Gecko;
2. **el manifiesto no puede llevar argumentos** — `NativeMessaging.sys.mjs` hace
`command = manifest.path` y los únicos argumentos son `[ruta-del-manifiesto, id]`. Y sin `--state`
el host corre en MEMORIA: cada arranque volvería a «aprendiendo» y nada alertaría nunca. De ahí el
lanzador `bin/puriy-costura-host`, que además decide la ruta del estado — dónde vive el estado de
un usuario es layout del FHS, o sea asunto de la distro y no del crate;
3. `filterResponseData` y el permiso `webRequestFilterResponse` SÍ están en nuestro `omni.ja`
(se le preguntó al artefacto, no a la documentación de Mozilla);
4. los scripts `inline` NO se ven por esta vía —`filterResponseData` entrega el cuerpo de una
PETICIÓN— y para v1 alcanza: el ataque que sct nombra es la sustitución en el CDN;
5. ⚠ **una carga de página puede producir dos peticiones del mismo documento, y una llega con
`tabId = -1`.** La primera versión agrupaba por `(tabId, documento)` y contaba esa carga como DOS
visitas. No es cosmético: inflar las visitas estabiliza el origen ANTES de conocer su código real,
y entonces alerta por churn legítimo — el falso positivo que la spec de puriy-sct pide evitar por
encima de todo. Ahora agrupa por documento (dos pestañas con la misma url cuentan UNA: es el error
seguro, tarda más en proteger y no alerta de más) y el guardián VIGILA el invariante «una carga,
una visita», así que si vuelve, falla ruidoso.
DOS CORRECCIONES DEL PROPIO §6.1, que decía «consulta al testigo antes de dejarla pasar»: el cable
del testigo es un POST con postcard, así que un JS no puede ser su cliente; y v1 OBSERVA Y AVISA, no
bloquea — es lo que puriy-sct dice de su propia v1, y poner un viaje entre procesos en el camino
crítico de cada script de cada página no es «más seguro», es un navegador que nadie usa.
EL AVISO SE MIDE, NO SE SUPONE: la extensión relee la insignia con `getBadgeText` después de ponerla,
y el guardián exige vacía en las cinco cargas sin novedad y "1" en la del script cambiado. Es la única
parte de la cadena que el usuario ve; dejarla en «se llamó a la API» era dejar sin medir el final.
CONTROL NEGATIVO: `--negative-control` borra el manifiesto y exige que NO haya veredicto — o sea que
el veredicto de la corrida positiva viene del host y no de la extensión inventándolo.
Y el aviso pasivo es decisión, no falta de tiempo: insignia y tooltip, no modal. Un modal por cada
despliegue de un sitio entrena a la gente a cerrarlo sin leer, y entonces el que importa también se
cierra.
Además: `rebrand.py` instala y CRUZA los manifiestos nativos (que el `path` exista y sea ejecutable
dentro del artefacto, y que sus `allowed_extensions` sean extensiones que de verdad empaquetamos), y
de paso se corrige el comentario del §4.ter que repetía la afirmación falsa sobre quién instala las
extensiones — lo mide `scripts/test-atuq-instalacion.py`: instala el escaneo de la carpeta.
`runtime = [..., "puriy-costura"]` en atuq.toml: sin eso la imagen llevaría manifiesto y extensión y
el host NO estaría, y la función se apagaría sola sin una línea de error. `yupana radio` confirma que
llega a las cuatro imágenes de escritorio.
⚠ Límite escrito en `fondo.js`, en el `lib.rs` del host y en los dos LEEME: se hashea el TEXTO ya
decodificado que entrega la extensión, no los bytes que sirvió el servidor. Vale para comparar dos
cargas nuestras; NO es comparable con el hash que publique un tercero sobre los bytes servidos, ni
con el de la v2, que engancha el script loader y ve los bytes reales.
Y una segunda cosa que el guardián encontró y que es del PRODUCTO, no del test: **la página que abre
el navegador al lanzarse puede no ser observada** — compite con la inicialización de la extensión, y
la carrera se gana o se pierde según la corrida. En uso real sólo afecta a esa primera página (después
la extensión ya está escuchando). Por eso las aserciones van sobre la SECUENCIA OBSERVADA y no sobre
un calendario: se exige que ninguna carga se observe dos veces, que la única que puede faltar sea la
del arranque, y que la secuencia aprender→estabilizar→no-alertar→alertar sea la correcta.
Sin regresiones: `test-atuq-politica.py` («guardianes: todos correctos», y sus cinco roturas siguen
matando el build con tres extensiones) y `test-atuq-inicio.py` (la home y la pestaña nueva siguen
siendo las nuestras) pasan sobre el artefacto final `b3:d3ced586`.
Cierra la fila 5 del plan con lo que hay: `puriy-costura` + `shared/foreign-webext` en tawasuyu
(`ebe41e96`, 14 tests) y `recipes/puriy-costura.toml` acá (`b3:3b633431`, verificado como artefacto:
contesta un marco de 4 bytes y hace la promesa del §6.1 completa).
El §7.quater deja tres cosas que no estaban escritas:
· POR QUÉ HACÍA FALTA el host y no se podía atajar: el §6.1 decía que la extensión «consulta al
testigo», y el cable del testigo es un POST con postcard, no JSON — un JS no puede ser su cliente.
Reimplementar el TOFU en la extensión sería el sustituto paralelo que la regla 10 de tawasuyu
prohíbe, además de duplicar la única pieza ya certificada.
· CÓMO SE ELIGIÓ EL NOMBRE, con el grep de la regla 10 hecho ANTES: ningún término del dominio
significaba esto, y los que suenan a mensajero están ocupados por dominios ajenos y grandes
(chasqui = type broker, chaka = puente a COBOL, paloma = correo, tampu = común de objetos). El
nombre salió del título de este propio §7: la costura.
· LOS DOS LÍMITES, que van en el código y en los LEEME y no en una nota de sesión: se hashea el texto
ya decodificado (vale para detectar, no es comparable con el hash de un tercero ni con el de la v2),
y la semilla del log sale del directorio de estado ⇒ a prueba de reescritura, no de suplantación.
Y lo medido para la unidad 6 preguntándole al artefacto: `filterResponseData` y el permiso
`webRequestFilterResponse` SÍ están en nuestro omni.ja ⇒ la extensión podrá leer el cuerpo de un
script. Los inline NO se ven por esa vía, y para v1 está bien: el ataque que sct nombra es la
sustitución en el CDN, o sea el caso `<script src>`.
El binario de «la costura» (SDD 26 §7) entra al corpus: `b3:3b633431`, ELF estático musl de 952 K
pineado a tawasuyu `ebe41e96`. Verificado como artefacto y no sólo como build — un marco de 4 bytes
por stdin y contesta `{"id":1,"ok":true,"verb":"ping","version":"0.1.0"}`; y la promesa del §6.1
entera contra el binario SELLADO: cuatro visitas aprendiendo, a la cuarta `stable`, y en la quinta un
script cambiado ⇒ un evento que nombra el recurso y el delta de tamaño (+6 bytes). Deja
`registro.postcard` y `bitacora.postcard` en el `--state`.
Las dos cosas que costaron, las dos con el mismo patrón (el error no nombra la causa):
1. `cargo vendor --locked` murió con «cannot update the lock file», que NO dice qué crate falta. El
lock de tawasuyu estaba desalineado con su propio `main`, y —esto es lo reutilizable— **un
`Cargo.lock` regenerado en ese clon compartido NO describe su `main`**: ese árbol tiene cientos de
ficheros en vuelo de otras sesiones. El nombre del crate culpable salió de diffear el lock
publicado contra el que resuelve un árbol LIMPIO del commit (`git archive <sha> | tar -x`, que
además no toca el `.git` compartido). Arreglado allá en dos commits; acá se pinea el que cierra.
2. Después murió por el `vendor/` propio del monorepo: tawasuyu parchea `smithay` a mano y lo enchufa
con `[patch.crates-io] path = "vendor/smithay"`. Un patch por RUTA no necesita
`.cargo-checksum.json` —y upstream no lo commitea—, pero `cargo vendor` convierte su directorio de
salida en un directorio de REEMPLAZO DE FUENTE, donde cada crate sí tiene que traerlo. El síntoma
fue 1 de 1699 crates sin checksum y un error que hablaba de `taffy`. La solución ya estaba en el
corpus: `cargo_vendor_dir`, que las otras cinco recetas de este monorepo ya llevan.
⚠ Y la explicación FÁCIL era falsa: «cargo vendor borró el checksum» — `git ls-tree` dice que ese
fichero nunca existió. Queda escrito en la receta para que nadie lo vuelva a deducir.
El precio, medido y anotado: el vendoreo es del workspace ENTERO (2,4 G, 1699 crates, `axum` y
`aws-lc-sys` incluidos) para un host que usa quince deps. Mismo precio que ya pagan las otras cinco.
NO se declara en ningún perfil de `targets.toml` todavía, y NO se agrega el manifiesto de native
messaging a atuq: sin la extensión que lo llame (unidad 6) sería un binario que nadie invoca y un
manifiesto apuntando a una extensión inexistente, que es un fallo silencioso — el navegador arranca
igual y la función no está.
Medido recorriendo los `.service` de D-Bus de TODO el store: los únicos backends de portal sellados
eran los de COSMIC y GNOME. Ninguno de KDE, y la cola `incoming-kde` no tenía ni el frontend.
Consecuencia concreta: la imagen KDE trae `obs-studio` con su plugin linux-pipewire, que captura
pantalla POR EL PORTAL — o sea que en Plasma no tenía con quién hablar mientras en los otros dos
escritorios ese frente estaba cerrado. Sin portal tampoco hay diálogo de fichero ni compartir
pantalla para nada que los use.
Van los DOS paquetes porque la cadena es de tres:
cliente → org.freedesktop.portal.Desktop [frontend] → org.freedesktop.impl.portal.* [backend]
El frontend se copia de la cola de COSMIC y **comparte artefacto**: hash idéntico (ecbe13f68eca6)
desde las dos colas ⇒ un solo directorio en el store, cero builds.
El backend es receta NUEVA, 6.7.2 — la MISMA versión que plasma-workspace, kwin y kscreenlocker de
esta cola, porque habla interfaces privadas de kwin para el ScreenCast y desalinear ahí es pedir que
dos mitades del mismo release se entiendan por casualidad. **Cero frontera nueva**: sus 22
dependencias (Qt6, KF6, KWayland, protocolos de Wayland, xkbcommon) ya estaban todas, verificadas
una por una antes de escribir la receta.
⚠ SIN EL PORTAL DE IMPRESIÓN, y la causa es más honda que el error. El build moría en
`src/print.cpp:27` con `'QtPrintSupport/private/qcups_p.h' file not found`, y la cadena está medida
entera: no hay NINGUNA receta de cups en el catálogo ⇒ qtbase 6.11.1 se construyó sin CUPS ⇒ su
`QtPrintSupport/private/` trae qpaintengine_alpha_p.h y qprintengine_pdf_p.h pero NO qcups_p.h.
No falta una cabecera: falta el subsistema. Un portal de impresión sin con qué imprimir no es
funcionalidad que se pierde, es código muerto que no enlaza.
Upstream no expone `option()` para portales —se compilan todos—, así que el parche va en TRES
sitios y no en uno: la fuente, la instanciación (desktopportal.cpp/.h) y **la lista de interfaces
que el backend ANUNCIA**. El tercero es el que se olvidaría y el que importa: anunciar
`impl.portal.Print` sin implementarlo hace que el frontend enrute a un portal que no contesta, que
es peor que no tenerlo. Cada `sed` lleva su `grep` de verificación al lado, para que el día que
upstream mueva esas líneas el parche falle RUIDOSO en vez de volverse inerte.
VERIFICADO sobre el artefacto sellado: publica `org.freedesktop.impl.portal.desktop.kde.service`,
y su `kde.portal` anuncia 17 interfaces con **0 apariciones de Print** e incluyendo ScreenCast,
Screenshot y RemoteDesktop, que es exactamente lo que OBS necesita. Y sobre el rootfs hidratado:
están el frontend (`org.freedesktop.portal.Desktop.service`), el backend y el `kde.portal`.
⚠ Y la licencia casi la firmo mal: leí la cabecera de UN fichero y generalicé. Contadas las
etiquetas SPDX del árbol entero: 88 `LGPL-2.0-or-later`, 23 de la fórmula de KDE e.V. y 1
`GPL-2.0-or-later`. El binario las enlaza a todas ⇒ la expresión es la CONJUNCIÓN.
El perfil pasa a 98 raíces, 361/361 listo, deuda 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
El README de la receta afirmaba (a) que las extensiones las instala la POLÍTICA porque el
sideloading desde `distribution/extensions/` «ya no funciona», y (b) que el chrome de verdad
—split view— exige entrar a `omni.ja` y escribirlo nosotros. Las dos son falsas, y ninguna
observación del artefacto las distinguía: hubo que romper un mecanismo por vez.
scripts/test-atuq-instalacion.py — tres escenarios:
as-is nada roto ⇒ las dos extensiones puestas
no-policy `policies.json` sin ExtensionSettings ⇒ SIGUEN puestas
policy-only los XPI fuera de la carpeta ⇒ NINGUNA, y la home vuelve a about:home
⇒ instala el ESCANEO de la carpeta; `install_url` con `file://` no instala nada. El §7.bis del
SDD 26 tenía razón. El control es por construcción: la sonda vive en la carpeta y ninguna política
la nombra, así que una corrida muda se declara ROTA en vez de leerse como «no instaló». Y la
segunda señal que probé NO discrimina, queda dicho: el `location` de `extensions.json` sale
`app-profile` también cuando instala la carpeta.
scripts/test-atuq-chrome.py — el chrome se programa desde `atuq.cfg`, sin abrir el zip: observando
`browser-delayed-startup-finished` se toca el `gBrowser` de cada ventana. Y la vista dividida ya la
trae el motor (fx 154) PRENDIDA de fábrica, ejercitada de verdad —`addTabSplitView` ⇒
`activeSplitView` + 2 navegadores—, no «el fichero está». Dos pendientes que eran de upstream.
Control negativo del propio motor: con las pestañas fijadas devuelve null (WRAPPER null, ACTIVA no).
Corolario para lo que venga: cada función del chrome que NO entre a `omni.ja` es una que no pelea
con el orden del `jarlog` del PGO — que es justo lo que tiene al jarlog aparcado.
Queda escrito lo que NO está: ninguno de los `test-atuq-*` corre en el latido (sólo lo hace
`vigia-sonames.py`), y meterlos cuesta ~3 min por ciclo más el rootfs hidratado en el hub.
Medido sobre atuq b3:fab2fbfb → b3:8f6d09c2 (el README entra en el hash: documentar la medición
re-hashea el artefacto medido, y los guardianes se niegan a medir uno que no sea el vigente).
ADR 0016 los listaba entre los CONGELADOS; el usuario pidió descongelarlos al abrir el SDD 28. La
enmienda queda escrita en el propio ADR, que si no la etiqueta deja de describir el hecho.
`hammer-install.sh` → `takana-install.sh`, `hammer-live-install.sh` → `takana-live-install.sh`,
`hammer-banner.txt` → `takana-banner.txt`, `BRIEFING-hammer.md` → `BRIEFING-takana.md`.
**Lo que hacía caro esto no es el nombre del fichero.** El instalador se inyecta en el ISO como
`/usr/bin/hammer-install` y su éxito se detecta con un `grep` de `HAMMER-INSTALL-OK` desde TRES
scripts de prueba. Renombrar un solo lado los deja casando NADA — sin fallar —, que es literalmente
el modo en que `atribuir-fallos.py` quedó mudo cuando el renombre movió el target de `tracing`.
Se renombraron las dos puntas en el mismo commit (`/usr/bin/takana-install`,
`TAKANA-INSTALL-OK/FAIL`, `TAKANA_INSTALL_*`, `work/takana-install.img`, `/run/takana-install`),
se comprobó por `grep` que no quede ningún token viejo fuera del ADR, y —lo que decide— se CORRIÓ
`install-tui-test.sh`: 4/4 casos verdes.
Las tres `TAKANA_INSTALL_*` caen al nombre viejo (`${TAKANA_INSTALL_X:-${HAMMER_INSTALL_X:-}}`):
el llamador puede ser un ISO anterior al renombre. Misma convención que `TAKANA_ROOT_PW` unas
líneas más arriba en ese mismo script.
NO se tocó `/usr/sbin/hammer-recover` ni su hook de arranque —renombrarlo rompe máquinas YA
INSTALADAS, no el repo—: sobrevive intacto dentro del script renombrado, verificado por conteo
antes y después (8 ocurrencias). Tampoco `hammerd`, `hammer-edit` (su `name` está en la ruta del
store), `/var/lib/hammer`, `HAMMER_LIVE` ni los siete literales de hash.
De paso: `scripts/.hammer-banner.txt.kate-swp` era un swap de editor commiteado por error. Fuera.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
El frente arranca con el inventario MEDIDO de gioser, no con un plan: PID 1 es `arje-zero` (nuestro
init ya gobierna un servidor real, aunque sobre userland Artix), Hetzner Cloud arranca por **BIOS**
—que es justo el layout que `takana-install.sh` ya escribe— y el kernel propio trae VIRTIO/EXT4 `=y`
con `netup` haciendo DHCPv4, que es exactamente el caso de una caja hcloud.
Dos hallazgos que cambian el trabajo:
- **La secuencia de arranque de gioser no está escrita en ningún lado.** `rc-status` dice `stopped`
para caddy, gitea, sshd, cronie y act-runner y los cinco están VIVOS colgando de PID 1. Un
`arje-absorb --from openrc` produciría una Semilla perfecta que no levanta el servidor.
- **La mitad del Caddyfile describe un servidor que ya no existe**: 4 de 19 dominios sin DNS y sin
ficheros en `/var/www`, uno en 502. Una mudanza fiel copia también la basura ⇒ la regla queda
escrita: no se muda lo que no responde.
`[perfil.servidor]` hereda `cli` (SDD 27 §7.1) y declara 5 raíces que existen (openssh, caddy, curl,
wget, tmux) y **5 que NO** (takana, chrony, nftables, cronie, logrotate). Quedan `wanted`, y `wanted`
no es `debt` ⇒ `drenaje.json` seguirá diciendo `deuda=0`. Se declaran igual: sin ellas el perfil
saldría N/N —completo y verde— describiendo un servidor sin hora, sin cortafuegos y sin latido. Es
la lección de `foot` en escritorio-sway, pagada por adelantado. Cuenta esperada: 5, verificada
resolviendo cada nombre contra el campo `name` de las 869 recetas.
La peor de esas cinco: **takana no tiene receta**. El corpus construye 869 y no la suya, así que el
host no puede instalar takana desde el repo de takana — el bloqueante de que el servidor se sirva a
sí mismo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
El usuario preguntó si va a poder ejecutar kate desde mirada o si kate «está para KDE y no se
hereda». La pregunta separa dos cosas que hoy comparten un solo mecanismo: qué CONTIENE un
metapaquete (lo que resolvió el §7.1) y qué PUEDE CORRER en un sistema.
MEDIDO sobre la clausura de kate: son 110 recetas, y lo único que suena a Plasma —kwindowsystem,
plasma-activities, plasma-wayland-protocols— son LIBRERÍAS. No aparece plasmashell, ni kwin, ni
plasma-workspace. O sea que kate necesita Qt6 + KF6 + un compositor, y mirada TIENE compositor: que
hoy no corra ahí no es técnico, es que no está instalado. Coste medido: mirada son 41 nodos, kate
110, ya coinciden 24 ⇒ faltan 86, que es traerse Qt6 y KF6. Ese precio no lo cobra KDE, lo cobra Qt.
Y la distinción YA EXISTE en el repo sin estar nombrada: mpv, atuq, swayimg y libnotify están
declaradas en los CUATRO escritorios —repetidas, no heredadas—, zathura en tres, foot en dos. El
repo ya trata las apps distinto de kwin, sólo que copiando la línea en cada perfil.
Propuesta escrita: que el metapaquete del escritorio sea el ESCRITORIO (shell, compositor, tema,
ajustes) y que las apps salgan a metapaquetes propios instalables desde cualquier sistema, de modo
que `takana install kate` funcione desde mirada sin instalar Plasma. Se lleva bien con el §8: con
vistas montables, «tengo KDE y GNOME y elijo en el greeter» y «uso kate dentro de GNOME» dejan de
ser dos problemas y pasan a ser uno solo — qué clausura se compone para esta sesión.
Cuesta mover ~20 declaraciones de targets.toml y no toca ninguna receta ni ningún artefacto: es
reclasificar, no reconstruir.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
Paso 1 del SDD 27. Resultó ser UNA LÍNEA POR PERFIL: el mecanismo `hereda` ya existía en
`scripts/targets.py` —transitivo, con detección de ciclos y orden preservado— y `escritorio-sway`
ya lo usaba desde el 2026-08-07. Los otros tres nunca lo declararon, y eso no era una decisión: era
composición no declarada.
LO QUE ARREGLA, medido antes: de los 27 paquetes del perfil `base`, la clausura de escritorio-kde
alcanzaba DOS. Contra el rootfs real: sin `bash`, sin `sudo`/`doas`, sin `git`, sin `useradd`, sin
`gpg` y **sin `dhcpcd`** —o sea sin con qué pedir una IP—; y lo que parecía estar (`ip`, `mount`,
`fsck`) eran applets de busybox (`/sbin/ip → ../bin/busybox`).
DESPUÉS, verificado hidratando de verdad y no leyendo el grafo: en el rootfs aparecen `bash`,
`sudo`, `doas`, `git`, `dhcpcd`, `useradd`, `gpg`, `rg`, `fd`, `bat`, y `/sbin/ip` pasa a ser el
BINARIO de iproute2 en vez del applet.
raíces kde 49→96 · gnome 39→86 · cosmic 43→88
nodos kde 301→356 · gnome 204→256 · cosmic 162→216
deuda 0 en los tres — no hubo que construir NADA, ya estaba todo sellado
colisiones nuevas al hidratar: CERO (los 28 .hammer-tmp son los de gmp/mpfr de antes)
⚠ `escritorio-mirada` NO hereda a propósito: es el rootfs *slim* del USB y sumarle 47 raíces
contradice su razón de ser. Si algún día se quiere, es la misma línea.
`kde-rootfs` en el volumen se rehidrató con la herencia (11 G) para que el nombre canónico no quede
viejo — que es el error que este mismo día costó encontrar en GNOME y COSMIC. El rootfs FUNDIDO y la
imagen siguen siendo los de antes: rehacerlos es un paso aparte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
El usuario señaló que «perfil» suena a preset exclusivo y preguntó lo que hay que preguntar: si no
va a poder instalar KDE, GNOME y mirada a la vez y elegir en un greeter.
MEDIDO, comparando los dos rootfs hidratados que hay en disco (kde-rootfs 78.934 ficheros,
gnome-rootfs 19.955): 14.455 rutas están en LOS DOS, y de ésas **12.766 comparten INODO** — o sea
que son el mismo artefacto del store, no un conflicto. Sólo **1.689 (12 %) colisionan de verdad**.
⇒ Lo que impide instalar dos escritorios NO es el store, que ya los tiene conviviendo: es
proyectarlos al MISMO /usr. Y las colisiones no son misteriosas —libuuid.so.1.3.0, dbus-daemon,
gdbus, gapplication, /etc/dbus-1/*.conf— salen de que cada cola tiene su propia glib y su propio
dbus. Es el mismo cuadro de las dos poppler que targets.toml ya documenta, y el de gmp/mpfr que
dejó 28 .hammer-tmp al hidratar KDE hoy.
Dos caminos, no excluyentes: bajar las colisiones unificando lo duplicado (una glib, un dbus), o no
proyectar a un /usr único — que es literalmente lo que el SDD 18 ya propone («activar un nodo =
montar su composefs como /usr»). Con vistas, las 1.689 dejan de existir por construcción. El
greeter encaja solo: el arranque por grafo del ADR 0010 ya elige qué clausura activar.
Y queda anotado el corolario de orden: el paso 5 del plan (unificar imagen e instalador) construiría
sobre el supuesto de un solo escritorio a la vez. Hay que decidir FHS plano vs vista composefs ANTES
de unificarlos, porque es la diferencia entre «instalás uno» y «instalás los tres».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
PROPUESTA, nada implementado. Escrito a pedido del usuario mientras se tapaba el hueco de upower.
⚠ EL HALLAZGO QUE LO MOTIVA, y es peor de lo que parecía: `targets.toml` ya avisaba que
«kde/gnome/cosmic NO heredan base», pero la consecuencia no estaba escrita en ningún lado. Medida:
**de los 27 paquetes del perfil `base`, la clausura de escritorio-kde alcanza 2**. Y comprobado
contra el rootfs real de la imagen: NO hay `bash`, ni `sudo`/`doas`, ni `git`, ni `useradd`, ni
`gpg`, **ni `dhcpcd`** — o sea que la imagen no sabe pedir una IP. Lo que sí hay (`ip`, `mount`,
`fsck`) son APPLETS DE BUSYBOX: `/sbin/ip → ../bin/busybox`.
O sea que la respuesta a «¿abrir un escritorio es como si base no existiera?» es **sí**. Y la causa
es que hay DOS cosas llamadas base que nadie había distinguido por escrito:
· base de ARRANQUE (`metal-rootfs`): arje-zero + busybox. La imagen la funde. Existe.
· base de USERLAND (`[perfil.base]`): las 27. No la funde nadie.
El documento separa las cuatro capas (piso / escritorio / extras / toolbox CLI), explica que un
«metapaquete» YA EXISTE y se llama perfil, y nombra las tres cosas que le faltan para ser
instalable: herencia entre perfiles, publicación al repo firmado, y un verbo que resuelva una LISTA
y no un paquete.
Y plantea la única decisión que no es de implementación: instalar un escritorio REPRODUCIENDO
(coherente con que el canal no distribuya binarios, pero son horas en la máquina del usuario) o
HIDRATANDO artefactos firmados (segundos, pero es entregar binarios y activa entera la obligación
de licencias que se cerró hoy). La salida propuesta es la del ADR 0014: que existan las dos y que
reproducir sea lo que hace verificable a hidratar.
Nota de nomenclatura, porque el usuario preguntó: `.swm` NO es residuo del renombre — significa
«Software Mutación» y describe lo que el fichero ES. El verbo ya es el correcto, `takana install`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
Primero de los dos huecos que dejaban a KDE POR DEBAJO de una instalación estándar. El perfil
declaraba `powerdevil` —el gestor de energía de Plasma— pero no a `upower`, que es con quien
powerdevil habla para leer la batería y aplicar perfiles. Medido: upower sólo existía en la cola de
GNOME, y las colas no se ven entre sí (HERMANO→corpus, nunca de reojo). En un portátil eso es un
escritorio sin indicador de batería, y el arranque no falla ⇒ no se ve hasta usarlo.
Cuesta TRES recetas y no siete, y la diferencia es una perilla: `-Dintrospection=disabled`.
GNOME la necesita ENCENDIDA porque su shell hace `imports.gi.UPowerGlib` desde JavaScript, o sea que
carga el typelib. powerdevil NO: habla D-Bus con `org.freedesktop.UPower`. Apagarla saca de la
frontera gobject-introspection, gi-foreign-girs, glib-introspected y py3-setuptools, que sólo viven
en la cola de GNOME.
⚠ Y NO SE PROMUEVEN AL CORPUS, aunque era lo primero que intenté. Al mover `libgudev` allá su
ArtifactHash CAMBIÓ (7404bc8d → 87ca5e73) y eso no es una mudanza: `glib` existe en las DOS colas y
son recetas distintas, así que desde `incoming-gnome/` se construye contra la glib de GNOME y desde
el corpus contra la del corpus. Promoverla habría arrastrado a GNOME a la glib del corpus — el
cuadro de las «dos glib» que este repo ya pagó caro. Revertido y verificado que vuelve al hash de
antes (7404bc8d), o sea GNOME intacto. Se COPIAN a la cola de KDE.
`udev-pc` sí comparte artefacto (sus deps son sólo pkgconf ⇒ mismo hash desde las dos colas, un
solo directorio en el store). `libgudev` es otro a propósito, por la glib.
VERIFICADO sobre el artefacto sellado, no supuesto: trae `usr/libexec/upowerd`, el
`org.freedesktop.UPower.service` de system-services y su política en `system.d` — que es exactamente
lo que powerdevil necesita para que el demonio arranque por activación D-Bus.
El perfil pasa a 49 raíces, 301/301 listo, deuda 0.
Queda el segundo hueco: KDE no tiene backend de portal (ni `xdg-desktop-portal` en su cola), o sea
que no hay compartir pantalla — y en particular la captura de obs-studio, que la imagen ya trae, no
tiene con quién hablar en Plasma. Eso es más caro: pide traer el portal a la cola y ESCRIBIR
`xdg-desktop-portal-kde`, que no existe como receta en ninguna cola.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
Arrancando la imagen KDE recién regenerada, el serial dijo:
"applications.menu" not found in QList("/etc/xdg/menus")
y el escritorio pintó perfecto igual: fondo, panel, reloj, bandeja. Ése es justo el modo de fallo
que este repo persigue — nada falla, simplemente el menú de aplicaciones no tiene qué mostrar.
⚠ Y EL DIAGNÓSTICO OBVIO ERA EL EQUIVOCADO. Lo primero que anoté fue «falta el fichero, hay que
empaquetarlo». **No falta**: `plasma-workspace` instala `/etc/xdg/menus/plasma-applications.menu`
—verificado en el store Y en el rootfs de la imagen, 9904 bytes—. Lo que faltaba es el PREFIJO:
Plasma construye el nombre como `${XDG_MENU_PREFIX}applications.menu`, y sin la variable busca
`applications.menu` a secas, que no existe con ese nombre en NINGUNA distro con Plasma. El error
nombra un fichero que nunca tuvo ese nombre. La etiqueta contra el hecho, otra vez.
El arreglo es una línea. Lo que no era una línea es DÓNDE: los cinco lanzadores de Plasma exportan
`XDG_DATA_DIRS` y ninguno exportaba éste —comprobado uno por uno—, así que arreglar sólo el de QEMU
habría dejado la misma trampa en metal, metal-sw, el anidado y el headless. Es el cable que este
repo ya vio caer cinco veces en `cosecha-cron.sh` y en la frontera del sembrador: arreglar el caso
y dejar la trampa. Van los cinco.
plasma-start-qemu.sh · plasma-start-metal.sh · plasma-start-metal-sw.sh export
nested-plasma.sh · run-plasma-headless.sh --setenv (arman con bwrap)
VERIFICADO, y digo exactamente qué: que el nombre que Plasma construirá con el prefijo EXISTE en la
ruta donde lo busca (`/etc/xdg/menus/plasma-applications.menu` está en el rootfs de la imagen). Lo
que NO está verificado es el menú renderizado — eso pide reconstruir la imagen y arrancarla otra
vez. El `plasma-start` horneado en el rootfs fundido ya se refrescó con el script corregido, así que
la imagen queda a un rebuild de distancia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
Misma divergencia que se encontró ayer en GNOME, medida igual: `hydrate-cosmic.sh` nombra 25 raíces
escritas a mano dentro del script, el perfil `escritorio-cosmic` declara 43, y **17 paquetes
declarados no llegaban al rootfs**:
adwaita-cursors · xkeyboard-config · bash · atuq (el navegador) · mpv · swayimg
zathura + zathura-pdf-poppler · dejavu-fonts y dejavu-fonts-nerd · libnotify
desktop-file-utils · hicolor-icon-theme · y las cinco *-shared de runtime
⚠ EL PRIMERO NO ES UNA COINCIDENCIA: `adwaita-cursors` es EXACTAMENTE el ejemplo con el que la
cabecera de `scripts/hydrate-profile.py` advirtió de esto el 2026-09-04 — «se añadió a las raíces de
cosmic y sway, y los scripts de hidratación, que no la conocen, habrían seguido armando un rootfs
sin cursores». No era hipotético: seguía pasando hoy, cinco días después. Y `xkeyboard-config` es la
cicatriz de sway repitiéndose: sin él el compositor arranca sin mapa de teclado.
Y AL REVÉS NO SE PIERDE NADA, que es lo que hace el cambio barato y lo separa del caso GNOME (donde
primero hubo que mover 11 raíces de runtime al perfil): de las 25 del script, la única que el perfil
no declara es `zlib-shared`, y la clausura del perfil la alcanza igual. O sea que el motivo que
justificaba la lista a mano —los componentes que `cosmic-session` lanza por PATH, que ningún grafo
de build ve— YA ESTÁ RESUELTO en el perfil, que los declara todos como raíces. La lista quedó
redundante sin que nadie lo notara.
VERIFICADO HIDRATANDO, no leyendo: `hydrate-profile.py escritorio-cosmic` proyecta 162/162 nodos
(24166 ficheros, 4,8 G) y ahí están los 17. (Ojo con la sonda: `bash` instala en `/bin/bash`, no en
`/usr/bin` — mi primer chequeo lo dio por ausente y era el chequeo el que miraba mal.)
⚠ Y UN DATO QUE MATIZA EL DIAGNÓSTICO Y LO EMPEORA. El rootfs que hay en disco
(`escritorios/cosmic-rootfs`, 3,9 G) SÍ tiene cursores, xkb y mpv — o sea que no se armó con la
lista por defecto, alguien le pasó raíces a mano. Y aun así le falta `atuq`. El problema real no es
que la lista esté mal: es que **lo que llega a la imagen depende de lo que alguien se acuerde de
teclear**. Derivarlo del perfil quita esa variable.
El script se deja porque su cabecera documenta por qué COSMIC necesita raíces de runtime explícitas
—sigue siendo cierto sobre la naturaleza de cosmic-session—; lo que ya no es cierto es que su lista
sea la verdad. Los dos armadores de imagen (qemu y metal) ahora apuntan a `hydrate-profile.py`.
── Y de paso, una nota vieja de targets.toml corregida ────────────────────────────────────────────
Decía que el perfil de KDE «son 13 raíces de ARRANQUE EN METAL, no la imagen completa», con
konsole/kate/dolphin y otras 19 apps selladas y sin declarar. Ya no: hoy son 48 raíces, con esas
apps, las fuentes y las *-shared declaradas — el frente KDE lo reordenó y la nota quedó atrás.
De su cola quedan 8 recetas fuera del perfil y son las que corresponde: libICE, libSM, libXtst y los
cinco eslabones huérfanos de xwayland desde que se retiró su receta.
Lo que a KDE le queda es OTRO problema, también medido hoy: su imagen no se hidrata del perfil sino
de un rootfs YA ARMADO (8,4 G) que nadie re-deriva. De las 18 hojas del perfil están 15 — faltan
`obs-studio` y `atuq`, justo las dos añadidas después de esa hidratación. Re-hidratarlo es decisión
del frente KDE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
El ASCII de densidad (#*+@) queda como fallback para consolas sin color; el
default ahora es MEDIOS BLOQUES: `▀` con color de frente Y de fondo, que mete
dos filas de pixeles por renglon de terminal. Trae la placa obsidiana y la celda
de espacio libre que pide la hoja de marca.
Y las JUNTAS entre teselas, que es lo que faltaba para que se pareciera al
simbolo de verdad: en el SVG las teselas son cuadrados de 100 con 8 de aire. La
tocapu son piezas SEPARADAS, no un bloque macizo, y sin la junta el dibujo se
lee como una mancha. Se apagan solas por debajo de escala 3, donde la junta se
comeria media tesela.
Lo que NO se hace, y va escrito: no se interpola ni se suaviza el gradiente.
El simbolo ES una reticula de cuadrados; difuminarlo seria «recolorear y
deformar», que la propia hoja de marca prohibe. Subir la escala repite pixeles.
Verificado reconstruyendo el dibujo desde los colores que emite el propio ANSI,
no mirando el codigo: sale el martillo con las cuatro tintas y la chispa en el
centro. Las tres formas y --escala=5 corren sin error.
scripts/logo-ascii.py emite la tocapu 7x7 en color (ANSI 24-bit) y en
monocromo. Se DERIVA del SVG en vez de dibujarse a mano: una copia aparte se
desincroniza el dia que la marca cambie y nadie se entera.
Para shuma: su fetch no lleva arte ASCII a proposito —su propio doc explica que
los *fetch clasicos empaquetan el ASCII de trescientas distros y que eso
envejece— pero expone SHUMA_FETCH_LOGO, que toma una IMAGEN.
Y el hallazgo que hay que dejar escrito: NO sirve exportarla desde el perfil de
la shell. shuma absorbe el entorno de la login shell, pero su es_denegada
rechaza todo lo que empiece con SHUMA_, por diseño. La via correcta es su propio
env.json, que aplica los grupos activos al proceso al arrancar.
El icono se instala fuera del repo (~/.local/share/pixmaps/takana.png): una ruta
dentro del arbol se rompe cuando el repo se mueve, que es literalmente lo que
paso hoy con ~/hammer.
GitHub: sergiovelasquezzeballos/hammer -> takana (sigue privado). El segundo
pushurl del origin y el default de espejo-setup.sh actualizados; push real
verificado contra los DOS destinos.
Las dos colas las encontro hammer-4a y las verifique antes de aplicarlas:
1) CINCO scripts hardcodeaban /home/sergio/hammer y hoy funcionaban SOLO por el
symlink que puse al mover. El peor era respaldo-storagebox.sh, cuya RAIZ por
defecto salia de ahi: si alguien limpia el symlink dando el renombre por
cerrado, el RESPALDO apunta a una ruta inexistente. Un respaldo que no
encuentra su raiz no falla ruidosamente — se descubre el dia que lo
necesitas. Ahora apuntan a /mnt/vvv/takana, la ruta real, sin depender de
ningun enlace.
2) Cuatro referencias a gitea.gioser.net/sergio/hammer. El diagnostico del peer
era el correcto y lo comprobe: ese HOST no resuelve, y no por el renombre —
ya estaba mal antes, el remoto real es git.gioser.net. Cambiar solo
hammer->takana las habria dejado igual de rotas. Van a
https://git.gioser.net/sergio/takana, que responde 200.
Se afirmo que la etapa 4 invalidaba el baseline of_tree porque el nombre del
crate va en los simbolos de hammerd. El razonamiento era correcto y la
conclusion no: recipes/hammerd.toml pinea su fuente por COMMIT, no por el arbol
de trabajo, asi que el renombre no la alcanza.
Medido: los cuatro componentes de Stage 1 tienen vigente == sellado
(musl ce952f72, busybox 2a2b1280, hammerd 48bbbe52, arje-zero b34c571b).
No hay nada que rehacer por causa del renombre.
Queda anotado aparte que el of_tree del stage1 sellado hoy (d152f060) no
coincide con ninguna de las dos constantes del script. No se re-ancla: sin
correr el verify in-VM seria poner una constante que nadie verifico, y esta
maquina no tiene KVM.
Rotura que introduje yo al renombrar el repo en gitea, y que no fallaba todavia
porque las tres tienen su artefacto sellado en cache:
git ls-remote sergio/hammer.git -> no responde
git ls-remote sergio/takana.git -> b393687d
hammerd, netup y portal-probe clonan el propio repo por ssh. Cualquier rebuild
de esas tres —o un hub nuevo sin store— habria muerto en el fetch.
El ArtifactHash NO se mueve, y esta comprobado receta por receta antes y despues
del cambio (48bbbe52, 8d093d74, 13ebb33d): la URL es locator y no entra en
hash_inputs, solo el commit (ADR 0013). Por eso mismo el arreglo es gratis.
Tambien el default de GITEA en espejo-setup.sh. git.tawasuyu.net y
git.gioser.net son la MISMA maquina (204.168.193.248), asi que el renombre le
aplica igual.
El espejo de GitHub sigue siendo sergiovelasquezzeballos/hammer: alla el repo no
se renombro. No rompe nada porque el push va por URL explicita, pero queda dicho.
El worker vive en /opt/takana con takana-farm.service, verificado por un ciclo
de build real alla y una cosecha completa aca.
Y queda anotada la recaida: el barrido ad-hoc de este mismo cambio no respeto
el aviso que este fichero tiene arriba y dio vuelta cuatro afirmaciones
HISTORICAS, dejandolo diciendo que a las 18:40 se reinicio takana-farm.service
cuando en ese momento se llamaba hammer-farm.service. Corregidas a mano. La
exclusion hay que ponerla en CADA barrido, no una sola vez.
Renombrado el directorio del worker y la unit, con las tres units del repo
(farm, deadman service y timer). Todas las rutas /opt/hammer del hub pasan a
/opt/takana: eran defaults REMOTE= y HAMMER_DIR=, mas los systemctl/journalctl
que nombraban la unit sin el sufijo .service, que el primer sed no casaba.
Orden, para que no quedara partido: el hub siembra por rsync a una ruta fija,
asi que se saco el cron ANTES de mover. Si se movia el worker con el hub
apuntando a la ruta vieja, la siguiente cosecha recreaba /opt/hammer y quedaban
dos arboles.
Verificado de punta a punta:
- el worker cerro un ciclo de build REAL desde /opt/takana (dunst sellado, 1/1)
- una cosecha completa del hub contra la ruta nueva: siembra, manifiesto, los
nueve grafos, static-audit (691 estaticos, MIENTEN 0) y estado pusheado
- la unit vieja quedo deshabilitada y borrada; /opt tiene una sola entrada
NO se tocan dos referencias a /opt/hammer que siguen siendo CORRECTAS:
docs/23-plan-rehasheo.md describe rutas EMBEBIDAS en artefactos ya construidos
—que literalmente dicen /opt/hammer/work en sus secciones .debug— y el HANDOFF
de la noche de KDE es registro. Cambiarlas haria que los documentos mientan.
Los cuatro bind-mounts desde /mnt/cosecha hacia dentro del arbol eran lo que
hacia imposible un mv a secas, y no estaban previstos. Tampoco que ~/hammer
fuera un symlink que quedaba colgado.
Avisar al otro agente sirvio: hammer-9f aporto dos sitios con la ruta absoluta
fuera del repo que no estaban en el inventario.
Verificado por el ciclo real del cron de las 20:00 desde la ruta nueva, que
ademas pusheo al gitea renombrado. Token de renombre revocado.
El directorio se movio de verdad, asi que las referencias absolutas dentro del
repo ya eran incorrectas. Tambien los ejemplos que usaban ~/hammer.
NO se tocan docs/evidencia/: son REGISTRO de lo que se corrio ese dia, y
reescribir una ruta ahi adentro falsifica la evidencia. Que nombren una ruta que
ya no existe es correcto: existia cuando se midio.
Verificando que las extensiones de la tanda anterior llegaran al rootfs apareció algo bastante peor
que lo que fui a buscar: **`scripts/gnome/hydrate-gnome.sh` no lee `targets.toml`**. Arma el rootfs
desde una lista de 14 raíces escrita a mano dentro del propio script, y de las 28 raíces que el
perfil declaraba sólo DOS coincidían.
MEDIDO recorriendo la clausura de esa lista (123 recetas) contra la membresía del perfil: **21
paquetes declarados en `escritorio-gnome` no llegaban al rootfs hidratado**. No son accesorios —
son las hojas, que es justo lo que un perfil declara porque no es dep de build de nadie:
atuq (el navegador) · foot (la terminal) · helix (el editor) · mpv · swayimg (el visor)
zathura + zathura-pdf-poppler (el lector de PDF) · dejavu-fonts y dejavu-fonts-nerd (las FUENTES)
xdg-desktop-portal-gnome · libnotify · desktop-file-utils
bzip2-shared, expat-shared, libffi-shared, ncurses-shared, xz-shared (las .so que se cazaron el
2026-09-03 recorriendo NEEDED, justamente para que el rootfs no resolviera contra el lab)
y las cuatro de ayer: las tres extensiones y gnome-tweaks
O sea que el trabajo de descubrir que la imagen no tenía terminal, ni editor, ni una sola fuente
—con su análisis largo escrito en este mismo fichero— estaba DECLARADO y no llegaba.
Y la divergencia iba en los dos sentidos: 11 raíces de RUNTIME vivían sólo dentro del script y el
perfil no las conocía (accountsservice, geoclue, libgdm, libgweather, upower, librsvg, ibus, los dos
gi-*-typelibs, gsd-schemas y wireplumber). Sin ellas el shell no arranca: las carga por
`imports.gi.*`, que es invisible para la clausura de deps.
ARREGLO: las 11 se mueven a `targets.toml` con la justificación que el script ya traía bien escrita,
y el script queda marcado como SUPERADO por `scripts/hydrate-profile.py`, que hidrata desde el
perfil — la única declaración que existe. Su propia cabecera ya advertía de este modo de fallo
(«DOS fuentes de verdad para lo mismo, y divergen sin que nada lo diga»); esto es esa advertencia
cumpliéndose. El perfil pasa de 28 a 39 raíces y de 190 a 204 nodos, deuda 0.
VERIFICADO HIDRATANDO DE VERDAD, no leyendo: `hydrate-profile.py escritorio-gnome` proyecta
210/210 nodos (27993 ficheros, 3,5 G) y ahí están las tres extensiones, `gnome-tweaks` con su
módulo `gtweak`, y los tres gschemas en el directorio GLOBAL. Después se corrió el bloque nuevo del
lanzador contra ESE árbol: enumeró las tres, escribió el override y `glib-compile-schemas` salió 0
con stderr vacío.
⚠ DOS COSAS QUE APRENDÍ MIDIENDO Y CONVIENE QUE ESTÉN DICHAS:
1. **El hub NO puede hidratar GNOME hoy.** `gjs`, `gnome-shell` y `spidermonkey` salen «no sellado»
localmente. No es deriva ni un grafo mintiendo: sus hashes vigentes SÍ están en
`work/farm-sellados.txt` — o sea sellados EN EL WORKER — y la cosecha baja el manifiesto y no el
store, a propósito (bajarlo deshacía la poda). `build-state` los cuenta bien porque mira también
el manifiesto; `hydrate-profile.py` mira sólo el disco local. Los dos tienen razón.
2. **EXDEV entre bind-mounts, otra vez.** Hidratar a `work/` o a `work/out` falla con «Invalid
cross-device link» AUNQUE `work/out` esté en el mismo /dev/sdb que el store: son bind-mounts
distintos y `linkat()` no cruza montajes. Hay que hidratar accediendo al store por su montaje
raíz (`--store /mnt/cosecha/store --into /mnt/cosecha/escritorios/…`). El mensaje de error del
script lo dice y aun así me costó dos intentos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
La etapa 6 no es un barrido: son seis despliegues coordinados. Dos hechos
(--takana-bin, y la unit del worker fijando las dos variables, que era lo que
bloqueaba retirar las caidas), dos que conviene esperar, uno atado al baseline
del selfhost, y el directorio del repo que necesita decision porque hay otro
agente trabajando adentro ahora mismo.
Y queda anotada la tabla de las 7 etiquetas de separacion de dominio que el
barrido ancho habria reescrito, con hammer-tree-v1 a la cabeza: es el prefijo
de ArtifactHash::of_tree, o sea los 4750 hashes del store.
Era la ultima superficie de CLI que quedaba con el nombre viejo. Mismo patron
que forja: el canonico es el nuevo, el viejo sigue aceptandose como alias.
El default pasa a .../release/takana.
El DESTINO no se toca: el binario se sigue instalando en /usr/bin/hammer dentro
del rootfs del producto, porque ese rootfs se hashea. Moverlo cambia el hash del
producto y obliga a rehacer el baseline del selfhost — es etapa 6 y decision
aparte, no efecto colateral de renombrar un flag.
Verificado en el subcomando correcto (bootstrap builder, no product): las dos
formas se aceptan. 605 tests en verde.
Lo destapó empaquetar gnome-tweaks: su meson pide `pygobject-3.0 >= 3.46` y resolvió sin receta
nueva, porque `recipes/incoming-gnome/py3-gobject.toml` ES PyGObject 3.50.0 —la escribió el frente
GNOME para el build de libgweather— y publica `pygobject-3.0.pc`.
Esta mañana la firmé `opcional` razonando que sólo la usan los tests de libsecret y modemmanager.
Eso sigue siendo cierto para esas dos recetas, pero el veredicto estaba mal de CATEGORÍA: `opcional`
dice «no la queremos» y el hecho es «ya la tenemos». La diferencia no es cosmética — un `provisto`
realimenta el sembrador como alias y la saca de la frontera; un `opcional` la deja saliendo en cada
barrido.
OCTAVO alias, y estrena la sexta forma de fallar el cruce por nombre: el prefijo `py3-` de nuestro
catálogo contra el nombre de upstream. Las anteriores eran puntuación (nlohmann_json), mayúsculas
(libxfont_2), prefijo lib (gusb), versión en el nombre (glad2) y homonimia cruzada (libmpc/mpc).
Y la lección de método, que es la que vale: el veredicto de esta mañana lo saqué mirando SÓLO a
quién la pedía y por qué. No miré si el catálogo ya la tenía bajo otro nombre — que es justo la
comprobación que la categoría `provisto` existe para hacer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
GNOME deja de ser la cáscara: tres extensiones y la app que el proyecto no instala y todo el mundo
termina instalando. El perfil queda 190/190 con 28 raíces, deuda 0.
dash-to-panel v73 junta dash, ventanas y bandeja en una sola barra (lo que más cambia el uso)
appindicator v64 devuelve los ICONOS DE BANDEJA que GNOME quitó; sin ella las apps que usan
StatusNotifierItem corren y no tienen dónde mostrarse
blur-my-shell v72 (ya estaba; se le saca el override, ver abajo)
gnome-tweaks 46.1 tema, iconos, cursor, tipografías, los botones de minimizar/maximizar que
GNOME esconde, y el interruptor de las extensiones
⚠ LA LISTA DE ACTIVAS SE MUEVE A UN SOLO SITIO, y esto era una bomba de relojería. `enabled-extensions`
es un ARRAY: un override de GSettings lo escribe ENTERO. Con la primera extensión el override vivía
en su receta y funcionaba; con la segunda NO se habrían sumado — ganaría la que compile última
(orden alfabético) y las otras quedarían instaladas y MUERTAS, sin un solo error. Ahora la arma
`scripts/gnome/gnome-start-qemu.sh`, que ya compilaba los esquemas al arrancar, enumerando los uuid
presentes en /usr/share/gnome-shell/extensions. La política es «lo que la imagen instala, se
activa»; el dconf del usuario gana sobre el default.
Una receta NO puede ser ese sitio: hammer exige `[source]` en toda receta («missing field `source`»),
así que no hay forma de escribir una receta de pura política. Se probó el bloque contra un árbol de
juguete: lista las dos extensiones, salta un directorio sin metadata.json, el override compila con
stderr vacío y `gsettings get` devuelve las dos. Y si la lista cambia borra `gschemas.compiled` para
forzar recompilación — sin eso, una imagen ya arrancada ignoraría la extensión nueva en silencio.
⚠ EL HALLAZGO CARO: `msgfmt` de gettext-tiny REVIENTA con las formas plurales del árabe. SIGILL,
exit 132, «index 6 out of bounds for type 'size_t[6]'», y deja un .mo de 0 bytes. Reproducido FUERA
de la receta con el binario sellado. No es el idioma sino el mensaje: blur-my-shell compila su
ar.po sin problema porque no tiene NINGÚN plural (msgstr[5] = 0); el de tweaks tiene exactamente
uno. Acá se saca `ar` del LINGUAS y se pierde la traducción de UNA app, con el diagnóstico escrito
en la receta. ARREGLARLO ES OTRA UNIDAD DE TRABAJO: gettext-tiny la usan 108 recetas.
Cada receta usa el mecanismo de SU upstream, que no es el mismo en las tres:
blur-my-shell `make build` llama a `gnome-extensions pack` —el CLI que nuestro shell apaga— así
que se copia el árbol a mano, con el layout LEÍDO del zip de EGO y comparado
contra él (idéntico, sin sobras ni faltantes)
dash-to-panel su Makefile tiene un `install` que honra DESTDIR y hace lo de un paquete de
distro: gschema y locale a los directorios GLOBALES. ⚠ `VERSION=73` se pasa a
mano porque sin ella el Makefile hace `git describe` y el árbol viene por
`git archive`, sin `.git` ⇒ metadata quedaría con el marcador "version": 9999
appindicator meson. ⚠ `-Dlocal_install=disabled` explícito: en `auto` decide mirando si el uid
es 0, o sea que hoy acierta POR ACCIDENTE — y si el lab dejara de correr como
root instalaría en $HOME/.local y el artefacto sellaría vacío. Y `jq` va en deps
aunque sea JavaScript puro: su meson saca el uuid del metadata.json con jq
⚠ gnome-tweaks NO costó ninguna receta nueva, y eso hubo que medirlo: su meson declara siete deps
mínimas de runtime y las siete ya estaban selladas. La que parecía traer una cadena de Python
entera —`pygobject-3.0`— la provee `py3-gobject`, que YA ESTABA en el corpus con otro nombre y
publica `pygobject-3.0.pc`. El nombre no es el hecho, otra vez.
Sí costó dos deps que el configure reclamó y que son el patrón `.pc Requires` → `[deps].build`:
`appstream` (la pide `libadwaita-1.pc` y el error culpa a libadwaita, que está bien instalada) y
`desktop-file-utils` (meson la busca como PROGRAMA para `update-desktop-database`).
Sus deps de RUNTIME —python3 y py3-gobject— van declaradas: es una app de Python, si no están en la
imagen se instala y no arranca, y ningún build lo delata.
Las cuatro licencias, verificadas en la fuente pineada y NINGUNA leída del COPYING —cuyo apéndice
«How to Apply» trae siempre la frase «or any later version»—: blur-my-shell del README del commit,
dash-to-panel y appindicator de la cabecera de sus fuentes, gnome-tweaks de su etiqueta SPDX.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
Medido: esta unit era el UNICO sitio fuera del repo que traia una variable
vieja puesta (Environment=HAMMER_DIR=/opt/hammer), y por eso es lo que impide
retirar la caida del codigo. Ahora fija las dos.
Se ponen las dos y no solo la nueva porque un script viejo —o una copia del
repo que todavia no sincronizo— solo mira HAMMER_DIR.
Desplegada y verificada en dev.gioser.net: daemon-reload + restart, servicio
active, y el entorno del servicio muestra las dos.
205 lineas en 73 ficheros de crates, mas la prosa de CLAUDE.md y del skill,
que se me habian quedado afuera de los barridos anteriores (no eran ni recetas
ni docs/ ni scripts/).
EL BARRIDO ANCHO ESTUVO A UN COMMIT DE ROMPER EL CORPUS ENTERO.
El primer intento reescribia los .rs completos, no solo los comentarios. Entre
las lineas de codigo que tocaba estaban SIETE etiquetas de separacion de
dominio, que son ENTRADA DE HASH:
b"hammer-tree-v1" <- el prefijo de ArtifactHash::of_tree (hash.rs:60)
b"hammer-seed-v1" la funcion que hashea TODOS los artefactos:
b"hammer-stage1-rootfs-v2" cambiarla mueve los 4750 hashes del store
b"hammer-product-rootfs-v3"
b"hammer-product-attested-v2"
b"hammer-builder-rootfs-v1"
b"hammer-attest-dev-rootkey-0001!!" <- clave raiz de atestacion, [u8;32]
Revertido y rehecho solo sobre comentarios, esquivando ademas las cadenas
crudas de Rust (r#"..."#) porque el SYSTEM_PROMPT del traductor tiene lineas
que empiezan como comentario.
Controles: las 7 etiquetas siguen ahi, el diff toca CERO lineas de codigo,
605 tests en verde y el hash de zlib sigue en b3:dc363f26.
La leccion es la misma de toda esta etapa: un literal que parece prosa puede
ser entrada de hash, y la unica forma de saberlo es mirar donde se usa.
5a recetas (1427 comentarios, cero hashes movidos y medido), 5b docs (59),
5c scripts (250, sin tocar una sola linea que no empiece por #).
Los dos guardianes que hicieron trabajo real: la linea base de hashes atrapo 3
recetas que se movian porque un comentario de SHELL dentro de una fase parece un
comentario de TOML; y el salteo de heredocs atrapo 3 MOTD que son texto del
producto, no comentarios.
Y queda anotado el bug que introdujo la etapa 4: atribuir-fallos.py casaba
contra el target de tracing, que es el module_path y por lo tanto el nombre del
crate. Quedo casando nada sin fallar.
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.
El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.
Y el hallazgo caro: casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.
Además 14 rutas de módulo en docs, que el barrido anterior no tocó
porque no es frontera de palabra.
645 líneas. Los ADR entran porque en este repo SON documentos vivos, no
registros inmutables: el 0013 tiene 5 commits, el 0009 dos. Eso se comprobó
antes de decidir, no se asumió por convención general.
EXCLUIDOS por ser REGISTRO o generado: docs/evidencia/ (6), el HANDOFF de la
noche de KDE (1) y docs/state/ (24, se regenera solo). Reescribir un comando
dentro de una evidencia la falsifica.
Y el ADR 0016 se excluye de todo barrido, con un aviso adentro para el próximo
que barra: habla SOBRE el renombre, así que necesita seguir diciendo 'hammer'.
El barrido se lo llevó puesto y lo dejó titulado 'Renombre del sistema: takana
→ takana'; revertido.
Congelados, verificados uno por uno con controles: /opt/hammer, /var/lib/hammer,
/usr/bin/hammer, /mnt/vvv/hammer, la URL de gitea, hammer-farm.service,
hammer-live-install.sh, BRIEFING-hammer.md, hammerd, hammer-recover y
HAMMER_LIVE.
Barrido de TEXTO puro: 1427 líneas de comentario TOML. Cero ArtifactHash
movidos, y eso está MEDIDO, no deducido: hasheé las 741 antes y después y los
ficheros de hashes son idénticos byte a byte (723 con hash real + 18 que ya no
hasheaban de antes).
El guardián valió la pena: el primer barrido, filtrando por 'la línea empieza
con #', movió el hash de helix, lsof y steam-runtime-sniper. La causa es que una
fase se escribe como compile = <triple> ... <triple> y sus comentarios de SHELL
también empiezan con #, pero viven dentro del VALOR — y las fases sí entran en
hash_inputs. El barrido ahora calcula los rangos de las cadenas multilínea de
TOML y no entra ahí.
Quedan intactos a propósito: .hammer-zig-cc y .hammer-cargo-vendor (literales
dentro de fases), hammerd, hammer-recover y toda ruta que empiece por /
Con lo que la medición cambió respecto del plan, los dos controles negativos
del helper, y la distinción que importa: la caída sirve al lector NUEVO; donde
el script exporta, hay que seguir poniendo la vieja porque el lector puede ser
un binario viejo.
TAKANA_X con caída a HAMMER_X (ADR 0016). No es un sed: estas variables son
contrato de usuario —knobs del instalador, el entorno del worker, mirror-env.sh—
y viven en perfiles de shell y units FUERA del repo, así que un renombre duro no
falla ruidosamente: la variable no aparece, se toma el default y el build se
comporta distinto sin que nada lo diga.
Rust: takana_core::env::{var,var_os} toma el nombre canónico y deriva el viejo
cambiando el prefijo — se le pasa TAKANA_* para que un grep del nombre nuevo
encuentre todas las lecturas. 5 tests, con DOS controles negativos: sin ninguna
de las dos no hay valor, y un nombre sin prefijo no inventa una caída.
Migrados los 17 sitios directos y los indirectos que el grep no mostraba
(bases_de_mirror, las constantes de kernel_cmd, ROOT_ENV de qorpa, env_path de
recover). takana-recover lleva la caída inline: es un mini-binario que se copia
a /usr/sbin y no vale arrastrarle una dep entera por dos líneas.
Scripts: 30 lecturas pasan a default, manteniendo el
nombre INTERNO de la variable para no tocar sus 190 usos.
Y donde el script EXPORTA en vez de leer, se ponen LAS DOS (mirror-env.sh y el
fragmento in-VM de bootstrap): ahí el lector puede ser un binario viejo —worker
sin recompilar, el /usr/bin/hammer pinado del baseline— que sólo conoce HAMMER_*.
La caída sirve para lectores nuevos; los viejos necesitan que la vieja siga puesta.
Verificado de punta a punta con el binario, no sólo con unit tests: HAMMER_LAB
sigue surtiendo efecto, TAKANA_LAB hace lo mismo, y con las dos gana TAKANA_LAB.
605 tests en verde y el hash de zlib sigue en b3:dc363f26… , intacto.
Cambio de comportamiento que va aparte y hay que decir: el hostname por defecto
de una instalación nueva pasa de 'hammer' a 'takana' (sólo si no se fija ninguna
de las dos variables).
NO se tocan: las rutas /var/lib/hammer de sistemas instalados, el volid
HAMMER_LIVE del ISO, ni el namespace HARKAQ_*, que es de otro subsistema.
La imagen de GNOME traía la CÁSCARA: el shell sabe cargar extensiones —`ui/extensionSystem.js` va
en el gresource sin opción que lo apague y su gschema sellado trae `enabled-extensions`— pero no
había ninguna que cargar. Esta es la primera, y fija el patrón para las otras ~1400.
⚠ NO HIZO FALTA TLS, que era la conclusión fácil del análisis de ayer. glib-networking sólo lo pide
BAJARLAS desde extensions.gnome.org con el descargador del shell. Empaquetada, una extensión se
instala como cualquier receta y no toca la red.
LA FUENTE ES EL REPO DEL AUTOR, NO EL ZIP DE EGO, por dos razones y las dos medidas:
1. hammer no sabe abrir un zip: el fetch extrae con `tar -x` y GNU tar contesta «This does not
look like a tar archive». Ninguna receta del corpus usa .zip.
2. Y aunque lo abriera, el zip de EGO es un ARTEFACTO que construye EGO desde este mismo repo.
Pinear el zip es pinear el binario de un tercero; pinear el commit es pinear el código.
El tag v72 es el que EGO sirve para shell 48 —preguntado a su API, no supuesto— y el metadata.json
declara ['46','47','48','49','50']. ⚠ Eso hay que preguntarlo por extensión: nixpkgs va por GNOME
50 y hay extensiones que ya sólo declaran el major nuevo.
SIN `gnome-extensions pack`, que es lo que usa el Makefile de upstream y es el CLI que nuestra
receta del shell apaga (-Dextensions_tool=false; encenderlo costaría gnome-autoar y re-hashear
gnome-shell entero). No hace falta: `pack` sólo arma un zip que después alguien descomprime, y lo
que el shell lee es un DIRECTORIO. El layout no se inventó — se leyó del zip de EGO v72, que es la
salida de ese mismo `pack`, y el artefacto sellado se comparó contra él: **entradas idénticas, ni
sobras ni faltantes**, 45 .mo compilados.
⚠ INSTALADA ≠ ACTIVA, y esto es lo que hace que la receta sirva. El shell sólo carga lo que esté en
`org.gnome.shell enabled-extensions`: sin eso el artefacto sella, el fichero está en la imagen y NO
PASA NADA al arrancar — el mismo cuadro de «sellado ≠ instalado» que este repo ya se comió con
foot, mpv y libnotify, un escalón más abajo. Se resuelve con un override de GSettings, que cambia
el DEFAULT y deja ganar al dconf del usuario que no la quiera.
PROBADO DE VERDAD, no deducido: se compiló el directorio de esquemas del shell con el override
puesto (`glib-compile-schemas`, exit 0 y stderr vacío — que hay que mirar, porque sale 0 aunque
RECHACE un esquema) y después `gsettings get org.gnome.shell enabled-extensions` devolvió
['blur-my-shell@aunetx'].
⚠ TRAMPA ANOTADA PARA LA SEGUNDA: `enabled-extensions` es un ARRAY y el override lo escribe ENTERO.
Dos recetas con su propio override no se suman — gana la que compile última y la otra queda
instalada y muerta, sin error. Cuando llegue la segunda, la lista pasa a un solo sitio.
Y una corrección sobre la licencia, que casi firmo mal: el LICENSE es el texto del GPLv3, y su
apéndice «How to Apply» CONTIENE la frase «either version 3 … or any later version» siempre, así
que grepearla ahí no distingue -only de -or-later (la trampa que la memoria del repo ya tenía
escrita). La evidencia buena es el README del commit pineado: «This program is distributed under
the terms of the GNU General Public License, version 3 or later». El árbol JS no lleva cabecera de
licencia en ningún fichero.
Va de RAÍZ en escritorio-gnome por la lección de `foot` que targets.toml ya aprendió tres veces.
El perfil queda 183/183 listo con 25 raíces, deuda 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
Decisión del usuario (2026-09-09): X11 se erradica, y eso incluye no construir el servidor que lo
mantiene vivo. La receta se retira; `xwayland` VUELVE a ser lo que sus propios documentos decían.
⚠ LO QUE APARECIÓ AL MIRAR: esto ya estaba decidido, por escrito y DOS VECES, el 2026-09-03
(commit f64859ba):
docs/state/targets.toml:143 «`xwayland` sigue acá como RAÍZ pero su receta NO se va a escribir:
lo provee una imagen ajena enjaulada … el hueco se disuelve sin
receta y sin tocar la postura Wayland-only»
docs/state/qorpa-ajenos.toml «X11 está fuera de alcance para toda la distro … el Xwayland vive
DENTRO de la imagen —Arch y Fedora ya lo traen— y se cuelga de
nuestro kwin por el socket. La receta no se escribe.»
El 2026-09-08 se escribió igual (3d8c6fc6, «cerrar los 3 huecos que quedaban»), y los dos
documentos siguieron diciendo lo contrario durante toda la jornada de hoy. No hubo que cambiar
ninguno de los dos: alcanzó con borrar la receta para que el repo volviera a coincidir con ellos.
MEDIDO ANTES DE SACARLA, no supuesto:
· Sólo kwin podía lanzarlo. mutter va con -Dxwayland=false y wlroots con -Dxwayland=disabled ⇒
GNOME, sway y COSMIC no podían usarlo aunque el binario existiera. Nunca sirvió a más de un
perfil de los cuatro.
· NINGUNA app del catálogo necesita servidor X: todo lo gráfico es Wayland nativo (Qt6 con
qtwayland, GTK3 Wayland-only, mpv, OBS con ENABLE_WAYLAND=ON, foot, fuzzel, swayimg). El
consumidor que lo justificaba está nombrado en qorpa-ajenos.toml y es Steam, que corre
enjaulado y TRAE EL SUYO ADENTRO.
· Y la imagen KDE ya arrancaba sin él: la propia receta contaba que KDE sellaba 1037/1037 con las
apps X11 muertas. Esto no estrena una configuración, vuelve a una ya probada.
EFECTO EN EL GRAFO, verificado regenerando: `xwayland` deja de ser una receta y pasa a clase
`ajeno` —que es como targets.toml decía que había que contarlo— y el perfil escritorio-kde baja de
304 a 299 nodos con deuda 0. Los cinco que se van son la cadena que sólo él usaba.
⚠ QUEDAN CINCO RECETAS HUÉRFANAS, y no las borro de paso: `libfontenc`, `libXfont2`, `libxkbfile`,
`libxshmfence` y `xkbcomp`. Medido: NADA más en el catálogo las declara, y ninguna es raíz de
ningún perfil ⇒ siguen selladas pero no entran en ninguna imagen. Sacarlas es su propia unidad de
trabajo y su propia decisión.
El triaje pasa de `provisto` a `opcional` con la historia entera en su `porque`, y con eso se cae
el alias `xwayland xwayland` de alias-triaje.txt, que habría afirmado que tenemos una receta con
ese nombre.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
Los 10 crates de librería + CLI renombrados, con los dos binarios congelados y
su razón. Anotada la consecuencia real: renombrar hammer-core mueve los bytes
de hammerd igual, así que el baseline of_tree del selfhost hay que rehacerlo.
Y la medición que cambia el plan: 'la variable HAMMER=' no existe como cosa
única — son 14 variables, ~260 apariciones, y el BINARIO las lee (HAMMER_LAB,
HAMMER_ZIG, HAMMER_WORK, HAMMER_ROOTFS, HAMMER_MIRROR_KEY, HAMMER_LLM_*). Con
knobs de instalador entre ellas y el entorno del worker trayéndolas puestas
desde fuera del repo. Eso es contrato de usuario, no churn: pide leer las dos
con caída a la vieja, que es código, no sed.
Sumada la evidencia del worker: tras reiniciar el servicio compiló los dos
binarios y cerró un ciclo real (dunst sellado, 1/1).
hammer-{core,build,bootstrap,overlay,journal,mirror,upgrade,agent,recover,cli}
→ takana-*, con sus deps de workspace, sus identificadores en el fuente y las
referencias -p de los scripts.
VERIFICADO que no mueve nada del corpus: `takana hash recipes/zlib.toml`
devuelve b3:dc363f26… , idéntico a antes del renombre. Los nombres de crate no
entran en hash_inputs, pero eso se comprueba, no se supone. 600 tests en verde.
DOS BINARIOS SE CONGELAN, y no por prolijidad:
- `hammerd` — paquete Y binario. Es componente de Stage 1 de la distro (musl,
busybox, hammerd, arje-zero), lo supervisa arje-zero en el sistema arrancado,
`PRESEED=hammerd` lo nombra en selfhost-verify y sus bytes anclan el baseline
of_tree. El nombre del crate va en los símbolos ⇒ renombrarlo mueve los bytes.
- `hammer-recover` — el PAQUETE se renombra a takana-recover, el BINARIO no.
hammer-live-install.sh lo copia a /usr/sbin/hammer-recover en sistemas ya
instalados y hornea un hook de arranque que lo invoca por ese nombre:
renombrarlo rompe máquinas instaladas, no el repo.
Consecuencia que hay que anotar igual: al renombrar hammer-core, los bytes de
hammerd cambian de todos modos porque linkea contra un crate con otro nombre.
El baseline of_tree del selfhost hay que rehacerlo — es efecto de la etapa 4,
no de un cambio de hammerd.
Las referencias en comentarios de recetas y docs (rutas hammer-core/src/…)
quedan para la etapa 5: son texto, no mueven hash.
Se commitea DESPUÉS de verificar, no antes: el ADR afirma que el cron corrió
limpio con los scripts migrados, y ese ciclo (18:30:00Z → 18:32:21Z) ya está
en el log — siembra ✓, manifiesto ✓, los 9 JSON del grafo regenerados por
build-state.py invocando takana, estado pusheado, cero errores.
Documenta además cómo converge el worker, que se midió en vez de suponerse:
/opt/hammer no es un clon git sino rsync, hammer-farm.service corre el loop
como servicio largo, y los dos estados intermedios (antes y después de
reiniciarlo) son coherentes porque la 3a puso los dos binarios en los cargo
build antes de tocar ninguna invocación.
Los llamadores EJECUTABLES: scripts/ (incluida toda la granja), los runbooks y
CLAUDE.md. Seguro porque la 3a ya garantiza que el worker emite los dos
binarios, y porque en farm-lab-sync.sh el cargo build remoto precede a la
invocación remota en el mismo script.
Verificado: sintaxis de los 49 (bash -n / py_compile — ojo que
why-differs-barrido.sh es Python con extensión .sh) y `takana hash` devuelve
hash real sobre el store.
NO se toca en esta etapa, a propósito:
- La variable de entorno HAMMER=. Es interfaz de los scripts entre sí y hay
llamadores que la fijan; renombrarla va con la etapa 4.
- docs/evidencia/ y los HANDOFF: son REGISTRO de lo que se corrió ese día.
Reescribir un comando dentro de una evidencia la falsifica.
- docs/state/: es generado, se regenera solo.
- Los ADR y los docs de diseño: texto, y `hammer` sigue funcionando. Van con
la etapa 5, que es la de churn de texto.
Va solo y ANTES de cambiar ninguna invocación, a propósito. El worker compila
desde fuente (farm-worker-loop.sh) y la siembra excluye /target: si primero
cambiara las llamadas a `takana` y después el build, habría una ventana en la
que el worker sincroniza scripts nuevos y sigue teniendo sólo el binario viejo
— y eso no falla ruidosamente, deja de cosechar en silencio.
Con los dos emitidos, cualquier orden de sincronización queda sano.
Etapa 2 (ADR 0016): el binario canónico es `takana` y `hammer` se sigue
emitiendo. Son DOS [[bin]] al mismo main.rs, no un symlink: la siembra de la
granja excluye /target (un symlink del hub no existiría en el worker) y
`cargo clean` lo borraría. Ningún llamador tocado; los 124 siguen andando.
Adoptados los dos puntos de la hoja de marca que chocaban con contratos:
- `forja` como ALIAS de clap sobre `build`, no como reemplazo. El canónico
sigue siendo el inglés, que es lo que usan scripts, cron y runbooks. Y se
enmienda la regla 4 de CLAUDE.md en el mismo commit: cambiar el
comportamiento dejando escrito el contrato viejo es lo peor de las dos
opciones, porque el otro agente del repo aplica lo que lee.
- `.tkn` como extensión de paquete. Salió barato y por una razón medida: la
extensión no es lógica sino salida — se escribe en UN solo lugar
(main.rs:1800) y el descubrimiento va por índice, no por glob
(PackageEntry.file, repo.rs:75). Los repos con entradas .swm siguen
resolviendo y un repo mixto es válido; cero ficheros .swm versionados.
Los tipos Swm/SwmBuild/swm_bridge no se tocan: son internos, van en la
etapa 4.
287 tests en verde (hammer-cli + hammer-core), incluidos los que fabrican
repos con nombres .swm a mano — que son justamente la prueba de que la
compatibilidad hacia atrás se sostiene.
El sistema pasa de hammer a takana (martillo en quechua/aimara): traducción
literal, conserva la metáfora de forja y mantiene el registro agentivo del
resto de la familia (khipu/yupana/harkaq/qorpa/churay).
Medido antes de tocar nada: hash_inputs es una LISTA DE CAMPOS, no el fichero
crudo (recipe.rs:535) ⇒ los comentarios de las 692 recetas importadas se pueden
renombrar GRATIS, sin mover un solo ArtifactHash. Pero las fases SÍ entran al
hash: las 10 recetas con .hammer-zig-cc dentro de una fase se congelan, igual
que los 5 cargo_vendor_dir (que ni siquiera están en hash_inputs y aun así
pueden cambiar bytes).
El renombre va por etapas con alias; el directorio /mnt/vvv/hammer es lo último
porque el cron de la cosecha lo referencia por ruta absoluta y moverlo mata el
latido en silencio.
No se adoptan dos ejemplos de la hoja de marca: 'takana forja' (viola la regla 4,
los verbos van en inglés) y .tkn (el formato es .swm, 266 menciones).
El documento decía que `lsof` y `tzdata` «siguen vetando a propósito», y ya no: las 26 recetas que
faltaban están hechas desde su fuente pineada. Se agrega la sección del cierre, con el fallo del
guardián que apareció al medir (resolvía por nombre de FICHERO y el paquete se llama por su campo
`name`: 14 falsos vetos) y los dos paquetes que NO se pueden redistribuir, que ahora el veto ve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
CAMPAÑA CERRADA: 1166/1166 recetas declaran `license`. Ninguna adivinada — cada una sale del
fichero de licencia de su fuente PINEADA (tarball del sha256 de la receta, o el commit exacto en
la forja), y la cita queda como comentario en la propia receta.
⚠ Y EL GUARDIÁN ESTABA MAL, que es el hallazgo que vale más que las 26. `licencias-rootfs.sh`
resolvía la receta por NOMBRE DE FICHERO (`ls recipes/$pkg.toml`), y el paquete se llama por su
campo `name`, que en 34 recetas NO coincide: nu.toml→`nushell`, dust.toml→`du-dust`,
incoming-kde/qtbase.toml→`qt6-qtbase`… Medido: **14 paquetes que SÍ declaran licencia salían como
«licencia desconocida»** y el guardián vetaba una imagen perfectamente publicable.
El falso veto se nota; el hermano silencioso NO: si existe un `<pkg>.toml` que pertenece a OTRO
paquete, la versión vieja reportaba la licencia EQUIVOCADA sin decir nada. Hoy no pasa —medido,
0 casos, y de los 34 nombres duplicados CERO declaran licencias distintas—, pero ahora es
imposible en vez de improbable.
El arreglo tuvo que ser por LOS DOS lados, y el primer intento rompió el otro: el grafo de estado
nombra sus nodos por el fichero (`dust`) y el artefacto del store por `name` (`du-dust`), así que
resolver sólo por `name` dejaba a `dust` sin licencia. Ahora busca por `name` y cae al fichero.
Medido en los dos sentidos: corpus entero 1128/1128, perfil base+cli 81/81, cero sin licencia.
Y probado CON ROTURA A PROPÓSITO además del control, que es lo único que distingue a un guardián
que sirve de uno que nunca salta:
paquete inexistente en la lista → exit 1 y «ESTA IMAGEN NO SE PUEDE PUBLICAR»
control (zlib nushell qt6-qtbase lsof tzdata) → exit 0, y escribe los textos
(⚠ ojo al medir: `script | tail` devuelve el exit de `tail`. La primera corrida dijo exit=0 sobre
la rotura y no era el guardián, era el pipe.)
SE LEVANTA EL VETO QUE SDD 20 DEJÓ ESCRITO. Decía que `base` y `cli` iban con 2 paquetes cada una
con binarios y licencia desconocida: `lsof` y `tzdata`, «que necesitan la vía LicenseRef- y siguen
vetando a propósito». Hechos los dos, con su texto real en licenses/:
lsof → LicenseRef-lsof (licencia propia de Purdue, sin identificador SPDX)
tzdata → LicenseRef-tz-public-domain (su LICENSE: «all files in the tz code and data … are in
the public domain»; los tres ficheros BSD-3-Clause que menciona NO se instalan — la
receta sólo compila zic y deja /usr/share/zoneinfo)
⚠ DOS QUE NO SE PUEDEN REDISTRIBUIR, y ahora el veto los ve:
duplicacy NO ES LIBRE. Su LICENSE.md: «Free for personal use or commercial trial; non-trial
commercial use requires per-computer CLI licenses … $50 per year»
waybackurls NO DECLARA LICENCIA: en el commit pineado la raíz es .gitignore, README.mkd,
go.mod, main.go y script/ — sin LICENSE ni COPYING, y el README no la menciona. Sin
concesión expresa, el defecto es «todos los derechos reservados»
Los dos con LicenseRef y un texto en licenses/ que explica qué hay, en vez de dejar el campo vacío,
que se lee como «todavía no lo poblamos». Ninguno está hoy en un perfil de imagen; si alguien los
mete, el guardián corta.
De paso queda escrito el texto de `LicenseRef-qorpa-ajena-no-enumerable`, que ya se usaba en
steam-runtime-sniper y no tenía fichero; y `licencias-textos.sh` bajó los canónicos nuevos
(BSL-1.0 para boost, GCC-exception-3.1 que ya hacía falta).
Hueco conocido y anotado en la receta: `XFree86-1.0` (rama del OR de hwdata) se queda sin texto —
SPDX no publica ese identificador, sólo XFree86-1.1, que es otra licencia, y el tarball lo nombra
sin incluirlo. El guardián avisa y no veta, que es correcto: la otra rama del OR es la GPL y su
texto sí está.
NADA SE RE-HASHEA: `license` está fuera de `hash_inputs`. Verificado, no supuesto — `hammer hash`
sobre pigz, lsof y boost después de editarlas devuelve el hash cuyo artefacto YA está en el store.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
El triaje sirve para algo o no sirve, y eso se mide corriendo el sembrador otra vez. Regenerados
los SIETE perfiles de `seed-frontera.json` (1,3 s cada uno: el eval de nix estaba cacheado).
perfil antes ahora
base 41 41 +0
cli 44 44 +0
escritorio-cosmic 148 143 -5
escritorio-gnome 191 181 -10
escritorio-kde 276 266 -10
escritorio-mirada 39 38 -1
escritorio-sway 175 171 -4
UNIÓN 334 319 -15
Los 15 que se fueron son EXACTAMENTE los que el lazo cerrado puede sacar: los siete alias
(bubblewrap, mesa-gl-headers, nlohmann_json, libxfont_2, gusb, glad2, libmpc), los nix-ismos
(CUnit, ffmpeg-headless, lzip, validate-pkg-config, python3-3.14.7-env) y tres que dejaron de ser
candidatos porque ahora TIENEN receta (bash-completion, desktop-file-utils, purpose).
Y **cero candidatos nuevos**, que era la otra mitad de la pregunta: el barrido no se movió por
debajo mientras lo triábamos.
⚠ LO QUE ESTE NÚMERO NO DICE. Los 327 veredictos `opcional` NO encogen la salida del sembrador —
sólo `nix-ismo` y `provisto` realimentan. Es correcto que así sea (un `opcional` es una decisión
nuestra, no un error de nixpkgs), pero significa que la frontera va a seguir saliendo con ~319
candidatos de los que 316 ya están contestados. Quien la lea tiene que cruzarla con
`frontera-triaje.toml`, no leerla sola: `scripts/triaje.py` sin argumentos ya lo hace y dice
«0 nuevos».
De paso: 46 candidatos quedaron marcados `ausente = true` (estaban en barridos viejos y ya no
salen). El fichero de triaje los conserva con su veredicto en vez de borrarlos, que es lo correcto
— si alguno vuelve, vuelve ya contestado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
Cierran las 24 que quedaban de una vuelta. El barrido de la frontera queda así: 327 opcional,
25 provisto, 10 nix-ismo, 3 pendientes. Arrancó la jornada en 152 pendientes.
⚠ SÉPTIMO ALIAS, y resuelve el enigma que dejé abierto esta mañana. libmpc → mpc: nixpkgs llama
`libmpc` a GNU MPC y `mpc` al cliente de MPD; acá GNU MPC se llama `mpc`. O sea que el «lo piden
mpc y yambar» de libmpdclient no era un error del sembrador cruzando mal: es que los dos catálogos
usan el MISMO nombre para paquetes DISTINTOS. Las dos puntas de esa confusión quedan cerradas.
Van siete alias y cinco formas: puntuación, mayúsculas, prefijo `lib`, versión en el nombre, y
ahora homonimia cruzada.
Lo demás, cada uno con su prueba en la receta o en el fuente: libev cae por el mismo
--enable-lib-only de nghttp2 que c-ares (configure.ac:230); graphite2 por -Dgraphite=disabled;
libliftoff por -Dlibliftoff=disabled; libtasn1 por -Dtrust_module=disabled; libasyncns por
-Dasyncns=disabled; inih por -DEXIV2_ENABLE_INIH=OFF contra un default ON; rdma-core por
--disable-rdma. nanosvg y resvg caen juntas porque fuzzel VENDORIZA nanosvg (su receta ya lo
decía) y fcft va con -Dsvg-backend=none. boost-build no hace falta porque la receta de boost es
sólo cabeceras. validate-pkg-config no es una librería sino un setup hook de nixpkgs → nix-ismo.
CAPACIDADES AUSENTES que quedan dichas: sin polkitd no corren las reglas .rules de JavaScript
(duktape); no se regula el brillo de un monitor EXTERNO por DDC/CI (ddcutil); los PDF con
tipografías CJK no incrustadas se ven mal (poppler-data); dolphin navega sin panel de metadatos
ni etiquetas (baloo-widgets).
⚠ LOS TRES QUE NO FIRMO, y por qué. No son «opcional»: son huecos de RUNTIME que ninguna receta
delata al construir, y decidirlos es elegir alcance de la distro, no triaje. Los tres con la
medición hecha y escrita en su `porque`:
gnome-keyring NINGÚN artefacto del store declara org.freedesktop.secrets (grep sobre el store
entero), y el artefacto de kwallet trae sólo libKF6Wallet.so, sin kwalletd6. Los
dos escritorios tienen el PROMPTER sellado y ninguno tiene el ALMACÉN
glib-networking los usr/lib/gio/modules/ de los cuatro artefactos de glib están VACÍOS ⇒ GIO no
tiene TLS, y libsoup 3 delega el TLS en GIO ⇒ HTTPS mudo en el stack GNOME
xdg-desktop-portal-kde los únicos backends de portal sellados son el de cosmic y el de gnome; la
cola kde no tiene ni receta de xdg-desktop-portal ⇒ en Plasma la captura de
pantalla de obs-studio (linux-pipewire → portal ScreenCast) no tiene con quién
hablar, mientras que en GNOME y COSMIC sí
Los tres son el mismo patrón que ya nos costó una noche con las fuentes: una raíz que no resuelve
queda `wanted` y NINGUNA métrica lo dice. Marcarlos `hueco` los mete como raíz en targets.toml y
al drenaje; eso lo decide el usuario.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
De 53 pendientes a 27. Cierran okular, libsndfile, libpsl, libtiff, colord, libplacebo, zxing-cpp,
glib, modemmanager, plasma-nm y evolution-data-server.
⚠ EL HALLAZGO QUE ABARATA MÁS: qtwebengine. En plasma-nm, Qt6WebEngineWidgets se pide REQUIRED —
pero SÓLO dentro de `if(BUILD_OPENCONNECT)` (CMakeLists.txt:54-56). O sea que un Chromium entero
colgaba de la vista web de SSO de una VPN, y el -DBUILD_OPENCONNECT=OFF que la receta ya pasaba lo
saca del grafo. Vale la pena mirar dónde MÁS aparece qtwebengine antes de darlo por deuda.
⚠ DOS ALIAS MÁS, y cada uno estrena una FORMA nueva de fallar el cruce por nombre:
gusb → libgusb el prefijo `lib` (libgusb 0.4.9, ya en el [deps].build de colord)
glad2 → glad la VERSIÓN metida en el nombre: recipes/glad.toml pinea la 2.0.8, que ES glad2.
nixpkgs parte glad(v1) y glad2(v2) en dos paquetes; acá hay uno solo
Van seis alias y cuatro formas distintas: puntuación, mayúsculas, prefijo y versión. Ya no es
casualidad — cuando el sembrador vuelva a tocarse, normalizar por ahí.
DOS BUNDLEOS que parecían deps y no lo son, los dos verificados en el fuente:
zint zxing-cpp lo trae adentro (CMakeLists.txt:7, ZXING_USE_BUNDLED_ZINT ON por defecto, y
core/CMakeLists.txt:532 compila core/src/libzint)
publicsuffix-list la lista VIENE en el tarball de libpsl y --enable-builtin la hornea en el .so.
Traerla aparte sería meter un dato mutable en un store direccionable por contenido
Y autogen es andamiaje, no dep: configure.ac:68 busca el PROGRAMA y si falta sólo hace
`touch tests/*.c`; el Makefile.am:414 dice que los ficheros generados vienen en el tarball
«to prevent stale files from calling autogen in tarball releases».
CAPACIDADES AUSENTES que quedan dichas: okular abre PDF pero no PostScript (libspectre), ni DjVu,
ni previsualiza Markdown; no hay cliente VPN AnyConnect (openconnect); no hay calibración de
pantalla por colorímetro (argyllcms) ni stack de escáner (sane-backends).
Quedan 27 pendientes, todos de 1×. Dos de ellos NO son «opcional» y no los firmo de paso —
gnome-keyring y glib-networking, los dos con la medición hecha y escrita.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
De 78 pendientes a 53. Caen enteras opencv, swayimg, networkmanager, gwenview, appstream y
plasma-desktop.
⚠ CORRECCIÓN, y es de las que cambian lo que uno haría. En dos commits de hoy avisé que cairo
(lzo) y xwayland (libdecor) piden esas libs con `auto`/`required:false` y que «si entran al
catálogo, el ArtifactHash cambia sin que nadie lo pida». El mecanismo está mal: el sandbox de
hammer monta SÓLO las deps declaradas (docs/02-build-lab.md §3.4 — raíz tmpfs y bind read-only de
las deps del store), así que meterla en [deps] mueve hash_inputs y SE VE. La vía por la que un
`auto` sí muerde en silencio es otra y ya está documentada: que la lib aparezca en el LAB, que no
entra en hash_inputs. Los dos `porque` quedan corregidos en el fichero; el aviso sigue valiendo,
pero apunta al lab y no al catálogo.
Lo demás, cada uno con su prueba:
opencv -DBUILD_LIST=core,imgproc ⇒ de todo OpenCV se construyen DOS módulos: gflags y
glog son del dnn y hdf5-cpp del hdf, ninguno en la lista. eigen y openblas caen
por -DWITH_EIGEN=OFF y -DWITH_LAPACK=OFF, explícitos
swayimg -Davif/-Dheif/-Draw/-Dsixel=disabled, los cuatro escritos en la receta
networkmanager newt es la dep de nmtui y el propio meson lo dice en su assert (meson.build:806:
«Use -Dnmtui=false to disable it»), que es justo lo que pasamos; slang es su
back-end. bpftools cae por -Debpf=false
gwenview cfitsio (FITS de astronomía), libkdcraw (RAW) y kimageannotator son TYPE OPTIONAL
en su CMakeLists; qtimageformats no aparece ahí — son plugins de Qt de runtime
appstream -Dstemming=false para libstemmer
plasma-desktop kaccounts-integration es TYPE OPTIONAL y su PURPOSE lo dice («OpenDesktop
integration plugin»); xf86-input-evdev y xf86-input-libinput son drivers del
SERVIDOR X y la distro no corre uno; plasma-sdk no aparece en su CMakeLists
⚠ QUINTO, SEXTO Y SÉPTIMO DESFASE DE VERSIÓN: xapian, libfyaml y libblake3 no aparecen en NINGÚN
meson de AppStream 1.0.5, que es la que pineamos. Ya van siete candidatos que salen de expresiones
de nixpkgs más nuevas que nuestros pines (openapv, libebur128, spandsp, libglycin y estos tres).
Es un patrón, no una casualidad: cuando el `porque` heurístico dice «lo pide una sola receta»,
mirar primero si la versión pineada la nombra sale más barato que razonar sobre la feature.
⚠ dnsmasq no es una librería: NM la declara `type: 'string'` (meson_options.txt:10), una RUTA a un
binario que ejecuta. Capacidad ausente —compartir la conexión y el DNS con caché— no dep rota.
Y gnome-keyring sigue pendiente, ahora con la medición terminada escrita en su `porque`: el grep
sobre el store ENTERO dice que NINGÚN artefacto declara org.freedesktop.secrets, y del lado KDE el
artefacto de kwallet trae sólo libKF6Wallet.so — la librería cliente— sin kwalletd6. Los dos
escritorios tienen el prompter sellado y ninguno tiene el almacén.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
De 87 pendientes a 78. Las 4 de mutter y 5 de las 6 de gnome-shell. La sexta —gnome-keyring—
queda PENDIENTE a propósito y con motivo, abajo.
Las de mutter caen contra su meson leído, no contra su comentario:
argcomplete tools/meson.build:17 lo usa como PROGRAMA para generar el completado de bash de
`gdctl`, dentro del bloque `if bash_completion` ⇒ -Dbash_completion=false lo mata.
No se enlaza: no es una librería, es un generador
sysprof meson.build:453, `if have_profiler` ⇒ -Dprofiler=false
libstartup-notification default TRUE en meson.options:114 y la receta lo apaga a mano. Es el
cursor «ocupado» de X11; en Wayland lo reemplaza xdg-activation, que mutter
implementa nativamente ⇒ no se pierde la función, cambia de dónde sale
libglycin ⚠ CUARTO DESFASE DE VERSIÓN del barrido: mutter 48.8 no nombra glycin en ningún
fichero. El cargador de imágenes enjaulado entra en la serie 49 de GNOME
Y las de gnome-shell:
gdm aparcado POR DISEÑO, targets.toml:263. La sesión la lanza arje
gnome-autoar sólo la pide extensions-tool (su meson.build:32) y va -Dextensions_tool=false:
desempaqueta el .zip de una extensión, no pinta escritorio
gnome-clocks cero referencias en los meson: es una APP que el panel consulta en runtime para
los relojes del mundo
gnome-bluetooth cero referencias, y el hueco de fondo es el mismo de ayer: no hay bluez
libnma ⚠ otra vez la etiqueta y el hecho: gnome-shell 48.8 NO la nombra. Su camino de red
es libnm + libsecret-1 (meson.build:106-108), apagado con -Dnetworkmanager=false
⚠ POR QUÉ gnome-keyring SIGUE PENDIENTE. Es el primero del barrido que no parece «opcional» sino
falta de verdad, y decidirlo cambia el trabajo de la granja (un `hueco` nace como raíz en
targets.toml y entra al drenaje), así que no lo firmo de paso. Lo medido hasta acá:
js/ui/components/keyring.js:221 hace `own_name('org.gnome.keyring.SystemPrompter')` ⇒ el shell es
el que PREGUNTA la contraseña, no el que guarda el secreto. El almacén es el demonio, y no hay
receta de gnome-keyring en el catálogo. libsecret está sellada, pero libsecret es el CLIENTE.
Del lado KDE hay kwallet y plasma-workspace publica org.kde.secretprompter.service, o sea que ese
perfil sí parece tener con qué. Queda una medición corriendo sobre el store entero (quién declara
org.freedesktop.secrets) y con eso se decide; si nadie lo declara, es hueco y hay que decirlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
De 105 pendientes a 87. Cuatro de las cinco recetas que más candidatos arrastraban ya están
cerradas (ffmpeg, obs-studio, xwayland, nodejs, pipewire).
Las 8 de nodejs son todas BUNDLEADAS, y no se dio por bueno el comentario de la receta: se listó
el propio node-v24.18.1.tar.gz y ahí están, deps/googletest, deps/histogram, deps/llhttp,
deps/merve, deps/nbytes, deps/simdjson, deps/uvwasi. simdutf es la excepción que confirma que
mirar sirve — NO está en deps/ de primer nivel sino en deps/v8/third_party/simdutf, que es
exactamente el fichero que la receta parchea para apagar el kernel AVX-512.
⚠ CAPACIDAD AUSENTE, y más honda que la perilla: ldacbt, libfreeaptx y liblc3 son los codecs de
audio Bluetooth (LDAC de Sony, aptX de Qualcomm, LC3 de LE Audio). Caen por -Dbluez5=disabled,
pero el hecho de fondo es que NO HAY receta de bluez en todo el catálogo: la distro no tiene
Bluetooth, ni de audio ni de nada. Eso conviene que esté dicho acá y no descubierto por alguien
que enchufa unos auriculares.
⚠ SEGUNDO Y TERCER DESFASE DE VERSIÓN del barrido (el primero fue openapv/ffmpeg): libebur128 y
spandsp no aparecen en NINGÚN fichero de pipewire 1.2.7, que es la que la receta pinea. Salen de
una expresión de nixpkgs para una pipewire más nueva. Van como 'opcional' y no 'nix-ismo' a
propósito, igual que openapv: mandarlas al descarte del sembrador las haría invisibles el día que
subamos de versión, que es justo cuando vuelven a ser una pregunta legítima.
El resto de pipewire son perillas apagadas a mano y verificadas contra su meson_options.txt:
libffado:350 (audio FireWire), libcamera:180, libmysofa:229 (HRTF de audio espacial), lv2:261
(lilv, el host de plugins) y roc:237 (audio en tiempo real por red).
Nota sobre el commit anterior: dije que convenía normalizar guiones y mayúsculas en seed-graph.py.
Medido después — normalizando nombre de candidato contra el campo `name` de las 1141 recetas, de
los 105 pendientes CERO son ese caso. nlohmann_json y libxfont_2 eran los únicos. Sigue siendo
buena profilaxis, pero no hay backlog que la pague: no se toca el sembrador por ahora.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
De 125 pendientes a 105. Las dos recetas más pedidas del ranking quedan sin un solo candidato
pendiente. Las de obs ya estaban medio contestadas en los comentarios de su propia receta; lo que
faltaba era mirar el ARTEFACTO y no sólo la línea de cmake.
⚠ DOS ALIAS MÁS, y los dos son el mismo defecto del sembrador: cruza por nombre literal.
nlohmann_json → nlohmann-json guion bajo vs guion
libxfont_2 → libXfont2 minúsculas vs mayúsculas (primero de esta forma)
Las dos recetas existen, están selladas y ya figuran en el [deps].build de quien «las pedía».
Van tres alias por puntuación o caso (con mesa-gl-headers, cuatro en total): conviene normalizar
en seed-graph.py antes que seguir triándolos de a uno.
MEDIDO, NO DEDUCIDO — el artefacto sellado de obs-studio:
· usr/lib/obs-plugins NO tiene obs-websocket.so ⇒ asio, websocket++ y qrcodegencpp no tienen a
quién servir. El `touch plugins/obs-websocket/CMakeLists.txt` de la receta funciona.
· readelf sobre los 13 plugins: CERO NEEDED a libcjson. El JSON que OBS enlaza es jansson.
· obs-x264.so está y enlaza libx264.so.165 ⇒ la codificación H.264 del escritorio existe y no
pasa por ffmpeg. Es lo que hace que -DENABLE_QSV11=OFF (libvpl) no cueste capacidad.
⚠ libxres es «la etiqueta no es el hecho» otra vez: lo que xwayland usa es `resourceproto`
(meson.build:90), las CABECERAS del protocolo XRes, que vienen en xorgproto y ya están en [deps].
libXres es la librería CLIENTE — el servidor IMPLEMENTA la extensión, no la consume.
⚠ Y libxaw/libxmu/libxpm/libxt no aparecen en NINGÚN meson del árbol: sólo en .appveyor.yml y
.gitlab-ci/debian-install.sh, que instalan las deps de todos los servidores del repo xserver
(Xorg, Xnest, Xvfb). El sembrador leyó la expresión de nixpkgs, que hereda esa lista entera.
⚠ SEGUNDA ESCOTILLA 'auto' de la jornada (la primera fue cairo/lzo): xwayland pide libdecor con
`required: false` y la opción en 'auto' (meson.build:210). Si libdecor entra al catálogo y cae en
su clausura, xwayland cambia de ArtifactHash sin que nadie lo haya pedido. Sólo decoraría la
ventana en modo ROOTFUL, que no es como lo lanza el compositor.
dri-pkgconfig-stub cierra limpio: include/meson.build:9 lo pide como
`dependency('dri', required: build_glx)` ⇒ obligatorio SÓLO con glx, y la receta va -Dglx=false
porque ninguna cola publica gl.pc. font-util aparece una vez y es el default de `fontrootdir`, la
raíz de las fuentes de mapa de bits legacy que la distro no publica.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
De 140 pendientes a 125. Todas caen por el mismo criterio ya establecido (--disable-autodetect ⇒
sólo entra lo que se pide, y no se pide nada), pero el veredicto NO se firmó de memoria: se midió
con nm/strings sobre store/…-ffmpeg/usr/lib/libavcodec.so.61.19.100, que es el artefacto que la
distro publica hoy.
LO QUE LA MEDICIÓN CAMBIA. La lectura fácil era «sin libaom/libvpx/x265 la distro no reproduce
AV1/VP9/HEVC», y es FALSA: los decoders nativos están compilados (aparecen «AV1 decoder», los bsf
vp9_*, hevc y theora). Lo que falta es CODIFICAR. El artefacto trae 34 encoders y ninguno de vídeo
moderno: AAC, FLAC, Opus, MPEG4, mjpeg, ffv1, prores, H.263, jpeg2000. O sea: la distro decodifica
lo de hoy y codifica lo de ayer.
Y esa frase tampoco alcanza sola, porque la codificación H.264 del escritorio NO pasa por ffmpeg:
obs-studio enlaza x264 —que SÍ está en el catálogo, recipes/x264.toml— por su propio plugin
obs-x264. Antes de anunciar «no se puede grabar vídeo» convenía mirar quién codifica de verdad.
⚠ openapv NO es una perilla que no encendimos: es DESFASE DE VERSIÓN. Sale de la expresión de
nixpkgs para ffmpeg 8.x y nuestra receta pinea 7.1, cuyo `configure` no nombra apv en ningún sitio
(verificado en el tarball). Queda 'opcional' y no 'nix-ismo' a propósito: el día que ffmpeg suba de
major vuelve a ser una pregunta legítima, y mandarla al descarte del sembrador la haría invisible.
⚠ ocl-icd y opencl-headers tienen el hueco una capa MÁS ABAJO: mesa va con -Dllvm=disabled
-Dgallium-rusticl=false ⇒ la distro no publica NINGÚN runtime de OpenCL. Un ICD loader sin ICD no
filtra nada; encender --enable-opencl en ffmpeg no compraría capacidad.
CAPACIDADES AUSENTES, dichas en voz alta y no descubiertas después:
libbluray no se navegan discos Blu-ray (menús, títulos, BD-J); los ficheros sueltos se leen
libopenmpt no suena música de módulos (MOD/XM/S3M/IT)
amf-headers / libvdpau codificación y decodificación por hardware de AMD y NVIDIA: piden driver
propietario, que la distro no trae ⇒ discutible sólo si eso cambia
El resto no cuesta nada: libtheora es el encoder de un formato de 2004 (su decoder es nativo),
xvidcore es REDUNDANTE con el «MPEG4 encoder» nativo que ya está, vid.stab agrega dos filtros de
estabilización y zimg sólo el filtro zscale (el escalado lo hace libswscale, compilada).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
De 152 pendientes a 140. Con esto el ranking del triaje queda entero en 1×: ninguna candidata
la piden ya dos recetas. Cada veredicto con la prueba en el fuente o en la receta, no de memoria.
⚠ mesa-gl-headers NO faltaba — SEGUNDO alias del barrido (el primero fue bubblewrap→bwrap). Es
el split de cabeceras de nixpkgs: el artefacto de mesa instala usr/include/{GL,GLES2,GLES3,KHR,
EGL}, verificado en el store y no deducido. El único hueco es GLES1, apagado a propósito en la
receta (-Dgles1=disabled).
⚠ libmpdclient destapa una FALSA ATRIBUCIÓN del sembrador: dice que la piden mpc y yambar, y
recipes/mpc.toml es GNU MPC (aritmética compleja, --with-gmp --with-mpfr), no el cliente de MPD.
Homónimo. El sembrador cruza por NOMBRE, así que este no va a ser el único; conviene mirar el
`piden` antes de creerle cuando el nombre es corto y genérico.
⚠ lzo deja una escotilla abierta que conviene que esté dicha: libarchive pasa --without-lzo2
explícito, pero cairo NO la apaga — meson.options:19 la declara feature 'auto' y meson.build:203
la tomaría si apareciera en el sandbox (sólo la usa util/cairo-script). Si algún día lzo entra al
catálogo y cae en la clausura de cairo, cairo cambia de ArtifactHash sin que nadie lo pida.
El resto, con su prueba:
c-ares configure.ac:225 de nghttp2 la fuerza a `no` DENTRO de --enable-lib-only, que es
lo que la receta pasa ⇒ ni presente se usaría; nodejs bundlea la suya
ada nodejs la bundlea a propósito y la receta lo dice con nombre y apellido
CUnit → nix-ismo: nghttp2 1.64.0 no la nombra en ningún fichero, sus tests usan munit
VENDORIZADO en tests/munit/. Dep rancia de nixpkgs; no vuelve ni con tests ON
egl-wayland EGLStream de NVIDIA: mutter meson.options:84 la trae en false y no la pedimos, y
xwayland 24.1.13 ya no la soporta (hw/xwayland/meson.build:171 have_eglstream=false)
glu CERO referencias en phonon 4.12.0 y xwayland 24.1.13, grep sobre el árbol entero
libssh ffmpeg --disable-autodetect sin --enable-libssh; konsole no la nombra: su plugin
SSHManager (ON, y lo construimos) lanza el binario ssh, que el corpus SÍ tiene
libxcb-errors wlroots -Dxcb-errors=disabled y yambar -Dbackend-x11=disabled, las dos escritas
dbus-python
pygobject sólo tests: mock-service*.py de libsecret; modemmanager -Dtests=false
Granja: las cinco colas selladas y deuda 0 (corpus 868, kde 1046, gnome 920, cosmic 894, wlr 879);
el worker está idle. Esto es trabajo de hub, que es lo que queda cuando no hay cola que drenar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
De 166 pendientes a 152.
⚠ bubblewrap NO faltaba: es el MISMO paquete con otro nombre. nixpkgs lo llama 'bubblewrap',
acá es 'bwrap' (recipes/bwrap.toml) y su artefacto instala /usr/bin/bwrap. Verificado —receta,
artefacto y binario— y marcado 'provisto' con su alias, que es justo para lo que existe esa
categoría. Es el primer alias real que aparece en todo el barrido.
ffmpeg-headless → nix-ismo: variante de empaquetado, ya tenemos ffmpeg. Mismo criterio que
git-minimal.
gnome-settings-daemon → aparcado POR DISEÑO y ya documentado en targets.toml:263: 'SIN
gnome-session / gnome-settings-daemon / gdm'. El camino vivo de GNOME es mutter → gnome-shell.
⚠ SEXTA capacidad ausente: mobile-broadband-provider-info destapa que la distro NO TIENE BANDA
ANCHA MÓVIL — networkmanager con modem_manager=false y ppp=false, modemmanager con mbim=false
qmi=false. De ahí caen también ppp y sbc (éste por el bluez ya apagado).
El resto son el cubo de ffmpeg --disable-autodetect (nv-codec-headers, librist, lame, fdk-aac,
zvbi) más openexr (opencv WITH_OPENEXR=OFF), libjxl (swayimg jxl=disabled) y libjack2
(jack=disabled en las tres que lo piden).
⚠ Y una corrección de método: mi primera búsqueda de alias fue por subcadena y produjo basura
—'nv-codec-headers' casaba con 'unconvert.toml'—. Sólo se marcó el que se verificó de verdad.
libssh tampoco se tocó: tenemos libssh2, que es OTRA librería, y konsole no lo menciona.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2