Commit Graph

429 Commits

Author SHA1 Message Date
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 05886022e0 feat(sandokan-remote): B1.4 — RemoteEngine vía SSH socket-forward
Opción B: RemoteEngine orquesta en un host remoto tunelando el wire
del daemon sobre un canal SSH direct-streamlocal hacia el sandokan.sock
remoto. El protocolo es idéntico al de DaemonEngine (postcard
length-prefixed) — sólo cambia el transporte, así que read_frame/
write_frame se reusan tal cual.

- brahman-ssh-multiplex: + SshSession::forward_unix — abre un canal
  direct-streamlocal y devuelve su ChannelStream (AsyncRead+AsyncWrite).
- sandokan-daemon: protocol ahora pub, exporta read_frame/write_frame.
- sandokan-remote: RemoteEngine { SshSession + remote_socket }.
  connect() o with_session(); cada operación abre un canal nuevo
  (multiplexado sobre la conexión maestra).
- sandokan umbrella re-exporta RemoteEngine.

Completa Fase B: sandokan tiene Local + Daemon + Remote + auto().
cargo check --workspace verde. RemoteEngine necesita un host remoto
con `sandokan daemon` para validación runtime (sin unit test).

Opción A (text-parse del CLI por compat) queda pendiente por decisión
del usuario.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:37:11 +00:00
sergio 0e13c35f3e feat(brahman-ssh-multiplex): A6 — sesión SSH multiplexada (russh)
Envuelve russh 0.54 con una API mínima: una SshSession mantiene el
Handle maestro; cada exec() concurrente abre su propio canal en
paralelo sobre la misma conexión TCP (SSH multiplexa canales por
diseño del protocolo).

- SshConfig (host/port/user/auth/keepalive) + SshAuth (Password | Key).
- SshSession::connect — config russh + keepalive + auth password o
  clave privada en disco; verificación de host key TOFU por default.
- SshSession::exec — corre un comando en un canal nuevo, junta
  stdout/stderr/exit_code.
- SshSession es Clone barato (comparte el Handle).

Base de sandokan RemoteEngine y del Linker SSH de matilda.
Compila contra russh 0.54. El test de conexión real requiere un
servidor SSH (fuera del unit test).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:26:16 +00:00
sergio 1e01dc27a5 feat(brahman-card-discovery): B4 — búsqueda de Cards local + DHT
- index — CardIndex: índice en memoria con filtros (by_label
  case-insensitive substring, by_kind, providing por Capability, by_id).
- registry — scan_dir: carga toda Card *.json de un directorio,
  saltando ruido y archivos rotos.
- discovery — CardDiscovery: une el índice local con la malla P2P;
  announce_all publica las Cards locales al DHT, find_remote busca
  proveedores. Modo local-only sin DHT también soportado.

Lo consumen el card-browser de nahual-shell y agorapura.
7 tests verdes. cargo check --workspace verde.

settings.local.json: defaultMode bypassPermissions (sesión desatendida).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:23:16 +00:00
sergio 27603c906d feat(brahman-dht): B3 — discovery typed sobre el Kademlia compartido
brahman-net corre un único Kademlia para todo el ecosistema.
brahman-dht le pone arriba un esquema de claves namespaced para que
distintos dominios coexistan sin colisión en la misma malla.

- key — RecordKind (Code/Card/Persona/Service/Custom) + DhtKey.
  Wire: [kind_tag] ++ blake3(id) = 33 bytes longitud fija. Custom(n)
  usa 0x80|n: nunca choca con los kinds estándar.
- Dht — wrapper sobre BrahmanNet: announce/withdraw/find (modelo de
  provider records).

Consumidores: minga (Code), brahman-card-discovery (Card), agorapura
(Persona). 5 tests verdes (incl. smoke async sobre un nodo libp2p real).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:11:40 +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 94ea0eaa53 feat(sandokan): CLI de prueba + fix wire serialization
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>
2026-05-20 15:05:03 +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 8cd8003dd5 feat(sandokan): B1.5 — umbrella + Engine::auto()
Crate sandokan (umbrella): re-exporta core/local/daemon y provee la
selección de transporte.

- auto(socket) — patrón "el primero que arranca gana": prueba si hay
  un daemon escuchando; si lo hay devuelve DaemonEngine, si no
  LocalEngine. Box<dyn Engine> (el trait es object-safe vía async_trait).
- auto_default() — auto() con default_socket_path().
- default_socket_path() — $XDG_RUNTIME_DIR/sandokan.sock o
  /run/brahman/sandokan.sock.

3 tests: fallback a Local sin daemon, pick Daemon con serve() activo,
default path absoluto. cargo check --workspace verde.

sandokan ya es usable end-to-end en modo local y daemon. Falta
RemoteEngine (B1.4, depende de brahman-ssh-multiplex).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:05:32 +00:00
sergio b7d9d7abd9 feat(sandokan-daemon): B1.3 — DaemonEngine + protocolo wire
DaemonEngine: implementación del trait Engine que delega a otro proceso
vía Unix socket. Materializa el patrón horizontal de sandokan (el
binario que arranca primero expone el engine; los demás se le suman).

