Commit Graph
343 Commits
Author SHA1 Message Date
SergioandClaude Opus 5 262a4aeb61 granja: forzar la recompilacion — rsync -a preserva mtime y cargo no rebuildea
Sincronizar el codigo nuevo al worker y llamar a cargo NO basta: rsync -a
preserva el mtime del hub, asi que el fuente recien llegado puede quedar mas
VIEJO que el binario que el worker compilo hace un rato. cargo dice
«Finished in 0.09s», el worker se queda con el hammer anterior, calcula la
huella vieja y el paso 4 falla sin que se vea por que.

Paso el 2026-08-12 al empujar el lab con los -dev: el worker tenia el lab.rs
correcto Y el rootfs correcto, y aun asi divergia. Se arregla con un `touch`
a los fuentes antes de compilar.

El guardian del paso 4 hizo su trabajo: detecto la divergencia y NO dejo
construir. Sin el, el worker habria molido horas sellando en direcciones que
el hub nunca iria a buscar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 01:05:01 +00:00
SergioandClaude Opus 5 248cd5b270 lab: pinear la imagen nueva (con los -dev de CPython)
sha256 3c9c1ef3... reemplaza a db8a3252... La anterior producia un python3 sin
_ctypes ni _curses y con el se caia la familia GNOME/KDE entera.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 01:01:57 +00:00
SergioandClaude Opus 5 1940a94a11 lab: los -dev al rootfs — es el unico sitio donde _curses se puede resolver
Decision del usuario. CPython construye sus modulos opcionales como .so
COMPARTIDOS y las .a del corpus NO son PIC («relocation R_X86_64_PC32 against
symbol 'stdscr'»), asi que declararlas en [deps] NO funciona: se probo una
por una. Las libs del rootfs son compartidas y si sirven.

Añadidos: libffi-dev ncurses-dev readline-dev sqlite-dev bzip2-dev xz-dev
expat-dev. Resultado VERIFICADO: el guardian de python3 importa los OCHO
modulos (_ctypes _curses readline sqlite3 bz2 lzma zlib pyexpat) y pasa. De
paso `libffi` sale de [deps]: ya lo aporta el lab.

openssl-dev NO va, deliberado: se de-Alpinizo porque el host-tool del kernel
enlaza el openssl del corpus desde el overlay; devolverlo podria cambiar el
artefacto del kernel. python3 sigue sin `ssl`.

Y SE CIERRA EL AGUJERO QUE ESTO ABRIA. Esas libs del rootfs ahora entran en
el CONTENIDO de un artefacto, asi que su version es parte de su identidad ⇒
van a TOOLCHAIN_PREFIXES. Sin eso habriamos reabierto en pequeño el mismo
fallo que 58d3161 cerro: instalar los -dev subio ncurses 6.5→6.6 y readline
8.3.1→8.3.3 —sin tocar gcc/rust/musl, lo confirmo el lock— y ese salto habria
cambiado el python3 producido SIN mover su direccion.

Coste: la huella del lab cambia ⇒ el corpus se re-hashea otra vez. Se hace
AHORA a proposito, con 194 artefactos, no con el corpus a medio construir.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:55:01 +00:00
SergioandClaude Opus 5 82f9055670 respaldo: el listado no recorre .dmerge — tardaba tanto que se cortaba
`du -s hammer/store/*` incluia `.dmerge`, la cache transitoria de fusion del
store: 48 GB en el box el 2026-08-12. Recorrerla hacia que --listar tardara
lo bastante como para cortarse por timeout, y entonces el box "devolvia 0
artefactos" — el guardian de los 0 hizo bien su trabajo y se nego a pisar el
manifiesto, pero la causa no era el enlace.

Con `store/[0-9a-f]*` el listado baja de cortarse a 3 s, y de paso descarta
en el origen `.bootstrap-tmp`/`.mirror-tmp`, que nunca son artefactos. Un
artefacto SIEMPRE empieza por hex: filtrar en el origen sale mas barato que
traerse la basura y descartarla despues.

Verificado: 2037 artefactos, 0 vacios.

⚠ Aparte: que .dmerge ESTE en el respaldo es en si un problema — son 48 GB de
cache reconstruible ocupando el box, y el propio script la excluye al subir
(`--exclude /.dmerge`). Llego por otra via. No se toca aqui: borrar en el
destino es una decision deliberada y aparte.

Y una limitacion del guardian de vacios que conviene conocer: durante una
SUBIDA en curso, los directorios a medio escribir son indistinguibles de los
rotos. Reporto 101 vacios a mitad del rsync y 0 al terminar. No es un falso
positivo del filtro: es que la pregunta no tiene respuesta estable mientras
se escribe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:31:15 +00:00
SergioandClaude Opus 5 a95a1126c3 corpus: excluir .deferred — son recetas aparcadas que no construyen sueltas
Sus deps.build resuelven contra el directorio hermano, que en .deferred no
existe: fallan al instante con «no pude cargar la dep de build 'zlib' de
'git' (recipes/incoming/.deferred/zlib.toml)». Son 25.

Intentarlas no es solo inutil: mete 25 FALLA en el log que parecen roturas
del corpus y esconden las de verdad. Un log lleno de fallos esperados es un
log que nadie lee.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:27:14 +00:00
SergioandClaude Opus 5 7279ea8780 granja: farm-up copiaba work/ ENTERO y podia dejar un worker sin dead-man
Dos fallos que se destaparon montando el primer worker con el lab anclado
(2026-08-11).

1. `--exclude /work` con barra inicial ancla SOLO la entrada `work`, no su
   contenido. Con `--include '/work/'` delante, rsync entraba al directorio y
   sus hijos no casaban con ninguna regla ⇒ se copiaba work/ ENTERO: 3,7 G,
   incluidos los mirrors de git A MEDIO COPIAR porque el propio rsync abortaba
   antes de terminarlos. El worker fallaba cada receta con `git fetch exit
   128`. Un work/ a medias es peor que ninguno: parece que esta. Se ancla con
   `/work/**`.

   Eso mismo causaba el abort: con --delete, rsync moria con «cannot delete
   non-empty directory» sobre los vendor/ de cargo, que quedan de SOLO
   LECTURA. Sin tocar work/, no hay nada que borrar ahi.

2. El `set -e` hacia que ese fallo abortara el script ANTES de armar el
   dead-man switch ⇒ server VIVO que no puede autodestruirse, que es
   exactamente lo que este script declara inadmisible. Ahora el rsync no es
   fatal: avisa y sigue hasta armarlo. El orden ideal es armar el dead-man
   ANTES de cualquier paso que pueda fallar; queda anotado en el codigo.

Verificado en seco contra un worker real: el patron nuevo no toca work/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:02:10 +00:00
SergioandClaude Opus 5 1c17f2f255 granja: empujar el lab anclado al worker y PROBAR que sella igual que el hub
farm-sync excluye /.dev-fs a proposito (el lab es del entorno, no del repo).
Eso valia cuando el toolchain no entraba en el hash. Desde 58d3161 si entra,
asi que un worker con el lab horneado de la golden (rustc 1.96) sella en
direcciones DISTINTAS a las del hub (1.97): no es que compile distinto, es
que lo guarda donde el hub nunca lo va a buscar.

Tampoco basta bootstrap-devfs.sh en el worker: su paso 0 solo trae la imagen
si NO hay rootfs, y la golden trae uno. Se reemplaza a la fuerza.

La imagen se EMPUJA por scp desde el hub en vez de bajarla del Storage Box,
para no poner la llave del box en el worker: el modelo hub-and-spoke dice que
el worker es compute puro sin secretos.

El paso 4 no es 'extraje la imagen', es comparar el hammer hash de una receta
testigo entre worker y hub. Si divergen FALLA: un worker que sella en otra
direccion quema dinero produciendo artefactos que nadie encuentra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 11:18:09 +00:00
SergioandClaude Opus 5 05e8ba853b corpus: driver de reconstruccion — en serie, reanudable y por objetivo
Hace falta cada vez que cambie la imagen del lab, porque desde 58d3161 el
toolchain entra en hash_inputs y eso mueve el hash de TODO el corpus. Es el
precio explicito de cambiar de compilador; antes se pagaba sin enterarse.

EN SERIE bajo el flock compartido (ADR 0012): dos builds que compartan una
dep se pisan el arbol de fuentes y queda roto para siempre.

REANUDABLE sin estado propio: pregunta `hammer hash --check` (~2 ms) en vez
de llevar un fichero de progreso. Matarlo y relanzarlo continua donde iba.

POR OBJETIVO, no alfabetico: hay 1186 recetas y solo 168 alcanzan una imagen.
Ordenado por targets.toml primero, cada perfil que cierra es utilizable ya;
por orden alfabetico, tras un dia de maquina no habria ni una imagen.
Dos correcciones en el camino: usar los nodos de build-state marcaba 1161 de
1161 como prioritarias (ese grafo ES el corpus entero, no las clausuras), y
`glob('recipes/**/*.toml')` se dejaba 25 ficheros fuera — os.walk da 1186,
que es lo que cuenta `find`.

La purga de work/sources va ENTRE recetas, con nada corriendo. Purgar con un
build en vuelo le arranca los ficheros al compilador: paso el 2026-08-10 y se
llevo un kernel a medias.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:21:10 +00:00
SergioandClaude Opus 5 0de4bf08aa lab: el rootfs se ANCLA en una imagen pineada — deja de resolverse con apk
Al entrar el toolchain en hash_inputs (58d3161) se abrio un agujero
operativo: los repos del rootfs apuntan a Alpine edge, que es RODANTE, asi
que dos maquinas que corran bootstrap-devfs.sh en fechas distintas resuelven
toolchains distintos y NO COMPARTEN NI UN ARTEFACTO. Ni cosecha de granja, ni
mirror pull, ni un cache-hit. Cada worker efimero nacia con un lab propio.

`apk add` contra edge es irrepetible por diseno (edge sirve solo la ultima
version). La salida no es repetir la resolucion, es no repetirla: el rootfs
se construye UNA vez y se TRAE pineado por sha256 — el mismo trato que ya
reciben alpine-minirootfs y zig en este script.

  imagen: 294 M comprimida (1,1 G extraida)
  sha256: db8a32526ad41fbcc76223842dbb4eb65c8ed576570b82152dc7c5691fc2a349

