Commit Graph

232 Commits

Author SHA1 Message Date
sergio e4b1d41b62 feat(arje): incluye CLIs admin en el initrd (brahman-status/busctl/brainctl)
Para "ver" la administración del init desde el shell que arrancó dentro
del initramfs hacían falta clientes que hablen los 4 sockets Unix. Los
exists como examples de cada crate; los empaquetamos ahora.

build-arje-initrd.sh:
- cargo build --example brahman-status -p brahman-admin
- cargo build --example busctl         -p ente-bus
- cargo build --example brainctl       -p ente-brain
- Copia los 3 a /usr/bin/ del initrd.

docs/arje-boot.md §6b reescrita con:
- Tabla de sockets corregida (defaults a /tmp/ cuando no hay
  XDG_RUNTIME_DIR/TMPDIR, que es el caso en initrd).
- Cookbook de los 3 CLIs con ejemplos de sesión típica dentro de la VM.
- Nota para vendorear socat-static via EXTRA_BINS si querés conectar
  crudo a un socket.

§1 layout actualizado con /usr/bin/{brahman-status,busctl,brainctl}.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:42:29 +00:00
sergio ec458b8a6f fix(arje): build-initrd compila musl-static por default
Sin musl, PID 1 panic con "error while loading shared libraries:
libgcc_s.so.1" porque el initramfs no incluye libgcc/glibc/ld-linux.
Solución estándar: target x86_64-unknown-linux-musl produce un ELF
totalmente estático.

Cambios en scripts/build-arje-initrd.sh:
- ARJE_TARGET=x86_64-unknown-linux-musl por default (override con env).
- Chequeo del target instalado antes de buildear; mensaje accionable
  con los comandos exactos (rustup target add..., apt install
  musl-tools, etc.) si falta.
