Commit Graph

158 Commits

Author SHA1 Message Date
sergio cba61e3549 feat(dominium): maqueta isométrica agnóstica — dominium-render-plan
build_plan(World, IsoProjector, ZWeights, PlanConfig) → RenderPlan:
un quad por celda (color = mezcla pesada de las 5 capas, relieve =
Z compuesto) + un quad-marca por Lemming posado sobre el terreno.
Quads ordenados por profundidad de pintor (depth = x+y) + caja
envolvente para centrado. Cero deps gráficas. 10 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:19:40 +00:00
sergio e2833a20c4 feat(dominium): dominium-iso — proyección pseudo-3D isométrica
Proyección calculada en CPU antes de emitir quads 2D (GPUI no maneja
matrices 3D ni mallas).

- ZWeights — pesos del Z compuesto, uno por capa; z_of() calcula el
  relieve como Σ wᵢ·capaᵢ (los 5 sliders del panel).
- IsoProjector — matriz iso fija: x=(x-y)·cos30, y=(x+y)·sin30 − Z·zf.
  cos/sin de 30° vía libm → proyección bit-exacta cross-platform.
- project() + shadow() (Lambert plano: la sombra cae en z=0 desplazada
  por la dirección de luz, larga en proporción a la altura).

6 tests verdes (origen, eje del rombo, Z eleva, Z compuesto lineal,
determinismo, sombra de punto en el suelo). cargo check verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:16:33 +00:00
sergio ed651d6ac5 feat(shuma): shuma-shell-render — draw-plan del Lienzo de Contexto
Layout agnóstico del grafo de intenciones del shell:

- layout(SessionGraph) → CanvasPlan: cada comando %cN es un NodeBox
  ubicado en una columna por su profundidad de dependencia
  (longest-path); cada ref %pN/%cN que consume genera una Edge hacia
  el comando que la produjo. Nodos colapsados se dibujan retraídos.
- paint(plan, canvas) → render directo contra pineal-render: aristas
  al fondo, cajas con borde coloreado por estado (ámbar/verde/rojo).

4 tests verdes (columnas por dependencia, aristas de buffer, comandos
independientes en col 0, paint emite draw calls). cargo check verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:12:54 +00:00
sergio cd3b41a401 feat(dominium): dominium-physics — ciclo del motor (difusión + tick)
- diffuse — ecuación de fluidos discreta sobre los 3 campos dinámicos
  (materia/psique/poder): cada celda intercambia con sus 4 vecinas +
  entropía. Buffer de lectura separado (lee estado viejo). oro y
  degradacion no difunden.
- tick — un paso completo: difusión → transiciones (agente exhausto se
  fuerza a Pelear) → acciones de los agentes → envejecimiento + cosecha
  (la energía del muerto vuelve como materia/fertilidad). run() corre N.

Determinista bit-exacto: aritmética f32 en orden fijo, sin HashMap ni
reducciones paralelas. Test `run_is_deterministic` verifica que mismo
input → mismo estado bit a bit.

7 tests verdes. cargo check --workspace verde. dominium ya CORRE
(core + physics = simulación funcional).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:08:01 +00:00
sergio d1727b1374 feat(dominium): dominium-core — núcleo del simulador de campo medio
- grid — el Sustrato Plano: grilla SoA de 5 capas f32 (materia, psique,
  poder, oro, degradación), indexada y*width+x.
- lemmings — Agentes Vectoriales en SoA: pos_x/y, edad, energia,
  vector_psi [Orden,Miedo,Curiosidad,Corruptibilidad], accion u8.
  spawn / swap_remove / nearest (determinista, empate por menor índice).
- world — World + las 6 acciones atómicas fijas: Mover (gravedad mental
  hacia el vecino más afín al psi), Extraer, Sincronizar, Intercambiar,
  Replicar, Degradar. step_lemming despacha por el byte accion.
- params — SimParams (las constantes que los sliders del panel ajustan).

Cero deps gráficas — sólo serde (regla inviolable de la spec).
11 tests verdes (acciones verificadas: Mover sigue la materia, Extraer
degrada, Replicar engendra, Intercambiar conserva energía, etc.).
cargo check --workspace verde.

Pendiente dominium: physics (difusión/entropía/cinemática), iso,
render-plan, canvas/panel GPUI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:01:42 +00:00
sergio 191e6b06e1 feat(fana): fana-semantic — scoring de intensidad semántica
Desbloqueado por verbo. fana-semantic embebe los átomos y mide su
afinidad a un conjunto de conceptos.

- ConceptSet — embebe el texto de referencia de cada concepto como su
  vector ancla (vía cualquier verbo Provider).
- SemanticScorer — embebe el contenido de un NarrativeAtom y llena
  atom.semantic_vectors con la similitud coseno concepto→intensidad.
  Limpia el scoring previo en cada pasada.

Agnóstico del backend (verbo_core::Provider). 3 tests verdes con
verbo-mock — incluye: texto idéntico al ancla puntúa coseno ≈ 1.
cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:54:42 +00:00
sergio e0ad7315be feat(verbo): verbo-mock — backend de embeddings determinista
Backend sin modelo real: FNV-1a del texto siembra un LCG que genera el
vector. Mismo texto → mismo vector siempre; textos distintos → vectores
distintos. Dimensión configurable (default 384d, típica de modelos
ligeros).

Desbloquea desarrollar y testear los consumidores de verbo
(fana-semantic, badu, chasqui) sin descargar modelos ONNX ni pegarle a
Cohere. Los backends reales (cohere/bge/fastembed) son swaps de config.

