feat(cosmobiologia): rectificador per-segundo + direcciones primarias reales

El rectificador deja la aproximación y pasa a la trigonometría exacta,
con precisión de segundo — el "microajuste argentino".

LA MATEMÁTICA. El rectificador ya NO usa el modelo simplificado
(directed_longitude, rotación uniforme de RA + convergencia GR). Ahora
usa `eternal_astrology::primary_direction::all_directions` — el método
Placidus-mundano: semi-arcos diurnos/nocturnos bajo el polo de cada
cuerpo, la trigonometría esférica de la escuela ascensional. No se
reimplementó nada: la matemática, ya probada, vive en eternal; el
engine sólo aporta la capa de optimización.

- error_de_carta: por cada evento, la distancia en años a la dirección
  primaria que perfecciona más cerca; el error total es la suma. Es la
  función de coste del microajuste — el valle es la hora real.

PRECISIÓN DE SEGUNDO. compute_natal_chart / build_eternal_inputs /
natal_cache pasan a trabajar en SEGUNDOS (compose convierte ×60). El
rectificador barre en dos pasadas: gruesa minuto a minuto sobre la
ventana (el perfil que dibuja la curva), fina segundo a segundo en
±60 s alrededor del mejor minuto.

- Rectificacion: mejor_offset_segundos; el perfil va en segundos.
- UI: panel y curva muestran «±Xm Ys · error N.NNa». Las barras siguen
  siendo clicables (scrub a esa hora candidata).

Tests verdes (engine 12, render 28). Limitación conocida: all_directions
es sólo directo — converso necesita crecer en eternal (upstream).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 16:56:06 +00:00
parent 5fdae159f0
commit 36d6645e7f
6 changed files with 165 additions and 167 deletions
@@ -186,7 +186,7 @@ fn aspect_kind_id(k: EAspectKind) -> &'static str {
/// (transits, sinastría) sin re-traducir.
fn build_eternal_inputs(
chart: &Chart,
offset_minutes: i64,
offset_seconds: i64,
) -> Result<(BirthData, ChartConfig, Observer), EngineError> {
chart.validate()?;
let bd = &chart.birth_data;
@@ -209,10 +209,12 @@ fn build_eternal_inputs(
)
.map_err(|e| EngineError::Eternal(format!("Instant::from_civil_local: {:?}", e)))?;
let instant = if offset_minutes == 0 {
// Microajuste temporal en SEGUNDOS — el rectificador automático
// barre la hora candidata con resolución de segundo.
let instant = if offset_seconds == 0 {
base_instant
} else {
let shifted_utc = base_instant.utc().add_seconds((offset_minutes as f64) * 60.0);
let shifted_utc = base_instant.utc().add_seconds(offset_seconds as f64);
ESInstant::from_utc(shifted_utc)
};
@@ -241,10 +243,10 @@ fn build_eternal_inputs(
/// automáticamente la entrada.
pub(crate) fn compute_natal_chart(
chart: &Chart,
offset_minutes: i64,
offset_seconds: i64,
) -> Result<(Arc<NatalChart>, ChartConfig, Observer), EngineError> {
let (birth_e, config_e, observer) = build_eternal_inputs(chart, offset_minutes)?;
let key = crate::natal_cache::key_for(&chart.birth_data, &chart.config, offset_minutes);
let (birth_e, config_e, observer) = build_eternal_inputs(chart, offset_seconds)?;
let key = crate::natal_cache::key_for(&chart.birth_data, &chart.config, offset_seconds);
if let Some(cached) = crate::natal_cache::get(key) {
return Ok((cached, config_e, observer));
}
@@ -265,7 +267,9 @@ pub fn compose(
natal_options: &crate::NatalOptions,
) -> Result<RenderModel, EngineError> {
let t0 = Instant::now();
let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes)?;
// `compute_natal_chart` trabaja en segundos; `compose` recibe el
// offset en minutos (el scrub del jog-dial, la API pública).
let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes * 60)?;
let orb_table = build_orb_table(natal_options.orb_multiplier);
let all_aspects = find_aspects(&natal, &orb_table);
let aspects: Vec<Aspect> = all_aspects