- Sanity check con `file`: aborta si ente-zero quedó dinámico.
- Sanity check para busybox: aborta si el BUSYBOX_BIN apunta a un
  binario dinámico (la otra causa #1 de panic).
- BIN_DIR ahora apunta a target/$TARGET/release/.

Docs (docs/arje-boot.md):
- §2a explica el porqué de musl.
- §2b lista requisitos del host (rustup target, musl-tools, busybox-static).
- §7 sección nueva de troubleshooting con el síntoma exacto del
  libgcc_s panic + 3 escenarios comunes más.
- Checklist pre-deploy actualizado con el chequeo de "statically linked".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:17:52 +00:00
sergio 0e66cda079 fix(tahuantinsuyu): hit-test del hover usa display_deg post-spread
El hover hacía hit-test contra la posición REAL del planeta
(`g.deg`) en vez de la posición de pintura (post-spread). Con
clusters esto generaba que el cursor sobre el disco visible NO
disparara el hover — había que apuntar al grado real (zona
vacía) para activarlo.

Fix: `on_hover_check` ahora corre el mismo `spread_angles` que
`render_wheel` con los inputs equivalentes, y compara la
posición del mouse contra `display_degs[i]` en lugar de
`g.deg`. Nuevo helper `body_disk_base(module_id, kind,
view_scale)` centraliza el cálculo del disco base — render y
hit-test ambos lo usan, así no divergen si más adelante se
ajusta el tamaño por tipo de capa.

11 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:11:54 +00:00
sergio e4882033cf fix(arje): validate.sh robusto frente a timeout 124 + pipefail
timeout 5 retorna 124 al matar ente-zero (dev mode termina solo). Bajo
set -euo pipefail eso abortaba el script antes de chequear el log, así
que build-arje-initrd.sh reportaba "seed inválida" aunque la Card
estuviera bien.

Cambios:
- Capturamos el output a un archivo (no pipeline) con set +e alrededor
  del timeout, así el exit code no importa.
- El check sigue siendo grep -q sobre "Tarjeta Semilla cargada y
  validada"; si falla, ahora imprimimos las primeras 40 líneas del log
  al stderr para debug.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:09:17 +00:00
sergio 074d8bcbc8 feat(tahuantinsuyu): cluster shrink, label compacto, hover destacado con z-order
Cuatro ajustes finos al esquema visual de planetas natales/topo:

1. **Discos achicados en cluster**: glyphs en cluster compartido
   (≥2 miembros) llevan un factor adicional `0.86×` sobre el
   shrink residual. Visualmente quedan apenas más pequeños — al
   estar pegados, achicar un poco evita la sensación de
   "amontonamiento" sin perder el unicode.

2. **Pill compartida más chica + libre de "espacios negros"**:
   - Cálculo del ancho ahora usa `text.chars().count()` (era
     `text.len()` en bytes — los chars unicode astronómicos
     cuentan 3 bytes c/u y inflaban el ancho).
   - Mínimo de ancho bajado de `font*2.0` a `font*1.4` y
     padding lateral reducido. Pills con 1-3 chars ya no llevan
     "espacios en negro" que sobrescriben elementos vecinos.
   - Font del label compartido normal bajado a 9.0×s (era 10);
     el hovereado sube a 10×s. Diferencial claro.
   - Label individual también bajó a 8.5×s.

3. **Hover destacado**: nuevo "hovered_idx" identifica el glyph
   bajo el cursor (de `HoverInfo::Body`). El glyph hovereado se
   pinta al FINAL del árbol DOM — queda con z-order encima del
   resto. Border al color pleno (vs 0.85), disco 1.18× y font
   1.12× para destacarlo.

4. **Label del cluster hovereado destacado**: el cluster que
   contiene al planeta bajo el cursor se renderiza con `fg_text`
   (vs `fg_muted` para los demás) y font un punto más grande.

11 tests verdes (sin cambios — los affectados son del path de
render, no del cómputo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:07:47 +00:00
sergio 121f19b915 fix(tahuantinsuyu): spread acotado + physics damping + label compartido por par
Tres problemas reportados sobre la pasada anterior:

1. **Planetas pisándose**: el "spread por centroides" dejaba a los
   miembros de cada cluster en sus posiciones REALES, así que pares
   en conjunción cerrada (5° real, disk ≈ 10°) seguían con discos
   solapados. Solución: spread directo sobre TODOS los glyphs, no
   solo sobre centroides.

2. **Empuje propagado a planetas lejanos** (era el motivo original
   de tirar el "spread directo"): ahora controlado con un **cap
   por glyph**: `max_shift_deg`. Ningún display puede alejarse más
   de `disk_angular` grados de su raw — un cluster denso no
   "empuja" a planetas que estaban lejos. El residual sube cuando
   el cap impide alcanzar el min_sep, y los discos se encogen.

3. **Algoritmo greedy oscilaba**: el empuje aplicado par-a-par
   reordenaba los displays a mitad de la pasada y nunca convergía
   (`tight_cluster_gets_spread` terminaba con 6.5° de diff cuando
   se pedían 10°). Reemplazado por **physics-step**: se acumulan
   las fuerzas de todos los pares en una pasada, se aplican con
   `damping = 0.6`, se clampea cada display al rango ±max_shift.
   80 iteraciones convergen siempre.

4. **Labels repetidos en pares cercanos**: el threshold del
   cluster compartido era min(4°, disk_angular*0.5). Para
   discos de 10° angular, eso daba 4° — dos planetas a 5°
   formaban clusters separados, cada uno con su pill diciendo
   casi lo mismo. Subido a `disk_angular * 1.2` → pares a <12°
   comparten label.

Nuevos tests:
- `shift_is_bounded`: con max_shift=2°, ningún glyph se aleja más.
- `distant_planet_unaffected_by_dense_cluster`: cluster denso en
  100° + planeta solo en 200° → el de 200° se queda a <5° de raw.

Total 11 tests verdes (6 spread + 5 coord).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:00:08 +00:00
sergio a0f67fd86f fix(tahuantinsuyu): spread no propaga + label lejos del disco + glyph legible
Tres bugs en la pasada anterior de anti-solapamiento:

1. **Empuje propagado en cadena**: el spread greedy aplicado a
   cada glyph movía planetas lejanos cuando un cluster denso los
   empujaba. Ejemplo reportado: planetas a 9° y 10° (conjunción
   real) terminaban moviéndose hacia el 26° por la propagación
   simétrica del empuje.

   Solución: spread en dos pasos.
   * `find_clusters` con threshold `min(4°, disk_angular*0.5)`
     agrupa solo los que realmente están en conjunción cerrada.
     Dentro del cluster los glyphs SE QUEDAN en sus pos reales —
     dos planetas a 1° se ven a 1° (sus discos se rozan, refleja
     la geometría astrológica).
   * `spread_angles` se aplica SOLO a los **centroides** de los
     clusters, con threshold = ancho angular del disco. El
     empuje queda contenido a la vecindad inmediata; planetas
     lejos del cluster no se mueven.
   * Cada glyph hereda el shift de su cluster (centroide
     displayed − centroide real, wrap a ±180°).

2. **Label pisaba al planeta**: `label_r = ring - disk*0.7`
   dejaba solo ~2 px entre el borde del disco y la pill. Movido
   a `ring - disk*1.3` para individuales y `ring - disk*1.5`
   para clusters compartidos. Gap visual ~12 px.

3. **Símbolo se perdía en clusters densos**: shrink agresivo
   (0.45 sobre residual) achicaba el font por debajo del
   umbral legible del unicode astronómico. Bajado a 0.30, piso
   del shrink subido a 0.60×, y piso absoluto del font a 11 px.

4. Threshold de label compartido bajado a ≥2 miembros (era ≥3).
   En astrología, dos planetas en conjunción ya cuentan como un
   stellium funcional y se beneficiarían del label combinado.

Tests: 10 verdes (5 spread + 5 coord).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 18:51:00 +00:00
sergio 8592bab19e docs(arje): organiza core/ + seeds canónicas + boot reproducible
- crates/core/README.md: agrupamiento lógico de los 31 crates absorbidos
  de arje (ente-*) y del protocolo brahman (brahman-*) en 6 grupos —
  Init/PID 1, contratos, discovery, IPC+CAS, cerebro, 14 shims compat
  systemd. No se movieron crates físicamente (rompería paths
  cross-workspace).

- seeds/arje-minimal.card.json: PID1 + /bin/sh, smoke test QEMU.
- seeds/arje-prod.card.json: PID1 + 14 shims compat + tmpfiles/binfmt
  one-shots + echo + getty (16 children). Validados con
  brahman_card::Card::validate.
- seeds/validate.sh: carga la seed vía ente-zero en dev mode.

- scripts/build-arje-initrd.sh: empaqueta CPIO+gzip newc layout
  /init→/sbin/ente-zero, /usr/sbin/ente-*-compat, /ente/seed.card.json,
  /bin/{sh,...} (busybox o glibc+ldd). Tested: produce 20 MB initrd OK.
- scripts/run-arje-qemu.sh: qemu-system-x86_64 con KVM auto-detect,
  -kernel/-initrd/-append "rdinit=/init console=ttyS0,115200 panic=10".

- docs/arje-boot.md: doc end-to-end — layout initramfs, QEMU (con kernel
  del host o externo), GRUB bare metal, Proxmox/libvirt args:, schema
  de Card con todas las validaciones, debugging (sockets de
  introspección, snapshot/restore, metrics), checklist pre-deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 18:40:05 +00:00
sergio a92fa15777 feat(tahuantinsuyu): anti-solapamiento de glyphs + selector Naibod/Ptolomeo
Tres mejoras de UX para manejar conjunciones (stelium) y dar más
control sobre el sistema GR:

1. `spread_angles(angles, min_sep_deg)`: reposiciona angularmente
   los glyphs adyacentes para que ningún par caiga más cerca que
   el threshold visual (derivado del ancho del label pill al
   radio del ring). Iterativo (≤60 pasos), re-ordena cada
   iteración para preservar el orden circular, devuelve también
   `residual` ∈ [0,1] = fracción de presión no resuelta. Las
   posiciones REALES no se tocan — solo afecta la geometría
   visual del glyph. 5 tests cubren: empty, separados intactos,
   cluster cerrado, orden preservado, cluster infactible.

2. Aplicación al render de Bodies (natal/topo/pd/outer): cada
   layer pasa por spread_angles antes de iterar glyphs. Si
   residual queda alta, los discos y fonts se encogen
   proporcionalmente (0.55..1.0×) y los coord labels se omiten —
   evita pillas montadas sobre el bloque.

3. `find_clusters(angles, threshold_deg)`: detecta grupos
   angularmente cercanos (incluye wrap-around 359°→1°). Glyphs en
   cluster de ≥3 miembros NO llevan coord label individual;
   en su lugar, al final del loop se pinta UN solo label
   compartido con los símbolos concatenados (ej. "☉ ☿ ♀  14°56'")
   posicionado en el centroide angular del cluster. El usuario
   sigue viendo cada planeta con su disco, pero no se ahoga en
   pills superpuestas.

4. Selector Naibod/Ptolomeo en PrimaryDirectionsModule via
   `Control::Select`. Default Naibod (0°59'08.33″/año, moderno).
   El shell extrae `module_configs["primary_directions"]["key"]`
   y lo pasa en `PipelineRequest::PrimaryDirections { key }`;
   el bridge mapea string → `DirectionKey` y pasa al cómputo.
   El overlay meta muestra qué clave se usó: "GR Direcciones ·
   30.5a · Naibod".

Tests: 16 verdes (6 shell + 5 spread + 5 coord).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 18:38:51 +00:00
sergio a7214e0498 refactor(tahuantinsuyu): aros a/b/c/d/e canónicos + un solo anillo por bloque
Reorganización de los radios siguiendo nomenclatura clara del
usuario (de afuera hacia adentro):

  Aro A (1.00·r)  externo zodiaco
  Zona AB         signos … (sign dial)
  Aro B (0.92·r)  interno zodiaco / externo bloque ascensional
  Zona BC         casas topo (cusps b→c) + planetas topo + coords
  Aro C (0.78·r)  separador ascensional / casas geo
  Zona CD         casas geo (cusps c→d) + sus coords
  Aro D (0.62·r)  externo planetas natales
  (junto a D)     planetas natales + coords
  Aro E (0.49·r)  anclaje invisible de líneas de aspecto

Overlays opcionales (transits, midpoints, progression, solar arc,
composite) ahora viven todos INTERIORES al aro E — solo se pintan
cuando su módulo está activo, así no compiten con el layout base.

Cambios concretos en Radii:
- Doc `Radii` reescrito con la nomenclatura a/b/c/d/e arriba.
- Eliminado `bodies_inner` (la idea del "carril doble" confundía
  con el sistema de casas; ahora hay un único anillo por bloque).
- Coord labels uniformes — `label_r = ring - disk_size * 0.7`
  (hacia adentro) tanto para natal como para topocéntrico, ya que
  cada bloque tiene su propia zona radial bien definida.
- Coord pills de cusps de casa ahora se posan dentro de su propia
  zona (`r_in + (r_out - r_in) * 0.18`) — no se salen del bloque.
- Stroke 3D del bloque de planetas natales se mueve a `houses_inner`
  (= aro D), que es el verdadero borde visible del cinturón.

Si el usuario quiere un anillo adicional para algo en particular
(p. ej. transits clásico afuera del zodiaco), se agrega cuando
ese módulo se active.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 18:25:07 +00:00
sergio 7eb620aa17 feat(tahuantinsuyu): GR dual-ring + topo ascensional pegado al dial + coords
Dos cambios mayores que cierran el sistema GR/ascensional:

1. Reordenamiento radial — la capa ascensional (topocéntrico
   Polich-Page) se ubica AHORA pegada al sign dial, y la
   geocéntrica clásica queda más adentro. Layout outer→inner:
   - sign_dial (1.00 → 0.88)
   - topo_houses_outer (0.875) / topo_houses_inner (0.79)  ← P-P pegadas al zodiaco
   - topocentric (0.755)                                    ← planetas topo con coords
   - transits (0.71)
   - houses_outer (0.66) / houses_inner (0.54)             ← Placidus geo
   - midpoints (0.50) / bodies (0.47) / bodies_inner (0.44) ← natal geo con coords
   - pd_direct (0.495) / pd_converse (0.425)               ← dual-ring GR
   - aspects (0.41) / progression (0.36) / solar_arc (0.30)

   Topocéntrico default ON (era OFF en la fase previa).
   Coord labels ahora se pintan también en planetas topocéntricos
   (label hacia adentro, no afuera, para no chocar con casas P-P).

2. Sistema GR Direcciones Primarias (dual-ring):
   - Nuevo `PipelineRequest::PrimaryDirections { target_age_years }`.
   - `build_primary_directions_overlay` proyecta cada cuerpo natal
     con `directed_longitude` (key Naibod) en dos direcciones —
     directa y conversa — y emite dos Layer Bodies con
     `module_id` "pd_direct" / "pd_converse".
   - Canvas: nuevos `pd_direct` y `pd_converse` en Radii; en el
     render de Bodies disco más chico y alpha 0.80. Los dos anillos
     se marcan con punteado fino que "abraza" el cinturón natal
     por afuera y por adentro — el natal queda en el centro.
   - Nuevo `PrimaryDirectionsModule` con toggle + slider de edad
     (0..120, step 0.05a). Activable desde el panel.

Tests: 6 shell + 5 coord siguen verdes; el motor matemático
(eternal-astrology directed_longitude) y house system Polich-Page
están testeados desde el commit `e385ab2` en eternal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 18:08:29 +00:00
sergio 1d49b9ff88 feat(tahuantinsuyu): capa "ascensional" topocéntrica completa
5 fases que cierran el sistema topocéntrico end-to-end, conviviendo
con el cómputo geocéntrico tradicional sin reemplazarlo:

T3 — Pipeline en tahuantinsuyu-engine:
- Nuevo `PipelineRequest::Topocentric`.
- `build_topocentric_overlay(natal, render)`: para cada placement
  natal aplica `topocentric_ecliptic` (paralaje horizontal con
  `distance_km/AU` + observer.lat_rad + LST + obliquidad), emite
  Layer Bodies en ring=0.50 con `module_id="topocentric"`.
  Recalcula cusps con `Houses::compute(PolichPage, ...)` y emite
  Layer Houses asociado. Si la latitud cae en el círculo polar y
  Polich-Page diverge, sigue con planetas topocéntricos solos.

T4 — Render overlay en canvas:
- Nuevo `Radii.topocentric = 0.555·r` (justo bajo el carril natal
  bodies=0.60). `body_ring("topocentric")` lo mapea.
- Glyphs topocéntricos con disco más chico (22→22*s) y alpha 0.75
  (vs 1.0 natal) — se distinguen como "el sutil debajo del
  fuerte". En Luna el shift natal↔topo es visible; en Saturno los
  dos glyphs casi se superponen.
- Cusps Polich-Page pintadas como línea punteada (dash 3/2.5px)
  en un anillo interior al de casas geocéntricas, color
  `house_cusp` α=0.55 — claramente sistema secundario sin
  esconderse.

T5 — Módulo TopocentricModule:
- Nuevo módulo en tahuantinsuyu-modules con id="topocentric",
  label "Topocéntrico (ascensional)". Toggle "Activar" default
  OFF (es overlay opcional). Registrado en `Registry::with_builtins`.
- Shell traduce `module_configs["topocentric"]["enabled"] = true`
  → `PipelineRequest::Topocentric` en `build_requests`. Persiste
  por carta vía el mismo mecanismo de `persist_module`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:56:40 +00:00
sergio 86c5fd8653 feat(tahuantinsuyu): coord labels con minutos + control en panel
- Precisión a minutos: `format_coord_compact` ahora emite
  "DD°MM'{signo}" (ej. "14°56'"). Trabaja en minutos enteros para
  evitar drift de floats acumulado, hace rollover correcto a través
  de bordes de signo (29°60' → 0° del siguiente) y wrap-around de
  ángulos negativos. 5 tests verdes:
  * 0° → "0°00'"
  * 14.9333° → "14°56'"
  * 29.9995° → "0°00'" (carry-over)
  * 270° → "0°00'"
  * -10° → "20°00'" (wrap)

- Toggle en panel: nuevo `Control::Toggle` "Coordenadas (grado°min')"
  en NatalModule, default ON, hotkey C. Sincronización bidireccional:
  panel → canvas via `set_show_coords` (idempotente, no emite),
  canvas → panel via nuevo evento `CanvasEvent::ShowCoordsChanged`
  que el shell traduce a `panel.set_toggle("natal","show_coords",…)`.
  Sin loop porque el setter no emite.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:49:34 +00:00
sergio 8ede06f8c4 feat(tahuantinsuyu): distinción eclíptica/casas, coord labels, jog-dial con Ctrl
- Anillo de casas claramente distinto del dial zodiacal: nuevo
  `house_ring_color(palette)` que toma `house_cusp` y le aplica
  un hue shift de 140° en paletas con color (en BW devuelve el
  color original — un shift cromático en monocromo es ruido sin
  información). El sistema ascensional (casas) ya no se confunde
  con el eclíptico (signos): dorado vs verde/teal.

- Coordenadas permanentes en planetas y cusps de casa: por
  default visibles, togglean con hotkey `C` o desde el panel
  vía `state.show_coords`. Cada planeta natal lleva una pill
  pequeña afuera del disco con "DD°{signo}" (ej. "14°"); cada
  cusp de casa lleva la misma pill por dentro del anillo
  interior. Helpers nuevos: `format_coord_compact`,
  `coord_label`.

- Jog-dial requiere Ctrl/Cmd para activar. Sin modifier, LMB
  drag es siempre pan — sobre o fuera del anillo. Con modifier
  + LMB sobre el anillo se activa la rotación de tiempo (el
  control de rectificación). Evita rotaciones accidentales al
  navegar la rueda.

- Hint del info_row actualizado: incluye [C]oords y la
  convención "Ctrl+drag = tiempo".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:40:19 +00:00
sergio e9369371db fix(tahuantinsuyu): crash al abrir modal + simplificación de anillos
- Crash fix (panic en gpui entity_map.rs:138 / double_lease):
  `render_chart_form` hacía `cx.entity().read(cx)` mientras
  estaba dentro del `render()` del tree — la entity ya estaba
  leased como `&mut self` y un read concurrente disparaba el
  double_lease_panic. Se cambió la firma para recibir
  `picker_open` y `city_atlas` como parámetros desde
  `render_modal` (que sí tiene `&self`).

- Simplificación de anillos: el carril de planetas se acerca
  (bodies 0.60·r / bodies_inner 0.57·r) — antes 0.05 de
  separación, ahora 0.03, se ve como "carril" en lugar de dos
  anillos sueltos. El stroke visible del círculo de aspectos
  se elimina — `radii.aspects` queda solo como punto de
  anclaje para las líneas. El `bodies_inner` cambia a stroke
  plano más sutil (no 3D) para no competir con `bodies`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:26:34 +00:00
sergio 9acdf68d67 feat(tahuantinsuyu): orden visual — zoom uniforme, círculo de aspectos, profundidad
Tercera tanda de UX a partir de feedback:

- Zoom uniforme sobre glyphs DOM: font_size y disk_size de signos,
  números de casa, planetas natales/overlay/outer y labels
  ASC/MC/DESC/IC se multiplican por view_scale. Antes solo escalaba
  la geometría del canvas (anillos, líneas), los símbolos quedaban
  fijos — sensación de "todo se mueve menos los iconos".

- Doble anillo de planetas + círculo de aspectos: nuevo `bodies_inner`
  en `Radii`, junto con `bodies` define el "cinturón" donde viven
  los glyphs natales. `aspects` movido de 0.24*r a 0.49*r (de
  cerca-del-centro a pegado al cinturón) — las líneas de aspecto
  ahora conectan cuerpos cerca de su anillo en lugar de cruzar
  toda la rueda. Los tres anillos (bodies, bodies_inner, aspects)
  se pintan con stroke_circle_3d para que sean visibles.

- Doble línea de casas más fuerte: houses_outer + houses_inner
  ambos con stroke_circle_3d y `house_cusp` α=0.85. Antes solo
  houses_inner tenía un stroke plano y débil.

- Líneas de aspecto por orbe + filtro de menores:
  `aspect_width(kind, orb, mono)` modula grosor inverso al orbe.
  Aspectos mayores arrancan en techo 2.1 px (orbe 0°) hasta 0.7 px
  (orbe 8°); menores entre 0.5 y 1.2 px sobre orbe 0-3°. Los
  aspectos menores se omiten directamente si orbe > 3°.

- Vignette en lugar de starfield: `paint_depth_field` reemplaza
  `paint_starfield`. Pinta ~28 anillos concéntricos del centro al
  borde con alpha cuadrática creciente (curve t²) — el centro
  permanece claro y el borde se oscurece. Da profundidad sin
  ruido de puntos. Solo en dark themes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:10:01 +00:00
sergio 1078e433f2 feat(tahuantinsuyu): rueda 3D, hover-highlight, universo, themes papel
Segunda tanda de UX a partir de feedback de uso real:

- Zoom/pan reasignados: wheel = zoom puro (sin modifier). LMB drag
  fuera del anillo de signos = pan; sobre el anillo = jog-dial
  (rectificación). MMB sigue como pan secundario. Tecla `0`
  resetea zoom + pan.

- Planetas legibles: el "dot rellenado" se reduce a 3 px (solo
  marca el grado exacto). Encima va `planet_glyph` con disco-halo
  del bg_panel y border del color del planeta — el glyph unicode
  astronómico (☉☽☿♀♂♃♄♅♆♇) ahora se lee contra cualquier fondo.

- Aspectos hover-highlight: al hovear un planeta, sus líneas se
  mantienen al 100 % y el resto cae a 18 %. Resuelve el "¿quién
  contra quién?" sin desordenar la rueda.

- Ascensionales: cruz completa ASC-DESC + MC-IC (4 radios) con
  α=0.55. Labels ASC/MC/DESC/IC como pills con bg-halo y border
  `angle_highlight`, font 11 — antes eran texto chico que se
  fundía con el dial.

- Universo: el wheel pierde su bg de cuadrado (que cortaba contra
  el panel). El root del canvas pinta un starfield sutil ~130
  puntos deterministas (xorshift32 con seed fija, sin parpadeo
  entre frames). Solo activo en themes dark — sobre fondos claros
  generaría ruido.

- Estilo 3D anillos: `stroke_circle_3d` (highlight +luma + base +
  shadow -luma) reemplaza al stroke plano en sign_outer, sign_inner
  y el outer ring. Más `paint_dial_bevel` con 10 strokes finos en
  bell curve entre sign_inner y sign_outer — simula gradient radial
  que gpui canvas no soporta nativo.

- Theme `Print Color`: papel crema, paleta astro con luminancia
  0.26-0.34 y saturación alta, sin glow ni gradients.

- Theme `Print B&W`: monocromático sobre blanco puro. Aspectos
  diferenciados por dash pattern en lugar de color:
  conjunction/opposition sólidos, square dash medio, trine dash
  largo, sextile dotted, minors dotted finísimo. `paint_segment`
  con `dash: Option<(on,off)>` para implementar dashes (gpui
  canvas no tiene stroke dash nativo).

Todos los tests siguen verdes (6 shell + 5 yahweh-theme +
2 tahuantinsuyu-theme).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:45:48 +00:00
sergio e09207b152 feat(tahuantinsuyu): UX pass — splitter, light wheel, scroll, zoom/pan, dock lateral
Seis fixes derivados de testing real, ordenados por costo:

- Splitter (yahweh-widget-splitter): `flex-basis: 0` por item para que
  el ratio flex-grow se respete sin importar el min-content de los
  hijos. Sin esto, al cambiar el canvas de Empty→Wheel (WHEEL_SIZE
  fijo de 580px) la suma de basis excedía el contenedor y flexbox
  abandonaba el ratio 1:4, aplastando el tree a 0px (síntoma
  reportado: "el tree desaparece al seleccionar carta"). También se
  amplió la hit-zone del divider de 4px a 12px manteniendo una franja
  visual de 4px centrada — la zona de pointer-capture y cursor es
  ahora mucho más generosa, el visual sigue fino.

- Light mode wheel (tahuantinsuyu-canvas + tahuantinsuyu-theme): el
  gradient del fondo del wheel pasa de alphas 0.06/0.03 (invisibles
  contra fondo claro) a 0.18/0.10 cuando el theme es light. Cusps y
  aspectos secundarios del light palette bajan luminancia y suben
  alpha para no lavarse contra blanco.

- Panel scroll (tahuantinsuyu-panel): body del control panel agrega
  `flex_grow + min_h(0) + overflow_y_scroll` para que cuando los
  controles no caben aparezca scroll vertical en lugar de cortarse.

- Canvas zoom + pan (tahuantinsuyu-canvas): nuevo estado
  view_scale / view_pan_x / view_pan_y. Ctrl+wheel zoomea
  multiplicativo (clamp 0.5..3.0); wheel solo paneja. MMB drag para
  pan libre. Hotkey `0` resetea zoom+pan. Hit-tests del jog-dial y
  hover derivan ahora el `r_outer` del width actual del canvas, así
  se autoescalan con el zoom.

- Panel dock lateral (shell.rs): nuevo `PanelDock { Bottom, Right,
  Left }` configurable desde 3 botones en el header (◧ ▭ ◨). Bottom
  mantiene el layout histórico (tree+canvas / panel); las variantes
  laterales colapsan los splitters anidados en uno solo horizontal
  de 3 columnas. El dock se persiste en `layout.panel_dock` y cada
  layout guarda sus flex en una key distinta para no pisarse.
  `load_split_flex_n` / `save_split_flex` generalizados a N hijos.

Tests: 6 pasan (incluye nuevo roundtrip de PanelDock y N-flex).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:10:16 +00:00
sergio d2b6b8b12e test(tahuantinsuyu): tests de integración del Shell con TestAppContext
Cubre los wiring points del binario que las unit tests por-crate no
ven: construcción end-to-end de Shell, selección de carta, derivación
de PipelineRequests y NatalOptions desde module_configs, y roundtrip
de layout via la tabla settings.

- `gpui` con `test-support` en dev-dependencies.
- 5 tests en `shell::tests`:
  * `shell_constructs_smoke` — instancia Shell con store in-memory
    sin panic. Cubre cableado de suscripciones (tree/panel/canvas
    + 2 splitters) y arranque del background loop del broker.
  * `select_chart_updates_current` — apply_selection(Chart(id))
    puebla `current_chart` y avanza `render_seq`.
  * `module_toggles_produce_requests` — al habilitar 3 módulos
    overlay, `build_requests` devuelve esos 3 PipelineRequest en
    orden; deshabilitar uno lo remueve.
  * `natal_options_read_from_configs` — orb_multiplier, show_minors,
    show_dignities se leen correctamente desde module_configs["natal"].
  * `split_flex_round_trip_via_store` — load/save_split_flex con
    settings, incluyendo defaults para valores corruptos o ≤0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 01:13:02 +00:00
sergio e044d47516 feat(tahuantinsuyu): persistir flex de los splitters entre sesiones
Hasta ahora cada boot reseteaba los splitters al default (1:4
horizontal, 4:1 vertical), forzando a rearrastrar manualmente cada
vez. Ahora el flex se guarda en la tabla `settings` ya existente.

- `tahuantinsuyu-store`: nuevos `get_setting`/`set_setting` con
  upsert + test de roundtrip.
- `tahuantinsuyu` shell: al boot, `load_split_flex` lee
  `layout.main_split` y `layout.outer_split` (formato "a,b" como
  texto). Si no hay entry o está corrupto cae a defaults.
- Subscribe a `SplitEvent::DragEnd` en cada splitter — `save_split_flex`
  escribe los flex actuales al settings. Mouseup-driven, no
  cada-frame: 0 escrituras durante el drag, 1 al final.

`module_configs` ya estaba persistido por carta vía la tabla
`module_state` (`persist_module` + `load_persisted_module_states`),
no requiere cambios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:51:31 +00:00
sergio 904f334069 perf(tahuantinsuyu): LRU cache de NatalChart por (birth, config, offset)
`NatalChart::compute` cuesta varios ms (VSOP2013 + casas + aspectos
base). Bajo drag de slider en el panel, el shell dispara `compose()`
decenas de veces — la natal del sujeto principal y la del partner
de Synastry/Composite son **idénticas** entre frames pero hoy se
recomputan.

Nuevo `natal_cache.rs`: LRU de 8 entradas con `Mutex<Vec<(Key, Arc)>>`,
key = hash de contenido `(StoredBirthData, StoredChartConfig,
offset_minutes)`. Move-to-front en hit, evict del back cuando se
llena. f64s se hashean vía `to_bits()`.

`compute_natal_chart` ahora consulta el cache antes de delegar a
eternal; firma cambia a devolver `Arc<NatalChart>` — los call sites
(natal principal, partner de Synastry/Composite) usan auto-deref a
través de `Arc::Deref` sin cambios.

Editar una carta (cualquier campo de `StoredBirthData` o
`StoredChartConfig`) invalida automáticamente su entrada porque el
hash cambia. Capacidad 8 cubre el caso típico (natal + partner) con
holgura.

Test nuevo `natal_cache_hits_are_faster` valida que `compose` con
offset_minutes repetido es más rápido que con offset distinto (HIT
vs MISS): 9 tests engine, todos verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:41:36 +00:00
sergio 2192c29d4f chore(tahuantinsuyu): fase 28 — limpieza de warnings y dead_code
- Reemplaza `Context<Self>` por `Context<'_, Self>` (y la misma
  fórmula para `Context<TahuantinsuyuTree>`) en tree/panel/canvas:
  60 warnings de "hidden lifetime parameters are deprecated" → 0.
- Borra `TREE_WIDTH` y `PANEL_HEIGHT` (constantes muertas) y el
  campo `main_split` del shell (vive como child de outer_split,
  no necesita retención aparte).
- Quita `yahweh-bus` de tahuantinsuyu — el `bus: Entity<AppBus>`
  estaba con `#[allow(dead_code)]` sin cablear. Cuando lo
  necesitemos para coordinación cross-app lo reagregamos.
- Suprime imports `Module` (panel), `AppContext` (canvas) y
  prefija el `cx` no usado en `on_jog_down`.
- Marca `BrahmanStatus::Offline.reason` y `Shell.tree` con
  `#[allow(dead_code)]` documentando por qué se retienen
  (logs y subscripciones).

Workspace ahora compila limpio salvo un warning conocido de
`eternal-validation` (variable `sin_i` sin usar — fuera de
brahman).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:28:17 +00:00
sergio a4d1e0dc17 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>
2026-05-18 00:21:00 +00:00
sergio e2da24239e feat(tahuantinsuyu): fase 26 — data plane brahman real (service socket + CLI)
Cierra el círculo del Card: el flow.input "chart-request" y output
"chart-result" declarados desde fase 1 ahora tienen data plane real.
Otros módulos brahman (incluyendo el CLI nuevo) pueden conectar al
Unix socket de Tahuantinsuyu y pedir cómputos de cartas natales sin
abrir la GUI.

## Protocolo y server

Nuevo módulo `tahuantinsuyu_card::service` con:

- `ComputeRequest { Ping, Natal { birth, config, offset_minutes,
  label } }` — postcard-serializable
- `ComputeResponse { Pong, Render { render }, Error { message } }`
- `serve(socket_path)` — async loop sobre tokio::UnixListener,
  spawn por conexión, frame `u32 length` LE + postcard payload
  (mismo molde que brahman-handshake). Cap defensivo a 1 MiB por
  frame.
- `request(&socket, &req) -> Response` — cliente async one-shot
  (abre, envía, recibe, cierra)
- `spawn_service_thread(path)` — thread dedicado con tokio
  current_thread runtime; loggea warn si bind falla, la app sigue
  standalone.

La Card ahora declara `service_socket: Some(default_service_socket())`
— el broker brahman puede revelar este path a consumidores que
matcheen el flow `chart-request`. Path canónico:
`$XDG_CACHE_HOME/tahuantinsuyu/service.sock` (con fallback a
/tmp).

## CLI nuevo

`crates/apps/tahuantinsuyu-cli` — binario standalone que usa el
helper cliente. Comandos:

- `tahuantinsuyu-cli ping` — health check
- `tahuantinsuyu-cli natal --year ... --month ... --day ... --hour
  ... --minute ... --tz-min ... --lat ... --lon ... [--alt ...]
  [--label "..."] [--offset-minutes N]` — pide compute y emite
  RenderModel como JSON pretty-print en stdout

Útil para:
- Smoke tests del data plane (CI puede levantar la app + ping)
- Scripts batch (computar 100 cartas y exportar JSON)
- Integraciones con otros tools del fractal brahman vía broker

## Cambios accesorios

- apps/tahuantinsuyu/main.rs: spawn_service_thread al boot junto al
  sidecar. Loggea el path del socket a stderr para debug.
- Cargo workspace: agrega tahuantinsuyu-cli como member.
- tahuantinsuyu-card Cargo: agrega deps (engine, model, postcard,
  tokio, tracing, directories, thiserror) para soportar el server.

Lo que falta para integración brahman 100%:
- Suscripción al broker como "consumer-aware" para detectar cuando
  otros módulos publican `chart-request`s
- Publishing de eventos al broker cuando se crean/borran cartas
- Ambos requieren protocolo handshake bidireccional sobre el Init
  socket (no service_socket) — fase posterior.

cargo check verde, 8 tests engine + 1 modules verdes. CLI compila;
prueba end-to-end (ping + natal) queda a manos del usuario que
levante la GUI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:09:22 +00:00
sergio 6191ce7dee feat(tahuantinsuyu): fase 25 — production polish (tree search + hotkey SVG + aspect tooltips)
Tres mejoras de usabilidad que aprovechan toda la infraestructura ya
construida. PNG export (#17) lo dejé para una fase futura — agregar
resvg suma ~1MB al binario y el SVG ya cubre el caso profesional.

## #9 — Búsqueda en el tree

Arriba del árbol de groups/contacts/cartas aparece un TextInput con
placeholder "Buscar nombre…". Al apretar Enter aplica el filtro:
case-insensitive substring match sobre group.name, contact.name y
chart.label. Auto-expande recursivamente todos los ancestros que
contienen un match — los resultados quedan visibles sin que el
usuario tenga que abrir chevrons. Escape limpia el filtro.

- tree: TahuantinsuyuTree gana `search_filter: String` y
  `search_input: Entity<TextInput>`. set_search_filter() actualiza +
  auto_expand_matches + refresh.
- group_has_match() / contact_has_match() recursivos chequean si
  algún descendiente matchea.
- append_groups/contacts/charts filtran por substring antes de
  emitir cada row.
- render: nueva barra search arriba con border-bottom.

## #10 — Hotkey [S] = export SVG

El canvas ya tenía botón "⬇ SVG" en el header del wheel. Ahora la
tecla [S] sobre el wheel (con focus) emite el mismo evento
ExportSvgRequested. La línea de hotkeys del footer pasa a:
"[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [S]vg [R]eset".

## #8 — Tooltips sobre líneas de aspecto

- engine: LineSeg gana fields opcionales `from_body: String`,
  `to_body: String`, `orb_deg: f32` (default empty/0.0 para
  back-compat serde). Bridge popula los 6 sites de LineSeg
  construction (natal aspects + 5 cross overlays) vía un script
  Python regex que añadió `from_body: body_symbol(a.X).into()` +
  `to_body: body_symbol(a.Y).into()` + `orb_deg: a.orb_abs_deg() as
  f32` a cada constructor.
- canvas: HoverInfo gana variante Aspect { module_id, from_body,
  to_body, kind, orb_deg, local_x, local_y }. on_hover_check itera
  Aspects layers después de los Bodies (precedencia a planetas) y
  computa distancia punto-segmento con `dist_point_segment` helper —
  threshold 4px para hover. Tooltip muestra "☉ △ ♂  ·  orb 2.3°" con
  prefix de módulo si no es natal.

cargo check verde, 8 tests engine + 1 modules verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:05:50 +00:00
sergio a539fab15c feat(tahuantinsuyu): fase 24 — observabilidad del broker brahman
Primera pieza concreta de integración con el fractal brahman. La app
deja de ser standalone visible: ahora muestra el estado del broker
en el header con un badge actualizado cada 30s.

- Shell gana enum BrahmanStatus { Pending, Connected { count },
  Offline { reason } } + field brahman_status.
- spawn_brahman_status_loop arma un task cx.spawn que cada 30s
  invoca brahman_sidecar::list_sessions_blocking sobre el
  background_executor (no UI thread — list_sessions_blocking abre su
  propio tokio runtime, hacerlo en el UI panicearía con "nested
  runtime"). Update via this.update + cx.notify dispara repintado del
  badge.
- header agrega pill "Brahman ✓ N sessions" (color accent cuando
  conectado), "Brahman · offline" (fg_disabled) o "Brahman · …"
  (fg_muted) según el último ping. Entre el separador flex_grow y
  el theme_switcher.
- apps Cargo agrega brahman-sidecar como dep directa.

La Card de tahuantinsuyu (fase 1) sigue declarando los flows
`chart-request` (input) y `chart-result` (output), pero ESTOS NO
ESTÁN CABLEADOS A UN DATA PLANE — solo aparecen en el broker como
declaración. Para que tahuantinsuyu PUBLIQUE/CONSUMA datos reales
(otra app del fractal recibiendo una carta serializada, o pidiendo
un cómputo) hay que:
1) Abrir un service_socket Unix server en el sidecar
2) Implementar protocolo postcard sobre ese socket
3) Otro módulo descubre el socket via broker → conecta y envía/recibe

Eso es una fase separada (25+). Esta fase 24 cubre la observabilidad
mínima: la app sabe que el fractal está vivo y muestra el head count.
Cubre el espíritu del brief inicial ("integrar con yahweh que maneja
gpui para intercomunicar widgets") al nivel de visibility — el data
plane real es un proyecto en sí mismo.

cargo check verde. Sin tests nuevos (la lógica nueva es interacción
UI + background task — los tests serían smoke tests del Shell que
no tenemos).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:51:22 +00:00
sergio 295c9ba554 feat(tahuantinsuyu): fase 22 — layout splitter + atlas loadable desde XDG
Dos features de producción que mejoran la usabilidad sustancialmente.

## #7 — Layout reorganizable con SplitContainer

Los 3 paneles ya no tienen tamaños hardcodeados. Reusamos
yahweh-widget-splitter (mismo que usa yahweh-shell para sus layouts
JSON-config) con 2 niveles:

- outer (Vertical): main_split arriba (flex 4) + panel abajo (flex 1)
- main_split (Horizontal): tree (flex 1) + canvas (flex 4)

El usuario puede arrastrar los dos divisores para redimensionar
libremente. Por ejemplo: en una pantalla ancha, dar más al canvas; en
una sesión de lectura analítica, agrandar el panel abajo para ver más
módulos expandidos.

- Shell gana fields main_split + outer_split: Entity<SplitContainer>.
- new() construye ambos con ChildSlots envolviendo tree/canvas/panel
  como AnyView (mismo patrón que LayoutHost de yahweh-shell).
- render() simplificado: header + body(outer_split). Las constants
  TREE_WIDTH y PANEL_HEIGHT desaparecen.
- Cargo añade deps: yahweh-core (NodeId, LayoutDirection),
  yahweh-widget-splitter, yahweh-widget-container-core (ChildSlot).

## #15 — Atlas de ciudades cargable desde TSV

El array `CITY_PRESETS` const de 90 ciudades hardcoded ahora es la
función `default_city_presets() -> Vec<CityPreset>`. CityPreset.name
pasa de `&'static str` a `String` para que el atlas sea construible
en runtime.

TahuantinsuyuTree gana `city_atlas: Vec<CityPreset>` + setter
`set_city_atlas(atlas, cx)`. Al boot, Shell intenta cargar
`$XDG_DATA_HOME/tahuantinsuyu/atlas.tsv` y, si existe + parsea bien,
reemplaza el atlas hardcoded.

Formato TSV (líneas):
  name<TAB>lat<TAB>lon<TAB>tz_offset_minutes
  Líneas vacías y `#` comentario se ignoran.
  Líneas con cualquier parse fallido se descartan en silencio.

API pública: `parse_city_atlas_tsv(&str) -> Vec<CityPreset>` (en
tahuantinsuyu-tree), reusable por tests/scripts.

El usuario que quiera 50.000 ciudades de GeoNames cities5000.txt:
1. wget cities5000.zip de geonames.org
2. awk para extraer (name, lat, lon, tz_offset) y escribir TSV
3. mover a $XDG_DATA_HOME/tahuantinsuyu/atlas.tsv
4. relanzar la app

Sin fricción adicional para el usuario común (los 90 hardcoded cubren
99% de casos típicos en español/inglés).

cargo check verde, 8 tests engine + 1 test modules verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:46:34 +00:00
sergio d890bd4b3a feat(tahuantinsuyu): fase 21 — background compute + UranianModule
Cierre del brief original — última pieza visual (Uraniano) + perf.

## #1 — Compute en background thread

render_current() pasa de bloqueante a async. La pipeline corre en
cx.background_executor().spawn (no UI thread), y al terminar el
update vuelve al UI vía cx.spawn. Sin esto, un drag del slider con
muchos overlays bloquea el frame por hasta 200ms.

Cancelación: Shell gana `render_seq: u64`. Cada render_current()
incrementa el counter y captura su número; el closure async compara
antes de aplicar. Si llegó un compute más nuevo en el medio (drag
rápido), el viejo se descarta — evita el race donde un cómputo
lento sobrescribe uno reciente y rápido.

Inputs al background: Chart clonado + offset + Vec<PipelineRequest>
+ NatalOptions. La sesión VSOP2013 sigue siendo `static OnceLock`
read-only, accesible desde cualquier thread.

## #11 — UranianModule (versión textual)

Cierra la última pieza del brief original. Toggle "Uraniano (90°)"
en el panel; engine detecta cuerpos natales cuya longitud módulo 90
cae dentro de ε=2° y los agrupa como "ejes". Footer renderea cada
grupo como pill con los unicodes (☉ ♃ · 14.3°) bajo el header
"Ejes uranianos (90°)".

El algoritmo:
1. mod90 = longitude.rem_euclid(90.0) para cada placement
2. Sort por mod90 ascendente
3. Walk lineal agrupando entradas con diff(mod90) ≤ ε
4. Wrap-around check: el primer y último grupo se mergean si
   abarcan el cierre del dial (88→2 = solo 4° de diff modular)
5. Solo emite grupos con 2+ miembros (singletons no son fórmulas)

- engine: PipelineRequest::Uranian + UranianGroup struct +
  build_uranian_groups helper. RenderModel gana uranian_groups field.
  push_overlay_meta tipo "Uraniano · N ejes" o "sin ejes".
- modules: uranian::UranianModule (toggle "Activar"). Registry pasa
  a 9 módulos para ChartKind::Natal. Test actualizado.
- shell: build_requests detecta uranian.enabled, pushea
  PipelineRequest::Uranian (sin parámetros).
- canvas: footer agrega sección "Ejes uranianos (90°)" con pills
  arriba de la lista de aspectos — border angle_highlight para
  invitar a la lectura.

La visualización geométrica completa del dial de 90° con árbol de
simetría al hover queda para una fase posterior — esta versión
textual cubre el caso analítico (ver qué cuerpos están "en
relación uraniana") sin requerir un canvas secundario.

cargo check verde, 8 tests engine + 1 test modules (9 módulos
aplicables a ChartKind::Natal) verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:34:35 +00:00
sergio d3649bfd1a feat(tahuantinsuyu): fase 20 — accordion + lunar shift + CompositeModule + 90 ciudades
Cuatro features que cierran el set inicial de funcionalidades de
fase 1:

## D — Acordeón colapsable en el panel

Cuando hay 8 módulos en el panel se llenaba de cards. Ahora cada card
es expandible/colapsable por click en el header. Defaults:
- Natal siempre expanded
- Módulos con toggle "enabled" = true → expanded
- Resto → collapsed

El usuario puede forzar cualquiera vía override (collapse_overrides
HashMap). Chevron ▾/▸ a la izquierda del header. Hover sobre el
header lo resalta para invitar al click.

## B — Lunar return shift (navegación mensual)

PipelineRequest::PlanetaryReturn gana campo `shift_days: i64` (range
±180). El bridge lo suma a after_seconds del search anchor antes de
next_return. Para Solar return típicamente 0 (mantiene comportamiento).
Para Moon return, mover el slider ±28 días salta al retorno lunar
anterior o siguiente, permitiendo navegar mes a mes la lunación que
le toca al sujeto cumplido N años. PlanetaryReturnModule.controls()
agrega un slider "Shift días (lunar nav)". El badge del overlay
muestra "Moon return 38a +14d" cuando shift_days != 0. Helper
`planetary_return_request(body, age)` para callers que no necesitan
shift (zero default).

## C — CompositeModule

Carta compuesta (midpoint Davison) entre la natal del sujeto y otra
carta partner. Cada placement compuesto es el angular midpoint entre
los dos correspondientes. Engine: `PipelineRequest::Composite {
partner_chart: Box<Chart> }` + build_composite_overlay que llama
`eternal_astrology::composite()`. Renderiza placements en
`radii.composite = r * 0.32` (entre solar_arc 0.40 y aspects 0.24,
re-balanced). Módulo `composite::CompositeModule` con toggle +
ChartPicker (mismo patrón que synastry).

Shell: resolve_composite_partner reusa el fallback al primer hermano
del contacto, igual que synastry.

## A — 90 ciudades expandidas + dropdown scrollable

CITY_PRESETS pasa de 25 a 90 ciudades cubriendo:
- Latinoamérica (35): todas las capitales + grandes ciudades de AR/
  VE/CO/PE/CL/EC/UY/PY/BO/MX/CU/PR/CR/PA/SV/GT/HN/NI/DO/BR
- España (5) + Europa (20): Madrid/Barcelona/Sevilla/Valencia/Bilbao
  + London/Paris/Berlin/München/Roma/Milano/Amsterdam/Bruxelles/Wien/
  Zürich/Lisboa/Dublin/Stockholm/Oslo/København/Helsinki/Warszawa/
  Praha/Budapest/Athina/İstanbul/Moskva
- USA + Canadá (12): NY/LA/Chicago/Miami/Houston/SF/Seattle/Boston/
  DC + Toronto/Montreal/Vancouver
- Asia (16): Tokyo/Beijing/Shanghai/HK/Singapore/Seoul/Bangkok/
  Jakarta/Manila/Mumbai/Delhi/Bangalore/Karachi/Tehran/Dubai/Tel Aviv
- África (6): Cairo/Lagos/Nairobi/Johannesburg/Cape Town/Casablanca
- Oceanía (3): Sydney/Melbourne/Auckland

El popup del dropdown ahora es scrollable (h=360px, overflow_y_scroll)
con id estable para no perder scroll position entre re-renders.

cargo check verde, 8 tests engine + 1 test modules (8 módulos
aplicables a ChartKind::Natal) verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:24:11 +00:00
sergio 32ab22f954 feat(tahuantinsuyu): fase 19 — theme switcher + house tooltips + midpoints + city presets
Fase completa con 4 mejoras independientes que aprovechan toda la
infraestructura previa. La aplicación ahora cubre lecturas profundas
(midpoints uranianos), accesibilidad visual (tooltips de cusps),
personalización (6 themes vía yahweh-widget-theme-switcher) y
usabilidad pragmática (city presets en el form).

## C — Theme switcher en header

- apps/tahuantinsuyu: nueva dep yahweh-widget-theme-switcher.
- shell render(): theme_switcher(cx) en el extremo derecho del header
  (con flex_grow del divider del medio). Click cicla entre los 6
  presets de yahweh-theme (Nebula, Aurora, Sunset, FlatDark,
  SolarizedLight, HighContrast). AstroPalette::for_theme(theme) lee
  is_dark, así toda la rueda se re-tinta automáticamente.

## B — Tooltips sobre house cusps

- canvas: HoverInfo deja de ser struct para ser enum con variantes
  Body { ... } y HouseCusp { house_number, deg, local_x, local_y }.
  Helpers .local() y .key() unifican el acceso.
- on_hover_check: primero hit-test bodies (threshold 14px); si no hubo
  match Y el mouse está dentro del anillo de casas
  (houses_inner..houses_outer ± 6px), calcula la longitud zodiacal
  desde el ángulo de pantalla (inversa de polar_to_screen) y busca el
  cusp más cercano (proximidad angular < 2.5°). HoverInfo::HouseCusp.
- Tooltip render: "Cusp Casa N · Signo XX.X°".

## D — MidpointsModule (Uranian-lite)

- engine: PipelineRequest::Midpoints (sin parámetros, default empty).
- bridge: build_midpoints_overlay computa midpoints entre todos los
  pares de placements donde involucran Sol o Luna (~10 puntos según
  body set). Fórmula: si |a-b|>180, mid=((a+b)/2+180) mod 360, sino
  (a+b)/2 mod 360. Emite como Layer { kind: Midpoints, module_id:
  "midpoints", ring: 0.62 } con Glyph.symbol="sun/jupiter" y
  annotation="Sun/Jupiter".
- modules: midpoints::MidpointsModule con toggle "Activar". Registry
  pasa a 7 módulos. Test actualizado.
- shell: build_requests detecta midpoints.enabled, pushea
  PipelineRequest::Midpoints (no toma age ni body — es derivado puro).
- canvas: Radii agrega midpoints: r * 0.62 (entre houses_inner y
  bodies natales). body_ring("midpoints") y aspect_endpoints retornan
  ese radio. paint_wheel agrega un loop para LayerKind::Midpoints
  pintando dots pequeños (r=0.012, alpha 0.7 sobre house_cusp color)
  — los midpoints no llevan unicode symbol propio (no existe en
  Unicode astrológico estándar). El detalle del par viene en hover.
- Hover sobre un midpoint: tooltip muestra "☉/♄ Tauro 14.3° ·
  Sun/Jupiter" (display_symbol parsea "a/b" en dos unicodes;
  annotation incluye nombres completos eternal).

## A — City presets en el ChartForm

- tree: nueva const CITY_PRESETS con 25 ciudades (Latinoamérica
  capitales + 5 europeas + 5 anglosajonas + Tokyo/Sydney/Mumbai/Cairo)
  con (name, lat, lon, tz_offset_minutes) sin DST. CityPreset struct.
- tree: TahuantinsuyuTree gana city_picker_open: bool. close_modal
  lo resetea. toggle_city_picker + apply_city_preset(preset) helpers.
  apply_city_preset lee el Modal activo (CreateChart o EditChart),
  llama TextInput::set_text en place/lat/lon/tz del ChartForm,
  cierra el picker.
- render_chart_form: title_row ahora tiene "📍 Ciudad rápida ▾"
  button a la derecha del title. Click → toggle. Cuando picker_open,
  popup absoluto debajo con la lista de presets. Click en preset →
  autocompleta + cierra. El usuario sigue pudiendo editar manualmente
  cualquier campo después; el preset es solo un punto de partida
  rápido para evitar tipear coordenadas a mano.

cargo check verde, 8 tests engine + 1 test modules (7 módulos)
verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:12:17 +00:00
sergio 2cd34c82da feat(tahuantinsuyu): fase 18 — aspect list + hover tooltips + dignidades + export SVG
Fase grande con 4 features que aprovechan toda la infraestructura ya
construida. Engine ganó 2 módulos nuevos (dignity table data-only +
svg_export), el RenderModel se enriqueció con AspectSummary y los
glyphs con dignity_marker, y el canvas trae hit-test pasivo + lista
textual + botón de export.

## C — Lista textual de aspectos

- engine: nuevo `AspectSummary { module_id, from_body, to_body, kind,
  orb_deg, applying }` + campo `aspect_summary: Vec<AspectSummary>`
  en RenderModel. populate_natal_aspect_summary y
  populate_cross_aspect_summary se llaman desde compose por cada
  pasada (natal + 4 overlays). Ordenado por orb_deg asc (los más
  exactos primero).
- canvas: nuevo aspect_unicode helper (☌ ☍ △ □ ⚹ ⚻ ⚺ ∠ ⚼ Q bQ).
  Footer agrega un grid flex-wrap con las top 12 entries del summary,
  cada una formateada como "[module_id] ☉ △ ☾  ·  2.3° A" coloreado
  por palette.aspect(kind).

## A — Tooltips al hover

- canvas: nuevo HoverInfo { module_id, symbol, deg, house, retrograde,
  dignity_marker, annotation, local_x, local_y } + state.hover.
  on_hover_check ejecuta hit-test sobre todos los glyphs Bodies +
  Outer (threshold 14px); se llama desde el handler MouseMoveEvent
  cuando NO está dragging (handler reescrito para soportar drag y
  hover en mismo callback). Cuando mouse sale del wheel, hover=None.
- Tooltip absoluto: "☉ Tauro · 23.4° · Casa 5 · ℞" con border
  angle_highlight. Posición offset arriba-derecha del planeta,
  clampada al wheel para no salirse.

## B — Dignidades esenciales clásicas

- engine: nuevo mod `dignity` con `Dignity { Rulership/Exaltation/
  Detriment/Fall }` + tabla rules_classical (7 planetas tradicionales)
  + exalts_at table. `essential_dignity(body, sign_index) -> Option`.
  4 tests cubren rulership/detriment/exaltation/fall + edge case
  modernos (Urano/Nept/Plutón sin dignidad clásica). 4 markers:
  + (domicilio), · (exaltación), − (exilio), * (caída).
- engine: Glyph gana campo `dignity_marker: Option<String>`. Default
  derive en Glyph para no romper N construction sites. bridge::
  annotate_dignities mutua RenderModel post-build agregando markers
  a glyphs natales según el signo de cada placement.
- NatalOptions agrega show_dignities. NatalModule.controls() agrega
  Toggle "Dignidades esenciales (+ · − *)" default false.
- canvas: glyph render append dignity_marker al texto después del ᴿ.

## D — Export SVG

- engine: nuevo `pub mod svg_export` con `render_to_svg(&RenderModel)
  -> String`. Reproduce la geometría del canvas en un SVG standalone
  800×800 escalable: anillos zodiacales, cusps, planetas con
  retrograde+dignity markers, aspectos coloreados por kind (cross
  conocen rings de origen/destino vía aspect_radii), labels ASC/MC/
  DESC/IC. Sin dependencias nuevas — write! sobre String. Test
  asserts well-formed XML.
- canvas: CanvasEvent::ExportSvgRequested + botón pequeño "⬇ SVG"
  en el header del wheel (al lado del title).
- shell: on_canvas_event ExportSvgRequested → export_current_to_svg
  recompose actual + svg_export::render_to_svg + write a
  $XDG_DATA_HOME/tahuantinsuyu/exports/<label>_<short_id>.svg.
  Ruta logueada a stderr para que el usuario encuentre el archivo.

`cargo check` y `cargo test` verdes con 8 tests en engine
(2 existentes + 4 dignity + 1 svg + 1 mock).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 12:07:45 +00:00
sergio 0ae622550d feat(tahuantinsuyu): fase 17 — filtros de aspectos + editor + cleanup + labels
Fase completa con 4 mejoras independientes que reusan toda la
infraestructura previa:

## A — Filtros de aspectos en NatalModule

NatalModule gana 3 controles nuevos que SÍ recomponen (a diferencia
de los show_* que solo togglean visibilidad):
- Toggle "Mayores (☌ ☍ △ □ ⚹)" default true
- Toggle "Menores (quincunx, semi-…)" default false
- Slider "Multiplicador de orbe" range 0.25..2.5 step 0.25 default 1.0

Engine API extendida sin romper la existente:
- pub struct NatalOptions { show_majors, show_minors, orb_multiplier }
- pub fn compose_with_options(chart, offset, requests, &NatalOptions)
- compose() queda como wrapper con NatalOptions::default()
- bridge::compose acepta el natal_options, construye OrbTable escalada
  (build_orb_table multiplier) y filtra aspects antes de pasarlos a
  build_render_model. Build_render_model dejó de filtrar majors
  internamente — ahora respeta lo que recibe.

Shell wire:
- build_natal_options() lee aspect_majors/aspect_minors/orb_multiplier
  desde module_configs["natal"] con defaults seguros.
- on_panel_event para natal: si key empieza con "show_" → canvas
  visibility (sin recompose); otherwise → update module_configs +
  persist + render_current.
- render_current pasa natal_options a compose_with_options.

## B — Editor de carta natal existente

- Store::update_chart(id, label, &birth, &config) — actualiza tres
  columnas preservando id/contact_id/related/created_at_ms y todo el
  module_state asociado (la FK CASCADE no se dispara por UPDATE).
- Tree: Modal::EditChart { id, form, error } reusa ChartForm que ya
  manejaba el create. open_edit_chart(id, w, cx) lee la carta con
  store.get_chart, pre-carga cada TextInput con el valor existente
  (label, birthplace, año, mes, día, hora, min, tz, lat, lon, alt).
  submit_modal::EditChart lee form, llama update_chart, preserva el
  config existente (zodiac/house_system/bodies no se editan acá).
  Menú contextual del chart agrega "Editar…" entre "Abrir" y
  "Renombrar".
- render_chart_form ahora toma `title: &str` parameter para que el
  modal muestre "Editar carta natal" vs "Nueva carta natal". El
  botón cambia "Crear carta" → "Guardar cambios" según el title.

## C — Single source of truth para OUTER_RING_MODULES

- engine exporta `pub const OUTER_RING_MODULES: &[&str] = &["transit",
  "synastry", "planetary_return"]`
- shell elimina su const local, importa del engine
- canvas elimina 4 listas hardcodeadas (paint_wheel outer ring active
  check + glyphs overlay + aspect_endpoints match) y usa contains() o
  early-return sobre el slice. Próximo módulo outer-ring solo necesita
  agregarse al const, no buscar copias.

## D — Labels ASC/MC/DESC/IC en el perímetro

Cuatro centered_glyphs en radii.sign_outer * 1.06 (justo afuera del
dial zodiacal, dentro del WHEEL_MARGIN) con color angle_highlight y
font 10px. El ojo identifica los 4 ángulos inmediatamente sin tener
que mapear la línea radial gruesa al ángulo correspondiente.
Las posiciones rotan con la rueda (drag del jog-dial los lleva).

`cargo check` y `cargo test` verdes. La fase agregó 6 controles
visibles al panel del NatalModule (4 view + 2 aspect filter + 1
slider) sin tocar la arquitectura de fases 6-15.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:45:58 +00:00
sergio cabdb2927e feat(tahuantinsuyu): fase 16 — polish místico (gradient sutil + glow en luminarias)
Dos toques pequeños que dan profundidad visual sin saturar:

1. **Gradient diagonal en el fondo del wheel**: linear_gradient 155°
   desde palette.dial_ring @ alpha 0.06 hasta palette.angle_highlight
   @ alpha 0.03. La opacidad mínima asegura que no compite con la
   geometría pintada encima; el efecto se ve sobre todo en las
   esquinas del cuadrado (afuera del círculo) y en los gaps entre
   anillos cuando no hay overlays. Da "shimmer mineral" muy velado.
   El wheel además ahora tiene rounded(12px) — perfila el cuadrado
   sin que se sienta como un container.

2. **Glow halo en luminarias natales**: paint_glow nuevo helper que
   pinta 3 fill_circle concéntricos con (radius × 5/3/1.8, alpha
   0.05/0.12/0.22). Aplicado solo a Sol y Luna del layer natal —
   son los puntos psicológicamente cargados y los que el ojo busca
   primero. El resto de planetas mantiene su dot limpio. GPUI 0.2
   no tiene radial_gradient nativo así que el shading concéntrico
   discreto cubre el rol.

- canvas: imports linear_color_stop + linear_gradient. paint_glow()
  helper. Loop de Bodies en paint_wheel detecta is_natal &&
  (sun|moon) → paint_glow antes del fill_circle del dot.

Convive con todo lo anterior: si la luminaria está oculta (toggle
[P] off), el glow también; si hay overlays con sus propios planetas,
solo las luminarias natales lucen halo (diferenciable de su contraparte
en transit/synastry/return).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:37:56 +00:00
sergio 1232e39397 feat(tahuantinsuyu): fase 15 — badges de overlays activos en el footer
Cuando hay overlays activos, debajo del info_row aparecen pills con
los nombres de cada uno (Natal, Tránsito ahora, Progresión 38.2a,
Sinastría · Ana, Saturn return 29a) — el usuario ve de un vistazo qué
está mirando sin tener que mapear los anillos manualmente.

El border de cada pill toma color según a qué slot del wheel
pertenece: outer ring (transit/synastry/planetary_return) →
palette.angle_highlight (dorado), inner overlays (progression/
solar_arc) → palette.house_cusp (tono apagado), natal → neutro.
Permite leer la pila de izquierda a derecha y ubicar visualmente cada
glyph del wheel.

- engine: nuevo OverlayMeta { module_id, label } + campo overlays:
  Vec<OverlayMeta> en RenderModel. build_render_model lo inicializa
  vacío; bridge::compose pushea un OverlayMeta por cada
  PipelineRequest después de su build_*_overlay correspondiente. Helper
  push_overlay_meta(render, id, label). Labels: "Tránsito ahora",
  "Progresión {age:.1}a", "Solar Arc {age:.1}a", "Sinastría · {name}"
  (lee partner_chart.label antes de mover el Box al builder),
  "{Body} return {age:.0}a" (usa eternal_sky body.name()).
- canvas: render_wheel separa el viejo footer en info_row (Asc/MC/ms +
  offset + hotkeys) y un badges_row opcional. badges_row aparece solo
  cuando render.overlays != empty. Pill helper centralizado: bg
  panel_alt, border 1px, text size 10, rounded 10. Border color
  decidido por module_id para correlacionar con el ring visual.

Compatible con compute_mock (que setea overlays = vec![] — ningún
mock badge). Persiste sin cambios — los configs siguen guardando su
estado, los OverlayMeta se reconstruyen en cada compose desde los
requests activos.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:34:54 +00:00
sergio 6d572c81ca feat(tahuantinsuyu): fase 14 — Return abstracto + Control::Select interactivo
El módulo SolarReturn se generaliza a PlanetaryReturn parametrizable
por cuerpo (Sun/Moon/Mercury/Venus/Mars/Jupiter/Saturn/Uranus/Neptune/
Pluto). Validado contra `Control::Select`, ahora interactivo como
tercer tipo de control draggable (después de Toggle/Slider/ChartPicker).

Refactor estructural: el dropdown del ChartPicker pasa a ser
infraestructura compartida — chart_picker_value/chart_picker_open
desaparecen, reemplazados por string_state/dropdown_open que sirven
a CUALQUIER control basado en string (picker + select).
render_chart_picker y render_select ahora son thin wrappers sobre
render_dropdown(options, include_auto).

- engine:
  - PipelineRequest::SolarReturn → PipelineRequest::PlanetaryReturn
    { body: String, target_age_years }. Body como string agnóstico
    (sun/moon/jupiter/...) que el bridge mapea a eternal_sky::Body
    vía map_body — el mismo helper que ya usa StoredChartConfig.
  - build_solar_return_overlay → build_planetary_return_overlay con
    parameter `body: Body`. next_return acepta cualquier body, así que
    Moon return (mensual) y Saturn return (29 años) funcionan igual.
    Mensajes de error incluyen body.name() para diagnóstico.
- modules:
  - SolarReturnModule → PlanetaryReturnModule (mod planetary_return).
    id "planetary_return". Controles: toggle "enabled" + Select "body"
    con 10 opciones de cuerpo (Sol → Plutón) + Slider edad. label
    "Retornos planetarios".
- panel:
  - Refactor: chart_picker_value/chart_picker_open → string_state/
    dropdown_open (compartido entre ChartPicker y Select).
  - set_string(module_id, key, value, cx) — API unificada. set_chart_picker
    queda como alias retrocompatible.
  - render_dropdown(options, include_auto, …) — helper común. picker
    pasa include_auto=true (muestra "(automático)" + separador);
    select pasa include_auto=false (las options son la única opción).
  - render_select implementado — el botón muestra la option's label
    (no value); click abre dropdown; click en opción emite ControlChanged
    con Value::String(option.value).
- shell:
  - OUTER_RING_MODULES const: "solar_return" → "planetary_return".
  - build_requests para planetary_return: lee body string del
    module_configs (default "sun"), arma PipelineRequest::PlanetaryReturn.
  - apply_selection inicializa target_age + body=sun default para
    planetary_return.
  - sync_panel_from_configs strings → set_string (era set_chart_picker).

Probarlo: en el panel del módulo "Retornos planetarios", click en el
dropdown "Cuerpo" abre el popup; click en "Saturno" + slider en 29
años + toggle "Activar" = ves la carta del primer retorno de Saturno
(cuando recién terminaba la primera vuelta) en el outer ring con
cross aspects al natal.

NOTE: La persistencia con id "solar_return" de fase 13 queda huérfana
en la DB de los users que ya hayan probado. No es destructivo —
simplemente esas rows quedan sin módulo que las lea. Pre-1.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:30:21 +00:00
sergio 8d95833c20 feat(tahuantinsuyu): fase 13 — Solar Return como sexto overlay
Sexto módulo siguiendo el patrón establecido. Cambio estructural:
3-way mutual exclusion para los módulos que comparten el outer ring
(transit + synastry + solar_return). Constante OUTER_RING_MODULES
abstrae el grupo para que fase 14+ pueda sumar lunar return / planet
returns sin tocar la lógica del shell.

- engine: PipelineRequest::SolarReturn { target_age_years } +
  build_solar_return_overlay. Llama eternal_astrology::next_return
  (Sun back to natal Sun, ventana ±1.5 años) desde un instante
  ~30 días antes del cumpleaños target. Computa la carta natal
  completa al return_instant (mismo observer + config natales —
  convención clásica) y la apila como Outer + Aspects cross natal ×
  return. z=12/13. Import: `next_return` añadido a la lista de
  re-exports del bridge.
- modules: solar_return::SolarReturnModule (id "solar_return", toggle
  + slider target_age_years 0..120 step 1.0). Registry pasa a 6
  módulos para Natal.
- shell: OUTER_RING_MODULES const con los tres ids; mutual exclusion
  generalizada de pair-wise a N-way (for-loop sobre el slice). Init
  de age en apply_selection ahora incluye solar_return. build_requests
  agrega la rama. Misma estructura que progression/solar_arc en la
  rama de age handling.
- canvas: aspect_endpoints("solar_return") = (bodies, transits). Tres
  loops del outer ring (paint dots, paint glyphs, anillos guía) ahora
  aceptan los tres module_ids.

Probarlo: en el panel, slider "Edad del retorno" en valor entero (ej.
36) + toggle "Activar" = ves la carta del año cuando volviste a tu
Sol natal a los 36, con todos sus planetas en el outer ring y cross
aspects con tu natal. Cambiando el slider podés explorar año por año.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:23:25 +00:00
sergio d9e21fbedc feat(tahuantinsuyu): fase 12 — Control::ChartPicker para Synastry
Cualquier carta del DB se puede elegir como partner de sinastría
(no solo hermanas del contacto actual). El módulo SynastryModule
declara un Control::ChartPicker; el panel renderea un dropdown que
abre un popup con todas las cartas; el shell inyecta las opciones y
resuelve el partner desde la selección persistida.

- modules: nueva variante Control::ChartPicker { key, label } sin
  default — las opciones las inyecta el host. SynastryModule.controls()
  agrega un picker con key="partner_chart_id".
- store: list_all_charts() — query sin filtros, ordenada por label
  case-insensitive. Pensada para selectores cross-contact.
- panel:
  - ChartOption struct público (id + label).
  - chart_options Vec + chart_picker_value HashMap + chart_picker_open
    Option (solo uno abierto a la vez).
  - APIs públicas set_chart_options / set_chart_picker para sync.
  - set_active_kind inicializa picker_value a None ("automático").
  - render_chart_picker: botón "▾ label" en bg_button; al click toggle
    un popup absolute con (automático) + separador + cada chart_option
    clickable. select_picker emite ControlChanged con Value::String(id)
    o Value::Null.
- shell:
  - new() llama refresh_chart_options al final para popular el panel
    desde el boot.
  - refresh_chart_options() builds Vec<ChartOption> desde
    list_all_charts (label + birth_brief "YYYY-MM-DD · Lugar"). Lo
    llamamos también desde TreeEvent::HierarchyChanged para que el
    dropdown refleje altas/bajas.
  - resolve_synastry_partner reemplaza find_synastry_partner: 1) lee
    module_configs["synastry"]["partner_chart_id"] y resuelve el chart;
    2) fallback al automático (primera hermana). El fallback significa
    que el módulo sigue funcionando sin elegir manualmente.
  - sync_panel_from_configs ahora maneja Value::String / Value::Null
    → set_chart_picker, así el picker se restaura al cargar una carta.

Persistencia: el partner_chart_id va al config_json (fase 11), así
que cada carta recuerda con quién hizo sinastría la última vez.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:17:35 +00:00
sergio 22e6ed6a71 feat(tahuantinsuyu): fase 11 — persistencia de module_configs por carta
Los toggles, sliders y partner pickers de cada overlay (transit,
progression, solar_arc, synastry) ahora persisten por carta en la
tabla SQLite `module_state` (que estaba creada desde fase 1 pero
sin cablear). Cambiar de carta y volver mantiene exactamente el
estado que el usuario dejó.

- shell:
  - apply_selection(Chart): tras setear defaults (target_age_years =
    edad actual), llama load_persisted_module_states(chart.id) que
    mergea sobre los defaults los valores guardados. Luego
    sync_panel_from_configs empuja todos los toggles/sliders al
    panel para reflejar el estado restaurado. Render al final.
  - load_persisted_module_states: lee list_module_states(chart_id),
    reconstruye el JSON combinado (mergea `enabled` de la columna
    SQL en el config), y lo mergea sobre lo que ya hay en
    module_configs. Vacant entries se insertan tal cual; occupied
    se patchean field-a-field para no perder defaults no guardados.
  - sync_panel_from_configs: itera module_configs, push toggle/slider
    al panel por cada key Bool/f64.
  - persist_module(module_id): extrae enabled del JSON, deja resto en
    config_json, llama upsert_module_state. Invocada desde
    on_panel_event "else" tras cada update + desde on_canvas_event
    para [T] + tras auto-disable del conflicting module en mutual
    exclusion.
- store: nuevo test module_state_roundtrip que cubre upsert/list +
  cambio de enabled vía upsert (UPSERT clause de fase 1 vuelve a
  validarse).

Flujo de usuario: ajustás el slider de progresión a 42.5 años,
activás synastry, cambiás de carta, volvés — todo está como lo
dejaste. La DB persiste por chart_id, así que distintos sujetos
mantienen estados independientes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:11:29 +00:00
sergio 97a6aab883 feat(tahuantinsuyu): fase 10 — Sinastría como overlay (bi-wheel con carta hermana)
Quinto módulo overlay. Cuando hay otra carta hermana del mismo
contacto, la sinastría pone las posiciones del partner en el outer
ring + dibuja cross aspects entre las dos personas. Mismo molde que
los overlays anteriores; única novedad: el PipelineRequest transporta
una `Chart` completa porque el partner no es derivable de la natal.

- engine: PipelineRequest::Synastry { partner_chart: Box<Chart> }.
  build_synastry_overlay(natal, partner_chart, render) llama
  compute_natal_chart sobre el partner y find_synastry_aspects entre
  los dos NatalCharts (sólo majors). Layers con module_id="synastry"
  y z=10/11. Reusa la helper compute_natal_chart de fase 5.
- modules: synastry::SynastryModule (id "synastry", toggle "Activar"
  sin hotkey por ahora). Registry agrega el quinto built-in. Test
  pasó a 5 módulos aplicables a ChartKind::Natal.
- shell: build_requests detecta synastry.enabled y llama
  find_synastry_partner — busca la primera carta hermana del contacto
  actual (mismo contact_id, distinto chart_id). Si no hay hermana,
  skip silencioso. Mutual exclusion: al prender transit o synastry
  se apaga el otro automáticamente (comparten outer ring) — sincroniza
  el toggle del panel + el layer_visibility del canvas.
- canvas: Radii::aspect_endpoints("synastry") devuelve (bodies,
  transits) — same slot que transit. Loops del outer ring aceptan
  module_id "transit" OR "synastry" (paint_wheel + glyph overlay).
  Sin radii nuevo — visualmente comparten el ring 0.82 con transit.

Para probarlo: creá dos cartas en el mismo contacto (ej. el sujeto +
su pareja). Abrí la primera y activá "Sinastría" en el panel. Verás
los planetas del partner en el outer ring + líneas que cruzan al
centro mostrando los aspectos entre las dos personas. Si tenés
transit prendido cuando lo activás, se apaga; al revés también.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:05:52 +00:00
sergio 1a3bc55016 feat(tahuantinsuyu): fase 9 — Solar Arc como segundo overlay
Confirma que la arquitectura de fase 6 escala: tres overlays simultáneos
(transit + progression + solar_arc) sin acoplamiento entre módulos, y
sin tocar el flujo del Shell salvo registrar el nuevo branch.

Tres puntos de extensión por overlay nuevo (exactamente los predichos):
1. variante en PipelineRequest
2. helper build_*_overlay en bridge + match arm en compose
3. módulo declarativo en modules/ + registro

- engine: PipelineRequest::SolarArc { target_age_years: f64 } +
  build_solar_arc_overlay que llama solar_arc_true(natal, session, age)
  → desplaza uniformemente cada placement y cusp por el arco solar
  (default ≈1°/año, vía true progressed Sun). Cross aspects natal ×
  dirigida vía find_synastry_aspects(majors). Layers con
  module_id="solar_arc" y z=8/9 (sobre todos los demás).
- modules: solar_arc::SolarArcModule con id="solar_arc", toggle
  "Activar" + slider target_age_years 0..120. Mismo shape que
  ProgressionModule. Registry.with_builtins lo registra. Test pasó a
  4 módulos aplicables a ChartKind::Natal.
- canvas: Radii.solar_arc = 0.40 (entre progression 0.48 y aspects),
  aspects shrunk a 0.32 para hacer lugar. Helpers Radii::body_ring()
  y Radii::aspect_endpoints() ahora reconocen "solar_arc". paint_wheel
  itera ambos overlays (progression + solar_arc) para dibujar dots,
  glyph overlays y anillos guía sutiles. Loop común `for (id, ring) in
  [..]` evita duplicación de código.
- shell: build_requests detecta solar_arc.enabled, agrega request con
  edad. apply_selection inicializa target_age_years para ambos
  overlays (progression + solar_arc) en current_age + sincroniza los
  sliders del panel. Helper module_age_or_current(id) factoriza la
  lectura de edad con fallback.

Activando los tres overlays al mismo tiempo el canvas se convierte en
una rueda de cinco anillos: zodíaco (1.00), tránsito (0.82), natal
(0.66-0.78), bodies natal (0.58), progression (0.48), solar arc (0.40),
con líneas de aspectos cross convergiendo desde el ring natal hacia
cada overlay simultáneamente.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:59:01 +00:00
sergio e0c5c02b8e feat(tahuantinsuyu): fase 8 — slider interactivo + slider de edad en progression
Los `Control::Slider` del panel ya no son display-only — son arrastrables
con el mismo patrón del splitter (canvas absoluto sobre el track + window
mouse handlers en cada frame). El `ProgressionModule` ahora expone un
slider de `target_age_years` (0..120) que el shell inicializa con la
edad actual del sujeto al cargar la carta.

- panel: SliderDrag struct + slider_state HashMap + slider_drag Option
  + métodos start/continue/end_slider_drag + apply_slider_position que
  calcula fraction desde la posición del mouse relativa al track y
  emite ControlChanged con el valor float. set_slider(module, key, val)
  para sincronización externa. set_active_kind ahora inicializa también
  los sliders desde sus defaults. render_slider pinta track + portion
  filled + thumb circular + canvas overlay con handlers de drag.
  Los Slider tienen un valor visible "X.X (min...max)" en el header.
- modules: ProgressionModule agrega Control::Slider target_age_years
  con range 0..120, step 0.25, default 30 (placeholder — el shell lo
  reescribe con la edad real al cargar la carta).
- shell: apply_selection(Chart) ahora calcula current_age, lo inserta
  en module_configs["progression"]["target_age_years"], y empuja al
  panel via set_slider. build_requests ya leía target_age_years desde
  el map (de fase 7), así que ahora el slider lo controla.

Mecánica: si activás "Progresión secundaria", el slider arranca en la
edad actual del sujeto. Arrastralo a la izquierda y la rueda recompone
la carta progresada para esa edad simbólica — vas viendo cómo el sujeto
"evoluciona" o "involuciona" a través de su línea temporal interna,
con los planetas progresados moviéndose por el anillo interno y los
cross aspects con la natal reorganizándose en tiempo real.

Same pattern aplica de aquí en más para cualquier slider futuro
(harmonic en NatalModule, target_year en SolarArc, orb_multiplier, …).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:54:06 +00:00
sergio 42e09fd7cd feat(tahuantinsuyu): fase 7 — progresión secundaria como overlay (prueba de la arquitectura)
Valida que el refactor de fase 6 escala: agregar un overlay nuevo
(progresión secundaria, "día por año") tocó exactamente lo predicho —
una variante en PipelineRequest, un helper en bridge, un módulo
declarativo en `progression`, una línea en build_requests, y el canvas
para pintarlo. Cero cambios en el flujo de eventos del Shell.

- engine: PipelineRequest::SecondaryProgression { target_age_years: f64 }
  + build_progression_overlay(natal, age, render) que delega en
  eternal_astrology::secondary_progression(natal, session, age), pinta
  los placements progresados en un anillo interno (ring 0.48), y suma
  cross-aspects natal × progresada vía find_synastry_aspects (sólo
  mayores, opacidad × 0.7). z = 6/7 — sobre las capas natal y
  transit.
- modules: progression::ProgressionModule con id "progression", toggle
  "Activar" (sin hotkey por ahora). Registry::with_builtins lo agrega.
  El test pasó de 2 a 3 módulos para ChartKind::Natal.
- shell: build_requests detecta progression.enabled, calcula la edad
  decimal desde StoredBirthData y SystemTime::now() (current_age_years
  helper, aproximación tropical) y arma el request con esa edad.
  El resto del flujo del shell se mantiene — la abstracción funciona.
- canvas: Radii agrega `progression: r * 0.48`, `aspects` shrunk a
  `r * 0.38` para hacer lugar. Helper aspect_endpoints(module_id)
  resuelve el par (r_from, r_to) según natal/transit/progression.
  paint_wheel pinta dots progresados con alpha 0.85 + anillo guía
  sutil que delimita el slot. Glyph overlay pinta planet symbols en
  el ring de progresión con font_size 14 y box 20 (menores que el
  natal para diferenciar visualmente).

Probarlo: en el panel, activar "Progresión secundaria" — verás los
planetas progresados en un anillo interno con su retrogradación
marcada, y líneas de aspectos que cruzan desde el ring de cuerpos
natales hacia el ring progresivo. Combinable con tránsitos: ambos
overlays apilan capas en orden bodies → transits, sin colisiones.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:42:25 +00:00
sergio d4761bf238 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>
2026-05-17 10:31:11 +00:00
sergio 4d14a4495f feat(tahuantinsuyu): fase 5 — overlay de tránsitos (bi-wheel natal × ahora)
Activá el toggle "Tránsitos (ahora)" en el panel (o hotkey [T] sobre
el wheel): la engine computa una segunda NatalChart al instante
SystemTime::now() con el mismo observer y dibuja un anillo externo de
planet glyphs encima del natal, más las cross-aspects entre ambos
charts (sólo mayores). Las líneas cross van del ring de cuerpos
natales al ring externo de tránsitos, con stroke más fino y opacidad
más baja para no taparle el ojo a las aspectos natal-natal.

- engine/bridge.rs: extraídas build_eternal_inputs y
  compute_natal_chart como helpers reutilizables. Nueva
  compute_with_transits(chart, offset, transit_at) que llama
  find_synastry_aspects entre natal y transit (AspectKind::MAJORS).
  Atajo compute_with_transits_at_now usa ESInstant::now(). Las capas
  extra van con module_id = "transit" y LayerKind::Outer /
  LayerKind::Aspects para que el canvas las distinga.
- engine/lib.rs: re-export de compute_with_transits_at_now con el
  mismo fallback al mock cuando feature `eternal-bridge` está off.
- canvas: nueva Radii::transits = 0.82, layout del wheel re-balanceado
  (houses_outer 0.78, houses_inner 0.66, bodies 0.58, aspects 0.50)
  para hacer lugar al anillo externo sin colisiones. paint_wheel:
  detecta layers de transit por module_id, pinta dots + glifos en el
  anillo nuevo + anillos guía sutiles. paint_cross_aspect_line con
  stroke 0.7 entre los dos radios. Glyph overlay para Outer ring con
  alpha 0.9 y font_size más chico que el natal. Hotkey [T] en
  on_key_down toggle LayerKind::Outer.
- modules: NatalModule.controls() agrega toggle show_transits con
  hotkey [T] (default false — no recomputar transits si nadie pidió).
- shell: nuevo show_transits flag. render_current despacha entre
  compute_at_offset y compute_with_transits_at_now según el flag.
  on_panel_event traduce ControlChanged show_transits a flip + redraw.
  on_canvas_event: el toggle de LayerKind::Outer dispara show_transits
  flip + render (no es un visibility toggle puro).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:24:36 +00:00
sergio 360797132e feat(tahuantinsuyu): fase 4 — jog-dial perimetral, hotkeys y panel interactivo
Time scrubbing por drag en el aro exterior del wheel: rota visualmente
mientras dura el drag, al soltar traduce el delta angular a minutos
(1° = 4 min sideral, CW = forward) y emite CanvasEvent::TimeOffsetChanged.
La Shell recomputa con engine::compute_at_offset y el ascendant rotado
queda en la nueva posición. Snap visual a 0° tras commit.

- engine: nueva variante compute_at_offset(chart, minutes) que suma
  segundos al UTC base via add_seconds + Instant::from_utc y corre la
  pipeline normal. compute() es ahora wrapper con offset=0.
- canvas: estado nuevo layer_visibility + drag_jog. Mouse handlers
  registrados desde el paint callback (mismo patrón que splitter/tiled).
  Hotkeys D/H/X/P toggle SignDial/Houses/Aspects/Bodies, R resetea
  offset. FocusHandle + click-to-focus para recibir teclas. Indicador
  ⏱ ±Xd HH:MM en el footer con color highlight cuando el offset != 0.
  paint_wheel + glyph overlays respetan layer_visibility (skip capas
  ocultas).
- modules: NatalModule.controls() ahora expone show_sign_dial /
  show_houses / show_aspects / show_bodies con hotkeys [D/H/X/P], más
  el slider de armónico.
- panel: ControlPanel mantiene toggle_state cache (module_id, key) →
  bool, inicializa desde defaults al cambiar de ChartKind. Click
  invierte el toggle visualmente y emite ControlChanged. Nuevo
  set_toggle(module, key, value) para que la Shell mantenga sync
  cuando el canvas se autotogglea por hotkey.
- shell: nuevo current_chart + current_offset_minutes. render_current()
  delega a compute_at_offset. Suscripción a CanvasEvent traduce
  TimeOffsetChanged → re-render, LayerVisibilityChanged → panel sync.
  Suscripción a PanelEvent::ControlChanged traduce show_* keys a
  set_layer_visible sobre el canvas.

Todos los tests verdes. La fase 5 sumará módulos extra (transit,
progression, synastry, uranian) + extracción de eternal de lo que falte.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:15:09 +00:00
sergio f4944218e2 fix(tahuantinsuyu-engine): leer longitude_rate_rad_per_day directo
`BodyPlacement::is_retrograde` cambió entre versiones de eternal:
en commits viejos es `pub fn is_retrograde(&self) -> bool`, en más
nuevos es `pub is_retrograde: bool`. Cualquiera de las dos formas
rompe la otra al usar `p.is_retrograde()` o `p.is_retrograde`.

Leemos el campo crudo `pub longitude_rate_rad_per_day: f64` (estable
en ambas) y aplicamos `< 0.0` localmente — el bridge queda inmune a
ese refactor upstream.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:50:48 +00:00
sergio 82fa370877 feat(tahuantinsuyu): fase 3 — engine real contra eternal + rueda pintada en GPUI
Bridge a eternal-astrology prendido por default. `engine::compute(chart)`
abre una EphemerisSession VSOP2013 (cacheada vía OnceLock global),
traduce los Stored* del modelo a BirthData/ChartConfig de eternal,
corre NatalChart::compute + find_aspects(modern_western) y devuelve un
RenderModel con cuatro capas: SignDial, Houses, Bodies, Aspects.

- tahuantinsuyu-engine: bridge.rs nuevo con map_house_system,
  map_zodiac (incl. 8 ayanamshas), map_body_set, body_symbol,
  aspect_kind_id. compute_mock se mantiene como fallback sin feature.
  Errores tipados (EngineError::Eternal). Test real verde con datos
  natales de demo.
- tahuantinsuyu-canvas: rewrite con gpui::canvas() + PathBuilder.
  Pinta: sectores zodiacales coloreados por elemento (Fire/Earth/Air/
  Water), anillos de sign-dial/houses/aspects, cusps zodiacales,
  cusps de casas (con énfasis para Asc/MC/Desc/IC), líneas radiales
  hasta el centro para los ejes, líneas de aspectos coloreadas por
  kind con opacidad por orb, dots de cuerpos.
  Glifos unicode (- signos, ☉-♇ planetas, ☊☋⚷⚸ puntos) como divs
  absolutos sobre el canvas. Marcador ᴿ cuando retrógrado.
  Rotación canónica: Asc a las 9, casas crecen contrarreloj.
- shell: ahora llama engine::compute() real y reporta errores por
  stderr sin caer la app.

Datos sintetizados: ascendente, MC, descendente, IC; 12 cusps de
casa según el sistema configurado; placements de los cuerpos del
BodySet con sus longitudes zodiacales, casa y flag retrógrado;
aspectos mayores con opacidad proporcional al orb.

`cargo check` y `cargo test --features eternal-bridge` verdes.
La fase 4 traerá el panel interactivo (jog-dial, toggles, sliders,
atajos teclado).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:43:11 +00:00
sergio bcb92b537e feat(tahuantinsuyu): fase 2 — CRUD UX sobre el tree (menú, modales, form natal)
Right-click sobre el explorador izquierdo abre menú contextual cuyas
opciones dependen del target (raíz, group, contact o chart). Modales
flotantes para crear/renombrar usando yahweh-widget-text-input; un
form más completo de 11 campos para la birth data al crear cartas
natales. Borrar pide confirmación por window.prompt nativo.

- tahuantinsuyu-store: rename_contact, rename_chart, move_group,
  move_contact (los `move_*` para fase posterior de drag-to-nest).
- tahuantinsuyu-tree: estado interno (Menu, Modal enum, ChartForm),
  handlers de ContextMenuRequested, render overlays.
  Soporta seis modales: rename de g/c/h, create group/contact, form
  natal completo con parseo + reporte de errores inline.
  Auto-expande el contact tras crear una carta.
  Nuevo evento TreeEvent::HierarchyChanged tras cada mutación.
- shell: maneja HierarchyChanged sin propagar selección.

`cargo check` y `cargo test` verdes. Fase 3 viene con engine real
contra eternal-astrology + pintado de la rueda.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:13:17 +00:00
sergio c48638fe87 feat(tahuantinsuyu): scaffolding del estudio astrológico (10 crates + ventana 3-panes)
Módulo nuevo `modules/tahuantinsuyu/` con 9 crates reusables + app
`apps/tahuantinsuyu` ejecutable que abre la ventana del explorador y
coordina los widgets:

- tahuantinsuyu-card: Card Brahman + spawn_sidecar (flows
  chart-request/chart-result).
- tahuantinsuyu-model: tipos agnósticos (Group/Contact/Chart,
  StoredBirthData, StoredChartConfig, ChartKind, TreeSelection).
- tahuantinsuyu-store: persistencia SQLite (rusqlite) con migración v1,
  CRUD por entidad y descenso recursivo `charts_under_group`.
- tahuantinsuyu-engine: bridge agnóstico al canvas vía `RenderModel`
  (Layer/Glyph/Geometry). Feature `eternal-bridge` (off por default)
  reservada para enchufar eternal-astrology desde ~/eternal.
- tahuantinsuyu-modules: registry de módulos pluggables (Module trait
  + Control schema) con `NatalModule` placeholder.
- tahuantinsuyu-theme: AstroPalette (elementos / modos / planetas /
  aspectos) con variantes dark + light sobre yahweh-theme.
- tahuantinsuyu-canvas: widget GPUI con CanvasState (Empty / Wheel /
  Thumbnails). Render placeholder hasta cablear la rueda real.
- tahuantinsuyu-tree: explorador izquierdo sobre yahweh-widget-tree,
  prefijos g:/c:/h: para Group/Contact/Chart.
- tahuantinsuyu-panel: control panel inferior que lee Controls de los
  módulos del registry y los pinta.
- apps/tahuantinsuyu: binario `tahuantinsuyu` (launch_app-style) con
  Shell coordinador (tree↔canvas↔panel), DB en $XDG_DATA_HOME.

Workspace Cargo.toml actualizado con los 10 miembros. `cargo check`
verde, tests unitarios verdes (model/store/engine/modules/theme/card).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:06:03 +00:00
sergio e8f97b50cb feat(gioser): luna con textura rica + terminador curvo, auras anchas, overlay nubes
Luna (FS_CHACANA::render_moon):
- Normales esféricas reales: nx=p.x/R, ny=p.y/R, nz=sqrt(1-nx²-ny²).
- Terminador CURVO: dot(normal, sun_dir) donde sun_dir gira en el plano
  X-Z según la fase. Resultado: la frontera luz/sombra es una elipse
  proyectada en pantalla, como en la luna real (no una vertical recta).
- Fase lineal: phi = fract(t/40) * 2π cicla new→first-q→full→last-q→new
  en ~40s.
- Limb darkening realista: pow(nz, 0.45) — bordes más oscuros que el
  centro (el regolito lunar dispersa).
- 6 capas de textura:
    maria_n     (escala 4.5) → mares oscuros (smoothstep 0.42..0.60)
    craters_mid (escala 13)  → cráteres grandes
    craters_small (escala 28) → cráteres chicos
    fine (escala 55) → granularidad del terreno
    micro (escala 110) → polvo
    ring_mid + ring_small → crests via pow(abs(n-0.5)*2, k) → bordes
                            elevados de cráteres
  Albedo final: 0.80 + craters±0.32 + small±0.22 + fine±0.20 + micro±0.10
  + rings (+0.22, +0.16) - maria 0.50, clamp [0.10, 1.15].

Auras elementales (FS_CHACANA::element_cloud):
- sigma_along  0.42 → 0.62 (más reach hacia afuera)
- sigma_perp   0.34 → 0.62 (mucho más ancho perpendicular)
- cloud_center offset 0.22 → 0.28 (más lejos del centro)
- multiplier 0.28 → 0.26 (compensa intensidad por la mayor cobertura)
- Resultado: las nubes elementales se solapan en las esquinas NE/NW/SE/SW
  y mezclan colores. El cuadrante entero respira el color del cardinal.

Overlay clouds (FS_OVERLAY_CLOUDS — nuevo shader):
- Tercer pase tras chacana, fullscreen quad.
- blend = SRC_ALPHA / ONE_MINUS_SRC_ALPHA (compositing normal, no aditivo)
  → las nubes COMPONEN sobre la escena en lugar de sumar luz.
- Dos capas FBM (escalas 0.55 y 1.30) con parallax inverso del mouse
  (-0.05 y -0.09) — se sienten "delante" del cosmos.
- Drift más lento que las nubes del cosmos (0.020 vs 0.055), para que se
  perciban como otra capa atmosférica.
- smoothstep(0.55..0.88, 0.50..0.82) → sólo crestas se vuelven nube;
  mucho del viewport queda transparente.
- Alpha máximo 0.10 — "apenas visible" como pidió el diseño.
- Color mix gris→blanco-azul según densidad local.

Renderer (gioser-canvas-web):
- Nuevo Program overlay_prog con uniforms u_resolution/u_time/u_parallax.
- render() ahora hace 3 pases: cosmos → chacana → overlay clouds.

Workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 04:53:05 +00:00