chore: monorepo inicial con arje + minga + yahweh absorbidos

Workspace en 4 ejes (core/modules/apps/shared):

- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
  ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
  ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
  minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
  widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
  image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial

Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.

cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 04:45:44 +00:00
commit 53dbdf0f1d
176 changed files with 34845 additions and 0 deletions
@@ -0,0 +1,83 @@
//! Almacén persistente de atestaciones firmadas.
//!
//! Layout: una sola `sled::Tree` cuya clave es la concatenación
//! `content_hash || author_did` (64 bytes) y cuyo valor es la
//! `Attestation` serializada. Esto permite:
//! - Idempotencia natural: misma `(autor, contenido)` = misma clave.
//! - Listar todas las atestaciones de un contenido vía `scan_prefix`
//! con los primeros 32 bytes (el `ContentHash`).
//!
//! `add` re-verifica criptográficamente cada atestación antes de
//! persistirla — el contrato es idéntico al de `AttestationStore` en
//! memoria: jamás se almacenan firmas inválidas.
use minga_core::{Attestation, AttestationError, ContentHash, Did};
use sled::{Db, Tree};
use crate::error::StoreError;
pub struct SledAttestationStore {
tree: Tree,
}
impl SledAttestationStore {
pub fn open_tree(db: &Db, name: &str) -> Result<Self, StoreError> {
Ok(Self {
tree: db.open_tree(name)?,
})
}
pub fn add(&self, att: Attestation) -> Result<(), StoreError> {
if !att.verify() {
return Err(StoreError::Attestation(AttestationError::InvalidSignature));
}
let key = compose_key(&att.content, &att.author);
let bytes = postcard::to_allocvec(&att)?;
self.tree.insert(&key, bytes)?;
Ok(())
}
/// Devuelve todas las atestaciones para `content` (vacío si
/// ninguna). Orden no especificado.
pub fn get(&self, content: &ContentHash) -> Result<Vec<Attestation>, StoreError> {
let mut out = Vec::new();
for kv in self.tree.scan_prefix(&content.0) {
let (_k, v) = kv?;
out.push(postcard::from_bytes(&v)?);
}
Ok(out)
}
pub fn authors_of(&self, content: &ContentHash) -> Result<Vec<Did>, StoreError> {
Ok(self.get(content)?.into_iter().map(|a| a.author).collect())
}
pub fn len(&self) -> usize {
self.tree.len()
}
pub fn is_empty(&self) -> bool {
self.tree.is_empty()
}
pub fn flush(&self) -> Result<(), StoreError> {
self.tree.flush()?;
Ok(())
}
/// Itera todas las atestaciones persistidas. Cargando un peer al
/// arrancar, esto repuebla el `AttestationStore` en memoria.
pub fn iter(&self) -> impl Iterator<Item = Result<Attestation, StoreError>> + '_ {
self.tree.iter().map(|kv| {
let (_k, v) = kv?;
Ok(postcard::from_bytes(&v)?)
})
}
}
fn compose_key(content: &ContentHash, author: &Did) -> [u8; 64] {
let mut k = [0u8; 64];
k[..32].copy_from_slice(&content.0);
k[32..].copy_from_slice(&author.0);
k
}
@@ -0,0 +1,16 @@
use minga_core::AttestationError;
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("sled: {0}")]
Sled(#[from] sled::Error),
#[error("postcard: {0}")]
Postcard(#[from] postcard::Error),
#[error("attestation: {0}")]
Attestation(#[from] AttestationError),
#[error("hash inconsistente con el contenido del nodo")]
HashMismatch,
}
@@ -0,0 +1,41 @@
//! Persistencia en disco de keypairs cifrados.
//!
//! El cifrado en sí (AES-GCM + Argon2id) vive en `minga-core`, que es
//! pure logic. Aquí solo se monta la parte de IO: leer/escribir
//! bytes a un archivo.
//!
//! Layout del archivo: el blob crudo que produce
//! `Keypair::encrypt(passphrase)`. 85 bytes total.
use std::fs;
use std::io;
use std::path::Path;
use minga_core::{Keypair, KeypairCryptoError};
#[derive(Debug, thiserror::Error)]
pub enum KeypairFileError {
#[error("io: {0}")]
Io(#[from] io::Error),
#[error("crypto: {0}")]
Crypto(#[from] KeypairCryptoError),
}
/// Guarda un keypair cifrado con la passphrase en `path`. Si el
/// archivo ya existe, lo sobrescribe.
pub fn save<P: AsRef<Path>>(
keypair: &Keypair,
path: P,
passphrase: &str,
) -> Result<(), KeypairFileError> {
let blob = keypair.encrypt(passphrase)?;
fs::write(path, blob)?;
Ok(())
}
/// Carga un keypair desde un archivo cifrado.
pub fn load<P: AsRef<Path>>(path: P, passphrase: &str) -> Result<Keypair, KeypairFileError> {
let blob = fs::read(path)?;
Ok(Keypair::decrypt(&blob, passphrase)?)
}
@@ -0,0 +1,36 @@
//! `minga-store`: backing persistente con `sled` para los stores de Minga.
//!
//! Tres stores paralelos a los de `minga-core`:
//! - [`SledNodeStore`]: hashes → `StoredNode`s, equivalente persistente
//! de `MemStore`.
//! - [`SledAttestationStore`]: pruebas criptográficas de autoría
//! indexadas por content hash.
//! - [`SledMstStore`]: conjunto de claves del MST. La estructura
//! probabilística del MST se reconstruye en memoria al cargar
//! ([`SledMstStore::to_in_memory`]) — solo persistimos las claves
//! porque el árbol es deterministicamente derivable de ellas.
//!
//! Una `PersistentRepo` agrupa los tres sobre una única `sled::Db`
//! (tres trees con namespaces separados).
//!
//! El núcleo (`minga-core`) sigue siendo agnóstico de IO: estos tipos
//! tienen APIs paralelas (devuelven `Result`, deserializan vía
//! postcard) y los protocolos de sync se quedan operando sobre los
//! tipos in-memory. La integración con `MingaPeer` (que hoy usa
//! `MemStore` concreto) llegará tras un trait genérico — esta
//! iteración se centra en que la capa de persistencia esté correcta
//! y testeada.
pub mod attestation_store;
pub mod error;
pub mod keypair_file;
pub mod mst_store;
pub mod node_store;
pub mod repo;
pub use attestation_store::SledAttestationStore;
pub use error::StoreError;
pub use keypair_file::KeypairFileError;
pub use mst_store::SledMstStore;
pub use node_store::SledNodeStore;
pub use repo::PersistentRepo;
@@ -0,0 +1,74 @@
//! Persistencia del MST.
//!
//! Solo persistimos las **claves** (los `ContentHash`es del conjunto).
//! La estructura probabilística del MST (niveles, separadores,
//! árbol de Merkle) es derivable determinísticamente de las claves,
//! así que reconstruirla en memoria al cargar es trivial.
//!
//! Layout: una `sled::Tree` cuyas claves son los 32 bytes del hash y
//! cuyos valores son vacíos. Los hashes se ordenan automáticamente
//! por sled (orden lexicográfico = orden por bytes), lo que coincide
//! con el orden que `Mst::iter` produce.
use minga_core::{ContentHash, Mst};
use sled::{Db, Tree};
use crate::error::StoreError;
pub struct SledMstStore {
tree: Tree,
}
impl SledMstStore {
pub fn open_tree(db: &Db, name: &str) -> Result<Self, StoreError> {
Ok(Self {
tree: db.open_tree(name)?,
})
}
pub fn insert(&self, h: ContentHash) -> Result<bool, StoreError> {
let prev = self.tree.insert(h.0, &[])?;
Ok(prev.is_none())
}
pub fn contains(&self, h: &ContentHash) -> Result<bool, StoreError> {
Ok(self.tree.contains_key(h.0)?)
}
pub fn len(&self) -> usize {
self.tree.len()
}
pub fn is_empty(&self) -> bool {
self.tree.is_empty()
}
/// Itera todas las claves del MST en orden ascendente por hash.
pub fn iter(&self) -> impl Iterator<Item = Result<ContentHash, StoreError>> + '_ {
self.tree.iter().map(|kv| {
let (k, _) = kv?;
if k.len() != 32 {
return Err(StoreError::HashMismatch);
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&k);
Ok(ContentHash(bytes))
})
}
/// Reconstruye un `Mst` en memoria a partir de las claves
/// persistidas. Útil al arrancar un peer: cargamos las claves
/// del disco y rehacemos la estructura para operaciones rápidas.
pub fn to_in_memory(&self) -> Result<Mst, StoreError> {
let mut mst = Mst::new();
for h in self.iter() {
mst.insert(h?);
}
Ok(mst)
}
pub fn flush(&self) -> Result<(), StoreError> {
self.tree.flush()?;
Ok(())
}
}
@@ -0,0 +1,134 @@
//! Almacén persistente de `StoredNode`s indexados por `ContentHash`.
//!
//! Cada nodo se serializa con postcard y se inserta en una `sled::Tree`
//! cuya clave son los 32 bytes del hash. La operación `put` es
//! recursiva sobre los hijos (igual que `MemStore::put`): cada
//! subárbol se hashea y persiste exactamente una vez.
use minga_core::{cas, hash_stored, ContentHash, SemanticNode, StoredNode};
use sled::{Db, Tree};
use crate::error::StoreError;
pub struct SledNodeStore {
tree: Tree,
}
impl SledNodeStore {
pub fn open_tree(db: &Db, name: &str) -> Result<Self, StoreError> {
Ok(Self {
tree: db.open_tree(name)?,
})
}
/// Inserta un árbol completo. Recursivamente desempaqueta hijos.
/// Devuelve el hash de la raíz. Idempotente: insertar el mismo
/// árbol dos veces no añade entradas nuevas.
pub fn put(&self, node: &SemanticNode) -> Result<ContentHash, StoreError> {
let mut child_hashes = Vec::with_capacity(node.children.len());
for c in &node.children {
child_hashes.push(self.put(c)?);
}
let h = cas::hash_components(
&node.kind,
node.field_name.as_deref(),
node.leaf_text.as_deref(),
&child_hashes,
);
if !self.tree.contains_key(h.0)? {
let stored = StoredNode {
kind: node.kind.clone(),
field_name: node.field_name.clone(),
leaf_text: node.leaf_text.clone(),
children: child_hashes,
};
let bytes = postcard::to_allocvec(&stored)?;
self.tree.insert(h.0, bytes)?;
}
Ok(h)
}
/// Inserta un nodo ya troceado por hash. Verifica que el hash
/// coincida con `hash_stored(stored)` antes de insertar — sin
/// esa verificación no podemos confiar en la integridad de lo
/// que viene del wire.
pub fn put_chunked(
&self,
hash: ContentHash,
stored: &StoredNode,
) -> Result<(), StoreError> {
if hash_stored(stored) != hash {
return Err(StoreError::HashMismatch);
}
if !self.tree.contains_key(hash.0)? {
let bytes = postcard::to_allocvec(stored)?;
self.tree.insert(hash.0, bytes)?;
}
Ok(())
}
pub fn get(&self, h: &ContentHash) -> Result<Option<StoredNode>, StoreError> {
match self.tree.get(h.0)? {
Some(bytes) => Ok(Some(postcard::from_bytes(&bytes)?)),
None => Ok(None),
}
}
pub fn contains(&self, h: &ContentHash) -> Result<bool, StoreError> {
Ok(self.tree.contains_key(h.0)?)
}
/// Reconstruye un `SemanticNode` resolviendo recursivamente todos
/// los hijos. `Ok(None)` si algún hash no está en el store
/// (almacén incompleto).
pub fn reconstruct(&self, h: &ContentHash) -> Result<Option<SemanticNode>, StoreError> {
let stored = match self.get(h)? {
Some(s) => s,
None => return Ok(None),
};
let mut children = Vec::with_capacity(stored.children.len());
for ch in &stored.children {
match self.reconstruct(ch)? {
Some(n) => children.push(n),
None => return Ok(None),
}
}
Ok(Some(SemanticNode {
kind: stored.kind,
field_name: stored.field_name,
leaf_text: stored.leaf_text,
children,
}))
}
pub fn len(&self) -> usize {
self.tree.len()
}
pub fn is_empty(&self) -> bool {
self.tree.is_empty()
}
pub fn flush(&self) -> Result<(), StoreError> {
self.tree.flush()?;
Ok(())
}
/// Itera todos los pares `(hash, stored_node)` persistidos. Sin
/// orden garantizado más allá del lexicográfico de sled. Usado al
/// arrancar para volcar el contenido a un `MemStore` en memoria.
pub fn iter(
&self,
) -> impl Iterator<Item = Result<(ContentHash, StoredNode), StoreError>> + '_ {
self.tree.iter().map(|kv| {
let (k, v) = kv?;
if k.len() != 32 {
return Err(StoreError::HashMismatch);
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&k);
let stored: StoredNode = postcard::from_bytes(&v)?;
Ok((ContentHash(bytes), stored))
})
}
}
@@ -0,0 +1,41 @@
//! `PersistentRepo`: agrupa los tres stores (nodos, atestaciones, MST)
//! sobre una única `sled::Db`. Cada store ocupa su propio tree
//! (namespace lógico) dentro del mismo directorio en disco.
use std::path::Path;
use sled::Db;
use crate::{
attestation_store::SledAttestationStore, error::StoreError, mst_store::SledMstStore,
node_store::SledNodeStore,
};
pub struct PersistentRepo {
db: Db,
pub nodes: SledNodeStore,
pub attestations: SledAttestationStore,
pub mst: SledMstStore,
}
impl PersistentRepo {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, StoreError> {
let db = sled::open(path)?;
let nodes = SledNodeStore::open_tree(&db, "nodes")?;
let attestations = SledAttestationStore::open_tree(&db, "attestations")?;
let mst = SledMstStore::open_tree(&db, "mst")?;
Ok(Self {
db,
nodes,
attestations,
mst,
})
}
/// Flushea los tres trees a disco. Llamar en puntos de
/// checkpoint o antes de cerrar para garantizar durabilidad.
pub fn flush(&self) -> Result<(), StoreError> {
self.db.flush()?;
Ok(())
}
}