tawasuyu

tawasuyu/brahman

Rust 492TOML 194Markdown 53JSON 176939 Definiciones

refactor(tahuantinsuyu): fase 6 — Modules pluggables vía compose + PipelineRequest

El shell ya no carga el flag `show_transits: bool` ni hardcodea qué
pipeline corre. La engine expone una sola API `compose(chart, offset,
&[PipelineRequest])` que la shell alimenta a partir de un map
`module_configs: HashMap<String, serde_json::Value>`. Los toggles de
overlay (transit hoy, progression/synastry/solar_arc en fase 7) viven
como módulos propios en el panel.

- engine: PipelineRequest enum (variante Transit por ahora; comentarios
  con el roadmap de SecondaryProgression/SolarArc/Synastry). compose()
  es la nueva entrada canónica; compute / compute_at_offset /
  compute_with_transits_at_now quedan como atajos retrocompatibles que
  delegan en compose. bridge.rs refactor: extraído build_transit_overlay
  como helper que muta &mut RenderModel, listo para que más pipelines
  apilen capas encima.
- modules: nuevo módulo `transit::TransitModule` (id "transit", toggle
  "enabled" con hotkey [T], applies_to Natal). Sacado el toggle
  show_transits de NatalModule — ahora cada módulo declara lo suyo.
  Registry::with_builtins() registra ambos. Test asegura los dos
  aplican a Natal.
- panel: sin cambios — ya itera Registry::for_kind(kind) y renderea
  cada módulo aplicable con sus controls. La adición del TransitModule
  aparece automática como segunda card en el panel.
- shell: replace show_transits por module_configs map. build_requests()
  deriva PipelineRequest::Transit cuando module_configs["transit"]
  ["enabled"] == true. on_panel_event: toggles del NatalModule afectan
  solo visibility del canvas; toggles de otros módulos van al
  module_configs y disparan render_current. on_canvas_event: [T]
  hotkey → flip transit.enabled + sync panel + recompose. apps Cargo
  agrega serde_json como dep directa.

Todos los tests verdes. Fase 7 puede sumar overlays adicionales
(progression, solar_arc) solo agregando variantes a PipelineRequest +
helpers en bridge + módulos declarativos — sin tocar el shell.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

d4761bf2380034298bbcf4dcc48cc6e8c5beacb3 · por sergio · (hace 5 meses) · padre 4d14a4495 · ver el árbol en este commit

Delta semántico · 31

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

6 archivos · +215 −117