4 tests verdes (determinismo, distinción, dimensión, batch).
cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:53:43 +00:00
sergio c2d6c15138 feat(verbo): verbo-core — contrato model-agnostic de embeddings
Primer crate de verbo (provider de embeddings compartido; desbloquea
fana-semantic, badu y la búsqueda de chasqui).

- ModelId — identidad de modelo (nombre + dimensión). Vectores de
  distinto ModelId no son comparables.
- EmbeddingVector — vector + su ModelId; new() valida la dimensión,
  cosine() rechaza comparar modelos distintos (error tipado, no
  sinsentido silencioso), norm() euclidiana.
- EmbedError — ModelMismatch / BadDimension / Backend.
- trait Provider — model_id + embed + embed_batch (default secuencial).
  Lo cumplen los backends concretos (cohere / bge / fastembed).

5 tests verdes (cosine idéntico/ortogonal/cross-model/zero, validación
de dimensión). cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:52:43 +00:00
sergio b9a6cd33fd feat(shuma): macros del shell — barra [RUN]
shuma-intent: + módulo macros.

- Macro — secuencia de intenciones nombrada, con tecla física opcional
  (F1-F3...). Builder bind()/step(). Serializable: compartible entre
  sesiones y usuarios (requisito de la spec).
- MacroBook — colección con lookup por tecla y por nombre; insert
  reemplaza por nombre.

Completa el núcleo agnóstico del shell shuma: prompt de intenciones +
grafo de contexto + macros. 11 tests verdes. cargo check --workspace
verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:47:57 +00:00
sergio 1da4ee11d7 feat(shuma): núcleo del shell — parser de intenciones + grafo de contexto
shuma-intent: el corazón agnóstico del shell shuma.

- parse — Intention: una línea del prompt parseada en etapas separadas
  por pipe. Ref (%cN comando / %pN buffer) + Stage (Exec | Inject).
  Parsea el ejemplo de la spec: `ssh nodo 'cat data.json' | %p1 | sort`.
- graph — SessionGraph: el grafo de contexto de la sesión. record()
  registra una intención (%cN), complete() le asigna buffer de salida
  (%pN) + estado, resolve() resuelve referencias, dangling_refs()
  valida una intención antes de ejecutar (la validación previa del
  prompt), collapse_succeeded() retrae nodos OK (quietud visual).

Todo puro y serializable (sesiones exportables). El front-end GPUI
(zonas RUN/SENS + lienzo central) lo rehidrata; la ejecución la hace
sandokan. 8 tests verdes. cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:47:11 +00:00
sergio 6884b3f8cb feat(fana): fana-store — persistencia del grafo narrativo (sled)
- fana-core: NarrativeAtom + CoherenceState ahora Serialize/Deserialize
  (serde con feature rc para el Arc<String>; uuid con feature serde).
- fana-graph: + atoms() iterator + from_atoms() constructor.
- fana-store: GraphStore sobre sled. put/get/remove_atom por Uuid,
  serialización bincode. save_graph persiste átomo por átomo;
  load_graph reconstruye el grafo (la adjacency se re-cablea desde las
  dependencies de cada átomo).

7 tests verdes (roundtrip put/get/remove + save/load_graph preserva
estructura). cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:43:01 +00:00
sergio 353e0bbb43 feat(fana): C1 — núcleo del writer DAG editor (core + graph)
Primer paso de fana (prioridad alta entre las apps Fase C).

- fana-core — NarrativeAtom: id + content_hash SHA-256 + content
  Arc<String> (structural sharing: ramificar es O(1)) + semantic_vectors
  + dependencies + branch_id + CoherenceState (Valid/InConflict/
  PendingEvaluation). Invariante hash↔content verificable; set_content
  re-hashea y marca PendingEvaluation.
