El keystone del frente. spidermonkey.toml es PRIMER BORRADOR — mozjs no sella a la
primera sobre musl (mach/configure no lo auto-detecta el lab ⇒ fases a mano, iterar
en worker ccx33+). Anclado al APKBUILD musl de Alpine: source del árbol firefox
128.14.0esr (sha real), clang, icu in-tree, nspr+zlib del sistema, shared-js.
Leaves que faltaban (ninguno estaba en el rootfs ni tenía receta):
nspr 4.36 (autotools), nasm 2.16.03 (autotools), cbindgen 0.27.0 (Cargo).
Las 4 parsean (hash dry-run OK) y pueblan el grafo: yupana radio nspr/cbindgen ya
ven a spidermonkey como dependiente. Capa grafo del yupana-gnome VIVA y reckonando.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build-state.py gana --gnome (análogo a --kde): carga recipes/incoming-gnome/ y sale
a build-state-gnome.json. drenar/yupana/seed-graph aprenden ese grafo (keystones,
drenar --perfil escritorio-gnome, radio de nodos incoming-gnome, frontera). Hoy la
cola está vacía ⇒ el grafo trae 0 nodos gnome; se poblará al aterrizar recetas y ahí
drenar/keystones reckonan solos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. targets.toml: [perfil.escritorio-gnome] (9 raíces de sesión, cola incoming-gnome)
⇒ `yupana objetivo` ya lo ve; fluye por targets.py. build-state lo SALTA hasta
que la cola se cargue (--gnome), así declararlo no reporta raíces fantasma.
2. seed-gnome.py OPTIMIZADO: hornea la triage (sustitución/tooling/opcional/espinazo)
como HIPÓTESIS con el caveat "el cierre de nixpkgs sobreestima ~5-10×", marca el
keystone (spidermonkey→gjs→gnome-shell), y deja de mentir con el 465 crudo. El
espinazo (353) se reporta como COTA ALTA, no como lista de build.
Falta (se activa al autorar): grafo build-state-gnome + flag --gnome ⇒ keystones/
drenar nativos sobre GNOME. Hoy la capa OBJETIVO del yupana-gnome está; la GRAFO no.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
seed-gnome.py corre el cierre transitivo de las raíces GNOME sobre metadata de
nixpkgs y clasifica conocida/frontera/nix-ismo/lab. Hallazgo: el cierre de nixpkgs
SOBREESTIMA ~5-10× la frontera de hammer (arrastra códecs ffmpeg, plugins gstreamer,
samba, openjdk, sphinx docs — todo opcional). El número real sólo emerge autorando
top-down feature-minimal.
Sólido: 155 deps ya tienen receta; palos largos = spidermonkey(mozjs)+gjs;
gobject-introspection es build-tooling. Cola aislada recipes/incoming-gnome/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El loop del worker está SIEMPRE vivo (idle-loopea con la cola vacía). Contarlo
en hay_trabajo() reseteaba los ticks a 0 cada 10 min ⇒ el worker NUNCA acumulaba
idle ⇒ NUNCA se auto-mataba. Quedó 2h17m idle quemando € (2ª vez).
El loop construyendo YA se detecta por su hijo `hammer build`; el loop vacío NO
es trabajo. Se afina el match a `release/hammer.* build ` y se suma `campana-deuda`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cazado con el 1er worker real: el token vive en /etc/hammer-deadman.env (lo carga
systemd como EnvironmentFile), pero corrido A MANO (farm-up --puede-borrar) ese
fichero no se lee solo ⇒ mi cadena de fallback no lo veía ⇒ decía 'sin token' y
farm-up habría DESTRUIDO un worker que sí puede matarse. Fix: sourcear el
EnvironmentFile antes de la cadena de tokens desnudos. Verificado en worker real:
'SÍ: puedo borrar mi id=154276696'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El worker DEBE matarse solo, sin depender de la laptop (es la razón de ser del
dead-man: vive en el worker; el volumen hace que morir no pierda nada). El reaper
del hub queda sólo como último recurso. El dead-man estaba TRIPLEMENTE roto:
1. TOKEN EN EL FICHERO EQUIVOCADO: hay 2 caminos de creación con el token en
lugares distintos (farm-up → /etc/hammer-deadman.env; harkaq-vol → /root/
.hcloud-token). La service lee sólo el primero ⇒ un worker del otro camino
quedaba sin token, disparaba pero salía 1 "sin HCLOUD_TOKEN". Fix: deadman.sh
busca el token EN CADENA (volumen primero, que es lo más persistente).
2. REGEX DEL ID ROTO: la API devuelve JSON con espacio ("id": 126, no "id":126)
y el dead-man usaba '"id":[0-9]+' ⇒ NUNCA resolvía su id, aun con token
válido. Fix: '"id":[[:space:]]*[0-9]+'. (Doblemente roto: por eso NUNCA murió.)
3. FIRING ≠ CAN-DELETE: farm-up sólo verificaba que el timer dispara. Ahora
corre `deadman.sh --puede-borrar` (token en cadena + ve su id en la API) y si
NO puede matarse, DESTRUYE el worker ahí mismo. Un worker que no se autodestruye
es inadmisible ⇒ jamás debe existir.
gioser DOBLE-BLINDADO en TODOS los sitios de borrado (lista negra por nombre +
label role=hammer-worker), igual que farm-down ya tenía: reaper, farm-up-destroy,
deadman, y el helper de harkaq-vol. "Ahí está nuestra vida." Verificado vivo (104d).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sin esto un server borrado se quedaba en .fleet para siempre (describe falla ⇒
'sin label' ⇒ nunca se dropeaba). Ahora si no existe en hcloud, se saca.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
INCIDENTE (2026-07-23): un worker quedó 3.5h idle quemando € mientras el usuario
dormía. El dead-man del worker estaba "activo" (timer disparando cada 10 min)
pero FALLABA con exit 1 en cada tick: llegaba a matarse pero su
/etc/hammer-deadman.env no tenía token válido ⇒ salía 1 "sin HCLOUD_TOKEN" y
nunca se borraba. Verificaron que el timer DISPARA, no que puede BORRAR —
firing ≠ can-delete.
Causa de fondo: la garantía dependía de que CADA worker se auto-provisione bien
un token, y eso falla EN SILENCIO. Un invariante que depende de una provisión
frágil no es un invariante.
FIX en dos capas (defensa en profundidad):
1. REAPER HUB-SIDE (cosecha-cron.sh): el hub tiene un token que funciona y el
latido corre cada 30 min aunque no haya sesión (setsid). Un worker sin trabajo
activo REAPER_MAX=2 ciclos (~1h) se BORRA desde el hub, con el MISMO blindaje
de label role=hammer-worker (gioser jamás pasa). Cubre "dead-man del worker
roto". El dead-man del worker sigue cubriendo "hub caído".
2. farm-up verifica CAN-DELETE, no sólo firing: que el token del worker vea su
propio id en la API (lo mismo que hace al morir). Si no puede, LO GRITA al
provisionar en vez de descubrirlo 3.5h tarde.
Acción inmediata tomada: worker borrado a mano (label verificado), €0 baseline
restaurado — sólo queda gioser (protegido, fijo). Todo estaba cosechado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La campaña de 40 nodos KDE selló 40/40, 0 fallidas: kio (desbloqueado por 6a0dc40)
arrastró la cascada y completó el escritorio. De 85/162 al abrir el catálogo a
162/162. 41 tiempos reales cosechados para el camino crítico pesado.
El grafo modela deps (R1: Receta×Receta). El entorno es OTRA capa: R2 (Receta×
Capacidad, qué exige un build) y R3 (Host×Capacidad, qué ofrece la máquina).
Construible = clausura(R1) ∧ R2⊆R3. Esos huecos viven FUERA del grafo (infra del
sandbox, toolchain, layout de fs) y una matemática de grafos sola no los ve —
pero SÍ se aprenden, como patrones con predictor, calibrados contra fallos reales.
`yupana preflight <receta>` corre los patrones ANTES de construir. Primer patrón:
overlay-lowerdir, la cicatriz de kio. Habría marcado kio antes de quemar la
campaña descubriéndolo a los golpes:
kio: 52 deps ⇒ lowerdir ~5346B > 4096B (una página) ⇒ depende del merge
CALIBRADO, no adivinado: el estimador daba corto (2974B "ok") hasta usar el
prefijo que bwrap VE en runtime (/oldroot/…, ruta del worker, no la del hub).
Con eso: kio estimado 5346B vs real 5315B — 31B de error, sin fudge. El primer
predictor "obvio" mentía con confianza; la lección de siempre.
preflight --perfil escritorio-kde destapa lo sistémico: las 40 recetas KDE
profundas CUELGAN del merge (plasma-desktop: 114 deps → 11708B, casi 3× el
límite). Todo el escritorio dependía del fix 6a0dc40. El pre-flight vuelve
VISIBLE y contable una fragilidad que antes sólo se manifestaba fallando.
Registro extensible (PATRONES_ENTORNO): futuros = capacidad-de-host (rootfs
laptop≠worker, techo MSRV — acumular observaciones), skew de toolchain (zig-skew
— fingerprint por artefacto). harkaq ya MIDE R2 (evidencia negativa); esto lo
vuelve consultable + pre-vuela. Guardián: kio marcado + estimador calibrado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
merge_deps_layer funde las deps en UNA capa overlay por hardlinks (cp -al) para
no desbordar el lowerdir de overlayfs (tope ~4096B por página). Pero calculaba
la ruta del merge en deps.parent().parent() = <store>/../.dmerge, asumiendo que
el store está en <base>/store del MISMO fs. En el worker el store es un VOLUMEN
bind-montado (/opt/hammer/store en /dev/sdb, /opt/hammer en /dev/sda1) ⇒ el merge
caía en la raíz, cp -al fallaba cross-device, y el fallback apilaba las ~50 deps
de kio directo → lowerdir 5315B > 4096B → "bwrap: Can't make overlay mount" →
kio (EL keystone, gatea 36) imposible de construir. Por eso estaba clavado.
Fix de una línea: el merge va en el padre INMEDIATO de las deps (= el store
mismo), garantizado mismo fs. Confirmado en el worker: cp -al a /opt/hammer/.dmerge
falla "Invalid cross-device link"; a /opt/hammer/store/.dmerge funciona.
En el laptop andaba por casualidad (store y su padre en el mismo fs).
`.dmerge` (dot-prefix) es invisible para el store lookup/build-state como .times.
cosecha excluye /.dmerge del rsync (es scratch de hardlinks; sin -H rsync lo
expandiría a copias reales).
Descubierto levantando el worker para poblar tiempos: yupana keystones apuntó a
kio, la campaña falló ahí, y el radio de lowerdirs (5315B) destapó la causa. Una
"sorpresa de entorno" (infra del sandbox, fuera del grafo) que el frente de
timing sacó a la luz.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El peso que le faltaba a las ondas: sin él, critical-path = nº de pasos; con él,
ETA real en segundos.
INSTRUMENTACIÓN (worker):
scripts/farm/build-timed.sh envuelve `hammer build` midiendo la pared y la
registra en $STORE/.times/<hash>-<host>.json — un SIDECAR del store, NUNCA
dentro del artefacto. La duración es no determinista (varía por máquina/carga)
⇒ no puede tocar el ArtifactHash. Keyed por hash (CAS-safe: dos workers no se
pisan) + host (varias muestras por receta). Viaja al hub con el rsync de store
que ya hace la cosecha; ninguna herramienta del store lo confunde con artefacto.
Sólo builds REALES (pared ≥ UMBRAL 3s) — los cache-hits no envenenan la mediana.
campana-deuda.sh ahora construye vía build-timed.sh (transparente, mismo exit).
CONSUMO (hub): yupana._tiempos() carga name→mediana de segundos; keystones
computa el CAMINO CRÍTICO pesado = longest weighted path del subgrafo de deuda
(peso = segundos de build). La cadena más larga hay que construirla en SERIE
aunque haya ∞ workers ⇒ es la ETA con paralelismo infinito. Degrada con gracia:
sin datos, peso=1 y el camino crítico = nº de pasos (lo que ya daban las ondas).
Verificado: con muestras sintéticas da "ETA 19 min sobre 5 pasos, kio → kparts →
frameworkintegration → breeze → plasma-integration"; sin datos degrada a "5 pasos
(SIN datos)". store/.times está git-ignorado (metadata de máquina, no se commitea).
LÍMITE honesto (en la cabecera de build-timed.sh): `hammer build` arrastra deps ⇒
la pared incluye deps no selladas. En orden topológico (drenar) las deps ya están
selladas y la pared mide sobre todo ESTA receta — cota superior buena para pesar.
La precisión exacta pediría instrumentar el sellado dentro de hammer-build (Rust).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El usuario pidió arreglar itstool y la sombra de dbus. Apliqué el reflejo
(`yupana radio` + diff completo antes de tocar) y NINGUNO era un bug: ambos eran
patrones deliberados y documentados que mi detector, demasiado superficial, leyó
mal. Si los "arreglaba" a ciegas rompía dos builds.
· dbus: corpus/dbus (estático) e incoming-kde/dbus (dinámico) NO son redundantes
— qtbase linkea libdbus-1.so dinámicamente para Qt6DBus. El detector comparó
sólo versión+sha+deps, no el BUILD. Borrar la sombra rompía KDE.
· itstool: es un STUB documentado (`[source]=carrier`). El itstool real es
Python con libxml2-bindings ausentes en el lab; este genera un script inline y
sólo PRESTA el tarball de gettext-tiny. Apuntarlo al itstool real rompía
appstream (radio 5) con un source que ni compila acá.
Los 3 *-hello son el mismo patrón carrier; prison/prison-scanner una variante
cross-nombre (misma fuente, WITH_ZXING distinto).
FIX = el detector, no las recetas. Tres señales que le faltaban:
1. huella de BUILD en la firma de sombra (estático≠dinámico ⇒ no redundante).
2. CARRIER: ≤1 receta del grupo construye la fuente ⇒ el resto presta el tarball.
Señal: ¿invoca make/meson/cmake/ninja/cargo? (comentarios strippeados — la
prosa "invocación de meson" de un stub daba falso builder, lo cazó el guardián).
3. variante cross-nombre: ≥2 builders con BUILD distinto = deliberado, no bug.
Resultado: 0 colisiones reales (invariante c se cumple), 4 carrier + 14 variantes
+ 12 sombras justificadas, todas benignas.
Guardián extendido: ancla que el detector agrupa por FUENTE (itstool↔gettext-tiny)
y NO cría lobos (itstool=carrier, dbus=variante deliberada).
LECCIÓN: el reflejo del radio/diff antes de tocar evitó dos borrados destructivos
guiados por un falso positivo de mi propia herramienta. Medir antes de creer, aun
a la propia yupana.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`yupana keystones` rankea los nudos EN DEUDA por cuánto trabajo BLOQUEADO libera
su sellado (cierre inverso transitivo restringido a la deuda). Sellá de arriba
hacia abajo y la cascada cae lo antes posible.
CORRIGE UN ERROR MÍO: dije "árbol de dominadores = keystones". Lo medí y es
FALSO. El dominador ingenuo asume alcanzabilidad OR (basta un camino), pero
construir es AND (hacen falta TODAS las deps): kcoreaddons depende de dbus/libdrm
/mesa además de qtbase ⇒ hay un camino de desbloqueo que evita qtbase y el
dominador-OR lo pierde. La métrica correcta bajo AND es el cierre inverso
transitivo. Y el crudo lo gana el toolchain (make gatea 313, inútil) ⇒ el
keystone accionable es el cierre restringido a la deuda, y sólo cuenta si el
nudo mismo está en deuda (un sellado ya está disponible, no gatea nada).
Estado actual (qtbase ya sellado, era EL keystone con 117 gateados):
kio 36 ← sellarlo libera 36 recetas KDE bloqueadas
kparts 19
kcmutils 17
ksvg 14
libplasma 13
Ése es el camino crítico ahora.
El dominador SÍ entra, en su uso legítimo: la columna `exclusivo` = retención
estilo GC (nodos que sólo dependen de mí). kwin=9 (sus plugins privados),
libksysguard=3. Responde "si suelto esta feature, cuánto más puedo soltar".
Sin datos nuevos (no necesita duración de build). Cuando instrumentemos tiempos,
el mismo cierre se vuelve camino crítico PESADO (ETA real).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`yupana duplicados` caza nudos que son el MISMO paquete bajo nombres/colas
distintos, agrupando por lo que BAJA (sha/url), no por el nombre. Clasifica:
colisión-fuente — mismo sha, familias de nombre DISTINTAS ⇒ casi seguro un
sha copiado por error. ACCIONABLE.
variante — mismo sha, misma familia (zlib/zlib-shared, mesa-*) ⇒
diversificación deliberada estático/shared, OK (14 vistas).
sombra — mismo nombre en >1 cola; redundante si versión+sha+deps
coinciden (una sobra), justificada si difieren.
HALLAZGO INMEDIATO — el detector encontró un bug real en su primera corrida:
itstool.toml dice versión 2.0.7 pero su url+sha apuntan al tarball de
gettext-tiny (29cc165e…). Construiría la fuente equivocada. radio itstool=1,
gettext-tiny=78 ⇒ el sha copiado es el de itstool.
Otras 3 colisiones son demos (adwaita-hello/libadwaita, sourceview-hello/
gtksourceview, radio 0) o subcomponentes (prison/prison-scanner) — a revisar,
no urgentes. Y 1 sombra redundante: incoming-kde/dbus es byte-idéntica a
corpus/dbus (mismo hash de artefacto) ⇒ los 114 consumidores KDE podrían
apuntar a corpus/dbus sin rebuild.
NO funde nada solo: reporta a docs/state/duplicados.json y deja el juicio al
humano (como el triaje). El radio de cada nudo dice cuál es el canónico (el de
mayor radio). Guardián extendido: test-yupana-radio.py falla si el detector
deja de ver colisiones de fuente (agrupar por nombre en vez de sha = invariante
c ciego).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`khipu` colisionaba con una app de tawasuyu. En vez de un nombre a dedo, el
significado más profundo RESUELVE la colisión: en los Andes el khipu GUARDABA
(el registro de nudos) y la yupana CALCULABA sobre él (el ábaco). Acá igual, y
la distinción es arquitectura:
docs/state/build-state.json = el KHIPU (el registro firme: nudos y cuerdas)
scripts/yupana.py = la YUPANA (el motor que reckona: radio, ondas, clausura)
Nunca fue del todo un khipu lo que construimos; era la yupana. La colisión
empujó al nombre más exacto.
git mv preserva historia. Verificado tras el rename: `yupana radio libdrm` da
126/132/[kde,mirada]; el guardián de regresión pasa; build-state importa yupana
y --check sigue en 0. Cero referencias `khipu` colgadas en código.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test-khipu-radio.py falla si el radio de un paquete compartido entre colas
vuelve a medirse recortado: verifica que libdrm lo consuman >1 cola y que su
membresía incluya escritorio-kde (la mentira original decía sólo mirada).
Regla que blinda: cuando khipu se equivoca, no sólo se arregla — queda el test
que impide repetirlo. Es la automejora arquitectónica hecha práctica.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El 2026-07-22 un cambio a libdrm (default_library=both, para gtk4) invalidó 118
recetas KDE. El radio se midió contra recipes/*.toml y no contra
recipes/incoming-kde/ ⇒ 8 en vez de 118. Y build-state.py sin --kde cometía el
MISMO error: cargaba sólo el corpus, y su campo `perfiles` AFIRMABA que tocar
libdrm sólo afectaba a escritorio-mirada. Una mentira con confianza.
Causa estructural: las dependencias INVERSAS son un hecho del disco entero, no
de la vista que uno cargó. khipu.py las calcula SIEMPRE sobre todas las colas,
con resolución sibling-first (el nodo se identifica por (cola, nombre), porque
corpus/fontconfig y incoming-kde/fontconfig son nudos distintos).
QUÉ NODOS PUNTÚO es decisión de vista. QUIÉN ME CONSUME es un hecho.
khipu es la PUERTA ÚNICA de la metodología: radio/perfiles nativos (cruzan todo
el repo) + delega estado/drenar/triaje/frontera/objetivo a sus órganos.
khipu radio libdrm → 126 directos, 132 transitivos, [escritorio-kde,
escritorio-mirada], 11 sellados que caen. El número
honesto que habría frenado el cambio.
build-state.py ahora saca `perfiles` y el nuevo `dependientes_total` de khipu
(grafo entero), no de los nodos que cargó. Verificado: el grafo por defecto ya
atribuye libdrm a escritorio-kde; sin_perfil bajó 674→666 (8 recetas del corpus
que sólo KDE consume, ahora bien atribuidas). --check sigue en 0, grafo cierra.
Repo-wide por diseño, no KDE-céntrico: zlib toca las 4 imágenes (233 transitivos),
make 313.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Evidencia dura primero: NINGUNO de los 170 bloquea un build. Toda receta que
pide uno de estos ya está sellada al menos una vez ⇒ hammer construye sin
ninguno. Eso descarta empíricamente "hueco de build" para los 170 y deja sólo
la pregunta de runtime.
VEREDICTOS
provisto 11 — no faltan: ya los da otra receta con otro nombre. nixpkgs parte
en varios paquetes lo que acá es uno solo. Verificado contra el
store: wayland-scanner→wayland, mesa-libgbm→mesa (nuestros tres
mesa producen libgbm.so.1), libxcb-*→xcb-util-*, poppler-qt6→
poppler, gmp-with-cxx→gmp, uname→coreutils.
nix-ismo 5 — andamiaje de nixpkgs (env wrappers, helpers del stdenv, glibc
asomando por su libc).
opcional 150 — software real que nixpkgs habilita y hammer no necesita, con el
porqué agrupado: systemd (usamos arje-zero), X11 heredado (el
escritorio es Wayland), Vulkan/shaders, audio/multimedia,
conectores de BD, tooling de docs/tests, bindings Python de Qt,
paquetería ajena (tenemos .swm).
hueco 4 — gaps de RUNTIME, no de build: shared-mime-info (sin base MIME
no hay tipos de fichero), xwayland (ninguna app X11 corre),
polkit-qt-1 (sin diálogos de autorización), qqc2-breeze-style
(los controles QML caen a un estilo genérico).
CORRIGE UN ERROR MÍO DE P3: dije que mesa-libgbm/libglvnd eran "el muro de
GBM/EGL". Falso — libgbm ya lo produce nuestro mesa. El muro era softpipe vs
llvmpipe, no un paquete ausente.
CUARTO VEREDICTO NUEVO (`provisto`) con su propio lazo: sale a alias-triaje.txt
y seed-graph.py lo carga en MAPA ⇒ esos 11 nombres dejan de contarse como hueco
para siempre. Junto con nixismos-triaje.txt, el sembrador aprende de su triaje.
Y LA CADENA SE EJERCE ENTERA POR PRIMERA VEZ: los 4 huecos entraron como raíces
de escritorio-kde → nacieron 4 nodos `wanted` (raíces 11, sin receta 4; el grafo
sigue cerrando, --check exit 0) → seed-graph los sembró (42 aristas conocidas,
21 candidatos nuevos de frontera) → drenar.py los ordena marcándolos [semilla],
que es la regla de la fuente única a la vista. qqc2-breeze-style no cae en la
onda 1 porque sus deps SEMBRADAS la traban (kcodecs, kirigami…): el andamio
funcionando como se diseñó.
De paso: qtbase se selló mientras corría esto ⇒ la onda 1 de KDE se abrió de 1 a
13 recetas. El cuello de botella que reportó P4 ya está destrabado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tandas/README.md declara los 55 ficheros ARCHIVO de campañas pasadas (no se
borran: su git log es la crónica de cada frente) y documenta de dónde sale el
trabajo ahora. import-batch.sh -f sigue vivo como vía de escape, no como camino.
Lo que faltaba para que la cola fuera derivada de verdad era el paso humano, y
estaba sin forma: 137 candidatos clasificables sólo en la cabeza de alguien.
triaje.py les da forma durable — docs/state/frontera-triaje.toml, 170 candidatos
unificados de los 3 perfiles. Cada veredicto cierra un lazo distinto:
hueco → raíz en targets.toml ⇒ nace un nodo `wanted`, el sembrador lo
siembra y el drenaje lo ordena (= trabajo nuevo para el worker)
opcional → registrado, deja de reaparecer como pregunta
nix-ismo → vuelve a seed-graph.py como descarte ⇒ el sembrador APRENDE de su
propio triaje y cada corrida sale más limpia
Sincronizar NUNCA pisa un juicio humano: agrega los nuevos y marca ausente=true
los que dejaron de aparecer (progreso, o cambio de nixpkgs — se ve en el diff).
`sugerencia` propone con evidencia mecánica pero `veredicto` arranca en
`pendiente`: la heurística abarata el juicio, no lo cierra.
Lazo verificado punta a punta: python3-3.14.6-env marcado nix-ismo → --aplicar
lo escribe al descarte → seed-graph.normalizar() lo devuelve clasificado y deja
de contarlo como hueco. Probado y revertido; los 170 siguen pendientes.
Cadena cerrada y corriendo: targets.toml → build-state.json → drenaje.json →
worker, regenerada por el latido cada 30min. El único paso que pide una persona
es el triaje.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`build-farm.sh` usa `xargs -P$JOBS`; con >1, dos recetas que comparten dep disparan la carrera del
árbol de fuentes del ADR 0012 y el árbol queda roto para siempre. Se vio a escala: al invalidar
libdrm, 118 de las 205 recetas KDE pasaron de cache-hit a rebuild real y ~93 murieron con
"/src/.zwrap/cc is not a full path to an existing compiler tool" — el wrapper de zig que la receta
deja en el ÁRBOL DE FUENTE, barrido por el fetch concurrente de otra receta sobre el mismo qtbase.
El reintento serial de build-farm tampoco las recupera: el árbol ya quedó inconsistente.
La caché estaba ocultando la carrera, no evitándola. Serializar es la única mitigación correcta
hasta que el ADR 0012 elija salida.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Las tres estaban en deuda por TRES causas distintas, encadenadas de modo que cada una tapaba a la
siguiente:
1. libinput moría con "no suitable Python interpreter found", un mensaje de AUTOTOOLS siendo libinput
meson puro: el que fallaba era su dep `libevdev` (arreglado en el commit anterior declarando
make/python3).
2. Destapado eso, el muro real de libinput era
"relocation R_X86_64_32S ... recompile with -fPIC" apuntando a `libmtdev.a`: libinput enlaza ese
archivo estático DENTRO de libinput.so, y libtool sólo compila los objetos estáticos con -fPIC si
se lo pedís. `mtdev` ahora configura con `--with-pic`.
3. fcft y foot fallaban con sha256 mismatch: el `/archive/<tag>.tar.gz` de codeberg lo genera la
forge AL VUELO, así que sus bytes dependen de la versión de git/gzip del servidor. El pin se
murió solo cuando codeberg actualizó su tooling — no es corrupción: hoy baja de forma estable,
pero con OTRO hash (esperado c0d8d485…, real b0c0f4a5…). Re-pinear arreglaría hoy y volvería a
romperse; pasan a fuente GIT por commit, que es inmune (ADR 0006).
OJO: los dos tags son ANOTADOS ⇒ el sha que devuelve `git ls-remote <tag>` es el objeto tag, no
el commit. Van los `^{}`.
Verificado: las seis (libinput, fcft, foot, mtdev, libevdev, utf8proc) SELLADAS, y el binario de
foot es ELF dinámico con NEEDED = sólo libc.so, sin fugas del rootfs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
drenar.py parte la deuda en ONDAS topológicas (una receta sólo espera por deps
que también estén en deuda; las selladas ya están en el store). Onda 1 =
construible ya; dentro de cada onda, orden por `unblocks` desc.
Da lo que una lista plana no da: el orden correcto, la PROFUNDIDAD real de la
cadena (pasos secuenciales = el reloj de pared) y qué paraleliza sin pisarse.
base 0 en deuda imagen completa
cli 0 en deuda imagen completa
escritorio-kde 77 en deuda 12 ondas onda 1 = 1 receta: qtbase
escritorio-mirada 4 en deuda 2 ondas onda 1 = 3
HALLAZGO: el escritorio KDE está serializado detrás de UNA receta. qtbase
destraba 117 nodos y es lo único de la onda 1 — hasta que no esté sellada no hay
nada que paralelizar, por más workers que se enciendan. Cambia la pregunta de
"¿cuántas faltan?" (77, poco informativo) a "¿cuál es el camino crítico?".
Dos correcciones salidas de mirar la salida:
· el grafo se elige por la `cola` declarada en targets.toml, NO por cuál tiene
más nodos: incoming-kde SOMBREA recetas canónicas, así que medir
escritorio-mirada contra el grafo KDE medía una imagen que nadie construye
(3 en deuda en vez de 4). El mismo atajo estaba en seed-graph.py --frontera;
corregido en los dos.
· regla de la fuente única implementada en deps_de(): manda la receta si existe,
la semilla sólo si el nodo es `wanted`, marcando procedencia en la salida.
Enchufado al latido: cosecha-cron regenera docs/state/drenaje.json junto al
grafo y lo commitea ⇒ cola derivada y versionada, el git diff entre ciclos
muestra qué salió de la deuda. NO lanza builds: eso cuesta € y sigue siendo
decisión explícita (farm-up + campana-deuda.sh).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sin una `configure` explícita hammer auto-detecta CMake (utf8proc trae CMakeLists.txt además del
Makefile) y genera `cmake -S . -B _build …`. Sellaba en el laptop —cuyo rootfs Alpine trae cmake— y
moría en el worker con `cmake: not found`, porque su rootfs NO lo trae: los dos rootfs no son
iguales (291 binarios vs 279). El build real es el Makefile; la salida no es declarar cmake sino no
pedirlo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
seed-graph.py extrae deps de la metadata de nixpkgs sin construir nada (leerla
no viola el ADR 0004, que prohíbe *construir* con nix). `--calibrar` mide la
semilla contra la única verdad disponible: las recetas escritas a mano.
VEREDICTO (60 recetas, 686 deps):
directas recall 39%*, precisión 59% (269 de 455)
transitivas recall 84%, precisión 6% (390 de 6760)
* contra la verdad APLANADA — comparación injusta, y el modo transitivo
prueba que la información sí está; sólo falta aplanarla.
Decisión: sembrar aristas DIRECTAS, la transitividad la hace el grafo. hammer
aplana la clausura en [deps].build (cada dep = capa --overlay-src), nixpkgs
declara directas y propaga; reproducir el aplanado arrastra 4629 nodos fantasma
del bootstrap de nixpkgs.
Lo que enseñó la calibración (3 rondas, cada fix salido de un dato):
· 4 clases de discrepancia, no 1: convención hammer (make/binutils/linux-headers,
implícitas en el stdenv de nix), variante nuestra (zlib vs zlib-shared),
provisto por el lab (cargo/rustc — las recetas Rust declaran deps=[]), y recién
después ruido real. Contarlas juntas hacía parecer irreducible lo clasificable.
· desalineación aburrida y arreglable: caja (libx11/libX11), implementación
(gettext→gettext-tiny, ninja→samurai), versión pegada al pname, hooks del
stdenv. Precisión 45%→59%; "sin attr en nixpkgs" 22/40 → 6/60.
· el escritorio KDE cuelga de kdePackages., no del top-level: sin ese fallback
el perfil objetivo era insembrable.
Hallazgo de secuencia: hay 0 nodos `wanted` (targets.toml se pobló por
lift-and-shift de lo existente), así que el sembrador no tenía a quién sembrar.
De ahí `--frontera <perfil>`: siembra las recetas que YA existen y reporta lo que
nixpkgs pide y hammer no puede construir. 137 candidatos en escritorio-kde
(kdoctools ×6, milou, polkit-qt-1, libkscreen, y mesa-libgbm/libglvnd/spirv-tools
= el muro de GBM/EGL ya documentado), 51 en cli, 46 en escritorio-mirada.
Cada candidato es una HIPÓTESIS a clasificar a mano: dep opcional no habilitada,
nix-ismo por filtrar, o hueco real. Nada se promueve a receta automáticamente.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`libinput` fallaba con "no suitable Python interpreter found" — un mensaje de AUTOTOOLS, y libinput
es meson puro: el que moría no era libinput sino su dep `libevdev`.
Causa raíz: el rootfs Alpine del worker NO trae python3; el del laptop SÍ. `libevdev` declaraba
sólo `pkgconf` pero su configure de autotools pide un intérprete Python y su compile es `make`, o
sea vivía de prestado del rootfs. Sellaba en el laptop y moría en el worker, arrastrando a libinput.
`utf8proc` es el mismo caso al desnudo: NO declaraba NADA y sus dos fases son `make`.
Es exactamente la deuda declarable del SDD 16 §harkaq: el store ya provee ambos ⇒ una línea en
`[deps]`. Se declara en vez de engordar el rootfs del worker porque así construyen en CUALQUIER
máquina, que es el punto; parchear el worker dejaría la receta igual de mentirosa.
Radio de re-sellado medido: 2 recetas (libevdev, utf8proc). El resto de la cadena afectada
(libinput, fcft, foot, mirada-*) ya estaba sin sellar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El grafo cruza ahora el corpus con el manifiesto de objetivo:
· estado `wanted` — una raíz declarada sin receta se materializa como nodo de
frontera. Se crea ANTES de buscar huérfanas a propósito: un objetivo declarado
no es una arista colgando, así `--check` conserva su significado (verificado:
exit 0, grafo CIERRA, topo OK). `wanted` ≠ `unhashable`.
· campo `perfiles[]` por nodo — qué imágenes lo alcanzan desde sus raíces.
· bloque `by_profile` — el "cuánto falta", POR IMAGEN.
Primeros números, derivados de raíces y no de listas a mano:
base 51/51 listo
cli 74/74 listo
escritorio-mirada 27/31 (falta libinput, mesa-swrast, mirada-{compositor,greeter})
escritorio-kde 85/162 faltan 77 ← de 7 raíces, antes ilegible
674 recetas no las alcanza NINGUNA imagen (catálogo, no distro)
La clausura es COTA INFERIOR mientras haya nodos `wanted`: sus deps no se
conocen hasta construirlos (o sembrarlas, P3). Está documentado en el header.
`totals.recipes` sigue contando sólo recetas con fichero (los consumidores lo
leen como tamaño del corpus); `totals.nodes` incluye la frontera. La vista HTML
regenera sin cambios de contrato.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
git.tawasuyu.net resuelve a gioser, marcado como FIJO y SIN BACKUP. Sin espejo, las dos únicas
copias del repo eran el laptop —que ya se corrompió una vez por un corte sucio, el 2026-07-22— y
esa. Ahora hay una tercera, en otro proveedor y PRIVADA.
Se implementa con `git remote set-url --add --push origin` en vez de un remoto `github` aparte,
para que TODO `git push origin main` que ya existe en los scripts (cosecha-cron.sh, el latido)
espeje solo, sin tocar un script. Si GitHub falla, el push devuelve != 0 aunque gitea haya
aceptado; cosecha-cron.sh ya lo tolera ("push falló, reintenta próximo ciclo") ⇒ el latido no se
rompe por eso.
La credencial va por HTTPS con el token de `gh`, no por SSH: las tres claves SSH del laptop
(github5, key25, sergiogithub) son DEPLOY KEYS de repos ajenos y dan "Repository not found" contra
este. El bloque `Host github.com` del ~/.ssh/config además no tiene `IdentitiesOnly`, así que ssh
ofrece todas y gana una deploy key.
El script existe porque las dos cosas que configura viven en `.git/config`, que NO se versiona: se
perderían exactamente en el escenario para el que se pusieron (el laptop muere, clonás de nuevo).
Incluye también el `core.fsync` del mismo incidente. Idempotente (los pushurl son acumulativos, así
que los reconstruye en vez de añadir) y con `--check` para auditar sin tocar nada.
Verificado: los dos remotos en el mismo commit, GitHub reporta PRIVATE, y correrlo tres veces deja
2 pushurl, no 6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El destino de la distro vivía en 2 strings de product-userland-from-repo.sh, 1
de mirada-usb.sh y 55 tandas planas. Ahora en docs/state/targets.toml: un perfil
= una imagen enviable, listando sólo las RAÍCES (lo que se pide por nombre); la
clausura la calcula el grafo, no un humano.
4 perfiles: base (26), cli (46, hereda base), escritorio-mirada (14),
escritorio-kde (7 raíces, cola incoming-kde). scripts/targets.py expande hereda
(transitivo, con detección de ciclo) preservando el orden — mirada lo necesita.
Lift-and-shift verificado: las 3 listas expandidas son byte-idénticas a los
strings que reemplazan. Ninguna imagen cambia de contenido.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Queda una carrera abierta que el lock de script NO cierra: `build-farm.sh` corre `xargs -P2` y dos
recetas de la misma cola que compartan dep se pisan igual, porque `fetch` nombra el árbol de forma
determinista y sin nada del constructor.
El fondo no es "falta un lock", es que el árbol cumple DOS papeles que se contradicen bajo
concurrencia: caché direccionada por contenido (clave = nombre+sha, re-extraer es caro) y workspace
mutable de build (lib.rs lo parchea, aísla el workspace Cargo y vendorea EN EL SITIO, y después el
sandbox lo bind-montea). Por eso un lock alrededor de la extracción parece correcto y NO lo es: un
segundo proceso puede borrar un árbol que un bwrap ya está compilando.
El ADR deja las tres salidas (lock por árbol durante todo el build / árbol privado por build /
separar caché inmutable + copia privada) con su contra concreta cada una —incluida que el worker
corre ext4, sin reflink, así que la copia de la opción C es real— y los tres números que habría que
medir para elegir en vez de opinar.
Se ancla en los dos sitios donde alguien va a chocar: el ADR indexado en docs/README.md (de paso se
agregan 0009-0011, que faltaban) y un comentario en el propio `fetch_tarball` que dice explícitamente
que no se arregle a medias.
Va como PENDIENTE y no decidido a propósito: hay otro agente sobre este repo y esto es lo que evita
que la carrera se "arregle" de una forma que parece bien y deja el bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El `echo "ciclo: N recetas en $Q"` sale ANTES de pedir el lock, así que con la campaña corriendo el
journal mostraba el anuncio del ciclo y después nada: parece un loop trabajando y es un loop
bloqueado. Es la misma clase de problema que el "no encuentro el ejecutable zig" apuntando al
directorio equivocado — un log que miente cuesta horas de diagnóstico.
`flock -n` primero, y sólo si falla se anuncia la espera y se bloquea. Sin coste cuando el lock
está libre, que es el caso normal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El grafo de lo que EXISTE cierra perfecto (768 nodos, 0 huérfanas, topo OK).
El de lo que la distro DEBE tener no existe: vive en 2 strings de shell de
product-userland-from-repo.sh, uno de mirada-usb.sh y 55 tandas planas. De ahí
que "cuánto falta" no tenga respuesta.
Plan en 5 piezas: targets.toml (perfiles = imágenes, listando RAÍCES y dejando
que la clausura salga del grafo), estado `wanted` + membresía de perfil en
build-state.py, siembra de aristas desde metadata upstream sin construir, y el
drenaje topológico donde harkaq reemplaza la arista hipotética por la medida.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los dos construyen sobre el MISMO work/, y `fetch` nombra el árbol de fuentes de forma determinista
(`work/sources/<receta>-<sha16>`, sin nada que dependa de QUIÉN construye) ⇒ dos procesos que
necesiten la misma DEP apuntan al mismo directorio: uno hace `remove_dir_all` mientras el otro
corre `tar -x`. El árbol queda a medias y devuelve "Directory not empty" (os error 39), y así se
queda hasta que alguien lo borra a mano.
No es teórico: la campaña de deuda y el loop pidieron `mesa` a la vez (el loop lo arrastraba desde
la cola KDE al invalidarse libdrm) y se llevó puesta media cascada GUI, con un error que no nombra
la causa. Casi borro ese árbol a mano antes de ver que había un bwrap montándolo.
Grano: la campaña toma el lock para toda su corrida; el loop, por ciclo de cola. Más fino rompería
el `xargs -P2` de build-farm.sh.
Dos honestidades en los comentarios, para no prometer de más:
- `flock` NO es FIFO. El loop vuelve a pedirlo enseguida y le gana a la campaña que espera:
medido, la campaña entra al terminar TODAS las colas, no entre dos. La ventana real es el
IDLE_SLEEP. Por eso espera con techo (LOCK_WAIT=7200) y sale limpia en vez de colgarse.
- Esto NO cierra la carrera del todo: el `-P2` interno del loop puede correr dos recetas de la
misma cola que compartan dep, y ésas se siguen pisando. El arreglo de fondo es un lock POR
ÁRBOL dentro de `fetch`, que cubriría los dos casos.
Verificado con flock real: exclusión mutua (la campaña entra justo cuando el loop suelta), salida
limpia por timeout con rc=0, y la no-equidad de flock medida en vez de supuesta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mismo fallo raíz que ya se corrigió en pixman, ahora en la siguiente capa de la cascada GUI: meson
produce SÓLO .so por defecto, el consumidor enlaza estáticamente y el link muere. gtk4 lo dice
explícito: "unable to find static system library 'xkbcommon'" y "Static library 'drm' not found
for dependency 'libdrm'".
NO son "las 30 recetas meson". De las que no tienen `default_library=both`, 16 declaran
`=static` a propósito (gtk4, glib, cairo, pango…) y pasarlas a `both` sería otro cambio, no este
arreglo. Quedan 13 sin ninguna forma del flag —ojo, hay que buscar las DOS grafías,
`-Ddefault_library` y `--default-library`, o fcft se cuela como falso positivo— y de ésas sólo 5
producen librería que alguien enlaza: libxkbcommon (9 consumidores), libdrm (12), tllist (2),
libinput (1), seatd (1). El resto son ejecutables puros (bwrap, foot, iputils, usbutils, los
mesa-*) donde el flag no produce nada y sólo re-hashearía sin ganancia, o datos (wayland-protocols
son XML de protocolos, no compila librería).
`mesa` queda FUERA a propósito: produce librería y tiene 10 consumidores, pero es un build enorme
y no aparece en la cadena de fallo. Si hace falta, se decide aparte.
Radio de re-sellado medido con `hammer hash --check` antes/después: 8 recetas, de 754 selladas a
746. Las 4 tocadas que estaban selladas (libdrm, libxkbcommon, tllist, seatd) más 4 dependientes
que se invalidan (libepoxy y la cadena mesa, que cuelgan de libdrm). libinput ya estaba en deuda.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>