Commit Graph

79 Commits

Author SHA1 Message Date
sergio 22e6ed6a71 feat(tahuantinsuyu): fase 11 — persistencia de module_configs por carta
Los toggles, sliders y partner pickers de cada overlay (transit,
progression, solar_arc, synastry) ahora persisten por carta en la
tabla SQLite `module_state` (que estaba creada desde fase 1 pero
sin cablear). Cambiar de carta y volver mantiene exactamente el
estado que el usuario dejó.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:42:25 +00:00
sergio d4761bf238 refactor(tahuantinsuyu): fase 6 — Modules pluggables vía compose + PipelineRequest
El shell ya no carga el flag `show_transits: bool` ni hardcodea qué
pipeline corre. La engine expone una sola API `compose(chart, offset,
&[PipelineRequest])` que la shell alimenta a partir de un map
`module_configs: HashMap<String, serde_json::Value>`. Los toggles de
overlay (transit hoy, progression/synastry/solar_arc en fase 7) viven
como módulos propios en el panel.

- engine: PipelineRequest enum (variante Transit por ahora; comentarios
  con el roadmap de SecondaryProgression/SolarArc/Synastry). compose()
  es la nueva entrada canónica; compute / compute_at_offset /
  compute_with_transits_at_now quedan como atajos retrocompatibles que
  delegan en compose. bridge.rs refactor: extraído build_transit_overlay
  como helper que muta &mut RenderModel, listo para que más pipelines
  apilen capas encima.
- modules: nuevo módulo `transit::TransitModule` (id "transit", toggle
  "enabled" con hotkey [T], applies_to Natal). Sacado el toggle
  show_transits de NatalModule — ahora cada módulo declara lo suyo.
  Registry::with_builtins() registra ambos. Test asegura los dos
  aplican a Natal.
- panel: sin cambios — ya itera Registry::for_kind(kind) y renderea
  cada módulo aplicable con sus controls. La adición del TransitModule
  aparece automática como segunda card en el panel.
- shell: replace show_transits por module_configs map. build_requests()
  deriva PipelineRequest::Transit cuando module_configs["transit"]
  ["enabled"] == true. on_panel_event: toggles del NatalModule afectan
  solo visibility del canvas; toggles de otros módulos van al
  module_configs y disparan render_current. on_canvas_event: [T]
  hotkey → flip transit.enabled + sync panel + recompose. apps Cargo
  agrega serde_json como dep directa.

Todos los tests verdes. Fase 7 puede sumar overlays adicionales
(progression, solar_arc) solo agregando variantes a PipelineRequest +
helpers en bridge + módulos declarativos — sin tocar el shell.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:06:03 +00:00
sergio d94dcfb5af feat(gioser): ciclo sol↔luna↔tierra, auras anchas, nuevo set Software/Quién Soy/Manifiesto/Mística
Shader (gioser-shaders):
- 3 cuerpos centrales renderizados realísticamente con interpolación
  gradual entre ellos (cross-fade smoothstep):
  - render_sun: núcleo gauss + corona pulsante + textura de plasma FBM
    (boiling surface).
  - render_moon: disco con limb darkening, cráteres + mares (2 octavas
    de fbm), terminador móvil (fase lunar), halo azulado en el limb
    iluminado.
  - render_earth: disco con continentes fbm (rotación lenta), polos
    blancos, nubes en otra capa, día/noche en hemisferio iluminado,
    halo atmosférico azul (Rayleigh simplificado).
- Uniforms u_body_a, u_body_b (int 0/1/2), u_body_blend (float).
- Cuerpo central se calcula sólo si inside > 0.001 (perf — saltea pixels
  fuera de la superficie de la chacana).
- radial_mult atenúa los rayos cuando luna/tierra están activos — el sol
  es el único que irradia tan intensamente.

- element_cloud(): aura ancha por cardinal (sigma_along=0.42,
  sigma_perp=0.34) con textura fbm animada y modulación por elemento.
  - AIRE: corrientes suaves que ondulan horizontalmente.
  - FUEGO: lengüetazos rápidos con flicker.
  - TIERRA: densidad sólida con variación lenta.
  - AGUA: ondulaciones grandes que viajan hacia afuera.
  Las nubes cubren todo el cuadrante del cardinal, no solo la punta.

- Helper functions vnoise_c + fbm_c agregadas (necesarias para superficies
  realistas de luna/tierra y para nubes elementales).

Renderer (gioser-canvas-web):
- body_state(t) -> (body_a, body_b, blend) state machine:
  - BODY_PHASE_SECS = 45 (≈10 pulsos del sol antes de transicionar).
  - BODY_TRANSITION_SECS = 4 (cross-fade gradual).
  - Total cycle: 147s = sol 45s → trans 4s → luna 45s → trans 4s → tierra 45s → trans 4s.
  - Smoothstep cubic en el blend para curva natural (no linear).
- Sube u_body_a/b como int (uniform1i) y u_body_blend como float.

App + contenido:
- index.html: nuevos labels en los 4 tips
  - NORTE (aire):  SOFTWARE / Tecnología
  - ESTE (fuego):  QUIÉN SOY / Bitácora
  - SUR (tierra):  MANIFIESTO / Invariantes
  - OESTE (agua):  MÍSTICA / Espiritualidad
- Íconos SVG nuevos relacionados al tema:
  - aire: chip de circuito con nodos y conexiones
  - fuego: libro abierto con líneas
  - tierra: hexagrama dentro de círculo (sacred geometry / invariante)
  - agua: ojo en triángulo (mística)
