66 Commits
Author SHA1 Message Date
sergioandClaude Opus 5 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>
2026-08-08 00:31:35 -04:00
sergioandClaude Opus 5 bc4d0c8aea gnome: los 7 typelibs CERRADOS — librsvg y ibus, y dos deudas viejas pagadas de paso
Rsvg-2.0   librsvg b3:351f4658
  IBus-1.0   ibus    b3:d4d3750a

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:06:10 -04:00
sergioandClaude Opus 4.8 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>
2026-07-22 17:34:50 -04:00
sergioandClaude Opus 4.8 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>
2026-07-22 16:41:32 -04:00
sergioandClaude Opus 4.8 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>
2026-07-15 17:50:34 -04:00
sergioandClaude Opus 4.8 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>
2026-07-15 17:19:50 -04:00
sergioandClaude Opus 4.8 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>
2026-07-15 15:55:29 -04:00
sergio 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.
2026-07-13 19:17:57 -04:00
sergioandClaude Opus 4.8 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>
2026-07-12 09:33:50 -04:00
sergioandClaude Opus 4.8 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>
2026-07-10 03:18:16 -04:00
sergioandClaude Opus 4.8 27066776b3 H4b: gate de compatibilidad de configs en la receta real + hammer install (SDD 15 §H4)
Sube el modelo de slots de wawa-memo (prototipo host) a hammer:
- Recipe + source_patch del .swm llevan bloque `slots` {claims, requires} (slot->b3:…),
  FUERA de hash_inputs (topología ≠ identidad, no mueve el artifact_hash). Viaja intacto
  por los dos sentidos del puente (Recipe->.swm->Recipe): test de round-trip.
- InstalledDb registra claims por paquete + system_state() -> slot->hash (el Estado del gate).
- hammer-core::compat::evaluar(estado, slots) -> Veredicto {Compatible, Colision, Incompatible}
  (el álgebra probada en wawa-memo, sobre tipos de hammer).
- Gate en `hammer install`: antes de tocar nada evalúa el paquete contra el estado instalado.
  Incompatible (requisito sin resolver, caso wayland) -> aborta; Colisión (caso logo) ->
  aborta pidiendo elección salvo --force-slots; Compatible -> procede y registra los claims.

Plumbing propagado por los 5 sitios de Mutation::SourcePatch (from_recipe, swm_bridge,
export, bus, orchestrator). Tests: compat (4) + slots-no-en-hash/round-trip (2) +
system_state (1) + puente receta<->swm (1). Workspace compila y verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:10:02 -04:00
sergioandClaude Opus 4.8 497d0d00e7 H1b (proof-carrying recipes): checker swm-verify --evidence que ejecuta la evidencia
El verificador que corre cada check en el sandbox reproducible y falla ⇒ no proponer
(SDD 15 §H1). Piezas: (1) hammer-core: EvidenceCheck::evaluate(exit, stdout) -> CheckOutcome
(pura, testeada: exige expected_exit y, si hay, blake3(stdout)==expected_output) +
ArtifactHash::of_bytes. (2) hammer-build: Sandbox::run_capture -> CmdOutcome (captura stdout
completo + exit, tee de stderr) + run_evidence(recipe,cfg,store) -> EvidenceReport que
reproduce el artefacto (cache-hit), levanta un sandbox con fuente+build-deps+el artefacto
instalado como capa overlay (binarios en PATH) y corre cada check. (3) CLI: swm-verify
--evidence reconstruye cada source_patch y corre run_evidence; imprime veredicto por check +
estrato máximo alcanzado; exit != 0 si algún check falla. Es un runner de comandos con hash
del output, no un framework — la confianza vive en el checker.