- protocol.rs — DaemonRequest/DaemonResponse (espejan los métodos de
  Engine) + framing postcard length-prefixed (u32 LE + bytes), con
  MAX_FRAME 16 MiB defensivo.
- client.rs — DaemonEngine: stateless, un round-trip por llamada;
  is_reachable() para el probe de auto().
- server.rs — serve(engine, socket): envuelve cualquier Engine, una
  task por conexión, multi-request por conexión.

EngineError ahora es Serialize/Deserialize (viaja por el wire);
NotFound se propaga tipado a través del socket.

1 test de integración: roundtrip real DaemonEngine ↔ serve ↔ LocalEngine
(list vacío + NotFound propagado). cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:04:22 +00:00
sergio cba3a9dd6e feat(sandokan-local): B1.2 — LocalEngine
Primera implementación del trait Engine: orquestación in-process.

- LocalEngine encarna Cards vía arje-incarnate, mantiene un registro
  HashMap<Ulid, Entity> de entidades activas.
- run()       — incarnate + registro; mergea env del contexto.
- stop()      — SIGTERM + período de gracia + SIGKILL + reap.
- list()      — reaping perezoso (waitpid WNOHANG) + handles activos.
- status()    — reaping perezoso + LifecycleState.
- telemetry() — lee /proc/<pid>/status (VmRSS + Threads), sin invocar
                binarios externos.
- Reaping sin task de fondo: cada consulta hace waitpid WNOHANG.

proc.rs: lectura directa de procfs (mem_bytes, thread_count, proc_exists).

4 tests verdes (2 proc + 2 engine: empty list, NotFound paths).
cargo check --workspace verde.

v1: IsolationLevel es advisory (Sealed reservado para cuando el Intent
transporte rootfs spec). CPU% pendiente (requiere 2 samples).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 13:57:26 +00:00
sergio af5d4a1f22 feat(sandokan-core): B1.1 — contrato del orquestador
Primer crate de la Fase B. Define SOLO el contrato del orquestador
sandokan (library horizontal embebible, no daemon supremo):

- Intent / ExecContext / IsolationLevel — qué orquestar
- ExecHandle — referencia a una entidad encarnada
- LifecycleEvent / TelemetryFrame — observabilidad (wire types)
- EngineError — taxonomía de fallas
- trait Engine — run/stop/list/status/telemetry (poll-based, sin
  streams sobre trait objects, para que las 3 impls lo cumplan
  uniformemente)

Las impls concretas (LocalEngine, DaemonEngine, RemoteEngine) vendrán
en crates separados (sandokan-local, sandokan-daemon, sandokan-remote).

3 tests verdes. cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:38:22 +00:00
sergio f8a2547b45 feat(arje-incarnate): A5 — pivot_root + OverlayFS
Dos ChildPreExec nuevos en el hook declarativo pre-execve:
- MountOverlay { target, options } — monta OverlayFS (capa base RO +
  capa de sesión RW + workdir).
- PivotRoot { new_root, put_old, old_root_after } — bind-mount de
  new_root sobre sí mismo + pivot_root + chdir("/") + umount2 lazy
  (MNT_DETACH) del root viejo.

Builders ergonómicos en ChildSetup:
- with_overlay(lower, upper, work, merged)
- with_pivot_root(new_root, put_old_name)

Ambas ops corren en el hijo post-clone, dentro del mount namespace,
async-signal-safe (solo libc, sin allocator). Las consumirán mirada
(compositor Wayland) y matilda Ghost para rootfs aislados.

19 tests arje-incarnate verdes (3 nuevos: builders overlay/pivot).
cargo check --workspace verde. Pendiente: integration test en entorno
con namespaces reales.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:35:31 +00:00
sergio 545dd59c72 feat(sandokan-lifecycle): A4 — primitivas de lifecycle agnósticas
Nuevo crate runtime/sandokan-lifecycle: lógica pura reutilizable por
cualquier supervisor de procesos (shuma, matilda Ghost, charka-shadow,
mirada). Sin syscalls, sin proceso, sin UI.

Módulos:
- backoff   — Backoff exponencial con tope
- ttl       — Ttl anclado a Instant
- quota     — ResourceQuota + check_quota + Breach + QuotaAction
- restart   — RestartPolicy + RestartTracker (conteo + backoff)
- state     — LifecycleState (Pending/Running/Exited/Failed/Killed)

15 tests verdes. cargo check --workspace verde.

Variante segura de A4: se crea la library limpia sin tocar shuma-core
(módulo maduro). La migración de WorkspaceManager a consumir estas
primitivas queda registrada como A4.2 (refactor diferido, no urgente).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:32:52 +00:00
sergio 67c0fcad11 refactor(loader): A3 — unificar loader, eliminar duplicación
El loader vivía partido: arje-brain/loader.rs cargaba EntityCards Y
Rules, mientras brahman-cards tenía su propia infra de card-loading.

