Compare commits

287 Commits

Author SHA1 Message Date
Sergio 88a3aae762 portada: reticula 2px, hover con glow masivo rgba(220,0.35) que llena, cuadrante bg+inset 2026-05-24 02:55:18 +00:00
Sergio ff28b3c5e5 portada: reticula 1.5px semi-brillo, glow 20px en hover, cuadrante con inset shadow 2026-05-24 02:53:38 +00:00
Sergio 6cb63b266b portada: reticula sutil 1px rgba(150,150,150,0.25), brillo + glow en hover 2026-05-24 02:51:09 +00:00
Sergio 6facc3c8ed portada: reticula via pseudoelemento ::before con gradientes, circulo y lineas 3px 2026-05-24 02:48:56 +00:00
Sergio b6330a22b4 recompilar wasm con canvas funcional z-index -1, reticula 3px #777 2026-05-24 02:46:11 +00:00
Sergio de97c34965 portada: reticula 3px #777, z-index 30, canvas z-index -1, hover via reticula-lit 2026-05-24 02:44:06 +00:00
Sergio be127def7a portada: reticula a 2px #444, z-index 30 sobre todo, circulo 28px 2px 2026-05-24 02:42:12 +00:00
Sergio 031a164f47 portada: alineacion flip corregida, reticula #3a3a3a, borde #333 2026-05-24 02:38:07 +00:00
Sergio 69a01460a3 portada: reticula mas visible (#2a2a2a), alineacion flip (izq=left, der=right), mas grande y espaciado 2026-05-24 02:35:46 +00:00
Sergio 05f916a7d1 portada: reticula ilumina en hover via JS, contenido centrado, orden 0x01|0x00 / 0x02|0x03 2026-05-24 02:29:52 +00:00
Sergio 2545d3108d portada chakana 2x2: reticula, 4 cuadrantes 0x00-0x03, hover metadatos, canvas opaco atras 2026-05-24 02:17:34 +00:00
Sergio d8866897ea tierra.md -> Modelo Triplanar: ontologia del campo consciente, 3 planos, vida, ego 2026-05-23 21:07:47 +00:00
Sergio 608a37749b tierra.md: resumir §II a Tesis — todo es consciente, posicion determina percepcion 2026-05-23 21:04:37 +00:00
Sergio 12e465037d tierra.md: agregar §II Campo de aplicacion — dominio, operadores funcionales, autocoherencia 2026-05-23 20:22:02 +00:00
Sergio 23e128355b tierra.md: limpiar metaexplicacion, quitar 'En espanol:', existencia como conciencia siempre 2026-05-23 20:18:09 +00:00
Sergio 9b00740796 tierra.md: reescritura completa — tono directo, axiomas en code, índice, enlaces internos 2026-05-23 20:10:58 +00:00
Sergio 794d3e932e expandir a 9 páginas: +cuerpo, sombra, cosmos, practica, olvido 2026-05-23 19:56:08 +00:00
Sergio 3d7f92ccd7 tierra.md: integrar chip cognitivo, campo unificado, métrica, protocolo
- Expandir manifiesto con las 5 fases del pipeline de intervención
  (detección, pausa, modulación, redirección, mantenimiento)
- Agregar teoría de campo unificado: formalización A ≈ U, algoritmo
  de perdón como procedimiento operativo sobre memoria
- Agregar métricas: Co, H, IR, PA + protocolo de práctica
- Agregar ontología operativa, epistemología probabilística y telos
- Mantener estructura de 10 secciones con tono uniforme
- 63 fragmentos indexados en Qdrant
2026-05-23 18:44:12 +00:00
Sergio c61ea22097 tierra.md: fusionar manifiesto poético con sistema axiomático
- Combinar 'Manifiesto del Ser Desnudo' (texto experiencial) con
  'Propósito y alcance' (axiomas, ecuaciones, dinámica de sistemas)
- Estructura en 7 secciones que integran poesía + matemática:
  I. Origen, II. Cuerpo Formal, III. Dinámica, IV. Sombras,
  V. Medicina, VI. Práctica, VII. Centro de la Nada
- Reindexado en Qdrant (45 fragmentos)
- Respaldo del original (tierra.md.bak)
2026-05-23 18:42:49 +00:00
Sergio a072538465 gioser-web: fix graph click — clone Element before dyn_into
- dyn_into() consumes the value; clicking after that is a no-op.
- Clone the Element first, then dyn_into to HtmlElement and call click()
2026-05-23 17:02:40 +00:00
Sergio 97bea4c99f gioser-web: fix controls delegation (doc-wide), fix graph click (use dyn_into)
- Changed install_deck_delegation → install_controls_delegation:
  listener on document, not just #deck (controls are outside deck now)
- Graph node click: use dyn_into::<HtmlElement>() then click() instead
  of dyn_ref which may fail on <a> elements
2026-05-23 16:57:48 +00:00
Sergio 786a22debe gioser-web: fix controls CSS selector, use HTMLElement.click() for graph nav
- .page-controls visibility: changed from  (wrong) to

- Graph node callback: use HTMLElement.click() instead of
  dispatch_event(MouseEvent), which wasn't working (untrusted event)
2026-05-23 16:53:12 +00:00
Sergio 5f4d260301 gioser-web: move page-controls to static HTML, simplify sync
- Moved the minimize/close buttons from dynamic JS creation to
  static HTML in index.html (always present, hidden by default)
- sync_page_controls() now just toggles opacity/pointer-events
  instead of creating DOM elements
2026-05-23 16:52:03 +00:00
Sergio fa8dfd6ed3 gioser-web: ambient page bg: blur(60px) + centered gradient for seamless glow
- filter: blur(60px) so the radial-gradient is a soft uniform glow,
  no visible circle or division
- Centered at 50% 50%, opacity 0.30→0.60 breathe
2026-05-23 16:46:15 +00:00
Sergio 0381585745 gioser-web: fix graph node click callback, dispatch MouseEvent on tip
- Graph callback now creates a web_sys::MouseEvent('click') and
  dispatches it on the corresponding .tip element.
- The existing install_tip_clicks listener captures it and calls
  open_or_switch with proper coordinates.
2026-05-23 16:46:04 +00:00
Sergio dac1fc71ad gioser-web: absolute paths for CSS, JS, md to fix history.pushState routing
- Changed all relative paths (./styles.css → /styles.css,
  ./pkg/gioser_web.js → /pkg/gioser_web.js,
  ./md/*.md → /md/*.md) to absolute so they don't break when
  pushState changes URL to /estudio/aire etc.
- Caddy already has try_files fallback to /index.html for SPA routing.
2026-05-23 16:43:22 +00:00
Sergio 05f2e54ed1 gioser-web: draggable graph nodes, fixed controls, history.pushState routing
- Graph: nodes are draggable via pointer events. Snap back with
  spring transition on release (cubic-bezier 0.34,1.56,0.64,1)
- Removed hover bounce animation (was distracting)
- Page controls (minimize/close): now fixed position in viewport
  (top-right, z-index 100), not inside the deck-page scroll area.
  Created once in sync_page_controls() on show/hide deck.
  Controls detect active page when data-minimize/close-page is empty.
- Hash routing → history.pushState: URLs are /estudio/aire etc.
  popstate listener handles back/forward. Initial path read on boot.
- Added PointerEvent feature to gioser-graph-web Cargo.toml
- Added History feature to gioser-web Cargo.toml
2026-05-23 16:21:01 +00:00
Sergio 4c7d716c0c gioser-web: hash routing, clickable graph nodes, hover bounce, line normalization
- Hash routing: each page sets location.hash to #/element (aire/fuego/etc)
  - Hash read on boot to open direct URL
  - hashchange listener for browser back/forward
  - minimize clears hash
- Graph node click: dispatches click on the corresponding .tip element,
  triggering page open animation (expand from center)
- Graph hover: CSS bounce animation (scale 1→1.08→0.96→1, 0.4s)
- Edge normalization: min-max scale (w-0.80)/0.15 then squared curve
  for visible weight differentiation in tight cluster (0.83-0.93)
- web-sys features: +Location
2026-05-23 16:12:26 +00:00
Sergio 1fdbd358c8 gioser-graph: min-max normalize weights, subtle transparency
- All edge weights are 0.83-0.93 (tight cluster). Now:
  min-max scale: (w - 0.80) / 0.15 → 0.0-1.0
  then quadratic curve: norm² to exaggerate differences
- Stroke width: 0.8 + expanded*3.0 (subtle, not chunky)
- Opacity: 12-42% (transparent and subtle)
- Brightness: moderate, maps to expanded value
2026-05-23 16:03:57 +00:00
Sergio ac894390f9 gioser-graph: exaggerated thickness & brightness, edges behind with pointer-events:none
- Stroke width: 1.5 + w*8.0 (range 3-10px) for dramatic variation
- Brightness: w*0.7 factor, low weight = pure blended color, high weight ≈ white
- Opacity: 0.40 + w*0.55 (40-95%)
- Edges group gets .gb-edge-group { pointer-events: none } so clicks pass to nodes
- Visual layer: edges_group (behind) → nodes_group (front) in SVG
2026-05-23 16:00:28 +00:00
Sergio 906e5f639c gioser-graph: edges behind nodes, variable thickness by raw weight
- Edges and nodes now in separate SVG <g> groups (edges first = behind)
- Stroke width: 0.6 + w*4.0 instead of normalized range (more visible variation)
- Brightness: uses raw weight directly, not normalized against max
- Edges group has no breathing animation (only nodes breathe)
2026-05-23 15:53:00 +00:00
Sergio ef3d698c4b gioser-graph: edge colors blend from connected node caminos
- Each edge color is a 50/50 blend of its two nodes' camino colors
- Extra brightness proportional to weight (higher weight = closer to white)
- Edge opacity/width also varies by weight
- Edges drawn before nodes in SVG (already behind them)
2026-05-23 15:51:21 +00:00
Sergio 2588673caf gioser-graph: fix panic slicing multi-byte chars (á, ñ, etc)
- Use .chars().take(18) instead of byte slicing &node.name[..18]
  which panics on accented characters
2026-05-23 15:42:32 +00:00
Sergio fa2bedf851 gioser-graph: fix edge UUID mapping, index 4 docs, unify page bg
- Fix: map positions by node.id (UUID) not doc_id — edges now draw
- Index the 4 docs/ files into Qdrant (15 fragments via index-gioser-docs.py)
- Page background: single smooth radial-gradient per element (no color
  divisions), animated 'page-breathe' — opacity pulses 0.35↔0.80
- Graph CSS: 'graph-breathe' 5s opacity animation (feels alive)
2026-05-23 15:40:42 +00:00
Sergio b5032de1e3 gioser-graph: grid layout 3 cols, line brightness by weight, breathing animation
- Replaced force-directed layout with explicit grid: 3 columns wide,
  rows based on node count (minimum 3 visual rows)
- Nodes are larger: 170x44px with 8px radius
- Lines: color brightness + stroke opacity based on normalized weight
  (weight 1.0 → white #ffff, 0.5 → dimmer rgb)
- CSS animation 'graph-breathe': opacity pulses slowly (4.2s ease-in-out)
- Hover: drop-shadow glow + fill-opacity increase
- Glow circle behind each node (subtle)
2026-05-23 15:34:54 +00:00
Sergio 7f7ba1fef9 gioser-web: revert Cytoscape, back to SVG gioser-graph-web
- Remove cytoscape-graph.js (wasn't working with hidden deck)
- Restore gioser-graph-web Rust SVG widget with gioser-web dep
- Rebuild WASM (514K)
2026-05-23 15:28:41 +00:00
Sergio 24218447e3 gioser-graph: poll container width before Cytoscape init
- Uses setInterval(50ms) waiting for offsetWidth > 0
- Falls back to minWidth/minHeight after 10s timeout
- Solves 'cannot access property w' when deck is hidden (scale 0)
2026-05-23 15:25:45 +00:00
Sergio 9db79617eb gioser-graph: wrap init in requestAnimationFrame for hidden deck
- Wraps the entire fetch+render in requestAnimationFrame
  so the container has real size before Cytoscape runs cose layout
- Uses 'preset' layout first, then cose with animate:'end'
  to play nice with 0-size containers during deck animation
2026-05-23 15:24:15 +00:00
Sergio a908d8420c gioser-web: fix graph not appearing — use MutationObserver
- cytoscape-graph.js now uses MutationObserver, not DOMContentLoaded
  (the <gioser-graph> element is created dynamically by WASM)
- Remove unused dispatchEvent from lib.rs
- Rebuild WASM
2026-05-23 15:22:13 +00:00
Sergio d4c31d70b7 gioser-web: replace Rust SVG graph with Cytoscape.js
- Add cytoscape-graph.js: fetches /graph, renders with Cytoscape.js
- Style: round-rect nodes, cose layout, edge width proportional to weight
- Click: center node + fade rest (wineandcheesemap effect)
- Double-click: trigger navigation callback
- Hover: tooltip with preview text
- Click background: restore all
- Remove gioser-graph-web crate dependency (no longer needed)
- Add CDN cytoscape@3.30.4 + defer script to index.html
- gioser-graph custom element auto-initialized on mount
2026-05-23 15:17:28 +00:00
Sergio 529287f01d gioser-web: fix graph widget — rect nodes, edge weight, CSS anim, layout
- Switch from circles to horizontal rounded rectangles with text inside
- Text size 12px body + 8px sublabel (camino), no overlaps
- Edge stroke-width proportional to semantic weight
- Fix 'Layout was forced' warning
- Reduce CSS page-ambience animations: only opacity (no transform)
  to fix 'breathing background' visual glitch
- Layout: more separation (k*1.6), 80 iterations
2026-05-23 15:12:48 +00:00
Sergio 8235391add gioser-web: rebuild WASM with graph widget integration
- Fix missing reader declaration in spawn_local
- Rebuild pkg/ with wasm-bindgen 0.2.121
- Graph widget now appears below page content (SVG force-directed)
2026-05-23 14:57:13 +00:00
Sergio 7a53fea13c gioser-graph-web: fix web-sys feature casing and compilation
- SvgSvgElement → SvgsvgElement
- Add Response feature
- Make color owned String for closure lifetime
2026-05-23 14:49:26 +00:00
Sergio cb5d83b1f7 gioser-web: integrate gioser-graph-web widget below page content
- Add gioser-graph-web dependency to gioser-web
- After markdown loads, mount SVG semantic graph below content
- Graph fetches from api.gioser.net/graph endpoint
- Uses Qdrant k-NN edges, colored by camino
- Callback navigation placeholder (will be wired in next commit)
2026-05-23 14:40:35 +00:00
Sergio 38e95e0620 gioser-web: add gioser-graph-web module for SVG semantic graph
- New crate: gioser-graph-web (WASM widget)
- Fetches /graph from the gioser API
- Force-directed layout (Fruchterman-Reingold) in Rust
- SVG inline rendering: nodes clickable, colored by camino
- Agnostic container: caller provides div + callback
2026-05-23 14:38:42 +00:00
Sergio b17149c528 gioser-web: add docs/ dir with frontmatter for Qdrant indexing
- 4 md files (aire, fuego, tierra, agua) with YAML frontmatter
- caminos mapped: logos, nomos, kay, uku
- original md/ unchanged
- add scripts/index-gioser-docs.py (adapted from gioserv)
2026-05-23 14:36:20 +00:00
sergio 12e3b1d4d0 docs(renaser): plan de la Fase 21 — Atlas, el explorador del grafo
Plan trazado para mañana. Tres capas:

  1. Cuatro capacidades de host read-only (sys_grafo_manifiesto,
     sys_grafo_raiz, sys_grafo_recuperar, sys_grafo_hijo) que abren
     el grafo de objetos al userspace. Mismo patron de validacion
     de memoria que sys_net_*.

  2. La app 'atlas': lienzo ~520x400, lee el grafo perezosamente con
     cache LRU de 16 entradas, navega con flechas / Enter / Backspace
     desde el manifiesto hacia los hijos.

  3. Representacion radial: foco central con su firma cromatica
     (3 primeros bytes del hash), hijos en circulo, padre en cima
     si hay historial, cartela inferior con hash completo + tamaño
     + previsualizacion (texto si pasa el test ASCII, hex si no).

Subfases 21a (caps), 21b (app navegable), 21c (paseo guiado).

Sinergias y mejoras del sistema documentadas: dedup visual del
grafo direccionado por contenido, integridad por uso (cada
navegacion rehashea), validacion del almacen, camino natural a la
escritura (Fase 22), encaje con el faro Akasha de la Fase 20
(quien recibe AnunciarRaiz puede explorar la raiz del par).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 05:24:35 +00:00
sergio 42fee6fcbc feat(renaser): Fase 20 — Akasha Over Ether (grafo distribuido)
Tres mensajes y un EtherType propio bastan para extender el grafo de
objetos —direccionado por contenido, ya BLAKE3— a otras maquinas
renaser que escuchen en la misma red de capa-2. Sin TCP, sin IP,
sin DNS.

Crate nueva 'akasha/' (no_std compartido, gemela de 'formato',
excluida del workspace):

  - MensajeAkasha enum con SolicitarObjeto(id), ProveedorObjeto(id,
    payload), AnunciarRaiz(id).
  - Codec: postcard (mismo que ya usa el grafo en disco).
  - EtherType: 0x88B5. MAX_PAYLOAD_AKASHA = 1486 (MTU sin fragmentar).
  - Helpers componer_frame(src, dst, msg) y analizar_frame(bytes) que
    distinguen EtherType ajeno, frame truncado y payload basura.
  - 6 pruebas unitarias en verde.

Modulo nuevo 'kernel/src/akasha.rs' con tres oficios:

  1. Demuxer (drenar_y_demultiplexar): drena la cola RX del dispositivo
     virtio-net y demultiplexa: frames AoE con payload valido los
     procesa el respondedor; el resto va a una cola del userspace que
     'sys_net_recibir' ahora lee. Frames 0x88B5 con payload
     no-postcard (saludo de pregon) se cuentan y tambien viajan al
     userspace.

  2. Atencion de mensajes (procesar):
     - SolicitarObjeto(id): consulta almacen::recuperar; si tenemos el
       objeto, respondemos ProveedorObjeto unicast con objeto.serializar()
       y re-hashing de defensa en profundidad.
     - ProveedorObjeto(id, payload): verifica blake3(payload)==id antes
       de absorber con almacen::almacenar.
     - AnunciarRaiz(id): si ignoramos el nodo, le solicitamos al emisor.

  3. Faro periodico (difundir_raiz cada 5 s): broadcast del hash del
     manifiesto actual. Cadencia medida contra reloj::milisegundos(),
     no contra los awaits — el interprete wasmi de los apps degrada
     la cadencia de EsperaFrame::await a varios cientos de ms, asi
     que se mide contra el reloj monotono y los oficios per-fotograma
     se enganchan al tic del compositor (cuyo latido es fiable).

Contadores ResumenAkasha (rx/tx por variante, descartados, cola del
usuario) listos para un futuro indicador AoE en la barra de tareas.

Cambios complementarios:

  - sys_net_recibir lee de akasha::pop_usuario, no de
    drivers::red::recibir_en (que queda #[allow(dead_code)] como
    primitiva del driver para diagnostico).
  - tarea_red queda corta: envia un ARP al gateway y termina. El
    demuxer y el faro viven en el tic del compositor.

Verificacion:

  - 'cargo test -p akasha' → 6 pruebas en verde.
  - QEMU headless 60 s con -object filter-dump → 14 frames: 11
    AnunciarRaiz (Δ promedio 5.86 s sobre 5.00 s de target), 2 ARP
    y el pregon hello. Cada AnunciarRaiz lleva el hash del manifiesto
    '2f3deadfcc7dae25..' en 33 bytes postcard sobre 47 bytes de frame.
  - COM1 vuelca 'akasha :: ANUNCIO emitido :: raiz=2f3deadfcc7dae25..'
    en cada disparo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 05:14:43 +00:00
sergio 07ab095d42 feat(renaser): Fase 19 — voz del userspace hacia la red (pregon)
Tres capacidades nuevas en wasm/env (12-14):

- sys_net_mac(salida) -> i32: escribe los seis bytes del MAC del
  dispositivo. 0 OK, -1 si no hay red.
- sys_net_enviar(ptr, len) -> i32: envia un frame Ethernet crudo.
  Valida rango contra la memoria lineal del modulo.
- sys_net_recibir(salida, capacidad) -> i32: drena UN paquete por
  llamada hacia el buffer del modulo. Devuelve los bytes copiados, 0
  si nada pendiente, codigos negativos diagnosticos.

Añadida red::recibir_en(buf) -> usize como su contraparte del driver:
gemelo cooperativo de drenar_rx que aterriza en un buffer del usuario.

App nueva pregon (apps/pregon/, 4.2 KiB WASM): lienzo 480x160, tipografia
8x8 (font8x8) escalada x2. Al init pide su MAC y anuncia su presencia
con un broadcast Ethernet — destino FF:FF:FF:FF:FF:FF, EtherType
experimental 0x88B5, payload ASCII 'renaser :: hola desde mi red'. En
cada tick drena un paquete con sys_net_recibir y muestra el titulo, el
MAC propio, las cuentas TX/RX, y los datos del ultimo frame entrante.

GENESIS 8 -> 9 apps (pregon en posicion 2 detras de bitacora);
CELDA_TASKBAR_ANCHO 130 -> 116 px para que las nueve pestañas + lanzador
+ reloj caben holgadas en 1280 px.

tarea_red del kernel ya no drena RX (la cola pertenece al userspace),
conserva solo el envio del ARP de prueba al arrancar.

Verificada en QEMU con -object filter-dump. El pcap captura tres frames
en orden: (1) broadcast 88B5 de pregon con su payload, (2) ARP request
del kernel, (3) ARP reply del gateway 52:55:0a:00:02:02. La consola
anuncia 'manifiesto :: 9 apps nacidas del grafo'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:26:22 +00:00
sergio b1be94e7c4 fix(install-arje): fuentes (DejaVu/Cantarell/Noto) en runtime libs
GPUI panic-ea si no encuentra ninguna fuente del fallback
(.SystemUIFont → Helvetica → Cantarell → DejaVu Sans → ...).
Fedora minimal no trae fuentes — sin esto el greeter crash al
inicializar text_system.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:11:57 +00:00
sergio 54f51ad2f8 fix(install-arje): vulkan-loader + lavapipe + seatd en runtime libs
GPUI (mirada-greeter) requiere libvulkan.so.1 al iniciar; sin un ICD
no levanta. mesa-vulkan-drivers trae lavapipe (Vulkan por software vía
llvmpipe), apto para VPS sin GPU real.

También se agrega seatd al hint (preferido sobre noop para producción
porque maneja VT-switching correctamente).

El check de runtime ahora detecta libEGL, libvulkan, libgbm y libseat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:10:38 +00:00
sergio bdd088b89e feat(renaser): Fase 18 — red: virtio-net y el primer hola al exterior
renaser hablaba consigo mismo. Esta fase abre una boca y una oreja al
exterior con una tarjeta de red, reutilizando el `KernelHal` del
disco y el mapeador MMIO (la pieza estructural que hizo esto posible).

- `drivers/red`: monta `VirtIONet<KernelHal, PciTransport, 16>`,
  expone `enviar(frame)` y `drenar_rx(callback)`. Sin pila TCP/IP —
  solo Ethernet crudo; la composición de paquetes la hace el llamante.
- `componer_arp_request(mac, ip, objetivo)` construye el saludo
  inicial: «¿quien tiene 10.0.2.2?» dirigido al gateway de QEMU.
- `interrupts::registrar_irq_red` + handler `irq_red`, gemelo del de
  disco. La IRQ del dispositivo activa `red::atender_irq`, que hace
  `ack_interrupt` y suelta la línea.
- `tarea_red` en el reactor: al arrancar envía el ARP, después cada
  fotograma drena la cola RX y vuelca cada paquete a COM1.
- QEMU args ganan `-netdev user,id=net0 -device virtio-net-pci`.

Verificado con `-object filter-dump,...,file=/tmp/red.pcap`:
  red :: virtio-net :: MAC 52:54:00:12:34:56 :: IRQ Some(11)
  red :: ARP REQUEST enviado :: ¿quien tiene 10.0.2.2?
  red :: RX 64 bytes :: src=52:55:0a:00:02:02 type=0x0806

El src del paquete entrante (`52:55:0a:00:02:02`) codifica `10.0.2.2`
dentro del MAC — es el gateway de QEMU respondiendo. Renaser ya habla
con el exterior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:06:23 +00:00
sergio 60553bec44 feat(renaser): Fase 17 — bitácora, el editor que recuerda
memoriosa (Fase 7c) demostró que un app podía persistir su huella.
Esta fase la lleva al gesto natural: un editor de texto. Tecleas,
reinicias renaser, el texto sigue ahí. La huella vive en el grafo
de objetos como todo lo demás.

- Nuevo crate `apps/bitacora/`: lienzo 480×280, tipografía 8×8
  embebida (`font8x8 = "0.3"`) escalada x2 a 16×16, render pixel
  a pixel desde la memoria del propio app. Buffer 512 bytes con
  wrap automático a 28 columnas; `Enter` salta línea, Backspace
  borra; al desbordar el buffer se descartan los 64 primeros para
  amortizar la mudanza. Cada cambio invoca `sys_estado_guardar`;
  al arrancar, `init` llama a `sys_estado_cargar` y reconstruye.
- Mapeo de scancodes US a ASCII (letras, dígitos, puntuación
  básica, espacio). Sin shift ni mayúsculas — minimalismo.
- `GENESIS` crece de 7 a 8 apps; `bitacora` es la PRIMERA — gana
  la celda maestra al arrancar y te invita a teclear.
- `CELDA_TASKBAR_ANCHO` baja de 150 a 130 px para que las ocho
  pestañas + lanzador + reloj quepan holgadas en 1280 px.

Verificado en QEMU: tras escribir "hola renaser" y reiniciar el
kernel con el mismo disk.img, bitácora muestra el texto donde lo
dejó. El `almacen` reporta 24 objetos en el grafo (frente a 9
antes de escribir) y `raiz presente` — cada `guardar` anexó una
versión al log direccionado por contenido.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 03:52:20 +00:00
sergio e100c5acff fix(install-arje): pixman-devel para mirada-compositor (smithay)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 03:39:51 +00:00
sergio 71ebcea899 feat(renaser): Fase 16 — la barra viva: botón lanzador + reloj
La barra de tareas era pasiva: nombraba pero no hacía. Ahora lleva
en sus extremos los dos adornos de toda barra digna.

- Botón «+» indigo a la izquierda (36 px). Un clic incrementa
  `PARTOS` — el mismo contador que `Alt+N` — y la tarea del
  compositor lo recoge para lanzar la siguiente app de la rotación.
  Teclado y ratón comparten ya la misma vía para crear ventanas.
- Reloj `mm:ss` a la derecha (80 px), leído de
  `reloj::milisegundos()`. Tinta blanca sobre slate.
- `compositor::tick_reloj()` lo invoca la tarea del compositor cada
  fotograma; recompone solo cuando el segundo del reloj monótono
  cambia respecto al último mostrado (`ULTIMO_SEGUNDO: AtomicU64`).
  Cero coste mientras no toca refrescar.
- `Taskbar` crece con `launcher`, `reloj` y `reloj_region`; el
  layout de las pestañas se ajusta entre ambas cuñas. La cruz del
  lanzador se dibuja en píxeles directos —dos rectángulos cruzados,
  independiente de la tipografía—.

Verificado en QEMU con dos capturas separadas: la barra muestra el
«+» indigo, las siete pestañas (con `glotona` ya legible) y el
reloj. En la primera marca `0:17`; diez segundos después, `0:29` —
la barra se refrescó doce veces sin intervención.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 03:36:46 +00:00
sergio 0422780dd9 fix(install-arje): libxkbcommon-x11-devel para mirada-greeter
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 03:34:57 +00:00
sergio 08ec152b7f fix(install-arje): pam-devel en la lista de dev-libs (brahman-auth → pam-sys)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 03:23:41 +00:00
sergio 9fe9c8319e fix(install-arje): clang-devel en la lista de dev-libs para libclang.so
Sin clang-devel, los crates *-sys que corren bindgen (libinput-sys,
wayland-sys) fallan en runtime con 'couldn't find libclang.so'. El
mensaje de WARN ahora lo lista explícitamente.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 03:12:40 +00:00
sergio d40382ad01 feat(arje): cadena DM activa — kmod virtio_gpu + carmen-dm en arje-prod
- seed arje-prod gana dos Cards antes de getty-tty1:
  * kmod-gpu (OneShot): modprobe virtio_gpu — pone /dev/dri/card0
    listo antes de que el compositor intente abrir DRM/KMS.
  * carmen-dm (Restart): /usr/bin/mirada-compositor --greeter --drm,
    con MIRADA_GREETER_BIN apuntando a /usr/bin/mirada-greeter.

- install-arje-as-init.sh gana paso 1b: build nativo (no-musl) de
  mirada-compositor y mirada-greeter. GPUI/Mesa/EGL son dinámicos
  contra glibc — la cadena DM no puede vivir en el binario musl
  estático de arje-zero. Si el build falla por dev-libs ausentes,
  se imprime la lista de paquetes Fedora que faltan y se sigue
  (la instalación de arje-zero no se rompe).

- En el paso de instalación, los binarios del compositor se copian
  a /usr/bin/ sólo si el build de paso 1b tuvo éxito; ldconfig
  comprueba libEGL en runtime y avisa si falta.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 02:51:07 +00:00
sergio 28c2e6af18 feat(renaser): Fase 15 — la voz del sistema (acorde + eventos)
La bocina pertenecía al app enfocado (Fase 12), pero el kernel
necesita hablar también. Ahora tiene voz propia, prioritaria.

- `altavoz`: cola `SECUENCIA: Mutex<VecDeque<(u32,u32)>>` (freq, ms)
  + reloj `FIN_NOTA: AtomicU64`. `agendar(&[...])` encola;
  `atender()` (tarea del compositor cada fotograma) avanza la
  secuencia y silencia al acabar; `kernel_sonando()` gatea a los
  apps — mientras el kernel suena, `sys_tono` no-op.
- Catálogo: VOZ_BIENVENIDA (Do5-Mi5-Sol5, 500 ms), VOZ_LANZAR
  (700→1050 Hz), VOZ_CERRAR (900→520 Hz), VOZ_DESALOJO (180 Hz).
- Hitos: `kernel_main` agenda el acorde antes de `ejecutor.run`;
  `nacer_ventana` (Alt+N), `cerrar` (Alt+Q), `desalojar` (falla)
  agendan al hacer su trabajo.
- De paso: las pestañas de la barra de tareas calculan su tinta por
  brillo del fondo (ITU-R BT.601); la pestaña crema del desalojo por
  memoria, que llevaba texto blanco invisible, ahora luce su nombre
  en tinta oscura.

Verificado en QEMU con `-audiodev wav -machine pcspk-audiodev=spk`:
el PCM crudo trae, en orden, el acorde de bienvenida (~520, 630, 760
Hz), un brevísimo 180 Hz (las balizas de discola/glotona desalojadas)
y después la escala de Do mayor de tonada.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 02:21:37 +00:00
sergio 6a29152feb feat(renaser): Fase 14 — identidad del escritorio (nombres + barra de tareas)
Las ventanas eran anónimas: el escritorio no sabía nombrar lo que
mostraba. Esta fase le pone un nombre a cada cuarto y una barra al
pie con la lista de quienes lo habitan.

- Cada `Ventana` lleva un `nombre: String` —del manifiesto, o del
  orquestador al engendrarla en vivo—. `Plantilla` lo guarda para las
  copias que `Alt+N` instancia.
- Franja `FRANJA_TASKBAR=40px` reservada al pie. `area_apps` la
  descuenta — las ventanas teselan y flotan sin tapar la barra.
- `consola`: tipos `Taskbar` / `CeldaTaskbar` + métodos `pintar_taskbar`
  y `pintar_etiqueta` (rasteriza una cadena en (x, base_y) sobre un
  fondo conocido, sin tocar la pluma). La pestaña enfocada se pinta con
  el índigo del foco, las desalojadas con su color de baliza, el resto
  con el slate del panel.
- `compositor::recomponer` arma las celdas y las pasa junto a las capas
  a la consola; un único repintado, una única presentación.
- `atender_raton`: si el clic cae en la franja de la barra,
  `celda_taskbar_en` localiza la pestaña pulsada y la enfoca (sin
  iniciar arrastre).

Verificado en QEMU: al arrancar, la barra al pie muestra las 7
pestañas con sus nombres; `tonada` enfocada en índigo, `discola` y
`glotona` en sus colores de baliza. Un clic sobre `pulso` traslada el
foco al instante — el borde del compositor envuelve `pulso` y su
pestaña se ilumina.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 02:05:51 +00:00
sergio be4de986be feat(seed): arje-prod levanta red + sshd, así se puede entrar por SSH
Sin systemd ni NetworkManager, arje-zero quedaba sin red y sin sshd:
útil como bare init, inútil para sacar logs de una VPS sin pegado en
la consola web. Dos Cards nuevas en el seed prod:

- `net-up`: corre `/usr/sbin/arje-net-up` (script nuevo en scripts/),
  que pone up todas las interfaces y arranca `dhclient -d` en
  foreground sobre la primera no-loopback. Fallback a dhcpcd o
  busybox-udhcpc si dhclient no está. Crea de paso /run/sshd y
  /var/empty/sshd para que sshd no tenga que pelearlos. Restart
  supervisión.

- `sshd`: corre `/usr/sbin/sshd -D -e` (foreground + log a stderr).
  Usa las host keys que Fedora ya tenía. Restart supervisión.

El install script copia arje-net-up.sh a /usr/sbin/arje-net-up.

Prerequisito en el host (no automatizable desde acá): si la VPS no
tiene un cliente DHCP (Fedora Cloud trae sólo NetworkManager por
defecto), el script duerme con el link up y no obtiene IPv4. En ese
caso instalar antes del próximo boot: `dnf install dhcp-client`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 01:56:31 +00:00
sergio 8fcc4dc067 fix(renaser): mapeador MMIO en el kernel — la causa real del colapso
El `-global pci-hole64-size=0` del commit anterior NO movía los BAR:
verifiqué con `info pci` que OVMF seguía alojando el BAR4 prefetchable
64-bit del virtio-blk en `0xc000000000` (mi Proxmox) o `0x800000000`
(la laptop del usuario). El cargador `bootloader_api` 0.11 mapea la
memoria física pero no extiende su mapeo hasta la ventana PCI de 64
bits, y `KernelHal::mmio_phys_to_virt` devolvía `phys + offset` a
ciegas — un puntero a memoria sin tabla de páginas, al primer registro
MMIO leído → #PF.

La solución: un mapeador MMIO propio del kernel.

- `memory::mmio`: envuelve la tabla L4 activa (vía CR3 + el mapeo de
  memoria física del cargador) en un `OffsetPageTable`. Su función
  `mapear(fisica, tam)` abre, para cada página de la región, una
  entrada en la L4 con `PRESENT | WRITABLE | NO_CACHE | WRITE_THROUGH`
  — las banderas habituales del MMIO.
- Los marcos para tablas intermedias salen del banco DMA del disco
  (`drivers::disco::asignar_marco_para_tabla`, sin pánico). Se ponen
  a cero antes de cederlos: las tablas empiezan vacías.
- Tratamos `PageAlreadyMapped` y `ParentEntryHugePage` como éxito: la
  región ya estaba cubierta por el cargador (con páginas 4 KiB o
  hugepages 2 MiB / 1 GiB) y el acceso ya funciona. Solo abortamos el
  mapeo si se nos agota la arena DMA.
- `KernelHal::mmio_phys_to_virt` llama a `memory::mmio::mapear` antes
  de devolver el puntero virtual. virtio-drivers lo invoca con la
  base y el tamaño exactos de cada BAR; el kernel asegura que cada
  uno sea accesible antes de devolverlo.
- `kernel_main` funda el mapeador justo después del heap (paso 4.5),
  antes del disco. Necesita `physical_memory_offset` para alcanzar
  la L4 activa.

Quito el `-global q35-pcihost.pci-hole64-size=0` que añadí antes: no
movía los BAR (verificado con `info pci`) y solo confundía la
descripción del fix. Esta solución es la robusta: el kernel sabe
mapear sus propios MMIOs y deja de depender del firmware.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 01:28:32 +00:00
sergio c715ee2dee fix(init): la salida de arje-zero ahora se ve en VGA Y serial
Síntoma: el screenshot del usuario en la VPS Hetzner mostraba systemd
booteando y se quedaba congelado en el último printk del kernel justo
antes del switch-root. arje-zero arrancaba bien pero su salida iba al
serial invisible.

Causa: el cmdline traía `console=tty1 console=ttyS0,115200` — y el
kernel hace que `/dev/console` apunte al ÚLTIMO `console=`, así toda la
salida de stdout/stderr de arje-zero (tracing + banner de la rescue
shell) caía en ttyS0 (serial), no en la VGA que muestra noVNC.

Dos arreglos:

- Orden de consolas invertido en el menuentry → `/dev/console` = tty1
  (lo que efectivamente se ve en la consola web del proveedor).
- arje-zero también escribe a `/dev/kmsg` (ring buffer del kernel), que
  el kernel hace eco a TODAS las consolas registradas — el mecanismo
  que usa systemd para que sus mensajes salgan tanto en VGA como en
  serial. Defense in depth: el banner de rescue y un eco temprano
  «despierta como PID 1» aparecen sí o sí en cualquier consola.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 01:16:26 +00:00
sergio 89fd927f76 fix(renaser): forzar BARs PCI a los primeros 4 GiB (pci-hole64-size=0)
Esta era la verdadera causa del «pantalla negra + franja roja» en máquinas
distintas a la del autor. El trace de boot del usuario lo cantó:

  boot :: disco :: init               <- llegamos aquí
  disco :: init offset=0x20000000000 region=[...] base=...
  boot :: almacen :: init              <- y aquí
  *** renaser :: panico ***
    EXCEPCION FATAL :: fallo de pagina (#PF) en 0x20800000014

  0x20800000014 - phys_offset(0x20000000000) = 0x800000014  (= 32 GiB + 0x14)

Un acceso vía el mapeo de memoria física a phys=32 GiB. Eso es el BAR
MMIO del virtio-blk: OVMF en QEMU q35 moderno con KVM aloja los BARs
prefetchables 64-bit en el «pci hole» de 64 bits (típicamente a partir
de 32 GiB). El `bootloader` 0.11 con `Mapping::Dynamic` mapea la RAM
del sistema, pero no extiende el mapeo hasta los BARs ahí arriba.

KernelHal::mmio_phys_to_virt devolvía `phys + offset` sin verificar
nada — el host esperaba que el BAR estuviese en los primeros 4 GiB,
como mi Proxmox con TCG y mi nightly. En la laptop con KVM y el OVMF
del usuario, OVMF lo subía y todo reventaba al leer el primer registro.

El parche: `-global q35-pcihost.pci-hole64-size=0` en los args por
defecto de QEMU. Apaga la ventana PCI de 64 bits, OVMF se ve forzada
a alojar todos los BARs en los primeros 4 GiB y el mapeo del cargador
los cubre. Verificado: arranca limpio en Proxmox y debería arrancar
también en la laptop del usuario.

(Las verificaciones unchecked_mul del commit anterior eran una pista
falsa — eran solo donde caía la IP del último build; el fallo de
escritura siempre fue el mismo BAR sin mapear.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 01:11:12 +00:00
sergio 2ad11b53c3 fix(renaser): apagar debug-assertions/overflow-checks vía rustflags
Era la causa raíz del «pantalla negra + franja roja» en máquinas con
nightlies recientes (laptops, KVM, etc.) que mi Proxmox no exhibía:

- En modo debug, la stdlib inyecta `<usize>::unchecked_mul::precondition_check`
  en cada `unchecked_mul` (RawVec::current_memory, in_place_collect…).
- El camino de pánico de ese check, en bare-metal, escribe en regiones
  que el cargador no mapeó → #PF → al panic handler del kernel, que pinta
  la franja roja: un colapso DENTRO de la red de seguridad del colapso.
- 1202 callsites en el binario debug — uno fallaba en la laptop del usuario.

El fix: `rustflags = ["-Cdebug-assertions=off", "-Coverflow-checks=off"]`
en `[target.x86_64-unknown-none]` de `.cargo/config.toml`. Los `[profile]`
del manifiesto no propagaban a las deps precompiladas (wasmi, virtio-
drivers, etc., que se quedan como artifact-deps fuera del workspace);
`rustflags` por target sí. Tras un `cargo clean` + rebuild, cero llamadas
a precondition_check, y el boot trace por COM1 corre completo.

También: `[profile.dev]`/`[profile.release]` del kernel y workspace
declaran los flags explícitamente, por si alguna ruta de cargo cambia.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 00:27:06 +00:00
sergio 5edd8de917 fix(scripts): Fedora/RHEL usan /boot/grub2/grub.cfg, no el wrapper EFI
Desde Fedora 34 / RHEL 9, /boot/efi/EFI/<distro>/grub.cfg es un wrapper
que sourcea /boot/grub2/grub.cfg — y grub2-mkconfig se niega a
sobreescribirlo ("will overwrite the GRUB wrapper. Please run [...] on
/boot/grub2/grub.cfg instead"). Mi loop de detección lo encontraba
primero y fallaba. Apuntamos directo al canónico.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 00:25:18 +00:00
sergio 89117f47cc diag(renaser): trazado por serie del arranque + DMA salta la página 0
Para localizar dónde colapsa el kernel en máquinas que no son la del
autor, cada hito de `kernel_main` deja una traza por COM1 (con el
panic-handler-a-serie de antes, ya tenemos boot trace + autopsia).

- `baliza::Serie` se hace `pub(crate)` para que cualquier módulo deje
  trazas con `writeln!(baliza::Serie, ...)`.
- `kernel_main`: traza tras adoptar el framebuffer, encender la baliza,
  fundar GDT/IDT/PIC, fundar el heap, fundar teclado/reloj/texto,
  publicar la consola, iniciar disco y almacén, arrancar el ratón,
  crear el ejecutor, cargar el userspace y arrancar el reactor. Y un
  volcado de `physical_memory_offset` + `region_dma` al inicio.
- `drivers::disco::init`: registra offset, región, base de la arena y
  número de marcos disponibles.
- Endurecimiento: `disco::init` ahora salta SIEMPRE la primera página
  física al elegir la base de la arena DMA. Algunos cargadores la dejan
  sin mapear como protección NULL; un marco DMA ahí se traduce a una
  dirección que peta al desreferenciar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 00:07:07 +00:00
sergio d85bb3819e feat(renaser): pánico y OOM dejan testimonio por COM1
En pantalla solo cabe la franja roja: un grito breve, sin matiz. Para
diagnosticar colapsos en máquinas distintas a la del autor —donde no
es posible reproducirlos a mano—, los manejadores de fallo escriben
ahora una pista por COM1, además de pintar la franja.

- `baliza`: sumidero `Serie` que formatea sin tocar el heap, escribe a
  0x3F8 con espera acotada (antes mudo que colgar el sistema).
- El panic-handler vuelca el mensaje y la ubicación del `panic!`.
- El alloc-error-handler vuelca el `Layout` que reventó el techo.

QEMU con `-serial stdio` enruta COM1 a la terminal de `cargo run` — la
pista llega a quien lanzó el kernel, aunque la pantalla esté en negro.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:51:40 +00:00
sergio 8787b0566a fix(scripts): install-arje no esconde el build — chequeo previo + salida visible
El `>/dev/null` del paso 1 ocultaba lo que pasaba: si faltaba un
prerrequisito o el build moría, sólo se veía «paso 1/5» y el script
salía silencioso. Cambios:

- Paso 0 nuevo: chequeo de cargo / musl-gcc / busybox / cpio / gzip /
  rust target con mensajes accionables por distro (Fedora + Debian).
- El build de cargo deja fluir su salida a la terminal — incluida la
  espera de 10-20 min la primera vez, así no parece un cuelgue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:48:24 +00:00
sergio 8fc1d99ddf feat(renaser): Fase 13 — ratón, puntero y arrastre de flotantes
renaser dialogaba sólo con el teclado; las ventanas flotantes nacían
en cascada y allí se quedaban. La Fase 13 trae el ratón.

- Driver `drivers/raton`: el ratón PS/2 cuelga del dispositivo
  auxiliar del 8042 + IRQ12. El driver despierta el aux, programa
  su IRQ, le ordena reportar, ensambla paquetes de 3 bytes con
  guarda del bit-3. Posición como atómicos, eventos como cola
  lock-free — el mismo guardarraíl que el teclado.
- El puntero, capa de PRESENTACIÓN: `Pantalla::estampar_puntero`
  pinta un sprite de flecha 12×18 sobre el framebuffer después de
  copiar el lienzo. El lienzo nunca lo contiene — hace de
  save-under natural—.
- Compositor: `atender_raton` drena eventos. Botón bajando es un
  clic-para-enfocar consistente con `mover_foco` (silencia bocina,
  alza si flota). Si la enfocada flota, arranca un arrastre con el
  desfase de agarre; el botón sostenido la sigue al puntero; al
  soltar, termina.
- `refrescar_puntero` reestampa el framebuffer si el puntero se
  movió en una vuelta tranquila en que ninguna app pintó.

Verificado en QEMU (mouse_move / mouse_button del monitor): el
puntero aparece al arrancar, se mueve por la pantalla, un clic
sobre pulso le da el foco, y un arrastre con el botón sostenido
mueve la flotante de la cascada al centro-abajo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:21:06 +00:00
sergio d1b700eb2b fix(init): el reboot-loop de Fedora — remount rw + /run tmpfs + shell de rescate
Diagnóstico: en el VPS Fedora arje-zero caía como PID 1 y el cmdline
traía `panic=10`, así que el kernel rebooteaba cada 10 s. Tres causas
encadenadas, todas arregladas:

1) **Cmdline `ro` + sin `/run` tmpfs.** El menuentry montaba `/` como
   sólo lectura (systemd lo remonta rw temprano; arje no). Sin eso, el
   socket del bus interno se intenta crear sobre un FS de sólo lectura
   y falla con EROFS → spawn_bus devuelve Err → PID 1 sale → kernel
   panic. arje-kernel ahora remonta `/` rw en el bootstrap y monta
   `/run`, `/tmp`, `/dev/pts`, `/dev/shm` como tmpfs — superficies
   escribibles aunque la raíz quede ro.

2) **PID 1 saliendo en cualquier `?`.** Doctrina dura nueva: PID 1
   NUNCA puede salir. Cualquier error de arranque ahora cae a una
   `emergency_shell()` que imprime el diagnóstico en `/dev/console`,
   abre `/bin/sh` y, si la shell muere, la reabre — así el operador
   puede reparar en vez de mirar la máquina reiniciarse en bucle.

3) **El script no conocía grub2 (Fedora).** `install-arje-as-init.sh`
   sólo probaba `update-grub` (Debian) y `grub-mkconfig` (Arch). Ahora
   detecta `grub2-mkconfig` y resuelve el `grub.cfg` correcto
   (UEFI/BIOS, fedora/redhat/centos/almalinux/rocky). El menuentry
   también pasa de `ro` a `rw` — el remount es belt-and-suspenders.
   Mismo arreglo en `uninstall-arje.sh`.

Renaser intacto: estos cambios son Linux-side puro (arje-kernel y
arje-zero usan nix/libc/tracing); renaser sólo comparte mirada-layout y
formato, ninguno tocado.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:02:45 +00:00
sergio 922ad1f86b feat(renaser): Fase 12 — la bocina del PC como capacidad de host
La Fase 11 dio al userspace un reloj; la Fase 12, una voz. Hasta hoy
renaser solo sabía dibujar para llamar la atención.

- Driver `drivers/altavoz`: el canal 2 del PIT como generador de onda
  cuadrada + la compuerta del puerto 0x61. El canal 0 —latido del
  kernel— no se toca. `tono(hz)` es su única vía; un 0 la silencia.
- Capacidad `sys_tono(frecuencia_hz)` — la undécima función del host.
  La bocina es un recurso único: pertenece a la ventana ENFOCADA,
  como el teclado desde la Fase 8c. Al cambiar el foco, el compositor
  la calla; la nueva dueña la reclama en su próximo fotograma.
- App nueva `tonada` (`apps/tonada/`, wasm32): toca una escala de Do
  mayor y la dibuja como una escalera de barras. Junta el reloj
  (`sys_tiempo_mono`) y la bocina (`sys_tono`).
- `GENESIS` crece de 6 a 7 apps; `tonada` es la maestra del escritorio.

Verificado en QEMU. Visual: la escalera de `tonada` recorre la escala
con el tiempo. Sonido: con la bocina enrutada a un WAV, el PCM
capturado es una onda cuadrada oscilante de ~375 Hz — la frecuencia
media de la escala de Do mayor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:54:48 +00:00
sergio a388ab14b7 fix(compat): auditoría de stubs — los métodos que mentían dejan de mentir
Repaso de los 11 shims restantes buscando métodos que devolvían éxito
sin hacer el trabajo (como los dos setters de localed). Resultado:

timedated — tres setters arreglados de verdad:
- SetTime: aplica el reloj con clock_settime(CLOCK_REALTIME) en vez de
  sólo loggear; si falla (sin CAP_SYS_TIME) devuelve error honesto.
- SetLocalRTC: escribe la tercera línea de /etc/adjtime (UTC|LOCAL),
  conservando las dos primeras.
- SetNTP: arje no gestiona un daemon NTP — en vez de fingir éxito,
  rechaza honestamente; `CanNTP` pasa a `false` para que GNOME deje el
  toggle deshabilitado y ni llegue a llamarlo.

systemd1 — StopUnit/RestartUnit/KillUnit dejaban creer que habían
detenido la unit; ahora devuelven NotSupported honesto (como StartUnit).

Lo demás del repaso ya era honesto: resolved/machined devuelven
NotSupported de frente; polkit/tmpfiles/notify/binfmt/journald no
mienten. timer-compat queda como hueco conocido y autodocumentado (sus
timers disparan pero el spawn es un no-op a la espera del bus).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:45:53 +00:00
sergio 4bcdc88c83 feat(renaser): Fase 11 — el reloj del sistema como capacidad de host
El userspace gana un sentido del tiempo: hasta ahora una app solo sabía
cuántas veces la habían llamado, no cuánto tiempo había pasado.

- Capacidad `sys_tiempo_mono() -> u64` — la décima función del host:
  los milisegundos monótonos desde el arranque. `reloj` expone la
  cuenta del PIT (100 Hz) como `milisegundos()`; `env` la inyecta.
  Lectura pura, no toca la memoria del módulo, jamás retrocede.
- App nueva `pulso` (`apps/pulso/`, wasm32): un compás visual cuya
  escena es una función PURA de `sys_tiempo_mono` — sin estado entre
  fotogramas—. Dos instancias laten al unísono nazcan cuando nazcan.
- `GENESIS` crece de 5 a 6 apps; `pulso` es la maestra del escritorio.

Verificado en QEMU (sendkey): la barra de `pulso` avanza con el tiempo
de pared; un segundo `pulso` lanzado con Alt+N ~15 s después aparece
sincronizado con el primero — el compás se rige por el reloj absoluto,
no por una cuenta de fotogramas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:43:17 +00:00
sergio 19d04a2766 feat(compat): logind — Inhibit real (fd vivo) + ListInhibitors
`Inhibit` dejó de ser un stub que devolvía NotSupported. Ahora cumple
el contrato de systemd-logind:

- Crea un pipe; el cliente recibe el extremo de escritura, el shim
  conserva el de lectura. Mientras el cliente no cierre su fd, el
  inhibidor sigue activo; al cerrarlo —o morir— el shim ve EOF y una
  tarea guardiana lo retira de la tabla.
- Tabla de inhibidores activos + método `ListInhibitors`.
- Las propiedades `BlockInhibited` / `DelayInhibited` ahora reflejan
  los inhibidores reales (tokens únicos del modo, unidos por `:`),
  en vez de devolver siempre vacío.

Es lo que GNOME/KDE usan para frenar la suspensión automática durante
una presentación o una descarga. 1 test (`inhibited_what`).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:41:10 +00:00
sergio 089afccbbc feat(renaser): Fase 10 — alta y baja de aplicaciones en vivo
El censo de aplicaciones deja de fijarse en el arranque: una app puede
nacer o cerrarse con el reactor ya en marcha.

- El reactor admite NACIMIENTOS en vivo: cola `NACIMIENTOS` +
  `engendrar()`, drenada al inicio de cada vuelta de `run()`;
  `Task::adoptar` acoge un futuro ya empaquetado.
- `Alt+Q` (`Mando::Cerrar`): baja limpia. El compositor saca la
  ventana enfocada del teselado y del orden-Z; la app advierte la
  baja (`ventana_cerrada`) y concluye su tarea — su memoria, su
  combustible y su canal de teclado se liberan. Sin baliza.
- `Alt+N` (`Mando::Lanzar`): alta en vivo. `nacer_ventana` añade la
  ventana y entrega su índice; el orquestador instancia el WASM y
  engendra su tarea. Las apps de génesis dejan su bytecode cacheado
  como `Plantilla`; cada `Alt+N` instancia una en rotación.

Verificado en QEMU (sendkey): tres Alt+N hacen crecer el escritorio
de 5 a 8 ventanas; tres Alt+Q lo reducen de 8 a 5. Kernel estable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:26:25 +00:00
sergio 8f946b449c feat(compat): arje-compat-common — núcleo puro y testeado para los shims
El hallazgo del repaso del monorepo: la capa compat (14 shims D-Bus de
systemd) era lo más incompleto relativo a su peso — load-bearing para
correr GNOME/KDE sobre arje, y con CERO tests. Cada shim copiaba su
propio `atomic_write`, su parseo `KEY=value` y sus validadores.

Primer golpe:

- `arje-compat-common`: crate nuevo con la lógica pura compartida
  (atomic_write, parse_kv, merge_kv, conf_entries, is_valid_hostname),
  cubierta con 8 tests. Antes esa lógica vivía duplicada y sin un test.
- `arje-hostnamed-compat` y `arje-localed-compat` migrados al núcleo —
  quedan más finos y su lógica pasa a estar cubierta.
- localed: los dos setters que eran stub (sólo loggeaban) ahora
  escriben de verdad — `SetVConsoleKeymap` → /etc/vconsole.conf,
  `SetX11Keyboard` → 00-keyboard.conf. + 2 tests propios.
- Bug corregido de paso: el parser xorg de localed devolvía el NOMBRE
  de la opción en vez del valor (tomaba la 1ª comilla); ahora toma la
  2ª cadena, la correcta.

Compat: de 0 a 10 tests. Quedan 12 shims con la misma migración
mecánica pendiente; el plato fuerte real es `Inhibit` en logind.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:25:58 +00:00
sergio c4d1dd7bc2 feat(cosmobiologia): corpus — capa de composición por evidencia vecina
La capa de composición, resuelta con honestidad. El producto numérico
de perfiles (Hadamard y parientes) se descarta: da falsos —una
dimensión en 0 nunca «se enciende»— y, sobre todo, un perfil compuesto
es una conjetura, no evidencia.

En su lugar, `Corpus::evidencia_relacionada`: para una combinación SIN
pasaje propio, junta la evidencia VECINA —pasajes que comparten un
componente (el planeta, el signo, la casa, el tipo de aspecto)—,
agrupada por lo que comparten. No sintetiza un texto; son citas reales
de contextos parecidos para que el astrólogo componga él.

En la rueda 2D, el panel de la tajada ahora muestra, bajo los pasajes
directos, una sección «Composición» con esa evidencia vecina por cada
combinación sin texto propio.

16 tests del corpus (2 nuevos) + 2 del engine verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:07:35 +00:00
sergio 2035e6dfa3 feat(cosmobiologia): corpus — las tajadas sobre la rueda
La interpretación por dominio, integrada al lienzo 2D. Tres botones
(Vital / Social / Psíquico, o tecla I para ciclar): al elegir una
tajada, la rueda resalta con un anillo los cuerpos de ese dominio y un
panel a la derecha lista los pasajes citados —combinación, texto,
fuente—, o avisa de los huecos sin texto.

- El canvas carga el corpus al arrancar: corpus.ron del directorio de
  datos del usuario, o la plantilla ejemplo.ron embebida como fallback.
- El JOIN corre con corpus_inputs (engine) + interpretar_por_dominio:
  cada longitud → signo, cada casa → su tajada, los aspectos puentean.
- El resalte es una capa transparente sobre la rueda, sin tocar el
  render del wheel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:54:23 +00:00
sergio ac787fb3b3 feat(cosmobiologia): corpus — puente carta → pasajes de interpretación
Primer paso para conectar el cosmobiologia-corpus a la app: el engine
gana `corpus_inputs(&RenderModel)`, que deriva de una carta sus
colocaciones (planeta·signo·casa) y sus aspectos en el shape que el
corpus consume. Cada longitud se traduce a su signo; la casa viene del
glyph. El caller hace luego `Corpus::interpretar_por_dominio`.

El engine reexporta los tipos del corpus (Corpus, Pasaje, Dominio,
Colocacion, AspectoEnCarta, CombinacionId) para que el shell/canvas los
usen sin importar el crate aparte.

2 tests del engine verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:44:54 +00:00
sergio 2523652e22 feat(renaser): Fase 9 — orden-Z y ventanas flotantes
Segundo modelo de composición sobre el teselado de la Fase 8: el
SOLAPAMIENTO. Una ventana puede abandonar el teselado y FLOTAR sobre
las demás.

- `Escritorio` gana `flotantes: Vec<usize>` — la pila orden-Z, de
  atrás hacia adelante; con `orden` forma una partición de las
  ventanas.
- Mando `Flotar` (`Alt+F`): alterna la ventana enfocada entre
  teselada y flotante; una flotante nace con marco propio en cascada,
  al frente del orden-Z.
- `compositor::recomponer` + `consola::recomponer` (tipos `Capa` /
  `Contenido`): con flotantes vivas el escritorio se repinta entero,
  capa a capa de atrás hacia adelante — el solapamiento se resuelve
  por el orden del pintado. Sin flotantes, camino rápido de la Fase 8.
- El foco recorre todas las ventanas y alza al frente la flotante
  enfocada.

Verificado en QEMU (sendkey): flotar, cascada, alzado-Z y regreso al
teselado.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:42:51 +00:00
sergio 6e30dc2d72 feat(cosmobiologia): esfera 3D — switch de constelaciones + luz de la Vía Láctea
- Switch de constelaciones: botón flotante «● Constelaciones» (o tecla
  B) que las enciende y apaga en la esfera 3D.
- La luminosidad se reparte: el brillo especular fijo a la pantalla se
  bajó mucho (no giraba, se sentía despegado), y en su lugar la Vía
  Láctea aporta un resplandor difuso a lo largo del plano galáctico —
  que SÍ gira con la esfera. Más intenso hacia el centro galáctico
  (Sagitario, como en el cielo real) y atenuado bajo el horizonte
  local: la franja como se ve desde la Tierra esa noche.

42 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:40:11 +00:00
sergio cfb37af0cf feat(cosmobiologia): Tierra interior — tinte mar/continente + día/noche
La Tierra interior ahora se lee como un planeta:

- Mar y continentes teñidos distinto: el mar es un disco azul, los
  continentes son polígonos rellenos de verde. Para eso se sumó la
  primitiva DrawCommand::Polygon (relleno + trazo) — agnóstica, con su
  traductor GPUI y su emisor SVG.
- Sombreado día/noche según el Sol de la carta: el hemisferio que mira
  al Sol se ilumina (resplandor concéntrico sobre el punto subsolar,
  que se apaga si el Sol queda detrás de la Tierra), el terminador
  marca la línea día/noche, y cada continente se tiñe verde claro u
  oscuro según esté de día o de noche. El observador se atenúa si
  naci­ó de noche.

42 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:33:46 +00:00
sergio 267e54f974 feat(cosmobiologia): esfera 3D — figuras de las 88 constelaciones
Las constelaciones de un catálogo REAL, no inventadas de memoria:
d3-celestial (dominio público), 89 figuras / 743 segmentos, en
coordenadas ecuatoriales J2000. El dataset se convirtió a un módulo
Rust generado (`constellations_data.rs`) — datos en el repo, auditables.

Cada figura: sus polilíneas unen estrellas reales del catálogo (un
punto por vértice) y el nombre va en el centroide. Capa tenue, atenuada
por profundidad — referencia, no protagonista. Se convierten al marco
eclíptico con la misma rotación por oblicuidad que el resto.

42 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:29:50 +00:00
sergio dacfbad124 feat(renaser): Fase 8d — manipulación de ventanas
El escritorio se podía recorrer con el foco, pero no reordenar. La 8d lo
hace manipulable: el orden de teselado se separa de la identidad.

- Escritorio gana `orden: Vec<usize>` — una permutacion que dice que
  ventana ocupa cada celda. Mover una ventana cambia su celda, no su
  indice_app: conserva su canal de teclado y su ranura de estado.
- aplicar_teselado reparte los marcos segun el orden.
- Alt+Enter promueve la ventana enfocada a la celda maestra; Alt+H/Alt+L
  la reordenan. mover_foco recorre ahora el orden, no los indices crudos.

Verificado en QEMU (sendkey): con memoriosa enfocada, Alt+Enter la
promueve a maestra y hola baja a la pila; Alt+L la devuelve a la pila. El
foco —el borde indigo— viaja siempre con la ventana, no con la celda.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:25:32 +00:00
sergio 3454b8ba1e feat(cosmobiologia): esfera 3D — Tierra interior con continentes + topocéntricos
Tierra interior: un globo pequeño y transparente en el centro de la
esfera celeste, con los continentes esquemáticos (referenciales, no un
mapa de precisión) y el observador marcado en su lugar real. Orientada
por la longitud geográfica y el RAMC, de modo que el punto del
observador mira exactamente al cénit — y gira con la vista, así que
delata la rotación que el sombreado fijo no daba.

Topocéntricos: la capa topocéntrica del motor se dibuja como disco
hueco con un conector hasta su par geocéntrico. El LARGO del conector
es la paralaje — honesto sobre su magnitud (un cinturón aparte la
exageraría: la diferencia es sub-grado salvo la Luna).

`RenderModel` gana `geo_longitude_deg` (lo puebla el bridge). 41 tests
verdes (3 nuevos: orientación de la Tierra, observador↔cénit,
continentes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:23:47 +00:00
sergio 5c462e6d30 feat(renaser): Fases 8b y 8c — el escritorio interactivo
El compositor de la 8a teselaba, pero era inmovil. Las 8b/8c lo hacen
vivo: el teclado reordena el escritorio y mueve el foco en caliente.

- Cache de fotogramas: cada ventana guarda en RAM del kernel su ultimo
  fotograma —reservada una vez, acotada al lienzo natural—. Al re-teselar
  o mover el foco, el kernel recompone desde la cache: las apps que solo
  pintan en init (cronista) conservan su imagen sin enterarse del cambio.
- compositor: el registro ESCRITORIO (ventanas, marcos, caches, modo);
  presentar_fotograma, desalojar, atender_mandos, ciclar_layout,
  mover_foco. Foco en un AtomicUsize, mandos en una cola lock-free.
- teclado: la IRQ1 deja de difundir. Alt es el modificador del sistema —
  Alt+Espacio cicla el teselado, Alt+J/K mueven el foco—; una tecla
  ordinaria va SOLO a la app enfocada (CANALES reindexado por indice_app).
- consola: borde de foco (indigo / gris) en cada marco.

Guardarrail anti-interbloqueo: la IRQ1 jamas bloquea ESCRITORIO; se
comunica por dos atomicos y una cola lock-free. Las caches se reservan
una sola vez, al tamaño natural — sin asignacion en el bucle del reactor.

Verificado en QEMU (screendump + sendkey): arranque teselado con hola
enfocada; Alt+Espacio cicla a CenteredMaster y las apps estaticas
conservan su contenido; Alt+J mueve el foco; las teclas llegan solo a la
app enfocada. Cierra la Fase 8 — el compositor teselante e interactivo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:19:21 +00:00
sergio e94023d8af feat(cosmobiologia): esfera 3D — estrellas fijas notables en su lugar real
La esfera ahora dibuja las 9 estrellas fijas del motor (Sirio, Régulo,
Antares, Spica, Aldebarán, Fomalhaut, Algol, Vega, Pólux) — disco
brillante con destello de cuatro rayos y su nombre.

La longitud eclíptica —la coordenada astrológicamente viva, que
precesiona— viene intacta del motor (`build_fixed_stars_overlay`). El
módulo nuevo solo le suma la **latitud eclíptica** (valor de catálogo,
~constante con la precesión) para situar cada estrella en su lugar
real de la esfera en vez de aplastada sobre la eclíptica: Sirio cae
bien al sur, Vega bien al norte, Régulo casi sobre la eclíptica.

Se ven al activar el módulo «Estrellas fijas» en el panel. 39 tests
verdes (3 nuevos: eclip_latlon, coherencia de latitudes, render).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:03:27 +00:00
sergio 93856cd4d7 feat(cosmobiologia): esfera 3D — la piel real del cielo: estrellas + Vía Láctea
La «piel» de una esfera celeste no son continentes —esos van en la
Tierra— sino las estrellas y la Vía Láctea. Y a diferencia del brillo
especular (fijo a la pantalla), esta piel gira CON la esfera, así que
delata la rotación de un vistazo.

- Campo de estrellas isótropo, decorativo (no un catálogo real),
  generado con un hash determinista — no titila entre frames.
- Vía Láctea: una sobredensidad de estrellas tenues a lo largo del
  plano galáctico, ubicado con el polo galáctico real (J2000, AR
  192.859° / Dec +27.128°).
- Estrellas con brillo y tinte variables (azuladas / cálidas),
  atenuadas por profundidad. Van detrás de la rejilla, delante del
  sombreado — un fondo de planetario. Solo en tema oscuro.

36 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:58:34 +00:00
sergio 65c88ccf25 feat(renaser): Fase 8a — el compositor teselante
El kernel deja de colocar las ventanas a mano: las tesela. El motor es
mirada-layout — el mismo nucleo no_std que ordena el compositor Wayland
de brahman, enlazado por path cruzando la frontera de workspace. Es el
primer consumo REAL del nucleo compartido brahman <-> renaser.

- kernel/compositor.rs: enlaza mirada-layout y calcula un marco por app
  con el algoritmo MasterStack, dentro del area de pantalla.
- consola::volcar_marco centra el fotograma natural de la app dentro de
  su marco teselado (antes lo depositaba en region.x/y fijos).
- ContextoCapacidades lleva marco + natural_ancho/alto; sys_render_frame
  valida el fotograma contra el tamaño natural.
- cargar_userspace tesela con el compositor y pinta el escenario antes
  de encender las apps. Las apps NO cambian: el compositor reordena la
  pantalla sin que ninguna toque una instruccion.

Verificado en QEMU (screendump): las cinco apps de genesis teseladas en
MasterStack — hola como ventana maestra, el resto apiladas a la derecha,
cada lienzo centrado en su panel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:56:40 +00:00
sergio 8fc26b0c0c feat(cosmobiologia): esfera 3D batch 2 — horizonte local, cénit y relieve
Sobre el batch 1 (eclíptica + ecuador + cuerpos):

- Horizonte local: círculo máximo perpendicular al cénit, derivado de
  la latitud geográfica y el RAMC. El cénit (declinación φ, AR RAMC,
  llevado al marco eclíptico) es el «punto del observador» — marcado
  como tal, con su nadir y el meridiano local.
- Día/noche: los cuerpos bajo el horizonte se atenúan — de un vistazo
  se ve qué planetas estaban sobre la tierra en el momento de la carta.
- Marcadores de polos: eclípticos (punto dorado) y celestes (anillo +
  cruz, etiquetados PN/PS) — el ángulo entre ambos ejes ES la
  oblicuidad, ahora visible.
- Relieve de la esfera: disco base + degradado radial + brillo
  especular desplazado a la luz — volumen sin gradientes nativos.
- RenderModel gana `geo_latitude_deg` (#[serde(default)]); el bridge
  lo puebla desde birth_data.

Verificación: 2 tests nuevos fijan la construcción del cénit — está a
la colatitud del polo celeste, y cénit/polo/MC son coplanares (el
plano del meridiano), lo que ancla el RAMC. 35 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:49:02 +00:00
sergio 900cd19e49 feat(renaser): Fase 7c — persistencia inter-sesión por-app
Cada app tiene ahora su propia ranura de estado en el Manifiesto de
Génesis (EntradaApp.estado): guarda y recobra lo suyo, sobrevive al
reinicio, y no pisa a ninguna otra app.

- apps/memoriosa: app WASM interactiva nueva. Cuenta las teclas pulsadas
  y persiste el recuento; al reiniciar despierta con su cuenta intacta.
  Reemplaza la 2a instancia de hola en la genesis.
- kernel: capacidades sys_estado_cargar / sys_estado_guardar. El kernel
  custodia un manifiesto VIVO (Mutex<Manifiesto>); fijar_estado lo muta,
  lo re-graba en el grafo y lo re-ancla. ContextoCapacidades.indice_app
  da a cada app su identidad — su ranura, jamas la de otra.
- cargar_userspace instala el manifiesto vivo antes de instanciar las
  apps: el init de una app ya consulta su estado al despertar.

Verificado en QEMU (screendump + sendkey): disco virgen -> memoriosa con
0 celdas, testigo verde; 5 pulsaciones -> 5 celdas; reinicio -> 5 celdas
intactas, testigo ambar (init releyo el estado del grafo).

Cierra la Fase 7 — el userspace nace del grafo, completa.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:43:58 +00:00
sergio 6a0781c0a8 feat(cosmobiologia): esfera celeste 3D — la carta como objeto rotable
GPUI no es 3D y empotrar wgpu sería frágil; la esfera celeste es de
alambre —círculos máximos y puntos— y eso se proyecta a software con
trigonometría pura. Cada superficie ya sabe dibujar DrawCommand, así
que el módulo nuevo solo decide dónde cae cada trazo: una esfera real,
rotable, sin una línea de GPU.

- cosmobiologia-render/sphere3d.rs: marco eclíptico (z=0), proyección
  ortográfica con yaw/pitch, eclíptica + ecuador celeste inclinado por
  la oblicuidad (se cruzan en los equinoccios, como en el cielo),
  rejilla de meridianos/paralelos, signos, ángulos y cuerpos natales.
  Algoritmo del pintor + atenuación del hemisferio lejano. 5 tests.
- compose_sphere emite Vec<DrawCommand> — lo consumen igual el canvas
  gpui y el SVG del cliente web.
- cosmobiologia-canvas: modo esfera 3D en el lienzo (tecla V o el botón
  flotante «Esfera 3D»), drag para orbitar, traductor DrawCommand→GPUI.

Falta (2da capa): el horizonte local + día/noche — necesita la latitud
geográfica, que aún no viaja en el RenderModel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:38:21 +00:00
sergio 7695dbf3ce feat(renaser): Fase 7b — boot siembra la imagen, muere el include_bytes!
El kernel deja de empotrar el userspace por completo. Ya no carga ni un
solo .wasm: es boot quien siembra el disco con el grafo poblado.

- kernel/almacen.rs y manifiesto.rs migran al nucleo compartido `formato`
  (tipos, postcard, BLAKE3, trazado de registros). El kernel pierde los
  include_bytes!, genesis() y sembrar_genesis().
- boot::sembrar_grafo siembra un disco virgen con el bytecode de las apps
  (deduplicado) y el Manifiesto de Genesis anclado en el superbloque.
- cargar_userspace sin rama de siembra; wasm/mod.rs sin TECHO_MEMORIA.
- alias `cargo kernel` -> --manifest-path (esquiva un ICE de cargo con
  formato compartido entre el kernel y boot via artifact-dep).

Verificado en QEMU (screendump): disco virgen -> boot siembra 5 objetos,
el kernel monta su grafo; segundo arranque -> boot respeta el disco, la
cronista persiste. formato: 5/5 pruebas.

Nota: el crate `formato` y los 3 Cargo.toml entraron antes en 43e6b32 por
un `git add -A` de un trabajo concurrente; este commit cierra el resto.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:29:23 +00:00
sergio 43e6b32e15 feat(cosmobiologia-corpus): tomografía por dominio + plantilla y guía
El corpus ya rebana la carta en tajadas vivenciales: una sola
configuración mirada plano a plano, sin promediar la contradicción.

- Colocacion / AspectoEnCarta: la posición real de un planeta en una
  carta — el puente entre el motor astronómico y las claves del JOIN.
- combinaciones_de_carta: deriva todas las CombinacionId de una carta.
- rebanar_por_dominio: la tomografía — cada planeta@cN cae en el
  dominio de su casa, cada planeta·signo hereda el de su casa, y un
  aspecto puentea apareciendo en las dos tajadas que conecta.
- Corpus::interpretar_por_dominio: el JOIN agrupado por dominio,
  entrada directa del gráfico «por tajadas».
- CombinacionId acepta el alias ASCII '/' del punto medio '·'.
- ejemplo.ron: plantilla cargable y comentada del corpus.
- GUIA.md: los pasos exactos para generar el corpus a mano.

12 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:18:16 +00:00
sergio 121aa130af feat(cosmobiologia): cosmobiologia-corpus — esquema del corpus de interpretación
El corpus es la biblioteca de interpretación indexada: fragmentos de
texto de los libros (y del astrólogo) etiquetados por la combinación
astrológica que describen. NO calcula nada — las reglas las computa el
motor; el corpus sólo guarda evidencia citable y la sirve por JOIN.

Esquema TIPADO (la astrología tiene gramática — planeta=función,
signo=estilo, casa=dominio, aspecto=relación; no son vectores
intercambiables de un espacio plano):

- CombinacionId — la «etiqueta de código de barras», con variantes por
  tipo de combinación; el aspecto normaliza el orden de sus extremos.
- Arquetipo / TipoArquetipo — los bloques con su PerfilSemantico
  (dimensiones con nombre que define el astrólogo, no el código).
- Pasaje — texto citado + fuente + combinación.
- Dominio — el plano vivencial (Vital/Social/Psíquico) por casa.
- Corpus::interpretar — el JOIN: combinaciones de la carta → pasajes.
  Cobertura total; la SÍNTESIS es de una capa superior.

6 tests verdes. La capa de composición (deducir combinaciones no
leídas) queda explícitamente sin construir — es un problema de diseño
abierto, no un producto Hadamard ingenuo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:05:10 +00:00
sergio 277b2ab8d2 docs(renaser): Fase 7a verificada en QEMU — el userspace nace del grafo
Captura headless por el monitor de QEMU (screendump) de los dos caminos
del Manifiesto de Génesis:

- Disco virgen: sembrar_genesis() puebla el grafo y ancla el manifiesto;
  consola imprime «genesis sembrada» + «5 apps nacidas del grafo».
- Disco ya sembrado: cargar() lee el manifiesto del ancla del superbloque
  sin resembrar; «grafo montado :: 6 objetos :: raíz presente», la
  cronista pinta su segunda celda (la cuenta de arranques perdura).

Pantalla idéntica a la Fase 6.2 en ambos casos. CHANGELOG: la sección
«Pendiente de verificación» de la 7a pasa a «Verificado».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:02:37 +00:00
sergio 36d6645e7f feat(cosmobiologia): rectificador per-segundo + direcciones primarias reales
El rectificador deja la aproximación y pasa a la trigonometría exacta,
con precisión de segundo — el "microajuste argentino".

LA MATEMÁTICA. El rectificador ya NO usa el modelo simplificado
(directed_longitude, rotación uniforme de RA + convergencia GR). Ahora
usa `eternal_astrology::primary_direction::all_directions` — el método
Placidus-mundano: semi-arcos diurnos/nocturnos bajo el polo de cada
cuerpo, la trigonometría esférica de la escuela ascensional. No se
reimplementó nada: la matemática, ya probada, vive en eternal; el
engine sólo aporta la capa de optimización.

- error_de_carta: por cada evento, la distancia en años a la dirección
  primaria que perfecciona más cerca; el error total es la suma. Es la
  función de coste del microajuste — el valle es la hora real.

PRECISIÓN DE SEGUNDO. compute_natal_chart / build_eternal_inputs /
natal_cache pasan a trabajar en SEGUNDOS (compose convierte ×60). El
rectificador barre en dos pasadas: gruesa minuto a minuto sobre la
ventana (el perfil que dibuja la curva), fina segundo a segundo en
±60 s alrededor del mejor minuto.

- Rectificacion: mejor_offset_segundos; el perfil va en segundos.
- UI: panel y curva muestran «±Xm Ys · error N.NNa». Las barras siguen
  siendo clicables (scrub a esa hora candidata).

Tests verdes (engine 12, render 28). Limitación conocida: all_directions
es sólo directo — converso necesita crecer en eternal (upstream).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 16:56:06 +00:00
sergio 5fdae159f0 feat(cosmobiologia): perfil del rectificador — barras clicables
Cada barra de la curva del barrido se vuelve clicable: un clic lleva
la carta a esa hora candidata, reusando el scrub de tiempo del
jog-dial (CanvasEvent::TimeOffsetChanged, ya cableado en el shell).

Cierra el lazo del rectificador: ahora se puede inspeccionar sobre el
wheel cualquier hora del barrido, no sólo leer la ganadora.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 16:44:26 +00:00
sergio 208dc15569 feat(cosmobiologia): rectificador automático — curva del perfil del barrido
Tercer y último incremento: la visualización. El rectificador ya
muestra POR QUÉ una hora gana, no sólo cuál.

- cosmobiologia-canvas: CanvasState gana `rectificacion` +
  `set_rectificacion`. render_rectify_profile dibuja el barrido como
  un histograma en el footer — cada barra es una hora candidata, su
  altura crece cuanto menor el puntaje; la barra más alta (el valle
  del puntaje) es la hora rectificada, resaltada. Etiqueta los hitos
  (mejor, 0, extremos).
- shell: run_rectificacion publica el Rectificacion al canvas además
  del resumen textual al panel.

Con esto el rectificador automático (#67) queda completo: motor de
escaneo GR + UI de entrada + visualización del perfil.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 16:42:59 +00:00
sergio a7e9662fad feat(cosmobiologia): rectificador automático — UI de entrada y disparo
Segundo incremento: el rectificador ya es usable de punta a punta
desde el panel, sin infraestructura de UI nueva.

- cosmobiologia-panel: Control::TextInput pasa a renderizarse desde
  string_state — deja de ser un display estático y se vuelve un campo
  de sólo-lectura que el shell escribe vía set_string (resultados,
  etiquetas).
- cosmobiologia-modules: el módulo primary_directions gana 3 sliders
  «Evento N · edad» (0 = ranura sin usar), un Action «Rectificar
  hora» y un TextInput «Resultado».
- shell: run_rectificacion lee las edades de los sliders, llama a
  engine::rectificar (ventana ±15 min, paso 1) y escribe la hora
  rectificada + el puntaje en el campo Resultado del panel.

El rectificador queda funcional: activar GR → fijar edades de eventos
→ «Rectificar hora» → leer el resultado. Falta sólo la curva del
perfil del barrido como visualización (incremento opcional).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:53:18 +00:00
sergio 0ada1050f7 feat(cosmobiologia): rectificador automático — escaneo GR (núcleo)
Primer incremento del rectificador automático (#67): dado un conjunto
de eventos conocidos de la vida del sujeto, barre las horas de
nacimiento candidatas y devuelve la que mejor los explica vía el
Sistema GR. La killer feature pro — desbloqueada al completar el GR.

- cosmobiologia-render: `convergencia_minima` — medida CONTINUA de qué
  tan bien una carta explica un evento (suma de orbes del directo +
  converso más cerrados sobre un punto natal). 3 tests.
- cosmobiologia-engine: módulo `rectify` — `rectificar` barre la
  ventana de horas candidatas; por candidata computa la carta (una
  vez, cacheada) y mide la convergencia GR a la edad de cada evento;
  elige el puntaje mínimo. Devuelve el perfil completo del barrido
  para que la UI lo dibuje como curva. Test end-to-end con eternal.
- bridge: `compute_natal_chart`/`body_symbol`/consts GR → pub(crate).

Falta: la UI (capturar eventos conocidos, lanzar el barrido, mostrar
la curva y la hora rectificada).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:49:43 +00:00
sergio bce4abd8cc feat(renaser): Fase 7a — el userspace nace del Grafo de Objetos
El kernel deja de empotrar las apps. Las cinco aplicaciones ya no
llegan por include_bytes! en main.rs: nacen del grafo, gobernadas por
un Manifiesto de Génesis que también vive en el grafo.

- almacen: el SuperBloque gana el ancla `manifiesto: Option<Hash>`
  (gemela de `raiz`, del lado del kernel) + accesores. VERSION 1→2 —
  un disco v1 se reformatea.
- manifiesto.rs: implementados `cargar` (lee el manifiesto del grafo)
  y `sembrar_genesis` (puebla un disco virgen con las 5 apps de
  génesis). El bytecode viaja empotrado AÚN, sólo como semilla
  transitoria (la Fase 7b lo mueve al constructor de imagen `boot`).
- kernel_main: `cargar_userspace` reemplaza las 5 `encender_app`
  escritas a mano; `encender_app` recupera el bytecode del grafo —
  `recuperar` verifica el hash, un módulo corrupto se niega y el
  arranque sigue.
- wasm: el techo de memoria pasa a ser por-app (del manifiesto).

Compila limpio. Verificación en QEMU pendiente (la corre el operador):
la pantalla debe verse idéntica a la Fase 6.2 + la línea «manifiesto».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:11:47 +00:00
sergio 4f31146533 feat(renaser): Fase 7 — apertura: plan y andamiaje del Manifiesto de Génesis
Abre la Fase 7 («apps que nacen del grafo»): destierra el include_bytes!
del userspace — las apps pasan a ser objetos del grafo, gobernadas por
un Manifiesto de Génesis que también vive en el grafo.

Este commit es sólo plan + andamiaje; el kernel se comporta idéntico a
la Fase 6.2.

- FASE7.md — el plan de ataque: el problema de la génesis, las
  sub-fases 7a/7b/7c y los guardarraíles.
- kernel/src/manifiesto.rs — andamiaje: tipos Manifiesto/EntradaApp +
  (de)serialización postcard completos; cargar/sembrar_genesis son
  esbozos hasta la 7a. Declarado en main.rs, aún sin cablear a
  kernel_main (#![allow(dead_code)] temporal).

CHANGELOG y DIARIO al día.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:02:19 +00:00
sergio 4c3b02c337 docs(renaser): wasmi unificado — actualiza el estado del plan
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:53:12 +00:00
sergio cb0c5c22a8 feat(arje-wasm): bump wasmi 0.40 → 1.0 — unifica el runtime WASM con renaser
brahman y renaser ya corren la misma versión de wasmi (1.0): el ABI
WASM del host es idéntico en Linux y en bare-metal. Desbloquea el
Paso 3 de la integración (converger el ABI Card/WASM).

El delta de la API resultó pequeño:
- `Linker::instantiate` + `InstancePre::start` → `instantiate_and_start`
  (wasmi 1.0 fusiona instanciación y arranque).
- Motor configurado en `CompilationMode::Eager` — traducción completa
  del módulo por adelantado, comportamiento predecible, paridad con el
  motor wasmi del kernel de renaser.

Primer test de arje-wasm: `demo_corre_en_wasmi_1` ejecuta el módulo
demo de punta a punta (WAT→wasm, instanciación, host imports
log/exit). arje-zero (PID 1, consumidor) compila sin cambios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:52:47 +00:00
sergio efcf6f825f feat: guardián no_std de núcleos compartidos + doc de integración renaser
Endurece y documenta la integración renaser↔brahman tras el piloto
mirada-layout.

- scripts/check-shared-cores.sh: compila cada núcleo compartido
  registrado para x86_64-unknown-none. Si un núcleo recobra `std` en
  silencio (dep descuidada), falla aquí y no semanas después en
  renaser. Hoy cubre mirada-layout.
- docs/renaser-integracion.md: por qué renaser es un workspace aparte,
  el modelo núcleos-duales/superficies-por-plataforma, cómo se hace
  no_std un núcleo, y el estado del plan por etapas.

Paso 2 (converger el CAS) DESCARTADO tras leer el código: arje-cas
(blobs SHA256 sobre FS), renaser/almacen (DAG blake3+postcard sobre
virtio-blk) y minga-core (Merkle-AST con hash estructural) son tres
capas distintas, no una duplicación — converger impondría una
abstracción equivocada. Razonado en el doc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:42:32 +00:00
sergio e2272c0ed3 feat: integra renaser (kernel SASOS bare-metal) al monorepo
renaser —kernel asíncrono de espacio de direcciones único, no-POSIX,
`no_std` x86_64— entra al monorepo como su PROPIO workspace de Cargo,
no fusionado: usa toolchain nightly, target `x86_64-unknown-none` y
`panic = "abort"`, incompatibles con los perfiles globales de brahman.

- `renaser/` — copia del proyecto (sin su `.git`; el repo original
  conserva su historia standalone). Workspace propio con su
  `rust-toolchain.toml` y `.cargo/`.
- `exclude = ["renaser"]` en el workspace de brahman: Cargo lo trata
  como ajeno.
- El kernel de renaser path-depende `mirada-layout` cruzando la
  frontera de workspace — primer núcleo compartido. Semilla de la
  Fase 8 (compositor): geometría de teselado compartida, framebuffer
  nativo de renaser; smithay se queda en el lado Linux.

Verificado: `cargo build -p boot` compila kernel + imagen UEFI con
mirada-layout enlazado para bare-metal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:37:14 +00:00
sergio 1c6aafbc24 feat(mirada): mirada-layout no_std — primer núcleo compartible con renaser
mirada-layout (el motor de teselado del compositor) pasa a `no_std +
alloc` para poder compilarse también en bare-metal — es el primer
crate-núcleo que brahman y renaser compartirán.

- `#![cfg_attr(not(test), no_std)]` + `extern crate alloc`: usa
  `alloc::{vec, collections::BTreeMap}` en vez de `std`.
- Matemática de punto flotante vía `libm` (`sqrt`/`ceil`/`round` viven
  en `std`, no en `core`).
- `serde` pasa a feature opcional: los consumidores Linux
  (mirada-protocol/brain) la activan; un consumidor bare-metal no
  necesita (de)serializar el layout.
- Deps declaradas directas (no `workspace = true`): un núcleo que
  cruzará fronteras de workspace se mantiene autocontenido.

Verificado: `cargo build --target x86_64-unknown-none` compila;
32 tests verdes; mirada-protocol/brain sin regresión.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:37:03 +00:00
sergio 5770759f2e docs(cosmobiologia): marca el dial uraniano como hecho en la SDD
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:03:42 +00:00
sergio 11d8bcb4af feat(cosmobiologia): dial uraniano de 90° — proyección geométrica
El módulo Uranian sólo listaba las fórmulas como texto; ahora también
las muestra geométricamente.

- cosmobiologia-canvas: render_uranian_dial pinta un eje horizontal
  0-90° con cada cuerpo natal proyectado en su longitud mod 90. Ticks
  en las divisiones duras (0/22½/45/67½/90°); los cuerpos que forman
  una fórmula uraniana van resaltados, y los clusters densos se
  escalonan en filas para legibilidad. La sección del footer combina
  el dial geométrico con la lista de pills de fórmulas.
- El dial aparece siempre que el módulo Uranian está activo (antes la
  sección sólo salía si había grupos detectados).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:03:14 +00:00
sergio 823eff0343 docs(cosmobiologia): marca armónicos como hechos en la SDD
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:58:07 +00:00
sergio 54de7849c5 feat(cosmobiologia): espectro de fuerza armónica — histograma clicable
Completa la feature de armónicos: además de la carta armónica, ahora
hay un espectro que guía qué armónico mirar.

- cosmobiologia-render: harmonic_spectrum computa la fuerza de cada
  armónica 1-32 (suma de cercanía a conjunción exacta de los pares de
  cuerpos en esa armónica). apply_harmonic lo puebla + expone el
  armónico activo. Campos RenderModel.harmonic / .harmonic_spectrum.
  2 tests nuevos (el pico cae en la armónica resonante).
- cosmobiologia-canvas: render_harmonic_spectrum pinta el histograma
  en el footer; cada barra es clicable y emite HarmonicSelected — un
  clic salta a esa armónica. La barra activa va resaltada.
- shell: select_harmonic fija el slider del módulo natal y recompone.
- modules: el slider de armónico pasa de 1-20 a 1-32 (rango del
  espectro).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:57:54 +00:00
sergio 968255f4cd feat(cosmobiologia): carta armónica — el slider de armónico ahora pinta
El slider "Armónico" del NatalModule existía pero no hacía nada.
Ahora re-renderiza la carta en el armónico de orden N.

- cosmobiologia-render: módulo `harmonic` agnóstico — apply_harmonic
  transforma los cuerpos natales a (longitud·N) mod 360 y recomputa
  los aspectos sobre las posiciones armónicas (conjunción, oposición,
  trígono, cuadratura, sextil). Las casas se conservan como marco.
  6 tests (incluye: quintil natal → conjunción en H5).
- cosmobiologia-engine: NatalOptions.harmonic; compose lo aplica tras
  la pasada natal, antes de los overlays. Test end-to-end.
- shell: build_natal_options lee el slider del módulo natal.

El título anota "· HN". Falta: histograma de fuerza por armónico.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:54:02 +00:00
sergio ed4d5ffe4c docs(cosmobiologia): marca el Sistema GR como completo en la SDD
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:48:54 +00:00
sergio ec111a2e27 feat(cosmobiologia): GR — scrubbing live de la edad con el jog-dial
Tercer y último incremento del Sistema GR: en modo GR (direcciones
primarias activas) el jog-dial deja de rotar el wheel y pasa a
scrubear la edad en vivo.

- canvas: CanvasState::gr_active() detecta el modo; on_jog_move emite
  CanvasEvent::GrAgeDelta (años por grado de jog, sensibilidad 0.1)
  en vez de rotar; on_jog_up no aplica snap de tiempo.
- shell: scrub_gr_age acumula el delta sobre target_age_years del
  módulo primary_directions, clampa a [0,120], sincroniza el slider
  del panel y recompone — los glifos dirigidos y el HUD se mueven en
  vivo bajo el cursor.

Con esto el Sistema GR queda completo: cómputo de triggers, resaltado
de convergencias, HUD de rectificación y scrubbing live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:47:57 +00:00
sergio 363f401b75 feat(cosmobiologia): GR — resaltado de eventos + HUD lateral de triggers
Segundo incremento del Sistema GR: el canvas ahora hace visible la
rectificación.

- Resaltado de convergencias: por cada punto natal donde un directo y
  un converso coinciden dentro del micro-orbe, un eje brillante cruza
  la zona del dual-ring hasta el cinturón natal, con marcador glow.
- HUD lateral: columna a la derecha del wheel cuando GR está activo,
  con los triggers ordenados por orbe. Color rojo→gris según orbe; las
  convergencias llevan ✦ y fondo resaltado.

paint_wheel recibe los gr_triggers; render_wheel monta el body como
fila wheel+HUD sólo en modo GR.

Falta: scrubbing live del jog-dial mapeado a la edad.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:44:14 +00:00
sergio 15e45ace9b feat(cosmobiologia): GR — cómputo de triggers y eventos de rectificación
Primer incremento del Sistema GR (García Rosas): la engine, además del
dual-ring directo/converso, ahora computa los triggers de rectificación
y detecta las convergencias directo+converso sobre un mismo punto natal.

- cosmobiologia-render: módulo `gr` agnóstico — tipos GrTrigger/GrDirection
  + compute_gr_triggers (emparejamiento puro, 7 tests). Campo gr_triggers
  en RenderModel (serde-default, back-compat).
- cosmobiologia-engine: build_primary_directions_overlay computa los
  triggers contra cuerpos natales + 4 ángulos; orbe HUD 2°, micro-orbe
  de evento 5'. Test end-to-end con eternal.

Falta: resaltado del evento en el canvas, HUD lateral, scrubbing live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:40:09 +00:00
sergio e77a32f4d6 feat(minga): minga-vfs — proyecta el repo como filesystem FUSE
minga-vfs deja de ser un stub: monta el repositorio direccionado por
contenido como un filesystem FUSE de sólo lectura. roots/<hash> da el
código fuente reconstruido (formato normalizado) de cada raíz del MST;
cas/<hash> resuelve cualquier hash bajo demanda como S-expression.

Capas separadas: render (SemanticNode→texto, puro) + source (contrato
NodeSource, backends sled/memoria) + fs (único módulo con fuser).
Nuevo subcomando `minga mount <punto>`. Dep fuser 0.15 sin libfuse-dev
(default-features = false). 14 tests nuevos, sin regresión en minga-cli.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:23:44 +00:00
sergio 762bf95dfd feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
Nuevo crate `crates/init/arje-absorb`: lee la configuración de un init
clásico y la traduce a una Tarjeta Semilla (Card JSON) con cada
servicio como hija genesis de arje-zero. El paso «absorber» de la
migración a arje — para no perder los servicios al cambiar de init.

- Absorbers: sysvinit (/etc/inittab), runit (runsvdir o /etc/sv),
  dinit (/etc/dinit.d), openrc (/etc/runlevels). Autodetección.
- Modelo intermedio ForeignService → Card vía brahman-card (validado).
- `--with-carmen`: agrega carmen-dm (gestor de login gráfico).
- CLI: --from/--root/--output/--label/--with-carmen. 24 tests, clippy
  limpio.

`scripts/migrate-to-arje.sh`: orquesta absorber → validar → (carmen:
compila+instala mirada dinámico) → install-arje-as-init.sh. El init
viejo queda intacto; arje se elige en GRUB. --dry-run no toca nada.

systemd no se absorbe (units no son texto trivial) — para systemd
sigue la capa de shims + seeds/arje-host.card.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 00:40:34 +00:00
sergio 3339fb009c fix(arje): saneo ente-→arje- en scripts y seeds de boot
El rename ente→arje dejó referencias stale al binario PID 1 y a los
shims. Los nombres reales (verificados con cargo metadata) son todos
arje-*: arje-zero, los 14 arje-*-compat, arje-echo, arje-policy-provider,
arje-bus, arje-brain.

- build-arje-initrd.sh, install-arje-as-init.sh, uninstall-arje.sh,
  run-arje-qemu.sh: `-p ente-*` → `-p arje-*`, paths /sbin/arje-zero y
  /usr/sbin/arje-*, RUST_LOG arje_zero=info.
- seeds/arje-prod y arje-host: los exec `/usr/sbin/ente-*-compat`
  apuntaban a binarios que no existirían tras instalar — corregidos a
  `/usr/sbin/arje-*`. (validate.sh no chequea exec, por eso «validaban»
  igual; al boot real habrían fallado.)

Intactos a propósito: `/ente/` (directorio canónico de la Semilla) y
`ente.slice/*` (jerarquía cgroup). Las 3 seeds validan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 00:40:34 +00:00
sergio 663fd6e38a feat(carmen): carmen-dm como Ente supervisado en la Semilla arje-host
Pieza 2 del DM: declara `mirada-compositor --greeter` como hijo genesis
de arje-zero en `seeds/arje-host.card.json`, reemplazando el agetty de
tty1. Con `supervision: Restart { 2000, 60000 }`, arje-zero lo
respawnea si panica — la resiliencia «PID 2 supervisado» (sshd queda
como rescate remoto). Va en arje-host (boot de hardware real con rootfs
y mesa), NO en arje-prod (seed del initrd, sin GPU).

Además: `seeds/validate.sh` tenía referencias `ente-zero` stale del
rename ente→arje — corregidas a `arje-zero` (paquete + binario). Las
tres seeds validan. Tabla de seeds actualizada en docs/arje-boot.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 00:22:19 +00:00
sergio 758f61f52a feat(carmen): modo greeter — mirada-compositor como DM
`mirada-compositor --greeter` arranca como gestor de login: lanza
mirada-greeter como proceso hijo, lee su stdout y, al recibir el
SessionTicket, muta de BodyMode::Greeter a BodyMode::Session sin
reiniciar el servidor Wayland — la «mutación atómica» del DM.

- BodyMode { Greeter, Session }: eje ortogonal a Brain (Embedded/Linked).
- modo greeter: sin atajos registrados, rechaza Spawn, sin autoarranque.
- traspaso (complete_greeter_handoff): registra los atajos y arranca la
  sesión — el comando del tiquet, o el autoarranque del usuario.
- privilegios: el compositor corre como root; spawn_command baja a
  setuid/setgid + grupos suplementarios del usuario autenticado.
- bandera ortogonal al backend (--greeter [--drm|--winit]); el tiquet
  llega por un canal calloop en DRM y por mpsc en winit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 00:06:59 +00:00
sergio 634a43006a feat(charka): PICTURE de edición — Z, coma de millares y punto decimal
El formateo de informes de COBOL: supresión de ceros a la izquierda,
coma de millares e inserción del punto decimal. Rebanada vertical.

- charka-lexer: el punto separador exige un espacio detrás; un punto
  pegado a un carácter (ZZ9.99) ya no es terminador, sino símbolo —
  el parser lo reensambla dentro de la cláusula PICTURE.
- charka-runtime: format_edited(valor, pic) — 9, Z, coma, punto, B.
- charka-ir: Field::edit guarda la PICTURE; el campo es texto.
- charka-codegen / charka-shadow: MOVE a un campo de edición pasa por
  format_edited antes de almacenar.
- Corpus: 19-reporte. Sombra y crate compilado dan la misma salida.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:00:15 +00:00
sergio b3278bdb0c feat(charka): E/S de ficheros — SELECT/FD/OPEN/READ/WRITE/CLOSE
El gran hueco que faltaba para el COBOL real: el procesamiento de
ficheros secuenciales. Una rebanada vertical por los seis crates.

- charka-parser: la ENVIRONMENT division ya no se ignora — se parsea
  FILE-CONTROL (SELECT name ASSIGN TO "ruta"); del FILE SECTION se
  asocia cada FD con su registro 01. Program::files.
- charka-runtime: tipo CobFile — un fichero «line sequential» (cada
  registro una línea). Lectura: carga a memoria. Escritura: acumula y
  vuelca al cerrar.
- charka-ir: Ir::files y los statements Open/Close/Read/Write. READ
  lleva sus bloques AT END / NOT AT END.
- charka-codegen: un campo CobFile por fichero en el struct Program;
  los verbos emiten llamadas al runtime.
- charka-shadow: el intérprete hace E/S de ficheros real.
- Corpus: programa nuevo 18-fichero — escribe tres líneas, las relee
  con READ ... AT END y las muestra. Verificado: el intérprete sombra
  y el crate compilado por scaffold dan la misma salida.

Alcance v1: organización line sequential; sin ficheros indexados ni
relativos, sin FILE STATUS.

Tests: charka-parser 17, charka-runtime 19, charka-ir 30,
charka-codegen 25, charka-shadow 23. fmt + clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:47:26 +00:00
sergio f250fd0765 feat(charka): PERFORM ... THRU como rango real de párrafos
PERFORM A THRU C ejecuta A, B y C; antes el transpilador sólo
ejecutaba A (lo marcaba como aproximado).

- charka-codegen: Symbols registra ahora los párrafos en orden con su
  nombre de método; Symbols::build toma el Ir completo.
  paragraph_range(name, thru) da los métodos del rango; emit_perform
  emite la llamada a cada uno.
- charka-shadow: run_paragraph_range ejecuta los párrafos de name a
  thru inclusive.
- Corpus: programa nuevo 17-rangopar (PERFORM PASO-A THRU PASO-C sobre
  tres párrafos). Verificado: el intérprete sombra y el crate
  compilado por scaffold dan la misma salida.

Tests: charka-codegen 24, charka-shadow 22. fmt + clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:36:53 +00:00
sergio 82ba0b7a1a feat(charka): SET ... TO TRUE — escribir nombres de condición (88)
La cara de escritura de los nombres de condición de COBOL: si
IF ES-VALIDO los lee, SET ES-VALIDO TO TRUE los escribe.

- IR: Stmt::SetTrue { conditions }.
- Parser: SET cond-1 cond-2 ... TO TRUE. Otras formas de SET
  (índices, TO FALSE) caen a Stmt::Unknown.
- Codegen y shadow: SET cond TO TRUE asigna a su dato padre el valor
  del 88 (un MOVE del valor a la variable).
- Corpus: programa nuevo 16-bandera (cambia banderas de texto y de
  número con SET). Verificado: el intérprete sombra y el crate
  compilado por scaffold dan la misma salida.

Tests: charka-ir 29, charka-codegen 23, charka-shadow 21. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:32:08 +00:00
sergio fa65f20206 feat(charka): INITIALIZE — resetear datos y grupos
El verbo de COBOL para volver un dato (o un registro entero) a su
valor por defecto.

- IR: Stmt::Initialize { targets }. El model de charka-ir registra
  ahora los grupos y sus datos elementales (DataModel::groups,
  GroupInfo { name, members }).
- Parser: INITIALIZE name-1 name-2 ...
- Codegen y shadow: cada destino, si es un grupo, se expande a sus
  miembros; cada dato elemental se pone a 0 (numérico) o a espacios
  (alfanumérico); una tabla OCCURS resetea todos sus elementos.
- Corpus: programa nuevo 15-resetear (resetea un grupo y un escalar).
  Verificado: el intérprete sombra y el crate compilado por scaffold
  dan la misma salida.

Tests: charka-ir 28, charka-codegen 22, charka-shadow 20. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:28:47 +00:00
sergio 7867d6830e feat(charka): EVALUATE TRUE y rangos WHEN ... THRU
Completa el EVALUATE con sus dos formas que faltaban.

- IR: la rama WhenBranch pasa de values: Vec<Operand> a
  tests: Vec<WhenTest>, donde WhenTest es Value (igualdad), Range
  (WHEN lo THRU hi) o Cond (EVALUATE TRUE WHEN cond).
- Parser: detecta EVALUATE TRUE y entonces cada WHEN parsea una
  condición; en modo valor reconoce WHEN lo THRU hi.
- Codegen y shadow: una prueba Range se traduce a lo <= s <= hi; una
  Cond, a la condición directa.
- Corpus: programa nuevo 14-clasifica (clasifica notas con rangos THRU
  y un EVALUATE TRUE). Verificado: intérprete sombra y crate compilado
  dan la misma salida.

Tests: charka-ir 27, charka-codegen 21, charka-shadow 19. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:22:43 +00:00
sergio 2728698f5e feat(charka): INSPECT — contar y reemplazar caracteres
El verbo de COBOL para analizar y limpiar campos de texto.

- IR: Stmt::Inspect { target, op } con InspectOp::TallyingForAll
  (cuenta apariciones y las suma a un contador) y
  InspectOp::ReplacingAll (reemplaza apariciones).
- Parser: INSPECT t TALLYING n FOR ALL lit y
  INSPECT t REPLACING ALL a BY b. Una forma no soportada cae a
  Stmt::Unknown.
- Codegen: TALLYING -> str::matches(..).count(); REPLACING ->
  str::replace.
- Shadow: el intérprete cuenta / reemplaza el texto.
- Corpus: programa nuevo 13-inspeccion. Verificado: el intérprete
  sombra y el crate compilado por scaffold dan la misma salida.

Alcance v1: TALLYING FOR ALL y REPLACING ALL; sin LEADING, FIRST,
CHARACTERS, BEFORE/AFTER.

Tests: charka-ir 26, charka-codegen 20, charka-shadow 18. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:17:47 +00:00
sergio 47c49acd47 feat(charka): STRING y UNSTRING — manejo de cadenas
Dos verbos comunes de COBOL para construir y partir cadenas.

- IR: Stmt::StringConcat { sources, into } y
  Stmt::Unstring { source, delimiter, into }.
- Parser: STRING a b DELIMITED BY SIZE INTO t END-STRING y
  UNSTRING s DELIMITED BY d INTO a b c END-UNSTRING.
- Codegen: STRING -> format! concatenado; UNSTRING -> un bloque que
  parte con str::split y reparte los trozos a los destinos.
- Shadow: el intérprete concatena / parte el texto y lo reparte.
- Corpus: programa nuevo 12-cadenas. Verificado: el intérprete sombra
  y el crate compilado por scaffold dan la misma salida.

Alcance v1: STRING con DELIMITED BY SIZE (otros delimitadores se
ignoran); sin WITH POINTER ni ON OVERFLOW.

Tests: charka-ir 25, charka-codegen 19, charka-shadow 17. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:09:10 +00:00
sergio 3902763daa feat(charka): OCCURS — tablas y referencias con subíndice
Los arrays de COBOL, que antes el transpilador descartaba en silencio.
Una rebanada vertical amplia que atraviesa el pipeline entero.

- Parser: la cláusula OCCURS n [TIMES] se captura en DataItem.
- IR: Operand::Indexed { name, index } — una referencia ELEM(I), con
  subíndice 1-based. Los destinos de los statements pasan de
  Vec<String> a Vec<Operand>, así que se puede escribir a un elemento
  de tabla (MOVE x TO ELEM(I), COMPUTE ELEM(I) = ...). model::Field
  gana occurs: Option<u32>.
- Codegen: un campo OCCURS se emite como Vec<Num>/Vec<Text>,
  inicializado con vec![..; n]; una referencia con subíndice indexa el
  vector (1-based -> 0-based).
- Shadow: en el intérprete todo campo es un vector — un escalar es de
  longitud 1, una tabla de n; las referencias se resuelven a
  (nombre, índice).
- Corpus: programa nuevo 11-tabla (llena una tabla con cuadrados y los
  suma). Verificado: el intérprete sombra y el crate compilado por
  scaffold dan ambos SUMA DE CUADRADOS = 000055.

Alcance v1: OCCURS elemental, una dimensión, subíndice de un operando.
Fuera: OCCURS de grupo, multidimensional, DEPENDING ON.

Tests: charka-parser 16, charka-ir 24, charka-codegen 18,
charka-shadow 16. fmt + clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:03:48 +00:00
sergio 28ee1ae260 feat(charka): nivel 88 + modelo de datos compartido en charka-ir
Los nombres de condición de COBOL (IF ES-VALIDO), que antes el
transpilador evaluaba siempre como false. Y, de paso, se elimina la
duplicación de la resolución del modelo de datos.

- charka-ir gana un módulo `model`: resolve_data(&[DataItem]) ->
  DataModel aplana el árbol de datos a campos elementales (Field con
  FieldKind) y a nombres de condición (ConditionName). El Ir lleva
  ahora un campo `model` — la fuente única de verdad sobre la
  clasificación de PICTURE.
- charka-codegen y charka-shadow consumen ir.model en vez de
  reimplementar cada uno la clasificación, el ancho de PICTURE y la
  normalización de VALUE. charka-codegen ya no depende de charka-bcd.
- Cond::Named (un nivel 88) se resuelve a `padre = valor`: el codegen
  emite la comparación, el intérprete sombra la evalúa.
- Corregido: un dato con hijos de nivel 88 antes se perdía como si
  fuera un grupo; ahora se reconoce como campo elemental.
- Corpus: programa nuevo 10-condicion (semáforo con 88 de texto y de
  número). Verificado: intérprete y crate compilado dan igual salida.

Tests: charka-ir 23, charka-codegen 17, charka-shadow 15. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:50:06 +00:00
sergio 4df7478b71 feat(charka): EVALUATE — el case de COBOL
EVALUATE atraviesa el pipeline entero — antes el parser lo guardaba
crudo como Stmt::Unknown.

- IR: Stmt::Evaluate { subject, whens, other } con
  WhenBranch { values, body }. Varios WHEN apilados comparten cuerpo;
  WHEN OTHER es el caso por defecto.
- Parser: EVALUATE subject WHEN v1 WHEN v2 ... [WHEN OTHER ...]
  END-EVALUATE.
- Codegen: lo baja a una cadena if / else if / else — una rama se
  elige si el sujeto es igual a alguno de sus valores, sin caída.
- Shadow: el intérprete evalúa el sujeto y ejecuta la primera rama
  cuyos valores casen, o el WHEN OTHER.
- Corpus: programa nuevo 09-evaluar (EVALUATE por valor anidado en un
  PERFORM VARYING, con WHEN apilados y WHEN OTHER). Verificado: el
  intérprete sombra y el crate compilado por scaffold dan la misma
  salida.

Alcance v1: EVALUATE por igualdad de valor; no la forma EVALUATE TRUE
con condiciones ni los rangos THRU.

Tests: charka-ir 19, charka-codegen 16, charka-shadow 14. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:37:28 +00:00
sergio 321e6f8e27 feat(charka): PERFORM VARYING — el bucle con variable de control
El bucle más usado de COBOL, que antes el parser degradaba a un
PERFORM vacío (un hueco de corrección real). Ahora atraviesa el
pipeline entero como una rebanada vertical.

- IR: PerformControl::Varying { var, from, by, until }.
- Parser: reconoce PERFORM VARYING var FROM x BY y UNTIL cond en
  línea (END-PERFORM) y fuera de línea (PERFORM párrafo VARYING ...).
- Codegen: emite var = from; while !(until) { cuerpo; var += by; }.
- Shadow: el intérprete inicializa la variable, evalúa la condición
  antes de cada vuelta e incrementa al final.
- Corpus: programa nuevo 08-varying (suma 1..10). Verificado: el
  intérprete sombra y el crate compilado por scaffold dan ambos
  SUMA 1 A 10 = 00055 — las dos rutas concuerdan.

Tests: charka-ir 18, charka-codegen 15, charka-shadow 13. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:33:14 +00:00
sergio b052c41e3c feat(charka): CLI del transpilador — transpile / scaffold / run / check
App nueva crates/apps/charka — el binario `charka`, que vuelve usable
el pipeline COBOL->Rust desde la terminal.

- transpile <in.cob> [-o out.rs] — emite el código Rust.
- scaffold <in.cob> -o <dir> — genera un crate Rust completo
  (Cargo.toml + src/main.rs) que depende de charka-runtime y compila.
- run <in.cob> — ejecuta el programa con el intérprete sombra, sin
  compilar nada, y muestra su salida.
- check <in.cob> -e <esperado> — ejecuta y diferencia contra una
  salida esperada; reporta las líneas que difieren.

Avisa de los verbos COBOL que aún no se transpilan. Verificado de
punta a punta contra el corpus: scaffold de 06-nomina genera un crate
que compila y produce la misma salida que el intérprete sombra — las
dos rutas de ejecución concuerdan.

4 tests; fmt + clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:28:36 +00:00
sergio 4d9ce11b1e feat(charka): charka-shadow — validador en sombra + corpus COBOL
El pipeline COBOL->Rust queda completo (7 crates) y validado de punta
a punta.

charka-shadow certifica que el transpilador preserva la semántica del
COBOL original con una ejecución sombra: un intérprete que corre el Ir
directamente sobre charka-runtime, sin compilar nada. Es una segunda
ruta de ejecución, independiente del código que emite charka-codegen
— si la sombra y el transpilado divergieran, sería un bug.

- interpret(&Ir) -> Outcome ejecuta el IR y captura las líneas de
  DISPLAY; run_source(&str) corre el pipeline completo.
- Tope de pasos (Halt::StepLimit): un bucle que no termina se corta
  en vez de colgarse.
- Módulos: field (datos -> campos vivos) / interp (el motor).

Corpus nuevo crates/modules/charka/corpus/ — 7 programas COBOL de
complejidad graduada (01-hola .. 07-clasificar) con sus salidas
esperadas verificadas a mano: DISPLAY, aritmética con GIVING,
IF/ELSE, PERFORM TIMES/UNTIL, grupos, COMPUTE con paréntesis,
ROUNDED, IF anidado con AND. Material de prueba del pipeline entero.

11 tests (los 7 del corpus + fuente vacío, STOP RUN, tope de pasos,
error de léxico); fmt + clippy limpios.

No hay GnuCOBOL en la máquina: la referencia v1 es el corpus; un modo
futuro diferenciará contra el compilador real.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:23:07 +00:00
sergio e52b3fb572 feat(charka): charka-codegen — emisión de Rust desde el IR
La etapa final del transpilador. generate(&Ir) -> String produce un
fuente Rust (un main.rs) que, compilado contra charka-runtime, ejecuta
la lógica del programa COBOL.

- struct Program con un campo Num/Text por dato elemental; new() lo
  inicializa desde las cláusulas VALUE.
- Un método p_<párrafo> por párrafo del PROCEDURE; run() los encadena
  en orden (el «caer» de COBOL); main() construye y corre.
- Cada Stmt -> código Rust: MOVE->.store/.fill, DISPLAY->println!,
  COMPUTE y aritmética -> expresiones Decimal, IF->if/else,
  PERFORM-> llamada / for / while, STOP RUN->process::exit.
- Tolerante: lo no transpilable (Stmt::Unknown, dato sin resolver, **)
  se emite como comentario // charka: — el código generado compila.
- Saneado de identificadores COBOL->Rust (choques con keywords).
- Verificado de punta a punta: un programa COBOL demo transpila a Rust
  que compila contra charka-runtime y produce la salida esperada.
- Módulos: emit / sym / expr / stmt. 14 tests; fmt + clippy limpios.

El pipeline COBOL->Rust corre de punta a punta. Falta sólo
charka-shadow (validador en sombra).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:36:26 +00:00
sergio 85156c1509 feat(charka): charka-runtime — soporte de ejecución (campos Num y Text)
El soporte que los programas COBOL transpilados enlazan. charka-codegen
emitirá Rust que llama a esta biblioteca, no Rust autónomo.

- Num: campo numérico (PIC 9(5)V99) — un Decimal conformado a su
  Picture. store trunca a la escala declarada, store_rounded redondea;
  al desbordar la parte entera conserva los dígitos de bajo orden (el
  ON SIZE ERROR de COBOL sin cláusula). display da los dígitos con
  relleno de ceros y signo.
- Text: campo alfanumérico (PIC X(n)) de longitud fija — store
  justifica a la izquierda y rellena/trunca; fill mueve figurativas.
- cobol_text_cmp: comparación alfanumérica con relleno de espacios.
- Reexporta Decimal/Picture/Rounding de charka-bcd.

Construido antes que charka-codegen (la nota de orden del plan los
listaba al revés): el codegen emite contra esta API. 17 tests; fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:27:28 +00:00
sergio 71a4068d12 feat(charka): charka-ir — representación intermedia con statements tipados
Tercera etapa del transpilador: Program -> Ir. El PROCEDURE division
pasa de sentencias con tokens crudos a un árbol de instrucciones
tipadas.

- lower(&Program) -> Ir: total y tolerante, nunca falla. La DATA
  division pasa tal cual y sirve de tabla de símbolos.
- Stmt cubre MOVE, DISPLAY, ACCEPT, COMPUTE, ADD, SUBTRACT, MULTIPLY,
  DIVIDE, IF/ELSE/END-IF, PERFORM (fuera de línea, en línea, TIMES,
  UNTIL), GO TO, STOP RUN, GOBACK, EXIT, CONTINUE.
- Expresiones de COMPUTE con precedencia y paréntesis (Pratt).
  Condiciones con comparadores símbolo/palabra, AND/OR/NOT y nombres
  de condición (nivel 88).
- Delimita statements por palabras frontera (COBOL no los separa con
  un símbolo). Verbo no soportado -> Stmt::Unknown con tokens crudos.
- Módulos: ast / kw / cursor / expr / stmt. 17 tests; fmt + clippy
  limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:23:19 +00:00
sergio b95383b01a chore: dejar de versionar nakui-ui-state.jsonl
El event-log que nakui-ui escribe en el cwd al correr es un artefacto
de runtime — entró por error en el commit anterior. Lo saca del
índice y lo agrega a .gitignore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:04:13 +00:00
sergio d3cdbb2d2d feat(charka): charka-parser — COBOL'85 (subconjunto) a AST
Segunda etapa del transpilador: Vec<Token> -> Program. Alcance v1 = el
esqueleto del programa.

- parse(&[Token]) -> Result<Program, ParseError>. AST: Program
  (program_id, data, paragraphs), DataItem, Paragraph, Sentence.
- Particiona el flujo en las 4 divisions por sus encabezados; extrae el
  PROGRAM-ID de la IDENTIFICATION.
- DATA division -> árbol de DataItem: nivel, nombre, PICTURE
  reensamblado (S9 ( 5 ) V99 -> S9(5)V99) y VALUE. Anida por número de
  nivel (01/77 raíces, 88 cuelga del precedente).
- PROCEDURE division -> Vec<Paragraph> con Sentence de tokens crudos
  (sin parseo de statement). Sentencias previas al primer encabezado
  van a un párrafo implícito "".
- Tolerante: salta SECTION, FD/SD y cláusulas que no sean PIC/VALUE.
- 15 tests verdes; fmt + clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:04:00 +00:00
sergio ab56b35e9f feat(charka): charka-lexer — tokenizador de COBOL
Primera etapa del transpilador COBOL→Rust (Fase D del plan macro):
texto COBOL → secuencia de Token. Lexer deliberadamente tonto (emite
Word para todo identificador, la clasificación es del parser). Tokens
Word/Number/String/Period/Symbol con línea+columna; soporta formato
fijo (tarjeta de 80 columnas) y libre; comentarios, comillas dobladas,
operadores de 1 y 2 caracteres. LexError tipado. 17 tests; clippy
limpio. Limitación v1: sin continuación de literales entre líneas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:54:54 +00:00
sergio c56ef25546 feat(nakui): Fase 7 del ERP — pulido (cierra el plan maestro)
Validación inline: al fallar un submit por campos required vacíos, el
form los marca (label destructivo + mensaje debajo), no sólo un toast.
MetaApp.form_errors + validate_required_fields. Secciones de formulario:
FieldSpec.section agrupa campos bajo encabezados; abrir_form del CRM las
usa. Campos condicionales y pulido puramente visual: scope-out conciente.

El plan docs/nakui-erp-masterplan.md queda completo (7/7 fases). Tests
verdes (meta-schema 16, meta-runtime 70, meta-form 8, nakui-ui 14);
clippy limpio en las libs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:43:44 +00:00
sergio b13486e240 feat(nakui): Fase 6 del ERP — export CSV de listas
Toda vista de lista gana un botón «⬇ CSV» que exporta las filas
filtradas/ordenadas (con refs resueltas y montos formateados) a un
archivo <entity>-<timestamp>.csv. Serializador to_csv (RFC 4180, con
escape) en el módulo nuevo meta-runtime/csv.rs. Refactor:
list_filtered_sorted extraído como helper compartido entre el render
de la lista y el export.

Tests de to_csv; meta-runtime 70 + meta-form 8 verdes, clippy limpio.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:32:48 +00:00
sergio ab2b8f6638 feat(nakui): Fase 5 del ERP — tablero de KPIs
View::Dashboard: grilla de tarjetas de agregados. Metric Count/Sum/
GroupBy con filtro opcional (CardFilter), computado por compute_metric
en meta-runtime (MetricResult Scalar/Breakdown). meta-form render_dashboard
pinta cada tarjeta con el número grande formateado o un breakdown con
barras de texto. El CRM gana una vista «Panorama»: clientes,
oportunidades, pipeline, ganadas, y breakdowns por etapa y canal.

Tests de compute_metric; verificación del panorama en nakui-ui. Clippy
limpio en las libs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:29:27 +00:00
sergio ab1cf9998a feat(nakui): Fase 4 del ERP — listas profesionales (orden/búsqueda/página)
Las vistas de lista de meta-form ganan: orden por columna (clic en
header cicla asc→desc→off con indicador ▲/▼), búsqueda en vivo (caja 🔍
que filtra por search_in mientras se teclea, vía cx.observe del
TextInput) y paginación (25/página, controles ◀▶). Sin cambios de
schema: son estado del widget. Helpers puros cmp_values (meta-runtime)
y next_sort con tests.

Tests verdes (meta-runtime 63, meta-form 8); clippy limpio.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:20:15 +00:00
sergio 6588d0ed6c feat(nakui): Fase 3 del ERP — ficha de detalle
View::Detail: ficha de un record con sus campos + listas de records
relacionados (RelatedList, back-references por via_field) + botones
Volver/Editar. ListView.row_detail enlaza lista→ficha con un botón 👁
por fila; Module::validate exige que apunte a una vista detail. En
meta-form: render_detail/render_related + select_detail con retorno.

El CRM: 👁 en Clientes y Oportunidades abre su ficha; la del cliente
lista sus oportunidades e interacciones. Tests en meta-schema y
nakui-ui verdes; clippy limpio.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:12:26 +00:00
sergio eba629a806 feat(nakui): Fase 2 del ERP — relaciones legibles + formato
Column.ref_entity resuelve un UUID al label del record referido;
Column.format (ValueFormat Number/Currency) agrupa miles y prefija
símbolo. El campo entity_ref en formularios muestra el record elegido
por su label, no el UUID. human_label_for_record reconoce nombre/titulo
(español). El módulo CRM: las listas muestran el nombre del cliente y
monto como $12,000.

Helper format_value en meta-runtime. Tests en meta-schema, meta-runtime
y nakui-ui verdes; clippy limpio.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:05:04 +00:00
sergio 86d06da020 feat(nakui): Fase 1 del ERP — FieldKind Select + AutoId, seed inyecta id
Primera fase del plan maestro. La metainterfaz gana dos tipos de campo:
Select (chips de un conjunto cerrado, con options validadas) y AutoId
(UUID autogenerado read-only). NakuiBackend::seed inyecta el id de la
entity = clave del store. El módulo CRM los adopta: etapa/canal son
selects, los ids de idempotencia se autogeneran, el form de cliente ya
no pide id. Ningún formulario pide un UUID a mano.

Tests en meta-schema, meta-runtime y nakui-ui verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:55:13 +00:00
sergio 0d1e378e42 docs(nakui): plan maestro del ERP profesional
7 fases ordenadas por dependencia e impacto para llevar nakui de
"listas y formularios que funcionan" a ERP terminado: captura sin
fricción, relaciones legibles, ficha de detalle, listas profesionales,
tablero/KPIs, reportes, pulido. Más estado actual y criterios de
"terminado".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:55:13 +00:00
sergio ec83dd7fb7 style(nakui-ui): cargo fmt en backend.rs
Sólo formato (orden de imports + wrapping), arrastrado por cargo fmt -p
nakui-ui al trabajar en el módulo CRM.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:36:04 +00:00
sergio e187ab4cd3 feat(nakui-ui): CRM como ERP — UiModule con listas y formularios
examples/nakui-modules/crm/module.json: el módulo crm se ve ahora como
un ERP en nakui-ui (sidebar + listas + formularios), no sólo como el
timeline del event log. 7 vistas — lista+form de Clientes, Oportunidades
e Interacciones — con los formularios de morfismo Abrir/Mover/Registrar
que disparan los morfismos reales del kernel (nakui_module_dir engancha
el módulo crm). 2 tests verifican parseo, validación y carga por el
camino brahman_cards.

Correr: NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:35:35 +00:00
sergio 78fbde12b4 feat(nakui): módulo crm — clientes, pipeline de ventas, interacciones
Módulo CRM declarativo (schema.ncl + nsmc.json + morfismos Rhai) con
tres entities (Cliente, Oportunidad, Interaccion) y tres morfismos:
abrir_oportunidad, mover_oportunidad (pipeline con validación de
transiciones) y registrar_interaccion.

crm_demo: demo realista de 18 eventos que —a diferencia de los otros
demos— conserva el event log e imprime el comando de nakui-explorer,
así el explorador muestra un CRM con cuerpo. tests/crm.rs: 8 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:21:09 +00:00
sergio bb21c28eb1 feat(mirada): mirada-greeter — greeter de login del escritorio carmen
App GPUI con app_id carmen.greeter: formulario usuario+contraseña que
autentica con brahman-auth en un hilo de fondo y, en éxito, emite un
SessionTicket por stdout para que el compositor haga el traspaso a modo
sesión. Backend mock (MIRADA_GREETER_MOCK) o PAM.

Incluye brahman-auth::SessionTicket (contrato de tiquet greeter→compositor,
serializado a una línea con prefijo versionado) y el modo enmascarado de
nahual-widget-text-input (TextInput::with_mask para contraseñas).

18 tests nuevos; greeter verificado por compilación + clippy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:59:12 +00:00
sergio 8a15b812f9 feat(auth): brahman-auth — autenticación del escritorio (PAM + mock)
Base del DM/greeter de carmen. Contrato Authenticator agnóstico:
authenticate(usuario, secreto) -> UserInfo (uid/gid/home/shell).
PamAuthenticator verifica contra PAM (/etc/pam.d/carmen); MockAuthenticator
con credenciales en memoria para tests. AuthError grueso: BadCredentials
vs AccountUnavailable, sin filtrar existencia de cuentas. resolve_user
vía getpwnam. data/carmen como servicio PAM; ejemplo auth-probe.

11 tests; el camino PAM real se ejercita.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:47:05 +00:00
sergio af3be482a9 feat(theme): exportación a GTK + inyección de entorno en el compositor
Segunda mitad de la uniformización del tema. nahual-theme::toolkit
traduce el Theme activo a gtk-3.0/gtk.css y gtk-4.0/gtk.css con overrides
@define-color (acento exacto + neutro claro/oscuro sintetizado).
Theme::set/install_default exportan best-effort; guarda de no-pisar
respeta un gtk.css ajeno. El compositor inyecta XDG_CURRENT_DESKTOP=mirada
y QT_QPA_PLATFORMTHEME=gtk3 a cada hijo, así GTK y Qt siguen el tema.

8 tests nuevos en toolkit; ejemplo dump-toolkit-css.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:41:35 +00:00
sergio 5369c307e4 feat(mirada): mirada-portal — backend de tema org.freedesktop.appearance
Backend de xdg-desktop-portal para carmen: implementa
org.freedesktop.impl.portal.Settings y publica color-scheme,
accent-color y contrast desde el tema activo de nahual. GTK4, Qt6,
Firefox y Chromium voltean claro/oscuro + acento por protocolo, sin
tocar sus configs. Watcher con notify del archivo de nahual-theme →
emite SettingChanged en vivo. 13 tests; smoke verificado sobre un bus
de sesión efímero.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:05:27 +00:00
sergio 2bd6aaad02 feat(shuma): cajón de resultados del shell — desplegable desde el pie
Fase 3c: el shell muestra la salida de los comandos en un cajón que se
despliega hacia arriba sobre el escritorio.

carmen — la ventana del shell deja de tener un alto fijo: `render_loc`
la ancla al pie de la salida y la coloca por su **tamaño real**, así
puede crecer hacia arriba. La franja reservada sigue siendo la barra
(40 px); el cajón, al abrirse, se solapa sobre las teseladas sin
re-teselar. `render_loc` toma ahora el alto de la salida.

shuma-shell — un clic en el estado alterna `drawer_open`: la ventana
crece (`Window::resize`, que GPUI 0.2 expone) a barra + cajón, o
vuelve a sólo barra. El cajón reusa `render_run` para pintar los
últimos comandos y su salida, con scroll. `render_launcher` pasa a una
columna: cajón opcional arriba, barra abajo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 07:05:14 +00:00
sergio a9e880240d feat(shuma): barra de ventanas en el modo launcher
Fase 3b: la barra del shell muestra ahora las ventanas abiertas del
escritorio y deja saltar entre ellas.

- `shuma-shell` depende de `mirada-brain` para hablar el protocolo de
  control de carmen.
- `start_loop` sondea el socket de control cada ~1 s con `ListWindows`
  — la llamada bloquea un instante, pero en el executor de fondo, no en
  el hilo de la UI. El resultado se guarda en `Shell.windows_bar`.
- `render_launcher` dibuja una cajita por ventana entre el input y el
  estado: la enfocada resaltada, las demás en gris. Un clic envía
  `Do(FocusWindow(id))` y refleja el cambio al instante (el sondeo lo
  confirma en el siguiente ciclo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 06:21:06 +00:00
sergio b17ac8c67a feat(shuma): modo launcher — shuma-shell como el shell de carmen
Fase 3a del plan «shell»: `shuma-shell --launcher` (o la variable
`MIRADA_SHELL`) arranca el shell como una barra compacta acoplada al
pie de carmen, en vez del panel de 3 columnas.

- `run_launcher` abre la ventana GPUI sin barra de título y con
  `app_id = "carmen.shell"` — el acople del compositor la reconoce y le
  reserva su franja. GPUI 0.2 admite `WindowOptions.app_id`.
- `Shell.launcher: bool`; `Render::render` deriva a `render_launcher`
  cuando está activo: una barra de una línea — un glifo, la línea de
  comandos y el estado del último comando (en curso / ✓ / ✗).
- La construcción de la fila del input (tokens coloreados + caret +
  sugerencia fantasma) sale a un helper `input_row` que comparten el
  panel completo y el modo launcher — sin duplicar el resaltado.

`shuma-shell --launcher` va al `autostart.example`. Falta (3b/c/d): la
barra de ventanas abiertas, el cajón de resultados y la config.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 05:45:36 +00:00
sergio ee27108f6c feat(mirada): acople del shell — ventana-dock al pie de la pantalla
Fase 2 del plan «shell»: carmen reconoce la ventana del shell y le
reserva su sitio, en vez de teselarla como una más.

Una ventana cuyo `app_id` es `carmen.shell` no entra en el teselado:
carmen le reserva una franja de 40 px al pie de la salida, la dimensiona
y la fija ahí, y la compone sobre todas las demás. El Cerebro tesela el
resto de ventanas en el área que queda.

- `mirada-protocol`: nuevo `BodyEvent::OutputResized { id, w, h }` — el
  Cerebro cambia el área útil de una salida **sin** perder el escritorio
  que muestra (a diferencia de quitar y volver a añadir la salida — que,
  de paso, era un bug latente al redimensionar la ventana winit).
- `mirada-brain`: `Desktop` atiende `OutputResized` (test nuevo).
- `mirada-body`: `BodyState::resize_output`.
- `mirada-compositor`: `ManagedWindow.is_shell`, `App.output_size`,
  `dock_shell`/`output_changed`; `register_toplevel` no registra el
  shell en el Cerebro; al cerrarse libera la franja. El shell se compone
  y se enfoca con el ratón aunque no viva en el Cerebro; no lleva marco.
  El backend winit usa ahora `resize_output` al redimensionar.

GPUI no habla `wlr-layer-shell`, así que el acople es por `app_id`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 05:38:12 +00:00
sergio 7b5c583a98 feat(mirada): zwp_linux_dmabuf — clientes que pintan por GPU
Fase 1 del plan «shell»: para que carmen pueda hospedar a `shuma-shell`
(y a cualquier app GPUI o navegador acelerado) hace falta que los
clientes con GPU puedan compartir su búfer de vídeo. carmen sólo hablaba
`wl_shm` (búferes de software) — por eso `foot` corría pero las apps
GPUI salían en negro.

- `App` lleva un `DmabufState`; `impl DmabufHandler` con `dmabuf_imported`
  que acepta el búfer (el `GlesRenderer` ya importa DMA-BUF al componer,
  vía `ImportAll`, así que la validación real ocurre al pintar).
- `delegate_dmabuf!(App)`.
- `announce_dmabuf` crea el global con los formatos de `dmabuf_formats()`
  del renderer — se llama en ambos backends una vez creado el renderer.

Pendiente del plan: Fase 2 (`wlr-layer-shell`) y Fase 3 (modo launcher
de `shuma-shell` — barra + input + cajón de resultados).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 05:00:36 +00:00
sergio b3b44e2c72 feat(mirada): mirada-launcher — lanzador de aplicaciones
Un escritorio en «modo launcher» necesita un lanzador. `mirada-launcher`
es una app nueva, sin dependencias: escanea los `.desktop` del estándar
XDG y lanza el que elijas desde una lista de terminal que se filtra
escribiendo.

- Recorre los directorios `applications/` de XDG en orden de prioridad
  (el del usuario tapa a los del sistema, dedup por id de archivo),
  parsea el grupo `[Desktop Entry]` (salta `NoDisplay`/`Hidden`, exige
  `Type=Application`), y limpia los códigos de campo del `Exec`.
- Interfaz de terminal sin raer modo: número = lanzar, texto = filtrar
  (si deja una sola, la lanza), Enter vacío = salir. Las apps con
  `Terminal=true` se envuelven en `foot -e`.
- Pensado para abrirse en una terminal pequeña; al lanzar termina y el
  programa queda corriendo, reparentado a init.

El keymap por defecto ata `Super+p` a `spawn:foot -e mirada-launcher`
(`Super+d` ya era el layout CenteredMaster).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:35:23 +00:00
sergio 5ede927d34 feat(mirada): sesión de escritorio — autostart y conmutación de VT
Dos piezas para usar carmen como tu escritorio de verdad.

Conmutación de VT — `Ctrl+Alt+Fn` salta a otra TTY y vuelve sin romper
la sesión. El `SessionEvent` de `libseat` ahora hace trabajo de verdad:
- al ceder la VT, pausa el `DrmDevice` y suspende `libinput`; `render()`
  no vuelve a tocar la GPU mientras la sesión esté cedida (`active`).
- al recuperarla, reanuda `libinput`, reactiva el `DrmDevice`, llama a
  `DrmCompositor::reset_state` y repinta.
`DrmState` conserva ahora `drm` y un clon del contexto `libinput`.

Sesión — `~/.config/mirada/autostart` (un comando por línea, `#`
comenta) se lanza al arrancar el backend DRM, vía un `spawn_autostart`
que reusa `spawn_command`. Y `session/`: el script `mirada-session`
(fija el entorno XDG y exec del compositor) y `carmen.desktop` para
registrarlo en un gestor de login, más un `autostart.example`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:31:55 +00:00
sergio 58e72c3d08 feat(mirada): el cursor toma la forma que pide el cliente
El cursor dejaba de ser un cuadrado fijo. Ahora honra
`wl_pointer.set_cursor`: sobre el texto de una terminal sale la «I»,
sobre un enlace la mano, etc. — la forma la dibuja el cliente en una
superficie y el compositor la compone.

- `App` guarda un `cursor_status: CursorImageStatus`; el handler
  `SeatHandler::cursor_image` lo actualiza.
- `render()` lo interpreta: `Surface` → compone el árbol de la
  superficie del cursor en `pointer_loc - hotspot` (helper
  `cursor_hotspot`, vía `CursorImageSurfaceData`); `Named` o sin tema →
  el cuadrado de siempre; `Hidden` → nada.
- Sobre el escritorio pelado (sin cliente debajo) el cursor vuelve al
  de por defecto, para que no se quede con la «I» de la última ventana.
- La superficie del cursor también recibe frame-callbacks (cursores
  animados).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:16:44 +00:00
sergio 751416252f feat(mirada): marco de ventana — distinguir y resaltar el foco
Sin decoración, las ventanas se confundían entre sí. Ahora el backend
DRM dibuja un marco fino alrededor de cada ventana: azul la que tiene
el foco del teclado, gris las demás.

- `ManagedWindow` gana `focused: bool` (lo fija `exec_op` al atender
  `BodyOp::Focus`/`Unfocus`) y `borders: [SolidColorBuffer; 4]` — un
  búfer por lado, cada uno con su `Id` estable para el seguimiento de
  daño; `SolidColorBuffer` sube su contador sólo si tamaño o color
  cambian, así un marco quieto no fuerza recomposición.
- El enum `Frame` pasa de `Cursor` a `Solid`: una variante de color
  sólido que sirve para el cursor y para los marcos (dos variantes con
  el mismo tipo chocarían en el `From` que genera `render_elements!`).
- `render()` en dos pasos: refresca los búferes (tamaño = contenido,
  color = foco) y luego arma los elementos. El marco va metido hacia
  adentro, sobre el borde de la superficie, así no pisa al vecino.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:10:32 +00:00
sergio fb3091d995 feat(mirada): acción spawn — lanzar programas desde el compositor
Un escritorio sin forma de abrir una terminal no es usable. Ahora el
keymap puede lanzar programas:

- `mirada-protocol`: nuevo `BrainCommand::Spawn(String)`.
- `mirada-brain`: `DesktopAction::Spawn(String)` con forma textual
  `spawn:<comando>` (`Display`/`FromStr`); `Desktop::apply` la traduce
  a `BrainCommand::Spawn`. El keymap por defecto trae
  `Super+Shift+Return` → `spawn:foot`. `DesktopAction` deja de ser
  `Copy` (lleva el comando) — `Keymap::lookup` clona en vez de copiar.
- `mirada-body`: `BodyOp::Spawn(String)`.
- `mirada-compositor`: `exec_op` ejecuta el spawn con un helper
  `spawn_command` (`sh -c`, hereda `WAYLAND_DISPLAY`), que también
  recoge el lanzamiento de `MIRADA_STARTUP` — antes duplicado.

`spawn:foot --title x` también funciona desde `mirada-ctl`. Tests
nuevos del round-trip textual y del flujo atajo→comando.

Nota: un keymap.ron ya existente no recibe el atajo nuevo; hay que
añadir la línea a mano o borrar el archivo para regenerarlo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 03:59:37 +00:00
sergio 90bffec3f1 feat(mirada): mover/redimensionar ventanas con el ratón
`Super`+arrastre interactivo en el backend DRM: botón izquierdo mueve
la ventana, botón derecho la redimensiona. Al arrastrarla, la ventana
pasa a flotar — comportamiento estilo dwm.

La verdad geométrica vive en el Cerebro, así que el arrastre viaja
hasta él:

- `mirada-protocol`: nuevo `BodyEvent::WindowFloatTo { id, rect }`.
- `mirada-brain`: `Desktop::on_event` lo atiende — busca el escritorio
  de la ventana y la hace flotar en ese rectángulo
  (`Workspace::set_floating`). Dos tests nuevos.
- `mirada-compositor`: `DragGrab`/`DragMode` en `App`; `handle_input`
  arranca el arrastre con `Super`+botón sobre una ventana
  (`keyboard.modifier_state().logo`), traga los botones mientras dura y
  lo cierra al soltar. `drag_update` recalcula el rectángulo (mover =
  esquina sigue al puntero; redimensionar = esquina inferior-derecha,
  con un mínimo de 120 px) y emite `WindowFloatTo`. Durante el arrastre
  el puntero no llega al cliente.

De paso, arregla un test de `mirada-link` que construía un
`WindowPlacement` sin los campos `floating`/`fullscreen`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 03:46:03 +00:00
sergio ae81399857 feat(mirada): xdg-decoration — ventanas sin marco en el compositor
Un escritorio teselante no quiere barras de título de cliente. El
compositor anuncia ahora `xdg-decoration` y a todo toplevel le impone
`Mode::ServerSide`; como el servidor no dibuja decoración alguna, las
ventanas quedan sin marco.

Sin esto, clientes como `foot` se dibujan su propia barra (CSD) con
botones de minimizar/maximizar/cerrar — ruido en un WM teselante.

- `XdgDecorationHandler` para `App`: `new_decoration`, `request_mode`
  y `unset_mode` fijan siempre `ServerSide` y reenvían el configure.
- `delegate_xdg_decoration!(App)`; el global se anuncia en `build_app`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 03:37:56 +00:00
sergio b4ddab9c06 feat(mirada): puntero/ratón en el backend DRM del compositor
El backend DRM del Cuerpo deja de ser sólo-teclado: `libinput` ahora
mueve un cursor de software y reenvía clics y rueda a los clientes.

- Enum `Frame` (vía `render_elements!`) que mezcla superficies de
  cliente y un `SolidColorRenderElement` para el cursor, marcado
  `Kind::Cursor` y compuesto encima de todo.
- `handle_input` atiende `PointerMotion`/`PointerMotionAbsolute`/
  `PointerButton`/`PointerAxis`; el puntero se acota a la salida.
- Foco-sigue-ratón: `window_at` hace el test de impacto (flotantes
  sobre teseladas, contra el rectángulo real de la superficie) y, al
  cambiar de ventana, emite `BodyEvent::PointerEntered`.
- `surface_px_size` en main.rs — tamaño presentado de una superficie,
  reusado por el test de impacto.

Compila + clippy limpio; pendiente de verificar en hardware.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 03:24:14 +00:00
sergio 5230d42b11 feat(mirada-compositor): centrar la ventana en su celda si el cliente no la llena
Un cliente que presenta una superficie más pequeña que su celda (p. ej.
un terminal que redondea a celdas de texto enteras) dejaba el hueco todo
a un lado. Ahora ManagedWindow recuerda el tamaño de la celda y
render_loc() centra la superficie en el sobrante. Lo usan los dos
backends (winit y DRM).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 03:12:44 +00:00
sergio c07356d8bc docs(mirada): el backend DRM funciona — README y SDD al día
mirada-compositor tiene dos backends: winit (anidado) y drm (nativo
sobre TTY, verificado en hardware). README con la selección de backend,
los requisitos de cada uno y MIRADA_STARTUP/MIRADA_DRM_TIMEOUT; SDD con
la estructura del backend DRM. Pendiente: puntero en DRM, VT switch,
hotplug.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 03:08:46 +00:00
sergio 84f94574f1 chore(mirada-compositor): backend DRM — quitar la instrumentación de depuración
El backend DRM funciona en hardware (sesión, render, teclado, atajos,
clientes, salida limpia). Diagnosticado: la franja al pie con `foot` es
que `foot` redondea su superficie a celdas de texto enteras (1920×1040
de 1080) — comportamiento del cliente, no del compositor.

Se retira la instrumentación: log por tecla, censo de dispositivos y
volcado de tamaños de superficie cada 2 s. El tope de tiempo
(MIRADA_DRM_TIMEOUT) pasa a estar desactivado por defecto — Super+Shift+e
y Ctrl+C son la salida normal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 03:07:26 +00:00
sergio 97a10aa173 chore(mirada-compositor): backend DRM — log del tamaño de las superficies
El panel sólo ofrece modos 1920×1080, así que la franja negra no es del
modo. Para localizarla, el bucle DRM registra cada ~2 s la posición y el
tamaño real de cada superficie — así se ve si el cliente llena la
pantalla o se queda corto.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 02:58:52 +00:00
sergio 30669f970b fix(mirada-compositor): backend DRM — modo de mayor área + log de modos
El borde negro seguía: la marca PREFERRED del panel no es fiable (a
veces apunta a un modo menor). Ahora se elige el modo de mayor área
(a igualdad, mayor refresco), y se registran todos los modos del
conector para diagnóstico.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 02:50:49 +00:00
sergio 97a725b870 fix(mirada-compositor): backend DRM — elegir el modo nativo del monitor
El backend DRM cogía conn.modes()[0], que no es el modo preferido —
en un panel 16:10 (1920×1200) suele ser un 1920×1080, dejando una
franja negra abajo.

Ahora elige el modo marcado PREFERRED (el nativo) y, si ninguno lo
está, el de mayor área.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 02:40:33 +00:00
sergio 8cd34bf173 fix(mirada-compositor): anunciar un wl_output — los clientes lo exigen
foot (y casi todo cliente Wayland) aborta con «no monitors available»
si el compositor no anuncia ningún wl_output. carmen no lo hacía.

- OutputHandler para App + delegate_output!.
- announce_output(): crea un Output, lo publica como global wl_output y
  le fija el modo. Helper compartido por los dos backends.
- winit y DRM lo llaman con su tamaño/modo real.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 02:35:06 +00:00
sergio fb2f3a2d01 feat(mirada-compositor): backend DRM — app de arranque + traza de ventanas
Primera prueba 2b en hardware: sesión, render, teclado y atajos
funcionan — Super+Shift+e cierra limpio. Faltaba ver una ventana de
cliente.

- MIRADA_STARTUP: si trae un comando, el backend DRM lo lanza como hijo
  al arrancar (hereda WAYLAND_DISPLAY) — así se prueba un cliente sin
  saltar de VT.
- Logs: cada cliente Wayland que se conecta, y el nº de ventanas en
  pantalla cuando cambia — para confirmar la cadena cliente→ventana.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 02:21:11 +00:00
sergio 782244b743 fix(mirada-compositor): backend DRM — salida garantizada + logs de teclado
Tras la primera prueba en hardware (el bucle arranca y compone el fondo,
pero el teclado no responde y no había forma de salir):

- Salida garantizada: el backend DRM se cierra solo a los 60 s (env
  MIRADA_DRM_TIMEOUT, 0 lo desactiva). Así una prueba nunca deja la
  pantalla atrapada aunque el teclado falle.
- handle_input instrumentado: registra cada dispositivo de entrada que
  libinput descubre y cada tecla con su combo y si es un atajo — para
  diagnosticar por qué no llega la entrada.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 02:14:50 +00:00
sergio fe221869d2 feat(mirada-compositor): backend DRM — fase 2b, bucle Wayland
Última fase del backend DRM: el bucle Wayland completo. Con esto
`mirada-compositor --drm` es un escritorio funcionando sobre una TTY,
sin sesión anfitriona.

main.rs: el armado del estado se extrae a build_app() -> Setup, que
comparten los dos backends (winit intacto).

drm_backend.rs — fase 2b sobre el pipeline de la 2a:
- DrmState: el estado que comparten los callbacks de calloop.
- bucle calloop con cinco fuentes: VBlank (DrmDeviceNotifier),
  teclado (libinput), clientes Wayland nuevos (ListeningSocket),
  peticiones de los clientes (poll fd del Display) y un timer ~60 Hz.
- render(): compone las ventanas de App en el DrmCompositor, encola el
  page-flip y reparte los frame-callbacks; el VBlank libera el flip.
- handle_input(): teclado libinput → interceptación de atajos (misma
  combo_string que winit) → keybind al Cerebro.
- tick(): Cerebro enlazado, recarga de keymap, mirada-ctl, composición.
- registra la salida con el modo del monitor; abre el socket Wayland.

Compila y pasa clippy aquí; se ejecuta y depura en hardware por logs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 02:01:29 +00:00
sergio f8cb80d867 feat(mirada-compositor): backend DRM — fase 2a, pipeline de render
Sobre el bring-up de la fase 1, drm_backend.rs monta ahora el pipeline
gráfico completo y lo prueba:

- elige la salida conectada (conector + CRTC + modo)
- GBM + EGL + GlesRenderer
- GbmAllocator + GbmFramebufferExporter + DrmCompositor para esa salida
- bucle calloop sincronizado al VBlank (DrmDeviceNotifier): pinta la
  pantalla de colores ~6 s y para (con tope de 10 s anti-cuelgue)

Es un test de hardware: si la pantalla cambia de color, EGL, GBM, el
modeset y el page-flip funcionan. Compila y pasa clippy aquí; se
ejecuta y depura en la máquina con GPU por logs. La fase 2b será el
bucle Wayland completo (clientes + libinput + composición de ventanas).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:53:04 +00:00
sergio 6c3a86fbec feat(mirada-compositor): backend DRM — fase 1, bring-up
mirada-compositor gana un segundo backend para correr sobre una TTY
pelada, sin sesión gráfica anfitriona. main() elige: --winit / --drm,
o automático (con DISPLAY/WAYLAND_DISPLAY → winit anidado; sin ellos →
DRM). run() pasa a llamarse run_winit().

drm_backend.rs — fase 1 (bring-up), construida para verificarse en
hardware real por etapas:
- abre la sesión con libseat (acceso a DRM/input sin root)
- localiza la GPU primaria (udev::primary_gpu)
- abre el dispositivo DRM por la sesión
- enumera los conectores y sus modos

Todo instrumentado con logs para diagnosticar sin el hardware delante.
La composición (GBM + EGL + GlesRenderer + DrmCompositor + libinput +
bucle calloop) es la fase 2. El backend winit queda intacto.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:44:40 +00:00
sergio f9c4bf594e feat(mirada): fullscreen iniciado por el cliente + HUD multi-salida
Dos remates de la tanda WM.

Fullscreen del cliente:
- BodyEvent::FullscreenRequest { id, fullscreen }. mirada-compositor
  implementa XdgShellHandler::fullscreen_request / unfullscreen_request
  y avisa al Cerebro; Desktop::on_event fija el fullscreen en el
  escritorio que tiene la ventana. Así un reproductor o un juego que
  llama a xdg set_fullscreen entra a pantalla completa solo.

HUD multi-salida (app mirada):
- El lienzo dibuja todas las salidas a escala (encaja su caja
  envolvente en el lienzo fijo; con una salida, 1:1), cada una con su
  marco y su número/escritorio. En simulación, Shift+n añade un monitor.

mirada-brain 63->65 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:32:08 +00:00
sergio 13d2ae71fb feat(mirada): scratchpad — ventana desplegable estilo terminal quake
Una ventana se puede guardar en el scratchpad (oculta, en ningún
escritorio) e invocar a voluntad como overlay flotante — el patrón de
la terminal desplegable.

- Desktop.scratchpad: Vec<WindowId>. SendToScratchpad saca la ventana
  enfocada del teselado y la guarda; ToggleScratchpad (Super+`) la
  invoca flotando y centrada en el escritorio activo, o la oculta.
- Invocarla desde otro escritorio la trae consigo (sale de donde
  estuviera). WindowClosed la quita del scratchpad.
- window_lines marca las guardadas como workspace 0; mirada-ctl windows
  las lista como «esc scratch».

Sin cambios de protocolo — una ventana del scratchpad invocada no es
más que una flotante. Verificado end-to-end con headless-ctl.
mirada-brain 58->63 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:23:17 +00:00
sergio 799dcef22e feat(mirada): multi-monitor real — cada salida tesela su escritorio
El Desktop deja de teselar sólo la salida primaria. Cada Output muestra
un escritorio virtual distinto y relayout() las tesela todas en un solo
Place que cubre todas las pantallas.

- Output { id, rect, workspace }; focused_output reemplaza al índice
  global active. active_index() = el escritorio de la salida enfocada.
- OutputAdded asigna el primer escritorio libre; OutputRemoved deja sus
  ventanas en su escritorio y reajusta el foco. reflow_outputs() las
  recoloca en fila.
- SwitchWorkspace actúa sobre la salida enfocada; si el escritorio
  pedido ya lo muestra otra salida, las intercambia (invariante: un
  escritorio se ve en una salida como mucho).
- DesktopAction::FocusOutputNext (Super+o) mueve el foco entre
  monitores. El foco del teclado es único — relayout() lo unifica a la
  ventana enfocada de la salida enfocada.

Verificado end-to-end con headless-ctl (ahora 2 salidas).
mirada-brain 52->58 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:18:12 +00:00
sergio be61ddb6eb feat(mirada): pantalla completa real — toggle-fullscreen
ToggleFullscreen (Super+Shift+f) lleva la ventana enfocada a pantalla
completa: cubre toda la salida sin gap, oculta al resto y se lleva el
foco. Distinto del modo Monocle (un modo de teselado): es un estado
por ventana que ignora el layout.

- Workspace.fullscreen: Option<WindowId>; set_fullscreen / fullscreen();
  remove() lo limpia si se cierra esa ventana.
- placements() da a la fullscreen el rect completo y marca al resto
  visible: false. WindowPlacement y BodyOp::Configure llevan
  fullscreen: bool.
- mirada-compositor fija el estado xdg_toplevel::Fullscreen en la
  superficie, para que el cliente lo sepa.
- Cableado en keymap, HUD de mirada y mirada-ctl.

Verificado end-to-end con headless-ctl. mirada-protocol 10->11,
mirada-brain 51->52.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:07:01 +00:00
sergio 6dfd9e62ac feat(mirada): reglas de ventana — escritorio y flotante por app_id
mirada-brain::rules — config declarativa que decide qué hacer con una
ventana al abrirse, mismo patrón que el keymap.

- Rule casa por subcadena de app_id y/o title (sin distinguir
  mayúsculas; vacío = cualquiera) y aplica un destino: workspace (1..9)
  y/o floating. Gana la primera regla que case.
- Rules en RON (~/.config/mirada/rules.ron); la primera vez se escribe
  una plantilla con ejemplos comentados, si está corrupta se ignora.
- Desktop consulta Rules::resolve en cada WindowOpened — el evento ya
  trae app_id/title — y abre la ventana en su escritorio, flotando si
  toca. set_rules en Desktop; las apps cargan rules.ron al arrancar.

mirada-brain 42->51 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:01:14 +00:00
sergio 4719f7c9f9 feat(mirada): ventanas flotantes — toggle-float
Una ventana puede salir del teselado y flotar: conserva su propio
rectángulo y se compone por encima de las teseladas.

- Workspace guarda las flotantes en un mapa aparte; layout() tesela
  sólo las no-flotantes y añade las flotantes al final (orden de
  pintado). set_floating / is_floating.
- WindowPlacement y BodyOp::Configure llevan floating: bool. BodyState
  detecta el cambio de floating como cualquier otro reconfigure.
- DesktopAction::ToggleFloat (Super+f): saca la enfocada a un
  rectángulo centrado al 60 % de la pantalla, o la devuelve al teselado.
  En Monocle, una flotante sigue visible.
- mirada-compositor ordena las flotantes al frente de la lista
  front-to-back de elementos → se pintan encima.
- HUD de mirada marca las flotantes; mirada-ctl toggle-float.

Verificado end-to-end con headless-ctl. mirada-layout 30->32,
mirada-protocol 9->10, mirada-body 13->14, mirada-brain 41->42.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:55:33 +00:00
sergio 2dd8ff139e feat(mirada): nmaster, promover a maestra y smart gaps (estilo dwm)
Tanda de funciones de tiling WM, toda pura (mirada-layout/brain), sin
tocar el protocolo:

- nmaster: LayoutParams.master_count — cuántas ventanas van en el área
  maestra. MasterStack y CenteredMaster apilan N maestras; sin pila, las
  maestras llenan la pantalla. Acciones inc-master/dec-master (Super+,
  Super+.), acotadas 1..9.
- Promover a maestra: Workspace::promote_focused lleva la ventana
  enfocada al puesto 0. Acción promote-to-master (Super+Return).
- Smart gaps: una sola ventana se tesela a sangre, sin margen.

combo_string del compositor canoniza ahora teclas con nombre (Return,
Tab, F5, flechas…) vía xkb::keysym_get_name, no sólo caracteres
imprimibles — sin eso Super+Return no sería un atajo expresable.

Cableado en keymap por defecto, HUD de mirada y mirada-ctl. Verificado
end-to-end con headless-ctl. mirada-layout 26->30, mirada-brain 39->41.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:45:47 +00:00
sergio 8821d34bd5 feat(mirada): 3 layouts nuevos + redimensionar el área maestra
mirada-layout pasa de 4 a 7 modos de teselado, todos intercambiables
por el API (SetLayout / CycleLayout / mirada-ctl layout <modo>):

- Rows: filas horizontales de igual alto (complemento de Columns).
- Spiral: espiral de Fibonacci — cada ventana parte por la mitad el
  espacio restante, alternando el sentido del corte.
- CenteredMaster: maestra centrada + pila a ambos lados (monitores
  anchos).

LayoutMode::ALL + next() definen el ciclo. Añade dos acciones,
GrowMaster/ShrinkMaster (Super+l / Super+h), que ajustan master_ratio
en caliente — ese parámetro existía pero no había forma de tocarlo.

Cableado completo: tile(), cycle, slugs Display/FromStr, keymap por
defecto (Super+r/d/s), HUD de mirada, mirada-ctl actions. El ejemplo
headless-ctl ahora imprime la geometría para verificar los layouts.

mirada-layout 22->26 tests, mirada-brain 37->39.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:37:16 +00:00
sergio b31f988833 feat(mirada): API de acciones — mirada-ctl + HUD interactivo
Toda acción de escritorio converge en Desktop::apply(DesktopAction); el
keymap era sólo un front-end. Esta tanda añade los otros tres.

- DesktopAction::FocusWindow(WindowId): direccionamiento directo de una
  ventana (no sólo ciclar); si está en otro escritorio, salta a él.
  DesktopAction pasa a ser Serialize/Deserialize (postcard) además de
  Display/FromStr.

- mirada-brain::ctl: el API de control externo. CtlRequest/CtlReply
  (marco postcard), CtlServer/CtlConn no bloqueantes y send_request.
  El Cerebro abre el socket y atiende en su bucle: la app mirada
  siempre, mirada-compositor sólo con el Cerebro embebido.

- mirada-ctl: CLI de control estilo swaymsg/hyprctl —
  `mirada-ctl focus-next | focus-window 5 | workspace 3 | windows`.
  Parsea la acción de los argumentos vía FromStr.

- HUD interactivo en la app mirada: pips de escritorio y ventanas del
  lienzo clicables (SwitchWorkspace / FocusWindow).

- Ejemplo headless-ctl: un Cerebro sin gráficos para probar mirada-ctl
  en modo desatendido. Verificado end-to-end.

mirada-brain: 29 -> 37 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:20:10 +00:00
sergio 8204852e3a feat(mirada): keymap configurable en RON, recargable en caliente
Los atajos de teclado dejan de estar cableados: ahora son un Keymap
configurable que vive sólo en el Cerebro. El Cuerpo nunca lo ve — sólo
recibe la lista de cadenas a interceptar (GrabKeys) y devuelve la
pulsada; es Desktop quien la traduce. Esa separación (qué interceptar
vs. qué significa) hace innecesario cualquier candado o Arc.

mirada-brain:
- keymap.rs — Keymap: from_ron/to_ron, load/save, load_or_init (escribe
  un archivo por defecto documentado si falta; default sin pisar si está
  corrupto), default_path (~/.config/mirada/keymap.ron), y watch sobre
  notify para la recarga en caliente (KeymapWatch).
- DesktopAction: Display + FromStr — vocabulario textual estable
  ("focus-next", "layout:grid", "workspace:3"); evita los guiones que
  romperían el RON de un enum.
- Desktop: with_keymap, set_keymap (cambio en caliente -> nuevo GrabKeys).
- Ejemplo keymap-default: imprime el archivo por defecto en RON.

Apps: mirada y mirada-compositor (modo embebido) cargan el keymap del
usuario al arrancar y lo recargan en caliente cuando el archivo cambia.

Disco RON, cable postcard (sólo la lista de cadenas), sin ejecutable
configurador. mirada-brain: 17 -> 29 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:04:11 +00:00
sergio 51398f89cf fix(mirada-compositor): no pisar WAYLAND_DISPLAY antes de winit::init
winit lee WAYLAND_DISPLAY/DISPLAY para encontrar la sesión gráfica
anfitriona donde anidarse. El código publicaba antes su propio socket
en WAYLAND_DISPLAY, así que winit intentaba anidarse en el propio
compositor —un socket que aún no atiende a nadie— y se colgaba.

Ahora winit::init() va primero (conecta a la sesión real) y el socket
propio + set_var se publican después. Si no hay sesión gráfica, aborta
con un mensaje claro en vez de colgarse o fallar en seco.

README: sección Requisitos — hace falta sesión X11/Wayland anfitriona;
receta Xvfb + VNC para cajas headless.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 23:25:44 +00:00
sergio c69eec794f chore: Cargo.lock — registra mirada-compositor en el workspace
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 22:44:46 +00:00
sergio d2e0cf4830 feat(mirada): mirada-compositor — el Cuerpo, compositor Wayland sobre smithay
Compositor Wayland teselante real sobre smithay, backend winit (corre
anidado como ventana dentro de la sesión X11/Wayland actual). Habla
wl_compositor/xdg_shell/wl_shm/wl_seat/wl_data_device y compone las
superficies de los clientes con GlesRenderer.

Dos modos: autónomo (Cerebro Desktop embebido, un solo proceso) o
enlazado (MIRADA_SOCKET → la app mirada decide la geometría). Reusa
mirada-body para la contabilidad y mirada-link para el cable.

Actualiza el SDD: el Cuerpo deja de ser pendiente. Añade README.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 22:44:39 +00:00
sergio f2455b0eca docs(mirada): SDD — arquitectura Cerebro↔Cuerpo y los 6 crates
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:10:10 +00:00
sergio 4caf92482f feat(mirada): mirada-body — contabilidad del Cuerpo del compositor
BodyState agnóstico de smithay: lleva salidas + superficies, traduce
BrainCommand a BodyOp (sólo lo que cambia) y emite BodyEvent desde los
mutadores del backend. Ejemplo headless: Cuerpo sin gráficos guiado por
stdin para ejercitar el bucle Cerebro↔Cuerpo. 13 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:09:09 +00:00
sergio 3e335df298 feat(mirada): app del Cerebro — ventana GPUI del compositor
Envuelve mirada-brain::Desktop y lo pinta: barra con escritorios + modo
+ foco, lienzo teselado con marco por ventana. Con MIRADA_SOCKET sondea
un Cuerpo por mirada-link; sin él, simulación con ventanas sintéticas y
teclado (n/w/j/k/tab/1-9). cargo build -p mirada limpio.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:07:11 +00:00
sergio f57c61fe3e feat(mirada): mirada-link — transporte Cerebro↔Cuerpo del compositor
Link<Out,In> sobre socket Unix: hilo lector de fondo + canal mpsc para
sondeo no bloqueante. BrainLink/BodyLink, connected_pair (socketpair),
connect/listen por ruta; Drop cierra el socket y propaga EOF. 7 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:01:56 +00:00
sergio 3e2777540f feat(mirada): mirada-brain — orquestador de escritorio del compositor
Desktop agnóstico de GPUI/smithay: salidas, 9 escritorios virtuales,
registro de ventanas y foco. on_event(BodyEvent) -> Vec<BrainCommand>;
DesktopAction + mapa de teclas estilo tiling WM (Super). 17 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:00:24 +00:00
sergio e736587857 feat(mirada): mirada-protocol — contrato Cerebro↔Cuerpo del compositor
BrainCommand/BodyEvent + WindowPlacement, marco postcard con prefijo u32
LE (write_frame/read_frame, guard MAX_FRAME) y el puente placements()
desde un Workspace de mirada-layout. 9 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:58:38 +00:00
sergio b7181da7d9 feat(matilda): detección de drift con docker inspect
matilda-discover gana discover_inventory(): corre docker inspect en
cada contenedor y compara contra el spec deseado — imagen, puertos,
env y volúmenes declarados. Si el contenedor que corre se desvió, el
plan emite un Update; si está al día, no hay acción. La comparación es
por satisfacción (lo extra que trae la imagen se ignora).

El CLI (--discover) y el shell (:matilda) ahora usan discover_inventory
en vez del descubrimiento por nombre: detectan no sólo qué crear y
eliminar, sino la deriva de configuración de lo que ya existe.

container_drift es puro — 6 tests nuevos con JSON de docker inspect.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:44:50 +00:00
sergio fdf820edbb feat(shuma-shell): matilda embebido — server admin desde la ventana
matilda deja de ser sólo un ejecutable aparte: el shell lo incorpora
como herramienta. Meta-comando `:matilda plan|script|apply
<inventario.json>` — reconcilia contra el estado real de la máquina
(matilda-discover) y vuelca el resultado al feed del shell:

- `plan`/`script` → una tarjeta sintética con el plan o el script.
- `apply` → ejecuta el script de verdad; su salida fluye en una
  tarjeta como cualquier comando (streaming, captura acotada, kill).

El panel [RUN] gana una sección [tools] con «⚙ matilda» que precarga
el comando. Reusa todo lo del shell —feed, ejecución, sesión— sin
panel nuevo ni peso extra: la herramienta es no invasiva.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:36:55 +00:00
sergio 7dc533dbbf docs(status): matilda — 7 crates + CLI, módulo funcional
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:32:01 +00:00
sergio 93e003be0d feat(matilda): matilda-discover — estado actual del servidor
Observa qué contenedores y vhosts existen (docker ps + sitios de
nginx) y reconstruye un Inventory "actual" que matilda-plan diferencia
contra el deseado: detecta correctamente qué crear y qué eliminar
(huérfanos). Parseo puro y testeable; sólo discover_local toca el
sistema. 6 tests.

La CLI gana el flag --discover en plan/script/apply: reconcilia
contra el estado real de la máquina en vez de partir de vacío.

matilda: 7 crates + CLI, ~48 tests. Pendiente: matilda-app (GPUI) y
la inspección detallada (docker inspect) para detectar drift de
configuración, no sólo presencia.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:31:26 +00:00
sergio df8f92fbb0 feat(matilda): ghost + linker + CLI — el ciclo completo de aplicación
matilda-ghost: el agente que ejecuta los ApplySteps en la máquina
destino — escribe archivos, corre comandos, reporta paso a paso;
semántica set -e (se detiene en el primer error). dry_run previsualiza
sin tocar nada. 5 tests.

matilda-linker: aplica los pasos en un host remoto por SSH sobre
brahman-ssh-multiplex; produce el mismo ApplyReport que el ghost local.

apps/matilda: deja de ser una demo hardcoded — ahora es una CLI real:
  matilda example | plan | script | apply  (local · --dry-run · --host)
Carga el inventario de un JSON, reconcilia y aplica.

matilda: 6 crates + CLI, ~42 tests. La cadena va de la declaración
a la aplicación local/remota.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:27:28 +00:00
sergio 5b9d8107fc feat(matilda): matilda-apply — puente del plan a la ejecución
Traduce un Plan de reconciliación a ApplySteps concretos: por cada
acción, los archivos a escribir en el servidor y los comandos a
correr. Contenedores → docker rm/run; vhosts → archivo nginx +
reload; hosts → sin pasos (son destino de conexión, no algo a
aplicar). steps_to_script() emite un script bash único con heredocs.

Sigue agnóstico de transporte — ejecutar los pasos (local, SSH o vía
matilda-ghost) es la capa de I/O. La demo CLI ahora imprime el script.

6 tests; matilda llega de la declaración al script ejecutable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:18:20 +00:00
sergio 22a0ae8c58 feat(shuma-exec): spill con splice (copia cero) + limpieza de temporales
El volcado de la salida excedente ya no copia por espacio de usuario:
pasado el tope, el lector escribe la línea que cruzó + lo bufereado y
luego mueve el resto del pipe al archivo con splice(2) —kernel a
kernel, sin copia—. Se aplica a stdout (el contenido principal).

shuma-shell limpia sus archivos de volcado al cerrar la sesión
(Drop). Los spills llevan el pid en el nombre para no chocar entre
instancias.

shuma-exec: 11 tests verdes (el de spill ahora verifica el camino
splice).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:15:30 +00:00
sergio b08d5cbe0a feat(shuma): ejecución directa + captura configurable por sesión
bash deja de ser el ejecutor. shuma-exec ahora tiene dos modos:
- Exec::Direct — brahman lanza y conecta cada etapa del pipe con
  descriptores reales; control total del árbol de procesos.
- Exec::Shell — fallback a `bash -c` para sintaxis que el modo directo
  aún no absorbe (globs, $VAR, redirecciones, &&). bash = un parser
  de sintaxis, no el ejecutor por defecto.

El shell elige: pipe simple (sólo comandos/args/`|`) → directo; algo
más → shell. La WorkSession lleva su CapturePolicy (límite + spill),
configurable con `:limit <MB>` y `:spill on|off`; la barra de estado
la muestra. Si spill está activo, la salida excedente se vuelca a un
archivo en vez de descartarse (RunEvent::Spilled).

shuma-exec: 11 tests (directo, pipes, spill, kill de pipeline).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:06:54 +00:00
sergio b4be5e1c72 feat(shuma): captura acotada + reproceso de salidas vía stdin
shuma-exec: cota dura de memoria. CommandSpec.capture_limit (bytes):
pasado el tope se emite RunEvent::Truncated una vez y el resto se
descarta —pero el pipe se sigue drenando, así el proceso no se
bloquea y termina normal. CommandSpec.stdin_data alimenta un texto
por la entrada estándar (escrito en su propio hilo).

shuma-session: CommandRun.truncated.

shuma-shell: tope de captura de 8 MiB por comando. Cada card con
salida muestra «⤳ reprocesar» — al pulsarlo, el próximo comando
filtra esa salida capturada (vía stdin) sin re-ejecutar el original;
un banner marca el modo. Las cards truncadas avisan «⚠ truncado».

shuma-exec: 12 tests (incluye truncado y reproceso por stdin).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:53:42 +00:00
sergio 0740d2e2af feat(shuma-shell): etapas de pipe en la card + sin truncar la salida
- La salida de un comando ya no se trunca: si hay contenido, se
  muestra entero (fuera el «N líneas antes»).
- Las cards de un pipe muestran una fila de etapas: un clic re-ejecuta
  la línea hasta esa etapa como un comando nuevo, así se inspeccionan
  los resultados intermedios. Eficiente — sólo corre lo que pedís, sin
  bufferizar intermedios ni cambiar el modelo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:41:27 +00:00
sergio 9d8f45a9f8 feat(shuma): disparo de patrones por estructura del directorio
shuma-infer: el directorio de una ocurrencia es ahora el cwd de su
último comando (el dir donde realmente operó el patrón, ya hechos los
cd). predict_next devuelve también el índice del patrón.

shuma-shell: la predicción se filtra por marcadores de proyecto
(.git, Cargo.toml, package.json, go.mod…). El shell deriva la
condición de disparo de un patrón —los marcadores comunes a sus
directorios— y sólo lo anticipa en un cwd que comparta esa estructura.
Así el ghost no sugiere `cargo build` en un directorio sin Cargo.toml.

Cierra la visión: del scripting a la intención, con precisión.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:34:39 +00:00
sergio 05ccab64f2 feat(shuma-shell): atajos dinámicos F1–F8 para los grupos
Cada grupo del panel [RUN] —incluidos los que el motor de inferencia
promueve ()— recibe un atajo de teclado según su posición. Pulsar
F1..F8 ejecuta el grupo sin tocar el mouse: la macro se vuelve ubicua,
parado donde estés. La etiqueta del grupo muestra su tecla.

Cierra el lazo del «scripting a la intención»: repetir un flujo →
patrón detectado → grupo promovido → atajo de un toque.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:32:46 +00:00
sergio be99ac3bbb feat(shuma): ghosting predictivo en el prompt
shuma-line: ghost_suggestion(line, corpus) — el resto de la línea que
el shell predice, a partir de un corpus priorizado.
shuma-infer: predict_next(recent, patterns) — si los últimos comandos
coinciden con el prefijo de un patrón, devuelve los pasos que faltan.

shuma-shell: mientras se escribe, el prompt pinta en gris tenue la
continuación predicha — historial reciente o, con prioridad, la
secuencia que el motor de inferencia anticipa (cd a un proyecto →
fantasma «git pull && cargo build»). La flecha → al final de la
línea, o Ctrl+Space, aceptan el fantasma.

13 tests shuma-infer, 37 shuma-line.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:30:57 +00:00
sergio 37ea535cb7 feat(shuma): shuma-infer — motor de inferencia de intenciones
Detecta patrones de comandos repetidos en el historial: ventana
deslizante sobre las firmas de binarios (sólo ventanas 100%
exitosas), abstracción de argumentos variables (cd /a vs cd /b →
cd <…>), patrones maximales, puntaje por largo × frecuencia.
10 tests, agnóstico y determinista.

El shell lo corre tras cada comando terminado y promueve el patrón
más fuerte a un grupo « ...» en el panel [RUN] — la rehidratación
que convierte la repetición orgánica en una receta de un clic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:12:47 +00:00
sergio 3bfb42f1cc feat(shuma): matar procesos + guardar grupos desde la UI
shuma-exec: RunHandle::kill() — el proceso se comparte con su hilo
coordinador (Arc<Mutex<Child>>) para poder terminarlo; los lectores
cierran al cerrarse los pipes. 8 tests (incluye kill de un sleep).

shuma-shell:
- Cada tarjeta de comando en curso (▷) muestra un botón «✕ matar».
- Meta-comando `:save <nombre>` guarda como grupo los comandos
  ejecutados desde el último guardado. El botón «+» del panel [RUN]
  precarga «:save » en el input para nombrarlo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:09:09 +00:00
sergio 0fde2aa273 feat(shuma-shell): tarjetas acordeón con filtro stdout/stderr
- shuma-session: la salida de un comando ahora distingue el flujo
  (OutputLine { stream, text }); CommandRun expone lines_of/count_of/
  has_stderr.
- Las tarjetas del feed se acordeonan (clic en la cabecera). El filtro
  de la cabecera muestra stdout por defecto; si hubo stderr aparece el
  switch «⚠ N» para verlo.
- Orden de terminal: los comandos nuevos se acolan abajo, los viejos
  suben y se autocolapsan — salvo que el usuario haya tocado el
  acordeón a mano (user_touched).
- El feed sigue al comando más reciente (ScrollHandle::scroll_to_bottom).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:04:12 +00:00
sergio 2540e74046 docs(shuma): SDD — shuma-session, shuma-exec y la distinción con sandokan
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:40:31 +00:00
sergio 90607bd7c0 feat(shuma-shell): divisores arrastrables entre los paneles
Cada panel lateral tiene ahora un divisor de 5px que se arrastra para
redimensionarlo (cursor resize, resaltado al hover). El arrastre se
sigue a nivel de ventana —on_mouse_move/up en la raíz— así que no se
pierde aunque el cursor salga del divisor. Ancho acotado 130–420px;
los divisores desaparecen con el panel colapsado.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:39:53 +00:00
sergio 2b2a92a72b feat(shuma-shell): ejecución real con streams + sesión de trabajo
El shell ahora ejecuta de verdad. Adiós a los cuadros verdes de
ejemplo: el panel central muestra los comandos ejecutados, cada uno
con su salida llegando en streaming (shuma-exec lanza el proceso,
un bucle de fondo drena stdout/stderr ~9 veces/s).

El shell vive dentro de una WorkSession (shuma-session): la barra de
estado muestra el directorio actual y su identificador de aislamiento
(hash estable del cwd — cd lo cambia). `cd` se maneja internamente.
El panel [RUN] lista los grupos de comandos reutilizables; un clic
ejecuta el grupo entero (lines unidas por &&).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:38:33 +00:00
sergio 0f2f1033eb feat(shuma): shuma-session + shuma-exec — sesiones de trabajo y ejecución
shuma-session: el shell trabaja dentro de una WorkSession — directorio
actual (que es el identificador de aislamiento, hash estable del cwd),
historial de comandos ejecutados (CommandRun con salida y estado) y
grupos de comandos guardados y reutilizables (CommandGroup).

shuma-exec: ejecutor con salida en streaming — lanza bash -c en un
directorio y entrega stdout/stderr línea a línea por un canal, sin
esperar al final. Es la capa que sandokan (poll-based, orquesta Cards)
deliberadamente no cubre.

15 tests. Agnósticos de UI, #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:32:59 +00:00
sergio 8250ecbc0f docs(shuma): SDD — registra shuma-line, shuma-sysmon y el shell vivo
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:15:51 +00:00
sergio 69cee95481 feat(shuma-shell): el shell, vivo — input inteligente + monitores
El input de abajo ahora está vivo sobre shuma-line: se escribe de
verdad (teclado completo, motions, Ctrl+a/e/u, UTF-8), con resaltado
por token en tiempo real (comando, flag, string, variable, pipe,
redirección…) y autocompletado posicional con popup navegable
(↑↓ Tab) — comandos del PATH, flags por comando, rutas del disco.
Enter registra la línea en el lienzo de intenciones; las etapas de
pipe se cuentan en la barra de estado.

Panel derecho [SENS]: monitores de CPU y memoria con curva en vivo
(shuma-sysmon, refresco ~1s). Paneles laterales colapsables.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:15:17 +00:00
sergio 7c38a8af4e feat(shuma): shuma-sysmon — muestreo de CPU/memoria con historial
Lee /proc/stat y /proc/meminfo; calcula uso de CPU (delta entre
muestras) y de memoria; mantiene un History circular para la curva
del monitor. Parseo puro (parse_cpu_stat/parse_meminfo) separado de
la lectura de archivos → testeable sin tocar el sistema.

8 tests. #![forbid(unsafe_code)], cero deps de UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:09:46 +00:00
sergio cf337c88d7 feat(shuma): shuma-line — el cerebro agnóstico del input del shell
Análisis de la línea de comandos bash, listo para GUI o TUI:
- lexer: tokeniza + clasifica (comando vs argumento por etapa),
  reconoce comillas, variables, tuberías, redirecciones, operadores.
- pipeline: descompone la línea en etapas separadas por |.
- complete: autocompletado posicional (comando / flag / ruta) con
  CompletionSource inyectable; diccionario de flags por comando.
- LineState: input editable UTF-8-safe (cursor, motions, completado).
- Dialect conmutable (bash hoy; zsh/fish/python a futuro).

32 tests. #![forbid(unsafe_code)], cero deps de UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:08:26 +00:00
sergio 2b340fdf40 docs(status): registra charka y mirada
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:25:02 +00:00
sergio b975dc7919 feat(mirada): mirada-layout — motor de teselado del compositor Wayland
Rect + split (reparto exacto de píxeles), 4 modos de layout
(MasterStack, Monocle, Grid, Columns) con tile(), y Workspace:
ventanas en orden de teselado, foco cíclico, reordenado y
resolución de geometría. Determinista, agnóstico de Wayland/smithay.

22 tests. #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:24:48 +00:00
sergio 737ae5a696 feat(charka): charka-bcd — aritmética decimal con semántica COBOL
Cimiento numérico del transpilador. Picture parsea la cláusula
PICTURE (9, V, S, 9(n)); Decimal es punto fijo exacto (mantissa i128
+ scale) con suma/resta/producto exactos, división con escala de
resultado fija, redondeo Truncate/HalfUp y coerce a un Picture con
detección de desbordamiento (ON SIZE ERROR).

22 tests. Determinista, sin deps de plataforma — base de Fase D.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:22:40 +00:00
sergio 9e7fa17411 docs(status): registra matilda y yachay
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:09:31 +00:00
sergio e3980d005f feat(yachay): notebooks reproducibles — yachay-core + demo
yachay-core: notebook como secuencia de celdas (orden de lectura) +
DAG de dependencias (orden de ejecución). Celdas markdown/código/embed
con content_hash BLAKE3; editar una propaga staleness a descendientes;
digest Merkle por celda (content_hash ‖ digests upstream) y
notebook_digest que certifica reproducibilidad. Demo CLI en apps/yachay.

14 tests. Sin kernel ni UI, #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:09:18 +00:00
sergio 3f8a3ea4b6 feat(matilda): administración de servidores — core + config + plan
matilda-core: modelo declarativo (Host, Container, VHost, Inventory).
matilda-config: renderiza Container→docker-compose/docker run y
VHost→bloque server nginx (con TLS + redirección :80→:443).
matilda-plan: reconciliación pura actual→deseado con acciones
ordenadas por dependencia (contenedores antes que vhosts, removes
en orden inverso). Demo CLI en apps/matilda.

29 tests. Funciones puras, cero Docker/SSH/disco.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:06:36 +00:00
sergio 71f6cf1306 docs(status): registra dominium, verbo, agorapura, badu, takiy; fana al día
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:46:56 +00:00
sergio 639381fd94 feat(takiy): takiy-core — teoría musical + modelo de partitura
Pitch MIDI (clase/octava/frecuencia ET A4=440), Scale (raíz + patrón
de semitonos: mayor, menor natural, pentatónica), Chord (7 cualidades,
voicing, nombres) y un Score multipista con tempo: ScoreNote en
pulsos, Track con inserción ordenada y transposición atómica.

24 tests. Agnóstico de síntesis y UI, #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:45:55 +00:00
sergio ea079a0b23 feat(badu): app demo — cuaderno con grafo de enlaces y gravedad
CLI que siembra un cuaderno (cocina/jardín/oficina), imprime el grafo
de wiki-links (forward/backlinks, huérfanas, colgantes) y los
clústeres por gravedad semántica + vecinos + layout 2D.
cargo run -p badu.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:43:42 +00:00
sergio d0a175a90a feat(badu): toma de notas — núcleo + gravedad semántica
badu-core: modelo Note + NoteStore (etiquetas, búsqueda) + grafo de
wiki-links [[...]] derivado del cuerpo (forward/backlinks, huérfanas,
enlaces colgantes; resolución case-insensitive).

badu-gravity: SemanticField sobre vectores semánticos — afinidad
coseno, vecinos más cercanos, clústeres por umbral (union-find) y
layout 2D dirigido por fuerzas (notas afines se atraen, todas se
repelen; determinista, sin RNG).

29 tests. Cero red, #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:42:28 +00:00
sergio 4e27065a15 feat(agorapura): app demo — escenario narrado del ágora
CLI que recorre el caso canónico de extremo a extremo: Venezuela
atestigua la nacionalidad de Yumaira, otras identidades corroboran,
una firma manipulada se rechaza, y tres políticas negociadas dan
veredictos distintos sobre la misma evidencia. cargo run -p agorapura.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:39:07 +00:00
sergio c1c136954e feat(agorapura): identidad humana federada — core + grafo de confianza
agorapura-core: identidades fractales (persona/comunidad/alianza/
institución) sobre claves ed25519, Claims sujeto-predicado-valor y
Attestations firmadas y autoverificables (la prueba viaja con el
dato). agorapura-graph: TrustGraph guarda sólo atestaciones con firma
válida; corroboration() devuelve evidencia cruda y TrustPolicy —un
umbral negociado, no una verdad del sistema— la traduce a sí/no.

22 tests. Cero red, cero estado global, #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:38:20 +00:00
sergio ad9781c2ee docs(fana): SDD — estado actualizado tras render-plan + editor-gpui + app
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:31:28 +00:00
sergio ced5853154 feat(fana): backend GPUI + app — editor de escritura DAG
fana-editor-gpui: EdgesElement pinta los conectores de dependencia
como paths; editor_view compone bloques de átomo (divs absolutos
coloreados por coherencia) + osciloscopio del sidepane. RenderPlan
ahora lleva su LayoutConfig para que el backend sea autosuficiente.

app fana: ventana con un relato de ejemplo (rama principal + alterna),
botón «Mutar raíz» que dispara la onda de choque lógica
(propagate_mutation), «Re-validar todo», leyenda y estadísticas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:31:12 +00:00
sergio 494fb7c0bc feat(fana): fana-render-plan — plan de dibujo agnóstico del editor DAG
build_plan(NarrativeGraph) → RenderPlan: AtomBlocks apilados por
profundidad topológica (una columna por rama), Edges de dependencia
(borde inferior → superior) y osciloscopio de coherencia en el
sidepane (tono + intensidad semántica normalizada). Determinista:
orden desempata por (profundidad, columna, id). 10 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:28:27 +00:00
sergio 781a310c8d docs(verbo): SDD del módulo — contrato + mock + daemon + backends pendientes
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:26:18 +00:00
sergio 649ca02d4d feat(verbo): verbo-daemon — embeddings compartidos entre procesos
Daemon que carga un Provider una vez y lo sirve sobre socket Unix;
DaemonClient lo consume desde otro proceso implementando el trait
Provider (indistinguible de un backend local). Multi-instancia: un
daemon por modelo, cada uno en su socket. Frames postcard con
prefijo de largo. 8 tests (wire + integración real sobre socket).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:25:56 +00:00
sergio cbca62f8f1 docs(dominium): SDD del módulo — cadena de 5 crates + app
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:23:00 +00:00
sergio f46c7b435f feat(dominium): backend GPUI + app — ventana viva del simulador
dominium-canvas-gpui: Element que pinta un RenderPlan como quads,
centrado en sus bounds (rgba→hsla, único crate que toca gpui).

app dominium: compone core→physics→iso→render-plan→canvas en una
ventana GPUI con bucle de simulación de fondo (~11 tps), panel de
estadísticas, controles play/pausa + re-sembrar, y re-siembra
automática al colapso poblacional.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:22:35 +00:00
sergio cba61e3549 feat(dominium): maqueta isométrica agnóstica — dominium-render-plan
build_plan(World, IsoProjector, ZWeights, PlanConfig) → RenderPlan:
un quad por celda (color = mezcla pesada de las 5 capas, relieve =
Z compuesto) + un quad-marca por Lemming posado sobre el terreno.
Quads ordenados por profundidad de pintor (depth = x+y) + caja
envolvente para centrado. Cero deps gráficas. 10 tests.

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:16:33 +00:00
sergio bbfa44ff35 feat(shuma): reescritura del shell — las 3 zonas
apps/shuma-shell deja de ser el dashboard legacy de la era shipote y
pasa a ser el shell de la spec: layout fijo de 3 zonas.

- status (arriba) — estado de sandokan + versión.
- [RUN] (izquierda) — barra de macros desde MacroBook (F1/F2/F3).
- Lienzo de Contexto (centro) — grafo de intenciones: cada %cN es una
  caja posicionada por shuma-shell-render::layout, borde coloreado por
  estado (ámbar Running / verde Ok / rojo Failed).
- [SENS] (derecha) — telemetría (CPU/MEM, placeholders).
- prompt fijo (abajo) — la línea de intención.

v1: renderiza la estructura con datos de ejemplo. Cableado interactivo
(typing, F-keys ejecutando vía sandokan, telemetría viva) es el paso
siguiente. cargo check --workspace verde.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:41:17 +00:00
sergio 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
939 changed files with 92050 additions and 11182 deletions
+3
View File
@@ -4,3 +4,6 @@ Cargo.lock.bak
.DS_Store
.claude/
/out/
# Artefacto de runtime del event-log de nakui-ui
nakui-ui-state.jsonl
+11 -5056
View File
File diff suppressed because it is too large Load Diff
Generated
+3159 -1248
View File
File diff suppressed because it is too large Load Diff
+311 -156
View File
@@ -2,44 +2,71 @@
resolver = "2"
members = [
# ============================================================
# core/ — Init y compat (arje absorbido)
# protocol/ — Contratos canónicos + routing entre módulos
# ============================================================
"crates/core/brahman-card",
"crates/core/brahman-card-wit",
"crates/core/brahman-cards",
"crates/core/brahman-handshake",
"crates/core/brahman-broker",
"crates/core/brahman-admin",
"crates/shared/brahman-sidecar",
"crates/shared/brahman-net",
"crates/shared/ente-incarnate",
"crates/core/ente-card",
"crates/core/ente-bus",
"crates/core/ente-cas",
"crates/core/ente-kernel",
"crates/core/ente-soma",
"crates/core/ente-wasm",
"crates/core/ente-snapshot",
"crates/core/ente-brain",
"crates/core/ente-zero",
"crates/core/ente-echo",
"crates/core/ente-policy-provider",
"crates/core/ente-logind-compat",
"crates/core/ente-hostnamed-compat",
"crates/core/ente-timedated-compat",
"crates/core/ente-localed-compat",
"crates/core/ente-journald-compat",
"crates/core/ente-resolved-compat",
"crates/core/ente-polkit-compat",
"crates/core/ente-machined-compat",
"crates/core/ente-tmpfiles-compat",
"crates/core/ente-systemd1-compat",
"crates/core/ente-notify-compat",
"crates/core/ente-binfmt-compat",
"crates/core/ente-timer-compat",
"crates/protocol/brahman-card",
"crates/protocol/brahman-card-wit",
"crates/protocol/brahman-cards",
"crates/protocol/brahman-handshake",
"crates/protocol/brahman-broker",
"crates/protocol/brahman-admin",
"crates/protocol/brahman-sidecar",
"crates/protocol/brahman-net",
"crates/protocol/brahman-dht",
"crates/protocol/brahman-card-discovery",
"crates/protocol/brahman-ssh-multiplex",
"crates/protocol/brahman-auth",
"crates/protocol/arje-card",
# ============================================================
# modules/semantic_dht/ — DHT semántico (minga absorbido)
# init/ — PID 1 + encarnación Linux (arje)
# ============================================================
"crates/init/arje-zero",
"crates/init/arje-kernel",
"crates/init/arje-soma",
"crates/init/arje-snapshot",
"crates/init/arje-incarnate",
"crates/init/arje-absorb",
# ============================================================
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
# ============================================================
"crates/runtime/arje-bus",
"crates/runtime/arje-cas",
"crates/runtime/arje-wasm",
"crates/runtime/arje-brain-rules",
"crates/runtime/arje-brain-cognitive",
"crates/runtime/arje-brain-audit",
"crates/runtime/arje-brain",
"crates/runtime/arje-echo",
"crates/runtime/sandokan-lifecycle",
"crates/runtime/sandokan-core",
"crates/runtime/sandokan-local",
"crates/runtime/sandokan-daemon",
"crates/runtime/sandokan-remote",
"crates/runtime/sandokan",
# ============================================================
# compat/ — Shims D-Bus para correr software systemd-aware
# ============================================================
"crates/compat/arje-compat-common",
"crates/compat/arje-policy-provider",
"crates/compat/arje-logind-compat",
"crates/compat/arje-hostnamed-compat",
"crates/compat/arje-timedated-compat",
"crates/compat/arje-localed-compat",
"crates/compat/arje-journald-compat",
"crates/compat/arje-resolved-compat",
"crates/compat/arje-polkit-compat",
"crates/compat/arje-machined-compat",
"crates/compat/arje-tmpfiles-compat",
"crates/compat/arje-systemd1-compat",
"crates/compat/arje-notify-compat",
"crates/compat/arje-binfmt-compat",
"crates/compat/arje-timer-compat",
# ============================================================
# modules/semantic_dht/ (minga) — DHT semántico de código
# ============================================================
"crates/modules/semantic_dht/minga-core",
"crates/modules/semantic_dht/minga-store",
@@ -48,132 +75,243 @@ members = [
"crates/modules/semantic_dht/minga-cli",
# ============================================================
# modules/ui_engine/ — Motor de widgets (yahweh libs+widgets)
# modules/nahual/ — Motor GPUI: libs + widgets (era yahweh)
# ============================================================
"crates/modules/ui_engine/libs/core",
"crates/modules/ui_engine/libs/theme",
"crates/modules/ui_engine/libs/launcher",
"crates/modules/ui_engine/libs/bus",
"crates/modules/ui_engine/libs/meta-schema",
"crates/modules/ui_engine/libs/meta-runtime",
"crates/modules/ui_engine/libs/providers/fs",
"crates/modules/ui_engine/libs/providers/sqlite",
"crates/modules/ui_engine/widgets/tree",
"crates/modules/ui_engine/widgets/container_core",
"crates/modules/ui_engine/widgets/splitter",
"crates/modules/ui_engine/widgets/tabs",
"crates/modules/ui_engine/widgets/tiled",
"crates/modules/ui_engine/widgets/text_input",
"crates/modules/ui_engine/widgets/meta-form",
"crates/modules/ui_engine/widgets/banner",
"crates/modules/ui_engine/widgets/card",
"crates/modules/ui_engine/widgets/stat-card",
"crates/modules/ui_engine/widgets/app-header",
"crates/modules/ui_engine/widgets/theme-switcher",
# --- lapaloma: módulo de gráficos data-viz (ver ARCHITECTURE.md fuente) ---
"crates/modules/ui_engine/libs/lapaloma-core",
"crates/modules/ui_engine/widgets/lapaloma-render",
"crates/modules/ui_engine/widgets/lapaloma-cartesian",
"crates/modules/ui_engine/widgets/lapaloma-stream",
"crates/modules/ui_engine/widgets/lapaloma-mesh",
"crates/modules/ui_engine/widgets/lapaloma-financial",
"crates/modules/ui_engine/widgets/lapaloma-polar",
"crates/modules/ui_engine/widgets/lapaloma-heatmap",
"crates/modules/ui_engine/widgets/lapaloma-treemap",
"crates/modules/ui_engine/widgets/lapaloma-flow",
"crates/modules/ui_engine/widgets/lapaloma-phosphor",
"crates/modules/ui_engine/widgets/lapaloma-export",
"crates/modules/ui_engine/widgets/lapaloma",
"crates/modules/nahual/libs/core",
"crates/modules/nahual/libs/theme",
"crates/modules/nahual/libs/launcher",
"crates/modules/nahual/libs/bus",
"crates/modules/nahual/libs/meta-schema",
"crates/modules/nahual/libs/meta-runtime",
"crates/modules/nahual/libs/providers/fs",
"crates/modules/nahual/libs/providers/sqlite",
"crates/modules/nahual/widgets/tree",
"crates/modules/nahual/widgets/container_core",
"crates/modules/nahual/widgets/splitter",
"crates/modules/nahual/widgets/tabs",
"crates/modules/nahual/widgets/tiled",
"crates/modules/nahual/widgets/text_input",
"crates/modules/nahual/widgets/meta-form",
"crates/modules/nahual/widgets/banner",
"crates/modules/nahual/widgets/card",
"crates/modules/nahual/widgets/stat-card",
"crates/modules/nahual/widgets/app-header",
"crates/modules/nahual/widgets/theme-switcher",
# ============================================================
# modules/nakui/ — ERP matemático (nakui absorbido)
# modules/pineal/ — Data-viz agnóstica con backends (era lapaloma)
# ============================================================
"crates/modules/pineal/core",
"crates/modules/pineal/render",
"crates/modules/pineal/cartesian",
"crates/modules/pineal/stream",
"crates/modules/pineal/mesh",
"crates/modules/pineal/financial",
"crates/modules/pineal/polar",
"crates/modules/pineal/heatmap",
"crates/modules/pineal/treemap",
"crates/modules/pineal/flow",
"crates/modules/pineal/phosphor",
"crates/modules/pineal/export",
"crates/modules/pineal/umbrella",
# ============================================================
# modules/verbo/ — Provider de embeddings model-agnostic
# ============================================================
"crates/modules/verbo/verbo-core",
"crates/modules/verbo/verbo-mock",
"crates/modules/verbo/verbo-daemon",
# ============================================================
# modules/agorapura/ — Identidad humana federada
# ============================================================
"crates/modules/agorapura/agorapura-core",
"crates/modules/agorapura/agorapura-graph",
# ============================================================
# modules/badu/ — Toma de notas con gravedad semántica
# ============================================================
"crates/modules/badu/badu-core",
"crates/modules/badu/badu-gravity",
# ============================================================
# modules/takiy/ — Composición musical asistida
# ============================================================
"crates/modules/takiy/takiy-core",
# ============================================================
# modules/matilda/ — Administración de servidores
# ============================================================
"crates/modules/matilda/matilda-core",
"crates/modules/matilda/matilda-config",
"crates/modules/matilda/matilda-plan",
"crates/modules/matilda/matilda-apply",
"crates/modules/matilda/matilda-ghost",
"crates/modules/matilda/matilda-linker",
"crates/modules/matilda/matilda-discover",
# ============================================================
# modules/yachay/ — Notebooks computacionales reproducibles
# ============================================================
"crates/modules/yachay/yachay-core",
# ============================================================
# modules/charka/ — Transpilador COBOL → Rust
# ============================================================
"crates/modules/charka/charka-bcd",
"crates/modules/charka/charka-lexer",
"crates/modules/charka/charka-parser",
"crates/modules/charka/charka-ir",
"crates/modules/charka/charka-runtime",
"crates/modules/charka/charka-codegen",
"crates/modules/charka/charka-shadow",
# ============================================================
# modules/mirada/ — Compositor Wayland
# ============================================================
"crates/modules/mirada/mirada-layout",
"crates/modules/mirada/mirada-protocol",
"crates/modules/mirada/mirada-brain",
"crates/modules/mirada/mirada-link",
"crates/modules/mirada/mirada-body",
# ============================================================
# modules/nakui/ — ERP matemático (categórico)
# ============================================================
"crates/modules/nakui/core",
# ============================================================
# modules/nouser/ — explorador de Mónadas (nuevo)
# modules/chasqui/ — Explorador semántico de nómadas (ex-nouser, ex-akasha)
# ============================================================
"crates/modules/nouser/card",
"crates/modules/nouser/core",
"crates/modules/nouser/nous",
"crates/modules/nouser/nous-mock",
"crates/modules/nouser/nous-real",
"crates/modules/chasqui/card",
"crates/modules/chasqui/core",
"crates/modules/chasqui/nous",
"crates/modules/chasqui/nous-mock",
"crates/modules/chasqui/nous-real",
# ============================================================
# modules/shipote/ — runtime de espacios aislados con flujo tipado
# modules/shuma/ — Runtime de espacios aislados (era shipote)
# ============================================================
"crates/modules/shipote/shipote-card",
"crates/modules/shipote/shipote-protocol",
"crates/modules/shipote/shipote-discern",
"crates/modules/shipote/shipote-core",
"crates/modules/shuma/shuma-card",
"crates/modules/shuma/shuma-protocol",
"crates/modules/shuma/shuma-discern",
"crates/modules/shuma/shuma-core",
"crates/modules/shuma/shuma-intent",
"crates/modules/shuma/shuma-line",
"crates/modules/shuma/shuma-sysmon",
"crates/modules/shuma/shuma-session",
"crates/modules/shuma/shuma-exec",
"crates/modules/shuma/shuma-infer",
"crates/modules/shuma/shuma-shell-render",
# ============================================================
# modules/gioser/ — landing WASM (chacana + 4 elementos)
# modules/dominium/ — Simulador psicológico de campo medio
# ============================================================
"crates/modules/dominium/dominium-core",
"crates/modules/dominium/dominium-physics",
"crates/modules/dominium/dominium-iso",
"crates/modules/dominium/dominium-render-plan",
"crates/modules/dominium/dominium-canvas-gpui",
# ============================================================
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
# ============================================================
"crates/modules/gioser/gioser-geom",
"crates/modules/gioser/gioser-physics",
"crates/modules/gioser/gioser-palette",
"crates/modules/gioser/gioser-shaders",
"crates/modules/gioser/gioser-canvas-web",
"crates/modules/gioser/gioser-graph-web",
# ==========================================================
# modules/fana/ — Writer DAG editor (absorbe pluma)
# ============================================================
"crates/modules/fana/fana-core",
"crates/modules/fana/fana-graph",
"crates/modules/fana/fana-render-plan",
"crates/modules/fana/fana-editor-gpui",
"crates/modules/fana/fana-store",
"crates/modules/fana/fana-semantic",
"crates/modules/fana/fana-md",
"crates/modules/fana/fana-md-reader-web",
# ============================================================
# modules/pluma/ — markdown agnóstico + visor web elegante
# modules/revista/ — Deck horizontal swipe (Flutter PageView)
# ============================================================
"crates/modules/pluma/pluma-md",
"crates/modules/pluma/pluma-reader-web",
"crates/modules/revista/revista-core",
"crates/modules/revista/revista-web",
# ============================================================
# modules/vista/ — deck horizontal swipe estilo Flutter PageView
# ============================================================
"crates/modules/vista/vista-web",
# ============================================================
# modules/barra/ — taskbar agnóstica estilo Windows
# modules/barra/ — Taskbar agnóstica estilo Windows
# ============================================================
"crates/modules/barra/barra-core",
"crates/modules/barra/barra-web",
# ============================================================
# modules/cosmobiologia/ — estudio de astrología profesional
# ============================================================
"crates/modules/cosmobiologia/cosmobiologia-card",
"crates/modules/cosmobiologia/cosmobiologia-model",
"crates/modules/cosmobiologia/cosmobiologia-store",
"crates/modules/cosmobiologia/cosmobiologia-render",
"crates/modules/cosmobiologia/cosmobiologia-engine",
"crates/modules/cosmobiologia/cosmobiologia-modules",
"crates/modules/cosmobiologia/cosmobiologia-theme",
"crates/modules/cosmobiologia/cosmobiologia-canvas",
"crates/modules/cosmobiologia/cosmobiologia-tree",
"crates/modules/cosmobiologia/cosmobiologia-panel",
#### # modules/cosmobiologia/ — Estudio de astrología profesional
#### # ============================================================
#### "crates/modules/cosmobiologia/cosmobiologia-card",
#### "crates/modules/cosmobiologia/cosmobiologia-model",
#### "crates/modules/cosmobiologia/cosmobiologia-store",
#### "crates/modules/cosmobiologia/cosmobiologia-render",
#### "crates/modules/cosmobiologia/cosmobiologia-corpus",
#### "crates/modules/cosmobiologia/cosmobiologia-engine",
#### "crates/modules/cosmobiologia/cosmobiologia-modules",
#### "crates/modules/cosmobiologia/cosmobiologia-theme",
#### "crates/modules/cosmobiologia/cosmobiologia-canvas",
#### "crates/modules/cosmobiologia/cosmobiologia-tree",
#### "crates/modules/cosmobiologia/cosmobiologia-panel",
#### "crates/modules/cosmobiologia/cosmobiologia-web",
# ============================================================
# apps/ — apps que consumen el protocolo (yahweh modules+shell)
# apps/ — Binarios finales que consumen el protocolo
# ============================================================
"crates/apps/file_explorer",
"crates/apps/database_explorer",
"crates/apps/text_viewer",
"crates/apps/image_viewer",
"crates/apps/yahweh-shell",
"crates/apps/nouser-explorer",
"crates/apps/brahman-broker-explorer",
"crates/apps/brahman-demo",
"crates/apps/sandokan",
"crates/apps/nahual-shell",
"crates/apps/nahual-file-explorer",
"crates/apps/nahual-database-explorer",
"crates/apps/nahual-text-viewer",
"crates/apps/nahual-image-viewer",
"crates/apps/chasqui-explorer",
"crates/apps/nakui-explorer",
"crates/apps/nakui-ui",
"crates/apps/minga-explorer",
"crates/apps/brahman-broker-explorer",
"crates/apps/brahman-demo",
"crates/apps/shipote-daemon",
"crates/apps/shipote-cli",
"crates/apps/shipote-gateway",
"crates/apps/shipote-shell",
"crates/apps/shuma-daemon",
"crates/apps/shuma-cli",
"crates/apps/shuma-gateway",
"crates/apps/shuma-shell",
"crates/apps/gioser-web",
"crates/apps/lapaloma-demo",
"crates/apps/lapaloma-stream-demo",
"crates/apps/lapaloma-phosphor-demo",
"crates/apps/lapaloma-financial-demo",
"crates/apps/cosmobiologia",
"crates/apps/cosmobiologia-cli",
"crates/apps/pineal-demo",
"crates/apps/pineal-stream-demo",
"crates/apps/pineal-phosphor-demo",
"crates/apps/pineal-financial-demo",
#### "crates/apps/cosmobiologia",
#### "crates/apps/cosmobiologia-cli",
#### "crates/apps/cosmobiologia-server",
"crates/apps/dominium",
"crates/apps/fana",
"crates/apps/agorapura",
"crates/apps/badu",
"crates/apps/matilda",
"crates/apps/yachay",
"crates/apps/mirada",
"crates/apps/mirada-compositor",
"crates/apps/mirada-ctl",
"crates/apps/mirada-launcher",
"crates/apps/mirada-portal",
"crates/apps/mirada-greeter",
"crates/apps/charka",
]
# renaser — el SO bare-metal SASOS. Vive en el mismo repo pero es su
# PROPIO workspace de Cargo: usa toolchain nightly, target
# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
# perfiles globales de este workspace. Cargo lo trata como ajeno; los
# crates compartidos se referencian por `path` cruzando la frontera.
exclude = ["renaser"]
[workspace.package]
version = "0.1.0"
edition = "2021"
@@ -190,6 +328,7 @@ serde_json = "1"
serde-big-array = "0.5"
postcard = { version = "1", features = ["use-std"] }
toml = "0.8"
ron = "0.8"
bincode = "1"
base64 = "0.22"
@@ -222,7 +361,9 @@ argon2 = "0.5"
rand = "0.8"
# === WASM (arje) ===
wasmi = "0.40"
# wasmi 1.0: unifica la versión con renaser (su kernel ya corre 1.0), para
# que el ABI WASM del host sea idéntico en Linux y en bare-metal.
wasmi = "1.0"
wat = "1"
# === Storage / DB ===
@@ -234,6 +375,12 @@ libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macr
libp2p-stream = "=0.4.0-alpha"
libp2p-allow-block-list = "0.6"
# === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) ===
russh = "0.54"
# === Math determinista cross-platform (dominium) ===
libm = "0.2"
# === Code parsing (minga) ===
tree-sitter = "0.24"
tree-sitter-rust = "0.23"
@@ -245,17 +392,25 @@ tree-sitter-go = "0.23"
# === FS notify ===
notify = "6.1"
# === FUSE (minga-vfs) ===
# default-features = false: prescinde de pkg-config/libfuse-dev en build.
# El montaje pasa a ser Rust puro (vía el helper `fusermount3` en runtime).
fuser = { version = "0.15", default-features = false }
# === CLI / auth (minga) ===
clap = { version = "4", features = ["derive"] }
rpassword = "7"
# === PAM (brahman-auth) ===
pam = "0.8"
# === D-Bus (arje compat) ===
zbus = { version = "4", default-features = false, features = ["tokio"] }
# === Tests ===
tempfile = "3"
# === GPUI (yahweh) ===
# === GPUI (nahual) ===
gpui = "0.2"
# === Filesystem helpers ===
@@ -272,40 +427,40 @@ glam = "0.30"
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
# ============================================================
# Intra-workspace deps de yahweh (referenciadas por workspace = true)
# Intra-workspace deps de nahual (referenciadas por workspace = true)
# ============================================================
yahweh-core = { path = "crates/modules/ui_engine/libs/core" }
yahweh-theme = { path = "crates/modules/ui_engine/libs/theme" }
yahweh-bus = { path = "crates/modules/ui_engine/libs/bus" }
yahweh-provider-fs = { path = "crates/modules/ui_engine/libs/providers/fs" }
yahweh-provider-sqlite = { path = "crates/modules/ui_engine/libs/providers/sqlite" }
yahweh-widget-tree = { path = "crates/modules/ui_engine/widgets/tree" }
yahweh-widget-container-core = { path = "crates/modules/ui_engine/widgets/container_core" }
yahweh-widget-splitter = { path = "crates/modules/ui_engine/widgets/splitter" }
yahweh-widget-tabs = { path = "crates/modules/ui_engine/widgets/tabs" }
yahweh-widget-tiled = { path = "crates/modules/ui_engine/widgets/tiled" }
yahweh-widget-text-input = { path = "crates/modules/ui_engine/widgets/text_input" }
yahweh-file-explorer = { path = "crates/apps/file_explorer" }
yahweh-database-explorer = { path = "crates/apps/database_explorer" }
yahweh-text-viewer = { path = "crates/apps/text_viewer" }
yahweh-image-viewer = { path = "crates/apps/image_viewer" }
nahual-core = { path = "crates/modules/nahual/libs/core" }
nahual-theme = { path = "crates/modules/nahual/libs/theme" }
nahual-bus = { path = "crates/modules/nahual/libs/bus" }
nahual-provider-fs = { path = "crates/modules/nahual/libs/providers/fs" }
nahual-provider-sqlite = { path = "crates/modules/nahual/libs/providers/sqlite" }
nahual-widget-tree = { path = "crates/modules/nahual/widgets/tree" }
nahual-widget-container-core = { path = "crates/modules/nahual/widgets/container_core" }
nahual-widget-splitter = { path = "crates/modules/nahual/widgets/splitter" }
nahual-widget-tabs = { path = "crates/modules/nahual/widgets/tabs" }
nahual-widget-tiled = { path = "crates/modules/nahual/widgets/tiled" }
nahual-widget-text-input = { path = "crates/modules/nahual/widgets/text_input" }
nahual-file-explorer = { path = "crates/apps/nahual-file-explorer" }
nahual-database-explorer = { path = "crates/apps/nahual-database-explorer" }
nahual-text-viewer = { path = "crates/apps/nahual-text-viewer" }
nahual-image-viewer = { path = "crates/apps/nahual-image-viewer" }
# ============================================================
# Intra-workspace deps de lapaloma (módulo de gráficos)
# Intra-workspace deps de pineal (módulo de gráficos)
# ============================================================
lapaloma-core = { path = "crates/modules/ui_engine/libs/lapaloma-core" }
lapaloma-render = { path = "crates/modules/ui_engine/widgets/lapaloma-render" }
lapaloma-cartesian = { path = "crates/modules/ui_engine/widgets/lapaloma-cartesian" }
lapaloma-stream = { path = "crates/modules/ui_engine/widgets/lapaloma-stream" }
lapaloma-mesh = { path = "crates/modules/ui_engine/widgets/lapaloma-mesh" }
lapaloma-financial = { path = "crates/modules/ui_engine/widgets/lapaloma-financial" }
lapaloma-polar = { path = "crates/modules/ui_engine/widgets/lapaloma-polar" }
lapaloma-heatmap = { path = "crates/modules/ui_engine/widgets/lapaloma-heatmap" }
lapaloma-treemap = { path = "crates/modules/ui_engine/widgets/lapaloma-treemap" }
lapaloma-flow = { path = "crates/modules/ui_engine/widgets/lapaloma-flow" }
lapaloma-phosphor = { path = "crates/modules/ui_engine/widgets/lapaloma-phosphor" }
lapaloma-export = { path = "crates/modules/ui_engine/widgets/lapaloma-export" }
lapaloma = { path = "crates/modules/ui_engine/widgets/lapaloma" }
pineal-core = { path = "crates/modules/pineal/core" }
pineal-render = { path = "crates/modules/pineal/render" }
pineal-cartesian = { path = "crates/modules/pineal/cartesian" }
pineal-stream = { path = "crates/modules/pineal/stream" }
pineal-mesh = { path = "crates/modules/pineal/mesh" }
pineal-financial = { path = "crates/modules/pineal/financial" }
pineal-polar = { path = "crates/modules/pineal/polar" }
pineal-heatmap = { path = "crates/modules/pineal/heatmap" }
pineal-treemap = { path = "crates/modules/pineal/treemap" }
pineal-flow = { path = "crates/modules/pineal/flow" }
pineal-phosphor = { path = "crates/modules/pineal/phosphor" }
pineal-export = { path = "crates/modules/pineal/export" }
pineal = { path = "crates/modules/pineal/umbrella" }
[profile.release]
lto = "thin"
+509
View File
@@ -0,0 +1,509 @@
[workspace]
resolver = "2"
members = [
# ============================================================
# protocol/ — Contratos canónicos + routing entre módulos
# ============================================================
"crates/protocol/brahman-card",
"crates/protocol/brahman-card-wit",
"crates/protocol/brahman-cards",
"crates/protocol/brahman-handshake",
"crates/protocol/brahman-broker",
"crates/protocol/brahman-admin",
"crates/protocol/brahman-sidecar",
"crates/protocol/brahman-net",
"crates/protocol/brahman-dht",
"crates/protocol/brahman-card-discovery",
"crates/protocol/brahman-ssh-multiplex",
"crates/protocol/brahman-auth",
"crates/protocol/arje-card",
# ============================================================
# init/ — PID 1 + encarnación Linux (arje)
# ============================================================
"crates/init/arje-zero",
"crates/init/arje-kernel",
"crates/init/arje-soma",
"crates/init/arje-snapshot",
"crates/init/arje-incarnate",
"crates/init/arje-absorb",
# ============================================================
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
# ============================================================
"crates/runtime/arje-bus",
"crates/runtime/arje-cas",
"crates/runtime/arje-wasm",
"crates/runtime/arje-brain-rules",
"crates/runtime/arje-brain-cognitive",
"crates/runtime/arje-brain-audit",
"crates/runtime/arje-brain",
"crates/runtime/arje-echo",
"crates/runtime/sandokan-lifecycle",
"crates/runtime/sandokan-core",
"crates/runtime/sandokan-local",
"crates/runtime/sandokan-daemon",
"crates/runtime/sandokan-remote",
"crates/runtime/sandokan",
# ============================================================
# compat/ — Shims D-Bus para correr software systemd-aware
# ============================================================
"crates/compat/arje-compat-common",
"crates/compat/arje-policy-provider",
"crates/compat/arje-logind-compat",
"crates/compat/arje-hostnamed-compat",
"crates/compat/arje-timedated-compat",
"crates/compat/arje-localed-compat",
"crates/compat/arje-journald-compat",
"crates/compat/arje-resolved-compat",
"crates/compat/arje-polkit-compat",
"crates/compat/arje-machined-compat",
"crates/compat/arje-tmpfiles-compat",
"crates/compat/arje-systemd1-compat",
"crates/compat/arje-notify-compat",
"crates/compat/arje-binfmt-compat",
"crates/compat/arje-timer-compat",
# ============================================================
# modules/semantic_dht/ (minga) — DHT semántico de código
# ============================================================
"crates/modules/semantic_dht/minga-core",
"crates/modules/semantic_dht/minga-store",
"crates/modules/semantic_dht/minga-p2p",
"crates/modules/semantic_dht/minga-vfs",
"crates/modules/semantic_dht/minga-cli",
# ============================================================
# modules/nahual/ — Motor GPUI: libs + widgets (era yahweh)
# ============================================================
"crates/modules/nahual/libs/core",
"crates/modules/nahual/libs/theme",
"crates/modules/nahual/libs/launcher",
"crates/modules/nahual/libs/bus",
"crates/modules/nahual/libs/meta-schema",
"crates/modules/nahual/libs/meta-runtime",
"crates/modules/nahual/libs/providers/fs",
"crates/modules/nahual/libs/providers/sqlite",
"crates/modules/nahual/widgets/tree",
"crates/modules/nahual/widgets/container_core",
"crates/modules/nahual/widgets/splitter",
"crates/modules/nahual/widgets/tabs",
"crates/modules/nahual/widgets/tiled",
"crates/modules/nahual/widgets/text_input",
"crates/modules/nahual/widgets/meta-form",
"crates/modules/nahual/widgets/banner",
"crates/modules/nahual/widgets/card",
"crates/modules/nahual/widgets/stat-card",
"crates/modules/nahual/widgets/app-header",
"crates/modules/nahual/widgets/theme-switcher",
# ============================================================
# modules/pineal/ — Data-viz agnóstica con backends (era lapaloma)
# ============================================================
"crates/modules/pineal/core",
"crates/modules/pineal/render",
"crates/modules/pineal/cartesian",
"crates/modules/pineal/stream",
"crates/modules/pineal/mesh",
"crates/modules/pineal/financial",
"crates/modules/pineal/polar",
"crates/modules/pineal/heatmap",
"crates/modules/pineal/treemap",
"crates/modules/pineal/flow",
"crates/modules/pineal/phosphor",
"crates/modules/pineal/export",
"crates/modules/pineal/umbrella",
# ============================================================
# modules/verbo/ — Provider de embeddings model-agnostic
# ============================================================
"crates/modules/verbo/verbo-core",
"crates/modules/verbo/verbo-mock",
"crates/modules/verbo/verbo-daemon",
# ============================================================
# modules/agorapura/ — Identidad humana federada
# ============================================================
"crates/modules/agorapura/agorapura-core",
"crates/modules/agorapura/agorapura-graph",
# ============================================================
# modules/badu/ — Toma de notas con gravedad semántica
# ============================================================
"crates/modules/badu/badu-core",
"crates/modules/badu/badu-gravity",
# ============================================================
# modules/takiy/ — Composición musical asistida
# ============================================================
"crates/modules/takiy/takiy-core",
# ============================================================
# modules/matilda/ — Administración de servidores
# ============================================================
"crates/modules/matilda/matilda-core",
"crates/modules/matilda/matilda-config",
"crates/modules/matilda/matilda-plan",
"crates/modules/matilda/matilda-apply",
"crates/modules/matilda/matilda-ghost",
"crates/modules/matilda/matilda-linker",
"crates/modules/matilda/matilda-discover",
# ============================================================
# modules/yachay/ — Notebooks computacionales reproducibles
# ============================================================
"crates/modules/yachay/yachay-core",
# ============================================================
# modules/charka/ — Transpilador COBOL → Rust
# ============================================================
"crates/modules/charka/charka-bcd",
"crates/modules/charka/charka-lexer",
"crates/modules/charka/charka-parser",
"crates/modules/charka/charka-ir",
"crates/modules/charka/charka-runtime",
"crates/modules/charka/charka-codegen",
"crates/modules/charka/charka-shadow",
# ============================================================
# modules/mirada/ — Compositor Wayland
# ============================================================
"crates/modules/mirada/mirada-layout",
"crates/modules/mirada/mirada-protocol",
"crates/modules/mirada/mirada-brain",
"crates/modules/mirada/mirada-link",
"crates/modules/mirada/mirada-body",
# ============================================================
# modules/nakui/ — ERP matemático (categórico)
# ============================================================
"crates/modules/nakui/core",
# ============================================================
# modules/chasqui/ — Explorador semántico de nómadas (ex-nouser, ex-akasha)
# ============================================================
"crates/modules/chasqui/card",
"crates/modules/chasqui/core",
"crates/modules/chasqui/nous",
"crates/modules/chasqui/nous-mock",
"crates/modules/chasqui/nous-real",
# ============================================================
# modules/shuma/ — Runtime de espacios aislados (era shipote)
# ============================================================
"crates/modules/shuma/shuma-card",
"crates/modules/shuma/shuma-protocol",
"crates/modules/shuma/shuma-discern",
"crates/modules/shuma/shuma-core",
"crates/modules/shuma/shuma-intent",
"crates/modules/shuma/shuma-line",
"crates/modules/shuma/shuma-sysmon",
"crates/modules/shuma/shuma-session",
"crates/modules/shuma/shuma-exec",
"crates/modules/shuma/shuma-infer",
"crates/modules/shuma/shuma-shell-render",
# ============================================================
# modules/dominium/ — Simulador psicológico de campo medio
# ============================================================
"crates/modules/dominium/dominium-core",
"crates/modules/dominium/dominium-physics",
"crates/modules/dominium/dominium-iso",
"crates/modules/dominium/dominium-render-plan",
"crates/modules/dominium/dominium-canvas-gpui",
# ============================================================
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
# ============================================================
"crates/modules/gioser/gioser-geom",
"crates/modules/gioser/gioser-physics",
"crates/modules/gioser/gioser-palette",
"crates/modules/gioser/gioser-shaders",
"crates/modules/gioser/gioser-canvas-web",
"crates/modules/gioser/gioser-graph-web",
# ==========================================================
# modules/fana/ — Writer DAG editor (absorbe pluma)
# ============================================================
"crates/modules/fana/fana-core",
"crates/modules/fana/fana-graph",
"crates/modules/fana/fana-render-plan",
"crates/modules/fana/fana-editor-gpui",
"crates/modules/fana/fana-store",
"crates/modules/fana/fana-semantic",
"crates/modules/fana/fana-md",
"crates/modules/fana/fana-md-reader-web",
# ============================================================
# modules/revista/ — Deck horizontal swipe (Flutter PageView)
# ============================================================
"crates/modules/revista/revista-core",
"crates/modules/revista/revista-web",
# ============================================================
# modules/barra/ — Taskbar agnóstica estilo Windows
# ============================================================
"crates/modules/barra/barra-core",
"crates/modules/barra/barra-web",
# ============================================================
#### # modules/cosmobiologia/ — Estudio de astrología profesional
#### # ============================================================
#### "crates/modules/cosmobiologia/cosmobiologia-card",
#### "crates/modules/cosmobiologia/cosmobiologia-model",
#### "crates/modules/cosmobiologia/cosmobiologia-store",
#### "crates/modules/cosmobiologia/cosmobiologia-render",
#### "crates/modules/cosmobiologia/cosmobiologia-corpus",
#### "crates/modules/cosmobiologia/cosmobiologia-engine",
#### "crates/modules/cosmobiologia/cosmobiologia-modules",
#### "crates/modules/cosmobiologia/cosmobiologia-theme",
#### "crates/modules/cosmobiologia/cosmobiologia-canvas",
#### "crates/modules/cosmobiologia/cosmobiologia-tree",
#### "crates/modules/cosmobiologia/cosmobiologia-panel",
#### "crates/modules/cosmobiologia/cosmobiologia-web",
# ============================================================
# apps/ — Binarios finales que consumen el protocolo
# ============================================================
"crates/apps/brahman-broker-explorer",
"crates/apps/brahman-demo",
"crates/apps/sandokan",
"crates/apps/nahual-shell",
"crates/apps/nahual-file-explorer",
"crates/apps/nahual-database-explorer",
"crates/apps/nahual-text-viewer",
"crates/apps/nahual-image-viewer",
"crates/apps/chasqui-explorer",
"crates/apps/nakui-explorer",
"crates/apps/nakui-ui",
"crates/apps/minga-explorer",
"crates/apps/shuma-daemon",
"crates/apps/shuma-cli",
"crates/apps/shuma-gateway",
"crates/apps/shuma-shell",
"crates/apps/gioser-web",
"crates/apps/pineal-demo",
"crates/apps/pineal-stream-demo",
"crates/apps/pineal-phosphor-demo",
"crates/apps/pineal-financial-demo",
#### "crates/apps/cosmobiologia",
#### "crates/apps/cosmobiologia-cli",
#### "crates/apps/cosmobiologia-server",
"crates/apps/dominium",
"crates/apps/fana",
"crates/apps/agorapura",
"crates/apps/badu",
"crates/apps/matilda",
"crates/apps/yachay",
"crates/apps/mirada",
"crates/apps/mirada-compositor",
"crates/apps/mirada-ctl",
"crates/apps/mirada-launcher",
"crates/apps/mirada-portal",
"crates/apps/mirada-greeter",
"crates/apps/charka",
]
# renaser — el SO bare-metal SASOS. Vive en el mismo repo pero es su
# PROPIO workspace de Cargo: usa toolchain nightly, target
# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
# perfiles globales de este workspace. Cargo lo trata como ajeno; los
# crates compartidos se referencian por `path` cruzando la frontera.
exclude = ["renaser"]
[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.80"
license = "MIT OR Apache-2.0"
authors = ["Brahman Contributors"]
publish = false
repository = "https://example.invalid/brahman"
[workspace.dependencies]
# === Serialización ===
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde-big-array = "0.5"
postcard = { version = "1", features = ["use-std"] }
toml = "0.8"
ron = "0.8"
bincode = "1"
base64 = "0.22"
# === Errores ===
thiserror = "2" # bump uniforme; arje (era 1) puede requerir ajustes menores
anyhow = "1"
# === Async ===
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["compat"] }
async-trait = "0.1"
futures = "0.3"
# === Observabilidad ===
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
# === Linux primitives (arje) ===
nix = { version = "0.29", features = ["signal", "process", "sched", "mount", "fs", "socket", "net", "user"] }
libc = "0.2"
# === IDs / Hash / Crypto ===
ulid = { version = "1", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
sha2 = "0.10"
blake3 = "1.5"
ed25519-dalek = "2"
aes-gcm = "0.10"
argon2 = "0.5"
rand = "0.8"
# === WASM (arje) ===
# wasmi 1.0: unifica la versión con renaser (su kernel ya corre 1.0), para
# que el ABI WASM del host sea idéntico en Linux y en bare-metal.
wasmi = "1.0"
wat = "1"
# === Storage / DB ===
sled = "0.34"
rusqlite = { version = "0.31", features = ["bundled", "blob"] }
# === P2P (minga) ===
libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macros", "kad", "identify"] }
libp2p-stream = "=0.4.0-alpha"
libp2p-allow-block-list = "0.6"
# === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) ===
russh = "0.54"
# === Math determinista cross-platform (dominium) ===
libm = "0.2"
# === Code parsing (minga) ===
tree-sitter = "0.24"
tree-sitter-rust = "0.23"
tree-sitter-python = "0.23"
tree-sitter-typescript = "0.23"
tree-sitter-javascript = "0.23"
tree-sitter-go = "0.23"
# === FS notify ===
notify = "6.1"
# === FUSE (minga-vfs) ===
# default-features = false: prescinde de pkg-config/libfuse-dev en build.
# El montaje pasa a ser Rust puro (vía el helper `fusermount3` en runtime).
fuser = { version = "0.15", default-features = false }
# === CLI / auth (minga) ===
clap = { version = "4", features = ["derive"] }
rpassword = "7"
# === PAM (brahman-auth) ===
pam = "0.8"
# === D-Bus (arje compat) ===
zbus = { version = "4", default-features = false, features = ["tokio"] }
# === Tests ===
tempfile = "3"
# === GPUI (nahual) ===
gpui = "0.2"
# === Filesystem helpers ===
directories = "5"
# === WASM web (gioser) ===
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = "0.3"
glam = "0.30"
# === Markdown (pluma) ===
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
# ============================================================
# Intra-workspace deps de nahual (referenciadas por workspace = true)
# ============================================================
nahual-core = { path = "crates/modules/nahual/libs/core" }
nahual-theme = { path = "crates/modules/nahual/libs/theme" }
nahual-bus = { path = "crates/modules/nahual/libs/bus" }
nahual-provider-fs = { path = "crates/modules/nahual/libs/providers/fs" }
nahual-provider-sqlite = { path = "crates/modules/nahual/libs/providers/sqlite" }
nahual-widget-tree = { path = "crates/modules/nahual/widgets/tree" }
nahual-widget-container-core = { path = "crates/modules/nahual/widgets/container_core" }
nahual-widget-splitter = { path = "crates/modules/nahual/widgets/splitter" }
nahual-widget-tabs = { path = "crates/modules/nahual/widgets/tabs" }
nahual-widget-tiled = { path = "crates/modules/nahual/widgets/tiled" }
nahual-widget-text-input = { path = "crates/modules/nahual/widgets/text_input" }
nahual-file-explorer = { path = "crates/apps/nahual-file-explorer" }
nahual-database-explorer = { path = "crates/apps/nahual-database-explorer" }
nahual-text-viewer = { path = "crates/apps/nahual-text-viewer" }
nahual-image-viewer = { path = "crates/apps/nahual-image-viewer" }
# ============================================================
# Intra-workspace deps de pineal (módulo de gráficos)
# ============================================================
pineal-core = { path = "crates/modules/pineal/core" }
pineal-render = { path = "crates/modules/pineal/render" }
pineal-cartesian = { path = "crates/modules/pineal/cartesian" }
pineal-stream = { path = "crates/modules/pineal/stream" }
pineal-mesh = { path = "crates/modules/pineal/mesh" }
pineal-financial = { path = "crates/modules/pineal/financial" }
pineal-polar = { path = "crates/modules/pineal/polar" }
pineal-heatmap = { path = "crates/modules/pineal/heatmap" }
pineal-treemap = { path = "crates/modules/pineal/treemap" }
pineal-flow = { path = "crates/modules/pineal/flow" }
pineal-phosphor = { path = "crates/modules/pineal/phosphor" }
pineal-export = { path = "crates/modules/pineal/export" }
pineal = { path = "crates/modules/pineal/umbrella" }
[profile.release]
lto = "thin"
codegen-units = 1
strip = "symbols"
panic = "abort"
[profile.dev]
opt-level = 0
# `line-tables-only` mantiene stack traces con archivo:línea correctos
# pero descarta el resto de symbols. Reduce target/ ~40% sin sacrificar
# debugging real para nuestro flujo (no usamos gdb sobre estos crates).
debug = "line-tables-only"
split-debuginfo = "unpacked"
incremental = true
# Más codegen-units = más paralelismo + builds incrementales más chicas
# (cada cambio re-compila menos). Default es 256 en dev pero lo
# anclamos para evitar regresiones.
codegen-units = 256
# Override puntual para deps grandes que NO debuggeamos: gpui, ort,
# fastembed, tokenizers, image. Subir opt-level acá hace que sus libs
# pesen menos en target/ (símbolos descartados durante la build).
[profile.dev.package."*"]
opt-level = 0
debug = "line-tables-only"
[profile.dev.package.gpui]
opt-level = 1
debug = false
[profile.dev.package.ort]
opt-level = 1
debug = false
[profile.dev.package.fastembed]
opt-level = 1
debug = false
[profile.dev.package.tokenizers]
opt-level = 1
debug = false
[profile.dev.package.image]
opt-level = 1
debug = false
+509
View File
@@ -0,0 +1,509 @@
[workspace]
resolver = "2"
members = [
# ============================================================
# protocol/ — Contratos canónicos + routing entre módulos
# ============================================================
"crates/protocol/brahman-card",
"crates/protocol/brahman-card-wit",
"crates/protocol/brahman-cards",
"crates/protocol/brahman-handshake",
"crates/protocol/brahman-broker",
"crates/protocol/brahman-admin",
"crates/protocol/brahman-sidecar",
"crates/protocol/brahman-net",
"crates/protocol/brahman-dht",
"crates/protocol/brahman-card-discovery",
"crates/protocol/brahman-ssh-multiplex",
"crates/protocol/brahman-auth",
"crates/protocol/arje-card",
# ============================================================
# init/ — PID 1 + encarnación Linux (arje)
# ============================================================
"crates/init/arje-zero",
"crates/init/arje-kernel",
"crates/init/arje-soma",
"crates/init/arje-snapshot",
"crates/init/arje-incarnate",
"crates/init/arje-absorb",
# ============================================================
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
# ============================================================
"crates/runtime/arje-bus",
"crates/runtime/arje-cas",
"crates/runtime/arje-wasm",
"crates/runtime/arje-brain-rules",
"crates/runtime/arje-brain-cognitive",
"crates/runtime/arje-brain-audit",
"crates/runtime/arje-brain",
"crates/runtime/arje-echo",
"crates/runtime/sandokan-lifecycle",
"crates/runtime/sandokan-core",
"crates/runtime/sandokan-local",
"crates/runtime/sandokan-daemon",
"crates/runtime/sandokan-remote",
"crates/runtime/sandokan",
# ============================================================
# compat/ — Shims D-Bus para correr software systemd-aware
# ============================================================
"crates/compat/arje-compat-common",
"crates/compat/arje-policy-provider",
"crates/compat/arje-logind-compat",
"crates/compat/arje-hostnamed-compat",
"crates/compat/arje-timedated-compat",
"crates/compat/arje-localed-compat",
"crates/compat/arje-journald-compat",
"crates/compat/arje-resolved-compat",
"crates/compat/arje-polkit-compat",
"crates/compat/arje-machined-compat",
"crates/compat/arje-tmpfiles-compat",
"crates/compat/arje-systemd1-compat",
"crates/compat/arje-notify-compat",
"crates/compat/arje-binfmt-compat",
"crates/compat/arje-timer-compat",
# ============================================================
# modules/semantic_dht/ (minga) — DHT semántico de código
# ============================================================
"crates/modules/semantic_dht/minga-core",
"crates/modules/semantic_dht/minga-store",
"crates/modules/semantic_dht/minga-p2p",
"crates/modules/semantic_dht/minga-vfs",
"crates/modules/semantic_dht/minga-cli",
# ============================================================
# modules/nahual/ — Motor GPUI: libs + widgets (era yahweh)
# ============================================================
"crates/modules/nahual/libs/core",
"crates/modules/nahual/libs/theme",
"crates/modules/nahual/libs/launcher",
"crates/modules/nahual/libs/bus",
"crates/modules/nahual/libs/meta-schema",
"crates/modules/nahual/libs/meta-runtime",
"crates/modules/nahual/libs/providers/fs",
"crates/modules/nahual/libs/providers/sqlite",
"crates/modules/nahual/widgets/tree",
"crates/modules/nahual/widgets/container_core",
"crates/modules/nahual/widgets/splitter",
"crates/modules/nahual/widgets/tabs",
"crates/modules/nahual/widgets/tiled",
"crates/modules/nahual/widgets/text_input",
"crates/modules/nahual/widgets/meta-form",
"crates/modules/nahual/widgets/banner",
"crates/modules/nahual/widgets/card",
"crates/modules/nahual/widgets/stat-card",
"crates/modules/nahual/widgets/app-header",
"crates/modules/nahual/widgets/theme-switcher",
# ============================================================
# modules/pineal/ — Data-viz agnóstica con backends (era lapaloma)
# ============================================================
"crates/modules/pineal/core",
"crates/modules/pineal/render",
"crates/modules/pineal/cartesian",
"crates/modules/pineal/stream",
"crates/modules/pineal/mesh",
"crates/modules/pineal/financial",
"crates/modules/pineal/polar",
"crates/modules/pineal/heatmap",
"crates/modules/pineal/treemap",
"crates/modules/pineal/flow",
"crates/modules/pineal/phosphor",
"crates/modules/pineal/export",
"crates/modules/pineal/umbrella",
# ============================================================
# modules/verbo/ — Provider de embeddings model-agnostic
# ============================================================
"crates/modules/verbo/verbo-core",
"crates/modules/verbo/verbo-mock",
"crates/modules/verbo/verbo-daemon",
# ============================================================
# modules/agorapura/ — Identidad humana federada
# ============================================================
"crates/modules/agorapura/agorapura-core",
"crates/modules/agorapura/agorapura-graph",
# ============================================================
# modules/badu/ — Toma de notas con gravedad semántica
# ============================================================
"crates/modules/badu/badu-core",
"crates/modules/badu/badu-gravity",
# ============================================================
# modules/takiy/ — Composición musical asistida
# ============================================================
"crates/modules/takiy/takiy-core",
# ============================================================
# modules/matilda/ — Administración de servidores
# ============================================================
"crates/modules/matilda/matilda-core",
"crates/modules/matilda/matilda-config",
"crates/modules/matilda/matilda-plan",
"crates/modules/matilda/matilda-apply",
"crates/modules/matilda/matilda-ghost",
"crates/modules/matilda/matilda-linker",
"crates/modules/matilda/matilda-discover",
# ============================================================
# modules/yachay/ — Notebooks computacionales reproducibles
# ============================================================
"crates/modules/yachay/yachay-core",
# ============================================================
# modules/charka/ — Transpilador COBOL → Rust
# ============================================================
"crates/modules/charka/charka-bcd",
"crates/modules/charka/charka-lexer",
"crates/modules/charka/charka-parser",
"crates/modules/charka/charka-ir",
"crates/modules/charka/charka-runtime",
"crates/modules/charka/charka-codegen",
"crates/modules/charka/charka-shadow",
# ============================================================
# modules/mirada/ — Compositor Wayland
# ============================================================
"crates/modules/mirada/mirada-layout",
"crates/modules/mirada/mirada-protocol",
"crates/modules/mirada/mirada-brain",
"crates/modules/mirada/mirada-link",
"crates/modules/mirada/mirada-body",
# ============================================================
# modules/nakui/ — ERP matemático (categórico)
# ============================================================
"crates/modules/nakui/core",
# ============================================================
# modules/chasqui/ — Explorador semántico de nómadas (ex-nouser, ex-akasha)
# ============================================================
"crates/modules/chasqui/card",
"crates/modules/chasqui/core",
"crates/modules/chasqui/nous",
"crates/modules/chasqui/nous-mock",
"crates/modules/chasqui/nous-real",
# ============================================================
# modules/shuma/ — Runtime de espacios aislados (era shipote)
# ============================================================
"crates/modules/shuma/shuma-card",
"crates/modules/shuma/shuma-protocol",
"crates/modules/shuma/shuma-discern",
"crates/modules/shuma/shuma-core",
"crates/modules/shuma/shuma-intent",
"crates/modules/shuma/shuma-line",
"crates/modules/shuma/shuma-sysmon",
"crates/modules/shuma/shuma-session",
"crates/modules/shuma/shuma-exec",
"crates/modules/shuma/shuma-infer",
"crates/modules/shuma/shuma-shell-render",
# ============================================================
# modules/dominium/ — Simulador psicológico de campo medio
# ============================================================
"crates/modules/dominium/dominium-core",
"crates/modules/dominium/dominium-physics",
"crates/modules/dominium/dominium-iso",
"crates/modules/dominium/dominium-render-plan",
"crates/modules/dominium/dominium-canvas-gpui",
# ============================================================
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
# ============================================================
"crates/modules/gioser/gioser-geom",
"crates/modules/gioser/gioser-physics",
"crates/modules/gioser/gioser-palette",
"crates/modules/gioser/gioser-shaders",
"crates/modules/gioser/gioser-canvas-web",
"crates/modules/gioser/gioser-graph-web",
# ==========================================================
# modules/fana/ — Writer DAG editor (absorbe pluma)
# ============================================================
"crates/modules/fana/fana-core",
"crates/modules/fana/fana-graph",
"crates/modules/fana/fana-render-plan",
"crates/modules/fana/fana-editor-gpui",
"crates/modules/fana/fana-store",
"crates/modules/fana/fana-semantic",
"crates/modules/fana/fana-md",
"crates/modules/fana/fana-md-reader-web",
# ============================================================
# modules/revista/ — Deck horizontal swipe (Flutter PageView)
# ============================================================
"crates/modules/revista/revista-core",
"crates/modules/revista/revista-web",
# ============================================================
# modules/barra/ — Taskbar agnóstica estilo Windows
# ============================================================
"crates/modules/barra/barra-core",
"crates/modules/barra/barra-web",
# ============================================================
#### # modules/cosmobiologia/ — Estudio de astrología profesional
#### # ============================================================
#### "crates/modules/cosmobiologia/cosmobiologia-card",
#### "crates/modules/cosmobiologia/cosmobiologia-model",
#### "crates/modules/cosmobiologia/cosmobiologia-store",
#### "crates/modules/cosmobiologia/cosmobiologia-render",
#### "crates/modules/cosmobiologia/cosmobiologia-corpus",
#### "crates/modules/cosmobiologia/cosmobiologia-engine",
#### "crates/modules/cosmobiologia/cosmobiologia-modules",
#### "crates/modules/cosmobiologia/cosmobiologia-theme",
#### "crates/modules/cosmobiologia/cosmobiologia-canvas",
#### "crates/modules/cosmobiologia/cosmobiologia-tree",
#### "crates/modules/cosmobiologia/cosmobiologia-panel",
#### "crates/modules/cosmobiologia/cosmobiologia-web",
# ============================================================
# apps/ — Binarios finales que consumen el protocolo
# ============================================================
"crates/apps/brahman-broker-explorer",
"crates/apps/brahman-demo",
"crates/apps/sandokan",
"crates/apps/nahual-shell",
"crates/apps/nahual-file-explorer",
"crates/apps/nahual-database-explorer",
"crates/apps/nahual-text-viewer",
"crates/apps/nahual-image-viewer",
"crates/apps/chasqui-explorer",
"crates/apps/nakui-explorer",
"crates/apps/nakui-ui",
"crates/apps/minga-explorer",
"crates/apps/shuma-daemon",
"crates/apps/shuma-cli",
"crates/apps/shuma-gateway",
"crates/apps/shuma-shell",
"crates/apps/gioser-web",
"crates/apps/pineal-demo",
"crates/apps/pineal-stream-demo",
"crates/apps/pineal-phosphor-demo",
"crates/apps/pineal-financial-demo",
#### "crates/apps/cosmobiologia",
#### "crates/apps/cosmobiologia-cli",
#### "crates/apps/cosmobiologia-server",
"crates/apps/dominium",
"crates/apps/fana",
"crates/apps/agorapura",
"crates/apps/badu",
"crates/apps/matilda",
"crates/apps/yachay",
"crates/apps/mirada",
"crates/apps/mirada-compositor",
"crates/apps/mirada-ctl",
"crates/apps/mirada-launcher",
"crates/apps/mirada-portal",
"crates/apps/mirada-greeter",
"crates/apps/charka",
]
# renaser — el SO bare-metal SASOS. Vive en el mismo repo pero es su
# PROPIO workspace de Cargo: usa toolchain nightly, target
# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
# perfiles globales de este workspace. Cargo lo trata como ajeno; los
# crates compartidos se referencian por `path` cruzando la frontera.
exclude = ["renaser"]
[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.80"
license = "MIT OR Apache-2.0"
authors = ["Brahman Contributors"]
publish = false
repository = "https://example.invalid/brahman"
[workspace.dependencies]
# === Serialización ===
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde-big-array = "0.5"
postcard = { version = "1", features = ["use-std"] }
toml = "0.8"
ron = "0.8"
bincode = "1"
base64 = "0.22"
# === Errores ===
thiserror = "2" # bump uniforme; arje (era 1) puede requerir ajustes menores
anyhow = "1"
# === Async ===
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["compat"] }
async-trait = "0.1"
futures = "0.3"
# === Observabilidad ===
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
# === Linux primitives (arje) ===
nix = { version = "0.29", features = ["signal", "process", "sched", "mount", "fs", "socket", "net", "user"] }
libc = "0.2"
# === IDs / Hash / Crypto ===
ulid = { version = "1", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
sha2 = "0.10"
blake3 = "1.5"
ed25519-dalek = "2"
aes-gcm = "0.10"
argon2 = "0.5"
rand = "0.8"
# === WASM (arje) ===
# wasmi 1.0: unifica la versión con renaser (su kernel ya corre 1.0), para
# que el ABI WASM del host sea idéntico en Linux y en bare-metal.
wasmi = "1.0"
wat = "1"
# === Storage / DB ===
sled = "0.34"
rusqlite = { version = "0.31", features = ["bundled", "blob"] }
# === P2P (minga) ===
libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macros", "kad", "identify"] }
libp2p-stream = "=0.4.0-alpha"
libp2p-allow-block-list = "0.6"
# === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) ===
russh = "0.54"
# === Math determinista cross-platform (dominium) ===
libm = "0.2"
# === Code parsing (minga) ===
tree-sitter = "0.24"
tree-sitter-rust = "0.23"
tree-sitter-python = "0.23"
tree-sitter-typescript = "0.23"
tree-sitter-javascript = "0.23"
tree-sitter-go = "0.23"
# === FS notify ===
notify = "6.1"
# === FUSE (minga-vfs) ===
# default-features = false: prescinde de pkg-config/libfuse-dev en build.
# El montaje pasa a ser Rust puro (vía el helper `fusermount3` en runtime).
fuser = { version = "0.15", default-features = false }
# === CLI / auth (minga) ===
clap = { version = "4", features = ["derive"] }
rpassword = "7"
# === PAM (brahman-auth) ===
pam = "0.8"
# === D-Bus (arje compat) ===
zbus = { version = "4", default-features = false, features = ["tokio"] }
# === Tests ===
tempfile = "3"
# === GPUI (nahual) ===
gpui = "0.2"
# === Filesystem helpers ===
directories = "5"
# === WASM web (gioser) ===
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = "0.3"
glam = "0.30"
# === Markdown (pluma) ===
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
# ============================================================
# Intra-workspace deps de nahual (referenciadas por workspace = true)
# ============================================================
nahual-core = { path = "crates/modules/nahual/libs/core" }
nahual-theme = { path = "crates/modules/nahual/libs/theme" }
nahual-bus = { path = "crates/modules/nahual/libs/bus" }
nahual-provider-fs = { path = "crates/modules/nahual/libs/providers/fs" }
nahual-provider-sqlite = { path = "crates/modules/nahual/libs/providers/sqlite" }
nahual-widget-tree = { path = "crates/modules/nahual/widgets/tree" }
nahual-widget-container-core = { path = "crates/modules/nahual/widgets/container_core" }
nahual-widget-splitter = { path = "crates/modules/nahual/widgets/splitter" }
nahual-widget-tabs = { path = "crates/modules/nahual/widgets/tabs" }
nahual-widget-tiled = { path = "crates/modules/nahual/widgets/tiled" }
nahual-widget-text-input = { path = "crates/modules/nahual/widgets/text_input" }
nahual-file-explorer = { path = "crates/apps/nahual-file-explorer" }
nahual-database-explorer = { path = "crates/apps/nahual-database-explorer" }
nahual-text-viewer = { path = "crates/apps/nahual-text-viewer" }
nahual-image-viewer = { path = "crates/apps/nahual-image-viewer" }
# ============================================================
# Intra-workspace deps de pineal (módulo de gráficos)
# ============================================================
pineal-core = { path = "crates/modules/pineal/core" }
pineal-render = { path = "crates/modules/pineal/render" }
pineal-cartesian = { path = "crates/modules/pineal/cartesian" }
pineal-stream = { path = "crates/modules/pineal/stream" }
pineal-mesh = { path = "crates/modules/pineal/mesh" }
pineal-financial = { path = "crates/modules/pineal/financial" }
pineal-polar = { path = "crates/modules/pineal/polar" }
pineal-heatmap = { path = "crates/modules/pineal/heatmap" }
pineal-treemap = { path = "crates/modules/pineal/treemap" }
pineal-flow = { path = "crates/modules/pineal/flow" }
pineal-phosphor = { path = "crates/modules/pineal/phosphor" }
pineal-export = { path = "crates/modules/pineal/export" }
pineal = { path = "crates/modules/pineal/umbrella" }
[profile.release]
lto = "thin"
codegen-units = 1
strip = "symbols"
panic = "abort"
[profile.dev]
opt-level = 0
# `line-tables-only` mantiene stack traces con archivo:línea correctos
# pero descarta el resto de symbols. Reduce target/ ~40% sin sacrificar
# debugging real para nuestro flujo (no usamos gdb sobre estos crates).
debug = "line-tables-only"
split-debuginfo = "unpacked"
incremental = true
# Más codegen-units = más paralelismo + builds incrementales más chicas
# (cada cambio re-compila menos). Default es 256 en dev pero lo
# anclamos para evitar regresiones.
codegen-units = 256
# Override puntual para deps grandes que NO debuggeamos: gpui, ort,
# fastembed, tokenizers, image. Subir opt-level acá hace que sus libs
# pesen menos en target/ (símbolos descartados durante la build).
[profile.dev.package."*"]
opt-level = 0
debug = "line-tables-only"
[profile.dev.package.gpui]
opt-level = 1
debug = false
[profile.dev.package.ort]
opt-level = 1
debug = false
[profile.dev.package.fastembed]
opt-level = 1
debug = false
[profile.dev.package.tokenizers]
opt-level = 1
debug = false
[profile.dev.package.image]
opt-level = 1
debug = false
+52
View File
@@ -0,0 +1,52 @@
# apps/ — Binarios finales
**Propósito.** Aplicaciones ejecutables que consumen el protocolo
brahman y los módulos. Cada app es un `[[bin]]` o `cdylib` standalone.
## Mapa
### Protocol / Init
- `brahman-broker-explorer` — probe GPUI del broker (matches + sessions)
- `brahman-demo` — bootstrap reproducible: broker + producer + consumer
### Nahual (GPUI suite)
- `nahual-shell` — shell standard de explorers (sidebar+main+status)
- `nahual-file-explorer`, `nahual-database-explorer`,
`nahual-text-viewer`, `nahual-image-viewer`
### Akasha
- `akasha-explorer` — descubre el daemon `akasha-core` y lista Mónadas
### Nakui (ERP)
- `nakui-ui` — MetaUi+MetaForm con event log + replay
- `nakui-explorer` — dashboard sobre stack nahual
### Minga
- `minga-explorer` — dashboard de DHT semántico + indexer status
### Shuma (sandboxes)
- `shuma-daemon` — dueño de Workspaces (postcard sobre Unix socket)
- `shuma-cli` — CLI admin (`shipote` binario por compat)
- `shuma-gateway` — HTTP/JSON ↔ postcard
- `shuma-shell` — GUI GPUI de Workspaces
### Pineal (demos data-viz)
- `pineal-demo`, `pineal-stream-demo`, `pineal-phosphor-demo`,
`pineal-financial-demo`
### Web targets (cdylib WASM)
- `gioser-web` — landing chacana
- `cosmobiologia-web` (en modules/, no apps/) — bridge SVG
### Cosmobiología
- `cosmobiologia` — app GPUI principal (tree + canvas + panel)
- `cosmobiologia-cli` — calcula cartas headless
- `cosmobiologia-server` — server HTTP single-user con CRUD
## Convenciones
- Cada app declara su `Card` y se anuncia al Init vía `brahman-sidecar`.
- Apps que viven dentro de GPUI consumen `nahual-shell` para el chrome.
- Apps headless usan `clap` para argv.
- Tests E2E: usar `gpui::TestAppContext` para GPUI; CLI tests via
`tempfile` + `assert_cmd`.
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "agorapura"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "agorapura — demostración narrada del ágora de identidad: identidades, claims, atestaciones firmadas y evaluación de confianza bajo políticas negociadas."
[[bin]]
name = "agorapura"
path = "src/main.rs"
[dependencies]
agorapura-core = { path = "../../modules/agorapura/agorapura-core" }
agorapura-graph = { path = "../../modules/agorapura/agorapura-graph" }
+91
View File
@@ -0,0 +1,91 @@
//! `agorapura` — demostración narrada del ágora de identidad.
//!
//! Recorre el escenario canónico de extremo a extremo: la institución
//! *Venezuela* atestigua la nacionalidad de la persona *Yumaira*, y
//! otras identidades la corroboran. Imprime la evidencia acumulada y
//! cómo distintas políticas *negociadas* la aceptan o no.
//!
//! No es la app definitiva — es un smoke test legible y la mejor forma
//! de ver el módulo funcionando: `cargo run -p agorapura`.
use agorapura_core::{Attestation, Claim, IdentityKind, Keypair};
use agorapura_graph::{TrustGraph, TrustPolicy};
/// Segundo Unix fijo para que la demo sea reproducible.
const T0: u64 = 1_700_000_000;
fn main() {
println!("\n ágora · demostración de identidad federada\n");
// --- Identidades. Semillas fijas → demo reproducible. ---
let yumaira = Keypair::from_seed([20; 32]);
let venezuela = Keypair::from_seed([10; 32]);
let comunidad = Keypair::from_seed([30; 32]);
let vecina = Keypair::from_seed([40; 32]);
let mut agora = TrustGraph::new();
agora.register(yumaira.identity(IdentityKind::Person, "Yumaira"));
agora.register(venezuela.identity(IdentityKind::Institution, "Venezuela"));
agora.register(comunidad.identity(IdentityKind::Community, "Vecinos del Valle"));
agora.register(vecina.identity(IdentityKind::Person, "Carmen"));
println!(" identidades registradas:");
for kp in [&yumaira, &venezuela, &comunidad, &vecina] {
let id = kp.identity_id();
let name = agora.identity(id).map(|i| i.display_name.as_str()).unwrap_or("?");
println!(" {id} {name}");
}
// --- Atestaciones sobre la nacionalidad de Yumaira. ---
let nacionalidad = |by: &Keypair| {
Attestation::create(
by,
Claim::new(yumaira.identity_id(), "nacionalidad", "venezolana", T0),
)
};
println!("\n atestaciones de «nacionalidad = venezolana» sobre Yumaira:");
for (by, label) in [
(&venezuela, "Venezuela (institución)"),
(&comunidad, "Vecinos del Valle (comunidad)"),
(&yumaira, "Yumaira (ella misma)"),
] {
let att = nacionalidad(by);
match agora.add_attestation(att) {
Ok(()) => println!(" ✔ firma verificada — {label}"),
Err(e) => println!(" ✘ rechazada — {label}: {e}"),
}
}
// --- Intento de fraude: una firma manipulada. ---
let mut falsa = nacionalidad(&vecina);
falsa.claim.value = "marciana".into(); // rompe la firma
print!("\n intento de atestación con firma manipulada: ");
match agora.add_attestation(falsa) {
Ok(()) => println!("ACEPTADA (esto sería un bug)"),
Err(e) => println!("rechazada — {e}"),
}
// --- Corroboración. ---
let c = agora.corroboration(yumaira.identity_id(), "nacionalidad", "venezolana");
println!("\n corroboración del claim:");
println!(" atestadores totales : {}", c.total());
println!(" terceros (no ella) : {}", c.third_party());
println!(" auto-atestado : {}", c.self_attested);
// --- Veredicto según la política negociada. ---
println!("\n veredicto según la política (la verdad depende de lo pactado):");
for (policy, label) in [
(TrustPolicy::strict(1), "laxa · 1 tercero basta"),
(TrustPolicy::strict(2), "media · 2 terceros"),
(TrustPolicy::strict(3), "estricta · 3 terceros"),
] {
let ok = policy.accepts(&c);
let mark = if ok { "ACEPTA" } else { "rechaza" };
println!(" [{mark}] {label}");
}
println!(
"\n el ágora no dicta la verdad: acumula evidencia firmada y\n \
cada quien la pesa con la política que negocie.\n"
);
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "badu"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "badu — demostración de la toma de notas: un cuaderno con wiki-links, backlinks, enlaces colgantes y clústeres por gravedad semántica."
[[bin]]
name = "badu"
path = "src/main.rs"
[dependencies]
badu-core = { path = "../../modules/badu/badu-core" }
badu-gravity = { path = "../../modules/badu/badu-gravity" }
+134
View File
@@ -0,0 +1,134 @@
//! `badu` — demostración del cuaderno de notas.
//!
//! Siembra un cuaderno personal, imprime el grafo de wiki-links
//! (forward-links, backlinks, huérfanas, enlaces colgantes) y luego la
//! gravedad semántica: los clústeres por afinidad y los vecinos más
//! cercanos de una nota.
//!
//! Los vectores semánticos van a mano —tres tópicos: cocina, jardín,
//! oficina— para que el clustering se vea con claridad. En la app real
//! los produce `verbo`. Corre con `cargo run -p badu`.
use badu_core::{NoteId, NoteStore};
use badu_gravity::{GravityConfig, SemanticField};
/// Vector de tópico con un leve sesgo — notas del mismo tema quedan
/// afines sin ser idénticas.
fn topic(base: [f32; 3], nudge: f32) -> Vec<f32> {
vec![base[0] + nudge, base[1] + nudge * 0.3, base[2] - nudge * 0.2]
}
fn main() {
let cocina = [1.0, 0.0, 0.0];
let jardin = [0.0, 1.0, 0.0];
let oficina = [0.0, 0.0, 1.0];
let mut store = NoteStore::new();
let mut field = SemanticField::new();
// (título, cuerpo, etiquetas, vector de tópico)
let seed: [(&str, &str, &[&str], Vec<f32>); 7] = [
(
"Índice",
"mi cuaderno: [[Recetas de la abuela]], [[Jardín]] y [[Oficina]]",
&["meta"],
topic(cocina, 0.0),
),
(
"Recetas de la abuela",
"sopa de auyama; ver también [[Lista del mercado]]",
&["cocina"],
topic(cocina, 0.05),
),
(
"Lista del mercado",
"auyama, cilantro, pan; vuelve al [[Índice]]",
&["cocina"],
topic(cocina, 0.10),
),
(
"Jardín",
"riego semanal; las [[Semillas de cilantro]] van en marzo",
&["jardín"],
topic(jardin, 0.05),
),
(
"Semillas de cilantro",
"germinan en diez días",
&["jardín"],
topic(jardin, 0.10),
),
(
"Oficina",
"[[Reunión del lunes]] y pendientes varios",
&["trabajo"],
topic(oficina, 0.05),
),
(
"Diario sin enlaces",
"una nota suelta, no la enlaza nadie y enlaza a [[Algo Perdido]]",
&["personal"],
topic(oficina, 0.50),
),
];
let mut ids: Vec<(NoteId, String)> = Vec::new();
for (title, body, tags, vector) in seed {
let tags = tags.iter().map(|t| t.to_string()).collect();
let id = store.create(title, body, tags, 1_700_000_000);
field.insert(id, vector);
ids.push((id, title.to_string()));
}
let name = |id: NoteId| {
ids.iter()
.find(|(i, _)| *i == id)
.map(|(_, n)| n.as_str())
.unwrap_or("?")
};
println!("\n badu · cuaderno de notas — {} notas\n", store.len());
println!(" grafo de enlaces:");
for note in store.iter() {
let fwd: Vec<&str> = store.forward_links(note.id).into_iter().map(name).collect();
let back: Vec<&str> = store.backlinks(note.id).into_iter().map(name).collect();
println!(" «{}»", note.title);
println!(" enlaza a : {}", fmt_list(&fwd));
println!(" backlinks : {}", fmt_list(&back));
}
let orphans: Vec<&str> = store.orphans().iter().map(|n| n.title.as_str()).collect();
println!("\n notas huérfanas (sin backlinks): {}", fmt_list(&orphans));
let dangling_owned = store.dangling_links();
let dangling: Vec<&str> = dangling_owned.iter().map(|s| s.as_str()).collect();
println!(" enlaces colgantes (destino inexistente): {}", fmt_list(&dangling));
println!("\n gravedad semántica — clústeres (afinidad ≥ 0.85):");
for (n, cluster) in field.clusters(0.85).iter().enumerate() {
let titles: Vec<&str> = cluster.iter().map(|id| name(*id)).collect();
println!(" grupo {}: {}", n + 1, fmt_list(&titles));
}
let pivot = ids[1].0; // "Recetas de la abuela"
println!("\n vecinos más afines a «{}»:", name(pivot));
for (id, score) in field.nearest(pivot, 3) {
println!(" {:.3} {}", score, name(id));
}
let layout = field.gravity_layout(&GravityConfig::default());
println!("\n layout 2D por gravedad ({} posiciones):", layout.len());
for p in &layout {
println!(" ({:7.1}, {:7.1}) {}", p.x, p.y, name(p.id));
}
println!();
}
/// Formatea una lista de nombres, o `—` si está vacía.
fn fmt_list(items: &[&str]) -> String {
if items.is_empty() {
"".to_string()
} else {
items.join(", ")
}
}
@@ -6,16 +6,16 @@ license.workspace = true
description = "Probe GUI del broker brahman: conecta cada N segundos vía await_provider_blocking con un Card observer agnóstico, reporta 3 estados (down / up sin provider / up con provider)."
[dependencies]
brahman-broker = { path = "../../core/brahman-broker" }
brahman-card = { path = "../../core/brahman-card" }
brahman-handshake = { path = "../../core/brahman-handshake" }
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
brahman-broker = { path = "../../protocol/brahman-broker" }
brahman-card = { path = "../../protocol/brahman-card" }
brahman-handshake = { path = "../../protocol/brahman-handshake" }
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
ulid = { workspace = true }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
yahweh-widget-stat-card = { path = "../../modules/ui_engine/widgets/stat-card" }
yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" }
nahual-theme = { path = "../../modules/nahual/libs/theme" }
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
nahual-widget-banner = { path = "../../modules/nahual/widgets/banner" }
nahual-widget-stat-card = { path = "../../modules/nahual/widgets/stat-card" }
nahual-widget-app-header = { path = "../../modules/nahual/widgets/app-header" }
gpui = { workspace = true }
[[bin]]
@@ -38,11 +38,11 @@ use ulid::Ulid;
use gpui::{
div, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
};
use yahweh_launcher::launch_app;
use yahweh_theme::Theme;
use yahweh_widget_app_header::app_header;
use yahweh_widget_banner::{banner_themed, Banner};
use yahweh_widget_stat_card::stat_card;
use nahual_launcher::launch_app;
use nahual_theme::Theme;
use nahual_widget_app_header::app_header;
use nahual_widget_banner::{banner_themed, Banner};
use nahual_widget_stat_card::stat_card;
const POLL_INTERVAL: Duration = Duration::from_secs(5);
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
+4 -4
View File
@@ -6,10 +6,10 @@ license.workspace = true
description = "Demo binaries de brahman: broker standalone + producer/consumer dummy. Pensados para que `scripts/bootstrap-demo.sh` arranque un escenario reproducible donde los 5 explorers ven sesiones, matches, y timeline."
[dependencies]
brahman-broker = { path = "../../core/brahman-broker" }
brahman-card = { path = "../../core/brahman-card" }
brahman-handshake = { path = "../../core/brahman-handshake" }
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
brahman-broker = { path = "../../protocol/brahman-broker" }
brahman-card = { path = "../../protocol/brahman-card" }
brahman-handshake = { path = "../../protocol/brahman-handshake" }
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "charka"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "charka — CLI del transpilador COBOL→Rust: transpila, ejecuta y valida programas COBOL."
[[bin]]
name = "charka"
path = "src/main.rs"
[dependencies]
charka-lexer = { path = "../../modules/charka/charka-lexer" }
charka-parser = { path = "../../modules/charka/charka-parser" }
charka-ir = { path = "../../modules/charka/charka-ir" }
charka-codegen = { path = "../../modules/charka/charka-codegen" }
charka-shadow = { path = "../../modules/charka/charka-shadow" }
clap = { workspace = true }
anyhow = { workspace = true }
+317
View File
@@ -0,0 +1,317 @@
//! `charka` — la CLI del transpilador COBOL → Rust.
//!
//! Envuelve el pipeline (lexer → parser → IR → codegen) y el validador
//! en sombra en cuatro comandos:
//!
//! - `transpile` — emite el código Rust de un fuente COBOL.
//! - `scaffold` — genera un crate Rust completo y compilable.
//! - `run` — ejecuta el programa (intérprete sombra) y lo imprime.
//! - `check` — ejecuta y compara la salida contra un archivo dado.
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{Context, Result};
use charka_ir::{Ir, PerformTarget, Stmt};
use clap::{Parser, Subcommand};
/// Ruta a `charka-runtime`, fijada al compilar — el crate generado por
/// `scaffold` la usa como dependencia.
const RUNTIME_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../modules/charka/charka-runtime"
);
/// El transpilador de COBOL a Rust.
#[derive(Parser)]
#[command(name = "charka", version, about = "Transpilador COBOL → Rust")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Transpila un fuente COBOL a código Rust.
Transpile {
/// El fuente COBOL (.cob), en formato libre.
input: PathBuf,
/// Archivo de salida; si se omite, va a la salida estándar.
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Genera un crate Rust completo y compilable.
Scaffold {
/// El fuente COBOL (.cob).
input: PathBuf,
/// El directorio del crate a crear.
#[arg(short, long)]
output: PathBuf,
},
/// Ejecuta un programa COBOL (intérprete sombra) y muestra su salida.
Run {
/// El fuente COBOL (.cob).
input: PathBuf,
},
/// Ejecuta un programa y compara su salida con un archivo esperado.
Check {
/// El fuente COBOL (.cob).
input: PathBuf,
/// El archivo con la salida esperada.
#[arg(short, long)]
expect: PathBuf,
},
}
fn main() -> ExitCode {
match dispatch(Cli::parse().command) {
Ok(code) => code,
Err(err) => {
eprintln!("charka: {err:#}");
ExitCode::FAILURE
}
}
}
fn dispatch(command: Command) -> Result<ExitCode> {
match command {
Command::Transpile { input, output } => transpile(&input, output.as_deref()),
Command::Scaffold { input, output } => scaffold(&input, &output),
Command::Run { input } => run(&input),
Command::Check { input, expect } => check(&input, &expect),
}
}
// ── Comandos ──────────────────────────────────────────────────────
fn transpile(input: &Path, output: Option<&Path>) -> Result<ExitCode> {
let rust = charka_codegen::generate(&load_ir(input)?);
match output {
Some(path) => {
fs::write(path, rust)
.with_context(|| format!("no se pudo escribir {}", path.display()))?;
eprintln!("charka: escrito {}", path.display());
}
None => print!("{rust}"),
}
Ok(ExitCode::SUCCESS)
}
fn scaffold(input: &Path, output: &Path) -> Result<ExitCode> {
let ir = load_ir(input)?;
let rust = charka_codegen::generate(&ir);
let name = crate_name(input);
fs::create_dir_all(output.join("src"))
.with_context(|| format!("no se pudo crear {}", output.display()))?;
fs::write(output.join("src/main.rs"), rust)?;
fs::write(output.join("Cargo.toml"), cargo_toml(&name))?;
eprintln!("charka: crate «{name}» generado en {}", output.display());
eprintln!(
" cargo run --manifest-path {}",
output.join("Cargo.toml").display()
);
warn_unknowns(&ir);
Ok(ExitCode::SUCCESS)
}
fn run(input: &Path) -> Result<ExitCode> {
let ir = load_ir(input)?;
let outcome = charka_shadow::interpret(&ir);
for line in &outcome.lines {
println!("{line}");
}
warn_unknowns(&ir);
if outcome.halt == charka_shadow::Halt::StepLimit {
eprintln!("charka: aviso — se agotó el tope de pasos (¿un bucle sin fin?)");
return Ok(ExitCode::FAILURE);
}
Ok(ExitCode::SUCCESS)
}
fn check(input: &Path, expect: &Path) -> Result<ExitCode> {
let ir = load_ir(input)?;
let outcome = charka_shadow::interpret(&ir);
let expected = fs::read_to_string(expect)
.with_context(|| format!("no se pudo leer {}", expect.display()))?;
let got: Vec<&str> = outcome.lines.iter().map(|l| l.trim_end()).collect();
let want: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect();
if got == want {
println!("charka: OK — {} líneas coinciden", got.len());
Ok(ExitCode::SUCCESS)
} else {
eprintln!("charka: FALLA — la salida difiere de {}", expect.display());
report_diff(&got, &want);
Ok(ExitCode::FAILURE)
}
}
// ── Apoyo ─────────────────────────────────────────────────────────
/// Lee un fuente COBOL y lo lleva hasta el IR.
fn load_ir(input: &Path) -> Result<Ir> {
let source = fs::read_to_string(input)
.with_context(|| format!("no se pudo leer {}", input.display()))?;
let tokens =
charka_lexer::lex(&source, charka_lexer::SourceFormat::Free).context("error de léxico")?;
let program = charka_parser::parse(&tokens).context("error de parseo")?;
Ok(charka_ir::lower(&program))
}
/// El `Cargo.toml` de un crate generado por `scaffold`.
fn cargo_toml(name: &str) -> String {
format!(
"[package]\n\
name = \"{name}\"\n\
version = \"0.1.0\"\n\
edition = \"2021\"\n\
\n\
[[bin]]\n\
name = \"{name}\"\n\
path = \"src/main.rs\"\n\
\n\
[dependencies]\n\
charka-runtime = {{ path = \"{RUNTIME_PATH}\" }}\n\
\n\
[workspace]\n"
)
}
/// Un nombre de crate válido derivado del nombre del archivo fuente.
fn crate_name(input: &Path) -> String {
let stem = input
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("programa");
let mut name: String = stem
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'_'
}
})
.collect();
if name.is_empty() || name.starts_with(|c: char| c.is_ascii_digit()) {
name = format!("cobol_{name}");
}
name
}
/// Avisa de los verbos COBOL que el transpilador no soporta todavía.
fn warn_unknowns(ir: &Ir) {
let mut verbs = Vec::new();
for proc in &ir.procedures {
collect_unknowns(&proc.body, &mut verbs);
}
if verbs.is_empty() {
return;
}
verbs.sort();
verbs.dedup();
eprintln!(
"charka: aviso — verbos no transpilados (se omitieron): {}",
verbs.join(", ")
);
}
/// Recoge los verbos de los `Stmt::Unknown`, incluso los anidados.
fn collect_unknowns(stmts: &[Stmt], out: &mut Vec<String>) {
for s in stmts {
match s {
Stmt::Unknown { verb, .. } => out.push(verb.clone()),
Stmt::If {
then_branch,
else_branch,
..
} => {
collect_unknowns(then_branch, out);
collect_unknowns(else_branch, out);
}
Stmt::Evaluate { whens, other, .. } => {
for w in whens {
collect_unknowns(&w.body, out);
}
collect_unknowns(other, out);
}
Stmt::Read {
at_end, not_at_end, ..
} => {
collect_unknowns(at_end, out);
collect_unknowns(not_at_end, out);
}
Stmt::Perform(p) => {
if let PerformTarget::Inline(body) = &p.target {
collect_unknowns(body, out);
}
}
_ => {}
}
}
}
/// Imprime las líneas en que la salida obtenida difiere de la esperada.
fn report_diff(got: &[&str], want: &[&str]) {
for i in 0..got.len().max(want.len()) {
let g = got.get(i).copied().unwrap_or("<falta>");
let w = want.get(i).copied().unwrap_or("<falta>");
if g != w {
eprintln!(" línea {}:", i + 1);
eprintln!(" obtenido: {g}");
eprintln!(" esperado: {w}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ir_of(src: &str) -> Ir {
let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).unwrap();
charka_ir::lower(&charka_parser::parse(&toks).unwrap())
}
#[test]
fn crate_name_is_sanitized() {
assert_eq!(crate_name(Path::new("/x/06-nomina.cob")), "cobol_06_nomina");
assert_eq!(crate_name(Path::new("PAYROLL.CBL")), "payroll");
}
#[test]
fn cargo_toml_names_the_crate_and_the_runtime() {
let toml = cargo_toml("demo");
assert!(toml.contains("name = \"demo\""));
assert!(toml.contains("charka-runtime"));
assert!(toml.contains("[workspace]"));
}
#[test]
fn unknown_verbs_are_collected() {
let ir = ir_of(
"PROCEDURE DIVISION.\n\
MAIN.\n\
CALL 'SUBPROG'.\n",
);
let mut verbs = Vec::new();
for proc in &ir.procedures {
collect_unknowns(&proc.body, &mut verbs);
}
assert_eq!(verbs, vec!["CALL".to_string()]);
}
#[test]
fn known_program_has_no_unknowns() {
let ir = ir_of("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'OK'.\n STOP RUN.\n");
let mut verbs = Vec::new();
for proc in &ir.procedures {
collect_unknowns(&proc.body, &mut verbs);
}
assert!(verbs.is_empty());
}
}
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "chasqui-explorer"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Explorador GPUI de Mónadas: panel que descubre al daemon nouser vía broker brahman y consulta sus Mónadas dinámicamente."
[dependencies]
brahman-card = { path = "../../protocol/brahman-card" }
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
chasqui-card = { path = "../../modules/chasqui/card" }
nahual-theme = { path = "../../modules/nahual/libs/theme" }
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
nahual-widget-banner = { path = "../../modules/nahual/widgets/banner" }
nahual-widget-card = { path = "../../modules/nahual/widgets/card" }
nahual-widget-app-header = { path = "../../modules/nahual/widgets/app-header" }
gpui = { workspace = true }
[[bin]]
name = "chasqui-explorer"
path = "src/main.rs"
@@ -1,21 +1,21 @@
//! `nouser-explorer` — panel GPUI que descubre al daemon `nouser`
//! `chasqui-explorer` — panel GPUI que descubre al daemon `chasqui`
//! vía broker brahman y muestra sus Mónadas en vivo.
//!
//! Diseño: ventana standalone que cada N segundos consulta el query
//! socket del daemon (`nouser_core::engine_socket::client::list_monads`).
//! socket del daemon (`chasqui_core::engine_socket::client::list_monads`).
//! El path del socket NO está hardcoded — se descubre vía
//! `brahman_sidecar::await_provider_blocking` para el flow
//! `monad-list:json`. Si el daemon cae, el socket cacheado se invalida
//! y la próxima iteración re-descubre.
//!
//! Sin integración con yahweh-shell — es su propio binario para que el
//! Sin integración con nahual-shell — es su propio binario para que el
//! ecosistema sea visible incluso sin la shell completa.
//!
//! Uso:
//! ```sh
//! cargo run -p nouser-explorer
//! cargo run -p chasqui-explorer
//! # con override del init socket (heredado de brahman-handshake):
//! BRAHMAN_INIT_SOCKET=/tmp/init.sock cargo run -p nouser-explorer
//! BRAHMAN_INIT_SOCKET=/tmp/init.sock cargo run -p chasqui-explorer
//! ```
use std::path::PathBuf;
@@ -25,14 +25,14 @@ use brahman_sidecar::{await_provider_blocking, build_consumer_card, ConsumerErro
use gpui::{
div, prelude::*, px, rgb, Context, IntoElement, Render, SharedString, Window,
};
use nouser_card::query::client as query_client;
use nouser_card::query::{transport, ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME};
use nouser_card::Lens;
use yahweh_launcher::launch_app;
use yahweh_theme::Theme;
use yahweh_widget_app_header::app_header;
use yahweh_widget_banner::{banner_themed, Banner};
use yahweh_widget_card::card_themed;
use chasqui_card::query::client as query_client;
use chasqui_card::query::{transport, ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME};
use chasqui_card::Lens;
use nahual_launcher::launch_app;
use nahual_theme::Theme;
use nahual_widget_app_header::app_header;
use nahual_widget_banner::{banner_themed, Banner};
use nahual_widget_card::card_themed;
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
@@ -176,7 +176,7 @@ fn resolve_socket() -> Result<(PathBuf, &'static str), String> {
/// Card con `flow.input = monad-list:json`, espera al primer
/// `MatchEvent::Available`, devuelve el `producer_service_socket`.
fn discover_via_broker() -> Result<PathBuf, ConsumerError> {
let card = build_consumer_card("nouser-explorer", FLOW_MONAD_LIST, FLOW_TYPE_NAME);
let card = build_consumer_card("chasqui-explorer", FLOW_MONAD_LIST, FLOW_TYPE_NAME);
await_provider_blocking(card, DISCOVERY_TIMEOUT)
}
@@ -184,7 +184,7 @@ impl Render for Explorer {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Chrome viene del Theme global; los acentos por kind
// (engine cyan, data purple) son señales semánticas del
// dominio nouser y se mantienen locales.
// dominio chasqui y se mantienen locales.
let theme = Theme::global(cx).clone();
let bg = theme.bg_app.clone();
let text = theme.fg_text;
@@ -205,7 +205,7 @@ impl Render for Explorer {
.map(|w| format!(" · watching: {}", w))
.unwrap_or_default()
),
_ => "Buscando daemon nouser vía brahman-broker…".to_string(),
_ => "Buscando daemon chasqui vía brahman-broker…".to_string(),
};
// Header standard via widget compartido.
@@ -0,0 +1,27 @@
[package]
name = "cosmobiologia-server"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "Cosmobiología — server HTTP single-user. axum + cosmobiologia-engine. Sirve cartas y assets del cliente web. CRUD completo de groups/contacts/charts."
[dependencies]
cosmobiologia-engine = { path = "../../modules/cosmobiologia/cosmobiologia-engine" }
cosmobiologia-model = { path = "../../modules/cosmobiologia/cosmobiologia-model" }
cosmobiologia-render = { path = "../../modules/cosmobiologia/cosmobiologia-render" }
cosmobiologia-store = { path = "../../modules/cosmobiologia/cosmobiologia-store" }
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
directories = { workspace = true }
clap = { version = "4", features = ["derive"] }
axum = "0.7"
tower-http = { version = "0.6", features = ["cors", "trace", "fs"] }
[[bin]]
name = "cosmobiologia-server"
path = "src/main.rs"
+298
View File
@@ -0,0 +1,298 @@
# Cosmobiología — guía de deploy
Server HTTP single-user, escrito en Rust + axum. Sirve cartas
astrológicas computadas con `cosmobiologia-engine` (VSOP2013 en Rust
puro) y la página web HTML/JS del cliente. Diseñado para correr
**local** o detrás de un reverse proxy con TLS.
---
## 1. Build
### Binario del server
```bash
cargo build --release -p cosmobiologia-server
# ./target/release/cosmobiologia-server
```
### Cliente WASM (opcional pero recomendado)
Sin esto, el cliente cae al **SSR**: cada interacción pide al server
el SVG recompuesto (~12 KB por click). Con WASM, el cliente compone
localmente — primera carga ~150 KB, después scrubbing instantáneo
sin round-trip.
```bash
# Una sola vez:
cargo install wasm-pack
# Cada vez que cambie cosmobiologia-render o cosmobiologia-web:
cd crates/modules/cosmobiologia/cosmobiologia-web
wasm-pack build --release --target web \
--out-dir ../../../../apps/cosmobiologia-server/static/wasm
```
`wasm-pack` produce `cosmobiologia_web.js` +
`cosmobiologia_web_bg.wasm` en
`crates/apps/cosmobiologia-server/static/wasm/`. El server los sirve
en `/static/wasm/*` y el `index.html` los importa con
`import init, { render_model_to_svg } from
'/static/wasm/cosmobiologia_web.js'`.
Si el directorio NO existe (build incompleto), el server devuelve
404 y el cliente cae al SSR automáticamente — sin error visible.
---
## 2. Levantar el server
### Local (single-user, sin reverse proxy)
```bash
./target/release/cosmobiologia-server \
--port 8787 \
--bind 127.0.0.1 \
--db ~/.local/share/cosmobiologia/charts.db
```
Abrí `http://127.0.0.1:8787/`. La DB es la misma que usa la app
desktop — cualquier carta creada en la app aparece en el browser
y viceversa.
### systemd (server público vía VPS)
```ini
# /etc/systemd/system/cosmobiologia.service
[Unit]
Description=Cosmobiología (server astrológico)
After=network.target
[Service]
Type=simple
User=cosmobio
Group=cosmobio
WorkingDirectory=/opt/cosmobiologia
ExecStart=/opt/cosmobiologia/cosmobiologia-server \
--port 8787 \
--bind 127.0.0.1 \
--db /var/lib/cosmobiologia/charts.db \
--static-wasm /opt/cosmobiologia/static/wasm
Environment=RUST_LOG=cosmobiologia_server=info,tower_http=warn
Restart=on-failure
RestartSec=3
# Sandboxing básico
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/cosmobiologia
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
```
```bash
sudo useradd -r -s /usr/sbin/nologin cosmobio
sudo mkdir -p /opt/cosmobiologia/static/wasm /var/lib/cosmobiologia
sudo cp target/release/cosmobiologia-server /opt/cosmobiologia/
sudo cp -r crates/apps/cosmobiologia-server/static/wasm/* \
/opt/cosmobiologia/static/wasm/
sudo chown -R cosmobio:cosmobio /opt/cosmobiologia /var/lib/cosmobiologia
sudo systemctl daemon-reload
sudo systemctl enable --now cosmobiologia
sudo systemctl status cosmobiologia
```
---
## 3. Reverse proxy (HTTPS + DNS bonito)
Con dos subdominios apuntando al host:
| DNS | Función |
|-----|---------|
| `cosmobiologia.gioser.net` | página web (HTML + WASM) |
| `api.cosmobiologia.gioser.net` | endpoints `/api/*` (JSON / SVG) |
Hoy el server sirve los dos roles en el mismo puerto — el split por
subdominio lo hace el proxy, **sin cambiar nada del Rust**.
### Caddyfile (recomendado — TLS automático con Let's Encrypt)
```Caddyfile
cosmobiologia.gioser.net {
encode gzip zstd
# Página web + estáticos + WASM
@api path /api/*
handle @api {
# Si el cliente pega un /api/ directo al subdominio principal,
# lo dejamos pasar (más amigable que 404).
reverse_proxy 127.0.0.1:8787
}
handle {
reverse_proxy 127.0.0.1:8787
}
}
api.cosmobiologia.gioser.net {
encode gzip zstd
# Solo los endpoints /api/*; rechaza el resto.
@api path /api/*
handle @api {
reverse_proxy 127.0.0.1:8787
}
handle {
respond "Use cosmobiologia.gioser.net para la página" 404
}
}
```
### nginx (alternativa)
```nginx
# /etc/nginx/sites-available/cosmobiologia
server {
server_name cosmobiologia.gioser.net;
listen 443 ssl http2;
# ssl_certificate / ssl_certificate_key — vía certbot
location / {
proxy_pass http://127.0.0.1:8787;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
gzip on;
gzip_types application/javascript application/wasm image/svg+xml application/json text/css text/html;
}
server {
server_name api.cosmobiologia.gioser.net;
listen 443 ssl http2;
location /api/ {
proxy_pass http://127.0.0.1:8787;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / { return 404; }
gzip on;
gzip_types application/json image/svg+xml;
}
```
### DNS
A records (o AAAA si IPv6) hacia tu VPS:
```
cosmobiologia.gioser.net. A <ip-del-VPS>
api.cosmobiologia.gioser.net. A <ip-del-VPS>
```
---
## 4. CORS y separación cliente↔API
Hoy el server tiene `CorsLayer::permissive()` — cualquier origen
puede hacer fetch contra `/api/*`. Eso es OK para:
- **Single-user local**: nadie más alcanza al server.
- **Demo público single-tenant**: misma DB para todos los visitantes,
sin datos sensibles. Los visitantes pueden leer y crear cartas
públicamente (es la naturaleza del demo).
**No use CorsLayer::permissive en producción multi-usuario**. Para
eso hay que:
1. Agregar auth (sesiones / JWT / API key).
2. Reemplazar con `CorsLayer::new().allow_origin(["https://cosmobiologia.gioser.net".parse().unwrap()])`.
3. Volverte el `AllowCredentials::yes()` si vas a usar cookies.
---
## 5. Separación demo público ↔ desktop personal
El path por default de la DB (`~/.local/share/cosmobiologia/charts.db`)
es **compartido entre el server y la app desktop**. Eso es lo que
querés en tu máquina local — abrís el browser y ves las mismas
cartas que tenés en la app gpui.
**Pero NO querés que el server público en
`cosmobiologia.gioser.net` exponga TUS cartas privadas**. Para el
demo público:
```bash
# En tu VPS:
mkdir -p /var/lib/cosmobiologia
# Empezás con DB vacía (la app crea las tablas al primer arranque).
cosmobiologia-server --db /var/lib/cosmobiologia/charts.db
```
Si querés precargar cartas demo (Einstein, una carta natal pública),
podés copiarlas desde tu DB local con la app, exportarlas como JSON
via `/api/charts/:id`, y postearlas al server público con POST
`/api/charts`. O simplemente abrir el browser, ir a "Nuevo
contacto" → "Nueva carta…" y cargarlas a mano.
---
## 6. Backup
La DB SQLite es **un solo archivo**. Backup = `cp` (mientras el
server está parado, o usá `sqlite3 charts.db ".backup
charts.bak"` con el server corriendo).
```bash
# Snapshot diario sin parar el server
sqlite3 /var/lib/cosmobiologia/charts.db ".backup /var/backups/cosmobiologia-$(date +%F).db"
```
---
## 7. Smoke test post-deploy
```bash
# Desde tu máquina:
curl https://cosmobiologia.gioser.net/api/health
# → {"status":"ok","service":"cosmobiologia-server"}
curl https://cosmobiologia.gioser.net/api/sky | jq .title
# → "Cielo 2026-05-19 00:55 UTC"
# Abrí la página:
open https://cosmobiologia.gioser.net/
# (deberías ver la rueda del cielo + sidebar con "Cielo ahora")
```
Si el cliente WASM cargó, en la barra inferior verás "WASM".
Si cayó al SSR, verás "SSR". Ambos modos son funcionales.
---
## 8. Endpoints públicos (referencia)
| Método | Path | Función |
|--------|------|---------|
| GET | `/api/health` | healthcheck |
| GET | `/api/tree` | árbol completo (groups/contacts/charts) |
| GET | `/api/sky` | RenderModel "Cielo ahora" |
| GET | `/api/sky.svg` | SVG agnóstico del cielo (server-side) |
| GET | `/api/charts/:id` | Chart JSON |
| GET | `/api/charts/:id/render?...` | RenderModel con overlays |
| GET | `/api/charts/:id/svg?...` | SVG vía engine (svg_export) |
| GET | `/api/charts/:id/wheel.svg?...` | SVG vía render agnóstico |
| POST | `/api/charts` | crear carta |
| PATCH | `/api/charts/:id` | editar label/birth/config |
| DELETE | `/api/charts/:id` | borrar |
| 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 |
Query params del render (`?...`):
- `offset_min=<i64>` — time scrubbing (minutos desde el natal).
- `transit=1` — activa overlay de tránsito al `now` del server.
- `prog_age=<f64>` — progresión secundaria a edad N.
- `sa_age=<f64>` — solar arc a edad N.
- `pd_age=<f64>` — primary directions GR (Naibod).
@@ -0,0 +1,603 @@
//! Cosmobiología — server HTTP single-user.
//!
//! - Reusa `cosmobiologia-engine` (VSOP2013 + LRU cache) nativo.
//! - Comparte (por default) la misma `charts.db` SQLite que la app
//! desktop, vía `directories::ProjectDirs::from("net", "gioser",
//! "cosmobiologia")`. La idea es: levantar `cosmobiologia-server`
//! en localhost y abrir el wheel desde el browser cuando no se está
//! con la app desktop.
//! - Single-user, sin auth, bind a `127.0.0.1` por default. NO debe
//! exponerse a la red pública sin agregar auth + HTTPS.
//!
//! ## Endpoints (v1)
//!
//! ```text
//! GET /api/health healthcheck
//! GET /api/tree tree completo (groups + contacts + charts)
//! 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 (contact_id + birth_data)
//! GET /api/charts/:id chart JSON
//! PATCH /api/charts/:id renombrar / editar birth_data
//! DELETE /api/charts/:id borrar
//! GET /api/charts/:id/render RenderModel JSON (overlays via query)
//! GET /api/charts/:id/svg SVG inline
//! GET /api/sky "Cielo ahora" — RenderModel UTC actual
//! ```
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, patch, post};
use axum::{Json, Router};
use clap::Parser;
use cosmobiologia_engine::{
compose_with_options, svg_export, EngineError, NatalOptions, PipelineRequest, RenderModel,
};
use cosmobiologia_render::{compose_wheel, draw_commands_to_svg, CompositionOpts};
use cosmobiologia_model::{
Chart, ChartId, ChartKind, Contact, ContactId, Group, GroupId, StoredBirthData,
StoredChartConfig,
};
use cosmobiologia_store::Store;
use serde::{Deserialize, Serialize};
use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
use tracing::info;
#[derive(Parser, Debug)]
#[command(
name = "cosmobiologia-server",
about = "Servidor HTTP single-user de Cosmobiología."
)]
struct Cli {
/// Puerto donde escuchar. Default 8787.
#[arg(long, default_value = "8787")]
port: u16,
/// IP a bindear. Default `127.0.0.1` (solo localhost — single-user
/// sin auth).
#[arg(long, default_value = "127.0.0.1")]
bind: String,
/// Path al archivo SQLite. Default = el mismo de la app desktop
/// (`$XDG_DATA_HOME/cosmobiologia/charts.db`).
#[arg(long)]
db: Option<PathBuf>,
/// Directorio con los assets estáticos del cliente WASM
/// (output de `wasm-pack build --out-dir <este path>`). Si el
/// directorio no existe, el endpoint `/static/wasm/*` devuelve
/// 404 y el cliente cae al SSR.
#[arg(long, default_value = "crates/apps/cosmobiologia-server/static/wasm")]
static_wasm: PathBuf,
}
#[derive(Clone)]
struct AppState {
store: Arc<Store>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "cosmobiologia_server=info,tower_http=info".into()),
)
.init();
let cli = Cli::parse();
let db_path = match cli.db {
Some(p) => p,
None => default_db_path()?,
};
info!("DB: {}", db_path.display());
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent).ok();
}
let store = Arc::new(Store::open(&db_path)?);
let state = AppState { store };
let app = router()
.nest_service("/static/wasm", ServeDir::new(&cli.static_wasm))
.with_state(state);
let addr: SocketAddr = format!("{}:{}", cli.bind, cli.port).parse()?;
info!("listening on http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
fn default_db_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
let dirs = directories::ProjectDirs::from("net", "gioser", "cosmobiologia")
.ok_or("no se pudo determinar XDG data dir")?;
Ok(dirs.data_dir().join("charts.db"))
}
fn router() -> Router<AppState> {
Router::new()
.route("/", get(get_index))
.route("/api/health", get(health))
.route("/api/tree", get(get_tree))
.route("/api/sky", get(get_sky))
// El render SVG agnóstico (via `cosmobiologia-render::compose_wheel`
// + `draw_commands_to_svg`) sirve a la fase 3 inicial: el
// cliente recibe SVG ya compuesto, sin necesidad de WASM.
// Cuando agreguemos el cliente WASM real, este endpoint se
// mantiene como fallback "ver SVG sin JS".
.route("/api/sky.svg", get(get_sky_svg))
.route("/api/charts/:id/wheel.svg", get(get_chart_wheel_svg))
.route("/api/groups", post(post_group))
.route("/api/groups/:id", patch(patch_group).delete(delete_group))
.route("/api/contacts", post(post_contact))
.route(
"/api/contacts/:id",
patch(patch_contact).delete(delete_contact),
)
.route("/api/charts", post(post_chart))
.route(
"/api/charts/:id",
get(get_chart).patch(patch_chart).delete(delete_chart),
)
.route("/api/charts/:id/render", get(get_chart_render))
.route("/api/charts/:id/svg", get(get_chart_svg))
.layer(CorsLayer::permissive()) // single-user, localhost: cors abierto
.layer(TraceLayer::new_for_http())
}
// =====================================================================
// Página HTML inicial
// =====================================================================
const INDEX_HTML: &str = include_str!("../static/index.html");
async fn get_index() -> Response {
(
[(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
INDEX_HTML.to_string(),
)
.into_response()
}
// SVG render agnóstico (no es el del engine — este viene de
// `cosmobiologia-render::compose_wheel` que es lo que mañana el
// cliente WASM también va a usar). Útil para demos sin WASM.
async fn get_sky_svg() -> Result<Response, ApiError> {
let chart = build_present_sky_chart();
let model = compose_with_options(&chart, 0, &[], &NatalOptions::default())?;
let cmds = compose_wheel(&model, &CompositionOpts::default());
let svg = draw_commands_to_svg(&cmds, 600.0);
Ok((
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
svg,
)
.into_response())
}
async fn get_chart_wheel_svg(
State(s): State<AppState>,
Path(id): Path<ChartId>,
Query(q): Query<RenderQuery>,
) -> Result<Response, ApiError> {
let chart = s
.store
.get_chart(id)
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
let model =
compose_with_options(&chart, q.offset_min, &build_requests(&q), &NatalOptions::default())?;
let cmds = compose_wheel(&model, &CompositionOpts::default());
let svg = draw_commands_to_svg(&cmds, 600.0);
Ok((
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
svg,
)
.into_response())
}
// =====================================================================
// Error
// =====================================================================
#[derive(thiserror::Error, Debug)]
enum ApiError {
#[error("not found: {0}")]
NotFound(String),
#[error("bad request: {0}")]
BadRequest(String),
#[error("store: {0}")]
Store(#[from] cosmobiologia_store::StoreError),
#[error("engine: {0}")]
Engine(#[from] EngineError),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (code, msg) = match &self {
ApiError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
ApiError::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
};
(code, Json(serde_json::json!({ "error": msg }))).into_response()
}
}
type ApiResult<T> = Result<Json<T>, ApiError>;
// =====================================================================
// Health
// =====================================================================
async fn health() -> Json<serde_json::Value> {
Json(serde_json::json!({ "status": "ok", "service": "cosmobiologia-server" }))
}
// =====================================================================
// Tree — listado completo
// =====================================================================
#[derive(Serialize)]
struct TreeNode {
id: String,
label: String,
kind: &'static str, // "group" | "contact" | "chart"
children: Vec<TreeNode>,
}
async fn get_tree(State(s): State<AppState>) -> ApiResult<Vec<TreeNode>> {
let mut roots = Vec::new();
// Grupos top-level
for g in s.store.list_groups(None)? {
roots.push(group_node(&s.store, &g)?);
}
// Contactos sin grupo (van bajo "General" en el tree desktop;
// acá los listamos directo al root para no confundir al cliente).
for c in s.store.list_contacts(None)? {
roots.push(contact_node(&s.store, &c)?);
}
Ok(Json(roots))
}
fn group_node(store: &Store, g: &Group) -> Result<TreeNode, ApiError> {
let mut children = Vec::new();
for sub in store.list_groups(Some(g.id))? {
children.push(group_node(store, &sub)?);
}
for c in store.list_contacts(Some(g.id))? {
children.push(contact_node(store, &c)?);
}
Ok(TreeNode {
id: format!("g:{}", g.id),
label: g.name.clone(),
kind: "group",
children,
})
}
fn contact_node(store: &Store, c: &Contact) -> Result<TreeNode, ApiError> {
let charts = store.list_charts(c.id).unwrap_or_default();
let children: Vec<TreeNode> = charts
.into_iter()
.map(|h| TreeNode {
id: format!("h:{}", h.id),
label: h.label,
kind: "chart",
children: Vec::new(),
})
.collect();
Ok(TreeNode {
id: format!("c:{}", c.id),
label: c.name.clone(),
kind: "contact",
children,
})
}
// =====================================================================
// Groups CRUD
// =====================================================================
#[derive(Deserialize)]
struct CreateGroupBody {
name: String,
parent: Option<GroupId>,
}
async fn post_group(
State(s): State<AppState>,
Json(b): Json<CreateGroupBody>,
) -> ApiResult<Group> {
let g = s.store.create_group(b.parent, &b.name, None)?;
Ok(Json(g))
}
#[derive(Deserialize)]
struct PatchGroupBody {
name: String,
}
async fn patch_group(
State(s): State<AppState>,
Path(id): Path<GroupId>,
Json(b): Json<PatchGroupBody>,
) -> ApiResult<serde_json::Value> {
s.store.rename_group(id, &b.name)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
async fn delete_group(
State(s): State<AppState>,
Path(id): Path<GroupId>,
) -> ApiResult<serde_json::Value> {
s.store.delete_group(id)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
// =====================================================================
// Contacts CRUD
// =====================================================================
#[derive(Deserialize)]
struct CreateContactBody {
name: String,
group: Option<GroupId>,
}
async fn post_contact(
State(s): State<AppState>,
Json(b): Json<CreateContactBody>,
) -> ApiResult<Contact> {
let c = s.store.create_contact(b.group, &b.name, None)?;
Ok(Json(c))
}
#[derive(Deserialize)]
struct PatchContactBody {
name: String,
}
async fn patch_contact(
State(s): State<AppState>,
Path(id): Path<ContactId>,
Json(b): Json<PatchContactBody>,
) -> ApiResult<serde_json::Value> {
s.store.rename_contact(id, &b.name)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
async fn delete_contact(
State(s): State<AppState>,
Path(id): Path<ContactId>,
) -> ApiResult<serde_json::Value> {
s.store.delete_contact(id)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
// =====================================================================
// Charts CRUD
// =====================================================================
#[derive(Deserialize)]
struct CreateChartBody {
contact_id: ContactId,
#[serde(default)]
kind: Option<ChartKind>,
label: String,
birth_data: StoredBirthData,
#[serde(default)]
config: Option<StoredChartConfig>,
}
async fn post_chart(
State(s): State<AppState>,
Json(b): Json<CreateChartBody>,
) -> ApiResult<Chart> {
let kind = b.kind.unwrap_or(ChartKind::Natal);
let cfg = b.config.unwrap_or_default();
let chart = s
.store
.create_chart(b.contact_id, kind, &b.label, &b.birth_data, &cfg, None)?;
Ok(Json(chart))
}
async fn get_chart(
State(s): State<AppState>,
Path(id): Path<ChartId>,
) -> ApiResult<Chart> {
let chart = s
.store
.get_chart(id)
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
Ok(Json(chart))
}
#[derive(Deserialize)]
struct PatchChartBody {
#[serde(default)]
label: Option<String>,
#[serde(default)]
birth_data: Option<StoredBirthData>,
#[serde(default)]
config: Option<StoredChartConfig>,
}
async fn patch_chart(
State(s): State<AppState>,
Path(id): Path<ChartId>,
Json(b): Json<PatchChartBody>,
) -> ApiResult<serde_json::Value> {
let current = s
.store
.get_chart(id)
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
let label = b.label.unwrap_or(current.label);
let birth = b.birth_data.unwrap_or(current.birth_data);
let cfg = b.config.unwrap_or(current.config);
s.store.update_chart(id, &label, &birth, &cfg)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
async fn delete_chart(
State(s): State<AppState>,
Path(id): Path<ChartId>,
) -> ApiResult<serde_json::Value> {
s.store.delete_chart(id)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
// =====================================================================
// Render
// =====================================================================
#[derive(Deserialize, Default)]
struct RenderQuery {
/// Offset de tiempo en minutos (para "scrubbing").
#[serde(default)]
offset_min: i64,
/// "1" = activar overlay de tránsitos al `now` del server.
#[serde(default)]
transit: u8,
/// Edad (años) — activa progresión secundaria si se setea.
#[serde(default)]
prog_age: Option<f64>,
/// Edad (años) — activa solar arc si se setea.
#[serde(default)]
sa_age: Option<f64>,
/// Edad (años) — activa primary directions si se setea.
#[serde(default)]
pd_age: Option<f64>,
}
fn build_requests(q: &RenderQuery) -> Vec<PipelineRequest> {
let mut r = Vec::new();
if q.transit == 1 {
r.push(PipelineRequest::Transit);
}
if let Some(a) = q.prog_age {
r.push(PipelineRequest::SecondaryProgression { target_age_years: a });
}
if let Some(a) = q.sa_age {
r.push(PipelineRequest::SolarArc { target_age_years: a });
}
if let Some(a) = q.pd_age {
r.push(PipelineRequest::PrimaryDirections {
target_age_years: a,
key: "naibod".into(),
});
}
r
}
async fn get_chart_render(
State(s): State<AppState>,
Path(id): Path<ChartId>,
Query(q): Query<RenderQuery>,
) -> ApiResult<RenderModel> {
let chart = s
.store
.get_chart(id)
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
let model =
compose_with_options(&chart, q.offset_min, &build_requests(&q), &NatalOptions::default())?;
Ok(Json(model))
}
async fn get_chart_svg(
State(s): State<AppState>,
Path(id): Path<ChartId>,
Query(q): Query<RenderQuery>,
) -> Result<Response, ApiError> {
let chart = s
.store
.get_chart(id)
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
let model =
compose_with_options(&chart, q.offset_min, &build_requests(&q), &NatalOptions::default())?;
let svg = svg_export::render_to_svg(&model);
Ok((
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
svg,
)
.into_response())
}
// =====================================================================
// Sky now — sin chart
// =====================================================================
async fn get_sky() -> ApiResult<RenderModel> {
let chart = build_present_sky_chart();
let model = compose_with_options(&chart, 0, &[], &NatalOptions::default())?;
Ok(Json(model))
}
fn build_present_sky_chart() -> Chart {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let (year, month, day, hour, minute, second) = unix_to_civil_utc(secs);
let birth = StoredBirthData {
year,
month,
day,
hour,
minute,
second: second as f64,
tz_offset_minutes: 0,
latitude_deg: 51.4769, // Greenwich
longitude_deg: 0.0,
altitude_m: 47.0,
time_certainty: Default::default(),
subject_name: Some("Cielo".into()),
birthplace_label: Some("Greenwich (UTC)".into()),
};
Chart {
id: ChartId::default(),
contact_id: ContactId::default(),
kind: ChartKind::Natal,
label: format!(
"Cielo {:04}-{:02}-{:02} {:02}:{:02} UTC",
year, month, day, hour, minute
),
birth_data: birth,
config: StoredChartConfig::default(),
related_chart_id: None,
created_at_ms: 0,
}
}
/// Howard Hinnant `days_to_civil` — Unix UTC → calendario.
/// Mismo algoritmo que en la app desktop; duplicado mínimo para no
/// arrastrar el shell entero como dep del server.
fn unix_to_civil_utc(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
let day_seconds: i64 = 86_400;
let z = secs.div_euclid(day_seconds);
let s = secs.rem_euclid(day_seconds);
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u32;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if month <= 2 { (y + 1) as i32 } else { y as i32 };
let hour = (s / 3600) as u32;
let minute = ((s % 3600) / 60) as u32;
let second = (s % 60) as u32;
(year, month, day, hour, minute, second)
}
@@ -0,0 +1,276 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cosmobiología</title>
<style>
:root {
--bg: #0f1115;
--bg-panel: #171a21;
--fg: #e8e6df;
--fg-muted: #9aa0a8;
--accent: #c79a4d;
--border: #2a2e38;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font-family: -apple-system, system-ui, sans-serif;
display: flex;
min-height: 100vh;
}
aside {
width: 280px;
background: var(--bg-panel);
border-right: 1px solid var(--border);
padding: 16px;
overflow-y: auto;
}
main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
}
h1 {
margin: 0 0 4px;
font-weight: 500;
letter-spacing: .02em;
}
h2 {
margin: 24px 0 8px;
font-size: 12px;
color: var(--fg-muted);
text-transform: uppercase;
letter-spacing: .08em;
}
.tree-node {
cursor: pointer;
padding: 4px 6px;
border-radius: 4px;
font-size: 13px;
}
.tree-node:hover { background: rgba(255,255,255,.05); }
.tree-node.active { background: rgba(199,154,77,.15); color: var(--accent); }
.tree-node .icon { margin-right: 6px; opacity: .7; }
.indent-1 { padding-left: 22px; }
.indent-2 { padding-left: 38px; }
#wheel-container {
width: 600px;
height: 600px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
#info {
margin-top: 12px;
font-size: 12px;
color: var(--fg-muted);
}
#info b { color: var(--fg); font-weight: 500; }
.toolbar {
width: 600px;
display: flex;
gap: 8px;
margin-bottom: 12px;
align-items: center;
}
.toolbar button {
background: var(--bg-panel);
color: var(--fg);
border: 1px solid var(--border);
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.toolbar button:hover { border-color: var(--accent); color: var(--accent); }
.toolbar label { font-size: 12px; color: var(--fg-muted); }
.toolbar input { width: 60px; padding: 4px; border: 1px solid var(--border);
background: var(--bg); color: var(--fg); border-radius: 4px; font-size: 12px; }
</style>
</head>
<body>
<aside>
<h1>Cosmobiología</h1>
<div style="font-size:11px;color:var(--fg-muted);">cliente web demo</div>
<h2>Cartas</h2>
<div id="tree"></div>
<h2>Acciones rápidas</h2>
<div class="tree-node" onclick="loadSky()">⏱ Cielo ahora</div>
<div class="tree-node" onclick="newGroup()"> Nuevo grupo</div>
<div class="tree-node" onclick="newContact()"> Nuevo contacto</div>
</aside>
<main>
<div class="toolbar">
<button onclick="refreshSelected()">↻ Refrescar</button>
<label>Offset (min):
<input type="number" id="offset" value="0" step="60">
</label>
<label>
<input type="checkbox" id="transit"> Tránsito
</label>
<button onclick="downloadSvg()">⬇ SVG</button>
</div>
<div id="wheel-container">
<div style="color:var(--fg-muted)">Seleccioná una carta o "Cielo ahora"</div>
</div>
<div id="info"></div>
</main>
<script>
let selectedChartId = null;
let mode = 'sky'; // 'sky' | 'chart'
async function loadTree() {
const tree = await fetch('/api/tree').then(r => r.json());
const el = document.getElementById('tree');
el.innerHTML = '';
renderNodes(tree, el, 0);
}
function renderNodes(nodes, container, depth) {
for (const n of nodes) {
const div = document.createElement('div');
div.className = `tree-node indent-${depth}`;
const icon = n.kind === 'group' ? '◇' : n.kind === 'contact' ? '◯' : '✦';
div.innerHTML = `<span class="icon">${icon}</span>${escapeHtml(n.label)}`;
if (n.kind === 'chart') {
const id = n.id.replace(/^h:/, '');
div.onclick = () => selectChart(id);
}
container.appendChild(div);
if (n.children && n.children.length) {
renderNodes(n.children, container, depth + 1);
}
}
}
async function selectChart(id) {
selectedChartId = id;
mode = 'chart';
document.querySelectorAll('.tree-node').forEach(el =>
el.classList.toggle('active', el.textContent.endsWith(' (active)'))
);
await refreshSelected();
}
async function loadSky() {
mode = 'sky';
selectedChartId = null;
await refreshSelected();
}
// Cliente WASM opcional. Si `/static/wasm/cosmobiologia_web.js`
// existe (= el usuario corrió `wasm-pack build` después de
// `cargo build`), el rendering se hace localmente sin pedirle
// el SVG al server por cada interacción. Si NO existe (deploy
// mínimo), caemos al SSR de `/api/*.svg`. Toggle automático,
// sin configuración.
let wasm = null;
async function tryLoadWasm() {
try {
const mod = await import('/static/wasm/cosmobiologia_web.js');
await mod.default(); // wasm-pack: inicializa
wasm = mod;
document.getElementById('info').textContent = 'WASM cargado — render local';
} catch (e) {
console.info('WASM no disponible, usando SSR:', e.message);
}
}
async function refreshSelected() {
const offset = document.getElementById('offset').value || 0;
const transit = document.getElementById('transit').checked ? '1' : '0';
const params = new URLSearchParams({ offset_min: offset, transit });
const jsonUrl = mode === 'sky'
? `/api/sky`
: `/api/charts/${selectedChartId}/render?${params}`;
const ssrUrl = mode === 'sky'
? `/api/sky.svg`
: `/api/charts/${selectedChartId}/wheel.svg?${params}`;
const render = await fetch(jsonUrl).then(r => r.json()).catch(() => null);
let svg;
if (wasm && render) {
// Render local — WASM compose_wheel + draw_commands_to_svg.
try {
svg = wasm.render_model_to_svg(JSON.stringify(render), 600, 0);
} catch (e) {
console.warn('WASM render falló, fallback SSR:', e);
svg = await fetch(ssrUrl).then(r => r.text());
}
} else {
// Fallback: SSR — el server devuelve el SVG ya compuesto.
svg = await fetch(ssrUrl).then(r => r.text());
}
document.getElementById('wheel-container').innerHTML = svg;
const info = document.getElementById('info');
if (render) {
const mode_label = wasm ? 'WASM' : 'SSR';
info.innerHTML =
`<b>${escapeHtml(render.title)}</b> · ` +
`Asc ${render.ascendant_deg.toFixed(2)}° · ` +
`MC ${render.midheaven_deg.toFixed(2)}° · ` +
`${render.compute_ms} ms · ${mode_label}`;
} else {
info.textContent = '';
}
}
function downloadSvg() {
const offset = document.getElementById('offset').value || 0;
const transit = document.getElementById('transit').checked ? '1' : '0';
const params = new URLSearchParams({ offset_min: offset, transit });
const url = mode === 'sky'
? `/api/sky.svg`
: `/api/charts/${selectedChartId}/wheel.svg?${params}`;
window.location.href = url;
}
async function newGroup() {
const name = prompt('Nombre del grupo:');
if (!name) return;
await fetch('/api/groups', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
loadTree();
}
async function newContact() {
const name = prompt('Nombre del contacto:');
if (!name) return;
await fetch('/api/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
loadTree();
}
function escapeHtml(s) {
return s.replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
(async () => {
await tryLoadWasm();
await loadTree();
await loadSky();
})();
</script>
</body>
</html>
+6 -6
View File
@@ -15,13 +15,13 @@ cosmobiologia-panel = { path = "../../modules/cosmobiologia/cosmobiologia-panel"
cosmobiologia-store = { path = "../../modules/cosmobiologia/cosmobiologia-store" }
cosmobiologia-theme = { path = "../../modules/cosmobiologia/cosmobiologia-theme" }
cosmobiologia-tree = { path = "../../modules/cosmobiologia/cosmobiologia-tree" }
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
yahweh-core = { workspace = true }
yahweh-theme = { workspace = true }
yahweh-widget-theme-switcher = { path = "../../modules/ui_engine/widgets/theme-switcher" }
yahweh-widget-splitter = { workspace = true }
yahweh-widget-container-core = { workspace = true }
nahual-core = { workspace = true }
nahual-theme = { workspace = true }
nahual-widget-theme-switcher = { path = "../../modules/nahual/widgets/theme-switcher" }
nahual-widget-splitter = { workspace = true }
nahual-widget-container-core = { workspace = true }
gpui = { workspace = true }
directories = { workspace = true }
serde_json = { workspace = true }
+2 -2
View File
@@ -5,7 +5,7 @@
//! (fire-and-forget; si no hay Init, la app sigue standalone).
//! 2. Abre la DB SQLite en `$XDG_DATA_HOME/cosmobiologia/charts.db`
//! (fallback a `~/.local/share/cosmobiologia/charts.db`).
//! 3. Levanta GPUI con [`yahweh_theme::Theme::install_default`].
//! 3. Levanta GPUI con [`nahual_theme::Theme::install_default`].
//! 4. Compone el shell: [`Shell`] dueño del tree (izq), canvas (centro)
//! y panel (abajo). Cablea las suscripciones cross-widget.
//!
@@ -34,7 +34,7 @@ use gpui::{
};
use cosmobiologia_store::Store;
use yahweh_theme::Theme;
use nahual_theme::Theme;
use crate::shell::Shell;
+134 -22
View File
@@ -32,8 +32,8 @@ use cosmobiologia_canvas::{
AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
};
use cosmobiologia_engine::{
LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest, compose_with_options,
svg_export,
EventoConocido, LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest,
compose_with_options, svg_export,
};
use cosmobiologia_model::{
Chart, ChartId, ChartKind, ContactId, FreeChartId, ModuleState, StoredBirthData,
@@ -44,11 +44,11 @@ use cosmobiologia_store::Store;
use cosmobiologia_tree::{
parse_city_atlas_tsv, FreeChartEntry, TahuantinsuyuTree, TreeEvent,
};
use yahweh_core::{LayoutDirection, NodeId};
use yahweh_theme::Theme;
use yahweh_widget_container_core::ChildSlot;
use yahweh_widget_splitter::{SplitContainer, SplitEvent};
use yahweh_widget_theme_switcher::theme_switcher;
use nahual_core::{LayoutDirection, NodeId};
use nahual_theme::Theme;
use nahual_widget_container_core::ChildSlot;
use nahual_widget_splitter::{SplitContainer, SplitEvent};
use nahual_widget_theme_switcher::theme_switcher;
/// Posición del panel de control dentro del shell. `Bottom` mantiene
/// el layout histórico (tree+canvas arriba, panel abajo); las variantes
@@ -945,6 +945,7 @@ impl Shell {
show_minors: read_bool("aspect_minors", false),
orb_multiplier: read_f64("orb_multiplier", 1.0),
show_dignities: read_bool("show_dignities", false),
harmonic: read_f64("harmonic", 1.0).round().clamp(1.0, 64.0) as u32,
}
}
@@ -1172,9 +1173,59 @@ impl Shell {
CanvasEvent::ExportSvgRequested => {
self.export_current_to_svg();
}
CanvasEvent::GrAgeDelta(delta) => {
self.scrub_gr_age(*delta, cx);
}
CanvasEvent::HarmonicSelected(n) => {
self.select_harmonic(*n, cx);
}
}
}
/// Fija el armónico de la carta natal (clic en una barra del
/// espectro): escribe `harmonic` en `module_configs["natal"]`,
/// sincroniza el slider del panel y recompone.
fn select_harmonic(&mut self, n: u32, cx: &mut Context<Self>) {
let entry = self
.module_configs
.entry("natal".into())
.or_insert_with(|| serde_json::json!({}));
if let serde_json::Value::Object(map) = entry {
map.insert("harmonic".into(), serde_json::json!(n));
}
self.panel.update(cx, |p, cx| {
p.set_slider("natal", "harmonic", n as f64, cx)
});
self.persist_module("natal");
self.render_current(cx);
}
/// Scrubbing en vivo de la edad GR vía jog-dial. Acumula `delta`
/// sobre `target_age_years` del módulo `primary_directions`,
/// clampa a [0,120], sincroniza el slider del panel y recompone.
fn scrub_gr_age(&mut self, delta_years: f64, cx: &mut Context<Self>) {
if !module_enabled(&self.module_configs, "primary_directions") {
return;
}
let current = self.module_age_or_current("primary_directions");
let next = (current + delta_years).clamp(0.0, 120.0);
if (next - current).abs() < 1e-6 {
return;
}
let entry = self
.module_configs
.entry("primary_directions".into())
.or_insert_with(|| serde_json::json!({}));
if let serde_json::Value::Object(map) = entry {
map.insert("target_age_years".into(), serde_json::json!(next));
}
self.panel.update(cx, |p, cx| {
p.set_slider("primary_directions", "target_age_years", next, cx)
});
self.persist_module("primary_directions");
self.render_current(cx);
}
/// Recompone la carta actual + escribe el SVG a un archivo en
/// `$XDG_DATA_HOME/cosmobiologia/exports/<label>_<short_id>.svg`.
/// Logea la ruta a stderr — futuro: file save dialog GPUI.
@@ -1314,24 +1365,85 @@ impl Shell {
/// Otros módulos overlay (progression, solar_arc, primary_directions)
/// son extensión natural — TODO.
fn on_panel_action(&mut self, module_id: String, key: String, cx: &mut Context<Self>) {
if key != "save_as_free" {
return;
}
match module_id.as_str() {
"planetary_return" => self.save_planetary_return_as_free(cx),
"transit" => self.save_transit_as_free(cx),
"progression" => self.save_progression_as_free(cx),
// Solar arc y direcciones primarias son transformaciones
// matemáticas puras (no tienen un birth_data real
// equivalente — un Chart natal computado en el "momento
// SA" daría posiciones distintas a las dirigidas). Para
// guardarlas haría falta extender Chart con un kind
// `Derived { source, transform, params }` que el engine
// sepa rehidratar. TODO.
match key.as_str() {
"save_as_free" => match module_id.as_str() {
"planetary_return" => self.save_planetary_return_as_free(cx),
"transit" => self.save_transit_as_free(cx),
"progression" => self.save_progression_as_free(cx),
// Solar arc y direcciones primarias son transformaciones
// matemáticas puras (no tienen un birth_data real
// equivalente). Guardarlas exigiría un `ChartKind`
// `Derived { source, transform, params }`. TODO.
_ => {}
},
"rectificar" => self.run_rectificacion(cx),
_ => {}
}
}
/// Lanza el rectificador automático (Sistema GR): lee las edades de
/// los eventos conocidos de los sliders del módulo, barre las horas
/// candidatas y escribe el resultado en el campo «Resultado» del
/// panel. El barrido es síncrono — para ±15 min son ~31 cartas.
fn run_rectificacion(&mut self, cx: &mut Context<Self>) {
// Clonamos la carta: `rectificar` necesita `&Chart` y luego
// `panel.update` toma `&mut self` — no pueden solaparse.
let Some(chart) = self.current_chart.clone() else {
return;
};
let cfg = self.module_configs.get("primary_directions");
let read_age = |key: &str| -> f64 {
cfg.and_then(|c| c.get(key))
.and_then(|v| v.as_f64())
.unwrap_or(0.0)
};
// Edades > 0 — una ranura en 0 es "sin usar".
let eventos: Vec<EventoConocido> = ["evento_1", "evento_2", "evento_3"]
.iter()
.map(|k| read_age(k))
.filter(|edad| *edad > 0.5)
.map(|edad| EventoConocido { edad_years: edad })
.collect();
let key_gr = cfg
.and_then(|c| c.get("key"))
.and_then(|v| v.as_str())
.unwrap_or("naibod")
.to_string();
// Ventana ±15 min — dos pasadas (minuto grueso, segundo fino).
match cosmobiologia_engine::rectificar(&chart, &eventos, 15, &key_gr) {
Ok(r) => {
// Offset en segundos → texto «±Xm Ys».
let seg = r.mejor_offset_segundos;
let signo = if seg < 0 { "-" } else { "+" };
let abs = seg.abs();
let resumen = format!(
"{signo}{}m {:02}s · error {:.2}a",
abs / 60,
abs % 60,
r.mejor_puntaje
);
self.panel.update(cx, |p, cx| {
p.set_string("primary_directions", "resultado", Some(resumen), cx)
});
// Publicar el perfil al canvas: dibuja la curva del
// barrido, cuyo valle marca la hora rectificada.
self.canvas
.update(cx, |c, cx| c.set_rectificacion(Some(r), cx));
}
Err(_) => {
self.panel.update(cx, |p, cx| {
p.set_string(
"primary_directions",
"resultado",
Some("define al menos un evento (edad > 0)".to_string()),
cx,
)
});
}
}
}
/// Snapshot del cielo en este instante anclado al lugar del
/// natal. Sufijo `transito-{fecha}`. Útil para guardar "qué
/// estaba pasando ahora en la carta de Pedro".
@@ -1671,7 +1783,7 @@ impl Shell {
/// Click llama a `apply_dock` que reorganiza splitters y persiste.
fn render_dock_switcher(
&self,
theme: &yahweh_theme::Theme,
theme: &nahual_theme::Theme,
cx: &mut Context<Self>,
) -> impl IntoElement {
let mut row = div()
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "dominium"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "dominium — simulador psicológico de campo medio: ventana GPUI con maqueta isométrica viva, panel de estadísticas y bucle de simulación."
[[bin]]
name = "dominium"
path = "src/main.rs"
[dependencies]
dominium-core = { path = "../../modules/dominium/dominium-core" }
dominium-physics = { path = "../../modules/dominium/dominium-physics" }
dominium-iso = { path = "../../modules/dominium/dominium-iso" }
dominium-render-plan = { path = "../../modules/dominium/dominium-render-plan" }
dominium-canvas-gpui = { path = "../../modules/dominium/dominium-canvas-gpui" }
nahual-theme = { path = "../../modules/nahual/libs/theme" }
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
gpui = { workspace = true }
+310
View File
@@ -0,0 +1,310 @@
//! `dominium` — la ventana viva del simulador de campo medio.
//!
//! Compone toda la cadena de dominium en un app GPUI:
//!
//! ```text
//! dominium-core ─► dominium-physics ─► dominium-iso ─►
//! dominium-render-plan ─► dominium-canvas-gpui ─► [esta ventana]
//! ```
//!
//! Un bucle de fondo avanza la simulación ~11 veces por segundo; cada
//! tick reconstruye la maqueta isométrica y la repinta. El panel
//! derecho muestra las estadísticas agregadas y dos controles
//! (play/pausa, re-sembrar). Cuando la población colapsa, el mundo se
//! re-siembra solo: la demo nunca se queda en negro.
use std::time::Duration;
use dominium_canvas_gpui::DominiumCanvas;
use dominium_core::{SimParams, World};
use dominium_iso::{IsoProjector, ZWeights};
use dominium_physics::tick;
use dominium_render_plan::{build_plan, PlanConfig};
use gpui::{
div, hsla, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
};
use nahual_launcher::launch_app;
use nahual_theme::Theme;
/// Lado de la grilla cuadrada del mundo.
const GRID: usize = 40;
/// Población inicial de Lemmings.
const LEMMINGS: usize = 50;
/// Periodo del bucle de simulación.
const TICK_MS: u64 = 90;
/// PRNG mínimo (LCG de 64 bits) — siembra reproducible sin dependencias.
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u32(&mut self) -> u32 {
// Constantes de Knuth (MMIX).
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.0 >> 33) as u32
}
/// Flotante uniforme en `[0, 1)`.
fn next_f32(&mut self) -> f32 {
(self.next_u32() >> 8) as f32 / (1u32 << 24) as f32
}
}
/// Siembra un mundo: continentes de `materia`, vetas de `oro`, niebla de
/// `psique` y una población de Lemmings con sesgos y acciones variadas.
fn seed(seed: u64) -> World {
let mut w = World::new(GRID, GRID);
let mut rng = Lcg::new(seed);
for cy in 0..GRID {
for cx in 0..GRID {
let idx = w.grid.idx(cx, cy);
// m² concentra la materia en parches → aspecto de continentes.
let m = rng.next_f32();
w.grid.materia[idx] = m * m * 60.0;
if rng.next_f32() > 0.92 {
w.grid.oro[idx] = rng.next_f32() * 40.0;
}
w.grid.psique[idx] = rng.next_f32() * 12.0;
}
}
for _ in 0..LEMMINGS {
let x = rng.next_f32() * (GRID as f32 - 1.0);
let y = rng.next_f32() * (GRID as f32 - 1.0);
let psi = [
rng.next_f32(),
rng.next_f32(),
rng.next_f32(),
rng.next_f32(),
];
let i = w.lemmings.spawn(x, y, 30.0 + rng.next_f32() * 40.0, psi);
w.lemmings.accion[i] = (rng.next_u32() % 6) as u8;
}
w
}
/// Estadísticas agregadas de un instante de la simulación.
struct Stats {
poblacion: usize,
materia: f32,
oro: f32,
energia: f32,
}
/// El estado del simulador y su presentación.
struct Sim {
world: World,
params: SimParams,
iso: IsoProjector,
weights: ZWeights,
cfg: PlanConfig,
running: bool,
/// Ticks transcurridos en la época actual.
tick: u64,
/// Cuántas veces se re-sembró el mundo (colapso poblacional).
epoch: u64,
/// Semilla rodante para cada re-siembra.
rng_seed: u64,
}
impl Sim {
fn new(cx: &mut Context<Self>) -> Self {
let rng_seed = 0xD0_31_31_07;
let sim = Self {
world: seed(rng_seed),
params: SimParams::default(),
iso: IsoProjector::new(12.0, 0.05),
weights: ZWeights::default(),
cfg: PlanConfig {
tile: 15.0,
lemming_size: 8.0,
lemming_lift: 0.7,
palette: Default::default(),
},
running: true,
tick: 0,
epoch: 0,
rng_seed,
};
sim.start_loop(cx);
sim
}
/// Lanza el bucle de fondo que avanza la simulación.
fn start_loop(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| loop {
cx.background_executor()
.timer(Duration::from_millis(TICK_MS))
.await;
let alive = this.update(cx, |sim, cx| {
if sim.running {
sim.advance();
cx.notify();
}
});
if alive.is_err() {
break; // la entidad murió → ventana cerrada.
}
})
.detach();
}
/// Un paso de simulación; re-siembra si la población colapsa.
fn advance(&mut self) {
tick(&mut self.world, &self.params);
self.tick += 1;
if self.world.lemmings.is_empty() {
self.epoch += 1;
self.rng_seed = self.rng_seed.wrapping_mul(2862933555777941757).wrapping_add(1);
self.world = seed(self.rng_seed);
self.tick = 0;
}
}
/// Re-siembra el mundo a mano (botón ↺).
fn reseed(&mut self) {
self.rng_seed = self.rng_seed.wrapping_add(0x9E3779B9);
self.world = seed(self.rng_seed);
self.tick = 0;
self.epoch += 1;
}
/// Calcula las estadísticas del instante actual.
fn stats(&self) -> Stats {
let g = &self.world.grid;
Stats {
poblacion: self.world.lemmings.len(),
materia: g.materia.iter().sum(),
oro: g.oro.iter().sum(),
energia: self.world.lemmings.energia.iter().sum(),
}
}
}
/// Fila etiqueta/valor del panel de estadísticas.
fn stat_row(label: &str, value: String, theme: &Theme) -> impl IntoElement {
div()
.flex()
.flex_row()
.justify_between()
.child(div().text_color(theme.fg_muted).child(SharedString::from(label.to_string())))
.child(div().text_color(theme.fg_text).child(SharedString::from(value)))
}
impl Render for Sim {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::global(cx).clone();
let panel = hsla(220.0 / 360.0, 0.18, 0.10, 1.0);
let chip = hsla(220.0 / 360.0, 0.16, 0.16, 1.0);
let canvas_bg = hsla(220.0 / 360.0, 0.22, 0.06, 1.0);
let accent = theme.accent;
let stats = self.stats();
// --- Barra de estado ---
let estado = if self.running { "● corriendo" } else { "‖ en pausa" };
let status = div()
.h(px(34.))
.flex()
.flex_row()
.items_center()
.justify_between()
.px(px(14.))
.bg(panel)
.text_color(theme.fg_text)
.child(SharedString::from(format!(
"dominium · campo medio · época {} · tick {}",
self.epoch, self.tick
)))
.child(div().text_color(accent).child(SharedString::from(estado.to_string())));
// --- Maqueta isométrica ---
let plan = build_plan(&self.world, &self.iso, &self.weights, &self.cfg);
let canvas = div()
.flex_1()
.overflow_hidden()
.child(DominiumCanvas::new(plan).background(canvas_bg));
// --- Botones de control ---
let play_label = if self.running { "‖ Pausar" } else { "▶ Reanudar" };
let play = div()
.id("play")
.px(px(10.))
.py(px(7.))
.bg(chip)
.rounded(px(5.))
.text_color(theme.fg_text)
.cursor_pointer()
.hover(|s| s.bg(theme.bg_row_hover))
.child(SharedString::from(play_label.to_string()))
.on_click(cx.listener(|sim, _ev, _w, cx| {
sim.running = !sim.running;
cx.notify();
}));
let reset = div()
.id("reset")
.px(px(10.))
.py(px(7.))
.bg(chip)
.rounded(px(5.))
.text_color(theme.fg_text)
.cursor_pointer()
.hover(|s| s.bg(theme.bg_row_hover))
.child("↺ Re-sembrar")
.on_click(cx.listener(|sim, _ev, _w, cx| {
sim.reseed();
cx.notify();
}));
// --- Panel de estadísticas ---
let side = div()
.w(px(216.))
.flex()
.flex_col()
.gap(px(10.))
.p(px(12.))
.bg(panel)
.text_color(theme.fg_text)
.child(div().text_color(theme.fg_muted).child("[SIM]"))
.child(play)
.child(reset)
.child(div().h(px(1.)).bg(theme.border))
.child(stat_row("Población", format!("{}", stats.poblacion), &theme))
.child(stat_row("Materia", format!("{:.0}", stats.materia), &theme))
.child(stat_row("Oro", format!("{:.0}", stats.oro), &theme))
.child(stat_row("Energía", format!("{:.0}", stats.energia), &theme))
.child(div().h(px(1.)).bg(theme.border))
.child(
div()
.text_color(theme.fg_muted)
.child(SharedString::from(format!("grilla {GRID}×{GRID}"))),
)
.child(
div()
.text_color(theme.fg_muted)
.child("relieve = materia (Z)"),
);
// --- Composición ---
div()
.size_full()
.flex()
.flex_col()
.bg(theme.bg_app)
.child(status)
.child(
div()
.flex()
.flex_row()
.flex_1()
.child(canvas)
.child(side),
)
}
}
fn main() {
launch_app("brahman · dominium", (1120., 720.), Sim::new);
}
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "fana"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "fana — editor de escritura DAG: ventana GPUI con el documento como grafo de átomos narrativos, conectores de dependencia y osciloscopio de coherencia."
[[bin]]
name = "fana"
path = "src/main.rs"
[dependencies]
fana-core = { path = "../../modules/fana/fana-core" }
fana-graph = { path = "../../modules/fana/fana-graph" }
fana-render-plan = { path = "../../modules/fana/fana-render-plan" }
fana-editor-gpui = { path = "../../modules/fana/fana-editor-gpui" }
nahual-theme = { path = "../../modules/nahual/libs/theme" }
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
gpui = { workspace = true }
uuid = { workspace = true }
+267
View File
@@ -0,0 +1,267 @@
//! `fana` — el editor de escritura DAG, ventana GPUI.
//!
//! Compone la cadena de fana:
//!
//! ```text
//! fana-core ─► fana-graph ─► fana-render-plan ─►
//! fana-editor-gpui ─► [esta ventana]
//! ```
//!
//! El documento no es un texto plano sino un grafo de átomos
//! narrativos. La ventana lo muestra en columnas por rama, con los
//! conectores de dependencia y el osciloscopio de coherencia. El botón
//! «Mutar raíz» reescribe el átomo origen y dispara la onda de choque
//! lógica: todo descendiente cae a «por evaluar».
use fana_core::{CoherenceState, NarrativeAtom};
use fana_editor_gpui::{editor_view, tone_color};
use fana_graph::NarrativeGraph;
use fana_render_plan::{build_plan, CoherenceTone, LayoutConfig};
use gpui::{div, prelude::*, px, Context, IntoElement, Render, SharedString, Window};
use nahual_launcher::launch_app;
use nahual_theme::Theme;
use uuid::Uuid;
/// Estado del editor.
struct Fana {
graph: NarrativeGraph,
/// Átomo raíz — el que muta el botón de demostración.
root: Uuid,
/// Cuántas veces se mutó la raíz (para variar el texto nuevo).
mutations: u32,
}
impl Fana {
fn new(_cx: &mut Context<Self>) -> Self {
let (graph, root) = seed_document();
Self { graph, root, mutations: 0 }
}
/// Reescribe la raíz y propaga la onda de choque a sus descendientes.
fn mutate_root(&mut self) {
self.mutations += 1;
let nuevo = format!(
"Capítulo 1 — versión {}: el viajero nunca llegó al puerto.",
self.mutations
);
if let Some(atom) = self.graph.get_mut(self.root) {
atom.set_content(nuevo); // marca la raíz como PendingEvaluation
}
// Marca en cascada todo descendiente transitivo.
self.graph.propagate_mutation(self.root);
}
/// Devuelve todos los átomos a estado coherente.
fn revalidate(&mut self) {
let ids: Vec<Uuid> = self.graph.atoms().map(|a| a.id).collect();
for id in ids {
if let Some(atom) = self.graph.get_mut(id) {
atom.coherence = CoherenceState::Valid;
}
}
}
/// Cuenta átomos en cada estado de coherencia: `(pendientes, conflictos)`.
fn coherence_counts(&self) -> (usize, usize) {
let mut pending = 0;
let mut conflict = 0;
for a in self.graph.atoms() {
match a.coherence {
CoherenceState::PendingEvaluation => pending += 1,
CoherenceState::InConflict { .. } => conflict += 1,
CoherenceState::Valid => {}
}
}
(pending, conflict)
}
}
/// Construye el documento de ejemplo: un relato corto con una rama
/// alterna. Devuelve el grafo y el id de la raíz.
fn seed_document() -> (NarrativeGraph, Uuid) {
let mut root = NarrativeAtom::new(
"Capítulo 1 — el viajero llega al puerto al amanecer.",
"principal",
);
root.semantic_vectors.insert("calma".into(), 0.6);
let root_id = root.id;
let mut posada = NarrativeAtom::new(
"El posadero le ofrece cuarto y un vaso de vino tibio.",
"principal",
)
.depends_on(root_id);
posada.semantic_vectors.insert("calma".into(), 0.4);
posada.semantic_vectors.insert("misterio".into(), 0.3);
let posada_id = posada.id;
let mut pasos = NarrativeAtom::new(
"Por la noche escucha pasos lentos en el pasillo.",
"principal",
)
.depends_on(posada_id);
pasos.semantic_vectors.insert("misterio".into(), 0.9);
pasos.semantic_vectors.insert("miedo".into(), 0.7);
let pasos_id = pasos.id;
let mut puerta = NarrativeAtom::new(
"Al amanecer, la puerta de su cuarto está entreabierta.",
"principal",
)
.depends_on(pasos_id);
puerta.semantic_vectors.insert("miedo".into(), 1.0);
puerta.coherence = CoherenceState::InConflict {
origin: pasos_id,
reason: "el amanecer ya se narró en el capítulo siguiente".into(),
};
// Rama alterna: el viajero rechaza la posada.
let mut muelle = NarrativeAtom::new(
"Pero el viajero rechaza el cuarto y duerme sobre el muelle.",
"alterna",
)
.depends_on(posada_id);
muelle.semantic_vectors.insert("soledad".into(), 0.8);
let graph = NarrativeGraph::from_atoms([root, posada, pasos, puerta, muelle]);
(graph, root_id)
}
/// Fila de leyenda: muestra el color de un tono y su etiqueta.
fn legend_row(tone: CoherenceTone, label: &str, theme: &Theme) -> impl IntoElement {
div()
.flex()
.flex_row()
.items_center()
.gap(px(8.))
.child(div().w(px(12.)).h(px(12.)).rounded(px(3.)).bg(tone_color(tone)))
.child(
div()
.text_size(px(12.))
.text_color(theme.fg_muted)
.child(SharedString::from(label.to_string())),
)
}
/// Fila etiqueta/valor del panel.
fn stat_row(label: &str, value: String, theme: &Theme) -> impl IntoElement {
div()
.flex()
.flex_row()
.justify_between()
.child(div().text_color(theme.fg_muted).child(SharedString::from(label.to_string())))
.child(div().text_color(theme.fg_text).child(SharedString::from(value)))
}
impl Render for Fana {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::global(cx).clone();
let panel = gpui::hsla(220.0 / 360.0, 0.18, 0.10, 1.0);
let chip = gpui::hsla(220.0 / 360.0, 0.16, 0.16, 1.0);
let (pending, conflict) = self.coherence_counts();
let plan = build_plan(&self.graph, &LayoutConfig::default());
// --- Barra de estado ---
let status = div()
.h(px(34.))
.flex()
.flex_row()
.items_center()
.justify_between()
.px(px(14.))
.bg(panel)
.text_color(theme.fg_text)
.child("fana · editor de escritura DAG")
.child(
div()
.text_color(theme.fg_muted)
.child(SharedString::from(format!("{} átomos", self.graph.len()))),
);
// --- Lienzo del editor (con scroll) ---
let canvas = div()
.id("editor-scroll")
.flex_1()
.overflow_x_scroll()
.overflow_y_scroll()
.bg(theme.bg_app)
.child(editor_view(&plan, &theme));
// --- Botones (los listeners se cablean abajo con cx.listener) ---
let btn_mutar = div()
.id("mutar")
.px(px(10.))
.py(px(7.))
.bg(chip)
.rounded(px(5.))
.text_color(theme.fg_text)
.cursor_pointer()
.hover(|s| s.bg(theme.bg_row_hover))
.child("⚡ Mutar raíz")
.on_click(cx.listener(|fana, _ev, _w, cx| {
fana.mutate_root();
cx.notify();
}));
let btn_revalidar = div()
.id("revalidar")
.px(px(10.))
.py(px(7.))
.bg(chip)
.rounded(px(5.))
.text_color(theme.fg_text)
.cursor_pointer()
.hover(|s| s.bg(theme.bg_row_hover))
.child("✓ Re-validar todo")
.on_click(cx.listener(|fana, _ev, _w, cx| {
fana.revalidate();
cx.notify();
}));
// --- Panel lateral ---
let side = div()
.w(px(240.))
.flex()
.flex_col()
.gap(px(10.))
.p(px(12.))
.bg(panel)
.text_color(theme.fg_text)
.child(div().text_color(theme.fg_muted).child("[DOCUMENTO]"))
.child(btn_mutar)
.child(btn_revalidar)
.child(div().h(px(1.)).bg(theme.border))
.child(stat_row("Átomos", format!("{}", self.graph.len()), &theme))
.child(stat_row("Por evaluar", format!("{pending}"), &theme))
.child(stat_row("En conflicto", format!("{conflict}"), &theme))
.child(div().h(px(1.)).bg(theme.border))
.child(div().text_color(theme.fg_muted).child("coherencia"))
.child(legend_row(CoherenceTone::Valid, "coherente", &theme))
.child(legend_row(CoherenceTone::Pending, "por evaluar", &theme))
.child(legend_row(CoherenceTone::Conflict, "en conflicto", &theme))
.child(div().h(px(1.)).bg(theme.border))
.child(
div()
.text_size(px(11.))
.text_color(theme.fg_muted)
.child(
"«Mutar raíz» reescribe el átomo origen: la onda \
de choque marca cada descendiente como «por \
evaluar».",
),
);
// --- Composición ---
div()
.size_full()
.flex()
.flex_col()
.bg(theme.bg_app)
.child(status)
.child(div().flex().flex_row().flex_1().child(canvas).child(side))
}
}
fn main() {
launch_app("brahman · fana", (1180., 760.), Fana::new);
}
+5 -2
View File
@@ -11,9 +11,10 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
gioser-canvas-web = { path = "../../modules/gioser/gioser-canvas-web" }
pluma-reader-web = { path = "../../modules/pluma/pluma-reader-web" }
vista-web = { path = "../../modules/vista/vista-web" }
fana-md-reader-web = { path = "../../modules/fana/fana-md-reader-web" }
revista-web = { path = "../../modules/revista/revista-web" }
barra-web = { path = "../../modules/barra/barra-web" }
gioser-graph-web = { path = "../../modules/gioser/gioser-graph-web" }
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
js-sys.workspace = true
@@ -38,4 +39,6 @@ features = [
"NodeList",
"Performance",
"console",
"Location",
"History",
]
+41
View File
@@ -0,0 +1,41 @@
---
title: Mística · Espiritualidad
camino: uku
tags: [mística, espiritualidad, meditación, contemplación]
---
> *La práctica como puente. El misterio como interlocutor.*
Acá vive lo místico, lo espiritual, las prácticas que sostienen la
atención. No es decoración: es la otra mitad del trabajo. Sin esto,
el resto se vuelve ruido.
## Prácticas
Lo que sostiene día a día:
- **Meditación.** Sentarse a observar lo que sucede, sin agarrarlo.
- **Lectura contemplativa.** Textos que se vuelven a leer hasta que
cambian.
- **Ceremonia.** Marcar inicios y cierres con gestos que pesan.
- **Naturaleza.** Estar en lugares donde uno no es el centro.
- **Silencio.** Día completo, una vez por mes mínimo.
## Por qué mística
Porque la racionalidad sola no alcanza para vivir. Y porque las
tradiciones llevan miles de años elaborando vocabulario para lo que
nos pasa cuando atendemos en serio: contemplación, ego, símbolo,
muerte, asombro.
**Mística aplicada** = no quedarse en el libro. Pasar por el cuerpo,
por la relación, por la vida cotidiana.
## Lo que leo
Andino, budista, cristiano-contemplativo, hindú, sufí. Sin
exclusividad: cada tradición resuelve algunas cosas mejor que otras.
## Próximamente
*Acá se va a ir armando una bitácora de lecturas, prácticas y notas.*
+37
View File
@@ -0,0 +1,37 @@
---
title: Software · Tecnología
camino: logos
tags: [software, tecnología, open-source, rust, python]
---
> *Lo público. Lo que se mantiene abierto.*
Acá viven los proyectos de **software libre**, herramientas, librerías
y exploraciones técnicas que voy publicando. La premisa es mantener
el código abierto, documentado y útil más allá del autor.
## Qué vas a encontrar acá
- Repos públicos de cosas que escribo (Rust, Python, embedded, web).
- Notas técnicas sobre arquitectura, sistemas distribuidos, runtimes.
- Ensayos sobre **IA aplicada** — sin hype, con ejemplos concretos.
- Bitácoras de exploración: lo que probé, lo que descarté, lo que sigo
usando.
## Por qué open source
Porque el conocimiento técnico se multiplica cuando circula. Y porque
mucho de lo que uso a diario me lo regaló alguien que decidió compartir.
La reciprocidad importa.
## Stack actual
- **Rust** para lo que necesita ser rápido, seguro y portable.
- **Python** para análisis, ML y prototipos rápidos.
- **Linux** (Artix/Arch) como sistema operativo de trabajo.
- **gitea** + **nix** para infraestructura personal.
## Próximamente
*Voy a ir enlazando proyectos específicos acá: tools, runtimes,
experimentos. Por ahora, este placeholder vive en `docs/aire.md`.*
+81
View File
@@ -0,0 +1,81 @@
---
title: Cosmovisión · Los 4 Elementos
camino: cosmos
tags: [cosmovisión, elementos, arquetipos, tierra, agua, aire, fuego, marco]
---
# Cosmovisión · Los 4 Elementos
> *Tierra, Agua, Aire, Fuego. No son decoración. Son la sintaxis del estudio.*
Este camino es el marco conceptual que organiza los cuatro caminos
originales. Los elementos no son metáforas poéticas: son arquetipos
operativos que describen modos de ser, hacer y relacionarse.
## Los Cuatro Caminos
### Tierra (kay)
Lo que se sostiene. Lo firme. El cuerpo, la materia, la invariante.
El camino de Tierra es el manifiesto: las axiomas que no cambian, el
protocolo de Presencia, el algoritmo de perdón. Es la base del sistema.
Sin Tierra, los demás elementos flotan sin anclaje.
Tierra pregunta: *¿Qué es verdad aunque todo lo demás cambie?*
### Agua (uku)
Lo que fluye. La emoción, la intuición, el vínculo con el misterio.
El camino de Agua es la mística: la espiritualidad aplicada, la
meditación, la ceremonia, el silencio. No es adorno: es el disolvente
que evita que el sistema se cristalice en dogma.
Agua pregunta: *¿Qué se mueve cuando dejo de controlar?*
### Aire (logos)
Lo que circula. La información, el código, la conexión.
El camino de Aire es el software: las herramientas técnicas, los
sistemas distribuidos, la IA aplicada, el open source. Es la red
que conecta los nodos del estudio.
Aire pregunta: *¿Qué podemos compartir que multiplique inteligencia?*
### Fuego (nomos)
Lo que transforma. La identidad, la bitácora, el testimonio.
El camino de Fuego es el quién soy: la crónica personal, el
testimonio de alguien que practica mantenerse despierto. Es el
calor humano que evita que el sistema sea pura abstracción.
Fuego pregunta: *¿Quién soy cuando dejo de ser lo que aprendí?*
## El centro
Los cuatro elementos no son jerárquicos. Son un campo. El centro
es la Presencia — el punto fijo desde el cual todos se experimentan.
*Cada elemento contiene a los otros.* Tierra sin agua se petrifica.
Agua sin tierra se desborda. Aire sin fuego es frío. Fuego sin aire
se sofoca.
## Integración
Los caminos nuevos — cuerpo, sombra, práctica, olvido — no son
elementos adicionales. Son **modos de aplicación** de los cuatro
elementos en dominios específicos de la experiencia:
- **Cuerpo**: Tierra + Agua aplicados a la somática
- **Sombra**: Agua + Fuego aplicados al inconsciente
- **Práctica**: los 4 en acción concreta
- **Olvido**: Aire + Tierra en modo de liberación
## Próximamente
*Este marco se irá expandiendo con diagramas, referencias a la
tradición hermética y aplicaciones concretas de cada elemento en
el trabajo diario.*
+58
View File
@@ -0,0 +1,58 @@
---
title: El Cuerpo como Portal
camino: cuerpo
tags: [somática, respiración, movimiento, cuerpo, presencia, encarnación]
---
# El Cuerpo como Portal
> *La respiración como ancla. El movimiento como oráculo. La carne como templo.*
No hay despertar sin cuerpo. La presencia puede pensarse desde la mente,
pero se encarna — literalmente — en el cuerpo. Este camino explora la
dimensión somática del trabajo interior.
## Respiración
La respiración es el puente más directo entre lo voluntario y lo autónomo.
Cuando no sabes qué hacer, respirar es siempre una respuesta válida.
Prácticas que sostienen el trabajo respiratorio:
- **Respiración diafragmática 4-7-8**: inspira 4", retén 7", exhala 8".
Dice el sistema nervioso que se regula solo si se lo dejas claro.
- **Pausa espiratoria**: exhalar completo y quedarse vacío unos segundos.
El silencio entre respiros es la presencia más limpia.
- **Respiración cuadrada**: 4 tiempos iguales (inspirar, retener, exhalar,
retener). Sin nombre fancy: simplemente simetría.
## Movimiento consciente
El cuerpo no es una carga que la mente debe arrastrar. Es el vehículo de la
atención. Moverse con presencia es una práctica completa.
- **Caminar meditativo**: pasos lentos, sentir el peso que se transfiere,
el contacto con el suelo.
- **Estiramientos** al despertar: 5 minutos de elongación consciente antes
de agarrar el teléfono.
- **Sacudidas**: literalmente agitar el cuerpo para resetear el estado
nervioso. Los animales lo hacen instintivamente.
## El cuerpo como sensor
Las emociones no son conceptuales. Son patrones somáticos: tensión en el
pecho, nudo en la garganta, mariposas en el estómago.
Aprender a leer estas señales es un alfabeto que pocos enseñan:
1. **Escaneo corporal rápido**: de pies a cabeza, 30 segundos, notar sin
cambiar nada.
2. **Mapa de tensión**: en qué parte del cuerpo viven mis patrones habituales?
Mandíbula? Hombros? Pelvis? Cada cual tiene su geografía.
3. **El suspiro como dato**: si suspiras hondo sin pensar, tu cuerpo te está
diciendo algo que quizás la mente ignora.
## Próximamente
*Acá se irá profundizando en técnicas somáticas, ejercicios de presencia
corporal y la integración del movimiento como práctica espiritual.*
+36
View File
@@ -0,0 +1,36 @@
---
title: Quién Soy · Bitácora
camino: nomos
tags: [identidad, bitácora, crónica, personal]
---
> *La identidad como verbo. La crónica como práctica.*
Acá vive lo personal: quién soy, qué hago, qué leo, qué pienso. Una
bitácora honesta, no curada para impresionar. Si vas a leer esto,
asumí que es borrador.
## Quién soy
**Sergio**. Programador, lector, padre, alguien que practica
mantenerse despierto. Vivo entre código, café, montañas y libros.
Las cosas que más me importan no son las que mejor cuento todavía.
Por eso escribo: para precisar lo que sé y lo que no.
## Bitácora
Notas más o menos diarias sobre lo que voy pensando, viviendo,
fallando. Sin algoritmo de engagement, sin métricas. Sólo crónica.
Las entradas se ordenan por fecha. Las más viejas a veces dicen cosas
que ya no pienso así — las dejo igual.
## Por qué publicarlo
Porque escribir en público obliga a precisar. Y porque a veces lo que
uno escribe para sí mismo le sirve a otra persona que no conoce.
## Próximamente
*Acá se va a ir armando una bitácora con entradas fechadas.*
+82
View File
@@ -0,0 +1,82 @@
---
title: El Arte de Olvidar
camino: olvido
tags: [olvido, desaprender, soltar, memoria, perdón, identidad, vaciar]
---
# El Arte de Olvidar
> *No se trata de recordar más. Se trata de poder soltar.*
Olvidar no es fallo de memoria. Es una capacidad activa — quizás la más
subestimada del trabajo interior. Este camino explora el desaprender,
soltar la identidad, vaciar el caché de la memoria complementando el
algoritmo de perdón del Manifiesto.
## Por qué olvidar es necesario
La memoria no es un disco duro. Es un proceso reconstructivo que se
actualiza cada vez que recordamos. Eso significa que el pasado no está
fijo: se reescribe en cada recuperación.
Si no olvidamos, cargamos con versiones obsoletas de nosotros mismos.
El rencor, la identificación con el trauma, la historia personal que
repetimos como un mantra — todo eso es memoria no liberada.
El *Algoritmo de perdón* del Manifiesto describe el protocolo formal.
Acá exploramos la práctica cotidiana.
## Prácticas de olvido activo
### 1. Reencuadre deliberado
Cuando una memoria perturbadora aparezca:
1. Reconócela sin resistencia.
2. Respira hondo.
3. Pregunta: *¿Esta versión de los hechos sigue siendo cierta?*
4. Pregunta: *¿Qué necesitaría soltar para que esta memoria pierda su carga?*
5. Reescribe mentalmente la escena desde otra perspectiva — desde afuera,
desde el otro, desde el testigo.
### 2. La ceremonia de cierre
Para memorias que se niegan a disolverse:
1. Escribe la memoria en detalle. Todo lo que carga.
2. Siéntate con el texto. Léelo en voz alta.
3. Quema el papel (con seguridad). O entiérralo. O arrójalo al agua.
4. El gesto físico ancla el cambio que la mente sola no completa.
### 3. Vaciar el caché diario
Antes de dormir:
1. Repasa el día como una película rápida.
2. Identifica los momentos que todavía tienen carga emocional.
3. Respira en cada uno hasta que la carga se normalice.
4. Suelta. Literalmente: di "suelto" o exhala con intención.
### 4. Desaprender una creencia
Elige una creencia sobre ti mismo que ya no te sirve:
- "Yo soy tímido"
- "Yo no sirvo para X"
- "A mí siempre me pasa Y"
Durante 30 días, actúa *como si* esa creencia no fuera cierta.
No necesitas creer lo opuesto. Solo suspender la creencia anterior.
## Olvido y algoritmo de perdón
El algoritmo del Manifiesto (escaneo → anclaje en P → remuestreo →
reescritura → garbage collection) es la versión técnica. Las prácticas
de acá son la versión accesible para el día a día.
El olvido no es borrar. Es crear espacio para lo nuevo.
## Próximamente
*Técnicas avanzadas de desidentificación, protocolos de olvido
asistido y textos de referencia sobre el tema.*
+81
View File
@@ -0,0 +1,81 @@
---
title: Prácticas de Transformación
camino: practica
tags: [prácticas, ejercicios, transformación, respiración, meditación, registro, acecho]
---
# Prácticas de Transformación
> *No basta comprender. Hay que hacer. Y hay que hacer de nuevo mañana.*
Este camino recolecta ejercicios concretos — no teoría, no contemplación,
sino prácticas que se hacen. Cada una con instrucciones claras, duración
estimada y criterios para saber si funcionó.
## Respiración consciente (10 min)
**Objetivo**: anclar la atención en el cuerpo a través del ritmo respiratorio.
**Instrucciones**:
1. Siéntate en una posición cómoda, columna erguida pero no rígida.
2. Cierra los ojos o deja la mirada suave en un punto.
3. Lleva toda la atención a la sensación del aire entrando y saliendo.
Puede ser en las fosas nasales, en el pecho o en el abdomen.
4. Cuando la mente se vaya (y se irá), vuelve a traerla. Sin drama.
5. Después de unos minutos, cuenta las exhalaciones del 1 al 10.
Si pierdes la cuenta, empieza de nuevo.
**Señal de efectividad**: al terminar, notas que el ritmo cardíaco bajó
o que hay una pausa natural entre respiros.
## Meditación de Presencia (20 min)
**Objetivo**: habitar P (Presencia) sin objeto de meditación.
**Instrucciones**:
1. Siéntate. Sin buscar un estado especial.
2. No sigas la respiración. No repitas un mantra. No visualices nada.
3. Solo quédate presente. Sin hacer nada para lograrlo.
4. Cuando notes que estás en un pensamiento, simplemente nota: "pensando".
Sin juicio. Y suelta.
5. El "suelta" no requiere acción. Solo deja de sostener el pensamiento.
**Nota**: esta práctica es difícil. Sentirás aburrimiento, sueño,
inquietud. Todo eso es parte de la práctica. No la estás haciendo mal.
**Señal de efectividad**: en algún momento hay una pausa donde el mundo
se siente más *real* que los pensamientos sobre el mundo.
## Registro de chips (15 min diarios)
**Objetivo**: identificar patrones cognitivos recurrentes.
**Formato**: diario con tres columnas.
| Fecha | Gatillo | Patrón | Respuesta |
|-------|---------|--------|-----------|
| hoy | correo del trabajo | "no soy suficiente" | apretar mandíbula |
| hoy | discusión con X | "siempre me pasa igual" | retirarme |
Sin interpretación. Solo registro. Los patrones se vuelven visibles
después de 2-3 semanas.
**Criterio**: si después de un mes no ves patrones, probablemente no
estás siendo honesto en el registro.
## Acecho de un patrón (1 semana)
**Objetivo**: observar un chip cognitivo sin intervenir.
1. Elige UN patrón de tu registro de chips. El que más se repita.
2. Durante una semana, solo obsérvalo cuando aparezca.
3. No intentes cambiarlo. No lo analices. Solo nótalo.
4. Anota cada aparición: contexto, intensidad (1-10), duración.
**Lo que sucede**: sin intervención, el sistema empieza a autorregularse.
La mera observación sostenida erosiona el atractor.
## Próximamente
*Más prácticas: meditación caminando, escaneo corporal guiado,
ejercicios de acecho avanzados y protocolos de intervención.*
+75
View File
@@ -0,0 +1,75 @@
---
title: Trabajo con la Sombra
camino: sombra
tags: [sombra, jung, integración, patrones, inconsciente, proyección, acecho]
---
# Trabajo con la Sombra
> *Lo negado. Lo que no quiero ver. Lo que me gobierna desde la oscuridad.*
La sombra no es el enemigo. Es todo aquello que la conciencia no ha integrado:
los patrones automáticos, las reacciones que juré que no tendría, las partes
de mí que niego. Trabajar con la sombra no es eliminarla — es hacerla
consciente.
## ¿Qué es la sombra?
En el marco de este estudio, la **Sombra (S)** es el conjunto de chips
cognitivos no escaneados. Patrones que operan por debajo del umbral de
la Presencia (P). No son malos. Son inconscientes.
Se manifiestan como:
- **Proyección**: lo que me irrita en otros suele ser lo que no acepto en mí.
- **Reactividad automática**: una respuesta desproporcionada que me sorprende
a mí mismo.
- **Patrones recurrentes**: las mismas situaciones, las mismas personas, los
mismos conflictos en bucle.
- **Negación**: "eso no soy yo" dicho con demasiada convicción.
## Protocolo de integración
No se trata de iluminar cada rincón oscuro con una linterna. Se trata de
ampliar la capacidad de albergar contradicción.
**Fase 1 — Cartografía de activación**
Llevar un registro de momentos de alta reactividad emocional. Anotar:
qué pasó, qué sentí, qué pensé, qué hice. Sin juzgar. Sin interpretar.
Solo registrar.
Con el tiempo emergen patrones.
**Fase 2 — Acecho**
Elegir un patrón identificado y observarlo activamente durante una semana.
No intervenir. Solo notar: cuándo aparece, qué lo gatilla, cómo se siente
en el cuerpo.
Acechar no es cazar. Es conocer el territorio.
**Fase 3 — Conversación con la sombra**
Técnica: escribir un diálogo con la parte rechazada. Ponerle voz. Preguntarle
qué necesita, qué protege, qué quiere. No asumir que sabes la respuesta.
**Fase 4 — Integración**
Encontrar un lugar funcional para esa parte en la vida consciente. No es
eliminarla: es darle un asiento en la mesa. A veces lo que la sombra protege
es legítimo — solo se expresaba mal.
## Herramientas cotidianas
- **El espejo**: preguntar a alguien de confianza "¿qué patrón ves en mí que
yo no veo?"
- **La pausa**: cuando sientas reactividad, no actuar. Esperar 24 horas antes
de responder.
- **El diario de sueños**: los sueños a menudo expresan material de sombra
que la vigilia censura.
## Próximamente
*Se irán añadiendo ejercicios específicos, técnicas gestálticas y referencias
a autores que han trabajado este terreno (Jung, Perls, Bly, Zweig).*
+194
View File
@@ -0,0 +1,194 @@
---
title: Modelo Triplanar — Ontología del Campo Consciente
camino: tierra
tags: [manifiesto, triplanar, ontologia, planos, pipeline]
---
# Modelo Triplanar
## Ontología del Campo Consciente
---
## I. Tesis Fundamental
El primer grado de evidencia es la propia existencia. Desde ese punto de partida: el universo mismo es consciente. La conciencia no es un epifenómeno que emerge en sistemas complejos — es una propiedad del campo en sí mismo.
Todo es consciente. Cada posición del plano multidimensional es un nodo de consciencia con su propio punto de vista. El contenido y la naturaleza de su percepción están determinados por su posición en ese campo.
No hay una "realidad base" que contenga a los planos. Son ontologías paralelas, cada una completa y verdadera desde su propia posición.
---
## II. Plano 1 — Sensible
<code>
S₁ = {afección sensorial pura}
∄ "yo" en S₁
∄ cuerpo en S₁
S₁ solo contiene datos: color, sonido, textura, temperatura
</code>
Lo que hay: el toque de los sentidos, sin cuerpo que los reporte. El cuerpo físico como tal no existe desde aquí. Hay afección sensible pura — colores, sonidos, texturas — sin un "yo" al que pertenezcan.
La reconstrucción del cuerpo es posterior y ocurre en otro plano (§III). Para el plano sensible solo hay datos sensoriales sin un centro que los unifique.
El tiempo se experimenta como flujo sucesivo. El instante presente es un punto de fuga entre lo que fue y lo que será — nunca se puede "agarrar".
La constatación física en su nivel más burdo: el cuerpo como evidencia inmediata con correlato material. Los pensamientos como circuitos eléctricos y químicos funcionando como mecanismo determinista.
**Verdad absoluta desde esta posición:** solo existe la afección sensible. El tiempo es real como sucesión. El cuerpo no está presente como tal.
---
## III. Plano 2 — Alma / Mental
<code>
S₂ = {imágenes mentales, conceptualizaciones, campo relacional}
cuerpo(S₂) = imagen mental del cuerpo, no carne
t(S₂) = objeto bloque (pasado, presente, futuro coexisten)
campo(S₂) = f(interacción de nodos de consciencia)
</code>
Lo que hay: la imagen mental de las cosas. Aquí viven las conceptualizaciones.
El cuerpo no existe como carne — existe como imagen mental del cuerpo. Es una reconstrucción desde los datos del plano sensible.
El alma surge de la organización e interrelación masiva de distintos puntos del universo. La interacción de nodos de consciencia genera un campo. Dos vías que convergen al mismo resultado:
- Si el campo del alma ya existe independiente de la tierra → se hereda
- Si no existe → se genera desde la interacción
El efecto final es el mismo: dos planos coexistiendo (S₁ + S₂).
### Tiempo
Aquí el tiempo ya no es flujo vivido. Es una cosa contenida. Toda la línea temporal se puede ver como una forma — pasado, presente y futuro coexistiendo como bloque. El alma puede ver la dimensión completa del tiempo desde fuera.
### El conflicto humano
<code>
pecado original = conflicto de transición entre zonas biológicas
no es moral — es el bamboleo de una especie entre dos equilibrios
</code>
El "pecado original" no es una mancha moral. Es la descripción del conflicto que aparece cuando una especie animal se asoma fuera de la corriente de la selva sin haber terminado el movimiento.
**Verdad absoluta desde esta posición:** el alma existe como campo. El tiempo es un objeto que se puede contemplar. El cuerpo es solo imagen mental.
---
## IV. Plano 3 — Presencia (0, 0)
<code>
S₃ = {fenomenología pura}
P = (0, 0, 0)
∀s ∈ S : s P = v
t(S₃) = ∅ // el tiempo no es categoría aplicable
S₃ es un punto que contiene el universo completo
</code>
Lo que hay: fenomenología pura. Un punto es el universo completo, porque todo lo demás es solo contenido de su visión.
El Nivel 1 de evidencia solo reporta la propia existencia — y eso es suficiente. Presencia como origen del sistema de coordenadas: (0, 0).
El tiempo aquí no existe. Solo hay instante presente, y ese instante es lo único que hay. No es que "se esté en el presente" — el presente es la totalidad. La sucesión ni siquiera es una categoría aplicable.
**Verdad absoluta desde esta posición:** solo existe el instante presente, y ese instante lo contiene todo.
---
## V. Ontología de la Vida
### La vida no es delimitable
Lo que comúnmente llamamos "vida" es sesgo de cercanía — lo que más se parece a nuestra forma de vida. Pero todo es vida, en diferentes expresiones:
- Unas se reproducen, otras no
- Unas son piedras, otras son bacterias, otras son inteligencia artificial
- La vida es la misma actividad de las partículas siguiendo su libertad de elección con forma partícula/onda
### Zonas biológicas
Dentro del mundo biológico existen "zonas" que cumplen leyes particulares donde coinciden:
- La influencia del hábitat
- La herencia genética
- Los eventos contingentes
La zona más cercana a nosotros es entre la tierra y el cielo. Es un lugar de dualidad que forma cuerpos con pies, cabeza, peso y la necesidad de hacer ejercicio.
### Postura erecta
<code>
pararse en dos pies = sistema de equilibrio en desequilibrio permanente
no es detalle biomecánico — es la forma física del desequilibrio
el cuerpo se sostiene cayendo hacia adelante y recuperándose
</code>
En algún punto de la evolución, la especie animal asomó la cabeza del mundo animal y comenzó a entreverse donde se es humano. No hay diferencias de valor entre zonas — solo zonas distintas. Pero la especie aún no se ha actualizado: empezó un viaje donde atraviesa un desequilibrio para volver a un nuevo equilibrio.
### El ego como síntoma
<code>
ego = mareo del viaje entre dos zonas
= síntoma de estar entre: vista fuera de la selva,
cuerpo aún no reequilibrado en la nueva posición
</code>
El ego no es error ni enfermedad. Es el mareo del viaje que enfrentó una especie animal incursionando en la humanidad. Es el síntoma de estar entre dos zonas: la vista ya no está en la selva, pero el cuerpo aún no se reequilibró en la nueva posición.
El conflicto humano (pecado original, §III) es el bamboleo de ese pasaje.
Las distintas culturas, sistemas morales, tecnologías — son intentos parciales de encontrar el nuevo piso. La especie sigue en el loop del desequilibrio, generando versiones temporales de lo humano que intentan estabilizarse sin lograrlo del todo. El movimiento no ha terminado.
---
## VI. Resumen
| Plano | Contenido | Tiempo | Cuerpo | Verdad desde su posición |
|---|---|---|---|---|
| 1. Sensible | Afección sensorial pura | Flujo sucesivo | No existe — solo datos | Solo existe el toque de los sentidos |
| 2. Alma/Mental | Imagen mental, conceptualizaciones, campo | Bloque (visible desde fuera) | Imagen mental del cuerpo | Solo existe el alma y sus reconstrucciones |
| 3. Presencia (0,0) | Fenomenología pura | No-tiempo / solo instante presente | No aplica | Solo existe el presente que lo contiene todo |
Cada plano es una verdad absoluta de por sí. No hay contradicción entre ellos — son verdades de posiciones distintas en el campo.
---
## VII. Operadores prácticos
A continuación, herramientas que operan dentro de los planos — principalmente en S₂, donde ocurren los chips cognitivos y las conceptualizaciones.
### Chip cognitivo (Cᵢ)
<code>
Cᵢ ⊂ S₂ = atractor local en el campo mental
bucle de retroalimentación que estabiliza patrones subóptimos
</code>
Ejemplos: rumiación, diálogo interno autoalimentado, pánico.
### Pipeline
1. **Detección**: identificar el patrón recurrente en S₂. T (testigo desde S₃) puede detectar sin intervenir.
2. **Pausa del script**: T interrumpe la ejecución automática. El bucle se corta al dejar de identificarse con él.
3. **Modulación**: herramientas para bajar la ganancia del sistema (respiración, movimiento, frío, silencio).
4. **Redirección plástica**: prácticas sostenidas que refuerzan nuevas rutas.
5. **Mantenimiento**: rutinas preventivas, análisis de triggers.
### Métrica
<code>
Co = señal / ruido psicofisiológico
H = H(señal conductual) // entropía temporal
IR = t(baseline | perturbación)
PA = Δ(IR, Co) tras intervención
</code>
### Protocolo
**Diario**: reactividad (0-10), duración, intensidad (0-10).
**Escalonamiento**: detección → pausa → modulación → consolidación → mantenimiento.
**Auditoría**: cada 4-12 semanas — Co, IR, H, PA. Sin mejora → ajustar.
+37
View File
@@ -0,0 +1,37 @@
---
title: Manifiesto · Invariantes
camino: kay
tags: [manifiesto, principios, invariantes, ética]
---
> *Lo que no cambia. La piedra de toque.*
Acá vive el manifiesto de GioSer: las **invariantes** que sostienen
todo lo demás. Lo que no negocio, lo que define la forma del trabajo
antes que cualquier proyecto particular.
## Invariantes
Cosas que considero **no-negociables** en cómo hago el trabajo:
- **Código abierto por defecto.** Si tiene sentido, se publica.
- **Honestidad por encima de marketing.** No prometo lo que no puedo
cumplir, ni vendo lo que no probé.
- **El cuerpo es infraestructura.** Cuidarlo es parte del trabajo, no
opuesto al trabajo. Sin cuerpo no hay nada.
- **Las ideas se prueban escribiéndolas.** Si no hay documento, todavía
no existe la idea.
- **Compatibilidad hacia abajo > novedad arriba.** Las invariantes
duran, las modas no.
- **Una sola voz.** Lo que digo en privado coincide con lo que publico.
## Por qué un manifiesto
Porque sin invariantes, cada decisión es ad hoc. Tener un set chico de
principios reduce la energía gastada en cada elección — y deja en
claro cuándo estoy contradiciéndome.
## Revisión
Este manifiesto se revisa una vez al año, no antes. Si una invariante
deja de aplicarse, se quita con una explicación pública.
+269
View File
@@ -0,0 +1,269 @@
---
title: Manifiesto del Ser Desnudo
camino: tierra
tags: [manifiesto, ciencia-autoexperimental, axiomas, practica, metrica, pipeline]
---
# Manifiesto del Ser Desnudo
## I. El Origen: Nacer Humano y Desnudo
Existir no es un accidente de la marea. Es el acto supremo de una voluntad que ha elegido estar aquí.
Naciste porque quisiste nacer. Mereces esta bendición que es la existencia por el simple hecho de respirar. No has de hacer nada más.
Eres la semilla. Eres el puente viviente entre el misterio y la materia. Echa raíces profundas en la Madre Tierra. Levanta tu columna recta hacia el Padre Cielo.
En esa verticalidad, tú eres el equilibrio. Eres un dios caminando, magnífico en tu propia fragilidad.
Reconoce tu pequeña luz humana: eres como un infante que fantasea que ya creció, petulante al negar el suelo bajo sus pies, pero perfecto en su inmadurez.
> *No somos un ser que se transforma. Somos un transformar que se es.*
---
## II. Del Cuerpo Formal
Que la poesía no nos distraiga del rigor. Si la experiencia es un territorio, necesita mapa. No un mapa que pretenda ser el territorio — eso es idolatría — sino uno que permita navegarlo con precisión.
### Axioma 1 — Presencia como origen
Existe un punto de referencia universal en el espacio experiencial, llamado **Presencia** (P). Se define operacionalmente como el punto de auto-evidencia en el que la experiencia se registra sin identificarse con ella.
En coordenadas experienciales: **P = (0, 0, 0)**. Todo vector se mide desde aquí. No hay afuera de P porque P es el punto desde el cual todo afuera se define.
### Axioma 2 — Separación procesual
La experiencia se compone de dos capas:
- **Observador (O)**: idéntico a P. El testigo.
- **Flujo de datos (D)**: el conjunto de variables sensibles — pensamientos, emociones, percepciones, sensaciones corporales.
La independencia funcional se escribe: **O ∩ D = ∅**. No eres tus pensamientos. No es una metáfora: es una condición del sistema.
### Axioma 3 — Conservación de coherencia
La consistencia del sistema depende de reglas de interpretación (R). Las anomalías — depresión, confusión, pánico — no invalidan el axioma de existencia. Son fallas en R, no en P. El punto de referencia permanece.
### Axioma 4 — Instante recálculable
El presente (t) se recalcula en cada iteración del sistema. La memoria es un módulo accesible pero no fiduciario del presente. El pasado es estado registrado, no estado operativo continuo.
En presencia óptima, cada instante nace virgen. El peso de la memoria se aproxima a cero.
### Definiciones clave del sistema
**Testigo Trascendental (T)**: la función de observación asociada a P. No actúa, no juzga, no retiene. Solo registra.
**Chip cognitivo (Cᵢ)**: circuito cerrado de retroalimentación definido por un patrón recurrente en D. Un atractor local en el espacio de estados que estabiliza patrones subóptimos. Ejemplos: la rumiación, el diálogo interno, el miedo que se alimenta de sí mismo.
**Fricción (F)**: medida de resistencia interna al flujo de información. Análoga a una resistencia R en circuitos eléctricos. A mayor fricción, menor fluidez experiencial.
**Coherencia operativa (Co)**: grado de alineación entre energía disponible y eficiencia de procesamiento. Mayor Co = menor F.
**Amor operativo**: estado de mínima fricción y máxima fluidez informativa. Se define como el máximo de Co bajo restricciones energéticas del sistema. No es un sentimiento: es una propiedad del campo.
### Espacio de estados experienciales
Sea **S** el espacio topológico de estados. Cada punto s ∈ S representa una configuración completa de D en un instante t.
**P** es un punto fijo desde el cual se miden vectores proyectivos: **v = s P**.
El presente exhibe auto-similitud a escalas temporales y atencionales. La transformación **T: S → S** es iterativa y contractiva en presencia óptima, generando una huella residual **h(t)** que actúa como inicialización para la siguiente iteración.
La memoria **M** es un caché probabilístico: almacena distribuciones p(D | t Δ) usadas como prior para la interpretación presente. En presencia óptima, el peso de M se regulariza hacia cero.
---
## III. Dinámica: Fricción, Chip Cognitivo y Pipeline de Intervención
### Variables del sistema
| Variable | Símbolo | Naturaleza |
|---|---|---|
| Fricción | F(t) | ≥ 0, escalar |
| Resistencia | R(t) | f(Ego, Identificación) |
| Sufrimiento | S(t) | ∝ R(t) · Var(D(t)) |
El sufrimiento es intensidad de resistencia por variabilidad del flujo de datos. Cuando la resistencia es alta y los datos son turbulentos, el sistema vibra en disonancia.
### Ecuación operativa
La evolución del estado s(t) puede modelarse como:
> **ds/dt = G(s, u, t) α·R(s) + ξ(t)**
Donde:
- **G** captura la dinámica base del sistema — tu fisiología, tu temperamento, el ruido de fondo del mundo
- **u** son inputs externos — lo que comes, lo que lees, con quién hablas
- **α** escala la influencia de la resistencia R
- **ξ(t)** es ruido estocástico — el factor Dios, la mariposa en Pekín
### Aceptación como control
La aceptación no es resignación. Es una maniobra de control sobre R.
Cuando reduces R → 0 — cuando dejas de identificarte con el flujo — la dinámica se simplifica:
> **ds/dt ≈ G(s, u, t) + ξ(t)**
En ese límite, maximizas la capacidad de respuesta del sistema y minimizas las pérdidas por fricción. El sufrimiento tiende a su mínimo estructural.
### Pipeline de intervención sobre chips cognitivos
Un chip Cᵢ es un atractor local en S. Identificarlo y desmantelarlo es la práctica central. El pipeline tiene cinco fases:
1. **Detección**: monitorizar actividad — autoinforme, métricas fisiológicas, registros de comportamiento — para identificar estados repetitivos, su período y su trigger.
2. **Pausa del script**: función de interferencia ejecutada por T. Atención sostenida que interrumpe la ejecución automática. Corte la retroalimentación del chip.
3. **Modulación exógena**: uso controlado de herramientas para bajar la ganancia del sistema y aumentar plasticidad temporal. Pueden ser neuromoduladores, respiración, movimiento, silencio. El criterio no es dogmático sino pragmático.
4. **Redirección plástica**: prácticas sostenidas — meditación, terapia, entornos enriquecidos — para reforzar nuevas rutas sinápticas. Optimización por refuerzo gradual, no por voluntad heroica.
5. **Mantenimiento**: rutinas preventivas para evitar reimplantación del chip. Análisis de triggers, autoobservación periódica.
### Precauciones éticas
Las intervenciones farmacológicas deben ser supervisadas clínicamente. Reducir un fenómeno humano a un circuito no implica deshumanización: la validación fenomenológica es co-requisito. El modelo es una herramienta, no una sentencia.
### Modelo esquemático simple
Sea s un escalar que representa nivel de activación problemática (ansiedad, rumiación):
> **ds/dt = k·s α·R(s) + I(t) + ε(t)**
Donde k > 0 es amortiguamiento natural, R(s) = β·sⁿ (n ≥ 1), I(t) es entrada externa, ε ruido.
La aceptación reduce β → 0. La modulación reduce k o α temporalmente para permitir reconfiguración. Este esquema permite simular fases de recaída, plasticidad y estabilización.
---
## IV. El Laberinto de las Sombras
Te has perdido en el murmullo de las viejas formas mentales. Tus creencias son prisiones. Tus pensamientos son efímeras polillas relampagueantes que habitan tu estructura mecanizada.
Nadie puede entrar en lo más sagrado de tu ser sin tu permiso y tu decisión. Ni la sociedad, ni el sistema, ni la opresión. Eres libre de considerarte libre, o libre de considerarte un esclavo.
El saboteador no es un enemigo externo. Es tu tendencia a la "cómoda miseria". Es el miedo a despertar lo que te encadena a personajes que ya no te pertenecen.
> Lo que crees ser: etiquetas, memorias de dolor, un nombre con historia, un manojo de miedos y certezas.
>
> Lo que eres: presencia silenciosa que atestigua el tiempo, el espacio donde las nubes aparecen, una mirada transparente que no necesita nombres.
---
## V. Teoría de Campo Unificado: No-dualidad y Lógica de Conjuntos
### Formalización de la no-separación
Sea **U** el conjunto universal de la experiencia. Un individuo es un subconjunto **A ⊆ U**.
La experiencia de no-separación — la disolución del límite entre yo y mundo — se formaliza como una identidad funcional **A ≈ U** en términos de acceso y efecto causal. Es decir: para toda propiedad p relevante al procesamiento, p(A) = p(U) en su medida operativa.
No es una declaración metafísica. Es una condición del sistema que puede alcanzarse y medirse.
### Algoritmo de perdón (protocolo de limpieza)
El perdón no es un acto moral. Es un procedimiento operativo sobre la memoria:
1. **Escaneo de archivos**: identificación de memorias perturbadoras.
2. **Anclaje en P**: situarse en Presencia, fuera del flujo de datos.
3. **Remuestreo**: reproducir la memoria en estado de baja fricción (R ≈ 0).
4. **Reescritura contextual**: re-etiquetar la memoria con menor ganancia emocional.
5. **Garbage collection**: liberar patrones redundantes que consumen presupuesto energético sin servir al presente.
---
## VI. La Medicina y el Despertar
El encuentro con el sagrado Yagé no es una huida. Es un retorno violento y amoroso a la realidad. Es el espejo de tus animalismos, de tus dragones y tus monstruos.
¿Soportarás el abismo infinito que eres? La estructura de tu mundo se destruirá. Quedarás sin piso, sin razón. Morirán tus pasados mientras te aferras a ellos con las uñas rotas y el sudor en la frente.
El chamán es solo un humano falible. No es un dios, ni un papa, ni un maestro. Es un hermano que pone su esfuerzo al servicio. La verdadera maestra es la medicina misma, que extrae la esencia de la tierra para tocarte.
Sobre la impecabilidad del guerrero: asume la responsabilidad total. No se vale acceder a los antojos ni desfallecer ante la pereza. Sé indiviso en tus pensamientos, palabras y obras. No te entregues a la medicina como una hoja llevada por el viento. Entrégate como quien pone orden en su propio mundo.
El "santo dolor" es la medicina amarga que limpia la ceguera y funde el plomo que arrastras.
---
## VII. Métrica y Protocolo
### Medidas operativas
**Coherencia operativa (Co)**: ratio señal/ruido en indicadores psicofisiológicos y rendimiento atencional. Una medida burda pero útil del estado del sistema.
**Entropía dinámica (H)**: entropía temporal de la señal psico-conductual. Una disminución de H en presencia sostenida sugiere estabilización útil.
**Índice de reactividad (IR)**: tiempo de retorno al baseline después de una perturbación. Mide qué tan rápido se recupera el sistema.
**Plasticidad adaptativa (PA)**: capacidad de implementar y consolidar rutas alternativas. Se mide por la variación en IR y Co tras una intervención.
### Protocolo de práctica recomendada
**Rutina diaria**: 1020 minutos de atención sostenida anclada en P. No es meditación en el sentido clásico — es pausa del script, punto cero.
**Registro**: diario de triggers y respuestas con métricas simples: reactividad (010), duración, intensidad.
**Intervención escalonada**: detección → pausa → modulación (lo que funcione: respiración, movimiento, silencio, apoyo externo) → consolidación conductual → mantenimiento.
**Auditoría periódica**: medir Co, IR, H y PA cada 412 semanas. Ajustar protocolo según resultado.
---
## VIII. La Práctica del Instante
La disciplina no es un castigo. Es el arte de obedecerse a sí mismo. Es el vigor de un acecho constante sobre tus propios impulsos.
Caminar no es un esfuerzo por llegar a otro lado. Escucha bien: *"El pie que deja huella es el que deja su camino atrás."* Lo que hoy es tierra firme, mañana será nada.
Da cada paso para mantenerte de pie en el lugar al que ya estás llegando. Esto es atenta ecuanimidad: sentir la brisa y la tormenta, probar el sabor de la batalla sin que nada te arrastre.
Máximas del instante:
- Detén el mundo en tu cabeza para ver el mundo real.
- Cierra los ojos y mira; cierra la boca y canta.
- La claridad no es luz, es saber mirar en la oscuridad.
- Si quieres llegar, deja de dar pasos hacia el futuro.
---
## IX. Ontología, Epistemología y Telos
### Ontología
Esta propuesta no obliga a un monismo ontológico último. Ofrece una **ontología operativa**: entidades definidas por su función en el sistema. P, D, M, Cᵢ, R — existen en la medida en que operan. No se pronuncia sobre su existencia fuera del modelo.
### Epistemología
El conocimiento accesible es siempre modular y probabilístico. El observador T dispone de medios para validar hipótesis — autoexperimentación, medición, replicación — pero existen sesgos interpretativos y culturales que condicionan tanto R como G.
El criterio de verdad no es la correspondencia con una realidad externa inaccesible, sino la **coherencia operativa**: el modelo funciona si permite describir, predecir y modular con mayor eficacia que su ausencia.
### Telos: la finalidad funcional
La "meta" del sistema es maximizar Co (coherencia operativa) relativa a restricciones energéticas y contextuales. Esto se traduce en:
- Mayor adaptabilidad a entornos cambiantes.
- Menor sufrimiento medido como R · Var(D).
- Expansión de la capacidad para integrar variables — aumentar U efectivamente.
No hay un destino. Hay una dirección: reducir fricción, expandir presencia. El resto es paisaje.
---
## X. El Centro de la Nada
La rendición final es el portal a la libertad. Reconoce que no eres nada ante la inmensidad, y en esa nada, lo eres todo. *"Soy nada, pues soy tú mismo"*, susurra el alma.
El silencio no es ausencia de ruido. Es la presencia majestuosa que atestigua tanto el estruendo como la calma. Es el fondo infinito donde se proyecta tu existencia.
Eres un misterio que no tiene a quién preguntar. Eres mortal y eres divino. Eres tierra y eres cielo. Eres carne y eres espíritu eterno.
Acepta tu dualidad y quédate en el centro. Sé el amor que lo ve todo y a todo agradece. Todo está perdonado desde el principio.
> *Todo está bien aquí.*
Has vuelto a casa, al sagrado y eterno presente. Sonríe, ser humano, y entona la canción de la alegría.
+265
View File
@@ -0,0 +1,265 @@
---
title: Manifiesto del Ser Desnudo
camino: tierra
tags: [manifiesto, axiomas, pipeline, metrica, ontologia-operativa]
---
# Manifiesto del Ser Desnudo
## Índice
- [I. El Origen](#i-el-origen)
- [II. Campo de aplicación](#ii-campo-de-aplicación)
- [III. Axiomas](#iii-axiomas)
- [IV. Definiciones](#iv-definiciones)
- [V. Dinámica](#v-dinámica)
- [VI. El Pipeline](#vi-el-pipeline)
- [VII. Campo Unificado](#vii-campo-unificado)
- [VIII. Métrica](#viii-métrica)
- [IX. Protocolo](#ix-protocolo)
- [X. Ontología](#x-ontología)
- [XI. Apéndice — modelo esquemático](#xi-apéndice--modelo-esquemático)
---
## I. El Origen
Existes y lo sabes. Ese saber no requiere prueba. Es anterior a cualquier pensamiento, emoción o percepción. Es el marco, no el contenido.
La existencia es enteramente consciente siempre. Un perro existe y lo sabe. Una piedra existe y lo sabe. La diferencia humana es que hay un cerebro que puede **conceptualizar** ese saber — nombrarlo, analizarlo, modelarlo, olvidarlo, negarlo. Eso no hace la existencia más real. Hace el modelo más complejo.
Nada que hacer. Ya estás aquí. El sistema ya está corriendo. El resto es optimización.
---
## II. Tesis
Dos niveles de evidencia:
**Primero**: existes y lo sabes. Eso no se demuestra — es el punto de partida.
**Segundo**: no puedes asegurar que todo sea consciente, pero puedes confirmarlo desde donde estás. El universo mismo es consciente. Todo lo es — cada posición del plano multidimensional tiene su propio punto de vista. El contenido y la naturaleza de su percepción están determinados por su posición en ese campo.
Un humano no es más consciente que una piedra. Solo tiene un sistema nervioso que le permite **conceptualizar** esa conciencia — nombrarla, modelarla, negarla. La piedra experimenta su propia existencia sin necesidad de nombrarla.
Este modelo opera desde esta tesis. No la demuestra — la toma como condición de trabajo. La matemática es incompleta pero autocoherente: las definiciones no se contradicen y cualquier contradicción aparente es error de aplicación, no del modelo. El criterio no es verdad metafísica sino **coherencia operativa**.
---
## III. Axiomas
### Axioma 1 — Presencia como origen
<code>
P = (0, 0, 0)
∀s ∈ S : s P = v
</code>
P es el punto de auto-evidencia desde el cual toda experiencia aparece. Ningún contenido cae fuera de P porque P es el sistema de coordenadas mismo.
### Axioma 2 — Separación funcional
<code>
O ≡ P
D = {pensamientos, emociones, percepciones, sensaciones}
O ∩ D = ∅
</code>
El observador no es el flujo de datos. Cuando esa línea se borra (identificación), el sistema pierde grados de libertad y se comporta como un circuito cerrado. Eso es un chip cognitivo (§VI).
### Axioma 3 — Conservación de coherencia
<code>
R = reglas de interpretación
A (anomalía) es fallo en R, no en P
P invariante bajo transformaciones de R
</code>
Ninguna crisis es terminal. El punto de referencia no se rompe — lo que falla es cómo lees el mapa.
### Axioma 4 — Instante recálculable
<code>
t ← t + 1 en cada iteración
M(t) = p(D | t-Δ) // memoria como prior probabilístico
P óptima: peso(M) → 0
</code>
El pasado no arrastra. Es un caché que el presente puede consultar o ignorar. §V expande la dinámica.
---
## IV. Definiciones
<code>
T — Testigo Trascendental.
f(P) que observa sin actuar, juzgar ni retener.
Cᵢ — Chip cognitivo.
Atractor local en S. Bucle de retroalimentación que estabiliza
patrones subóptimos (rumiación, diálogo interno, pánico).
F — Fricción.
Resistencia al flujo de información. Análoga a R en circuitos.
Co — Coherencia operativa.
Eficiencia del procesamiento. Mayor Co = menor F.
Amor operativo — Estado de mínima F y máxima fluidez.
Co máximo bajo restricciones energéticas.
</code>
---
## V. Dinámica
<code>
F(t) ≥ 0
R(t) = f(identificación, ego)
S(t) ∝ R(t) · Var(D)
</code>
El sufrimiento es el producto de la resistencia por la turbulencia de los datos. R alta + D caótico = disonancia.
<code>
Aceptación := R → 0
R ≈ 0 :
ds/dt ≈ G(s, u, t) + ξ(t)
// dinámica base + ruido, sin amplificación
</code>
Aceptar no es resignarse. Es bajar la resistencia. El sistema deja de forcejear consigo mismo. Lo que queda no es necesariamente placentero — pero no es una pelea. Ver §VIII.
La memoria no es un registro. Es una reconstrucción probabilística que el presente genera bajo demanda. Cada recuerdo es una versión nueva, no una copia. Eso implica que puedes reescribir la memoria. §VII formaliza esto como algoritmo de perdón.
---
## VI. El Pipeline
Cinco fases para intervenir sobre un chip Cᵢ:
### 6.1 Detección
Identificar el chip: patrón recurrente en D, trigger, período. T puede detectar sin intervenir.
### 6.2 Pausa del script
<code>T := interrumpir ejecución automática de Cᵢ</code>
No discutas el contenido del chip. Obsérvalo como fenómeno. El bucle se corta cuando dejas de identificarte con él.
### 6.3 Modulación exógena
Herramientas para bajar la ganancia del sistema: respiración, movimiento, frío, silencio, neuromoduladores (con supervisión clínica). El criterio es pragmático.
### 6.4 Redirección plástica
Prácticas sostenidas que refuerzan nuevas rutas: meditación, terapia, repetición deliberada. Refuerzo gradual, no voluntad heroica.
### 6.5 Mantenimiento
Rutinas preventivas. Análisis de triggers, autoobservación periódica. El chip puede re-implantarse si el contexto persiste.
---
## VII. Campo Unificado
<code>
U = conjunto universal de la experiencia
A ⊆ U
No-separación: A ≈ U
≡ ∀p relevante al procesamiento: p(A) = p(U)
</code>
La experiencia de unidad no es mística. Es un estado del sistema donde los bordes entre "yo" y "mundo" dejan de filtrar. §VIII da métricas.
<code>
Algoritmo de perdón:
for m in M_perturbadora:
1. anclar en P
2. reproducir m con R ≈ 0
3. peso_emocional(m) ← 0
4. if redundante(m): liberar(m)
</code>
El perdón no es moral. Es un procedimiento de limpieza de memoria: tomar un recuerdo que duele, reproducirlo sin identificación, quitarle la carga, soltarlo si no sirve.
---
## VIII. Métrica
<code>
Co = señal / ruido psicofisiológico
H = H(señal conductual) // entropía temporal
IR = t(baseline | perturbación)
PA = Δ(IR, Co) tras intervención
</code>
Heurísticas para evaluar el sistema: ¿la atención fluye con menos esfuerzo? ¿El estado mental es menos errático? ¿Te recuperas más rápido? ¿Los cambios se mantienen?
---
## IX. Protocolo
### Diario
- Reactividad (0-10)
- Duración del estado
- Intensidad (0-10)
### Escalonamiento
1. Detección
2. Pausa del script
3. Modulación
4. Consolidación
5. Mantenimiento
### Auditoría
Cada 4-12 semanas: Co, IR, H, PA. Sin mejora → ajustar protocolo.
---
## X. Ontología
<code>
Entidades: P, D, Cᵢ, R, T
Existencia: operativa (funcionan en el modelo)
Criterio de verdad: coherencia operativa
</code>
El modelo no afirma que la realidad sea así. Afirma que actuar como si fuera así mejora los resultados. Es una ontología instrumental.
Límites:
- Sesgos interpretativos y culturales en R
- Conocimiento probabilístico y parcial
- Validación fenomenológica co-requisito
<code>
Telos: max(Co) bajo restricciones energéticas
≡ mayor adaptabilidad + menor S + mayor U efectivo
</code>
---
## XI. Apéndice — modelo esquemático
<code>
ds/dt = -k·s - α·R(s) + I(t) + ε(t)
k > 0 amortiguamiento natural
R(s) = β·sⁿ (n ≥ 1) resistencia
α escala de resistencia
I(t) entrada externa
ε(t) ruido estocástico
Aceptación: β → 0
Modulación: k↑ o α↓
Regímenes:
Recaída: I alto + β alto → s divergente
Plasticidad: β→0, luego k alto consolida
Estable: R ≈ 0 → ds/dt ≈ -k·s + I + ε
</code>
+160 -73
View File
@@ -3,107 +3,194 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>GioSer · En el centro, el ser</title>
<link rel="stylesheet" href="./styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Inter:wght@300;500;600&family=JetBrains+Mono:wght@400;600&display=swap">
<meta name="description" content="GioSer · arquitectura de sistemas distribuidos">
<title>GioSer · 0x000x03</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<canvas id="gioser-canvas" aria-hidden="true"></canvas>
<!-- Canvas de fondo (opcional para portada, necesario para WASM) -->
<canvas id="gioser-canvas" style="position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:-1;" aria-hidden="true"></canvas>
<main id="tips" aria-label="Cuatro cardinales">
<!-- NORTE (aire): SOFTWARE · Tecnología — circuito + nodos -->
<a id="tip-aire" class="tip tip-aire" href="#aire" data-md="./md/aire.md" aria-label="Software · Tecnología">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<rect x="18" y="18" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.7" rx="1"/>
<circle cx="24" cy="24" r="2" fill="currentColor"/>
<path d="M22 18 V14 M26 18 V14 M22 30 V34 M26 30 V34 M18 22 H14 M18 26 H14 M30 22 H34 M30 26 H34"
stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<circle cx="10" cy="10" r="1.6" fill="currentColor" opacity="0.7"/>
<circle cx="38" cy="10" r="1.6" fill="currentColor" opacity="0.7"/>
<circle cx="10" cy="38" r="1.6" fill="currentColor" opacity="0.7"/>
<circle cx="38" cy="38" r="1.6" fill="currentColor" opacity="0.7"/>
<path d="M11 11 L17 17 M37 11 L31 17 M11 37 L17 31 M37 37 L31 31"
stroke="currentColor" stroke-width="1.1" opacity="0.45"/>
</svg>
<span class="tip-label">SOFTWARE</span>
<span class="tip-sub">Tecnología</span>
</a>
<!-- Portada: retícula 2×2 tipo chakana -->
<div id="portada" class="portada">
<!-- Cuadrante I: 0x01 - Systems & Compositors (sup-izq) -->
<button class="cuadrante cuadrante-01" data-q="01" aria-label="0x01 Systems & Compositors">
<div class="cuadrante-border"></div>
<div class="cuadrante-content">
<span class="cuadrante-hex">0x01</span>
<span class="cuadrante-title">Systems &amp; Compositors</span>
<span class="cuadrante-context">Entorno operativo intermedio para Linux libres de telemetría.</span>
<div class="cuadrante-meta">
<span class="meta-cta">⏎ shell</span>
<span class="meta-stat">compositor · launcher</span>
</div>
<ul class="cuadrante-crates">
<li>eidolon (Compositor Wayland)</li>
<li>shimi (Shell/Launcher)</li>
</ul>
</div>
</button>
<!-- ESTE (fuego): QUIÉN SOY · Bitácora — libro abierto -->
<a id="tip-fuego" class="tip tip-fuego" href="#fuego" data-md="./md/fuego.md" aria-label="Quién Soy · Bitácora">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<path d="M8 14 L24 18 L40 14 V36 L24 32 L8 36 Z"
fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
<path d="M24 18 V32" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 22 H21 M12 26 H21 M12 30 H19" stroke="currentColor" stroke-width="1.1" opacity="0.65" stroke-linecap="round"/>
<path d="M27 22 H36 M27 26 H36 M29 30 H36" stroke="currentColor" stroke-width="1.1" opacity="0.65" stroke-linecap="round"/>
</svg>
<span class="tip-label">QUIÉN SOY</span>
<span class="tip-sub">Bitácora</span>
</a>
<!-- Cuadrante II: 0x00 - Bare-Metal OS (sup-der) -->
<button class="cuadrante cuadrante-00" data-q="00" aria-label="0x00 Bare-Metal OS">
<div class="cuadrante-border"></div>
<div class="cuadrante-content">
<span class="cuadrante-hex">0x00</span>
<span class="cuadrante-title">Bare-Metal OS</span>
<span class="cuadrante-context">El control directo sobre el silicio y la secuencia de arranque.</span>
<div class="cuadrante-meta">
<span class="meta-cta">⏎ init</span>
<span class="meta-stat">init · boot · sovereign</span>
</div>
<ul class="cuadrante-crates">
<li>arjé (Init/Nivel 0)</li>
</ul>
</div>
</button>
<!-- SUR (tierra): MANIFIESTO · Invariantes — hexagrama + círculo -->
<a id="tip-tierra" class="tip tip-tierra" href="#tierra" data-md="./md/tierra.md" aria-label="Manifiesto · Invariantes">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<circle cx="24" cy="24" r="18" fill="none" stroke="currentColor" stroke-width="1.4" opacity="0.55"/>
<path d="M24 8 L39 32 L9 32 Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
<path d="M24 40 L9 16 L39 16 Z" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.85" stroke-linejoin="round"/>
<circle cx="24" cy="24" r="1.8" fill="currentColor"/>
</svg>
<span class="tip-label">MANIFIESTO</span>
<span class="tip-sub">Invariantes</span>
</a>
<!-- Cuadrante III: 0x02 - Simulators & Geometry (inf-izq) -->
<button class="cuadrante cuadrante-02" data-q="02" aria-label="0x02 Simulators & Geometry">
<div class="cuadrante-border"></div>
<div class="cuadrante-content">
<span class="cuadrante-hex">0x02</span>
<span class="cuadrante-title">Simulators &amp; Geometry</span>
<span class="cuadrante-context">Modelado del tiempo en bloque, frecuencias celestes, simulación determinista.</span>
<div class="cuadrante-meta">
<span class="meta-cta">⏎ simulate</span>
<span class="meta-stat">efemérides · orbital · mundos</span>
</div>
<ul class="cuadrante-crates">
<li>eternal (Efemérides analíticas)</li>
<li>cosmobiología (Canvas orbital)</li>
<li>dominium (Simulador de mundos)</li>
</ul>
</div>
</button>
<!-- OESTE (agua): MÍSTICA · Espiritualidad — ojo en triángulo -->
<a id="tip-agua" class="tip tip-agua" href="#agua" data-md="./md/agua.md" aria-label="Mística · Espiritualidad">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<path d="M24 6 L42 40 L6 40 Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<path d="M13 26 Q24 16 35 26 Q24 34 13 26 Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
<circle cx="24" cy="26" r="3.2" fill="currentColor"/>
<circle cx="22.5" cy="24.5" r="0.9" fill="rgba(255,255,255,0.55)"/>
</svg>
<span class="tip-label">MÍSTICA</span>
<span class="tip-sub">Espiritualidad</span>
</a>
</main>
<!-- DECK: contenedor único con strip horizontal. Las páginas se inyectan
dinámicamente desde WASM. vista-web maneja swipe + snap. -->
<div id="deck" class="deck vista-deck" aria-hidden="true">
<div id="deck-strip" class="deck-strip vista-strip" role="region" aria-label="Vistas abiertas"></div>
<!-- Cuadrante IV: 0x03 - Core & Orchestration (inf-der) -->
<button class="cuadrante cuadrante-03" data-q="03" aria-label="0x03 Core & Orchestration">
<div class="cuadrante-border"></div>
<div class="cuadrante-content">
<span class="cuadrante-hex">0x03</span>
<span class="cuadrante-title">Core &amp; Orchestration</span>
<span class="cuadrante-context">Motor invisible: persistencia local-first, P2P, ERP, encriptación.</span>
<div class="cuadrante-meta">
<span class="meta-cta">⏎ orchestrate</span>
<span class="meta-stat">dht · p2p · erp</span>
</div>
<ul class="cuadrante-crates">
<li>brahman (Supervisor)</li>
<li>minga / minga-vfs (FS distribuido)</li>
<li>brahman-auth · nakui (ERP)</li>
</ul>
</div>
</button>
</div>
<!-- TASKBAR estilo Windows: home + GioSer + tabs + copyleft -->
<!-- DECK: contenedor para páginas md (oculto hasta expandir cuadrante) -->
<div id="deck" class="deck" aria-hidden="true">
<div id="deck-strip" class="deck-strip" role="region" aria-label="Vistas abiertas"></div>
</div>
<!-- Controles fijos -->
<div class="page-controls" id="global-page-controls" style="opacity:0;pointer-events:none;">
<button class="page-control-btn page-minimize" data-minimize type="button" aria-label="Minimizar">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19 H19" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</button>
<button class="page-control-btn page-close" data-close-page type="button" aria-label="Cerrar">×</button>
</div>
<!-- Taskbar -->
<nav class="taskbar" aria-label="Barra de vistas">
<button class="taskbar-home" data-home aria-label="Volver al home" type="button">
<button class="taskbar-home" data-home aria-label="Volver a la chakana" type="button">
<svg viewBox="0 0 24 24" class="taskbar-home-glyph" aria-hidden="true">
<path d="M3 12 L12 3 L21 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5 11 V20 H10 V14 H14 V20 H19 V11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<circle cx="12" cy="17" r="0.8" fill="currentColor"/>
</svg>
</button>
<a class="taskbar-brand" data-home href="#home" aria-label="GioSer · home">
<a class="taskbar-brand" href="#" aria-label="GioSer">
<span>Gio</span><span class="brand-dot">·</span><span>Ser</span>
</a>
<span class="taskbar-divider" aria-hidden="true"></span>
<ul class="taskbar-list" id="taskbar-list" role="presentation"></ul>
<span class="taskbar-spacer" aria-hidden="true"></span>
<a class="taskbar-credit" href="https://sergio.gioser.net" target="_blank" rel="noopener noreferrer" aria-label="Copyleft sergio arroba gioser punto net">
<a class="taskbar-credit" href="https://sergio.gioser.net" target="_blank" rel="noopener noreferrer" aria-label="Copyleft sergio">
<span class="copyleft-mark" aria-hidden="true">©</span>
<span class="taskbar-credit-text">sergio@gioser.net</span>
</a>
</nav>
<!-- Tips ocultos para navegación WASM (se mantienen para compatibilidad) -->
<div id="tips" hidden aria-hidden="true">
<a id="tip-aire" href="#aire" data-md="/md/aire.md"></a>
<a id="tip-fuego" href="#fuego" data-md="/md/fuego.md"></a>
<a id="tip-tierra" href="#tierra" data-md="/md/tierra.md"></a>
<a id="tip-agua" href="#agua" data-md="/md/agua.md"></a>
<a id="tip-cuerpo" href="#cuerpo" data-md="/md/cuerpo.md"></a>
<a id="tip-sombra" href="#sombra" data-md="/md/sombra.md"></a>
<a id="tip-cosmos" href="#cosmos" data-md="/md/cosmos.md"></a>
<a id="tip-practica" href="#practica" data-md="/md/practica.md"></a>
<a id="tip-olvido" href="#olvido" data-md="/md/olvido.md"></a>
</div>
<script type="module">
import init from "./pkg/gioser_web.js";
// Mapa: cuadrante hex → camino md
const Q_TO_CAMINO = {
'00': 'aire', // Bare-Metal → Software
'01': 'fuego', // Systems → Quién Soy
'02': 'tierra', // Simulators → Manifiesto
'03': 'agua', // Core → Mística
};
// JS de portada: hover/click en cuadrantes
const portada = document.getElementById('portada');
const deck = document.getElementById('deck');
const controls = document.getElementById('global-page-controls');
const reticulaV = document.querySelector('.reticula-v');
const reticulaH = document.querySelector('.reticula-h');
const reticulaC = document.querySelector('.reticula-center');
// Hover en cuadrantes ilumina portada
document.querySelectorAll('.cuadrante').forEach(q => {
q.addEventListener('mouseenter', () => {
portada.classList.add('portada-hover');
});
q.addEventListener('mouseleave', () => {
portada.classList.remove('portada-hover');
});
q.addEventListener('click', function(e) {
const hex = this.dataset.q;
const camino = Q_TO_CAMINO[hex] || 'aire';
// cerrar portada
portada.classList.add('portada-closing');
// mostrar deck
deck.removeAttribute('aria-hidden');
controls.style.opacity = '1';
controls.style.pointerEvents = 'auto';
// navegar vía hash para que el WASM lo atrape
history.pushState({}, '', '/estudio/' + camino);
window.dispatchEvent(new PopStateEvent('popstate'));
});
});
// Botón home en taskbar: volver a portada
document.querySelectorAll('[data-home]').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
portada.classList.remove('portada-closing');
deck.setAttribute('aria-hidden', 'true');
controls.style.opacity = '0';
controls.style.pointerEvents = 'none';
history.pushState({}, '', '/');
});
});
// Importar WASM existente
import init from "/pkg/gioser_web.js";
init().catch(err => {
console.error(err);
document.body.insertAdjacentHTML("beforeend",
document.body.insertAdjacentHTML('beforeend',
'<pre style="color:#f59056;position:fixed;left:1rem;bottom:1rem;max-width:90vw;white-space:pre-wrap;font-family:monospace;z-index:9999">' +
String(err) + '</pre>');
});
+127
View File
@@ -0,0 +1,127 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>GioSer · En el centro, el ser</title>
<link rel="stylesheet" href="/styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Inter:wght@300;500;600&family=JetBrains+Mono:wght@400;600&display=swap">
</head>
<body>
<canvas id="gioser-canvas" aria-hidden="true"></canvas>
<main id="tips" aria-label="Cuatro cardinales">
<!-- NORTE (aire): SOFTWARE · Tecnología — circuito + nodos -->
<a id="tip-aire" class="tip tip-aire" href="#aire" data-md="/md/aire.md" aria-label="Software · Tecnología">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<rect x="18" y="18" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.7" rx="1"/>
<circle cx="24" cy="24" r="2" fill="currentColor"/>
<path d="M22 18 V14 M26 18 V14 M22 30 V34 M26 30 V34 M18 22 H14 M18 26 H14 M30 22 H34 M30 26 H34"
stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<circle cx="10" cy="10" r="1.6" fill="currentColor" opacity="0.7"/>
<circle cx="38" cy="10" r="1.6" fill="currentColor" opacity="0.7"/>
<circle cx="10" cy="38" r="1.6" fill="currentColor" opacity="0.7"/>
<circle cx="38" cy="38" r="1.6" fill="currentColor" opacity="0.7"/>
<path d="M11 11 L17 17 M37 11 L31 17 M11 37 L17 31 M37 37 L31 31"
stroke="currentColor" stroke-width="1.1" opacity="0.45"/>
</svg>
<span class="tip-label">SOFTWARE</span>
<span class="tip-sub">Tecnología</span>
</a>
<!-- ESTE (fuego): QUIÉN SOY · Bitácora — libro abierto -->
<a id="tip-fuego" class="tip tip-fuego" href="#fuego" data-md="/md/fuego.md" aria-label="Quién Soy · Bitácora">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<path d="M8 14 L24 18 L40 14 V36 L24 32 L8 36 Z"
fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
<path d="M24 18 V32" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 22 H21 M12 26 H21 M12 30 H19" stroke="currentColor" stroke-width="1.1" opacity="0.65" stroke-linecap="round"/>
<path d="M27 22 H36 M27 26 H36 M29 30 H36" stroke="currentColor" stroke-width="1.1" opacity="0.65" stroke-linecap="round"/>
</svg>
<span class="tip-label">QUIÉN SOY</span>
<span class="tip-sub">Bitácora</span>
</a>
<!-- SUR (tierra): MANIFIESTO · Invariantes — hexagrama + círculo -->
<a id="tip-tierra" class="tip tip-tierra" href="#tierra" data-md="/md/tierra.md" aria-label="Manifiesto · Invariantes">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<circle cx="24" cy="24" r="18" fill="none" stroke="currentColor" stroke-width="1.4" opacity="0.55"/>
<path d="M24 8 L39 32 L9 32 Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
<path d="M24 40 L9 16 L39 16 Z" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.85" stroke-linejoin="round"/>
<circle cx="24" cy="24" r="1.8" fill="currentColor"/>
</svg>
<span class="tip-label">MANIFIESTO</span>
<span class="tip-sub">Invariantes</span>
</a>
<!-- OESTE (agua): MÍSTICA · Espiritualidad — ojo en triángulo -->
<a id="tip-agua" class="tip tip-agua" href="#agua" data-md="/md/agua.md" aria-label="Mística · Espiritualidad">
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
<path d="M24 6 L42 40 L6 40 Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<path d="M13 26 Q24 16 35 26 Q24 34 13 26 Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
<circle cx="24" cy="26" r="3.2" fill="currentColor"/>
<circle cx="22.5" cy="24.5" r="0.9" fill="rgba(255,255,255,0.55)"/>
</svg>
<span class="tip-label">MÍSTICA</span>
<span class="tip-sub">Espiritualidad</span>
</a>
<!-- Tips ocultos para navegación JS: cuerpo, sombra, cosmos, practica, olvido -->
<a id="tip-cuerpo" class="tip tip-cuerpo tip-hidden" href="#cuerpo" data-md="/md/cuerpo.md" aria-hidden="true"></a>
<a id="tip-sombra" class="tip tip-sombra tip-hidden" href="#sombra" data-md="/md/sombra.md" aria-hidden="true"></a>
<a id="tip-cosmos" class="tip tip-cosmos tip-hidden" href="#cosmos" data-md="/md/cosmos.md" aria-hidden="true"></a>
<a id="tip-practica" class="tip tip-practica tip-hidden" href="#practica" data-md="/md/practica.md" aria-hidden="true"></a>
<a id="tip-olvido" class="tip tip-olvido tip-hidden" href="#olvido" data-md="/md/olvido.md" aria-hidden="true"></a>
</main>
<!-- DECK: contenedor único con strip horizontal. Las páginas se inyectan
dinámicamente desde WASM. vista-web maneja swipe + snap. -->
<div id="deck" class="deck vista-deck" aria-hidden="true">
<div id="deck-strip" class="deck-strip vista-strip" role="region" aria-label="Vistas abiertas"></div>
</div>
<!-- Controles fijos de página: minimizar y cerrar (siempre en el DOM,
ocultos por CSS hasta que el deck se abre) -->
<div class="page-controls" id="global-page-controls" style="opacity:0;pointer-events:none;">
<button class="page-control-btn page-minimize" data-minimize="" type="button" aria-label="Minimizar">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19 H19" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</button>
<button class="page-control-btn page-close" data-close-page="" type="button" aria-label="Cerrar">×</button>
</div>
<!-- TASKBAR estilo Windows: home + GioSer + tabs + copyleft -->
<nav class="taskbar" aria-label="Barra de vistas">
<button class="taskbar-home" data-home aria-label="Volver al home" type="button">
<svg viewBox="0 0 24 24" class="taskbar-home-glyph" aria-hidden="true">
<path d="M3 12 L12 3 L21 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M5 11 V20 H10 V14 H14 V20 H19 V11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<circle cx="12" cy="17" r="0.8" fill="currentColor"/>
</svg>
</button>
<a class="taskbar-brand" data-home href="#home" aria-label="GioSer · home">
<span>Gio</span><span class="brand-dot">·</span><span>Ser</span>
</a>
<span class="taskbar-divider" aria-hidden="true"></span>
<ul class="taskbar-list" id="taskbar-list" role="presentation"></ul>
<span class="taskbar-spacer" aria-hidden="true"></span>
<a class="taskbar-credit" href="https://sergio.gioser.net" target="_blank" rel="noopener noreferrer" aria-label="Copyleft sergio arroba gioser punto net">
<span class="copyleft-mark" aria-hidden="true">©</span>
<span class="taskbar-credit-text">sergio@gioser.net</span>
</a>
</nav>
<script type="module">
import init from "/pkg/gioser_web.js";
init().catch(err => {
console.error(err);
document.body.insertAdjacentHTML("beforeend",
'<pre style="color:#f59056;position:fixed;left:1rem;bottom:1rem;max-width:90vw;white-space:pre-wrap;font-family:monospace;z-index:9999">' +
String(err) + '</pre>');
});
</script>
</body>
</html>
+75
View File
@@ -0,0 +1,75 @@
# Cosmovisión · Los 4 Elementos
> *Tierra, Agua, Aire, Fuego. No son decoración. Son la sintaxis del estudio.*
Este camino es el marco conceptual que organiza los cuatro caminos
originales. Los elementos no son metáforas poéticas: son arquetipos
operativos que describen modos de ser, hacer y relacionarse.
## Los Cuatro Caminos
### Tierra (kay)
Lo que se sostiene. Lo firme. El cuerpo, la materia, la invariante.
El camino de Tierra es el manifiesto: las axiomas que no cambian, el
protocolo de Presencia, el algoritmo de perdón. Es la base del sistema.
Sin Tierra, los demás elementos flotan sin anclaje.
Tierra pregunta: *¿Qué es verdad aunque todo lo demás cambie?*
### Agua (uku)
Lo que fluye. La emoción, la intuición, el vínculo con el misterio.
El camino de Agua es la mística: la espiritualidad aplicada, la
meditación, la ceremonia, el silencio. No es adorno: es el disolvente
que evita que el sistema se cristalice en dogma.
Agua pregunta: *¿Qué se mueve cuando dejo de controlar?*
### Aire (logos)
Lo que circula. La información, el código, la conexión.
El camino de Aire es el software: las herramientas técnicas, los
sistemas distribuidos, la IA aplicada, el open source. Es la red
que conecta los nodos del estudio.
Aire pregunta: *¿Qué podemos compartir que multiplique inteligencia?*
### Fuego (nomos)
Lo que transforma. La identidad, la bitácora, el testimonio.
El camino de Fuego es el quién soy: la crónica personal, el
testimonio de alguien que practica mantenerse despierto. Es el
calor humano que evita que el sistema sea pura abstracción.
Fuego pregunta: *¿Quién soy cuando dejo de ser lo que aprendí?*
## El centro
Los cuatro elementos no son jerárquicos. Son un campo. El centro
es la Presencia — el punto fijo desde el cual todos se experimentan.
*Cada elemento contiene a los otros.* Tierra sin agua se petrifica.
Agua sin tierra se desborda. Aire sin fuego es frío. Fuego sin aire
se sofoca.
## Integración
Los caminos nuevos — cuerpo, sombra, práctica, olvido — no son
elementos adicionales. Son **modos de aplicación** de los cuatro
elementos en dominios específicos de la experiencia:
- **Cuerpo**: Tierra + Agua aplicados a la somática
- **Sombra**: Agua + Fuego aplicados al inconsciente
- **Práctica**: los 4 en acción concreta
- **Olvido**: Aire + Tierra en modo de liberación
## Próximamente
*Este marco se irá expandiendo con diagramas, referencias a la
tradición hermética y aplicaciones concretas de cada elemento en
el trabajo diario.*
+52
View File
@@ -0,0 +1,52 @@
# El Cuerpo como Portal
> *La respiración como ancla. El movimiento como oráculo. La carne como templo.*
No hay despertar sin cuerpo. La presencia puede pensarse desde la mente,
pero se encarna — literalmente — en el cuerpo. Este camino explora la
dimensión somática del trabajo interior.
## Respiración
La respiración es el puente más directo entre lo voluntario y lo autónomo.
Cuando no sabes qué hacer, respirar es siempre una respuesta válida.
Prácticas que sostienen el trabajo respiratorio:
- **Respiración diafragmática 4-7-8**: inspira 4", retén 7", exhala 8".
Dice el sistema nervioso que se regula solo si se lo dejas claro.
- **Pausa espiratoria**: exhalar completo y quedarse vacío unos segundos.
El silencio entre respiros es la presencia más limpia.
- **Respiración cuadrada**: 4 tiempos iguales (inspirar, retener, exhalar,
retener). Sin nombre fancy: simplemente simetría.
## Movimiento consciente
El cuerpo no es una carga que la mente debe arrastrar. Es el vehículo de la
atención. Moverse con presencia es una práctica completa.
- **Caminar meditativo**: pasos lentos, sentir el peso que se transfiere,
el contacto con el suelo.
- **Estiramientos** al despertar: 5 minutos de elongación consciente antes
de agarrar el teléfono.
- **Sacudidas**: literalmente agitar el cuerpo para resetear el estado
nervioso. Los animales lo hacen instintivamente.
## El cuerpo como sensor
Las emociones no son conceptuales. Son patrones somáticos: tensión en el
pecho, nudo en la garganta, mariposas en el estómago.
Aprender a leer estas señales es un alfabeto que pocos enseñan:
1. **Escaneo corporal rápido**: de pies a cabeza, 30 segundos, notar sin
cambiar nada.
2. **Mapa de tensión**: en qué parte del cuerpo viven mis patrones habituales?
Mandíbula? Hombros? Pelvis? Cada cual tiene su geografía.
3. **El suspiro como dato**: si suspiras hondo sin pensar, tu cuerpo te está
diciendo algo que quizás la mente ignora.
## Próximamente
*Acá se irá profundizando en técnicas somáticas, ejercicios de presencia
corporal y la integración del movimiento como práctica espiritual.*
+76
View File
@@ -0,0 +1,76 @@
# El Arte de Olvidar
> *No se trata de recordar más. Se trata de poder soltar.*
Olvidar no es fallo de memoria. Es una capacidad activa — quizás la más
subestimada del trabajo interior. Este camino explora el desaprender,
soltar la identidad, vaciar el caché de la memoria complementando el
algoritmo de perdón del Manifiesto.
## Por qué olvidar es necesario
La memoria no es un disco duro. Es un proceso reconstructivo que se
actualiza cada vez que recordamos. Eso significa que el pasado no está
fijo: se reescribe en cada recuperación.
Si no olvidamos, cargamos con versiones obsoletas de nosotros mismos.
El rencor, la identificación con el trauma, la historia personal que
repetimos como un mantra — todo eso es memoria no liberada.
El *Algoritmo de perdón* del Manifiesto describe el protocolo formal.
Acá exploramos la práctica cotidiana.
## Prácticas de olvido activo
### 1. Reencuadre deliberado
Cuando una memoria perturbadora aparezca:
1. Reconócela sin resistencia.
2. Respira hondo.
3. Pregunta: *¿Esta versión de los hechos sigue siendo cierta?*
4. Pregunta: *¿Qué necesitaría soltar para que esta memoria pierda su carga?*
5. Reescribe mentalmente la escena desde otra perspectiva — desde afuera,
desde el otro, desde el testigo.
### 2. La ceremonia de cierre
Para memorias que se niegan a disolverse:
1. Escribe la memoria en detalle. Todo lo que carga.
2. Siéntate con el texto. Léelo en voz alta.
3. Quema el papel (con seguridad). O entiérralo. O arrójalo al agua.
4. El gesto físico ancla el cambio que la mente sola no completa.
### 3. Vaciar el caché diario
Antes de dormir:
1. Repasa el día como una película rápida.
2. Identifica los momentos que todavía tienen carga emocional.
3. Respira en cada uno hasta que la carga se normalice.
4. Suelta. Literalmente: di "suelto" o exhala con intención.
### 4. Desaprender una creencia
Elige una creencia sobre ti mismo que ya no te sirve:
- "Yo soy tímido"
- "Yo no sirvo para X"
- "A mí siempre me pasa Y"
Durante 30 días, actúa *como si* esa creencia no fuera cierta.
No necesitas creer lo opuesto. Solo suspender la creencia anterior.
## Olvido y algoritmo de perdón
El algoritmo del Manifiesto (escaneo → anclaje en P → remuestreo →
reescritura → garbage collection) es la versión técnica. Las prácticas
de acá son la versión accesible para el día a día.
El olvido no es borrar. Es crear espacio para lo nuevo.
## Próximamente
*Técnicas avanzadas de desidentificación, protocolos de olvido
asistido y textos de referencia sobre el tema.*
+75
View File
@@ -0,0 +1,75 @@
# Prácticas de Transformación
> *No basta comprender. Hay que hacer. Y hay que hacer de nuevo mañana.*
Este camino recolecta ejercicios concretos — no teoría, no contemplación,
sino prácticas que se hacen. Cada una con instrucciones claras, duración
estimada y criterios para saber si funcionó.
## Respiración consciente (10 min)
**Objetivo**: anclar la atención en el cuerpo a través del ritmo respiratorio.
**Instrucciones**:
1. Siéntate en una posición cómoda, columna erguida pero no rígida.
2. Cierra los ojos o deja la mirada suave en un punto.
3. Lleva toda la atención a la sensación del aire entrando y saliendo.
Puede ser en las fosas nasales, en el pecho o en el abdomen.
4. Cuando la mente se vaya (y se irá), vuelve a traerla. Sin drama.
5. Después de unos minutos, cuenta las exhalaciones del 1 al 10.
Si pierdes la cuenta, empieza de nuevo.
**Señal de efectividad**: al terminar, notas que el ritmo cardíaco bajó
o que hay una pausa natural entre respiros.
## Meditación de Presencia (20 min)
**Objetivo**: habitar P (Presencia) sin objeto de meditación.
**Instrucciones**:
1. Siéntate. Sin buscar un estado especial.
2. No sigas la respiración. No repitas un mantra. No visualices nada.
3. Solo quédate presente. Sin hacer nada para lograrlo.
4. Cuando notes que estás en un pensamiento, simplemente nota: "pensando".
Sin juicio. Y suelta.
5. El "suelta" no requiere acción. Solo deja de sostener el pensamiento.
**Nota**: esta práctica es difícil. Sentirás aburrimiento, sueño,
inquietud. Todo eso es parte de la práctica. No la estás haciendo mal.
**Señal de efectividad**: en algún momento hay una pausa donde el mundo
se siente más *real* que los pensamientos sobre el mundo.
## Registro de chips (15 min diarios)
**Objetivo**: identificar patrones cognitivos recurrentes.
**Formato**: diario con tres columnas.
| Fecha | Gatillo | Patrón | Respuesta |
|-------|---------|--------|-----------|
| hoy | correo del trabajo | "no soy suficiente" | apretar mandíbula |
| hoy | discusión con X | "siempre me pasa igual" | retirarme |
Sin interpretación. Solo registro. Los patrones se vuelven visibles
después de 2-3 semanas.
**Criterio**: si después de un mes no ves patrones, probablemente no
estás siendo honesto en el registro.
## Acecho de un patrón (1 semana)
**Objetivo**: observar un chip cognitivo sin intervenir.
1. Elige UN patrón de tu registro de chips. El que más se repita.
2. Durante una semana, solo obsérvalo cuando aparezca.
3. No intentes cambiarlo. No lo analices. Solo nótalo.
4. Anota cada aparición: contexto, intensidad (1-10), duración.
**Lo que sucede**: sin intervención, el sistema empieza a autorregularse.
La mera observación sostenida erosiona el atractor.
## Próximamente
*Más prácticas: meditación caminando, escaneo corporal guiado,
ejercicios de acecho avanzados y protocolos de intervención.*
+69
View File
@@ -0,0 +1,69 @@
# Trabajo con la Sombra
> *Lo negado. Lo que no quiero ver. Lo que me gobierna desde la oscuridad.*
La sombra no es el enemigo. Es todo aquello que la conciencia no ha integrado:
los patrones automáticos, las reacciones que juré que no tendría, las partes
de mí que niego. Trabajar con la sombra no es eliminarla — es hacerla
consciente.
## ¿Qué es la sombra?
En el marco de este estudio, la **Sombra (S)** es el conjunto de chips
cognitivos no escaneados. Patrones que operan por debajo del umbral de
la Presencia (P). No son malos. Son inconscientes.
Se manifiestan como:
- **Proyección**: lo que me irrita en otros suele ser lo que no acepto en mí.
- **Reactividad automática**: una respuesta desproporcionada que me sorprende
a mí mismo.
- **Patrones recurrentes**: las mismas situaciones, las mismas personas, los
mismos conflictos en bucle.
- **Negación**: "eso no soy yo" dicho con demasiada convicción.
## Protocolo de integración
No se trata de iluminar cada rincón oscuro con una linterna. Se trata de
ampliar la capacidad de albergar contradicción.
**Fase 1 — Cartografía de activación**
Llevar un registro de momentos de alta reactividad emocional. Anotar:
qué pasó, qué sentí, qué pensé, qué hice. Sin juzgar. Sin interpretar.
Solo registrar.
Con el tiempo emergen patrones.
**Fase 2 — Acecho**
Elegir un patrón identificado y observarlo activamente durante una semana.
No intervenir. Solo notar: cuándo aparece, qué lo gatilla, cómo se siente
en el cuerpo.
Acechar no es cazar. Es conocer el territorio.
**Fase 3 — Conversación con la sombra**
Técnica: escribir un diálogo con la parte rechazada. Ponerle voz. Preguntarle
qué necesita, qué protege, qué quiere. No asumir que sabes la respuesta.
**Fase 4 — Integración**
Encontrar un lugar funcional para esa parte en la vida consciente. No es
eliminarla: es darle un asiento en la mesa. A veces lo que la sombra protege
es legítimo — solo se expresaba mal.
## Herramientas cotidianas
- **El espejo**: preguntar a alguien de confianza "¿qué patrón ves en mí que
yo no veo?"
- **La pausa**: cuando sientas reactividad, no actuar. Esperar 24 horas antes
de responder.
- **El diario de sueños**: los sueños a menudo expresan material de sombra
que la vigilia censura.
## Próximamente
*Se irán añadiendo ejercicios específicos, técnicas gestálticas y referencias
a autores que han trabajado este terreno (Jung, Perls, Bly, Zweig).*
+178 -28
View File
@@ -1,39 +1,189 @@
# Manifiesto · Invariantes
# Modelo Triplanar
> *Lo que no cambia. La piedra de toque.*
## Ontología del Campo Consciente
Acá vive el manifiesto de GioSer: las **invariantes** que sostienen
todo lo demás. Lo que no negocio, lo que define la forma del trabajo
antes que cualquier proyecto particular.
---
## Invariantes
## I. Tesis Fundamental
Cosas que considero **no-negociables** en cómo hago el trabajo:
El primer grado de evidencia es la propia existencia. Desde ese punto de partida: el universo mismo es consciente. La conciencia no es un epifenómeno que emerge en sistemas complejos — es una propiedad del campo en sí mismo.
- **Código abierto por defecto.** Si tiene sentido, se publica.
- **Honestidad por encima de marketing.** No prometo lo que no puedo
cumplir, ni vendo lo que no probé.
- **El cuerpo es infraestructura.** Cuidarlo es parte del trabajo, no
opuesto al trabajo. Sin cuerpo no hay nada.
- **Las ideas se prueban escribiéndolas.** Si no hay documento, todavía
no existe la idea.
- **Compatibilidad hacia abajo > novedad arriba.** Las invariantes
duran, las modas no.
- **Una sola voz.** Lo que digo en privado coincide con lo que publico.
Todo es consciente. Cada posición del plano multidimensional es un nodo de consciencia con su propio punto de vista. El contenido y la naturaleza de su percepción están determinados por su posición en ese campo.
## Por qué un manifiesto
No hay una "realidad base" que contenga a los planos. Son ontologías paralelas, cada una completa y verdadera desde su propia posición.
Porque sin invariantes, cada decisión es ad hoc. Tener un set chico de
principios reduce la energía gastada en cada elección — y deja en
claro cuándo estoy contradiciéndome.
---
## Revisión
## II. Plano 1 — Sensible
Este manifiesto se revisa una vez al año, no antes. Si una invariante
deja de aplicarse, se quita con una explicación pública.
<code>
S₁ = {afección sensorial pura}
∄ "yo" en S₁
∄ cuerpo en S₁
S₁ solo contiene datos: color, sonido, textura, temperatura
</code>
## Próximamente
Lo que hay: el toque de los sentidos, sin cuerpo que los reporte. El cuerpo físico como tal no existe desde aquí. Hay afección sensible pura — colores, sonidos, texturas — sin un "yo" al que pertenezcan.
*Esta sección va a recibir el manifiesto completo + revisiones
históricas. Por ahora este placeholder verifica el tema **tierra**
(ocre cálido).*
La reconstrucción del cuerpo es posterior y ocurre en otro plano (§III). Para el plano sensible solo hay datos sensoriales sin un centro que los unifique.
El tiempo se experimenta como flujo sucesivo. El instante presente es un punto de fuga entre lo que fue y lo que será — nunca se puede "agarrar".
La constatación física en su nivel más burdo: el cuerpo como evidencia inmediata con correlato material. Los pensamientos como circuitos eléctricos y químicos funcionando como mecanismo determinista.
**Verdad absoluta desde esta posición:** solo existe la afección sensible. El tiempo es real como sucesión. El cuerpo no está presente como tal.
---
## III. Plano 2 — Alma / Mental
<code>
S₂ = {imágenes mentales, conceptualizaciones, campo relacional}
cuerpo(S₂) = imagen mental del cuerpo, no carne
t(S₂) = objeto bloque (pasado, presente, futuro coexisten)
campo(S₂) = f(interacción de nodos de consciencia)
</code>
Lo que hay: la imagen mental de las cosas. Aquí viven las conceptualizaciones.
El cuerpo no existe como carne — existe como imagen mental del cuerpo. Es una reconstrucción desde los datos del plano sensible.
El alma surge de la organización e interrelación masiva de distintos puntos del universo. La interacción de nodos de consciencia genera un campo. Dos vías que convergen al mismo resultado:
- Si el campo del alma ya existe independiente de la tierra → se hereda
- Si no existe → se genera desde la interacción
El efecto final es el mismo: dos planos coexistiendo (S₁ + S₂).
### Tiempo
Aquí el tiempo ya no es flujo vivido. Es una cosa contenida. Toda la línea temporal se puede ver como una forma — pasado, presente y futuro coexistiendo como bloque. El alma puede ver la dimensión completa del tiempo desde fuera.
### El conflicto humano
<code>
pecado original = conflicto de transición entre zonas biológicas
no es moral — es el bamboleo de una especie entre dos equilibrios
</code>
El "pecado original" no es una mancha moral. Es la descripción del conflicto que aparece cuando una especie animal se asoma fuera de la corriente de la selva sin haber terminado el movimiento.
**Verdad absoluta desde esta posición:** el alma existe como campo. El tiempo es un objeto que se puede contemplar. El cuerpo es solo imagen mental.
---
## IV. Plano 3 — Presencia (0, 0)
<code>
S₃ = {fenomenología pura}
P = (0, 0, 0)
∀s ∈ S : s P = v
t(S₃) = ∅ // el tiempo no es categoría aplicable
S₃ es un punto que contiene el universo completo
</code>
Lo que hay: fenomenología pura. Un punto es el universo completo, porque todo lo demás es solo contenido de su visión.
El Nivel 1 de evidencia solo reporta la propia existencia — y eso es suficiente. Presencia como origen del sistema de coordenadas: (0, 0).
El tiempo aquí no existe. Solo hay instante presente, y ese instante es lo único que hay. No es que "se esté en el presente" — el presente es la totalidad. La sucesión ni siquiera es una categoría aplicable.
**Verdad absoluta desde esta posición:** solo existe el instante presente, y ese instante lo contiene todo.
---
## V. Ontología de la Vida
### La vida no es delimitable
Lo que comúnmente llamamos "vida" es sesgo de cercanía — lo que más se parece a nuestra forma de vida. Pero todo es vida, en diferentes expresiones:
- Unas se reproducen, otras no
- Unas son piedras, otras son bacterias, otras son inteligencia artificial
- La vida es la misma actividad de las partículas siguiendo su libertad de elección con forma partícula/onda
### Zonas biológicas
Dentro del mundo biológico existen "zonas" que cumplen leyes particulares donde coinciden:
- La influencia del hábitat
- La herencia genética
- Los eventos contingentes
La zona más cercana a nosotros es entre la tierra y el cielo. Es un lugar de dualidad que forma cuerpos con pies, cabeza, peso y la necesidad de hacer ejercicio.
### Postura erecta
<code>
pararse en dos pies = sistema de equilibrio en desequilibrio permanente
no es detalle biomecánico — es la forma física del desequilibrio
el cuerpo se sostiene cayendo hacia adelante y recuperándose
</code>
En algún punto de la evolución, la especie animal asomó la cabeza del mundo animal y comenzó a entreverse donde se es humano. No hay diferencias de valor entre zonas — solo zonas distintas. Pero la especie aún no se ha actualizado: empezó un viaje donde atraviesa un desequilibrio para volver a un nuevo equilibrio.
### El ego como síntoma
<code>
ego = mareo del viaje entre dos zonas
= síntoma de estar entre: vista fuera de la selva,
cuerpo aún no reequilibrado en la nueva posición
</code>
El ego no es error ni enfermedad. Es el mareo del viaje que enfrentó una especie animal incursionando en la humanidad. Es el síntoma de estar entre dos zonas: la vista ya no está en la selva, pero el cuerpo aún no se reequilibró en la nueva posición.
El conflicto humano (pecado original, §III) es el bamboleo de ese pasaje.
Las distintas culturas, sistemas morales, tecnologías — son intentos parciales de encontrar el nuevo piso. La especie sigue en el loop del desequilibrio, generando versiones temporales de lo humano que intentan estabilizarse sin lograrlo del todo. El movimiento no ha terminado.
---
## VI. Resumen
| Plano | Contenido | Tiempo | Cuerpo | Verdad desde su posición |
|---|---|---|---|---|
| 1. Sensible | Afección sensorial pura | Flujo sucesivo | No existe — solo datos | Solo existe el toque de los sentidos |
| 2. Alma/Mental | Imagen mental, conceptualizaciones, campo | Bloque (visible desde fuera) | Imagen mental del cuerpo | Solo existe el alma y sus reconstrucciones |
| 3. Presencia (0,0) | Fenomenología pura | No-tiempo / solo instante presente | No aplica | Solo existe el presente que lo contiene todo |
Cada plano es una verdad absoluta de por sí. No hay contradicción entre ellos — son verdades de posiciones distintas en el campo.
---
## VII. Operadores prácticos
A continuación, herramientas que operan dentro de los planos — principalmente en S₂, donde ocurren los chips cognitivos y las conceptualizaciones.
### Chip cognitivo (Cᵢ)
<code>
Cᵢ ⊂ S₂ = atractor local en el campo mental
bucle de retroalimentación que estabiliza patrones subóptimos
</code>
Ejemplos: rumiación, diálogo interno autoalimentado, pánico.
### Pipeline
1. **Detección**: identificar el patrón recurrente en S₂. T (testigo desde S₃) puede detectar sin intervenir.
2. **Pausa del script**: T interrumpe la ejecución automática. El bucle se corta al dejar de identificarse con él.
3. **Modulación**: herramientas para bajar la ganancia del sistema (respiración, movimiento, frío, silencio).
4. **Redirección plástica**: prácticas sostenidas que refuerzan nuevas rutas.
5. **Mantenimiento**: rutinas preventivas, análisis de triggers.
### Métrica
<code>
Co = señal / ruido psicofisiológico
H = H(señal conductual) // entropía temporal
IR = t(baseline | perturbación)
PA = Δ(IR, Co) tras intervención
</code>
### Protocolo
**Diario**: reactividad (0-10), duración, intensidad (0-10).
**Escalonamiento**: detección → pausa → modulación → consolidación → mantenimiento.
**Auditoría**: cada 4-12 semanas — Co, IR, H, PA. Sin mejora → ajustar.
+39
View File
@@ -0,0 +1,39 @@
# Manifiesto · Invariantes
> *Lo que no cambia. La piedra de toque.*
Acá vive el manifiesto de GioSer: las **invariantes** que sostienen
todo lo demás. Lo que no negocio, lo que define la forma del trabajo
antes que cualquier proyecto particular.
## Invariantes
Cosas que considero **no-negociables** en cómo hago el trabajo:
- **Código abierto por defecto.** Si tiene sentido, se publica.
- **Honestidad por encima de marketing.** No prometo lo que no puedo
cumplir, ni vendo lo que no probé.
- **El cuerpo es infraestructura.** Cuidarlo es parte del trabajo, no
opuesto al trabajo. Sin cuerpo no hay nada.
- **Las ideas se prueban escribiéndolas.** Si no hay documento, todavía
no existe la idea.
- **Compatibilidad hacia abajo > novedad arriba.** Las invariantes
duran, las modas no.
- **Una sola voz.** Lo que digo en privado coincide con lo que publico.
## Por qué un manifiesto
Porque sin invariantes, cada decisión es ad hoc. Tener un set chico de
principios reduce la energía gastada en cada elección — y deja en
claro cuándo estoy contradiciéndome.
## Revisión
Este manifiesto se revisa una vez al año, no antes. Si una invariante
deja de aplicarse, se quita con una explicación pública.
## Próximamente
*Esta sección va a recibir el manifiesto completo + revisiones
históricas. Por ahora este placeholder verifica el tema **tierra**
(ocre cálido).*
+264
View File
@@ -0,0 +1,264 @@
# Manifiesto del Ser Desnudo
## I. El Origen: Nacer Humano y Desnudo
Existir no es un accidente de la marea. Es el acto supremo de una voluntad que ha elegido estar aquí.
Naciste porque quisiste nacer. Mereces esta bendición que es la existencia por el simple hecho de respirar. No has de hacer nada más.
Eres la semilla. Eres el puente viviente entre el misterio y la materia. Echa raíces profundas en la Madre Tierra. Levanta tu columna recta hacia el Padre Cielo.
En esa verticalidad, tú eres el equilibrio. Eres un dios caminando, magnífico en tu propia fragilidad.
Reconoce tu pequeña luz humana: eres como un infante que fantasea que ya creció, petulante al negar el suelo bajo sus pies, pero perfecto en su inmadurez.
> *No somos un ser que se transforma. Somos un transformar que se es.*
---
## II. Del Cuerpo Formal
Que la poesía no nos distraiga del rigor. Si la experiencia es un territorio, necesita mapa. No un mapa que pretenda ser el territorio — eso es idolatría — sino uno que permita navegarlo con precisión.
### Axioma 1 — Presencia como origen
Existe un punto de referencia universal en el espacio experiencial, llamado **Presencia** (P). Se define operacionalmente como el punto de auto-evidencia en el que la experiencia se registra sin identificarse con ella.
En coordenadas experienciales: **P = (0, 0, 0)**. Todo vector se mide desde aquí. No hay afuera de P porque P es el punto desde el cual todo afuera se define.
### Axioma 2 — Separación procesual
La experiencia se compone de dos capas:
- **Observador (O)**: idéntico a P. El testigo.
- **Flujo de datos (D)**: el conjunto de variables sensibles — pensamientos, emociones, percepciones, sensaciones corporales.
La independencia funcional se escribe: **O ∩ D = ∅**. No eres tus pensamientos. No es una metáfora: es una condición del sistema.
### Axioma 3 — Conservación de coherencia
La consistencia del sistema depende de reglas de interpretación (R). Las anomalías — depresión, confusión, pánico — no invalidan el axioma de existencia. Son fallas en R, no en P. El punto de referencia permanece.
### Axioma 4 — Instante recálculable
El presente (t) se recalcula en cada iteración del sistema. La memoria es un módulo accesible pero no fiduciario del presente. El pasado es estado registrado, no estado operativo continuo.
En presencia óptima, cada instante nace virgen. El peso de la memoria se aproxima a cero.
### Definiciones clave del sistema
**Testigo Trascendental (T)**: la función de observación asociada a P. No actúa, no juzga, no retiene. Solo registra.
**Chip cognitivo (Cᵢ)**: circuito cerrado de retroalimentación definido por un patrón recurrente en D. Un atractor local en el espacio de estados que estabiliza patrones subóptimos. Ejemplos: la rumiación, el diálogo interno, el miedo que se alimenta de sí mismo.
**Fricción (F)**: medida de resistencia interna al flujo de información. Análoga a una resistencia R en circuitos eléctricos. A mayor fricción, menor fluidez experiencial.
**Coherencia operativa (Co)**: grado de alineación entre energía disponible y eficiencia de procesamiento. Mayor Co = menor F.
**Amor operativo**: estado de mínima fricción y máxima fluidez informativa. Se define como el máximo de Co bajo restricciones energéticas del sistema. No es un sentimiento: es una propiedad del campo.
### Espacio de estados experienciales
Sea **S** el espacio topológico de estados. Cada punto s ∈ S representa una configuración completa de D en un instante t.
**P** es un punto fijo desde el cual se miden vectores proyectivos: **v = s P**.
El presente exhibe auto-similitud a escalas temporales y atencionales. La transformación **T: S → S** es iterativa y contractiva en presencia óptima, generando una huella residual **h(t)** que actúa como inicialización para la siguiente iteración.
La memoria **M** es un caché probabilístico: almacena distribuciones p(D | t Δ) usadas como prior para la interpretación presente. En presencia óptima, el peso de M se regulariza hacia cero.
---
## III. Dinámica: Fricción, Chip Cognitivo y Pipeline de Intervención
### Variables del sistema
| Variable | Símbolo | Naturaleza |
|---|---|---|
| Fricción | F(t) | ≥ 0, escalar |
| Resistencia | R(t) | f(Ego, Identificación) |
| Sufrimiento | S(t) | ∝ R(t) · Var(D(t)) |
El sufrimiento es intensidad de resistencia por variabilidad del flujo de datos. Cuando la resistencia es alta y los datos son turbulentos, el sistema vibra en disonancia.
### Ecuación operativa
La evolución del estado s(t) puede modelarse como:
> **ds/dt = G(s, u, t) α·R(s) + ξ(t)**
Donde:
- **G** captura la dinámica base del sistema — tu fisiología, tu temperamento, el ruido de fondo del mundo
- **u** son inputs externos — lo que comes, lo que lees, con quién hablas
- **α** escala la influencia de la resistencia R
- **ξ(t)** es ruido estocástico — el factor Dios, la mariposa en Pekín
### Aceptación como control
La aceptación no es resignación. Es una maniobra de control sobre R.
Cuando reduces R → 0 — cuando dejas de identificarte con el flujo — la dinámica se simplifica:
> **ds/dt ≈ G(s, u, t) + ξ(t)**
En ese límite, maximizas la capacidad de respuesta del sistema y minimizas las pérdidas por fricción. El sufrimiento tiende a su mínimo estructural.
### Pipeline de intervención sobre chips cognitivos
Un chip Cᵢ es un atractor local en S. Identificarlo y desmantelarlo es la práctica central. El pipeline tiene cinco fases:
**1. Detección**: monitorizar actividad — autoinforme, métricas fisiológicas, registros de comportamiento — para identificar estados repetitivos, su período y su trigger.
**2. Pausa del script**: función de interferencia ejecutada por T. Atención sostenida que interrumpe la ejecución automática. Corte la retroalimentación del chip.
**3. Modulación exógena**: uso controlado de herramientas para bajar la ganancia del sistema y aumentar plasticidad temporal. Pueden ser neuromoduladores, respiración, movimiento, silencio. El criterio no es dogmático sino pragmático.
**4. Redirección plástica**: prácticas sostenidas — meditación, terapia, entornos enriquecidos — para reforzar nuevas rutas sinápticas. Optimización por refuerzo gradual, no por voluntad heroica.
**5. Mantenimiento**: rutinas preventivas para evitar reimplantación del chip. Análisis de triggers, autoobservación periódica.
### Precauciones éticas
Las intervenciones farmacológicas deben ser supervisadas clínicamente. Reducir un fenómeno humano a un circuito no implica deshumanización: la validación fenomenológica es co-requisito. El modelo es una herramienta, no una sentencia.
### Modelo esquemático simple
Sea s un escalar que representa nivel de activación problemática (ansiedad, rumiación):
> **ds/dt = k·s α·R(s) + I(t) + ε(t)**
Donde k > 0 es amortiguamiento natural, R(s) = β·sⁿ (n ≥ 1), I(t) es entrada externa, ε ruido.
La aceptación reduce β → 0. La modulación reduce k o α temporalmente para permitir reconfiguración. Este esquema permite simular fases de recaída, plasticidad y estabilización.
---
## IV. El Laberinto de las Sombras
Te has perdido en el murmullo de las viejas formas mentales. Tus creencias son prisiones. Tus pensamientos son efímeras polillas relampagueantes que habitan tu estructura mecanizada.
Nadie puede entrar en lo más sagrado de tu ser sin tu permiso y tu decisión. Ni la sociedad, ni el sistema, ni la opresión. Eres libre de considerarte libre, o libre de considerarte un esclavo.
El saboteador no es un enemigo externo. Es tu tendencia a la "cómoda miseria". Es el miedo a despertar lo que te encadena a personajes que ya no te pertenecen.
> Lo que crees ser: etiquetas, memorias de dolor, un nombre con historia, un manojo de miedos y certezas.
>
> Lo que eres: presencia silenciosa que atestigua el tiempo, el espacio donde las nubes aparecen, una mirada transparente que no necesita nombres.
---
## V. Teoría de Campo Unificado: No-dualidad y Lógica de Conjuntos
### Formalización de la no-separación
Sea **U** el conjunto universal de la experiencia. Un individuo es un subconjunto **A ⊆ U**.
La experiencia de no-separación — la disolución del límite entre yo y mundo — se formaliza como una identidad funcional **A ≈ U** en términos de acceso y efecto causal. Es decir: para toda propiedad p relevante al procesamiento, p(A) = p(U) en su medida operativa.
No es una declaración metafísica. Es una condición del sistema que puede alcanzarse y medirse.
### Algoritmo de perdón (protocolo de limpieza)
El perdón no es un acto moral. Es un procedimiento operativo sobre la memoria:
1. **Escaneo de archivos**: identificación de memorias perturbadoras.
2. **Anclaje en P**: situarse en Presencia, fuera del flujo de datos.
3. **Remuestreo**: reproducir la memoria en estado de baja fricción (R ≈ 0).
4. **Reescritura contextual**: re-etiquetar la memoria con menor ganancia emocional.
5. **Garbage collection**: liberar patrones redundantes que consumen presupuesto energético sin servir al presente.
---
## VI. La Medicina y el Despertar
El encuentro con el sagrado Yagé no es una huida. Es un retorno violento y amoroso a la realidad. Es el espejo de tus animalismos, de tus dragones y tus monstruos.
¿Soportarás el abismo infinito que eres? La estructura de tu mundo se destruirá. Quedarás sin piso, sin razón. Morirán tus pasados mientras te aferras a ellos con las uñas rotas y el sudor en la frente.
El chamán es solo un humano falible. No es un dios, ni un papa, ni un maestro. Es un hermano que pone su esfuerzo al servicio. La verdadera maestra es la medicina misma, que extrae la esencia de la tierra para tocarte.
Sobre la impecabilidad del guerrero: asume la responsabilidad total. No se vale acceder a los antojos ni desfallecer ante la pereza. Sé indiviso en tus pensamientos, palabras y obras. No te entregues a la medicina como una hoja llevada por el viento. Entrégate como quien pone orden en su propio mundo.
El "santo dolor" es la medicina amarga que limpia la ceguera y funde el plomo que arrastras.
---
## VII. Métrica y Protocolo
### Medidas operativas
**Coherencia operativa (Co)**: ratio señal/ruido en indicadores psicofisiológicos y rendimiento atencional. Una medida burda pero útil del estado del sistema.
**Entropía dinámica (H)**: entropía temporal de la señal psico-conductual. Una disminución de H en presencia sostenida sugiere estabilización útil.
**Índice de reactividad (IR)**: tiempo de retorno al baseline después de una perturbación. Mide qué tan rápido se recupera el sistema.
**Plasticidad adaptativa (PA)**: capacidad de implementar y consolidar rutas alternativas. Se mide por la variación en IR y Co tras una intervención.
### Protocolo de práctica recomendada
**Rutina diaria**: 1020 minutos de atención sostenida anclada en P. No es meditación en el sentido clásico — es pausa del script, punto cero.
**Registro**: diario de triggers y respuestas con métricas simples: reactividad (010), duración, intensidad.
**Intervención escalonada**: detección → pausa → modulación (lo que funcione: respiración, movimiento, silencio, apoyo externo) → consolidación conductual → mantenimiento.
**Auditoría periódica**: medir Co, IR, H y PA cada 412 semanas. Ajustar protocolo según resultado.
---
## VIII. La Práctica del Instante
La disciplina no es un castigo. Es el arte de obedecerse a sí mismo. Es el vigor de un acecho constante sobre tus propios impulsos.
Caminar no es un esfuerzo por llegar a otro lado. Escucha bien: *"El pie que deja huella es el que deja su camino atrás."* Lo que hoy es tierra firme, mañana será nada.
Da cada paso para mantenerte de pie en el lugar al que ya estás llegando. Esto es atenta ecuanimidad: sentir la brisa y la tormenta, probar el sabor de la batalla sin que nada te arrastre.
Máximas del instante:
- Detén el mundo en tu cabeza para ver el mundo real.
- Cierra los ojos y mira; cierra la boca y canta.
- La claridad no es luz, es saber mirar en la oscuridad.
- Si quieres llegar, deja de dar pasos hacia el futuro.
---
## IX. Ontología, Epistemología y Telos
### Ontología
Esta propuesta no obliga a un monismo ontológico último. Ofrece una **ontología operativa**: entidades definidas por su función en el sistema. P, D, M, Cᵢ, R — existen en la medida en que operan. No se pronuncia sobre su existencia fuera del modelo.
### Epistemología
El conocimiento accesible es siempre modular y probabilístico. El observador T dispone de medios para validar hipótesis — autoexperimentación, medición, replicación — pero existen sesgos interpretativos y culturales que condicionan tanto R como G.
El criterio de verdad no es la correspondencia con una realidad externa inaccesible, sino la **coherencia operativa**: el modelo funciona si permite describir, predecir y modular con mayor eficacia que su ausencia.
### Telos: la finalidad funcional
La "meta" del sistema es maximizar Co (coherencia operativa) relativa a restricciones energéticas y contextuales. Esto se traduce en:
- Mayor adaptabilidad a entornos cambiantes.
- Menor sufrimiento medido como R · Var(D).
- Expansión de la capacidad para integrar variables — aumentar U efectivamente.
No hay un destino. Hay una dirección: reducir fricción, expandir presencia. El resto es paisaje.
---
## X. El Centro de la Nada
La rendición final es el portal a la libertad. Reconoce que no eres nada ante la inmensidad, y en esa nada, lo eres todo. *"Soy nada, pues soy tú mismo"*, susurra el alma.
El silencio no es ausencia de ruido. Es la presencia majestuosa que atestigua tanto el estruendo como la calma. Es el fondo infinito donde se proyecta tu existencia.
Eres un misterio que no tiene a quién preguntar. Eres mortal y eres divino. Eres tierra y eres cielo. Eres carne y eres espíritu eterno.
Acepta tu dualidad y quédate en el centro. Sé el amor que lo ve todo y a todo agradece. Todo está perdonado desde el principio.
> *Todo está bien aquí.*
Has vuelto a casa, al sagrado y eterno presente. Sonríe, ser humano, y entona la canción de la alegría.
+260
View File
@@ -0,0 +1,260 @@
# Manifiesto del Ser Desnudo
## Índice
- [I. El Origen](#i-el-origen)
- [II. Campo de aplicación](#ii-campo-de-aplicación)
- [III. Axiomas](#iii-axiomas)
- [IV. Definiciones](#iv-definiciones)
- [V. Dinámica](#v-dinámica)
- [VI. El Pipeline](#vi-el-pipeline)
- [VII. Campo Unificado](#vii-campo-unificado)
- [VIII. Métrica](#viii-métrica)
- [IX. Protocolo](#ix-protocolo)
- [X. Ontología](#x-ontología)
- [XI. Apéndice — modelo esquemático](#xi-apéndice--modelo-esquemático)
---
## I. El Origen
Existes y lo sabes. Ese saber no requiere prueba. Es anterior a cualquier pensamiento, emoción o percepción. Es el marco, no el contenido.
La existencia es enteramente consciente siempre. Un perro existe y lo sabe. Una piedra existe y lo sabe. La diferencia humana es que hay un cerebro que puede **conceptualizar** ese saber — nombrarlo, analizarlo, modelarlo, olvidarlo, negarlo. Eso no hace la existencia más real. Hace el modelo más complejo.
Nada que hacer. Ya estás aquí. El sistema ya está corriendo. El resto es optimización.
---
## II. Tesis
Dos niveles de evidencia:
**Primero**: existes y lo sabes. Eso no se demuestra — es el punto de partida.
**Segundo**: no puedes asegurar que todo sea consciente, pero puedes confirmarlo desde donde estás. El universo mismo es consciente. Todo lo es — cada posición del plano multidimensional tiene su propio punto de vista. El contenido y la naturaleza de su percepción están determinados por su posición en ese campo.
Un humano no es más consciente que una piedra. Solo tiene un sistema nervioso que le permite **conceptualizar** esa conciencia — nombrarla, modelarla, negarla. La piedra experimenta su propia existencia sin necesidad de nombrarla.
Este modelo opera desde esta tesis. No la demuestra — la toma como condición de trabajo. La matemática es incompleta pero autocoherente: las definiciones no se contradicen y cualquier contradicción aparente es error de aplicación, no del modelo. El criterio no es verdad metafísica sino **coherencia operativa**.
---
## III. Axiomas
### Axioma 1 — Presencia como origen
<code>
P = (0, 0, 0)
∀s ∈ S : s P = v
</code>
P es el punto de auto-evidencia desde el cual toda experiencia aparece. Ningún contenido cae fuera de P porque P es el sistema de coordenadas mismo.
### Axioma 2 — Separación funcional
<code>
O ≡ P
D = {pensamientos, emociones, percepciones, sensaciones}
O ∩ D = ∅
</code>
El observador no es el flujo de datos. Cuando esa línea se borra (identificación), el sistema pierde grados de libertad y se comporta como un circuito cerrado. Eso es un chip cognitivo (§VI).
### Axioma 3 — Conservación de coherencia
<code>
R = reglas de interpretación
A (anomalía) es fallo en R, no en P
P invariante bajo transformaciones de R
</code>
Ninguna crisis es terminal. El punto de referencia no se rompe — lo que falla es cómo lees el mapa.
### Axioma 4 — Instante recálculable
<code>
t ← t + 1 en cada iteración
M(t) = p(D | t-Δ) // memoria como prior probabilístico
P óptima: peso(M) → 0
</code>
El pasado no arrastra. Es un caché que el presente puede consultar o ignorar. §V expande la dinámica.
---
## IV. Definiciones
<code>
T — Testigo Trascendental.
f(P) que observa sin actuar, juzgar ni retener.
Cᵢ — Chip cognitivo.
Atractor local en S. Bucle de retroalimentación que estabiliza
patrones subóptimos (rumiación, diálogo interno, pánico).
F — Fricción.
Resistencia al flujo de información. Análoga a R en circuitos.
Co — Coherencia operativa.
Eficiencia del procesamiento. Mayor Co = menor F.
Amor operativo — Estado de mínima F y máxima fluidez.
Co máximo bajo restricciones energéticas.
</code>
---
## V. Dinámica
<code>
F(t) ≥ 0
R(t) = f(identificación, ego)
S(t) ∝ R(t) · Var(D)
</code>
El sufrimiento es el producto de la resistencia por la turbulencia de los datos. R alta + D caótico = disonancia.
<code>
Aceptación := R → 0
R ≈ 0 :
ds/dt ≈ G(s, u, t) + ξ(t)
// dinámica base + ruido, sin amplificación
</code>
Aceptar no es resignarse. Es bajar la resistencia. El sistema deja de forcejear consigo mismo. Lo que queda no es necesariamente placentero — pero no es una pelea. Ver §VIII.
La memoria no es un registro. Es una reconstrucción probabilística que el presente genera bajo demanda. Cada recuerdo es una versión nueva, no una copia. Eso implica que puedes reescribir la memoria. §VII formaliza esto como algoritmo de perdón.
---
## VI. El Pipeline
Cinco fases para intervenir sobre un chip Cᵢ:
### 6.1 Detección
Identificar el chip: patrón recurrente en D, trigger, período. T puede detectar sin intervenir.
### 6.2 Pausa del script
<code>T := interrumpir ejecución automática de Cᵢ</code>
No discutas el contenido del chip. Obsérvalo como fenómeno. El bucle se corta cuando dejas de identificarte con él.
### 6.3 Modulación exógena
Herramientas para bajar la ganancia del sistema: respiración, movimiento, frío, silencio, neuromoduladores (con supervisión clínica). El criterio es pragmático.
### 6.4 Redirección plástica
Prácticas sostenidas que refuerzan nuevas rutas: meditación, terapia, repetición deliberada. Refuerzo gradual, no voluntad heroica.
### 6.5 Mantenimiento
Rutinas preventivas. Análisis de triggers, autoobservación periódica. El chip puede re-implantarse si el contexto persiste.
---
## VII. Campo Unificado
<code>
U = conjunto universal de la experiencia
A ⊆ U
No-separación: A ≈ U
≡ ∀p relevante al procesamiento: p(A) = p(U)
</code>
La experiencia de unidad no es mística. Es un estado del sistema donde los bordes entre "yo" y "mundo" dejan de filtrar. §VIII da métricas.
<code>
Algoritmo de perdón:
for m in M_perturbadora:
1. anclar en P
2. reproducir m con R ≈ 0
3. peso_emocional(m) ← 0
4. if redundante(m): liberar(m)
</code>
El perdón no es moral. Es un procedimiento de limpieza de memoria: tomar un recuerdo que duele, reproducirlo sin identificación, quitarle la carga, soltarlo si no sirve.
---
## VIII. Métrica
<code>
Co = señal / ruido psicofisiológico
H = H(señal conductual) // entropía temporal
IR = t(baseline | perturbación)
PA = Δ(IR, Co) tras intervención
</code>
Heurísticas para evaluar el sistema: ¿la atención fluye con menos esfuerzo? ¿El estado mental es menos errático? ¿Te recuperas más rápido? ¿Los cambios se mantienen?
---
## IX. Protocolo
### Diario
- Reactividad (0-10)
- Duración del estado
- Intensidad (0-10)
### Escalonamiento
1. Detección
2. Pausa del script
3. Modulación
4. Consolidación
5. Mantenimiento
### Auditoría
Cada 4-12 semanas: Co, IR, H, PA. Sin mejora → ajustar protocolo.
---
## X. Ontología
<code>
Entidades: P, D, Cᵢ, R, T
Existencia: operativa (funcionan en el modelo)
Criterio de verdad: coherencia operativa
</code>
El modelo no afirma que la realidad sea así. Afirma que actuar como si fuera así mejora los resultados. Es una ontología instrumental.
Límites:
- Sesgos interpretativos y culturales en R
- Conocimiento probabilístico y parcial
- Validación fenomenológica co-requisito
<code>
Telos: max(Co) bajo restricciones energéticas
≡ mayor adaptabilidad + menor S + mayor U efectivo
</code>
---
## XI. Apéndice — modelo esquemático
<code>
ds/dt = -k·s - α·R(s) + I(t) + ε(t)
k > 0 amortiguamiento natural
R(s) = β·sⁿ (n ≥ 1) resistencia
α escala de resistencia
I(t) entrada externa
ε(t) ruido estocástico
Aceptación: β → 0
Modulación: k↑ o α↓
Regímenes:
Recaída: I alto + β alto → s divergente
Plasticidad: β→0, luego k alto consolida
Estable: R ≈ 0 → ds/dt ≈ -k·s + I + ε
</code>
+48
View File
@@ -0,0 +1,48 @@
/* tslint:disable */
/* eslint-disable */
export function boot(): void;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly boot: () => void;
readonly __wasm_bindgen_func_elem_234: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_1466: (a: number, b: number, c: number, d: number) => void;
readonly __wasm_bindgen_func_elem_233: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_233_3: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_520: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_640: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_520_6: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_312: (a: number, b: number, c: number) => void;
readonly __wasm_bindgen_func_elem_313: (a: number, b: number) => void;
readonly __wbindgen_export: (a: number, b: number) => number;
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export3: (a: number) => void;
readonly __wbindgen_export4: (a: number, b: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
File diff suppressed because it is too large Load Diff
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const boot: () => void;
export const __wasm_bindgen_func_elem_234: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_1466: (a: number, b: number, c: number, d: number) => void;
export const __wasm_bindgen_func_elem_233: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_233_3: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_520: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_640: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_520_6: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_312: (a: number, b: number, c: number) => void;
export const __wasm_bindgen_func_elem_313: (a: number, b: number) => void;
export const __wbindgen_export: (a: number, b: number) => number;
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_export3: (a: number) => void;
export const __wbindgen_export4: (a: number, b: number) => void;
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
export const __wbindgen_start: () => void;
+214 -26
View File
@@ -21,8 +21,9 @@ use std::rc::Rc;
use barra_web::{Task, TaskList};
use gioser_canvas_web::{tips, Renderer};
use pluma_reader_web::Reader;
use vista_web::Deck;
use gioser_graph_web::GraphWidget;
use fana_md_reader_web::Reader;
use revista_web::Deck;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{
@@ -35,7 +36,7 @@ const TASKBAR_HEIGHT_PX: f32 = 52.0;
const BUTTON_HALF_W_PX: f32 = 90.0;
const BUTTON_HALF_H_PX: f32 = 64.0;
const VIEWPORT_MARGIN_PX: f32 = 14.0;
const ELEMENTS: [&str; 4] = ["aire", "fuego", "tierra", "agua"];
const ELEMENTS: [&str; 9] = ["aire", "fuego", "tierra", "agua", "cuerpo", "sombra", "cosmos", "practica", "olvido"];
#[derive(Default)]
struct DeckState {
@@ -78,6 +79,17 @@ impl AppState {
self.sync_active_class();
self.sync_taskbar();
self.load_md_if_empty(element, md_url);
// Actualizar URL con history.pushState (sin #)
if let Some(win) = web_sys::window() {
if let Ok(hist) = win.history() {
let path = format!("/estudio/{}", element);
let _ = hist.push_state_with_url(
&wasm_bindgen::JsValue::NULL,
"",
Some(&path),
);
}
}
}
fn restore_from_tab(&self, element: &str, origin_x: f64, origin_y: f64) {
@@ -105,6 +117,16 @@ impl AppState {
self.sync_active_class();
self.sync_taskbar();
self.hide_deck(origin_x, origin_y);
// Restaurar URL
if let Some(win) = web_sys::window() {
if let Ok(hist) = win.history() {
let _ = hist.push_state_with_url(
&wasm_bindgen::JsValue::NULL,
"",
Some("/"),
);
}
}
}
fn close(&self, element: &str, origin_x: f64, origin_y: f64) {
@@ -155,6 +177,7 @@ impl AppState {
if let Some(body) = self.document.body() {
let _ = body.class_list().add_1("deck-visible");
}
self.sync_page_controls();
}
fn hide_deck(&self, x: f64, y: f64) {
@@ -166,6 +189,18 @@ impl AppState {
if let Some(body) = self.document.body() {
let _ = body.class_list().remove_1("deck-visible");
}
self.sync_page_controls();
}
fn sync_page_controls(&self) {
if let Some(ctl) = self.document.get_element_by_id("global-page-controls") {
let is_visible = self.state.borrow().active.is_some();
ctl.set_attribute("style", if is_visible {
"opacity:1;pointer-events:auto;"
} else {
"opacity:0;pointer-events:none;"
}).ok();
}
}
fn deck_el(&self) -> Option<HtmlElement> {
@@ -230,16 +265,15 @@ impl AppState {
"fuego" => ("Quién Soy", "Bitácora · Crónica"),
"tierra" => ("Manifiesto", "Invariantes · Piedra de toque"),
"agua" => ("Mística", "Espiritualidad aplicada"),
"cuerpo" => ("El Cuerpo", "Somática · Respiración · Portal"),
"sombra" => ("La Sombra", "Integración · Patrones · Acecho"),
"cosmos" => ("Cosmovisión", "4 Elementos · Arquetipos"),
"practica" => ("Prácticas", "Ejercicios · Transformación"),
"olvido" => ("El Olvido", "Desaprender · Soltar · Vaciar"),
_ => return,
};
let html = format!(
"<article class=\"deck-page\" data-element=\"{el}\" id=\"deck-page-{el}\">\
<div class=\"page-controls\">\
<button class=\"page-control-btn page-minimize\" data-minimize=\"{el}\" type=\"button\" aria-label=\"Minimizar {title}\">\
<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M5 19 H19\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/></svg>\
</button>\
<button class=\"page-control-btn page-close\" data-close-page=\"{el}\" type=\"button\" aria-label=\"Cerrar {title}\">×</button>\
</div>\
<div class=\"page-ambience\" aria-hidden=\"true\"></div>\
<header class=\"page-head\">\
<span class=\"page-mark\">{el}</span>\
@@ -272,13 +306,85 @@ impl AppState {
if inner.contains("pluma-doc") {
return; // ya hidratado
}
let reader = Reader::new(content);
let document_clone = self.document.clone();
let element_owned = element.to_string();
let url_owned = md_url.to_string();
let reader = fana_md_reader_web::Reader::new(content.clone());
wasm_bindgen_futures::spawn_local(async move {
let content_clone = content.clone();
if let Err(e) = reader.open_url(&url_owned, &element_owned).await {
web_sys::console::warn_1(&e);
}
// Después de cargar el md, montar el grafo debajo
let graph_container_id = format!("graph-{}-container", element_owned);
// Si ya existe, no lo duplicamos
if document_clone.get_element_by_id(&graph_container_id).is_some() {
return;
}
// Crear contenedor debajo del content
let wrapper: HtmlElement = document_clone
.create_element("div")
.ok()
.and_then(|e| e.dyn_into::<HtmlElement>().ok())
.unwrap();
wrapper.set_id(&graph_container_id);
wrapper.style().set_property("margin-top", "1rem").ok();
wrapper.style().set_property("padding-top", "1rem").ok();
wrapper.style().set_property("border-top", "1px solid rgba(255,255,255,0.08)").ok();
// Label
let label: HtmlElement = document_clone
.create_element("div")
.ok()
.and_then(|e| e.dyn_into::<HtmlElement>().ok())
.unwrap();
label.set_inner_html(
"<span style=\"font-family: Inter, sans-serif; font-size: 0.75rem; \
letter-spacing: 0.3em; text-transform: uppercase; color: rgba(232,234,245,0.45);\">
· grafo semántico ·
</span>"
);
wrapper.append_child(&label).ok();
content_clone.append_child(&wrapper).ok();
// Callback: recibe 'camino' del nodo clickeado y navega
let cb: Box<dyn FnMut(String)> = Box::new(move |target| {
web_sys::console::log_1(&format!("DEBUG grafo: click target={}", target).into());
// Mapa: camino → id del tip en HTML
let el = match target.as_str() {
"logos" | "aire" => "aire",
"nomos" | "fuego" => "fuego",
"kay" | "tierra" => "tierra",
"uku" | "agua" => "agua",
"cuerpo" => "cuerpo",
"sombra" => "sombra",
"cosmos" => "cosmos",
"practica" => "practica",
"olvido" => "olvido",
_ => "aire",
};
web_sys::console::log_1(&format!("DEBUG grafo: el={}", el).into());
let sel = format!(".tip[data-md][id='tip-{}']", el);
web_sys::console::log_1(&format!("DEBUG grafo: selector={}", sel).into());
match document_clone.query_selector(&sel).ok().flatten() {
Some(tip) => {
web_sys::console::log_1(&"DEBUG grafo: tip encontrado, llamando click()".into());
let tip_html: HtmlElement = tip.clone().dyn_into().unwrap();
tip_html.click();
web_sys::console::log_1(&"DEBUG grafo: click() ejecutado".into());
}
None => {
web_sys::console::log_1(&"DEBUG grafo: tip NO encontrado".into());
}
}
});
let mut graph = GraphWidget::new(
wrapper,
"https://api.gioser.net",
Some(cb),
);
if let Err(e) = graph.load().await {
web_sys::console::warn_1(&format!("grafo: error al cargar: {:?}", e).into());
return;
}
});
}
@@ -374,11 +480,51 @@ pub fn boot() -> Result<(), JsValue> {
install_canvas_pointer(&canvas, &renderer)?;
install_canvas_leave(&canvas, &renderer)?;
install_tip_clicks(&document, &app)?;
install_deck_delegation(&document, &app)?;
install_controls_delegation(&document, &app)?;
install_taskbar(&document, &app)?;
install_keyboard(&document, &app)?;
install_popstate_listener(&window, &app)?;
install_raf(&window, &document, &canvas, &renderer);
// Leer ruta inicial para abrir página directa
if let Ok(pathname) = window.location().pathname() {
let clean = pathname.trim_start_matches('/').trim_start_matches("estudio/");
if !clean.is_empty() {
if let Some(el) = document.query_selector(&format!(".tip[data-md][id='tip-{}']", clean)).ok().flatten() {
let rect = el.get_bounding_client_rect();
let cx = rect.left() + rect.width() / 2.0;
let cy = rect.top() + rect.height() / 2.0;
if let Some(md_url) = el.get_attribute("data-md") {
app.open_or_switch(clean, cx, cy, &md_url);
}
}
}
}
Ok(())
}
fn install_popstate_listener(window: &Window, app: &Rc<AppState>) -> Result<(), JsValue> {
let app2 = app.clone();
let doc = app.document.clone();
let win2 = window.clone();
let cb = Closure::<dyn FnMut(Event)>::new(move |_e: Event| {
if let Ok(pathname) = win2.location().pathname() {
let clean = pathname.trim_start_matches('/').trim_start_matches("estudio/");
if clean.is_empty() || clean == "/" {
app2.home();
} else if let Some(el) = doc.query_selector(&format!(".tip[data-md][id='tip-{}']", clean)).ok().flatten() {
let rect = el.get_bounding_client_rect();
let cx = rect.left() + rect.width() / 2.0;
let cy = rect.top() + rect.height() / 2.0;
if let Some(md_url) = el.get_attribute("data-md") {
app2.open_or_switch(clean, cx, cy, &md_url);
}
}
}
});
window.add_event_listener_with_callback("popstate", cb.as_ref().unchecked_ref())?;
cb.forget();
Ok(())
}
@@ -474,11 +620,14 @@ fn install_tip_clicks(document: &Document, app: &Rc<AppState>) -> Result<(), JsV
let md_url = el.get_attribute("data-md").unwrap_or_default();
let app2 = app.clone();
let el_for_rect = el.clone();
let el_name = element.clone();
let cb = Closure::<dyn FnMut(Event)>::new(move |e: Event| {
web_sys::console::log_1(&format!("DEBUG tip: click en {} isTrusted={}", el_name, e.is_trusted()).into());
e.prevent_default();
let rect = el_for_rect.get_bounding_client_rect();
let cx = rect.left() + rect.width() / 2.0;
let cy = rect.top() + rect.height() / 2.0;
web_sys::console::log_1(&format!("DEBUG tip: llamando open_or_switch({}, {}, {})", el_name, cx, cy).into());
app2.open_or_switch(&element, cx, cy, &md_url);
});
el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
@@ -490,10 +639,7 @@ fn install_tip_clicks(document: &Document, app: &Rc<AppState>) -> Result<(), JsV
/// Un listener en el deck delega clicks de minimize y close en cada página.
/// Las páginas se crean dinámicamente, así que no podemos adjuntar listeners
/// por botón en boot.
fn install_deck_delegation(document: &Document, app: &Rc<AppState>) -> Result<(), JsValue> {
let Some(deck_el) = document.get_element_by_id("deck") else {
return Ok(());
};
fn install_controls_delegation(document: &Document, app: &Rc<AppState>) -> Result<(), JsValue> {
let app2 = app.clone();
let cb = Closure::<dyn FnMut(MouseEvent)>::new(move |e: MouseEvent| {
let Some(target) = e.target() else { return };
@@ -504,25 +650,38 @@ fn install_deck_delegation(document: &Document, app: &Rc<AppState>) -> Result<()
if let Ok(Some(btn)) = target_el.closest("[data-minimize]") {
e.stop_propagation();
let element = btn.get_attribute("data-minimize").unwrap_or_default();
// Origin = la cajita correspondiente en la taskbar (efecto
// visual: la página se "encoge" hacia su entrada del taskbar).
let origin = app2
.taskbar_item_center(&element)
.unwrap_or_else(|| center_of_element(&btn));
app2.minimize(origin.0, origin.1);
// Si el data-minimize está vacío, usar el elemento activo
let el = if element.is_empty() {
app2.state.borrow().active.clone().unwrap_or_default()
} else {
element
};
if !el.is_empty() {
let origin = app2
.taskbar_item_center(&el)
.unwrap_or_else(|| center_of_element(&btn));
app2.minimize(origin.0, origin.1);
}
return;
}
// Close
if let Ok(Some(btn)) = target_el.closest("[data-close-page]") {
e.stop_propagation();
let element = btn.get_attribute("data-close-page").unwrap_or_default();
let origin = app2
.taskbar_item_center(&element)
.unwrap_or_else(|| center_of_element(&btn));
app2.close(&element, origin.0, origin.1);
let el = if element.is_empty() {
app2.state.borrow().active.clone().unwrap_or_default()
} else {
element
};
if !el.is_empty() {
let origin = app2
.taskbar_item_center(&el)
.unwrap_or_else(|| center_of_element(&btn));
app2.close(&el, origin.0, origin.1);
}
}
});
deck_el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
document.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
cb.forget();
Ok(())
}
@@ -639,6 +798,35 @@ fn position_tips(document: &Document, canvas: &HtmlCanvasElement, renderer: &Ren
}
}
/// Mapea un doc_id de Qdrant al nombre del elemento (aire/fuego/tierra/agua)
/// y su ruta md. Los doc_ids se generan con uuid5 en el indexador, pero
/// podemos inferir por el nombre del camino o del elemento.
fn map_doc_id_to_element(doc_id: &str) -> (String, String) {
// Inferir del doc_id: contiene el nombre del elemento
let el = if doc_id.contains("aire") || doc_id.contains("logos") {
"aire"
} else if doc_id.contains("fuego") || doc_id.contains("nomos") {
"fuego"
} else if doc_id.contains("tierra") || doc_id.contains("kay") {
"tierra"
} else if doc_id.contains("agua") || doc_id.contains("uku") {
"agua"
} else if doc_id.contains("cuerpo") {
"cuerpo"
} else if doc_id.contains("sombra") {
"sombra"
} else if doc_id.contains("cosmos") {
"cosmos"
} else if doc_id.contains("practica") {
"practica"
} else if doc_id.contains("olvido") {
"olvido"
} else {
"aire"
};
(el.to_string(), format!("./md/{}.md", el))
}
fn install_panic_hook() {
static SET: std::sync::Once = std::sync::Once::new();
SET.call_once(|| {
File diff suppressed because it is too large Load Diff
+679
View File
@@ -0,0 +1,679 @@
/* === Tokens === */
:root {
--bg: #06050d;
--fg: #e8eaf5;
--gold: #d8a85d;
--gold-deep: #b77e34;
--aire: #d0dbff;
--agua: #6cd0f3;
--fuego: #f59056;
--tierra: #d49873;
--cuerpo: #e07a5f;
--sombra: #4a4a5a;
--cosmos: #d4a843;
--practica: #2d936c;
--olvido: #b0b8c0;
--ease-emerge: cubic-bezier(0.22, 0.61, 0.20, 1);
--ease-magma: cubic-bezier(0.32, 0, 0.05, 1);
--ease-page: cubic-bezier(0.22, 0.61, 0.36, 1);
--taskbar-height: 52px;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: var(--bg);
color: var(--fg);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 300;
overflow: hidden;
}
/* === Canvas WebGL === */
#gioser-canvas {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
display: block;
z-index: 0;
transition: opacity 600ms var(--ease-emerge), filter 600ms var(--ease-emerge);
}
body.deck-visible #gioser-canvas {
opacity: 0.30;
filter: blur(4px) saturate(80%);
}
/* === Tips (botones cardinales sobre el aro) === */
#tips {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 5;
transition: opacity 250ms ease;
}
body.deck-visible #tips {
opacity: 0;
pointer-events: none;
}
.tip {
position: absolute;
top: 0; left: 0;
pointer-events: auto;
text-decoration: none;
color: var(--fg);
user-select: none;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.55rem;
padding: 1.1rem 1.6rem 0.95rem;
min-width: 168px;
background:
radial-gradient(ellipse at top, rgba(255, 255, 255, 0.04), transparent 65%),
rgba(8, 6, 22, 0.55);
backdrop-filter: blur(12px) saturate(140%);
-webkit-backdrop-filter: blur(12px) saturate(140%);
border-radius: 20px;
border: 1px solid rgba(216, 168, 93, 0.25);
box-shadow:
0 12px 36px rgba(0, 0, 0, 0.40),
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
transition:
box-shadow 350ms var(--ease-emerge),
border-color 350ms var(--ease-emerge),
background 350ms var(--ease-emerge);
}
.tip::before {
content: "";
position: absolute;
inset: -10px;
border-radius: 26px;
border: 1px solid currentColor;
opacity: 0;
transition: opacity 400ms ease, inset 400ms var(--ease-emerge);
pointer-events: none;
}
.tip:hover {
border-color: currentColor;
background:
radial-gradient(ellipse at top, rgba(255, 255, 255, 0.07), transparent 65%),
rgba(20, 14, 40, 0.72);
}
.tip:hover::before {
opacity: 0.45;
inset: -16px;
}
.tip-glyph {
width: 54px;
height: 54px;
color: currentColor;
filter: drop-shadow(0 0 6px currentColor) drop-shadow(0 0 16px currentColor);
transition: filter 320ms ease, transform 350ms var(--ease-emerge);
}
.tip:hover .tip-glyph {
filter: drop-shadow(0 0 14px currentColor) drop-shadow(0 0 28px currentColor);
transform: translateY(-3px);
}
.tip-label {
font-family: 'Cinzel', serif;
font-size: 0.95rem;
letter-spacing: 0.42em;
font-weight: 600;
text-indent: 0.42em;
margin-top: 0.15rem;
}
.tip-sub {
font-family: 'Inter', sans-serif;
font-size: 0.7rem;
letter-spacing: 0.22em;
font-weight: 300;
color: rgba(232, 234, 245, 0.62);
text-transform: uppercase;
text-indent: 0.22em;
}
.tip-aire { color: var(--aire); }
.tip-fuego { color: var(--fuego); }
.tip-agua { color: var(--agua); }
.tip-tierra { color: var(--tierra); }
.tip-cuerpo { color: var(--cuerpo); }
.tip-sombra { color: var(--sombra); }
.tip-cosmos { color: var(--cosmos); }
.tip-practica { color: var(--practica); }
.tip-olvido { color: var(--olvido); }
/* === DECK: contenedor único de páginas swipeable === */
.deck {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: var(--taskbar-height);
z-index: 100;
pointer-events: none;
opacity: 0;
visibility: hidden;
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
transform: scale(0.0);
overflow: hidden;
touch-action: pan-y;
background:
radial-gradient(ellipse at center, var(--deck-glow, rgba(216, 168, 93, 0.15)), transparent 65%),
rgba(6, 5, 13, 0.96);
backdrop-filter: blur(28px) saturate(140%);
-webkit-backdrop-filter: blur(28px) saturate(140%);
transition:
transform 600ms var(--ease-magma),
opacity 450ms ease,
visibility 0s 600ms;
}
.deck.open {
pointer-events: auto;
opacity: 1;
visibility: visible;
transform: scale(1);
transition:
transform 600ms var(--ease-magma),
opacity 450ms ease,
visibility 0s;
}
/* Acento del deck según elemento activo: glow radial del color. */
body.deck-active-aire .deck { --deck-glow: rgba(208, 219, 255, 0.22); }
body.deck-active-fuego .deck { --deck-glow: rgba(245, 144, 86, 0.28); }
body.deck-active-agua .deck { --deck-glow: rgba(108, 208, 243, 0.22); }
body.deck-active-tierra .deck { --deck-glow: rgba(212, 152, 115, 0.24); }
/* Strip horizontal con páginas — vista-web traslada esto.
touch-action: pan-y declara al browser "yo manejo horizontal, el
vertical (scroll interno de cada página) lo dejas pasar". Sin esto
el navegador móvil se traga el gesto horizontal antes de que JS
pueda capturarlo. */
.deck-strip {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
transform: translate3d(var(--vista-offset, 0px), 0, 0);
transition: transform 360ms var(--ease-page);
will-change: transform;
touch-action: pan-y;
}
/* Asegurar que TODOS los descendientes del strip hereden el contrato
touch-action — si el toque llega a un párrafo o <a>, el browser
chequea el touch-action del target, no del padre. */
.deck-strip * {
touch-action: pan-y;
}
.deck-strip.vista-dragging,
.deck-strip.vista-instant {
transition: none;
}
.deck-page {
flex: 0 0 100%;
width: 100%;
height: 100%;
position: relative;
overflow-y: auto;
overflow-x: hidden;
touch-action: pan-y;
}
.deck-page[data-element="aire"] { --page-accent: var(--aire); }
.deck-page[data-element="fuego"] { --page-accent: var(--fuego); }
.deck-page[data-element="agua"] { --page-accent: var(--agua); }
.deck-page[data-element="tierra"] { --page-accent: var(--tierra); }
/* Ambience por página */
.page-ambience {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
transition: opacity 2s ease;
filter: blur(60px);
opacity: 0.5;
}
.deck-page[data-element="aire"] .page-ambience {
background: radial-gradient(circle at 50% 50%, rgba(208,219,255,0.22), transparent 60%);
animation: page-breathe 8s ease-in-out infinite alternate;
}
.deck-page[data-element="fuego"] .page-ambience {
background: radial-gradient(circle at 50% 50%, rgba(245,144,86,0.22), transparent 60%);
animation: page-breathe 6s ease-in-out infinite alternate;
}
.deck-page[data-element="agua"] .page-ambience {
background: radial-gradient(circle at 50% 50%, rgba(108,208,243,0.22), transparent 60%);
animation: page-breathe 10s ease-in-out infinite alternate;
}
.deck-page[data-element="tierra"] .page-ambience {
background: radial-gradient(circle at 50% 50%, rgba(140,100,60,0.22), transparent 60%);
animation: page-breathe 7s ease-in-out infinite alternate;
}
@keyframes page-breathe {
from { opacity: 0.30; }
to { opacity: 0.60; }
}
/* Head + controls — fijos en el deck, no dentro de la página */
.page-controls {
position: fixed;
top: 1.2rem;
right: 1.2rem;
z-index: 100;
display: flex;
gap: 0.5rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
body.deck-visible .page-controls {
opacity: 1;
pointer-events: auto;
}
.page-control-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.18);
color: var(--page-accent, var(--gold));
font-size: 1.2rem;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
font-family: 'Inter', sans-serif;
transition:
background 200ms ease,
border-color 200ms ease,
transform 250ms var(--ease-emerge);
}
.page-control-btn:hover {
background: rgba(255, 255, 255, 0.10);
border-color: currentColor;
}
.page-minimize svg {
width: 18px;
height: 18px;
}
.page-close {
font-size: 1.6rem;
}
.page-close:hover { transform: rotate(90deg); }
.page-head {
position: relative;
z-index: 2;
text-align: center;
padding: 7vh 8vw 2vh;
color: var(--page-accent, var(--gold));
}
.page-mark {
display: inline-block;
font-family: 'Inter', sans-serif;
font-size: 0.7rem;
letter-spacing: 0.55em;
text-transform: uppercase;
color: rgba(232, 234, 245, 0.6);
margin-bottom: 0.4rem;
text-indent: 0.55em;
}
.page-title {
font-family: 'Cinzel', serif;
font-weight: 700;
font-size: clamp(2.4rem, 6vw, 4.6rem);
margin: 0;
letter-spacing: 0.08em;
color: var(--page-accent, var(--gold));
text-shadow: 0 0 28px currentColor, 0 0 56px rgba(255, 255, 255, 0.10);
}
.page-tag {
display: block;
font-family: 'Inter', sans-serif;
font-size: 0.78rem;
letter-spacing: 0.32em;
text-transform: uppercase;
color: rgba(232, 234, 245, 0.55);
margin-top: 0.7rem;
text-indent: 0.32em;
}
.page-content {
position: relative;
z-index: 2;
padding: 1vh 10vw 8vh;
opacity: 0;
transition: opacity 400ms ease 250ms;
}
.deck.open .deck-page .page-content {
opacity: 1;
}
/* === pluma-doc dentro de la página === */
.pluma-doc {
max-width: 760px;
margin: 0 auto;
font-family: 'Inter', sans-serif;
font-weight: 300;
font-size: 1.05rem;
line-height: 1.78;
color: rgba(232, 234, 245, 0.92);
}
.pluma-doc > * + * { margin-top: 1.0em; }
.pluma-doc h1 {
font-family: 'Cinzel', serif;
font-weight: 700;
font-size: clamp(1.7rem, 3vw, 2.4rem);
color: var(--page-accent);
text-shadow: 0 0 18px currentColor;
letter-spacing: 0.04em;
margin-top: 1.4em;
}
.pluma-doc h2 {
font-family: 'Cinzel', serif;
font-weight: 500;
font-size: 1.5rem;
color: var(--page-accent);
letter-spacing: 0.04em;
margin-top: 1.6em;
padding-bottom: 0.3em;
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
}
.pluma-doc h3 {
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 1.18rem;
color: rgba(255, 255, 255, 0.92);
letter-spacing: 0.03em;
margin-top: 1.6em;
}
.pluma-doc p { margin: 0; }
.pluma-doc a {
color: var(--page-accent);
text-decoration: none;
border-bottom: 1px solid currentColor;
transition: opacity 200ms ease;
}
.pluma-doc a:hover { opacity: 0.7; }
.pluma-doc strong { color: rgba(255, 255, 255, 0.98); font-weight: 600; }
.pluma-doc em { color: rgba(255, 255, 255, 0.92); }
.pluma-doc code {
font-family: 'JetBrains Mono', ui-monospace, monospace;
background: rgba(255, 255, 255, 0.06);
padding: 0.12em 0.45em;
border-radius: 4px;
font-size: 0.92em;
color: var(--page-accent);
}
.pluma-doc pre {
background: rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 255, 255, 0.08);
border-left: 3px solid var(--page-accent);
border-radius: 8px;
padding: 1rem 1.2rem;
overflow-x: auto;
font-size: 0.92rem;
}
.pluma-doc pre code {
background: transparent;
color: rgba(232, 234, 245, 0.92);
padding: 0;
}
.pluma-doc blockquote {
border-left: 3px solid var(--page-accent);
padding: 0.4em 1.2em;
color: rgba(232, 234, 245, 0.75);
font-style: italic;
background: rgba(255, 255, 255, 0.03);
border-radius: 0 6px 6px 0;
}
.pluma-doc ul, .pluma-doc ol { padding-left: 1.6em; }
.pluma-doc li { margin: 0.4em 0; }
.pluma-doc li::marker { color: var(--page-accent); }
.pluma-doc hr {
border: none;
height: 1px;
background: linear-gradient(to right, transparent, var(--page-accent), transparent);
margin: 2em 0;
}
.pluma-doc table {
border-collapse: collapse;
width: 100%;
font-size: 0.95rem;
}
.pluma-doc th, .pluma-doc td {
padding: 0.55em 0.9em;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
text-align: left;
}
.pluma-doc th {
color: var(--page-accent);
font-weight: 600;
letter-spacing: 0.04em;
}
.pluma-loading, .pluma-error {
display: flex;
align-items: center;
justify-content: center;
min-height: 30vh;
color: rgba(232, 234, 245, 0.55);
font-family: 'Cinzel', serif;
letter-spacing: 0.4em;
font-size: 0.9rem;
}
.pluma-loading::before {
content: "";
width: 28px;
height: 28px;
margin-right: 1rem;
border: 1px solid var(--page-accent);
border-top-color: transparent;
border-radius: 50%;
animation: pluma-spin 1s linear infinite;
}
.pluma-error { color: var(--fuego); font-style: italic; }
@keyframes pluma-spin { to { transform: rotate(360deg); } }
/* === Taskbar estilo Windows === */
.taskbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: var(--taskbar-height);
z-index: 200;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 0.8rem;
background: linear-gradient(to top, rgba(6, 5, 13, 0.94), rgba(8, 6, 22, 0.80));
backdrop-filter: blur(24px) saturate(140%);
-webkit-backdrop-filter: blur(24px) saturate(140%);
border-top: 1px solid rgba(216, 168, 93, 0.22);
box-shadow: 0 -10px 36px rgba(0, 0, 0, 0.45);
}
.taskbar-home {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid rgba(216, 168, 93, 0.32);
color: var(--gold);
border-radius: 9px;
cursor: pointer;
padding: 0;
transition: all 220ms var(--ease-emerge);
}
.taskbar-home:hover {
border-color: var(--gold);
background: rgba(216, 168, 93, 0.12);
box-shadow: 0 0 16px rgba(216, 168, 93, 0.35);
transform: translateY(-1px);
}
.taskbar-home-glyph {
width: 22px;
height: 22px;
filter: drop-shadow(0 0 6px currentColor);
}
.taskbar-brand {
font-family: 'Cinzel', serif;
font-weight: 700;
font-size: 1.30rem;
letter-spacing: 0.07em;
color: #f4eedf;
text-decoration: none;
text-shadow: 0 0 14px rgba(216, 168, 93, 0.45);
padding: 0 0.55rem;
user-select: none;
transition: text-shadow 220ms ease, color 220ms ease;
white-space: nowrap;
}
.taskbar-brand:hover {
color: #ffffff;
text-shadow: 0 0 20px rgba(216, 168, 93, 0.7), 0 0 36px rgba(245, 144, 86, 0.30);
}
.taskbar-brand .brand-dot {
color: var(--gold);
margin: 0 0.05em;
}
.taskbar-divider {
display: inline-block;
width: 1px;
height: 26px;
background: rgba(255, 255, 255, 0.12);
margin: 0 0.25rem;
}
.taskbar-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 0.4rem;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.taskbar-list::-webkit-scrollbar { display: none; }
.taskbar-item {
height: 38px;
padding: 0 1.0rem;
display: inline-flex;
align-items: center;
gap: 0.55rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 9px;
color: var(--task-color, var(--fg));
font-family: 'Cinzel', serif;
font-size: 0.72rem;
letter-spacing: 0.36em;
text-indent: 0.36em;
font-weight: 600;
cursor: pointer;
transition: all 260ms var(--ease-emerge);
white-space: nowrap;
}
.taskbar-item:hover {
background: rgba(255, 255, 255, 0.09);
border-color: var(--task-color, currentColor);
transform: translateY(-1px);
}
.taskbar-item.active {
background: rgba(255, 255, 255, 0.11);
border-color: var(--task-color);
box-shadow:
0 0 18px var(--task-color),
inset 0 -2px 0 0 var(--task-color);
}
.taskbar-item-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--task-color, var(--gold));
box-shadow: 0 0 8px currentColor;
}
.taskbar-item[data-task="aire"] { --task-color: var(--aire); }
.taskbar-item[data-task="fuego"] { --task-color: var(--fuego); }
.taskbar-item[data-task="tierra"] { --task-color: var(--tierra); }
.taskbar-item[data-task="agua"] { --task-color: var(--agua); }
.taskbar-spacer {
flex: 1;
min-width: 0.6rem;
}
.taskbar-credit {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-family: 'Inter', sans-serif;
font-size: 0.78rem;
letter-spacing: 0.04em;
color: rgba(216, 168, 93, 0.75);
text-decoration: none;
padding: 0.35rem 0.7rem;
border-radius: 8px;
border: 1px solid transparent;
transition: all 220ms var(--ease-emerge);
white-space: nowrap;
}
.taskbar-credit:hover {
color: var(--gold);
border-color: rgba(216, 168, 93, 0.25);
background: rgba(216, 168, 93, 0.06);
}
.copyleft-mark {
display: inline-block;
font-size: 1rem;
/* © con escala horizontal -1 = copyleft visual. */
transform: scaleX(-1);
color: currentColor;
}
@media (max-width: 720px) {
.tip { min-width: 110px; padding: 0.7rem 0.9rem; }
.tip-glyph { width: 36px; height: 36px; }
.tip-label { font-size: 0.72rem; }
.tip-sub { display: none; }
.page-head { padding: 5vh 5vw 1vh; }
.page-content { padding: 0 5vw 5vh; }
.taskbar { height: 46px; padding: 0 0.4rem; gap: 0.3rem; }
.taskbar-home { width: 36px; height: 36px; }
.taskbar-item { height: 34px; padding: 0 0.7rem; font-size: 0.65rem; }
.taskbar-brand { font-size: 1.05rem; padding: 0 0.3rem; }
.taskbar-credit-text { display: none; }
.deck { bottom: 46px; }
:root { --taskbar-height: 46px; }
}
/* Los tips nuevos (cuerpo, sombra, cosmos, practica, olvido) existen
en el DOM para navegación JS (popstate, grafo), pero no se ven en
la chacana — no hay canvas-glyph para ellos. Se ocultan por defecto. */
.tip-hidden {
position: absolute;
visibility: hidden;
pointer-events: none;
width: 0;
height: 0;
overflow: hidden;
}
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "lapaloma-demo"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — demo app: una serie sin(x) sobre ChartViewport rendereada con LapalomaChartElement. Valida la cadena core → render → cartesian → gpui en vivo."
[dependencies]
gpui = { workspace = true }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
lapaloma-core = { path = "../../modules/ui_engine/libs/lapaloma-core" }
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
lapaloma-cartesian = { path = "../../modules/ui_engine/widgets/lapaloma-cartesian" }
@@ -1,16 +0,0 @@
[package]
name = "lapaloma-financial-demo"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — demo de candlesticks OHLC. Random walk sintético de 120 días con pan + zoom."
[dependencies]
gpui = { workspace = true }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
lapaloma-cartesian = { path = "../../modules/ui_engine/widgets/lapaloma-cartesian" }
lapaloma-financial = { path = "../../modules/ui_engine/widgets/lapaloma-financial" }
@@ -1,16 +0,0 @@
[package]
name = "lapaloma-phosphor-demo"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — demo del trail CRT (phosphor) sobre un RingBuffer streaming a 60Hz. Compará con lapaloma-stream-demo para ver el contraste."
[dependencies]
gpui = { workspace = true }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
lapaloma-core = { path = "../../modules/ui_engine/libs/lapaloma-core" }
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
lapaloma-phosphor = { path = "../../modules/ui_engine/widgets/lapaloma-phosphor" }
@@ -1,16 +0,0 @@
[package]
name = "lapaloma-stream-demo"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
publish = { workspace = true }
description = "Lapaloma — demo de streaming: RingBuffer + timer 60 Hz + sweep render. Showcase del zero-alloc en hot path."
[dependencies]
gpui = { workspace = true }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
lapaloma-core = { path = "../../modules/ui_engine/libs/lapaloma-core" }
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
lapaloma-stream = { path = "../../modules/ui_engine/widgets/lapaloma-stream" }
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "matilda"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "matilda — CLI de administración de servidores: carga un inventario, muestra el plan, emite el script y lo aplica (local, remoto por SSH, o en seco)."
[[bin]]
name = "matilda"
path = "src/main.rs"
[dependencies]
matilda-core = { path = "../../modules/matilda/matilda-core" }
matilda-config = { path = "../../modules/matilda/matilda-config" }
matilda-plan = { path = "../../modules/matilda/matilda-plan" }
matilda-apply = { path = "../../modules/matilda/matilda-apply" }
matilda-ghost = { path = "../../modules/matilda/matilda-ghost" }
matilda-linker = { path = "../../modules/matilda/matilda-linker" }
matilda-discover = { path = "../../modules/matilda/matilda-discover" }
clap = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
+238
View File
@@ -0,0 +1,238 @@
//! `matilda` — CLI de administración de servidores.
//!
//! Carga un inventario declarativo (JSON), lo reconcilia contra el
//! estado actual y aplica los cambios — localmente, en seco, o en un
//! servidor remoto por SSH:
//!
//! ```text
//! matilda example imprime un inventario de ejemplo
//! matilda plan inv.json muestra el plan de reconciliación
//! matilda script inv.json emite el script de aplicación
//! matilda apply inv.json aplica localmente
//! matilda apply inv.json --dry-run simula
//! matilda apply inv.json --host deploy@srv aplica por SSH
//! ```
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use matilda_apply::{plan_to_steps, steps_to_script, ApplyStep};
use matilda_core::{Container, Host, Inventory, RestartPolicy, VHost};
use matilda_ghost::ApplyReport;
use matilda_linker::{Linker, SshAuth, SshConfig};
use matilda_plan::{plan, Op};
#[derive(Parser)]
#[command(name = "matilda", about = "Administración declarativa de servidores")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Imprime un inventario de ejemplo para editar.
Example,
/// Muestra el plan de reconciliación del inventario.
Plan {
inventory: PathBuf,
/// Estado actual del servidor (por defecto: vacío).
#[arg(long)]
current: Option<PathBuf>,
/// Descubre el estado actual de esta máquina (docker + nginx).
#[arg(long)]
discover: bool,
},
/// Emite el script de shell que aplicaría el plan.
Script {
inventory: PathBuf,
#[arg(long)]
current: Option<PathBuf>,
#[arg(long)]
discover: bool,
},
/// Aplica el plan: local, en seco, o remoto por SSH.
Apply {
inventory: PathBuf,
#[arg(long)]
current: Option<PathBuf>,
/// Descubre el estado actual de esta máquina antes de reconciliar.
#[arg(long)]
discover: bool,
/// Simula sin tocar nada.
#[arg(long)]
dry_run: bool,
/// Aplica en un host remoto, `usuario@host`.
#[arg(long)]
host: Option<String>,
/// Contraseña SSH (si no se da, se usa la clave por defecto).
#[arg(long)]
password: Option<String>,
},
}
/// Carga un inventario JSON desde un archivo.
fn load(path: &PathBuf) -> Result<Inventory, String> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("no se pudo leer {}: {e}", path.display()))?;
serde_json::from_str(&text).map_err(|e| format!("JSON inválido en {}: {e}", path.display()))
}
/// Resuelve el inventario "actual" contra el que reconciliar:
/// `--discover` observa esta máquina; `--current` lee un archivo; si no,
/// se parte de un inventario vacío (todo es creación).
fn current_inventory(
discover: bool,
current: &Option<PathBuf>,
desired: &Inventory,
) -> Result<Inventory, String> {
if discover {
// Descubrimiento detallado: `docker inspect` detecta el drift.
Ok(matilda_discover::discover_inventory(desired))
} else {
match current {
Some(p) => load(p),
None => Ok(Inventory::new()),
}
}
}
/// Construye un inventario de ejemplo.
fn example_inventory() -> Inventory {
let mut inv = Inventory::new();
inv.add_host(Host::new("edge-1", "10.0.0.1").with_tag("prod"));
inv.add_container(
Container::new("web", "nginx:1.27")
.with_port(8080, 80)
.with_volume("/srv/site", "/usr/share/nginx/html")
.with_restart(RestartPolicy::Always),
);
inv.add_container(
Container::new("api", "ghcr.io/ejemplo/api:1.0")
.with_port(9000, 9000)
.with_env("DATABASE_URL", "postgres://db/app")
.with_restart(RestartPolicy::UnlessStopped),
);
inv.add_vhost(
VHost::to_container("sitio.com", "web", 80)
.with_alias("www.sitio.com")
.with_tls(),
);
inv
}
/// Imprime un `ApplyReport` legible.
fn print_report(report: &ApplyReport) {
for r in &report.results {
println!("\n{} {}", if r.ok { "" } else { "" }, r.describe);
for l in &r.log {
println!(" {l}");
}
}
println!(
"\n{} de {} pasos aplicados.",
report.applied(),
report.results.len()
);
if !report.all_ok() {
println!("✘ se detuvo en el primer error.");
}
}
/// Aplica los pasos en un host remoto por SSH.
async fn apply_remote(
target: &str,
password: Option<String>,
steps: &[ApplyStep],
) -> Result<ApplyReport, String> {
let (user, host) = target
.split_once('@')
.ok_or_else(|| format!("host inválido (esperaba usuario@host): {target}"))?;
let auth = match password {
Some(pw) => SshAuth::Password(pw),
None => {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
SshAuth::Key {
path: PathBuf::from(format!("{home}/.ssh/id_ed25519")),
passphrase: None,
}
}
};
let config = SshConfig::new(host, user, auth);
let linker = Linker::connect(&config)
.await
.map_err(|e| format!("conexión SSH: {e}"))?;
Ok(linker.apply(steps).await)
}
fn run() -> Result<(), String> {
match Cli::parse().cmd {
Cmd::Example => {
let json = serde_json::to_string_pretty(&example_inventory())
.map_err(|e| e.to_string())?;
println!("{json}");
}
Cmd::Plan { inventory, current, discover } => {
let desired = load(&inventory)?;
let p = plan(&current_inventory(discover, &current, &desired)?, &desired);
if p.is_empty() {
println!("Sin cambios: el servidor ya está al día.");
} else {
for (i, action) in p.actions.iter().enumerate() {
println!("{:>2}. {}", i + 1, action.describe());
}
println!(
"\n{} acciones — {} crear, {} actualizar, {} eliminar.",
p.len(),
p.count(Op::Create),
p.count(Op::Update),
p.count(Op::Remove),
);
}
}
Cmd::Script { inventory, current, discover } => {
let desired = load(&inventory)?;
let p = plan(&current_inventory(discover, &current, &desired)?, &desired);
print!("{}", steps_to_script(&plan_to_steps(&p, &desired)));
}
Cmd::Apply { inventory, current, discover, dry_run, host, password } => {
let desired = load(&inventory)?;
let p = plan(&current_inventory(discover, &current, &desired)?, &desired);
let steps = plan_to_steps(&p, &desired);
if steps.is_empty() {
println!("Sin cambios: nada que aplicar.");
return Ok(());
}
let report = if dry_run {
println!("— simulación (no se toca nada) —");
matilda_ghost::dry_run(&steps)
} else if let Some(target) = host {
println!("— aplicando en {target} por SSH —");
let rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
rt.block_on(apply_remote(&target, password, &steps))?
} else {
println!("— aplicando localmente —");
matilda_ghost::apply(&steps)
};
print_report(&report);
if !report.all_ok() {
return Err("la aplicación falló".into());
}
}
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
+5 -5
View File
@@ -7,11 +7,11 @@ description = "Dashboard GPUI del repo Minga: counts de nodos AST, atestaciones,
[dependencies]
minga-store = { path = "../../modules/semantic_dht/minga-store" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
yahweh-widget-stat-card = { path = "../../modules/ui_engine/widgets/stat-card" }
yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" }
nahual-theme = { path = "../../modules/nahual/libs/theme" }
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
nahual-widget-banner = { path = "../../modules/nahual/widgets/banner" }
nahual-widget-stat-card = { path = "../../modules/nahual/widgets/stat-card" }
nahual-widget-app-header = { path = "../../modules/nahual/widgets/app-header" }
gpui = { workspace = true }
[[bin]]
+8 -8
View File
@@ -12,9 +12,9 @@
//! (`minga status`) cuando hace falta el DID. El explorer foco es
//! observabilidad rápida.
//!
//! Stack visual: yahweh-theme + banner_themed + card_themed +
//! Stack visual: nahual-theme + banner_themed + card_themed +
//! theme_switcher. Mismo patrón que `nakui-explorer` /
//! `nouser-explorer`.
//! `chasqui-explorer`.
//!
//! Uso:
//! ```sh
@@ -30,11 +30,11 @@ use gpui::{
div, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
};
use minga_store::PersistentRepo;
use yahweh_launcher::launch_app;
use yahweh_theme::Theme;
use yahweh_widget_app_header::app_header;
use yahweh_widget_banner::{banner_themed, Banner};
use yahweh_widget_stat_card::stat_card;
use nahual_launcher::launch_app;
use nahual_theme::Theme;
use nahual_widget_app_header::app_header;
use nahual_widget_banner::{banner_themed, Banner};
use nahual_widget_stat_card::stat_card;
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
const REPO_DIRNAME: &str = "repo";
@@ -288,7 +288,7 @@ impl Render for Explorer {
}
}
// `stat_card` se promovió a `yahweh-widget-stat-card` y se importa
// `stat_card` se promovió a `nahual-widget-stat-card` y se importa
// arriba. La fn local fue eliminada en la iter 15 del refactor.
#[cfg(test)]
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "mirada-compositor"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "mirada — el Cuerpo del compositor: un compositor Wayland teselante sobre smithay (backend winit, nested). Tesela con un Cerebro embebido o uno externo por mirada-link."
[[bin]]
name = "mirada-compositor"
path = "src/main.rs"
[dependencies]
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
mirada-body = { path = "../../modules/mirada/mirada-body" }
mirada-link = { path = "../../modules/mirada/mirada-link" }
brahman-auth = { path = "../../protocol/brahman-auth" }
nix = { workspace = true }
smithay = "0.7"
+221
View File
@@ -0,0 +1,221 @@
# mirada-compositor — el Cuerpo de carmen
Un compositor Wayland teselante real, sobre [`smithay`]. Es el **Cuerpo**
de la arquitectura Cerebro↔Cuerpo de `mirada` (ver
`crates/modules/mirada/SDD.md`): habla el protocolo Wayland con los
clientes, compone sus superficies y aplica la geometría que decide el
Cerebro.
Tiene **dos backends gráficos**:
- **`winit`** — corre **anidado**, como una ventana dentro de tu sesión
gráfica actual (X11 o Wayland). Para desarrollar y probar sin dejar el
escritorio.
- **`drm`** — corre **nativo** sobre una TTY, sin sesión anfitriona:
toma la GPU (DRM/KMS/GBM/EGL), el teclado (`libinput`) y la pantalla
entera. Es carmen como tu escritorio de verdad.
Sin argumentos elige solo: con `DISPLAY`/`WAYLAND_DISPLAY``winit`;
sin ellos → `drm`. O fuérzalo: `mirada-compositor --winit` / `--drm`.
La bandera `--greeter` (ortogonal al backend) arranca el compositor como
gestor de login — ver **Modo greeter (DM)** más abajo.
## Backends
### winit — anidado
```sh
cargo run -p mirada-compositor -- --winit
```
Necesita una sesión gráfica anfitriona (X11 o Wayland) donde dibujar su
ventana; sin ella aborta con un mensaje que lo explica.
### drm — nativo sobre TTY
```sh
cargo run -p mirada-compositor -- --drm
```
Corre directo sobre el hardware. Requiere una **TTY** (`Ctrl+Alt+F3`),
una GPU con `/dev/dri`, y `seatd` o `logind` para la sesión. Toma la
pantalla completa; sal con `Super+Shift+e` o `Ctrl+C`.
Lleva teclado y ratón por `libinput`: el foco sigue al puntero y los
clics y la rueda llegan a la ventana que tienes debajo. El cursor toma
la forma que pide el cliente (la «I» sobre texto, una mano…) y cae a un
cuadrado por defecto sobre el escritorio. **`Super`+arrastre** con el
botón izquierdo mueve una ventana, con el derecho la redimensiona — al
arrastrarla, la ventana pasa a flotar. Cada ventana lleva un marco
fino: azul la que tiene el foco, gris las demás.
- `MIRADA_STARTUP=<cmd>` — lanza una app al arrancar (`MIRADA_STARTUP=foot`).
- `MIRADA_DRM_TIMEOUT=<s>` — cierra el compositor solo tras N segundos
(0 o sin definir = sin tope).
## Como sesión de escritorio
Para usar carmen como tu escritorio de verdad — entrar a una sesión, no
sólo probarlo:
1. Compila e instala los binarios en el `PATH`:
```sh
cargo build --release -p mirada-compositor -p mirada-ctl -p mirada-launcher
sudo install -m755 target/release/mirada-compositor \
target/release/mirada-ctl target/release/mirada-launcher /usr/local/bin/
sudo install -m755 session/mirada-session /usr/local/bin/
```
2. Arranca desde una TTY:
```sh
mirada-session
```
O regístralo en un gestor de login copiando `session/carmen.desktop`
a `/usr/share/wayland-sessions/` — aparecerá carmen como sesión.
3. **Autoarranque** — los programas que quieras al iniciar van en
`~/.config/mirada/autostart`, uno por línea (`#` comenta). Tienes un
ejemplo en `session/autostart.example`:
```sh
mkdir -p ~/.config/mirada
cp crates/apps/mirada-compositor/session/autostart.example \
~/.config/mirada/autostart
```
Dentro de la sesión, `Ctrl+Alt+F1…F12` salta a otra TTY y vuelve sin
romper carmen.
## Modo greeter (DM)
`mirada-compositor --greeter` arranca el compositor como **gestor de
login**: en vez de la sesión, compone el greeter (`mirada-greeter`),
que lanza como proceso hijo. El usuario teclea sus credenciales; cuando
el login es válido el greeter emite un `SessionTicket` por su stdout y
el compositor **muta a modo sesión sin reiniciar el servidor Wayland**
— el mismo proceso, la misma GPU, las mismas ventanas («mutación
atómica»). Desde ahí baja privilegios al usuario autenticado
(`setuid`/`setgid` + grupos) para todo lo que lanza.
La bandera es ortogonal al backend: `--greeter` solo (auto), o
`--greeter --drm` / `--greeter --winit`.
```sh
# DM real, sobre una TTY — el compositor corre como root: PAM lo exige
sudo mirada-compositor --greeter --drm
# iterar el greeter anidado, con credenciales de prueba
MIRADA_GREETER_MOCK=demo:demo \
cargo run -p mirada-compositor -- --greeter --winit
```
En modo greeter no se registran atajos (todas las teclas van al
greeter — que el usuario no pueda lanzar nada ni cerrar el compositor),
se rechaza `spawn:` y no corre el autoarranque; los atajos y la sesión
arrancan sólo tras el traspaso. `MIRADA_GREETER_BIN` apunta a otro
binario de greeter (cómodo para señalar a `target/…` en desarrollo).
## Lanzador de aplicaciones
`mirada-launcher` escanea los `.desktop` del sistema y lanza el que
elijas. Es un programa de terminal sin dependencias: lo abres en una
terminal pequeña y filtras escribiendo. El keymap por defecto ata
`Super+p` a `spawn:foot -e mirada-launcher` — pulsa el atajo, escribe
unas letras del nombre, Enter.
Necesita `mirada-launcher` y `foot` en el `PATH` (ver la instalación de
arriba). Suelto también vale: `mirada-launcher` en cualquier terminal.
## Dos modos
- **Autónomo** (por defecto) — lleva un `Desktop` (de `mirada-brain`)
embebido. Es un compositor teselante completo en un solo proceso.
```sh
cargo run -p mirada-compositor
```
- **Enlazado** — el Cuerpo escucha en un socket y la app `mirada` (el
Cerebro GPUI) se conecta y decide la geometría.
```sh
# terminal 1 — el Cuerpo
MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada-compositor
# terminal 2 — el Cerebro
MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada
```
## Probarlo
Al arrancar imprime el `WAYLAND_DISPLAY` que abrió. Lanza cualquier
cliente Wayland contra él:
```sh
WAYLAND_DISPLAY=wayland-1 foot # o weston-terminal, alacritty, …
```
Las ventanas se teselan solas. El teclado, con la ventana del compositor
enfocada, maneja el escritorio con atajos `Super+…`: el lanzador de
aplicaciones `Super+p`, una terminal `Super+Shift+Return`, foco
`Super+j/k`, los 7 layouts en `Super+t/m/g/c/r/d/s` (o ciclar con
`Super+space`), área maestra `Super+h/l`, `nmaster` `Super+,/.`,
promover a maestra `Super+Return`, escritorios `Super+1..9`, cerrar
`Super+q`. Cierra la ventana del compositor para salir.
## Atajos de teclado
Los atajos son configurables en RON: `~/.config/mirada/keymap.ron`. En
modo autónomo, el Cuerpo lo carga al arrancar (si no existe, escribe uno
por defecto documentado) y lo **recarga en caliente** — edita el archivo,
guarda, y los atajos cambian sin reiniciar. En modo enlazado el keymap es
asunto del Cerebro (la app `mirada`).
```sh
cargo run -p mirada-brain --example keymap-default # ver el formato
```
El compositor en sí no interpreta atajos: sólo intercepta las
combinaciones que el Cerebro le pide (`GrabKeys`) y le devuelve la
pulsada. *Qué significa* cada una lo decide `mirada-brain`. Ver el SDD.
## Control externo
En modo autónomo, el compositor abre un socket de control y `mirada-ctl`
lo maneja desde la terminal — al estilo de `swaymsg`/`hyprctl`:
```sh
mirada-ctl focus-next # cambia el foco
mirada-ctl focus-window 5 # enfoca una ventana concreta
mirada-ctl workspace 3 # va al escritorio 3
mirada-ctl windows # lista las ventanas
```
En modo enlazado el socket de control lo abre el Cerebro (la app
`mirada`), no el compositor.
## Qué implementa
`wl_compositor`, `xdg_shell` (toplevels y popups), `wl_shm`, `wl_seat`
(teclado, y ratón en el backend DRM), `wl_output`, `wl_data_device`
(selección), `xdg-decoration` — fuerza decoración del servidor y no
dibuja ninguna, así las ventanas van sin barra de título — y
`zwp_linux_dmabuf`, que deja conectarse a los clientes que pintan por
GPU (apps GPUI, navegadores acelerados). Composición con `GlesRenderer`
— en `winit` sobre la ventana, en `drm` con un `DrmCompositor` por
salida.
Reusa `mirada-body` para la contabilidad de salidas y superficies, y
`mirada-link` para el cable hacia un Cerebro externo. Toda la lógica
espacial es agnóstica de Wayland y vive en los crates de
`crates/modules/mirada/`.
## Pendiente
Del backend DRM: conmutación de VT, hotplug de monitores, multi-GPU.
Puntero en el backend `winit`. Aislamiento de clientes. Ver el SDD.
[`smithay`]: https://github.com/Smithay/smithay
@@ -0,0 +1,17 @@
# Autoarranque de carmen — cópialo a ~/.config/mirada/autostart
#
# Un comando por línea; se lanza al arrancar el compositor, con
# WAYLAND_DISPLAY ya puesto. Las líneas en blanco y las que empiezan
# por # se ignoran. Cada línea se pasa a `sh -c`, así que valen las
# variables, las tuberías y el `&` final no hace falta.
# El shell de carmen — barra acoplada al pie con su línea de comandos.
# carmen la reconoce por su app_id y le reserva la franja.
shuma-shell --launcher
# Una terminal para empezar.
foot
# Ejemplos (descoméntalos si los quieres):
# mirada-ctl layout spiral
# wbg ~/fondo.png
@@ -0,0 +1,6 @@
[Desktop Entry]
Name=carmen
Comment=Compositor Wayland teselante (mirada)
Exec=mirada-session
Type=Application
DesktopNames=carmen
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
# mirada-session — arranca carmen (el compositor mirada) como una sesión
# de escritorio. Pensado para lanzarse desde una TTY o desde un gestor de
# login (greetd, ly, …).
#
# Instálalo en el PATH (p. ej. /usr/local/bin/mirada-session) junto al
# binario `mirada-compositor`.
# Carmen es un compositor Wayland.
export XDG_SESSION_TYPE=wayland
export XDG_CURRENT_DESKTOP=carmen
export XDG_SESSION_DESKTOP=carmen
# Que las apps GUI prefieran sus backends Wayland.
export MOZ_ENABLE_WAYLAND=1
export QT_QPA_PLATFORM="wayland;xcb"
export SDL_VIDEODRIVER=wayland
export _JAVA_AWT_WM_NONREPARENTING=1
# El backend DRM toma la TTY entera. Los programas de arranque van en
# ~/.config/mirada/autostart (uno por línea).
exec mirada-compositor --drm
@@ -0,0 +1,937 @@
//! `drm_backend` — el Cuerpo del compositor sobre **DRM/KMS**, sin
//! sesión gráfica anfitriona: corre directo sobre una TTY, como tu
//! escritorio de verdad.
//!
//! Construido por fases para verificarlo en hardware paso a paso:
//!
//! - **Fase 1 — bring-up**: sesión (`libseat`), GPU, dispositivo DRM,
//! enumerar salidas.
//! - **Fase 2a — pipeline de render**: GBM, EGL y `GlesRenderer`, con un
//! `DrmCompositor` para la salida conectada.
//! - **Fase 2b — bucle Wayland** (esto): un bucle `calloop` que atiende
//! a los clientes Wayland, el teclado (`libinput`) y el VBlank, y
//! compone las ventanas de verdad. Aquí `mirada-compositor --drm` ya
//! es un escritorio funcionando.
//!
//! Todo con logs para diagnosticar sin el hardware delante.
use std::error::Error;
use std::sync::Arc;
use std::time::{Duration, Instant};
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags};
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent};
use smithay::backend::egl::{EGLContext, EGLDisplay};
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, InputEvent, KeyState, KeyboardKeyEvent,
PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::{render_elements, Id, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{ImportAll, ImportDma};
use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev;
use smithay::input::keyboard::FilterResult;
use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
use smithay::output::OutputModeSource;
use smithay::reexports::calloop::channel::{channel as ticket_channel, Event as TicketEvent};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{EventLoop, Interest, Mode as CalloopMode, PostAction};
use smithay::reexports::drm::control::connector::State as ConnectorState;
use smithay::reexports::drm::control::{Device as ControlDevice, ModeTypeFlags};
use smithay::reexports::input::Libinput;
use smithay::reexports::rustix::fs::OFlags;
use smithay::reexports::wayland_server::{Display, ListeningSocket};
use smithay::utils::{
DeviceFd, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
};
use brahman_auth::SessionTicket;
use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect};
use crate::{
combo_string, send_frames_surface_tree, App, BodyMode, Brain, ClientState, DragGrab, DragMode,
Setup,
};
/// El `DrmCompositor` concreto para la salida (un solo GPU).
type Compositor =
DrmCompositor<GbmAllocator<DrmDeviceFd>, GbmFramebufferExporter<DrmDeviceFd>, (), DrmDeviceFd>;
render_elements! {
/// Lo que el backend DRM compone en un cuadro: superficies de cliente
/// y rectángulos de color sólido (el cursor y los marcos de ventana).
Frame<R> where R: ImportAll;
Window = WaylandSurfaceRenderElement<R>,
Solid = SolidColorRenderElement,
}
/// Color de fondo del escritorio cuando no hay nada que lo tape.
const CLEAR_COLOR: [f32; 4] = [0.05, 0.05, 0.08, 1.0];
/// Lado del cursor de software, en píxeles.
const CURSOR_SIZE: i32 = 12;
/// Color del cursor — un cuadrado casi blanco, opaco.
const CURSOR_COLOR: [f32; 4] = [0.95, 0.95, 0.97, 1.0];
/// Lado mínimo de una ventana al redimensionarla con el ratón.
const MIN_WINDOW: i32 = 120;
/// Grosor del marco de una ventana, en píxeles.
const BORDER_WIDTH: i32 = 2;
/// Color del marco de la ventana enfocada — un azul que resalta.
const BORDER_FOCUS: [f32; 4] = [0.36, 0.56, 0.92, 1.0];
/// Color del marco de las ventanas sin foco — gris discreto.
const BORDER_NORMAL: [f32; 4] = [0.22, 0.22, 0.27, 1.0];
/// Los 4 rectángulos `(x, y, w, h)` del marco de una ventana cuyo
/// contenido ocupa `(sx, sy, sw, sh)`. El marco va *hacia adentro* (pisa
/// el borde de la superficie), así nunca se solapa con el de la ventana
/// vecina: arriba, abajo, izquierda, derecha.
fn border_rects(sx: i32, sy: i32, sw: i32, sh: i32) -> [(i32, i32, i32, i32); 4] {
let bw = BORDER_WIDTH;
let side_h = (sh - 2 * bw).max(0);
[
(sx, sy, sw, bw),
(sx, sy + sh - bw, sw, bw),
(sx, sy + bw, bw, side_h),
(sx + sw - bw, sy + bw, bw, side_h),
]
}
/// Códigos de botón de `<linux/input-event-codes.h>`.
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
/// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`.
struct DrmState {
app: App,
display: Display<App>,
/// El dispositivo DRM — se conserva para pausarlo y reactivarlo al
/// conmutar de VT.
drm: DrmDevice,
compositor: Compositor,
renderer: GlesRenderer,
/// Contexto `libinput` — se suspende y reanuda al conmutar de VT.
libinput: Libinput,
/// `false` mientras la sesión está cedida a otra VT — no se compone.
active: bool,
/// `true` entre que se encola un page-flip y llega su VBlank.
pending_flip: bool,
keymap_path: Option<std::path::PathBuf>,
keymap_watch: Option<mirada_brain::KeymapWatch>,
ctl: Option<crate::CtlServer>,
/// Inicio del compositor — base de tiempos para los frame-callbacks.
start: Instant,
/// Nº de ventanas en el último `tick` — para registrar los cambios.
last_windows: usize,
/// Identidad estable del cursor de software — el seguimiento de daño
/// la usa para no recomponer todo cuando el cursor sólo se mueve.
cursor_id: Id,
/// Ventana sobre la que estaba el puntero — para el foco-sigue-ratón.
last_pointer_window: Option<u64>,
/// Tamaño de la salida, en píxeles — los topes del puntero.
output_size: (f64, f64),
}
impl DrmState {
/// Compone el cursor y las ventanas y, si hubo cambios, encola el cuadro.
fn render(&mut self) {
if !self.active {
return; // la sesión está en otra VT — no tocamos la GPU
}
if self.pending_flip {
return; // aún esperamos el VBlank del cuadro anterior
}
let output_h = self.app.output_size.1;
// Paso 1 · refresca los búferes del marco de cada ventana — su
// tamaño (sigue al contenido) y su color (según el foco). Cada
// `SolidColorBuffer` sube su contador de daño sólo si algo cambió.
for w in &mut self.app.windows {
if !w.visible || w.is_shell {
continue; // el shell no lleva marco
}
let (x, y) = crate::render_loc(w, output_h);
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
let color = if w.focused { BORDER_FOCUS } else { BORDER_NORMAL };
let rects = border_rects(x, y, sw, sh);
for (buf, (_, _, bw, bh)) in w.borders.iter_mut().zip(rects) {
buf.update((bw, bh), color);
}
}
// Paso 2 · arma los elementos — lista front-to-back (índice 0 =
// encima): el cursor, y por cada ventana su marco sobre su
// superficie. Las flotantes van antes que las teseladas.
let elements: Vec<Frame<GlesRenderer>> = {
let mut out: Vec<Frame<GlesRenderer>> = Vec::new();
// El cursor — la superficie que pidió el cliente (la «I» del
// texto, una mano…), o el cuadrado por defecto si pidió un
// cursor con nombre y no hay tema. `Hidden` no pinta nada.
let (cx, cy) = self.app.pointer_loc;
match &self.app.cursor_status {
CursorImageStatus::Hidden => {}
CursorImageStatus::Surface(surface) if surface.alive() => {
let (hx, hy) = crate::cursor_hotspot(surface);
let loc = (cx.round() as i32 - hx, cy.round() as i32 - hy);
for el in render_elements_from_surface_tree(
&mut self.renderer,
surface,
loc,
1.0,
1.0,
Kind::Cursor,
) {
out.push(Frame::Window(el));
}
}
_ => {
let cursor_rect = Rectangle::new(
Point::<i32, Physical>::from((cx.round() as i32, cy.round() as i32)),
Size::<i32, Physical>::from((CURSOR_SIZE, CURSOR_SIZE)),
);
out.push(Frame::Solid(SolidColorRenderElement::new(
self.cursor_id.clone(),
cursor_rect,
CommitCounter::default(),
CURSOR_COLOR,
Kind::Cursor,
)));
}
}
// El shell va sobre todo; luego las flotantes; luego las
// teseladas. `sort_by_key` es estable: respeta el orden de
// apertura dentro de cada grupo.
let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
shown.sort_by_key(|w| (!w.is_shell, !w.floating));
for w in &shown {
let (x, y) = crate::render_loc(w, output_h);
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
// El marco, encima de la propia superficie de la ventana
// — el shell no lleva.
if !w.is_shell {
let rects = border_rects(x, y, sw, sh);
for (buf, (bx, by, _, _)) in w.borders.iter().zip(rects) {
out.push(Frame::Solid(SolidColorRenderElement::from_buffer(
buf,
(bx, by),
1.0,
1.0,
Kind::Unspecified,
)));
}
}
for el in render_elements_from_surface_tree(
&mut self.renderer,
&w.surface,
(x, y),
1.0,
1.0,
Kind::Unspecified,
) {
out.push(Frame::Window(el));
}
}
out
};
match self.compositor.render_frame::<_, _>(
&mut self.renderer,
&elements,
CLEAR_COLOR,
FrameFlags::DEFAULT,
) {
Ok(result) => {
if !result.is_empty {
match self.compositor.queue_frame(()) {
Ok(()) => self.pending_flip = true,
Err(e) => eprintln!("mirada-compositor · queue_frame: {e}"),
}
}
}
Err(e) => eprintln!("mirada-compositor · render_frame: {e}"),
}
// Avisa a cada cliente de que puede dibujar el siguiente cuadro.
let time = self.start.elapsed().as_millis() as u32;
for w in &self.app.windows {
send_frames_surface_tree(&w.surface, time);
}
// También a la superficie del cursor, por si es un cursor animado.
if let CursorImageStatus::Surface(surface) = &self.app.cursor_status {
if surface.alive() {
send_frames_surface_tree(surface, time);
}
}
}
/// La sesión se cede a otra VT (`Ctrl+Alt+Fn`): suelta la GPU y deja
/// de leer el ratón y el teclado, para no chocar con quien ahora
/// manda en la pantalla.
fn pause_session(&mut self) {
self.active = false;
self.drm.pause();
self.libinput.suspend();
println!("mirada-compositor · sesión cedida a otra VT.");
}
/// La sesión vuelve a esta VT: recupera la GPU y la entrada, reinicia
/// el estado del compositor y repinta.
fn resume_session(&mut self) {
if self.libinput.resume().is_err() {
eprintln!("mirada-compositor · libinput.resume falló.");
}
if let Err(e) = self.drm.activate(false) {
eprintln!("mirada-compositor · drm.activate falló: {e}");
}
if let Err(e) = self.compositor.reset_state() {
eprintln!("mirada-compositor · compositor.reset_state falló: {e}");
}
self.active = true;
self.pending_flip = false;
self.render();
println!("mirada-compositor · sesión recuperada.");
}
/// Tarea periódica: Cerebro enlazado, recarga del keymap, API de
/// control, composición y vaciado hacia los clientes.
fn tick(&mut self) {
self.app.brain_poll();
let n = self.app.windows.len();
if n != self.last_windows {
eprintln!("mirada-compositor · ventanas en pantalla: {n}");
self.last_windows = n;
}
if self.keymap_watch.as_ref().is_some_and(|w| w.changed()) {
if let Some(path) = &self.keymap_path {
match Keymap::load(path) {
Ok(km) => {
let cmd = if let Brain::Embedded(d) = &mut self.app.brain {
Some(d.set_keymap(km))
} else {
None
};
if let Some(cmd) = cmd {
self.app.apply_commands(vec![cmd]);
}
println!("mirada-compositor · keymap recargado.");
}
Err(e) => eprintln!("mirada-compositor · keymap inválido: {e}"),
}
}
}
if let Some(ctl) = &self.ctl {
while let Some(mut conn) = ctl.poll() {
let reply = match conn.read_request() {
Ok(Some(req)) => self.app.serve_ctl(req),
Ok(None) => continue,
Err(e) => CtlReply::Error(format!("{e}")),
};
let _ = conn.reply(&reply);
}
}
self.render();
let _ = self.display.flush_clients();
}
/// Procesa un evento de `libinput`: teclado y puntero.
fn handle_input(&mut self, event: InputEvent<LibinputInputBackend>) {
let time = self.start.elapsed().as_millis() as u32;
match event {
// --- Teclado: intercepta los atajos del Cerebro --------------
InputEvent::Keyboard { event } => {
let Some(keyboard) = self.app.keyboard.clone() else {
return;
};
let code = event.key_code();
let key_state = event.state();
let pressed = key_state == KeyState::Pressed;
keyboard.input::<(), _>(
&mut self.app,
code,
key_state,
SERIAL_COUNTER.next_serial(),
time,
|st, mods, handle| {
if !pressed {
return FilterResult::Forward;
}
if let Some(combo) = combo_string(mods, handle.modified_sym()) {
if st.grabs.contains(&combo) {
st.pending_keybind = Some(combo);
return FilterResult::Intercept(());
}
}
FilterResult::Forward
},
);
if let Some(combo) = self.app.pending_keybind.take() {
let ev = self.app.body.keybind(combo);
self.app.brain_feed(ev);
}
}
// --- Puntero: movimiento relativo (ratón, touchpad) ----------
InputEvent::PointerMotion { event } => {
let (mut x, mut y) = self.app.pointer_loc;
x = (x + event.delta_x()).clamp(0.0, self.output_size.0);
y = (y + event.delta_y()).clamp(0.0, self.output_size.1);
self.app.pointer_loc = (x, y);
if !self.drag_update() {
self.pointer_motion(time);
}
}
// --- Puntero: movimiento absoluto (táctil, tableta) ----------
InputEvent::PointerMotionAbsolute { event } => {
let space = Size::<i32, Logical>::from((
self.output_size.0 as i32,
self.output_size.1 as i32,
));
let pos = event.position_transformed(space);
self.app.pointer_loc = (
pos.x.clamp(0.0, self.output_size.0),
pos.y.clamp(0.0, self.output_size.1),
);
if !self.drag_update() {
self.pointer_motion(time);
}
}
// --- Puntero: botones ----------------------------------------
InputEvent::PointerButton { event } => {
let pressed = event.state() == ButtonState::Pressed;
let button = event.button_code();
// ¿Empieza un arrastre? `Super`+botón sobre una ventana:
// izquierdo mueve, derecho redimensiona.
if pressed && self.app.drag.is_none() {
let super_held = self
.app
.keyboard
.as_ref()
.is_some_and(|kb| kb.modifier_state().logo);
let mode = match button {
BTN_LEFT if super_held => Some(DragMode::Move),
BTN_RIGHT if super_held => Some(DragMode::Resize),
_ => None,
};
if let Some(mode) = mode {
let (x, y) = self.app.pointer_loc;
if let Some(i) = self.window_at(x, y) {
let w = &self.app.windows[i];
let grab = DragGrab {
id: w.id,
mode,
start_pointer: (x, y),
start_rect: (w.loc.0, w.loc.1, w.size.0, w.size.1),
};
self.app.drag = Some(grab);
return; // el arrastre captura el botón
}
}
}
// Durante un arrastre los botones no llegan al cliente;
// soltar cualquiera lo termina.
if self.app.drag.is_some() {
if !pressed {
self.app.drag = None;
}
return;
}
// Botón normal: a la ventana bajo el puntero.
let Some(pointer) = self.app.pointer.clone() else {
return;
};
pointer.button(
&mut self.app,
&ButtonEvent {
serial: SERIAL_COUNTER.next_serial(),
time,
button,
state: event.state(),
},
);
pointer.frame(&mut self.app);
}
// --- Puntero: rueda / desplazamiento -------------------------
InputEvent::PointerAxis { event } => {
let Some(pointer) = self.app.pointer.clone() else {
return;
};
let source = event.source();
let mut frame = AxisFrame::new(time).source(source);
for axis in [Axis::Horizontal, Axis::Vertical] {
match event.amount(axis) {
Some(v) if v != 0.0 => frame = frame.value(axis, v),
Some(_) if source == AxisSource::Finger => {
frame = frame.stop(axis);
}
_ => {}
}
if let Some(d) = event.amount_v120(axis) {
frame = frame.v120(axis, d as i32);
}
}
pointer.axis(&mut self.app, frame);
pointer.frame(&mut self.app);
}
_ => {} // otros dispositivos: aún no
}
}
/// Reenvía el puntero a la ventana que tiene debajo y, si esa ventana
/// cambió, aplica el foco-sigue-ratón avisando al Cerebro.
fn pointer_motion(&mut self, time: u32) {
let Some(pointer) = self.app.pointer.clone() else {
return;
};
let (x, y) = self.app.pointer_loc;
let hit = self.window_at(x, y);
let focus = hit.map(|i| {
let w = &self.app.windows[i];
let (lx, ly) = crate::render_loc(w, self.app.output_size.1);
(
w.surface.clone(),
Point::<f64, Logical>::from((lx as f64, ly as f64)),
)
});
pointer.motion(
&mut self.app,
focus,
&MotionEvent {
location: Point::from((x, y)),
serial: SERIAL_COUNTER.next_serial(),
time,
},
);
pointer.frame(&mut self.app);
// Sobre el escritorio pelado no manda ningún cliente: el cursor
// vuelve al de por defecto (si no, se queda con la «I» del texto
// de la última ventana).
if hit.is_none() {
self.app.cursor_status = CursorImageStatus::default_named();
}
// Foco-sigue-ratón: al pasar a otra ventana, que la enfoque quien
// corresponda — el Cerebro para las teseladas, carmen mismo para
// el shell (que no vive en el Cerebro).
let hovered = hit.map(|i| self.app.windows[i].id);
if hovered != self.last_pointer_window {
self.last_pointer_window = hovered;
match hit {
Some(i) if self.app.windows[i].is_shell => {
let surf = self.app.windows[i].surface.clone();
if let Some(kb) = self.app.keyboard.clone() {
kb.set_focus(&mut self.app, Some(surf), SERIAL_COUNTER.next_serial());
}
}
Some(i) => {
let id = self.app.windows[i].id;
let ev = self.app.body.pointer_enter(id);
self.app.brain_feed(ev);
}
None => {}
}
}
}
/// Si hay un arrastre en curso, recalcula el rectángulo de la ventana
/// y se lo manda al Cerebro (que la hace flotar ahí). Devuelve `true`
/// si consumió el movimiento — entonces el puntero no llega al cliente.
fn drag_update(&mut self) -> bool {
let Some(drag) = self.app.drag.as_ref() else {
return false;
};
let mode = drag.mode;
let (spx, spy) = drag.start_pointer;
let (sx, sy, sw, sh) = drag.start_rect;
let id = drag.id;
let (px, py) = self.app.pointer_loc;
let dx = (px - spx) as i32;
let dy = (py - spy) as i32;
let rect = match mode {
DragMode::Move => Rect::new(sx + dx, sy + dy, sw, sh),
DragMode::Resize => Rect::new(
sx,
sy,
(sw + dx).max(MIN_WINDOW),
(sh + dy).max(MIN_WINDOW),
),
};
self.app.brain_feed(BodyEvent::WindowFloatTo { id, rect });
true
}
/// El índice de la ventana visible bajo el punto `(x, y)`, si la hay
/// — en orden front-to-back (el shell gana a las flotantes, y éstas a
/// las teseladas).
fn window_at(&self, x: f64, y: f64) -> Option<usize> {
let mut idx: Vec<usize> = (0..self.app.windows.len())
.filter(|&i| self.app.windows[i].visible)
.collect();
idx.sort_by_key(|&i| {
let w = &self.app.windows[i];
(!w.is_shell, !w.floating)
});
let output_h = self.app.output_size.1;
idx.into_iter().find(|&i| {
let w = &self.app.windows[i];
let (lx, ly) = crate::render_loc(w, output_h);
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
x >= lx as f64 && y >= ly as f64 && x < (lx + sw) as f64 && y < (ly + sh) as f64
})
}
}
/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b. Con `greeter`,
/// el compositor nace en modo DM: ver [`BodyMode`].
pub fn run(greeter: bool) -> Result<(), Box<dyn Error>> {
println!("mirada-compositor · backend DRM.");
println!("──────────────────────────────────────────────────");
// 1 · Sesión.
println!("[1/8] abriendo la sesión (libseat) …");
let (mut session, session_notifier) = LibSeatSession::new().map_err(|e| {
format!(
"no pude abrir la sesión libseat: {e}\n \
¿estás en una TTY de verdad (Ctrl+Alt+F3), con `seatd` o `logind`?"
)
})?;
let seat_name = session.seat();
println!(" sesión abierta · seat «{seat_name}»");
// 2 · GPU primaria.
println!("[2/8] buscando la GPU primaria …");
let gpu = udev::primary_gpu(&seat_name)
.map_err(|e| format!("error consultando udev: {e}"))?
.ok_or("no encontré ninguna GPU — ¿existe algún /dev/dri/card*?")?;
println!(" GPU primaria: {}", gpu.display());
// 3 · Dispositivo DRM.
println!("[3/8] abriendo el dispositivo DRM …");
let fd = session
.open(&gpu, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NONBLOCK)
.map_err(|e| format!("no pude abrir {}: {e}", gpu.display()))?;
let drm_fd = DrmDeviceFd::new(DeviceFd::from(fd));
let (mut drm, drm_notifier) =
DrmDevice::new(drm_fd.clone(), true).map_err(|e| format!("DrmDevice::new falló: {e}"))?;
println!(" dispositivo DRM listo.");
// 4 · Elegir la salida conectada: conector + CRTC + modo.
println!("[4/8] eligiendo salida …");
let resources = drm
.resource_handles()
.map_err(|e| format!("no pude leer los recursos DRM: {e}"))?;
let mut chosen = None;
for &conn_handle in resources.connectors() {
let conn = match drm.get_connector(conn_handle, false) {
Ok(c) => c,
Err(_) => continue,
};
if conn.state() != ConnectorState::Connected {
continue;
}
let name = format!("{:?}-{}", conn.interface(), conn.interface_id());
// Registra todos los modos del panel — diagnóstico.
for m in conn.modes() {
let (mw, mh) = m.size();
let pref = if m.mode_type().contains(ModeTypeFlags::PREFERRED) {
" [PREFERRED]"
} else {
""
};
eprintln!(" modo de «{name}»: {mw}×{mh} @ {} Hz{pref}", m.vrefresh());
}
// Elige el modo de mayor área (a igualdad, mayor refresco) — el
// nativo del panel. La marca PREFERRED no es fiable: a veces
// señala un modo menor.
let mode = conn
.modes()
.iter()
.max_by_key(|m| {
let (mw, mh) = m.size();
(mw as u32 * mh as u32, m.vrefresh())
})
.copied();
let Some(mode) = mode else {
continue;
};
let crtc = conn
.encoders()
.iter()
.filter_map(|enc| drm.get_encoder(*enc).ok())
.find_map(|enc| resources.filter_crtcs(enc.possible_crtcs()).into_iter().next());
if let Some(crtc) = crtc {
let (w, h) = mode.size();
println!(" salida «{name}» · {w}×{h} · CRTC {crtc:?}");
chosen = Some((conn_handle, crtc, mode, name));
break;
}
}
let (conn_handle, crtc, mode, out_name) =
chosen.ok_or("ninguna salida conectada con CRTC disponible")?;
let (mode_w, mode_h) = mode.size();
// 5 · GBM + EGL + GlesRenderer.
println!("[5/8] inicializando GBM + EGL + GlesRenderer …");
let gbm = GbmDevice::new(drm_fd.clone()).map_err(|e| format!("GbmDevice::new falló: {e}"))?;
let egl_display =
unsafe { EGLDisplay::new(gbm.clone()) }.map_err(|e| format!("EGLDisplay::new falló: {e}"))?;
let egl_context =
EGLContext::new(&egl_display).map_err(|e| format!("EGLContext::new falló: {e}"))?;
let renderer =
unsafe { GlesRenderer::new(egl_context) }.map_err(|e| format!("GlesRenderer falló: {e}"))?;
println!(" renderer GLES listo.");
// 6 · Superficie DRM + DrmCompositor de la salida.
println!("[6/8] creando la superficie DRM y el compositor …");
let surface = drm
.create_surface(crtc, mode, &[conn_handle])
.map_err(|e| format!("create_surface falló: {e}"))?;
let allocator =
GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
let exporter = GbmFramebufferExporter::new(gbm.clone(), None);
let renderer_formats = renderer.dmabuf_formats();
let mode_source = OutputModeSource::Static {
size: Size::from((mode_w as i32, mode_h as i32)),
scale: Scale::from(1.0),
transform: Transform::Normal,
};
let compositor: Compositor = DrmCompositor::new(
mode_source,
surface,
None,
allocator,
exporter,
[Fourcc::Argb8888, Fourcc::Xrgb8888],
renderer_formats,
drm.cursor_size(),
Some(gbm.clone()),
)
.map_err(|e| format!("DrmCompositor::new falló: {e}"))?;
println!(" compositor de «{out_name}» listo.");
// 7 · El estado Wayland (Cerebro, teclado, keymap, control).
println!("[7/8] armando el estado Wayland …");
let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } =
crate::build_app(greeter)?;
// Con el renderer ya creado, anuncia dmabuf — sin esto las apps que
// pintan por GPU (GPUI, navegadores acelerados) no pueden conectarse.
crate::announce_dmabuf(&mut app, &display.handle(), &renderer);
// La salida del Cerebro = el modo del monitor.
let ev = app.body.add_output(0, mode_w as i32, mode_h as i32);
app.brain_feed(ev);
app.output_size = (mode_w as i32, mode_h as i32);
// El puntero arranca en el centro de la pantalla.
app.pointer_loc = (mode_w as f64 / 2.0, mode_h as f64 / 2.0);
// Anuncia el monitor en el protocolo Wayland — los clientes lo exigen.
let _wl_output = crate::announce_output(
&display.handle(),
&out_name,
mode_w as i32,
mode_h as i32,
mode.vrefresh() as i32 * 1000,
);
// El socket Wayland por el que se conectan los clientes.
let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
let socket_name = listener
.socket_name()
.and_then(|s| s.to_str())
.unwrap_or("wayland-?")
.to_string();
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
println!(" escuchando en WAYLAND_DISPLAY={socket_name}");
// Modo DM: lanza el greeter y recibe su tiquet por un canal de
// `calloop`. Modo normal: autoarranque + `MIRADA_STARTUP`.
let greeter_rx = if app.mode == BodyMode::Greeter {
let (tx, rx) = ticket_channel::<SessionTicket>();
crate::spawn_greeter(move |ticket| {
let _ = tx.send(ticket);
})?;
Some(rx)
} else {
// Autoarranque: los programas de `~/.config/mirada/autostart`.
crate::spawn_autostart(None);
// App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza
// como hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin
// saltar de VT.
if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
crate::spawn_command(&cmd, None);
}
None
};
// 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
println!("[8/8] montando el bucle de eventos …");
let mut event_loop: EventLoop<DrmState> =
EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
let handle = event_loop.handle();
// Sesión: pausa/activación al conmutar de VT.
handle
.insert_source(session_notifier, |event, _, state: &mut DrmState| match event {
SessionEvent::PauseSession => state.pause_session(),
SessionEvent::ActivateSession => state.resume_session(),
})
.map_err(|e| format!("insert session: {e}"))?;
// VBlank: el page-flip terminó.
handle
.insert_source(drm_notifier, |event, _meta, state| match event {
DrmEvent::VBlank(_crtc) => {
if let Err(e) = state.compositor.frame_submitted() {
eprintln!("mirada-compositor · frame_submitted: {e}");
}
state.pending_flip = false;
}
DrmEvent::Error(e) => eprintln!("mirada-compositor · DRM: {e}"),
})
.map_err(|e| format!("insert drm: {e}"))?;
// Teclado y ratón vía libinput. Guardamos un clon del contexto (es
// un manejador con contador de referencias) para suspenderlo y
// reanudarlo al conmutar de VT.
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
libinput
.udev_assign_seat(&seat_name)
.map_err(|()| "libinput: no pude asignar el seat")?;
let libinput_handle = libinput.clone();
handle
.insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| {
state.handle_input(event);
})
.map_err(|e| format!("insert libinput: {e}"))?;
// Clientes Wayland nuevos.
handle
.insert_source(
Generic::new(listener, Interest::READ, CalloopMode::Level),
|_readiness, listener, state| {
while let Some(stream) = listener.accept()? {
eprintln!("mirada-compositor · cliente Wayland conectado.");
let _ = state
.display
.handle()
.insert_client(stream, Arc::new(ClientState::default()));
}
Ok(PostAction::Continue)
},
)
.map_err(|e| format!("insert socket: {e}"))?;
// Peticiones de los clientes ya conectados.
let poll_fd = display.backend().poll_fd().try_clone_to_owned()?;
handle
.insert_source(
Generic::new(poll_fd, Interest::READ, CalloopMode::Level),
|_readiness, _fd, state| {
let DrmState { display, app, .. } = state;
if let Err(e) = display.dispatch_clients(app) {
eprintln!("mirada-compositor · dispatch: {e}");
}
let _ = display.flush_clients();
Ok(PostAction::Continue)
},
)
.map_err(|e| format!("insert display: {e}"))?;
// Timer de composición + tareas — ~60 Hz.
handle
.insert_source(Timer::immediate(), |_instant, _meta, state| {
state.tick();
TimeoutAction::ToDuration(Duration::from_millis(16))
})
.map_err(|e| format!("insert timer: {e}"))?;
// Tiquet del greeter (modo DM): al llegar, el traspaso a la sesión.
// El hilo lector del greeter despierta el bucle por este canal.
if let Some(rx) = greeter_rx {
handle
.insert_source(rx, |event, _, state: &mut DrmState| {
if let TicketEvent::Msg(ticket) = event {
state.app.complete_greeter_handoff(ticket);
}
})
.map_err(|e| format!("insert greeter: {e}"))?;
}
// Tope de tiempo opcional: `MIRADA_DRM_TIMEOUT=<segundos>` cierra el
// compositor solo (0 o sin definir = sin tope). El teclado ya
// funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.
let timeout_secs: u64 = std::env::var("MIRADA_DRM_TIMEOUT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
println!("──────────────────────────────────────────────────");
println!("mirada-compositor · escritorio en marcha sobre «{out_name}».");
println!(" Lanza un cliente: WAYLAND_DISPLAY={socket_name} foot");
println!(" Salir: Super+Shift+e · o Ctrl+C en esta TTY.");
if timeout_secs > 0 {
println!(" Se cerrará solo a los {timeout_secs}s (MIRADA_DRM_TIMEOUT=0 lo quita).");
}
let mut state = DrmState {
app,
display,
drm,
compositor,
renderer,
libinput: libinput_handle,
active: true,
pending_flip: false,
keymap_path,
keymap_watch,
ctl,
start: Instant::now(),
last_windows: 0,
cursor_id: Id::new(),
last_pointer_window: None,
output_size: (mode_w as f64, mode_h as f64),
};
let signal = event_loop.get_signal();
event_loop
.run(None, &mut state, |state| {
let timed_out =
timeout_secs > 0 && state.start.elapsed() > Duration::from_secs(timeout_secs);
if !state.app.running || timed_out {
if timed_out {
println!("mirada-compositor · tope de tiempo — cerrando.");
}
signal.stop();
}
})
.map_err(|e| format!("el bucle de eventos falló: {e}"))?;
println!("mirada-compositor · adiós.");
Ok(())
}
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "mirada-ctl"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "mirada-ctl — control del compositor carmen por línea de comandos (estilo swaymsg/hyprctl): aplica acciones de escritorio y consulta ventanas vía el socket de control de mirada-brain."
[[bin]]
name = "mirada-ctl"
path = "src/main.rs"
[dependencies]
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
+143
View File
@@ -0,0 +1,143 @@
//! `mirada-ctl` — el control del compositor carmen por línea de comandos.
//!
//! Al estilo de `swaymsg` / `hyprctl`: dispara una acción de escritorio o
//! consulta el estado, hablando con el Cerebro por su socket de control
//! ([`mirada_brain::ctl`]). El Cerebro es la app `mirada`, o
//! `mirada-compositor` cuando lleva el Cerebro embebido.
//!
//! ```sh
//! mirada-ctl focus-next # cambia el foco
//! mirada-ctl focus-window 5 # enfoca una ventana concreta
//! mirada-ctl workspace 3 # va al escritorio 3
//! mirada-ctl layout grid # fija el modo de teselado
//! mirada-ctl windows # lista las ventanas
//! mirada-ctl actions # lista las acciones
//! ```
use std::process::ExitCode;
use mirada_brain::ctl::{self, CtlReply, CtlRequest, WindowLine};
use mirada_brain::DesktopAction;
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(msg) => {
eprintln!("mirada-ctl: {msg}");
ExitCode::FAILURE
}
}
}
fn run(args: &[String]) -> Result<(), String> {
match args.first().map(String::as_str) {
None | Some("-h" | "--help" | "help") => {
print_help();
Ok(())
}
Some("actions") => {
print_actions();
Ok(())
}
Some("windows") => match request(CtlRequest::ListWindows)? {
CtlReply::Windows(ws) => {
print_windows(&ws);
Ok(())
}
CtlReply::Error(e) => Err(e),
CtlReply::Ok => Err("respuesta inesperada del Cerebro".into()),
},
// Todo lo demás es una acción. `focus-window 5` y `workspace 3`
// se unen con `:` a la forma canónica (`focus-window:5`).
Some(_) => {
let spec = args.join(":");
let action: DesktopAction = spec
.parse()
.map_err(|e| format!("{e}\n lista de acciones: mirada-ctl actions"))?;
match request(CtlRequest::Do(action))? {
CtlReply::Ok => Ok(()),
CtlReply::Error(e) => Err(e),
CtlReply::Windows(_) => Err("respuesta inesperada del Cerebro".into()),
}
}
}
}
/// Manda una petición al Cerebro y devuelve su respuesta.
fn request(req: CtlRequest) -> Result<CtlReply, String> {
let path = ctl::default_socket_path();
ctl::send_request(&path, &req).map_err(|e| {
format!(
"no pude hablar con el Cerebro en {} ({e})\n \
¿está corriendo `mirada` o `mirada-compositor`?",
path.display()
)
})
}
/// Imprime la lista de ventanas, marcando la enfocada con `*`.
fn print_windows(windows: &[WindowLine]) {
if windows.is_empty() {
println!("(no hay ventanas)");
return;
}
for w in windows {
let mark = if w.focused { '*' } else { ' ' };
// El escritorio 0 es el scratchpad (ventana guardada).
let ws = if w.workspace == 0 {
"scratch".to_string()
} else {
w.workspace.to_string()
};
println!("{mark} id {:<4} esc {:<7} {:<24} {}", w.id, ws, w.app_id, w.title);
}
}
fn print_help() {
println!(
"mirada-ctl — control del compositor carmen\n\
\n\
USO:\n \
mirada-ctl <acción> aplica una acción de escritorio\n \
mirada-ctl windows lista las ventanas\n \
mirada-ctl actions lista las acciones disponibles\n\
\n\
EJEMPLOS:\n \
mirada-ctl focus-next\n \
mirada-ctl focus-window 5\n \
mirada-ctl workspace 3\n \
mirada-ctl layout grid"
);
}
fn print_actions() {
// Cadena multilínea literal: la indentación de cada línea es la que
// se imprime (el `\` tras la comilla se come sólo el primer salto).
print!(
"\
Acciones de mirada-ctl:
focus-next mueve el foco a la siguiente ventana
focus-prev mueve el foco a la anterior
focus-window <id> enfoca la ventana <id> (ver: mirada-ctl windows)
move-forward adelanta la ventana enfocada en el teselado
move-backward la atrasa
close-focused cierra la ventana enfocada
toggle-float alterna flotante / teselada la enfocada
toggle-fullscreen alterna pantalla completa en la enfocada
send-to-scratchpad guarda la ventana enfocada en el scratchpad
toggle-scratchpad invoca u oculta la ventana del scratchpad
cycle-layout pasa al siguiente modo de teselado
layout <modo> master-stack · centered-master · spiral
grid · columns · rows · monocle
grow-master agranda el área de la ventana maestra
shrink-master la encoge
inc-master / dec-master de ventanas en el área maestra (nmaster)
promote-to-master la ventana enfocada al puesto maestro
workspace <n> activa el escritorio n (1..9)
send-to-workspace <n> manda la enfocada al escritorio n
focus-output-next pasa el foco al siguiente monitor
quit apaga el compositor
"
);
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "mirada-greeter"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "mirada-greeter — el greeter de carmen: ventana GPUI de login que autentica con brahman-auth y emite un SessionTicket al compositor por stdout."
[[bin]]
name = "mirada-greeter"
path = "src/main.rs"
[dependencies]
gpui = { workspace = true }
nahual-theme = { workspace = true }
nahual-widget-text-input = { workspace = true }
brahman-auth = { path = "../../protocol/brahman-auth" }
+44
View File
@@ -0,0 +1,44 @@
# mirada-greeter
El greeter (pantalla de login) del escritorio carmen.
Una ventana GPUI: el compositor `mirada-compositor`, cuando bootea en
modo greeter, la arranca como proceso hijo, la compone a pantalla
completa (la reconoce por `app_id = "carmen.greeter"`) y le lee el
stdout.
## Flujo
1. El usuario teclea usuario + contraseña. `Enter` en «usuario» pasa el
foco a «contraseña»; `Enter` en «contraseña» autentica.
2. La autenticación corre con [`brahman-auth`] en un hilo de fondo (PAM
puede demorar ~2 s ante un fallo, no se congela la UI).
3. En éxito, el greeter **imprime un `SessionTicket` a stdout** y
termina. El compositor parsea esa línea y hace el traspaso a modo
sesión sin reiniciar el servidor gráfico.
La línea de tiquet lleva el prefijo `MIRADA-SESSION-TICKET-v1`; el resto
del stdout (logs) se ignora.
## Backend de autenticación
| Entorno | Backend |
|---|---|
| (por defecto) | PAM, servicio `carmen` (`/etc/pam.d/carmen`) |
| `MIRADA_GREETER_PAM=<servicio>` | PAM con otro servicio |
| `MIRADA_GREETER_MOCK=usuario:secreto` | Mock — credenciales fijas |
El modo mock sirve para iterar la UI en cajas sin PAM o con el greeter
anidado dentro de otro escritorio:
```sh
MIRADA_GREETER_MOCK=demo:demo cargo run -p mirada-greeter
```
## Integración con el compositor
El consumo del tiquet ya está cableado. `mirada-compositor --greeter`
lanza este greeter, lee su stdout y, al recibir el `SessionTicket`,
muta de `BodyMode::Greeter` a `BodyMode::Session` y arranca la sesión
del usuario con `setuid`/`setgid` — sin reiniciar el servidor Wayland.
Ver el README de `mirada-compositor`, sección **Modo greeter (DM)**.
+242
View File
@@ -0,0 +1,242 @@
//! `mirada-greeter` — el greeter del escritorio carmen.
//!
//! Una ventana GPUI de login. El compositor (`mirada-compositor`) la
//! arranca como proceso hijo cuando bootea en modo greeter, la compone a
//! pantalla completa (la reconoce por `app_id = "carmen.greeter"`) y le
//! lee el stdout.
//!
//! Flujo: el usuario teclea usuario + contraseña, el greeter autentica
//! con [`brahman_auth`], y en éxito **imprime un [`SessionTicket`] a
//! stdout** y termina. El compositor parsea esa línea, hace el traspaso
//! a modo sesión (setuid al usuario + arranque) sin reiniciar el
//! servidor gráfico — la «mutación atómica» del DM.
//!
//! Backend de autenticación (ver [`pick_authenticator`]):
//! - por defecto, PAM contra el servicio `carmen`;
//! - `MIRADA_GREETER_MOCK="usuario:secreto"` usa el mock, para iterar la
//! UI en cajas sin PAM o con el greeter anidado en otro escritorio.
use std::io::Write;
use std::sync::Arc;
use brahman_auth::{
AuthError, Authenticator, MockAuthenticator, PamAuthenticator, SessionTicket, UserInfo,
DEFAULT_SERVICE,
};
use gpui::{
div, prelude::*, px, App, Application, Bounds, Context, Entity, IntoElement, Render,
SharedString, Window, WindowBounds, WindowOptions,
};
use nahual_theme::Theme;
use nahual_widget_text_input::{TextInput, TextInputEvent};
/// `app_id` con el que el compositor reconoce y compone el greeter.
const GREETER_APP_ID: &str = "carmen.greeter";
/// Autenticador compartible entre el hilo de UI y el de fondo.
type DynAuth = Arc<dyn Authenticator + Send + Sync>;
fn main() {
Application::new().run(|cx: &mut App| {
Theme::install_default(cx);
let auth = pick_authenticator();
let bounds = Bounds::centered(None, gpui::size(px(960.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: None,
app_id: Some(GREETER_APP_ID.into()),
..Default::default()
},
|window, cx| cx.new(|cx| Greeter::new(auth, window, cx)),
)
.expect("abrir la ventana del greeter");
cx.activate(true);
});
}
/// Elige el backend de autenticación según el entorno.
fn pick_authenticator() -> DynAuth {
// Modo dev: credenciales fijas, sin tocar PAM.
if let Ok(spec) = std::env::var("MIRADA_GREETER_MOCK") {
if let Some((user, secret)) = spec.split_once(':') {
eprintln!("mirada-greeter · backend mock (usuario «{user}»)");
return Arc::new(MockAuthenticator::new().with_user(user, secret));
}
eprintln!("mirada-greeter · MIRADA_GREETER_MOCK mal formado (falta «:»), ignorado");
}
// Camino real: PAM. Servicio sobreescribible con `MIRADA_GREETER_PAM`.
let service =
std::env::var("MIRADA_GREETER_PAM").unwrap_or_else(|_| DEFAULT_SERVICE.to_string());
eprintln!("mirada-greeter · backend PAM (servicio «{service}»)");
Arc::new(PamAuthenticator::new(service))
}
/// Estado del intento de login en curso.
enum Status {
/// Esperando que el usuario teclee.
Idle,
/// Autenticación en vuelo (en el hilo de fondo).
Authenticating,
/// Último intento falló; el mensaje es para mostrar.
Failed(String),
}
struct Greeter {
auth: DynAuth,
username: Entity<TextInput>,
password: Entity<TextInput>,
status: Status,
}
impl Greeter {
fn new(auth: DynAuth, window: &mut Window, cx: &mut Context<Self>) -> Self {
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
let username = cx.new(|cx| TextInput::new("", cx).with_placeholder("usuario"));
let password = cx.new(|cx| {
TextInput::new("", cx)
.with_placeholder("contraseña")
.with_mask()
});
// Enter en «usuario» pasa el foco a «contraseña».
cx.subscribe_in(&username, window, |this, _u, ev, window, cx| {
if let TextInputEvent::Confirmed(_) = ev {
this.password.read(cx).request_focus(window);
}
})
.detach();
// Enter en «contraseña» dispara la autenticación.
cx.subscribe_in(&password, window, |this, _p, ev, window, cx| {
if let TextInputEvent::Confirmed(_) = ev {
this.submit(window, cx);
}
})
.detach();
// Foco inicial en «usuario».
username.read(cx).request_focus(window);
Self {
auth,
username,
password,
status: Status::Idle,
}
}
/// Valida el formulario y lanza la autenticación en el hilo de fondo
/// (PAM puede tardar — `pam_unix` demora ~2 s ante un fallo).
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if matches!(self.status, Status::Authenticating) {
return; // intento ya en curso
}
let user = self.username.read(cx).text().trim().to_string();
let secret = self.password.read(cx).text().to_string();
if user.is_empty() {
self.status = Status::Failed("ingresá un usuario".into());
self.username.read(cx).request_focus(window);
cx.notify();
return;
}
self.status = Status::Authenticating;
cx.notify();
let auth = Arc::clone(&self.auth);
cx.spawn(async move |this, cx| {
let result = cx
.background_executor()
.spawn(async move { auth.authenticate(&user, &secret) })
.await;
let _ = this.update(cx, |me, cx| me.finish(result, cx));
})
.detach();
}
/// Procesa el resultado de la autenticación.
fn finish(&mut self, result: Result<UserInfo, AuthError>, cx: &mut Context<Self>) {
match result {
Ok(user) => {
// El compositor lee esta línea del stdout del greeter.
emit_ticket(&SessionTicket::new(user));
cx.quit();
}
Err(e) => {
self.status = Status::Failed(e.to_string());
// Limpia la contraseña; el foco ya está en ese campo.
self.password.update(cx, |p, cx| p.set_text("", cx));
cx.notify();
}
}
}
}
impl Render for Greeter {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::global(cx).clone();
let (status_msg, status_color) = match &self.status {
Status::Idle => (SharedString::default(), theme.fg_muted),
Status::Authenticating => (SharedString::from("verificando…"), theme.fg_muted),
Status::Failed(m) => (SharedString::from(m.clone()), theme.accent_destructive()),
};
div()
.size_full()
.flex()
.items_center()
.justify_center()
.bg(theme.bg_app)
.child(
div()
.flex()
.flex_col()
.gap(px(12.0))
.w(px(320.0))
.p(px(28.0))
.bg(theme.bg_panel)
.border_1()
.border_color(theme.border)
.rounded(px(12.0))
.child(
div()
.text_size(px(22.0))
.text_color(theme.fg_text)
.child("carmen"),
)
.child(
div()
.text_size(px(12.0))
.text_color(theme.fg_muted)
.child("iniciá tu sesión"),
)
.child(caption(&theme, "usuario"))
.child(self.username.clone())
.child(caption(&theme, "contraseña"))
.child(self.password.clone())
.child(
div()
.h(px(16.0))
.text_size(px(11.0))
.text_color(status_color)
.child(status_msg),
),
)
}
}
/// Etiqueta pequeña sobre un campo del formulario.
fn caption(theme: &Theme, text: &'static str) -> impl IntoElement {
div()
.text_size(px(10.0))
.text_color(theme.fg_muted)
.child(text)
}
/// Imprime el tiquet a stdout y fuerza el flush antes de terminar.
fn emit_ticket(ticket: &SessionTicket) {
println!("{}", ticket.to_line());
let _ = std::io::stdout().flush();
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "mirada-launcher"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "mirada-launcher — lanzador de aplicaciones para carmen: escanea los .desktop del sistema, los lista en la terminal y lanza el que elijas. Sin dependencias; pensado para correr en una terminal pequeña que el compositor abre con un atajo."
[[bin]]
name = "mirada-launcher"
path = "src/main.rs"
[dependencies]
+274
View File
@@ -0,0 +1,274 @@
//! `mirada-launcher` — un lanzador de aplicaciones para carmen.
//!
//! Escanea los archivos `.desktop` del sistema (el estándar XDG), los
//! lista en la terminal y lanza el que elijas. No tiene dependencias: la
//! interfaz es una lista numerada que se filtra escribiendo.
//!
//! Pensado para correr dentro de una terminal pequeña que el compositor
//! abre con un atajo — p. ej. atando `Super+d` a
//! `spawn:foot -e mirada-launcher` en el keymap de mirada. Al elegir una
//! aplicación, la lanza y termina (la terminal se cierra sola); el
//! programa lanzado queda corriendo, reparentado a init.
//!
//! También sirve suelto: `mirada-launcher` en cualquier terminal.
use std::collections::HashSet;
use std::io::{self, Write};
use std::path::PathBuf;
/// Una aplicación lista para lanzar, sacada de un `.desktop`.
struct DesktopApp {
/// Nombre visible (`Name=`).
name: String,
/// Comando a ejecutar, ya sin los códigos de campo (`%u`, `%F`…).
exec: String,
/// `true` si la app necesita una terminal (`Terminal=true`).
needs_terminal: bool,
}
fn main() {
let mut apps = scan_apps();
apps.sort_by_key(|a| a.name.to_lowercase());
if apps.is_empty() {
eprintln!("mirada-launcher · no encontré ninguna aplicación .desktop.");
std::process::exit(1);
}
run_ui(&apps);
}
// ---------------------------------------------------------------------
// Escaneo de los .desktop
// ---------------------------------------------------------------------
/// Recorre los directorios XDG de aplicaciones y devuelve las que se
/// pueden lanzar. Un `.desktop` de un directorio de mayor prioridad
/// tapa a otro con el mismo nombre de archivo en uno de menor.
fn scan_apps() -> Vec<DesktopApp> {
let mut apps = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for dir in application_dirs() {
collect_desktop_files(&dir, &dir, &mut seen, &mut apps);
}
apps
}
/// Los directorios `applications/` del estándar XDG, en orden de
/// prioridad: primero el del usuario, luego los del sistema.
fn application_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
let data_home = std::env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")));
if let Some(home) = data_home {
dirs.push(home.join("applications"));
}
let data_dirs = std::env::var("XDG_DATA_DIRS")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "/usr/local/share:/usr/share".to_string());
for d in data_dirs.split(':').filter(|s| !s.is_empty()) {
dirs.push(PathBuf::from(d).join("applications"));
}
dirs
}
/// Recoge los `.desktop` de `dir` (y subdirectorios) sin repetir id.
fn collect_desktop_files(
root: &PathBuf,
dir: &PathBuf,
seen: &mut HashSet<String>,
apps: &mut Vec<DesktopApp>,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_desktop_files(root, &path, seen, apps);
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("desktop") {
continue;
}
// El id XDG: la ruta relativa al directorio raíz, con `/` → `-`.
let id = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.replace('/', "-");
if !seen.insert(id) {
continue; // ya lo tapó un directorio de más prioridad
}
if let Ok(text) = std::fs::read_to_string(&path) {
if let Some(app) = parse_desktop(&text) {
apps.push(app);
}
}
}
}
/// Extrae una [`DesktopApp`] del texto de un `.desktop`. `None` si no es
/// una aplicación lanzable o está marcada para no mostrarse.
fn parse_desktop(text: &str) -> Option<DesktopApp> {
let mut in_entry = false;
let (mut name, mut exec, mut kind) = (None, None, None);
let (mut no_display, mut hidden, mut terminal) = (false, false, false);
for line in text.lines() {
let line = line.trim();
if line.starts_with('[') {
// Sólo nos interesa el grupo principal; otros (acciones,
// etc.) se ignoran.
in_entry = line == "[Desktop Entry]";
continue;
}
if !in_entry || line.is_empty() || line.starts_with('#') {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let value = value.trim();
match key.trim() {
"Name" => name = Some(value.to_string()),
"Exec" => exec = Some(value.to_string()),
"Type" => kind = Some(value.to_string()),
"NoDisplay" => no_display = value == "true",
"Hidden" => hidden = value == "true",
"Terminal" => terminal = value == "true",
_ => {} // Name[es], Icon, Categories…: no los usamos
}
}
if no_display || hidden {
return None;
}
if kind.as_deref() != Some("Application") {
return None;
}
let name = name?;
let exec = strip_field_codes(&exec?);
if name.is_empty() || exec.is_empty() {
return None;
}
Some(DesktopApp { name, exec, needs_terminal: terminal })
}
/// Quita los códigos de campo de un `Exec` de `.desktop` (`%u`, `%F`,
/// `%i`…), que sólo tienen sentido al abrir archivos. `%%` queda en `%`.
fn strip_field_codes(exec: &str) -> String {
let mut out = String::new();
let mut chars = exec.chars();
while let Some(c) = chars.next() {
if c == '%' {
// `%%` es un `%` literal; cualquier otro `%x` es un código de
// campo y se descarta entero.
if let Some('%') = chars.next() {
out.push('%');
}
} else {
out.push(c);
}
}
out.trim().to_string()
}
// ---------------------------------------------------------------------
// Interfaz de terminal
// ---------------------------------------------------------------------
/// Cuántas aplicaciones se listan como mucho de una vez.
const MAX_SHOWN: usize = 40;
/// El bucle de la interfaz: muestra la lista, lee una línea y según sea
/// un número lanza, texto filtra, o vacía sale.
fn run_ui(apps: &[DesktopApp]) {
let mut filter = String::new();
loop {
let needle = filter.to_lowercase();
let matches: Vec<&DesktopApp> = apps
.iter()
.filter(|a| needle.is_empty() || a.name.to_lowercase().contains(&needle))
.collect();
// Limpia la pantalla y dibuja la lista.
print!("\x1b[2J\x1b[H");
if filter.is_empty() {
println!("mirada-launcher · {} aplicaciones", matches.len());
} else {
println!(
"mirada-launcher · {} de {} · filtro «{filter}»",
matches.len(),
apps.len()
);
}
println!();
if matches.is_empty() {
println!(" (sin coincidencias)");
}
for (i, a) in matches.iter().take(MAX_SHOWN).enumerate() {
println!(" {:>2} {}", i + 1, a.name);
}
if matches.len() > MAX_SHOWN {
println!(" … y {} más — afina el filtro", matches.len() - MAX_SHOWN);
}
println!();
println!(" nº = lanzar · texto = filtrar · Enter vacío = salir");
print!("> ");
io::stdout().flush().ok();
let mut line = String::new();
if io::stdin().read_line(&mut line).unwrap_or(0) == 0 {
return; // fin de entrada (Ctrl+D)
}
let line = line.trim();
if line.is_empty() {
return;
}
// ¿Un número? Lanza esa entrada de la lista visible.
if let Ok(n) = line.parse::<usize>() {
if (1..=matches.len().min(MAX_SHOWN)).contains(&n) {
launch(matches[n - 1]);
return;
}
continue; // número fuera de rango: vuelve a pedir
}
// Texto: es un filtro nuevo. Si deja una sola, lánzala directo.
filter = line.to_string();
let needle = filter.to_lowercase();
let now: Vec<&DesktopApp> = apps
.iter()
.filter(|a| a.name.to_lowercase().contains(&needle))
.collect();
if now.len() == 1 {
launch(now[0]);
return;
}
}
}
/// Lanza la aplicación elegida como proceso hijo y devuelve. Hereda el
/// entorno —`WAYLAND_DISPLAY` incluido—; al terminar el lanzador, el
/// proceso queda corriendo, reparentado a init.
fn launch(app: &DesktopApp) {
let cmd = if app.needs_terminal {
format!("foot -e {}", app.exec)
} else {
app.exec.clone()
};
print!("\x1b[2J\x1b[H");
println!("mirada-launcher · lanzando «{}» …", app.name);
match std::process::Command::new("sh").arg("-c").arg(&cmd).spawn() {
Ok(_) => {}
Err(e) => {
eprintln!("mirada-launcher · no pude lanzar «{cmd}»: {e}");
std::process::exit(1);
}
}
}
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "mirada-portal"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "mirada-portal — backend xdg-desktop-portal de carmen: implementa org.freedesktop.impl.portal.Settings y publica el tema activo de nahual (claro/oscuro + acento + contraste) a GTK, Qt, Firefox y Chromium por protocolo."
[[bin]]
name = "mirada-portal"
path = "src/main.rs"
[dependencies]
zbus = { workspace = true }
notify = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
anyhow = { workspace = true }
+76
View File
@@ -0,0 +1,76 @@
# mirada-portal
Backend de `xdg-desktop-portal` para el escritorio **carmen**. Implementa
`org.freedesktop.impl.portal.Settings` y publica un único namespace:
`org.freedesktop.appearance`.
## Qué resuelve
GTK y Qt leen su configuración de sitios incompatibles entre sí. Pero
**ambos** —además de Firefox y Chromium— consultan el portal de
FreeDesktop para saber:
- `color-scheme` — claro (`2`) u oscuro (`1`),
- `accent-color` — el color de acento como `(ddd)` RGB,
- `contrast` — contraste alto (`1`) o normal (`0`).
`mirada-portal` responde esas tres claves a partir del tema activo de
`nahual` y, cuando el tema cambia, emite `SettingChanged`: todo el
ecosistema voltea en vivo, **sin tocar un solo archivo de config de las
apps**.
## Fuente del tema
El daemon lee `$XDG_CONFIG_HOME/nahual/theme` (el archivo que persiste
`nahual-theme`, con el nombre del preset activo) y lo vigila con
`notify`. La traducción nombre → hechos del portal está en
[`src/theme_facts.rs`], que espeja `nahual_theme::Theme::all()` sin
enlazar GPUI.
## Arquitectura
Esto es el **backend** del portal. El frontend genérico
`xdg-desktop-portal` (paquete agnóstico, liviano) enruta las llamadas de
las apps hacia este backend según el archivo `mirada.portal`. No hay que
implementar el frontend.
## Instalación de los archivos de `data/`
```sh
install -Dm644 data/mirada.portal \
/usr/share/xdg-desktop-portal/portals/mirada.portal
install -Dm644 data/mirada-portals.conf \
/usr/share/xdg-desktop-portal/mirada-portals.conf
install -Dm644 data/org.freedesktop.impl.portal.desktop.mirada.service \
/usr/share/dbus-1/services/org.freedesktop.impl.portal.desktop.mirada.service
install -Dm755 target/release/mirada-portal /usr/bin/mirada-portal
```
El frontend casa `UseIn=mirada` contra `XDG_CURRENT_DESKTOP`, así que
carmen debe exportar `XDG_CURRENT_DESKTOP=mirada`. Alternativamente, el
`mirada-portals.conf` lo fuerza con `default=mirada`.
`mirada-portal` se puede arrancar desde `~/.config/mirada/autostart` o
dejar que el frontend lo active por D-Bus (de ahí el `.service`).
## Smoke test (sin frontend ni apps GTK)
Con un bus de sesión vivo, el backend se puede interrogar directo:
```sh
busctl --user introspect org.freedesktop.impl.portal.desktop.mirada \
/org/freedesktop/portal/desktop
busctl --user call org.freedesktop.impl.portal.desktop.mirada \
/org/freedesktop/portal/desktop \
org.freedesktop.impl.portal.Settings ReadAll as 0
```
Cambiar `~/.config/nahual/theme` debe disparar una señal `SettingChanged`
(observable con `busctl --user monitor`).
## Límite conocido (v1)
El portal `org.freedesktop.appearance` sólo lleva claro/oscuro + acento +
contraste. **No** lleva la paleta completa de `nahual`. Para recolorear
GTK/Qt a los colores exactos del tema hace falta, además, inyección de
entorno + CSS generado en el `spawn` de carmen — siguiente paso del plan.
@@ -0,0 +1,3 @@
[preferred]
default=mirada
org.freedesktop.impl.portal.Settings=mirada
@@ -0,0 +1,4 @@
[portal]
DBusName=org.freedesktop.impl.portal.desktop.mirada
Interfaces=org.freedesktop.impl.portal.Settings
UseIn=mirada
@@ -0,0 +1,3 @@
[D-BUS Service]
Name=org.freedesktop.impl.portal.desktop.mirada
Exec=/usr/bin/mirada-portal
+430
View File
@@ -0,0 +1,430 @@
//! `mirada-portal` — backend de `xdg-desktop-portal` para el escritorio
//! carmen.
//!
//! Implementa la interfaz `org.freedesktop.impl.portal.Settings` y
//! publica un único namespace: `org.freedesktop.appearance`. Con eso,
//! GTK4/libadwaita, Qt6, Firefox y Chromium leen del sistema —
//! **por protocolo, sin tocar sus archivos de config** — si el
//! escritorio está en modo claro u oscuro, su color de acento y si pide
//! contraste alto. Cuando el tema de `nahual` cambia, el portal emite
//! `SettingChanged` y todas esas apps voltean en vivo.
//!
//! Fuente del tema: el archivo que persiste `nahual-theme`
//! (`$XDG_CONFIG_HOME/nahual/theme`, contiene el nombre del preset
//! activo). El portal lo vigila con `notify` y reexpone sus hechos —
//! ver [`theme_facts`].
//!
//! Este crate es el **backend** del portal: el frontend genérico
//! `xdg-desktop-portal` lo enruta vía el archivo `mirada.portal`. Ver
//! el README para la instalación de los archivos de `data/`.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tokio::signal::unix::{signal, SignalKind};
use tracing::{info, warn};
use tracing_subscriber::EnvFilter;
use zbus::zvariant::{OwnedValue, Value};
use zbus::{fdo, interface, SignalContext};
mod theme_facts;
use theme_facts::ThemeFacts;
/// Nombre de bus del backend. El patrón `org.freedesktop.impl.portal.
/// desktop.<id>` es el que espera el frontend `xdg-desktop-portal`; el
/// `<id>` (`mirada`) tiene que coincidir con el `DBusName` del archivo
/// `mirada.portal`.
const BUS_NAME: &str = "org.freedesktop.impl.portal.desktop.mirada";
/// Ruta de objeto canónica de los portales del escritorio.
const OBJ_PATH: &str = "/org/freedesktop/portal/desktop";
/// Único namespace que servimos. El estándar moderno que leen GTK, Qt,
/// Firefox y Chromium para claro/oscuro + acento.
const APPEARANCE_NS: &str = "org.freedesktop.appearance";
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
init_tracing();
info!("mirada-portal: arrancando backend org.freedesktop.impl.portal.Settings");
let theme_path = theme_config_path();
let initial = read_facts(theme_path.as_deref());
info!(
?theme_path,
color_scheme = initial.color_scheme(),
contrast = initial.contrast(),
"tema inicial resuelto"
);
let facts = Arc::new(Mutex::new(initial));
let portal = SettingsPortal {
facts: Arc::clone(&facts),
};
// El portal vive en el bus de **sesión** (no el de sistema): es un
// servicio del escritorio del usuario, no del sistema.
let conn_result = zbus::connection::Builder::session()
.and_then(|b| b.name(BUS_NAME))
.and_then(|b| b.serve_at(OBJ_PATH, portal));
match conn_result {
Ok(builder) => match builder.build().await {
Ok(conn) => {
info!(name = BUS_NAME, "name adquirido en el bus de sesión");
run(conn, facts, theme_path).await
}
Err(e) => {
warn!(?e, "no se pudo construir la conexión D-Bus — modo idle");
wait_for_term().await
}
},
Err(e) => {
warn!(?e, "builder D-Bus falló (¿hay bus de sesión?) — modo idle");
wait_for_term().await
}
}
}
/// Conectado al bus: monta el watcher del tema y espera la señal de
/// término. El watcher se guarda en `_watcher` para que no se dropee
/// (al dropearse dejaría de vigilar).
async fn run(
conn: zbus::Connection,
facts: Arc<Mutex<ThemeFacts>>,
theme_path: Option<PathBuf>,
) -> anyhow::Result<()> {
let _watcher = match &theme_path {
Some(path) => match spawn_theme_watcher(conn.clone(), Arc::clone(&facts), path.clone()) {
Ok(w) => Some(w),
Err(e) => {
warn!(
?e,
"watcher del tema no disponible — el portal no actualizará en vivo"
);
None
}
},
None => {
warn!("sin ruta de config de tema — el portal sirve un valor fijo");
None
}
};
wait_for_term().await
}
// ============================================================================
// Interfaz D-Bus: org.freedesktop.impl.portal.Settings
// ============================================================================
struct SettingsPortal {
/// Hechos del tema activo. El watcher los reescribe cuando cambia.
facts: Arc<Mutex<ThemeFacts>>,
}
#[interface(name = "org.freedesktop.impl.portal.Settings")]
impl SettingsPortal {
/// Versión de la interfaz impl. `ReadOne` se agregó en la 2.
#[zbus(property, name = "version")]
fn version(&self) -> u32 {
2
}
/// `ReadAll(namespaces) -> a{sa{sv}}`. Los `namespaces` son patrones
/// (sufijo `*` = prefijo); lista vacía = todos. Sólo respondemos
/// `org.freedesktop.appearance`.
async fn read_all(
&self,
namespaces: Vec<String>,
) -> fdo::Result<HashMap<String, HashMap<String, OwnedValue>>> {
let mut out = HashMap::new();
if namespace_requested(&namespaces, APPEARANCE_NS) {
let facts = *self.facts.lock().unwrap();
out.insert(APPEARANCE_NS.to_string(), appearance_map(&facts)?);
}
Ok(out)
}
/// `ReadOne(namespace, key) -> v`. Lee un único valor.
async fn read_one(&self, namespace: String, key: String) -> fdo::Result<OwnedValue> {
let facts = *self.facts.lock().unwrap();
lookup(&facts, &namespace, &key)
}
/// `Read(namespace, key) -> v`. Deprecado a favor de `ReadOne` desde
/// la versión 2 del portal, pero apps viejas lo siguen llamando.
async fn read(&self, namespace: String, key: String) -> fdo::Result<OwnedValue> {
let facts = *self.facts.lock().unwrap();
lookup(&facts, &namespace, &key)
}
/// `SettingChanged(namespace, key, value)`. Lo emite el watcher
/// cuando el tema persistido cambia.
#[zbus(signal)]
async fn setting_changed(
ctxt: &SignalContext<'_>,
namespace: &str,
key: &str,
value: Value<'_>,
) -> zbus::Result<()>;
}
// ============================================================================
// Mapeo tema → valores del portal
// ============================================================================
/// Construye el mapa `a{sv}` del namespace `org.freedesktop.appearance`.
fn appearance_map(facts: &ThemeFacts) -> fdo::Result<HashMap<String, OwnedValue>> {
Ok(HashMap::from([
(
"color-scheme".to_string(),
into_owned(Value::U32(facts.color_scheme()))?,
),
(
"contrast".to_string(),
into_owned(Value::U32(facts.contrast()))?,
),
("accent-color".to_string(), into_owned(accent_value(facts))?),
]))
}
/// Resuelve una clave concreta dentro de `org.freedesktop.appearance`.
fn lookup(facts: &ThemeFacts, namespace: &str, key: &str) -> fdo::Result<OwnedValue> {
if namespace != APPEARANCE_NS {
return Err(fdo::Error::Failed(format!(
"namespace no servido por mirada-portal: {namespace}"
)));
}
let value = match key {
"color-scheme" => Value::U32(facts.color_scheme()),
"contrast" => Value::U32(facts.contrast()),
"accent-color" => accent_value(facts),
other => {
return Err(fdo::Error::Failed(format!(
"clave desconocida en {APPEARANCE_NS}: {other}"
)));
}
};
into_owned(value)
}
/// El acento como structure `(ddd)` — tres dobles RGB en 0..1.
fn accent_value(facts: &ThemeFacts) -> Value<'static> {
let (r, g, b) = facts.accent_rgb();
Value::from((r, g, b))
}
/// `Value` → `OwnedValue`. Sólo falla con valores que llevan fds; los
/// nuestros (enteros y dobles) nunca lo hacen.
fn into_owned(value: Value<'_>) -> fdo::Result<OwnedValue> {
OwnedValue::try_from(value).map_err(|e| fdo::Error::Failed(format!("zvariant: {e}")))
}
/// ¿El patrón de namespaces de un `ReadAll` pide `ns`? Lista vacía =
/// todos. Un patrón con sufijo `*` matchea por prefijo; sino, exacto.
fn namespace_requested(patterns: &[String], ns: &str) -> bool {
if patterns.is_empty() {
return true;
}
patterns.iter().any(|p| match p.strip_suffix('*') {
Some(prefix) => ns.starts_with(prefix),
None => p == ns,
})
}
// ============================================================================
// Watcher del tema persistido
// ============================================================================
/// Vigila el archivo de tema de `nahual`; cuando cambia, recomputa los
/// hechos y emite `SettingChanged`. Devuelve el watcher, que el caller
/// debe mantener vivo.
fn spawn_theme_watcher(
conn: zbus::Connection,
facts: Arc<Mutex<ThemeFacts>>,
path: PathBuf,
) -> notify::Result<notify::RecommendedWatcher> {
use notify::{RecursiveMode, Watcher};
// Canal acotado: el callback de notify (en su propio hilo) sólo
// hace `try_send`; si el buffer está lleno ya hay un evento
// pendiente y da igual perder éste — coalescencia natural.
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(8);
let mut watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
if res.is_ok() {
let _ = tx.try_send(());
}
})?;
// Vigilamos el **directorio padre**: así captamos también la
// creación del archivo si aún no existe.
let watch_target = path.parent().unwrap_or(&path).to_path_buf();
watcher.watch(&watch_target, RecursiveMode::NonRecursive)?;
info!(dir = ?watch_target, "vigilando el directorio del tema");
tokio::spawn(async move {
while rx.recv().await.is_some() {
let fresh = read_facts(Some(&path));
let changed = {
let mut guard = facts.lock().unwrap();
let differs = *guard != fresh;
*guard = fresh;
differs
};
if changed {
info!(
color_scheme = fresh.color_scheme(),
contrast = fresh.contrast(),
"el tema cambió — emitiendo SettingChanged"
);
if let Err(e) = emit_appearance_changed(&conn, &fresh).await {
warn!(?e, "no se pudo emitir SettingChanged");
}
}
}
});
Ok(watcher)
}
/// Emite `SettingChanged` para las tres claves de `appearance`.
async fn emit_appearance_changed(conn: &zbus::Connection, facts: &ThemeFacts) -> zbus::Result<()> {
let ctxt = SignalContext::new(conn, OBJ_PATH)?;
SettingsPortal::setting_changed(
&ctxt,
APPEARANCE_NS,
"color-scheme",
Value::U32(facts.color_scheme()),
)
.await?;
SettingsPortal::setting_changed(
&ctxt,
APPEARANCE_NS,
"contrast",
Value::U32(facts.contrast()),
)
.await?;
SettingsPortal::setting_changed(&ctxt, APPEARANCE_NS, "accent-color", accent_value(facts))
.await?;
Ok(())
}
// ============================================================================
// Lectura del tema persistido
// ============================================================================
/// Lee el nombre de tema del archivo y resuelve sus hechos. Si el
/// archivo falta o está vacío, asume `Nebula` — el default de
/// `nahual_theme::install_default`.
fn read_facts(path: Option<&Path>) -> ThemeFacts {
let name = path
.and_then(|p| std::fs::read_to_string(p).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "Nebula".to_string());
theme_facts::facts_for(&name)
}
/// Ruta del archivo donde `nahual-theme` persiste el nombre del tema
/// activo. Réplica de `nahual_theme::config_path()` — `mirada-portal`
/// no enlaza GPUI, así que no puede llamarla directamente. Convención
/// XDG: `$XDG_CONFIG_HOME/nahual/theme`, sino `$HOME/.config/...`.
fn theme_config_path() -> Option<PathBuf> {
let base = std::env::var("XDG_CONFIG_HOME")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(|| {
std::env::var("HOME")
.ok()
.filter(|s| !s.is_empty())
.map(|h| PathBuf::from(h).join(".config"))
})?;
Some(base.join("nahual").join("theme"))
}
// ============================================================================
// Plomería
// ============================================================================
async fn wait_for_term() -> anyhow::Result<()> {
let mut term = signal(SignalKind::terminate())?;
let mut int_ = signal(SignalKind::interrupt())?;
tokio::select! {
_ = term.recv() => info!("SIGTERM"),
_ = int_.recv() => info!("SIGINT"),
}
Ok(())
}
fn init_tracing() {
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("mirada_portal=info"));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(true)
.init();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn namespace_empty_matches_all() {
assert!(namespace_requested(&[], APPEARANCE_NS));
}
#[test]
fn namespace_exact_match() {
assert!(namespace_requested(
&[APPEARANCE_NS.to_string()],
APPEARANCE_NS
));
assert!(!namespace_requested(
&["org.example".to_string()],
APPEARANCE_NS
));
}
#[test]
fn namespace_wildcard_prefix() {
assert!(namespace_requested(
&["org.freedesktop.*".to_string()],
APPEARANCE_NS
));
assert!(!namespace_requested(
&["org.gnome.*".to_string()],
APPEARANCE_NS
));
}
#[test]
fn appearance_map_has_three_keys() {
let facts = theme_facts::facts_for("Nebula");
let m = appearance_map(&facts).unwrap();
assert_eq!(m.len(), 3);
assert!(m.contains_key("color-scheme"));
assert!(m.contains_key("accent-color"));
assert!(m.contains_key("contrast"));
}
#[test]
fn lookup_unknown_namespace_errs() {
let facts = theme_facts::facts_for("Nebula");
assert!(lookup(&facts, "org.gnome.desktop.interface", "color-scheme").is_err());
}
#[test]
fn lookup_unknown_key_errs() {
let facts = theme_facts::facts_for("Nebula");
assert!(lookup(&facts, APPEARANCE_NS, "no-such-key").is_err());
}
#[test]
fn lookup_color_scheme_ok() {
let facts = theme_facts::facts_for("Solarized Light");
assert!(lookup(&facts, APPEARANCE_NS, "color-scheme").is_ok());
}
}
@@ -0,0 +1,199 @@
//! Tabla de hechos del tema relevantes para el portal.
//!
//! El portal sólo necesita tres hechos de cada tema: si es oscuro, su
//! color de acento, y si es de alto contraste. La fuente de verdad de
//! la paleta completa es `nahual_theme::Theme` (crate `nahual-theme`);
//! esta tabla la **espeja deliberadamente** para que el daemon del
//! portal no tenga que enlazar GPUI (que `nahual-theme` arrastra por
//! sus tipos `Hsla`/`Background`).
//!
//! Si se agrega un preset nuevo a `nahual_theme::Theme::all()`, hay que
//! reflejarlo aquí. Un nombre desconocido cae a [`FALLBACK`] — el
//! portal degrada a "oscuro, sin acento marcado" en vez de romperse.
/// Hechos de un tema que el portal expone por `org.freedesktop.appearance`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ThemeFacts {
/// `true` → el tema es oscuro (`color-scheme` = 1).
pub is_dark: bool,
/// `true` → alto contraste (`contrast` = 1).
pub high_contrast: bool,
/// Color de acento en HSL: `(matiz 0..360, saturación 0..1, luz 0..1)`.
/// Se guarda en HSL porque así está escrito en `nahual-theme` — la
/// conversión a RGB se hace al servir el valor.
pub accent_hsl: (f64, f64, f64),
}
impl ThemeFacts {
/// `color-scheme` de `org.freedesktop.appearance`: 0 = sin
/// preferencia, 1 = oscuro, 2 = claro. El escritorio siempre tiene
/// un tema activo, así que nunca devolvemos 0.
pub fn color_scheme(&self) -> u32 {
if self.is_dark {
1
} else {
2
}
}
/// `contrast` de `org.freedesktop.appearance`: 0 = normal,
/// 1 = contraste alto.
pub fn contrast(&self) -> u32 {
u32::from(self.high_contrast)
}
/// Acento como RGB en 0..1, el formato `(ddd)` que pide el portal.
pub fn accent_rgb(&self) -> (f64, f64, f64) {
let (h, s, l) = self.accent_hsl;
hsl_to_rgb(h, s, l)
}
}
/// Tema por defecto si el nombre persistido no se reconoce: oscuro, sin
/// acento marcado (gris neutro). Degradación segura ante un preset
/// futuro que esta tabla aún no conozca.
pub const FALLBACK: ThemeFacts = ThemeFacts {
is_dark: true,
high_contrast: false,
accent_hsl: (0.0, 0.0, 0.5),
};
/// Mapea el nombre persistido de un tema a sus hechos. Espeja
/// `nahual_theme::Theme::all()` (8 presets al 2026-05-21). Los números
/// de acento están copiados literalmente de `nahual-theme/src/lib.rs`.
pub fn facts_for(name: &str) -> ThemeFacts {
match name.trim() {
"Nebula" => ThemeFacts {
is_dark: true,
high_contrast: false,
accent_hsl: (280.0, 0.65, 0.65),
},
"Aurora" => ThemeFacts {
is_dark: true,
high_contrast: false,
accent_hsl: (150.0, 0.70, 0.55),
},
"Sunset" => ThemeFacts {
is_dark: true,
high_contrast: false,
accent_hsl: (15.0, 0.78, 0.62),
},
"Flat Dark" => ThemeFacts {
is_dark: true,
high_contrast: false,
accent_hsl: (210.0, 0.70, 0.55),
},
"Solarized Light" => ThemeFacts {
is_dark: false,
high_contrast: false,
accent_hsl: (205.0, 0.69, 0.42),
},
"High Contrast" => ThemeFacts {
is_dark: true,
high_contrast: true,
accent_hsl: (60.0, 1.00, 0.60),
},
"Print Color" => ThemeFacts {
is_dark: false,
high_contrast: false,
accent_hsl: (15.0, 0.70, 0.40),
},
"Print B&W" => ThemeFacts {
is_dark: false,
high_contrast: false,
accent_hsl: (0.0, 0.00, 0.20),
},
_ => FALLBACK,
}
}
/// HSL → RGB. `h` en grados 0..360, `s` y `l` en 0..1. Devuelve RGB en
/// 0..1. Algoritmo estándar (croma + segmento del matiz).
fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (f64, f64, f64) {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let h_prime = h.rem_euclid(360.0) / 60.0;
let x = c * (1.0 - (h_prime % 2.0 - 1.0).abs());
let (r1, g1, b1) = match h_prime as u32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let m = l - c / 2.0;
(r1 + m, g1 + m, b1 + m)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
fn rgb_eq(got: (f64, f64, f64), want: (f64, f64, f64)) -> bool {
approx(got.0, want.0) && approx(got.1, want.1) && approx(got.2, want.2)
}
#[test]
fn hsl_primaries() {
assert!(rgb_eq(hsl_to_rgb(0.0, 1.0, 0.5), (1.0, 0.0, 0.0)));
assert!(rgb_eq(hsl_to_rgb(120.0, 1.0, 0.5), (0.0, 1.0, 0.0)));
assert!(rgb_eq(hsl_to_rgb(240.0, 1.0, 0.5), (0.0, 0.0, 1.0)));
}
#[test]
fn hsl_grays() {
assert!(rgb_eq(hsl_to_rgb(0.0, 0.0, 0.0), (0.0, 0.0, 0.0)));
assert!(rgb_eq(hsl_to_rgb(0.0, 0.0, 1.0), (1.0, 1.0, 1.0)));
// Acento de "Print B&W": gris medio-oscuro.
assert!(rgb_eq(hsl_to_rgb(0.0, 0.0, 0.2), (0.2, 0.2, 0.2)));
}
#[test]
fn known_themes_map_color_scheme() {
assert_eq!(facts_for("Nebula").color_scheme(), 1);
assert_eq!(facts_for("Aurora").color_scheme(), 1);
assert_eq!(facts_for("Solarized Light").color_scheme(), 2);
assert_eq!(facts_for("Print Color").color_scheme(), 2);
}
#[test]
fn high_contrast_only_for_high_contrast_theme() {
assert!(facts_for("High Contrast").high_contrast);
assert_eq!(facts_for("High Contrast").contrast(), 1);
assert!(!facts_for("Nebula").high_contrast);
assert_eq!(facts_for("Nebula").contrast(), 0);
}
#[test]
fn unknown_theme_falls_back() {
let f = facts_for("NoSuchTheme");
assert_eq!(f, FALLBACK);
assert_eq!(f.color_scheme(), 1, "FALLBACK es oscuro");
}
#[test]
fn accent_rgb_in_range() {
for name in [
"Nebula",
"Aurora",
"Sunset",
"Flat Dark",
"Solarized Light",
"High Contrast",
"Print Color",
"Print B&W",
] {
let (r, g, b) = facts_for(name).accent_rgb();
for ch in [r, g, b] {
assert!(
(0.0..=1.0).contains(&ch),
"{name}: canal fuera de rango: {ch}"
);
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "mirada"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "mirada — el Cerebro del compositor: ventana GPUI que tesela el escritorio sobre mirada-brain y manda la geometría al Cuerpo (smithay) por mirada-link. Sin Cuerpo, corre en simulación."
[[bin]]
name = "mirada"
path = "src/main.rs"
[dependencies]
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
mirada-link = { path = "../../modules/mirada/mirada-link" }
nahual-theme = { path = "../../modules/nahual/libs/theme" }
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
gpui = { workspace = true }
+607
View File
@@ -0,0 +1,607 @@
//! `mirada` — la ventana del Cerebro del compositor.
//!
//! Es el "Cerebro" de la arquitectura carmen hecho app GPUI: envuelve
//! [`mirada_brain::Desktop`] (toda la lógica de teselado y foco) y lo
//! pinta. La cadena completa:
//!
//! ```text
//! mirada-layout ─► mirada-protocol ─► mirada-brain ─► [esta ventana]
//! │
//! mirada-link ─► mirada-compositor (Cuerpo)
//! ```
//!
//! Con un Cuerpo conectado (variable `MIRADA_SOCKET`) sondea sus
//! [`BodyEvent`]s y le devuelve [`BrainCommand`]s por el socket. Sin
//! Cuerpo arranca en **simulación**: las ventanas son sintéticas y el
//! teclado de esta ventana maneja el escritorio — útil para ver el
//! motor de teselado sin hardware.
//!
//! Teclas (simulación):
//!
//! ```text
//! n / Shift+n abre ventana / monitor tab / espacio cicla layout
//! w cierra la enfocada t m g c r d s layout directo
//! f / Shift+f flota / pantalla completa h / l área maestra /+
//! j / k foco siguiente/anterior , / . nmaster /+
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
//! Enter promueve a maestra Ctrl+1..9 enviar a escritorio
//! o siguiente monitor ` / Shift+` scratchpad ver/guardar
//! ```
//!
//! Los pips de escritorio y las ventanas del lienzo son **clicables**, y
//! `mirada-ctl` controla el escritorio desde la terminal — ambos pasan
//! por el mismo `Desktop::apply`.
use std::path::PathBuf;
use std::time::Duration;
use gpui::{
div, hsla, prelude::*, px, Context, FocusHandle, IntoElement, KeyDownEvent, MouseButton,
Render, SharedString, Window,
};
use mirada_brain::{
BodyEvent, BrainCommand, CtlConn, CtlReply, CtlRequest, CtlServer, Desktop, DesktopAction,
Keymap, KeymapWatch, LayoutMode, Rules, WindowId, WindowPlacement,
};
use mirada_link::BrainLink;
use nahual_launcher::launch_app;
use nahual_theme::Theme;
/// Pantalla virtual del modo simulación — coincide con el lienzo.
const SCREEN_W: i32 = 1280;
const SCREEN_H: i32 = 720;
/// Periodo del sondeo del Cuerpo, en ms (~60 Hz).
const POLL_MS: u64 = 16;
/// Nombres de app ficticios para las ventanas de simulación.
const APPS: &[&str] = &[
"shuma", "fana", "revista", "cosmobiología", "matilda", "yachay", "barra",
];
/// El Cerebro: el estado del escritorio + lo último colocado + el cable.
struct Mirada {
desktop: Desktop,
/// Geometría vigente — lo que se pinta. Es la última `Place` emitida.
placements: Vec<WindowPlacement>,
/// Contador de ids para las ventanas sintéticas.
next_id: WindowId,
/// Cable al Cuerpo; `None` en simulación.
link: Option<BrainLink>,
/// Última acción, para la barra de estado.
note: SharedString,
focus: FocusHandle,
focused_once: bool,
/// Ruta del keymap del usuario, para recargarlo en caliente.
keymap_path: Option<PathBuf>,
/// Vigía del keymap; `None` en simulación o si no hay archivo.
keymap_watch: Option<KeymapWatch>,
/// Socket del API de control externo (`mirada-ctl`).
ctl: Option<CtlServer>,
}
impl Mirada {
fn new(cx: &mut Context<Self>) -> Self {
// Keymap del usuario (~/.config/mirada/keymap.ron): define los
// atajos que el Cuerpo intercepta y nos devuelve como `Keybind`.
let keymap_path = Keymap::default_path();
let keymap = match &keymap_path {
Some(p) => Keymap::load_or_init(p),
None => Keymap::default(),
};
let link = connect_body();
// Vigilar el keymap sólo tiene sentido con un Cuerpo conectado;
// en simulación, mirada usa las teclas de su propia ventana.
let keymap_watch = if link.is_some() {
keymap_path.as_deref().and_then(|p| Keymap::watch(p).ok())
} else {
None
};
// API de control: mirada siempre posee el Desktop, así que
// siempre abre el socket de `mirada-ctl`.
let ctl = match CtlServer::bind(&mirada_brain::ctl::default_socket_path()) {
Ok(s) => Some(s),
Err(e) => {
eprintln!("mirada · sin API de control: {e}");
None
}
};
// Reglas de ventana (~/.config/mirada/rules.ron): a qué
// escritorio va cada ventana, si flota.
let mut desktop = Desktop::with_keymap(keymap);
desktop.set_rules(load_user_rules());
let mut app = Self {
desktop,
placements: Vec::new(),
next_id: 1,
link,
note: SharedString::from("listo"),
focus: cx.focus_handle(),
focused_once: false,
keymap_path,
keymap_watch,
ctl,
};
if let Some(link) = app.link.as_mut() {
// Registra los atajos globales en el Cuerpo.
let _ = link.send(&app.desktop.grab_keys());
app.note = SharedString::from("Cuerpo conectado");
} else {
// Simulación: una pantalla virtual y tres ventanas de muestra.
app.feed(BodyEvent::OutputAdded { id: 0, width: SCREEN_W, height: SCREEN_H });
for _ in 0..3 {
app.open_window();
}
app.note = SharedString::from("simulación — sin Cuerpo");
}
// El sondeo corre siempre: drena el Cuerpo (si lo hay), vigila el
// keymap y atiende `mirada-ctl`.
app.start_poll(cx);
app
}
/// Bucle de fondo: drena los eventos del Cuerpo y los procesa.
fn start_poll(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| loop {
cx.background_executor()
.timer(Duration::from_millis(POLL_MS))
.await;
let alive = this.update(cx, |app, cx| {
let events: Vec<BodyEvent> = match app.link.as_ref() {
Some(link) => link.drain(),
None => Vec::new(),
};
let had_events = !events.is_empty();
let keymap_changed = app.keymap_watch.as_ref().is_some_and(|w| w.changed());
if keymap_changed {
app.reload_keymap();
}
let ctl_served = app.poll_ctl();
for ev in events {
app.feed(ev);
}
if had_events || keymap_changed || ctl_served {
cx.notify();
}
});
if alive.is_err() {
break; // ventana cerrada
}
})
.detach();
}
/// Abre una ventana sintética (sólo tiene sentido en simulación).
fn open_window(&mut self) {
let id = self.next_id;
self.next_id += 1;
let app = APPS[(id as usize) % APPS.len()];
self.feed(BodyEvent::WindowOpened {
id,
app_id: format!("org.brahman.{app}"),
title: format!("{app} · ventana {id}"),
});
self.note = SharedString::from(format!("abierta ventana {id}"));
}
/// Inyecta un evento del Cuerpo en el `Desktop` y despacha la salida.
fn feed(&mut self, event: BodyEvent) {
let cmds = self.desktop.on_event(event);
self.dispatch(cmds);
}
/// Aplica una acción de escritorio (desde una tecla de esta ventana).
fn act(&mut self, action: DesktopAction) {
let cmds = self.desktop.apply(action);
self.dispatch(cmds);
}
/// Recarga el keymap del disco y re-registra los atajos en el Cuerpo.
fn reload_keymap(&mut self) {
let Some(path) = self.keymap_path.clone() else {
return;
};
match Keymap::load(&path) {
Ok(km) => {
let cmd = self.desktop.set_keymap(km);
self.dispatch(vec![cmd]);
self.note = SharedString::from("keymap recargado");
}
Err(e) => self.note = SharedString::from(format!("keymap inválido: {e}")),
}
}
/// Atiende las peticiones pendientes del API de control. Devuelve
/// `true` si sirvió alguna (para repintar).
fn poll_ctl(&mut self) -> bool {
let conns: Vec<CtlConn> = match &self.ctl {
Some(ctl) => std::iter::from_fn(|| ctl.poll()).collect(),
None => return false,
};
let mut served = false;
for mut conn in conns {
let reply = match conn.read_request() {
Ok(Some(req)) => {
served = true;
self.serve_ctl(req)
}
Ok(None) => continue,
Err(e) => CtlReply::Error(format!("{e}")),
};
let _ = conn.reply(&reply);
}
served
}
/// Resuelve una petición de control: la acción pasa por el mismo
/// `apply` que el teclado; la consulta lee el `Desktop`.
fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply {
match req {
CtlRequest::Do(action) => {
self.act(action);
CtlReply::Ok
}
CtlRequest::ListWindows => CtlReply::Windows(self.desktop.window_lines()),
}
}
/// Reparte los comandos del Cerebro: actualiza lo pintado y, o bien
/// los manda al Cuerpo, o bien —en simulación— cierra las ventanas
/// por su cuenta (no hay nadie que devuelva el `WindowClosed`).
fn dispatch(&mut self, cmds: Vec<BrainCommand>) {
for cmd in &cmds {
if let BrainCommand::Place(p) = cmd {
self.placements = p.clone();
}
}
match self.link.as_mut() {
Some(link) => {
for cmd in &cmds {
let _ = link.send(cmd);
}
}
None => {
for cmd in cmds {
match cmd {
BrainCommand::Close(id) | BrainCommand::Kill(id) => {
self.feed(BodyEvent::WindowClosed { id });
}
_ => {}
}
}
}
}
}
/// Traduce una tecla de la ventana a una acción de escritorio.
fn handle_key(&mut self, event: &KeyDownEvent, _w: &mut Window, cx: &mut Context<Self>) {
let ks = &event.keystroke;
let ctrl = ks.modifiers.control;
let shift = ks.modifiers.shift;
let connected = self.link.is_some();
match ks.key.as_str() {
"n" if shift && !connected => {
// Simulación: añade un monitor más, en fila a la derecha.
let id = self.desktop.outputs().len() as u32;
self.feed(BodyEvent::OutputAdded { id, width: SCREEN_W, height: SCREEN_H });
}
"n" if !connected => self.open_window(),
"w" => self.act(DesktopAction::CloseFocused),
"f" if shift => self.act(DesktopAction::ToggleFullscreen),
"f" => self.act(DesktopAction::ToggleFloat),
"j" if shift => self.act(DesktopAction::MoveForward),
"k" if shift => self.act(DesktopAction::MoveBackward),
"j" => self.act(DesktopAction::FocusNext),
"k" => self.act(DesktopAction::FocusPrev),
"tab" | "space" => self.act(DesktopAction::CycleLayout),
"t" => self.act(DesktopAction::SetLayout(LayoutMode::MasterStack)),
"m" => self.act(DesktopAction::SetLayout(LayoutMode::Monocle)),
"g" => self.act(DesktopAction::SetLayout(LayoutMode::Grid)),
"c" => self.act(DesktopAction::SetLayout(LayoutMode::Columns)),
"r" => self.act(DesktopAction::SetLayout(LayoutMode::Rows)),
"d" => self.act(DesktopAction::SetLayout(LayoutMode::CenteredMaster)),
"s" => self.act(DesktopAction::SetLayout(LayoutMode::Spiral)),
"h" => self.act(DesktopAction::ShrinkMaster),
"l" => self.act(DesktopAction::GrowMaster),
"o" => self.act(DesktopAction::FocusOutputNext),
"`" if shift => self.act(DesktopAction::SendToScratchpad),
"`" => self.act(DesktopAction::ToggleScratchpad),
"enter" => self.act(DesktopAction::PromoteToMaster),
"," => self.act(DesktopAction::IncMaster),
"." => self.act(DesktopAction::DecMaster),
d if d.len() == 1 && d.as_bytes()[0].is_ascii_digit() && d != "0" => {
let n = (d.as_bytes()[0] - b'1') as usize;
if ctrl {
self.act(DesktopAction::SendToWorkspace(n));
} else {
self.act(DesktopAction::SwitchWorkspace(n));
}
}
_ => return,
}
cx.notify();
}
}
/// Conecta con el Cuerpo si `MIRADA_SOCKET` apunta a un socket vivo.
fn connect_body() -> Option<BrainLink> {
let path = std::env::var("MIRADA_SOCKET").ok()?;
BrainLink::connect(&path).ok()
}
/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
fn load_user_rules() -> Rules {
match Rules::default_path() {
Some(p) => Rules::load_or_default(&p),
None => Rules::default(),
}
}
/// Nombre legible de un modo de teselado.
fn mode_name(m: LayoutMode) -> &'static str {
match m {
LayoutMode::MasterStack => "maestro + pila",
LayoutMode::Monocle => "monóculo",
LayoutMode::Grid => "rejilla",
LayoutMode::Columns => "columnas",
LayoutMode::Rows => "filas",
LayoutMode::CenteredMaster => "maestro centrado",
LayoutMode::Spiral => "espiral",
}
}
impl Render for Mirada {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// El lienzo necesita el foco del teclado desde el primer frame.
if !self.focused_once {
window.focus(&self.focus);
self.focused_once = true;
}
let theme = Theme::global(cx).clone();
let win_bg = hsla(220.0 / 360.0, 0.16, 0.13, 1.0);
let bar_bg = hsla(220.0 / 360.0, 0.20, 0.09, 1.0);
let canvas_bg = hsla(220.0 / 360.0, 0.24, 0.05, 1.0);
// Texto legible sobre un fondo de acento.
let on_accent = hsla(220.0 / 360.0, 0.24, 0.06, 1.0);
let active = self.desktop.active_index();
let mode = self.desktop.active_workspace().params().mode;
let loads = self.desktop.workspace_loads();
let focused = self.desktop.focused_window();
// --- Barra superior: identidad + escritorios + modo ----------
let pips = loads.iter().enumerate().map(|(i, &load)| {
let is_active = i == active;
let fg = if is_active {
on_accent
} else if load > 0 {
theme.fg_text
} else {
theme.fg_disabled
};
div()
.w(px(24.))
.h(px(22.))
.flex()
.items_center()
.justify_center()
.rounded(px(4.))
.cursor_pointer()
.when(is_active, |d| d.bg(theme.accent))
.when(!is_active && load > 0, |d| d.bg(theme.bg_row_hover))
.text_color(fg)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |app, _, _, cx| {
app.act(DesktopAction::SwitchWorkspace(i));
cx.notify();
}),
)
.child(SharedString::from(format!("{}", i + 1)))
});
let focus_label = match focused.and_then(|id| self.desktop.window_info(id)) {
Some(info) => info.title.clone(),
None => "".to_string(),
};
let bar = div()
.h(px(44.))
.flex()
.flex_row()
.items_center()
.gap(px(12.))
.px(px(14.))
.bg(bar_bg)
.text_color(theme.fg_text)
.child(div().text_color(theme.accent).child("mirada"))
.child(div().text_color(theme.fg_disabled).child("·"))
.child(div().flex().flex_row().gap(px(4.)).children(pips))
.child(div().text_color(theme.fg_disabled).child("·"))
.child(
div()
.text_color(theme.fg_muted)
.child(SharedString::from(format!("layout: {}", mode_name(mode)))),
)
.child(div().flex_1())
.child(
div()
.text_color(theme.fg_muted)
.child(SharedString::from(format!("foco: {focus_label}"))),
);
// --- Lienzo: el escritorio teselado, a escala ----------------
// El lienzo es de tamaño fijo; el contenido vive en el espacio
// global de las salidas. `scale` encaja ese espacio en el lienzo
// — con una sola salida, escala 1:1.
let outs = self.desktop.outputs();
let (bb_w, bb_h) = if outs.is_empty() {
(SCREEN_W as f32, SCREEN_H as f32)
} else {
let w = outs.iter().map(|o| o.rect.x + o.rect.w).max().unwrap_or(SCREEN_W);
let h = outs.iter().map(|o| o.rect.y + o.rect.h).max().unwrap_or(SCREEN_H);
(w as f32, h as f32)
};
let scale = (SCREEN_W as f32 / bb_w)
.min(SCREEN_H as f32 / bb_h)
.min(1.0);
let mut canvas = div()
.relative()
.w(px(SCREEN_W as f32))
.h(px(SCREEN_H as f32))
.bg(canvas_bg)
.overflow_hidden();
// Un marco por salida, con su número y el escritorio que muestra.
for (i, o) in outs.iter().enumerate() {
let is_focused_out = i == self.desktop.focused_output();
canvas = canvas.child(
div()
.absolute()
.left(px(o.rect.x as f32 * scale))
.top(px(o.rect.y as f32 * scale))
.w(px(o.rect.w as f32 * scale))
.h(px(o.rect.h as f32 * scale))
.border_1()
.border_color(if is_focused_out {
theme.accent
} else {
theme.border
})
.child(
div()
.absolute()
.top(px(2.))
.left(px(4.))
.text_color(theme.fg_disabled)
.child(SharedString::from(format!(
"salida {} · escritorio {}",
i + 1,
o.workspace + 1
))),
),
);
}
let visible = self.placements.iter().filter(|p| p.visible).count();
if visible == 0 {
canvas = canvas.child(
div()
.absolute()
.size_full()
.flex()
.items_center()
.justify_center()
.text_color(theme.fg_disabled)
.child("escritorio vacío — pulsa n para abrir una ventana"),
);
}
for p in self.placements.iter().filter(|p| p.visible) {
let info = self.desktop.window_info(p.id);
let title = info
.map(|i| i.title.clone())
.unwrap_or_else(|| format!("ventana {}", p.id));
let app_id = info.map(|i| i.app_id.clone()).unwrap_or_default();
let border = if p.focused { theme.accent } else { theme.border };
let tb_bg = if p.focused { theme.accent } else { theme.bg_row_hover };
let tb_fg = if p.focused { on_accent } else { theme.fg_muted };
let pid = p.id;
let kind_label = if p.fullscreen {
"· pantalla completa ·"
} else if p.floating {
"· ventana flotante ·"
} else {
"· superficie del Cuerpo ·"
};
canvas = canvas.child(
div()
.absolute()
.left(px(p.rect.x as f32 * scale))
.top(px(p.rect.y as f32 * scale))
.w(px(p.rect.w as f32 * scale))
.h(px(p.rect.h as f32 * scale))
.border_2()
.border_color(border)
.bg(win_bg)
.rounded(px(5.))
.overflow_hidden()
.cursor_pointer()
.on_mouse_down(
MouseButton::Left,
cx.listener(move |app, _, _, cx| {
app.act(DesktopAction::FocusWindow(pid));
cx.notify();
}),
)
.flex()
.flex_col()
.child(
// Barra de título de la ventana.
div()
.h(px(22.))
.flex()
.items_center()
.px(px(8.))
.bg(tb_bg)
.text_color(tb_fg)
.child(SharedString::from(title)),
)
.child(
// Interior: en el compositor real lo compone el
// Cuerpo (zero-copy); aquí es un marcador.
div()
.flex_1()
.flex()
.flex_col()
.items_center()
.justify_center()
.gap(px(4.))
.text_color(theme.fg_disabled)
.child(SharedString::from(app_id))
.child(kind_label),
),
);
}
// --- Composición ---------------------------------------------
div()
.track_focus(&self.focus)
.key_context("Mirada")
.on_key_down(cx.listener(Self::handle_key))
.size_full()
.flex()
.flex_col()
.bg(theme.bg_app)
.text_color(theme.fg_text)
.child(bar)
.child(
div()
.flex_1()
.flex()
.items_center()
.justify_center()
.bg(theme.bg_app)
.child(canvas),
)
.child(
// Pie: el estado.
div()
.h(px(26.))
.flex()
.items_center()
.px(px(14.))
.bg(bar_bg)
.text_color(theme.fg_disabled)
.child(self.note.clone()),
)
}
}
fn main() {
launch_app("brahman · mirada", (SCREEN_W as f32, (SCREEN_H + 70) as f32), Mirada::new);
}
@@ -1,5 +1,5 @@
[package]
name = "yahweh-database-explorer"
name = "nahual-database-explorer"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,7 +7,7 @@ description = "Explorer de SQLite — composición TreeView + SqliteProvider con
[dependencies]
gpui = { workspace = true }
yahweh-core = { workspace = true }
yahweh-theme = { workspace = true }
yahweh-widget-tree = { workspace = true }
yahweh-provider-sqlite = { workspace = true }
nahual-core = { workspace = true }
nahual-theme = { workspace = true }
nahual-widget-tree = { workspace = true }
nahual-provider-sqlite = { workspace = true }
@@ -1,6 +1,6 @@
//! `yahweh_database_explorer` — explorer de SQLite.
//! `nahual_database_explorer` — explorer de SQLite.
//!
//! Mismo patrón que `yahweh_file_explorer` pero con `SqliteProvider`. La
//! Mismo patrón que `nahual_file_explorer` pero con `SqliteProvider`. La
//! UX es idéntica (TreeView con lazy load por chevron); cambia solo el
//! origen de los datos: filas de una tabla `items(id, parent_id, name,
//! display_type, content)` en lugar del filesystem.
@@ -13,10 +13,10 @@ use gpui::{
px,
};
use yahweh_core::{DataProvider, DisplayType, EntityNode};
use yahweh_provider_sqlite::SqliteDataProvider;
use yahweh_theme::Theme;
use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
use nahual_core::{DataProvider, DisplayType, EntityNode};
use nahual_provider_sqlite::SqliteDataProvider;
use nahual_theme::Theme;
use nahual_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
#[derive(Clone, Debug)]
#[allow(dead_code)] // Consumido por el AppBus en Fase 4+.
@@ -1,5 +1,5 @@
[package]
name = "yahweh-file-explorer"
name = "nahual-file-explorer"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,8 +7,8 @@ description = "Explorer de filesystem — composición TreeView + FsProvider con
[dependencies]
gpui = { workspace = true }
yahweh-core = { workspace = true }
yahweh-theme = { workspace = true }
yahweh-widget-tree = { workspace = true }
yahweh-widget-text-input = { workspace = true }
yahweh-provider-fs = { workspace = true }
nahual-core = { workspace = true }
nahual-theme = { workspace = true }
nahual-widget-tree = { workspace = true }
nahual-widget-text-input = { workspace = true }
nahual-provider-fs = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_file_explorer` — explorer de filesystem con menú contextual.
//! `nahual_file_explorer` — explorer de filesystem con menú contextual.
//!
//! Composición canónica del patrón "explorer = TreeView + provider":
//!
@@ -42,11 +42,11 @@ use gpui::{
PromptLevel, Render, SharedString, Window, div, prelude::*, px,
};
use yahweh_core::{DataProvider, DisplayType, EntityNode};
use yahweh_provider_fs::FileDataProvider;
use yahweh_theme::Theme;
use yahweh_widget_text_input::{TextInput, TextInputEvent};
use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
use nahual_core::{DataProvider, DisplayType, EntityNode};
use nahual_provider_fs::FileDataProvider;
use nahual_theme::Theme;
use nahual_widget_text_input::{TextInput, TextInputEvent};
use nahual_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
#[derive(Clone, Debug)]
#[allow(dead_code)]
@@ -1,5 +1,5 @@
[package]
name = "yahweh-image-viewer"
name = "nahual-image-viewer"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
@@ -7,5 +7,5 @@ description = "Visor de imágenes. Suscribe al AppBus y renderea con gpui::img(p
[dependencies]
gpui = { workspace = true }
yahweh-bus = { workspace = true }
yahweh-theme = { workspace = true }
nahual-bus = { workspace = true }
nahual-theme = { workspace = true }
@@ -1,4 +1,4 @@
//! `yahweh_image_viewer` — visor de imágenes.
//! `nahual_image_viewer` — visor de imágenes.
//!
//! Suscribe al `AppBus` y, en cada `EntitySelected` cuyo provider sea
//! `local_fs` y la extensión sugiera imagen (jpg, png, webp, gif), pasa el
@@ -17,8 +17,8 @@ use gpui::{
Context, Entity, IntoElement, Render, SharedString, Window, div, img, prelude::*, px,
};
use yahweh_bus::{AppBus, AppEvent};
use yahweh_theme::Theme;
use nahual_bus::{AppBus, AppEvent};
use nahual_theme::Theme;
const FS_PROVIDER: &str = "local_fs";
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "nahual-shell"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "Bootstrap GPUI + LayoutHost de Yahweh."
[dependencies]
nahual-core = { workspace = true }
nahual-theme = { workspace = true }
nahual-provider-fs = { workspace = true }
nahual-provider-sqlite = { workspace = true }
nahual-widget-tree = { workspace = true }
nahual-widget-container-core = { workspace = true }
nahual-widget-splitter = { workspace = true }
nahual-widget-tabs = { workspace = true }
nahual-widget-tiled = { workspace = true }
nahual-bus = { workspace = true }
nahual-file-explorer = { workspace = true }
nahual-database-explorer = { workspace = true }
nahual-text-viewer = { workspace = true }
nahual-image-viewer = { workspace = true }
gpui = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
notify = { workspace = true }
# Brahman protocol — sidecar thread que se presenta al Init.
brahman-card = { path = "../../protocol/brahman-card" }
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
ulid = { workspace = true }
[[bin]]
name = "nahual"
path = "src/main.rs"
@@ -1,7 +1,7 @@
//! Card de yahweh-shell + spawn del sidecar brahman compartido.
//! Card de nahual-shell + spawn del sidecar brahman compartido.
//!
//! La lógica de thread + tokio + ping-loop vive en `brahman-sidecar`;
//! aquí sólo declaramos la identidad de yahweh como módulo Widget.
//! aquí sólo declaramos la identidad de nahual como módulo Widget.
use std::collections::BTreeSet;
@@ -11,7 +11,7 @@ use brahman_card::{
};
use ulid::Ulid;
/// Spawn del sidecar con la Card de yahweh.
/// Spawn del sidecar con la Card de nahual.
pub fn spawn() {
brahman_sidecar::spawn(build_card());
}
@@ -26,7 +26,7 @@ use std::time::Duration;
use gpui::{App, AsyncApp, Entity};
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use yahweh_core::LayerConfig;
use nahual_core::LayerConfig;
use crate::layout_model::LayoutModel;

Some files were not shown because too many files have changed in this diff Show More