Una tanda volvió a llenar el libro de veredictos falsos: 47 recetas sanas anotadas como
NO-DETERMINISMO. Esta vez la cadena empezó antes de donde yo había mirado.
El `mv` que APARTA el artefacto falló (el directorio de apartado no estaba) y yo nunca comprobaba que
hubiera funcionado. A partir de ahí todo lo que sigue es basura: el artefacto se queda en su sitio,
`hammer why-differs` compara contra una ruta que no existe y devuelve `Error: store: no existe…`, y
mi código leía cualquier salida no-cero como «difieren». Un error de comparación disfrazado de
veredicto, 47 veces.
Es la MISMA lección que arreglé hace un rato para el build, en un segundo sitio que no miré:
**no poder medir no es un resultado negativo**. Dos guardas nuevas:
· el apartado se comprueba (`mv || SIN VEREDICTO`) y la receta se salta ruidosamente;
· `why-differs` se lee, no sólo su exit code: si dice `Error:`/`no existe`, es SIN VEREDICTO, se
restaura el artefacto y NO se anota nada.
Probadas las dos: con el apartadero sin permiso de escritura sale «no pude apartar el artefacto ⇒
SIN VEREDICTO», el artefacto queda intacto y el libro no crece.
Comprobado además que la tanda mala no perdió NADA: como el `mv` fallaba, los 47 artefactos nunca
salieron del store (`faltan: 0`). El daño fue sólo el registro, y está purgado — el libro queda con
51 verificaciones buenas.
La regla, ya por triplicado hoy: cuando un guardián no puede establecer algo, el estado es *sin
veredicto*, y eso no se escribe. Un libro que anota lo que no midió se cree igual que uno que sí.
318 lines
20 KiB
Bash
Executable File
318 lines
20 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.
|
|
# ⇒ 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
|
|
# 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 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}"
|
|
|
|
# ── 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
|
|
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
|
|
|
|
# ── EL LIBRO DE LO YA VERIFICADO ───────────────────────────────────────────────────────────────
|
|
# Sin registro, cada corrida es una muestra aleatoria que se olvida: nadie puede decir qué fracción
|
|
# del corpus se verificó nunca, y las mismas recetas salen sorteadas una y otra vez mientras otras
|
|
# no se miran jamás. La clave es (receta, ArtifactHash) — el hash de HOY. Si la receta o cualquiera
|
|
# de sus deps cambia, el hash cambia, la entrada deja de casar y hay que volver a verificar. Un
|
|
# «ya lo verifiqué» que sobrevive a un re-hash sería una mentira; éste no puede.
|
|
LIBRO="${LIBRO:-docs/state/repro-verificado.tsv}"
|
|
[ -s "$LIBRO" ] || printf '%s\n' \
|
|
'# repro-verificado.tsv — qué artefactos se comprobaron que REPRODUCEN bit a bit, y cuándo.' \
|
|
'#' \
|
|
'# Clave = (receta, ArtifactHash). El hash resume la receta MÁS su cierre de deps, así que una' \
|
|
'# entrada deja de casar en cuanto algo aguas arriba cambia: no se puede dar por verificado lo' \
|
|
'# que ya no es lo mismo. Lo escribe scripts/verificar-repro.sh; se lee para medir cobertura y' \
|
|
'# para no volver a sortear lo ya hecho.' \
|
|
'#' \
|
|
'# receta<TAB>hash<TAB>fecha<TAB>veredicto' > "$LIBRO"
|
|
|
|
# ── PODAR EL ÁRBOL DE FUENTES TRAS CADA VEREDICTO SANO ─────────────────────────────────────────
|
|
# Un barrido deja un árbol extraído por receta en `work/sources` y no limpia hasta el final: en el
|
|
# hub eso llevó el disco del 95% al 98% dos veces el 2026-09-05, y un disco lleno se lee como receta
|
|
# rota ([[disco-lleno-se-lee-como-receta-rota]]). Guardarlos no vale nada para el barrido, porque
|
|
# `fetch.rs` borra y re-extrae el árbol en CADA build por diseño.
|
|
#
|
|
# Pero sólo se poda cuando el veredicto es SANO. Si algo falló o divergió, el árbol es justamente lo
|
|
# que hace falta para mirarlo: un verificador que limpia la escena del problema que acaba de
|
|
# encontrar sirve para poco.
|
|
#
|
|
# ⚠ Sólo el árbol de ESTA receta. Las deps que se hayan extraído se quedan: son compartidas, y
|
|
# borrarlas mientras otra cosa las usa es el ADR 0012.
|
|
podar_fuentes() {
|
|
[ -n "${1:-}" ] || return 0
|
|
rm -rf "$ROOT/work/sources/$1"-* 2>/dev/null || true
|
|
}
|
|
|
|
ya_verificado() { grep -qF "$(printf '%s\t%s\t' "$1" "$2")" "$LIBRO" 2>/dev/null; }
|
|
anotar() { printf '%s\t%s\t%s\t%s\n' "$1" "$2" "$(date -u +%F)" "$3" >> "$LIBRO"; }
|
|
|
|
# `--coverage`: qué fracción del corpus PRESENTE en este store se comprobó, al hash de hoy. Es la
|
|
# pregunta que el libro existe para poder contestar — antes no había forma de saberlo.
|
|
if [ "${1:-}" = "--coverage" ]; then
|
|
tot=0; hechas=0; sin_art=0
|
|
for f in recipes/*.toml recipes/incoming-*/*.toml; do
|
|
[ -f "$f" ] || continue
|
|
nn=$(awk -F'"' '/^name[[:space:]]*=/{print $2; exit}' "$f"); [ -n "$nn" ] || nn=$(basename "$f" .toml)
|
|
hh=$("$HAMMER" --store "$STORE" hash "$f" 2>/dev/null | tail -1 | sed 's/^b3://')
|
|
[ -n "$hh" ] || continue
|
|
[ -d "$STORE/$hh-$nn" ] || { sin_art=$((sin_art+1)); continue; }
|
|
tot=$((tot+1)); ya_verificado "$nn" "$hh" && hechas=$((hechas+1))
|
|
done
|
|
echo "── cobertura de reproducibilidad (contra el hash de HOY)"
|
|
echo " verificadas $hechas de $tot artefactos presentes en $STORE"
|
|
echo " sin artefacto $sin_art (selladas en otro lado, o en deuda: acá no hay qué comparar)"
|
|
exit 0
|
|
fi
|
|
|
|
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).
|
|
# ── `MAX_SECONDS`: sortear sólo entre builds BARATOS ─────────────────────────────────────────
|
|
# Elegir muestra «por artefacto chico» es una trampa: el artefacto de `nodejs` pesa 0,2 MB y su
|
|
# build son horas de V8. Lo que hace cara una verificación es el TIEMPO DE CONSTRUIR, y ese dato
|
|
# ya existe — `scripts/farm/build-timed.sh` deja un sidecar por build en `store/.times/` y llega
|
|
# con la cosecha. Con `MAX_SECONDS=60` la muestra sale de ahí.
|
|
#
|
|
# Las recetas SIN medición quedan fuera cuando se pone el tope: no se puede prometer que algo es
|
|
# barato sin haberlo medido nunca. El modo de ampliar esa lista no es relajar el filtro sino
|
|
# correr builds instrumentados en el worker, que es donde el tiempo se mide.
|
|
#
|
|
# ⚠ DOS LÍMITES DEL NÚMERO, medidos el 2026-09-05 con `MAX_SECONDS=45` en el hub:
|
|
# · El sidecar lo escribe el WORKER. Una receta Rust que allá tarda 40 s con la caché de cargo
|
|
# caliente puede tardar muchos minutos acá en frío — `angle-grinder` y `amp` entraron por el
|
|
# filtro y se comieron el disco. El tope acota el build DE ESA RECETA en LA MÁQUINA QUE LO
|
|
# MIDIÓ, no acá.
|
|
# · Y no cuenta la CASCADA: `hammer build` reconstruye las deps que falten en el store local, y
|
|
# eso no está en el tiempo del sidecar. Es la misma sorpresa que dio `gjs`, cuyo grafo decía
|
|
# «una receta» y se puso a rehacer spidermonkey entero.
|
|
# ⇒ con disco justo, mejor pasar las recetas a mano que confiar en el tope.
|
|
if [ -n "${MAX_SECONDS:-}" ]; then
|
|
TODOS=$(python3 - "$MAX_SECONDS" <<'PY'
|
|
import glob, json, os, sys, re
|
|
from statistics import median
|
|
cap = float(sys.argv[1])
|
|
m = {}
|
|
for f in glob.glob("store/.times/*.json"):
|
|
try: d = json.load(open(f))
|
|
except Exception: continue
|
|
if d.get("name") and isinstance(d.get("seconds"), (int, float)):
|
|
m.setdefault(d["name"], []).append(d["seconds"])
|
|
baratas = {k for k, v in m.items() if median(v) < cap}
|
|
for ruta in glob.glob("recipes/*.toml") + glob.glob("recipes/incoming-wlr/*.toml"):
|
|
txt = open(ruta, encoding="utf8", errors="replace").read(4000)
|
|
mm = re.search(r'^name\s*=\s*"([^"]+)"', txt, re.M)
|
|
if mm and mm.group(1) in baratas:
|
|
print(ruta)
|
|
PY
|
|
)
|
|
[ -n "$TODOS" ] || { echo "── no hay recetas medidas por debajo de ${MAX_SECONDS}s (¿falta store/.times?)"; exit 0; }
|
|
fi
|
|
|
|
# Se sortea entre lo que NO está en el libro, para que corridas sucesivas ACUMULEN cobertura en
|
|
# vez de repetir las mismas. Con `TODO=1` se sortea entre todas (para re-verificar a propósito).
|
|
TODOS=$(ls recipes/*.toml recipes/incoming-wlr/*.toml 2>/dev/null)
|
|
if [ -z "${TODO:-}" ]; then
|
|
PENDIENTES=""
|
|
for f in $TODOS; do
|
|
nn=$(awk -F'"' '/^name[[:space:]]*=/{print $2; exit}' "$f"); [ -n "$nn" ] || nn=$(basename "$f" .toml)
|
|
hh=$("$HAMMER" --store "$STORE" hash "$f" 2>/dev/null | tail -1 | sed 's/^b3://')
|
|
[ -n "$hh" ] || continue
|
|
ya_verificado "$nn" "$hh" || PENDIENTES="$PENDIENTES $f"
|
|
done
|
|
[ -n "$PENDIENTES" ] && TODOS="$PENDIENTES"
|
|
fi
|
|
RECETAS=$(printf '%s\n' $TODOS | shuf -n "$N" --random-source=/dev/zero | tr '\n' ' ')
|
|
fi
|
|
|
|
|
|
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)
|
|
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; }
|
|
|
|
# ⚠ Si el apartado FALLA, todo lo que sigue es basura: el `why-differs` de después compara contra
|
|
# algo que no existe, devuelve error, y un error leído como «difieren» inventa un no-determinismo.
|
|
# Pasó el 2026-09-05 con 47 recetas sanas de una sola tanda. Falla ruidoso y se salta la receta.
|
|
mkdir -p "$GUARDA"
|
|
if ! mv "$STORE/$D" "$GUARDA/$D" 2>/dev/null; then
|
|
printf " ⚠ %-26s no pude apartar el artefacto ⇒ SIN VEREDICTO\n" "$n"
|
|
fallo=$((fallo+1)); continue
|
|
fi
|
|
PENDIENTE="$PENDIENTE $D"
|
|
if "$HAMMER" --store "$STORE" build "$f" >/dev/null 2>&1 && [ -d "$STORE/$D" ]; then
|
|
# `why-differs` devuelve no-cero tanto si DIFIEREN como si NO PUDO comparar (una ruta que no
|
|
# existe, por ejemplo). Tratar las dos igual es inventar un veredicto: se mira su salida.
|
|
cmp_out=$("$HAMMER" why-differs "$GUARDA/$D" "$STORE/$D" 2>&1); cmp_rc=$?
|
|
if [ "$cmp_rc" -ne 0 ] && printf '%s' "$cmp_out" | grep -qiE '^Error:|no existe'; then
|
|
printf " ⚠ %-26s no pude comparar (%s) ⇒ SIN VEREDICTO\n" "$n" \
|
|
"$(printf '%s' "$cmp_out" | head -1 | cut -c1-60)"
|
|
fallo=$((fallo+1))
|
|
rm -rf "$STORE/$D"; mv "$GUARDA/$D" "$STORE/$D"
|
|
PENDIENTE=$(echo "$PENDIENTE" | sed "s| $D||")
|
|
continue
|
|
fi
|
|
if [ "$cmp_rc" -eq 0 ]; then
|
|
printf " ✓ %-26s REPRODUCE\n" "$n"; ok=$((ok+1))
|
|
anotar "$n" "$H" "reproduce"
|
|
rm -rf "$GUARDA/$D"; podar_fuentes "$n"
|
|
else
|
|
# ── 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"
|
|
# ⚠ «la segunda reconstrucción FALLÓ» NO es «difiere». Meterlas en la misma rama fue un error
|
|
# mío del 2026-09-05 y lo pagó el libro: se llenó el disco a mitad de una tanda, los rebuilds
|
|
# empezaron a morir por ENOSPC, y quedaron anotadas 21 recetas sanas como NO-DETERMINISMO —
|
|
# un registro falso y persistente, que es peor que no tener registro. Es la misma trampa de
|
|
# siempre: un disco lleno se lee como receta rota, y la pista es que mueren varias seguidas.
|
|
# El script original ya distinguía «no reproduce» de «no construye acá»; esto lo restituye.
|
|
if ! "$HAMMER" --store "$STORE" build "$f" >/dev/null 2>&1 || [ ! -d "$STORE/$D" ]; then
|
|
printf " ⚠ %-26s la 2ª reconstrucción no construyó ⇒ SIN VEREDICTO (no lo anoto)\n" "$n"
|
|
fallo=$((fallo+1))
|
|
rm -rf "$STORE/$D"; mv "$GUARDA/$D" "$STORE/$D"; rm -rf "$GUARDA/$D.r1"
|
|
PENDIENTE=$(echo "$PENDIENTE" | sed "s| $D.r1||;s| $D||")
|
|
continue
|
|
fi
|
|
if "$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))
|
|
# Se anota igual: las dos reconstrucciones coincidieron entre sí, que es justo lo que
|
|
# significa «reproduce». Lo viejo era el ejemplar guardado, no el determinismo.
|
|
anotar "$n" "$H" "reproduce (tras poner al día un artefacto derivado)"
|
|
podar_fuentes "$n"
|
|
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"
|
|
anotar "$n" "$H" "NO-DETERMINISMO"
|
|
"$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
|
|
# «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 · 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 + deriva)) -gt 0 ] && echo " ✅ PUERTA SUPERADA: 0 no-determinismos sobre $((ok + deriva)) verificadas"
|
|
exit $(( dif > 0 ? 1 : 0 ))
|