tawasuyu

tawasuyu/brahman

Rust 483TOML 191Markdown 50JSON 176771 Sutichaykuna

feat: Phase B-1 — unificación ontológica de Cards (Ente ↔ Data)

La Card pasa a ser EL protocolo de presentación del ecosistema. Una
Mónada Nouser y un Ente Brahman son ambos "entidades que se presentan";
el consumidor (UI, broker, admin) discrimina por `kind` cuando importa,
pero todos hablan el mismo idioma.

brahman-card:
- CardKind { Ente (default), Data }. Backward-compat: Cards existentes
  quedan Ente.
- DataFacet { summary, keywords, centroid, member_count, dispersion,
  presentation_hint } — vista liviana para el wire. Listas grandes
  (members reales, embeddings completos) se consultan al daemon dueño
  bajo demanda.
- Card.kind y Card.data agregados. WireCard espeja, conversiones From
  propagan ambos campos.
- Default impl actualizado.

brahman-broker:
- BrokeredCard propaga kind y data desde la Card registrada. No afecta
  el matching (sigue por TypeRef + priority + pin_to); permite a
  observadores discriminar sin re-query.

nouser-card:
- Depende ahora de brahman-card.
- MonadManifest::to_brahman_card() proyecta una Mónada a Card brahman:
  - id, label, lineage directos.
  - payload Virtual, supervision Delegate, lifecycle Daemon
    (placeholder — la Mónada no se ejecuta).
  - kind = Data.
  - data = Some(DataFacet { summary, keywords, centroide,
    member_count, entropy → dispersion, presentation_hint del Lens }).
- Test nuevo projects_to_brahman_card.

brahman-status:
- Prefijo [ente] o [data] por sesión.
- Sesiones data renderean también summary, members + dispersion,
  keywords y lens hint.

Resultado: la UI ve una sola lista uniforme — no necesita saber si
mira procesos o cúmulos de datos, sólo lee el Card y se adapta por
kind. La función de presentarse es la misma para todos.

Tests: 59 (card 11, broker 15, handshake codec+tr 2 + integ 7,
card-wit 4, admin 0, nouser-card 7 +1, nouser-core 13).
cargo check --workspace: 0 errores, 0 warnings.

Próximo: Phase B-2 — bin nouser daemon que sidecarea cada Mónada como
sesión brahman, mezclándolas con los entes en brahman-status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

b85700c5385d53766ac531a12fe324c4c1007066 · ruwaq Sergio · (5 killa ñawpaq) · tayta 7bdc26e61 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 12

crates/core/brahman-admin/examples/brahman-status.rs

  • ukhunpi t'ikrakurqan fn main

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

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

  • silq'uyninpi t'ikrakurqan struct Card pub
  • musuq enum CardKind pub
  • musuq struct DataFacet pub
  • silq'uyninpi t'ikrakurqan struct WireCard pub
  • ukhunpi t'ikrakurqan fn default
  • ukhunpi t'ikrakurqan fn from
  • ukhunpi t'ikrakurqan fn from

crates/modules/nouser/card/src/lib.rs

7 qillqakuna · +231 −3

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd16c20..24feb81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,56 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-08
 
+### feat: Phase B-1 — unificación ontológica de Cards (Ente ↔ Data)
+La Card es **el** protocolo de presentación del ecosistema, no sólo de
+los procesos. Una Mónada Nouser y un Ente Brahman son ambos "entidades
+que se presentan"; el consumidor (UI, broker, admin) discrimina por
+`kind` cuando importa, pero todos hablan el mismo idioma.
+
+Cambios:
+
+- `brahman-card`:
+  - `CardKind { Ente (default), Data }`. Conserva back-compat:
+    Cards existentes son `Ente` por default.
+  - `DataFacet { summary, keywords, centroid, member_count, dispersion,
+    presentation_hint }`. Liviano para el wire — listas grandes
+    (members, embeddings completos) se consultan al daemon dueño bajo
+    demanda.
+  - `Card.kind` y `Card.data: Option<DataFacet>` agregados. WireCard
+    espeja, conversiones `From` propagan.
+  - Default impl actualizado.
+
+- `brahman-broker::BrokeredCard`: propaga `kind` y `data` desde la Card
+  registrada. No afecta el matching (sigue siendo por TypeRef +
+  priority + pin_to); permite a observadores discriminar sin re-query.
+
+- `nouser-card`: depende ahora de `brahman-card`. Nuevo método
+  `MonadManifest::to_brahman_card()` que proyecta:
+  - id, label, lineage → directos.
+  - payload Virtual, supervision Delegate, lifecycle Daemon (placeholder
+    semántico — la Mónada no se ejecuta).
+  - kind = Data.
+  - data = Some(DataFacet) con summary, keywords, centroide,
+    member_count, entropy → dispersion, y un `presentation_hint` derivado
+    del `Lens` (`Code` → `"code"`, `Gallery` → `"gallery"`, etc.).
+  - Test nuevo: `projects_to_brahman_card`.
+
+- `brahman-status`: cada sesión muestra ahora `[ente]` o `[data]` como
+  prefijo. Para sesiones `data`, render adicional con summary, members
+  + dispersion, keywords y lens hint.
+
+Resultado: la UI (yahweh, brahman-status, futuro explorer) ve una sola
+lista uniforme. No tiene que saber si está mirando un proceso o un
+cúmulo de datos — sólo lee el Card y se adapta por `kind`.
+
+Tests acumulados: 59 (card 11, broker 15, handshake codec+transport 2 +
+integ 7, card-wit 4, admin 0, nouser-card 7, nouser-core 13).
+cargo check --workspace: 0 errores, 0 warnings.
+
+Próximo: **Phase B-2** — bin `nouser daemon <dir>` que sidecarea cada
+Mónada como una sesión brahman, publicándola al broker. Brahman-status
+las verá junto a los entes.
+
 ### feat(nouser): Phase A — mecanismo determinista de Mónadas
 Primer trozo del módulo Nouser (Kairos): explorador de Mónadas como
 "imanes semánticos" sobre el filesystem. Phase A cubre el 90% de los