Resolución por linaje:
- Card-loading (load_card_file, extract_card_from_json) → brahman-cards
  (entity_loader.rs). Toda card-loading del ecosistema vive ahí.
- Rule-loading (load_rules_file, extract_rules_from_json) → arje-brain-rules
  (loader.rs), junto a la definición de Rule.
- arje-brain/loader.rs eliminado.

arje-brain re-exporta ambos para compat de consumidores (arje-zero).
cargo check --workspace verde. Tests: 13 arje-brain-rules + 31 brahman-cards.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:28:20 +00:00
sergio 848fc7a072 refactor(brain): A2 — split arje-brain en 3 sub-crates
DAG de dependencias limpio (modularidad horizontal):
- arje-brain-rules     — rules + engine + dispatch (motor determinista)
- arje-brain-cognitive — observer + crystallize (estadística)
- arje-brain-audit     — audit chain → CAS (accountability)
- arje-brain           — umbrella de integración (introspect +
                         autopromote + metrics + loader)

Habilitador clave: TimedEvent movido de observer.rs a rules.rs
(engine lo necesitaba, era el único acoplo que rompía el DAG).

arje-brain re-exporta la API de los 3 sub-crates: arje-zero y chasqui
(consumidores) no requieren cambios. cargo check --workspace verde.
24 tests del brain pasan (4 rules + 6 cognitive + 5 audit + 9 umbrella).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:24:48 +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 3fc6dcfa72 docs: PLAN_MACRO.md — plan de ejecución global del ecosistema
Compilación del input de planificación recibido el 2026-05-19:
21 módulos totales (12 existentes + 9 nuevos + 1 rename), 6 servicios
compartidos como libraries horizontales, ~60-110K LOC nuevo código.

Estructura del plan:
- Fase A (sem 1-2): foundations cleanup (rename, brain split, lifecycle
  extract, pivot_root+overlayfs, transport-ssh)
- Fase B (sem 2-5): core libraries (sandokan, dime, discovery-dht)
- Fase C (sem 4-12): apps standalone paralelos (carmen, akashi, matilda,
  takiy, dominium, ágora)
- Fase D (multi-mes): charka outlier (parser COBOL completo)
- Fase E (sem 12-18): yachay integrador
- Fase F (continuo): cobertura tests + cerrar stubs cosmo/pineal
- Fase G (post): backlog (rimay, yuyay, apu, tinkuy, nutu + 4)

Critical path: 17-23 semanas (4-5.5 meses) sin charka completo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:57:44 +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 d341004f59 feat(cosmobiologia-server): server HTTP single-user con CRUD completo (fase 2)
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>
2026-05-19 00:55:51 +00:00
sergio 9b12958660 merge: rename tahuantinsuyu → cosmobiologia 2026-05-19 00:46:07 +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 ca5dd04176 feat(arje): reemplaza systemd en máquina real con coexistencia GRUB
Flujo seguro de adopción: arje se instala como entrada GRUB
alternativa, no toca systemd ni /sbin/init. Booteás arje cuando
querés, volvés a systemd si rompe (rollback instantáneo desde el
menú).

Artefactos nuevos:

- scripts/install-arje-as-init.sh: instala binarios musl-static a
  /usr/sbin/ y /usr/bin/, copia seed a /ente/seed.card.json, agrega
  menuentry "arje" a /etc/grub.d/40_custom usando init=/sbin/ente-zero
  con kernel + initrd nativos. NO cambia GRUB_DEFAULT. Idempotente
  (regenera el bloque ARJE-MENUENTRY si existe).

- scripts/uninstall-arje.sh: revierte binarios + menuentry. Conserva
  /ente/seed.card.json por si la editaste.

- seeds/arje-host.card.json: seed para máquina real con 15 cards:
  tmpfiles + mount-fstab + swap-on + dbus-system + 11 compat shims +
  dhcpcd + sshd + agetty. Validada.

- docs/arje-replace-systemd.md: filosofía vs systemd ("no acapara
  porque no genera, sólo arranca lo declarado"), lista exhaustiva de
  servicios systemd que NO deben migrarse (ModemManager, snapd, cups,
  unattended-upgrades, etc.), tabla diferencial de UX vs systemd
  (systemctl restart → kill PID, systemctl enable → editar seed),
  checklist pre-primer-boot, instrucciones de rollback y cómo hacer
  arje default sólo cuando estés seguro.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 20:01:12 +00:00
sergio e4b1d41b62 feat(arje): incluye CLIs admin en el initrd (brahman-status/busctl/brainctl)
Para "ver" la administración del init desde el shell que arrancó dentro
del initramfs hacían falta clientes que hablen los 4 sockets Unix. Los
exists como examples de cada crate; los empaquetamos ahora.

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

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

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

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

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

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

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

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

11 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:11:54 +00:00