feat(cosmobiologia-corpus): tomografía por dominio + plantilla y guía
El corpus ya rebana la carta en tajadas vivenciales: una sola configuración mirada plano a plano, sin promediar la contradicción. - Colocacion / AspectoEnCarta: la posición real de un planeta en una carta — el puente entre el motor astronómico y las claves del JOIN. - combinaciones_de_carta: deriva todas las CombinacionId de una carta. - rebanar_por_dominio: la tomografía — cada planeta@cN cae en el dominio de su casa, cada planeta·signo hereda el de su casa, y un aspecto puentea apareciendo en las dos tajadas que conecta. - Corpus::interpretar_por_dominio: el JOIN agrupado por dominio, entrada directa del gráfico «por tajadas». - CombinacionId acepta el alias ASCII '/' del punto medio '·'. - ejemplo.ron: plantilla cargable y comentada del corpus. - GUIA.md: los pasos exactos para generar el corpus a mano. 12 tests verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+8
@@ -3047,6 +3047,14 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cosmobiologia-corpus"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ron",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmobiologia-engine"
|
name = "cosmobiologia-engine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -28,15 +28,19 @@
|
|||||||
//! un bloque tiene en 0 se queda en 0, no «se enciende»). Este crate
|
//! 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.
|
//! 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
|
//! La **rebanada por dominio** —ver el cuerpo de la carta en tajadas—
|
||||||
//! resuelven en capas superiores; este crate sólo modela el almacén y
|
//! sí vive aquí ([`rebanar_por_dominio`]): es geometría sobre las
|
||||||
//! el JOIN.
|
//! 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)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
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,
|
/// Perfil semántico: dimensiones psicológicas/vivenciales con un peso,
|
||||||
/// por convención en `[-1.0, 1.0]`. Los **nombres** de las dimensiones
|
/// 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
|
/// contradicción «hiperdisciplinado vs. disperso» no se promedia: cada
|
||||||
/// fuerza vive intacta en su dominio (general en la oficina, poeta
|
/// fuerza vive intacta en su dominio (general en la oficina, poeta
|
||||||
/// disperso en la soledad).
|
/// 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")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Dominio {
|
pub enum Dominio {
|
||||||
/// Cuerpo, salud, acción directa (casas 1/5/9).
|
/// 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 «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
|
/// la clave del JOIN. Respeta la gramática: cada variante es un tipo
|
||||||
/// distinto de combinación, no una bolsa plana.
|
/// 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 {
|
pub enum CombinacionId {
|
||||||
/// Un planeta en un signo — `mars·virgo`.
|
/// Un planeta en un signo — `mars·virgo`.
|
||||||
PlanetaSigno { planeta: String, signo: String },
|
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<Self, String> {
|
||||||
|
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<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||||
|
s.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for CombinacionId {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||||
|
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<CombinacionId> {
|
||||||
|
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<Dominio, Vec<CombinacionId>> {
|
||||||
|
let casa_de: BTreeMap<&str, u8> = colocaciones
|
||||||
|
.iter()
|
||||||
|
.map(|c| (c.planeta.as_str(), c.casa))
|
||||||
|
.collect();
|
||||||
|
let dominio_de = |planeta: &str| -> Option<Dominio> {
|
||||||
|
casa_de.get(planeta).copied().and_then(Dominio::de_casa)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tajadas: BTreeMap<Dominio, Vec<CombinacionId>> = BTreeMap::new();
|
||||||
|
for id in combinaciones {
|
||||||
|
let dominios: Vec<Dominio> = 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
|
/// Un fragmento de interpretación: el texto de un autor (o del propio
|
||||||
/// astrólogo) recortado y etiquetado con la combinación que describe.
|
/// astrólogo) recortado y etiquetado con la combinación que describe.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -222,6 +363,29 @@ impl Corpus {
|
|||||||
out
|
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<Dominio, Vec<&Pasaje>> {
|
||||||
|
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
|
/// Combinaciones del corpus que NO tienen ni un solo pasaje — los
|
||||||
/// huecos que habría que escribir, o cubrir con composición.
|
/// huecos que habría que escribir, o cubrir con composición.
|
||||||
pub fn huecos(&self, combinaciones: &[CombinacionId]) -> Vec<CombinacionId> {
|
pub fn huecos(&self, combinaciones: &[CombinacionId]) -> Vec<CombinacionId> {
|
||||||
@@ -342,4 +506,106 @@ mod tests {
|
|||||||
assert_eq!(Dominio::de_casa(12), Some(Dominio::Psiquico));
|
assert_eq!(Dominio::de_casa(12), Some(Dominio::Psiquico));
|
||||||
assert_eq!(Dominio::de_casa(13), None);
|
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::<CombinacionId>().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<Colocacion>, Vec<AspectoEnCarta>) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,10 @@ resolver = "2"
|
|||||||
members = ["boot"]
|
members = ["boot"]
|
||||||
# El kernel (bare-metal) y las apps WASM (target wasm32) se compilan aparte,
|
# 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.
|
# 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`.
|
# Metadatos compartidos: cada miembro hereda esta identidad con `*.workspace`.
|
||||||
|
|||||||
@@ -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`.
|
# Constructor de la imagen de disco UEFI. Corre en el anfitrion, usa `std`.
|
||||||
bootloader.workspace = true
|
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
|
# Dependencia de ARTEFACTO (RFC 3028). Cargo compila el kernel para
|
||||||
# `x86_64-unknown-none` —en aislamiento total de arquitectura— y nos inyecta la
|
# `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`,
|
# ruta de su ELF en la variable de entorno `CARGO_BIN_FILE_KERNEL_kernel`,
|
||||||
|
|||||||
@@ -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 <gerencia@jlsoltech.com>"]
|
||||||
|
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",
|
||||||
|
] }
|
||||||
@@ -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<u8>,
|
||||||
|
/// Los hashes de los objetos hijos: las aristas salientes del DAG.
|
||||||
|
pub hijos: Vec<Hash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Hash>,
|
||||||
|
/// 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<Hash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<EntradaApp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Hash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// (De)serializacion — la forma binaria que viaja al disco
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
impl Objeto {
|
||||||
|
/// Serializa el objeto a su forma binaria `postcard`.
|
||||||
|
pub fn serializar(&self) -> Result<Vec<u8>, &'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<Objeto, &'static str> {
|
||||||
|
postcard::take_from_bytes::<Objeto>(bytes)
|
||||||
|
.map(|(objeto, _)| objeto)
|
||||||
|
.map_err(|_| "objeto :: deserializacion fallida")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuperBloque {
|
||||||
|
/// Serializa el superbloque a su forma binaria `postcard`.
|
||||||
|
pub fn serializar(&self) -> Result<Vec<u8>, &'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<SuperBloque, &'static str> {
|
||||||
|
postcard::take_from_bytes::<SuperBloque>(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<Vec<u8>, &'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<Manifiesto, &'static str> {
|
||||||
|
let (manifiesto, _) = postcard::take_from_bytes::<Manifiesto>(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<u8> {
|
||||||
|
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<usize> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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` bare-metal: el kernel implementa su `trait Hal` para el DMA.
|
||||||
virtio-drivers = { version = "0.13", default-features = false, features = ["alloc"] }
|
virtio-drivers = { version = "0.13", default-features = false, features = ["alloc"] }
|
||||||
|
|
||||||
# --- Fase 6.1c :: el grafo de objetos direccionado por contenido ---
|
# --- Fase 6.1c / 7b :: el grafo de objetos direccionado por contenido ---
|
||||||
# `serde` da el rasgo de (de)serializacion; `postcard` lo materializa en un
|
# El formato del grafo —tipos, (de)serializacion postcard, hash BLAKE3, trazado
|
||||||
# formato binario compacto, pensado para sistemas empotrados — el que viaja al
|
# de registros del log— vive en la crate `formato`, un nucleo `no_std`
|
||||||
# disco. Ambos `no_std`, apoyados en `alloc`.
|
# COMPARTIDO con `boot` (que lo usa para sembrar la imagen de disco). El kernel
|
||||||
serde = { version = "1", default-features = false, features = ["alloc", "derive"] }
|
# ya no declara `serde`/`postcard`/`blake3` por su cuenta: los hereda —con las
|
||||||
postcard = { version = "1", default-features = false, features = ["alloc"] }
|
# mismas features, BLAKE3 escalar puro incluido— a traves de `formato`. Una
|
||||||
# `blake3`: la funcion hash que da identidad a cada objeto. Se fuerza la
|
# sola verdad del formato de disco, imposible de divergir entre los dos lados.
|
||||||
# implementacion ESCALAR pura (`pure` + los cuatro `no_*`): el target del kernel
|
formato = { path = "../formato" }
|
||||||
# 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 8 (preparación) :: el compositor ---
|
# --- Fase 8 (preparación) :: el compositor ---
|
||||||
# `mirada-layout` es el motor de teselado del compositor de brahman —
|
# `mirada-layout` es el motor de teselado del compositor de brahman —
|
||||||
|
|||||||
Reference in New Issue
Block a user