- fana-graph — NarrativeGraph: DAG de átomos + adjacency
  dependencia→dependientes. propagate_mutation: BFS que marca
  PendingEvaluation en cascada a todo descendiente (la "onda de choque
  lógica" de la spec), agnóstico de UI — devuelve los ids afectados.
  topological_order con detección de ciclo.

10 tests verdes. cargo check --workspace verde.
Pendiente fana: semantic (cliente verbo), store (sled), llm, render-plan,
editor-gpui.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:41:17 +00:00
sergio dc8554d123 feat(pineal): cierra stub mesh — viz de grafos (núcleo)
Fase F: sexto stub de pineal cerrado (6/6).

mesh resultó ser un módulo de viz de grafos, no un triangle-mesh.
Núcleo implementado:
- buffers — NodeBuffer (stride 3: x,y,radius) + EdgeBuffer (stride 2),
  Vec planos contiguos, raw() para subir a GPU.
- spatial_hash — uniform grid; rebuild + query (nodo bajo un punto,
  revisa celda + 8 vecinas).
- force — layout force-directed Fruchterman-Reingold naïve O(n²):
  repulsión todo-par + atracción por arista + cooling. Jitter
  determinista para nodos coincidentes.
- tree — layout de árbol por ancho de subárbol (post-order, padres
  centrados sobre hijos), soporta bosque, ciclos sin colgar.
- camera — pan/zoom con zoom anclado al cursor (anchor-preserving).

13 tests verdes. cargo check --workspace verde.

Pendiente (follow-up): hierarchical (Sugiyama) + Barnes-Hut para
escalar el force-directed a grafos masivos.

Pineal: 6/6 stubs cerrados.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:09:22 +00:00
sergio 0042fe3f1f feat(pineal): cierra stub flow — diagrama Sankey
Fase F: quinto stub de pineal cerrado.

- layout — pipeline Sankey: columnas por longest-path en el DAG
  (back-edges detectadas por DFS y descartadas para romper ciclos),
  valor de nodo = max(entrante, saliente), apilado vertical por columna
  escalado a la altura, una pasada de barycenter para reducir cruces,
  anclas de cada banda en los bordes de sus nodos.
- ribbon — teselado de bandas como triangle-strip con curva S
  (x lineal, y por smoothstep → tangentes horizontales). paint_ribbon
  + paint_sankey (ribbons al fondo, nodos encima).

Painters agnósticos (trait Canvas). 6 tests verdes (columnas, ciclos
sin loop infinito, proporcionalidad, conteo de draw calls).

Pineal: 5/6 stubs cerrados. Resta mesh (viz de grafos: force-directed
+ Sugiyama + tree layout — módulo, no stub).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:06:58 +00:00
sergio 590572b5bb feat(pineal): cierra stub treemap — squarified
Fase F: cuarto stub de pineal cerrado.

- squarify — algoritmo de Bruls, Huizing & van Wijk (2000): asigna a
  cada peso un rect de área proporcional minimizando el peor aspect
  ratio (rects lo más cuadrados posible). Pre-escala pesos al área del
  rect; ordena descendente; tiende filas sobre el lado corto cerrándolas
  cuando agregar un item empeora el ratio. Pesos <=0 → rect vacío.
- paint — painter agnóstico: tiles → fill_rect con gap configurable.

7 tests verdes (proporcionalidad, bounds, edge cases). cargo check
--workspace verde.

Pineal: 4/6 stubs cerrados (export, heatmap, polar, treemap).
Restan flow (sankey) y mesh (graph layout: force-directed/Sugiyama) —
ambos requieren algoritmos de layout sustantivos, foco dedicado.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:16:31 +00:00
sergio 370a593ad8 feat(pineal): cierra stub polar — pie/donut + radar
Fase F: tercer stub de pineal cerrado.

- pie — paint_pie: pie y donut (inner_radius > 0). Porciones desde las
  12 en punto, horario; valores negativos → 0. Cada cuña se tesela en
  un triangle strip [in,out,in,out,…] con segmentos de arco escalados
  al ángulo.
- radar — paint_radar: M ejes equiespaciados, valores proyectados a
  distancia proporcional; relleno (fan) + contorno (polilínea cerrada).

Painters 100% agnósticos (trait Canvas). 5 tests verdes.
cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:14:37 +00:00
sergio 4528e08e04 feat(pineal): cierra stub heatmap — matrix + viridis + encoder + paint
Fase F: segundo stub de pineal cerrado.

- matrix — HeatmapMatrix densa width×height de f32, con revision para
  invalidación de textura; get/set/min_max/replace_data.
- palette — Ramp::{Viridis, Grayscale}; Viridis por interpolación
  lineal de 5 control points perceptualmente uniformes.
- encoder — encode_argb: normaliza por min/max + rampa + pack 0xAARRGGBB
  para subir como textura (camino de matrices grandes).
- paint — painter agnóstico: un fill_rect por celda contra un Canvas
  (camino de matrices chicas + export SVG).

12 tests verdes. cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:13:10 +00:00
sergio b75e22fa91 feat(pineal): cierra stub export — PlanRecorder + exporter SVG
Fase F: primer stub de pineal cerrado.

pineal-render:
- PlanRecorder — un Canvas que graba cada llamada como RenderCmd en un
  RenderPlan. Es el puente painter→backend-diferido y la infraestructura
  de testing (snapshot de planes).

pineal-export:
- svg::to_svg(plan, w, h) — RenderPlan → documento SVG completo.
  Cubre FillRect/StrokeRect/StrokeLine/StrokePolyline/DrawText +
  FillTriangleStrip (strip→polígonos con color promedio). XML-escape
  en texto. v1: clips ignorados (documentado).
- pdf queda como placeholder documentado.

Tests: 1 recorder + 4 svg (well-formed, primitivas, xml-escape,
triangle-strip→polygons). cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:11:03 +00:00
sergio b83d40a833 refactor(naming): A1 — ente→arje, vista→revista, pluma→fana
Rename batch de la Fase A del PLAN_MACRO:
- 25 crates ente-* → arje-* (protocol/init/runtime/compat). El linaje
  arje (init Linux) queda con prefijo coherente.
- vista → revista (revista-core + revista-web).
- pluma → fana (fana-md + fana-md-reader-web). fana absorbe el linaje
  markdown de pluma; será el writer DAG editor (prioridad alta).

Cambios:
- git mv de 29 crate dirs + 2 SDDs
- package/lib/bin names + path refs + imports .rs reescritos
- workspace Cargo.toml + comentarios de sección
- SDDs de init/runtime/compat/protocol actualizados a arje-
- SDD de revista + SDD de fana (reescrito: writer DAG editor)
- docs/STATUS.md, ROADMAP.md, PLAN_MACRO.md, arje-boot.md,
  arje-replace-systemd.md actualizados
- docs/changelog/akasha.md → chasqui.md

scripts/rename-fase-a.py idempotente (--dry-run soportado).
cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:10:14 +00:00
sergio e570c6ca6f docs: fix factual errors en SDDs y STATUS/ROADMAP
Errores detectados al auditar afirmaciones técnicas contra el código:

1. minga-vfs: NO está relacionado con Mónadas (esas son de akasha).
   Es FUSE que proyecta el índice de minga (git semántico) como
   filesystem, resolviendo paths virtuales a blobs por hash.

2. protocol/SDD.md: Card tiene 19 campos, no 6. Añadido bloque con
   anatomía completa del struct.

3. STATUS.md: LOC por capa corregidos contra wc -l real
   - protocol: 6,260 → 7,278
   - init:     ~3,600 → 4,301
   - compat:   ~5,000 → 3,435 (estaba sobrestimado)

4. pineal: 6 stubs (<30 LOC c/u), no 5. Export (23 LOC) también es
   stub funcional. LOC reales por sub-crate documentados.

5. init/SDD.md: ente-soma es wrapper de 44 LOC, no ~30.

6. akasha/SDD.md: fastembed está detrás de feature `embeddings`,
   ort es transitivo. Sin feature, akasha-nous-real es stub mínimo.

7. vista/barra: LOC ajustados (vista-core 177, barra-core 108).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:03:05 +00:00
sergio 550c98f275 refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/:
- core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/
- shared/ (3 crates) se redistribuye en protocol/ e init/
- lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/

Renames de proyectos:
- shipote → shuma (runtime de sandboxes)
- nouser → akasha (explorador de Mónadas)
- yahweh → nahual (motor GPUI, antes ui_engine/)
- lapaloma → pineal (data-viz agnóstica)

Fraccionamiento UI → core agnóstico:
- vista-core (DeckState + snap, 175 LOC, 5 tests verdes)
- barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes)
- vista-web y barra-web ahora son thin DOM bindings

