gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Sutichaykuna

feat: Crossreferencia — Card.references como grafo del fractal

Las Cards ahora declaran sus relaciones con otras Cards. El Engine
posee Mónadas; las Mónadas declaran que son poseídas por el Engine.

- brahman-card:
  - RelationshipKind { Owns, OwnedBy, Processes, ProcessedBy, Sibling }
  - CardReference { kind, target_id: Ulid, target_label: String }.
    target_label es cache para que la UI renderee sin resolver.
  - Card.references: Vec<CardReference> + espejo en WireCard.
    Conversiones From propagan.
- brahman-broker::BrokeredCard propaga references.
- brahman-status imprime "ref OwnedBy → label (id)" por sesión.
- nouser daemon: cada Mónada publicada añade OwnedBy apuntando al
  engine. Declaración unilateral — el engine no necesita conocer
  Mónada IDs de antemano.

Validación end-to-end:
  $ ente-zero & nouser daemon crates/core
  $ brahman-status
  Sessions (6):
    [ente]  brahman.nouser_engine
    [data]  brahman-handshake/src
        ref OwnedBy  →  brahman.nouser_engine  (01K...)
    [data]  ente-brain/src
        ref OwnedBy  →  brahman.nouser_engine  (01K...)
    ...

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

b3feaf667ca5f34292ea23c0c70ae4895962491f · ruwaq Sergio · (5 killa ñawpaq) · tayta 5edc912ed · kay commitpi sach'ata qhaway

Semántico sayk'uy · 11

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

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

5 qillqakuna · +103 −5

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 873a906..9e9c4dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,37 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-08
 
+### feat: Crossreferencia — Card.references como grafo del fractal
+Las Cards ahora declaran sus relaciones con otras Cards. El Engine
+posee Mónadas; las Mónadas declaran que son poseídas por el Engine.
+La UI puede cruzar el grafo sin discovery especial.
+
+- `brahman-card`:
+  - `RelationshipKind { Owns, OwnedBy, Processes, ProcessedBy, Sibling }`.
+  - `CardReference { kind, target_id, target_label }` — `target_label`
+    es cache del label en el momento de declarar (la UI puede pintar
+    sin resolver).
+  - `Card.references: Vec<CardReference>` y espejo en `WireCard`.
+    Conversiones `From` propagan.
+- `brahman-broker::BrokeredCard` propaga `references`.
+- `brahman-status` imprime cada referencia: `ref OwnedBy → label (id)`.
+- **nouser daemon**: cada Mónada que publica añade
+  `RelationshipKind::OwnedBy` apuntando al engine. La declaración es
+  unilateral; el engine no necesita conocer las IDs de antemano.
+
+Validación end-to-end:
+
+  $ ente-zero & nouser daemon crates/core
+  $ brahman-status
+  Sessions (6):
+    [ente]  ... brahman.nouser_engine
+    [data]  ... brahman-handshake/src
+        ref OwnedBy  →  brahman.nouser_engine  (01K...)
+        summary: 6 archivos...
+    [data]  ... ente-brain/src
+        ref OwnedBy  →  brahman.nouser_engine  (01K...)
+    ...
+
 ### feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten
 Cierra el ciclo del swap automático de Nous (mock↔real):
 
