Files
hammer/scripts/verificar-repro.sh
T
sergioandClaude Opus 5 102aabd1ff SEGUNDA corrección: lo que medía el test era DERIVA, no no-determinismo — el corpus SÍ reproduce
El §1.bis dijo «sí hay no-determinismo» porque appstream y bison divergían. También estaba mal,
y por un fallo de diseño del propio test.

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

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

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

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

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

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

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

89 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# verificar-repro.sh — ¿reproducen los artefactos bit a bit? Reconstruye y COMPARA. Etapa 1 del SDD 23.
#
# ── POR QUÉ ESTE SCRIPT Y NO UN GREP ───────────────────────────────────────────────────────────
# El no-determinismo se mide RECONSTRUYENDO Y COMPARANDO, no buscando cadenas sospechosas en los
# binarios. El plan heredado daba 92 paquetes por «no-deterministas probables» porque tenían rutas
# `/src` en `.debug_*`; al mirarlo resultó que `/src` es un bind CONSTANTE del lab, y que las únicas
# rutas realmente ajenas venían de terceros (la ruta de compilación de Alpine horneada en el musl
# prebuilt, cadenas literales del fuente, rutas de autores en assets SVG). Un grep sobre binarios da
# CANDIDATOS, no veredictos. Ver SDD 23 §1.
#
# ── ⚠ QUÉ MIDE ESTE SCRIPT, EXACTAMENTE (leer antes de sacar conclusiones) ─────────────────────
# Compara el artefacto GUARDADO en el store contra una reconstrucción de hoy. Eso NO es lo mismo que
# medir no-determinismo, y confundirlos costó dos conclusiones equivocadas en el SDD 23:
# · si el artefacto guardado es viejo, se construyó con OTRO estado del lab ⇒ una diferencia es
# DERIVA («el mundo se movió»), no no-determinismo;
# · el no-determinismo es: mismas entradas, MISMO lab, salidas distintas.
# ⇒ Para medir NO-DETERMINISMO hay que correr el script DOS VECES: la primera pone el artefacto al
# día con el lab actual, y es la SEGUNDA la que contesta la pregunta. Medido: anew, gron y age
# divergían en la primera corrida y REPRODUCEN en la segunda.
#
# ── EL MÉTODO, Y POR QUÉ ES BARATO ─────────────────────────────────────────────────────────────
# Se APARTA el artefacto (no se borra) y se reconstruye. Como las DEPS siguen en el store, el rebuild
# es sólo el paquete en cuestión, no su cadena — que es lo que hace viable verificar decenas. Después
# `hammer why-differs` compara los dos árboles y nombra la causa si divergen.
#
# ── LA RED DE SEGURIDAD ────────────────────────────────────────────────────────────────────────
# Si el rebuild FALLA (y fallará en alguna: hay recetas que sólo construyen en ciertas máquinas), el
# artefacto original se RESTAURA. Un verificador que destruye lo que verifica es peor que no tenerlo.
# Por eso se aparta a un directorio temporal en vez de borrar, y hay `trap` para restaurar todo lo
# pendiente si se interrumpe.
#
# Uso: scripts/verificar-repro.sh [receta.toml ...] (sin args: muestra por clase)
# Env: STORE (def ./store) N (def 30, tamaño de la muestra)
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
STORE="${STORE:-./store}"; HAMMER="${HAMMER:-./target/release/hammer}"; N="${N:-30}"
GUARDA="$(mktemp -d)"; PENDIENTE=""
restaurar_todo() {
for h in $PENDIENTE; do
[ -d "$GUARDA/$h" ] && [ ! -d "$STORE/$h" ] && mv "$GUARDA/$h" "$STORE/$h" && echo " ↩ restaurado $h"
done
rm -rf "$GUARDA"
}
trap restaurar_todo EXIT INT TERM
if [ $# -gt 0 ]; then RECETAS="$*"
else
# Muestra por CLASE de build: si sólo se verifican paquetes C con meson, el resultado no dice nada
# de Rust ni de Go, que son justamente donde el no-determinismo suele vivir (codegen paralelo,
# orden de símbolos, timestamps de módulos).
RECETAS=$(ls recipes/*.toml recipes/incoming-wlr/*.toml 2>/dev/null | shuf -n "$N" --random-source=/dev/zero | tr '\n' ' ')
fi
ok=0; dif=0; fallo=0; saltada=0; DIVERGEN=""
for f in $RECETAS; do
[ -f "$f" ] || continue
n=$(awk -F'"' '/^name[[:space:]]*=/{print $2; exit}' "$f"); [ -n "$n" ] || n=$(basename "$f" .toml)
H=$("$HAMMER" --store "$STORE" hash "$f" 2>/dev/null | tail -1 | sed 's/^b3://')
[ -n "$H" ] || { saltada=$((saltada+1)); continue; }
D="$H-$n"
[ -d "$STORE/$D" ] || { saltada=$((saltada+1)); continue; }
mv "$STORE/$D" "$GUARDA/$D"; PENDIENTE="$PENDIENTE $D"
if "$HAMMER" --store "$STORE" build "$f" >/dev/null 2>&1 && [ -d "$STORE/$D" ]; then
if "$HAMMER" why-differs "$GUARDA/$D" "$STORE/$D" >/dev/null 2>&1; then
printf " ✓ %-26s REPRODUCE\n" "$n"; ok=$((ok+1))
rm -rf "$GUARDA/$D"
else
printf " ✗ %-26s DIVERGE\n" "$n"; dif=$((dif+1)); DIVERGEN="$DIVERGEN $n"
"$HAMMER" why-differs "$GUARDA/$D" "$STORE/$D" 2>&1 | tail -4 | sed 's/^/ /'
rm -rf "$GUARDA/$D"
fi
else
# El rebuild falló: restaurar y NO contarlo como divergencia (no es lo mismo «no reproduce» que
# «no construye acá»). Distinguirlos importa: mezclarlos inventaría un problema de determinismo.
printf " ⚠ %-26s no construyó (artefacto restaurado)\n" "$n"; fallo=$((fallo+1))
rm -rf "$STORE/$D"; mv "$GUARDA/$D" "$STORE/$D"
fi
PENDIENTE=$(echo "$PENDIENTE" | sed "s| $D||")
done
echo "════════════════════════════════════════════════════════════"
echo " REPRODUCEN: $ok · DIVERGEN: $dif · no construyeron: $fallo · sin artefacto: $saltada"
[ "$dif" -gt 0 ] && echo " ⚠ divergen:$DIVERGEN"
[ "$dif" -eq 0 ] && [ "$ok" -gt 0 ] && echo " ✅ PUERTA SUPERADA: 0 divergencias sobre $ok verificadas"
exit $(( dif > 0 ? 1 : 0 ))