Documentación nueva:
- 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat
  + 10 módulos + apps/
- docs/STATUS.md con cifras reales por proyecto
- docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas)
- CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets)

Automatización:
- scripts/reorg.py — script idempotente que: git mv directorios, renombra
  package names, recomputa path = refs, reescribe imports rust, actualiza
  workspace Cargo.toml. Soporta --dry-run.
- scripts/split-changelog.py — particiona CHANGELOG por componente.

Validación:
- cargo check --workspace pasa (124 crates + 2 nuevos cores).
- 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 14:48:34 +00:00
sergio 86fb6ae20b feat(cosmobiologia-render): compose_wheel rico con palette + dial 3D + spread + coord labels
El render agnóstico ya no es un esqueleto — porta al WASM la mayoría
de los detalles visuales que tenía solo el canvas gpui nativo:

- palette.rs: Palette dark/light replicando AstroPalette del theme
  nativo, pero en Rgba (no Hsla de gpui). Métodos planet/aspect/sign
  para resolver color por id simbólico, + house_ring con hue-shift.
- CompositionOpts extendido: palette, dial_3d, draw_ascensional_cross,
  show_coord_labels, show_minor_aspects. Defaults razonables.
- compose_wheel ahora dibuja: background panel, dial 3D bevel (4
  strokes concéntricos con alpha decreciente), subdivisiones cada 10°
  con sign boundaries reforzados, signos con color elemental, casas
  topocéntricas + geocéntricas en sus rings canónicos, cuerpos con
  spread anti-solapamiento + clusters + disco coloreado por planeta,
  coord labels "DD°MM'" en natal, aspectos con width inversa al
  orbe + filtrado opcional de minors, cruz ascensional dashed +
  pills ASC/MC/DESC/IC.
- cosmobiologia-web: nuevo render_model_to_svg_themed(dark: bool)
  para que el cliente JS elija palette según preferencia del UA.

