feat(cosmobiologia): corpus — puente carta → pasajes de interpretación

Primer paso para conectar el cosmobiologia-corpus a la app: el engine
gana `corpus_inputs(&RenderModel)`, que deriva de una carta sus
colocaciones (planeta·signo·casa) y sus aspectos en el shape que el
corpus consume. Cada longitud se traduce a su signo; la casa viene del
glyph. El caller hace luego `Corpus::interpretar_por_dominio`.

El engine reexporta los tipos del corpus (Corpus, Pasaje, Dominio,
Colocacion, AspectoEnCarta, CombinacionId) para que el shell/canvas los
usen sin importar el crate aparte.

2 tests del engine verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 19:44:54 +00:00
parent 2523652e22
commit ac787fb3b3
2 changed files with 120 additions and 0 deletions
@@ -8,6 +8,7 @@ description = "Tahuantinsuyu — bridge entre el modelo agnóstico y eternal-ast
[dependencies]
cosmobiologia-model = { path = "../cosmobiologia-model" }
cosmobiologia-render = { path = "../cosmobiologia-render" }
cosmobiologia-corpus = { path = "../cosmobiologia-corpus" }
serde = { workspace = true }
thiserror = { workspace = true }
@@ -41,6 +41,13 @@ pub use cosmobiologia_render::{
UranianGroup, OUTER_RING_MODULES,
};
// El corpus de interpretación es agnóstico (no conoce eternal ni gpui).
// El engine lo reexporta para que el shell y el canvas trabajen los
// pasajes sin importar el crate aparte.
pub use cosmobiologia_corpus::{
AspectoEnCarta, Colocacion, CombinacionId, Corpus, Dominio, Pasaje,
};
// `Chart` reexportado arriba es lo que `PipelineRequest::Synastry`
// transporta — el caller (shell) lee del Store y pasa el Chart entero
// para que el bridge construya su NatalChart en eternal.
@@ -415,6 +422,59 @@ const ZODIAC_GLYPHS: [&str; 12] = [
"pisces",
];
// =====================================================================
// Corpus de interpretación — puente carta → pasajes
// =====================================================================
/// Deriva las colocaciones y aspectos natales de un [`RenderModel`]
/// para alimentar el corpus de interpretación: cada cuerpo natal se
/// traduce a su `planeta·signo` + `planeta@casa`, y cada aspecto a su
/// terna. El caller hace luego `Corpus::interpretar_por_dominio`.
pub fn corpus_inputs(render: &RenderModel) -> (Vec<Colocacion>, Vec<AspectoEnCarta>) {
let mut colocaciones = Vec::new();
let mut aspectos = Vec::new();
for layer in &render.layers {
if layer.module_id != "natal" {
continue;
}
match layer.kind {
LayerKind::Bodies => {
for g in &layer.glyphs {
colocaciones.push(Colocacion {
planeta: g.symbol.clone(),
signo: signo_de_longitud(g.deg).to_string(),
casa: g.house.unwrap_or(0),
});
}
}
LayerKind::Aspects => {
if let Geometry::Lines(segs) = &layer.geometry {
for s in segs {
if !s.from_body.is_empty() && !s.to_body.is_empty() {
aspectos.push(AspectoEnCarta {
a: s.from_body.clone(),
kind: s.kind.clone(),
b: s.to_body.clone(),
});
}
}
}
}
_ => {}
}
}
(colocaciones, aspectos)
}
/// El signo zodiacal (id agnóstico) de una longitud eclíptica.
fn signo_de_longitud(deg: f32) -> &'static str {
const SIGNOS: [&str; 12] = [
"aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra",
"scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
];
SIGNOS[(deg.rem_euclid(360.0) / 30.0) as usize % 12]
}
// =====================================================================
// Tests
// =====================================================================
@@ -461,6 +521,65 @@ mod tests {
assert_eq!(model.layers[0].glyphs.len(), 12);
}
#[test]
fn corpus_inputs_extrae_colocaciones_y_aspectos() {
let render = RenderModel {
chart_id: ChartId::new(),
chart_kind: ChartKind::Natal,
title: "x".into(),
subtitle: None,
compute_ms: 0,
ascendant_deg: 0.0,
midheaven_deg: 270.0,
descendant_deg: 180.0,
imum_coeli_deg: 90.0,
geo_latitude_deg: 0.0,
geo_longitude_deg: 0.0,
layers: vec![
Layer {
module_id: "natal".into(),
kind: LayerKind::Bodies,
ring: 0.0,
z: 0,
geometry: Geometry::GlyphsOnly,
glyphs: vec![
Glyph { deg: 12.0, symbol: "mars".into(), house: Some(6), ..Default::default() },
Glyph { deg: 200.0, symbol: "venus".into(), house: Some(1), ..Default::default() },
],
},
Layer {
module_id: "natal".into(),
kind: LayerKind::Aspects,
ring: 0.0,
z: 0,
geometry: Geometry::Lines(vec![LineSeg {
from_deg: 12.0,
to_deg: 200.0,
kind: "square".into(),
opacity: 1.0,
from_body: "mars".into(),
to_body: "venus".into(),
orb_deg: 1.0,
}]),
glyphs: vec![],
},
],
overlays: vec![],
aspect_summary: vec![],
uranian_groups: vec![],
gr_triggers: vec![],
harmonic: 1,
harmonic_spectrum: vec![],
};
let (col, asp) = corpus_inputs(&render);
assert_eq!(col.len(), 2);
assert_eq!(col[0].planeta, "mars");
assert_eq!(col[0].signo, "aries", "12° cae en Aries");
assert_eq!(col[0].casa, 6);
assert_eq!(asp.len(), 1);
assert_eq!(asp[0].kind, "square");
}
#[cfg(feature = "eternal-bridge")]
#[test]
fn real_compute_natal_demo() {