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>
ac787fb3b37c7565bd74ac66497d21259bca20d9 · por sergio · 2026-05-22T19:44:54Z (hace 5 meses) · padre 2523652e2 · ver el árbol en este commit
Delta semántico · 4
crates/modules/cosmobiologia/cosmobiologia-engine/src/lib.rs
diff --git a/crates/modules/cosmobiologia/cosmobiologia-engine/Cargo.toml b/crates/modules/cosmobiologia/cosmobiologia-engine/Cargo.toml
index 1d277f2..d6fa780 100644
--- a/crates/modules/cosmobiologia/cosmobiologia-engine/Cargo.toml
+++ b/crates/modules/cosmobiologia/cosmobiologia-engine/Cargo.toml
@@ -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 }
diff --git a/crates/modules/cosmobiologia/cosmobiologia-engine/src/lib.rs b/crates/modules/cosmobiologia/cosmobiologia-engine/src/lib.rs
index 8135603..979bdc4 100644
--- a/crates/modules/cosmobiologia/cosmobiologia-engine/src/lib.rs
+++ b/crates/modules/cosmobiologia/cosmobiologia-engine/src/lib.rs
@@ -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() {