⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229
crates/ente-zero/src/brain_glue.rs
Historia
Quién tocó qué
crudo
Pausa: 11 crates del fractal Ente #0 con cerebro completo
1//! Glue entre el bucle primordial y `ente-brain`.
2//!
3//! Tres responsabilidades:
4//! 1. Traducir eventos del grafo (`GraphEvent`) a `ente_brain::EventKind`
5//! + `SubjectInfo` para el observador y el motor.
6//! 2. Implementar `ActionSink` para que las Acciones del cerebro tengan
7//! un canal de salida hacia el grafo (Spawn → SpawnRequest, etc.).
8//! 3. Encapsular el snapshot de SubjectInfo desde el grafo sin filtrar
9//! detalles internos al cerebro.
10
11use crate::events::GraphEvent;
12use crate::graph::EnteGraph;
13use ente_brain::{ActionSink, EventKind as BrainEventKind, SubjectInfo};
14use ente_card::Capability;
15use serde::Deserialize;
16use tokio::sync::mpsc;
17use tracing::warn;
18use ulid::Ulid;
19
20/// Traduce un GraphEvent a (EventKind, SubjectInfo) para alimentar el cerebro.
21///
22/// Devuelve `None` para eventos puramente internos del bus (Response, Close)
23/// que no son interesantes para reglas o estadística.
24pub fn graph_event_to_brain<'a>(
25 evt: &'a GraphEvent,
26 graph: &EnteGraph,
27) -> Option<(BrainEventKind, SubjectInfo)> {
28 match evt {
29 GraphEvent::EnteDied { id, .. } => {
30 Some((BrainEventKind::EnteDied, subject_info_for(graph, *id)))
31 }
32 GraphEvent::SpawnRequest { card, .. } => {
33 // El "sujeto" del spawn es el child que va a nacer.
34 let info = SubjectInfo {
35 id: Some(card.id),
36 label: Some(card.label.clone()),
37 capabilities: card.provides.iter().cloned().collect(),
38 };
39 Some((BrainEventKind::EnteSpawned, info))
40 }
41 GraphEvent::BusRequest { from, request, .. } => {
42 let kind = match request {
43 ente_bus::BusRequest::Announce { .. } => BrainEventKind::BusAnnounce,
44 ente_bus::BusRequest::Invoke { cap, .. } => {
45 BrainEventKind::BusInvokeOf(cap.clone())
46 }
47 _ => BrainEventKind::BusInvoke,
48 };
49 let info = match from {
50 Some(id) => subject_info_for(graph, *id),
51 None => SubjectInfo::default(),
52 };
53 Some((kind, info))
54 }
55 GraphEvent::CapabilityRequested { from, .. } => {
56 Some((BrainEventKind::BusInvoke, subject_info_for(graph, *from)))
57 }
58 // Responses, ConnClosed, Shutdown — irrelevantes para reglas
59 _ => None,
60 }
61}
62
63fn subject_info_for(graph: &EnteGraph, id: Ulid) -> SubjectInfo {
64 // Acceso de sólo lectura — usamos el método público lookup_pid + cards
65 // virtuales en el grafo. Si el Ente no existe (ya disuelto), info vacía.
66 if let Some(card) = graph.card_for(&id) {
67 SubjectInfo {
68 id: Some(id),
69 label: Some(card.label.clone()),
70 capabilities: card.provides.iter().cloned().collect(),
71 }
72 } else {
73 SubjectInfo { id: Some(id), label: None, capabilities: Vec::new() }
74 }
75}
76
77/// `ActionSink` que enruta acciones del cerebro al bucle primordial.
78pub struct GraphSink {
79 pub graph_tx: mpsc::Sender<GraphEvent>,
80 pub requester: Ulid,
81}
82
83impl ActionSink for GraphSink {
84 fn spawn(&self, card_blob: &str) {
85 // El blob es JSON de EntityCard.
86 match serde_json::from_str::<ente_card::EntityCard>(card_blob) {
87 Ok(card) => {
88 let evt = GraphEvent::SpawnRequest { card, requester: self.requester };
89 if self.graph_tx.try_send(evt).is_err() {
90 warn!("brain spawn: graph_tx lleno o cerrado");
91 }
92 }
93 Err(e) => warn!(?e, "brain spawn: blob no parseable como EntityCard JSON"),
94 }
95 }
96
97 fn invoke(&self, target_cap: Capability, blob: Vec<u8>) {
98 // Sin BusClient en proceso — el sink registra la intención. Una mejora
99 // futura: spawn un BusClient::connect + call. Por ahora log estructurado.
100 warn!(?target_cap, blob_len = blob.len(), "brain invoke: no bus client en glue (TODO)");
101 }
102
103 fn notify(&self, target_id: Ulid, message: &str) {
104 warn!(%target_id, %message, "brain notify: no implementado en glue");
105 }
106
107 fn inhibit(&self, reason: &str) {
108 warn!(%reason, "brain inhibit: no implementado en glue");
109 }
110}
111
112/// Helper para que el grafo exponga la Card de un Ente vivo. Lo añadimos como
113/// trait extension porque graph::EnteGraph mantiene `incarnated` privado.
114pub trait GraphCardLookup {
115 fn card_for(&self, id: &Ulid) -> Option<&ente_card::EntityCard>;
116}
117
118impl GraphCardLookup for EnteGraph {
119 fn card_for(&self, id: &Ulid) -> Option<&ente_card::EntityCard> {
120 // Acceso vía método público que añadiremos en graph/mod.rs.
121 self.peek_card(id)
122 }
123}
124
125// Eliminar el campo `_unused` que rustc puede quejarse — placeholder para
126// evitar warning si algún field queda sin uso.
127#[allow(dead_code)]
128#[derive(Deserialize)]
129struct _Touch {}