From a8bb83add5fe005262e255297b3b261a70075ad9 Mon Sep 17 00:00:00 2001 From: sergio Date: Tue, 21 Jul 2026 22:42:32 -0400 Subject: [PATCH] =?UTF-8?q?why-differs-barrido:=20no=20llamar=20"no-reprod?= =?UTF-8?q?ucci=C3=B3n"=20a=20lo=20que=20no=20est=C3=A1=20probado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El artefacto guarda su recipe.toml pero NO los hashes de sus deps, y el ArtifactHash sí los incluye ⇒ dos sellados con el mismo recipe.toml pueden diferir LEGÍTIMAMENTE porque cambió una dependencia. La primera versión gritaba "NO-REPRODUCCIÓN REAL" sobre 128 paquetes; eso era un superconjunto, no una prueba. Ahora separa por la CAUSA, que sí discrimina: si TODO lo que difiere son metadatos que un cambio de dep no explica (MTIME de gzip, cabecera ar, ruta de build, secciones ELF informativas), es no-determinismo probable; si difiere .text/.data o hay ficheros de más, es indistinguible de un cambio de dep sin reconstruir. 128 → 92 probables + 36 no concluyentes. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/why-differs-barrido.sh | 58 +++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/scripts/why-differs-barrido.sh b/scripts/why-differs-barrido.sh index 72182151..4f5d96ea 100755 --- a/scripts/why-differs-barrido.sh +++ b/scripts/why-differs-barrido.sh @@ -6,9 +6,22 @@ Comparar dos sellados del mismo nombre responde una pregunta que hasta ahora nad **¿por qué difieren?** Y sobre todo separa los dos casos que se confunden a ojo: · `.hammer/recipe.toml` DIFIERE ⇒ son recetas distintas. Que el artefacto cambie es lo esperado. - · `.hammer/recipe.toml` IDÉNTICO ⇒ **misma receta, distinto artefacto = NO-REPRODUCCIÓN REAL.** - Eso es un bug de reproducibilidad, y el informe ya dice la causa (gzip mtime, cabecera ar, - sección ELF, ruta de build embebida…). + · `.hammer/recipe.toml` IDÉNTICO ⇒ misma receta, distinto artefacto: **sospechoso**. + +CUIDADO CON EL SEGUNDO CASO (y por qué este script no grita "no-reproducción"): el artefacto sólo +guarda su `recipe.toml`, NO los hashes de sus dependencias — pero el ArtifactHash SÍ los incluye. +Dos sellados con el mismo `recipe.toml` pueden diferir **legítimamente** porque una dep cambió +(glib nuevo ⇒ gtk4 se re-sella con contenido distinto). "Mismo recipe.toml, artefacto distinto" es +un SUPERCONJUNTO de las no-reproducciones, no una prueba. + +Lo que sí se puede afirmar mirando la CAUSA, y por eso el informe separa dos grupos: + + · **no-determinismo probable** — todas las divergencias son metadatos que no dependen de las + deps: MTIME de gzip, cabecera `ar`, ruta de build embebida, o secciones ELF puramente + informativas (`.debug_*`, `.comment`, `.note.gnu.build-id`, tablas de símbolos). Una dep + distinta no explica que cambie SÓLO eso. + · **puede ser una dep** — difiere `.text`/`.data`/`.rodata` o hay ficheros presentes/ausentes. + Indistinguible de "la dependencia cambió" sin reconstruir. Reconstruir es la única prueba. Es el "multiplicador de granja" del SDD 17 §1.2: debuggear una no-reproducción deja de ser artesanal. @@ -44,6 +57,21 @@ def sellados_por_nombre(): return {n: sorted(v, key=lambda p: p.stat().st_mtime) for n, v in m.items() if len(v) >= 2} +# Secciones ELF que son metadatos: su contenido no cambia porque una dependencia cambie. +SECCIONES_META = (".debug", ".zdebug", ".comment", ".note", ".symtab", ".strtab", ".shstrtab") +# Causas que, por sí solas, no las explica un cambio de dependencia. +CAUSAS_META = {"gzip_mtime", "ar_cabecera", "ruta_de_build"} + + +def solo_metadatos(d): + """¿Esta divergencia es explicable SÓLO por no-determinismo del build?""" + if d["causa"] in CAUSAS_META: + return True + if d["causa"] != "elf_secciones": + return False + return all(s.split(" ")[0].startswith(SECCIONES_META) for s in d["secciones"]) + + def main(): if not HAMMER.exists(): sys.exit(f"falta {HAMMER} (cargo build --release --bin hammer)") @@ -55,7 +83,8 @@ def main(): print(f"== barrido why-differs: {len(nombres)} paquetes con ≥2 sellados\n") causas = collections.Counter() - no_reproducen = [] # misma receta, distinto artefacto ⇒ bug real + no_determinismo = [] # misma receta y sólo metadatos ⇒ el build no es determinista + quiza_dep = [] # misma receta pero difiere código ⇒ pudo cambiar una dependencia recetas_distintas = 0 errores = [] @@ -79,13 +108,16 @@ def main(): causas[d["causa"]] += 1 if receta_cambio: recetas_distintas += 1 + elif all(solo_metadatos(d) for d in divs): + no_determinismo.append((nombre, a.name[:12], b.name[:12], divs)) else: - no_reproducen.append((nombre, a.name[:12], b.name[:12], divs)) + quiza_dep.append((nombre, a.name[:12], b.name[:12], divs)) if i % 50 == 0: print(f" … {i}/{len(nombres)}") print(f"\n── recetas distintas (divergencia esperada): {recetas_distintas}") - print(f"── MISMA receta, artefacto distinto (NO REPRODUCE): {len(no_reproducen)}") + print(f"── misma receta, SÓLO metadatos difieren (no-determinismo probable): {len(no_determinismo)}") + print(f"── misma receta, difiere código/ficheros (puede ser una dep — no concluyente): {len(quiza_dep)}") if errores: print(f"── no analizados: {len(errores)} ({errores[:3]}…)") @@ -93,9 +125,10 @@ def main(): for c, n in causas.most_common(): print(f" {c:22s} {n}") - if no_reproducen: - print("\n══ NO-REPRODUCCIONES (misma receta ⇒ el artefacto debería ser idéntico):") - for nombre, ha, hb, divs in no_reproducen: + if no_determinismo: + print("\n══ NO-DETERMINISMO PROBABLE (misma receta; sólo difieren metadatos que las deps") + print(" no explican — reconstruir para confirmar):") + for nombre, ha, hb, divs in no_determinismo: print(f"\n {nombre} [{ha}… vs {hb}…] {len(divs)} divergencia(s)") for d in divs[:4]: print(f" · {d['ruta']}: {d['explicacion'].splitlines()[0][:150]}") @@ -106,8 +139,11 @@ def main(): pathlib.Path(SALIDA_JSON).write_text(json.dumps({ "causas": dict(causas), "recetas_distintas": recetas_distintas, - "no_reproducen": [ - {"nombre": n, "a": a, "b": b, "divergencias": d} for n, a, b, d in no_reproducen + "no_determinismo": [ + {"nombre": n, "a": a, "b": b, "divergencias": d} for n, a, b, d in no_determinismo + ], + "quiza_dep": [ + {"nombre": n, "a": a, "b": b, "divergencias": d} for n, a, b, d in quiza_dep ], }, indent=2, ensure_ascii=False)) print(f"\n informe → {SALIDA_JSON}")