Una ventana puede salir del teselado y flotar: conserva su propio
rectángulo y se compone por encima de las teseladas.
- Workspace guarda las flotantes en un mapa aparte; layout() tesela
sólo las no-flotantes y añade las flotantes al final (orden de
pintado). set_floating / is_floating.
- WindowPlacement y BodyOp::Configure llevan floating: bool. BodyState
detecta el cambio de floating como cualquier otro reconfigure.
- DesktopAction::ToggleFloat (Super+f): saca la enfocada a un
rectángulo centrado al 60 % de la pantalla, o la devuelve al teselado.
En Monocle, una flotante sigue visible.
- mirada-compositor ordena las flotantes al frente de la lista
front-to-back de elementos → se pintan encima.
- HUD de mirada marca las flotantes; mirada-ctl toggle-float.
Verificado end-to-end con headless-ctl. mirada-layout 30->32,
mirada-protocol 9->10, mirada-body 13->14, mirada-brain 41->42.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tanda de funciones de tiling WM, toda pura (mirada-layout/brain), sin
tocar el protocolo:
- nmaster: LayoutParams.master_count — cuántas ventanas van en el área
maestra. MasterStack y CenteredMaster apilan N maestras; sin pila, las
maestras llenan la pantalla. Acciones inc-master/dec-master (Super+,
Super+.), acotadas 1..9.
- Promover a maestra: Workspace::promote_focused lleva la ventana
enfocada al puesto 0. Acción promote-to-master (Super+Return).
- Smart gaps: una sola ventana se tesela a sangre, sin margen.
combo_string del compositor canoniza ahora teclas con nombre (Return,
Tab, F5, flechas…) vía xkb::keysym_get_name, no sólo caracteres
imprimibles — sin eso Super+Return no sería un atajo expresable.
Cableado en keymap por defecto, HUD de mirada y mirada-ctl. Verificado
end-to-end con headless-ctl. mirada-layout 26->30, mirada-brain 39->41.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
mirada-layout pasa de 4 a 7 modos de teselado, todos intercambiables
por el API (SetLayout / CycleLayout / mirada-ctl layout <modo>):
- Rows: filas horizontales de igual alto (complemento de Columns).
- Spiral: espiral de Fibonacci — cada ventana parte por la mitad el
espacio restante, alternando el sentido del corte.
- CenteredMaster: maestra centrada + pila a ambos lados (monitores
anchos).
LayoutMode::ALL + next() definen el ciclo. Añade dos acciones,
GrowMaster/ShrinkMaster (Super+l / Super+h), que ajustan master_ratio
en caliente — ese parámetro existía pero no había forma de tocarlo.
Cableado completo: tile(), cycle, slugs Display/FromStr, keymap por
defecto (Super+r/d/s), HUD de mirada, mirada-ctl actions. El ejemplo
headless-ctl ahora imprime la geometría para verificar los layouts.
mirada-layout 22->26 tests, mirada-brain 37->39.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Toda acción de escritorio converge en Desktop::apply(DesktopAction); el
keymap era sólo un front-end. Esta tanda añade los otros tres.
- DesktopAction::FocusWindow(WindowId): direccionamiento directo de una
ventana (no sólo ciclar); si está en otro escritorio, salta a él.
DesktopAction pasa a ser Serialize/Deserialize (postcard) además de
Display/FromStr.
- mirada-brain::ctl: el API de control externo. CtlRequest/CtlReply
(marco postcard), CtlServer/CtlConn no bloqueantes y send_request.
El Cerebro abre el socket y atiende en su bucle: la app mirada
siempre, mirada-compositor sólo con el Cerebro embebido.
- mirada-ctl: CLI de control estilo swaymsg/hyprctl —
`mirada-ctl focus-next | focus-window 5 | workspace 3 | windows`.
Parsea la acción de los argumentos vía FromStr.
- HUD interactivo en la app mirada: pips de escritorio y ventanas del
lienzo clicables (SwitchWorkspace / FocusWindow).
- Ejemplo headless-ctl: un Cerebro sin gráficos para probar mirada-ctl
en modo desatendido. Verificado end-to-end.
mirada-brain: 29 -> 37 tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Los atajos de teclado dejan de estar cableados: ahora son un Keymap
configurable que vive sólo en el Cerebro. El Cuerpo nunca lo ve — sólo
recibe la lista de cadenas a interceptar (GrabKeys) y devuelve la
pulsada; es Desktop quien la traduce. Esa separación (qué interceptar
vs. qué significa) hace innecesario cualquier candado o Arc.
mirada-brain:
- keymap.rs — Keymap: from_ron/to_ron, load/save, load_or_init (escribe
un archivo por defecto documentado si falta; default sin pisar si está
corrupto), default_path (~/.config/mirada/keymap.ron), y watch sobre
notify para la recarga en caliente (KeymapWatch).
- DesktopAction: Display + FromStr — vocabulario textual estable
("focus-next", "layout:grid", "workspace:3"); evita los guiones que
romperían el RON de un enum.
- Desktop: with_keymap, set_keymap (cambio en caliente -> nuevo GrabKeys).
- Ejemplo keymap-default: imprime el archivo por defecto en RON.
Apps: mirada y mirada-compositor (modo embebido) cargan el keymap del
usuario al arrancar y lo recargan en caliente cuando el archivo cambia.
Disco RON, cable postcard (sólo la lista de cadenas), sin ejecutable
configurador. mirada-brain: 17 -> 29 tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
winit lee WAYLAND_DISPLAY/DISPLAY para encontrar la sesión gráfica
anfitriona donde anidarse. El código publicaba antes su propio socket
en WAYLAND_DISPLAY, así que winit intentaba anidarse en el propio
compositor —un socket que aún no atiende a nadie— y se colgaba.
Ahora winit::init() va primero (conecta a la sesión real) y el socket
propio + set_var se publican después. Si no hay sesión gráfica, aborta
con un mensaje claro en vez de colgarse o fallar en seco.
README: sección Requisitos — hace falta sesión X11/Wayland anfitriona;
receta Xvfb + VNC para cajas headless.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Compositor Wayland teselante real sobre smithay, backend winit (corre
anidado como ventana dentro de la sesión X11/Wayland actual). Habla
wl_compositor/xdg_shell/wl_shm/wl_seat/wl_data_device y compone las
superficies de los clientes con GlesRenderer.
Dos modos: autónomo (Cerebro Desktop embebido, un solo proceso) o
enlazado (MIRADA_SOCKET → la app mirada decide la geometría). Reusa
mirada-body para la contabilidad y mirada-link para el cable.
Actualiza el SDD: el Cuerpo deja de ser pendiente. Añade README.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Envuelve mirada-brain::Desktop y lo pinta: barra con escritorios + modo
+ foco, lienzo teselado con marco por ventana. Con MIRADA_SOCKET sondea
un Cuerpo por mirada-link; sin él, simulación con ventanas sintéticas y
teclado (n/w/j/k/tab/1-9). cargo build -p mirada limpio.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
matilda-discover gana discover_inventory(): corre docker inspect en
cada contenedor y compara contra el spec deseado — imagen, puertos,
env y volúmenes declarados. Si el contenedor que corre se desvió, el
plan emite un Update; si está al día, no hay acción. La comparación es
por satisfacción (lo extra que trae la imagen se ignora).
El CLI (--discover) y el shell (:matilda) ahora usan discover_inventory
en vez del descubrimiento por nombre: detectan no sólo qué crear y
eliminar, sino la deriva de configuración de lo que ya existe.
container_drift es puro — 6 tests nuevos con JSON de docker inspect.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
matilda deja de ser sólo un ejecutable aparte: el shell lo incorpora
como herramienta. Meta-comando `:matilda plan|script|apply
<inventario.json>` — reconcilia contra el estado real de la máquina
(matilda-discover) y vuelca el resultado al feed del shell:
- `plan`/`script` → una tarjeta sintética con el plan o el script.
- `apply` → ejecuta el script de verdad; su salida fluye en una
tarjeta como cualquier comando (streaming, captura acotada, kill).
El panel [RUN] gana una sección [tools] con «⚙ matilda» que precarga
el comando. Reusa todo lo del shell —feed, ejecución, sesión— sin
panel nuevo ni peso extra: la herramienta es no invasiva.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Observa qué contenedores y vhosts existen (docker ps + sitios de
nginx) y reconstruye un Inventory "actual" que matilda-plan diferencia
contra el deseado: detecta correctamente qué crear y qué eliminar
(huérfanos). Parseo puro y testeable; sólo discover_local toca el
sistema. 6 tests.
La CLI gana el flag --discover en plan/script/apply: reconcilia
contra el estado real de la máquina en vez de partir de vacío.
matilda: 7 crates + CLI, ~48 tests. Pendiente: matilda-app (GPUI) y
la inspección detallada (docker inspect) para detectar drift de
configuración, no sólo presencia.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
matilda-ghost: el agente que ejecuta los ApplySteps en la máquina
destino — escribe archivos, corre comandos, reporta paso a paso;
semántica set -e (se detiene en el primer error). dry_run previsualiza
sin tocar nada. 5 tests.
matilda-linker: aplica los pasos en un host remoto por SSH sobre
brahman-ssh-multiplex; produce el mismo ApplyReport que el ghost local.
apps/matilda: deja de ser una demo hardcoded — ahora es una CLI real:
matilda example | plan | script | apply (local · --dry-run · --host)
Carga el inventario de un JSON, reconcilia y aplica.
matilda: 6 crates + CLI, ~42 tests. La cadena va de la declaración
a la aplicación local/remota.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Traduce un Plan de reconciliación a ApplySteps concretos: por cada
acción, los archivos a escribir en el servidor y los comandos a
correr. Contenedores → docker rm/run; vhosts → archivo nginx +
reload; hosts → sin pasos (son destino de conexión, no algo a
aplicar). steps_to_script() emite un script bash único con heredocs.
Sigue agnóstico de transporte — ejecutar los pasos (local, SSH o vía
matilda-ghost) es la capa de I/O. La demo CLI ahora imprime el script.
6 tests; matilda llega de la declaración al script ejecutable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
El volcado de la salida excedente ya no copia por espacio de usuario:
pasado el tope, el lector escribe la línea que cruzó + lo bufereado y
luego mueve el resto del pipe al archivo con splice(2) —kernel a
kernel, sin copia—. Se aplica a stdout (el contenido principal).
shuma-shell limpia sus archivos de volcado al cerrar la sesión
(Drop). Los spills llevan el pid en el nombre para no chocar entre
instancias.
shuma-exec: 11 tests verdes (el de spill ahora verifica el camino
splice).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bash deja de ser el ejecutor. shuma-exec ahora tiene dos modos:
- Exec::Direct — brahman lanza y conecta cada etapa del pipe con
descriptores reales; control total del árbol de procesos.
- Exec::Shell — fallback a `bash -c` para sintaxis que el modo directo
aún no absorbe (globs, $VAR, redirecciones, &&). bash = un parser
de sintaxis, no el ejecutor por defecto.
El shell elige: pipe simple (sólo comandos/args/`|`) → directo; algo
más → shell. La WorkSession lleva su CapturePolicy (límite + spill),
configurable con `:limit <MB>` y `:spill on|off`; la barra de estado
la muestra. Si spill está activo, la salida excedente se vuelca a un
archivo en vez de descartarse (RunEvent::Spilled).
shuma-exec: 11 tests (directo, pipes, spill, kill de pipeline).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
shuma-exec: cota dura de memoria. CommandSpec.capture_limit (bytes):
pasado el tope se emite RunEvent::Truncated una vez y el resto se
descarta —pero el pipe se sigue drenando, así el proceso no se
bloquea y termina normal. CommandSpec.stdin_data alimenta un texto
por la entrada estándar (escrito en su propio hilo).
shuma-session: CommandRun.truncated.
shuma-shell: tope de captura de 8 MiB por comando. Cada card con
salida muestra «⤳ reprocesar» — al pulsarlo, el próximo comando
filtra esa salida capturada (vía stdin) sin re-ejecutar el original;
un banner marca el modo. Las cards truncadas avisan «⚠ truncado».
shuma-exec: 12 tests (incluye truncado y reproceso por stdin).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- La salida de un comando ya no se trunca: si hay contenido, se
muestra entero (fuera el «N líneas antes»).
- Las cards de un pipe muestran una fila de etapas: un clic re-ejecuta
la línea hasta esa etapa como un comando nuevo, así se inspeccionan
los resultados intermedios. Eficiente — sólo corre lo que pedís, sin
bufferizar intermedios ni cambiar el modelo.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
shuma-infer: el directorio de una ocurrencia es ahora el cwd de su
último comando (el dir donde realmente operó el patrón, ya hechos los
cd). predict_next devuelve también el índice del patrón.
shuma-shell: la predicción se filtra por marcadores de proyecto
(.git, Cargo.toml, package.json, go.mod…). El shell deriva la
condición de disparo de un patrón —los marcadores comunes a sus
directorios— y sólo lo anticipa en un cwd que comparta esa estructura.
Así el ghost no sugiere `cargo build` en un directorio sin Cargo.toml.
Cierra la visión: del scripting a la intención, con precisión.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cada grupo del panel [RUN] —incluidos los que el motor de inferencia
promueve (✨)— recibe un atajo de teclado según su posición. Pulsar
F1..F8 ejecuta el grupo sin tocar el mouse: la macro se vuelve ubicua,
parado donde estés. La etiqueta del grupo muestra su tecla.
Cierra el lazo del «scripting a la intención»: repetir un flujo →
patrón detectado → grupo promovido → atajo de un toque.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
shuma-line: ghost_suggestion(line, corpus) — el resto de la línea que
el shell predice, a partir de un corpus priorizado.
shuma-infer: predict_next(recent, patterns) — si los últimos comandos
coinciden con el prefijo de un patrón, devuelve los pasos que faltan.
shuma-shell: mientras se escribe, el prompt pinta en gris tenue la
continuación predicha — historial reciente o, con prioridad, la
secuencia que el motor de inferencia anticipa (cd a un proyecto →
fantasma «git pull && cargo build»). La flecha → al final de la
línea, o Ctrl+Space, aceptan el fantasma.
13 tests shuma-infer, 37 shuma-line.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Detecta patrones de comandos repetidos en el historial: ventana
deslizante sobre las firmas de binarios (sólo ventanas 100%
exitosas), abstracción de argumentos variables (cd /a vs cd /b →
cd <…>), patrones maximales, puntaje por largo × frecuencia.
10 tests, agnóstico y determinista.
El shell lo corre tras cada comando terminado y promueve el patrón
más fuerte a un grupo «✨ ...» en el panel [RUN] — la rehidratación
que convierte la repetición orgánica en una receta de un clic.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
shuma-exec: RunHandle::kill() — el proceso se comparte con su hilo
coordinador (Arc<Mutex<Child>>) para poder terminarlo; los lectores
cierran al cerrarse los pipes. 8 tests (incluye kill de un sleep).
shuma-shell:
- Cada tarjeta de comando en curso (▷) muestra un botón «✕ matar».
- Meta-comando `:save <nombre>` guarda como grupo los comandos
ejecutados desde el último guardado. El botón «+» del panel [RUN]
precarga «:save » en el input para nombrarlo.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- shuma-session: la salida de un comando ahora distingue el flujo
(OutputLine { stream, text }); CommandRun expone lines_of/count_of/
has_stderr.
- Las tarjetas del feed se acordeonan (clic en la cabecera). El filtro
de la cabecera muestra stdout por defecto; si hubo stderr aparece el
switch «⚠ N» para verlo.
- Orden de terminal: los comandos nuevos se acolan abajo, los viejos
suben y se autocolapsan — salvo que el usuario haya tocado el
acordeón a mano (user_touched).
- El feed sigue al comando más reciente (ScrollHandle::scroll_to_bottom).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cada panel lateral tiene ahora un divisor de 5px que se arrastra para
redimensionarlo (cursor resize, resaltado al hover). El arrastre se
sigue a nivel de ventana —on_mouse_move/up en la raíz— así que no se
pierde aunque el cursor salga del divisor. Ancho acotado 130–420px;
los divisores desaparecen con el panel colapsado.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
El shell ahora ejecuta de verdad. Adiós a los cuadros verdes de
ejemplo: el panel central muestra los comandos ejecutados, cada uno
con su salida llegando en streaming (shuma-exec lanza el proceso,
un bucle de fondo drena stdout/stderr ~9 veces/s).
El shell vive dentro de una WorkSession (shuma-session): la barra de
estado muestra el directorio actual y su identificador de aislamiento
(hash estable del cwd — cd lo cambia). `cd` se maneja internamente.
El panel [RUN] lista los grupos de comandos reutilizables; un clic
ejecuta el grupo entero (lines unidas por &&).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
El input de abajo ahora está vivo sobre shuma-line: se escribe de
verdad (teclado completo, motions, Ctrl+a/e/u, UTF-8), con resaltado
por token en tiempo real (comando, flag, string, variable, pipe,
redirección…) y autocompletado posicional con popup navegable
(↑↓ Tab) — comandos del PATH, flags por comando, rutas del disco.
Enter registra la línea en el lienzo de intenciones; las etapas de
pipe se cuentan en la barra de estado.
Panel derecho [SENS]: monitores de CPU y memoria con curva en vivo
(shuma-sysmon, refresco ~1s). Paneles laterales colapsables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yachay-core: notebook como secuencia de celdas (orden de lectura) +
DAG de dependencias (orden de ejecución). Celdas markdown/código/embed
con content_hash BLAKE3; editar una propaga staleness a descendientes;
digest Merkle por celda (content_hash ‖ digests upstream) y
notebook_digest que certifica reproducibilidad. Demo CLI en apps/yachay.
14 tests. Sin kernel ni UI, #![forbid(unsafe_code)].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
matilda-core: modelo declarativo (Host, Container, VHost, Inventory).
matilda-config: renderiza Container→docker-compose/docker run y
VHost→bloque server nginx (con TLS + redirección :80→:443).
matilda-plan: reconciliación pura actual→deseado con acciones
ordenadas por dependencia (contenedores antes que vhosts, removes
en orden inverso). Demo CLI en apps/matilda.
29 tests. Funciones puras, cero Docker/SSH/disco.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CLI que siembra un cuaderno (cocina/jardín/oficina), imprime el grafo
de wiki-links (forward/backlinks, huérfanas, colgantes) y los
clústeres por gravedad semántica + vecinos + layout 2D.
cargo run -p badu.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CLI que recorre el caso canónico de extremo a extremo: Venezuela
atestigua la nacionalidad de Yumaira, otras identidades corroboran,
una firma manipulada se rechaza, y tres políticas negociadas dan
veredictos distintos sobre la misma evidencia. cargo run -p agorapura.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fana-editor-gpui: EdgesElement pinta los conectores de dependencia
como paths; editor_view compone bloques de átomo (divs absolutos
coloreados por coherencia) + osciloscopio del sidepane. RenderPlan
ahora lleva su LayoutConfig para que el backend sea autosuficiente.
app fana: ventana con un relato de ejemplo (rama principal + alterna),
botón «Mutar raíz» que dispara la onda de choque lógica
(propagate_mutation), «Re-validar todo», leyenda y estadísticas.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dominium-canvas-gpui: Element que pinta un RenderPlan como quads,
centrado en sus bounds (rgba→hsla, único crate que toca gpui).
app dominium: compone core→physics→iso→render-plan→canvas en una
ventana GPUI con bucle de simulación de fondo (~11 tps), panel de
estadísticas, controles play/pausa + re-sembrar, y re-siembra
automática al colapso poblacional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
apps/shuma-shell deja de ser el dashboard legacy de la era shipote y
pasa a ser el shell de la spec: layout fijo de 3 zonas.
- status (arriba) — estado de sandokan + versión.
- [RUN] (izquierda) — barra de macros desde MacroBook (F1/F2/F3).
- Lienzo de Contexto (centro) — grafo de intenciones: cada %cN es una
caja posicionada por shuma-shell-render::layout, borde coloreado por
estado (ámbar Running / verde Ok / rojo Failed).
- [SENS] (derecha) — telemetría (CPU/MEM, placeholders).
- prompt fijo (abajo) — la línea de intención.
v1: renderiza la estructura con datos de ejemplo. Cableado interactivo
(typing, F-keys ejecutando vía sandokan, telemetría viva) es el paso
siguiente. cargo check --workspace verde.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
apps/sandokan (binario `sandokan`): CLI para probar el orquestador.
Subcomandos: daemon, run <exec> [args], list, status, telemetry, stop.
Fix: Intent serializaba Card directo, pero Card tiene un campo
`#[serde(flatten)] extensions` incompatible con postcard ("sequence
length must be known"). Intent::card ahora usa #[serde(with)] que
proyecta Card↔WireCard en el límite de serialización (las extensions
locales se descartan al cruzar el wire — comportamiento correcto).
Smoke test verificado end-to-end: daemon + run /bin/sleep + list +
status Running + telemetry + stop + status Killed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Crate nuevo `cosmobiologia-server` (binario axum, nativo) que
monta `cosmobiologia-engine` + `cosmobiologia-store` y expone
la rueda + el CRUD del tree por HTTP.
Endpoints v1:
- `GET /api/health`
- `GET /api/tree` tree completo anidado
- `POST /api/groups` crear grupo
- `PATCH /api/groups/:id` renombrar
- `DELETE /api/groups/:id` borrar
- `POST /api/contacts` crear contacto
- `PATCH /api/contacts/:id` renombrar
- `DELETE /api/contacts/:id` borrar
- `POST /api/charts` crear carta
- `GET /api/charts/:id` chart JSON
- `PATCH /api/charts/:id` editar (label/birth/config)
- `DELETE /api/charts/:id` borrar
- `GET /api/charts/:id/render` RenderModel JSON
- `GET /api/charts/:id/svg` SVG inline (reusa
svg_export del engine)
- `GET /api/sky` "Cielo ahora" — RenderModel
UTC actual sin chart_id real
Query params del render para activar overlays sin POST:
- `offset_min=<i64>` time scrubbing
- `transit=1` overlay de tránsito al now
- `prog_age=<f64>` progresión secundaria
- `sa_age=<f64>` solar arc
- `pd_age=<f64>` primary directions (Naibod)
Decisiones:
- Single-user, sin auth. Bind por default a `127.0.0.1:8787` —
el server NO debe exponerse a la red pública en esta fase.
- DB por default = misma del desktop (`$XDG_DATA_HOME/cosmobiologia/
charts.db`). `--db` permite override.
- CORS permissive (es localhost, single-user, sin auth).
- `ApiError` con mapeo a HTTP status: 404 NotFound,
400 BadRequest, 500 todo lo demás. Body JSON `{ "error": "..." }`.
Smoke test:
cargo run -p cosmobiologia-server -- --port 18787
curl /api/health → {"status":"ok",...}
curl POST /api/groups → {"id":"01KRYVP...","name":"Familia",...}
curl POST /api/contacts → {"id":"01KRYVP...","group_id":...}
curl /api/tree → árbol anidado
curl /api/sky → RenderModel con VSOP real
Pendiente (fase 3): cliente `cosmobiologia-web` (cdylib WASM)
que consuma estos endpoints y pinte SVG/Canvas2D.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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).
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>
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>
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>
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>
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>
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>
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>
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>
- 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>