diff --git a/Cargo.lock b/Cargo.lock
index b04183e..010c816 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10906,6 +10906,7 @@ version = "0.1.0"
 dependencies = [
  "directories",
  "gpui",
+ "serde_json",
  "tahuantinsuyu-canvas",
  "tahuantinsuyu-card",
  "tahuantinsuyu-engine",
diff --git a/crates/apps/tahuantinsuyu/Cargo.toml b/crates/apps/tahuantinsuyu/Cargo.toml
index a8c02d7..82a8ad5 100644
--- a/crates/apps/tahuantinsuyu/Cargo.toml
+++ b/crates/apps/tahuantinsuyu/Cargo.toml
@@ -20,6 +20,7 @@ yahweh-bus = { workspace = true }
 yahweh-theme = { workspace = true }
 gpui = { workspace = true }
 directories = { workspace = true }
+serde_json = { workspace = true }
 
 [[bin]]
 name = "tahuantinsuyu"
diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs
index c050a89..0f5de20 100644
--- a/crates/apps/tahuantinsuyu/src/shell.rs
+++ b/crates/apps/tahuantinsuyu/src/shell.rs
@@ -6,13 +6,22 @@
 //! Flujo:
 //!
 //! ```text
-//!   Tree.Selected(Chart)        →  Shell  →  load chart + compute + set_mode(Wheel)
+//!   Tree.Selected(Chart)        →  Shell  →  load chart + compose + set_mode(Wheel)
 //!   Tree.Selected(Group/Contact)→  Shell  →  charts_under_*  + set_mode(Thumbnails)
-//!   Canvas.TimeOffsetChanged    →  Shell  →  compute_at_offset(current_chart, off)
-//!                                          →  set_mode(Wheel) con la rueda re-pintada
-//!   Canvas.LayerVisibility...   →  Shell  →  Panel.set_toggle (mantener sync visual)
-//!   Panel.ControlChanged        →  Shell  →  Canvas.set_layer_visible (show_*)
+//!   Canvas.TimeOffsetChanged    →  Shell  →  compose(current_chart, off, requests)
+//!   Canvas.LayerVisibility[T]   →  Shell  →  flip module_configs[transit][enabled]
+//!   Panel.ControlChanged        →  Shell  →  update module_configs OR canvas visibility
 //! ```
+//!
+//! ## module_configs
+//!
+//! Mapa `module_id → JSON` con la configuración persistente de cada
+//! módulo (transit, progression, …). De ahí derivamos los
+//! `PipelineRequest` que la engine consume. Los toggles "visuales"
+//! del NatalModule (`show_sign_dial`, `show_houses`, …) NO viven acá
+//! — afectan solo el render del canvas, no la composición.
+
+use std::collections::HashMap;
 
 use gpui::{
     Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div,
@@ -22,7 +31,7 @@ use gpui::{
 use tahuantinsuyu_canvas::{
     AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
 };
-use tahuantinsuyu_engine::{LayerKind, compute_at_offset, compute_with_transits_at_now};
+use tahuantinsuyu_engine::{LayerKind, PipelineRequest, compose};
 use tahuantinsuyu_model::{Chart, TreeSelection};
 use tahuantinsuyu_panel::{ControlPanel, PanelEvent};
 use tahuantinsuyu_store::Store;
@@ -40,13 +49,12 @@ pub struct Shell {
     tree: Entity<TahuantinsuyuTree>,
     canvas: Entity<AstrologyCanvas>,
     panel: Entity<ControlPanel>,
-    /// Carta abierta actualmente en el canvas. La cacheamos para poder
-    /// recomputarla con time-offsets sin re-leer la DB cada vez.
     current_chart: Option<Chart>,
     current_offset_minutes: i64,
-    /// Overlay de tránsitos al instante actual sobre la natal. Disparado
-    /// por el toggle `show_transits` del panel o la hotkey `[T]`.
-    show_transits: bool,
+    /// Estado de los módulos overlay (transit, progression, …) por
+    /// `module_id`. Las claves dentro del JSON dependen del módulo (la
+    /// convención es `"enabled": bool` para el toggle principal).
+    module_configs: HashMap<String, serde_json::Value>,
 }
 
 impl Shell {
@@ -81,7 +89,7 @@ impl Shell {
             panel,
             current_chart: None,
             current_offset_minutes: 0,
-            show_transits: false,
+            module_configs: HashMap::new(),
         }
     }
 
@@ -164,23 +172,28 @@ impl Shell {
         }
     }
 
+    /// Deriva los `PipelineRequest` activos a partir del `module_configs`.
+    fn build_requests(&self) -> Vec<PipelineRequest> {
+        let mut requests = Vec::new();
+        if module_enabled(&self.module_configs, "transit") {
+            requests.push(PipelineRequest::Transit);
+        }
+        requests
+    }
+
     fn render_current(&mut self, cx: &mut Context<Self>) {
         let Some(chart) = self.current_chart.as_ref() else {
             return;
         };
-        let result = if self.show_transits {
-            compute_with_transits_at_now(chart, self.current_offset_minutes)
-        } else {
-            compute_at_offset(chart, self.current_offset_minutes)
-        };
-        let render = match result {
+        let requests = self.build_requests();
+        let render = match compose(chart, self.current_offset_minutes, &requests) {
             Ok(r) => r,
             Err(e) => {
                 eprintln!(
-                    "[shell] compute {}{} (+{}min): {}",
+                    "[shell] compose {} (+{}min, {} reqs): {}",
                     chart.id,
-                    if self.show_transits { " +transits" } else { "" },
                     self.current_offset_minutes,
+                    requests.len(),
                     e
                 );
                 return;
@@ -205,17 +218,19 @@ impl Shell {
                 }
             }
             CanvasEvent::LayerVisibilityChanged { kind, visible } => {
-                // El toggle de Outer (hotkey [T]) significa "transit
-                // overlay" — no es solo un layer hide, dispara un
-                // recompute distinto. El resto son visibility puros.
+                // El toggle de Outer ([T]) no es visibility puro: dispara
+                // un pipeline distinto. Lo traducimos a un cambio en
+                // module_configs["transit"]["enabled"] + re-render.
                 if matches!(kind, LayerKind::Outer) {
-                    self.show_transits = *visible;
+                    set_module_enabled(&mut self.module_configs, "transit", *visible);
                     self.panel.update(cx, |p, cx| {
-                        p.set_toggle("natal", "show_transits", *visible, cx)
+                        p.set_toggle("transit", "enabled", *visible, cx)
                     });
                     self.render_current(cx);
                     return;
                 }
+                // El resto son visibility puros sobre el canvas. Sync el
+                // panel para que el toggle visual coincida con la hotkey.
                 let key = match kind {
                     LayerKind::SignDial => "show_sign_dial",
                     LayerKind::Houses => "show_houses",
@@ -227,7 +242,7 @@ impl Shell {
                     .update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx));
             }
             CanvasEvent::ChartRequested(_) => {
-                // Fase 5: doble click sobre un thumbnail abre la carta.
+                // Fase 7: doble click sobre un thumbnail abre la carta.
             }
         }
     }
@@ -237,16 +252,10 @@ impl Shell {
             PanelEvent::ControlChanged {
                 module_id, key, value,
             } => {
-                let visible = value.as_bool().unwrap_or(true);
+                let bool_val = value.as_bool().unwrap_or(true);
                 if module_id == "natal" {
-                    if key == "show_transits" {
-                        self.show_transits = visible;
-                        self.canvas.update(cx, |c, cx| {
-                            c.set_layer_visible(LayerKind::Outer, visible, cx)
-                        });
-                        self.render_current(cx);
-                        return;
-                    }
+                    // Toggles puramente visuales — solo afectan visibility
+                    // del render actual, sin recomponer.
                     let kind = match key.as_str() {
                         "show_sign_dial" => Some(LayerKind::SignDial),
                         "show_houses" => Some(LayerKind::Houses),
@@ -256,19 +265,62 @@ impl Shell {
                     };
                     if let Some(k) = kind {
                         self.canvas
-                            .update(cx, |c, cx| c.set_layer_visible(k, visible, cx));
+                            .update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx));
                     }
+                } else {
+                    // Cualquier otro módulo: actualizamos su config y
+                    // recompomemos. La engine vuelve a llamarse con el
+                    // PipelineRequest derivado del nuevo estado.
+                    let entry = self
+                        .module_configs
+                        .entry(module_id.clone())
+                        .or_insert_with(|| serde_json::json!({}));
+                    if let serde_json::Value::Object(map) = entry {
+                        map.insert(key.clone(), value.clone());
+                    }
+                    // Sincronizar visualmente el toggle [T] del canvas
+                    // cuando el cambio fue al "enabled" del transit.
+                    if module_id == "transit" && key == "enabled" {
+                        self.canvas.update(cx, |c, cx| {
+                            c.set_layer_visible(LayerKind::Outer, bool_val, cx)
+                        });
+                    }
+                    self.render_current(cx);
                 }
             }
             PanelEvent::ModuleToggled { .. } => {
-                // Fase 6: encender/apagar módulos enteros (Progression,
-                // Synastry, Uranian).
+                // Fase 7: encender/apagar módulos enteros desde un
+                // header con switch (vs. el toggle por-control de hoy).
             }
         }
         let _ = (&self.store, &self.tree, &self.bus);
     }
 }
 
