kernel contract: un objetivo sin sellado vigente NO es un objetivo aprobado
Faltaban dos agujeros del mismo tamaño que el que este guardián vino a tapar: 1. La receta derivada de gioser vive fuera de recipes/ a propósito, así que sus deps no resolvían y su hash no se podía calcular ⇒ su sellado viejo se comprobaba como si fuera el vigente. Ahora se le presta el catálogo (base_dir), salvo que traiga patches — que base_dir también los resuelve y moverlo los rompería en silencio. 2. Un objetivo del contrato cuya receta de hoy no tiene NINGÚN sellado simplemente no aparecía en el barrido, y no aparecer se leía como que no había nada que objetar. Ahora se nombra con el hash que le tocaría y sale != 0: no comprobar no es aprobar. Hoy eso dice, con nombre y hash, exactamente los 4 kernels que hay que construir para dar H1 por pagado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
This commit is contained in:
@@ -1355,6 +1355,16 @@ fn store_dir_of(path: &Path) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// ¿Resuelven las `deps.build` de esta receta desde `dir`? Misma regla que el build: hermana, y
|
||||
/// si no, en el directorio padre (que es como una receta de `recipes/incoming-*/` alcanza el
|
||||
/// catálogo).
|
||||
fn deps_resuelven_desde(dir: &Path, deps: &[String]) -> bool {
|
||||
deps.iter().all(|d| {
|
||||
let f = format!("{d}.toml");
|
||||
dir.join(&f).exists() || dir.parent().is_some_and(|p| p.join(&f).exists())
|
||||
})
|
||||
}
|
||||
|
||||
/// La receta cuyo campo `name` es `artifact`, buscada en `dirs`.
|
||||
///
|
||||
/// Primero por convención (`<dir>/<artifact>.toml`) y sólo si eso no da, barriendo el directorio:
|
||||
@@ -1365,7 +1375,7 @@ fn receta_de(artifact: &str, dirs: &[PathBuf]) -> Option<hammer_core::Recipe> {
|
||||
let directo = d.join(format!("{artifact}.toml"));
|
||||
if let Ok(r) = hammer_core::Recipe::load_from_path(&directo) {
|
||||
if r.name == artifact {
|
||||
return Some(r);
|
||||
return Some(con_catalogo(r, dirs));
|
||||
}
|
||||
}
|
||||
let Ok(rd) = std::fs::read_dir(d) else { continue };
|
||||
@@ -1374,7 +1384,7 @@ fn receta_de(artifact: &str, dirs: &[PathBuf]) -> Option<hammer_core::Recipe> {
|
||||
if p.extension().is_some_and(|x| x == "toml") {
|
||||
if let Ok(r) = hammer_core::Recipe::load_from_path(&p) {
|
||||
if r.name == artifact {
|
||||
return Some(r);
|
||||
return Some(con_catalogo(r, dirs));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1383,6 +1393,34 @@ fn receta_de(artifact: &str, dirs: &[PathBuf]) -> Option<hammer_core::Recipe> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Le presta un catálogo a una receta que vive fuera de uno.
|
||||
///
|
||||
/// Las recetas DERIVADAS (`hammer kernel plan --recipe-out`) se guardan a propósito fuera de
|
||||
/// `recipes/`: dejarlas ahí las mete en el grafo compartido como deuda. Pero sus `deps.build` son
|
||||
/// las del catálogo padre, así que desde su directorio no resuelven y el hash no se puede calcular
|
||||
/// — y sin hash no hay vigencia que comparar. Acá se les apunta el `base_dir` al primer directorio
|
||||
/// de `dirs` donde sí resuelven, que es exactamente lo que hace el runbook cuando la copia a
|
||||
/// `recipes/incoming-kernel/` para construirla.
|
||||
///
|
||||
/// NO se toca si la receta trae `patches`: `base_dir` también los resuelve, y moverlo los rompería
|
||||
/// en silencio. Ahí es mejor no saber la vigencia (se comprueba todo) que hashear otra cosa.
|
||||
fn con_catalogo(mut r: hammer_core::Recipe, dirs: &[PathBuf]) -> hammer_core::Recipe {
|
||||
if r.deps.build.is_empty() || deps_resuelven_desde(&r.base_dir, &r.deps.build) {
|
||||
return r;
|
||||
}
|
||||
if !r.source.patches.is_empty() {
|
||||
eprintln!(
|
||||
"aviso: «{}» vive fuera de un catálogo y trae patches ⇒ no le presto base_dir",
|
||||
r.name
|
||||
);
|
||||
return r;
|
||||
}
|
||||
if let Some(d) = dirs.iter().find(|d| deps_resuelven_desde(d, &r.deps.build)) {
|
||||
r.base_dir = d.clone();
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// Qué directorio del store le toca HOY a cada kernel del contrato: `artefacto → <hash>-<nombre>`.
|
||||
///
|
||||
/// POR QUÉ. El store guarda TODOS los sellados, no el último. Sin esta pregunta `--sealed` mide
|
||||
@@ -1432,6 +1470,10 @@ struct ContractRun {
|
||||
superados: Vec<hammer_core::kernel::ContractReport>,
|
||||
/// Configs que NO se comprobaron por no tener perfil declarado. No cuentan como aprobados.
|
||||
sin_perfil: Vec<String>,
|
||||
/// Objetivos del contrato cuya receta de hoy NO tiene ni un sellado en el store. No se
|
||||
/// comprobó nada de ellos: se nombran para que la ausencia no pase por aprobado.
|
||||
#[serde(default)]
|
||||
sin_construir: Vec<String>,
|
||||
}
|
||||
|
||||
fn contract_cmd(
|
||||
@@ -1540,6 +1582,7 @@ fn contract_cmd(
|
||||
reports: Vec::new(),
|
||||
superados: Vec::new(),
|
||||
sin_perfil: Vec::new(),
|
||||
sin_construir: Vec::new(),
|
||||
};
|
||||
|
||||
for (label, artifact, path) in &objetivos {
|
||||
@@ -1570,6 +1613,18 @@ fn contract_cmd(
|
||||
}
|
||||
}
|
||||
|
||||
// Un objetivo del contrato cuya receta de hoy no tiene NINGÚN sellado no aparece en el
|
||||
// barrido — y no aparecer se lee como que no hay nada que objetar. Se nombra.
|
||||
if let Some(v) = &vigentes {
|
||||
let vistos: std::collections::BTreeSet<String> =
|
||||
objetivos.iter().filter_map(|(_, _, p)| store_dir_of(p)).collect();
|
||||
for (art, dir) in v {
|
||||
if !vistos.contains(dir) {
|
||||
run.sin_construir.push(format!("{art} · {dir}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&run)?);
|
||||
} else {
|
||||
@@ -1584,14 +1639,13 @@ fn contract_cmd(
|
||||
if bloquean > 0 {
|
||||
anyhow::bail!("el contrato NO se cumple en {bloquean} kernel(es)");
|
||||
}
|
||||
// Cero vigentes con superados a la vista NO es aprobado: es que la receta cambió y todavía no
|
||||
// se reconstruyó nada. Dejarlo pasar en verde sería el fallo de CLAUDE.md §3 —el vacío leído
|
||||
// como presencia— en el propio guardián.
|
||||
if sealed && run.reports.is_empty() && !run.superados.is_empty() {
|
||||
// Un objetivo sin sellado vigente no se comprobó, y no comprobar no es aprobar: es el fallo
|
||||
// de CLAUDE.md §3 —el vacío leído como presencia— en el propio guardián.
|
||||
if !run.sin_construir.is_empty() {
|
||||
anyhow::bail!(
|
||||
"no hay UN SOLO kernel vigente sellado ({} superado(s) en el store): el contrato no se \
|
||||
comprobó contra nada. Construí las recetas de kernel antes de leer esto como aprobado.",
|
||||
run.superados.len()
|
||||
"{} objetivo(s) del contrato sin un sellado vigente: no se comprobó nada de ellos. \
|
||||
Construí sus recetas antes de leer esto como aprobado.",
|
||||
run.sin_construir.len()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
@@ -1688,6 +1742,17 @@ fn imprimir_contrato(run: &ContractRun) {
|
||||
}
|
||||
);
|
||||
println!();
|
||||
if !run.sin_construir.is_empty() {
|
||||
println!(
|
||||
"⚠ {} objetivo(s) del contrato SIN UN SELLADO VIGENTE — no se comprobó nada de ellos:",
|
||||
run.sin_construir.len()
|
||||
);
|
||||
for s in &run.sin_construir {
|
||||
println!(" {s}");
|
||||
}
|
||||
println!(" no cuentan como aprobados: el barrido no tuvo qué mirar. Construí sus recetas.");
|
||||
println!();
|
||||
}
|
||||
if !run.sin_perfil.is_empty() {
|
||||
println!("⚠ {} config(s) SIN COMPROBAR (ningún perfil declarado):", run.sin_perfil.len());
|
||||
for s in &run.sin_perfil {
|
||||
|
||||
Reference in New Issue
Block a user