refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG

Reorganización física de crates/:
- core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/
- shared/ (3 crates) se redistribuye en protocol/ e init/
- lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/

Renames de proyectos:
- shipote → shuma (runtime de sandboxes)
- nouser → akasha (explorador de Mónadas)
- yahweh → nahual (motor GPUI, antes ui_engine/)
- lapaloma → pineal (data-viz agnóstica)

Fraccionamiento UI → core agnóstico:
- vista-core (DeckState + snap, 175 LOC, 5 tests verdes)
- barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes)
- vista-web y barra-web ahora son thin DOM bindings

Documentación nueva:
- 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat
  + 10 módulos + apps/
- docs/STATUS.md con cifras reales por proyecto
- docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas)
- CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets)

Automatización:
- scripts/reorg.py — script idempotente que: git mv directorios, renombra
  package names, recomputa path = refs, reescribe imports rust, actualiza
  workspace Cargo.toml. Soporta --dry-run.
- scripts/split-changelog.py — particiona CHANGELOG por componente.

Validación:
- cargo check --workspace pasa (124 crates + 2 nuevos cores).
- 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-19 14:48:34 +00:00
parent 86fb6ae20b
commit 550c98f275
375 changed files with 8512 additions and 7155 deletions
+29
View File
@@ -0,0 +1,29 @@
# modules/akasha/ — Explorador semántico de Mónadas (era nouser)
**Propósito.** Daemon que descubre y consulta "Mónadas" (unidades
semánticas auto-descriptivas) vía broker brahman. Provee embeddings
locales (mock o real LLM) y un protocolo `Nous` line-delimited.
## Crates
| crate | tipo | rol |
| --------------- | ----- | --------------------------------------------------------- |
| `akasha-card` | lib | Definición Card del daemon + capabilities |
| `akasha-core` | bin | Daemon: scanner FS + DB sled + cluster por embedding |
| `akasha-nous` | lib | Protocolo Nous (JSON line-delimited) |
| `akasha-nous-mock` | bin | Proveedor de embeddings deterministas (testing) |
| `akasha-nous-real` | bin | Proveedor con fastembed/ort (LLM real) |
## Dependencias
- `akasha-core``protocol/brahman-card`, `protocol/brahman-sidecar`,
`akasha-card`, `akasha-nous`, `shuma-discern`.
- `akasha-nous-real``fastembed` + `ort` (heavy; profile opt-level=1
en root Cargo.toml).
- Consumido por: `apps/akasha-explorer` (GPUI dashboard).
## Estado
LOC 4,395. Pipeline de scan + embed + cluster funcional. Pendiente:
cobertura de tests sobre el cluster engine (k-means actual es naive).
Ver `docs/changelog/akasha.md`.
@@ -1,5 +1,5 @@
[package]
name = "nouser-card"
name = "akasha-card"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -9,7 +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" }
brahman-card = { path = "../../../protocol/brahman-card" }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
@@ -1,4 +1,4 @@
//! `nouser-card` — manifiesto de Mónada.
//! `akasha-card` — manifiesto de Mónada.
//!
//! Una **Mónada** es una agrupación semántica de archivos: el archivo
//! físico no se mueve, pero su pertenencia se modela por un objeto
@@ -10,7 +10,7 @@
//!
//! Diferencia con `brahman-card::Card`:
//!
//! | brahman::Card | nouser::MonadManifest |
//! | brahman::Card | akasha::MonadManifest |
//! |-------------------------------------|-------------------------------|
//! | Describe una **entidad runtime** | Describe una **agrupación** |
//! | Tiene `payload`/`soma`/`supervision`| No tiene proceso detrás |
@@ -18,7 +18,7 @@
//! | Fluye por handshake/postcard | Fluye por queries del backend |
//!
//! Este crate sólo define los tipos. La lógica de scan, cluster,
//! attraction vive en `nouser-core`.
//! attraction vive en `akasha-core`.
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
@@ -71,7 +71,7 @@ pub struct FileEntry {
// Lens — la "vista" preferida de una Mónada
// =====================================================================
/// Lente de visualización dominante. La UI (yahweh) elige cómo renderizar
/// Lente de visualización dominante. La UI (nahual) elige cómo renderizar
/// los miembros de una Mónada según este hint.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
@@ -1,12 +1,12 @@
//! Wire types para consultar al daemon `nouser` por sus Mónadas.
//! Wire types para consultar al daemon `akasha` por sus Mónadas.
//!
//! El daemon expone un Unix socket (cuyo path se publica en
//! `Card.service_socket` y se descubre vía broker MatchEvent). Cada
//! conexión es single-shot: una request JSON terminada en `\n`,
//! una response JSON terminada en `\n`, cierre.
//!
//! Mismo patrón que `nouser-nous` (mock/real ↔ nouser-core), reusado
//! ahora para que la UI (`nouser-explorer`) descubra y consulte al
//! Mismo patrón que `akasha-nous` (mock/real ↔ akasha-core), reusado
//! ahora para que la UI (`akasha-explorer`) descubra y consulte al
//! daemon sin hardcodear sockets ni pasar por brahman-admin.
//!
//! ## Contrato
@@ -118,7 +118,7 @@ impl MonadView {
/// Error de protocolo retornado en lugar de la response normal.
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
#[error("nouser-engine: {error}")]
#[error("akasha-engine: {error}")]
pub struct ErrorResponse {
pub error: String,
}
@@ -135,7 +135,7 @@ pub mod transport {
pub const SOCKET_ENV: &str = "NOUSER_ENGINE_SOCKET";
/// Nombre por defecto del socket.
pub const SOCKET_NAME: &str = "nouser-engine.sock";
pub const SOCKET_NAME: &str = "akasha-engine.sock";
/// Ruta canónica al socket del daemon. Honra `NOUSER_ENGINE_SOCKET`
/// si está set, sino arma sobre `$XDG_RUNTIME_DIR` (con fallback
@@ -154,7 +154,7 @@ pub mod transport {
// =====================================================================
// Cliente blocking — vive con los wire types para que un consumer
// (UI, CLI, otro módulo) pueda hablar con el daemon importando sólo
// `nouser-card`, sin arrastrar `nouser-core` (notify/walkdir/sled/blake3).
// `akasha-card`, sin arrastrar `akasha-core` (notify/walkdir/sled/blake3).
// =====================================================================
/// Cliente síncrono para el query socket del daemon. Sólo Unix (el
@@ -1,5 +1,5 @@
[package]
name = "nouser-core"
name = "akasha-core"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -9,12 +9,12 @@ publish.workspace = true
description = "Nouser — explorador de Mónadas: scanner, clustering determinista, DB en memoria."
[dependencies]
nouser-card = { path = "../card" }
nouser-nous = { path = "../nous" }
shipote-discern = { path = "../../shipote/shipote-discern" }
brahman-card = { path = "../../../core/brahman-card" }
brahman-handshake = { path = "../../../core/brahman-handshake" }
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
akasha-card = { path = "../card" }
akasha-nous = { path = "../nous" }
shuma-discern = { path = "../../shuma/shuma-discern" }
brahman-card = { path = "../../../protocol/brahman-card" }
brahman-handshake = { path = "../../../protocol/brahman-handshake" }
brahman-sidecar = { path = "../../../protocol/brahman-sidecar" }
blake3 = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -29,5 +29,5 @@ notify = { workspace = true }
tempfile = { workspace = true }
[[bin]]
name = "nouser"
name = "akasha"
path = "src/bin/nouser.rs"
@@ -1,4 +1,4 @@
//! `nouser` CLI — explorador de Mónadas.
//! `akasha` CLI — explorador de Mónadas.
//!
//! Subcomandos:
//!
@@ -13,14 +13,14 @@
use std::path::PathBuf;
use std::process::ExitCode;
use nouser_core::{
use akasha_core::{
cluster, db, embed,
scanner::{self, ScanConfig},
};
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
let prog = args.first().cloned().unwrap_or_else(|| "nouser".into());
let prog = args.first().cloned().unwrap_or_else(|| "akasha".into());
let sub = match args.get(1).map(String::as_str) {
Some(s) => s,
None => {
@@ -41,7 +41,7 @@ fn main() -> ExitCode {
return ExitCode::SUCCESS;
}
other => {
eprintln!("nouser: comando desconocido '{other}'");
eprintln!("akasha: comando desconocido '{other}'");
print_usage(&prog);
return ExitCode::from(2);
}
@@ -50,7 +50,7 @@ fn main() -> ExitCode {
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("nouser: {e}");
eprintln!("akasha: {e}");
ExitCode::from(1)
}
}
@@ -181,7 +181,7 @@ fn cmd_daemon(args: &[String]) -> Cmd {
// 1. Decidir el path del query socket ANTES de armar el engine
// Card (porque viaja como service_socket en la Card).
let query_socket = nouser_card::query::transport::default_socket_path();
let query_socket = akasha_card::query::transport::default_socket_path();
// 2. Engine como Ente. Declara service_socket + flow.output para
// que el broker pueda emitir MatchEvent::Available a consumers
@@ -190,7 +190,7 @@ fn cmd_daemon(args: &[String]) -> Cmd {
let engine_id = engine_card.id;
let engine_label = engine_card.label.clone();
eprintln!(
"nouser daemon: publicando engine '{}' (kind=Ente, id={}, socket={})",
"akasha daemon: publicando engine '{}' (kind=Ente, id={}, socket={})",
engine_label,
engine_id,
query_socket.display()
@@ -227,7 +227,7 @@ fn cmd_daemon(args: &[String]) -> Cmd {
hydrated += 1;
}
eprintln!(
"nouser daemon: hidratadas {} mónadas previas{} en O(1)",
"akasha daemon: hidratadas {} mónadas previas{} en O(1)",
hydrated,
if skipped_model > 0 {
format!(" ({} dropeadas por centroid_model distinto)", skipped_model)
@@ -245,7 +245,7 @@ fn cmd_daemon(args: &[String]) -> Cmd {
let monads = cluster::by_directory_hydrated(&files, min_files(), Some(&db));
let scanned_count = monads.len();
eprintln!(
"nouser daemon: re-scan {} archivos en {} → {} mónadas",
"akasha daemon: re-scan {} archivos en {} → {} mónadas",
n_files,
dir.display(),
scanned_count
@@ -276,7 +276,7 @@ fn cmd_daemon(args: &[String]) -> Cmd {
db.replace_monads(monads);
eprintln!(
"nouser daemon: 1 ente + {} mónadas vivas ({} nuevas vs hidratación)",
"akasha daemon: 1 ente + {} mónadas vivas ({} nuevas vs hidratación)",
scanned_count, newly_spawned
);
@@ -285,8 +285,8 @@ fn cmd_daemon(args: &[String]) -> Cmd {
// Si el bind falla, seguimos sin él — la UI degrada a "no
// alcanzable" pero el daemon sigue procesando cambios.
let db_shared = std::sync::Arc::new(std::sync::Mutex::new(db));
let _query_listener = match nouser_core::engine_socket::spawn_listener(
nouser_core::engine_socket::ListenerConfig {
let _query_listener = match akasha_core::engine_socket::spawn_listener(
akasha_core::engine_socket::ListenerConfig {
socket_path: query_socket.clone(),
engine_id,
engine_label: engine_label.clone(),
@@ -296,14 +296,14 @@ fn cmd_daemon(args: &[String]) -> Cmd {
) {
Ok(h) => {
eprintln!(
"nouser daemon: query socket activo en {} (proto: nouser_card::query)",
"akasha daemon: query socket activo en {} (proto: akasha_card::query)",
query_socket.display()
);
Some(h)
}
Err(e) => {
eprintln!(
"nouser daemon: query socket NO disponible ({e}) — explorer no podrá consultar"
"akasha daemon: query socket NO disponible ({e}) — explorer no podrá consultar"
);
None
}
@@ -322,14 +322,14 @@ fn cmd_daemon(args: &[String]) -> Cmd {
) {
Ok(w) => {
eprintln!(
"nouser daemon: watcher activo en {} (debounce 150ms, re-publish on) — Ctrl-C para terminar.",
"akasha daemon: watcher activo en {} (debounce 150ms, re-publish on) — Ctrl-C para terminar.",
dir.display()
);
Some(w)
}
Err(e) => {
eprintln!(
"nouser daemon: watcher deshabilitado ({e}) — Ctrl-C para terminar."
"akasha daemon: watcher deshabilitado ({e}) — Ctrl-C para terminar."
);
None
}
@@ -385,7 +385,7 @@ fn spawn_fs_watcher(
// Dispatcher: notify → filtro → canal de paths.
let dispatch_dir = dir.clone();
std::thread::Builder::new()
.name("nouser-watcher-dispatch".into())
.name("akasha-watcher-dispatch".into())
.spawn(move || {
for res in notify_rx {
let event = match res {
@@ -416,7 +416,7 @@ fn spawn_fs_watcher(
// Coordinator: debounce + batch dispatch.
let coord_dir = dir;
std::thread::Builder::new()
.name("nouser-watcher-coord".into())
.name("akasha-watcher-coord".into())
.spawn(move || {
let debounce = std::time::Duration::from_millis(WATCHER_DEBOUNCE_MS);
let mut pending: std::collections::HashMap<
@@ -494,7 +494,7 @@ fn process_change_batch(
}
};
let prior_monads: Vec<nouser_card::MonadManifest> = db_lock.monads().cloned().collect();
let prior_monads: Vec<akasha_card::MonadManifest> = db_lock.monads().cloned().collect();
let prior_ref: &db::MonadDb = &db_lock;
let monads = cluster::by_directory_hydrated(&files, min_files(), Some(prior_ref));
@@ -601,8 +601,8 @@ fn cmd_attract(args: &[String]) -> Cmd {
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let target = nouser_card::FileEntry {
id: nouser_card::FileId::from(nouser_card::ulid::Ulid::new()),
let target = akasha_card::FileEntry {
id: akasha_card::FileId::from(akasha_card::ulid::Ulid::new()),
path: file_path.clone(),
content_hash: None,
size: metadata.len(),
@@ -630,7 +630,7 @@ fn cmd_attract(args: &[String]) -> Cmd {
// Filtramos Mónadas cuyo centroid_model NO matchee. Mezclar
// 32-d con 384-d daría scores sin sentido (diferente semántica
// y cosine no compara cross-modelo).
let mut ranked: Vec<(&nouser_card::MonadManifest, f32)> = db
let mut ranked: Vec<(&akasha_card::MonadManifest, f32)> = db
.monads()
.filter(|m| !m.centroid.is_empty())
.filter(|m| match &m.centroid_model {
@@ -700,7 +700,7 @@ fn cmd_attract(args: &[String]) -> Cmd {
/// Devuelve `(embedding, model_id)` — el caller necesita ambos para
/// comparar contra centroides taggeados con su mismo `centroid_model`.
fn remote_embed(
file: &nouser_card::FileEntry,
file: &akasha_card::FileEntry,
) -> Result<(Vec<f32>, String), Box<dyn std::error::Error>> {
if let Ok(explicit) = std::env::var("NOUSER_NOUS_SOCKET") {
let sock = std::path::PathBuf::from(explicit);
@@ -708,9 +708,9 @@ fn remote_embed(
}
let consumer = brahman_sidecar::build_consumer_card(
"nouser.attract-cli",
nouser_nous::FLOW_EMBED_RESULT,
nouser_nous::FLOW_TYPE_NAME,
"akasha.attract-cli",
akasha_nous::FLOW_EMBED_RESULT,
akasha_nous::FLOW_TYPE_NAME,
);
let producer_sock = brahman_sidecar::await_provider_blocking(
consumer,
@@ -719,12 +719,12 @@ fn remote_embed(
embed_via(&producer_sock, file)
}
/// RPC blocking contra un socket nouser-nous concreto. Devuelve
/// RPC blocking contra un socket akasha-nous concreto. Devuelve
/// `(embedding, model_id)` — el `model_id` viaja en la response y
/// permite al caller saber qué centroides son comparables.
fn embed_via(
sock_path: &std::path::Path,
file: &nouser_card::FileEntry,
file: &akasha_card::FileEntry,
) -> Result<(Vec<f32>, String), Box<dyn std::error::Error>> {
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
@@ -734,9 +734,9 @@ fn embed_via(
}
let mut stream = UnixStream::connect(sock_path)?;
let req = nouser_nous::EmbedRequest {
kind: nouser_nous::RequestKind::EmbedFile,
payload: serde_json::to_value(nouser_nous::EmbedFilePayload {
let req = akasha_nous::EmbedRequest {
kind: akasha_nous::RequestKind::EmbedFile,
payload: serde_json::to_value(akasha_nous::EmbedFilePayload {
path: file.path.display().to_string(),
extension: file.extension.clone(),
size: file.size,
@@ -752,14 +752,14 @@ fn embed_via(
let mut response = String::new();
reader.read_line(&mut response)?;
if response.is_empty() {
return Err("nouser-nous cerró sin respuesta".into());
return Err("akasha-nous cerró sin respuesta".into());
}
if let Ok(resp) = serde_json::from_str::<nouser_nous::EmbedResponse>(&response) {
if let Ok(resp) = serde_json::from_str::<akasha_nous::EmbedResponse>(&response) {
return Ok((resp.embedding, resp.model));
}
let err: nouser_nous::ErrorResponse = serde_json::from_str(&response)?;
Err(format!("nouser-nous: {}", err.error).into())
let err: akasha_nous::ErrorResponse = serde_json::from_str(&response)?;
Err(format!("akasha-nous: {}", err.error).into())
}
/// Card del propio engine (kind=Ente). Es el "ser" que produce y
@@ -771,7 +771,7 @@ fn embed_via(
/// brahman-admin.
fn build_engine_card(service_socket: std::path::PathBuf) -> brahman_card::Card {
use brahman_card::{Card, CardKind, Flow, Flows, Lifecycle, Payload, Priority, Supervision, TypeRef};
use nouser_card::query::{FLOW_MONAD_LIST, FLOW_TYPE_NAME};
use akasha_card::query::{FLOW_MONAD_LIST, FLOW_TYPE_NAME};
Card {
payload: Payload::Virtual,
@@ -15,7 +15,7 @@
use std::collections::BTreeMap;
use std::path::PathBuf;
use nouser_card::{FileEntry, Lens, MonadManifest};
use akasha_card::{FileEntry, Lens, MonadManifest};
use crate::embed;
@@ -153,7 +153,7 @@ fn top_extensions(files: &[&FileEntry], n: usize) -> Vec<String> {
}
/// Elige el lente dominante según la extensión más frecuente, con
/// fallback a `shipote-discern` sobre el head del archivo más
/// fallback a `shuma-discern` sobre el head del archivo más
/// representativo cuando la extensión no da hint claro (Lens::Grid).
fn pick_lens(files: &[&FileEntry]) -> Lens {
let dominant = top_extensions(files, 1).into_iter().next();
@@ -170,7 +170,7 @@ fn pick_lens(files: &[&FileEntry]) -> Lens {
if by_ext != Lens::Grid {
return by_ext;
}
// Fallback: samplear el primer archivo del grupo con shipote-discern.
// Fallback: samplear el primer archivo del grupo con shuma-discern.
// Sólo si tiene path real (FileEntry con path absoluto/relativo).
if let Some(first) = files.first() {
if let Some(lens) = discern_lens(&first.path) {
@@ -186,11 +186,11 @@ fn discern_lens(path: &std::path::Path) -> Option<Lens> {
let mut f = std::fs::File::open(path).ok()?;
let n = f.read(&mut buf).ok()?;
buf.truncate(n);
let pipeline = shipote_discern::DiscernPipeline::default_pipeline();
let pipeline = shuma_discern::DiscernPipeline::default_pipeline();
let path_str = path.to_str();
let d = pipeline.discern(
&buf,
&shipote_discern::Hint {
&shuma_discern::Hint {
path: path_str,
size_total: None,
},
@@ -236,7 +236,7 @@ fn shannon_entropy_normalized(files: &[&FileEntry]) -> f32 {
#[cfg(test)]
mod tests {
use super::*;
use nouser_card::FileId;
use akasha_card::FileId;
use std::path::PathBuf;
use ulid::Ulid;
@@ -12,7 +12,7 @@
use std::collections::BTreeMap;
use std::path::Path;
use nouser_card::{FileEntry, FileId, MonadId, MonadManifest};
use akasha_card::{FileEntry, FileId, MonadId, MonadManifest};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -188,7 +188,7 @@ fn decode_key(k: &[u8]) -> Result<ulid::Ulid, MonadDbError> {
#[cfg(test)]
mod tests {
use super::*;
use nouser_card::Lens;
use akasha_card::Lens;
use ulid::Ulid;
fn mk_file(path: &str) -> FileEntry {
@@ -27,7 +27,7 @@
//! - Dirs distintos + misma ext → similitud ~ 0.5.
//! - Sin parecido → similitud < 0.3.
use nouser_card::{FileEntry, MonadId, MonadManifest};
use akasha_card::{FileEntry, MonadId, MonadManifest};
/// Dimensión del vector embedding.
pub const EMBED_DIM: usize = 32;
@@ -37,7 +37,7 @@ pub const EMBED_DIM: usize = 32;
/// este string contra el suyo antes de hacer cosine similarity.
/// Mezclar centroides de distinto MODEL_ID corrompe scores
/// silenciosamente (dimensiones distintas, semántica distinta).
pub const MODEL_ID: &str = "nouser-pseudo-32d";
pub const MODEL_ID: &str = "akasha-pseudo-32d";
/// Computa el embedding de un archivo. Determinístico: misma input
/// → mismo vector. El vector queda L2-normalizado.
@@ -185,7 +185,7 @@ pub const DEFAULT_ATTRACTION_THRESHOLD: f32 = 0.7;
#[cfg(test)]
mod tests {
use super::*;
use nouser_card::FileId;
use akasha_card::FileId;
use std::path::PathBuf;
use ulid::Ulid;
@@ -1,13 +1,13 @@
//! Listener Unix-socket que sirve [`nouser_card::query::QueryRequest`].
//! Listener Unix-socket que sirve [`akasha_card::query::QueryRequest`].
//!
//! El daemon `nouser` lo monta para que cualquier consumer (UI, CLI,
//! El daemon `akasha` lo monta para que cualquier consumer (UI, CLI,
//! otro módulo) pueda preguntarle por sus Mónadas sin pasar por
//! brahman-admin. El path del socket viaja en el `Card.service_socket`
//! del engine; el broker brahman lo enseña vía MatchEvent::Available
//! cuando un consumer declara `flow.input = monad-list:json`.
//!
//! Wire: line-delimited JSON, single-shot por conexión. Mismo patrón
//! que `nouser-nous` (mock/real ↔ nouser-core), reutilizado.
//! que `akasha-nous` (mock/real ↔ akasha-core), reutilizado.
//!
//! Threading: un thread dedicado, blocking I/O. No vale la pena traer
//! tokio acá — la frecuencia esperada es muy baja (UI poll cada 2s)
@@ -18,10 +18,10 @@ use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use nouser_card::query::{
use akasha_card::query::{
EngineInfo, ErrorResponse, ListMonadsResponse, MonadView, QueryRequest,
};
use nouser_card::ulid::Ulid;
use akasha_card::ulid::Ulid;
use crate::db::MonadDb;
@@ -54,7 +54,7 @@ pub fn spawn_listener(
let listener = UnixListener::bind(&config.socket_path)?;
let handle = std::thread::Builder::new()
.name("nouser-engine-listener".into())
.name("akasha-engine-listener".into())
.spawn(move || {
for conn in listener.incoming() {
match conn {
@@ -126,17 +126,17 @@ fn encode_error(msg: String) -> String {
serde_json::to_string(&err).unwrap_or_else(|_| "{\"error\":\"encode\"}".into())
}
// El cliente blocking vive en `nouser_card::query::client` — junto a
// El cliente blocking vive en `akasha_card::query::client` — junto a
// los wire types — para que un consumer pueda hablar con el daemon
// importando sólo `nouser-card`, sin arrastrar el peso de
// `nouser-core` (scanner / db / sled / notify / walkdir / blake3).
// importando sólo `akasha-card`, sin arrastrar el peso de
// `akasha-core` (scanner / db / sled / notify / walkdir / blake3).
#[cfg(test)]
mod tests {
use super::*;
use crate::db::MonadDb;
use nouser_card::query::client as query_client;
use nouser_card::MonadManifest;
use akasha_card::query::client as query_client;
use akasha_card::MonadManifest;
use std::time::Duration;
fn fresh_socket_path(name: &str) -> PathBuf {
@@ -147,7 +147,7 @@ mod tests {
#[test]
fn list_monads_roundtrip_empty() {
let socket = fresh_socket_path("nouser-engine-test");
let socket = fresh_socket_path("akasha-engine-test");
let db = Arc::new(Mutex::new(MonadDb::new()));
let engine_id = Ulid::new();
let _h = spawn_listener(
@@ -178,7 +178,7 @@ mod tests {
#[test]
fn list_monads_returns_views() {
let socket = fresh_socket_path("nouser-engine-test-views");
let socket = fresh_socket_path("akasha-engine-test-views");
let db = Arc::new(Mutex::new(MonadDb::new()));
let m1 = MonadManifest::new("alpha");
let m2 = MonadManifest::new("beta");
@@ -209,7 +209,7 @@ mod tests {
#[test]
fn invalid_request_returns_error_response() {
let socket = fresh_socket_path("nouser-engine-test-bad");
let socket = fresh_socket_path("akasha-engine-test-bad");
let db = Arc::new(Mutex::new(MonadDb::new()));
let _h = spawn_listener(
ListenerConfig {
@@ -1,4 +1,4 @@
//! `nouser-core` — el explorador de Mónadas.
//! `akasha-core` — el explorador de Mónadas.
//!
//! Implementa la pipeline determinista descrita en el diseño de Kairos:
//!
@@ -31,4 +31,4 @@ pub mod embed;
pub mod engine_socket;
pub mod scanner;
pub use nouser_card::*;
pub use akasha_card::*;
@@ -6,7 +6,7 @@
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use nouser_card::{FileEntry, FileId};
use akasha_card::{FileEntry, FileId};
use thiserror::Error;
use ulid::Ulid;
use walkdir::WalkDir;
@@ -1,5 +1,5 @@
[package]
name = "nouser-nous-mock"
name = "akasha-nous-mock"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -9,11 +9,11 @@ publish.workspace = true
description = "Nouser — Nous mock determinístico: implementa el contrato nouser-nous con pseudo-embeddings de Phase C. Stand-in para tests y para `BRAHMAN_BROKER_CONTEXT=test`."
[dependencies]
brahman-card = { path = "../../../core/brahman-card" }
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
nouser-card = { path = "../card" }
nouser-core = { path = "../core" }
nouser-nous = { path = "../nous" }
brahman-card = { path = "../../../protocol/brahman-card" }
brahman-sidecar = { path = "../../../protocol/brahman-sidecar" }
akasha-card = { path = "../card" }
akasha-core = { path = "../core" }
akasha-nous = { path = "../nous" }
serde_json = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
@@ -21,5 +21,5 @@ tracing-subscriber = { workspace = true }
ulid = { workspace = true }
[[bin]]
name = "nouser-nous-mock"
name = "akasha-nous-mock"
path = "src/main.rs"
@@ -1,7 +1,7 @@
//! `nouser-nous-mock` — proveedor de embeddings determinista (sin LLM).
//! `akasha-nous-mock` — proveedor de embeddings determinista (sin LLM).
//!
//! Implementa el contrato `nouser-nous` usando los pseudo-embeddings
//! de Phase C (`nouser_core::embed`). Sirve como:
//! Implementa el contrato `akasha-nous` usando los pseudo-embeddings
//! de Phase C (`akasha_core::embed`). Sirve como:
//!
//! - **Mock para tests**: en `BRAHMAN_BROKER_CONTEXT=test`, el
//! `priority_offset` per-contexto declarado en su Card lo prioriza
@@ -16,7 +16,7 @@
//! `priority_contexts.test = { priority_offset: +1 }` lo prioriza
//! cuando el broker corre bajo contexto test.
//! 2. Bind del Unix socket en `$NOUSER_NOUS_SOCKET` (default
//! `$XDG_RUNTIME_DIR/nouser-nous.sock`).
//! `$XDG_RUNTIME_DIR/akasha-nous.sock`).
//! 3. Loop: accept → read line JSON → process → write line JSON → close.
//! 4. Cada request se loggea (info) — útil para verificar que el
//! consumidor está usando este proveedor.
@@ -29,9 +29,9 @@ use brahman_card::{
ulid::Ulid, Card, CardKind, ContextBias, Flow, Flows, Lifecycle, Payload, Priority,
Supervision, TypeRef,
};
use nouser_card::FileEntry;
use nouser_core::embed;
use nouser_nous::{
use akasha_card::FileEntry;
use akasha_core::embed;
use akasha_nous::{
transport, EmbedFilePayload, EmbedRequest, EmbedResponse, EmbedTextPayload, ErrorResponse,
PingResponse, RequestKind, FLOW_EMBED_REQUEST, FLOW_EMBED_RESULT, FLOW_TYPE_NAME,
};
@@ -39,11 +39,11 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
use tracing::{info, warn};
/// El mock implementa el MISMO algoritmo que `nouser_core::embed`,
/// El mock implementa el MISMO algoritmo que `akasha_core::embed`,
/// así que reportamos el mismo `MODEL_ID` que él. De otro modo el
/// consumer filtraría las Mónadas como "modelo distinto" y los
/// scores quedarían vacíos.
const MODEL_ID: &str = nouser_core::embed::MODEL_ID;
const MODEL_ID: &str = akasha_core::embed::MODEL_ID;
#[tokio::main(flavor = "current_thread")]
async fn main() -> std::io::Result<()> {
@@ -60,7 +60,7 @@ async fn main() -> std::io::Result<()> {
std::fs::create_dir_all(parent)?;
}
let listener = UnixListener::bind(&sock_path)?;
info!(socket = %sock_path.display(), "nouser-nous-mock escuchando");
info!(socket = %sock_path.display(), "akasha-nous-mock escuchando");
// 2. Sidecar al brahman-init con la Card que declara el socket.
let card = build_card(sock_path.clone());
@@ -107,7 +107,7 @@ fn build_card(service_socket: std::path::PathBuf) -> Card {
Card {
schema_version: brahman_card::CARD_SCHEMA_VERSION,
id: Ulid::new(),
label: "nouser.nous_mock".into(),
label: "akasha.nous_mock".into(),
payload: Payload::Virtual,
supervision: Supervision::Delegate,
lifecycle: Lifecycle::Daemon,
@@ -178,7 +178,7 @@ fn handle_embed_file(payload: serde_json::Value, started: Instant) -> Result<Str
info!(path = %p.path, "embed_file");
let file = FileEntry {
id: nouser_card::FileId::from(Ulid::new()),
id: akasha_card::FileId::from(Ulid::new()),
path: PathBuf::from(p.path),
content_hash: None,
size: p.size,
@@ -204,7 +204,7 @@ fn handle_embed_text(payload: serde_json::Value, started: Instant) -> Result<Str
// resto del vector con ceros. No es semánticamente útil, pero respeta
// la forma para que el cliente no se rompa.
let synthetic = FileEntry {
id: nouser_card::FileId::from(Ulid::new()),
id: akasha_card::FileId::from(Ulid::new()),
path: PathBuf::from(format!("synthetic://{}", p.text)),
content_hash: None,
size: p.text.len() as u64,
@@ -1,5 +1,5 @@
[package]
name = "nouser-nous-real"
name = "akasha-nous-real"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -18,10 +18,10 @@ default = []
embeddings = ["dep:fastembed"]
[dependencies]
brahman-card = { path = "../../../core/brahman-card" }
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
ente-cas = { path = "../../../core/ente-cas" }
nouser-nous = { path = "../nous" }
brahman-card = { path = "../../../protocol/brahman-card" }
brahman-sidecar = { path = "../../../protocol/brahman-sidecar" }
ente-cas = { path = "../../../runtime/ente-cas" }
akasha-nous = { path = "../nous" }
serde_json = { workspace = true }
sled = { workspace = true }
tokio = { workspace = true }
@@ -36,5 +36,5 @@ fastembed = { version = "4", optional = true }
tempfile = { workspace = true }
[[bin]]
name = "nouser-nous-real"
name = "akasha-nous-real"
path = "src/main.rs"
@@ -2,7 +2,7 @@
//!
//! Razón de existir: el modelo real (`fastembed-allMiniLML6V2`) es
//! caro (1-50 ms por archivo según tamaño y CPU). Cada vez que el
//! daemon de nouser re-publica una Mónada o el watcher dispara un
//! daemon de akasha re-publica una Mónada o el watcher dispara un
//! re-cluster por cambio de FS, todos los archivos pasan otra vez
//! por embed. Para árboles de 1000 archivos, eso son segundos
//! desperdiciados re-embedidando contenido que no cambió.
@@ -17,7 +17,7 @@
//! little-endian (4 bytes por f32). Compacto, sin overhead de
//! bincode/postcard para datos numéricos puros.
//! - **Backend**: sled, tree único `embed_cache_v1`. Path:
//! `$XDG_CACHE_HOME/brahman/nouser-nous-real-embed-cache.sled`.
//! `$XDG_CACHE_HOME/brahman/akasha-nous-real-embed-cache.sled`.
//!
//! ## Versionado
//!
@@ -102,7 +102,7 @@ fn default_path() -> PathBuf {
.map(|h| PathBuf::from(h).join(".cache"))
})
.unwrap_or_else(std::env::temp_dir);
base.join("brahman").join("nouser-nous-real-embed-cache.sled")
base.join("brahman").join("akasha-nous-real-embed-cache.sled")
}
fn build_key(file_sha: &[u8; 32], model_id: &str) -> Vec<u8> {
@@ -20,7 +20,7 @@ use std::sync::Arc;
use std::time::Instant;
use fastembed::{EmbeddingModel, InitOptions, TextEmbedding};
use nouser_nous::{
use akasha_nous::{
EmbedFilePayload, EmbedRequest, EmbedResponse, EmbedTextPayload, ErrorResponse, PingResponse,
RequestKind,
};
@@ -1,21 +1,21 @@
//! `nouser-nous-real` — proveedor Nous con LLM real (gated por feature).
//! `akasha-nous-real` — proveedor Nous con LLM real (gated por feature).
//!
//! ## Build modes
//!
//! - `cargo build -p nouser-nous-real`
//! - `cargo build -p akasha-nous-real`
//! Compila como **stub**: bin que arranca, sidecarea al brahman-init
//! pero rechaza toda request con un error explicando que falta la
//! feature. Útil para que `cargo build --workspace` no requiera ML
//! deps.
//!
//! - `cargo build -p nouser-nous-real --features embeddings`
//! - `cargo build -p akasha-nous-real --features embeddings`
//! Compila con `fastembed` + ONNX Runtime descargado por Cargo.
//! Modelo default: `all-MiniLM-L6-v2` (384-d, ~80 MB descargado al
//! primer run y cacheado en `~/.cache/fastembed`).
//!
//! ## Diseño
//!
//! Mismo contrato wire que `nouser-nous-mock` (`nouser-nous` crate). La
//! Mismo contrato wire que `akasha-nous-mock` (`akasha-nous` crate). La
//! diferencia operativa: real produce 384-d con semantic content
//! (text-embedding del modelo); mock produce 32-d con metadata-hashing.
//! No son intercambiables a media-deployment — los centroides de
@@ -36,7 +36,7 @@ use brahman_card::{
ulid::Ulid, Card, CardKind, ContextBias, Flow, Flows, Lifecycle, Payload, Priority,
Supervision, TypeRef,
};
use nouser_nous::{transport, FLOW_EMBED_REQUEST, FLOW_EMBED_RESULT, FLOW_TYPE_NAME};
use akasha_nous::{transport, FLOW_EMBED_REQUEST, FLOW_EMBED_RESULT, FLOW_TYPE_NAME};
use tokio::net::UnixListener;
use tracing::info;
@@ -63,11 +63,11 @@ async fn main() -> std::io::Result<()> {
#[cfg(not(feature = "embeddings"))]
info!(
"nouser-nous-real corriendo en modo STUB (compilá con \
"akasha-nous-real corriendo en modo STUB (compilá con \
--features embeddings para activar el modelo)"
);
// 1. Resolver socket del data-plane (default `nouser-nous-real.sock`,
// 1. Resolver socket del data-plane (default `akasha-nous-real.sock`,
// distinto del mock para coexistir).
let sock_path = transport::provider_socket_path("real");
if sock_path.exists() {
@@ -77,7 +77,7 @@ async fn main() -> std::io::Result<()> {
std::fs::create_dir_all(parent)?;
}
let listener = UnixListener::bind(&sock_path)?;
info!(socket = %sock_path.display(), "nouser-nous-real escuchando");
info!(socket = %sock_path.display(), "akasha-nous-real escuchando");
// 2. Sidecar al brahman-init con Card declarando el socket.
let card = build_card(sock_path.clone());
@@ -144,7 +144,7 @@ fn init_tracing() {
}
/// Card que real-nous anuncia. Idéntica al mock excepto por:
/// - label distinto (`nouser.nous_real`) para que coexistan en el broker.
/// - label distinto (`akasha.nous_real`) para que coexistan en el broker.
/// - `priority_contexts.prod = +1` (gana en contexto prod).
/// - `service_socket` propio para que clientes lo descubran directo.
fn build_card(service_socket: std::path::PathBuf) -> Card {
@@ -160,7 +160,7 @@ fn build_card(service_socket: std::path::PathBuf) -> Card {
Card {
schema_version: brahman_card::CARD_SCHEMA_VERSION,
id: Ulid::new(),
label: "nouser.nous_real".into(),
label: "akasha.nous_real".into(),
payload: Payload::Virtual,
supervision: Supervision::Delegate,
lifecycle: Lifecycle::Daemon,
@@ -1,7 +1,7 @@
//! Modo stub: arranca el bin pero rechaza las requests con un error
//! que explica que falta la feature `embeddings`.
use nouser_nous::{EmbedRequest, ErrorResponse};
use akasha_nous::{EmbedRequest, ErrorResponse};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
use tracing::warn;
@@ -21,8 +21,8 @@ pub async fn handle_conn(stream: UnixStream) -> std::io::Result<()> {
let resp = ErrorResponse {
error: format!(
"nouser-nous-real compilado sin la feature `embeddings`. \
Rebuild con: cargo build -p nouser-nous-real --features embeddings"
"akasha-nous-real compilado sin la feature `embeddings`. \
Rebuild con: cargo build -p akasha-nous-real --features embeddings"
),
};
let mut stream = reader.into_inner();
@@ -1,5 +1,5 @@
[package]
name = "nouser-nous"
name = "akasha-nous"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -1,6 +1,6 @@
//! `nouser-nous` — el contrato del proveedor de embeddings.
//! `akasha-nous` — el contrato del proveedor de embeddings.
//!
//! Define el wire-format compartido entre `nouser-core` (consumidor) y
//! Define el wire-format compartido entre `akasha-core` (consumidor) y
//! cualquier implementación de Nous (mock determinista o LLM real). El
//! protocolo es **line-delimited JSON** sobre Unix socket: cada conexión
//! envía una request, recibe una response, y cierra. Single-shot por
@@ -21,8 +21,8 @@
//!
//! ## Por qué un crate aparte
//!
//! El consumidor (nouser-core) y el proveedor (nouser-nous-mock,
//! nouser-nous-real) deben acordar en types EXACTOS. Tener el contrato
//! El consumidor (akasha-core) y el proveedor (akasha-nous-mock,
//! akasha-nous-real) deben acordar en types EXACTOS. Tener el contrato
//! en su crate evita que cada lado declare structs paralelos que se
//! desincronizan. Si bumpeás el wire, bumpeás aquí.
//!
@@ -32,7 +32,7 @@
//! el mismo `flow.output: { name: "embed-result", type: ... }` y
//! `flow.input: "embed-request"`. El broker brahman los matchea contra
//! los consumidores; el `priority_offset` per-contexto del Card hace que
//! mock-nous gane en `test` y real-nous en `prod`. nouser-core sólo
//! mock-nous gane en `test` y real-nous en `prod`. akasha-core sólo
//! consume el flow, sin saber cuál implementación corre.
#![forbid(unsafe_code)]
@@ -118,7 +118,7 @@ pub mod transport {
pub const SOCKET_ENV: &str = "NOUSER_NOUS_SOCKET";
/// Nombre genérico del socket cuando hay un solo proveedor.
pub const SOCKET_NAME: &str = "nouser-nous.sock";
pub const SOCKET_NAME: &str = "akasha-nous.sock";
/// Ruta canónica al socket cuando un único proveedor está activo
/// (consumidores que no quieren elegir).
@@ -136,7 +136,7 @@ pub mod transport {
if let Ok(p) = std::env::var(SOCKET_ENV) {
return PathBuf::from(p);
}
runtime_base().join(format!("nouser-nous-{}.sock", provider))
runtime_base().join(format!("akasha-nous-{}.sock", provider))
}
fn runtime_base() -> PathBuf {
+41
View File
@@ -0,0 +1,41 @@
# modules/barra/ — Taskbar agnóstica (estilo Windows)
**Propósito.** Lista de tareas (cajitas, una por ventana abierta) que
se renderiza dentro de un `<ul>` provisto por el host. Modelo + render
puros viven en `barra-core`; el binding click+DOM en `barra-web`.
## Crates
| crate | tipo | rol |
| ------------- | ---- | --------------------------------------------------------- |
| `barra-core` | lib | `Task` + `render_html(&[Task]) -> String` + sanitizadores |
| `barra-web` | lib | Mount sobre `<ul>` + listener de click + lookup centers |
## Dependencias
- `barra-core`: sin deps.
- `barra-web``barra-core`, `wasm-bindgen`, `web-sys`.
## Contrato HTML
```html
<ul id="taskbar" class="taskbar-list" role="presentation"></ul>
```
Clases generadas (host estiliza):
- `.taskbar-item`, `.taskbar-item.active`, `.taskbar-item-dot`
- atributo `data-task="<id>"` para theming CSS por tarea
## API
```rust
let bar = TaskList::mount(list_el)?;
bar.set_tasks(&[Task::new("aire", "AIRE"),
Task::new("fuego", "FUEGO").active()]);
bar.on_click(|id, cx, cy| { /* center en CSS pixels */ });
```
## Estado
barra-core: 5 tests verdes (sanitize + escape + render). barra-web:
binding mínimo (mount + click + center lookup). LOC ~280.
@@ -0,0 +1,8 @@
[package]
name = "barra-core"
description = "Barra — modelo de taskbar agnóstico: Task + render-to-html + sanitizadores. Sin dependencias web/DOM."
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
+108
View File
@@ -0,0 +1,108 @@
//! Barra core — modelo agnóstico de taskbar.
//!
//! Provee la lista de `Task`, los helpers de sanitización para atributos
//! HTML, y `render_html` puro. El binding DOM vive en `barra-web`.
/// Una tarea (cajita) en la barra.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Task {
pub id: String,
pub label: String,
pub active: bool,
}
impl Task {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self { id: id.into(), label: label.into(), active: false }
}
pub fn active(mut self) -> Self {
self.active = true;
self
}
}
/// Renderiza un slice de tareas a markup HTML. Sanitiza IDs y escapa
/// labels. La salida es la lista de `<li>` que el host inyecta en su `<ul>`.
pub fn render_html(tasks: &[Task]) -> String {
let mut html = String::new();
for t in tasks {
let id_safe = sanitize_attr(&t.id);
let label_safe = escape_text(&t.label);
let active_cls = if t.active { " active" } else { "" };
html.push_str(&format!(
"<li><button class=\"taskbar-item{active_cls}\" data-task=\"{id_safe}\" type=\"button\">\
<span class=\"taskbar-item-dot\" aria-hidden=\"true\"></span>{label_safe}</button></li>"
));
}
html
}
/// Filtra a `[a-zA-Z0-9_-]` para uso seguro en atributos HTML.
pub fn sanitize_attr(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
/// HTML-escape de texto para insertarlo en posiciones de contenido.
pub fn escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&amp;"),
'<' => out.push_str("&lt;"),
'>' => out.push_str("&gt;"),
'"' => out.push_str("&quot;"),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn task_builder_defaults_inactive() {
let t = Task::new("aire", "AIRE");
assert!(!t.active);
assert!(Task::new("f", "F").active().active);
}
#[test]
fn sanitize_attr_strips_unsafe() {
assert_eq!(sanitize_attr("aire"), "aire");
assert_eq!(sanitize_attr("a-b_c"), "a-b_c");
assert_eq!(sanitize_attr("ai<re>"), "aire");
assert_eq!(sanitize_attr("a\"b"), "ab");
}
#[test]
fn escape_text_escapes_html() {
assert_eq!(escape_text("AIRE"), "AIRE");
assert_eq!(escape_text("<script>"), "&lt;script&gt;");
assert_eq!(escape_text("a & b"), "a &amp; b");
}
#[test]
fn render_html_emits_active_class() {
let tasks = [
Task::new("aire", "AIRE"),
Task::new("fuego", "FUEGO").active(),
];
let html = render_html(&tasks);
assert!(html.contains("data-task=\"aire\""));
assert!(html.contains("data-task=\"fuego\""));
assert!(html.contains("taskbar-item active"));
}
#[test]
fn render_html_escapes_label_and_sanitizes_id() {
let tasks = [Task::new("a<b", "x<script>y")];
let html = render_html(&tasks);
assert!(html.contains("data-task=\"ab\""));
assert!(html.contains("x&lt;script&gt;y"));
assert!(!html.contains("<script>"));
}
}
@@ -7,6 +7,7 @@ authors.workspace = true
publish.workspace = true
[dependencies]
barra-core = { path = "../barra-core" }
wasm-bindgen.workspace = true
js-sys.workspace = true
+12 -129
View File
@@ -1,9 +1,7 @@
//! Barra-web — taskbar estilo Windows, agnóstica del dominio.
//!
//! Maneja la lista dinámica de "tareas" (cajitas, una por ventana abierta)
//! dentro de un elemento `<ul>` provisto por el host. El layout del resto
//! de la barra (home button, brand, créditos, dividers, etc.) es
//! responsabilidad del host — el módulo sólo se encarga del LIST + CLICK.
//! Barra-web — binding DOM de la taskbar. Re-exporta `Task` desde
//! `barra-core` y delega el render-to-html al core. Aquí sólo viven el
//! mount sobre un `<ul>`, el listener de click delegado y los lookups
//! de posición (bounding rects) que son intrínsecos al DOM.
//!
//! Contrato HTML mínimo:
//! ```html
@@ -13,55 +11,18 @@
//! Convenciones de clase generadas:
//! - `.taskbar-item` — cada cajita
//! - `.taskbar-item.active` — la cajita visible/foreground
//! - `.taskbar-item-dot` — punto decorativo dentro de la cajita
//! - `.taskbar-item-dot` — punto decorativo
//! - `data-task="<id>"` — identificador único usable por CSS para theming
//! (`.taskbar-item[data-task="aire"] { --task-color: ... }`)
//!
//! El módulo NO inyecta CSS — el host estiliza estas clases.
//!
//! ```rust,ignore
//! let list: HtmlElement = doc.get_element_by_id("my-tasks")?.dyn_into()?;
//! let bar = barra_web::TaskList::mount(list)?;
//! bar.set_tasks(&[
//! Task::new("aire", "AIRE"),
//! Task::new("fuego", "FUEGO").active(),
//! ]);
//! bar.on_click(|id, cx, cy| {
//! // El click cayó en la cajita `id`. (cx, cy) es el centro de la
//! // cajita en CSS pixels — útil como origin de animaciones.
//! });
//! ```
use std::cell::RefCell;
use std::rc::Rc;
pub use barra_core::Task;
use barra_core::{render_html, sanitize_attr};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, HtmlElement, MouseEvent};
/// Una tarea (cajita) en la barra.
#[derive(Clone, Debug)]
pub struct Task {
pub id: String,
pub label: String,
pub active: bool,
}
impl Task {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: id.into(),
label: label.into(),
active: false,
}
}
pub fn active(mut self) -> Self {
self.active = true;
self
}
}
#[derive(Clone)]
pub struct TaskList {
list: HtmlElement,
@@ -69,24 +30,15 @@ pub struct TaskList {
}
impl TaskList {
/// Monta el módulo sobre el elemento `<ul>` provisto. Instala un único
/// listener de click delegado: cualquier click dentro del list que caiga
/// sobre un `.taskbar-item` dispara `on_click(id, cx, cy)`.
pub fn mount(list: HtmlElement) -> Result<Self, JsValue> {
let on_click: Rc<RefCell<Option<Box<dyn FnMut(&str, f64, f64)>>>> =
Rc::new(RefCell::new(None));
let on_click2 = on_click.clone();
let cb = Closure::<dyn FnMut(MouseEvent)>::new(move |e: MouseEvent| {
let Some(target) = e.target() else { return };
let Ok(target_el): Result<Element, _> = target.dyn_into() else {
return;
};
let Ok(Some(item)) = target_el.closest(".taskbar-item") else {
return;
};
let Some(id) = item.get_attribute("data-task") else {
return;
};
let Ok(target_el): Result<Element, _> = target.dyn_into() else { return };
let Ok(Some(item)) = target_el.closest(".taskbar-item") else { return };
let Some(id) = item.get_attribute("data-task") else { return };
let rect = item.get_bounding_client_rect();
let cx = rect.left() + rect.width() / 2.0;
let cy = rect.top() + rect.height() / 2.0;
@@ -99,91 +51,22 @@ impl TaskList {
Ok(Self { list, on_click })
}
/// Reemplaza el contenido de la lista con las tareas dadas.
/// Los IDs se filtran a `[a-zA-Z0-9_-]` para uso seguro en atributos.
/// Los labels se HTML-escapan.
pub fn set_tasks(&self, tasks: &[Task]) {
let mut html = String::new();
for t in tasks {
let id_safe = sanitize_attr(&t.id);
let label_safe = escape_text(&t.label);
let active_cls = if t.active { " active" } else { "" };
html.push_str(&format!(
"<li><button class=\"taskbar-item{active_cls}\" data-task=\"{id_safe}\" type=\"button\">\
<span class=\"taskbar-item-dot\" aria-hidden=\"true\"></span>{label_safe}</button></li>"
));
}
self.list.set_inner_html(&html);
self.list.set_inner_html(&render_html(tasks));
}
/// Registra (o reemplaza) el callback al click sobre una cajita.
/// El callback recibe `(id, center_x, center_y)` en CSS pixels.
pub fn on_click<F: FnMut(&str, f64, f64) + 'static>(&self, cb: F) {
*self.on_click.borrow_mut() = Some(Box::new(cb));
}
/// Centro en CSS pixels de la cajita con `id` dado, o `None` si no existe.
pub fn task_center(&self, id: &str) -> Option<(f64, f64)> {
let sel = format!(".taskbar-item[data-task=\"{}\"]", sanitize_attr(id));
let el = self.list.query_selector(&sel).ok().flatten()?;
let rect = el.get_bounding_client_rect();
Some((
rect.left() + rect.width() / 2.0,
rect.top() + rect.height() / 2.0,
))
Some((rect.left() + rect.width() / 2.0, rect.top() + rect.height() / 2.0))
}
/// Acceso al elemento `<ul>` host por si el caller quiere modificar
/// styling o ARIA atributos directamente.
pub fn list_el(&self) -> &HtmlElement {
&self.list
}
}
fn sanitize_attr(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
fn escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&amp;"),
'<' => out.push_str("&lt;"),
'>' => out.push_str("&gt;"),
'"' => out.push_str("&quot;"),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn task_builder_defaults_inactive() {
let t = Task::new("aire", "AIRE");
assert!(!t.active);
let t2 = Task::new("fuego", "FUEGO").active();
assert!(t2.active);
}
#[test]
fn sanitize_attr_removes_unsafe_chars() {
assert_eq!(sanitize_attr("aire"), "aire");
assert_eq!(sanitize_attr("a-b_c"), "a-b_c");
assert_eq!(sanitize_attr("ai<re>"), "aire");
assert_eq!(sanitize_attr("a\"b"), "ab");
}
#[test]
fn escape_text_escapes_html() {
assert_eq!(escape_text("AIRE"), "AIRE");
assert_eq!(escape_text("<script>"), "&lt;script&gt;");
assert_eq!(escape_text("a & b"), "a &amp; b");
}
}
+47
View File
@@ -0,0 +1,47 @@
# modules/cosmobiologia/ — Estudio de astrología profesional
**Propósito.** App completa de astrología: cálculo de cartas natales
(swiss ephemeris vía `eternal-astrology`), almacenamiento de
contactos, render agnóstico (carta natal, dial GR, harmonics) con
backends GPUI nativo + SVG/Canvas2D web.
## Crates
| crate | tipo | rol |
| ------------------------- | ---- | -------------------------------------------------- |
| `cosmobiologia-card` | lib | Card del daemon |
| `cosmobiologia-model` | lib | Tipos: Chart, Contact, Group, Aspect, Coordinates |
| `cosmobiologia-store` | lib | SQLite store (contactos + cartas) |
| `cosmobiologia-engine` | lib | Cálculo astrológico (bridge a eternal-astrology) |
| `cosmobiologia-modules` | lib | Sub-modules: aspects, harmonics, GR dual-ring |
| `cosmobiologia-render` | lib | **AGNÓSTICO**: RenderModel + DrawCommand + compose |
| `cosmobiologia-theme` | lib | Paleta astrológica + presets dark/light |
| `cosmobiologia-canvas` | lib | Widget GPUI consumidor de render |
| `cosmobiologia-tree` | lib | Widget GPUI: Groups/Contacts/Charts sidebar |
| `cosmobiologia-panel` | lib | Widget GPUI: controles dexterior (dial GR, FFT) |
| `cosmobiologia-web` | lib | Bridge WASM: DrawCommand → SVG/Canvas2D |
## Dependencias
- Core: `cosmobiologia-model``cosmobiologia-engine` (cálculo) +
`cosmobiologia-store` (persistencia).
- Render path agnóstico: `engine``render::compose_wheel`
`DrawCommand`s → renderer (gpui nativo o SVG web).
- Widgets GPUI consumen `nahual/widgets/tree`, `nahual-theme`.
## Estado
LOC 21,502 (más grande del monorepo). Cobertura desigual:
- AGNÓSTICOS estables: model (478), store (760), render (1474), engine
(2687).
- GPUI sin tests: canvas (2850), tree (2295), panel (1037).
- 20 TODOs concentrados en engine (sistema GR + harmonics).
## Roadmap (memorias de proyecto)
1. **Sistema GR (8 tareas #55-#62)**: dual-ring directas+conversas,
scrubbing live, HUD triggers.
2. **FFT armónico (#63)**: detección automática de ciclos.
3. **Research bank (#64-#65)**: corpus de cartas + queries.
4. **3D celestial sphere (#66)**: render esférico no 2D.
5. **Rectificador automático (#67)**: ajuste de hora natal vía eventos.
@@ -11,5 +11,5 @@ cosmobiologia-model = { path = "../cosmobiologia-model" }
cosmobiologia-modules = { path = "../cosmobiologia-modules" }
cosmobiologia-render = { path = "../cosmobiologia-render" }
cosmobiologia-theme = { path = "../cosmobiologia-theme" }
yahweh-theme = { workspace = true }
nahual-theme = { workspace = true }
gpui = { workspace = true }
@@ -43,7 +43,7 @@ use gpui::{
use cosmobiologia_engine::{Geometry, Layer, LayerKind, OUTER_RING_MODULES, RenderModel};
use cosmobiologia_model::{ChartId, ContactId, GroupId};
use cosmobiologia_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
use yahweh_theme::Theme;
use nahual_theme::Theme;
// =====================================================================
// Eventos
@@ -6,8 +6,8 @@ license = { workspace = true }
description = "Tahuantinsuyu — Tarjeta de Presentación brahman + spawn del sidecar + protocolo del service socket."
[dependencies]
brahman-card = { path = "../../../core/brahman-card" }
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
brahman-card = { path = "../../../protocol/brahman-card" }
brahman-sidecar = { path = "../../../protocol/brahman-sidecar" }
cosmobiologia-engine = { path = "../cosmobiologia-engine" }
cosmobiologia-model = { path = "../cosmobiologia-model" }
ulid = { workspace = true }
@@ -9,6 +9,6 @@ description = "Tahuantinsuyu — panel de control inferior. Toggles, sliders y s
cosmobiologia-model = { path = "../cosmobiologia-model" }
cosmobiologia-modules = { path = "../cosmobiologia-modules" }
cosmobiologia-theme = { path = "../cosmobiologia-theme" }
yahweh-theme = { workspace = true }
nahual-theme = { workspace = true }
gpui = { workspace = true }
serde_json = { workspace = true }
@@ -31,7 +31,7 @@ use gpui::{
use cosmobiologia_model::ChartKind;
use cosmobiologia_modules::{Control, Registry, SelectOption};
use yahweh_theme::Theme;
use nahual_theme::Theme;
// =====================================================================
// Eventos
@@ -5,10 +5,10 @@
//! migración inicial corre la primera vez que se abre un archivo nuevo
//! (idempotente vía `CREATE TABLE IF NOT EXISTS`).
//!
//! Patrón inspirado en `yahweh_provider_sqlite::SqliteDataProvider` pero
//! Patrón inspirado en `nahual_provider_sqlite::SqliteDataProvider` pero
//! con dominio propio (no extiende el `DataProvider` agnóstico — esa
//! integración viene en `cosmobiologia-tree` que envuelve este store
//! detrás del trait de yahweh).
//! detrás del trait de nahual).
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
@@ -7,4 +7,4 @@ description = "Tahuantinsuyu — paleta astrológica (elementos, planetas, signo
[dependencies]
gpui = { workspace = true }
yahweh-theme = { workspace = true }
nahual-theme = { workspace = true }
@@ -1,6 +1,6 @@
//! `cosmobiologia-theme` — paleta simbólica + presets místicos.
//!
//! Una capa fina sobre [`yahweh_theme::Theme`]: el theme base aporta los
//! Una capa fina sobre [`nahual_theme::Theme`]: el theme base aporta los
//! slots de panel/foreground/accent; nosotros agregamos paletas
//! semánticas para los elementos (fuego/tierra/aire/agua), los modos
//! (cardinal/fijo/mutable), los planetas y los aspectos.
@@ -72,7 +72,7 @@ pub enum AspectKind {
/// Paleta completa de símbolos astrológicos resuelta a colores HSLA. Las
/// dos variantes (`dark` / `light`) comparten estructura — el canvas
/// elige según `yahweh_theme::Theme::is_dark`.
/// elige según `nahual_theme::Theme::is_dark`.
#[derive(Debug, Clone)]
pub struct AstroPalette {
pub is_dark: bool,
@@ -319,7 +319,7 @@ impl AstroPalette {
}
}
pub fn for_theme(theme: &yahweh_theme::Theme) -> Self {
pub fn for_theme(theme: &nahual_theme::Theme) -> Self {
// Dispatcher por nombre para los themes "papel"; el resto cae
// al binary dark/light según `is_dark`. Mantenemos el match
// case-insensitive por defensa contra cambios de naming.
@@ -8,7 +8,7 @@ description = "Tahuantinsuyu — explorador izquierdo (Groups/Contacts/Charts) s
[dependencies]
cosmobiologia-model = { path = "../cosmobiologia-model" }
cosmobiologia-store = { path = "../cosmobiologia-store" }
yahweh-theme = { workspace = true }
yahweh-widget-tree = { workspace = true }
yahweh-widget-text-input = { path = "../../ui_engine/widgets/text_input" }
nahual-theme = { workspace = true }
nahual-widget-tree = { workspace = true }
nahual-widget-text-input = { path = "../../nahual/widgets/text_input" }
gpui = { workspace = true }
@@ -1,6 +1,6 @@
//! `cosmobiologia-tree` — explorador jerárquico Groups → Contacts → Charts.
//!
//! Envuelve [`yahweh_widget_tree::TreeView`] con la lógica de dominio
//! Envuelve [`nahual_widget_tree::TreeView`] con la lógica de dominio
//! de Tahuantinsuyu. Los `RowId` codifican el tipo con prefijo:
//!
//! - `g:<ulid>` → Group
@@ -17,7 +17,7 @@
//! - **Borrar** pide confirmación con `window.prompt`.
//!
//! El host (la app) se suscribe a [`TreeEvent`] y traduce a `AppEvent`
//! del bus de yahweh para que el canvas/panel reaccionen.
//! del bus de nahual para que el canvas/panel reaccionen.
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
@@ -34,9 +34,9 @@ use cosmobiologia_model::{
TimeCertainty, TreeSelection,
};
use cosmobiologia_store::Store;
use yahweh_theme::Theme;
use yahweh_widget_text_input::{TextInput, TextInputEvent};
use yahweh_widget_tree::{RowId, RowKind, TreeEvent as InnerTreeEvent, TreeRow, TreeView};
use nahual_theme::Theme;
use nahual_widget_text_input::{TextInput, TextInputEvent};
use nahual_widget_tree::{RowId, RowKind, TreeEvent as InnerTreeEvent, TreeRow, TreeView};
const PREFIX_GROUP: &str = "g:";
const PREFIX_CONTACT: &str = "c:";
+26
View File
@@ -0,0 +1,26 @@
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
**Propósito.** Sitio personal animado en WASM: chacana 3D con tilt
físico al mouse + 4 tips cardinales (agua/fuego/aire/tierra). Demuestra
el patrón "crates agnósticos + cdylib WASM" del monorepo.
## Crates
| crate | tipo | rol |
| --------------------- | ------- | ------------------------------------------------ |
| `gioser-geom` | lib | Geometría puramatemática (chacana, polígonos) |
| `gioser-physics` | lib | Inercia + spring para tilt (puro) |
| `gioser-palette` | lib | Paletas dark/light de los 4 elementos |
| `gioser-shaders` | lib | Shaders WebGL2 (vertex + fragment) como strings |
| `gioser-canvas-web` | lib | Renderer WebGL2: GL context + program + draw |
## Dependencias
- Los 4 primeros: sin deps web. Reutilizables en cualquier backend.
- `gioser-canvas-web``wasm-bindgen`, `web-sys` (WebGL2RenderingContext).
- App final `apps/gioser-web` es cdylib.
## Estado
LOC 2,535. Landing operativa, deploys con `scripts/build-gioser-web.sh`.
Tests en geom/physics/palette. Estable. Ver `docs/changelog/gioser.md`.
+39
View File
@@ -0,0 +1,39 @@
# modules/nahual/ — Motor de UI GPUI (era yahweh)
**Propósito.** Framework de widgets sobre GPUI para apps de escritorio
nativas: theme persistente, meta-runtime declarativo, providers de
datos (fs+sqlite), bus de eventos, launcher de paneles.
## Estructura
```
nahual/
libs/ — núcleo del framework
core, theme, launcher, bus,
meta-schema, meta-runtime,
providers/{fs, sqlite}
widgets/ — widgets reutilizables
tree, container_core, splitter, tabs, tiled,
text_input, meta-form, banner, card,
stat-card, app-header, theme-switcher
```
## Dependencias
- Todos los crates `libs/*` y `widgets/*``gpui` (acoplo intencional).
- `theme``directories` (persistencia de preferencias).
- `meta-runtime` evalúa esquemas Nickel.
- Consumido por: `apps/nahual-*` (file/db/text/image explorer + shell)
y `apps/cosmobiologia` (vía cosmobiologia-canvas/panel/tree),
`apps/akasha-explorer`, `apps/nakui-*`, `apps/minga-explorer`.
## Patrón estándar de explorer
`nahual-shell` define el shell standard (sidebar + main + status panel
+ hot-reload). Cada app explorer la encarna con su backend custom.
## Estado
LOC 15,968 (sin contar pineal). Tests E2E con `gpui::TestAppContext`.
Maduro y estable; backbone visual del monorepo. Ver
`docs/changelog/nahual.md`.
@@ -1,5 +1,5 @@
[package]
name = "yahweh-bus"
name = "nahual-bus"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_bus` — `AppBus` + `AppEvent` para comunicación cross-widget.
//! `nahual_bus` — `AppBus` + `AppEvent` para comunicación cross-widget.
//!
//! Es un `Entity<AppBus>` que emite [`AppEvent`]. Cualquier widget se
//! subscribe con `cx.subscribe(&bus, |this, _, ev, cx| { ... })`. La
@@ -1,5 +1,5 @@
[package]
name = "yahweh-core"
name = "nahual-core"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_core` — tipos compartidos por toda la app, sin dependencias de UI.
//! `nahual_core` — tipos compartidos por toda la app, sin dependencias de UI.
//!
//! Contiene tres bloques:
//! 1. **Providers** (`DataProvider`, `EntityNode`, `DisplayType`) — fuente de
@@ -1,5 +1,5 @@
[package]
name = "yahweh-launcher"
name = "nahual-launcher"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,7 +7,7 @@ description = "Launcher GPUI reusable: Application::new + Theme::install_default
[dependencies]
gpui = { workspace = true }
yahweh-theme = { path = "../theme" }
nahual-theme = { path = "../theme" }
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
@@ -36,7 +36,7 @@ use gpui::{
App, AppContext, Application, Bounds, Context, Render, SharedString, TitlebarOptions,
WindowBounds, WindowOptions, px,
};
use yahweh_theme::Theme;
use nahual_theme::Theme;
/// Configuración del primer (y normalmente único) ventana del app.
///
@@ -1,5 +1,5 @@
[package]
name = "yahweh-meta-runtime"
name = "nahual-meta-runtime"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -9,4 +9,4 @@ description = "Yahweh — meta-runtime: helpers puros (parse, delta, validación
serde_json = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true, features = ["serde"] }
yahweh-meta-schema = { path = "../meta-schema" }
nahual-meta-schema = { path = "../meta-schema" }
@@ -1,5 +1,5 @@
//! `MetaBackend` trait — la frontera entre el widget metainterfaz
//! (yahweh) y la implementación concreta de persistencia/ejecución
//! (nahual) y la implementación concreta de persistencia/ejecución
//! (nakui-core, Surreal, mocks para tests).
//!
//! El widget consume este trait; el binario lo implementa con su
@@ -44,7 +44,7 @@ impl WriteOutcome {
}
/// Backend que un widget de metainterfaz usa para leer y mutar
/// records. Decoupla el widget (yahweh) de la implementación
/// records. Decoupla el widget (nahual) de la implementación
/// concreta (nakui-core, Surreal, mock para tests).
///
/// # Convención sobre ids
@@ -58,7 +58,7 @@ impl WriteOutcome {
///
/// El backend ES la fuente de verdad sobre invariantes (KCL/Nickel
/// post-checks, conservación, etc.). El widget pre-valida shape
/// (yahweh-meta-runtime: `parse_field_value`, `validate_entity_refs`)
/// (nahual-meta-runtime: `parse_field_value`, `validate_entity_refs`)
/// pero el backend puede rebotar con `Err(...)` si su validación
/// adicional falla — el widget muestra el error al usuario.
///
@@ -1,6 +1,6 @@
//! `yahweh-meta-runtime` — helpers puros para runtimes metainterfaz.
//! `nahual-meta-runtime` — helpers puros para runtimes metainterfaz.
//!
//! Consume [`yahweh_meta_schema`] (los tipos `Module`/`View`/`FieldSpec`/
//! Consume [`nahual_meta_schema`] (los tipos `Module`/`View`/`FieldSpec`/
//! `FieldKind`/`Action`/etc.) y aporta funciones puras que cualquier
//! widget renderer o backend ejecutor necesita:
//!
@@ -16,7 +16,7 @@
//! Sin GPUI, sin acoplamiento a un backend específico. Cualquier
//! implementación de store/log puede consumirlos.
//!
//! El widget render (form/list/modal) vive en otro crate yahweh
//! El widget render (form/list/modal) vive en otro crate nahual
//! que esto consume; el runtime concreto (`nakui-ui`) implementa la
//! conexión a su event-log/executor y compone ambos.
@@ -3,7 +3,7 @@
use serde_json::{json, Value};
use uuid::Uuid;
use yahweh_meta_schema::{FieldKind, FieldSpec};
use nahual_meta_schema::{FieldKind, FieldSpec};
/// Convierte el texto raw de un input al `Value` tipado según el
/// `kind` del spec.
@@ -101,7 +101,7 @@ pub fn infer_param_value(raw: &str) -> Value {
#[cfg(test)]
mod tests {
use super::*;
use yahweh_meta_schema::FieldSpec;
use nahual_meta_schema::FieldSpec;
fn spec(name: &str, kind: FieldKind, required: bool) -> FieldSpec {
FieldSpec {
@@ -4,7 +4,7 @@
//! del trait, sin acoplamiento a stores reales (event log,
//! SurrealDB, etc.). Útil para:
//!
//! - Tests del widget [`yahweh_widget_meta_form::MetaApp`] que
//! - Tests del widget [`nahual_widget_meta_form::MetaApp`] que
//! necesitan un backend funcional sin levantar nakui-core.
//! - Tests de cualquier consumer que tome `B: MetaBackend` y quiera
//! asserts sobre lecturas/escrituras sin tocar disco.
@@ -1,5 +1,5 @@
[package]
name = "yahweh-meta-schema"
name = "nahual-meta-schema"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -1,4 +1,4 @@
//! Schema declarativo de la metainterfaz (yahweh meta-schema).
//! Schema declarativo de la metainterfaz (nahual meta-schema).
//!
//! Cada **módulo** declara aquí qué menús, vistas, listas y
//! formularios expone, sin escribir código GPUI ni Rust. Cualquier
@@ -4,12 +4,12 @@
//! corra `NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui`
//! va a obtener los 6 módulos cargados sin tocar nada.
use yahweh_meta_schema::{load_modules_from_dir, FieldKind, View};
use nahual_meta_schema::{load_modules_from_dir, FieldKind, View};
fn examples_dir() -> std::path::PathBuf {
// Tests corren desde el dir del crate; el repo root está dos
// niveles arriba: crates/modules/nakui/ui-schema → repo.
// Tras el lift a yahweh, el crate vive en
// Tras el lift a nahual, el crate vive en
// `crates/modules/ui_engine/libs/meta-schema`, así que el repo
// root queda 5 niveles arriba.
let here = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
@@ -53,8 +53,8 @@ fn sales_engine_declares_nakui_module_dir_and_morphism() {
"sales_engine debería declarar nakui_module_dir"
);
let has_morphism_view = sales.views.values().any(|v| match v {
yahweh_meta_schema::View::Form(form) => {
matches!(form.on_submit, yahweh_meta_schema::Action::Morphism { .. })
nahual_meta_schema::View::Form(form) => {
matches!(form.on_submit, nahual_meta_schema::Action::Morphism { .. })
}
_ => false,
});
@@ -1,13 +1,13 @@
[package]
name = "yahweh-provider-fs"
name = "nahual-provider-fs"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "DataProvider de filesystem local con discernimiento de contenido (shipote-discern)."
[dependencies]
yahweh-core = { workspace = true }
nahual-core = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
notify = { workspace = true }
shipote-discern = { path = "../../../../../modules/shipote/shipote-discern" }
shuma-discern = { path = "../../../../shuma/shuma-discern" }
@@ -1,16 +1,16 @@
//! Provider de filesystem local. Crate puro: cero dependencia de UI.
//! Implementa `yahweh_core::DataProvider` listando hijos de un path con
//! Implementa `nahual_core::DataProvider` listando hijos de un path con
//! `std::fs::read_dir` y leyendo archivos a `Vec<u8>` via `tokio::io`.
use async_trait::async_trait;
use shipote_discern::{DiscernPipeline, Hint};
use shuma_discern::{DiscernPipeline, Hint};
use std::fs;
use std::io::{Cursor, Read};
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncWrite};
use yahweh_core::{DataProvider, DisplayType, EntityNode};
use nahual_core::{DataProvider, DisplayType, EntityNode};
pub const PROVIDER_ID: &str = "local_fs";
@@ -1,12 +1,12 @@
[package]
name = "yahweh-provider-sqlite"
name = "nahual-provider-sqlite"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "DataProvider de SQLite (jerarquía vía parent_id)."
[dependencies]
yahweh-core = { workspace = true }
nahual-core = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
rusqlite = { workspace = true }
@@ -8,7 +8,7 @@ use std::io::Cursor;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncRead, AsyncWrite};
use yahweh_core::{DataProvider, DisplayType, EntityNode};
use nahual_core::{DataProvider, DisplayType, EntityNode};
pub const PROVIDER_ID: &str = "sqlite_db";
@@ -1,5 +1,5 @@
[package]
name = "yahweh-theme"
name = "nahual-theme"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_theme` — paleta de colores y backgrounds compartidos.
//! `nahual_theme` — paleta de colores y backgrounds compartidos.
//!
//! El `Theme` se instala como `Global` de GPUI. Los widgets lo leen vía
//! `cx.global::<Theme>()` durante su `render`, y se subscriben con
@@ -475,13 +475,13 @@ impl Theme {
use std::path::{Path, PathBuf};
const CONFIG_SUBDIR: &str = "yahweh";
const CONFIG_SUBDIR: &str = "nahual";
const CONFIG_FILE: &str = "theme";
/// Path al archivo donde se persiste la preferencia de theme.
///
/// Convención XDG: `$XDG_CONFIG_HOME/yahweh/theme` si está set;
/// sino `$HOME/.config/yahweh/theme`. `None` si ni `XDG_CONFIG_HOME`
/// Convención XDG: `$XDG_CONFIG_HOME/nahual/theme` si está set;
/// sino `$HOME/.config/nahual/theme`. `None` si ni `XDG_CONFIG_HOME`
/// ni `HOME` están definidos (típicamente en sandboxes / CI).
pub fn config_path() -> Option<PathBuf> {
let base = std::env::var("XDG_CONFIG_HOME")
@@ -543,7 +543,7 @@ mod persistence_tests {
fn unique_path(label: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"yahweh-theme-test-{}-{}-{}",
"nahual-theme-test-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -611,6 +611,6 @@ mod persistence_tests {
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
assert_eq!(p, PathBuf::from("/custom/xdg/yahweh/theme"));
assert_eq!(p, PathBuf::from("/custom/xdg/nahual/theme"));
}
}
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-app-header"
name = "nahual-widget-app-header"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -7,8 +7,8 @@ description = "Yahweh — widget app-header: tira superior con label flex_grow +
[dependencies]
gpui = { workspace = true }
yahweh-theme = { path = "../../libs/theme" }
yahweh-widget-theme-switcher = { path = "../theme-switcher" }
nahual-theme = { path = "../../libs/theme" }
nahual-widget-theme-switcher = { path = "../theme-switcher" }
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
@@ -1,4 +1,4 @@
//! `yahweh-widget-app-header` — tira superior estándar de las apps
//! `nahual-widget-app-header` — tira superior estándar de las apps
//! del repo.
//!
//! Compone:
@@ -8,14 +8,14 @@
//! border-bottom = `theme.border`.
//! - Padding 16/12, text_size 14.
//!
//! Patrón emergente: `nakui-explorer`, `nouser-explorer`,
//! Patrón emergente: `nakui-explorer`, `akasha-explorer`,
//! `minga-explorer`, `brahman-broker-explorer` declaran headers
//! idénticos sólo cambiando el label. Ahora es 1 línea.
//!
//! # Ejemplo
//!
//! ```ignore
//! use yahweh_widget_app_header::app_header;
//! use nahual_widget_app_header::app_header;
//!
//! let header = app_header(cx, format!("Log: {} · {} entries", path, n));
//! div().child(header).child(body)
@@ -24,8 +24,8 @@
#![forbid(unsafe_code)]
use gpui::{div, prelude::*, px, App, IntoElement, SharedString};
use yahweh_theme::Theme;
use yahweh_widget_theme_switcher::theme_switcher;
use nahual_theme::Theme;
use nahual_widget_theme_switcher::theme_switcher;
/// Construye el header standard. Lee `Theme::global(cx)` para los
/// colors; falla si no hay theme instalado (panic propagado de
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-banner"
name = "nahual-widget-banner"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -7,4 +7,4 @@ description = "Yahweh — widget banner: tira horizontal de status (info/success
[dependencies]
gpui = { workspace = true }
yahweh-theme = { path = "../../libs/theme" }
nahual-theme = { path = "../../libs/theme" }
@@ -1,4 +1,4 @@
//! `yahweh-widget-banner` — tiras horizontales de status.
//! `nahual-widget-banner` — tiras horizontales de status.
//!
//! Cuatro variants con paleta consistente entre apps:
//!
@@ -16,7 +16,7 @@
//! # Ejemplo
//!
//! ```ignore
//! use yahweh_widget_banner::{banner, Banner};
//! use nahual_widget_banner::{banner, Banner};
//!
//! // Toast simple (success):
//! let toast = banner(Banner::Success, "guardado");
@@ -30,7 +30,7 @@
#![forbid(unsafe_code)]
use gpui::{div, hsla, prelude::*, px, App, Background, Div, Hsla, Rgba, SharedString};
use yahweh_theme::Theme;
use nahual_theme::Theme;
/// Severidad / tono del banner. Determina los colores del fondo,
/// texto y border (si aplica). El caller no debería mezclar
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-card"
name = "nahual-widget-card"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -7,4 +7,4 @@ description = "Yahweh — widget card: container con padding + rounded + flex_co
[dependencies]
gpui = { workspace = true }
yahweh-theme = { path = "../../libs/theme" }
nahual-theme = { path = "../../libs/theme" }
@@ -1,4 +1,4 @@
//! `yahweh-widget-card` — container card-shape para entries de
//! `nahual-widget-card` — container card-shape para entries de
//! timeline, info cards y similares.
//!
//! Aporta la **forma**: padding consistente (12/8), `rounded(4)`,
@@ -11,7 +11,7 @@
//! # Ejemplo
//!
//! ```ignore
//! use yahweh_widget_card::card;
//! use nahual_widget_card::card;
//! use gpui::{rgb, prelude::*, px};
//!
//! // Card con accent border-l (típico timeline entry):
@@ -31,7 +31,7 @@
#![forbid(unsafe_code)]
use gpui::{div, prelude::*, px, App, Div};
use yahweh_theme::Theme;
use nahual_theme::Theme;
/// Container card-shape: `flex_col` con padding `12/8`, `rounded(4)`,
/// `gap(2)` interno entre children y `mb(4)` para separación
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-container-core"
name = "nahual-widget-container-core"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,4 +7,4 @@ description = "Tipos compartidos para contenedores (ChildSlot, etc.). Imported p
[dependencies]
gpui = { workspace = true }
yahweh-core = { workspace = true }
nahual-core = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_widget_container_core` — tipos compartidos por todos los
//! `nahual_widget_container_core` — tipos compartidos por todos los
//! contenedores (Splitter, Tabs, Tiled, futuros).
//!
//! La pieza más relevante es [`ChildSlot`]: el "paquete" con que la Shell
@@ -17,7 +17,7 @@
//! - Tiled: usa ambos opcionalmente (peso de tile, label hover).
use gpui::AnyView;
use yahweh_core::NodeId;
use nahual_core::NodeId;
/// Slot de un hijo entregado a un contenedor. La Shell construye el
/// `Vec<ChildSlot>` haciendo DFS sobre el `LayerConfig` del JSON.
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-meta-form"
name = "nahual-widget-meta-form"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -9,12 +9,12 @@ description = "Yahweh — widget metainterfaz: lista, formulario, modal de delet
gpui = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true, features = ["serde"] }
yahweh-meta-runtime = { path = "../../libs/meta-runtime" }
yahweh-meta-schema = { path = "../../libs/meta-schema" }
yahweh-theme = { path = "../../libs/theme" }
yahweh-widget-banner = { path = "../banner" }
yahweh-widget-theme-switcher = { path = "../theme-switcher" }
yahweh-widget-text-input = { path = "../text_input" }
nahual-meta-runtime = { path = "../../libs/meta-runtime" }
nahual-meta-schema = { path = "../../libs/meta-schema" }
nahual-theme = { path = "../../libs/theme" }
nahual-widget-banner = { path = "../banner" }
nahual-widget-theme-switcher = { path = "../theme-switcher" }
nahual-widget-text-input = { path = "../text_input" }
[dev-dependencies]
# Activar TestAppContext + helpers para tests del widget que
@@ -1,7 +1,7 @@
//! `yahweh-widget-meta-form` — widget GPUI de metainterfaz.
//! `nahual-widget-meta-form` — widget GPUI de metainterfaz.
//!
//! Renderea cualquier conjunto de [`yahweh_meta_schema::Module`] sobre
//! cualquier impl de [`yahweh_meta_runtime::MetaBackend`]: sidebar con
//! Renderea cualquier conjunto de [`nahual_meta_schema::Module`] sobre
//! cualquier impl de [`nahual_meta_runtime::MetaBackend`]: sidebar con
//! menús, list/form views, modal de delete, EntityRef selector.
//!
//! El widget es **app-agnostic**: no asume Nakui, ni storage
@@ -28,16 +28,16 @@ use gpui::{
use serde_json::Value;
use uuid::Uuid;
use yahweh_meta_runtime::{
use nahual_meta_runtime::{
compute_clear_fields, compute_field_delta, human_label_for_record, parse_field_value,
render_value, resolve_param_value, short_uuid, validate_entity_refs, value_to_input_text,
MetaBackend, WriteOutcome,
};
use yahweh_meta_schema::{Action, FieldKind, FieldSpec, FormView, ListView, Module, View};
use yahweh_theme::Theme;
use yahweh_widget_banner::{banner_themed, themed_colors, Banner};
use yahweh_widget_text_input::TextInput;
use yahweh_widget_theme_switcher::theme_switcher;
use nahual_meta_schema::{Action, FieldKind, FieldSpec, FormView, ListView, Module, View};
use nahual_theme::Theme;
use nahual_widget_banner::{banner_themed, themed_colors, Banner};
use nahual_widget_text_input::TextInput;
use nahual_widget_theme_switcher::theme_switcher;
/// Estado del runtime de UI. Toda la persistencia/ejecución está
/// detrás del trait [`MetaBackend`]; este struct sólo conoce GPUI
@@ -1182,7 +1182,7 @@ mod tests {
//! Tests del widget. Funciones puras (sin GPUI cx) sólo. Los
//! tests de backend impl viven en el binario que provee el
//! backend (ej: nakui-ui). Los helpers movidos a
//! yahweh-meta-runtime se testean allí.
//! nahual-meta-runtime se testean allí.
use super::*;
use serde_json::json;
@@ -1,5 +1,5 @@
//! Tests E2E del widget [`MetaApp`] usando
//! [`yahweh_meta_runtime::testing::MockBackend`] +
//! [`nahual_meta_runtime::testing::MockBackend`] +
//! `gpui::TestAppContext`.
//!
//! Cubren el flujo "construir el widget con un backend mock,
@@ -15,12 +15,12 @@ use std::collections::BTreeMap;
use gpui::TestAppContext;
use serde_json::json;
use yahweh_meta_runtime::testing::MockBackend;
use yahweh_meta_schema::{
use nahual_meta_runtime::testing::MockBackend;
use nahual_meta_schema::{
Action, Column, EntitySpec, FieldKind, FieldSpec, FormView, ListView, MenuItem, Module, View,
};
use yahweh_theme::Theme;
use yahweh_widget_meta_form::MetaApp;
use nahual_theme::Theme;
use nahual_widget_meta_form::MetaApp;
/// Helper: módulo demo simple con una entity Customer + view list.
fn customers_module() -> Module {
@@ -177,7 +177,7 @@ fn backend_state_visible_from_widget_perspective(cx: &mut TestAppContext) {
id,
json!({"name": "Acme"}),
)]);
use yahweh_meta_runtime::MetaBackend;
use nahual_meta_runtime::MetaBackend;
let rows = mock_check.list_records("Customer");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].0, id);
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-splitter"
name = "nahual-widget-splitter"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,6 +7,6 @@ description = "SplitContainer — n hijos con flex weights y divisores arrastrab
[dependencies]
gpui = { workspace = true }
yahweh-core = { workspace = true }
yahweh-theme = { workspace = true }
yahweh-widget-container-core = { workspace = true }
nahual-core = { workspace = true }
nahual-theme = { workspace = true }
nahual-widget-container-core = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_widget_splitter` — `SplitContainer` genérico.
//! `nahual_widget_splitter` — `SplitContainer` genérico.
//!
//! Aloja `n` hijos `AnyView` con flex weights individuales y un divisor
//! arrastrable entre cada par adyacente. Dirección horizontal o vertical
@@ -24,9 +24,9 @@ use gpui::{
MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, Window, canvas, div, prelude::*, px,
};
use yahweh_core::{LayoutDirection, NodeId};
use yahweh_theme::Theme;
pub use yahweh_widget_container_core::ChildSlot;
use nahual_core::{LayoutDirection, NodeId};
use nahual_theme::Theme;
pub use nahual_widget_container_core::ChildSlot;
#[derive(Clone, Debug)]
pub enum SplitEvent {
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-stat-card"
name = "nahual-widget-stat-card"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -7,8 +7,8 @@ description = "Yahweh — widget stat card: tarjeta de dashboard con border-l ac
[dependencies]
gpui = { workspace = true }
yahweh-widget-card = { path = "../card" }
nahual-widget-card = { path = "../card" }
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
yahweh-theme = { path = "../../libs/theme" }
nahual-theme = { path = "../../libs/theme" }
@@ -1,7 +1,7 @@
//! `yahweh-widget-stat-card` — tarjeta de dashboard con accent.
//! `nahual-widget-stat-card` — tarjeta de dashboard con accent.
//!
//! Compone:
//! - **`card_themed(cx)`** del [`yahweh_widget_card`] como contenedor.
//! - **`card_themed(cx)`** del [`nahual_widget_card`] como contenedor.
//! - **Border-l-4** con un color de accent que el caller decide
//! (verde = OK, rojo = error, etc.).
//! - **Label** chico arriba en el color del accent.
@@ -21,7 +21,7 @@
//! # Ejemplo
//!
//! ```ignore
//! use yahweh_widget_stat_card::stat_card;
//! use nahual_widget_stat_card::stat_card;
//! use gpui::{rgb, Hsla};
//!
//! let cell = stat_card(
@@ -39,7 +39,7 @@
#![forbid(unsafe_code)]
use gpui::{div, prelude::*, px, App, IntoElement, SharedString};
use yahweh_widget_card::card_themed;
use nahual_widget_card::card_themed;
/// Construye una stat card. Devuelve `impl IntoElement` para que el
/// caller pueda meterla directo como child de cualquier
@@ -124,7 +124,7 @@ pub fn stat_card(
mod tests {
use super::*;
use gpui::TestAppContext;
use yahweh_theme::Theme;
use nahual_theme::Theme;
/// Smoke test: el constructor lee el theme global y devuelve un
/// IntoElement. Sin TestAppContext no podemos asertar render
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-tabs"
name = "nahual-widget-tabs"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,6 +7,6 @@ description = "TabContainer — n hijos, uno visible, header con tabs clickeable
[dependencies]
gpui = { workspace = true }
yahweh-core = { workspace = true }
yahweh-theme = { workspace = true }
yahweh-widget-container-core = { workspace = true }
nahual-core = { workspace = true }
nahual-theme = { workspace = true }
nahual-widget-container-core = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_widget_tabs` — `TabContainer`.
//! `nahual_widget_tabs` — `TabContainer`.
//!
//! `n` hijos `AnyView`, **uno visible** por vez (la pestaña activa). Header
//! horizontal con un botón por hijo; click cambia la pestaña activa. La
@@ -13,9 +13,9 @@ use gpui::{
px,
};
use yahweh_core::NodeId;
use yahweh_theme::Theme;
use yahweh_widget_container_core::ChildSlot;
use nahual_core::NodeId;
use nahual_theme::Theme;
use nahual_widget_container_core::ChildSlot;
#[derive(Clone, Debug)]
#[allow(dead_code)]
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-text-input"
name = "nahual-widget-text-input"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,4 +7,4 @@ description = "TextInput minimalista para diálogos (rename, prompts). Single-li
[dependencies]
gpui = { workspace = true }
yahweh-theme = { workspace = true }
nahual-theme = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_widget_text_input` — input de texto minimal.
//! `nahual_widget_text_input` — input de texto minimal.
//!
//! Diseñado para diálogos cortos (rename, prompts). NO es un editor — no
//! soporta:
@@ -27,7 +27,7 @@ use gpui::{
SharedString, Task, Window, div, prelude::*, px,
};
use yahweh_theme::Theme;
use nahual_theme::Theme;
/// Período de toggle del caret. 500ms es el estándar de los inputs
/// del SO; ni rápido demasiado (distrae) ni lento (parece muerto).
@@ -187,7 +187,7 @@ impl Render for TextInput {
};
div()
.id("yahweh-text-input")
.id("nahual-text-input")
.track_focus(&self.focus_handle)
.key_context("YahwehTextInput")
.on_key_down(cx.listener(Self::handle_key_down))
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-theme-switcher"
name = "nahual-widget-theme-switcher"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -7,7 +7,7 @@ description = "Yahweh — widget para ciclar entre presets de Theme en runtime.
[dependencies]
gpui = { workspace = true }
yahweh-theme = { path = "../../libs/theme" }
nahual-theme = { path = "../../libs/theme" }
[dev-dependencies]
# TestAppContext + #[gpui::test] para tests del switcher.
@@ -1,4 +1,4 @@
//! `yahweh-widget-theme-switcher` — botón clickable para ciclar
//! `nahual-widget-theme-switcher` — botón clickable para ciclar
//! entre los presets de `Theme`.
//!
//! El botón muestra el nombre del theme actual; al click avanza al
@@ -16,7 +16,7 @@
//! # Uso
//!
//! ```ignore
//! use yahweh_widget_theme_switcher::theme_switcher;
//! use nahual_widget_theme_switcher::theme_switcher;
//!
//! // Adentro de Render::render:
//! let switcher = theme_switcher(cx);
@@ -26,7 +26,7 @@
#![forbid(unsafe_code)]
use gpui::{div, prelude::*, px, App, ClickEvent, IntoElement, SharedString, Window};
use yahweh_theme::Theme;
use nahual_theme::Theme;
/// Construye el switcher: una `Div` clickable con el nombre del
/// theme actual + flecha indicadora. Al click rota al siguiente
@@ -44,7 +44,7 @@ pub fn theme_switcher(cx: &mut App) -> impl IntoElement {
let label = format!("Tema: {}", theme.name);
div()
.id("yahweh-theme-switcher")
.id("nahual-theme-switcher")
.px(px(8.))
.py(px(4.))
.bg(theme.bg_panel_alt.clone())
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-tiled"
name = "nahual-widget-tiled"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,6 +7,6 @@ description = "TiledContainer — n hijos en grid auto cols×rows."
[dependencies]
gpui = { workspace = true }
yahweh-core = { workspace = true }
yahweh-theme = { workspace = true }
yahweh-widget-container-core = { workspace = true }
nahual-core = { workspace = true }
nahual-theme = { workspace = true }
nahual-widget-container-core = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_widget_tiled` — `TiledContainer`.
//! `nahual_widget_tiled` — `TiledContainer`.
//!
//! Distribuye `n` hijos en una grilla auto-calculada: `cols = ⌈√n⌉`,
//! `rows = ⌈n/cols⌉`. Las celdas tienen el mismo peso.
@@ -33,9 +33,9 @@ use gpui::{
MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, Window, canvas, div, prelude::*, px,
};
use yahweh_core::NodeId;
use yahweh_theme::Theme;
use yahweh_widget_container_core::ChildSlot;
use nahual_core::NodeId;
use nahual_theme::Theme;
use nahual_widget_container_core::ChildSlot;
#[derive(Clone, Debug)]
#[allow(dead_code)]
@@ -1,5 +1,5 @@
[package]
name = "yahweh-widget-tree"
name = "nahual-widget-tree"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,4 +7,4 @@ description = "TreeView genérico — widget agnóstico de dominio sobre GPUI."
[dependencies]
gpui = { workspace = true }
yahweh-theme = { workspace = true }
nahual-theme = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_widget_tree` — TreeView genérico, agnóstico del dominio.
//! `nahual_widget_tree` — TreeView genérico, agnóstico del dominio.
//!
//! Anatomía: el host (FileExplorer, DatabaseExplorer, …) calcula una lista
//! plana `Vec<TreeRow>` por DFS y la empuja con `set_rows`. El TreeView solo
@@ -20,7 +20,7 @@ use gpui::{
MouseDownEvent, Pixels, Point, Render, SharedString, Window, div, prelude::*, px,
uniform_list,
};
use yahweh_theme::Theme;
use nahual_theme::Theme;
// =====================================================================
// Modelo público
@@ -253,7 +253,7 @@ impl Render for TreeView {
let list_id: ElementId = self.list_id.clone().into();
div()
.id("yahweh-tree-root")
.id("nahual-tree-root")
.key_context("YahwehTree")
.size_full()
.bg(theme.bg_panel.clone())
+35
View File
@@ -0,0 +1,35 @@
# modules/nakui/ — ERP categórico
**Propósito.** Backend de un ERP modelado como teoría categórica:
objetos = entidades, morfismos = operaciones de negocio. Schema
declarativo en Nickel, persistencia event-log con replay, validación
de tipos cross-field.
## Crates
| crate | tipo | rol |
| ------------- | ----- | ---------------------------------------------------- |
| `nakui-core` | lib | Schema (Nickel) + evaluador + event log + store |
## Dependencias
- Consume `protocol/brahman-cards` (templates Nickel).
- `nakui-core` evalúa Nickel in-process (no más kcl_wrapper).
- Consumido por: `apps/nakui-ui` (MetaUi+MetaForm) y
`apps/nakui-explorer` (dashboard sobre stack nahual).
## Modelo
```
Schema -> { entities: [Entity], morphisms: [Morphism] }
Entity -> { name, fields: [FieldSpec] }
Morphism -> { domain, codomain, compute_fn }
Action -> Create | Update | Delete | Morphism(name, params)
EventLog -> [Action] // append-only, replay al startup
```
## Estado
LOC 5,618 (nakui-core) + 1,021 (nakui-ui) + 424 (nakui-explorer).
Tests robustos. 6 módulos ERP estándar incluidos como ejemplos
(ventas, inventario, etc.). Ver `docs/changelog/nakui.md`.
+2 -2
View File
@@ -38,8 +38,8 @@ nickel-lang = "2.0.0"
surrealdb = { version = "2", default-features = false, features = ["kv-mem"] }
# Brahman protocol — presencia ante el Init cuando `nakui run` arranca.
brahman-card = { path = "../../../core/brahman-card" }
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
brahman-card = { path = "../../../protocol/brahman-card" }
brahman-sidecar = { path = "../../../protocol/brahman-sidecar" }
[[bin]]
name = "nakui"
+1 -1
View File
@@ -463,7 +463,7 @@ fn cmd_verify_log(args: &[String]) -> Result<(), CliError> {
/// Lifecycle Daemon (proceso largo). Flujos JSON: consume `command`
/// (queries del UI), produce `report` (resultados de cómputo). Los
/// nombres están escogidos para que el broker pueda matchearlos contra
/// `user-intent` / `render-data` de yahweh-shell por compatibilidad de
/// `user-intent` / `render-data` de nahual-shell por compatibilidad de
/// tipo (todos `json`).
fn brahman_card_for_nakui() -> brahman_card::Card {
use brahman_card::{
+35
View File
@@ -0,0 +1,35 @@
# modules/pineal/ — Data-viz agnóstica (era lapaloma)
**Propósito.** Motor de gráficos con backends pluggables. Los crates
de visualización no conocen `gpui` ni `wgpu`: hablan contra el trait
`Canvas`, y producen un `RenderPlan` que cada backend ejecuta.
## Crates
| crate | tipo | rol |
| ------------------ | ---- | ---------------------------------------------------- |
| `pineal-core` | lib | Datos: Series, Point, Rect, Color, RingBuffer |
| `pineal-render` | lib | Trait `Canvas` + `RenderPlan` + backend GPUI (feat) |
| `pineal-cartesian` | lib | Axes + viewport + series sobre coord cartesiano |
| `pineal-financial` | lib | Candlestick, OHLC sobre cartesian |
| `pineal-stream` | lib | Streaming a 60Hz desde RingBuffer |
| `pineal-phosphor` | lib | Trail CRT con alpha decay sobre stream |
| `pineal-polar` | lib | Coord polar (stub) |
| `pineal-heatmap` | lib | Grid 2D color-mapped (stub) |
| `pineal-treemap` | lib | Rectangular treemap (stub) |
| `pineal-flow` | lib | Sankey/flow diagrams (stub) |
| `pineal-mesh` | lib | Triangle mesh + barycentric (stub) |
| `pineal-export` | lib | SVG/PNG export del RenderPlan |
| `pineal-umbrella` | lib | Re-export con features `core/render/cartesian/...` |
## Dependencias
- Todos ← `pineal-core`. Render-dependientes ← `pineal-render`.
- Backend GPUI: feature gate `gpui` en pineal-render activa
`gpui_backend.rs` (consumido por widgets en `apps/pineal-*-demo`).
## Estado
LOC ~3,900. 5 charts implementados (cartesian/financial/stream/phosphor
/export). 5 stubs (<50 LOC c/u: polar, heatmap, treemap, flow, mesh).
Roadmap: completar los 5 stubs en orden polar → heatmap → treemap.
@@ -1,5 +1,5 @@
[package]
name = "lapaloma-cartesian"
name = "pineal-cartesian"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -8,10 +8,10 @@ publish = { workspace = true }
description = "Lapaloma — gráficos cartesianos: LineSeries / BarSeries / AreaSeries, viewport con pan/zoom, picture cache, ejes con decimación, tooltips."
[dependencies]
lapaloma-core = { path = "../../libs/lapaloma-core" }
lapaloma-render = { path = "../lapaloma-render" }
pineal-core = { path = "../core" }
pineal-render = { path = "../render" }
gpui = { workspace = true, optional = true }
[features]
default = ["gpui"]
gpui = ["dep:gpui", "lapaloma-render/gpui"]
gpui = ["dep:gpui", "pineal-render/gpui"]
@@ -19,8 +19,8 @@
//! formato (epoch ms → "HH:MM:SS"), `format_tick` no entiende
//! semántica.
use lapaloma_core::scale::nice_step;
use lapaloma_render::{Canvas, Color, Point, StrokeStyle};
use pineal_core::scale::nice_step;
use pineal_render::{Canvas, Color, Point, StrokeStyle};
use crate::coord_system::CoordinateSystem;
use crate::viewport::ChartViewport;
@@ -8,7 +8,7 @@
//! La proyección invierte Y para que un valor alto quede arriba.
use crate::viewport::ChartViewport;
use lapaloma_render::{Point, Rect};
use pineal_render::{Point, Rect};
#[derive(Debug, Clone, Copy)]
pub struct CoordinateSystem {
@@ -31,8 +31,8 @@ use gpui::{
Pixels, Style, Window,
};
use lapaloma_core::buffer::DataBuffer;
use lapaloma_render::{Canvas, Color, Rect, StrokeStyle, WindowCanvas};
use pineal_core::buffer::DataBuffer;
use pineal_render::{Canvas, Color, Rect, StrokeStyle, WindowCanvas};
use crate::axis::{self, AxisStyle};
use crate::coord_system::CoordinateSystem;
@@ -1,11 +1,11 @@
//! `lapaloma-cartesian` — gráficos cartesianos.
//! `pineal-cartesian` — gráficos cartesianos.
//!
//! Este crate trae:
//!
//! - **`viewport`** — `ChartViewport` con `(x_min, x_max, y_min, y_max)`
//! y helpers de pan/zoom anchor-preserving.
//! - **`coord_system`** — proyecta valores de dominio → pixeles del
//! plot usando las escalas de `lapaloma-core::scale`.
//! plot usando las escalas de `pineal-core::scale`.
//! - **`series`** — trait `Series` + impls `LineSeries`, `BarSeries`,
//! `AreaSeries`. Cada serie decide LTTB vs raw según densidad.
//! - **`axis`** — ejes con nice-ticks (Wilkinson) y decimación de
@@ -14,7 +14,7 @@
//! invalidación. Clipea el outer canvas antes del translate
//! (bug 0.3.0 del Flutter).
//! - **`element`** — el `Element` GPUI que envuelve todo lo de
//! arriba y se inserta en un layout yahweh.
//! arriba y se inserta en un layout nahual.
//!
//! Hoy todos los módulos están como placeholders; la primera
//! impl real va a ser `LineSeries` + `element` end-to-end para
@@ -2,12 +2,12 @@
//! sobre coordenadas cartesianas, + impl [`LineSeries`].
//!
//! La firma es agnóstica de `gpui`: el painter dibuja contra
//! `lapaloma_render::Canvas`. El Element GPUI envuelve esto y
//! `pineal_render::Canvas`. El Element GPUI envuelve esto y
//! pasa un adaptador del Canvas trait sobre el PaintContext nativo.
use lapaloma_core::buffer::DataBuffer;
use lapaloma_core::lttb;
use lapaloma_render::{Canvas, StrokeStyle};
use pineal_core::buffer::DataBuffer;
use pineal_core::lttb;
use pineal_render::{Canvas, StrokeStyle};
use crate::coord_system::CoordinateSystem;
@@ -39,7 +39,7 @@ pub trait Series {
/// pixel pasado, si está dentro del threshold de hit (default
/// 8 px). El default impl asume que el data buffer expuesto
/// por la serie está sorted por X (caso de [`LineSeries`]).
fn hit_test(&self, _pixel: lapaloma_render::Point, _cs: &CoordinateSystem) -> Option<usize> {
fn hit_test(&self, _pixel: pineal_render::Point, _cs: &CoordinateSystem) -> Option<usize> {
None
}
}
@@ -97,9 +97,9 @@ impl<'a> Series for LineSeries<'a> {
canvas.stroke_polyline(ctx.scratch, self.stroke);
}
fn hit_test(&self, pixel: lapaloma_render::Point, cs: &CoordinateSystem) -> Option<usize> {
fn hit_test(&self, pixel: pineal_render::Point, cs: &CoordinateSystem) -> Option<usize> {
let (target_x, _) = cs.pixel_to_data(pixel);
let idx = lapaloma_core::spatial::SpatialIndex::new(self.data.coords())
let idx = pineal_core::spatial::SpatialIndex::new(self.data.coords())
.nearest(target_x as f32)?;
// Threshold de 8px sobre la distancia en pixeles real,
// no sólo la X — evita match cuando el punto está lejos
@@ -115,7 +115,7 @@ impl<'a> Series for LineSeries<'a> {
mod tests {
use super::*;
use crate::viewport::ChartViewport;
use lapaloma_render::{Color, Point, Rect, RenderCmd, RenderPlan};
use pineal_render::{Color, Point, Rect, RenderCmd, RenderPlan};
/// Canvas mock que captura comandos en un `RenderPlan`.
struct Capture {
@@ -261,7 +261,7 @@ mod tests {
Rect::new(0.0, 0.0, 100.0, 100.0),
);
// Punto (2,4) en data → ¿qué pixel? (2/4)·100=50, (1-4/16)·100=75
let target = lapaloma_render::Point::new(50.0, 75.0);
let target = pineal_render::Point::new(50.0, 75.0);
let hit = series.hit_test(target, &cs);
assert_eq!(hit, Some(2));
}
@@ -277,7 +277,7 @@ mod tests {
Rect::new(0.0, 0.0, 100.0, 100.0),
);
// Pixel muy lejos de la línea.
let far = lapaloma_render::Point::new(50.0, 99.0);
let far = pineal_render::Point::new(50.0, 99.0);
assert!(series.hit_test(far, &cs).is_none());
}
}
@@ -9,7 +9,7 @@
//! P2 zero-alloc: los buffers de DataBuffer / RingBuffer se quedan
//! quietos; sólo cambian cuatro `f64` en el viewport.
use lapaloma_render::Rect;
use pineal_render::Rect;
/// Rango visible en coordenadas de dominio. `f64` porque ejes
/// temporales con epoch ms se desbordan en `f32`.
@@ -1,5 +1,5 @@
[package]
name = "lapaloma-core"
name = "pineal-core"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }

Some files were not shown because too many files have changed in this diff Show More