Tests del módulo math siguen verdes (10/10). Smoke test del server:
/api/sky.svg ahora emite 22 circles, 77 lines, 52 texts con paleta
real (vs ~6 circles, 24 lines, 36 texts del esqueleto previo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 01:41:36 +00:00
sergio 4619ba3a2b feat(cosmobiologia): crate WASM + fallback inteligente + DEPLOY.md (fase 3b)
Cierra el requerimiento del módulo web. El cliente puede correr en
modo WASM (render local, scrubbing instantáneo, sin round-trip) o
caer al SSR (server compone el SVG) si el bundle WASM no está
desplegado. Switch automático sin configuración.

cosmobiologia-web (crate nuevo, cdylib + rlib):
- `lib.rs` con un único export wasm-bindgen
  `render_model_to_svg(json, size, rot_offset_deg) -> String` que
  deserializa un `RenderModel`, llama `compose_wheel` +
  `draw_commands_to_svg` de cosmobiologia-render, y devuelve el
  SVG inline listo para `wheel.innerHTML = svg`.
- Cargo.toml con `wasm-bindgen` + `getrandom` con feature
  `wasm_js` solo bajo `target_arch = "wasm32"` (en nativo no se
  arrastran).
- `.cargo/config.toml` con `--cfg getrandom_backend="wasm_js"`
  para que la transitividad
  `uuid → cosmobiologia-model → cosmobiologia-render` compile a
  wasm32-unknown-unknown.
- `cargo check -p cosmobiologia-web` pasa en nativo (valida la
  signature). Build WASM real lo dispara el usuario con
  `wasm-pack build --target web --out-dir ../../../apps/
  cosmobiologia-server/static/wasm` — comando documentado en
  DEPLOY.md y en doc del crate.

cosmobiologia-server — soporte cliente WASM:
- Nuevo flag `--static-wasm <dir>` (default = static/wasm relativo
  al cwd). Si el directorio existe, los archivos WASM se sirven
  en `/static/wasm/*`. Si no existe, devuelve 404 y el cliente
  cae al SSR.
- ServeDir de `tower-http` para fileserver simple.

index.html:
- Nueva función `tryLoadWasm()` que hace `import dinámico` del
  módulo WASM al boot. Si carga OK, `wasm` global queda set; si
  falla (archivo no existe o error de WASM), se loguea info y
  sigue.
- `refreshSelected()` ahora hace fetch del RenderModel JSON
  (`/api/sky` o `/api/charts/:id/render`); si hay WASM, llama
  `wasm.render_model_to_svg(json)` localmente; si no hay WASM o
  el render WASM falla, hace fetch del SVG SSR como fallback.
- Info row muestra "WASM" o "SSR" según el modo activo —
  visualmente claro qué pipeline está corriendo.

cosmobiologia-server/DEPLOY.md (nuevo):
- Build del binario + build del WASM (con wasm-pack).
- systemd service template (sandboxing básico: ProtectSystem
  strict, ProtectHome, PrivateTmp, NoNewPrivileges).
- Caddyfile y nginx para reverse proxy con TLS.
- DNS: A records para cosmobiologia.gioser.net + api.*.
- CORS: warnings sobre permissive vs producción multi-usuario.
- Separación demo público (DB vacía en VPS) vs desktop personal
  (DB compartida en `~/.local/share/cosmobiologia/`).
- Backup con SQLite `.backup`.
- Smoke test post-deploy con curl.
- Tabla de referencia de TODOS los endpoints.

Tests: 10 verdes (cosmobiologia-render::math). El cliente WASM
no agrega tests propios — la lógica testeable vive en render.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 01:25:48 +00:00
sergio eac8c58974 feat(cosmobiologia): cliente web demo SSR + DrawCommand agnóstico (fase 3a)
Fase 3a — render web operativo sin WASM. Demo funcional inmediata
con server-side rendering del SVG; el cliente WASM puro se hace en
fase 3b cuando wasm-pack / wasm-bindgen-cli esté instalado.

cosmobiologia-render — nuevo módulo `draw`:
- `Rgba { r, g, b, a }` color agnóstico (no Hsla, no hex CSS).
- `DrawCommand` enum tagged-serde: `Circle`, `Line`, `Text`. Listo
  para WASM o nativo — solo primitivas.
- `CompositionOpts { size, rot_offset_deg, include_bodies }`.
- `compose_wheel(model, opts) -> Vec<DrawCommand>` primera versión:
  anillo zodiacal (A+B), 12 cusps cada 30°, glyphs de signos,
  corona de casas (C+D), cusps de casas (Asc/IC/Desc/MC con peso
  doble), house numbers, anillo de aspectos (E), líneas de
  aspectos coloreadas por kind, glyphs de cuerpos natales con
  disco halo.
- `draw_commands_to_svg(cmds, size) -> String` serializa la lista
  a SVG inline. SVG-escape, `text-anchor` configurable, `dominant
  -baseline=central` para centrar verticalmente.

Pendiente en `compose_wheel` (extender en commits siguientes,
copiando lo del canvas gpui): spread anti-solapamiento, clusters
compartidos, coord labels, dial 3D bevel, vignette, themes
PrintColor/PrintBW. Por ahora es un MVP suficiente para verificar
end-to-end y para que el usuario tenga algo visible YA.

cosmobiologia-server:
- Nuevos endpoints:
  * `GET /`                     → HTML del cliente (single-page)
  * `GET /api/sky.svg`          → SVG agnóstico del "cielo ahora"
  * `GET /api/charts/:id/wheel.svg` → SVG agnóstico de carta con
                                     overlays via query (offset,
                                     transit, prog, sa, pd)
- Página HTML embebida (`include_str!` de `static/index.html`):
  * Sidebar con tree (groups → contacts → charts), click selecciona
  * "⏱ Cielo ahora" siempre disponible como botón rápido
  * Toolbar con input offset minutos + checkbox tránsito + botón
    refresh + botón download SVG
  * Botones "Nuevo grupo / Nuevo contacto" con prompt + POST
  * Wheel renderizado en SVG inline, info row con título/asc/mc/ms

Smoke test:
  cargo run -p cosmobiologia-server -- --port 18787
  curl /                       → HTML (página completa)
  curl /api/sky.svg            → 12 KB SVG con 17 circles +
                                 51 lines + 36 texts
  curl /api/tree               → árbol JSON
  curl POST /api/groups        → crea grupo
  Browser http://127.0.0.1:8787 → wheel visible

Próximo (fase 3b): cliente cdylib WASM `cosmobiologia-web` que
reemplace el SSR — recibe RenderModel JSON, llama compose_wheel +
draw_commands_to_svg en WASM, monta SVG via DOM. Trade-off: el
SSR de hoy es 12 KB transferidos por click (sólido); WASM
descarga ~150 KB una sola vez y luego compone localmente
(scrubbing instantáneo, sin round-trip al server).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 01:08:44 +00:00
sergio 06a1ca11ce chore: rename tahuantinsuyu → cosmobiologia
Rename clean del proyecto astrológico antes de empezar el módulo
web (fase 2 = server axum, fase 3 = cliente WASM). Hacerlo ahora
ahorra refactor de URLs, package.json, paths de assets HTML y
deploy configs que aparecerían con el nombre en cuanto exista el
server.

Mecánica:
- `git mv` de los 10 crates de módulo + 2 apps:
  * `crates/modules/tahuantinsuyu/` → `cosmobiologia/`
  * `crates/modules/tahuantinsuyu/tahuantinsuyu-*` →
    `cosmobiologia/cosmobiologia-*`
  * `crates/apps/tahuantinsuyu` y `tahuantinsuyu-cli` análogos.
- Sed sobre todos los `.rs` y `.toml`: `tahuantinsuyu` →
  `cosmobiologia` (cubre crate names, deps paths, use
  statements, ProjectDirs literals, binary names).
- Workspace `Cargo.toml`: members con paths nuevos.
- Memoria del proyecto (`~/.claude/.../memory/project_*.md`)
  actualizada.

Cero leftovers: `grep -rn tahuantinsuyu --include="*.rs"
--include="*.toml" crates/` devuelve vacío.

DB & XDG: clean slate. La nueva app arranca con DB vacía en
`$XDG_DATA_HOME/cosmobiologia/charts.db`. Si tenías cartas
guardadas, viven todavía en `~/.local/share/tahuantinsuyu/` —
las podés migrar manualmente con un `cp`.

IDs UI inalterados: el prefijo `tts-` de gpui ElementIds queda
igual (cosmético, no afecta funcionalidad). Cambiarlo a `cb-`
ahora sería 3-4 líneas más de sed pero ningún beneficio
operativo.

Tests: 20 verdes (10 shell + 10 render math). Compila full:
`cargo check -p cosmobiologia` OK.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:45:48 +00:00
sergio 9084cf4b79 refactor(tahuantinsuyu): extrae tahuantinsuyu-render — preparación para WASM
Fase 1 de "módulo web": extracción del modelo y la matemática
agnóstica de surface a un crate separado, sin dependencia de
gpui ni de eternal. Es la base sobre la que el cliente WASM y
el canvas nativo van a converger.

Crate nuevo `tahuantinsuyu-render`:
- Tipos del RenderModel migrados desde `tahuantinsuyu-engine`:
  `RenderModel`, `Layer`, `LayerKind`, `Geometry`, `LineSeg`,
  `PointMark`, `Glyph`, `OverlayMeta`, `UranianGroup`,
  `AspectSummary`, `OUTER_RING_MODULES`. El engine los
  reexporta — ningún call site del shell/canvas/modules/tree/
  panel cambia su `use`.
- Módulo `math` con la geometría canónica del wheel migrada
  desde `tahuantinsuyu-canvas`:
  * `Radii` con los aros A/B/C/D/E + helpers `body_ring` y
    `aspect_endpoints`
  * `polar_to_screen` (Asc a las 9 del reloj)
  * `spread_angles` (anti-solapamiento con damping + clamp por
    glyph)
  * `find_clusters` (con wrap-around)
  * `format_coord_compact` ("DD°MM'{signo}")
- 10 tests del math (5 spread + 4 coord + 1 polar) viajaron con
  las implementaciones. El canvas se queda solo con los tests
  de UI.

Por qué un crate aparte:
- `tahuantinsuyu-engine` arrastra `eternal-sky` (VSOP2013 +
  I/O de tablas) que NO compila a WASM sin empaquetar 30+ MB
  de efemérides. Los tipos del modelo son serde puro y sí
  compilan a WASM — extraerlos libera al cliente web futuro
  de la dependencia transitiva.
- Cuando llegue la fase 2 (`tahuantinsuyu-server` axum) y la
  fase 3 (`tahuantinsuyu-web` cdylib WASM), ambos consumen
  `tahuantinsuyu-render` con la misma fuente de verdad sobre
  el layout, evitando duplicar la lógica entre desktop y web.

Pendiente: `tahuantinsuyu-model` arrastra `uuid → getrandom`
que falla a WASM sin `wasm_js` feature flag. Lo resuelvo en la
fase del cliente WASM (necesita su propio Cargo.toml con la
config getrandom + .cargo/config con RUSTFLAGS).

Tests: 20 verdes (10 shell + 10 render math). Compilación
nativa OK; canvas sin cambios visuales (mismo código,
diferente origen).
2026-05-19 00:33:39 +00:00
sergio 8e95c884ed feat(tahuantinsuyu): "Guardar como…" en Tránsito y Progresada
Extiende el patrón de F4 a dos módulos más:

- **Tránsito**: nuevo `Control::Action "💾 Guardar tránsito como
  carta libre"`. Captura el momento actual (UTC `now()`) anclado
  a las coordenadas del natal. Label `{natal} transito · YYYY-MM-DD
  HH:MM UTC`. Útil para "qué pasaba en el cielo de Pedro ahora
  mismo, pegado como carta".

- **Progresada secundaria**: análogo, sufijo `prog-{N}a`. El
  birth_data del Chart resultante es REAL (natal_instant + N días
  simbólicos × 86400 s), así que cuando se computa de nuevo como
  natal produce las posiciones progresadas correctas. El usuario
  edita el slider, click → la carta queda guardada como libre
  para después persistir.

Backend:
- Dos funciones nuevas en `tahuantinsuyu-engine`:
  `compute_transit_chart(chart)` y
  `compute_progression_chart(chart, age)`. Reusan
  `parse_iso8601_components` (introducido en el commit del PR).
- En el shell: nuevo helper `insert_derived_free_chart(source,
  birth, label)` que el callsite de PR ahora reusa (refactor
  pequeño).

Sobre solar_arc y primary_directions:
- NO se agrega el botón. SA y PD son transformaciones matemáticas
  puras — un Chart natal computado en el "momento dirigido" daría
  posiciones distintas a las dirigidas (porque SA rota uniforme y
  PD es función de RA, no de longitud eclíptica). Para guardarlas
  haría falta extender `Chart` con un kind
  `Derived { source, transform, params }` que el engine sepa
  rehidratar al render. TODO en otra fase.

Tests: 10 verdes (sin cambios en los paths probados).
2026-05-19 00:13:31 +00:00
sergio 9db0591f28 feat(tahuantinsuyu): "Guardar como…" en módulo Retorno planetario (F4)
Cierra la fase B con el botón pedido por el usuario: tener una
carta natal abierta, activar el módulo Retorno planetario con
edad N + cuerpo (ej. Sol, 34 años), y al click guardar la carta
resultante con sufijo automático `rs-34` en el mismo contacto.

Infraestructura nueva (extensible a otros overlays):
- `Control::Action { key, label }` en tahuantinsuyu-modules —
  un botón sin estado que el panel pinta como pill clickeable.
- `PanelEvent::Action { module_id, key }` que el panel emite
  al click y el shell despacha.
- `render_action` en tahuantinsuyu-panel: pill con bg_button
  + hover + border. Wrap en Div plano para tipo coherente.

Backend (eternal-bridge):
- Nueva función pública `compute_planetary_return_chart(chart,
  body, target_age_years, shift_days) -> (StoredBirthData,
  instant_label)` en `tahuantinsuyu-engine`. Reusa el cómputo
  ya existente del overlay: `next_return` + parser ISO-8601
  para extraer year/mm/dd/hh:mm:ss del instant del retorno.
  Hereda lat/lon/alt/TZ del natal — convención clásica del
  Solar return en la ciudad de nacimiento.

Flujo en el shell:
- Handler `on_panel_action` despacha por `(module_id, key)`. Hoy
  solo `planetary_return.save_as_free` está cableado; otros
  módulos overlay (progression, solar_arc, primary_directions,
  transit) son extensión natural — TODO.
- `save_planetary_return_as_free`:
  1) lee config (body, age, shift_days) del module_configs
  2) llama `compute_planetary_return_chart`
  3) construye un `Chart` clonando el natal con birth_data
     nuevo + label `{contacto} rs-34 · 2024-08-12 14:23 UTC`
     (sufijo según cuerpo: `rs` para Sol, `lunar` para Luna,
     nombre directo para los demás)
  4) inserta como FreeChart con id `free-{N}` y la
     selecciona para que el usuario la vea
