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>
This commit is contained in:
Sergio
2026-05-08 18:20:51 +00:00
parent 7bdc26e61a
commit b85700c538
7 changed files with 231 additions and 3 deletions
+1
View File
@@ -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 }
+74
View File
@@ -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");