- gioser-web src/lib.rs: ensure_page_dom usa nuevos title+tag por elemento.
- 4 md/*.md reescritos con contenido seed para los nuevos temas, con
  manifiesto explícito en tierra.md.

Workspace verde + 21 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 04:31:02 +00:00
sergio 7728013012 feat(gioser/web): fix mobile swipe, taskbar agnóstica, trazos zodiacales
Mobile drag fix (vista-web):
- pointermove listener ahora con `AddEventListenerOptions { passive: false }`.
  Sin esto, en navegadores móviles `preventDefault()` es no-op y el browser
  se traga el gesto horizontal como pan/scroll antes de que JS pueda
  detectar la dirección y capturar el pointer.
- CSS: `.deck-strip` y `.deck-strip *` y `.deck-page` con
  `touch-action: pan-y`. El touch-action del target inmediato es lo que
  el browser consulta; sin esto, sobre un <p> dentro del strip el browser
  asume `auto` y reclama horizontal.

Taskbar agnóstica (barra-web):
- Nuevo crate `crates/modules/barra/barra-web` que maneja sólo el LIST
  dinámico de tareas; el resto del layout (home, brand, credits) es del
  host. Misma filosofía que vista-web: separar lo reusable.
- API: Task::new(id, label).active() builder; TaskList::mount(ul) +
  set_tasks/on_click/task_center. Click delegado, callback recibe
  (id, cx, cy) en CSS pixels para origin de animaciones.
- Sanitiza IDs a [a-zA-Z0-9_-] y HTML-escapa labels.
- 3 tests unitarios.
- gioser-web refactoreado para consumir TaskList: sync_taskbar arma
  Vec<Task> y delega; on_click del taskbar dispara minimize/restore_from_tab
  según estado. install_taskbar reducido a sólo home buttons.

Trazos zodiacales (gioser-shaders + canvas-web):
- 12 líneas radiales muy sutiles entre la chacana y el aro principal, una
  por signo, con colores significativos:
    Aries→fuego rojo, Tauro→tierra verde, Géminis→aire amarillo,
    Cáncer→agua plata, Leo→fuego dorado, Virgo→tierra marrón,
    Libra→aire rosa, Escorpio→agua rojo profundo, Sagitario→fuego púrpura,
    Capricornio→tierra verde oscuro, Acuario→aire celeste, Piscis→agua
    verde mar.
- Aries empieza en el norte, giran en sentido horario (rueda zodiacal
  clásica). Banda radial r∈[1.05*L, 0.96*ringR_main], gauss angular
  con σ=0.0042 rad (~0.24° de ancho), multiplier 0.55 → apenas visible.
- Uniform `vec3 u_zodiac[12]` subido como array plano de 36 floats vía
  uniform3fv. Constante ZODIAC_COLORS expuesta en canvas-web por si otros
  callers la quieren.

Workspace verde + 21 tests (geom 6 + palette 4 + physics 3 + pluma-md 5
+ barra-web 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:42:50 +00:00
sergio 62058ab193 feat(gioser): deck swipeable + minimize, brand+copyleft en taskbar (vista-web)
Nuevo módulo agnóstico:
- `crates/modules/vista/vista-web` — Deck::mount(strip) instala swipe
  horizontal con snap a la página más cercana, estilo Flutter PageView.
  goto(idx, smooth) navega programáticamente. on_change(cb) fires tras
  snap. Drag decision: horizontal si dx > 8px y > 1.3*dy; sino cede al
  pan-y nativo (scroll vertical del contenido). resize listener ajusta
  --vista-offset sin animar usando .vista-instant un frame.

App rediseñada:
- Brand "GioSer" sacado del centro de la chacana → ahora en la taskbar
  junto al botón home (data-home). brand-dot dorado entre Gio·Ser. El
  centro de la chacana queda con sol limpio.
- Copyleft + sergio@gioser.net a la derecha de la taskbar, abre
  https://sergio.gioser.net en nueva pestaña (target=_blank rel=noopener).
- 4 drawers separados → reemplazados por un único `.deck` con `.deck-strip`
  vista-web manejado. Las páginas se crean dinámicamente al abrir un
  elemento por primera vez (`ensure_page_dom`).
- Cada página tiene controles minimizar (─) y cerrar (×) arriba a la
  derecha, con ambience animada por elemento.
- Click minimize → active=None, deck scale(0) hacia la cajita del
  taskbar (origin = bounding rect del taskbar-item). Página queda en
  memoria, tab sigue en la barra.
- Click cajita del taskbar:
  - Si está activa → minimize (toggle).
  - Si está minimizada → restore con scale-up desde la cajita.
- Click home / brand → minimize all (estilo Show Desktop, no destruye).
- Swipe horizontal o click cajita → deck.goto(idx, smooth=true) con snap
  animado por vista-web. on_swipe sync de taskbar active state.
- Cerrar página → remueve del strip + del Vec pages; si era activa,
  reemplaza por neighbor o hide deck si era la última.

CSS:
- Eliminado `.brand` fixed center y `.drawer` × 4 individuales.
- `.deck` único + `.deck-strip` con `transform: translate3d(--vista-offset)`
  y transition transform 360ms cubic-bezier(0.22, 0.61, 0.36, 1).
- `.deck-strip.vista-dragging` / `.vista-instant` → transition: none.
- `.deck-page[data-element]` cada una con su page-ambience animada
  (aire-drift, fuego-flicker, agua-tide, tierra static).
- `.taskbar-brand` Cinzel 1.3rem dorado + .brand-dot.
- `.taskbar-credit` con `.copyleft-mark` (© con scaleX(-1) = copyleft
  visual).
- `.taskbar-spacer { flex:1 }` empuja credit a la derecha.
- `.taskbar-item.active` glow del color del elemento + border-bottom.
- `body.deck-visible` baja opacity del canvas + esconde tips y brand.

Workspace verde + 18 tests (geom 6 + palette 4 + physics 3 + pluma-md 5).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:20:34 +00:00
sergio 5e0fcae4b4 feat(gioser): shake on click, mouseleave rebound, element particles, taskbar
Renderer (gioser-canvas-web):
- Spring shake (SpringDamper1, 7.5 Hz / ζ=0.13) aplicado como rotación Z
  en el MVP. impulse_click() inyecta velocidad alternada → vibración fuerte
  con ~5 ciclos decayendo en ~0.8s.
- release_tilt() pone target del tilt en (0,0) → la chacana cae al frente
  con el rebote natural del spring sub-crítico.
- world_scale_for_aspect(): en portrait (aspect<1) escala baja proporcional
  para que el aro exterior no se corte por los lados. Base 1.05, piso 0.45.
- click_radius_css_px() expone radio del aro en CSS-pixels desde el centro
  del canvas; la app lo usa para hit-test del impulso.
- set_client_size() separa CSS-pixels de device-pixels (DPR).
- tilt_degrees() ahora retorna (pitch, yaw, roll) — el brand replica los 3.
- 4 nuevos uniforms u_aire/fuego/tierra/agua_color para el shader de
  partículas.

Shader (gioser-shaders/FS_CHACANA):
- Función element_particles(tip, outward, color, kind) → 4 partículas por
  cardinal con personalidad: AIRE drift+sway, FUEGO rise+flicker (siempre
  hacia +Y), TIERRA cae, AGUA ondula descendiendo. Gauss + envelope
  sinusoidal en la vida. ~16 partículas total, costo modesto.

App (gioser-web):
- pointerdown en canvas → si distancia al centro < click_radius_css_px →
  impulse_click(). Touch y mouse vienen unificados por PointerEvent.
- mouseleave en canvas → release_tilt(). Sin set_target, el spring se
  quedaría en la última posición — ahora vuelve al frente con rebote.
- position_tips ahora clampea raw_x/raw_y a [margin, viewport - taskbar -
  margin] en CSS pixels. Los botones NUNCA salen del canvas ni cubren la
  taskbar incluso en aspect extremos o tilt máximo.
- AppState + TaskbarState (RefCell): trackea drawers abiertos + activo.
  open_tab/switch_tab/close_tab/home aplican mutación + sync().
- sync() rebuild de taskbar-list innerHTML por cada cambio de estado,
  más swap de body classes + drawer .open classes.
- Click delegation en taskbar-list — un listener para todas las cajitas.
- Botón home con data-home en la barra (svg de casa) cierra todo y
  limpia el taskbar.
- Escape también cierra el drawer activo.
- update_tilt_css ahora setea --tilt-z también — brand title roll
  visible en el shake.

CSS:
- .drawer bottom: 52px (reserva taskbar).
- .taskbar full ancho fixed bottom, glass + gold border, scrollable horiz
  para muchas cajitas.
- .taskbar-item con --task-color por elemento (aire/fuego/tierra/agua),
  .active glow del color + inset border bottom.
- .taskbar-home con svg de casa dorado, hover glow.
- Responsive: taskbar 46px en mobile + ajustes.
- .brand transform agrega rotateZ(--tilt-z) para que el título vibre con
  la chacana en click impulses.

Workspace verde + 18 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 01:46:05 +00:00
sergio fce630c8d0 feat(gioser): sol detrás, título central, drawers MD + pluma agnóstico
Visual de la chacana retrabajado contra chakana.png de referencia:
- Sol detrás (gauss + corona, masked al interior de la chacana — sólo
  asoma por la superficie de la cruz, no se cuela afuera).
- Doble outline dorado (línea principal + paralela offset 0.020), color
  CHACANA_LINE pasa de cyan helado a dorado-ámbar del logo.
- Interior con niebla violeta-noche (u_dark_color) y rayos radiales
  sutiles desde el centro, modulados por sin(t * 0.3).
- Aro doble exterior: ring fino interior + ring grueso con 4 grupos de
  3 puntos cardinales (calculados angularmente, no rayos largos).
- WORLD_SCALE 1.45→1.05, MAX_TILT 35°→28° (más sólido, menos caricaturesco).

Título "GioSer" centrado dentro de la superficie de la chacana, sin
subtítulo. Se inclina junto con la chacana vía CSS perspective +
rotateX/rotateY desde u-tilt-x/y inyectadas cada frame por WASM.

Botones (4 tips):
- Reposicionados a `arm_extent * 1.32` (entre punta y aro grueso).
- Bigger: min-width 168px, glyph 54px, label Cinzel 0.95rem.
- Doble anillo en hover (::before con border + glow).
- Cuando un drawer se abre, fade-out de tips + canvas + brand.

Drawers MD (uno por elemento):
- `<aside class="drawer drawer-{element}">` con transform-origin desde
  CSS vars (--origin-x/y) seteadas por WASM al click — crece desde la
  posición exacta del botón hasta fullscreen en 700ms con cubic-bezier.
- Ambience por elemento: AIRE (radial drift), FUEGO (flicker keyframe),
  AGUA (tide vertical), TIERRA (warm earth gradient).
- Cerrado con botón X, Escape o data-close-drawer.
- Carga MD desde ./md/{element}.md via spawn_local + Reader::open_url.

Pluma (visor MD agnóstico, dos crates nuevos):
- `crates/modules/pluma/pluma-md` — wrapper sobre pulldown-cmark 0.12.
  API: to_html(), to_themed_html(md, theme) con sanitización del theme,
  events() para AST stream. GFM completo. No deps web. 5 tests.
- `crates/modules/pluma/pluma-reader-web` — toma HtmlElement, expone
  open_url async (fetch via wasm-bindgen-futures), render_md sync,
  show_loading/show_error. NO inyecta CSS — el host estiliza
  `.pluma-doc[data-pluma-theme="..."]` con sus colores.

CSS pluma-doc completo: h1/h2/h3, code/pre con border-left accent,
blockquote, tables, lists, hr gradient. Loader spinner + error state.

Placeholders en md/{aire,fuego,tierra,agua}.md con texto seed.

Workspace verde + 18 tests (6 geom + 4 palette + 3 physics + 5 pluma-md).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 23:38:37 +00:00
sergio e701df4a2b feat(gioser): chacana mística stepped, nubes/estrellas/meteoros, tilt 35°
- gioser-geom: ChacanaSpec paramétrica con `steps` (default 2). bounding box
  cuadrado (no cruz alargada), centro 6s×6s, brazos cortos de 2 niveles que
  adelgazan hacia la punta. arm_extent = 0.65 con thickness=0.13.
- gioser-shaders: nubes FBM 5× más rápidas, 3 estratos de estrellas con
  twinkle independiente, 4 meteoros procedurales con cola/cabeza y vida
  cíclica. Chacana SDF rediseñada para 2 escalones, aro doble (interior +
  exterior), 12 rayos angulares y 4 marcas cardinales animadas.
- gioser-canvas-web: MAX_TILT 22°→35°, WORLD_SCALE 0.92→1.45, spring
  1.8 Hz / ζ=0.62 (más languido). uniform `u_center_half` agregado.
  Las puntas DOM se desplazan visiblemente con el tilt.
- README: fix wasm-bindgen-cli 0.2.99 → 0.2.121 + `--locked`.

13 tests pasan (6 geom + 4 palette + 3 physics).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 14:23:23 +00:00
sergio d1ce4c8970 feat(lapaloma-financial): OHLC + candlesticks con preserva-volatilidad
- axis.rs: paint_axes extraído a función pública reusable entre
  crates de visualización. LapalomaChartElement::paint_axes ahora
  es un thin wrapper.
- OhlcBuffer: stride 6 f32 por bar (t, o, h, l, c, v). Bar struct
  con is_bull/is_bear. price_range y time_range. 5 tests.
- aggregate_time_bucketed (sección 3.2 del ARCHITECTURE.md):
  buckets por TIEMPO (no índice) — open=first, close=last,
  high=max, low=min, volume=sum. Preserva volatilidad (los wicks
  sobreviven al downsample, a diferencia de LTTB). Fallback a
  copy 1:1 si el span temporal es cero. 4 tests cubren bucket
  count, preservation of volatility, fallback, empty input.
- paint_candlesticks: render agnóstico contra el trait Canvas.
  Wick = stroke_line vertical (high → low). Body = fill_rect
  open ↔ close con color bull/bear/neutral. body_width derivado
  del spacing entre bars (con body_min_width floor).
- LapalomaCandlestickElement: Element GPUI que reusa paint_axes
  + paint_candlesticks. Sin pan-blit cache en v0.1 (≤500 bars
  on-screen no lo necesita).
- crates/apps/lapaloma-financial-demo: random walk determinístico
  (xorshift32 inline + seed fijo) de 120 bars, pan + zoom + reset
  igual que el cartesian demo. Paleta nórdica para bull (#a3be8c)
  y bear (#bf616a).

60 tests verdes (28 cartesian + 20 core + 9 financial + 3 render).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 03:22:21 +00:00
sergio 2b8e990cf9 feat(lapaloma-cartesian): picture cache pan-blit
- ChartCache + ChartCacheHandle (Arc<Mutex<...>>) cacheable entre
  frames. El Render host crea uno con chart_cache() y lo pasa al
  Element con .with_cache(handle). Sin handle, cada frame rebuild
  completo (correcto pero sin la optimización).
- Hash estructural: plot rect + viewport.span (no x_min/y_min) +
  per-series (data.revision + data.len + stroke). 5 tests cubren
  estabilidad, pan no invalida, zoom invalida, data revision
  invalida, plot rect invalida.
- En paint: si el hash matches, pan-blit = copia las coords
  cacheadas con offset (dx_px, dy_px) calculado del diff entre
  viewport.x_min cached vs actual. Salteamos LTTB + projection.
- LineSeries::compute_projected expone el pipe LTTB + project_buffer
  como método público para que el Element pueda cachear sin pasar
  por paint().
- Demo multi-series usa el cache; header muestra "cache: N
  pan-blits / M rebuilds" en vivo para que se vea la métrica al
  draguear (pan-blits crece) y al zoomear (rebuilds crece).

Limitación v0.1 anotada en código: el doc canónico (sección 4.4)
usa una textura offscreen blitable; GPUI 0.2 no expone esa primitiva
directa. La impl actual cachea coords proyectadas y emite las
polilíneas con offset — mismo ahorro de CPU (saltea LTTB) sin GPU
texture cache.

51 tests verdes (28 cartesian incluyendo 5 nuevos del structural_hash,
20 core, 3 render).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 03:12:02 +00:00
sergio ab03a61db4 feat(lapaloma-phosphor): trail CRT con alpha decay + glow
- lapaloma-phosphor: feature `gpui` (default). LapalomaPhosphorElement
  divide el RingBuffer en N segmentos (default 16, configurable) y
  pinta cada uno como una stroke_polyline con alpha = (k+1)/N. El
  segmento más nuevo va con alpha 1.0, el más viejo casi
  transparente — efecto fósforo persistente.
- Cada segmento incluye el primer punto del siguiente para evitar
  gaps visibles entre tramos.
- Wraparound se parte en dos sub-polilíneas (no concatenadas) para
  no introducir la línea horizontal "del slot cap-1 al slot 0".
- Glow opcional: pasada adicional con width × glow_width_mult y
  alpha × glow_alpha — efecto halo CRT.
- crates/apps/lapaloma-phosphor-demo: misma señal sintética que
  stream-demo, paleta verde Tektronix (#9bff8c sobre #050805),
  trail 24 segs + glow 4× α 0.18.

Limitación v0.1: el doc canónico usa triangle strip con per-vertex
color (sección 4.3); GPUI 0.2 no expone esa API directa. La impl
actual es funcionalmente equivalente con N draw calls en lugar de 1.
Cuando wgpu directo esté disponible, swap inmediato sin tocar las
API públicas.

46 tests verdes (sin cambios; phosphor se valida via demo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 03:05:16 +00:00
sergio 4796f2652d feat(lapaloma-stream): osciloscopio CRT con RingBuffer en sweep mode
- lapaloma-stream: feature `gpui` (default). LapalomaStreamElement
  pinta un RingBuffer en modo sweep — dos polilíneas split-at-head
  (segmento [head..cap) viejo + [0..head) nuevo) para evitar la
  línea horizontal del wraparound. Pre-fill (count < cap) sólo
  pinta [0, head) para evitar el flat-line del 1.0.2 fix.
- y_range configurable, background opcional, padding.
- crates/apps/lapaloma-stream-demo: osciloscopio sintético con
  RingBuffer cap=512. Timer en cx.background_executor que hace
  push(synthesize(t)) + cx.notify() cada 16ms (60 Hz). Señal =
  suma de dos sinusoides desfasadas + jitter determinístico.
  Header muestra cap / head / filled% / t / revision en vivo.
- Workspace: registrada la app lapaloma-stream-demo.

Showcase del P2 zero-alloc: push(v) son 2 writes + 2 increments,
zero allocations per frame, ningún Vec se reasigna.

46 tests verdes (sin cambios; el stream se valida en runtime via demo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 02:58:25 +00:00
sergio 66d18ab47c feat(lapaloma-cartesian): multi-series en LapalomaChartElement
- Element ahora mantiene Vec<ChartSeriesItem> con DataBuffer +
  StrokeStyle + nombre opcional por serie. Builder add_series y
  add_series_named.
- En paint(), una pasada por cada serie reusando el mismo scratch.
  N series = N paint_path (no N × por punto). Cumple P3 del
  ARCHITECTURE.md por serie.
- `lapaloma_chart(data, vp, stroke)` queda como helper retrocompat
  para el caso una-serie.
- Demo: 3 series simultáneas (sin, cos, mix) con colores nórdicos
  + leyenda textual en el header.

46 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 02:55:15 +00:00
sergio 138307c2ba feat(lapaloma-demo): pan + zoom + double-click reset interactivos
- viewport.rs: `pan_fraction(fx, fy)` — pan en fracción del viewport
  independiente del plot_rect. Útil cuando el handler GPUI trabaja
  en coords de window y no conoce el rect interno del chart.
- lapaloma-demo: state machine de drag (DragAnchor con snapshot del
  viewport al click) + handlers on_mouse_down/move/up para pan,
  on_scroll_wheel con sensitivity exponencial 0.0015 para zoom
  anchor-preserving al cursor, on_click con click_count >= 2 para
  reset al viewport inicial. El header muestra estado dragging.
- Maneja ScrollDelta::Pixels (trackpad) y ::Lines (mouse wheel
  tradicional) unificando con line-height 16px.

46 tests verdes en lapaloma-{core,cartesian,render}.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 02:52:51 +00:00
sergio 97c09bc96a feat(lapaloma): backend GPUI + LapalomaChartElement + app demo
Cadena end-to-end DataBuffer → LineSeries → Canvas → gpui::Window
funcionando. cargo run -p lapaloma-demo abre una ventana con sin(x)
sobre 1024 muestras y una sola paint_path por frame.

- lapaloma-render: feature `gpui` opcional. WindowCanvas adapter
  traduce el trait Canvas a paint_quad/paint_path de gpui 0.2.
  Conversión RGB→HSL para integrar con el sistema de colores Hsla
  del resto del codebase yahweh. 3 tests de conversión.
- lapaloma-cartesian: feature `gpui` (default). element::LapalomaChartElement
  con impl Element + IntoElement. Arma WindowCanvas en paint() y
  delega a LineSeries — un solo paint_path por chart.
- crates/apps/lapaloma-demo registrado en workspace.

Limitaciones conocidas v0.1: clip stack, triangle strips y draw_text
no implementados (los necesitan phosphor / Sankey / axes; se
agregan en sus fases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 02:36:41 +00:00
sergio 52acaabcf4 gioser 2026-05-12 18:55:29 +00:00
sergio 6596c81271 feat(shipote): audit log persistente + HTTP gateway (fase S)
- Daemon escribe append-only a $XDG_STATE_HOME/shipote/audit.log además
  del tracing. Single-line: ts=<ms> uid=<peer> action=<verb> <detail>.
  Rotación simple a .log.1 al pasar 1 MiB.
- shipote-gateway: TCP listener 127.0.0.1:7378 default. POST /rpc traduce
  JSON ↔ postcard contra daemon socket. GET / health text. HTTP parser
  ad-hoc (~70 LOC), sin dep de hyper/axum. Sin auth — bind a localhost
  + SHIPOTE_TRUST_ANYONE=1 en prod.

E2E: curl --noproxy '*' POST /rpc → "Pong", Health JSON, Capabilities
JSON. Audit log persiste mutaciones con uid del peer.

85 tests pasan (features nuevos son binarios, no library mods).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 17:16:11 +00:00
sergio a9124449f9 feat(shipote): health endpoint + audit log + token-bucket real (fase R)
- Request::Health → Response::Health { version, uptime_ms, alive_*,
  active_flows, dirty }. CLI: shipote health.
- handle_client lee peer_uid una vez al accept. audit_request emite
  info!(target: "audit", uid, action, detail) por mutación (create/stop/
  run/pipeline.*/flow.drop). Reads omitidos. Filtrable con SHIPOTE_LOG=
  warn,audit=info.
