diff --git a/Cargo.lock b/Cargo.lock index 2cd2d81..ecd98df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3047,6 +3047,14 @@ dependencies = [ "tokio", ] +[[package]] +name = "cosmobiologia-corpus" +version = "0.1.0" +dependencies = [ + "ron", + "serde", +] + [[package]] name = "cosmobiologia-engine" version = "0.1.0" diff --git a/crates/modules/cosmobiologia/cosmobiologia-corpus/GUIA.md b/crates/modules/cosmobiologia/cosmobiologia-corpus/GUIA.md new file mode 100644 index 0000000..a8c14c7 --- /dev/null +++ b/crates/modules/cosmobiologia/cosmobiologia-corpus/GUIA.md @@ -0,0 +1,158 @@ +# Cómo generar el corpus de interpretación + +Esta guía dice **exactamente** qué hacer, a mano, para construir el +corpus que `cosmobiologia` usará para interpretar cartas sin que ninguna +IA invente una palabra. + +## Qué es (y qué NO es) el corpus + +El corpus **no es un set de reglas matemáticas**. No "calcula" la +interpretación. Las reglas —qué planeta en qué signo, qué aspecto con +qué orbe— las computa el motor astronómico. El corpus es la **biblioteca +de evidencia**: fragmentos de texto —de los libros y de tu propia +escritura— recortados y **etiquetados** con la combinación exacta que +describen. + +En runtime, las combinaciones de una carta hacen un **JOIN** contra el +corpus y traen los textos, citados y con fuente. La síntesis (tejerlos +en un párrafo continuo) es una capa posterior; el corpus solo +**almacena** y **recupera**. + +La contradicción no se promedia. Un Marte hiperdisciplinado en el +trabajo y disperso en la soledad **no** se colapsa a "medio +disciplinado": cada fuerza vive intacta en su **dominio** vivencial. Por +eso el corpus rebana la carta en tajadas (`Vital`, `Social`, `Psiquico`) +— como ver un cuerpo en cortes tomográficos. + +## El formato + +Un archivo `.ron`. Mira `ejemplo.ron` en esta misma carpeta: es una +plantilla cargable y comentada. Tiene dos secciones, `arquetipos` y +`pasajes`. + +### El "código de barras" de una combinación + +Cada pasaje se etiqueta con una clave-cadena: + +| Tipo | Sintaxis | Ejemplo | +|---|---|---| +| Planeta en signo | `planeta·signo` o `planeta/signo` | `mars·virgo`, `mars/virgo` | +| Planeta en casa | `planeta@cN` | `mars@c6` | +| Aspecto entre dos planetas | `a kind b` (tres palabras) | `mars square saturn` | + +Reglas de los identificadores: + +- minúscula, ASCII, **una sola palabra** (usa `_`: `north_node`); +- usa siempre el **mismo** nombre — `mars`, no `Marte` aquí y `mars` + allá, o el JOIN no engancha; +- en un aspecto el orden da igual: `mars square saturn` y + `saturn square mars` quedan como la misma clave. + +## Los pasos + +### Paso 1 — Crea tu archivo + +```sh +cd crates/modules/cosmobiologia/cosmobiologia-corpus +cp ejemplo.ron corpus.ron +``` + +Trabaja sobre `corpus.ron`. Borra los tres pasajes-plantilla cuando +tengas los tuyos. + +### Paso 2 — (Opcional, recomendado) Escribe la ontología + +En la sección `arquetipos`, una entrada por cada planeta, signo, casa y +aspecto que uses. Cada una lleva un `perfil`: un mapa de **dimensiones +psicológicas** —las nombras tú— con un peso en `[-1.0, 1.0]`. + +```ron +( + nombre: "mars", + tipo: planeta, // planeta | signo | casa | aspecto + perfil: { + "accion": 0.9, + "deseo": 0.7, + }, +), +``` + +Esto **no es obligatorio para el JOIN** (el JOIN solo usa `pasajes`), +pero es la base para, más adelante, deducir el perfil de una combinación +que no llegaste a escribir. Si recién empiezas, puedes dejar +`arquetipos: []` y volver luego. + +### Paso 3 — Cosecha los pasajes + +Esta es la carne. Una entrada en `pasajes` por cada fragmento de +interpretación: + +```ron +( + combinacion: "mars·virgo", + texto: "Cita literal, corta, del libro — o tu propia redacción.", + fuente: "Autor, Título de la obra, p. 123", +), +``` + +Dos formas de avanzar; elige una: + +- **Por fuente** — tomas un libro y lo vacías combinación por + combinación. Bueno para cubrir un autor entero de forma pareja. +- **Por carta** — tomas la carta que estás leyendo *ahora*, listas sus + combinaciones y solo escribes esas. Bueno para tener algo útil ya, sin + esperar a "terminar" el corpus (que nunca termina). + +Recomendado: empieza **por carta**. El corpus crece con cada consulta +real. + +### Paso 4 — Cuida la fuente y el derecho de autor + +- Cita **corto** y **textual**, y **atribuye siempre** (autor, obra, + página). Fragmentos breves con cita son uso legítimo. +- No copies capítulos enteros. Si quieres volcar una idea larga, + **reescríbela con tus palabras** y pon `fuente: "propio"`. +- Convención reservada: `fuente: "deducido"` queda para perfiles + compuestos por código a futuro, no para texto de libro. + +### Paso 5 — Acota el dominio cuando el texto lo pida + +Si un pasaje describe la combinación **solo en un plano** de la vida, +márcalo: + +```ron +( + combinacion: "mars square saturn", + texto: "...", + fuente: "...", + dominio: Some(psiquico), // vital | social | psiquico +), +``` + +Sin `dominio`, el pasaje aplica al dominio que le toque por la posición +del planeta en la carta. Con `dominio`, lo fuerzas. Úsalo poco: solo +cuando el autor habla de un plano concreto. + +### Paso 6 — Valida el archivo + +```sh +cargo test -p cosmobiologia-corpus +``` + +Si tu RON tiene un error de sintaxis, el test `ejemplo_ron_carga` +te marca el formato correcto; para validar `corpus.ron` directamente, +cárgalo desde un binario o un test propio con `Corpus::desde_ron`. + +### Paso 7 — Busca los huecos + +Con la carta cargada, `Corpus::huecos(&combinaciones)` devuelve las +combinaciones de esa carta que **no tienen ni un pasaje**. Esa lista es, +literalmente, tu cola de trabajo: lo que falta escribir. + +## Cuánto es "suficiente" + +El universo completo es grande (≈10 planetas × 12 signos = 120, otras +120 planeta-en-casa, y los aspectos). No lo persigas. El 80 % del valor +sale del 20 %: las combinaciones que de verdad aparecen en las cartas +que lees. Empieza con una carta, deja que `huecos` te guíe, y el corpus +se llena solo, consulta a consulta. diff --git a/crates/modules/cosmobiologia/cosmobiologia-corpus/ejemplo.ron b/crates/modules/cosmobiologia/cosmobiologia-corpus/ejemplo.ron new file mode 100644 index 0000000..266ffc5 --- /dev/null +++ b/crates/modules/cosmobiologia/cosmobiologia-corpus/ejemplo.ron @@ -0,0 +1,72 @@ +// ejemplo.ron — plantilla del corpus de interpretación. +// +// Cópialo a un archivo propio (p. ej. `corpus.ron`) y reemplaza el +// contenido por el tuyo. Tiene dos secciones: +// +// arquetipos — la ontología: cada planeta / signo / casa / aspecto +// con su perfil semántico (las dimensiones las nombras +// TÚ; el código no presupone ninguna). +// pasajes — la evidencia: texto real, etiquetado por combinación, +// con su fuente. Es lo que el JOIN recupera. +// +// Sintaxis de la clave `combinacion` (el "código de barras"): +// "mars·virgo" o "mars/virgo" — un planeta en un signo +// "mars@c6" — un planeta en una casa +// "mars square saturn" — un aspecto (TRES palabras) +// +// Identificadores: minúscula, ASCII, una sola palabra (usa "_" para +// nombres compuestos, p. ej. "north_node"). Un aspecto se guarda con +// sus extremos ordenados, así "mars square saturn" y +// "saturn square mars" son la misma clave. +// +// Los campos `perfil` y `dominio` de un pasaje son opcionales: omítelos +// hasta que los necesites. +( + arquetipos: [ + ( + nombre: "mars", + tipo: planeta, + perfil: { + "accion": 0.9, + "deseo": 0.7, + "agresion": 0.5, + }, + ), + ( + nombre: "saturn", + tipo: planeta, + perfil: { + "estructura": 0.9, + "limite": 0.8, + "miedo": 0.5, + }, + ), + ( + nombre: "virgo", + tipo: signo, + perfil: { + "precision": 0.9, + "servicio": 0.6, + "ansiedad": 0.4, + }, + ), + ], + pasajes: [ + ( + combinacion: "mars·virgo", + texto: "La energía marciana se vuelve cirujana: actúa con método, corrige, perfecciona. El impulso ya no arrasa, disecciona.", + fuente: "plantilla — reemplaza por tu cita y su autor", + ), + ( + combinacion: "mars@c6", + texto: "El deseo se descarga en el trabajo cotidiano y en el cuidado del cuerpo. Riesgo de agotamiento por exceso de tarea.", + fuente: "plantilla — reemplaza por tu cita y su autor", + ), + ( + combinacion: "mars square saturn", + texto: "Acción y freno tironean a la vez. La frustración, con los años, forja una voluntad templada.", + fuente: "plantilla — reemplaza por tu cita y su autor", + dominio: Some(psiquico), + ), + ], +) diff --git a/crates/modules/cosmobiologia/cosmobiologia-corpus/src/lib.rs b/crates/modules/cosmobiologia/cosmobiologia-corpus/src/lib.rs index ef6abdb..064c4f5 100644 --- a/crates/modules/cosmobiologia/cosmobiologia-corpus/src/lib.rs +++ b/crates/modules/cosmobiologia/cosmobiologia-corpus/src/lib.rs @@ -28,15 +28,19 @@ //! un bloque tiene en 0 se queda en 0, no «se enciende»). Este crate //! trae las capas 1-2 y deja la 3 sin resolver a propósito. //! -//! La síntesis narrativa y la separación por dominios vivenciales se -//! resuelven en capas superiores; este crate sólo modela el almacén y -//! el JOIN. +//! La **rebanada por dominio** —ver el cuerpo de la carta en tajadas— +//! sí vive aquí ([`rebanar_por_dominio`]): es geometría sobre las +//! claves, no síntesis. La carta es una sola configuración; cortarla +//! por dominio vivencial no la promedia, la MIRA desde un plano. Lo +//! único que queda fuera es la síntesis narrativa —tejer los pasajes +//! recuperados en un texto continuo—, trabajo de una capa superior. #![forbid(unsafe_code)] use std::collections::BTreeMap; +use std::str::FromStr; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// Perfil semántico: dimensiones psicológicas/vivenciales con un peso, /// por convención en `[-1.0, 1.0]`. Los **nombres** de las dimensiones @@ -75,7 +79,7 @@ pub struct Arquetipo { /// contradicción «hiperdisciplinado vs. disperso» no se promedia: cada /// fuerza vive intacta en su dominio (general en la oficina, poeta /// disperso en la soledad). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Dominio { /// Cuerpo, salud, acción directa (casas 1/5/9). @@ -102,7 +106,12 @@ impl Dominio { /// La «etiqueta de código de barras» de una combinación astrológica — /// la clave del JOIN. Respeta la gramática: cada variante es un tipo /// distinto de combinación, no una bolsa plana. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +/// +/// Se (de)serializa como una **cadena** legible (`mars·virgo`, +/// `mars@c6`, `mars square saturn`) para que el corpus se escriba a +/// mano sin pelear con la sintaxis de enums. El punto medio `·` admite +/// el alias ASCII `/` (`mars/virgo`), más fácil de teclear. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CombinacionId { /// Un planeta en un signo — `mars·virgo`. PlanetaSigno { planeta: String, signo: String }, @@ -158,6 +167,138 @@ impl std::fmt::Display for CombinacionId { } } +impl FromStr for CombinacionId { + type Err = String; + + /// Parsea el código de barras: `planeta·signo` (o `planeta/signo`), + /// `planeta@cN`, o `a kind b` (tres tokens separados por espacios). + fn from_str(s: &str) -> Result { + let s = s.trim(); + if let Some((planeta, signo)) = s.split_once('·').or_else(|| s.split_once('/')) { + return Ok(CombinacionId::planeta_signo(planeta.trim(), signo.trim())); + } + if let Some((planeta, casa)) = s.split_once("@c") { + let casa: u8 = casa + .trim() + .parse() + .map_err(|_| format!("casa inválida en '{s}'"))?; + return Ok(CombinacionId::planeta_casa(planeta.trim(), casa)); + } + let toks: Vec<&str> = s.split_whitespace().collect(); + if toks.len() == 3 { + return Ok(CombinacionId::aspecto(toks[0], toks[1], toks[2])); + } + Err(format!("combinación no reconocida: '{s}'")) + } +} + +impl Serialize for CombinacionId { + fn serialize(&self, s: S) -> Result { + s.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for CombinacionId { + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + s.parse().map_err(serde::de::Error::custom) + } +} + +/// La posición de un planeta en una carta concreta: en qué signo y en +/// qué casa cae. Es la materia prima desde la que se derivan las +/// [`CombinacionId`] de la carta — el puente entre lo que el motor +/// astronómico calcula y las claves del JOIN del corpus. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Colocacion { + pub planeta: String, + pub signo: String, + pub casa: u8, +} + +/// Un aspecto medido en una carta: dos planetas y el ángulo que los une. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AspectoEnCarta { + pub a: String, + pub kind: String, + pub b: String, +} + +/// Deriva TODAS las combinaciones de una carta: por cada planeta, su +/// `planeta·signo` y su `planeta@cN`; por cada aspecto medido, su +/// `a kind b`. El resultado es la lista que se le pasa a +/// [`Corpus::interpretar`] para hacer el JOIN. +pub fn combinaciones_de_carta( + colocaciones: &[Colocacion], + aspectos: &[AspectoEnCarta], +) -> Vec { + let mut out = Vec::with_capacity(colocaciones.len() * 2 + aspectos.len()); + for c in colocaciones { + out.push(CombinacionId::planeta_signo(&c.planeta, &c.signo)); + out.push(CombinacionId::planeta_casa(&c.planeta, c.casa)); + } + for a in aspectos { + out.push(CombinacionId::aspecto(&a.a, &a.kind, &a.b)); + } + out +} + +/// La **tomografía** de la carta: reparte cada combinación en el dominio +/// —o dominios— vivencial donde descarga su energía. +/// +/// La carta es UNA sola configuración; rebanarla por dominio no la +/// promedia ni la mutila, la MIRA desde un plano —como ver un cuerpo en +/// tajadas—. Las reglas del corte: +/// +/// - un `planeta@cN` cae en el dominio de su casa; +/// - un `planeta·signo` hereda el dominio de la casa donde ESE planeta +/// está colocado en la carta; +/// - un aspecto **puentea**: aparece en el dominio de cada uno de sus +/// dos extremos. Que una misma combinación salga en dos rebanadas no +/// es un error — es la conexión real entre dos planos. +/// +/// Una combinación cuyo planeta no figura en `colocaciones` se omite (no +/// hay forma de saber en qué dominio ubicarla). +pub fn rebanar_por_dominio( + colocaciones: &[Colocacion], + combinaciones: &[CombinacionId], +) -> BTreeMap> { + let casa_de: BTreeMap<&str, u8> = colocaciones + .iter() + .map(|c| (c.planeta.as_str(), c.casa)) + .collect(); + let dominio_de = |planeta: &str| -> Option { + casa_de.get(planeta).copied().and_then(Dominio::de_casa) + }; + + let mut tajadas: BTreeMap> = BTreeMap::new(); + for id in combinaciones { + let dominios: Vec = match id { + CombinacionId::PlanetaCasa { casa, .. } => { + Dominio::de_casa(*casa).into_iter().collect() + } + CombinacionId::PlanetaSigno { planeta, .. } => { + dominio_de(planeta).into_iter().collect() + } + CombinacionId::Aspecto { a, b, .. } => { + let mut ds = Vec::new(); + for p in [a.as_str(), b.as_str()] { + if let Some(d) = dominio_de(p) { + if !ds.contains(&d) { + ds.push(d); + } + } + } + ds + } + }; + for d in dominios { + tajadas.entry(d).or_default().push(id.clone()); + } + } + tajadas +} + /// Un fragmento de interpretación: el texto de un autor (o del propio /// astrólogo) recortado y etiquetado con la combinación que describe. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -222,6 +363,29 @@ impl Corpus { out } + /// El JOIN **rebanado por dominio**: para cada plano vivencial, los + /// pasajes que lo interpretan. Es la entrada directa de un gráfico + /// «por tajadas» — una rebanada, una vista del cuerpo de la carta. + /// Un aspecto que puentea dos dominios trae sus pasajes a las dos + /// rebanadas. + pub fn interpretar_por_dominio( + &self, + colocaciones: &[Colocacion], + aspectos: &[AspectoEnCarta], + ) -> BTreeMap> { + let combinaciones = combinaciones_de_carta(colocaciones, aspectos); + rebanar_por_dominio(colocaciones, &combinaciones) + .into_iter() + .map(|(dominio, ids)| { + let mut pasajes = Vec::new(); + for id in &ids { + pasajes.extend(self.pasajes_de(id)); + } + (dominio, pasajes) + }) + .collect() + } + /// Combinaciones del corpus que NO tienen ni un solo pasaje — los /// huecos que habría que escribir, o cubrir con composición. pub fn huecos(&self, combinaciones: &[CombinacionId]) -> Vec { @@ -342,4 +506,106 @@ mod tests { assert_eq!(Dominio::de_casa(12), Some(Dominio::Psiquico)); assert_eq!(Dominio::de_casa(13), None); } + + #[test] + fn combinacion_id_roundtrip_string() { + for id in [ + CombinacionId::planeta_signo("venus", "leo"), + CombinacionId::planeta_casa("sun", 10), + CombinacionId::aspecto("moon", "trine", "jupiter"), + ] { + let s = id.to_string(); + let vuelta: CombinacionId = s.parse().expect("parsea su propio Display"); + assert_eq!(vuelta, id); + } + } + + #[test] + fn barra_es_alias_ascii_del_punto_medio() { + assert_eq!( + "mars/virgo".parse::().unwrap(), + CombinacionId::planeta_signo("mars", "virgo"), + ); + } + + /// Una carta mínima: Marte en Virgo en casa 6 (Social), Saturno en + /// Aries en casa 1 (Vital), y una cuadratura que los une. + fn carta_de_prueba() -> (Vec, Vec) { + let colocaciones = vec![ + Colocacion { + planeta: "mars".into(), + signo: "virgo".into(), + casa: 6, + }, + Colocacion { + planeta: "saturn".into(), + signo: "aries".into(), + casa: 1, + }, + ]; + let aspectos = vec![AspectoEnCarta { + a: "mars".into(), + kind: "square".into(), + b: "saturn".into(), + }]; + (colocaciones, aspectos) + } + + #[test] + fn combinaciones_de_carta_deriva_signo_casa_y_aspectos() { + let (colocaciones, aspectos) = carta_de_prueba(); + let combos = combinaciones_de_carta(&colocaciones, &aspectos); + // 2 planetas × (signo + casa) + 1 aspecto. + assert_eq!(combos.len(), 5); + assert!(combos.contains(&CombinacionId::planeta_signo("mars", "virgo"))); + assert!(combos.contains(&CombinacionId::planeta_casa("saturn", 1))); + assert!(combos.contains(&CombinacionId::aspecto("mars", "square", "saturn"))); + } + + #[test] + fn rebanar_por_dominio_reparte_y_el_aspecto_puentea() { + let (colocaciones, aspectos) = carta_de_prueba(); + let combos = combinaciones_de_carta(&colocaciones, &aspectos); + let tajadas = rebanar_por_dominio(&colocaciones, &combos); + + // Marte en casa 6 → Social ; Saturno en casa 1 → Vital. + let social = tajadas.get(&Dominio::Social).expect("hay tajada social"); + let vital = tajadas.get(&Dominio::Vital).expect("hay tajada vital"); + assert_eq!(social.len(), 3); + assert_eq!(vital.len(), 3); + + // El aspecto cruza los dos planos: sale en las dos tajadas. + let aspecto = CombinacionId::aspecto("mars", "square", "saturn"); + assert!(social.contains(&aspecto)); + assert!(vital.contains(&aspecto)); + } + + #[test] + fn interpretar_por_dominio_agrupa_pasajes() { + let (colocaciones, aspectos) = carta_de_prueba(); + let corpus = Corpus { + arquetipos: Vec::new(), + pasajes: vec![ + pasaje(CombinacionId::planeta_casa("mars", 6), "trabajo intenso"), + pasaje(CombinacionId::planeta_casa("saturn", 1), "cuerpo severo"), + ], + }; + let por_dominio = corpus.interpretar_por_dominio(&colocaciones, &aspectos); + assert_eq!(por_dominio[&Dominio::Social].len(), 1); + assert_eq!(por_dominio[&Dominio::Vital].len(), 1); + assert_eq!(por_dominio[&Dominio::Social][0].texto, "trabajo intenso"); + } + + #[test] + fn ejemplo_ron_carga() { + let corpus = Corpus::desde_ron(include_str!("../ejemplo.ron")) + .expect("ejemplo.ron debe ser RON válido"); + assert!(!corpus.arquetipos.is_empty(), "la plantilla trae arquetipos"); + assert!(!corpus.pasajes.is_empty(), "la plantilla trae pasajes"); + // El pasaje del aspecto fija su dominio explícitamente. + let aspecto = CombinacionId::aspecto("mars", "square", "saturn"); + let p = corpus.pasajes_de(&aspecto); + assert_eq!(p.len(), 1); + assert_eq!(p[0].dominio, Some(Dominio::Psiquico)); + } } diff --git a/renaser/Cargo.toml b/renaser/Cargo.toml index f014b24..50fbca4 100644 --- a/renaser/Cargo.toml +++ b/renaser/Cargo.toml @@ -15,7 +15,10 @@ resolver = "2" members = ["boot"] # El kernel (bare-metal) y las apps WASM (target wasm32) se compilan aparte, # cada cual con su propio target; quedan fuera del espacio de trabajo. -exclude = ["kernel", "apps"] +# `formato` —el formato del grafo en disco— tambien se excluye: es un nucleo +# `no_std` que enlaza el kernel bare-metal, asi que se compila como dependencia +# de cada lado (kernel y boot) y no como miembro del workspace anfitrion. +exclude = ["kernel", "apps", "formato"] # ----------------------------------------------------------------------------- # Metadatos compartidos: cada miembro hereda esta identidad con `*.workspace`. diff --git a/renaser/boot/Cargo.toml b/renaser/boot/Cargo.toml index b8f6485..cafc65e 100644 --- a/renaser/boot/Cargo.toml +++ b/renaser/boot/Cargo.toml @@ -17,6 +17,12 @@ description = "renaser :: constructor de imagen de disco UEFI y lanzador de QEMU # Constructor de la imagen de disco UEFI. Corre en el anfitrion, usa `std`. bootloader.workspace = true +# El formato del grafo de objetos en disco (Fase 7b). Es el MISMO nucleo +# `no_std` que enlaza el kernel: gracias a el, `boot` siembra la imagen de +# disco con el grafo ya poblado —objetos de bytecode y Manifiesto de Genesis— +# hablando byte a byte el idioma que el kernel leera. +formato = { path = "../formato" } + # Dependencia de ARTEFACTO (RFC 3028). Cargo compila el kernel para # `x86_64-unknown-none` —en aislamiento total de arquitectura— y nos inyecta la # ruta de su ELF en la variable de entorno `CARGO_BIN_FILE_KERNEL_kernel`, diff --git a/renaser/formato/Cargo.toml b/renaser/formato/Cargo.toml new file mode 100644 index 0000000..fddc3e8 --- /dev/null +++ b/renaser/formato/Cargo.toml @@ -0,0 +1,38 @@ +# ============================================================================= +# renaser :: formato — el formato del grafo de objetos en disco +# ----------------------------------------------------------------------------- +# Nucleo `#![no_std]` COMPARTIDO: lo enlaza el kernel bare-metal (target +# `x86_64-unknown-none`) y, por ser no_std, tambien lo compila sin friccion el +# anfitrion `boot`. Es la unica verdad del formato del grafo —tipos, +# (de)serializacion postcard, hash BLAKE3, trazado de registros—, de modo que +# kernel y constructor de imagen hablen exactamente el mismo idioma de disco. +# +# Queda EXCLUIDO del espacio de trabajo (ver el Cargo.toml raiz), como el +# kernel: lo consume un paquete bare-metal, asi que fija sus versiones de +# forma explicita, sin herencia del workspace. +# ============================================================================= + +[package] +name = "formato" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" +authors = ["JL Soltech "] +description = "renaser :: formato del grafo de objetos en disco — compartido kernel ↔ boot" + +[lib] +bench = false +doctest = false + +[dependencies] +# `serde` da el rasgo de (de)serializacion; `postcard` lo materializa en un +# formato binario compacto — el que viaja al disco. Ambos `no_std`, sobre `alloc`. +serde = { version = "1", default-features = false, features = ["alloc", "derive"] } +postcard = { version = "1", default-features = false, features = ["alloc"] } +# `blake3`: la funcion hash que da identidad a cada objeto. Se fuerza la +# implementacion ESCALAR pura (`pure` + los cuatro `no_*`): el target del kernel +# corre sin SSE, y un camino SIMD por deteccion en tiempo de ejecucion +# ejecutaria instrucciones que la CPU, sin `CR4.OSFXSR`, rechazaria con un #UD. +blake3 = { version = "1", default-features = false, features = [ + "pure", "no_sse2", "no_sse41", "no_avx2", "no_avx512", +] } diff --git a/renaser/formato/src/lib.rs b/renaser/formato/src/lib.rs new file mode 100644 index 0000000..7449b42 --- /dev/null +++ b/renaser/formato/src/lib.rs @@ -0,0 +1,286 @@ +// ============================================================================= +// renaser :: formato — el formato del grafo de objetos en disco +// ----------------------------------------------------------------------------- +// Hasta la Fase 7a, el formato del grafo de objetos —el superbloque, los +// registros del log, el manifiesto— vivia disperso entre `kernel/almacen.rs` +// y `kernel/manifiesto.rs`. Lo conocia solo el kernel. +// +// La Fase 7b se lo entrega tambien a `boot`: el constructor de imagen de +// ANFITRION debe sembrar el disco con el grafo ya poblado —los objetos de +// bytecode y el Manifiesto de Genesis— para que el kernel jamas vuelva a +// empotrar una sola app. Para ello, kernel y boot han de hablar EXACTAMENTE +// el mismo formato: la misma serializacion, el mismo hash, el mismo trazado +// de registros en el log. +// +// Esta crate es esa unica verdad. Es un nucleo `#![no_std]` —el kernel +// bare-metal la enlaza— y, por ser no_std, el anfitrion `boot` la compila sin +// friccion. Define los tipos del grafo, su (de)serializacion `postcard`, la +// funcion hash BLAKE3 que da identidad a cada objeto y el trazado de un +// registro en el log. Ni kernel ni boot vuelven a definir nada de esto. +// ============================================================================= + +#![no_std] + +extern crate alloc; + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use serde::{Deserialize, Serialize}; + +// ============================================================================= +// Constantes del formato en disco +// ============================================================================= + +/// Firma magica del superbloque — «RENASer GRaFo». Distingue un disco de +/// renaser de uno virgen o ajeno. +pub const MAGIA: [u8; 8] = *b"RENASGRF"; + +/// Version del formato del superbloque en disco. Un disco con otra version se +/// reformatea al arrancar. v2 (Fase 7) — el superbloque porta el ancla +/// `manifiesto`, gemela de `raiz`. +pub const VERSION_SUPERBLOQUE: u32 = 2; + +/// Version del formato del manifiesto serializado. Independiente de la del +/// superbloque: el manifiesto es un objeto del grafo, no una estructura fija +/// del disco. +pub const VERSION_MANIFIESTO: u32 = 1; + +/// Techo del tamaño de un objeto serializado: 1 MiB. Acota los buferes de E/S +/// y permite descartar un registro corrupto sin leer un disparate. +pub const MAX_OBJETO: usize = 1024 * 1024; + +/// Tamaño de un sector del disco, en bytes. El log se traza en multiplos de +/// esta unidad — la misma que expone el transporte virtio-blk. +pub const TAM_SECTOR: usize = 512; + +/// El identificador de un objeto: el hash BLAKE3 de su forma serializada. En +/// un almacen direccionado por contenido, la identidad ES el contenido. +pub type Hash = [u8; 32]; + +// ============================================================================= +// Los tipos del grafo +// ============================================================================= + +/// Un objeto del grafo: una carga util opaca y las aristas que lo enlazan con +/// otros objetos. Los `hijos` hacen del almacen un DAG —no un arbol—: un +/// objeto puede ser hijo de muchos, y el direccionamiento por contenido +/// garantiza que cada contenido distinto se guarda una sola vez. +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct Objeto { + /// La carga util del objeto: bytes crudos, que nadie interpreta aqui. + pub datos: Vec, + /// Los hashes de los objetos hijos: las aristas salientes del DAG. + pub hijos: Vec, +} + +/// El superbloque: el sector 0 del disco. Ancla el grafo entero — dice por +/// donde continua el log, cual es el objeto raiz y cual el manifiesto. +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct SuperBloque { + /// Firma magica: debe ser [`MAGIA`]. + pub magia: [u8; 8], + /// Version del formato: debe ser [`VERSION_SUPERBLOQUE`]. + pub version: u32, + /// Proximo sector libre del log — donde se anexara el siguiente objeto. + pub cursor: u64, + /// El objeto raiz del DAG: el punto de entrada que el userspace fija y lee. + pub raiz: Option, + /// El Manifiesto de Genesis: el objeto que dicta que apps nacen del grafo + /// al arrancar. Ancla del kernel, gemela de `raiz` (del userspace). + pub manifiesto: Option, +} + +/// El Manifiesto de Genesis: la lista de aplicaciones que el kernel instancia +/// al arrancar. Vive como un objeto del grafo; el superbloque guarda su hash. +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct Manifiesto { + /// Version del formato — debe ser [`VERSION_MANIFIESTO`]. + pub version: u32, + /// Las aplicaciones del userspace, en orden de arranque. + pub apps: Vec, +} + +/// Una entrada del manifiesto: una aplicacion del userspace y todo lo que el +/// kernel necesita para darle vida — su bytecode, su ventana, su cuota de +/// memoria y, si lo tuviera, su ultimo estado persistido. +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct EntradaApp { + /// Nombre legible — para los rotulos de la consola y la baliza. + pub nombre: String, + /// Hash del objeto del grafo que contiene el bytecode WASM de la app. + pub bytecode: Hash, + /// Sub-region del framebuffer asignada a la app. Campos de ancho fijo + /// `u32` A PROPOSITO: esto es un formato EN DISCO. La `RegionPantalla` del + /// kernel usa `usize` (ancho dependiente de plataforma) y no serializa. + pub region_x: u32, + pub region_y: u32, + pub region_ancho: u32, + pub region_alto: u32, + /// Techo de memoria lineal de la app, en bytes. Cada app lleva su cuota. + pub techo_memoria: u32, + /// Hash del ultimo estado persistido de la app (Fase 7c). `None` hasta que + /// la app guarde estado por primera vez. + pub estado: Option, +} + +// ============================================================================= +// (De)serializacion — la forma binaria que viaja al disco +// ============================================================================= + +impl Objeto { + /// Serializa el objeto a su forma binaria `postcard`. + pub fn serializar(&self) -> Result, &'static str> { + postcard::to_allocvec(self).map_err(|_| "objeto :: serializacion fallida") + } + + /// Reconstruye un objeto desde su forma binaria. Tolera bytes sobrantes + /// tras el objeto —el relleno del registro—: solo consume su prefijo. + pub fn deserializar(bytes: &[u8]) -> Result { + postcard::take_from_bytes::(bytes) + .map(|(objeto, _)| objeto) + .map_err(|_| "objeto :: deserializacion fallida") + } +} + +impl SuperBloque { + /// Serializa el superbloque a su forma binaria `postcard`. + pub fn serializar(&self) -> Result, &'static str> { + postcard::to_allocvec(self).map_err(|_| "superbloque :: serializacion fallida") + } + + /// Reconstruye el superbloque desde el sector 0. Tolera el relleno a cero + /// que completa el sector: solo consume el prefijo serializado. + pub fn deserializar(bytes: &[u8]) -> Result { + postcard::take_from_bytes::(bytes) + .map(|(sb, _)| sb) + .map_err(|_| "superbloque :: deserializacion fallida") + } +} + +impl Manifiesto { + /// Serializa el manifiesto a su forma binaria `postcard` — la carga util + /// del objeto del grafo que lo aloja. + pub fn serializar(&self) -> Result, &'static str> { + postcard::to_allocvec(self).map_err(|_| "manifiesto :: serializacion fallida") + } + + /// Reconstruye un manifiesto desde la carga util de su objeto. Rechaza un + /// formato de version desconocida en lugar de malinterpretarlo. + pub fn deserializar(bytes: &[u8]) -> Result { + let (manifiesto, _) = postcard::take_from_bytes::(bytes) + .map_err(|_| "manifiesto :: deserializacion fallida")?; + if manifiesto.version != VERSION_MANIFIESTO { + return Err("manifiesto :: version de formato desconocida"); + } + Ok(manifiesto) + } +} + +// ============================================================================= +// El hash y el trazado de un registro en el log +// ============================================================================= + +/// La identidad de un objeto: el hash BLAKE3 de su forma serializada. Kernel y +/// `boot` la calculan por aqui — una sola definicion del hash, jamas dos. +pub fn hash(bytes: &[u8]) -> Hash { + *blake3::hash(bytes).as_bytes() +} + +/// Numero de sectores que ocupa un registro cuyo payload mide `longitud` +/// bytes. Cada registro es `[longitud: u32 LE][payload postcard][relleno 0]`. +pub fn sectores_registro(longitud: usize) -> u64 { + (4 + longitud).div_ceil(TAM_SECTOR) as u64 +} + +/// Compone el registro en disco de un payload: `[longitud u32 LE][payload] +/// [relleno a cero]`, alineado a un numero entero de sectores. Es el trazado +/// exacto que el kernel lee al reconstruir su indice — lo escriben tanto +/// `kernel::almacen` (al anexar un objeto) como `boot` (al sembrar la imagen). +pub fn componer_registro(payload: &[u8]) -> Vec { + let n = sectores_registro(payload.len()) as usize; + let mut registro = vec![0u8; n * TAM_SECTOR]; + registro[0..4].copy_from_slice(&(payload.len() as u32).to_le_bytes()); + registro[4..4 + payload.len()].copy_from_slice(payload); + registro +} + +/// Lee la cabecera de longitud de un registro (sus 4 primeros bytes). Devuelve +/// `None` si la longitud es cero —fin del log— o supera [`MAX_OBJETO`] +/// —corrupcion—. Gemela de [`componer_registro`]. +pub fn longitud_registro(cabecera: &[u8]) -> Option { + if cabecera.len() < 4 { + return None; + } + let longitud = + u32::from_le_bytes([cabecera[0], cabecera[1], cabecera[2], cabecera[3]]) as usize; + if longitud == 0 || longitud > MAX_OBJETO { + None + } else { + Some(longitud) + } +} + +// ============================================================================= +// Pruebas — el formato debe ser un espejo perfecto: lo escrito se relee igual +// ============================================================================= + +#[cfg(test)] +mod pruebas { + use super::*; + + #[test] + fn objeto_ida_y_vuelta() { + let objeto = Objeto { + datos: vec![1, 2, 3, 4, 5], + hijos: vec![[7u8; 32], [9u8; 32]], + }; + let bytes = objeto.serializar().unwrap(); + assert_eq!(Objeto::deserializar(&bytes).unwrap(), objeto); + } + + #[test] + fn registro_alineado_a_sector() { + let payload = vec![0xABu8; 600]; + let registro = componer_registro(&payload); + // 4 + 600 = 604 bytes => dos sectores de 512. + assert_eq!(registro.len(), 2 * TAM_SECTOR); + assert_eq!(registro.len() % TAM_SECTOR, 0); + assert_eq!(longitud_registro(®istro), Some(600)); + assert_eq!(®istro[4..604], &payload[..]); + } + + #[test] + fn cabecera_a_cero_es_fin_del_log() { + assert_eq!(longitud_registro(&[0, 0, 0, 0]), None); + assert_eq!(longitud_registro(&[0xFF, 0xFF, 0xFF, 0xFF]), None); + assert_eq!(longitud_registro(&[3, 0, 0, 0]), Some(3)); + } + + #[test] + fn manifiesto_rechaza_version_ajena() { + let mut manifiesto = Manifiesto { + version: 99, + apps: Vec::new(), + }; + let bytes = postcard::to_allocvec(&manifiesto).unwrap(); + assert!(Manifiesto::deserializar(&bytes).is_err()); + manifiesto.version = VERSION_MANIFIESTO; + assert!(Manifiesto::deserializar(&manifiesto.serializar().unwrap()).is_ok()); + } + + #[test] + fn superbloque_cabe_en_un_sector_y_vuelve_intacto() { + let sb = SuperBloque { + magia: MAGIA, + version: VERSION_SUPERBLOQUE, + cursor: 4096, + raiz: Some([1u8; 32]), + manifiesto: Some([2u8; 32]), + }; + let bytes = sb.serializar().unwrap(); + assert!(bytes.len() <= TAM_SECTOR); + assert_eq!(SuperBloque::deserializar(&bytes).unwrap(), sb); + } +} diff --git a/renaser/kernel/Cargo.toml b/renaser/kernel/Cargo.toml index 3ea7bda..658cf1e 100644 --- a/renaser/kernel/Cargo.toml +++ b/renaser/kernel/Cargo.toml @@ -45,19 +45,14 @@ wasmi = { version = "1.0", default-features = false, features = ["hash-collectio # `virtio-drivers` bare-metal: el kernel implementa su `trait Hal` para el DMA. virtio-drivers = { version = "0.13", default-features = false, features = ["alloc"] } -# --- Fase 6.1c :: el grafo de objetos direccionado por contenido --- -# `serde` da el rasgo de (de)serializacion; `postcard` lo materializa en un -# formato binario compacto, pensado para sistemas empotrados — el que viaja al -# disco. Ambos `no_std`, apoyados en `alloc`. -serde = { version = "1", default-features = false, features = ["alloc", "derive"] } -postcard = { version = "1", default-features = false, features = ["alloc"] } -# `blake3`: la funcion hash que da identidad a cada objeto. Se fuerza la -# implementacion ESCALAR pura (`pure` + los cuatro `no_*`): el target del kernel -# corre sin SSE, y un camino SIMD activado por deteccion en tiempo de ejecucion -# ejecutaria instrucciones que la CPU, sin `CR4.OSFXSR`, rechazaria con un #UD. -blake3 = { version = "1", default-features = false, features = [ - "pure", "no_sse2", "no_sse41", "no_avx2", "no_avx512", -] } +# --- Fase 6.1c / 7b :: el grafo de objetos direccionado por contenido --- +# El formato del grafo —tipos, (de)serializacion postcard, hash BLAKE3, trazado +# de registros del log— vive en la crate `formato`, un nucleo `no_std` +# COMPARTIDO con `boot` (que lo usa para sembrar la imagen de disco). El kernel +# ya no declara `serde`/`postcard`/`blake3` por su cuenta: los hereda —con las +# mismas features, BLAKE3 escalar puro incluido— a traves de `formato`. Una +# sola verdad del formato de disco, imposible de divergir entre los dos lados. +formato = { path = "../formato" } # --- Fase 8 (preparación) :: el compositor --- # `mirada-layout` es el motor de teselado del compositor de brahman —