gioser

tawasuyu/brahman

Rust 496TOML 195Markdown 55JSON 197001 Definiciones

feat(card): Card::new(label) — alternativa segura a Default::default()

Cierra la trap documentada de Card::default() que devuelve id =
Ulid::nil(). Usar Card::default() viva colisionaba con cualquier otra
Card default-construida bajo el mismo id 00000...

La fix no es romper Default (sigue siendo determinista, requerido por
callers que lo usan como template para deserializacion y patterns de
busqueda) sino agregar un constructor explicito Card::new(label) que
asigna id = Ulid::new() + label provisto, manteniendo defaults seguros
en todo lo demas.

Pensado para struct-literals con override parcial:

    let card = Card {
        kind: CardKind::Data,
        payload: Payload::Embedded(json),
        ..Card::new("mi-modulo.algo")
    };

Refactor de call sites en codigo de produccion:
- brahman_sidecar::discovery::build_consumer_card
- nouser daemon::build_engine_card

Default queda con docstring expandida que apunta a Card::new para uso
"vivo". to_brahman_card en nouser-card NO se modifica porque asigna
el id estable de la Monada, no uno fresco.

Tests: 3 unitarios nuevos en brahman-card. 15 tests verdes (era 12).

4c9e4c396263d803ce0b8349703590fd6a66f872 · por Sergio · (hace 5 meses) · padre 006640057 · ver el árbol en este commit

Delta semántico · 6

crates/core/brahman-card/src/lib.rs

crates/modules/nouser/core/src/bin/nouser.rs

crates/shared/brahman-sidecar/src/discovery.rs

4 archivos · +97 −13

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a80821..975da0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,40 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-09
 
+### feat(card): `Card::new(label)` — alternativa segura a `Default::default()`
+Cierra la trap documentada de `Card::default()` que devuelve `id =
+Ulid::nil()`. Usar `Card::default()` "viva" colisionaba con cualquier
+otra Card default-construida bajo el mismo id `00000000…`. La fix no
+es romper `Default` (sigue siendo determinista, requerido por callers
+que lo usan como template para deserialización), sino agregar un
+constructor explícito:
+
+  let card = Card {
+      kind: CardKind::Data,
+      payload: Payload::Embedded(json),
+      ..Card::new("mi-modulo.algo")
+  };
+
+`Card::new(label)` asigna `id = Ulid::new()` (único) + `label`
+provisto, dejando el resto en defaults seguros (Virtual / OneShot /
+Ente). Pensado para usarse en struct-literals con override parcial,
+igual sintaxis que el patrón viejo pero sin la trap.
+
+Refactor de call sites:
+- `brahman_sidecar::discovery::build_consumer_card` → `..Card::new(label)`
+- `nouser daemon::build_engine_card` → `..Card::new("brahman.nouser_engine")`
+
+`Default` se mantiene tal cual con docstring expandida que advierte
+explícitamente sobre el uso "vivo" y apunta a `Card::new`. Tests
+existentes y el patrón `nouser_card::MonadManifest::to_brahman_card`
+(que asigna el id estable de la Mónada, no uno fresco) NO se
+modifican — `Default` sigue siendo correcto cuando el caller
+sobreescribe `id` explícitamente.
+
+Tests: 3 unitarios nuevos en brahman-card (`new_assigns_real_ulid_and_label`,
+`new_yields_distinct_ids_per_call`, `default_keeps_nil_id_for_struct_update_pattern`).
+15 tests verdes (era 12).
+
 ### feat(sidecar): API reusable de discovery vía broker
 Promueve el patrón ad-hoc `discover_producer_socket` (que vivía
 inline en `nouser attract --remote`) a un módulo público