- TokenBucket real reemplaza rate_limit_sleep: refill por wall time,
  capacity = 1s de rate, debt negativo dispara sleep proporcional.
  Permite burst real, no chunk-by-chunk uniforme.

85 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 26, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 16:58:10 +00:00
sergio 18c0344a52 feat(shipote): throughput card + rate-limit + snapshot incremental (fase Q)
- shipote-shell Flow channels card extiende con bytes_total + bytes/s
  por socket. Lookup helper evita borrows en closures.
- DiscernPolicy.max_bytes_per_sec: splitter task hace sleep proporcional
  al tamaño de chunk tras cada broadcast. Token-bucket simple v1.
- WorkspaceManager.dirty: AtomicBool. mark_dirty() en mutaciones que
  afectan al snapshot. save_snapshot skip si clean y path existe.
  restore_snapshot resetea dirty=false (hidratación no es mutation).

85 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 26, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 16:20:50 +00:00
sergio 3486949d24 feat(shipote): throughput + stats persistente + auth peer (fase P)
- FlowMeter (atomic u64 + rolling window 32 samples) en cada FlowChannel.
  flow_throughput() → (socket, bytes_total, bytes_per_sec). CLI:
  shipote flow throughput. Idle threshold 5s = rate 0.0.
- Snapshot v4 con stats_history persistente por workspace (cap 16).
  PersistedStats separado para evitar Instant. Restore hidrata el VecDeque
  con source="persisted".