- El usuario después puede usar el menú contextual de la
  free chart para "Guardar como…" → modal F3 → persiste
  bajo el contacto que elija (típicamente el del natal).

UX completa:
1. Tener natal abierta
2. Panel: módulo "Retorno planetario" → Activar + elegir
   cuerpo + slider edad
3. Click "💾 Guardar retorno como carta libre"
4. La nueva carta aparece en "Cartas libres" seleccionada
5. Click derecho → "Guardar como…" → elegir contacto +
   confirmar nombre

10 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:01:49 +00:00
sergio dd836522ab feat(tahuantinsuyu): editor inline para cartas libres (F2)
Las cartas libres ahora se pueden editar en su totalidad (fecha,
hora, lugar, lat/lon/alt, TZ, label) desde el menú contextual.
La edición es **in-memory** — la carta se queda como libre tras
el cambio; para persistirla hay que usar "Guardar como…".

Tree:
- Nuevo `Modal::EditFreeChart { source_id, form, error }`
  paralelo al `Modal::EditChart` existente. Reusa la misma
  `ChartForm` (11 TextInputs: name + place + date + time + TZ
  + lat/lon/alt) y la misma función `render_chart_form` para
  pintarlo. El title cambia a "Editar carta libre".
- `open_edit_free_chart_modal(source_id, w, cx)`: lee el entry
  de `self.free_charts` (que ahora trae `birth_data` además
  de id+label), pre-puebla el form, y abre el modal.