Verificado e2e: tree con [[evidence.checks]] cmd-exit 'tree --version' → pack cache-hitea
(evidencia no cambia el hash) → swm-verify --evidence corre el check en el sandbox: pasa (exit
0) y falla con expected_exit=7 (exit 1, 'NO proponer'). Núcleo puro con tests unitarios.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:03:34 -04:00
sergioandClaude Opus 4.8 3116ccfc93 H1a (proof-carrying recipes): bloque evidence en receta y .swm
Frontera AI-nativa SDD 15 §H1. Agrega `Evidence { checks: Vec<EvidenceCheck> }` a Recipe
(TOML) y a Mutation::SourcePatch (.swm YAML): cada check es {kind, cmd, expected_exit,
expected_output?} con kind ∈ {cmd-exit, proptest, contract, kani} (estratos de confianza
crecientes, EvidenceKind: Ord). DECISIÓN CLAVE: la evidencia NO entra en hash_inputs —
certifica comportamiento, no identidad ⇒ no mueve el artifact_hash (baseline de
reproducibilidad intacto). Round-trip completo: from_recipe (forward) + swm_bridge
synthesize_recipe (reverse) + camino de pack (cli). verify_schema valida forma (cmd no
vacío, expected_output con prefijo b3:). El checker que EJECUTA la evidencia es H1b; el
cableado al Orchestrator VERIFY es H1c (marcado con evidence: _). Tests: recipe + swm,
incl. que la evidencia no cambia el hash. Sin warnings clippy nuevos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:41:23 -04:00
sergio 3f9121798f Etapa G: fetch extrae tarballs con tar --no-same-owner (fix de clase)
Como root (granja), tar preservaba el uid del tarball → el sandbox (userns) no podía chmod/escribir
esos ficheros → configure autotools fallaba (oniguruma: Permission denied creando Makefile). Extraer
como root sin preservar owner lo arregla para TODO tarball. oniguruma vuelve a configure plano.
2026-06-29 15:54:46 -04:00
sergio c583c4d679 Etapa G: frontera go.mod-en-subdir — knob [build] subdir (vendor/detect/compile en src/<subdir>)
Destraba proyectos Go cuyo módulo no está en la raíz (dolt→go, csvtk→subdir): vendor_go_deps corre en
el subdir, detect_build_system/detect_go_main operan ahí, y el compile hace 'cd <subdir>' primero.
Test go_subdir_compiles_in_subdir.
2026-06-29 15:06:35 -04:00
sergio d47be16a8b Etapa G: frontera CGO — knob 'cgo=true' (CC=zig cc + static) destraba sqlite bundled-C
vendor_go_deps/BuildSys::Go por defecto compila CGO_ENABLED=0 (Go puro estático). Nuevo campo
[build] cgo=true ⇒ CGO_ENABLED=1 CC="zig cc" (musl nativo del sandbox) + -linkmode=external
-extldflags=-static. Validado: usql + sq (driver sqlite3 mattn, bundled-C) → estáticos y CORREN
(usql 0.0.0-dev, sq v0.0.0-dev). Tests go_cgo_knob_switches_compile_env. cgo contra libs C EXTERNAS
(gpgme/libvirt en werf/minikube/skopeo) aún requiere esas recetas; esto cubre la clase bundled-C.
2026-06-29 11:04:27 -04:00
sergio f5880331ed Etapa G: soporte go.work multimódulo (vendor_go_deps usa 'go work vendor')
Destraba la clase workspace (go-mockery/k3d/git-town/csvtk): con go.work, GOWORK=off + 'go mod
vendor' sólo vendoreaba el módulo raíz → 'inconsistent vendoring' en el build. Ahora si hay go.work
se usa 'go work vendor' (go 1.22+, vendor consistente de todo el workspace); el trigger y
detect_build_system también reconocen go.work (workspace sin go.mod raíz). Test detect_go_when_only_go_work.
2026-06-29 09:57:06 -04:00
sergio 7545fc635d Etapa G: detect_go_main prefiere cmd/<nombre-receta> entre varios mains
eksctl tiene cmd/eksctl (CLI) + cmd/schema (helper de codegen que panica); detect elegía
arbitrariamente y selló el binario equivocado (schema) — un fallo SILENCIOSO (sella OK pero envía
la herramienta incorrecta). Ahora, a igual clase, el dir cuyo file_name == recipe.name gana. Test
detect_go_main_prefers_cmd_matching_recipe_name. Lección: smoke-testear que el binario instalado es
el ESPERADO, no solo que selló.
2026-06-27 06:28:41 -04:00
sergio b3be6c8e3c Etapa G: Go compile usa GOCACHE/GOTMPDIR en /src (disk), no en el tmpfs /tmp
telegraf/vault (binarios Go gigantes) fallaban en el LINK con 'mapping output file failed: no space
left on device': el sandbox monta --tmpfs /tmp (~½ RAM) y el GOCACHE + temp del linker desbordan ese
tmpfs aunque el disco tenga 18G libres. Ahora GOCACHE/GOTMPDIR van a /src (árbol de fuentes bind-
montado, en disco, per-build, lo limpia el watchdog; no se sella). No entra al hash de input (fase
generada) ⇒ no re-sella el corpus Go.
2026-06-27 03:17:50 -04:00
sergio 3b23463ac1 Etapa G: detect_go_main ve el main bajo header de comentario de bloque
cmd/helm/helm.go arranca con un header Apache /* ... */ cuyas líneas internas no llevan '*';
el filtro por-línea ingenuo de is_main_go cortaba en 'Copyright ...' y daba 'no es main' ⇒
detect_go_main no hallaba cmd/helm y caía a '.' (raíz sin .go → 'no Go files in /src'). Ahora
rastrea el estado dentro/fuera de bloque /* */. Afecta a cualquier .go con header de bloque.
Test: detect_go_main_sees_main_under_block_comment_header.
2026-06-26 23:38:47 -04:00
sergioandClaude Opus 4.8 5877363f5b Etapa G frente Go: AUTOMATIZADO end-to-end (BuildSys::Go + importador)
Cierra la fragilidad manual del patron Go (path del main por receta). Ahora
importar Go es tan automatico como Rust: import -> pin -> build, sin tocar nada.

- lib.rs: BuildSys::Go (go.mod, prioritario sobre configure/make auxiliares que
  traen muchos proyectos Go). resolve_phases deriva el compile generico:
  'go install -trimpath -ldflags=-buildid=' del paquete main. install='true'
  (go install ya deja en GOBIN=/out/usr/bin).
- detect_go_main(): detecta el dir del main por FILESYSTEM (no compila, evita
  contaminarse con mains de ejemplo rotos en docs/scripts que rompen go list).
  Heuristica raiz > cmd/<x> > menor profundidad; excluye vendor/docs/test/etc.
  go install nombra el binario solo (cmd/mlr->mlr, raiz->modulo).
- nix_import.rs + nix-import.sh: detecta is_go (vendorHash de buildGoModule),
  emite deps.build=['go'] sin phases (BuildSys::Go las deriva).
- tests: go_mod_wins_over_configure, detect_go_main_picks_cmd_over_docs.

Validado end-to-end: miller (cmd/mlr->mlr, esquiva docs rotos), amfora (raiz),
duf (import->pin->build 100% automatico, binario estatico que corre).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 12:34:27 -04:00
sergioandClaude Opus 4.8 74d5ab0b78 Etapa G frente Go: 10 recetas Go al corpus (repo 282->292) + fix GOWORK
Aplica el patron de amfora a la escoria Go. Build-yield 11/13:
- raiz: croc/gron/jump/gtrash/walk/qrcp (go build .)
- main en ./cmd/<x>: cheat/dnsx/dyff + miller (binario mlr)
Todos estaticos y corren (mlr 6.18.1, croc v10.4.4, cheat 4.5.0, ...).