- Auth SO_PEERCRED: daemon rechaza peers con uid distinto al propio.
  SHIPOTE_TRUST_ANYONE=1 = escape hatch documentado.

84 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 25, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:58:41 +00:00
sergio 1cce50b290 feat(shipote): collision detection + stats history server-side (fase O)
- Flow socket names usan pipeline_id full (ULID 26 chars) + edge_idx.
  Cero colisiones entre pipelines (ULID es único global). Fallback con
  suffix -N si el path existe (cap 1000 retries).
- WorkspaceState.stats_history (VecDeque cap 64) — workspace_stats
  appendea cada call. API workspace_stats_history(id, tail). Protocol
  WorkspaceStatsHistory. Shell pide history al primer probe → sparkline
  hidratada al boot, sobrevive restart del shell.

84 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 25, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:55:21 +00:00
sergio a823c40fe1 feat(shipote): drain shutdown + persist live pipelines + batched query (fase N)
- Daemon SIGTERM/SIGINT: snapshot ANTES, stop_with_grace(1s) de todos
  los workspaces DESPUÉS. Grace permite app-level cleanup.
- Snapshot v3 con live_pipelines: pipeline_supervisors se persisten;
  daemon relanza al restore con sus recursos (Incarnator+DiscernPipeline).
  RestoreOutcome separado para que core no necesite incarnator.
  Forward-compat v1/v2 via #[serde(default)].
