`no construyó` no es ruido del verificador: es EL hallazgo. Un `sealed` en el grafo dice que alguien construyó eso alguna vez con algún lab, no que se construya hoy — y el lab rueda desde Alpine edge. Este script es lo único que convierte esa sospecha en un número SIN RIESGO, porque aparta en vez de borrar y restaura si el build falla. Barriendo las 15 recetas CMake baratas del corpus: REPRODUCEN 9 · DERIVA 1 · no construyeron 5. Cinco selladas y rotas a la vez, todas por el mismo crash del lld de zig con `--dependency-file`. Y la nota de método que hace legible el censo: barrer por FAMILIA de sistema de build. Si el fallo es del toolchain se concentra en una familia y el patrón salta; barrer al azar lo diluye.
348 lines
22 KiB
Bash
Executable File
348 lines
22 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
|
|
# `takana why-differs` compara los dos árboles y nombra la causa si divergen.
|
|
#
|
|
# ── ADEMÁS DE MEDIR REPRODUCIBILIDAD, ESTE SCRIPT ES EL CENSO DE LA DEUDA INVISIBLE ────────────
|
|
# «no construyó» no es ruido del verificador: es EL hallazgo. Un artefacto sellado dice que ALGUIEN
|
|
# lo construyó alguna vez con ALGÚN lab, no que se construya HOY — y el lab rueda (Alpine edge). La
|
|
# única forma de convertir «N recetas podrían estar rotas» en un número es reconstruirlas, y este
|
|
# script es lo único que lo hace SIN RIESGO, porque aparta en vez de borrar y restaura si el build
|
|
# falla (ver la red de seguridad, abajo).
|
|
#
|
|
# Medido el 2026-09-12 barriendo las 15 recetas CMake baratas del corpus:
|
|
#
|
|
# REPRODUCEN: 9 · DERIVA: 1 · NO-DETERMINISMO: 0 · no construyeron: 5
|
|
#
|
|
# Cinco selladas y rotas a la vez (`brotli`, `json-c`, `libjpeg-turbo`, `libjpeg-turbo-shared`,
|
|
# `libtiff-shared`), todas por el mismo crash del `lld` de zig con el `--dependency-file` que CMake
|
|
# ≥3.27 emite. Ninguna métrica del repo podía verlo: el grafo las contaba `sealed`.
|
|
#
|
|
# ⇒ **Barrer por FAMILIA de sistema de build es lo que hace el censo legible**: si el fallo es del
|
|
# toolchain, se concentra en una familia y el patrón salta a la vista. Barrer al azar lo diluye.
|
|
#
|
|
# ── 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="${TAKANA:-${HAMMER:-./target/release/takana}}"; N="${N:-30}"
|
|
|
|
# ⚠ EL MIRROR SE ACTIVA ACÁ, Y NO ES COMODIDAD: sin él, toda receta cuya fuente viva SÓLO en nuestro
|
|
# mirror —las que llevan una URL `.invalid` a propósito (ADR 0013): `firefox-pgo-profile`, los dos
|
|
# modelos de IA— no se puede reconstruir, y este guardián lo informa como «no construyó». Medido el
|
|
# 2026-09-12: `ia-modelo-embeddings` dio REPRODUCE y `ia-modelo-chat` «no construyó», y la única
|
|
# diferencia entre las dos era que el tar de la primera **seguía en el caché local**. O sea que el
|
|
# veredicto dependía de la suerte del caché, que es exactamente lo que un gate no puede permitirse.
|
|
# Es aditivo (ADR 0013): si el fichero no está, todo sigue como antes.
|
|
if [ -f scripts/fuentes/mirror-env.sh ]; then
|
|
# shellcheck disable=SC1091
|
|
. scripts/fuentes/mirror-env.sh >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# ── EL LOCK LO TOMA EL SCRIPT, Y ANTES DE TOCAR NADA ───────────────────────────────────────────
|
|
# Este script llama a `takana 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 takana 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: `takana 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 ))
|