fetch.rs: vendor_go_deps ahora setea GOWORK=off (go mod vendor aborta en
modo workspace si el arbol trae go.work).

Diferidas 2 (casos especiales): csvtk (go.mod en subdir, el vendor no corre
en /src) y git-town (go.work multimodulo -> inconsistent vendoring; necesita
go work vendor, no go mod vendor).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 05:06:21 -04:00
sergioandClaude Opus 4.8 4291b2adeb Etapa G frente Go: vendoring integrado en fetch — amfora construye, repro
Cierra el muro del frente Go (vendoring) replicando el patron de Rust:
vendorear in-situ en el fetch (host, con red), no tarball-al-mirror.

- fetch.rs: vendor_go_deps() analoga a vendor_cargo_deps — corre go mod vendor
  en el host si hay go.mod; el sandbox compila offline con -mod=vendor.
- lib.rs: engancha vendor_go_deps tras los patches cuando el arbol trae go.mod
  (sin BuildSys::Go; el compile lo fija la receta en build.phases).
- bootstrap-devfs.sh: instala el toolchain go del host (1.26.4, estatico) en
  .dev-fs/tools/go + symlink en ~/.cargo/bin (PATH del worker via .cargo/env).
- amfora: PRIMERA receta Go del corpus. Build end-to-end validado (repo+commit
  -> fetch vendorea -> sandbox offline -> binario estatico que corre, Amfora
  v1.11.0). REPRODUCIBLE bit-a-bit: dos stores independientes dan of_tree
  b3:9ec1d5fe (trimpath + buildid= en el compile). Promovida al repo (282).

Quedan 12 recetas Go en tandas/staged-2026-06-26/ (cheat/dnsx/croc/dyff/csvtk/
git-town/miller/jump/gron/qrcp/walk/gtrash): mecanico, copiar el patron de
amfora ajustando el nombre del binario.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 04:34:56 -04:00
sergioandClaude Opus 4.8 d41f1705e6 imagen↔repo: install hidrata userland del repo firmado + 2 fixes de path
Cierra el lazo Etapa F+G: el userland del producto se arma vía `hammer install
--repo --prefix --require-signed` desde el repo firmado, no del bootstrap
hardcodeado. scripts/product-userland-from-repo.sh hidrata un set curado (13+
tools validados estáticos del repo).

Dos bugs reales del path install/pack encontrados+arreglados:
1. pack derivaba target_bin=/usr/bin/{name}; para paquetes con binario≠nombre
   (ripgrep→rg, repgrep→rgr) quedaba mal y el sanity-check de install fallaba.
   Ahora pack lo deriva del flag `--bin <X>` de la receta Cargo.
2. La reproducción del source_patch derivaba el NOMBRE de la receta del
   target_bin ⇒ "rg" ≠ "ripgrep" del corpus ⇒ no cache-hit ⇒ rebuild + dup en
   el store (find_by_hash ambiguo). build_source_patch/run_apply ahora reciben
   el nombre del paquete (install/bootstrap lo pasan; apply=None) ⇒ cache-hit
   del artefacto del corpus, sin duplicar.

(El "EACCES" inicial era sólo --store ausente: DEFAULT_STORE=/store root-only.)
hammer-build 49/49 tests ok; ripgrep install validado e2e (rg hidratado).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:54:39 -04:00
sergioandClaude Opus 4.8 fd456ca9f0 Etapa G: destraba frontera openssl-sys (OPENSSL_DIR + fallback de deps al corpus)
Dos fixes en hammer-build que juntos destraban las recetas con C-FFI:
- Inyecta OPENSSL_DIR=/usr + OPENSSL_STATIC + OPENSSL_NO_VENDOR cuando la receta declara
  'openssl' como dep ⇒ openssl-sys usa la openssl hammer-built (estática/musl) en vez de
  buscar en el host y abortar.
- resolve_dep_path(): fallback al catálogo PADRE — una receta en recipes/incoming/ ahora ve
  las deps del corpus recipes/ sin duplicarlas (la granja construye desde incoming/).
Recetas: +dep openssl a monolith/mdcat/mise; tuc saca 'cargo auditable'→'cargo build'.
VALIDADO: monolith v2.10.1 sella (openssl-sys linkea contra la openssl del corpus).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:38:31 -04:00
sergio 7c1a2ac8d5 Etapa G: gueto gcc para build-scripts de crates Rust con dep-C (frente sistematico)
La granja revelo una clase entera de fallos Rust: crates con dep-C cuyo build-script
(cc-rs) compila C con zig-cc, que lo RECHAZA/miscompila (delta=libgit2-sys,
gitui=openssl-sys, jless=libc-stdhandle). Mismo frente que file/jq en C puro -> misma
solucion: el GUETO gcc.

FIX (hammer-build/lib.rs, camino Cargo nativo): cuando la receta declara compiler="gcc",
cc-rs usa CC=gcc (el Alpine musl gcc del sandbox, probado en C) para compilar el C de
las deps; el LINKER sigue siendo el wrapper zig-cc. Los .o/.a de gcc-musl y el Rust
zig-musl son ABI-compatibles (ambos musl, mismo ELF x86-64). ADITIVO: una receta Rust
sin compiler= queda en ZigCc -> baseline (ripgrep/fd/...) intacto. 2 tests nuevos.

PROBADO END-TO-END: delta 0.19.2 construye+corre ELF estatico (libgit2-sys vendorea y
compila libgit2 desde decenas de .c con gcc; + dep zlib para -lz que libgit2/libz-sys
enlazan). Publicado al repo firmado (83 paquetes). Corpus 82->83.

