gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-voxel/src/historia.rs Rust · 281 siq'ikuna · 10.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1// =============================================================================
2//  llimphi-voxel :: historia — versiones y alternativas de un proyecto
3// -----------------------------------------------------------------------------
4//  El DAG sobre el que se monta todo el trabajo creativo. Estilo git, alineado
5//  al repo (BLAKE3 + DAG + postcard):
6//
7//    * Cada SNAPSHOT del proyecto es un BLOB direccionado por su contenido
8//      (`BLAKE3(postcard(Project))`). Dos snapshots idénticos comparten el mismo
9//      hash → dedup automático, sin remapear ids (a diferencia de `combinar`).
10//    * Una [`Version`] (commit) apunta a un snapshot y a su(s) PADRE(s). La
11//      cadena de padres es la HISTORIA; dos versiones con el mismo padre son
12//      ALTERNATIVAS.
13//    * Las RAMAS son alternativas nombradas: una punta móvil de la historia.
14//
15//  La [`Historia`] entera —almacén + versiones + ramas— serializa a un archivo
16//  (postcard). No reescribe el modelo `Project`: los `u64` ids viven DENTRO del
17//  snapshot; el direccionamiento por contenido es la capa de almacenamiento.
18//  Determinista: el orden de las versiones lo da un contador, no el reloj.
19// =============================================================================
20
21use std::collections::BTreeMap;
22use std::path::Path;
23
24use serde::{Deserialize, Serialize};
25
26use crate::documento::ErrorDoc;
27use crate::studio::Project;
28
29/// Hash BLAKE3 de un contenido — la dirección de un blob o el id de una versión.
30pub type Hash = [u8; 32];
31
32/// La rama por defecto.
33pub const RAMA_PRINCIPAL: &str = "principal";
34
35fn hash(bytes: &[u8]) -> Hash {
36    *blake3::hash(bytes).as_bytes()
37}
38
39/// Los primeros bytes de un hash en hex — para mostrarle una versión al usuario
40/// sin volcar 64 caracteres.
41pub fn hex_corto(h: &Hash) -> String {
42    h[..4].iter().map(|b| format!("{b:02x}")).collect()
43}
44
45/// Una VERSIÓN (commit): un snapshot del proyecto atado a su historia. Se
46/// direcciona por su propio contenido ([`id`](Self::id)).
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Version {
49    /// Hash del snapshot del proyecto (contenido en el almacén).
50    pub snapshot: Hash,
51    /// Versiones anteriores (vacío = raíz; >1 = una fusión).
52    pub padres: Vec<Hash>,
53    /// Etiqueta humana: "el nacimiento", "más bruma", "sin montañas"…
54    pub etiqueta: String,
55    /// Orden monótono de creación (sin reloj: determinista y reproducible).
56    pub orden: u64,
57}
58
59impl Version {
60    /// El id de la versión = hash de su contenido. Dos versiones idénticas
61    /// (mismo snapshot, padres, etiqueta y orden) tienen el mismo id.
62    pub fn id(&self) -> Hash {
63        hash(&postcard::to_allocvec(self).expect("postcard version"))
64    }
65}
66
67/// La HISTORIA: el almacén direccionado por contenido, el grafo de versiones y
68/// las ramas. Todo lo que el usuario crea se acumula aquí y se puede revisitar,
69/// ramificar y fusionar.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Historia {
72    /// Almacén: hash → bytes. Snapshots de proyecto (y, a futuro, artefactos).
73    contenidos: BTreeMap<Hash, Vec<u8>>,
74    /// Versiones por su id.
75    versiones: BTreeMap<Hash, Version>,
76    /// Ramas (alternativas nombradas): nombre → id de su versión punta.
77    ramas: BTreeMap<String, Hash>,
78    /// La rama sobre la que se commitea.
79    rama_actual: String,
80    /// La CABEZA (HEAD): la versión en la que estamos parados. Un `commit` cuelga
81    /// de aquí; un `checkout` la mueve a cualquier versión (para revisitar o
82    /// bifurcar desde el pasado). `None` = historia recién nacida.
83    cabeza: Option<Hash>,
84    /// Siguiente número de orden (monótono).
85    siguiente_orden: u64,
86}
87
88impl Default for Historia {
89    fn default() -> Self {
90        Self {
91            contenidos: BTreeMap::new(),
92            versiones: BTreeMap::new(),
93            ramas: BTreeMap::new(),
94            rama_actual: RAMA_PRINCIPAL.to_string(),
95            cabeza: None,
96            siguiente_orden: 0,
97        }
98    }
99}
100
101impl Historia {
102    /// Una historia vacía, en la rama principal.
103    pub fn nueva() -> Self {
104        Self::default()
105    }
106
107    /// Guarda un contenido crudo y devuelve su hash. Idempotente: el mismo
108    /// contenido no se almacena dos veces (dedup por contenido).
109    fn guardar_contenido(&mut self, bytes: Vec<u8>) -> Hash {
110        let h = hash(&bytes);
111        self.contenidos.entry(h).or_insert(bytes);
112        h
113    }
114
115    /// Recupera un contenido por su hash.
116    fn contenido(&self, h: &Hash) -> Option<&[u8]> {
117        self.contenidos.get(h).map(Vec::as_slice)
118    }
119
120    /// La CABEZA: la versión en la que estamos parados (`None` si aún no hay
121    /// ninguna). Alias histórico [`punta`](Self::punta).
122    pub fn actual(&self) -> Option<Hash> {
123        self.cabeza
124    }
125
126    /// La versión en la que estamos parados. Sinónimo de [`actual`](Self::actual).
127    pub fn punta(&self) -> Option<Hash> {
128        self.cabeza
129    }
130
131    /// El nombre de la rama actual.
132    pub fn rama_actual(&self) -> &str {
133        &self.rama_actual
134    }
135
136    /// Mueve la CABEZA a `version_id` (checkout) y devuelve su proyecto. Es el
137    /// "navegar": pararse en cualquier versión para revisitarla o bifurcar desde
138    /// ahí. `None` si la versión no existe. No toca las ramas (queda "suelta"
139    /// hasta el próximo `ramificar`/`cambiar_rama`).
140    pub fn checkout(&mut self, version_id: &Hash) -> Option<Project> {
141        let p = self.proyecto_de(version_id)?;
142        self.cabeza = Some(*version_id);
143        Some(p)
144    }
145
146    /// Todas las versiones ordenadas por creación (`orden`), con su id. Es la
147    /// lista que la UI pinta como árbol/historial navegable.
148    pub fn versiones_ordenadas(&self) -> Vec<(Hash, &Version)> {
149        let mut v: Vec<(Hash, &Version)> = self.versiones.iter().map(|(h, ver)| (*h, ver)).collect();
150        v.sort_by_key(|(_, ver)| ver.orden);
151        v
152    }
153
154    /// Registra `proyecto` como una versión nueva sobre la punta de la rama
155    /// actual, con la `etiqueta` dada. Avanza la rama a la versión nueva.
156    /// Devuelve su id. El snapshot se dedupea por contenido.
157    pub fn commit(&mut self, proyecto: &Project, etiqueta: impl Into<String>) -> Result<Hash, ErrorDoc> {
158        let bytes = postcard::to_allocvec(proyecto).map_err(|e| ErrorDoc::Serializar(e.to_string()))?;
159        let snapshot = self.guardar_contenido(bytes);
160        let padres = self.cabeza.into_iter().collect();
161        let orden = self.siguiente_orden;
162        self.siguiente_orden += 1;
163        let version = Version { snapshot, padres, etiqueta: etiqueta.into(), orden };
164        let id = version.id();
165        self.versiones.insert(id, version);
166        // La cabeza avanza a la versión nueva, y la rama actual la sigue.
167        self.cabeza = Some(id);
168        self.ramas.insert(self.rama_actual.clone(), id);
169        Ok(id)
170    }
171
172    /// El proyecto de una versión (reconstruye el snapshot). `None` si la versión
173    /// o su snapshot no existen, o si el snapshot no decodifica.
174    pub fn proyecto_de(&self, version_id: &Hash) -> Option<Project> {
175        let v = self.versiones.get(version_id)?;
176        let bytes = self.contenido(&v.snapshot)?;
177        postcard::from_bytes(bytes).ok()
178    }
179
180    /// El proyecto de la CABEZA (donde estamos parados).
181    pub fn proyecto_actual(&self) -> Option<Project> {
182        self.proyecto_de(&self.cabeza?)
183    }
184
185    /// Una versión por su id.
186    pub fn version(&self, id: &Hash) -> Option<&Version> {
187        self.versiones.get(id)
188    }
189
190    /// Crea (o remueve) una rama `nombre` apuntando a la punta actual — una
191    /// ALTERNATIVA que parte de aquí. No cambia la rama actual.
192    pub fn ramificar(&mut self, nombre: impl Into<String>) -> Result<(), ErrorDoc> {
193        let cabeza = self.cabeza.ok_or(ErrorDoc::EscenaAusente(0))?;
194        self.ramas.insert(nombre.into(), cabeza);
195        Ok(())
196    }
197
198    /// Cambia la rama actual a `nombre` y mueve la cabeza a su punta. `false` si
199    /// la rama no existe.
200    pub fn cambiar_rama(&mut self, nombre: &str) -> bool {
201        if let Some(&punta) = self.ramas.get(nombre) {
202            self.rama_actual = nombre.to_string();
203            self.cabeza = Some(punta);
204            true
205        } else {
206            false
207        }
208    }
209
210    /// Las ramas (alternativas nombradas): nombre → punta.
211    pub fn ramas(&self) -> &BTreeMap<String, Hash> {
212        &self.ramas
213    }
214
215    /// La HISTORIA de una versión: ella y su linaje hacia atrás (primer padre),
216    /// de la más nueva a la más vieja. La cadena de versiones.
217    pub fn linaje(&self, version_id: &Hash) -> Vec<Hash> {
218        let mut out = Vec::new();
219        let mut actual = Some(*version_id);
220        while let Some(id) = actual {
221            if !self.versiones.contains_key(&id) || out.contains(&id) {
222                break;
223            }
224            out.push(id);
225            actual = self.versiones.get(&id).and_then(|v| v.padres.first().copied());
226        }
227        out
228    }
229
230    /// Las ALTERNATIVAS de una versión: las otras versiones que comparten alguno
231    /// de sus padres (sus hermanas en el DAG). Vacío para una versión sin padres
232    /// o sin hermanas.
233    pub fn alternativas(&self, version_id: &Hash) -> Vec<Hash> {
234        let Some(v) = self.versiones.get(version_id) else {
235            return Vec::new();
236        };
237        let mut out = Vec::new();
238        for (id, otra) in &self.versiones {
239            if id == version_id {
240                continue;
241            }
242            if otra.padres.iter().any(|p| v.padres.contains(p)) {
243                out.push(*id);
244            }
245        }
246        out
247    }
248
249    /// Cuántas versiones acumula la historia.
250    pub fn len(&self) -> usize {
251        self.versiones.len()
252    }
253
254    /// ¿Sin ninguna versión todavía?
255    pub fn is_empty(&self) -> bool {
256        self.versiones.is_empty()
257    }
258
259    // --- Persistencia (postcard nativo) ---
260
261    /// Serializa la historia entera al format nativo compacto.
262    pub fn a_bytes(&self) -> Result<Vec<u8>, ErrorDoc> {
263        postcard::to_allocvec(self).map_err(|e| ErrorDoc::Serializar(e.to_string()))
264    }
265
266    /// Reconstruye una historia desde el format nativo.
267    pub fn de_bytes(bytes: &[u8]) -> Result<Historia, ErrorDoc> {
268        postcard::from_bytes(bytes).map_err(|e| ErrorDoc::Parsear(e.to_string()))
269    }
270
271    /// Guarda la historia en `path`.
272    pub fn guardar(&self, path: impl AsRef<Path>) -> Result<(), ErrorDoc> {
273        std::fs::write(path, self.a_bytes()?).map_err(|e| ErrorDoc::Io(e.to_string()))
274    }
275
276    /// Abre una historia desde `path`.
277    pub fn abrir(path: impl AsRef<Path>) -> Result<Historia, ErrorDoc> {
278        let bytes = std::fs::read(path).map_err(|e| ErrorDoc::Io(e.to_string()))?;
279        Self::de_bytes(&bytes)
280    }
281}