diff --git a/crates/core/brahman-admin/examples/brahman-status.rs b/crates/core/brahman-admin/examples/brahman-status.rs
index c25347b..0857385 100644
--- a/crates/core/brahman-admin/examples/brahman-status.rs
+++ b/crates/core/brahman-admin/examples/brahman-status.rs
@@ -35,6 +35,12 @@ async fn main() -> anyhow::Result<()> {
             if let Some(sock) = &s.service_socket {
                 println!("      socket:     {}", sock.display());
             }
+            for r in &s.references {
+                println!(
+                    "      ref {:?}  →  {}  ({})",
+                    r.kind, r.target_label, r.target_id
+                );
+            }
             if let Some(data) = &s.data {
                 if !data.summary.is_empty() {
                     println!("      summary:    {}", data.summary);
diff --git a/crates/core/brahman-broker/src/lib.rs b/crates/core/brahman-broker/src/lib.rs
index b650e63..42b0a1d 100644
--- a/crates/core/brahman-broker/src/lib.rs
+++ b/crates/core/brahman-broker/src/lib.rs
@@ -32,7 +32,8 @@ use std::collections::BTreeMap;
 use std::path::PathBuf;
 
 use brahman_card::{
-    Card, CardKind, ContextBias, DataFacet, Flow, Lifecycle, Priority, TypeRef, WitInterface,
+    Card, CardKind, CardReference, ContextBias, DataFacet, Flow, Lifecycle, Priority, TypeRef,
+    WitInterface,
 };
 use serde::{Deserialize, Serialize};
 use ulid::Ulid;
@@ -90,6 +91,9 @@ pub struct BrokeredCard {
     /// Socket de servicio (data plane) si lo declara la Card.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub service_socket: Option<PathBuf>,
+    /// Referencias a otras Cards (relaciones declaradas por esta Card).
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub references: Vec<CardReference>,
 }
 
 impl BrokeredCard {
@@ -106,6 +110,7 @@ impl BrokeredCard {
             kind: card.kind,
             data: card.data.clone(),
             service_socket: card.service_socket.clone(),
+            references: card.references.clone(),
         }
     }
 }
diff --git a/crates/core/brahman-card/src/lib.rs b/crates/core/brahman-card/src/lib.rs
index 1f95980..8d8a165 100644
--- a/crates/core/brahman-card/src/lib.rs
+++ b/crates/core/brahman-card/src/lib.rs
@@ -134,6 +134,13 @@ pub struct Card {
     #[serde(default)]
     pub service_socket: Option<PathBuf>,
 
+    /// Referencias a otras Cards: "soy procesado por X", "poseo Y",
+    /// etc. Forma el grafo de relaciones del fractal. Cada Card las
+    /// declara unilateralmente; los consumidores pueden cruzarlas para
+    /// reconstruir vínculos bidireccionales.
+    #[serde(default)]
+    pub references: Vec<CardReference>,
+
     /// Naturaleza de la entidad detrás de la Card. Por defecto `Ente`
     /// para mantener compatibilidad con Cards existentes.
     #[serde(default)]
@@ -185,6 +192,7 @@ impl Default for Card {
             flow: Flows::default(),
             genesis: Vec::new(),
             service_socket: None,
+            references: Vec::new(),
             kind: CardKind::default(),
             data: None,
             priority_contexts: BTreeMap::new(),
@@ -403,6 +411,40 @@ pub enum Lifecycle {
     Widget,
 }
 
+/// Tipo de relación entre dos Cards.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum RelationshipKind {
+    /// Esta Card administra/posee al target (Ente sobre Mónada).
+    Owns,
+    /// Esta Card es administrada/poseída por el target (Mónada bajo Ente).
+    OwnedBy,
+    /// Esta Card procesa al target (Ente que consume Mónada).
+    Processes,
+    /// Esta Card es procesada por el target (Mónada siendo consumida).
+    ProcessedBy,
+    /// Relación lateral genérica.
+    Sibling,
+}
+
+/// Referencia desde una Card a otra. Forma el grafo de relaciones del
+/// fractal: "el Engine X posee la Mónada Y", "el Worker A procesa la
+/// Tarea B", etc.
+///
+/// Es responsabilidad del que declara mantener `target_id` apuntando a
+/// una Card que existe (o existió) en el ecosistema. El `target_label`
+/// es redundante con el lookup en runtime, pero se incluye para que la
+/// UI pueda renderear sin resolver.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct CardReference {
+    pub kind: RelationshipKind,
+    pub target_id: Ulid,
+    /// Label humano del target en el momento de declararse la
+    /// referencia (cache; el target real puede haber cambiado de label).
+    #[serde(default)]
+    pub target_label: String,
+}
+
 /// Naturaleza de la entidad detrás de la Card.
 ///
 /// La función de presentarse es la misma para todos: tener identidad,
@@ -814,6 +856,8 @@ pub struct WireCard {
     #[serde(default)]
     pub service_socket: Option<PathBuf>,
     #[serde(default)]
+    pub references: Vec<CardReference>,
+    #[serde(default)]
     pub kind: CardKind,
     #[serde(default)]
     pub data: Option<DataFacet>,
@@ -839,6 +883,7 @@ impl From<Card> for WireCard {
             flow: c.flow,
             genesis: c.genesis.into_iter().map(WireCard::from).collect(),
             service_socket: c.service_socket,
+            references: c.references,
             kind: c.kind,
             data: c.data,
             priority_contexts: c.priority_contexts,
@@ -864,6 +909,7 @@ impl From<WireCard> for Card {
             flow: w.flow,
             genesis: w.genesis.into_iter().map(Card::from).collect(),
             service_socket: w.service_socket,
+            references: w.references,
             kind: w.kind,
             data: w.data,
             priority_contexts: w.priority_contexts,
diff --git a/crates/modules/nouser/core/src/bin/nouser.rs b/crates/modules/nouser/core/src/bin/nouser.rs
index 4f7792b..f61d241 100644
--- a/crates/modules/nouser/core/src/bin/nouser.rs
+++ b/crates/modules/nouser/core/src/bin/nouser.rs
@@ -166,9 +166,11 @@ fn cmd_daemon(args: &[String]) -> Cmd {
 
     // 1. El propio engine se presenta como Ente.
     let engine_card = build_engine_card();
+    let engine_id = engine_card.id;
+    let engine_label = engine_card.label.clone();
     eprintln!(
-        "nouser daemon: publicando engine '{}' (kind=Ente)",
-        engine_card.label
+        "nouser daemon: publicando engine '{}' (kind=Ente, id={})",
+        engine_label, engine_id
     );
     brahman_sidecar::spawn(engine_card);
 
@@ -181,10 +183,18 @@ fn cmd_daemon(args: &[String]) -> Cmd {
         db.monad_count()
     );
 
-    // 3. Cada Mónada se presenta como Card de tipo Data.
+    // 3. Cada Mónada se presenta como Card de tipo Data, declarando
+    //    su relación OwnedBy con el engine. La UI puede entonces
+    //    cruzar referencias para reconstruir el grafo
+    //    "nouser_engine posee Mónada X" sin lookup adicional.
     let mut handles = Vec::with_capacity(db.monad_count());
     for monad in db.monads() {
-        let card = monad.to_brahman_card();
+        let mut card = monad.to_brahman_card();
+        card.references.push(brahman_card::CardReference {
+            kind: brahman_card::RelationshipKind::OwnedBy,
+            target_id: engine_id,
+            target_label: engine_label.clone(),
+        });
         match brahman_sidecar::spawn_with_handle(brahman_sidecar::SidecarConfig::new(card)) {
             Ok(h) => handles.push(h),
             Err(e) => eprintln!(