LEVERAGE: libgit2-sys/openssl-sys/onig_sys/libc-stdhandle aparecen en CIENTOS de crates
-> el gueto gcc (+ resolver libs externas via deps.build) destraba esa clase entera, no
solo delta. jless queda staged (su feature de clipboard pide libxcb/X11, fuera de
alcance, NO toolchain). gitui (openssl-src vendoreado) deberia construir con el mismo
patron (compiler=gcc), pendiente de correr.
2026-06-21 14:27:16 -04:00
sergio a9cba5002e Etapa F dogfood: fix bug de patch en install (.swm con patch + build-dep)
Dogfoodear el corpus entero (build-repo.sh: 55/56 con expected_hash anclado, repo
firmado Ed25519) destapó un bug latente en la reconstruccion desde .swm: el path
del patch se prefijaba con el catalogo (swm-recipes/) TRES veces, y el install
abortaba con 'No such file'. jq es el PRIMER paquete con patch + build-dep que se
instala (fd/bwrap no tienen patch) -> nunca se habia ejercido ese camino.

Causa: recipe_from_source_patch metia catalog_dir en el path Y synthesize_recipe lo
re-unia a base_dir, redundante con el base_dir.join(p) que el lab hace al construir
(fetch::apply_patches / Recipe::hash). Fix: source.patches guarda solo el NOMBRE
relativo a base_dir; un unico join lo resuelve. El hash usa el CONTENIDO del patch,
no el string del path -> el expected_hash anclado sigue casando.

Verificado e2e: install jq desde el repo firmado -> trusted -> reproduce desde
fuente (hash e770e04d = el anclado) -> patch aplicado -> 18 ficheros hidratados ->
registrado en DB -> jq corre. + test de regresion inline_patch_resuelve_con_un_solo_join.
2026-06-21 11:12:52 -04:00
sergioandClaude Opus 4.8 41aec17048 Etapa G Fase 3: flags autotools $CBUILD/$CHOST — el lab provee el triple nativo
El residuo autotools de los imports de Alpine (configure --build=$CBUILD --host=$CHOST
del abuild) ya no es trabajo a mano:

- El lab exporta CBUILD/CHOST con el triple nativo SANEADO (x86_64-linux-musl, el
  mismo que el wrapper zig-cc emite) ⇒ las fases traducidas de Alpine que referencian
  $CBUILD/$CHOST literal resuelven en runtime en vez de quedar vacías (config.guess
  detectaría x86_64-alpine-linux-musl, vendor que zig rechaza).
- La heurística autotools inyecta --build/--host al triple saneado cuando la receta no
  los puso ya (juicio per-paquete gana). build==host ⇒ NATIVO: autotools sigue corriendo
  sus AC_RUN tests; sólo normaliza el triple.
- Inerte para Cargo/CMake/Meson (no leen esas envs ni el triple).

VALIDADO REAL: e2e autotools BUILDEA (configure 'cross compiling... no', sella+corre).
3 tests nuevos de heurística + import comment actualizado.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:57:05 -04:00
sergioandClaude Opus 4.8 a157b8c455 Etapa G Fase 1: extractor de workspaces Rust — -p <pkg> auto en workspaces virtuales
Sube el yield Rust a casi-todo el ecosistema CLI moderno (sd/fd/… usan workspace virtual: root
sólo agrupa, el bin vive en un sub-paquete ≠ pname). Antes exigía `-p` manual; ahora el lab lo
resuelve solo.

