diff --git a/CHANGELOG.md b/CHANGELOG.md index cd16c20..24feb81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,56 @@ ratio/diff ver `git show `. ## 2026-05-08 +### feat: Phase B-1 — unificación ontológica de Cards (Ente ↔ Data) +La Card es **el** protocolo de presentación del ecosistema, no sólo de +los procesos. Una Mónada Nouser y un Ente Brahman son ambos "entidades +que se presentan"; el consumidor (UI, broker, admin) discrimina por +`kind` cuando importa, pero todos hablan el mismo idioma. + +Cambios: + +- `brahman-card`: + - `CardKind { Ente (default), Data }`. Conserva back-compat: + Cards existentes son `Ente` por default. + - `DataFacet { summary, keywords, centroid, member_count, dispersion, + presentation_hint }`. Liviano para el wire — listas grandes + (members, embeddings completos) se consultan al daemon dueño bajo + demanda. + - `Card.kind` y `Card.data: Option` agregados. WireCard + espeja, conversiones `From` propagan. + - Default impl actualizado. + +- `brahman-broker::BrokeredCard`: propaga `kind` y `data` desde la Card + registrada. No afecta el matching (sigue siendo por TypeRef + + priority + pin_to); permite a observadores discriminar sin re-query. + +- `nouser-card`: depende ahora de `brahman-card`. Nuevo método + `MonadManifest::to_brahman_card()` que proyecta: + - id, label, lineage → directos. + - payload Virtual, supervision Delegate, lifecycle Daemon (placeholder + semántico — la Mónada no se ejecuta). + - kind = Data. + - data = Some(DataFacet) con summary, keywords, centroide, + member_count, entropy → dispersion, y un `presentation_hint` derivado + del `Lens` (`Code` → `"code"`, `Gallery` → `"gallery"`, etc.). + - Test nuevo: `projects_to_brahman_card`. + +- `brahman-status`: cada sesión muestra ahora `[ente]` o `[data]` como + prefijo. Para sesiones `data`, render adicional con summary, members + + dispersion, keywords y lens hint. + +Resultado: la UI (yahweh, brahman-status, futuro explorer) ve una sola +lista uniforme. No tiene que saber si está mirando un proceso o un +cúmulo de datos — sólo lee el Card y se adapta por `kind`. + +Tests acumulados: 59 (card 11, broker 15, handshake codec+transport 2 + +integ 7, card-wit 4, admin 0, nouser-card 7, nouser-core 13). +cargo check --workspace: 0 errores, 0 warnings. + +Próximo: **Phase B-2** — bin `nouser daemon ` que sidecarea cada +Mónada como una sesión brahman, publicándola al broker. Brahman-status +las verá junto a los entes. + ### feat(nouser): Phase A — mecanismo determinista de Mónadas Primer trozo del módulo Nouser (Kairos): explorador de Mónadas como "imanes semánticos" sobre el filesystem. Phase A cubre el 90% de los diff --git a/Cargo.lock b/Cargo.lock index 34ded5b..baaf039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6049,6 +6049,7 @@ dependencies = [ name = "nouser-card" version = "0.1.0" dependencies = [ + "brahman-card", "serde", "serde_json", "thiserror 2.0.18", diff --git a/crates/core/brahman-admin/examples/brahman-status.rs b/crates/core/brahman-admin/examples/brahman-status.rs index d8dc44c..b35ce97 100644 --- a/crates/core/brahman-admin/examples/brahman-status.rs +++ b/crates/core/brahman-admin/examples/brahman-status.rs @@ -24,10 +24,31 @@ async fn main() -> anyhow::Result<()> { } else { for s in &snap.sessions { let conscious_marker = if s.wit.is_some() { " 🧠" } else { "" }; + let kind_marker = match s.kind { + brahman_card::CardKind::Ente => "ente", + brahman_card::CardKind::Data => "data", + }; println!( - " {} {}{} lifecycle={:?} priority={:?}", - s.session, s.label, conscious_marker, s.lifecycle, s.priority + " [{}] {} {}{} lifecycle={:?} priority={:?}", + kind_marker, s.session, s.label, conscious_marker, s.lifecycle, s.priority ); + if let Some(data) = &s.data { + if !data.summary.is_empty() { + println!(" summary: {}", data.summary); + } + if data.member_count > 0 { + println!( + " members: {} (dispersion={:.2})", + data.member_count, data.dispersion + ); + } + if !data.keywords.is_empty() { + println!(" keywords: {}", data.keywords.join(", ")); + } + if !data.presentation_hint.is_empty() { + println!(" lens hint: {}", data.presentation_hint); + } + } if let Some(wit) = &s.wit { println!(" wit: {} / {}", wit.package, wit.world); if !wit.imports.is_empty() { diff --git a/crates/core/brahman-broker/src/lib.rs b/crates/core/brahman-broker/src/lib.rs index 33799d8..58e2a19 100644 --- a/crates/core/brahman-broker/src/lib.rs +++ b/crates/core/brahman-broker/src/lib.rs @@ -30,7 +30,9 @@ use std::collections::BTreeMap; -use brahman_card::{Card, ContextBias, Flow, Lifecycle, Priority, TypeRef, WitInterface}; +use brahman_card::{ + Card, CardKind, ContextBias, DataFacet, Flow, Lifecycle, Priority, TypeRef, WitInterface, +}; use serde::{Deserialize, Serialize}; use ulid::Ulid; @@ -77,6 +79,13 @@ pub struct BrokeredCard { /// Biases per-contexto, propagados desde `Card.priority_contexts`. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub priority_contexts: BTreeMap, + /// Naturaleza de la entidad. Diferencia procesos (Ente) de + /// agrupaciones de datos (Data — p. ej. Mónadas Nouser). + #[serde(default)] + pub kind: CardKind, + /// Faceta de datos cuando `kind != Ente`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data: Option, } impl BrokeredCard { @@ -90,6 +99,8 @@ impl BrokeredCard { outputs: card.flow.output.clone(), wit, priority_contexts: card.priority_contexts.clone(), + kind: card.kind, + data: card.data.clone(), } } } diff --git a/crates/core/brahman-card/src/lib.rs b/crates/core/brahman-card/src/lib.rs index ae364e6..ed48cde 100644 --- a/crates/core/brahman-card/src/lib.rs +++ b/crates/core/brahman-card/src/lib.rs @@ -126,6 +126,16 @@ pub struct Card { #[serde(default)] pub flow: Flows, + /// Naturaleza de la entidad detrás de la Card. Por defecto `Ente` + /// para mantener compatibilidad con Cards existentes. + #[serde(default)] + pub kind: CardKind, + + /// Faceta de datos cuando `kind != Ente`. `None` para entes + /// runtime; `Some(...)` para Mónadas, índices, etc. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data: Option, + /// Hijas a instanciar inmediatamente al encarnar esta Card. #[serde(default)] pub genesis: Vec, @@ -166,6 +176,8 @@ impl Default for Card { priority: Priority::default(), flow: Flows::default(), genesis: Vec::new(), + kind: CardKind::default(), + data: None, priority_contexts: BTreeMap::new(), extensions: BTreeMap::new(), } @@ -382,6 +394,56 @@ pub enum Lifecycle { Widget, } +/// Naturaleza de la entidad detrás de la Card. +/// +/// La función de presentarse es la misma para todos: tener identidad, +/// resumen, capacidades, y poder ser encontrada por otros. Pero NO todas +/// las entidades son procesos — algunas son agrupaciones de datos +/// (Mónadas de Nouser, índices, streams). +/// +/// El kind permite a consumidores (UI, broker, observadores) discriminar +/// sólo cuando importa, pero todos hablan el mismo protocolo de Card. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CardKind { + /// Entidad runtime con `payload`/`soma`/`supervision` activos + /// (proceso, módulo, daemon). + #[default] + Ente, + /// Agrupación de datos sin proceso detrás (Mónadas Nouser, índices, + /// resultados cacheados). `payload` típicamente `Virtual`. + Data, +} + +/// Faceta de datos: campos relevantes cuando `Card.kind != Ente`. +/// +/// Optimizada para el wire — incluye sólo metadatos de presentación, NO +/// listas pesadas (los miembros, embeddings completos, etc. se consultan +/// al daemon dueño bajo demanda). El "presentation_hint" es un string +/// libre que la UI mapea a su lente (p. ej. `"code"` → editor de código). +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct DataFacet { + /// Resumen humano (1-2 oraciones). Generado por el daemon dueño. + #[serde(default)] + pub summary: String, + /// Tokens dominantes / palabras clave (5-10 típicamente). + #[serde(default)] + pub keywords: Vec, + /// Centroide vectorial. Vacío si no hay embeddings calculados. + #[serde(default)] + pub centroid: Vec, + /// Cantidad de elementos contenidos (archivos, registros, ...). + #[serde(default)] + pub member_count: u32, + /// Métrica de dispersión interna [0, 1] (típicamente entropía). + #[serde(default)] + pub dispersion: f32, + /// Hint de presentación. Strings libres como `"code"`, `"gallery"`, + /// `"markdown"`, `"database"`, `"grid"`, `"tree"`. La UI los mapea. + #[serde(default)] + pub presentation_hint: String, +} + /// Prioridad de scheduling. Orden: `Low < Normal < High < Critical` — /// usable como tiebreaker en el broker (mayor priority gana). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] @@ -741,6 +803,10 @@ pub struct WireCard { #[serde(default)] pub genesis: Vec, #[serde(default)] + pub kind: CardKind, + #[serde(default)] + pub data: Option, + #[serde(default)] pub priority_contexts: BTreeMap, } @@ -761,6 +827,8 @@ impl From for WireCard { priority: c.priority, flow: c.flow, genesis: c.genesis.into_iter().map(WireCard::from).collect(), + kind: c.kind, + data: c.data, priority_contexts: c.priority_contexts, } } @@ -783,6 +851,8 @@ impl From for Card { priority: w.priority, flow: w.flow, genesis: w.genesis.into_iter().map(Card::from).collect(), + kind: w.kind, + data: w.data, priority_contexts: w.priority_contexts, extensions: BTreeMap::new(), } diff --git a/crates/modules/nouser/card/Cargo.toml b/crates/modules/nouser/card/Cargo.toml index 4409b30..5fdfb49 100644 --- a/crates/modules/nouser/card/Cargo.toml +++ b/crates/modules/nouser/card/Cargo.toml @@ -9,6 +9,7 @@ publish.workspace = true description = "Nouser — manifiesto de Mónada (agrupación semántica de archivos). Espejo de brahman-card pero para datos." [dependencies] +brahman-card = { path = "../../../core/brahman-card" } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/modules/nouser/card/src/lib.rs b/crates/modules/nouser/card/src/lib.rs index 1a55a6d..887651b 100644 --- a/crates/modules/nouser/card/src/lib.rs +++ b/crates/modules/nouser/card/src/lib.rs @@ -266,6 +266,56 @@ impl MonadManifest { .map(|d| d.as_millis() as u64) .unwrap_or(0); } + + /// Proyecta el `MonadManifest` a la `brahman_card::Card` que viaja + /// por el protocolo. La Card resultante: + /// + /// - hereda `id` y `label` del manifiesto (ULID estable). + /// - `kind = CardKind::Data` (se distingue de un Ente). + /// - `payload = Virtual`, `supervision = Delegate`, + /// `lifecycle = Daemon` — placeholder semántico: la Mónada no se + /// "ejecuta", el daemon dueño la mantiene viva. + /// - `data = Some(DataFacet { ... })` con summary, keywords, + /// centroide, member_count, dispersión y un hint de presentación + /// derivado del `dominant_lens`. + /// - Los miembros completos NO viajan en la Card — se consultan al + /// daemon dueño bajo demanda. Lo que viaja es metadata liviana + /// apta para el wire postcard. + pub fn to_brahman_card(&self) -> brahman_card::Card { + use brahman_card::{ + Card, CardKind, DataFacet, Lifecycle, Payload, Priority, Supervision, + }; + + let presentation_hint = match self.dominant_lens { + Lens::Grid => "grid", + Lens::Code => "code", + Lens::Gallery => "gallery", + Lens::Database => "database", + Lens::Markdown => "markdown", + Lens::Tree => "tree", + } + .to_string(); + + Card { + schema_version: brahman_card::CARD_SCHEMA_VERSION, + id: self.id, + label: self.label.clone(), + payload: Payload::Virtual, + supervision: Supervision::Delegate, + lifecycle: Lifecycle::Daemon, + priority: Priority::Normal, + kind: CardKind::Data, + data: Some(DataFacet { + summary: self.summary.clone(), + keywords: self.keywords.clone(), + centroid: self.centroid.clone(), + member_count: self.cardinality, + dispersion: self.entropy, + presentation_hint, + }), + ..Default::default() + } + } } #[cfg(test)] @@ -315,6 +365,30 @@ mod tests { )); } + #[test] + fn projects_to_brahman_card() { + let mut m = MonadManifest::new("test-monad"); + m.summary = "monad de prueba".into(); + m.keywords = vec!["rs".into(), "toml".into()]; + m.dominant_lens = Lens::Code; + m.entropy = 0.42; + m.members.insert(Ulid::new()); + m.members.insert(Ulid::new()); + m.members.insert(Ulid::new()); + m.touch(); + + let bc = m.to_brahman_card(); + assert_eq!(bc.id, m.id); + assert_eq!(bc.label, "test-monad"); + assert_eq!(bc.kind, brahman_card::CardKind::Data); + let data = bc.data.expect("data facet presente"); + assert_eq!(data.summary, "monad de prueba"); + assert_eq!(data.keywords, vec!["rs".to_string(), "toml".to_string()]); + assert_eq!(data.member_count, 3); + assert!((data.dispersion - 0.42).abs() < 1e-6); + assert_eq!(data.presentation_hint, "code"); + } + #[test] fn json_roundtrip() { let mut m = MonadManifest::new("test-monad");