feat(tahuantinsuyu): fase 27 — Lots helenísticos + 9 fixed stars
Dos módulos astrológicos pluggables más:
- LotsModule: 7 Arabic Parts vía `all_lots(natal)` (Fortune,
Spirit, Eros, Necessity, Courage, Victory, Nemesis). Glifos
`lot:Fo` en ring 0.54, hover muestra el nombre completo.
- FixedStarsModule: 9 estrellas notables (Aldebaran, Regulus,
Antares, Fomalhaut, Spica, Sirius, Algol, Vega, Pollux) con
longitudes tropicales J2000 + precesión general de 50.29″/año
proyectada al año natal. Marcadores `✦Xxx` en ring 1.04.
Registry pasa de 9 a 11 módulos; test actualizado. Sin cambios
de esquema en RenderModel — los `LayerKind::Lots` y
`LayerKind::FixedStars` ya existían.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
a4d1e0dc17b6fba7522adc49e007557321778fa0 · ruwaq sergio · 2026-05-18T00:21:00Z (5 killa ñawpaq) · tayta e2da24239 · kay commitpi sach'ata qhaway
Semántico sayk'uy · 26
crates/apps/tahuantinsuyu/src/shell.rs
crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs
index 2a680c0..6b31c23 100644
--- a/crates/apps/tahuantinsuyu/src/shell.rs
+++ b/crates/apps/tahuantinsuyu/src/shell.rs
@@ -383,6 +383,12 @@ impl Shell {
if module_enabled(&self.module_configs, "uranian") {
requests.push(PipelineRequest::Uranian);
}
+ if module_enabled(&self.module_configs, "lots") {
+ requests.push(PipelineRequest::Lots);
+ }
+ if module_enabled(&self.module_configs, "fixed_stars") {
+ requests.push(PipelineRequest::FixedStars);
+ }
if module_enabled(&self.module_configs, "composite") {
if let Some(partner) = self.resolve_composite_partner() {
requests.push(PipelineRequest::Composite {
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
index 68ebf8b..7192559 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
@@ -9,7 +9,7 @@ use std::sync::OnceLock;
use std::time::Instant;
use eternal_astrology::{
- composite, find_aspects, find_synastry_aspects, next_return, secondary_progression,
+ all_lots, composite, find_aspects, find_synastry_aspects, next_return, secondary_progression,
solar_arc_true, Aspect, AspectKind as EAspectKind, BirthData, BodySet, ChartConfig,
HouseSystem as EHouseSystem, NatalChart, OrbTable, Zodiac as EZodiac,
};
@@ -356,6 +356,18 @@ pub fn compose(
},
);
}
+ crate::PipelineRequest::Lots => {
+ let count = build_lots_overlay(&natal, &mut render)?;
+ push_overlay_meta(&mut render, "lots", format!("Lots · {}", count));
+ }
+ crate::PipelineRequest::FixedStars => {
+ let count = build_fixed_stars_overlay(chart, &mut render);
+ push_overlay_meta(
+ &mut render,
+ "fixed_stars",
+ format!("Estrellas fijas · {}", count),
+ );
+ }
}
}
@@ -1104,6 +1116,91 @@ fn push_overlay_meta(render: &mut RenderModel, module_id: &str, label: String) {
});
}
+/// Helper: agrega al `RenderModel` los Lots arábigos clásicos
+/// (helenísticos) — Fortune, Spirit, Eros, Necessity, Courage, Victory,
+/// Nemesis. Cada uno se renderea como un glifo `lot:Fo` en el anillo
+/// `0.54` (entre midpoints y cuerpos progresados). Retorna la cantidad
+/// de lots renderizados.
+fn build_lots_overlay(
+ natal: &NatalChart,
+ render: &mut RenderModel,
+) -> Result<usize, EngineError> {
+ let lots = all_lots(natal)
+ .map_err(|e| EngineError::Eternal(format!("all_lots: {:?}", e)))?;
+ let glyphs: Vec<Glyph> = lots
+ .iter()
+ .map(|l| {
+ let name = l.name.map(|n| n.label()).unwrap_or("Lot");
+ // Tres-letras compactas para no recargar la rueda.
+ let abbrev: String = name.chars().take(2).collect();
+ Glyph {
+ deg: l.longitude.longitude_deg() as f32,
+ symbol: format!("lot:{}", abbrev),
+ annotation: Some(name.to_string()),
+ retrograde: false,
+ house: Some(l.house_number),
+ dignity_marker: None,
+ }
+ })
+ .collect();
+ let count = glyphs.len();
+ render.layers.push(Layer {
+ module_id: "lots".into(),
+ kind: LayerKind::Lots,
+ ring: 0.54,
+ z: 13,
+ geometry: Geometry::GlyphsOnly,
+ glyphs,
+ });
+ Ok(count)
+}
+
+/// Helper: agrega al `RenderModel` 9 estrellas fijas notables. Las
+/// longitudes están en J2000 ecliptica tropical; aplicamos precesión
+/// general de 50.29″/año hacia adelante hasta el año natal — basta
+/// para el orbe de conjunción de ±1.5° con que se interpretan.
+fn build_fixed_stars_overlay(chart: &Chart, render: &mut RenderModel) -> usize {
+ // (símbolo, nombre, longitud tropical J2000 en grados)
+ const STARS: &[(&str, &str, f64)] = &[
+ ("✦Ald", "Aldebaran", 69.79), // 09°47′ Gem
+ ("✦Reg", "Regulus", 149.83), // 29°50′ Leo
+ ("✦Ant", "Antares", 249.77), // 09°46′ Sag
+ ("✦Fom", "Fomalhaut", 333.87), // 03°52′ Pis
+ ("✦Spi", "Spica", 203.84), // 23°50′ Lib
+ ("✦Sir", "Sirius", 104.10), // 14°06′ Can
+ ("✦Alg", "Algol", 56.18), // 26°10′ Tau
+ ("✦Veg", "Vega", 285.31), // 15°19′ Cap
+ ("✦Pol", "Pollux", 113.27), // 23°16′ Can
+ ];
+ let years_from_j2000 = (chart.birth_data.year - 2000) as f64;
+ // 50.29″/año ≈ 0.01397°/año de precesión en longitud eclíptica.
+ let precession_deg = years_from_j2000 * (50.29 / 3600.0);
+ let glyphs: Vec<Glyph> = STARS
+ .iter()
+ .map(|(sym, name, j2000_deg)| {
+ let lon = (j2000_deg + precession_deg).rem_euclid(360.0) as f32;
+ Glyph {
+ deg: lon,
+ symbol: (*sym).to_string(),
+ annotation: Some((*name).to_string()),
+ retrograde: false,
+ house: None,
+ dignity_marker: None,
+ }
+ })
+ .collect();
+ let count = glyphs.len();
+ render.layers.push(Layer {
+ module_id: "fixed_stars".into(),
+ kind: LayerKind::FixedStars,
+ ring: 1.04,
+ z: 16,
+ geometry: Geometry::GlyphsOnly,
+ glyphs,
+ });
+ count
+}
+
/// Decora cada Glyph de Bodies (module_id="natal") con su dignity
/// marker en `glyph.dignity_marker`. Usa `essential_dignity(body, sign)`
/// — los cuerpos modernos quedan sin marker.
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
index e4f17ed..a649793 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
@@ -313,6 +313,17 @@ pub enum PipelineRequest {
/// fórmulas analíticas. La visualización geométrica completa del
/// dial de 90° queda pendiente para una fase posterior.
Uranian,
+ /// `module_id = "lots"` — Lots arábigos (helenísticos) calculados
+ /// via `eternal_astrology::compute_lot`: Fortune, Spirit, Eros,
+ /// Necessity, Courage, Victory, Nemesis. Renderea cada lot como
+ /// un texto pequeño en el ring de bodies natales.
+ Lots,
+ /// `module_id = "fixed_stars"` — overlay con ~9 estrellas fijas
+ /// notables (Aldebaran, Regulus, Antares, Fomalhaut, Spica,
+ /// Sirius, Algol, Vega, Pollux). Posiciones tropicales J2000
+ /// aproximadas + precesión simple (~50.29″/año). Renderea como
+ /// marcadores chicos justo afuera del sign dial.
+ FixedStars,
}
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
index fd35da8..a0948f9 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
@@ -140,6 +140,8 @@ impl Registry {
r.register(Box::new(midpoints::MidpointsModule));
r.register(Box::new(composite::CompositeModule));
r.register(Box::new(uranian::UranianModule));
+ r.register(Box::new(lots::LotsModule));
+ r.register(Box::new(fixed_stars::FixedStarsModule));
r
}
@@ -665,12 +667,99 @@ mod tests {
assert!(r.find("midpoints").is_some());
assert!(r.find("composite").is_some());
assert!(r.find("uranian").is_some());
- // Natal kind tiene 9 módulos aplicables.
- assert_eq!(r.for_kind(ChartKind::Natal).len(), 9);
+ assert!(r.find("lots").is_some());
+ assert!(r.find("fixed_stars").is_some());
+ // Natal kind tiene 11 módulos aplicables.
+ assert_eq!(r.for_kind(ChartKind::Natal).len(), 11);
assert!(r.for_kind(ChartKind::Synastry).is_empty());
}
}
+// =====================================================================
+// LotsModule — Lots helenísticos (Fortune, Spirit, Eros, …)
+// =====================================================================
+
+pub mod lots {
+ use super::*;
+
+ /// Calcula los 7 Lots arábigos clásicos via eternal-astrology y
+ /// los renderea como pequeños labels en un ring justo debajo de
+ /// los cuerpos natales. Hover muestra el nombre completo.
+ pub struct LotsModule;
+
+ impl Module for LotsModule {
+ fn id(&self) -> &'static str {
+ "lots"
+ }
+ fn label(&self) -> &'static str {
+ "Lots (helenísticos)"
+ }
+ fn description(&self) -> &'static str {
+ "Fortune, Spirit, Eros, Necessity, Courage, Victory, Nemesis."
+ }
+ fn applies_to(&self, kind: ChartKind) -> bool {
+ matches!(kind, ChartKind::Natal)
+ }
+ fn enabled_by_default(&self) -> bool {
+ false
+ }
+ fn controls(&self) -> Vec<Control> {
+ vec![Control::Toggle {
+ key: "enabled".into(),
+ label: "Activar".into(),
+ default: false,
+ hotkey: None,
+ }]
+ }
+ fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
+ Vec::new()
+ }
+ }
+}
+
+// =====================================================================
+// FixedStarsModule — 9 estrellas astrológicamente notables
+// =====================================================================
+
+pub mod fixed_stars {
+ use super::*;
+
+ /// 9 estrellas fijas (Aldebaran, Regulus, Antares, Fomalhaut,
+ /// Spica, Sirius, Algol, Vega, Pollux) con posición tropical
+ /// aproximada (J2000 + precesión simple). Marcadores chicos en el
+ /// margen exterior del sign dial.
+ pub struct FixedStarsModule;
+
+ impl Module for FixedStarsModule {
+ fn id(&self) -> &'static str {
+ "fixed_stars"
+ }
+ fn label(&self) -> &'static str {
+ "Estrellas fijas"
+ }
+ fn description(&self) -> &'static str {
+ "9 estrellas notables — conjunciones con planetas natales."
+ }
+ fn applies_to(&self, kind: ChartKind) -> bool {
+ matches!(kind, ChartKind::Natal)
+ }
+ fn enabled_by_default(&self) -> bool {
+ false
+ }
+ fn controls(&self) -> Vec<Control> {
+ vec![Control::Toggle {
+ key: "enabled".into(),
+ label: "Activar".into(),
+ default: false,
+ hotkey: None,
+ }]
+ }
+ fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
+ Vec::new()
+ }
+ }
+}
+
// =====================================================================
// UranianModule — ejes del dial uraniano de 90° (versión textual)
// =====================================================================