chore: rename tahuantinsuyu → cosmobiologia

Rename clean del proyecto astrológico antes de empezar el módulo
web (fase 2 = server axum, fase 3 = cliente WASM). Hacerlo ahora
ahorra refactor de URLs, package.json, paths de assets HTML y
deploy configs que aparecerían con el nombre en cuanto exista el
server.

Mecánica:
- `git mv` de los 10 crates de módulo + 2 apps:
  * `crates/modules/tahuantinsuyu/` → `cosmobiologia/`
  * `crates/modules/tahuantinsuyu/tahuantinsuyu-*` →
    `cosmobiologia/cosmobiologia-*`
  * `crates/apps/tahuantinsuyu` y `tahuantinsuyu-cli` análogos.
- Sed sobre todos los `.rs` y `.toml`: `tahuantinsuyu` →
  `cosmobiologia` (cubre crate names, deps paths, use
  statements, ProjectDirs literals, binary names).
- Workspace `Cargo.toml`: members con paths nuevos.
- Memoria del proyecto (`~/.claude/.../memory/project_*.md`)
  actualizada.

Cero leftovers: `grep -rn tahuantinsuyu --include="*.rs"
--include="*.toml" crates/` devuelve vacío.

DB & XDG: clean slate. La nueva app arranca con DB vacía en
`$XDG_DATA_HOME/cosmobiologia/charts.db`. Si tenías cartas
guardadas, viven todavía en `~/.local/share/tahuantinsuyu/` —
las podés migrar manualmente con un `cp`.

IDs UI inalterados: el prefijo `tts-` de gpui ElementIds queda
igual (cosmético, no afecta funcionalidad). Cambiarlo a `cb-`
ahora sería 3-4 líneas más de sed pero ningún beneficio
operativo.