EL TAR ES DETERMINISTA A PROPOSITO (--sort=name --mtime=@1 --owner=0
--numeric-owner): sin eso el sha depende del orden del directorio y del uid
de quien empaqueta, dos empaquetados del MISMO rootfs darian imagenes
distintas y no habria forma de verificar que una imagen es la que dice ser.
Comprobado: dos pasadas, mismo sha.

EL REEMPLAZO ES POR ROTACION, no extrayendo encima: a medias quedaria una
MEZCLA de dos rootfs, que es justo lo que esto existe para evitar.

VERIFICADO de punta a punta: bajada del box, sha intacto tras el viaje,
extraida, y la huella de lab que produce es IDENTICA a la del rootfs vivo
(zlib -> b3:c7a7338f... en las dos). O sea que una maquina que arranque de
esta imagen comparte store.

--from-scratch conserva el camino viejo: es como se FABRICA una imagen nueva.
Reconstruir el corpus es el precio de ese cambio, y ahora es explicito.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 18:57:13 +00:00
SergioandClaude Opus 5 39873fd86e lab: el TODO mandaba a pinear un snapshot de edge que no existe
Alpine no publica snapshots datados de edge y el repo sólo sirve la última
versión, así que ese camino no estaba pendiente: estaba cerrado. Apunta al
lock del paso 3a-quater, que es lo que sí se puede hacer hoy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:05:08 +00:00
SergioandClaude Opus 5 a8f77acc9e lab: lock del toolchain, porque edge es rodante y la deriva no se veia
Los repos del rootfs apuntan a Alpine edge (bump deliberado por el techo
MSRV). Es RODANTE: el mismo script en dos fechas da compiladores distintos.
Medido el 2026-08-10 al bootstrapear gioser — el laptop tenia rust 1.96 y
aqui edge resolvio 1.97.0-r0. Para un proyecto cuyo invariante es reproducir
eso es deriva del LAB, y no aparece en build-state.json.

No es un pin y no puede serlo: edge sirve solo la ultima version (`apk policy
rust` lista unicamente 1.97.0-r0), asi que `apk add rust=1.96.0-r0` rompe en
cuanto edge avanza, y Alpine no publica snapshots datados de edge. Anclar de
verdad pide espejar APKINDEX + los .apk, que es un frente aparte.

Lo que si se puede hoy: registrar los 92 paquetes resueltos en
docs/state/lab-toolchain.lock —que viaja por git, que es como los dos hubs lo
comparan— y AVISAR con el diff delante cuando la maquina difiere. Aceptar el
cambio es deliberado: --relock. Un lock que no se puede imponer sigue
valiendo si al menos nombra lo que cambio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:04:45 +00:00
SergioandClaude Opus 5 0377f4fa7a guardianes: un artefacto VACIO deja de contar como presente
Dos eslabones de la misma cadena, que el 2026-08-10 dejo cuatro recetas con
OK sin producir un solo fichero.

Store::has era path_of(..).is_dir(): un directorio vacio contaba como
sellado, asi que build() hacia cache-hit y devolvia Ok sin construir. Ahora
exige al menos una entrada. NO exige el sidecar .hammer/recipe.toml aunque
seria mas expresivo: ese lo escriben los llamantes, no seal(), y
hammer-bootstrap sella sin el ⇒ pedirlo lo haria reconstruir siempre. Va con
test de regresion.

--listar armaba el manifiesto con `ls`, que lista NOMBRES: un vacio es
identico a uno bueno, y de ahi build-state.py lo daba por sellado. Ahora usa
`du -s` (8,6 s sobre 1751, frente a un ls instantaneo), separa los vacios a
work/respaldo-vacios.txt y los DICE siempre, tambien cuando son 0.

Cuidado con el orden en ese awk: recortar la ruta antes se come el contador
de bloques y el filtro compara el nombre en vez del tamano — daba 406 vacios
falsos. Primero filtrar por numero, despues recortar.

Quedan 3 vacios sin curar en el respaldo (dbus x2 y un libxkbcommon de hash
superado); no caen en ninguna clausura construida.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:02:34 +00:00
SergioandClaude Opus 5 05a05cbac5 espejo: el gitea canónico sale del origin, no de una constante
Estaba fijo en ssh://…git.tawasuyu.net:2345/… mientras el clon de gioser
fetchea de gitea@git.gioser.net:… — la misma máquina, pero otra forma de URL
y otro puerto. Correr el script tal cual hacía --unset-all y le cambiaba el
destino canónico al clon sin avisar: un push que se va a donde no era y no
se nota hasta que importa.

Ahora deriva del origin de cada clon y el hardcodeado queda de fallback,
así sirve igual en el laptop y en gioser. Sigue siendo idempotente.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 04:32:17 +00:00
sergioandClaude Opus 5 7dc6808567 respaldo: el progreso de rsync sólo con terminal — 2,6 MB de log ilegible
Sin TTY, --info=progress2 reescribe con \r y el progreso se acumula en
miles de copias de la misma línea. El respaldo corre DESATENDIDO, así que
ese log es la única forma de saber qué pasó; a 2,6 MB no se lee.

Mismo defecto y mismo arreglo que en worker-depositar.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 00:06:45 -04:00
sergioandClaude Opus 5 36b8dd70e0 estado: sealed_remoto sale del JSON — dependía de la máquina, y ahora hay dos
Montando gioser como segundo hub salió el defecto: `sealed_remoto` cuenta
cuántos sellados NO están en el disco de QUIEN calcula. El laptop tiene 219
artefactos y gioser 19, así que el mismo corpus da 700 y 751. Metido en un
fichero commiteado, las dos máquinas se lo pisarían en cada regeneración,
para siempre.

El estado del corpus es compartido; cuánto de él tiene esta máquina en el
disco, no. Se sigue diciendo en el resumen, donde es útil y no genera churn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 00:00:48 -04:00
sergioandClaude Opus 5 6a92bd3d19 granja: dos fallos que se repetían idénticos, que es la firma de que nadie reintenta
1. build-farm.sh — vulkan-loader fallaba con «io: File name too long (os
   error 36)» en TODOS los ciclos y sella sin una queja ejecutado en serie
   (comprobado a mano en el worker). La firma no estaba en la lista de
   colisiones del reintento serial, así que nunca se reintentaba: fallaba,
   contaba como deuda, y al ciclo siguiente fallaba igual.

   Un fallo que se repite IDÉNTICO no es intermitente: es uno que nadie
   está reintentando.

   Se añade la firma, pero el defecto de fondo era que faltarla fuese MUDO
   — la lista sólo puede crecer si alguien se entera de que se quedó corta.
   Ahora lo no reintentado se dice, con las primeras 120 letras del log.

2. farm-up.sh — no sembraba work/farm-sellados.txt. Un worker recién
   creado arrancaba con el manifiesto rancio horneado en la golden: 1172
   contra 1241 del hub, así que reconstruía lo que el hub ya tenía sellado
   (vulkan-loader entre ellos, y encima fallando).

   Es el mismo error de método que farm-sync.sh ya se documentó a sí mismo:
   «lo puse allí, di el bucle por cerrado, y la churn siguió porque hay DOS
   rutas». Había dos otra vez y sólo una estaba arreglada.

Tras el arreglo las cuatro colas dan 1/1 construyen (0 fallan) y el perfil
escritorio-kde cierra 162/162.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 22:28:30 -04:00
sergioandClaude Opus 5 ddc8561e07 buzón verificado en un worker real — y las dos veces que la prueba mintió
Cadena completa medida hoy: el worker depositó 63 artefactos (1,34 GB) en
8 s a 153 MB/s, y el hub los promovió con mv. Respaldo 1542 → 1605, buzón
en 0, guardián cuadrando. Contra los 440 kB/s del laptop, 348×.

Dos fallos de la prueba, que importan más que el resultado:

1. `ssh` cortaba con «Host key verification failed» porque la granja REUSA
   IPs y la host key cambia con razón. Como el stderr iba a /dev/null, el
   error salía como «no obtuve la pública del worker»: culpaba al worker
   cuando el problema estaba en el known_hosts del laptop. Ahora va por
   ssh_worker(), que purga la entrada vieja — correcto sólo acá, porque la
   identidad del worker es su label hcloud, no su llave.

2. verificar-buzon daba «aislado ✓» sin haber probado que escribe. El
   aviso de host key se comía el head -1, y el testigo se llamaba .btest,
   que `ls` no muestra. Un testigo invisible no prueba nada. Ahora exige
   las dos mitades y las dos están verdes: escribe en su buzón, y
   ../hammer/store no existe para él.

Y el progreso de rsync sólo con TTY: sin terminal reescribe con \r y deja
miles de copias de la misma línea. 8 segundos bastaron para un log
ilegible; en el latido sería cada media hora.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:58:01 -04:00
sergioandClaude Opus 5 400f7171e0 respaldo: escribir sin poder borrar no es un permiso, es un alcance
Los permisos de subcuenta del Storage Box son BINARIOS: --readonly sí o no.
No hay append-only ni write-sin-delete (verificado en la API, no recordado).

Así que la contención se hace por alcance: el worker escribe en un BUZÓN
cuyo home es incoming/, y hammer/store sencillamente no existe para él. Lo
máximo que puede destruir es lo que él mismo depositó y aún no se promovió,
que sigue estando en el volumen. Un permiso puede estar mal puesto; un
directorio fuera de tu home, no.

La promoción es un mv DENTRO del mismo filesystem: un rename, instantáneo,
cero bytes por la red. Eso es lo que la hace viable con un uplink de
440 kB/s — el laptop manda órdenes, los datos van worker→box por dentro de
Hetzner (122 MB/s, 280×).

Medido de la shell del box, que NO es un bash:
  · no hay `for`   ⇒ los lotes se arman en el hub
  · `a; b` no encadena de fiar ⇒ una orden por conexión
  · `mv a b c dest/` sí acepta varios orígenes ⇒ cientos por conexión
  · `find` no existe y devuelve 0 EN SILENCIO ⇒ parece «no hay respaldo»

Y la trampa que motivó partir el trabajo en dos conjuntos: `mv A dest/` con
dest/A ya existente NO falla, mete A DENTRO y deja dest/A/A. Como el store
es CAS, un artefacto ya respaldado es idéntico ⇒ no se mueve, se borra del
buzón. Los dos caminos probados de punta a punta con un artefacto falso,
recuento verificado y sin anidar.