- WorkspaceFullSummary: stats+quota+commands+flow_sockets en 1 roundtrip.
  Shell reduce N×4 requests/probe a N×1 + 4 globales.

83 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 24, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:48:11 +00:00
sergio c3f9c9e36a feat(shipote): pipeline backoff + quota card + logs follow (fase M)
- PipelineSpec.restart_backoff_ms + restart_max_backoff_ms + restart_max:
  backoff exponencial entre relaunches (anti-thrash). take_pending_restarts
  aplica restart_max (0 = infinito); excedido = supervisor descartado con
  warning. Daemon hace tokio::sleep(backoff) antes del relaunch y escala
  current_backoff x2 hasta el cap.
- shipote-shell card "Quota breaches": probe extiende con WorkspaceQuota
  por workspace. Color rojo si hay breaches, verde si no.
- shipote logs --follow: poll cada 200ms al daemon, imprime suffix nuevo
  hasta que el comando termine. Sin cambios al protocolo. Best-effort:
  si el ring rota más rápido que el poll, se pierden bytes.

83 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 24, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:34:27 +00:00
sergio 4c9d1b4c1d feat(shipote): quota enforce + cgroup memory.max + pipeline restart (fase L)
- WorkspaceSpec.quota_enforce: QuotaAction (None|Log|Kill) por recurso
  (mem, nproc). reap_dead aplica policy; Kill usa stop_with_grace(ZERO).
- ente_incarnate::cgroup::apply_rlimits_to_cgroup escribe memory.max y
  pids.max. WorkspaceManager::create_with_id lo invoca si soma.cgroup.path
  y delegation. Kernel hace OOM kill al exceder; falla silenciosa si no
  hay delegation.
- PipelineSpec.restart_on_failure: bool. register_pipeline_supervisor
  retiene spec; reap_dead detecta all-dead + any-failed → push a queue;
  daemon reaper drena y relanza pipeline ENTERO (los pipes intermedios
  no permiten restart parcial).

82 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 24, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:22:46 +00:00
sergio 324a0c2d5d feat(shipote): multi-core CPU% + quota report + restart-on-failure (fase K)
- WorkspaceStats.cpu_cores via sysconf cacheado. CLI muestra
  `cpu_pct: 98.7 % (24.7% total / 4 cores)`.
- workspace_quota compara SomaSpec.rlimits contra accounting actual.
  Reporta breaches humanos. NO enforcement automático en v1.