Tests: 20 verdes (10 shell + 10 render math). Compila full:
`cargo check -p cosmobiologia` OK.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-19 00:45:48 +00:00
parent 9084cf4b79
commit 06a1ca11ce
34 changed files with 325 additions and 315 deletions
@@ -0,0 +1,19 @@
[package]
name = "cosmobiologia-card"
version = { workspace = true }
edition = { workspace = true }
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" }
cosmobiologia-engine = { path = "../cosmobiologia-engine" }
cosmobiologia-model = { path = "../cosmobiologia-model" }
ulid = { workspace = true }
serde = { workspace = true }
postcard = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
directories = { workspace = true }
thiserror = { workspace = true }
@@ -0,0 +1,95 @@
//! `cosmobiologia-card` — Tarjeta de Presentación + sidecar de la app.
//!
//! Cualquier binario que levante Tahuantinsuyu llama [`spawn_sidecar`]
//! antes de abrir la ventana GPUI. La lógica de thread / tokio /
//! ping-loop vive en `brahman-sidecar`; aquí solo declaramos quién es
//! Tahuantinsuyu como módulo Brahman.
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
pub mod service;
use std::collections::BTreeSet;
use brahman_card::{
Card, Flow, Flows, FsPolicy, IpcPolicy, Lifecycle, Payload, Permissions, Priority, Supervision,
TypeRef, CARD_SCHEMA_VERSION,
};
use ulid::Ulid;
/// Label canónico — coincide con el binario y aparece en `ListEntes`.
pub const LABEL: &str = "brahman.cosmobiologia";
/// Spawn fire-and-forget. Si el Init no está corriendo, el sidecar
/// loggea y termina; la app sigue ejecutándose standalone.
pub fn spawn_sidecar() {
brahman_sidecar::spawn(build_card());
}
/// Construye la Card. Expuesto público para tests + para shells que
/// quieran inspeccionar el manifiesto antes de spawnear. Anuncia el
/// path del service socket en `Card.service_socket` para que otros
/// módulos brahman, después de matchear via el broker, puedan conectar
/// directo al data plane.
pub fn build_card() -> Card {
Card {
schema_version: CARD_SCHEMA_VERSION,
id: Ulid::new(),
lineage: None,
label: LABEL.into(),
service_socket: Some(service::default_service_socket()),
provides: BTreeSet::new(),
requires: BTreeSet::new(),
payload: Payload::Virtual,
supervision: Supervision::Delegate,
lifecycle: Lifecycle::Widget,
priority: Priority::Normal,
permissions: Permissions {
// La app guarda su DB SQLite en disco; necesita RW filesystem.
filesystem: FsPolicy::ReadWrite,
ipc: IpcPolicy {
allow: vec!["wit-v1".into()],
},
..Default::default()
},
flow: Flows {
// Recibe peticiones de cómputo (carta natal, transit, etc.)
// serializadas como JSON. La forma exacta la define
// `cosmobiologia-engine`.
input: vec![Flow {
name: "chart-request".into(),
ty: TypeRef::Primitive {
name: "json".into(),
},
pin_to: None,
}],
// Publica el resultado de un cómputo (placements, aspectos,
// casas) también como JSON. Otras apps brahman pueden
// consumirlo para visualizar o derivar.
output: vec![Flow {
name: "chart-result".into(),
ty: TypeRef::Primitive {
name: "json".into(),
},
pin_to: None,
}],
},
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn card_label_and_flow() {
let c = build_card();
assert_eq!(c.label, LABEL);
assert_eq!(c.flow.input.len(), 1);
assert_eq!(c.flow.output.len(), 1);
assert_eq!(c.flow.input[0].name, "chart-request");
assert_eq!(c.flow.output[0].name, "chart-result");
}
}
@@ -0,0 +1,244 @@
//! Service socket de Tahuantinsuyu — protocolo y server.
//!
//! La Card de Tahuantinsuyu declara desde fase 1 los flows
//! `chart-request` (input) y `chart-result` (output). Acá vive el
//! **data plane** real que los implementa: un Unix socket sobre el que
//! cualquier módulo brahman puede pedir un cómputo de carta y recibir
//! el RenderModel ya armado.
//!
//! ## Protocolo
//!
//! Frame: `u32 length` little-endian + `postcard`-serialized payload.
//! Misma forma que `brahman-handshake` para reducir sorpresas.
//!
//! ## Endpoints
//!
//! - `ComputeRequest::Natal { birth, config, offset_minutes }` →
//! `ComputeResponse::Render { render }` o `Error { message }`.
//! - `ComputeRequest::Ping` → `ComputeResponse::Pong`.
//!
//! El service no expone los overlays (transit / synastry / etc) por
//! ahora — son una pasada futura. Cubre el caso 80%: "necesito la
//! carta natal de estos datos".
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use cosmobiologia_engine::{compose_with_options, NatalOptions, RenderModel};
use cosmobiologia_model::{Chart, ChartId, ChartKind, ContactId, StoredBirthData, StoredChartConfig};
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
use tracing::{debug, error, info, warn};
/// Path canónico del service socket. Usa `XDG_RUNTIME_DIR` si está
/// (por usuario, no persistente), sino cae a `/tmp/cosmobiologia.sock`.
pub fn default_service_socket() -> PathBuf {
if let Some(rt) = directories::ProjectDirs::from("net", "gioser", "cosmobiologia") {
// ProjectDirs no expone runtime_dir directo en todas las
// plataformas — usamos cache_dir como fallback estable.
let mut p = rt.cache_dir().to_path_buf();
std::fs::create_dir_all(&p).ok();
p.push("service.sock");
return p;
}
PathBuf::from("/tmp/cosmobiologia.sock")
}
// =====================================================================
// Tipos del protocolo
// =====================================================================
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComputeRequest {
/// Salud del server. Usá para verificar que el sidecar está vivo.
Ping,
/// Pide el cómputo de una carta natal pura (sin overlays).
Natal {
birth: StoredBirthData,
config: StoredChartConfig,
/// Offset en minutos sobre el instante natal — útil para
/// rectificación rápida sin guardar variantes.
#[serde(default)]
offset_minutes: i64,
/// Label opcional para que el render lo lleve en su title.
#[serde(default)]
label: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComputeResponse {
Pong,
Render { render: RenderModel },
Error { message: String },
}
// =====================================================================
// Errores
// =====================================================================
#[derive(Debug, Error)]
pub enum ServiceError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("postcard: {0}")]
Postcard(#[from] postcard::Error),
#[error("frame demasiado grande: {0} bytes")]
FrameTooLarge(u32),
#[error("connect a {path}: {source}")]
Connect {
path: PathBuf,
source: std::io::Error,
},
}
/// Cap de tamaño de frame — defensivo contra peers malformados.
const MAX_FRAME_BYTES: u32 = 1024 * 1024; // 1 MiB
// =====================================================================
// Server
// =====================================================================
/// Arranca el server async sobre `socket_path`. Cada conexión nueva
/// procesa una secuencia de Request/Response hasta que el peer cierra.
pub async fn serve(socket_path: PathBuf) -> Result<(), ServiceError> {
// Si quedó un socket viejo del run anterior, lo borramos.
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path)?;
info!(socket = %socket_path.display(), "cosmobiologia service socket arriba");
loop {
let (stream, _addr) = listener.accept().await?;
tokio::spawn(async move {
if let Err(e) = serve_connection(stream).await {
warn!(?e, "connection terminó con error");
}
});
}
}
async fn serve_connection(mut stream: UnixStream) -> Result<(), ServiceError> {
loop {
let request: ComputeRequest = match read_frame(&mut stream).await {
Ok(r) => r,
Err(ServiceError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
debug!("peer cerró");
return Ok(());
}
Err(e) => return Err(e),
};
let response = handle(request);
write_frame(&mut stream, &response).await?;
}
}
fn handle(req: ComputeRequest) -> ComputeResponse {
match req {
ComputeRequest::Ping => ComputeResponse::Pong,
ComputeRequest::Natal {
birth,
config,
offset_minutes,
label,
} => {
let chart = Chart {
id: ChartId::new(),
contact_id: ContactId::new(),
kind: ChartKind::Natal,
label: label.unwrap_or_else(|| "Service request".into()),
birth_data: birth,
config,
related_chart_id: None,
created_at_ms: 0,
};
match compose_with_options(&chart, offset_minutes, &[], &NatalOptions::default()) {
Ok(render) => ComputeResponse::Render { render },
Err(e) => ComputeResponse::Error {
message: format!("{}", e),
},
}
}
}
}
// =====================================================================
// Client helper
// =====================================================================
/// Cliente async: abre el socket, envía un request, espera la response.
/// Cierra la conexión al volver (no reusable; útil para CLI/tests).
pub async fn request(
socket: &Path,
req: &ComputeRequest,
) -> Result<ComputeResponse, ServiceError> {
let mut stream = UnixStream::connect(socket).await.map_err(|source| {
ServiceError::Connect {
path: socket.to_path_buf(),
source,
}
})?;
write_frame(&mut stream, req).await?;
read_frame(&mut stream).await
}
// =====================================================================
// Framing
// =====================================================================
async fn write_frame<T: Serialize>(stream: &mut UnixStream, value: &T) -> Result<(), ServiceError> {
let bytes = postcard::to_allocvec(value)?;
let len = u32::try_from(bytes.len()).map_err(|_| ServiceError::FrameTooLarge(u32::MAX))?;
if len > MAX_FRAME_BYTES {
return Err(ServiceError::FrameTooLarge(len));
}
stream.write_u32_le(len).await?;
stream.write_all(&bytes).await?;
stream.flush().await?;
Ok(())
}
async fn read_frame<T: for<'de> Deserialize<'de>>(
stream: &mut UnixStream,
) -> Result<T, ServiceError> {
let len = stream.read_u32_le().await?;
if len > MAX_FRAME_BYTES {
return Err(ServiceError::FrameTooLarge(len));
}
let mut buf = vec![0u8; len as usize];
stream.read_exact(&mut buf).await?;
let value = postcard::from_bytes(&buf)?;
Ok(value)
}
// =====================================================================
// Spawn helper para uso desde el binario GUI
// =====================================================================
/// Spawn fire-and-forget: thread aparte con tokio runtime current_thread
/// corriendo el server. Si la initialización falla, loggea warn y el
/// thread termina. El binario GUI sigue funcionando standalone.
pub fn spawn_service_thread(socket_path: PathBuf) {
std::thread::Builder::new()
.name("cosmobiologia-service".into())
.spawn(move || {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_io()
.build()
{
Ok(rt) => rt,
Err(e) => {
error!(?e, "no pude crear runtime para service thread");
return;
}
};
if let Err(e) = rt.block_on(serve(socket_path)) {
error!(?e, "service server terminó con error");
}
})
.map(|_| ())
.unwrap_or_else(|e| {
error!(?e, "no pude spawnear thread del service socket");
});
}