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>
This commit is contained in:
Sergio
2026-05-08 19:44:47 +00:00
parent 5edc912ed8
commit b3feaf667c
5 changed files with 103 additions and 5 deletions
@@ -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);
+6 -1
View File
@@ -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(),
}
}
}
+46
View File
@@ -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,