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:
@@ -0,0 +1,279 @@
|
||||
//! `MetaBackend` trait — la frontera entre el widget metainterfaz
|
||||
//! (nahual) y la implementación concreta de persistencia/ejecución
|
||||
//! (nakui-core, Surreal, mocks para tests).
|
||||
//!
|
||||
//! El widget consume este trait; el binario lo implementa con su
|
||||
//! stack particular. Esto es lo que hace que el widget sea reusable.
|
||||
//!
|
||||
//! Convenciones documentadas en el doc del trait abajo.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Resultado uniforme de una operación de escritura del backend.
|
||||
///
|
||||
/// La UI lo usa para componer el toast: `id` para mostrar el
|
||||
/// short_uuid, `changed` para diferenciar "actualizado X (3 campos)"
|
||||
/// vs "sin cambios", `post_status` para concatenar mensajes
|
||||
/// emitidos por hooks internos del backend (ej. "auto-compact:
|
||||
/// snapshot @ seq 49") sin que la UI tenga que conocer el detalle.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct WriteOutcome {
|
||||
/// Id del record afectado. `Some` para seed/update/delete;
|
||||
/// `None` para morphism cuando afecta múltiples records.
|
||||
pub id: Option<Uuid>,
|
||||
/// Cantidad de cambios efectivos. `0` = no-op (edit que no
|
||||
/// modificó ningún campo, etc.).
|
||||
pub changed: usize,
|
||||
/// Mensaje de status opcional para concatenar al toast del op
|
||||
/// original con el separator estándar.
|
||||
pub post_status: Option<String>,
|
||||
}
|
||||
|
||||
impl WriteOutcome {
|
||||
/// Constructor para no-op writes (edits sin cambios).
|
||||
pub fn no_change(id: Uuid) -> Self {
|
||||
Self {
|
||||
id: Some(id),
|
||||
changed: 0,
|
||||
post_status: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Backend que un widget de metainterfaz usa para leer y mutar
|
||||
/// records. Decoupla el widget (nahual) de la implementación
|
||||
/// concreta (nakui-core, Surreal, mock para tests).
|
||||
///
|
||||
/// # Convención sobre ids
|
||||
///
|
||||
/// `Uuid` canónico. Backends que internamente usan otros tipos
|
||||
/// deben mapear via Uuid (hash determinista, wrapper, lo que sirva).
|
||||
/// Esto evita generic associated types que complicarían el dispatch
|
||||
/// en `cx.listener` de GPUI.
|
||||
///
|
||||
/// # Convención sobre validación
|
||||
///
|
||||
/// El backend ES la fuente de verdad sobre invariantes (KCL/Nickel
|
||||
/// post-checks, conservación, etc.). El widget pre-valida shape
|
||||
/// (nahual-meta-runtime: `parse_field_value`, `validate_entity_refs`)
|
||||
/// pero el backend puede rebotar con `Err(...)` si su validación
|
||||
/// adicional falla — el widget muestra el error al usuario.
|
||||
///
|
||||
/// # Convención sobre threading
|
||||
///
|
||||
/// `'static` (no `Send + Sync`): el widget vive en `Entity<MetaApp<B>>`
|
||||
/// que requiere `'static`, pero los handlers son single-threaded en
|
||||
/// el main UI thread de GPUI. Si en el futuro un backend necesita
|
||||
/// `cx.spawn`, agregamos los marker traits.
|
||||
///
|
||||
/// # Convención sobre delta computation
|
||||
///
|
||||
/// El widget pre-computa `set` y `clear` con
|
||||
/// [`crate::delta::compute_field_delta`] +
|
||||
/// [`crate::delta::compute_clear_fields`] *antes* de llamar a
|
||||
/// [`MetaBackend::update`]. El backend no recomputa: si recibe ambos
|
||||
/// vacíos devuelve `changed = 0` sin escribir nada. Esto evita
|
||||
/// double-roundtrip al store por el mismo dato.
|
||||
pub trait MetaBackend: 'static {
|
||||
/// Snapshot ordenado de records de una entity.
|
||||
/// Orden estable (lexicográfico por id) para UI determinista.
|
||||
/// Vacío si no hay records.
|
||||
fn list_records(&self, entity: &str) -> Vec<(Uuid, Value)>;
|
||||
|
||||
/// Lee un record por id. `None` si no existe.
|
||||
fn load_record(&self, entity: &str, id: Uuid) -> Option<Value>;
|
||||
|
||||
/// Crea un record nuevo. El backend asigna el `Uuid`
|
||||
/// (devuelve en `WriteOutcome.id`). `changed = 1` siempre.
|
||||
fn seed(
|
||||
&mut self,
|
||||
entity: &str,
|
||||
data: serde_json::Map<String, Value>,
|
||||
) -> Result<WriteOutcome, String>;
|
||||
|
||||
/// Edita un record existente. Aplica `set` (overrides) y
|
||||
/// `clear` (key removal). `changed = set.len() + clear.len()`.
|
||||
/// Si ambos están vacíos (no-op edit), devuelve
|
||||
/// `WriteOutcome::no_change(id)` sin error y sin escribir al log.
|
||||
fn update(
|
||||
&mut self,
|
||||
entity: &str,
|
||||
id: Uuid,
|
||||
set: serde_json::Map<String, Value>,
|
||||
clear: Vec<String>,
|
||||
) -> Result<WriteOutcome, String>;
|
||||
|
||||
/// Borra un record. `changed = 1` si existía, error si no.
|
||||
fn delete(&mut self, entity: &str, id: Uuid) -> Result<WriteOutcome, String>;
|
||||
|
||||
/// Ejecuta un morphism declarado por un módulo. El backend
|
||||
/// resuelve la implementación, valida, computa ops, las aplica.
|
||||
/// `changed = N ops aplicadas`.
|
||||
///
|
||||
/// `module_id` ubica al módulo (el trait no asume estructura del
|
||||
/// manifest — el backend lo resuelve internamente).
|
||||
fn morphism(
|
||||
&mut self,
|
||||
module_id: &str,
|
||||
name: &str,
|
||||
inputs: BTreeMap<String, Uuid>,
|
||||
params: Value,
|
||||
) -> Result<WriteOutcome, String>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Tests del trait via [`crate::testing::MockBackend`]. Verifican
|
||||
//! el contrato genérico (object-safety, semantica de seed/update/
|
||||
//! delete) sin atar a un backend concreto. Los tests del mock en
|
||||
//! sí (constructores, with_morphism, etc.) viven en
|
||||
//! `crate::testing::tests`.
|
||||
|
||||
use super::*;
|
||||
use crate::testing::MockBackend;
|
||||
use serde_json::json;
|
||||
|
||||
fn map_of(items: &[(&str, Value)]) -> serde_json::Map<String, Value> {
|
||||
items.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_then_load_round_trip() {
|
||||
let mut b = MockBackend::new();
|
||||
let out = b
|
||||
.seed("Customer", map_of(&[("name", json!("Acme"))]))
|
||||
.unwrap();
|
||||
let id = out.id.expect("seed devuelve id");
|
||||
assert_eq!(out.changed, 1);
|
||||
assert!(out.post_status.is_none());
|
||||
|
||||
let rec = b.load_record("Customer", id).unwrap();
|
||||
assert_eq!(rec.get("name"), Some(&json!("Acme")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_records_filters_by_entity_and_orders_stably() {
|
||||
let mut b = MockBackend::new();
|
||||
let _ = b.seed("A", map_of(&[("k", json!(1))])).unwrap();
|
||||
let _ = b.seed("B", map_of(&[("k", json!(2))])).unwrap();
|
||||
let _ = b.seed("A", map_of(&[("k", json!(3))])).unwrap();
|
||||
|
||||
let a = b.list_records("A");
|
||||
assert_eq!(a.len(), 2);
|
||||
let b_only = b.list_records("B");
|
||||
assert_eq!(b_only.len(), 1);
|
||||
let none = b.list_records("Missing");
|
||||
assert!(none.is_empty());
|
||||
|
||||
// Orden estable: re-llamadas devuelven mismo orden.
|
||||
let a_again = b.list_records("A");
|
||||
assert_eq!(
|
||||
a.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
|
||||
a_again.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_with_set_changes_field() {
|
||||
let mut b = MockBackend::new();
|
||||
let id = b
|
||||
.seed("Customer", map_of(&[("name", json!("Acme")), ("notes", json!("x"))]))
|
||||
.unwrap()
|
||||
.id
|
||||
.unwrap();
|
||||
|
||||
let out = b
|
||||
.update(
|
||||
"Customer",
|
||||
id,
|
||||
map_of(&[("name", json!("Acme S.A."))]),
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(out.changed, 1);
|
||||
assert_eq!(out.id, Some(id));
|
||||
|
||||
let rec = b.load_record("Customer", id).unwrap();
|
||||
assert_eq!(rec.get("name"), Some(&json!("Acme S.A.")));
|
||||
assert_eq!(rec.get("notes"), Some(&json!("x")), "notes intacto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_with_clear_removes_key() {
|
||||
let mut b = MockBackend::new();
|
||||
let id = b
|
||||
.seed("Customer", map_of(&[("name", json!("Acme")), ("notes", json!("x"))]))
|
||||
.unwrap()
|
||||
.id
|
||||
.unwrap();
|
||||
|
||||
let out = b
|
||||
.update("Customer", id, serde_json::Map::new(), vec!["notes".into()])
|
||||
.unwrap();
|
||||
assert_eq!(out.changed, 1);
|
||||
|
||||
let rec = b.load_record("Customer", id).unwrap();
|
||||
assert_eq!(rec.get("name"), Some(&json!("Acme")));
|
||||
assert!(rec.get("notes").is_none(), "notes debería estar borrado");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_with_empty_set_and_clear_returns_no_change() {
|
||||
let mut b = MockBackend::new();
|
||||
let id = b
|
||||
.seed("Customer", map_of(&[("name", json!("Acme"))]))
|
||||
.unwrap()
|
||||
.id
|
||||
.unwrap();
|
||||
|
||||
let out = b
|
||||
.update("Customer", id, serde_json::Map::new(), vec![])
|
||||
.unwrap();
|
||||
assert_eq!(out, WriteOutcome::no_change(id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_on_missing_record_errors() {
|
||||
let mut b = MockBackend::new();
|
||||
let id = Uuid::new_v4();
|
||||
let err = b
|
||||
.update("Customer", id, map_of(&[("x", json!(1))]), vec![])
|
||||
.unwrap_err();
|
||||
assert!(err.contains("not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_removes_and_then_load_returns_none() {
|
||||
let mut b = MockBackend::new();
|
||||
let id = b
|
||||
.seed("Customer", map_of(&[("name", json!("Acme"))]))
|
||||
.unwrap()
|
||||
.id
|
||||
.unwrap();
|
||||
let out = b.delete("Customer", id).unwrap();
|
||||
assert_eq!(out.changed, 1);
|
||||
assert_eq!(out.id, Some(id));
|
||||
assert!(b.load_record("Customer", id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_on_missing_record_errors() {
|
||||
let mut b = MockBackend::new();
|
||||
let id = Uuid::new_v4();
|
||||
assert!(b.delete("Customer", id).is_err());
|
||||
}
|
||||
|
||||
/// Sanity: el trait acepta llamadas via `&mut dyn MetaBackend`
|
||||
/// (object-safety). Esto permite que el widget tenga
|
||||
/// `Box<dyn MetaBackend>` si el use case requiere borrado de
|
||||
/// tipo (vs. el path normal con `MetaApp<B: MetaBackend>`).
|
||||
#[test]
|
||||
fn trait_is_object_safe() {
|
||||
let mut b: Box<dyn MetaBackend> = Box::new(MockBackend::new());
|
||||
let _ = b.seed("X", map_of(&[("k", json!(1))])).unwrap();
|
||||
assert_eq!(b.list_records("X").len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user