feat: Phase B-1 — unificación ontológica de Cards (Ente ↔ Data)
La Card pasa a ser EL protocolo de presentación del ecosistema. 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.
brahman-card:
- CardKind { Ente (default), Data }. Backward-compat: Cards existentes
quedan Ente.
- DataFacet { summary, keywords, centroid, member_count, dispersion,
presentation_hint } — vista liviana para el wire. Listas grandes
(members reales, embeddings completos) se consultan al daemon dueño
bajo demanda.
- Card.kind y Card.data agregados. WireCard espeja, conversiones From
propagan ambos campos.
- Default impl actualizado.
brahman-broker:
- BrokeredCard propaga kind y data desde la Card registrada. No afecta
el matching (sigue por TypeRef + priority + pin_to); permite a
observadores discriminar sin re-query.
nouser-card:
- Depende ahora de brahman-card.
- MonadManifest::to_brahman_card() proyecta una Mónada a Card brahman:
- id, label, lineage directos.
- payload Virtual, supervision Delegate, lifecycle Daemon
(placeholder — la Mónada no se ejecuta).
- kind = Data.
- data = Some(DataFacet { summary, keywords, centroide,
member_count, entropy → dispersion, presentation_hint del Lens }).
- Test nuevo projects_to_brahman_card.
brahman-status:
- Prefijo [ente] o [data] por sesión.
- Sesiones data renderean también summary, members + dispersion,
keywords y lens hint.
Resultado: la UI ve una sola lista uniforme — no necesita saber si
mira procesos o cúmulos de datos, sólo lee el Card y se adapta por
kind. La función de presentarse es la misma para todos.
Tests: 59 (card 11, broker 15, handshake codec+tr 2 + integ 7,
card-wit 4, admin 0, nouser-card 7 +1, nouser-core 13).
cargo check --workspace: 0 errores, 0 warnings.
Próximo: Phase B-2 — bin nouser daemon que sidecarea cada Mónada como
sesión brahman, mezclándolas con los entes en brahman-status.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b85700c5385d53766ac531a12fe324c4c1007066 · ruwaq Sergio · 2026-05-08T18:20:51Z (5 killa ñawpaq) · tayta 7bdc26e61 · kay commitpi sach'ata qhaway
Semántico sayk'uy · 12
crates/core/brahman-admin/examples/brahman-status.rs
ukhunpi t'ikrakurqan fn main
crates/core/brahman-broker/src/lib.rs
crates/core/brahman-card/src/lib.rs
silq'uyninpi t'ikrakurqan struct Card pub musuq enum CardKind pub musuq struct DataFacet pub silq'uyninpi t'ikrakurqan struct WireCard pub ukhunpi t'ikrakurqan fn default ukhunpi t'ikrakurqan fn from ukhunpi t'ikrakurqan fn from
crates/modules/nouser/card/src/lib.rs
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 <sha>`.
## 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<DataFacet>` 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 <dir>` 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<String, ContextBias>,
+ /// 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<DataFacet>,
}
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<DataFacet>,
+
/// Hijas a instanciar inmediatamente al encarnar esta Card.
#[serde(default)]
pub genesis: Vec<Card>,
@@ -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<String>,
+ /// Centroide vectorial. Vacío si no hay embeddings calculados.
+ #[serde(default)]
+ pub centroid: Vec<f32>,
+ /// 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<WireCard>,
#[serde(default)]
+ pub kind: CardKind,
+ #[serde(default)]
+ pub data: Option<DataFacet>,
+ #[serde(default)]
pub priority_contexts: BTreeMap<String, ContextBias>,
}
@@ -761,6 +827,8 @@ impl From<Card> 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<WireCard> 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");