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
+31
View File
@@ -6,6 +6,37 @@ ratio/diff ver `git show <sha>`.
## 2026-05-08 ## 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 ### feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten
Cierra el ciclo del swap automático de Nous (mock↔real): Cierra el ciclo del swap automático de Nous (mock↔real):
@@ -35,6 +35,12 @@ async fn main() -> anyhow::Result<()> {
if let Some(sock) = &s.service_socket { if let Some(sock) = &s.service_socket {
println!(" socket: {}", sock.display()); 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 let Some(data) = &s.data {
if !data.summary.is_empty() { if !data.summary.is_empty() {
println!(" summary: {}", data.summary); println!(" summary: {}", data.summary);
+6 -1
View File
@@ -32,7 +32,8 @@ use std::collections::BTreeMap;
use std::path::PathBuf; use std::path::PathBuf;
use brahman_card::{ 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 serde::{Deserialize, Serialize};
use ulid::Ulid; use ulid::Ulid;
@@ -90,6 +91,9 @@ pub struct BrokeredCard {
/// Socket de servicio (data plane) si lo declara la Card. /// Socket de servicio (data plane) si lo declara la Card.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub service_socket: Option<PathBuf>, 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 { impl BrokeredCard {
@@ -106,6 +110,7 @@ impl BrokeredCard {
kind: card.kind, kind: card.kind,
data: card.data.clone(), data: card.data.clone(),
service_socket: card.service_socket.clone(), service_socket: card.service_socket.clone(),
references: card.references.clone(),
} }
} }
} }
+46
View File
@@ -134,6 +134,13 @@ pub struct Card {
#[serde(default)] #[serde(default)]
pub service_socket: Option<PathBuf>, 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` /// Naturaleza de la entidad detrás de la Card. Por defecto `Ente`
/// para mantener compatibilidad con Cards existentes. /// para mantener compatibilidad con Cards existentes.
#[serde(default)] #[serde(default)]
@@ -185,6 +192,7 @@ impl Default for Card {
flow: Flows::default(), flow: Flows::default(),
genesis: Vec::new(), genesis: Vec::new(),
service_socket: None, service_socket: None,
references: Vec::new(),
kind: CardKind::default(), kind: CardKind::default(),
data: None, data: None,
priority_contexts: BTreeMap::new(), priority_contexts: BTreeMap::new(),
@@ -403,6 +411,40 @@ pub enum Lifecycle {
Widget, 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. /// Naturaleza de la entidad detrás de la Card.
/// ///
/// La función de presentarse es la misma para todos: tener identidad, /// La función de presentarse es la misma para todos: tener identidad,
@@ -814,6 +856,8 @@ pub struct WireCard {
#[serde(default)] #[serde(default)]
pub service_socket: Option<PathBuf>, pub service_socket: Option<PathBuf>,
#[serde(default)] #[serde(default)]
pub references: Vec<CardReference>,
#[serde(default)]
pub kind: CardKind, pub kind: CardKind,
#[serde(default)] #[serde(default)]
pub data: Option<DataFacet>, pub data: Option<DataFacet>,
@@ -839,6 +883,7 @@ impl From<Card> for WireCard {
flow: c.flow, flow: c.flow,
genesis: c.genesis.into_iter().map(WireCard::from).collect(), genesis: c.genesis.into_iter().map(WireCard::from).collect(),
service_socket: c.service_socket, service_socket: c.service_socket,
references: c.references,
kind: c.kind, kind: c.kind,
data: c.data, data: c.data,
priority_contexts: c.priority_contexts, priority_contexts: c.priority_contexts,
@@ -864,6 +909,7 @@ impl From<WireCard> for Card {
flow: w.flow, flow: w.flow,
genesis: w.genesis.into_iter().map(Card::from).collect(), genesis: w.genesis.into_iter().map(Card::from).collect(),
service_socket: w.service_socket, service_socket: w.service_socket,
references: w.references,
kind: w.kind, kind: w.kind,
data: w.data, data: w.data,
priority_contexts: w.priority_contexts, priority_contexts: w.priority_contexts,
+14 -4
View File
@@ -166,9 +166,11 @@ fn cmd_daemon(args: &[String]) -> Cmd {
// 1. El propio engine se presenta como Ente. // 1. El propio engine se presenta como Ente.
let engine_card = build_engine_card(); let engine_card = build_engine_card();
let engine_id = engine_card.id;
let engine_label = engine_card.label.clone();
eprintln!( eprintln!(
"nouser daemon: publicando engine '{}' (kind=Ente)", "nouser daemon: publicando engine '{}' (kind=Ente, id={})",
engine_card.label engine_label, engine_id
); );
brahman_sidecar::spawn(engine_card); brahman_sidecar::spawn(engine_card);
@@ -181,10 +183,18 @@ fn cmd_daemon(args: &[String]) -> Cmd {
db.monad_count() 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()); let mut handles = Vec::with_capacity(db.monad_count());
for monad in db.monads() { 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)) { match brahman_sidecar::spawn_with_handle(brahman_sidecar::SidecarConfig::new(card)) {
Ok(h) => handles.push(h), Ok(h) => handles.push(h),
Err(e) => eprintln!( Err(e) => eprintln!(