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>
This commit is contained in:
sergio
2026-05-19 14:48:34 +00:00
parent 86fb6ae20b
commit 550c98f275
375 changed files with 8512 additions and 7155 deletions
+108
View File
@@ -0,0 +1,108 @@
//! Hot-reload del `layout.json` vía `notify` watcher.
//!
//! Anatomía:
//! 1. Un thread del SO corre el watcher (`notify::recommended_watcher`) que
//! spawnea su propio thread de polling. Cuando detecta cambios en el
//! archivo objetivo, manda `()` por un `std::sync::mpsc::channel`.
//! 2. Una task de gpui (`cx.spawn`) hace `try_recv` cada N ms (timer en el
//! `background_executor`). Si llega algo, relee el JSON y actualiza el
//! `LayoutModel` con `replace_tree`.
//!
//! Esquema separado intencional: notify trabaja en hilos del SO (no
//! integra con el executor de gpui), así que rebotamos vía mpsc para no
//! tocar entities desde threads ajenos. El tradeoff es una latencia de
//! poll N (250ms por default) — imperceptible para edición manual de un
//! JSON.
//!
//! Ignoramos cambios cuando el JSON quedó inválido (parse error) — el
//! `LayerConfig::load_or_default` cae al árbol default. Si querés que la
//! UI muestre el error, agregar un AppEvent::ConfigError y un toast en
//! Fase 8.
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, channel};
use std::time::Duration;
use gpui::{App, AsyncApp, Entity};
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use nahual_core::LayerConfig;
use crate::layout_model::LayoutModel;
/// Frecuencia de polling del receiver. 250ms es el sweet spot:
/// suficientemente rápido para sentirse "instantáneo" pero sin gastar CPU.
const POLL_INTERVAL: Duration = Duration::from_millis(250);
/// Spawnea el watcher + el polling task. Devuelve el `RecommendedWatcher`
/// — el caller debe mantenerlo vivo (drop ⇒ stop watching). Por
/// conveniencia retorna también nada más; el caller suele guardar el
/// watcher en una global o filed-leakeada.
pub fn spawn_watch(
path: PathBuf,
model: Entity<LayoutModel>,
cx: &mut App,
) -> notify::Result<RecommendedWatcher> {
let (tx, rx) = channel::<()>();
// Watcher: el cierre se ejecuta en el thread que `notify` provee. Solo
// empujamos `()` al canal — el side mpsc maneja toda la lógica.
let mut watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
if let Ok(ev) = res {
// Solo nos interesan modify/create — los Access se ignoran
// para no triggerear en lecturas (ej. cat).
if matches!(
ev.kind,
notify::EventKind::Modify(_)
| notify::EventKind::Create(_)
| notify::EventKind::Remove(_)
) {
let _ = tx.send(());
}
}
})?;
// Watcheamos el directorio padre, no el archivo en sí. Muchos editores
// hacen "rename + create" al guardar (atomic write), lo que rompe
// watching del file directo. Ver el dir y filtrar por path es robusto.
let parent = path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
watcher.watch(&parent, RecursiveMode::NonRecursive)?;
// Spawnea el polling task en el ForegroundExecutor para poder llamar
// model.update sin cross-thread issues.
let path_for_task = path.clone();
cx.foreground_executor()
.spawn(poll_loop(rx, path_for_task, model, cx.to_async()))
.detach();
Ok(watcher)
}
async fn poll_loop(
rx: Receiver<()>,
path: PathBuf,
model: Entity<LayoutModel>,
mut cx: AsyncApp,
) {
let timer = cx.background_executor().clone();
loop {
timer.timer(POLL_INTERVAL).await;
// Drenamos todos los eventos acumulados en este ciclo —
// múltiples writes seguidos colapsan a UN solo reload.
let mut had_event = false;
while rx.try_recv().is_ok() {
had_event = true;
}
if !had_event {
continue;
}
// Releemos el JSON desde disco. Si parsea bien, replace_tree;
// si no, el `load_or_default` cae al default (no rompe la UI).
let tree = LayerConfig::load_or_default(path.to_string_lossy().as_ref());
let _ = model.update(&mut cx, |m, cx| m.replace_tree(tree, cx));
}
}