- run_with_options(.., restart_on_failure): si exit != 0, reaper
  relaunch con backoff exponencial 200ms → 30s cap. Inner.restart_specs
  persiste el spec entre intentos.

81 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 22, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 01:32:39 +00:00
sergio d8727a3038 feat(shipote): CPU% + pipeline live-tail + replay por bytes (fase J)
- CPU% derivado server-side entre samples (WorkspaceState.last_cpu_sample).
  100% = 1 core saturado. Primer sample devuelve None (sin baseline).
- shipote pipeline run --tail: tras lanzar, suscribe al primer flow_socket
  y vuelca bytes hasta EOF. Auto-implica --tap.
- DiscernPolicy.replay_bytes: cap adicional por bytes para el replay
  buffer del FlowChannel. evict_for_incoming considera el chunk entrante
  para que post-push el buffer NUNCA exceda los caps.
- shipote-shell: stats history extiende sparkline con %CPU.

80 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 21, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:57:04 +00:00
sergio 36dac00c8d feat(shipote): data plane + DAG fan-in/out + stats + lifecycle (fases F-I)
Pipeline runtime:
- Fan-out 1→N (splitter task replica al N consumers) y fan-in N→1 (merger
  task con mpsc + reader-per-input). DAGs no lineales soportados.
- Flow channels: Unix socket + tokio broadcast con replay buffer
  configurable por pipeline (DiscernPolicy.replay_chunks). Subscribers
  externos vía `shipote flow tail <socket>`.
- Templating en specs con `${KEY}` (CLI `--var KEY=VALUE`). Walk
  recursivo sobre serde_json::Value, soporta todos los strings del schema.
- Pipelines guardados (`pipeline save/saved-list/drop/run-saved`)
  persisten con el snapshot.

Lifecycle de comandos:
- Log capture per-stream (stdout/stderr separados) via pipe O_CLOEXEC +
  AsyncFd. CLI `shipote logs <ws> <cmd> --stream {stdout,stderr,both}`.
- Stop graceful con tiempo configurable: SIGTERM → grace → SIGKILL.
  Tanto a nivel workspace como pipeline individual.
- TTL auto-stop ya existente (Fase C) sigue funcionando.

ente-incarnate:
- ChildStdio declarativo (Fase C) + ChildPreExec declarativo nuevo:
  NoNewPrivs, ParentDeathSig, Dumpable, NewSession, Chdir, Umask.
- Aplicación pre-execve async-signal-safe en ambos paths (plain via
  Command::pre_exec, namespaced via callback del clone(2)).

Observabilidad:
- WorkspaceStats: RSS + RSS peak (VmHWM o memory.peak cgroup) + CPU usec
  + uptime. Fuente per-proc o cgroup según delegation.
- shipote-shell con sparkline ASCII por workspace (history cap 24),
  card de flow channels activos, vista de comandos + saved pipelines.
- Tap → broker: cada edge enriquecido con TypeRef se anuncia como Card
  efímera vía SidecarPool (graceful si broker no corre).

Discern:
- Integrado en yahweh-provider-fs (mime_type en EntityNode).
- Integrado en nouser-core::cluster::pick_lens como fallback cuando la
  extensión cae a Lens::Grid.

