gioser

tawasuyu/brahman

Rust 496TOML 195Markdown 55JSON 197001 Definiciones

feat(fana): fana-store — persistencia del grafo narrativo (sled)

- fana-core: NarrativeAtom + CoherenceState ahora Serialize/Deserialize
  (serde con feature rc para el Arc<String>; uuid con feature serde).
- fana-graph: + atoms() iterator + from_atoms() constructor.
- fana-store: GraphStore sobre sled. put/get/remove_atom por Uuid,
  serialización bincode. save_graph persiste átomo por átomo;
  load_graph reconstruye el grafo (la adjacency se re-cablea desde las
  dependencies de cada átomo).

7 tests verdes (roundtrip put/get/remove + save/load_graph preserva
estructura). cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

6884b3f8cb5e27ab0a48870229a19c8bf50a18b8 · por sergio · (hace 5 meses) · padre 353e0bbb4 · ver el árbol en este commit

Delta semántico · 16

crates/modules/fana/fana-graph/src/lib.rs

crates/modules/fana/fana-store/src/lib.rs

7 archivos · +178 −3

diff --git a/Cargo.lock b/Cargo.lock
index fd43879..33895b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3949,6 +3949,7 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
 name = "fana-core"
 version = "0.1.0"
 dependencies = [
+ "serde",
  "sha2",
  "uuid",
 ]
@@ -3979,6 +3980,19 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "fana-store"
+version = "0.1.0"
+dependencies = [
+ "bincode",
+ "fana-core",
+ "fana-graph",
+ "sled",
+ "tempfile",
+ "thiserror 2.0.18",
+ "uuid",
+]
+
 [[package]]
 name = "fastembed"
 version = "4.9.1"
