cierre #2 del SDD 17: hammer why-differs — el diffoscope propio

Cuando un artefacto no reproduce, el store sólo sabe decir "el hash no coincide" y el resto es
trabajo artesanal. Esto responde POR QUÉ, en términos de la CAUSA y no del byte:

- gzip con MTIME embebido (bytes 4..8)      → remedio: `gzip -n`
- cabecera `ar` de un `.a` (mtime/uid/gid)  → remedio: modo determinista (`ar D`)
- secciones ELF, con lectura experta: sólo `.comment` ⇒ otra versión de compilador; sólo
  `.debug_*` ⇒ rutas de build; sólo `.symtab`/`.dynsym` ⇒ orden de símbolos (código idéntico);
  sólo build-id ⇒ residuo, no causa raíz. En un `.a` dice QUÉ MIEMBRO difiere.
- ruta del árbol de build embebida, texto (línea que difiere), y bytes como último recurso.

Y sobre todo trae la EVIDENCIA, no sólo la hipótesis: para las secciones de texto extrae las
cadenas que están en un ELF y no en el otro. Caso real que lo motivó (alsa-lib): la
interpretación decía "típicamente rutas de build" y la evidencia mostró
`/src/target/release/build/libsodium-sys-<hash-cargo>/out/…`. Sin la cadena era una corazonada;
reproducir eso a mano cuesta varios readelf, la herramienta lo da en 40ms.

Descenso, no comparación total: sólo baja donde los hashes difieren (el cruce con format/
reconcile del SDD 17). Sin dependencias externas — parsers gzip/ar/ELF propios, como manda el
ADR 0004: un diffoscope de verdad se apoya en medio mundo de binarios ajenos.

`--json` para el bucle agéntico; exit 0 si reproduce, 1 si diverge (encadenable en scripts).
`scripts/why-differs-barrido.sh` lo pasa por todo el store y separa los dos casos que se
confunden a ojo: recipe.toml distinto (divergencia esperada) vs recipe.toml IDÉNTICO y artefacto
distinto (no-reproducción a investigar).

5 tests nuevos; los 142 de hammer-core siguen en verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 22:35:03 -04:00
co-authored by Claude Opus 4.8
parent 6b38b46293
commit 895401b479
4 changed files with 967 additions and 0 deletions
+49
View File
@@ -49,6 +49,20 @@ enum Cmd {
#[arg(long)]
check: bool,
},
/// [SDD 17 §1.2] Explica POR QUÉ dos artefactos difieren — el diffoscope propio. Cuando un hash
/// no reproduce, el store sólo sabe decir "no coincide"; esto nombra la CAUSA: un MTIME embebido
/// en un gzip, la cabecera `ar` de un `.a`, qué sección ELF cambió (`.comment` ⇒ otro compilador),
/// una ruta del árbol de build filtrada al binario. Desciende sólo donde los hashes difieren.
/// Exit 0 si los dos árboles son idénticos, 1 si divergen.
WhyDiffers {
/// Artefacto A: un directorio del store (`store/<hash>-<nombre>`) o cualquier árbol.
a: String,
/// Artefacto B, el que se compara contra A.
b: String,
/// Informe JSON — evidencia legible por máquina para el barrido de granja y el bucle agéntico.
#[arg(long)]
json: bool,
},
/// [Atestación] Verifica un árbol FHS (p.ej. un product-rootfs hidratado) contra su
/// `/ente/attest.json`: recomputa el BLAKE3 de cada binario crítico y lo compara. Es el gate de
/// integridad que arje aplicará al boot, hecho hoy por hammer ("reproducir, no confiar"). Exit≠0
@@ -785,6 +799,41 @@ fn main() -> anyhow::Result<()> {
println!("{hash}");
}
}
Cmd::WhyDiffers { a, b, json } => {
let informe = hammer_core::why_differs(std::path::Path::new(&a), std::path::Path::new(&b))?;
if json {
println!("{}", serde_json::to_string_pretty(&informe)?);
} else {
println!("== why-differs");
println!(" A: {}", informe.a);
println!(" B: {}", informe.b);
println!(
" {} entradas idénticas · {} divergen",
informe.identicos,
informe.divergencias.len()
);
for d in &informe.divergencias {
println!("\n {}", d.ruta);
println!(" causa: {}", d.explicacion);
if let Some(r) = &d.remedio {
println!("{r}");
}
}
if informe.reproduce() {
println!("\n ✓ los dos árboles son idénticos: REPRODUCE");
} else {
let resumen: Vec<String> = informe
.por_causa()
.iter()
.map(|(c, n)| format!("{c}×{n}"))
.collect();
println!("\n resumen: {}", resumen.join(" "));
}
}
if !informe.reproduce() {
std::process::exit(1);
}
}
Cmd::Attest { rootfs } => {
let report = hammer_bootstrap::verify_attestation(std::path::Path::new(&rootfs))?;
for p in &report.ok {