verificar-repro: el apartadero iba a tmpfs, no tomaba el lock, y dejaba el veredicto a medias

Tres defectos, encontrados corriéndolo y pagando uno de ellos: perdí el artefacto de `aichat` al
interrumpir una corrida. Está reconstruyéndose en el worker.

**1. El apartadero estaba en `/tmp`.** Para verificar hay que sacar el artefacto de su sitio y dejar
que hammer lo reconstruya; eso iba a un `mktemp -d`. La cabecera prometía «un verificador que
destruye lo que verifica es peor que no tenerlo», y esa ubicación rompía la promesa en tres sitios a
la vez: `/tmp` es **tmpfs**, así que apartar copiaba el artefacto a RAM en una caja de 7,6 GiB sin
swap; al ser otro sistema de ficheros el `mv` no era un rename atómico sino copiar-y-borrar, o sea
que una interrupción a mitad lo dejaba en ninguna parte; y lo que sobreviviera moría al reiniciar.
Ahora va a `store/.verificar-repro`: el `mv` es instantáneo, no cuesta RAM ni disco, y lo apartado
SOBREVIVE a un kill — la corrida siguiente lo repone sola antes de empezar.

**2. Llamaba a `hammer build` sin tomar el `flock`.** Es la regla 1 del repo, y saltársela arriesga
el árbol compartido de `work/sources` de forma irreversible (ADR 0012). Lo toma el script y no quien
lo llama, por lo mismo que `poda-fuentes.sh`: así no hay forma de correrlo mal — y correrlo mal fue
exactamente lo que hice. Con `-E 3`, porque sin eso «no conseguí el lock» sale como exit 1, que es lo
que este script usa para «hay divergencias»: informar «el corpus no reproduce» cuando lo que pasaba
era que había otro build corriendo es peor que no correr.

**3. Confundía DERIVA con NO-DETERMINISMO y lo delegaba en la memoria de quien leía.** Que el
guardado difiera de una reconstrucción de hoy puede ser que el lab se movió (deriva, sano) o que las
mismas entradas dan salidas distintas (no-determinismo, bug). La cabecera decía «corré el script dos
veces y mirá la segunda» — con lo que la primera corrida informaba «DIVERGE» sobre cosas sanas y
entrenaba a no creerle. Ahora, ante una diferencia, reconstruye una segunda vez y compara las dos
reconstrucciones ENTRE SÍ: si coinciden es DERIVA, si no, NO-DETERMINISMO. El rebuild extra sólo se
paga cuando ya hubo una diferencia. Y cuando es no-determinismo de verdad, los dos ejemplares se
conservan en `store/.divergen/` en vez de borrarse: antes el guardián tiraba la única evidencia justo
en el caso que existe para encontrar.

