16e2be84d4e4f22ba4f7eb5b15fdcefdc4b7090b
170
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8b2ff3cfd4 |
digest: blake3 pelado, no length-prefijado — el mismo archivo tenía dos hex
Lo destapó escribir la contraparte del ADR 0014 para churay: churay direcciona sus blobs con blake3(bytes) pelado y hammer iba a sellar el digest con of_inputs(&[bytes]), los dos bajo el prefijo `b3:`. El mismo archivo con dos hex distintos, y un CAS compartido que no falla ruidosamente: cada lado busca un nombre distinto para los mismos bytes. Con una sola entrada el length-prefijado no desambigua ninguna concatenación — sólo hace que el nombre deje de ser verificable por un tercero con b3sum en la mano. `ArtifactHash::of_bytes` YA EXISTÍA y ya era blake3 pelado: es la convención de of_file y la del expected_hash de un .swm, así que of_inputs era además la pieza fuera de sitio dentro del propio repo. of_inputs se queda para lo que fue escrito: hashear una LISTA. Coste: una línea, porque ningún índice publicado lleva todavía el campo. Es el argumento del ADR aplicado a sí mismo — decidir antes de que haya usuarios. La corrección queda en el ADR, no reescrita en silencio. Guardián: digest_es_blake3_pelado_y_no_length_prefijado, con vector fijo de b3sum y un assert_ne contra of_inputs que nombra la consecuencia. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CK6HpSoHcN9M4GBpqRSusR |
||
|
|
832ade63b4 |
repo: el índice firmado ancla el digest del .swm, y --repo acepta lista de orígenes
La cadena de confianza estaba rota en el último eslabón y no se veía. La firma del índice cubre la lista de entradas, pero `file` es una RUTA, no un contenido: quien sirviera los bytes podía devolver otro .swm bajo el mismo nombre y la firma seguía casando. La red de aguas abajo no alcanza — `expected_hash` es opcional y el .swm se lee mucho antes (el gate de colisiones de `install` ya decide con su contenido). `PackageEntry::digest` (BLAKE3, la misma función que sella artefactos) se sella al publicar y se verifica antes de escribir un byte: raíz → firma del índice → digest → bytes. Con eso el origen deja de necesitar confianza, que es la condición para replicar en N espejos. Ausencia ⇒ se sigue al siguiente origen. Contenido distinto ⇒ ABORTA, no cae al siguiente: el fallback ahí convertiría una manipulación en silencio, con el paquete instalándose desde el espejo bueno y nadie enterándose de que uno miente. Campo Option con skip_serializing_if ⇒ un índice ya firmado serializa idéntico y su firma sigue siendo válida (test). Sin digests informa CUÁNTOS no verificó, para que un índice viejo servido desde un espejo ajeno no parezca verificado. El mensaje nombra el origen que sirvió de verdad, no la lista entera. 2 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CK6HpSoHcN9M4GBpqRSusR |
||
|
|
927dee60cf |
fuentes: HAMMER_MIRROR es una LISTA — un espejo propio era el mismo punto único de fallo
El ADR 0013 existe para no depender de 79 servidores ajenos, y lo resolvió creando una dependencia de UNO nuestro. Las dos variables aceptan ahora varios orígenes separados por comas y se prueban todos antes de caer a upstream. El orden se ROTA con una semilla determinista tomada del hash del propio objeto: el mismo objeto sale siempre del mismo origen (un fallo se reproduce), pero objetos distintos se reparten ⇒ una tanda del corpus ejercita la lista entera. Con orden fijo, el segundo origen no se tocaría jamás hasta la emergencia — que es literalmente «un espejo que nadie prueba», la lección del 0013 un nivel más arriba. La parte pura sale a `rotar_bases` para poder probarla sin tocar el entorno del proceso (que es global y haría los tests dependientes del orden en que corren). 3 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CK6HpSoHcN9M4GBpqRSusR |
||
|
|
1df77985fa |
tasas: T19 — el sandbox apila una capa overlay por dep, y lo caro no es lo que parece
Nadie había medido qué cuesta el apilado de capas de `bwrap_args`. El 95% de las recetas (1 108 de 1 171) construye con las deps APILADAS, hasta ~27. Un lookup que FALLA cuesta +1 925 ns por capa (a 64 capas, 124 µs = 1 580 syscalls nulas), pero se paga UNA VEZ POR RUTA: la dentry fusionada queda cacheada y el montaje dura toda la fase. Un `cc -O2 -c` toca 381 rutas distintas que fallan ⇒ 2,9 ms por fase a 24 capas, la misma décima de por ciento que la jaula de T17/T18. ⇒ `merge_deps_layer` no es una optimización de velocidad: sólo se pagaría sola a partir de ~35 200 primeros fallos en una misma fase. Es el rodeo de un muro, y el muro quedó bisecado al byte: `mount(2)` monta 41 capas (4 059 B de opciones) y falla en 42 con un `ENOENT` que miente. El `mount(8)` de util-linux se rinde entre 8 y 12, así que bisecarlo desde la shell da un muro FALSO. Consecuencias anotadas en sandbox.rs: no bajar el umbral «para que vaya más rápido», y podar `.dmerge` es gratis en rendimiento. Y hay puerta medida: la API de montaje nueva (`fsconfig lowerdir+`) monta las 64 capas que `mount(2)` rechaza. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
9142cd0565 |
upgrade: el mismo agujero que hydrate, en el proyector de generaciones
project_plan es target_root.join(rel) + escribir por ruta, igual que hydrate: un symlink de directorio dejado por una generación anterior se seguía. Reproducido — la generación 2 escribía usr/share/pkg/archivo FUERA del root y ApplyReport salía en verde. Con --root / no cambia nada (todo empieza por /); el caso real es , que es como se arma una imagen. Se mide el DIRECTORIO PADRE, no el destino: un Replaced sobre un symlink que apunta afuera es legítimo porque rename pisa el symlink en vez de escribir a través de él, y medir el destino lo rechazaría por error. Y raiz_real() resuelve el tramo existente del root dejando pegado el que todavía no existe, para que la comprobación valga también sobre una imagen nueva. Dos tests, uno por dirección: el que sale falla ruidoso, el que se queda dentro (usr-merge, temas de iconos) sigue proyectando. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
726068e653 |
hidratación: no se escribe a través de un symlink que sale del root (SDD 25 H3)
Buscando el sitio de H3 —«harkaq valida rutas a mano»— resultó que harkaq NO valida rutas a mano: delega en Landlock. Donde no se validaba NADA era en hydrate. Reproducido: hidratar es target_fhs.join(rel) y escribir por ruta, así que un symlink de directorio ya puesto en el FHS se sigue. Artefacto A trae usr/share/pkg → /algún/lado (los symlinks se replican literales, y así debe ser); al hidratar B, usr/share/pkg/archivo aterrizaba FUERA del root y hydrate devolvía Ok(1). Con --into sobre una imagen eso es escribir en el anfitrión diciendo que todo fue bien. Cuánto se estaba disparando: cero. En el store hay 160 symlinks absolutos y NINGUNO apunta a un directorio; de ~42 000 relativos ninguno sale de su artefacto. Mina desactivada, no incendio. La comprobación va por DIRECTORIO (un canonicalize por fichero sería un realpath por entrada, T8) y la semántica es la de RESOLVE_IN_ROOT, no la de NO_SYMLINKS: un symlink que se queda dentro tiene que seguir andando —usr-merge /lib → usr/lib, los 22@2x → 22 de los iconos— y hay test para cada dirección. Lo que queda de la propuesta (la versión sin carrera, con openat2 + linkat/renameat) queda escrito con su precio: arrastra el camino de patchelf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
8faede66bd |
kernel contract: un objetivo sin sellado vigente NO es un objetivo aprobado
Faltaban dos agujeros del mismo tamaño que el que este guardián vino a tapar: 1. La receta derivada de gioser vive fuera de recipes/ a propósito, así que sus deps no resolvían y su hash no se podía calcular ⇒ su sellado viejo se comprobaba como si fuera el vigente. Ahora se le presta el catálogo (base_dir), salvo que traiga patches — que base_dir también los resuelve y moverlo los rompería en silencio. 2. Un objetivo del contrato cuya receta de hoy no tiene NINGÚN sellado simplemente no aparecía en el barrido, y no aparecer se leía como que no había nada que objetar. Ahora se nombra con el hash que le tocaría y sale != 0: no comprobar no es aprobar. Hoy eso dice, con nombre y hash, exactamente los 4 kernels que hay que construir para dar H1 por pagado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
697d3f21a1 |
tasas H4: un test hace regla lo que era suerte — cero pre_exec en el workspace
pre_exec apaga el camino posix_spawn de Rust y devuelve el spawn a fork+exec: 231 µs contra 3 949 µs con 256 MB tocados en el padre (SDD 25 T1). hammer no usa ninguno hoy, pero nadie lo comprobaba. El test se comprobó en los dos sentidos: inyectando un pre_exec en sandbox.rs se pone rojo, y si el barrido no encuentra fuentes también falla — un guardián que aprueba por vacío es el fallo de CLAUDE.md §3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
3c716dfd9f |
kernel contract: el sellado VIGENTE bloquea, el superado se informa
El store guarda todos los sellados, no el último. Como `--sealed` los miraba a todos por igual, al cambiar una receta de kernel el gate quedaba rojo para siempre por artefactos que nadie va a volver a construir — y un portón que no puede ponerse verde deja de leerse. Ahora clasifica cada sellado contra el ArtifactHash de la receta de hoy (misma vigencia que `hammer hash --check`, lab incluido): el vigente bloquea, el superado sale en su propia sección con lo que le falta, porque sigue siendo cierto que una máquina que arranque ese kernel corre sus Cards sin tope. Dos negativas explícitas: si no se puede resolver la vigencia se comprueba TODO y se dice por qué; y cero vigentes con superados a la vista sale != 0 en vez de verde — el vacío leído como presencia es justo el fallo que este guardián vino a arreglar. Recetas derivadas incluidas: --recipes mira `recipes/` y `docs/state/kernel-plans/`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
7433bcba83 |
kernel: un contrato de capacidades que hace RUIDOSA la falta de MEMCG
SDD 25 §4 dejó el hallazgo escrito y sin guardián: los kernels de hammer se construyen sin CONFIG_MEMCG, `memory.max` no existe, y arje descarta el error al escribirlo ⇒ una Card pide un tope de memoria, corre SIN tope, y la única huella es un `warn!`. Nadie lo veía porque la máquina de desarrollo SÍ trae MEMCG: el fallo sólo existe del lado del artefacto sellado. `hammer kernel contract` declara qué pedazos de interfaz de kernel usa el userland POR NOMBRE (con consumidor, fichero y CÓMO FALLA HOY si no está) y los comprueba contra un `.config` YA PRODUCIDO — no contra la receta: entre el `scripts/config -e X` y el `.config` hay un `olddefconfig` que puede tragarse el símbolo en silencio. Por perfil, no global — misma lección que el gate de hardware: `linux.toml` es el kernel de QEMU del selfhost-verify, no hospeda Cards, y su hash es load-bearing del baseline `of_tree`. Exigirle contabilidad de memoria sería rechazar una receta sana. Medido sobre el store: **2 de 11 configs sellados cumplen su perfil**; los 9 `anfitrion-cards` fallan por MEMCG (apagado A MANO: `# CONFIG_MEMCG is not set`) y les falta PSI. El kernel vivo de esta máquina pasa las 11 exigidas — que es exactamente por qué el bug sobrevivió. Distingue apagado explícito de ausente (un símbolo que el .config ni nombra puede ser un renombrado entre versiones), y un kernel sin perfil declarado queda SIN COMPROBAR en vez de contar como aprobado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih |
||
|
|
ccb3d6af2f |
hydrate: un artefacto VACÍO ya no se proyecta como éxito
Tercer sitio con la misma pregunta mal hecha, encontrado con el grep que la lección del commit anterior pedía hacer: `has` (tapado 2026-08-10), `seal` (tapado hoy) y `hydrate`, que seguía con `is_dir()` pelado. Un directorio vacío pasaba el chequeo, proyectaba 0 ficheros y devolvía un HydrateReport de éxito. Duele especial acá porque el escritorio se hidrata así (`hammer hydrate <hash> --into`, 137/137 en KDE): un FHS proyectado desde nada sale «OK» y falla mucho más tarde, sin rastro de qué artefacto faltaba. Queda a propósito sin tocar `hammer-mirror::index`: ahí el vacío se autocorrige, porque indexa por `of_tree` y el contenido difiere del origen ⇒ el sync lo trae. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4 |
||
|
|
1eb39976b0 |
store: un destino VACÍO ya no se traga el sellado en silencio
`has()` define presencia como «tiene al menos una entrada» —hay test desde hace tiempo— pero `seal()` usaba `is_dir()`, que también es cierto para un directorio vacío. Con eso `seal` devolvía Ok, tiraba el árbol recién construido, y el build logueaba `sealed` y salía 0. Dos nociones distintas de «está» en el mismo struct, y la que decide si el trabajo se guarda era la mala. Medido en el worker dev.gioser.net: 1457 directorios vacíos en el store se comían cada sellado. `openssl-threads` compilaba entero —headers, libcrypto.a, libssl.a, los .pc— y sellaba NADA; python3 se construía después sin openssl y quedaba sin `_ssl`, y con eso morían glib y 4 recetas más de GNOME. El error salía a tres recetas de distancia de la causa y no nombraba openssl ni una vez. Probado quitando el vacío: el MISMO build sella 150 ficheros con OPENSSL_THREADS, mismo hash. Se usa `remove_dir` y no `remove_dir_all`: sólo tiene éxito sobre un vacío, así que si algo dejó contenido ahí entremedio falla ruidoso en vez de borrar un artefacto bueno. No mueve hashes (testigo zlib idéntico). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4 |
||
|
|
7106ec0645 |
sandbox: forzar archivos ar deterministas — dos builds sellaban bytes distintos
CÓMO SE VIO. Construir zlib en gioser y en el worker nuevo dio el MISMO ArtifactHash y BYTES DISTINTOS. `hammer why-differs` lo nombró exacto: 13 entradas idénticas, 1 diverge — `usr/lib/libz.a`, cabecera `ar` del miembro `/` (la tabla de símbolos), mtime 1787326910 vs 1787950875. Es la hora de pared de cada build. CAUSA, AISLADA POR BISECCIÓN EN EL LAB (no por lectura de código): · `zig ar` (llvm-ar) → mtime 0 ✅ · `ranlib` del rootfs Alpine → mtime 0 ✅ (Alpine compila binutils con --enable-deterministic-archives) · `ar` y `ranlib` de NUESTRO recipes/binutils.toml → hora de pared ❌ · los mismos con `-D` → mtime 0 ✅ La receta de binutils no pasa --enable-deterministic-archives, así que toda receta que lo materialice como build-dep y llame a ar/ranlib hereda la hora. Y el comentario de SOURCE_DATE_EPOCH en este mismo fichero AFIRMABA cubrir `ar`. Era falso: binutils no lee esa variable. Corregido, porque una suposición escrita en un comentario es la que impide que alguien vuelva a medir. ARREGLO. Wrappers `ar`/`ranlib` en /usr/local/bin (que gana al /usr/bin del PATH) que anteponen `-D`. Mismo patrón que ya usa el sandbox para arreglar esta CLASE de bug sin re-hashear: -mcpu=baseline, CARGO_PROFILE_RELEASE_CODEGEN_UNITS, CARGO_BUILD_JOBS. `-D` antepuesto y no como modificador de la cadena de operación, porque la operación la pone el llamador en "$@" y no se puede reescribir. Verificado: la secuencia exacta de zlib (`ar rc` + `ranlib`) da mtime 0 y el índice del archivo sigue siendo legible (`ar t`, `nm -s`). POR QUÉ AQUÍ Y NO EN LA RECETA. Arreglar recipes/binutils.toml es la cura de raíz, pero `yupana radio binutils` da 421 dependientes transitivos y 365 sellados a deuda, en las siete imágenes — desproporcionado frente a los 9 artefactos realmente contaminados (medidos barriendo los 185 artefactos con .a del store: elfutils, elfutils-libdw ×4, zlib, musl-fts, musl-obstack, argp-standalone). La cura de raíz se agenda para el próximo rebuild del corpus que ya haya que hacer por otro motivo. PENDIENTE Y DICHO EN VOZ ALTA: esto cambia los BYTES sin cambiar la DIRECCIÓN de esos 9. Hay que re-sellarlos a propósito — es el mismo peligro que la huella del lab existe para eliminar, y no se cierra solo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4 |
||
|
|
c2b1cafff1 |
mirror git: soportar pines que son objetos tag anotados
diffutils, findutils-xargs y kustomize pinean el SHA de un tag, no de un commit. Rompía las dos
puntas por la misma suposición: el poblador apuntaba una rama al tag (imposible) y hammer escribía
ese SHA en el fichero shallow (que sólo admite commits).
El poblador pela con ^{commit} y manda el objeto tag aparte en refs/tags/hammer-objeto; sin él el
cat-file -e del otro lado no encuentra lo que la receta pide. hammer lee la frontera del bundle con
git bundle list-heads, que no necesita los objetos, y trae refs/*:refs/*.
Los 339 bundles del formato viejo siguen sirviendo: comprobado con una receta de cada forma contra
un repo que no resuelve por DNS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
|
||
|
|
8ab5040a8d |
ADR 0013: mirror de fuentes git — bundles shallow por commit
Los 606 repos por commit son el 52% de las fuentes y el mirror de tarballs no los cubría. Se espejan
como `hammer/fuentes-git/{commit}.bundle` — 571 commits distintos, porque hay commits compartidos
entre colas y se espeja uno solo.
La identidad es el commit y la verificación la hace git: al desempaquetar comprueba cada objeto
contra su SHA, así que un bundle alterado no pasa. No hace falta índice ni sha256 aparte.
SHALLOW, NO CLONES COMPLETOS. El bundle sale de un `fetch --depth 1` del commit exacto: para `act`
son 9,3 MB en vez del repo entero, y con 571 fuentes eso decide si el mirror cabe. Es legítimo
porque hammer NUNCA usa la historia — lo único que hace con un repo es `git archive <commit> |
tar -x`, materializar un árbol.
EL DETALLE QUE COSTÓ ENCONTRAR. Un bundle hecho desde un repo shallow no lleva la frontera de
historia, y al desempaquetarlo git aborta con «Failed to traverse parents … did not send all
necessary objects». El mensaje dice que faltan objetos y es ENGAÑOSO: llegan enteros —`git archive`
ya funciona pese al error—; lo que falta es decirle a git dónde termina la historia. Se escribe el
propio commit en `<destino>/shallow` antes del fetch. No hay nada que transportar: la frontera de un
`--depth 1` es exactamente ese commit.
Y UN BUG PROPIO QUE VALE DOCUMENTAR: se pasaba al `git fetch` la ruta RELATIVA del bundle, y como
`run_git` invoca `git -C <destino>`, git la resolvía dentro de `<destino>`. Como el fallo del mirror
se traga a propósito para caer a upstream, el síntoma salía lejísimos: el build moría con «commit …
no existe en <repo> tras fetch», culpando a upstream de un error de ruta local. Ése es el precio de
que el mirror falle en silencio, y por eso el silencio se paga con comentarios explícitos.
Verificado igual que el de tarballs, con receta EFÍMERA para que no haya cache-hit: commit de `act`
ya espejado + un repo cuyo host no resuelve por DNS. Sin HAMMER_MIRROR_GIT falla en el clone; con
él, sella — y el artefacto trae el README.md real de act.
`cargo test -p hammer-build`: 5/5. Población de los 571 bundles corriendo aparte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
|
||
|
|
45b95f78b9 |
ADR 0013: mirror de fuentes — la URL es transporte, el sha256 es la identidad
`rsync` (404 de samba.org) y `musl` (musl.libc.org no responde) no se pueden construir hoy, y no por
culpa nuestra. Es el estado estacionario: una distro que construye TODO desde fuente tiene tantos
puntos de fallo como fuentes, y son servidores de terceros que nadie nos prometió mantener.
LA MEDIDA, peor de lo que parecía. Sobre 1167 fuentes (561 tarball + 606 git, 79 hosts):
github.com sostiene 742 — el 64% del corpus depende de UN host. Doce hosts sostienen el 89%. Y 43
hosts sostienen exactamente UNA receta cada uno: ahí es donde muerde el bit-rot lento.
El vigía, en su primera corrida: 8 URLs muertas de 1167. Tres de ellas —busybox, freetype,
freetype-shared— están SELLADAS Y EN USO: son el shell y las fuentes del escritorio que se capturó
hoy. Se salvan sólo porque el tarball sigue en la caché local de esta máquina.
LA URL NUNCA FUE LA IDENTIDAD, y el código ya lo sabía: `hash_inputs` usa `tarball:{sha256}` /
`git:{commit}` y el `..` descarta la URL; la caché se nombra `{sha256}.tar` con un comentario que
dice literalmente que cambiar de mirror no la invalida. Añadir mirrors NO re-hashea NADA. Faltaba el
mecanismo, no el diseño.
Orden: caché local → mirror propio → upstream. El mirror va ANTES, no como rescate: el sha256 se
verifica igual, así que no hay diferencia de contenido posible, y un mirror que sólo se usa cuando
upstream falla es un mirror que nadie prueba — se descubre roto el día que hace falta.
LO QUE HAY QUE HACER BIEN. Un mirror que sirve calladamente lo que upstream perdió convierte un
fallo ruidoso en silencio. Por eso construir y vigilar van SEPARADOS: `hammer build` nunca avisa
(sería ruido en 561 recetas), y `fuentes-vigia.sh` pide cabeceras, escribe
docs/state/fuentes-vigia.json y lo corre el latido. Sin ese contrapeso las URLs se mueren una a una
y el corpus queda irreconstruible con todo en verde — el mismo modo de fallo que dejó el grafo de
wlr 17 días anunciando un 121/121 falso.
⛔ PROHIBIDO cambiar el sha256 para "arreglar" una URL muerta. Es la tentación natural ante un 404 y
no arregla una descarga: cambia lo que la distro construye. Otro sha256 es otro contenido, y la
receta seguiría diciendo `rsync 3.4.4` mientras construye otra cosa.
VERIFICADO DE PUNTA A PUNTA. El primer intento —construir busybox con el mirror puesto— dijo BUILD
OK y NO PROBÓ NADA: cache-hit del artefacto, cero bytes descargados. La prueba válida usa una receta
efímera con un sha256 que sí está en el mirror y una URL que ni resuelve por DNS. Sin HAMMER_MIRROR:
`curl (6) Could not resolve host`. Con él: sella, con el contenido real.
Mirror poblado: 126 objetos en el Storage Box que ya se paga. `cargo test -p hammer-build`: 5/5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
|
||
|
|
6d99ca669b |
sandbox: el wrapper no inyecta -mcpu=baseline en modo preprocesador (-E)
util-linux —perfil BASE— no construia: `zig: error: unsupported option '-mcpu=' for target`. En modo `-dM -E -` zig traduce `-mcpu=baseline` a un `-mcpu=` VACIO y lo rechaza, asi que `errnos.h` no se generaba. La receta YA trae un apaño para esto: parchea sus generadores all_errnos y all_syscalls para filtrar el flag. Pero filtra `"$@"`, y el flag NO viene ahi — lo inyecta el wrapper DESPUES, en el `exec`. El apaño quedo inerte cuando se introdujo el wrapper, y no hay forma de esquivarlo desde una receta. Omitirlo con `-E` es inofensivo por construccion: preprocesar no genera codigo, asi que no hay CPU al que apuntar. ⚠ MISMO PATRON QUE make/--export-dynamic: el wrapper cambia lo que producen los builds FUTUROS sin mover ningun hash (no esta en hash_inputs), asi que la rotura quedo congelada por cache-hit hasta que el corpus se reconstruyo. Este cambio tiene la misma propiedad ⇒ se hace AHORA, con el corpus a medio reconstruir y casi nada sellado, no dentro de un mes. Verificado: util-linux sella (370530ff...). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
d797fc472d |
kernel: el gate contra un .config producido — ve los huecos que la receta base ya traía
El gate que había mira lo que apaga el PLAN, así que sólo ve regresiones que introduce el plan: un hueco que ya venía en la receta base le pasa por debajo. El modo nuevo compara DOS configs y define regresión como «funcionaba y dejó de funcionar», que es la formulación literal del #6 del handoff: hammer kernel gate --config <producido> [--baseline /proc/config.gz] --objective X El referente por defecto es /proc/config.gz: el kernel que arrancó esta máquina es la prueba viva de qué hace falta para arrancarla. Y comparar DOS configs, en vez de mirar sólo el nuevo, mata de raíz un falso positivo que tenía: los nombres de módulo cortos colisionan. El driver que /sys llama `usb` mapea a QE_USB (el USB de las QUICC Engine de Freescale) y `port` a PORT_CHAN. Mirando sólo el config nuevo aparecen como perdidos y el gate bloquearía un plan sano; exigiendo que estuvieran encendidos en el referente, el falso positivo se cae solo. Con test. Probado contra gioser (Hetzner vServer, 38 drivers bindeados) partiendo de linux-metal: destapó cuatro pérdidas que NINGÚN bundle causaba — aer, iTCO_wdt, lpc_ich y pcspkr están encendidos en el kernel que corre y linux-metal no los enciende nunca. El gate viejo no podía verlas por construcción. De paso, un mensaje que mandaba a buscar donde no está: sin culpable atribuido decía «lo apaga una perilla», cuando la causa es que la receta base no lo enciende. Catálogo, tres entradas nuevas nacidas de medir esta máquina: · bundle sin-gpu-intel — DRM_I915 es de los drivers más grandes del kernel y no sirve en una VM con virtio-gpu. NO apaga DRM: el vídeo sigue por simpledrm/EFI o virtio-gpu. · knob invitado-virtio — VIRTIO_BALLOON y HW_RANDOM_VIRTIO no vienen en el defconfig y ninguna receta del repo los enciende; en gioser los dos están BINDEADOS. Un kernel sin ellos arranca, pero la VM pierde el globo de memoria y la entropía del anfitrión. · knob plataforma-pc — PCIEAER, LPC_ICH, INPUT_PCSPKR y el watchdog ITCO_WDT. El watchdog necesita además WATCHDOG, que linux-metal apaga a propósito: por eso va en una perilla y no en la base. En una máquina sin acceso físico, el watchdog es lo que la reinicia cuando se cuelga. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
58d31618b7 |
hash: el toolchain del lab entra en hash_inputs — el corpus entero se re-hashea
Decision del usuario tras la medicion de los 4 kernels: dos labs con distinto rustc producian bytes distintos en la MISMA direccion, y el store no tenia como notarlo. Ahora el toolchain es una entrada del ArtifactHash. QUE ENTRA: 29 paquetes del rootfs cuya VERSION puede cambiar los bytes — compiladores/enlazadores (gcc, clang, llvm, binutils, rust, cargo), las libs de codegen de gcc (gmp, mpfr4, mpc1, isl), el runtime que se enlaza (musl, libgcc, libstdc++, libatomic, libgomp) y los headers que se compilan dentro (linux-headers, fortify-headers). NO entra el rootfs entero: cada paquete de mas invalida el corpus en cada bump, y con edge rodante curl se actualiza sin que cambie una sola instruccion emitida. Quedan fuera a proposito, y no es una afirmacion de que no influyan: los autotools y las shells pueden cambiar ficheros generados. Es una decision de coste. Si algun dia se ve una divergencia que rastree ahi, se anaden — y ese dia el corpus se re-hashea otra vez. DE DONDE SALE: del apk db del rootfs REAL (cfg.rootfs, que respeta HAMMER_LAB/HAMMER_ROOTFS), no de docs/state/lab-toolchain.lock. El lock sigue siendo el registro legible que viaja por git; hashearlo permitiria sellar con un lab distinto del declarado. Derivar la ruta del padre del store se descarto: esa suposicion ya rompio al worker cuando su store se anclo a un volumen (ver defaults_for_store_with_lab). SIN CAMINO SILENCIOSO: el parametro es obligatorio, no Option. Sin rootfs falla y dice que hacer. Un default aqui reintroduciria la divergencia que esto cierra. Trae test de regresion de un fallo MUDO: la primera lista de prefijos llevaba el guion de version (`gcc-`) y en el apk db el campo P: es solo el nombre (`gcc`) ⇒ no casaba ninguno y la huella salia la del conjunto vacio. Un hash valido, constante e inutil, que mirando el hash no se nota. COSTE, medido y no estimado: sealed 768 -> 0, debt 777. Los 1745 artefactos del respaldo quedan SUPERADOS, no perdidos. Ninguna imagen queda lista. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ca9bde0251 |
kernel: la clausura contrastada contra un olddefconfig de verdad — 65/68 con 0 falsos positivos
Hasta acá el armador era análisis puro: la clausura decía qué debía morir y nadie lo había
contrastado con el resolvedor real. NO hace falta construir un kernel para hacerlo: lo caro
es la fase compile (35-60 min); toda la cadena del armador vive en configure y corre en
segundos.
Método: árbol 6.16.12 entero, `make defconfig` de base (el config de linux.toml NO sirve de
base: ya apaga wifi/audio/fs a mano, así que el lado disable no apagaría nada y la
predicción no se pondría a prueba — fue mi primer error), el fragmento del plan, y
olddefconfig. Verdad de campo = los símbolos que pasaron de encendidos a apagados.
sólo depends on ....... clausura 1775 aciertos 64/68 SOBRA 0 falta 4
+ huérfanos select .... clausura 1794 aciertos 65/68 SOBRA 0 falta 3
+ comparaciones ....... clausura 1795 aciertos 65/68 SOBRA 0 falta 3
SOBRA 0 en las tres: el predictor nunca dice que muere algo que sobrevive, que es la única
dirección en la que puede equivocarse sin fabricar un ladrillo.
Dos refinamientos que salieron de la medición, cada uno con su test:
· HUÉRFANOS DE SELECT. Un símbolo sin prompt no se marca a mano: sólo entra por select.
Si caen todos sus selectores, cae él, aunque nadie dependa de él (caso ACPI_NHLT). Se
exige >=1 selector: sin ninguno entra por un default, y darlo por muerto mataría media
tabla.
· `X = y` SÍ ES DEPENDENCIA DURA. Medio drivers/video/fbdev declara su dependencia de FB
como `depends on (FB = y) && ARM`. Tratar toda comparación como opaca dejaba esos
drivers fuera. `X = n` sigue fuera a propósito: con X en n es VERDADERA. De regalo, las
fugas select sin declarar de sin-graficos cayeron de 8+ a 1.
Los 3 que faltan NO son un fallo, son otra pregunta: CRYPTO_LIB_ARC4, REGMAP y
SYSTEM_DATA_VERIFICATION tienen selectores FUERA de la clausura (PPP_MPPE, 111 usuarios más
de REGMAP…). Se quedaron sin usuarios en ESE config; encendés PPP y ARC4 vuelve.
«Inalcanzable» y «apagado ahora» no son lo mismo, y la clausura contesta la primera.
Y un bug que sólo aparece corriendo el resolvedor: el diff-back contaba como promesa
incumplida todo símbolo pedido ausente del .config. Pero Kconfig NO EMITE un símbolo cuyas
dependencias no se cumplen ⇒ un `-d WLAN` cuya raíz ya cayó simplemente no sale. Con esa
cuenta un plan perfecto se reportaba roto (2 falsos incumplidos de 16). Ahora: ausente +
se pedía apagar = éxito; ausente + se pedía encender = fallo. La corrida real sale 14
cumplidos, 0 incumplidos.
GUARDIÁN NUEVO, y hacía falta: las cuatro recetas de kernel NO son el mismo kernel — linux
y linux-metal van por 6.16.12, linux-metal-dual y linux-generic por 7.1.2. Planear una
contra el árbol de la otra calcularía clausuras sobre símbolos que ahí no existen, y
saldría SIN RUIDO. KconfigTree lee ahora su versión del Makefile de arriba y `plan` FALLA
si no coincide con la de la receta (los diagnósticos sólo avisan). Aviso de la sesión de
granja/store, verificado antes de implementarlo.
Catálogo: las dos fugas que destapó la clausura más grande quedan declaradas con motivo
(FB_SYSMEM_HELPERS_DEFERRED por HID_PICOLCD_FB; DRM_DISPLAY_DP_TUNNEL_STATE_DEBUG por
DRM_I915_DEBUG), y las notas sobre THUNDERBOLT/REISERFS_FS pasan a pasado: ya se
corrigieron en
|
||
|
|
fe382b0d26 |
kernel: el gate de no-regresión, por objetivo — el mismo plan pasa en QEMU y bloquea en metal
Paso 3 del §8 del SDD 22, y cierra el orden que fijaba: reversa, clausura, gate, diff-back. La pieza que faltaba no era el gate sino el MAPA driver → símbolo. El kernel sabe qué driver tiene bindeado cada dispositivo, pero no de qué CONFIG_* salió: esa relación sólo existe en los Makefiles de kbuild. modmap.rs lee 15.789 reglas obj-$(CONFIG_X) += y.o en 3182 Makefiles. Dos trampas de nombres, cada una con su test: · el módulo cargado usa _ donde el fichero usa - (snd-hda-intel.o → snd_hda_intel) · un módulo puede salir de VARIOS símbolos, y sobrevive si sobrevive cualquiera Sin resolverlas el gate no encontraría nada y diría que todo está bien, que es el peor resultado posible para un portón. POR OBJETIVO, no global. La regla es "todo dispositivo en uso debe seguir teniendo driver"; aplicada global rechazaría recipes/linux.toml, que apaga USB, HID e INPUT A PROPÓSITO por ser el kernel de QEMU con consola serie. El gate NO CORRE sin --objective, y un allow_bundles con un id mal escrito es error de CARGA del catálogo (si no, autorizaría nada y bloquearía sin que se entienda por qué). Medido con el mismo plan (sin-usb + sin-entrada-humana + sin-graficos + sin-wifi) y el hardware real de gioser: qemu-serial ........ PASA — 5 pérdidas autorizadas metal-escritorio ... BLOQUEA — las mismas 5 como regresiones, con el bundle culpable Ése es todo el punto del §5. Y lo que el gate no puede comprobar, lo dice: de los 38 drivers bindeados, 15 no se mapearon a ningún símbolo (pcieport, serial8250 — built-ins cuyo nombre de driver no coincide con el del módulo). Quedan listados como SIN COMPROBAR, nunca como aprobados. hammer kernel hw vuelca la huella y los drivers de la máquina DESTINO, que no tiene por qué ser la de build — el SDD lo pedía explícitamente. Cuatro objetivos en el catálogo (qemu-serial, servidor, metal-escritorio, portatil) y un runbook nuevo: docs/runbooks/armador-de-kernel.md, con el ataque de punta a punta y una lista honesta de lo que todavía NO está (sonda en VM, atestación por huella, bisección, curación del delta con modelo, perillas side=recipe). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
02991dd9d8 |
kernel: plan (receta derivada) y diff-back — dos hashes para el mismo código fuente
Paso 4 del §8 del SDD 22, y el corazón del armador.
hammer kernel plan --recipe recipes/linux.toml --bundle sin-wifi --bundle sin-audio
--bundle solo-ext4 --knob jaula-y-eio-moderna
base b3:cb926743…
derivada b3:47a52b2e… (16 banderas, 1775 símbolos con su clausura)
El config vive en la fase `configure` y las fases entran en hash_inputs ⇒ el config ES la
identidad del artefacto. Por eso el plan emite una RECETA DERIVADA y no finge que el
kernel sea un binario parametrizable. Y como el plan DETERMINA el artefacto, el JSON lleva
su ArtifactHash: la UI puede decir "esto ya está construido y firmado" sin construir nada.
Tres decisiones que no eran obvias:
· Se emiten RAÍCES, no clausuras: 16 banderas, no 1775 líneas. La clausura la calcula el
olddefconfig del propio kernel. hammer la sabe sólo para poder explicarla — la app
nunca escribe un .config.
· La fase derivada AÑADE una segunda ronda (…&& scripts/config … && make olddefconfig)
en vez de reescribir la base: no hay que parsear el shell de nadie, olddefconfig es
idempotente, y la base sigue siendo literalmente la de siempre en el diff.
· Los conflictos se RECHAZAN, no se ordenan. Resolver por orden de aparición sería una
respuesta plausible y arbitraria. Y hay un segundo conflicto que el símbolo solo no
delata: encender algo que cae DENTRO de la clausura de lo que otro bundle apaga —
olddefconfig lo descartaría sin decir nada.
diff-back: la mitad que faltaba del §6 del handoff. Clasifica cada símbolo pedido en
cumplido / INCUMPLIDO (el .config dice otra cosa) / ausente (el kernel ni lo menciona: la
bandera fue un no-op), con la procedencia de quién lo pidió, y sale != 0 si el config no
honra el plan. Probado contra /proc/config.gz de gioser: 3 incumplidos, 1 ausente.
La procedencia por símbolo (#7 del handoff) sale de regalo: cada bandera carga quién la
pidió y por qué (raíz del bundle / fuga select cerrada / perilla).
Y el orden del fragmento es estable a propósito: ese texto entra al hash, así que un orden
que dependiera del recorrido daría dos hashes para el mismo plan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cf2cf54914 |
kernel: modo reversa y catálogo de bundles — y las cuatro recetas apagan dos símbolos que ya no existen
Paso 1 del §8 del SDD 22 (va después de la clausura porque necesitaba el grafo). Sigue sin
compilar ni escribir nada.
hammer kernel probe — lee /proc/config.gz (o /boot/config-<release>) y muestra el kernel
que YA CORRE por el lente de los bundles: cuánto de cada uno rige, qué capacidad carga
esta máquina y no usa, y dónde el hardware CONTRADICE a un bundle aplicado. Corrido en
gioser: 10.551 símbolos, 20 dispositivos PCI, 38 drivers bindeados, 15 bundles, 7 con
capacidad que este hardware no usa.
hammer kernel bundles [--check] — el catálogo con las clausuras resueltas contra un árbol
concreto, y el control de frescura.
Piezas nuevas en hammer-core/src/kernel/:
catalog.rs bundles N1 y perillas N2. El campo `side` NO es decorativo: la mitad de N2
son variables de receta, no símbolos; mueven el hash igual pero se aplican en
otra fase y fallan distinto. Una perilla side=recipe sin recipe_field es
error de carga, porque es un diff que la UI no podría explicar.
hw.rs huella DMI+PCI+flags de CPU. El USB se LEE y se REPORTA pero NO se hashea: un
pendrive no puede cambiar la clase de hardware bajo la que se cachea un
kernel. Tampoco entra el serial: la huella agrupa máquinas, no las identifica.
Tres tests fijan esas tres propiedades.
reverse.rs el análisis. Y una tercera salida que no estaba pedida: cada fuga `select`
que entra a un bundle y NO está declarada en el catálogo es un símbolo que
upstream agregó y nadie revisó ⇒ la mitad barata de la curación del delta
(§3 del handoff) sale de comparar grafo con catálogo, sin IA.
docs/state/kernel-bundles.toml — 15 bundles N1 y 8 perillas N2, cada fuga resuelta a mano
una vez: `close_leaks` (se apaga también al que la provoca) o `accept_leaks` (se deja
abierta a sabiendas, con el motivo escrito). Ejemplo de por qué hacían falta las dos:
"sin-audio" NO cierra — DRM_I915/NOUVEAU/AMD_DC hacen select del códec HDMI, y cerrarlo
sería quedarse sin GPU. Se acepta y queda por escrito.
LO QUE DESTAPÓ EL CONTROL DE FRESCURA: las CUATRO recetas de kernel (linux, linux-metal,
linux-metal-dual, linux-generic) apagan `THUNDERBOLT` y `REISERFS_FS`, y 6.16.12 NO TIENE
NINGUNO DE LOS DOS. Thunderbolt se llama USB4 desde que upstream lo fundió con USB4;
reiserfs fue retirado. Los dos `-d` son no-ops silenciosos: el driver USB4 sigue entrando
por el defconfig mientras la receta dice que está apagado. NO las toco — cambiarlo mueve
el ArtifactHash de los cuatro kernels y es una decisión, no una limpieza.
Y el propio probe destapó un desajuste que ahora avisa: el config vivo de gioser es de la
serie 7.1 y el catálogo se revisó contra la 6.16 ⇒ las clausuras son aproximadas. Se dice
en vez de callarlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
040c768def |
kernel: el lector de Kconfig y la validación del §3 — la clausura reproduce los bundles a mano
Primer paso del SDD 22 (armador de kernel), en el orden que fija su §8. Regla dura
respetada literalmente: hammer LEE el grafo de Kconfig, no lo resuelve — el .config lo
sigue produciendo el olddefconfig del propio kernel.
hammer-core/src/kernel/: lector tolerante (18.212 símbolos, 1646 ficheros, 0 avisos de
parseo sobre 6.16.12) + lector de .config. hammer kernel {stats,closure}.
La semántica de arista, que el §3 pedía definir antes de escribir el predicado:
· dependencia dura = símbolo en posición CONJUNTIVA (en "A && (B|C)" sólo A). La
disyunción, la negación y las comparaciones no aportan. Conservador a propósito:
apagar de menos se nota, apagar de más hace un ladrillo.
· símbolo con varias definiciones ⇒ INTERSECCIÓN entre ellas, no unión.
· select es el portillo, no una arista más: fuerza el destino IGNORANDO sus depends.
select_leaks las enumera; closure_off_fixpoint cierra el bundle contra ellas y REPORTA
el precio en vez de aplicarlo solo.
La medición que decide §2.1, contra el bundle N1 hecho a mano de recipes/linux.toml:
clausura estricta de WIRELESS ......................... 350
punto fijo (3 fugas: WLAN, IWLEGACY, GELIC_WIRELESS) .. 406, cierra en 1 ronda
bundle a mano ......................................... 421
SOBRA 0 · falta 15
Los 15 son todos RFKILL, que no es wifi sino el interruptor de radio compartido con
bluetooth y NFC. El humano apagó DOS bundles en la misma línea ⇒ el catálogo necesita
"sin radios" como entrada propia. §2.1 es viable.
Y el punto fijo también dice cuándo no: cerrar "sin audio" exige tragarse DRM_I915/
NOUVEAU/AMD_DC, que hacen select del códec HDMI. En linux.toml sale gratis porque los
gráficos ya están apagados; en un escritorio sería una decisión.
De regalo: linux.toml apaga REISERFS_FS, que 6.16.12 ya no tiene. Un -d a un símbolo
inexistente se pierde HOY en silencio — justo lo que el diff-back (paso 4) va a atrapar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
fbd586f9b2 |
etapa 2: el split de debug FUNCIONA — y el piloto destruyó los artefactos antes de lograrlo
RESULTADO, con los tres criterios medidos a la vez sobre las dos recetas que divergían: bison 6M → 3M · 0 ficheros vacíos · 0 secciones .debug_ · «bison (GNU Bison) 3.8.2» appstream 56M → 18M · 0 ficheros vacíos · 0 secciones .debug_ · «AppStream version: 1.0.5» y las DOS pasan de DIVERGIR a REPRODUCIR. O sea que un solo cambio recupera espacio Y cierra la fuga de reproducibilidad, como predijo el §1.bis. Pero se llegó ahí después de tres errores que conviene dejar escritos. 🧨 1. `zig objcopy --strip-debug X X` (mismo fichero de entrada y salida) TRUNCA EL FICHERO A 0 BYTES. Destruyó los artefactos del piloto — y lo grave es que LOS TRES INDICADORES DECÍAN QUE IBA BIEN: el tamaño cayó 84% (porque los ficheros quedaron vacíos), `why-differs` dijo REPRODUCE (porque dos árboles vacíos son idénticos) y no quedaban secciones .debug_ (porque no quedaba ninguna sección). Se cazó al EJECUTAR el binario: 0 bytes. ⇒ La verificación de un artefacto tiene que incluir que SIGA FUNCIONANDO, no sólo que pese menos y reproduzca. Un artefacto vacío cumple las dos y no sirve para nada. Es la lección de esta campaña aplicada a la campaña misma: una métrica que parece éxito. 2. Al arreglarlo con fichero temporal, el strip pasó a ser un NO-OP SILENCIOSO: los binarios quedaban intactos y el tamaño no bajaba, porque no se pudo confirmar que `zig objcopy` acepte `--strip-debug`. Cambiado al `strip` de binutils, que sí funciona, a costa de declarar la dep. ⇒ Preferible una dep explícita que funciona a una comodidad que no se sabe si hace algo. 3. Con el strip real, apareció una fuga NUEVA: los artefactos seguían divergiendo, ahora por la CABECERA `ar` de los `.a` — `strip` los reescribe con los timestamps de cada corrida. Lo nombró `why-differs` exacto («archivar en modo determinista»). Arreglado con `strip -D` (= --enable-deterministic-archives). ⇒ Arreglar media causa deja el invariante igual de roto: el debug ya no divergía y el archivo sí. DISEÑO: `strip_debug` es un campo de la receta que ENTRA en `hash_inputs` y sólo si está fijado (mismo patrón que `zig_version`). Las dos mitades importan y están clavadas en un test: si no entrara, el lab cambiaría el contenido del artefacto sin mover el hash y el store MENTIRÍA; y al entrar sólo si está fijado, se despliega receta a receta sin re-hashear las 1161 de golpe — verificado: con el campo añadido al código, los 1161 hashes existentes NO se movieron. Va como paso del lab y no en la fase install de cada receta porque 383 de las 1161 no tienen install explícita: meterlo receta a receta obligaría a escribir a mano ese install por defecto en las 383, con riesgo de no clavarlo exacto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
bc4d0c8aea |
gnome: los 7 typelibs CERRADOS — librsvg y ibus, y dos deudas viejas pagadas de paso
Rsvg-2.0 librsvg b3:351f4658 IBus-1.0 ibus b3:d4d3750a **1. PERILLA NUEVA DEL LAB: `[source] cargo_vendor`.** `detect_build_system` asume UN sistema de build por árbol, y librsvg genuinamente tiene dos: `configure.ac` gana por precedencia pero su Makefile llama a `cargo build`, que dentro del sandbox hermético no tiene red. La perilla fuerza el vendoreo (que ocurre en el fetch, donde sí hay red) sin tocar la detección. **No entra al ArtifactHash** —decide de dónde salen las deps, no cuáles: eso lo fija el Cargo.lock, ya bajo el sha256 de la fuente— así que se puede prender en una receta ya sellada sin re-hashear nada. Con test, y verificado en vivo: libelogind no movió su hash tras el cambio. **2. LA DEUDA DEL UNWINDER, PAGADA.** librsvg moría en `undefined reference: _Unwind_DeleteException`. No es de librsvg: es de la `std` de rustc, que trae landing pads y espera el runtime que en glibc vive en libgcc_s. Es la deuda que el corpus arrastra desde matar-gcc —«las 12 recetas Rust son UN problema, no 12»— y el remedio estaba a mano: **zig empaqueta la libunwind de LLVM** y exporta los `_Unwind_*` (verificado con nm). Alcanza `LIBS=-lunwind`. Va por LIBS y no por LDFLAGS porque autotools pone LIBS al FINAL de la línea de enlace, que es donde tiene que ir una librería que resuelve símbolos indefinidos. librsvg va en 2.58.5 y no 2.59+: en 2.59 cambió a meson + cargo-c, y cargo-cbuild enlaza el crate `cargo` entero (libgit2, libssh2, libcurl, openssl) — una campaña propia por un binario que sólo corre en el constructor. 2.58.5 produce el mismo Rsvg-2.0. Mismo criterio que gnome-desktop 44.5. **3. `x11-compose-data`, y es una tensión que vale la pena tener escrita.** ibus COMPILA la tabla de teclas muertas de X11 dentro de libibus (Makefile.am:310, incondicional, sin `--disable-`). Sin datos no construye. Los datos viven en el tarball de libX11 por historia, no por necesidad técnica: son 5192 líneas de reglas. La receta extrae SÓLO los ficheros de locale reproduciendo la regla de upstream (`cpprules.in`: cpp crudo + CPP_SED_MAGIC literal) y no compila una línea de X11. Un escritorio Wayland-only sigue necesitando la tabla de composición del mundo Unix. Gotchas de ibus, los tres medidos: su ayuda MIENTE (`--enable-gtk4`/`--enable-wayland` sugieren default apagado; el default es `yes`); `--disable-emoji-dict` NO apaga `--disable-unicode-dict`; y con `--disable-wayland` el build muere igual porque `tools/main.c` llama wl_display_* sin `#ifdef` mientras WAYLAND_LIBS sólo se agrega si la opción está prendida — bug de upstream en su propia configuración sin Wayland. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d4eb77df14 |
harness: -static cede ante TODO link dinámico (no sólo -shared)
El wrapper anterior (quita -static con -shared) destrabó la extensión Python de g-i, pero g-i es dinámico en 3 puntos y el fallo saltó a los otros dos: "error: using shared libraries requires dynamic linking" en (a) las tools que linkean libgirepository-1.0.so y (b) el dumper del scanner con -Wl,--export-dynamic. Generalizo la regla del wrapper: -static se quita ante cualquier marcador de link dinámico — -shared, -rdynamic, --export-dynamic, o un shared object (.so/.so.N) en la línea. Todos son casos donde -static es contradictorio de por sí. El link estático normal (exe/.a sin marcadores) y el compile puro quedan byte-idénticos. Verificado contra las 2 líneas reales que fallaban + 2 casos de no-regresión. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
437d5bb103 |
harness: wrapper linker-driver que quita -static en links -shared
Las extensiones Python (python.extension_module → shared_module) son .so que el intérprete del sandbox debe dlopen; intrínsecamente dinámicas. El harness inyecta LDFLAGS=-static para link=static, y meson/autotools lo aplican a TODOS los targets por igual ⇒ el .so sale ET_EXEC y dlopen da "Exec format error" (el muro de gobject-introspection: _giscanner.cpython-312.so no cargaba). Fix estructural (elegido sobre flip-a-dynamic): ensure_layout materializa en el rootfs dos wrappers (hammer-zig-cc/-cxx sobre zig cc/c++) que QUITAN -static SÓLO cuando el link lleva -shared. -static+-shared es contradictorio ⇒ correcto por construcción: transparente (byte-idéntico) para todo link normal, arregla TODA extensión Python futura, no sólo g-i. Hash-safe: el ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒ no re-hashea ninguno de los 700+ sellados (cache-hit intacto); sólo cambia builds nuevos con .so dinámicos. Verificado: rota positional params, preserva orden y espacios; transparente sin -shared. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6a0dc40a05 |
sandbox: el merge de deps cae en el store (mismo fs), no en la raíz — desbloquea kio
merge_deps_layer funde las deps en UNA capa overlay por hardlinks (cp -al) para no desbordar el lowerdir de overlayfs (tope ~4096B por página). Pero calculaba la ruta del merge en deps.parent().parent() = <store>/../.dmerge, asumiendo que el store está en <base>/store del MISMO fs. En el worker el store es un VOLUMEN bind-montado (/opt/hammer/store en /dev/sdb, /opt/hammer en /dev/sda1) ⇒ el merge caía en la raíz, cp -al fallaba cross-device, y el fallback apilaba las ~50 deps de kio directo → lowerdir 5315B > 4096B → "bwrap: Can't make overlay mount" → kio (EL keystone, gatea 36) imposible de construir. Por eso estaba clavado. Fix de una línea: el merge va en el padre INMEDIATO de las deps (= el store mismo), garantizado mismo fs. Confirmado en el worker: cp -al a /opt/hammer/.dmerge falla "Invalid cross-device link"; a /opt/hammer/store/.dmerge funciona. En el laptop andaba por casualidad (store y su padre en el mismo fs). `.dmerge` (dot-prefix) es invisible para el store lookup/build-state como .times. cosecha excluye /.dmerge del rsync (es scratch de hardlinks; sin -H rsync lo expandiría a copias reales). Descubierto levantando el worker para poblar tiempos: yupana keystones apuntó a kio, la campaña falló ahí, y el radio de lowerdirs (5315B) destapó la causa. Una "sorpresa de entorno" (infra del sandbox, fuera del grafo) que el frente de timing sacó a la luz. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c963b24b5e |
ADR 0012: registrar como PENDIENTE el dilema del árbol de fuentes (caché vs workspace)
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> |
||
|
|
a107649250 |
build: desacoplar el lab (.dev-fs) del padre del store
Atar el lab al PADRE DEL STORE era una suposición oculta y cara. Al anclar el store de un worker a un volumen persistente, el lab pasó a buscarse en /mnt/cosecha/.dev-fs —que no existe— y toda receta con `zig_version` murió con "no encuentro el ejecutable zig en …", un mensaje que apunta al lugar equivocado: el zig estaba, y estaba bien, en /opt/hammer/.dev-fs/tools/. Dos campañas leyeron eso como deuda de la cascada GUI. El commit anterior lo tapó con un bind-mount, que respeta la suposición en vez de eliminarla; esto la elimina. Son dos decisiones independientes: dónde se GUARDA lo sellado (almacenamiento) y dónde vive el toolchain de desarrollo (entorno). `work_root` NO se desacopla, y es deliberado: el seal final es un rename, que sólo es atómico dentro del mismo filesystem, así que work DEBE seguir al store. Ése es un acoplamiento real, no una suposición — el test lo fija para que nadie lo "arregle" de más. Resolución del lab, de más explícito a más adivinado: `HAMMER_LAB` → hermano del store si existe (preserva EXACTAMENTE el comportamiento histórico en la topología normal) → hacia arriba desde el CWD, como git con .git (ésta es la que desacopla) → None, que cae al default de siempre para que los errores sigan apuntando a un lugar previsible. La parte que huele el entorno (env + CWD) queda sólo en `from_env_or_defaults`; `defaults_for_store` se mantiene PURA para que los tests sigan siendo herméticos. Los hashes NO se mueven: `artifact_hash` no recibe BuildConfig y `hash_inputs` sólo mezcla contenido de la receta (source id, compiler, target, link, el string zig_version, patches, flags, phases, hashes de deps) — ninguna ruta del lab. Verificado empíricamente además de por lectura: `hammer hash --check` sobre el corpus da 754 selladas / 14 sin sellar de 768, calcado al grafo de estado (12 deuda + 2 nunca). Si algún hash se hubiera movido, una sellada diría NO-SELLADO. Verificado también end-to-end: con un store cuyo padre no tiene .dev-fs, hammer sube desde el CWD, encuentra el lab y construye (antes moría en el acto); y la topología normal sigue dando cache-hit instantáneo con el mismo hash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
895401b479 |
cierre #2 del SDD 17: hammer why-differs — el diffoscope propio
Cuando un artefacto no reproduce, el store sólo sabe decir "el hash no coincide" y el resto es trabajo artesanal. Esto responde POR QUÉ, en términos de la CAUSA y no del byte: - gzip con MTIME embebido (bytes 4..8) → remedio: `gzip -n` - cabecera `ar` de un `.a` (mtime/uid/gid) → remedio: modo determinista (`ar D`) - secciones ELF, con lectura experta: sólo `.comment` ⇒ otra versión de compilador; sólo `.debug_*` ⇒ rutas de build; sólo `.symtab`/`.dynsym` ⇒ orden de símbolos (código idéntico); sólo build-id ⇒ residuo, no causa raíz. En un `.a` dice QUÉ MIEMBRO difiere. - ruta del árbol de build embebida, texto (línea que difiere), y bytes como último recurso. Y sobre todo trae la EVIDENCIA, no sólo la hipótesis: para las secciones de texto extrae las cadenas que están en un ELF y no en el otro. Caso real que lo motivó (alsa-lib): la interpretación decía "típicamente rutas de build" y la evidencia mostró `/src/target/release/build/libsodium-sys-<hash-cargo>/out/…`. Sin la cadena era una corazonada; reproducir eso a mano cuesta varios readelf, la herramienta lo da en 40ms. Descenso, no comparación total: sólo baja donde los hashes difieren (el cruce con format/ reconcile del SDD 17). Sin dependencias externas — parsers gzip/ar/ELF propios, como manda el ADR 0004: un diffoscope de verdad se apoya en medio mundo de binarios ajenos. `--json` para el bucle agéntico; exit 0 si reproduce, 1 si diverge (encadenable en scripts). `scripts/why-differs-barrido.sh` lo pasa por todo el store y separa los dos casos que se confunden a ojo: recipe.toml distinto (divergencia esperada) vs recipe.toml IDÉNTICO y artefacto distinto (no-reproducción a investigar). 5 tests nuevos; los 142 de hammer-core siguen en verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1e823d377b |
hammer hash: dry-run que faltaba — cierra el sobre-reporte del static-audit de raíz
El agujero que dejé señalado 3 veces esta sesión: nada podía saber el sellado VIGENTE de una receta sin construirla, así que static-audit.sh auditaba el más reciente por mtime (ls -dt) y acusaba a recetas ya sanas (dbus/libnl) por un sellado anterior a sus flags. FIX = subcomando `hammer hash <receta> [--check]`. Calcula el ArtifactHash puro sobre las recetas (source_id + compiler/target/link + patches + flags + fases + hashes de deps recursivos) SIN bajar fuentes ni compilar. artifact_hash() ya era pub; el CLI sólo lo expone. Cero cambios en la lógica de hashing ⇒ NINGÚN sellado se mueve. Verificado: `hash` da EXACTAMENTE el mismo hash que `build` (samurai, byte a byte); `--check` sobre receta editada → NO-SELLADO en 2ms, exit 1, sin construir nada. static-audit.sh ahora selecciona el artefacto VIGENTE por hash, no el más reciente por mtime. Con fallback a ls -dt si hammer no está compilado. Efecto en el store completo: las ~65 recetas cuyo sellado no es el vigente pasan de 'auditadas' (falsa cobertura) a 'sin artefacto' (deuda de rebuild REAL, ahora visible): estáticas de verdad 615 | MIENTEN 0 | sin artefacto 123. Corre en 15s, sin build. El '58 sin medir' de antes estaba enmascarando ~65 recetas más. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e8e25fb88e |
boot: activate --from-select idempotente — el hook de apagado de arje-zero lo invoca incondicional
Contrato PLAN-KIKIN §9 (respondido en HANDOFF-KIKIN-DESDE-HAMMER.md): mirada escribe /run/hammer/boot-select y dispara el reboot SIN salir; /run es tmpfs, así que la selección se aplica en el apagado o nunca. arje-zero corre esto en toda secuencia de apagado: - sin fichero o vacío → no-op limpio (exit 0) - con selección → activa (rollback E4) y CONSUME el canal; si falla, el fichero queda para diagnóstico - --select parametrizable (testeable); 3 tests nuevos (d/e/f) - 'boot menu' documentado como HARNESS de dev/VM: en producción mirada no sale Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
49fd38d53b |
arje-link: resync del espejo con arje-bus (Subscribe 13→14 + Parked/Refloored
arje inserto StopCardFromDisk en el medio de BusRequest el 2026-06-26 y corrio Subscribe a indice 14 — el puente estaba suscribiendose con el byte viejo desde entonces (roto en silencio; lo delato el golden test de arje-bus el 2026-07-16, lado tawasuyu 41f4accc7). Ademas el espejo de BusEvent no conocia EnteParked/EnteRefloored: se agregan como consumo-sin-senal (to_lifecycle -> Option). Tests unit+e2e actualizados y verdes. |
||
|
|
13233ee4e9 |
harkaq: primer barrido en la granja — la deuda irreducible de la muestra es perl (§4.10)
harkaq-farm-setup.sh: provisiona un worker para barrer (idempotente; gatea por
ABI>=7 y aborta si no llega — con la golden vieja el barrido daría SinEvidencia
en TODO). Deriva el runtime base DEL ROOTFS DEL WORKER, no copia el del laptop:
es por-rootfs (§4.3) y los sonames dependen de la versión de Alpine.
Barrido: 24 recetas, 14 con veredicto. Primer número crudo: 4 irreducibles (29%).
ERA FALSO, y por dos motivos propios:
1. GAP DE POLÍTICA. Los `list` salían sólo de los ancestros de la clausura ⇒ una
receta SIN deps (bash) no generaba ni uno y todo escaneo del árbol caía como
denegación (/usr/lib). Listar NO es leer: Landlock separa READ_DIR de
READ_FILE ⇒ la estructura del árbol es CONTRATO. Se puede `ls /usr/lib` sin
leer un solo fichero no declarado; la deuda es leer lo ajeno, no saber que
existe. Idem /var/tmp: con --tmp-overlay / es scratch descartable, como /tmp.
2. La heurística del catálogo es por NOMBRE y un fichero no se llama como su
paquete: /usr/bin/ranlib lo trae `binutils`, /usr/bin/diff lo trae
`diffutils`. El único que sabe la verdad es el STORE (conoce la lista de
ficheros de cada artefacto) — y el worker tiene 103 artefactos contra los
cientos del hub.
⇒ EL WORKER MIDE, EL HUB CLASIFICA. Misma separación que lector/clasificador: el
que tiene el privilegio —o los datos— hace lo mínimo. Reclasificado en el hub:
bash ranlib,/usr/lib,/var/tmp → nada (gap de política)
doas /usr/bin/diff → nada (diffutils lo provee)
ca-certificates /usr/bin/perl → /usr/bin/perl
curl /usr/bin/perl → /usr/bin/perl
RESULTADO: la deuda irreducible de toda la muestra es UN path — /usr/bin/perl
(2 de 14 = 14%). No hay recipes/perl.toml ni artefacto: deuda genuina y nombrada.
Todo lo demás era declarable (make ×10, ranlib→binutils, diff→diffutils).
Sigue por encima del <5% del §7, pero el perfil es el que importa: NO hay cola
larga de sorpresas. La deuda de un catálogo entero se resume en "declarar make" y
"no tenemos perl".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
b122ded31f |
matar gcc: cmake deja de arrastrar el runtime C++ de Alpine (§4.9) + /proc es contrato
harkaq (§4.6) dejó UN solo caso irreducible en el primer barrido: brotli pidiendo
libstdc++.so.6.0.34 + libgcc_s.so.1. Pero brotli es C, no C++ — la libstdc++ no
era suya: era de `cmake`, su dep de build.
store/99864dcc…-cmake/usr/bin/cmake
NEEDED: libstdc++.so.6, libgcc_s.so.1, libc.so
"gcc retenido para {kernel, cmake}" no era una concesión de BUILD-TIME como
sonaba: el artefacto de cmake arrastraba el runtime C++ de Alpine hacia dentro de
CADA build que lo declarara como dep. Un agujero de soberanía viajando por el
grafo de deps, invisible en la receta del consumidor. harkaq lo señaló desde el
consumidor, que es donde se ve.
EL ARREGLO NO TOCA EL COMPILADOR — gcc sigue compilando cmake (camino
conocido-bueno; zig c++ segfaultea el cmake mínimo). Sólo deja de enlazar su
runtime en dinámico:
CXX='g++ -static-libstdc++ -static-libgcc' LDFLAGS='-static-libstdc++ -static-libgcc'
Medido:
antes NEEDED: libstdc++.so.6, libgcc_s.so.1, libc.so
después NEEDED: libc.musl-x86_64.so.1 (y corre: cmake version 3.31.6)
EL PAGO: brotli —el único irreducible del barrido— pasa a Hermetico ×3 fases,
artefacto sellado b3:bc900676…. El barrido queda 0 irreducibles de 6. El caso que
iba a contar contra el <5% no era una receta mal escrita: era una herramienta de
hammer filtrando Alpine.
+ /proc como superficie de CONTRATO (lo destapó el configure de brotli, que lee
/proc/cpuinfo y /proc/meminfo): bwrap monta un /proc FRESCO dentro del pidns, no
sale del rootfs Alpine y no ve al host ⇒ contrato, no deuda. Mismo caso que
/cache. El hash del artefacto es idéntico antes y después de añadirlo: cambia el
veredicto, no el build.
COSTO DEL ROLLOUT: cambiar recipes/cmake.toml re-hashea cmake y sus 3
consumidores (brotli, libjpeg-turbo, libtiff = 6 sellados). Radio chico, PERO
libjpeg-turbo y libtiff son la cadena GUI y la regla es no rebuildearla en el
laptop (zig-skew rompe cairo) ⇒ el rebuild va al worker.
LO QUE NO CIERRA: /usr/bin/gcc, c89, c99, ldd y el plugin LTO (§4.7) siguen
siendo SONDAS — la jaula las deniega, los builds completan igual, y denegarlas es
lo correcto. El gcc de Alpine sigue en el rootfs y sigue haciendo falta para
{kernel, cmake} en BUILD-TIME. Lo cerrado es la filtración a RUNTIME, que es la
que contaminaba artefactos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
47982d7c6a |
harkaq: HITO DE FASE 1 — wiring en Rust; veredicto por fase sobre una receta real
crates/hammer-build/src/harkaq.rs + Sandbox::bwrap_args_harkaq: harkaq sale de `hammer build`, no de un script. Deriva la política de la clausura declarada (D1), lanza el lector, mete harkaq-exec como último eslabón antes del `sh -c`, y recoge el Verdict por FASE (configure/compile/install son bwraps distintos ⇒ dominios distintos ⇒ un veredicto cada uno). EL HITO, sobre recipes/zlib.toml (receta real del catálogo): configure → Hermetico esperadas (14): /usr/bin/gcc deuda: ninguna ✓ compile → Impuro DEUDA (1): /usr/bin/make → rc=126, la jaula lo frenó El diagnóstico completo de zlib cabe en una frase: lo único que toma de Alpine sin declararlo es `make`. INERTE sin HARKAQ=1: mismos args de bwrap, mismo entorno, ningún proceso extra. Requisito duro, no cortesía — 700+ artefactos sellados no pueden cambiar de hash por encender un diagnóstico. Los 57 tests previos del crate siguen verdes sin tocar, que es la prueba. harkaq-policy es PURO y se testea sin kernel (4 tests nuevos, como pide §4): traduce store→sandbox, excluye la metadata `.hammer/` del artefacto, y deja listables los ancestros de cada fichero de la clausura. La integración se delató sola en su PRIMERA corrida real: faltaba /cache como superficie de contrato (ZIG_GLOBAL_CACHE_DIR=/cache/zig — zig CREA dirs ahí) y la fase moría con `fs.make_dir /cache/zig/tmp`. De ahí que las superficies de contrato las decida el Sandbox (que sabe qué montó) y no harkaq: /cache sólo existe si hay cache_dir, y una política que nombre un path inexistente aborta el build a propósito. libc en hammer-build: Child::kill() manda SIGKILL y el lector moriría MUDO, sin emitir el Verdict — justo lo que no queremos de un componente cuyo producto ES el veredicto. Hace falta SIGTERM. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0f3ec4feac |
build: memoizar artifact_hash (DAG-diamante) — plasma-workspace (~90 deps) colgaba en walk exponencial de caminos
El grafo de build de KF6/Qt converge en qtbase/kcoreaddons por decenas de rutas; artifact_hash_rec recorría cada camino sin caché ⇒ O(nº caminos) exponencial. Memo por path canónico de receta. El hash resultante es idéntico (sólo cachea) ⇒ no invalida artefactos sellados. |
||
|
|
e69eaaaa3f |
build/sandbox: fusionar deps en UNA capa overlay cuando son muchas (fix límite lowerdir)
overlayfs concatena todas las lowerdir en un string de opciones acotado a ~1 página (4096B). KIO (~43 deps) desbordaba → bwrap 'Can't make overlay mount ... No such file or directory' (truncado). bwrap CANONICALIZA cada --overlay-src a su ruta real, así que symlinks cortos no ayudan (probado). Fix: cuando la suma de rutas supera ~3000B, se funden todas las deps en un dir real por hardlinks (cp -alf, last-wins = misma semántica que el apilado overlay) y se pasa esa ÚNICA capa ⇒ lowerdir constante, escala a cientos de deps (todo Plasma/apps). Cacheado por build con marker .done (una vez aunque bwrap_args corra por fase). Pocas deps = ruta directa sin I/O (sandbox idéntico, tests intactos). Validado: KIO configura con 43 deps visibles y compila. 57 tests verdes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a5be781ef6 |
arranque-grafo: hammer boot menu — orquestador del menú + wiring en el init (ADR 0010)
hammer boot menu ata las tres piezas del contrato: emite el grafo → lanza el compositor (mirada, --compositor configurable) → activa el nodo que el usuario dejó en boot-select. Graceful: sin compositor (servidor headless) emite el grafo y sigue el arranque. Refactor: activate_and_report compartido con boot activate; --out/--select configurables (testeable sin /run/hammer root-only). Wiring: iso-image INSTALLER=1 + install-image-efi bundlean el CLI hammer (static musl) al sistema instalado (el producto trae arje-zero/hammerd pero no el CLI); el wrapper /sbin/init lo invoca tras hammer-recover, salida al serial (no pinta tty0 ⇒ respeta cero-parpadeo). Cierra la pieza #3 del handoff mirada (quién lanza el menú en el boot): lo ownea hammer, desde el hook de init. Tests: boot_menu.rs (3 caminos del glue: graceful/sin-selección/selección→activate) + efi-disk-boot-test asevera que el menú corre. Validado en OVMF: pivote → INIT-OK → menú (emite boot-graph.json + saltea sin mirada) → arje-zero. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b580c5bfc5 |
fetch: cargo_vendor_dir per-receta — no pisar el vendor/ propio del proyecto
`cargo vendor vendor` borra lo que no reconoce del dir destino; proyectos que commitean su PROPIO vendor/ (mise → vendor/aqua-registry, 3.1MB que build.rs lee) lo perdían → build.rs falla 'No such file'. Nuevo campo opcional [source] cargo_vendor_dir (default 'vendor', sin cambios para las ~200 recetas selladas) manda el vendoreo cargo a otro dir; el .cargo/config.toml que emite cargo vendor ya apunta ahí. mise.toml usa '.hammer-cargo-vendor'. Validado en vivo: aqua-registry sobrevive + compila 591 crates pasando los muros previos (openssl vía rustls + AR). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ef86e4f986 |
arranque-grafo: current/default siempre presentes — interop con el lector de mirada
Verificado contra el parser REAL del otro agente (mirada-boot-core::BootGraph,
recién commiteado en tawasuyu): su struct declara `current: String` y
`default: String` OBLIGATORIOS (sin Option, sin default). Mi emisor los omitía
(`skip_serializing_if`) cuando no hay generación viva ⇒ en un sistema recién
instalado (cero upgrades) el JSON era `{"version":1,"nodes":[]}` y mirada
fallaba al parsear con "missing field `current`".
Fix del lado productor (adaptar la salida a la forma publicada del contrato):
BootGraph.current/default pasan a String, presentes siempre, cadena vacía = "sin
generación viva" (degrada limpio: default_index() de mirada cae al primer
bootable). Probado e2e pasando el JSON real de `hammer boot graph` (vacío +
poblado) por mirada-boot-core::BootGraph::from_path → ambos OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
3c343bee9a |
arranque-grafo: paso 3 del ADR 0010 — grafo de estados + hammer boot
El menú de arranque como navegación del grafo content-addressed de estados (no una lista de kernels). Sube el modelo de generaciones in-place a un grafo navegable y define el contrato de datos con mirada. - hammer_upgrade::boot_graph: BootGraph/BootNode (formato exacto del contrato), build() arma el DAG desde las generaciones (id = of_tree sin b3:, parents = of_tree del padre, Base para la raíz del linaje), nodo Recovery sintético colgando de la viva, emit() atómico a /run/hammer/boot-graph.json. - activate(): resuelve el id content-addressed a una generación y deja el sistema en ese nodo — no-op si ya viva, rollback paso a paso a un ancestro (reusa el rollback E4), recovery = un rollback, error honesto ante un "redo" hacia una generación huérfana (pide re-aplicar el árbol). - CLI `hammer boot graph [--out|--stdout]` y `hammer boot activate <id> [--from-select]` (lee el id que mirada deja en /run/hammer/boot-select). - Tests: DAG + activate a ancestro + recovery + redo-falla; rfc3339 sin deps. Validado e2e por el binario (3 generaciones → grafo → activar → recovery). Cierra el lado `proceso` de SDD 15 §H4 (arrancar = activar un nodo del DAG). mirada ya puede maquetar contra el grafo real (HANDOFF-arranque-grafo.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3e520d4477 |
H4e: requires OBSERVADOS — dep de runtime divergida = incompatible (SDD 15 §H4)
Cierra la simetría de la vía observada: además de observar lo que un paquete escribe (H4c), observa de qué depende A UNA VERSIÓN. Fuente observable sin declaración: el paquete se construyó contra la versión de sus deps que hay en el repo (deps.runtime del .swm + expected_hash de cada dep en el índice); si el usuario tiene esa dep instalada a OTRO hash, la divergió -> rechazo duro. Es el caso wayland DERIVADO (el que H4b captura cuando el autor declara requires, ahora leído del cierre). - compat::observed_requires(swm, index) + version_conflicts(db, req) — reusan deps del .swm, expected_hash del índice e InstalledDb.hash (cero declaración nueva). - Cableado en install (rechazo duro, no lo salva --force-slots) y en `hammer compat`. - Verificado e2e real: `hammer compat` marca app INCOMPATIBLE por su dep wayland-protocol instalada a un hash divergido del repo (read-only, ve el source_patch sin construirlo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
bbf4411af0 |
H4d: hammer compat <repo> — la búsqueda que particiona un repo contra lo instalado (SDD 15 §H4)
Responde la pregunta que arrancó §H4 ("cuando busco, ¿cuáles puedo adoptar?"): es el `filtrar`
del prototipo wawa-memo, ahora sobre el repo real y READ-ONLY (no construye ni toca nada).
Evalúa cada paquete contra el estado instalado y lo parte en {compatibles, requieren-elección,
incompatibles}, combinando la vía declarada (slots, H4b) con la observada (paths, H4c):
incompatible domina, colisión (de slot o fichero) -> elección, si no compatible.
Verificado e2e real (tests/compat_gate.rs): un repo de dos paquetes se parte correctamente
(uno choca de fichero con lo instalado -> elección; otro limpio -> compatible).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
9be7bdd707 |
H4c: superficies OBSERVADAS — colisión de fichero en install sin declarar slots (SDD 15 §H4)
La misma subida de H1 (prometer -> verificar), ahora sobre topología: la superficie más común de un paquete es el conjunto de paths que escribe, y esos paths ya están declarados en el .swm (target_bin + file_drop.path), conocidos ANTES de hidratar. - compat::output_paths(swm) lee esos paths; compat::path_collisions(db, name, paths) detecta cuáles ya posee OTRO paquete instalado (reusa InstalledDb.files + owner_of, cero declaración nueva). Reinstalar el mismo paquete sobre sus propios paths NO colisiona (upgrade). - Gate en `install` corre el chequeo observado JUNTO al declarado (H4b): pisar el fichero de otro paquete = caso logo a nivel de fichero (elección) -> aborta salvo --force-slots. - Verificado e2e REAL (tests/compat_gate.rs, shell-ea al binario hammer): dos paquetes escriben /share/logo.png; el 2do aborta con "COLISIÓN de fichero" sin escribir nada; con --force-slots la elección se respeta y el fichero se escribe. Un paquete SIN declarar slots ya participa del gate por lo que de verdad toca. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |