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 `. ## 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) -> Self { + Self { + id: Ulid::new(), + label: label.into(), + ..Self::default() + } + } + /// Deserializa una Card desde JSON y valida. pub fn from_json(src: &str) -> Result { 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, ) -> 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) } }