+// =====================================================================
+// Helpers de module_configs
+// =====================================================================
+
+fn module_enabled(cfgs: &HashMap<String, serde_json::Value>, id: &str) -> bool {
+    cfgs.get(id)
+        .and_then(|c| c.get("enabled"))
+        .and_then(|v| v.as_bool())
+        .unwrap_or(false)
+}
+
+fn set_module_enabled(
+    cfgs: &mut HashMap<String, serde_json::Value>,
+    id: &str,
+    enabled: bool,
+) {
+    let entry = cfgs
+        .entry(id.to_string())
+        .or_insert_with(|| serde_json::json!({}));
+    if let serde_json::Value::Object(map) = entry {
+        map.insert("enabled".into(), serde_json::Value::Bool(enabled));
+    }
+}
+
 impl Render for Shell {
     fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let theme = Theme::global(cx).clone();
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
index 5511832..d4638d3 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
@@ -237,42 +237,45 @@ fn compute_natal_chart(
     Ok((natal, config_e, observer))
 }
 
-pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result<RenderModel, EngineError> {
-    let t0 = Instant::now();
-    let (natal, _, _) = compute_natal_chart(chart, offset_minutes)?;
-    let aspects = find_aspects(&natal, &OrbTable::modern_western());
-    Ok(build_render_model(chart, &natal, &aspects, t0))
-}
-
-/// Pipeline natal + overlay de tránsitos. Computa la carta natal
-/// (eventualmente con un `offset_minutes` aplicado) **y además** una
-/// segunda `NatalChart` con el mismo observer pero al instante
-/// `transit_at` (usualmente `Instant::now()`). Devuelve un `RenderModel`
-/// con dos capas extra:
-///
-/// - `LayerKind::Outer` con `module_id = "transit"` — glifos
-///   planetarios del cielo actual, pintados en un anillo externo.
-/// - `LayerKind::Aspects` con `module_id = "transit"` — aspectos cross
-///   natal × transit (sólo mayores). Convención: `LineSeg.from_deg` =
-///   longitud natal, `LineSeg.to_deg` = longitud transit.
-pub fn compute_with_transits(
+/// Composición principal: natal + overlays pedidos. Es la función que
+/// `lib::compose` delega cuando el feature `eternal-bridge` está activo.
+pub fn compose(
     chart: &Chart,
     offset_minutes: i64,
-    transit_at: ESInstant,
+    requests: &[crate::PipelineRequest],
 ) -> Result<RenderModel, EngineError> {
     let t0 = Instant::now();
     let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes)?;
     let aspects = find_aspects(&natal, &OrbTable::modern_western());
     let mut render = build_render_model(chart, &natal, &aspects, t0);
 
-    // Carta de tránsito: mismo observer, mismo config, instante "ahora".
+    for req in requests {
+        match req {
+            crate::PipelineRequest::Transit => {
+                build_transit_overlay(&natal, &config_e, observer, ESInstant::now(), &mut render)?;
+            }
+        }
+    }
+
+    render.compute_ms = t0.elapsed().as_millis() as u64;
+    Ok(render)
+}
+
+/// Helper: agrega al `RenderModel` las dos capas del overlay de
+/// tránsitos (Outer + cross Aspects).
+fn build_transit_overlay(
+    natal: &NatalChart,
+    config_e: &ChartConfig,
+    observer: Observer,
+    transit_at: ESInstant,
+    render: &mut RenderModel,
+) -> Result<(), EngineError> {
     let transit_birth = BirthData::new(transit_at, observer);
     let session = session()?;
-    let transit = NatalChart::compute(&transit_birth, &config_e, session).map_err(|e| {
+    let transit = NatalChart::compute(&transit_birth, config_e, session).map_err(|e| {
         EngineError::Eternal(format!("NatalChart::compute (transit): {:?}", e))
     })?;
 
-    // Outer ring de glifos: planetas del cielo actual.
     let outer_glyphs: Vec<Glyph> = transit
         .placements
         .iter()
@@ -293,10 +296,8 @@ pub fn compute_with_transits(
         glyphs: outer_glyphs,
     });
 
-    // Cross aspects natal × transit. find_synastry_aspects toma una lista
-    // de `AspectKind`s — usamos solo mayores para no saturar.
     let cross = find_synastry_aspects(
-        &natal,
+        natal,
         &transit,
         &OrbTable::modern_western(),
         EAspectKind::MAJORS,
@@ -311,8 +312,6 @@ pub fn compute_with_transits(
                 from_deg: natal_p.longitude.longitude_deg() as f32,
                 to_deg: transit_p.longitude.longitude_deg() as f32,
                 kind: aspect_kind_id(a.kind).into(),
-                // Apagamos un poco más los cross para distinguirlos del
-                // tejido natal-natal.
                 opacity: opacity * 0.75,
             })
         })
@@ -325,17 +324,7 @@ pub fn compute_with_transits(
         geometry: Geometry::Lines(cross_lines),
         glyphs: Vec::new(),
     });