Defensa en profundidad aparte: plan de snapshots diario (03:17 UTC, retiene
7 de 10) y la carpeta ZFS visible para recuperar ficheros sueltos sin
rollback. La subcuenta no las alcanza: viven en la cuenta, con el token que
el worker no tiene.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:39:30 -04:00
sergioandClaude Opus 5 9388beba49 store en el volumen: «está sellado» dejó de ser «está en este disco»
El store de trabajo se mudó al volumen de la granja y el laptop quedó con
219 artefactos: los de fuente privada (que la granja no puede rehacer) más
lo que todavía no está respaldado. 979 artefactos verificados en el box se
borraron de acá, y con ellos el caché .dmerge de 92 G que sostenía sus
bloques: 128 G → 8,2 G, 115 G libres.

Eso rompía tres cosas que leían el disco local como si fuera la verdad:

- build-state.py habría reportado `never` sobre ~950 sellados y el latido
  lo habría COMMITEADO. Un grafo recién escrito miente con más autoridad
  que uno viejo. Ahora resuelve la presencia contra la unión del store y
  los manifiestos, y expone `sealed_remoto` para que «el store se mudó»
  no se lea nunca como «el corpus creció».
- farm-sync.sh armaba el manifiesto con `ls ./store`, así que le habría
  dicho al worker «el hub tiene 220» y el worker habría rehecho ~950. Es
  el bucle de churn que el propio fichero documenta, al revés. Ahora es la
  unión, con un guardián que aborta si el manifiesto encoge.
- cosecha-cron.sh bajaba el store entero cada 30 min: habría deshecho la
  mudanza sola, como la poda de 24 G que se deshacía en 2026-08-07. Ahora
  baja sólo la lista de nombres.

Los cuatro grafos regenerados dan idéntico a antes del recorte
(766/11/2), que es la prueba de que no se perdió nada.

Queda abierto: los artefactos nuevos viven SÓLO en el volumen hasta que
alguien corra una pasada de respaldo. El volumen tiene borrado protegido,
pero es una copia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:22:36 -04:00
sergioandClaude Opus 5 2d6a3c774b respaldo→volumen: la -H de rsync no es opcional, y al llegar hay que podar los superados
Dos cosas medidas al traer los 167 GB.

1. `rsync -a` SIN `-H` no preserva los enlaces duros, y el store comparte ficheros entre artefactos
   justamente así. Llegó inflado: 76 G en el box → 159 G en el volumen, con **0 ficheros de nlink>1**
   al llegar — ésa es la prueba de que se perdieron, no una sospecha. (El 76 G del box es además ZFS
   comprimido, así que las dos cosas se sumaban y parecía peor.) El contenido es correcto —es CAS y
   los hashes casan—, pero ocupa de más y el volumen no sobra.

2. El respaldo conserva artefactos que el hub YA podó. Son SUPERADOS: existe otro con el hash
   vigente, y no pueden dar cache-hit nunca porque la receta que los nombraba cambió. Eran 544 de
   1536 · 31 G. Podados con work/store-gc-superados.txt como lista.
   El guardián es RECONTAR tras el rm: «borré 544» y «hay 544 menos» son afirmaciones distintas, y
   sólo la segunda es la que importa. Cuadró.

Volumen: 992 artefactos vigentes, 95 G libres de 246.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 11:02:48 -04:00
sergioandClaude Opus 5 17c4194f5d granja: subcuenta de SÓLO LECTURA del Storage Box — el respaldo entra al volumen por dentro de Hetzner
MEDIDO, no supuesto: el uplink de la oficina da 440 kB/s ⇒ los 127 G del store son 82 horas. Subir
el store desde el laptop no es lento, es imposible. Pero el Storage Box está en hel1 y el worker
está en hel1: por la red interna la misma copia va a 122 MB/s. Son 280×.
⇒ El laptop deja de ser el camino de los datos. Sólo manda las RECETAS (26 M).

POR QUÉ SUBCUENTA Y NO LA CUENTA PRINCIPAL. El worker es efímero y se borra solo; darle la
credencial principal sería darle permiso de borrado sobre el ÚNICO respaldo que existe. Un respaldo
al que puede escribir la máquina de la que hay que protegerse no es un respaldo. La subcuenta acota
el daño a cero por construcción: --readonly, --reachable-externally=false, home acotado a hammer/.

LA VERIFICACIÓN ES NEGATIVA. «Sólo lectura» es una afirmación sobre lo que el sistema IMPIDE, así
que leer no la prueba. Hay que intentar escribir y borrar y exigir que fallen:
  rm  → «Read-only file system» · scp → «dest open: Failure» · respaldo intacto.
El script trae ese paso como subcomando `verificar` y sale ≠0 si la subcuenta resulta escribir.

Dos gotchas que costaron:
· la API exige contraseña con mayúscula+minúscula+número+símbolo aunque después se use llave;
· la shell del Storage Box es RESTRINGIDA: acepta ls/mkdir/rm pero NO redirección, así que
  `echo k > authorized_keys` falla EN SILENCIO (crea el directorio, no el fichero). Va con scp.

La llave se genera EN EL WORKER (nunca viaja una privada desde el laptop) y muere con él.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:43:16 -04:00
sergioandClaude Opus 5 4c06eaef88 farm-up: los comentarios nuevos llevaban backticks DENTRO de la cadena que va por SSH
El bloque del volumen es una cadena entrecomillada que se manda al worker, así que un backtick ahí
no es tipografía: es SUSTITUCIÓN DE ÓRDENES. Mis comentarios de la commit anterior citaban
`volume attach` y `blkid` con backticks ⇒ la shell intentó EJECUTAR «volume attach» («volume:
command not found») y a partir de ahí el resto del bloque se mandó mutilado: mkfs.ext4 sin
dispositivo, mountpoint sin argumento.