- Submit: `build_chart_from_form` parsea + valida; al éxito
  emite nuevo evento `TreeEvent::FreeChartEditConfirmed
  { source_id, birth_data, label }`. Al error, conserva el
  modal con la pill destructiva.
- City picker funciona como antes — el branch de
  `apply_city_preset` se extendió para que reconozca
  `Modal::EditFreeChart` además de Create/Edit.

Modelo:
- `FreeChartEntry` ahora incluye `birth_data: StoredBirthData`
  además de id+label. El shell se lo pasa al setter; el tree
  lo usa para pre-poblar el form sin tener que pedirlo al
  shell.

Shell:
- `push_free_charts_to_tree` clona `birth_data` en cada entry.
- Handler `FreeChartEditConfirmed`: actualiza
  `free_charts[id]` con los nuevos datos + label, re-publica
  al tree, y si la carta editada era la activa, re-renderea
  el wheel.

Menú contextual de "Cartas libres" / `<carta libre>` ahora:
- Editar datos…
- Guardar como…
- Borrar  (no se ofrece sobre sky-now)

10 tests verdes (sin afectar lo testeado).

Próximo y último: F4 — botón "Guardar como…" en cada módulo
overlay (RS, prog, sa, gr) que captura la carta derivada con
un sufijo automático en el contacto original.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:51:51 +00:00
sergio a83b0396ce feat(tahuantinsuyu): modal "Guardar como…" real para cartas libres (F3)
Reemplaza el `save_free_chart_quick` MVP de la fase A por un
modal completo que el usuario controla:

Tree:
- Nuevo `Modal::SaveFreeChart { source_id, name, new_contact_name,
  selected_contact, all_contacts, error }`.
- `open_save_free_chart_modal` abre el modal pre-poblando `name`
  con el label de la carta libre y `selected_contact` con el
  primer contacto existente (o `None` = nuevo contacto si no
  hay ninguno).
- `gather_all_contacts` recorre la jerarquía recursivamente
  devolviendo `(ContactId, "Grupo / Subgrupo / Contacto")` —
  el usuario ve la ruta completa, no solo el nombre.