diff --git a/Cargo.lock b/Cargo.lock
index 34ded5b..baaf039 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6049,6 +6049,7 @@ dependencies = [
 name = "nouser-card"
 version = "0.1.0"
 dependencies = [
+ "brahman-card",
  "serde",
  "serde_json",
  "thiserror 2.0.18",
diff --git a/crates/core/brahman-admin/examples/brahman-status.rs b/crates/core/brahman-admin/examples/brahman-status.rs
index d8dc44c..b35ce97 100644
--- a/crates/core/brahman-admin/examples/brahman-status.rs
+++ b/crates/core/brahman-admin/examples/brahman-status.rs
@@ -24,10 +24,31 @@ async fn main() -> anyhow::Result<()> {
     } else {
         for s in &snap.sessions {
             let conscious_marker = if s.wit.is_some() { " 🧠" } else { "" };
+            let kind_marker = match s.kind {
+                brahman_card::CardKind::Ente => "ente",
+                brahman_card::CardKind::Data => "data",
+            };
             println!(
-                "  {}  {}{}  lifecycle={:?}  priority={:?}",
-                s.session, s.label, conscious_marker, s.lifecycle, s.priority
+                "  [{}]  {}  {}{}  lifecycle={:?}  priority={:?}",
+                kind_marker, s.session, s.label, conscious_marker, s.lifecycle, s.priority
             );
+            if let Some(data) = &s.data {
+                if !data.summary.is_empty() {
+                    println!("      summary:    {}", data.summary);
+                }
+                if data.member_count > 0 {
+                    println!(
+                        "      members:    {} (dispersion={:.2})",
+                        data.member_count, data.dispersion
+                    );
+                }
+                if !data.keywords.is_empty() {
+                    println!("      keywords:   {}", data.keywords.join(", "));
+                }
+                if !data.presentation_hint.is_empty() {
+                    println!("      lens hint:  {}", data.presentation_hint);
+                }
+            }
             if let Some(wit) = &s.wit {
                 println!("      wit: {} / {}", wit.package, wit.world);
                 if !wit.imports.is_empty() {
diff --git a/crates/core/brahman-broker/src/lib.rs b/crates/core/brahman-broker/src/lib.rs
index 33799d8..58e2a19 100644
--- a/crates/core/brahman-broker/src/lib.rs
+++ b/crates/core/brahman-broker/src/lib.rs
@@ -30,7 +30,9 @@
 
 use std::collections::BTreeMap;
 
-use brahman_card::{Card, ContextBias, Flow, Lifecycle, Priority, TypeRef, WitInterface};
+use brahman_card::{
+    Card, CardKind, ContextBias, DataFacet, Flow, Lifecycle, Priority, TypeRef, WitInterface,
+};
 use serde::{Deserialize, Serialize};
 use ulid::Ulid;
 
@@ -77,6 +79,13 @@ pub struct BrokeredCard {
     /// Biases per-contexto, propagados desde `Card.priority_contexts`.
     #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
     pub priority_contexts: BTreeMap<String, ContextBias>,
+    /// Naturaleza de la entidad. Diferencia procesos (Ente) de
+    /// agrupaciones de datos (Data — p. ej. Mónadas Nouser).
+    #[serde(default)]
+    pub kind: CardKind,
+    /// Faceta de datos cuando `kind != Ente`.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub data: Option<DataFacet>,
 }
 
 impl BrokeredCard {
@@ -90,6 +99,8 @@ impl BrokeredCard {
             outputs: card.flow.output.clone(),
             wit,
             priority_contexts: card.priority_contexts.clone(),
+            kind: card.kind,
+            data: card.data.clone(),
         }
     }
 }
diff --git a/crates/core/brahman-card/src/lib.rs b/crates/core/brahman-card/src/lib.rs
index ae364e6..ed48cde 100644
--- a/crates/core/brahman-card/src/lib.rs
+++ b/crates/core/brahman-card/src/lib.rs
@@ -126,6 +126,16 @@ pub struct Card {
     #[serde(default)]
     pub flow: Flows,
 
+    /// Naturaleza de la entidad detrás de la Card. Por defecto `Ente`
+    /// para mantener compatibilidad con Cards existentes.
+    #[serde(default)]
+    pub kind: CardKind,
+
+    /// Faceta de datos cuando `kind != Ente`. `None` para entes
+    /// runtime; `Some(...)` para Mónadas, índices, etc.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub data: Option<DataFacet>,
+
     /// Hijas a instanciar inmediatamente al encarnar esta Card.
     #[serde(default)]
     pub genesis: Vec<Card>,
@@ -166,6 +176,8 @@ impl Default for Card {
             priority: Priority::default(),
             flow: Flows::default(),
             genesis: Vec::new(),
+            kind: CardKind::default(),
+            data: None,
             priority_contexts: BTreeMap::new(),
             extensions: BTreeMap::new(),
         }
@@ -382,6 +394,56 @@ pub enum Lifecycle {
     Widget,
 }
 
+/// Naturaleza de la entidad detrás de la Card.
+///
+/// La función de presentarse es la misma para todos: tener identidad,
+/// resumen, capacidades, y poder ser encontrada por otros. Pero NO todas
+/// las entidades son procesos — algunas son agrupaciones de datos
+/// (Mónadas de Nouser, índices, streams).
+///
+/// El kind permite a consumidores (UI, broker, observadores) discriminar
+/// sólo cuando importa, pero todos hablan el mismo protocolo de Card.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum CardKind {
+    /// Entidad runtime con `payload`/`soma`/`supervision` activos
+    /// (proceso, módulo, daemon).
+    #[default]
+    Ente,
+    /// Agrupación de datos sin proceso detrás (Mónadas Nouser, índices,
+    /// resultados cacheados). `payload` típicamente `Virtual`.
+    Data,
+}
+
+/// Faceta de datos: campos relevantes cuando `Card.kind != Ente`.
+///
+/// Optimizada para el wire — incluye sólo metadatos de presentación, NO
+/// listas pesadas (los miembros, embeddings completos, etc. se consultan
+/// al daemon dueño bajo demanda). El "presentation_hint" es un string
+/// libre que la UI mapea a su lente (p. ej. `"code"` → editor de código).
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
+pub struct DataFacet {
+    /// Resumen humano (1-2 oraciones). Generado por el daemon dueño.
+    #[serde(default)]
+    pub summary: String,
+    /// Tokens dominantes / palabras clave (5-10 típicamente).
+    #[serde(default)]
+    pub keywords: Vec<String>,
+    /// Centroide vectorial. Vacío si no hay embeddings calculados.
+    #[serde(default)]
+    pub centroid: Vec<f32>,
+    /// Cantidad de elementos contenidos (archivos, registros, ...).
+    #[serde(default)]
+    pub member_count: u32,
+    /// Métrica de dispersión interna [0, 1] (típicamente entropía).
+    #[serde(default)]
+    pub dispersion: f32,
+    /// Hint de presentación. Strings libres como `"code"`, `"gallery"`,
+    /// `"markdown"`, `"database"`, `"grid"`, `"tree"`. La UI los mapea.
+    #[serde(default)]
+    pub presentation_hint: String,
+}
+
 /// Prioridad de scheduling. Orden: `Low < Normal < High < Critical` —
 /// usable como tiebreaker en el broker (mayor priority gana).
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
@@ -741,6 +803,10 @@ pub struct WireCard {
     #[serde(default)]
     pub genesis: Vec<WireCard>,
     #[serde(default)]
+    pub kind: CardKind,
+    #[serde(default)]
+    pub data: Option<DataFacet>,
+    #[serde(default)]
     pub priority_contexts: BTreeMap<String, ContextBias>,
 }
 
@@ -761,6 +827,8 @@ impl From<Card> for WireCard {
             priority: c.priority,
             flow: c.flow,
             genesis: c.genesis.into_iter().map(WireCard::from).collect(),
+            kind: c.kind,
+            data: c.data,
             priority_contexts: c.priority_contexts,
         }
     }
@@ -783,6 +851,8 @@ impl From<WireCard> for Card {
             priority: w.priority,
             flow: w.flow,
             genesis: w.genesis.into_iter().map(Card::from).collect(),
+            kind: w.kind,
+            data: w.data,
             priority_contexts: w.priority_contexts,
             extensions: BTreeMap::new(),
         }
diff --git a/crates/modules/nouser/card/Cargo.toml b/crates/modules/nouser/card/Cargo.toml
index 4409b30..5fdfb49 100644
--- a/crates/modules/nouser/card/Cargo.toml
+++ b/crates/modules/nouser/card/Cargo.toml
@@ -9,6 +9,7 @@ publish.workspace = true
 description = "Nouser — manifiesto de Mónada (agrupación semántica de archivos). Espejo de brahman-card pero para datos."
 
 [dependencies]
+brahman-card = { path = "../../../core/brahman-card" }
 serde = { workspace = true }
 serde_json = { workspace = true }
 thiserror = { workspace = true }
diff --git a/crates/modules/nouser/card/src/lib.rs b/crates/modules/nouser/card/src/lib.rs
index 1a55a6d..887651b 100644
--- a/crates/modules/nouser/card/src/lib.rs
+++ b/crates/modules/nouser/card/src/lib.rs
@@ -266,6 +266,56 @@ impl MonadManifest {
             .map(|d| d.as_millis() as u64)
             .unwrap_or(0);
     }
+
+    /// Proyecta el `MonadManifest` a la `brahman_card::Card` que viaja
+    /// por el protocolo. La Card resultante:
+    ///
+    /// - hereda `id` y `label` del manifiesto (ULID estable).
+    /// - `kind = CardKind::Data` (se distingue de un Ente).
+    /// - `payload = Virtual`, `supervision = Delegate`,
+    ///   `lifecycle = Daemon` — placeholder semántico: la Mónada no se
+    ///   "ejecuta", el daemon dueño la mantiene viva.
+    /// - `data = Some(DataFacet { ... })` con summary, keywords,
+    ///   centroide, member_count, dispersión y un hint de presentación
+    ///   derivado del `dominant_lens`.
+    /// - Los miembros completos NO viajan en la Card — se consultan al
+    ///   daemon dueño bajo demanda. Lo que viaja es metadata liviana
+    ///   apta para el wire postcard.
+    pub fn to_brahman_card(&self) -> brahman_card::Card {
+        use brahman_card::{
+            Card, CardKind, DataFacet, Lifecycle, Payload, Priority, Supervision,
+        };
+
+        let presentation_hint = match self.dominant_lens {
+            Lens::Grid => "grid",
+            Lens::Code => "code",
+            Lens::Gallery => "gallery",
+            Lens::Database => "database",
+            Lens::Markdown => "markdown",
+            Lens::Tree => "tree",
+        }
+        .to_string();
+
+        Card {
+            schema_version: brahman_card::CARD_SCHEMA_VERSION,
+            id: self.id,
+            label: self.label.clone(),
+            payload: Payload::Virtual,
+            supervision: Supervision::Delegate,
+            lifecycle: Lifecycle::Daemon,
+            priority: Priority::Normal,
+            kind: CardKind::Data,
+            data: Some(DataFacet {
+                summary: self.summary.clone(),
+                keywords: self.keywords.clone(),
+                centroid: self.centroid.clone(),
+                member_count: self.cardinality,
+                dispersion: self.entropy,
+                presentation_hint,
+            }),
+            ..Default::default()
+        }
+    }
 }
 
 #[cfg(test)]
@@ -315,6 +365,30 @@ mod tests {
         ));
     }
 
+    #[test]
+    fn projects_to_brahman_card() {
+        let mut m = MonadManifest::new("test-monad");
+        m.summary = "monad de prueba".into();
+        m.keywords = vec!["rs".into(), "toml".into()];
+        m.dominant_lens = Lens::Code;
+        m.entropy = 0.42;
+        m.members.insert(Ulid::new());
+        m.members.insert(Ulid::new());
+        m.members.insert(Ulid::new());
+        m.touch();
+
+        let bc = m.to_brahman_card();
+        assert_eq!(bc.id, m.id);
+        assert_eq!(bc.label, "test-monad");
+        assert_eq!(bc.kind, brahman_card::CardKind::Data);
+        let data = bc.data.expect("data facet presente");
+        assert_eq!(data.summary, "monad de prueba");
+        assert_eq!(data.keywords, vec!["rs".to_string(), "toml".to_string()]);
+        assert_eq!(data.member_count, 3);
+        assert!((data.dispersion - 0.42).abs() < 1e-6);
+        assert_eq!(data.presentation_hint, "code");
+    }
+
     #[test]
     fn json_roundtrip() {
         let mut m = MonadManifest::new("test-monad");