gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 188Rust 55Shell 30Markdown 281211 Definiciones

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

8faede66bdacd61db2cbd8fa67027b3866fee7b2 · por Sergio · (hace 1 mes) · padre 697d3f21a · ver el árbol en este commit

Delta semántico · 6

crates/hammer-cli/src/kernel_cmd.rs

1 archivos · +74 −9

diff --git a/crates/hammer-cli/src/kernel_cmd.rs b/crates/hammer-cli/src/kernel_cmd.rs
index 8505e73a..5f39e17d 100644
--- a/crates/hammer-cli/src/kernel_cmd.rs
+++ b/crates/hammer-cli/src/kernel_cmd.rs
@@ -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 {