- `render_save_free_chart` pinta:
  * Input "Nombre" pre-cargado
  * Lista de contactos como botones radio (● / ○) + opción
    "Nuevo contacto…" al final
  * Si "Nuevo contacto…" seleccionado, aparece input
    "Nombre del contacto nuevo"
  * Botones Cancelar / Guardar
- `set_save_modal_contact` alterna el radio sin recrear inputs.
- Validaciones: nombre de carta no vacío; si `selected_contact`
  es `None`, exigir `new_contact_name` no vacío. Errores se
  muestran en una pill destructiva dentro del modal.
- Submit emite nuevo evento `TreeEvent::FreeChartSaveConfirmed
  { source_id, chart_name, contact, new_contact_name }`.

Shell:
- `persist_free_chart` resuelve el contacto destino (existente
  o crea uno nuevo), llama `store.create_chart`, y al éxito
  remueve la carta libre del mapa (salvo `sky-now`, que es
  persistente). Si la carta libre estaba seleccionada, vuelve
  al Cielo. Refresca opciones del picker para que el dropdown
  ChartPicker incluya la carta recién guardada.
- El handler `SaveFreeChartRequested` queda como hook vacío;
  el menú del tree abre el modal directamente con `window`.

10 tests verdes (no se afectaron los paths probados).

Próximo: F2 (editor inline de fecha/lugar/hora de la carta
libre) y F4 (botón "Guardar como…" en cada módulo overlay
con sufijo automático).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:36:30 +00:00
sergio 72da2934e8 feat(tahuantinsuyu): "Cartas libres" como sección + guardar a contacto (fase A)
Estructura de cartas no-persistidas con CRUD básico en la UI.

Modelo:
- `FreeChartId(String)` con sentinela `sky_now()` reservado para la
  carta del cielo. Otros ids se generan al vuelo como `free-N`.
- `TreeSelection::FreeChart(FreeChartId)` y `FreeChartsRoot`
  reemplazan al variante puntual `PresentSky` (que era un caso
  especial paralelo).

Tree:
- Sección **"🜨 Cartas libres"** branch fijo al FONDO del tree
  (al contrario de "◇ General" que va arriba). Contiene "Cielo
  ahora" como primera leaf + cualquier carta libre creada.
  Expandida por default.
- Menu contextual:
  * sobre la sección: "Nueva carta libre" → `NewFreeChartRequested`
  * sobre una carta libre: "Guardar como…" + "Borrar" (`sky-now`
    no admite borrar)
- Setter `set_free_charts(Vec<FreeChartEntry>)` actualizado por
  el shell tras cada mutación.

Shell:
- Nuevo state: `free_charts: HashMap<FreeChartId, Chart>` +
  `next_free_id: u32`.
- `ensure_sky_now` inserta/refresca "Cielo ahora" contra el
  reloj actual. Al boot se llama y la carta queda seleccionada.
- `push_free_charts_to_tree` publica la lista al tree
  (sky-now primero, después los `free-N` ordenados).
- Handlers de los 3 nuevos eventos:
  * `NewFreeChartRequested` → crea entry, selecciona
  * `SaveFreeChartRequested(id)` → `save_free_chart_quick`
    (MVP: crea contacto nuevo con el label de la carta + carta
    bajo él; la fase B reemplaza por modal con dropdown)
  * `DeleteFreeChartRequested(id)` → quita de free_charts;
    si era la activa, vuelve al Cielo

10 tests verdes (sin cambios — la lógica nueva afecta paths que
no están cubiertos en los smoke tests actuales).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:11:36 +00:00
sergio 72758e75ce feat(tahuantinsuyu): "Cielo ahora" + "General" como rows fijos al top del tree
Dos entradas siempre presentes en la cima del árbol:

1. **⏱ Cielo ahora** (leaf): selecciona una carta efímera del
   instante actual en Greenwich (UTC, lat 51.4769°, lon 0°,
   alt 47 m). NO se persiste en la store — `build_present_sky_chart`
   la construye al vuelo con `Chart { id: Default::default(), ... }`
   y birth_data tomado de `SystemTime::now()` via `unix_to_civil_utc`
   (algoritmo Howard Hinnant, exacto y proleptic-Gregoriano).

   La carta queda **seleccionada por default** al boot — el usuario
   abre la app y ya está viendo el firmamento actual, incluso si
   no tiene contactos cargados.

2. **◇ General** (branch): contenedor virtual para los contactos
   sin grupo asignado (parent=None). Antes esos contactos
   aparecían sueltos al nivel raíz; ahora viven dentro de
   "General" y se ofrece como destino claro para "Nuevo
   contacto" desde su menú. Click sobre General muestra
   thumbnails de TODAS las cartas de esos contactos en el canvas.

Soporte en `TreeSelection`: dos variantes nuevas `PresentSky` y
`GeneralRoot`. `parse_row` reconoce los IDs sentinela `sky:now`
y `general`. El shell maneja ambos casos en `apply_selection`:
- PresentSky → set `current_chart` + render
- GeneralRoot → grilla de thumbnails

`MenuTarget::from_selection` mapea PresentSky/GeneralRoot →
MenuTarget::Root (mismo menú "Nuevo grupo / Nuevo contacto").

`unix_to_civil_utc` con 4 tests cubre: epoch (1970-01-01),
2024-02-29 (año bisiesto), pre-epoch (-1 → 1969-12-31), y
year 2000.

Total 10 tests verdes (6 anteriores + 4 nuevos del calendario).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 20:11:43 +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 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 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 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