Por eso los comentarios originales de ese mismo bloque escapan los backticks con \`. Yo escribí los
míos con el estilo del resto del fichero, que es correcto FUERA de la cadena y venenoso dentro.
Arreglado quitándolos: en un comentario que viaja por SSH, la comilla no vale lo que cuesta.

El volumen no sufrió: sigue con su filesystem del 17 de julio y montado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:35:08 -04:00
sergioandClaude Opus 5 68d8ae0aa9 dead-man: una transferencia rsync también es trabajo
Poblar el volumen con el store del hub son horas de rsync sin un solo `hammer build`. El dead-man
sólo miraba builds y latido ⇒ acumulaba ticks y borraba el worker A MITAD DE LA COPIA. El volumen
sobrevive (para eso está), pero la transferencia muere y hay que reanudarla a mano; sobre un enlace
lento eso puede no converger nunca.

`pgrep -x` y no `pgrep -f`: `-f` mira la línea de órdenes entera y se auto-matchea con el propio
dead-man si su ruta contiene la cadena. Ese error ya nos hizo informar cuatro veces procesos «vivos»
que estaban muertos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:29:07 -04:00
sergioandClaude Opus 5 36081cf1cf farm-up: DOS bombas de pérdida de datos — una ya explotó y se llevó ~900 artefactos del volumen
Investigando por qué el worker nuevo arrancó con 2 artefactos en vez de ~900, apareció esto.

BOMBA 1 — `rm -rf $REMOTE/store` sin comprobar si es punto de montaje.
La guarda era `[ -d ] && [ ! -L ]`: basta en un server creado desde la golden ORIGINAL, donde
/opt/hammer/store es un directorio normal. Pero al re-snapshotear un worker (ayer, para meterle
Rust y Go) la imagen se llevó también LA CONFIGURACIÓN DE MONTAJE ⇒ el server nuevo monta el
volumen en esa ruta AL ARRANCAR, antes de que corra el «rescate». Entonces el `rm -rf` borra A
TRAVÉS DEL MONTAJE y se lleva el store del volumen entero — justo lo que el volumen existe para
proteger.
Evidencia: el volumen pasó de ~900 artefactos a 2, pero NO se perdió el filesystem (lost+found
de julio, 17 montajes, .dmerge intacto). Se borró el CONTENIDO, no el disco. Ahora hay
`mountpoint -q`: si ya está montado no hay nada que rescatar ni que borrar.

BOMBA 2 — `mount … || { mkfs.ext4 -F …; }`. Formatear ante CUALQUIER fallo de montaje, incluida
la carrera normal entre `volume attach` y la aparición del dispositivo. Un hipo de segundos
destruía el volumen. Ahora sólo formatea si `blkid` dice que NO tiene filesystem; si lo tiene y
no montó, aborta y lo dice.

Las dos comparten la misma forma: un fallback destructivo que asume la causa benigna. `mv -n`
está bien elegido (no pisa), pero el `rm -rf` que lo sigue anulaba esa prudencia.

Y una consecuencia que hay que asumir: re-snapshotear un worker NO conserva el store — los
artefactos viven en el volumen, que no entra en la imagen. La golden aporta toolchain y sistema;
la caché la aporta el volumen. Hoy la imagen decía traer store y no lo trae.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 08:11:57 -04:00
sergioandClaude Opus 5 889720cac0 granja: golden NUEVA con Rust y Go — desbloquea el 76% del catálogo
Imagen 417847948 («hammer-golden-rust-go-2026-08-08») reemplaza a 408909310 como default de
farm-up.

EL PROBLEMA QUE CIERRA: hammer invoca `cargo vendor` y el toolchain Go en el HOST, no dentro
del sandbox — el vendoreo pasa ANTES de entrar a la caja. La golden vieja no los traía, así que
la granja no podía construir NINGUNA receta Rust ni Go: COSMIC entero, las 228 Rust y las 362
Go del corpus. El 76% del catálogo dependía de UNA SOLA máquina, el hub — justo lo que el
respaldo de ayer intentaba dejar de asumir.

VERIFICADO ANTES DE SNAPSHOTEAR, no después: `cosmic-bg` —que minutos antes moría con
`spawn cargo vendor: No such file`— sella en el worker. Un snapshot de una máquina a medio
arreglar es peor que ninguno.

MISMAS VERSIONES QUE EL HUB (cargo/rustc 1.96.0, go 1.26.4) a propósito: introducir un skew de
toolchain nuevo mientras se arregla otro es cómo se fabrican los fallos que nadie reproduce. Y
1.96 es además el techo MSRV del sandbox ya documentado.

NO RELAJA LA HERMETICIDAD: cargo/go son herramientas de FETCH, del mismo orden que `git` para
clonar. El build sigue ocurriendo dentro del sandbox con el árbol ya vendoreado. Es lo contrario
del caso «no engordar el rootfs del worker», que habla del rootfs DEL SANDBOX.

El PATH queda persistido en /etc/profile.d/hammer-toolchain.sh para que sobreviva a los logins
no interactivos de la granja. Y la imagen trae el store del worker al momento del snapshot (KDE
y GNOME reconstruidos hoy), así que los workers nuevos arrancan con más caché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:33:57 -04:00
sergioandClaude Opus 5 040d174bbe desplegar-strip: ordenar por IMPACTO EN EL GRAFO, no alfabéticamente
Medido sobre el corpus: **686 de 779 recetas son HOJAS** (cero dependientes transitivos) ⇒ el
88% se puede desplegar sin provocar UNA SOLA reconstrucción extra. Las caras son pocas y
conocidas: make 416 · pkgconf 360 · go 359 · binutils 343 · zlib 315 · python3 287 ·
samurai 272 · meson 257.

El orden alfabético anterior era activamente malo: la primera tanda de la etapa 4 tocó expat,
zstd y ncurses —tres bibliotecas base— y dejó 54 recetas sin artefacto vigente, incluida la
cadena wlroots/sway sellada la noche anterior. Sin este orden, cada tanda pequeña provoca una
cascada grande y la campaña avanza hacia atrás.

Hojas primero; las ~93 con dependientes, en una ola coordinada y con la granja arriba.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 10:49:50 -04:00
sergioandClaude Opus 5 b38db3f35d respaldo: .dmerge tumbó la subida 40 veces seguidas y el mensaje nunca lo dijo
El respaldo se detuvo en 79 G de 127 G con «40 intentos y sigue cayéndose». El código era 23
(«some files/attrs were not transferred»), que está en la lista de reintentables — así que el
bucle lo reintentó cuarenta veces contra la misma pared y se rindió.

LA CAUSA, al mirar los errores de verdad en vez del código de salida: todos eran rutas
`/home/hammer/store/.dmerge/...`. Son directorios TRANSITORIOS de fusión del store, que aparecen
y desaparecen mientras hammer sella. rsync los empieza a copiar, se esfuman a media
transferencia, y falla. `cosecha-cron.sh` ya los excluía; este script no, y ésa era toda la
diferencia.

⇒ Añadido `--exclude /.dmerge`.

Y una lección para el propio bucle de reintentos: **un error reintentable que se repite 40 veces
no es un corte de red, es algo estructural**. Cuarenta reintentos idénticos deberían haber
gritado «esto no se arregla esperando» en vez de agotarse en silencio. Queda anotado; el bucle
todavía no distingue «se cayó una vez» de «falla siempre igual».

De paso, `appstream` —la única de las cinco del split que se perdió al borrarse el worker— se
reconstruyó en el hub: 18 M, 0 secciones .debug_. Las cinco quedan consistentes (hash vigente
con artefacto presente): bison 3M · appstream 18M · zstd 2M · expat 1M · ncurses 2M.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 08:41:28 -04:00
sergioandClaude Opus 5 102aabd1ff SEGUNDA corrección: lo que medía el test era DERIVA, no no-determinismo — el corpus SÍ reproduce
El §1.bis dijo «sí hay no-determinismo» porque appstream y bison divergían. También estaba mal,
y por un fallo de diseño del propio test.

`verificar-repro.sh` compara el artefacto GUARDADO contra una reconstrucción de hoy. Pero el
guardado puede tener meses: se construyó con OTRO estado del lab. El test conflaba dos cosas:
  · NO-DETERMINISMO — mismas entradas, mismo lab, salidas distintas (rompe el invariante);
  · DERIVA — el artefacto viejo no es lo que el lab de hoy produce (el mundo se movió).

LA PRUEBA QUE LAS SEPARA es construir DOS VECES HOY, y se dio sin querer: al reejecutar el
verificador sobre recetas ya reconstruidas, TODAS pasaron a reproducir.
    anew  ✗ diverge → ✓ REPRODUCE
    gron  ✗ diverge → ✓ REPRODUCE
    age   ✗ diverge → ✓ REPRODUCE   (y en la 1ª ni instalaba los mismos ficheros: faltaba age-inspect)

⇒ EL CORPUS ES DETERMINISTA HOY. Lo que hay es deriva contra artefactos viejos.

QUÉ SIGNIFICA, sin adornos:
· El argumento de reproducibilidad para el split de debug SE CAE. El split se justifica por
  ESPACIO (~60%), que sigue siendo real y grande. Nada más.
· Pero la DERIVA es un problema por derecho propio y mayor: el store contiene artefactos que el
  lab de hoy no reproduciría, así que «nuestros artefactos son verificables por terceros» es
  falso para parte del corpus — quien reconstruya no obtendrá lo publicado. `age` es el caso
  feo: la reconstrucción ni siquiera instala los mismos ficheros.
· ⇒ La reconstrucción masiva SIGUE valiendo la pena, pero por otra razón: no para arreglar el
  determinismo sino para PONER EL STORE AL DÍA CON EL LAB y que la promesa sea cierta.

Y otro hallazgo del mismo experimento: en el hub las recetas Go SÍ reconstruyen (0 fallos). Lo
que fallaba en el worker era la RED para bajar los módulos, no las recetas. Eso cambia el
bloqueante de la etapa 4: no es «Go no reconstruye», es «el worker no tiene red para módulos».

LA LECCIÓN, que es la misma tres veces en este documento: un test hay que diseñarlo contra la
PREGUNTA, no contra lo que es fácil de comparar. Comparar con lo que hay en el store es cómodo;
comparar dos builds de hoy es lo que contesta. El script queda anotado con esto en la cabecera.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 08:34:15 -04:00
sergioandClaude Opus 5 b344c5803b etapa 4: primera tanda desplegada y verificada (3/3) + el desplegador por tandas
zstd 12M→2M (−83%) · ncurses 6M→2M (−67%) · expat 2M→1M (−50%). Cero ficheros vacíos y
**las tres REPRODUCEN**. La maquinaria de la etapa 4 queda validada de punta a punta:
activar → construir en la granja → verificar con why-differs.

LA DEP DE binutils VA EXPLÍCITA, y es la decisión de diseño de esta etapa. El paso de strip usa
`strip --strip-debug -D` de binutils. Se podría hacer que el lab lo materialice solo, sin tocar
las recetas — pero entonces la VERSIÓN de binutils sería un input INVISIBLE: dos corridas con
binutils distintos darían artefactos distintos con el mismo hash. Los `deps` sí entran en
`hash_inputs`, así que declararlo es lo único que mantiene el invariante. Cuesta una edición
mecánica por receta; el invariante no se negocia por comodidad.

EXCLUSIONES, y son exactamente dos: `binutils` y `make`. binutils provee el strip y depende de
make ⇒ activarles el split los haría necesitarse a sí mismos para construirse. No es preferencia,
es la circularidad. (Las recetas CERRADAS POR DECISIÓN tampoco se tocan.)

POR TANDAS Y NO DE GOLPE: activar re-hashea la receta a propósito y en cascada todo lo que
dependa de ella. Hacerlo sobre las 775 candidatas a la vez dejaría el corpus entero sin sellar
al mismo tiempo — días de granja antes de poder verificar NADA, y el disco aguantando artefactos
viejos y nuevos a la vez. Por tandas se mide, se verifica y se poda entre medias, que es lo que
hace la campaña reversible.

La primera tanda NO se tomó del orden alfabético que propone el script: ésas son CLIs Go/Rust
que ya vimos que no reconstruyen en el worker (necesitan red para sus módulos), y validar la
maquinaria con recetas que fallan por otro motivo no habría probado nada. Se eligieron tres
paquetes C con artefacto presente. Para las tandas grandes hay que resolver antes el acceso a
red de los módulos Go/Rust, o restringirse a lo que reconstruye.

Estado: 775 candidatas, 5 desplegadas (bison y appstream del piloto + estas 3).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 00:53:26 -04:00
sergioandClaude Opus 5 1eb469337d etapa 3: el ahorro real es ~60%, no 79% — la cifra publicada medía otra cosa
El «79%» que este plan y los SDD 19/20 venían citando era **el 79% del CONTENIDO BINARIO**,
medido sobre una muestra de .so/.a. Es cierto y es la cifra equivocada para planificar, porque un
artefacto no es sólo binarios: trae cabeceras, datos, iconos, locales, .pc y documentación, que
no encogen.

MEDIDO con `scripts/medir-debug.sh` (suma los tamaños reales de las secciones .debug_* vía
readelf -S y los compara con el tamaño del árbol, sin reconstruir nada):
    40 artefactos  ·   876 MB · 27%
   100 artefactos  ·  2955 MB · 38%   (truncada a 60 ficheros/artefacto)
   250 artefactos  ·  8874 MB · 60%   ← la que manda: sin truncar, ~7% del store

La varianza es alta porque unos pocos artefactos grandes dominan el total, así que el número
necesitaba validación independiente — y la tiene: el piloto de la etapa 2, donde se reconstruyó y
se pesó de verdad, dio bison −50% y appstream −68%. Los dos ENCIERRAN el 60% de la muestra
grande. El modelo predice lo que el rebuild produjo.

CIFRAS CORREGIDAS, con el store en 127 G:
                      se venía diciendo    medido
   reserva de disco         ~96 G          ~76 G
   store tras el split      ~30 G          ~51 G
   espejo público           ~30 G          ~51 G
Sigue siendo el mayor ahorro disponible en el proyecto —76 G no es poco— pero no es lo
prometido, y la diferencia importa para dimensionar el alojamiento del espejo, que es una
decisión con factura.

LA LECCIÓN: una cifra medida sobre una cosa (contenido binario) se citó durante semanas como si
midiera otra (tamaño de artefacto), y llegó a tres documentos. El número no era falso; LA UNIDAD
sí. Cuando un número vaya a decidir un gasto, hay que volver a la medición original y comprobar
QUÉ estaba midiendo.

Corregido en los tres sitios donde se había propagado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 00:42:08 -04:00
sergioandClaude Opus 5 eea1c02f43 etapa 1: la puerta funcionó — me equivoqué, SÍ hay no-determinismo (y las dos mitades son una)
Ayer escribí en el SDD 23 que el `-ffile-prefix-map` global «se cancela porque su premisa es
falsa». **Era falso**, y la etapa 1 lo demostró en veinte minutos. Corregido en el §1.bis.

LA MEDIDA: `scripts/verificar-repro.sh` aparta el artefacto, reconstruye con las deps cacheadas
y compara con `why-differs`. Sobre las que pudieron reconstruirse: binutils ✓ y wl-clipboard ✓
reproducen; **appstream ✗ y bison ✗ DIVERGEN**. Causa idéntica en ambas:
    difieren [.debug_aranges, .debug_info, .debug_pubnames, .debug_pubtypes, .debug_str]
     — sólo info de depuración (el código ejecutable es idéntico)
     · .debug_str sólo en B: /src/output/meson-private

DÓNDE ME EQUIVOQUÉ, exactamente: mi barrido buscaba rutas DEL HOST (/home/<user>/,
/tmp/<aleatorio>) y no halló ninguna nuestra — cierto. Pero el no-determinismo no venía de una
ruta del host sino de rutas INTERNAS al árbol de build (/src/output/meson-private) que varían
entre corridas aunque /src sea constante. Buscar la forma equivocada de ruta y no encontrarla no
prueba que no haya otra. Y una sola muestra que reproduce no es una muestra: leí `wl-clipboard`
como si probara más de lo que probaba, que es el error contra el que el propio documento
advertía dos párrafos antes.

Lo que me salvó fue haber puesto la PUERTA antes de la campaña en vez de después. Si hubiera
seguido mi conclusión, habría cerrado como «resuelto» un invariante roto.

🎁 Y de ahí sale la mejor noticia del plan: LAS DOS MITADES SON EL MISMO TRABAJO. La divergencia
vive ENTERA en secciones .debug_* y el código ejecutable es idéntico ⇒ separar el debug del
artefacto principal hace que el artefacto principal REPRODUZCA, sin tocar -ffile-prefix-map en
720 recetas. Queda decidir qué hacer con el contenido del paquete -debug, pero eso afecta a un
artefacto secundario que nadie instala por defecto.

El script queda como herramienta: muestra por clase de build (C, Rust, Go: el no-determinismo
suele vivir en codegen paralelo y orden de símbolos, no sólo en C), y RESTAURA el artefacto si
el rebuild falla — distinguir «no reproduce» de «no construye acá» importa, porque mezclarlos
inventaría un problema de determinismo que no existe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 23:53:21 -04:00
sergioandClaude Opus 5 d70098ade5 GTK3: decisión del usuario — NO entra; gnome-session, gsd y gdm quedan CERRADAS por escrito
Decidido el 2026-08-08. Las tres recetas dependen de GTK3 (gnome-settings-daemon además de
gtk+-x11-3.0) y quedan cerradas, **no «en deuda»**. La diferencia no es semántica: una deuda
invita a reintentarla en cada informe y en cada ciclo de granja; una decisión se respeta.

POR QUÉ ES RAZONABLE Y NO UNA RENDICIÓN:
· GNOME funciona sin esto — el camino vivo es mutter → gnome-shell arrancado por arje, y
  ninguno de los dos depende de gnome-session (su cierre es mutter+gjs+gobject-introspection+
  gnome-desktop).
· Traer GTK3 sería una torre de C muerta —y para waybar además gtkmm, sus bindings de C++—
  por un gestor de sesión y un login manager que este escritorio no necesita.
· La función está cubierta: la sesión la levanta arje y la barra de estado es `yambar`, C puro,
  sellado anoche en el frente wlr.
Se reabre si alguien trae GTK3 por otro motivo con peso propio (una app gráfica que lo exija).

EL MARCADOR VIVE EN LA RECETA, NO EN UNA LISTA APARTE. `build-farm.sh` salta las recetas cuya
cabecera dice «CERRADA POR DECISIÓN». Podría haber hecho un fichero de exclusiones, pero una
lista y una cabecera se desincronizan solas: así, quien lee el porqué y quien decide saltarla
miran EL MISMO TEXTO. Y el drenador lo dice en su salida, para que la decisión sea visible en
vez de silenciosa.

Con esto la granja deja de quemar CPU en tres recetas que nadie va a arreglar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 23:31:08 -04:00
sergioandClaude Opus 5 e3403ff20e sway ARRANCA EN QEMU con pantalla — evidencia, y seis muros que el grafo no podía ver
`docs/evidencia/sway-qemu-2026-08-08.png`: 1280×800, **102 colores distintos**, sway con foot a
pantalla completa y un prompt vivo, la barra de título del compositor arriba y el fondo
#1a4b8c asomando en los bordes. Sobre el kernel de hammer con arje-zero como PID1, sin X11 y
sin systemd.

EL VEREDICTO ES CONTAR COLORES, NO LEER EL LOG — y esta campaña lo demuestra sola: hubo un
arranque con TODO verde en el log (salida activada, modo 1280x800, «Commit of 1 outputs
succeeded», workspace creado) y la captura salía **100% negra**. Un compositor puede estar
perfectamente vivo y no pintar nada.

LOS SEIS MUROS, ninguno visible en un grafo de dependencias que decía 121/121:

1. `PATH` vacío — el console-getty no lo exporta y ni `mkdir` se encontraba. «mkdir: not found»
   se lee como «falta busybox» cuando busybox está entero: mismo engaño que el exit 127 de meson.
2. `libz.so.1` AUSENTE del rootfs. El zlib del corpus es sólo estático; la compartida vive en
   `zlib-shared`, en otra cola. Se cazó comparando los NEEDED del ELF contra el rootfs, no
   leyendo logs.
3. `LIBSEAT_BACKEND=builtin` NO EXISTE en nuestro libseat: `recipes/seatd.toml` construye con
   `-Dlibseat-seatd=enabled -Dlibseat-logind=disabled`, o sea UN backend, confirmado con
   `strings`. La receta manda sobre lo que uno cree recordar del proyecto — y la misma receta
   traía la salida: `-Dserver=enabled` construye `seatd-launch`.
4. `/run` de SÓLO LECTURA. La raíz de hammer es inmutable por diseño, y sway moría con «unable
   to open lockfile … check permissions», que suena a permisos de directorio y era un
   filesystem read-only. Se resolvió montando un tmpfs, que es lo que hace cualquier init.
5. `xkeyboard-config` — «failed to add default include path /usr/share/X11/xkb». Es EXACTAMENTE
   lo que dejé anotado en la receta de swaylock: «para que el teclado tenga distribución en una
   imagen real hará falta incluirlo por el lado del perfil». Apareció donde se dijo.
6. SIN TIPOGRAFÍAS no hay terminal. `fcft: failed to match font` → `failed to load primary
   fonts`: foot arrancaba y no dibujaba. `dejavu-fonts` estaba en el catálogo pero en otra cola.
   Va en la lista de §I.5 del SDD 20 («tipografías») y acá se ve por qué no es un detalle.

Y un fallo de MÉTODO que costó dos ciclos: mandé el log de sway a /var/log/sway.log DENTRO de
la VM, así que la primera captura negra vino sin una sola línea que la explicara — el
diagnóstico estaba dentro de la caja que no arranca. Un log que no se puede leer desde fuera no
es un log. Ahora va al serial.

Los `exec` de la config de sway disparan ANTES de que exista el workspace (0,259 s vs 0,305 s)
y mueren con «Failed to create launch context. No workspace». Los clientes se lanzan desde
sway-start esperando el socket de Wayland, que es la condición real.

Andamiaje reutilizable en scripts/wlr/: qemu-sway-image.sh (hermano del de COSMIC, sin logind
ni dbus, que sway no necesita), sway-start.sh y sway-config.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 22:38:51 -04:00
sergioandClaude Opus 5 55dd36d97a perfil escritorio-sway: 121/121 CERRADO — la cuarta imagen de escritorio, y la más barata
Declarado en targets.toml y verificado con el grafo: **121 nodos, 121 sellados, falta 0**. Es
una imagen construible hoy, no una intención.

Nueve raíces (sway, yambar, fuzzel, swaybg, swaylock, swayidle, grim, slurp, wl-clipboard) más
`hereda = ["cli"]`, porque un WM sin userland debajo no se usa: hacen falta la terminal y las
herramientas. `foot` no se lista porque ya entra por la clausura de `cli`. El resto de la
imagen —wlroots, mesa, wayland, libinput, seatd, cairo, pango, fcft…— lo calcula el grafo
siguiendo las aristas: escribir la clausura a mano es justo lo que targets.toml existe para
evitar.

LA COMPARACIÓN QUE VALE: KDE y GNOME fueron campañas de semanas porque debajo tienen una torre
de C (Qt entero, GTK, la cascada de mesa). Esto es una decena de binarios pequeños sobre
wlroots y salió en una noche. Ya estaba escrito en el SDD 20 como hipótesis —«los WMs ligeros
son la mejor relación esfuerzo/resultado que queda»—; ahora está medido.

`--wlr` en build-state.py, con su flag propio por la misma razón que COSMIC: sin él el frente
es INVISIBLE para el khipu, y `yupana radio` sobre una receta compartida (wayland, libinput,
pixman, mesa, libxkbcommon…) no reportaría la imagen sway entre las afectadas — o sea que el
próximo que toque una de ésas mediría de menos y creería que no rompe nada.

Queda lo que no puede decidir el grafo: probarlo en QEMU CON PANTALLA. La regla ya pagada cara
es que las imágenes de escritorio no se validan con `-nographic`, porque el compositor puede
«arrancar» en los logs y no pintar nada.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 22:01:24 -04:00
sergioandClaude Opus 5 39b88ebd2a churn del store: eran CUATRO rutas de cosecha, no una — arreglé una y di el bucle por cerrado
Podé el store por tercera vez hoy: 363 artefactos superados, 24 G. Comparado con la poda de la
tarde: **362 en común**. Volvieron otra vez, pese al arreglo de esta tarde.

LA CAUSA de que el arreglo no sirviera: puse el `--exclude-from` del registro de podados sólo en
`farm-sync.sh`, y el latido NO usa ese script. `cosecha-cron.sh` tiene su PROPIO rsync del store
(línea 71), y corre cada 30 minutos por cron ⇒ una poda de 24 G quedaba deshecha en menos de una
hora. Los tres commits de «cosecha granja» de la madrugada son exactamente eso.

Y al ir a arreglarlo, en vez de parchear y seguir, busqué si había más: **son CUATRO** las rutas
que bajan el store del worker — `cosecha-cron.sh`, `farm-sync.sh`, `harvest-go.sh` y
`farm-down.sh`. Tenía protegida una de cuatro.

El error de método vale más que el bug: arreglar una de varias copias y declarar victoria es
PEOR que no arreglar, porque el síntoma se atenúa lo justo para dejar de mirar. Lo que lo
delató fue comparar los manifiestos de dos podas (`comm -12`), no leer código. Dos podas con el
mismo número no son coincidencia — ya está anotado como regla, y hoy la regla pagó dos veces.

Las cuatro llevan ahora el mismo `--exclude-from` y una nota que dice que son cuatro, para que
la quinta —si aparece— nazca protegida.

Disco: 112 G → 136 G tras la poda de ahora.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 21:52:46 -04:00
sergioandClaude Opus 5 59c4973bd9 granja: el filtro de «ya sellado» miraba el nombre equivocado — lo arreglo antes de que cueste
El bloque que añadí hace un rato usaba `basename $f .toml` como nombre del artefacto. **Está
mal**: el artefacto en el store se llama por el campo `name` de la RECETA, no por el fichero,
y en 17 recetas del catálogo esos dos no coinciden. El arquetipo es
`recipes/incoming-kde/qtbase.toml`, cuyo `name` es `qt6-qtbase` ⇒ el artefacto es
`<hash>-qt6-qtbase` y mi `grep` lo buscaba como `<hash>-qtbase`.

Efecto: las 17 salían «no selladas» y el worker las habría reconstruido — exactamente el
desperdicio que este filtro existe para evitar. Se vio porque los 10 «pendientes» de
incoming-kde eran todos módulos Qt, que es demasiada coincidencia para ser casualidad; el
hub tenía el artefacto con el MISMO hash, sólo que bajo otro nombre.

Con el filtro correcto, la foto de la granja es exacta por primera vez:
  · incoming-kde    205 recetas · 205 en el hub ⇒ **0 pendientes**
  · incoming-gnome   90 recetas ·  87 en el hub ⇒ 3 pendientes: gdm, gnome-session y
                                    gnome-settings-daemon, o sea el trío bloqueado por GTK3
  · incoming-go / incoming-clib: colas vacías

⇒ **La granja no tiene trabajo construible.** Y ya no lo tenía antes de esto: el worker no
posee ni UN artefacto que el hub no tenga (comparadas las dos listas: 0 exclusivos suyos),
así que sus últimas 22 horas produjeron cero valor neto. Lo que queda por hacer no es
construir sino AUTORAR recetas (wlroots y los WM ligeros, cups, bluez, las gráficas de
terceros), y eso es trabajo de hub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:32:31 -04:00
sergioandClaude Opus 5 129aff9dd9 granja: el worker rehacía 87 de 90 recetas por ciclo — el sync del store es de UN SOLO SENTIDO
Fui a mirar los fallos de la granja y el número no cuadraba: de 20 «fallos» de incoming-gnome,
17 correspondían a recetas CON ARTEFACTO VIGENTE en el hub. Primero pensé que eran marcadores
`.fail` rancios, y me equivoqué: `build-farm.sh` hace `rm -rf "$FARM"` al empezar cada ciclo,
así que el status es siempre del ciclo actual. Después pensé que el worker corría recetas
viejas, y también me equivoqué: comparados los md5, hub y worker tienen copias IDÉNTICAS.

LA CAUSA REAL está en `farm-sync.sh`: sube el código EXCLUYENDO /store y baja el store del
worker. El store viaja worker→hub y nunca hub→worker. O sea que **el worker no se entera de
nada de lo que se sella en el hub**, y cada ciclo reintenta —y vuelve a fallar— trabajo ya
hecho. Medido: de las 90 recetas de incoming-gnome, **87 ya están selladas en el hub**. El
worker estaba quemando el 97% de esa cola en repetir lo hecho.

EL ARREGLO no es mandarle los artefactos (gigabytes por un enlace de 8 Mbps) sino la LISTA:
`work/farm-sellados.txt` son los nombres `<hash>-<paquete>` del store del hub, unos kilobytes.
`farm-sync.sh` la regenera en cada sync (para que no envejezca sola) y `build-farm.sh` salta
la receta cuyo hash vigente ya esté ahí. Ojo al detalle de rsync: la subida excluye /work, así
que el manifiesto necesita un `--include` ANTES del `--exclude` — rsync aplica la primera regla
que casa, y sin ese orden el fichero no viajaba y el arreglo no habría hecho nada en silencio.

Y LAS TRES DEUDAS REALES DE LA GRANJA SON UNA. gnome-session, gnome-settings-daemon y gdm
mueren todas en GTK3, que el frente GNOME aparcó a propósito. Verificado contra el meson.build
de cada tag: gnome-session 48.0 lo pide incondicional; gnome-settings-daemon 48.1 pide gtk+-3.0
Y gtk+-x11-3.0 (dos de las tres deudas aparcadas, no una); gdm 48.0 lo tiene condicionado a
`if have_xdmcp` pero da igual, porque muere construyendo gnome-session.

De paso, gnome-session pasaba `-Dsystemd=false -Dsystemd_journal=false`, dos opciones que NO
EXISTEN en 48.0 (sus opciones reales son seis: deprecation_flags, session_selector,
systemduserunitdir, docbook, man, x11). Meson aborta en la primera opción desconocida, así que
la receta ni llegaba a configurar y el fallo real quedaba tapado. Corregido, más -Dx11=false.
El diagnóstico de fondo YA ESTABA en la receta desde el 2026-07-27 y era más completo que el
mío (dice GTK3 **y** libsystemd); quité la nota duplicada que había añadido.

Y en el respaldo: la comprobación inicial de SSH era de un solo intento y tiró la corrida al
relanzarlo llegando a casa, con el wifi aún sin levantar. Es el peor momento para rendirse —
quien relanza un respaldo interrumpido acaba de cambiar de red. Ahora reintenta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:27:29 -04:00
sergioandClaude Opus 5 2c2ee951d6 licencias: resolver -only vs -or-later CON LA CITA — y tres «pruebas» que no probaban nada
Los identificadores obsoletos (`GPL-2.0`, `LGPL-2.1`, `AGPL-3.0`) no dicen si el proyecto
concede «sólo esta versión» o «ésta o cualquier posterior», y eso decide con qué se puede
combinar el paquete y bajo qué términos puede redistribuirlo quien lo reciba.

NO SE PUEDE MIRAR EL COPYING, que es la trampa evidente: el texto de la GPL es IDÉNTICO en
los dos casos —es la licencia, no la concesión— y encima su apéndice «cómo aplicar la
licencia» contiene literalmente «or (at your option) any later version», así que buscarla
ahí da SIEMPRE positivo y parece evidencia siendo plantilla. Misma trampa que la regla del
`.a` no-PIC, donde grep contaba reubicaciones de `.debug_*`. La concesión vive en las
cabeceras de los fuentes y en el README; ahí se busca, excluyendo los ficheros de licencia.

SE GUARDA LA CITA, no sólo el veredicto: fichero + frase exacta que decidió. Una licencia es
una afirmación legal y quien la revise tiene que poder ver POR QUÉ dice lo que dice sin
repetir el trabajo.

Y eso pagó de inmediato: de las 12 primeras resoluciones, TRES eran evidencia inválida y
sólo se vieron porque estaba la cita. Cada una de una clase distinta:
 · caligula   — la frase salía de `checks/headless/expected.iso`, un FIXTURE DE TEST;
 · libnl      — de `include/linux-private/linux/seg6.h`, una cabecera del KERNEL vendorizada.
                La licencia del kernel no es la de libnl;
 · lm-sensors — declarado GPL-2.0 y la cita decía «version 2.1 of the License», que es la
                LGPL. La frase era real pero no sostenía lo que se le atribuía.

Las tres clases quedan filtradas en el script: ficheros de licencia, rutas de test/fixture, y
código vendorizado; más una comprobación nueva de que **la cita hable de la misma versión que
la licencia declarada**.

Sembradas las 9 auditadas (13 ficheros; algunas viven en varias colas). Ambiguas: 55 → 41.
Las 40 sin evidencia quedan listadas y siguen contadas — la mayoría son Go y la familia
cosmic, donde la frase no aparece fuera del COPYING.

Hashes verificados: 0 movidos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:15:20 -04:00
sergioandClaude Opus 5 6263d24788 store-gc + farm-sync: cortar el BUCLE DE CHURN de 24 G que hacía inútil la poda
Podé el store por la mañana: 362 artefactos superados, 24 G. Lo volví a podar por la tarde:
362 artefactos, 24 G. Comparados los dos manifiestos, **solapamiento del 100%: son los
MISMOS 362**. No era casualidad ni recuento nuevo — es un bucle.

LA CAUSA no estaba en el gc sino en `farm/farm-sync.sh`, que bajaba el store del worker
ENTERO sin filtro. El worker nunca se poda, así que conservaba los superados y nos los
devolvía en cada cosecha. Podar → cosechar → vuelven → podar. El disco pagaba 24 G por
vuelta y el gc informaba éxito cada vez, lo que es peor que fallar: parecía que se avanzaba.

Es la misma familia de fallo que el gc que reportaba borrados falsos, sólo que un nivel más
arriba: entonces el borrado no ocurría, ahora ocurre y se deshace solo. En los dos casos el
mensaje de éxito era cierto y engañoso a la vez.

EL ARREGLO es darle memoria al sistema. `store-gc.sh` acumula lo podado en
`work/store-gc-superados.txt` y `farm-sync.sh` lo usa como `--exclude-from` al bajar el
store del worker. Es seguro porque el store es CAS: los nombres `<hash>-<paquete>` son
inmutables y un artefacto superado no vuelve a ser vigente salvo que una receta retroceda —
y si retrocediera, se reconstruye, que es barato.

Disco: 159 G → 182 G tras la poda de hoy, y esta vez debería quedarse.

De paso, en el respaldo: rsync 24 («ficheros del origen desaparecieron») NO es un fallo, es
justo lo que pasa al podar mientras se sube. El bucle de reintentos lo tomaba por error real
y habría parado el respaldo entero por algo inofensivo — y encima cuando el disco aprieta,
que es cuando menos conviene quedarse sin copia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:11:31 -04:00
sergioandClaude Opus 5 89c02db639 respaldo: rsync 24 (ficheros desaparecidos) no es un fallo — es podar mientras se sube
El bucle de reintentos trataba el 24 como «error que no es de red» y habría PARADO el
respaldo entero. Pero 24 significa que ficheros del origen desaparecieron durante la
transferencia, que en este proyecto es una combinación NORMAL: el respaldo dura ~19 h y
`store-gc.sh` es la válvula del disco, así que podar mientras se sube va a pasar. Y pasaría
justo cuando el disco aprieta, o sea cuando menos conviene quedarse sin copia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:06:26 -04:00
sergioandClaude Opus 5 be4e91086c licencias: el texto dentro de la imagen, con veto — y el SDD 19/20 se equivocaba de sitio
═══ LA CORRECCIÓN DE FONDO: no es `pack`, son las IMÁGENES ═══
El SDD 19 y el 20 decían «inyectar el texto de la licencia en `hammer pack`, que es aguas
abajo del ArtifactHash y sale gratis». La conclusión sobre el coste era correcta pero el
SITIO estaba mal, y por una razón que sólo se ve leyendo el código: `pack` produce un
`.swm`, que es una RECETA DE TRANSFORMACIÓN sobre fuente pública y por diseño explícito
«NUNCA transporta binarios cocidos»; e `install` REPRODUCE construyendo, con cache-hit del
corpus, en vez de bajar binarios. O sea que **el canal de paquetes de hammer no distribuye
binarios** y la obligación de acompañar-el-binario ahí casi no aplica.

Donde sí aplica, con toda su fuerza, es en la IMAGEN INSTALABLE: su rootfs se puebla con
`hammer install` sobre un prefix y de ahí sale un USB/ISO lleno de binarios que se le da a
alguien. Ése es el punto de entrega. (La obligación del espejo de FUENTES es otra y sigue
entera: las recetas apuntan a URLs de terceros que se caen.)

═══ LO QUE ENTRA ═══
· `scripts/licencias-textos.sh` + `licenses/`: los 44 textos CANÓNICOS de SPDX
  (spdx/license-list-data), uno por identificador realmente en uso — la lista se saca de las
  recetas, no de un fichero escrito a mano que se desincroniza. 764K.
· `scripts/licencias-rootfs.sh`: escribe /usr/share/licenses/<pkg>/ con el texto de CADA
  licencia de la expresión (`Unlicense OR MIT` ⇒ los DOS textos: en un OR el destinatario
  elige, y entregar uno solo le quita una opción que el autor le concedió), más un
  MANIFEST.tsv. Medido: 1,0 MB para el perfil `cli` de 44 paquetes.

═══ Y SOBRE TODO: ES UN VETO ═══
Sale ≠0 si algún paquete del rootfs no declara licencia. Un informe que no puede bloquear no
cambia lo que ocurre; esto corta el paso. Y se ganó el sueldo al primer intento: reveló que
los perfiles `base` y `cli` —los dos más simples, los que primero se publicarían— llevaban
10 y 12 paquetes con binarios y licencia desconocida. Curados los 10 que se podían afirmar
(doas, iputils, less, mandoc, procps-ng, rsync, strace, sudo, tree, usbutils); ambos perfiles
bajan a 2.

Esos 2 NO se rellenan a propósito, y quedan vetando:
 · lsof   — licencia propia del proyecto, sin identificador SPDX. Toca `LicenseRef-lsof` con
            su texto, que ya viaja en el tarball (la receta instala su COPYING).
 · tzdata — dominio público por declaración de sus autores, y SPDX no tiene identificador
            para eso (es una AUSENCIA de licencia, no una licencia). Toca
            `LicenseRef-PublicDomain` documentado, no forzar uno de la lista.

De paso: normalizado `GPL-3.0+` (sufijo antiguo) a `GPL-3.0-or-later`, misma equivalencia
documentada que la barra de Cargo. Y el caso especial que puse para `Linux-syscall-note`
(«las excepciones viven en exceptions/») era una suposición razonable y FALSA: SPDX las
publica en el mismo `text/`; ese directorio no existe.

1063 de 1141 (93%). Hashes verificados: 0 movidos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:53:20 -04:00
sergioandClaude Opus 5 487e155f74 licencias: leer el LICENSE de verdad cuando GitHub dice «no sé» — 1053 de 1141 (92%)
Último recurso mecánico, `scripts/licencias-texto.sh`: para las 60 recetas donde la API de
GitHub devuelve NOASSERTION (el repo TIENE un LICENSE pero licensee no lo reconoce: texto
retocado, encabezado propio, dos licencias en un fichero), baja el texto y lo clasifica acá.
34 identificadas — 16 MIT, 10 Apache-2.0, 7 BSD-2-Clause, 1 BSD-3-Clause.

SÓLO PERMISIVAS, Y NO ES PEREZA. MIT, Apache-2.0, BSD, ISC, MPL-2.0 y Unlicense se
reconocen por frases inconfundibles y NO tienen variantes -only/-or-later ⇒ reconocer el
texto da el SPDX completo.

La familia GPL se deja al humano A PROPÓSITO, y la razón es sutil: el COPYING de la GPL es
IDÉNTICO tanto si el proyecto es «sólo v3» como «v3 o posterior» — lo que las distingue vive
en las cabeceras de los fuentes. Y encima el propio COPYING incluye, en su apéndice «cómo
aplicar la licencia», la frase «or (at your option) any later version», así que buscarla da
SIEMPRE positivo y parecería evidencia siendo texto de plantilla. Es exactamente la trampa
de la regla del `.a` no-PIC, donde grep contaba reubicaciones de .debug_* y mentía.

Comprobado a mano un caso que daba mala espina: el LICENSE de `conftest` empieza
«Conftest — Write tests against your config files / Copyright (C) 2019 …», que es el formato
típico de una cabecera GPL. Leído entero, dice «Licensed under the Apache License, Version
2.0». La clasificación era correcta; la sospecha, barata.

Hashes verificados sobre las 36 tocadas: idénticos.

Quedan 88, ya sin vía mecánica: los repos donde ni la API ni el texto deciden, la familia
GPL sin desambiguar, y tarballs de sitios propios (gmplib, xiph, sr.ht, codeberg…).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:06:59 -04:00
sergioandClaude Opus 5 c8d1357f06 licencias: la declaración del autor manda — y arreglo 5 recetas que ROMPÍ y commiteé
Dos cosas: una mejora de calidad y un fallo mío que hay que contar entero.

═══ EL FALLO: escribí un error JSON dentro de 5 recetas y las commiteé rotas ═══
En `a6a9d59`, cinco recetas quedaron con
    license = "{"message":"Not Found","documentation_url":"...","status":"404"}"
Causa: cuando el repo da 404 (renombrado, borrado, privado), `gh api` escribe el CUERPO DEL
ERROR en stdout y el filtro `--jq` falla, así que la variable queda valiendo el JSON. Mi
`case` sólo descartaba ""/null/NOASSERTION/other ⇒ el JSON pasó como si fuera una licencia.
Las comillas ROMPEN el TOML, y esas 5 recetas dejaron de parsear y de tener ArtifactHash.

No lo vio nadie leyendo: lo cazó la comparación de hashes, al detectar que `cargo-sort`
había cambiado. Afectadas: cargo-sort, cargo-audit, cargo-binstall, waybackurls, usbutils.

La lección no es «se me pasó un caso», es que **validé los «no sé» conocidos en vez de la
FORMA del dato**. Ahora se acepta sólo lo que tiene forma de expresión SPDX, y la barrera
está en los DOS sitios que escriben (detectar y sembrar), más un GUARDIÁN en el informe que
sale ≠0 si alguna licencia tiene forma imposible. Un fallo aguas arriba disfrazado de dato
hay que verlo sin buscarlo.

Y de paso normalizadas 11 licencias con la barra antigua de Cargo (`MIT/Apache-2.0`), que
no es SPDX válido. Traducir la barra a OR no es inferir: Cargo documentó esa equivalencia.

═══ LA MEJORA: `scripts/licencias-cargo.sh` ═══
Lee la licencia que el AUTOR declara en su Cargo.toml, pidiéndola AL TAG QUE LA RECETA
PINEA (`?ref=v<version>`, con HEAD como último recurso y anotando cuál se usó). 176
obtenidas, 167 al tag exacto. Es la fuente más autoritativa de las que usamos y corrige los
dos defectos de la detección por API de una vez:
 · las licencias DOBLES dejan de colapsar: `fd` pasa de `Apache-2.0` a `MIT OR Apache-2.0`,
   `ripgrep` a `Unlicense OR MIT`, `bat` a `MIT OR Apache-2.0`;
 · los SPDX AMBIGUOS caen de 71 a 55, porque el autor sí escribe -only/-or-later.
66 corregidas, 7 nuevas. Y aparecieron cosas que la detección había perdido: `eza` es
EUPL-1.2, una copyleft europea que se habría empaquetado creyendo otra cosa.

Jerarquía de evidencia, escrita en la cabecera del script: nombre (prohibido) < familia/URL
de fuente < API de licencias de GitHub < Cargo.toml del autor.

997 de 1141 (87%). Verificación COMPLETA sobre las 77 recetas tocadas —no una muestra—:
0 hashes cambiados, y 5 que ahora parsean y en HEAD no.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:51:09 -04:00
sergioandClaude Opus 5 5e42d201b1 respaldo: reintentar los cortes de red — el primero murió a los 2,3 G
La primera corrida real subió el cerebro (estado + repo, lo irreemplazable) y 2,3 G de
store, y entonces murió: `Connection reset by peer` / `Broken pipe`, rsync 255. Con ~19 h
de subida sobre un enlace de oficina, que la conexión se corte NO es un accidente: es lo
normal. Y un respaldo que hay que relanzar a mano cada vez que se cae no se completa nunca,
porque nadie está mirando.

Como rsync ya es incremental y `--partial-dir` conserva los trozos a medio subir,
reintentar es barato y seguro: cada intento retoma donde quedó, no reempieza. El bucle
insiste sólo ante fallos de RED (10/12/23/30/35/255) con espera creciente hasta 60 s; un
error de verdad —permisos, destino lleno, ruta inexistente— sale a la primera en vez de
repetirse 40 veces contra la misma pared.

DE PASO, UN ERROR MÍO QUE VALE REGISTRAR: comprobé el respaldo con `pgrep -f
respaldo-storagebox` y dijo que corría, cuando llevaba rato muerto — el pgrep se estaba
matcheando A SÍ MISMO, porque la cadena buscada está en la propia línea de comando del
shell que la ejecuta. Ya me había pasado hoy con el worker. La comprobación buena es mirar
lo que el proceso PRODUCE (bytes en el destino, última línea del log), no si hay un proceso
vivo. Es la misma lección que el guardián de store-gc: un `rm` que devuelve 0 no prueba que
el fichero se fue, y un proceso vivo no prueba que esté avanzando.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:30:58 -04:00
sergioandClaude Opus 5 a6a9d59e4f licencias: 936 de 1141 (82%) — detección por evidencia, con sus dos imprecisiones declaradas
Esta mañana eran 0. Tras la tabla curada (228) y este paso, 936. Ninguna receta se
re-hasheó: verificado en 30 de las 708 tocadas, 30 hashes idénticos, 0 cambiados.

DE DÓNDE SALE EL DATO, Y POR QUÉ NO ES ADIVINAR. `scripts/licencias-detectar.sh` consulta
la API /repos/{o}/{r}/license de GitHub para las 673 recetas cuya fuente vive allí. Eso
devuelve el resultado de DETECTAR el fichero LICENSE que el repo tiene de verdad
(licensee), no una etiqueta escrita a mano en una web: es la misma evidencia que veríamos
abriendo el tarball, obtenida sin bajar 673 tarballs por un enlace de 8 Mbps. 610 con SPDX
definido; las 63 que GitHub marca NOASSERTION/other se DESCARTAN — un «no sé» de la fuente
se propaga como hueco, no se redondea a una licencia plausible.

Las familias no-GitHub van curadas por la URL DE FUENTE, que es la evidencia que el nombre
no da. Y ahí cometí el error simétrico al que este mismo fichero advertía: había puesto
`knighttime` entre las «herramientas Go» POR SU NOMBRE, y su tarball sale de
download.kde.org — es un Framework de KDE, LGPL. Descartar por nombre falla igual que
aceptar por nombre. Corregido en la cabecera.

LAS DOS IMPRECISIONES, contables con `licencias.sh --revisar` en vez de escondidas:

 1. SPDX OBSOLETOS Y AMBIGUOS (71 recetas). GitHub devuelve `GPL-3.0`, `LGPL-2.1`,
    `AGPL-3.0`… identificadores que SPDX declaró obsoletos PRECISAMENTE porque no
    distinguen `-only` de `-or-later`, y esa diferencia decide con qué se puede combinar el
    paquete. NO se normalizan a ciegas: mapear GPL-3.0 → GPL-3.0-or-later sería inventar el
    dato que falta. Quedan marcadas para resolver mirando el fuente.

 2. LICENCIAS DOBLES COLAPSADAS. La API devuelve UNA sola licencia y muchos proyectos Rust
    son «MIT OR Apache-2.0» (p.ej. `fd` quedó como Apache-2.0). No es falso —cumplir una de
    las opciones concedidas basta— pero es incompleto. Lo resuelve el cierre estructural:
    leer el campo `license` del Cargo.toml en la fase de fetch.

Y un fallo que habría escrito basura en silencio: `licencias-detectadas.tsv` tiene TRES
columnas (añade el owner/repo consultado, para poder auditar) y el sembrador leía dos, así
que la licencia se habría llevado pegado el slug — `license = "MIT<TAB>owner/repo"`, sin
que nada lo validara. Arreglado antes de sembrar.

Quedan 205 sin licencia y 71 por desambiguar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:26:22 -04:00
sergioandClaude Opus 5 346cd59706 licencias: campo license en la receta — de 0 a 228 de 1141, sin re-hashear nada
La deuda legal bloqueante del SDD 19 §2.1. Medido hoy: **0 de 1141 recetas** declaraban
licencia, no «5 de 771» como decía el informe anterior. Los dos números estaban mal: los
«5» eran falsos positivos de `grep license` (el paquete `addlicense`, el paquete
`cargo-bundle-licenses`, una línea `install .../share/licenses/` y un comentario), y las
recetas son 1141. Contar con `grep -l <palabra>` sobre TOML cuenta comentarios y nombres,
no campos; `scripts/licencias.sh` cuenta el campo de verdad (clave en la raíz, antes del
primer `[table]`).

LO QUE HACE LA DEUDA PAGABLE: `Recipe::hash_inputs` es una LISTA BLANCA — sólo entran
source, compiler, target, link, patches, flags, phases y deps. `license` no entra, igual
que `evidence` y `slots`. Por eso se puede poblar en las recetas YA SELLADAS sin mover un
solo ArtifactHash. Verificado, no supuesto: en 40 recetas modificadas se comparó el hash
con y sin la línea — 40 idénticos, 0 cambiados. Si el campo entrara al hash, declarar la
licencia costaría reconstruir el corpus entero y no se haría nunca. Clavado con el test
`licencia_round_trip_y_no_afecta_el_hash`.

TRAMPA DE TOML: una clave suelta después de un `[table]` pertenece a esa tabla. Puesta al
final del fichero, `license` acaba dentro de `[deps]` y se pierde EN SILENCIO, porque serde
ignora los campos que no conoce — no hay error, simplemente no está. Va arriba, junto a
`name` y `version`; el sembrador la inserta tras `version`.

NO SE ADIVINA. Declarar mal una licencia es peor que dejarla vacía: convierte un hueco
visible en una afirmación falsa. Sólo se puebla desde una tabla curada entrada por entrada
(`docs/licencias-conocidas.tsv`); lo que no tiene evidencia queda vacío y se CUENTA.
Concretamente se descartó el atajo «k* = KDE ⇒ LGPL»: en este catálogo `kail`, `kind`,
`ko`, `kopia`, `krew`, `kustomize`, `kyverno`, `katana`, `kibi`, `kmon` y toda la familia
`kube*` son herramientas Go sin relación con KDE. El nombre no es evidencia.

Quedan 913, casi todas CLIs Go/Rust importados en masa — y ésas sí son automatizables con
evidencia real: Cargo.toml trae el campo `license` y los módulos Go traen su LICENSE en el
árbol. El cierre estructural es capturarlo en la fase de fetch, que ya descarga y extrae
cada tarball, y inyectar el texto en `hammer pack` (aguas abajo del ArtifactHash) en vez de
en la fase install (que sí re-hashearía).

De paso, respaldo-storagebox.sh reordenado por valor irreemplazable y con zstd: medido en
la oficina, el uplink da 8 Mbps iguales por cable y por wifi ⇒ 128 G no caben en una
sentada, así que sube primero el cerebro (estado + repo) y `--partial-dir` hace que cortar
a mitad de un artefacto no tire lo ya subido.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 10:24:11 -04:00
sergioandClaude Opus 5 60d8ca9bbf respaldo: Storage Box BX11 en Hetzner — no dependemos más de un laptop
El usuario pidió respaldar en «el volumen, aunque sea montándolo aquí». No se
puede: un HC Volume es un dispositivo de bloque por RED, sólo se adjunta a
servidores de Hetzner del mismo DC, y sólo a uno a la vez. harkaq-cosecha es el
disco del worker efímero y vvv está al 78%.

El producto correcto es un Storage Box: se monta desde el laptop (SSHFS/CIFS) y
habla rsync/borg/restic por SSH. BX11 = 1 TiB, €3,20/mes, sin alta. Creado en
hel1 con protección de borrado y la clave github5.

rsync plano y no borg: el store es CAS, los ficheros son inmutables y se nombran
por hash, así que incremental es exactamente lo correcto y no hay repo ni claves
que mantener. borg comprimiría 4x (79% del store es .debug_) pero 126 G en 1 TiB
no aprieta.

El store va SIN --delete a propósito: un respaldo que replica los borrados no
protege del borrado por error, y store-gc.sh acaba de demostrar que puede
equivocarse en silencio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 08:10:30 -04:00
sergioandClaude Opus 5 514ae44135 store-gc: guardián — no reportar «borrado» sin comprobarlo
El script informó «364 artefactos borrados · libres: 24G → 48G» y no había
borrado ninguno: los 364 de su propio manifiesto seguían enteros en el store.
`xargs rm -rf` salió con 0 y el echo se lo creyó. Se reprodujo: corriéndolo en
SEGUNDO PLANO el borrado no se materializa; en primer plano sí.

Da igual la causa: un rm que devuelve 0 no es prueba de que el fichero se fue.
La válvula de escape del disco estaba rota EN SILENCIO y encima reportaba
éxito, que es peor que fallar — el disco llegó al 98% mientras yo creía haber
liberado 24G.

Ahora recuenta sobre el filesystem y sale distinto de cero si sobrevivió algo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:44:54 -04:00
sergioandClaude Opus 5 14bed923ad granja: el worker no molía GNOME — faltaban dos colas en QUEUES
El default era fósil de cuando `incoming-gnome-onda1` era el frente entero:
listaba esa cola (6 recetas) pero NO `incoming-gnome` (86, donde viven gtk4,
mutter y gnome-shell) ni `incoming-gnome-onda2` (la isla dinámica, 22). El
worker reportaba moler GNOME y molía 6 de 114.

Se agregan las dos y se mueven las tres ANTES de incoming-kde: con 205 recetas
KDE por delante, un ciclo se consumía sin darles turno aunque estuvieran
listadas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 19:51:41 -04:00
sergioandClaude Opus 5 213ebfbb03 📶 metal: el enlace WiFi se caía solo y cortaba la depuración remota a mitad
Depurando la OptiPlex por SSH sobre un hotspot, la máquina desaparecía de la red cada
pocos minutos. No es «la WiFi anda mal»: son dos causas que se suman y ninguna deja
rastro en dmesg.

  - `udhcpc -q` (el que usa wifi-up) pide la dirección UNA vez y termina. Nadie renueva
    el lease.
  - el AP olvida a los clientes ociosos y deja de contestar ARP. Desde fuera se ve «No
    route to host» mientras la máquina se cree perfectamente conectada.

El síntoma engaña justo en la dirección peor: la máquina no reporta nada, así que parece
que se colgó lo que estabas depurando.

`wifi-keep` hace ping al gateway cada 10s, que resuelve las dos a la vez — detecta la
caída para reasociar Y mantiene viva la entrada del cliente en la tabla del AP.

Sin esto cada sesión remota se interrumpe sola y hay que volver físicamente al teclado,
que es exactamente lo que la red venía a evitar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 11:33:30 -04:00