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:
@@ -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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
Reference in New Issue
Block a user