diff --git a/Cargo.toml b/Cargo.toml
index 404e504..f8277ea 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -148,6 +148,7 @@ members = [
     # ============================================================
     "crates/modules/fana/fana-core",
     "crates/modules/fana/fana-graph",
+    "crates/modules/fana/fana-store",
     "crates/modules/fana/fana-md",
     "crates/modules/fana/fana-md-reader-web",
 
diff --git a/crates/modules/fana/fana-core/Cargo.toml b/crates/modules/fana/fana-core/Cargo.toml
index d77a82f..9cbd3d1 100644
--- a/crates/modules/fana/fana-core/Cargo.toml
+++ b/crates/modules/fana/fana-core/Cargo.toml
@@ -8,5 +8,6 @@ publish.workspace = true
 description = "fana — átomo narrativo (NarrativeAtom) + estado de coherencia. Tipos puros del editor DAG, agnósticos de UI."
 
 [dependencies]
-uuid = { workspace = true }
+uuid = { workspace = true, features = ["serde"] }
 sha2 = { workspace = true }
+serde = { workspace = true, features = ["rc"] }
diff --git a/crates/modules/fana/fana-core/src/lib.rs b/crates/modules/fana/fana-core/src/lib.rs
index fb8e3bb..70c881a 100644
--- a/crates/modules/fana/fana-core/src/lib.rs
+++ b/crates/modules/fana/fana-core/src/lib.rs
@@ -10,12 +10,13 @@
 
 #![forbid(unsafe_code)]
 
+use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 use std::sync::Arc;
 use uuid::Uuid;
 
 /// Estado de coherencia lógica de un átomo dentro del grafo narrativo.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub enum CoherenceState {
     /// Consistente con sus dependencias.
     Valid,
@@ -26,7 +27,7 @@ pub enum CoherenceState {
 }
 
 /// Un átomo narrativo: la unidad atómica del documento.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct NarrativeAtom {
     pub id: Uuid,
     /// SHA-256 del contenido — verifica integridad de toda mutación.
diff --git a/crates/modules/fana/fana-graph/src/lib.rs b/crates/modules/fana/fana-graph/src/lib.rs
index d3f8bd7..8488f4f 100644
--- a/crates/modules/fana/fana-graph/src/lib.rs
+++ b/crates/modules/fana/fana-graph/src/lib.rs
@@ -45,6 +45,20 @@ impl NarrativeGraph {
         self.nodes.get_mut(&id)
     }
 
+    /// Itera todos los átomos del grafo (orden no determinista).
+    pub fn atoms(&self) -> impl Iterator<Item = &NarrativeAtom> {
+        self.nodes.values()
+    }
+
+    /// Construye un grafo desde una colección de átomos.
+    pub fn from_atoms(atoms: impl IntoIterator<Item = NarrativeAtom>) -> Self {
+        let mut g = Self::new();
+        for a in atoms {
+            g.insert(a);
+        }
+        g
+    }
+
     /// Inserta un átomo y conecta las aristas desde sus dependencias.
     pub fn insert(&mut self, atom: NarrativeAtom) {
         let id = atom.id;
diff --git a/crates/modules/fana/fana-store/Cargo.toml b/crates/modules/fana/fana-store/Cargo.toml
new file mode 100644
index 0000000..9c8bbf6
--- /dev/null
+++ b/crates/modules/fana/fana-store/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "fana-store"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "fana — persistencia del grafo narrativo en un store key-value embebido (sled), serializado con bincode."
+
+[dependencies]
+fana-core = { path = "../fana-core" }
+fana-graph = { path = "../fana-graph" }
+sled = { workspace = true }
+bincode = { workspace = true }
+uuid = { workspace = true }
+thiserror = { workspace = true }
+
+[dev-dependencies]
+tempfile = { workspace = true }
diff --git a/crates/modules/fana/fana-store/src/lib.rs b/crates/modules/fana/fana-store/src/lib.rs
new file mode 100644
index 0000000..e74d882
--- /dev/null
+++ b/crates/modules/fana/fana-store/src/lib.rs
@@ -0,0 +1,125 @@
+//! `fana-store` — persistencia del grafo narrativo.
+//!
+//! Store key-value embebido (`sled`): cada `NarrativeAtom` se guarda con
+//! su `Uuid` como clave y su serialización `bincode` como valor. El
+//! grafo completo se reconstruye leyendo todos los átomos y re-cableando
+//! la adjacency desde sus `dependencies`.
+
+#![forbid(unsafe_code)]
+
+use fana_core::NarrativeAtom;
+use fana_graph::NarrativeGraph;
+use uuid::Uuid;
+
+/// Falla de una operación de store.
+#[derive(Debug, thiserror::Error)]
+pub enum StoreError {
+    #[error("sled: {0}")]
+    Db(#[from] sled::Error),
+    #[error("serialización: {0}")]
+    Serde(#[from] bincode::Error),
+}
+
+/// Store del grafo narrativo sobre sled.
+pub struct GraphStore {
+    db: sled::Db,
+}
+
+impl GraphStore {
+    /// Abre (o crea) el store en `path`.
+    pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, StoreError> {
+        Ok(Self { db: sled::open(path)? })
+    }
+
+    /// Guarda (o reemplaza) un átomo.
+    pub fn put_atom(&self, atom: &NarrativeAtom) -> Result<(), StoreError> {
+        let bytes = bincode::serialize(atom)?;
+        self.db.insert(atom.id.as_bytes(), bytes)?;
+        Ok(())
+    }
+
+    /// Lee un átomo por id.
+    pub fn get_atom(&self, id: Uuid) -> Result<Option<NarrativeAtom>, StoreError> {
+        match self.db.get(id.as_bytes())? {
+            Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)),
+            None => Ok(None),
+        }
+    }
+
+    /// Elimina un átomo.
+    pub fn remove_atom(&self, id: Uuid) -> Result<(), StoreError> {
+        self.db.remove(id.as_bytes())?;
+        Ok(())
+    }
+
+    /// Cantidad de átomos persistidos.
+    pub fn len(&self) -> usize {
+        self.db.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.db.is_empty()
+    }
+
+    /// Persiste el grafo completo (un `put` por átomo).
+    pub fn save_graph(&self, graph: &NarrativeGraph) -> Result<(), StoreError> {
+        for atom in graph.atoms() {
+            self.put_atom(atom)?;
+        }
+        self.db.flush()?;
+        Ok(())
+    }
+
+    /// Reconstruye el grafo leyendo todos los átomos persistidos.
+    pub fn load_graph(&self) -> Result<NarrativeGraph, StoreError> {
+        let mut atoms = Vec::with_capacity(self.db.len());
+        for entry in self.db.iter() {
+            let (_, bytes) = entry?;
+            atoms.push(bincode::deserialize::<NarrativeAtom>(&bytes)?);
+        }
+        Ok(NarrativeGraph::from_atoms(atoms))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn temp_store() -> (GraphStore, tempfile::TempDir) {
+        let dir = tempfile::tempdir().unwrap();
+        let store = GraphStore::open(dir.path().join("fana.sled")).unwrap();
+        (store, dir)
+    }
+
+    #[test]
+    fn put_get_remove_roundtrip() {
+        let (store, _d) = temp_store();
+        let atom = NarrativeAtom::new("capítulo uno", "main");
+        let id = atom.id;
+        store.put_atom(&atom).unwrap();
+        let loaded = store.get_atom(id).unwrap().expect("debe existir");
+        assert_eq!(loaded.id, id);
+        assert_eq!(*loaded.content, *atom.content);
+        assert!(loaded.hash_matches());
+        store.remove_atom(id).unwrap();
+        assert!(store.get_atom(id).unwrap().is_none());
+    }
+
+    #[test]
+    fn save_and_load_graph_preserves_structure() {
+        let (store, _d) = temp_store();
+        let a = NarrativeAtom::new("a", "main");
+        let b = NarrativeAtom::new("b", "main").depends_on(a.id);
+        let (a_id, b_id) = (a.id, b.id);
+        let mut g = NarrativeGraph::new();
+        g.insert(a);
+        g.insert(b);
+        store.save_graph(&g).unwrap();
+
+        let reloaded = store.load_graph().unwrap();
+        assert_eq!(reloaded.len(), 2);
+        assert!(reloaded.contains(a_id) && reloaded.contains(b_id));
+        // La adjacency se reconstruye desde las dependencies.
+        assert_eq!(reloaded.dependents(a_id), &[b_id]);
+    }
+}