-
-    render.compute_ms = t0.elapsed().as_millis() as u64;
-    Ok(render)
-}
-
-/// Atajo: tránsitos al instante actual del reloj.
-pub fn compute_with_transits_at_now(
-    chart: &Chart,
-    offset_minutes: i64,
-) -> Result<RenderModel, EngineError> {
-    compute_with_transits(chart, offset_minutes, ESInstant::now())
+    Ok(())
 }
 
 // =====================================================================
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
index f0e0912..97b4e16 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
@@ -151,53 +151,60 @@ pub enum EngineError {
 // API pública
 // =====================================================================
 
-/// Computa el RenderModel real contra eternal-astrology si el feature
-/// está prendido; sino cae al mock.
-pub fn compute(chart: &Chart) -> Result<RenderModel, EngineError> {
-    compute_at_offset(chart, 0)
+/// Pedidos que el host (Shell) eleva a la engine para componer un
+/// `RenderModel`. La capa natal **siempre** se computa; estos requests
+/// son **overlays adicionales**.
+///
+/// Cada variante mapea 1-a-1 con un Module declarado en
+/// `tahuantinsuyu-modules` por id string. Esto deja la engine como
+/// dueña única del cómputo (no depende del trait Module — los módulos
+/// son sólo metadata + UI controls).
+#[derive(Debug, Clone)]
+pub enum PipelineRequest {
+    /// `module_id = "transit"` — anillo externo con planetas al
+    /// instante actual (reloj de pared) + cross aspects natal × transit.
+    Transit,
+    // ── Fase 7 ──────────────────────────────────────────────────────
+    // SecondaryProgression { target_year: i32 },
+    // SolarArc { target_year: i32 },
+    // Synastry { partner: tahuantinsuyu_model::ChartId },
 }
 
-/// Variante con offset temporal en minutos sobre el instante del chart.
-/// Útil para time-scrubbing: el jog-dial del canvas pasa el offset
-/// acumulado y la engine recompone toda la pipeline (Asc, casas,
-/// posiciones planetarias, aspectos) para ese instante desplazado.
-pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result<RenderModel, EngineError> {
+/// Composición canónica: carta natal + todos los overlays pedidos.
+/// Es la única función que el Shell necesita llamar — `compute_at_offset`
+/// y `compute_with_transits_at_now` quedan como atajos retrocompatibles.
+pub fn compose(
+    chart: &Chart,
+    offset_minutes: i64,
+    requests: &[PipelineRequest],
+) -> Result<RenderModel, EngineError> {
     #[cfg(feature = "eternal-bridge")]
     {
-        bridge::compute_at_offset(chart, offset_minutes)
+        bridge::compose(chart, offset_minutes, requests)
     }
     #[cfg(not(feature = "eternal-bridge"))]
     {
-        let _ = offset_minutes;
+        let _ = (offset_minutes, requests);
         Ok(compute_mock(chart))
     }
 }
 
-/// Variante con overlay de tránsitos al **instante actual** (reloj de
-/// pared). Computa la carta natal igual que [`compute_at_offset`] y le
-/// suma dos capas extras:
-///
-/// - `LayerKind::Outer` con `module_id = "transit"` — glifos
-///   planetarios del cielo del momento, sobre un anillo externo.
-/// - `LayerKind::Aspects` con `module_id = "transit"` — líneas natal ↔
-///   transit (sólo aspectos mayores). Por convención, en cada
-///   `LineSeg` el `from_deg` es la longitud natal y el `to_deg` la
-///   longitud del planeta de tránsito.
-///
-/// Sin el feature `eternal-bridge` cae al mock (sin overlay).
+/// Atajo: natal sin overlays. Equivalente a `compose(chart, 0, &[])`.
+pub fn compute(chart: &Chart) -> Result<RenderModel, EngineError> {
+    compose(chart, 0, &[])
+}
+
+/// Atajo: natal con time-scrubbing pero sin overlays.
+pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result<RenderModel, EngineError> {
+    compose(chart, offset_minutes, &[])
+}
+
+/// Atajo: natal + overlay de tránsitos al instante actual.
 pub fn compute_with_transits_at_now(
     chart: &Chart,
     offset_minutes: i64,
 ) -> Result<RenderModel, EngineError> {
-    #[cfg(feature = "eternal-bridge")]
-    {
-        bridge::compute_with_transits_at_now(chart, offset_minutes)
-    }
-    #[cfg(not(feature = "eternal-bridge"))]
-    {
-        let _ = offset_minutes;
-        Ok(compute_mock(chart))
-    }
+    compose(chart, offset_minutes, &[PipelineRequest::Transit])
 }
 
 /// Stub determinista — útil para tests + para la UI sin eternal.
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
index 2c7eaaa..6c2add1 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
@@ -123,6 +123,7 @@ impl Registry {
     pub fn with_builtins() -> Self {
         let mut r = Self { modules: Vec::new() };
         r.register(Box::new(natal::NatalModule));
+        r.register(Box::new(transit::TransitModule));
         r
     }
 
@@ -203,12 +204,6 @@ pub mod natal {
                     default: true,
                     hotkey: Some("P".into()),
                 },
-                Control::Toggle {
-                    key: "show_transits".into(),
-                    label: "Tránsitos (ahora)".into(),
-                    default: false,
-                    hotkey: Some("T".into()),
-                },
                 Control::Slider {
                     key: "harmonic".into(),
                     label: "Armónico".into(),
@@ -229,15 +224,68 @@ pub mod natal {
     }
 }
 
+// =====================================================================
+// TransitModule — overlay del cielo del momento sobre la carta natal
+// =====================================================================
+
+pub mod transit {
+    use super::*;
+
+    /// Anillo externo con las posiciones planetarias del **instante
+    /// actual** (reloj de pared) sobre el sujeto natal, más las
+    /// cross-aspects natal × transit. La engine despacha al pipeline
+    /// `PipelineRequest::Transit` cuando este módulo está activo en el
+    /// `module_configs` del shell.
+    pub struct TransitModule;
+
+    impl Module for TransitModule {
+        fn id(&self) -> &'static str {
+            "transit"
+        }
+        fn label(&self) -> &'static str {
+            "Tránsitos"
+        }
+        fn description(&self) -> &'static str {
+            "Cielo del momento sobre la natal + cross aspects."
+        }
+        fn applies_to(&self, kind: ChartKind) -> bool {
+            // Por ahora solo overlay sobre cartas natales — más adelante
+            // podríamos overlayar tránsitos sobre Progresiones, etc.
+            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: Some("T".into()),
+            }]
+        }
+        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
+            // Las capas de tránsito se construyen en la engine vía
+            // `PipelineRequest::Transit` porque necesitan acceso a la
+            // NatalChart cruda + EphemerisSession. Este método queda
+            // como no-op — el módulo es puramente declarativo.
+            Vec::new()
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
 
     #[test]
-    fn registry_finds_natal() {
+    fn registry_finds_builtins() {
         let r = Registry::with_builtins();
         assert!(r.find("natal").is_some());
-        assert_eq!(r.for_kind(ChartKind::Natal).len(), 1);
+        assert!(r.find("transit").is_some());
+        // Natal kind tiene 2 módulos aplicables: el propio + transit overlay.
+        assert_eq!(r.for_kind(ChartKind::Natal).len(), 2);
+        // Synastry kind no tiene módulos hoy.
         assert!(r.for_kind(ChartKind::Synastry).is_empty());
     }
 }