tawasuyu

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…

swm: `strip_debug` no viajaba en el paquete — y es ENTRADA DE HASH, así que 40 recetas no instalaban

Encontrado instalando `takana` desde su propio repo (SDD 28 §5), que es la primera vez que se hace
el viaje completo receta → `.tkn` → `install --require-signed` sobre un paquete con `strip_debug`.

    Error: expected_hash no coincide:
      declarado = b3:941d1857e6e93ebbfbad8240a1c30405263e5ff8fc881a923aee312160563eea
      obtenido  = b3:2727050ebdbd7817b53e79ffe9796569e9364454ed35474b51e881fcc6048766

`why-differs` sobre los dos artefactos lo nombró: las recetas selladas diferían en `version`,
`license` y **`strip_debug`**. Los dos binarios pesaban EXACTAMENTE lo mismo (3.374.768 bytes) y sólo
divergían en `.shstrtab` — la firma de un `strip` que corrió una vez y la otra no.

`swm_bridge` re-inyecta con cuidado `flags`, `phases`, `zig_version`, `strip_components`, `patches`,
`deps`, `evidence` y `slots` («fidelidad de reconstrucción», dice su comentario) y **se olvidaba de
`strip_debug`**, que ni siquiera existía en `SwmBuild` ⇒ no viajaba en el `.swm` en absoluto. Como
ENTRA EN `hash_inputs` (`recipe.rs:572`, con el comentario «cambia el CONTENIDO del artefacto, así
que TIENE que entrar»), el receptor reconstruía con `None` y sellaba en otra dirección.

**Alcance medido: 40 recetas del corpus lo usan, 18 de las 88 del perfil `servidor`.** Para todas,
el `expected_hash` anclado por `pack --build` no coincidía NUNCA y `install --require-signed`
abortaba. O sea: una quinta parte del repo no se podía instalar, y nadie lo sabía porque nunca se
había cerrado el lazo.

Por qué no lo cazó nadie antes: `tree` —sin `strip_debug`— da cache-hit y funciona perfecto. El bug
sólo aparece en las recetas que lo declaran, y el dogfood previo no las tocaba.

Test de regresión con su control: una receta CON `strip_debug` lo lleva en el `.swm`, y una SIN él no
gana un `false` inventado. 221 tests de takana-core en verde.

Verificado end-to-end tras el arreglo: `install takana --repo http://<caja> --require-signed` da
cache-hit en `b3:941d1857…` —el mismo hash anclado— e hidrata los 3 ficheros.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x

9bc518899e80b19fb156e950ac5899c57fd0f4a2 · por Sergio · (hace 1 mes) · padre 5beb3aa46 · ver el árbol en este commit

Delta semántico · 11

crates/hammerd/src/bus.rs

crates/takana-build/src/swm_bridge.rs

crates/takana-cli/src/main.rs

crates/takana-core/src/compat.rs

crates/takana-core/src/swm.rs

5 archivos · +54 −0

diff --git a/crates/hammerd/src/bus.rs b/crates/hammerd/src/bus.rs
index 803ec0c7..5d036511 100644
--- a/crates/hammerd/src/bus.rs
+++ b/crates/hammerd/src/bus.rs
@@ -378,6 +378,7 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
             flags: recipe.flags,
             phases: Default::default(),
             zig_version: None,
+            strip_debug: None,
         },
         target_bin: format!("/usr/bin/{name}"),
         expected_hash: None,
diff --git a/crates/takana-build/src/swm_bridge.rs b/crates/takana-build/src/swm_bridge.rs
index c49ddacc..349cb58b 100644
--- a/crates/takana-build/src/swm_bridge.rs
+++ b/crates/takana-build/src/swm_bridge.rs
@@ -218,6 +218,10 @@ flags    = []
     // el .swm (si vienen) y se reinyectan en la receta efímera tal cual los tenía el original.
     recipe.build.phases = build_cfg.phases.clone();
     recipe.build.zig_version = build_cfg.zig_version.clone();
+    // `strip_debug` ENTRA EN `hash_inputs`: sin esta línea el receptor reconstruye con `None`,
+    // sella en otra dirección y el `expected_hash` anclado no coincide NUNCA. Ver la doc del campo
+    // en `swm::SwmBuild::strip_debug`.
+    recipe.build.strip_debug = build_cfg.strip_debug;
     if let Some(n) = strip_components {
         recipe.source.strip_components = n;
     }
diff --git a/crates/takana-cli/src/main.rs b/crates/takana-cli/src/main.rs
index ce3df589..10bbae3f 100644
--- a/crates/takana-cli/src/main.rs
+++ b/crates/takana-cli/src/main.rs
@@ -3020,6 +3020,7 @@ fn build_export_mutations(
                 flags: recipe.build.flags.clone(),
                 phases: recipe.build.phases.clone(),
                 zig_version: recipe.build.zig_version.clone(),
+                strip_debug: recipe.build.strip_debug,
             },
             target_bin,
             expected_hash: Some(art_hash),
