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:
@@ -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
|
||||
|
||||
Generated
+1
@@ -6049,6 +6049,7 @@ dependencies = [
|
||||
name = "nouser-card"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"brahman-card",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user