tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
llimphi-voxel/src/historia.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
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}