diff --git a/crates/takana-core/src/compat.rs b/crates/takana-core/src/compat.rs
index 2b45863f..8ed53a45 100644
--- a/crates/takana-core/src/compat.rs
+++ b/crates/takana-core/src/compat.rs
@@ -352,6 +352,7 @@ mod tests {
                     flags: vec![],
                     phases: Default::default(),
                     zig_version: None,
+                    strip_debug: None,
                 },
                 target_bin: "/usr/bin/app".into(),
                 expected_hash: Some("b3:app".into()),
diff --git a/crates/takana-core/src/swm.rs b/crates/takana-core/src/swm.rs
index 4ac9cfc9..c952d378 100644
--- a/crates/takana-core/src/swm.rs
+++ b/crates/takana-core/src/swm.rs
@@ -157,6 +157,16 @@ pub struct SwmBuild {
     /// `None` ⇒ el zig por defecto del receptor.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub zig_version: Option<String>,
+    /// Split de la info de depuración (`Build::strip_debug`, SDD 23 etapa 4).
+    ///
+    /// **Tiene que viajar, y que faltara era un bug de reproducibilidad medible**: `strip_debug`
+    /// ENTRA EN `hash_inputs` (cambia el contenido del artefacto), así que un `.swm` que no lo
+    /// llevara hacía que el receptor reconstruyera con `strip_debug` en `None` y sellara en OTRA
+    /// dirección. El `expected_hash` anclado por `pack --build` no coincidía nunca y
+    /// `install --require-signed` abortaba — para 40 recetas del corpus, 18 de las 88 del perfil
+    /// `servidor`. Encontrado el 2026-09-10 instalando `takana` desde su propio repo (SDD 28 §5).
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub strip_debug: Option<bool>,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -292,6 +302,7 @@ impl Swm {
             flags: recipe.build.flags.clone(),
             phases: recipe.build.phases.clone(),
             zig_version: recipe.build.zig_version.clone(),
+            strip_debug: recipe.build.strip_debug,
         };
 
         let swm = Swm {
@@ -609,6 +620,7 @@ mutations:
                     flags: vec![],
                     phases: Default::default(),
                     zig_version: None,
+                    strip_debug: None,
                 },
                 target_bin: "/bin/x".into(),
                 expected_hash: None,
@@ -641,6 +653,7 @@ mutations:
                     flags: vec![],
                     phases: Default::default(),
                     zig_version: None,
+                    strip_debug: None,
                 },
                 target_bin: "/bin/grep".into(),
                 expected_hash: None,
@@ -670,6 +683,7 @@ mutations:
                 flags: vec![],
                 phases: Default::default(),
                 zig_version: None,
+                strip_debug: None,
             },
             target_bin: "/bin/x".into(),
             expected_hash: None,
@@ -755,6 +769,38 @@ zig_version = "0.13.0"
         }
     }
 
+    /// `strip_debug` ENTRA EN `hash_inputs`, así que un `.swm` que no lo llevara hacía que el
+    /// receptor sellara en OTRA dirección y `install --require-signed` abortara siempre. Medido el
+    /// 2026-09-10 instalando `takana` desde su propio repo: declarado `b3:941d1857…`, obtenido
+    /// `b3:2727050e…`. Afectaba a 40 recetas del corpus. Este test es el que faltaba.
+    #[test]
+    fn from_recipe_lleva_strip_debug_porque_es_entrada_de_hash() {
+        let con = crate::Recipe::from_toml(
+            "name = \"takana\"\nversion = \"0.0.1\"\n[source]\nrepo = \"ssh://x/y.git\"\ncommit = \"abc\"\n[build]\nstrip_debug = true\n",
+        )
+        .unwrap();
+        let swm = Swm::from_recipe(&con, "/usr/bin/takana", None, None, "dev").unwrap();
+        match &swm.mutations[0] {
+            Mutation::SourcePatch { build, .. } => {
+                assert_eq!(build.strip_debug, Some(true), "strip_debug tiene que viajar en el .swm");
+            }
+            other => panic!("esperaba SourcePatch, obtuve {other:?}"),
+        }
+
+        // Y el control que hace que el test valga: sin el campo, no se inventa un `false`.
+        let sin = crate::Recipe::from_toml(
+            "name = \"tree\"\nversion = \"2.3.2\"\n[source]\nrepo = \"ssh://x/y.git\"\ncommit = \"abc\"\n[build]\n",
+        )
+        .unwrap();
+        let swm2 = Swm::from_recipe(&sin, "/usr/bin/tree", None, None, "dev").unwrap();
+        match &swm2.mutations[0] {
+            Mutation::SourcePatch { build, .. } => {
+                assert_eq!(build.strip_debug, None, "una receta sin strip_debug no debe ganar uno");
+            }
+            other => panic!("esperaba SourcePatch, obtuve {other:?}"),
+        }
+    }
+
     #[test]
     fn verify_schema_source_patch_rechaza_sin_origen() {
         let m = Mutation::SourcePatch {
@@ -772,6 +818,7 @@ zig_version = "0.13.0"
                 flags: vec![],
                 phases: Default::default(),
                 zig_version: None,
+                strip_debug: None,
             },
             target_bin: "/bin/x".into(),
             expected_hash: None,