diff --git a/crates/core/brahman-card/src/lib.rs b/crates/core/brahman-card/src/lib.rs
index 8d8a165..02146bc 100644
--- a/crates/core/brahman-card/src/lib.rs
+++ b/crates/core/brahman-card/src/lib.rs
@@ -172,9 +172,16 @@ pub struct Card {
 }
 
 impl Default for Card {
-    /// Default razonable para `..Default::default()` en struct-literals.
-    /// `id` queda en `Ulid::nil()` y `label` vacío — el consumidor debe
-    /// sobreescribirlos antes de validar.
+    /// Default determinista pensado para el patrón `..Default::default()`
+    /// en struct-literals donde el caller sobreescribe `id` y `label`.
+    ///
+    /// **Trap conocida**: `id` queda en `Ulid::nil()`. Si construís una
+    /// Card "viva" para registrar en el broker, NUNCA dejes el `id`
+    /// derivado de `Default` — todas las Cards default-construidas
+    /// colisionarían bajo el mismo `00000000000000000000000000`. Para
+    /// Cards frescas usá [`Card::new`], que asigna `Ulid::new()`.
+    /// `Ulid::nil()` queda reservado para patterns de búsqueda y
+    /// sentinel values en serialización.
     fn default() -> Self {
         Self {
             schema_version: CARD_SCHEMA_VERSION,
@@ -591,6 +598,32 @@ pub enum TypeRef {
 // =====================================================================
 
 impl Card {
+    /// Construye una Card "viva" lista para registrarse en el broker:
+    /// `id = Ulid::new()` (único), `label` provisto, todo lo demás en
+    /// los defaults seguros (Payload::Virtual, Supervision::OneShot,
+    /// CardKind::Ente, etc.).
+    ///
+    /// Diseñada para usarse en struct-literals con override parcial,
+    /// igual que `Default` pero sin la trap de `Ulid::nil()`:
+    ///
+    /// ```ignore
+    /// let card = Card {
+    ///     kind: CardKind::Data,
+    ///     payload: Payload::Embedded(serde_json::json!({"foo": 1})),
+    ///     ..Card::new("mi-modulo.algo")
+    /// };
+    /// ```
+    ///
+    /// Para Cards de búsqueda/sentinel donde `nil` es semánticamente
+    /// significativo, usá `Card::default()` directamente.
+    pub fn new(label: impl Into<String>) -> Self {
+        Self {
+            id: Ulid::new(),
+            label: label.into(),
+            ..Self::default()
+        }
+    }
+
     /// Deserializa una Card desde JSON y valida.
     pub fn from_json(src: &str) -> Result<Self, CardError> {
         let c: Self = serde_json::from_str(src)?;
@@ -1230,4 +1263,28 @@ mod tests {
         let decoded: WireCard = postcard::from_bytes(&bytes).expect("WireCard debe decodear");
         assert_eq!(decoded.label, "x");
     }
+
+    #[test]
+    fn new_assigns_real_ulid_and_label() {
+        let c = Card::new("nouser.engine");
+        assert_eq!(c.label, "nouser.engine");
+        assert_ne!(c.id, Ulid::nil(), "Card::new no debe dejar id en nil");
+    }
+
+    #[test]
+    fn new_yields_distinct_ids_per_call() {
+        let a = Card::new("x");
+        let b = Card::new("x");
+        assert_ne!(a.id, b.id);
+    }
+
+    #[test]
+    fn default_keeps_nil_id_for_struct_update_pattern() {
+        // Mantener este invariante explícito: Default::default() es
+        // determinista y devuelve nil. Cualquier cambio aquí rompería
+        // el patrón `..Default::default()` en patterns de búsqueda.
+        let d = Card::default();
+        assert_eq!(d.id, Ulid::nil());
+        assert!(d.label.is_empty());
+    }
 }
diff --git a/crates/modules/nouser/core/src/bin/nouser.rs b/crates/modules/nouser/core/src/bin/nouser.rs
index b52b12b..4c4a12c 100644
--- a/crates/modules/nouser/core/src/bin/nouser.rs
+++ b/crates/modules/nouser/core/src/bin/nouser.rs
@@ -727,16 +727,13 @@ fn embed_via(
 /// Card del propio engine (kind=Ente). Es el "ser" que produce y
 /// administra Mónadas; aparece en brahman-status junto a sus Mónadas.
 fn build_engine_card() -> brahman_card::Card {
-    use brahman_card::{ulid::Ulid, Card, CardKind, Lifecycle, Payload, Priority, Supervision};
+    use brahman_card::{Card, CardKind, Lifecycle, Payload, Priority, Supervision};
     Card {
-        schema_version: brahman_card::CARD_SCHEMA_VERSION,
-        id: Ulid::new(),
-        label: "brahman.nouser_engine".into(),
         payload: Payload::Virtual,
         supervision: Supervision::Delegate,
         lifecycle: Lifecycle::Daemon,
         priority: Priority::Normal,
         kind: CardKind::Ente,
-        ..Default::default()
+        ..Card::new("brahman.nouser_engine")
     }
 }
diff --git a/crates/shared/brahman-sidecar/src/discovery.rs b/crates/shared/brahman-sidecar/src/discovery.rs
index f044131..b57fbf2 100644
--- a/crates/shared/brahman-sidecar/src/discovery.rs
+++ b/crates/shared/brahman-sidecar/src/discovery.rs
@@ -26,7 +26,6 @@ use std::time::{Duration, Instant};
 
 use brahman_card::{
     ulid::Ulid, Card, CardKind, Flow, Flows, Lifecycle, Payload, Priority, Supervision, TypeRef,
-    CARD_SCHEMA_VERSION,
 };
 use brahman_handshake::client::{Client, ClientError};
 use brahman_handshake::messages::MatchEventKind;
@@ -63,9 +62,6 @@ pub fn build_consumer_card(
     type_name: impl Into<String>,
 ) -> Card {
     Card {
-        schema_version: CARD_SCHEMA_VERSION,
-        id: Ulid::new(),
-        label: consumer_label.into(),
         payload: Payload::Virtual,
         supervision: Supervision::OneShot,
         lifecycle: Lifecycle::Oneshot,
@@ -81,7 +77,7 @@ pub fn build_consumer_card(
             }],
             output: vec![],
         },
-        ..Default::default()
+        ..Card::new(consumer_label)
     }
 }