From b3feaf667ca5f34292ea23c0c70ae4895962491f Mon Sep 17 00:00:00 2001 From: Sergio Date: Fri, 8 May 2026 19:44:47 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Crossreferencia=20=E2=80=94=20Card.refe?= =?UTF-8?q?rences=20como=20grafo=20del=20fractal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 + 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) --- CHANGELOG.md | 31 +++++++++++++ .../brahman-admin/examples/brahman-status.rs | 6 +++ crates/core/brahman-broker/src/lib.rs | 7 ++- crates/core/brahman-card/src/lib.rs | 46 +++++++++++++++++++ crates/modules/nouser/core/src/bin/nouser.rs | 18 ++++++-- 5 files changed, 103 insertions(+), 5 deletions(-) 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 `. ## 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` 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, + /// Referencias a otras Cards (relaciones declaradas por esta Card). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub references: Vec, } 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, + /// 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, + /// 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, #[serde(default)] + pub references: Vec, + #[serde(default)] pub kind: CardKind, #[serde(default)] pub data: Option, @@ -839,6 +883,7 @@ impl From 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 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!(