tawasuyu

tawasuyu/brahman

Rust 332TOML 127JSON 17Markdown 154710 Sutichaykuna

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 · ruwaq sergio · (5 killa ñawpaq) · tayta 2523652e2 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 4

crates/modules/cosmobiologia/cosmobiologia-engine/src/lib.rs

2 qillqakuna · +120 −0

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() {