feat(minga): minga-vfs — proyecta el repo como filesystem FUSE
minga-vfs deja de ser un stub: monta el repositorio direccionado por contenido como un filesystem FUSE de sólo lectura. roots/<hash> da el código fuente reconstruido (formato normalizado) de cada raíz del MST; cas/<hash> resuelve cualquier hash bajo demanda como S-expression. Capas separadas: render (SemanticNode→texto, puro) + source (contrato NodeSource, backends sled/memoria) + fs (único módulo con fuser). Nuevo subcomando `minga mount <punto>`. Dep fuser 0.15 sin libfuse-dev (default-features = false). 14 tests nuevos, sin regresión en minga-cli. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
//! El contrato [`NodeSource`] —lo mínimo que el VFS necesita de un
|
||||
//! repositorio Minga— y sus dos backends. Agnóstico de `fuser`.
|
||||
//!
|
||||
//! El VFS no quiere conocer `sled` ni la estructura interna del store:
|
||||
//! sólo necesita (a) enumerar las raíces del MST y (b) resolver un nodo
|
||||
//! por hash. Eso es [`NodeSource`]. [`RepoSource`] lo implementa sobre
|
||||
//! el [`PersistentRepo`] en disco; [`MemSource`] sobre un `MemStore` en
|
||||
//! RAM (tests, índices efímeros recién sincronizados).
|
||||
|
||||
use minga_core::{ContentHash, SemanticNode, StoredNode};
|
||||
use minga_store::PersistentRepo;
|
||||
|
||||
/// Lo que el VFS necesita de un repositorio para proyectarlo.
|
||||
pub trait NodeSource {
|
||||
/// Hashes raíz: el conjunto de claves del MST, un elemento por
|
||||
/// archivo ingerido. Es lo que se lista bajo `roots/`.
|
||||
fn roots(&self) -> Vec<ContentHash>;
|
||||
|
||||
/// Resuelve un único nodo (un eslabón del grafo) por su hash.
|
||||
/// `None` si no está en el almacén.
|
||||
fn get(&self, hash: &ContentHash) -> Option<StoredNode>;
|
||||
}
|
||||
|
||||
/// Reconstruye el `SemanticNode` completo de un hash, resolviendo
|
||||
/// recursivamente sus hijos contra `source`.
|
||||
///
|
||||
/// Devuelve `None` si el almacén está incompleto: o el propio `hash`
|
||||
/// falta, o lo hace algún descendiente (puede ocurrir en un repo a
|
||||
/// medio sincronizar).
|
||||
pub fn reconstruct<S>(source: &S, hash: &ContentHash) -> Option<SemanticNode>
|
||||
where
|
||||
S: NodeSource + ?Sized,
|
||||
{
|
||||
let stored = source.get(hash)?;
|
||||
let mut children = Vec::with_capacity(stored.children.len());
|
||||
for child in &stored.children {
|
||||
children.push(reconstruct(source, child)?);
|
||||
}
|
||||
Some(SemanticNode {
|
||||
kind: stored.kind,
|
||||
field_name: stored.field_name,
|
||||
leaf_text: stored.leaf_text,
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
/// [`NodeSource`] respaldado por un [`PersistentRepo`] de `minga-store`
|
||||
/// (almacén `sled` en disco). Es la fuente que usa `minga mount`.
|
||||
pub struct RepoSource {
|
||||
repo: PersistentRepo,
|
||||
}
|
||||
|
||||
impl RepoSource {
|
||||
/// Envuelve un repo ya abierto. La propiedad pasa al `RepoSource`:
|
||||
/// el repo se cierra cuando éste se dropea.
|
||||
pub fn new(repo: PersistentRepo) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeSource for RepoSource {
|
||||
fn roots(&self) -> Vec<ContentHash> {
|
||||
// Las claves del MST corruptas (si las hubiera) se descartan en
|
||||
// silencio: un par de entradas ilegibles no deben tirar el `ls`.
|
||||
self.repo.mst.iter().filter_map(Result::ok).collect()
|
||||
}
|
||||
|
||||
fn get(&self, hash: &ContentHash) -> Option<StoredNode> {
|
||||
self.repo.nodes.get(hash).ok().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
/// [`NodeSource`] en memoria: un `MemStore` más un conjunto explícito
|
||||
/// de raíces. Para tests y para montar índices que viven sólo en RAM.
|
||||
#[derive(Default)]
|
||||
pub struct MemSource {
|
||||
store: minga_core::MemStore,
|
||||
roots: Vec<ContentHash>,
|
||||
}
|
||||
|
||||
impl MemSource {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Inserta un árbol como raíz (un "archivo") y devuelve su hash.
|
||||
/// Idempotente: ingerir dos veces el mismo árbol no lo duplica.
|
||||
pub fn add_root(&mut self, node: &SemanticNode) -> ContentHash {
|
||||
use minga_core::NodeStore;
|
||||
let hash = self.store.put(node);
|
||||
if !self.roots.contains(&hash) {
|
||||
self.roots.push(hash);
|
||||
}
|
||||
hash
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeSource for MemSource {
|
||||
fn roots(&self) -> Vec<ContentHash> {
|
||||
self.roots.clone()
|
||||
}
|
||||
|
||||
fn get(&self, hash: &ContentHash) -> Option<StoredNode> {
|
||||
use minga_core::NodeStore;
|
||||
self.store.get(hash).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use minga_core::ast::SemanticNode;
|
||||
|
||||
fn leaf(kind: &str, text: &str) -> SemanticNode {
|
||||
SemanticNode {
|
||||
kind: kind.to_string(),
|
||||
field_name: None,
|
||||
leaf_text: Some(text.as_bytes().to_vec()),
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mem_source_reconstructs_what_it_stored() {
|
||||
let tree = SemanticNode {
|
||||
kind: "root".to_string(),
|
||||
field_name: None,
|
||||
leaf_text: None,
|
||||
children: vec![leaf("a", "1"), leaf("b", "2")],
|
||||
};
|
||||
let mut src = MemSource::new();
|
||||
let hash = src.add_root(&tree);
|
||||
|
||||
assert_eq!(src.roots(), vec![hash]);
|
||||
let back = reconstruct(&src, &hash).expect("debe reconstruir");
|
||||
assert_eq!(back, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_root_is_idempotent() {
|
||||
let tree = leaf("only", "x");
|
||||
let mut src = MemSource::new();
|
||||
let h1 = src.add_root(&tree);
|
||||
let h2 = src.add_root(&tree);
|
||||
assert_eq!(h1, h2);
|
||||
assert_eq!(src.roots().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_hash_reconstructs_to_none() {
|
||||
let src = MemSource::new();
|
||||
assert!(reconstruct(&src, &ContentHash([0u8; 32])).is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user