Medido de paso, y es la parte buena: `libassuan`, `libksba`, `libffi` y `packaging` —artefactos del
21 de agosto— reproducen BIT A BIT con el lab de hoy. `scdoc` derivaba y ya está al día; `gron`,
`age` y `anew` reproducen. La rama de DERIVA se probó ensuciando a propósito una copia guardada:
clasifica bien y el store queda con la reconstrucción limpia.
This commit is contained in:
Sergio
2026-09-05 19:03:48 +00:00
parent 5420d2db0d
commit d5863ae554
+95 -13
View File
@@ -15,9 +15,14 @@
# · 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.
# ⇒ Esto YA NO lo tiene que hacer quien lee: ante una diferencia el script reconstruye una SEGUNDA
# vez y compara las dos reconstrucciones ENTRE SÍ. Si coinciden, el que estaba fuera de fecha era
# el guardado ⇒ `DERIVA`, que no es un fallo. Si no coinciden ⇒ `NO-DETERMINISMO`, y ahí sí. Antes
# la cabecera decía «corré el script dos veces y mirá la segunda», y eso dejaba el veredicto a
# cargo de que alguien se acordara: la primera corrida informaba «DIVERGE» sobre cosas sanas y
# entrenaba a no creerle. El rebuild extra sólo se paga cuando ya hubo una diferencia.
# (Medido antes de automatizarlo: anew, gron, age y scdoc divergían en la primera 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
@@ -27,24 +32,74 @@
# ── 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.
# Por eso se APARTA en vez de borrar, y hay `trap` para reponer lo pendiente si se interrumpe.
#
# ⚠ El apartadero vive DENTRO del store (`store/.verificar-repro`), no en `/tmp`. Estaba en `/tmp` y
# eso rompía la promesa de arriba en tres sitios a la vez: `/tmp` es tmpfs, así que apartar copiaba
# el artefacto a RAM en una caja de 7,6 GiB sin swap; al ser otro sistema de ficheros el `mv` no era
# un rename atómico sino copiar-y-borrar, así que una interrupción a mitad lo dejaba en ninguna
# parte; y lo que sobreviviera se perdía al reiniciar. Costó un artefacto el 2026-09-05. Dentro del
# store el `mv` es instantáneo y lo apartado SOBREVIVE a un kill: la corrida siguiente lo repone sola
# antes de empezar. Y cuando hay NO-DETERMINISMO de verdad, los dos ejemplares se conservan en
# `store/.divergen/` — antes se borraban, o sea que el guardián tiraba la única evidencia justo en el
# caso que existe para encontrar.
#
# 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=""
# ── EL LOCK LO TOMA EL SCRIPT, Y ANTES DE TOCAR NADA ───────────────────────────────────────────
# Este script llama a `hammer build`, y la regla 1 del repo es que todo build va envuelto en el
# `flock` de la granja: dos builds que compartan una dep se pisan el árbol de `work/sources` y lo
# rompen PARA SIEMPRE (ADR 0012). Lo toma el script y no quien lo llama, por lo mismo que en
# `poda-fuentes.sh`: así no hay forma de correrlo mal — y correrlo mal fue exactamente lo que pasó.
#
# `-E 3` importa: sin él, «no conseguí el lock» sale como exit 1, que es lo que este script usa para
# «hay divergencias». Un guardián que informa «el corpus no reproduce» cuando lo que pasó es que
# había otro build corriendo es peor que uno que no corre.
if [ -z "${VERIFICAR_REPRO_CON_LOCK:-}" ]; then
export VERIFICAR_REPRO_CON_LOCK=1
flock -w 120 -E 3 "$ROOT/work/.farm-build.lock" "$0" "$@"
rc=$?
[ "$rc" -eq 3 ] && echo "── verificar-repro: build en vuelo (lock no tomado en 120s) ⇒ no toco nada"
exit "$rc"
fi
# ── ⚠ EL APARTADERO VA DENTRO DEL STORE, NO EN /tmp ────────────────────────────────────────────
# Para verificar hay que sacar el artefacto de su sitio y dejar que hammer lo reconstruya. Ese
# `mv` era a un `mktemp -d`, o sea a `/tmp`, y ahí se juntan tres cosas que costaron un artefacto
# el 2026-09-05:
#
# 1. `/tmp` es **tmpfs** en gioser: mover un artefacto ahí lo copia a RAM, en una máquina de
# 7,6 GiB y swap 0, compitiendo con el build que se está verificando.
# 2. Al ser otro sistema de ficheros, `mv` NO es un rename atómico sino copiar-y-borrar. Si el
# proceso muere a mitad, el artefacto no está ni en un sitio ni en el otro.
# 3. Y lo que quede ahí se pierde al reiniciar.
#
# Dentro del store el `mv` es un rename instantáneo, no cuesta RAM ni disco, y **sobrevive a un
# kill**: lo que quedó apartado sigue en disco y la próxima corrida lo repone sola. El apartadero
# empieza por punto, como `.dmerge`, y los consumidores del store filtran por `^[0-9a-f]{64}-`.
GUARDA="$STORE/.verificar-repro"; mkdir -p "$GUARDA"; 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"
rmdir "$GUARDA" 2>/dev/null || true
}
trap restaurar_todo EXIT INT TERM
# Repone lo que dejó apartado una corrida ANTERIOR que no llegó a su trap. Sin esto el apartadero
# es un agujero silencioso: el artefacto existe, no está donde se lo busca, y el grafo lo llama
# `debt` sin que nadie sepa por qué.
for d in "$GUARDA"/*; do
b=$(basename "$d"); [ -d "$d" ] || continue
case "$b" in '*') continue ;; esac
if [ -d "$STORE/$b" ]; then rm -rf "$d"; else mv "$d" "$STORE/$b"; echo " ↩ repuesto de una corrida anterior: $b"; fi
done
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
@@ -53,7 +108,7 @@ else
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=""
ok=0; dif=0; fallo=0; saltada=0; deriva=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)
@@ -68,9 +123,35 @@ for f in $RECETAS; do
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"
# ── DERIVA vs NO-DETERMINISMO: SE DECIDE ACÁ, NO EN LA CABEZA DE QUIEN LEE ──────────────
# Que el guardado difiera de una reconstrucción de hoy puede ser dos cosas MUY distintas:
# DERIVA (el artefacto es viejo y el lab se movió) o NO-DETERMINISMO (mismas entradas, mismo
# lab, salidas distintas). La cabecera decía «corré el script dos veces y mirá la segunda»,
# y eso deja el veredicto a cargo de que alguien se acuerde — con lo que la primera corrida
# informa «DIVERGE» sobre cosas sanas y entrena a no creerle.
#
# Una segunda reconstrucción lo resuelve sin ambigüedad: si las DOS coinciden entre sí, el
# que estaba fuera de fecha era el guardado ⇒ deriva. Si no coinciden, es no-determinismo de
# verdad. Sólo se paga el rebuild extra cuando ya hubo una diferencia.
mv "$STORE/$D" "$GUARDA/$D.r1"; PENDIENTE="$PENDIENTE $D.r1"
if "$HAMMER" --store "$STORE" build "$f" >/dev/null 2>&1 && [ -d "$STORE/$D" ] \
&& "$HAMMER" why-differs "$GUARDA/$D.r1" "$STORE/$D" >/dev/null 2>&1; then
printf " ~ %-26s DERIVA (el guardado era viejo; dos reconstrucciones coinciden)\n" "$n"
deriva=$((deriva+1))
rm -rf "$GUARDA/$D" "$GUARDA/$D.r1"
else
printf " ✗ %-26s NO-DETERMINISMO (dos reconstrucciones difieren entre sí)\n" "$n"
dif=$((dif+1)); DIVERGEN="$DIVERGEN $n"
"$HAMMER" why-differs "$GUARDA/$D.r1" "$STORE/$D" 2>&1 | tail -4 | sed 's/^/ /'
# ⚠ NO se borran los ejemplares. Borrarlos destruía la evidencia justo en el único caso que
# este guardián existe para encontrar: decía «no reproduce» y tiraba las copias con las que
# se podía averiguar POR QUÉ. Ocupan disco a propósito.
mkdir -p "$STORE/.divergen"; rm -rf "$STORE/.divergen/$D" "$STORE/.divergen/$D.r1"
mv "$GUARDA/$D" "$STORE/.divergen/$D" 2>/dev/null
mv "$GUARDA/$D.r1" "$STORE/.divergen/$D.r1" 2>/dev/null
printf " ejemplares guardados en %s/.divergen/ para poder mirarlos\n" "$STORE"
fi
PENDIENTE=$(echo "$PENDIENTE" | sed "s| $D.r1||")
fi
else
# El rebuild falló: restaurar y NO contarlo como divergencia (no es lo mismo «no reproduce» que
@@ -82,7 +163,8 @@ for f in $RECETAS; do
done
echo "════════════════════════════════════════════════════════════"
echo " REPRODUCEN: $ok · DIVERGEN: $dif · no construyeron: $fallo · sin artefacto: $saltada"
echo " REPRODUCEN: $ok · DERIVA: $deriva · NO-DETERMINISMO: $dif · no construyeron: $fallo · sin artefacto: $saltada"
[ "$deriva" -gt 0 ] && echo " ~ la deriva NO es un fallo: el artefacto guardado quedó viejo y ya se puso al día"
[ "$dif" -gt 0 ] && echo " ⚠ divergen:$DIVERGEN"
[ "$dif" -eq 0 ] && [ "$ok" -gt 0 ] && echo " ✅ PUERTA SUPERADA: 0 divergencias sobre $ok verificadas"
[ "$dif" -eq 0 ] && [ $((ok + deriva)) -gt 0 ] && echo " ✅ PUERTA SUPERADA: 0 no-determinismos sobre $((ok + deriva)) verificadas"
exit $(( dif > 0 ? 1 : 0 ))