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,134 @@
|
||||
//! Cálculo del delta entre el record actual y la propuesta del form.
|
||||
//!
|
||||
//! Sirve a un runtime de edición para emitir SOLO los Set/Clear que
|
||||
//! cambian algo: log + apply minimales, no-op edits = 0 entries.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Calcula el delta entre el record actual y los valores propuestos
|
||||
/// del form. Devuelve un Map con sólo los campos cuyo valor difiere.
|
||||
///
|
||||
/// Comparación: igualdad estructural sobre `serde_json::Value`. Un
|
||||
/// `current=Value::Null` (record no encontrado) hace que todos los
|
||||
/// campos del `proposed` sean considerados nuevos. Un campo del
|
||||
/// proposed que coincide con el del current se omite. Campos que
|
||||
/// están en current pero NO en proposed se preservan tal cual (el
|
||||
/// edit no los toca; ver [`compute_clear_fields`] para borrar
|
||||
/// explícito desde un input vacío).
|
||||
pub fn compute_field_delta(
|
||||
current: &Value,
|
||||
proposed: &serde_json::Map<String, Value>,
|
||||
) -> serde_json::Map<String, Value> {
|
||||
proposed
|
||||
.iter()
|
||||
.filter(|(field, value)| current.get(field.as_str()) != Some(*value))
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Decide cuáles fields del `to_clear` candidate list ameritan
|
||||
/// realmente un `FieldOp::Clear`: sólo los que existen en el current
|
||||
/// con un valor non-null. Para fields ausentes o ya null, Clear es
|
||||
/// no-op semántico (el post-state es el mismo) y dropearlos
|
||||
/// preserva la propiedad "1 op = 1 cambio efectivo" del log.
|
||||
///
|
||||
/// Preserva el orden del input para que el log entry sea estable.
|
||||
pub fn compute_clear_fields(current: &Value, to_clear: &[String]) -> Vec<String> {
|
||||
to_clear
|
||||
.iter()
|
||||
.filter(|f| match current.get(f.as_str()) {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(_) => true,
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn map(items: &[(&str, Value)]) -> serde_json::Map<String, Value> {
|
||||
items.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_empty_when_all_fields_match() {
|
||||
let current = json!({"name": "Acme", "saldo": 100_i64, "currency": "USD"});
|
||||
let proposed = map(&[
|
||||
("name", json!("Acme")),
|
||||
("saldo", json!(100_i64)),
|
||||
("currency", json!("USD")),
|
||||
]);
|
||||
assert!(compute_field_delta(¤t, &proposed).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_includes_only_changed_field() {
|
||||
let current = json!({"name": "Acme", "saldo": 100_i64});
|
||||
let proposed = map(&[("name", json!("Acme")), ("saldo", json!(200_i64))]);
|
||||
let d = compute_field_delta(¤t, &proposed);
|
||||
assert_eq!(d.len(), 1);
|
||||
assert_eq!(d.get("saldo"), Some(&json!(200_i64)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_treats_missing_record_as_all_new() {
|
||||
let current = Value::Null;
|
||||
let proposed = map(&[("name", json!("Acme")), ("saldo", json!(0_i64))]);
|
||||
assert_eq!(compute_field_delta(¤t, &proposed).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_distinguishes_int_from_string_repr() {
|
||||
let current = json!({"qty": 100_i64});
|
||||
let proposed = map(&[("qty", json!(100_i64))]);
|
||||
assert!(compute_field_delta(¤t, &proposed).is_empty());
|
||||
|
||||
let current_str = json!({"qty": "100"});
|
||||
let proposed_int = map(&[("qty", json!(100_i64))]);
|
||||
assert_eq!(compute_field_delta(¤t_str, &proposed_int).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_skips_fields_absent_from_proposed() {
|
||||
let current = json!({"name": "Acme", "saldo": 100_i64, "extra": "x"});
|
||||
let proposed = map(&[("name", json!("Acme")), ("saldo", json!(150_i64))]);
|
||||
let d = compute_field_delta(¤t, &proposed);
|
||||
assert_eq!(d.len(), 1);
|
||||
assert!(!d.contains_key("extra"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_fields_skips_absent_and_null() {
|
||||
let current = json!({"name": "Acme", "notes": "lorem", "tag": null});
|
||||
let to_clear = vec![
|
||||
"name".into(),
|
||||
"notes".into(),
|
||||
"tag".into(),
|
||||
"missing".into(),
|
||||
];
|
||||
assert_eq!(
|
||||
compute_clear_fields(¤t, &to_clear),
|
||||
vec!["name".to_string(), "notes".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_fields_preserves_input_order() {
|
||||
let current = json!({"a": 1, "b": 2, "c": 3});
|
||||
let to_clear = vec!["c".into(), "a".into(), "b".into()];
|
||||
assert_eq!(
|
||||
compute_clear_fields(¤t, &to_clear),
|
||||
vec!["c", "a", "b"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_fields_empty_when_current_is_null() {
|
||||
let current = Value::Null;
|
||||
let to_clear = vec!["name".into()];
|
||||
assert!(compute_clear_fields(¤t, &to_clear).is_empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user