79 tests pasan: ente-incarnate (16), nouser-core (27), shipote-card (8),
shipote-core (20), shipote-discern (5), yahweh-provider-fs (3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:29:46 +00:00
sergio c22d2480b9 shell 2026-05-10 21:58:16 +00:00
Sergio 3d55f189c0 feat(brahman-demo): bootstrap script reproducible — broker + producer + consumer + 4 explorers
Iter 22. Cierra el set de hoy: future-me (o cualquier nuevo collab)
levanta el escenario completo con un comando.

crates/apps/brahman-demo/ con 3 binarios:
- brahman-demo-broker: Server::bind standalone con Broker. Reemplaza
  a ente-zero para demos (ente-zero es PID 1 con kernel surface,
  child subreaper, bus, brain, audit — overkill).
- brahman-demo-producer: Card con flow.output[demo-stream:json].
- brahman-demo-consumer: Card con flow.input[demo-feed:json] —
  mismo type → matchea con producer.

Env vars en los 3: BRAHMAN_INIT_SOCKET, BRAHMAN_BROKER_CONTEXT,
BRAHMAN_DEMO_LABEL/FLOW/TYPE, RUST_LOG.

scripts/bootstrap-demo.sh:
- Modes: all (default) / broker / only.
- Cleanup-safe: trap mata todos los PIDs spawneados (SIGTERM grace
  + SIGKILL fallback) y borra el socket.
- Espera al socket antes de spawnear (evita ENOENT en handshake).
- Logs separados por proceso bajo $BRAHMAN_DEMO_LOG_DIR.

Smoke end-to-end (sin DISPLAY): consumer recibe MatchEvent
{ Available, demo-feed ← demo-stream, via: Exact, pinned: false }
automáticamente cuando entra el producer. Match fluye por el push
channel del broker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:06:11 +00:00
Sergio a97f6b98f3 feat(brahman-handshake): ListMatches endpoint + timeline en broker-explorer
Iter 21. Cierra el loop iniciado en iter 20: ahora se ven sesiones
+ matches actuales + cómo cambian a través del tiempo.

brahman-handshake/messages:
- Frame::ListMatches → Frame::MatchList(Vec<brahman_broker::Match>).

brahman-handshake/server:
- run_post_handshake pasa Option<&SharedBroker> a handle_inbound_frame.
- Sin broker configurado → MatchList vacía (no error).

brahman-handshake/client + brahman-sidecar:
- Client::list_matches() análogo a list_sessions, drena MatchEvents.
- list_matches / list_matches_blocking, mismo patrón.

brahman-broker-explorer:
- Poll-tick agrega list_matches_blocking además de list_sessions.
- last_match_keys: HashSet<MatchKey> para diff entre ticks.
- timeline: VecDeque<TimelineEntry> cap 50.
- diff_matches (free fn): Available para keys nuevas, Lost para
  desaparecidas. Primer tick marca todo Available (boot UX).
- Render: stat_card "Timeline" con HH:MM:SS {+/-} formato compacto.

5 tests broker-explorer (3 nuevos del diff). Stack verde.

Decisión: timeline polled cada POLL_INTERVAL=5s, no push. MatchEvents
del broker son consumer-céntricos (cada session ve sólo SUS matches);
"system-wide timeline" requeriría broker subscribe-all (mucho scope).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:53:38 +00:00
Sergio 99cd685dc1 feat(brahman-handshake): ListSessions endpoint + cliente + UI broker-explorer
Iter 20. Nuevo flujo end-to-end para observabilidad: cualquier módulo
conectado puede pedir al broker la lista de sesiones activas y mostrar
labels + flows in/out por cada una.

brahman-handshake/messages:
- Frame::ListSessions(ListSessions{session}) → Frame::SessionList(SessionList{entries}).
- SessionEntry: session, label, schema_version, outputs, inputs, conscious.

brahman-handshake/server:
- run_post_handshake pasa SessionRegistry a handle_inbound_frame.
- build_session_list helper proyecta el snapshot bajo lock.
- Validación session_id mismatched → Unauthorized.

brahman-handshake/client:
- Client::list_sessions() async, drena MatchEvents intermedios al
  pending_events buffer, mismo patrón que ping().

brahman-sidecar/discovery:
- list_sessions / list_sessions_blocking arman Card observer mínima,
  piden, Farewell.

brahman-broker-explorer:
- Poll-tick agrega list_sessions_blocking cuando broker está UP*.
- stat_card "Sesiones activas" con count + items ordenados por Ulid:
  label · in:[flows] out:[flows]  (wit)?.

Test list_sessions_returns_currently_registered: 3 clientes
conectados, observer pide list, verifica labels + schema_version
+ conscious=false. 24 handshake tests + sidecar + broker-explorer
verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:40:33 +00:00
Sergio 37e40073ef feat(yahweh-launcher): F3 — extracción del shell standard de explorers
Iter 19. Patrón con 4 consumers idénticos: cada main() repetía el mismo
~20 líneas de boot (Application::new + Theme::install_default +
cx.open_window + WindowOptions + cx.activate). Sólo varían título,
tamaño y root factory.

crates/modules/ui_engine/libs/launcher/:
- pub fn launch_app(title, size, root_factory) → 1-line boot.
- pub fn launch_app_with(config, root_factory) → variante con config
  armado afuera (env-var driven, etc).
- pub struct AppLaunchConfig::new(title, size).
- 2 tests cubren normalización del config.

Migración 4 consumers (nakui/nouser/minga/brahman-broker explorer):
- main() pasa de ~20 líneas a 1: launch_app(...).
- Imports gpui podados (no más App/Application/Bounds/WindowOpts/etc).
- Cada uno agrega dep yahweh-launcher.

Naming: yahweh-shell ya existe (bootstrap heavyweight con file/db/text
viewers en crates/apps/). Helper liviano queda como yahweh-launcher.

Ahorro ~75 líneas de boot hardcoded. Cambios de window/theme boot
ahora en un solo lugar.

2/2 tests launcher; 4 consumer suites intactas, todo verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:19:17 +00:00
Sergio be3a0e78fc feat(yahweh-widget-app-header): promover header standard de explorers
Iter 16. Patrón con 4 consumers idénticos: nakui-explorer,
nouser-explorer, minga-explorer, brahman-broker-explorer todos
declaraban un header flex_row + flex_grow(label) + theme_switcher
+ bg(panel) + border-bottom + text_size(14) + padding(16/12).
Ahora es 1 línea.

crates/modules/ui_engine/widgets/app-header/:
- pub fn app_header(cx, label: impl Into<SharedString>) -> impl IntoElement.
- pub fn app_header_with(cx, label_child: impl IntoElement) — variante
  para left side no-text.
- 3 tests #[gpui::test].

Migración 4 consumers:
- Cada uno: bloque de ~13 líneas → 1 línea app_header(cx, text).
- Borra dep yahweh-widget-theme-switcher (incluida vía app-header).
- Reemplaza import.

Ahorro ~50 líneas UI hardcoded. Cambios visuales del header (padding,
border, text_size) en un solo lugar.

Decisión: sidebar header del MetaApp NO se migra — es de sidebar,
no de app top, styling distinto. Diferente patrón → diferente widget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:40:56 +00:00
Sergio af7417ce08 feat(yahweh-widget-stat-card): promover patrón stat card como widget
Iter 15. El patrón "tarjeta de dashboard con border-l accent +
label + value grande + descripción + listing opcional" tenía 2
consumers (minga-explorer + brahman-broker-explorer). Ahora vale
extraer al stack yahweh.

crates/modules/ui_engine/widgets/stat-card/:
- pub fn stat_card(cx, label, value: impl Into<SharedString>,
  description, accent, text, text_dim, recent_items: &[String])
  -> impl IntoElement.
- Compone yahweh-widget-card::card_themed; sin theme directo
  (caller pasa text/text_dim ya resueltos).
- 3 tests #[gpui::test] con TestAppContext + theme: smoke con/sin
  items, type-check value.

minga-explorer:
- Borra fn stat_card local (~60 líneas).
- Borra dep yahweh-widget-card.
- 3 callsites pasan value.to_string() (widget acepta
  Into<SharedString>).

brahman-broker-explorer:
- fn state_card refactorizada como wrap del stat_card compartido,
  preservando la traducción ProbeState→(accent,value,description)
  como helper local app-specific.
- Borra dep yahweh-widget-card.

Sub-header del listing: pasa de "recent (N de TOTAL):" a
"recent (N):" — el widget no conoce TOTAL; el caller lo pone en
label si quiere ("Nodos AST (5 de 247)"). Trade-off aceptable
por reusabilidad genérica.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:33:02 +00:00
Sergio 1493d616fd feat(brahman-broker-explorer): nueva app probe del broker brahman
Iter 14. Cierra otro frente: visibilidad del broker handshake. La
app probe cada 5s vía await_provider_blocking y reporta 4 estados
claros (Pending / Down / UpNoProvider / UpWithProvider).

crates/apps/brahman-broker-explorer/:
- Deps: brahman-handshake + brahman-sidecar + stack yahweh themed.
- ProbeState enum con 4 variants.
- Polling cx.spawn cada 5s; el probe blocking se ejecuta en
  cx.background_executor().spawn para no congelar el main thread.
- Configuración via env: BRAHMAN_INIT_SOCKET, BRAHMAN_BROKER_PROBE_FLOW
  (default broker-health), BRAHMAN_BROKER_PROBE_TYPE (default ping).
- UI: header probe info + theme switcher; banner permanente
  Error/Warning/Success según estado; stat card con accent color.
- 2 tests sanity.

Smoke run verificado: bootstrap OK, panic esperado sin display.

Apps GUI themed: 5 (nakui-ui + 3 explorers + ahora broker-explorer).

Limitaciones: observer Card se registra/desregistra en cada probe;
no muestra lista global de Cards (handshake no expone API). Para
timeline real de MatchEvents hace falta mantener el Client vivo
entre probes — scope futuro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:23:16 +00:00
Sergio 99838b849b feat(minga-explorer): listings de items recientes en cada stat card
Iter 12. Hasta ahora minga-explorer mostraba sólo counts. Ahora
cada stat card muestra un sample de los 5 primeros items: hashes
truncados de nodos AST (con kind), atestaciones (content_hash ←
did_short) y claves MST.

- RepoSnapshot agrega 3 Vec<String/(String,String)> con recent
  items, cap RECENT_LIMIT=5.
- load_snapshot itera los stores con filter_map(Result::ok) +
  take(5). Errores per-item silenciados — dashboard tolerante a
  corrupción puntual.
- short_hash(&str) local: trunca a 12 chars (48 bits).
- stat_card extendido con recent_items: &[String]. Si no vacío,
  agrega "recent (N de TOTAL):" + una linea por item.

Tests: 2 → 4 (sanity defaults + short_hash).

Beneficio: tras `minga ingest`, el explorer muestra los hashes
de los nodos creados sin necesitar queries SQL.

Limitación: los "recent" no son cronológicos (sled ordena lex
por hash). Timeline real requiere timestamp al schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:37:31 +00:00
Sergio 2790b6dc8a feat(minga-explorer): nueva app dashboard del repo Minga sobre stack yahweh
Iter 11. Cierra integración del módulo Minga (VCS semántico P2P)
al ecosistema GUI. Antes Minga sólo tenía CLI; ahora hay un
dashboard GPUI con counts del repo en vivo.

crates/apps/minga-explorer/:
- Deps: minga-store + yahweh-theme + 3 widgets compartidos.
  Sin minga-cli (sin passphrase prompts) ni minga-core.
- PersistentRepo abierto directo (counts son lectura pública,
  sin passphrase). El DID sigue requiriendo `minga status` CLI.
- Refresh polling 2s (mismo pattern que nakui/nouser explorer).
- 3 stat cards: Nodos AST, Atestaciones, Claves MST. Cada una
  con border-l accent + label + número grande + descripción.
- Helper stat_card() factoriza la card.
- Header con título dinámico + theme switcher.
- error_banner themed.
- 2 tests sanity (missing dir errors, RepoSnapshot default).

Smoke run verificado: bootstrap completo OK, panic esperado en
open_window sin display.

Apps GUI themed: 4 (nakui-ui, nakui-explorer, nouser-explorer,
minga-explorer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:27:59 +00:00
Sergio 2f426b0171 feat(nouser-explorer): integración al stack yahweh themed
Iter 10. nouser-explorer (paralela a nakui-explorer pero para ver
Mónadas via daemon nouser) tenía colors hardcoded idénticos al
patrón previo. Aplico el mismo refactor: theme global instalado,
chrome a slots, widgets compartidos.

- Nuevas deps: yahweh-theme + 3 widgets (banner, card,
  theme-switcher).
- main() instala Theme::install_default.
- render: 4 vars rgb() chrome → theme slots.
- Header: flex_row + theme switcher a la derecha (mismo pattern
  que nakui-explorer).
- error_banner: div hardcoded → banner_themed(Error).
- 2 cards (Engine/Monad): div().flex_col().p().bg().rounded()...
  → card_themed(cx).border_l_4().border_color(accent).
- Accents semánticos (engine cyan, data purple) quedan locales
  como señales de dominio.

Las dos apps explorer del repo ahora comparten paleta themed +
switcher común.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:09:33 +00:00
Sergio 8fdef818cc feat(yahweh-widget-theme-switcher): control para ciclar themes en runtime
Iter 6. Cierra el ciclo del theme: ya teníamos paleta themed +
widgets que la consumen, faltaba el control UI para rotar entre
presets en vivo. Ahora hay un botón yahweh que muestra el theme
actual y al click avanza al siguiente.

crates/modules/ui_engine/widgets/theme-switcher/:
- pub fn theme_switcher(cx: &mut App) -> impl IntoElement: botón
  clickable con bg=panel_alt, hover=row_hover, label "Tema: <name> ▸".
  Al click hace Theme::set(cx, Theme::next_after(current.name)).
- 2 tests #[gpui::test]: smoke + verificación de cambio de global.
- Dev-dep gpui con test-support.

Migración:
- nakui-explorer: header pasa a flex_row con label flex_grow + switcher
  alineado derecha.
- yahweh-widget-meta-form (sidebar): mismo pattern en el header
  "Nakui" del sidebar.

Tests stack: 115 → 117.

Beneficio: click cambia toda la paleta en vivo. 6 presets disponibles
(Nebula, Aurora, Sunset, Flat Dark, Solarized Light, High Contrast)
ciclables circularmente.

Limitación: TextInput entities tienen colors hardcoded; migrar
text_input al theme es iter futura.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:37:49 +00:00
Sergio 9632d8c4a7 feat(yahweh): theme integration en banner + card + nakui-explorer consume themed
Iter 4 de integración nakui↔yahweh. Los widgets banner y card
ofrecen variants _themed(cx, ...) que leen Theme::global(cx).
Versiones sin theme preservadas para apps sin theme global.

yahweh-widget-card:
- Nueva dep yahweh-theme.
- pub fn card_themed(cx: &App) -> Div: card() + bg(theme.bg_panel).

yahweh-widget-banner:
- Nueva dep yahweh-theme.
- pub fn banner_themed(cx, kind, msg) -> Div: deriva (bg, fg) según
  kind + theme.is_dark. Info usa accent del theme; Success/Warning/
  Error usan hue fijo (verde/amber/rojo) + lightness flippeada.
- pub fn themed_colors(kind, theme) -> (Background, Hsla) helper.
- 3 tests nuevos del derivation.

nakui-explorer:
- Nueva dep yahweh-theme.
- main() instala Theme::install_default antes de open_window.
- render: 5 vars rgb() locales → theme slots (bg_app/fg_text/etc).
- card() → card_themed(cx). banner() → banner_themed(cx).
- Accents semánticos del log (seed azul, morphism verde) quedan
  locales: son señales del dominio, no chrome.

Tests: 112 → 115 (+3). Stack intacto.

Beneficio: cambiar de Theme refleja en nakui-explorer automático.
Próximo candidato: migrar MetaApp (paleta hardcoded de 6 colors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:11:29 +00:00
Sergio 1be7bf9b1e feat(yahweh-widget-card): container card-shape compartido para timeline entries
Iter 3 de integración nakui↔yahweh. El card visual pattern (padding
consistente + rounded + flex_col + gap) que vivía duplicado en cada
timeline entry de nakui-explorer ahora es un widget yahweh reusable.

crates/modules/ui_engine/widgets/card/:
- pub fn card() -> Div: flex_col + px(12) + py(8) + mb(4) +
  rounded(4) + gap(2). Sin colores (caller decide via builder).
- 1 test smoke.

nakui-explorer:
- Los 2 timeline entry patterns (Seed/Morphism) pasan de ~7
  calls a ~3, intención "card with accent" emerge del nombre.

Tests stack: 111 → 112. App-agnostic — el widget no impone paleta,
permite themes diversos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:02:23 +00:00