- hammer-build/lib.rs: cargo_root_is_virtual (root con [workspace] sin [package]) +
  resolve_cargo_bin_package (lee los members vía `cargo metadata` —fuente autoritativa: globs,
  [[bin]], src/bin/*, nested— y devuelve el paquete que expone el bin) + inject_cargo_package_selector
  (si virtual y flags piden --bin X sin -p, antepone `-p <pkg>`). Determinista ⇒ reproducible; el
  hash usa los flags ORIGINALES, el -p es resolución interna. Crate suelto / ripgrep: intactos
  (no virtual). serde_json a deps de hammer-build.
- VALIDADO REAL: sd (workspace virtual, bin en `sd-cli`≠pname) ahora BUILDEA SOLO (sd 1.0.0),
  sin tocar la receta (flags quedan --bin sd, el lab resuelve -p sd-cli). 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:45:04 -04:00
sergioandClaude Opus 4.8 6e4d8da779 Etapa G build-yield: imports Rust de nix BUILDEAN (aislamiento [workspace] genérico + filtro toolchain)
Midiendo build-yield real con la capa puesta: hyperfine (nix) construye end-to-end → ELF estático
musl que corre. Dos fixes que lo desbloquean genéricamente (sin patch por receta):

- nix_import.rs: rustc/cargo/rust se filtran de deps (son el LAB, no paquetes) — sin esto el build
  abortaba buscando rustc.toml. Default de flags Rust vuelve a `--bin <bin>` (el `-p <pname>` no
  generaliza: el paquete cargo del bin puede ≠ pname, p.ej. sd→sd-cli).
- hammer-build/lib.rs: `ensure_cargo_workspace_isolation` inyecta `[workspace]` vacío al Cargo.toml
  de la fuente si no lo tiene, ANTES de vendor. Idempotente ⇒ no choca con las recetas del corpus
  que lo parchean a mano. Resuelve el gotcha "fuente dentro del workspace hammer ⇒ cargo vendor
  aborta" para CUALQUIER import Rust.

BUILD-YIELD medido (real, con la capa): lz4 (C/Alpine, escape gcc) ✓ · hyperfine (Rust/nix) ✓ ·
sd (Rust) ✗ workspace-virtual con bin en paquete ≠pname (necesita `-p` manual). Texture honesta:
los bien-estructurados buildean solos; los con quirks de workspace necesitan toque per-paquete.
31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:31:56 -04:00
sergioandClaude Opus 4.8 2d81bc6eaa Etapa G capa-de-adaptación #1: el lab honra build.compiler (escape de linker declarativo)
El muro del piloto: zig-cc filtra los flags de lld (rechaza -Wl,--allow-multiple-definition y
-z muldefs) ⇒ paquetes C con símbolos duplicados (lz4) no linkean. El escape es usar un toolchain
real (gcc de Alpine = musl + GNU ld, que SÍ acepta -z muldefs), pero el sandbox FIJABA CC="zig cc"
ignorando el campo `compiler` de la receta.

- hammer-build/lib.rs: `build.compiler` ahora setea CC/CXX/AR reales en el env (gcc→gcc/g++/ar,
  clang→clang/clang++/llvm-ar; zig-cc = default sin cambio). `self.env` pisa los defaults del
  sandbox. gcc/clang default a x86-64 genérico ⇒ reproducible, sin -mcpu=baseline.
- PURAMENTE ADITIVO: ningún recipe del corpus declara compiler=gcc (el gueto usa CC=gcc en fases),
  y el 4/4 núcleo es zig-cc ⇒ baseline of_tree intacto.
- VALIDADO REAL: lz4 (importado de Alpine) con `compiler = "gcc"` declarativo (fase SIN CC=gcc)
  → construye + corre (lz4 v1.10.0). El paquete que moría en el lld de zig ahora sella. 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:14:34 -04:00
sergioandClaude Opus 4.8 b89e2dba51 Etapa F paquetería #3: deps entre paquetes — install resuelve el cierre y reconstruye el catálogo
Cierra el hueco que pack/install advertían: un paquete con build-deps (bwrap→libcap,
openssh→zlib,openssl) ahora se instala por nombre reproduciéndose desde fuente CON sus deps.

Modelo: las build-deps viajan por NOMBRE en el source_patch y en la PackageEntry; install
resuelve el cierre transitivo desde el índice y reconstruye un catálogo de recetas que el lab
consulta al materializar deps en el sandbox.

- hammer-core: `Mutation::SourcePatch.deps` (Deps, serde-skip si vacío) + `from_recipe` lo
  carga. `Deps::is_empty`. `RepoIndex`/`PackageEntry.deps` + `resolve_closure(name)` (DFS
  topológico, deps antes que dependientes, detecta dep faltante y ciclo). 8 tests nuevos.
- hammer-build/swm_bridge: refactor — `recipe_from_source_patch` (síntesis pública, setea deps
  + base_dir=catálogo) + `catalog_dir_for` (dir determinista compartido). build_source_patch
  lo reusa. synthesize_recipe ahora setea recipe.deps + base_dir al catálogo (no "/").
- hammer-cli: pack puebla PackageEntry.deps; install resuelve el cierre y escribe un {dep}.toml
  por dep en el catalog_dir ANTES de aplicar el target (mismo dir determinista que usa
  build_source_patch ⇒ el lab resuelve {dep}.toml por nombre). Warning de pack actualizado.
- VALIDADO E2E REAL contra ./store: `install bwrap` resuelve libcap del catálogo y reproduce
  el artefacto CACHEADO EXACTO (b3:f89e716…) → hidrata bwrap (1.8MB ELF). El paquete con dep
  hashea bit-idéntico al original. Resolución/diamante/faltante/ciclo unit-tested. 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:43:30 -04:00
sergioandClaude Opus 4.8 339a7b07ef Etapa F paquetería #1: hammer pack — receta del corpus → paquete .swm (source_patch)
Cierra la dirección forward que faltaba (SDD 06 §6 la marcaba "para más adelante"):
una receta que el sistema ya sabe construir se vuelve un paquete distribuible y
reproducible-desde-fuente, inversa de `hammer apply`.

- hammer-core: `Swm::from_recipe(recipe, target_bin, patch_text, expected, distro)`
  (constructor puro: el caller lee los patches). `SwmBuild` gana `phases`+`zig_version`
  y `SourcePatch` gana `strip_components` (Option/skip ⇒ .swm viejos parsean igual) para
  reproducir con fidelidad el corpus real (22/34 recetas usan phases, 8 usan zig 0.13).
- hammer-build/swm_bridge: la dirección inversa (source_patch→Recipe→build) ahora traslada
  phases/zig_version/strip_components a la receta efímera ⇒ apply rehace idéntico.
- hammer-cli: `hammer pack <recipe> [--target-bin] [--out] [--expected|--build] [--sign]`.
  Concatena los patches inline; avisa si la receta declara deps (el source_patch aún no
  las modela = pieza posterior). `export` también enriquece su source_patch.
- Validado en host: ripgrep (git+patch+install custom), openssl (tarball+zig 0.13+phases),
  coreutils (multicall), findutils firmado → swm-verify "trusted". Tests core+bridge verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:20:44 -04:00
sergioandClaude Opus 4.8 7bf49fb960 build: zig_version por-receta — 5 víctimas C dropean gcc (zig 0.13)
Mata gcc para 5 de las 7 recetas que lo forzaban, vía una escotilla nueva:

- hammer-core/hammer-build: campo `[build].zig_version` por receta. Cuando se
  fija, el lab resuelve ese zig (hermano del por defecto, `zig-x86_64-linux-<v>`)
  en vez del global, y entra al hash SÓLO si está presente (baseline 9adefb82
  intacto). `effective_zig_dir` lo aplica en ensure_layout + Sandbox.

- Causa: BISECT con oráculo flex (reproducido sólo vía lab: musl DINÁMICO) — el
  miscompile es una REGRESIÓN de zig 0.14; 0.13.0 compila limpio, 0.14/0.15/0.16
  fallan. Es C/musl-dinámico, NO afecta C++.

- Flip a zig_version="0.13.0" (quitando CC=gcc): flex, openssl, elfutils,
  binutils, python3. Verificados: `as` 2.45.1 corre, python3 3.12.10 corre
  (deepfreeze OK), libcrypto/libelf sellan. Todas son tools (no inputs del 4/4).

cmake queda en gcc: su segfault es C++ (libc++/musl), bug distinto que 0.13 NO
arregla (ni con -static). El kernel queda pendiente de verificar.

Tests: hammer-core/hammer-build verdes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:20:17 -04:00
sergioandClaude Opus 4.8 98f0b2d228 bootstrap: orquestador all + export del manifiesto (Etapa A)
Cierra la Etapa A del camino a la distro (SDD 11 §5):

- `hammer_bootstrap::all(seed, recipes_dir, base_cfg, store) -> AllReport`
  encadena stage0→stage1→stage2 en una corrida del host y deja el
  `bootstrap.json` poblado. El veredicto ✓ REPRODUCIBLE lo sigue sellando el
  rebuild in-VM (selfhost-verify.sh); `all` ancla la referencia para comparar.
- CLI: `hammer bootstrap all --url … --sha256 … --version …` y
  `hammer bootstrap manifest` (imprime el log de transparencia, §4).
- Refactor: `parse_seed_kind` factoriza el match de `--seed` (estaba duplicado
  en 4 sitios del despacho).
- hammer-build: arregla el test stale `detect_cargo` — la fase Cargo evolucionó
  a `RF=…`+`-C link-self-contained=no` condicional, pero la aserción esperaba la
  forma vieja `RUSTFLAGS="-C linker=…`. Restaura el workspace en verde (43/43).

Tests: cargo test --workspace verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:11:56 -04:00
sergioandClaude Opus 4.8 9f7e75901b hammer-build: -C link-self-contained=no CONDICIONAL (Alpine rechaza la opción)
Fix de 503e09a, que añadía el flag SIEMPRE: el rust de Alpine (host x86_64-alpine-linux-
musl) NO sólo tiene self-contained apagado — PARCHEA la opción para que sea un error
("option `-C link-self-contained` is not supported on this target"), así que pasarla
rompía el build baseline (verificado: baseline-check falló en el build-script de
proc-macro2). Un rust vanilla (host x86_64-unknown-linux-musl, p. ej. hammer-rust) SÍ la
soporta y la NECESITA (su musl trae self-contained/rcrt1.o que choca con el crt1.o de zig
⇒ duplicate _start).

Ahora condicional por host-triple del rustc: se añade el flag SÓLO si el host NO es
`-alpine-`. Para Alpine ⇒ comando idéntico al pre-503e09a (sin flag) ⇒ 9adefb82 intacto
por construcción. Para hammer-rust ⇒ con flag ⇒ linkea con zig sin duplicar _start.
(`--print`/`--version` no validan -C; sólo el link real lo hace — por eso se discrimina
por host-triple, no por probe.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 22:50:10 -04:00
sergioandClaude Opus 4.8 503e09aeed hammer-build: -C link-self-contained=no en el build rust nativo (zig provee crt1.o)
El link de las recetas Cargo nativas lo hace `zig cc` (un driver de compilador completo
que ya aporta sus startfiles crt1.o). El target musl vanilla `x86_64-unknown-linux-musl`
trae CRT AUTOCONTENIDO (self-contained/rcrt1.o) y lo pasaría también al linker ⇒
`ld.lld: duplicate symbol: _start` (rcrt1.o vs crt1.o de zig). Añadir
`-C link-self-contained=no` a RUSTFLAGS hace que rustc NO aporte startfiles propios y
deje que zig los provea — para TODAS las unidades (deps, build-scripts, proc-macros y la
crate top; RUSTFLAGS las alcanza a todas, los bin_flags de `cargo rustc --` sólo a la top).

No-op para el rust de Alpine (host x86_64-alpine-linux-musl): su self-contained ya está
APAGADO — de hecho el baseline 9adefb82 linkea sin chocar con zig, lo que sólo es posible
si Alpine NO aporta startfiles self-contained. Imprescindible para un rust vanilla, p. ej.
el hammer-rust del frente self-host (SWAP_RUST): con él, hammerd YA compila y sella con
hammer-rust (antes fallaba en el build-script de proc-macro2).

Verificación baseline (of_tree(stage1) sin swap == 9adefb82 con este hammer) en cola.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 22:12:31 -04:00
SergioandClaude Opus 4.8 f6b337f6cb feat(hydrate): hidratación dinámica real con patchelf (LinkMode::Dynamic)
Deja de ser un error "pendiente": hydrate(.., DynamicSpec{interpreter,rpath})
detecta ELF por magic, copia los que necesitan parcheo (no hardlink: patchelf
mutaría el store) y aplica --set-interpreter/--set-rpath atómicamente
(copy→patch→rename); no-ELF y spec vacío siguen hardlinkeando. ensure_patchelf
falla limpio antes de tocar el FHS si falta la herramienta. HydrateReport.patched
cuenta los reescritos. Callers (bus/cli/bootstrap/e2e) pasan None=estático.
4 tests nuevos (incl. camino de error sin patchelf y real gated). Doc §4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 03:23:12 +00:00
SergioandClaude Opus 4.8 0f42c38106 feat(swm): source_patch modela tarballs además de git (Fase 4)
Mutation::SourcePatch y RecipeInline ganan campos repo/commit XOR tarball/sha256
(Option, serde-default ⇒ compat con .swm git existentes), resueltos por
swm::swm_source_kind con la misma regla que recipe::Source::kind. swm_bridge
sintetiza la receta según el modo; hammer export reconstruye fuentes tarball
como source_patch en vez de caer a file_drop (provenance fina recuperada).
Prompt del traductor y roadmap actualizados. Tests nuevos para tarball + XOR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 03:06:35 +00:00
sergioandClaude Opus 4.8 e9f535a1bb selfhost-verify: pieza 4 (bwrap swap) + materialización de build-deps en el lab
Cuarta pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): bubblewrap, el
sandbox del propio lab. Binario estático ⇒ swap de archivo sobre /toolchain/usr/bin/bwrap.
Como herramienta del toolchain (no input del 4/4) no necesita casar byte-a-byte con Alpine,
sólo aislar igual.

Tres piezas:
- recipes/libcap.toml (2.78): dep obligatoria de bwrap; el toolchain Alpine no trae el -dev
  (libcap.a / sys/capability.h). Build estático musl con zig cc, sin patches (es tool, no input).
- materialización de build-deps (hammer-build): deps.build ahora se CONSTRUYE recursivamente
  (build() llama build() por cada dep) y cada artefacto sellado se apila como capa --overlay-src
  bajo el rootfs del sandbox, dejando usr/{include,lib,lib/pkgconfig} en /usr. pkgconf y zig cc
  las hallan sin plumbing de flags. Recetas sin deps: sandbox byte-igual (baseline intacto).
  Tests nuevos: no_deps_emits_single_overlay_src, deps_stack_as_overlay_layers_under_rootfs.
- recipes/bwrap.toml (0.11.0): el toolchain no trae meson/ninja/python, así que bypaseamos meson
  compilando los 4 .c de bubblewrap directo con zig cc (+config.h trivial). deps.build=["libcap"].

Validación host fuerte: hammer-bwrap es estático, corre --version y sandboxea, y musl rebuildeó
BYTE-IDÉNTICO usándolo de sandbox (bisección). Expuesto con SWAP_BWRAP=1. Tests verdes.
Pendiente: corrida in-VM acumulando swaps para el sello ✓ REPRODUCIBLE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:05:55 -04:00
sergioandClaude Opus 4.8 be6f2e6e89 fix(sandbox): CARGO_BUILD_JOBS=1 para serializar el backend paralelo de rustc (determinismo)
[VERIFICACIÓN EN CURSO — no confirmado aún] Stage 2 variante (b) destapó que
codegen-units=1 NO basta para reproducibilidad: el backend paralelo de rustc/LLVM
(ThinLTO + codegen) dimensiona su pool de hilos por el paralelismo disponible y sus
decisiones varían build-a-build a >1 hilo.

Evidencia: arje-zero divergía ~62 KB entre dos builds host idénticos (mismo rustc
1.91.1, mismo commit fijado, mismos flags), mientras un build serial (la VM tiene
1 vCPU, o taskset -c 0 en host) reproducía bit-a-bit a of_tree=9adefb82. El baseline
0039b2b9 se construyó en paralelo ⇒ referencia no-fiable. make/musl/busybox/hammerd
están limpios (descartados uno a uno); el único flaky es arje-zero por paralelismo.

CARGO_BUILD_JOBS=1 limita el jobserver a 1 token para serializar el backend. Test
in-flight: rebuild de arje-zero con todas las CPUs + JOBS=1 debe dar 9adefb82. Si el
pool de LLVM ignora el jobserver (usa affinity), habrá que fijar afinidad o LTO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 04:34:22 -04:00
sergioandClaude Opus 4.8 4c9bcc016b sandbox: CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 — arje-zero reproducible (Stage 2 no-det #3)
El rebuild in-VM daba ✗ DIVERGENTE (of_tree host 984e002f ≠ VM 94b93585) pese a
que los 4 componentes sellaban bajo el mismo key of_inputs. Bisección por componente
en el host: musl, busybox y hammerd reconstruyen byte-idéntico, y la ensambladura del
rootfs (of_tree) es determinista e independiente del entorno. El culpable era arje-zero:
dos builds del MISMO host daban binarios distintos (Δ ~9.6 KB por .text/.rodata/.eh_frame/
.gcc_except_table) — firma del codegen paralelo de rustc.

Raíz: el workspace de hammer pinea [profile.release] codegen-units=1 (por eso hammerd
reproducía), pero el monorepo tawasuyu no declara perfil → cargo default codegen-units=16,
cuyo reparto del crate en N objetos varía build-a-build.

Fix: el sandbox impone codegen-units=1 para TODAS las crates Cargo (el var de cargo gana
sobre el profile del repo fuente). El lab elimina el no-determinismo en vez de confiar en
upstream (SDD 09 §2). Verificado: dos builds cu=1 de arje-zero → byte-idénticos.

Nueva referencia reproducible 4/4: of_tree(stage1)=b3:0039b2b9… (reemplaza 198f209f… de
cu=16). Actualizados EXPECT_REF (selfhost-verify.sh) y runbook §8b/§8c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 04:31:51 -04:00
sergioandClaude Opus 4.8 1ab53a650f selfhost-verify: módulos del kernel booteado + build a stderr (E2BIG)
Dos bugs latentes que sólo aparecían en un build real (in-VM); en el host
quedan tapados porque todo sale de caché.

1. scripts/selfhost-verify.sh: los módulos se sacaban de /lib/modules/$(uname -r)
   pero la VM bootea $KERNEL (/boot/vmlinuz-linux), que puede ser otra versión.
   Mismatch de version-magic ⇒ overlay.ko no carga ⇒ bwrap muere "No such device".
   Ahora la versión se deriva del bzImage booteado (file -bL "$KERNEL"; fallback uname -r).

2. crates/hammer-build/src/sandbox.rs: spawn_pump teeaba el stdout del build al
   stdout del padre. rebuild-stage1 hace PRIME=$(hammer ... bootstrap stage1); en
   un build real son miles de líneas de configure/make capturadas en $PRIME ⇒ el
   paso siguiente --rootfs "$PRIME" exec con un arg gigante ⇒ E2BIG (Argument list
   too long). Ahora el tee va a stderr; stdout queda para el hash legible-por-máquina.

Con esto el verify corre end-to-end in-VM: los 4 componentes reproducen bit a bit,
pero of_tree(stage1) diverge (host vs VM vs baseline) — no-determinismo del ensamblado
a cazar (SDD 09 §2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 03:04:45 -04:00
SergioandClaude Opus 4.8 4fe452848f bootstrap: -mcpu=baseline en zig cc — reproducibilidad CPU-independiente (Stage 2)
El rebuild in-rootfs de Stage 2 cazó un no-determinismo real: stage1 reconstruido
en la VM divergía del host (of_tree host=b3:94a4f1… vs VM=b3:ab615b…), aun con el
seal hash idéntico (el seal es of_inputs: mismo key ≠ mismos bytes).

Diagnóstico (control en el host, sin VM): un rebuild de hammerd en el host es
byte-idéntico al cacheado, así que la receta es determinista host-a-host y el cache
no está stale. La divergencia es del entorno de build de la VM: `zig cc` default a
`-mcpu=native` y hornea la ISA del builder en el C/asm de los build-scripts (blake3,
curve25519-dalek). El host tiene SHA-NI+AVX2; `-cpu Broadwell` (VM bajo TCG) no tiene
SHA-NI ⇒ codegen distinto ⇒ bytes distintos ⇒ of_tree distinto. Misma raíz que el
SIGILL de AVX del runbook §7 (binarios al CPU del builder, no a un baseline genérico).

Fix: `-mcpu=baseline` en TODOS los sitios zig cc — CC/CXX del sandbox, los dos
wrappers Cargo (.hammer-zig-cc) y CC/HOSTCC de la receta de busybox. Las rutas SIMD
del runtime (blake3/sha2) son asm con dispatch en runtime: siguen presentes. Bonus:
cierra el AVX/SIGILL del §7 (corre en qemu64 sin -cpu Broadwell).

Validado en el host: con baseline hammerd cambia de bytes (1672448 vs 1677584 —
confirma que el default no era baseline) y los 3 componentes rebuildan limpio
(stage1 rc=0). Nueva referencia CPU-independiente of_tree(stage1-baseline)=
b3:4408e44e2ec51c3769deffcd64dd1f6c2010d414418937c3fa7930a0ded3f845. Runbook §8c
documenta el cruce del muro Rust-offline (cargo vendor por la NIC, hammerd sellado
in-VM en 74m35s) y este diagnóstico+fix.

Nota (plan C.2): el flag de CPU no entra en of_inputs ⇒ cambio de toolchain/flags da
cache-hit con bytes viejos; cerrar ese "pin de toolchain al hash" es trabajo aparte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 20:07:36 +00:00
SergioandClaude Opus 4.8 974504e068 build: SOURCE_DATE_EPOCH + TZ=UTC en el sandbox (determinismo pre-Stage 2)
Stage 2 compara el content-hash (of_tree) de stage1 vs stage1' rebuildeado; para
que coincidan, el build debe ser determinista. Las rutas ya lo son (el source se
bindea en /src, constante entre rebuilds); el knob que faltaba es el timestamp:
SOURCE_DATE_EPOCH=1 (+ TZ=UTC) fija los que tar/ar/gzip y __DATE__/__TIME__ embeben.

No cambia ningún ArtifactHash (input-addressed) ⇒ caché intacta; sólo normaliza
los bytes de salida de builds futuros. Plan C.2 #5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:49:35 +00:00
SergioandClaude Opus 4.8 879af17f5c build: flags estáticos vía cargo rustc -- … (no RUSTFLAGS) para no romper proc-macros
Tercera iteración del fix de la corrida real (runbook §8). +crt-static (y
relocation-model=static) en RUSTFLAGS rompen los proc-macros en build nativo:
RUSTFLAGS alcanza a las deps, y un proc-macro es un dylib PIC que no puede ser
estático ("cannot produce proc-macro for clap_derive ... crate types").

Fix: pasar los flags de codegen del binario por `cargo rustc --release … -- <flags>`,
que los aplica SÓLO a la crate top-level, dejando proc-macros/deps intactos. Así el
binario final es crt-static + relocation-model=static (AUTOCONTENIDO y ET_EXEC sin
interpreter, como busybox: sin libc.so, sin loader, sin soname). El linker (zig cc)
sigue por RUSTFLAGS. musl queda --disable-shared (no se necesita loader).

Validado por unit tests; la corrida real rebuildea hammerd/arje-zero crt-static.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:24:01 +00:00
SergioandClaude Opus 4.8 ce7b570a62 build: recetas Cargo estáticas = crt-static + no-PIE (binarios autocontenidos)
Cierra el último bloqueo del boot que la corrida en QEMU destapó (runbook §8):
arje-zero/hammerd buildeaban DINÁMICOS contra musl (el rust de Alpine es dinámico
por defecto) → DT_NEEDED libc.musl-x86_64.so.1, y nuestro libc.so (musl shared con
zig cc) no exporta memcpy/memset/… → el loader falla y el init muere.

Fix golden-path: para link=static, RUSTFLAGS ahora fuerza
  -C target-feature=+crt-static -C relocation-model=static
⇒ binarios AUTOCONTENIDOS (musl dentro), ET_EXEC sin interpreter, como busybox:
sin libc.so, sin loader, sin el soname de Alpine. musl vuelve a --disable-shared
(no se necesita el loader). Boot en QEMU: usar -cpu Broadwell (el qemu64 default no
tiene el AVX que zig emite).

Validado por la corrida real: los 4 componentes buildan y el rootfs se sella; el
kernel arranca el initramfs y ejecuta arje-zero como PID 1. Falta rebuildear
hammerd/arje-zero con crt-static para cerrar el boot (cache-bust + rebuild).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 06:45:11 +00:00