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
+70
View File
@@ -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(),
}