refactor(naming): A1 — ente→arje, vista→revista, pluma→fana
Rename batch de la Fase A del PLAN_MACRO: - 25 crates ente-* → arje-* (protocol/init/runtime/compat). El linaje arje (init Linux) queda con prefijo coherente. - vista → revista (revista-core + revista-web). - pluma → fana (fana-md + fana-md-reader-web). fana absorbe el linaje markdown de pluma; será el writer DAG editor (prioridad alta). Cambios: - git mv de 29 crate dirs + 2 SDDs - package/lib/bin names + path refs + imports .rs reescritos - workspace Cargo.toml + comentarios de sección - SDDs de init/runtime/compat/protocol actualizados a arje- - SDD de revista + SDD de fana (reescrito: writer DAG editor) - docs/STATUS.md, ROADMAP.md, PLAN_MACRO.md, arje-boot.md, arje-replace-systemd.md actualizados - docs/changelog/akasha.md → chasqui.md scripts/rename-fase-a.py idempotente (--dry-run soportado). cargo check --workspace verde. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
//! DB de Mónadas y archivos. Backend dual:
|
||||
//!
|
||||
//! - **Memoria** (default, cache): `BTreeMap<Id, T>` para reads O(log n).
|
||||
//! - **Persistencia** (opcional): sled-backed write-through. Si se abre
|
||||
//! con `MonadDb::open(path)`, cada `insert_*` escribe a sled además
|
||||
//! de la cache. Reads siempre vienen de la cache.
|
||||
//!
|
||||
//! Wire format: JSON via serde_json. Los manifestos son chicos y
|
||||
//! ocasionalmente inspeccionables a mano (`sled-cli`); JSON gana sobre
|
||||
//! postcard en debuggability.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use chasqui_card::{FileEntry, FileId, MonadId, MonadManifest};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MonadDbError {
|
||||
#[error("sled: {0}")]
|
||||
Sled(#[from] sled::Error),
|
||||
#[error("JSON: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("ULID inválido en clave: {0}")]
|
||||
BadKey(String),
|
||||
}
|
||||
|
||||
const TREE_FILES: &str = "files";
|
||||
const TREE_MONADS: &str = "monads";
|
||||
|
||||
/// Store de Mónadas + archivos. Cache en memoria + persistencia
|
||||
/// opcional sled.
|
||||
pub struct MonadDb {
|
||||
files: BTreeMap<FileId, FileEntry>,
|
||||
monads: BTreeMap<MonadId, MonadManifest>,
|
||||
persistence: Option<sled::Db>,
|
||||
}
|
||||
|
||||
impl Default for MonadDb {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MonadDb {
|
||||
/// Store en memoria pura (sin persistencia). El estado se pierde al salir.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
files: BTreeMap::new(),
|
||||
monads: BTreeMap::new(),
|
||||
persistence: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Abre (o crea) un store sled-backed en `path`. Carga el contenido
|
||||
/// existente a la cache antes de devolver.
|
||||
pub fn open(path: impl AsRef<Path>) -> Result<Self, MonadDbError> {
|
||||
let db = sled::open(path)?;
|
||||
let mut files = BTreeMap::new();
|
||||
let mut monads = BTreeMap::new();
|
||||
|
||||
let files_tree = db.open_tree(TREE_FILES)?;
|
||||
for kv in files_tree.iter() {
|
||||
let (k, v) = kv?;
|
||||
let id = decode_key(&k)?;
|
||||
let entry: FileEntry = serde_json::from_slice(&v)?;
|
||||
files.insert(id, entry);
|
||||
}
|
||||
let monads_tree = db.open_tree(TREE_MONADS)?;
|
||||
for kv in monads_tree.iter() {
|
||||
let (k, v) = kv?;
|
||||
let id = decode_key(&k)?;
|
||||
let monad: MonadManifest = serde_json::from_slice(&v)?;
|
||||
monads.insert(id, monad);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
files,
|
||||
monads,
|
||||
persistence: Some(db),
|
||||
})
|
||||
}
|
||||
|
||||
/// `true` si tiene backend persistente.
|
||||
pub fn is_persistent(&self) -> bool {
|
||||
self.persistence.is_some()
|
||||
}
|
||||
|
||||
// ---- Files ----
|
||||
|
||||
pub fn insert_file(&mut self, file: FileEntry) -> Option<FileEntry> {
|
||||
if let Some(db) = &self.persistence {
|
||||
// Write-through: si falla el persist, lo logeamos pero la
|
||||
// memoria queda actualizada. Filosofía: cache nunca miente
|
||||
// sobre el último estado conocido en este proceso.
|
||||
if let Err(e) = persist_file(db, &file) {
|
||||
eprintln!("[MonadDb] persist file falló: {e}");
|
||||
}
|
||||
}
|
||||
self.files.insert(file.id, file)
|
||||
}
|
||||
|
||||
pub fn ingest_files(&mut self, files: Vec<FileEntry>) {
|
||||
for f in files {
|
||||
self.insert_file(f);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file(&self, id: FileId) -> Option<&FileEntry> {
|
||||
self.files.get(&id)
|
||||
}
|
||||
|
||||
pub fn files(&self) -> impl Iterator<Item = &FileEntry> + '_ {
|
||||
self.files.values()
|
||||
}
|
||||
|
||||
pub fn file_count(&self) -> usize {
|
||||
self.files.len()
|
||||
}
|
||||
|
||||
// ---- Monads ----
|
||||
|
||||
pub fn insert_monad(&mut self, monad: MonadManifest) -> Option<MonadManifest> {
|
||||
if let Some(db) = &self.persistence {
|
||||
if let Err(e) = persist_monad(db, &monad) {
|
||||
eprintln!("[MonadDb] persist monad falló: {e}");
|
||||
}
|
||||
}
|
||||
self.monads.insert(monad.id, monad)
|
||||
}
|
||||
|
||||
pub fn replace_monads(&mut self, monads: Vec<MonadManifest>) {
|
||||
// Si hay persistencia, limpiar tree antes de insertar.
|
||||
if let Some(db) = &self.persistence {
|
||||
if let Ok(tree) = db.open_tree(TREE_MONADS) {
|
||||
let _ = tree.clear();
|
||||
}
|
||||
}
|
||||
self.monads.clear();
|
||||
for m in monads {
|
||||
self.insert_monad(m);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn monad(&self, id: MonadId) -> Option<&MonadManifest> {
|
||||
self.monads.get(&id)
|
||||
}
|
||||
|
||||
pub fn monads(&self) -> impl Iterator<Item = &MonadManifest> + '_ {
|
||||
self.monads.values()
|
||||
}
|
||||
|
||||
pub fn monad_count(&self) -> usize {
|
||||
self.monads.len()
|
||||
}
|
||||
|
||||
/// Resuelve los archivos miembros de una Mónada como referencias.
|
||||
/// Skipea silenciosamente IDs que ya no estén en la tabla `files`.
|
||||
pub fn resolve_members(&self, monad_id: MonadId) -> Vec<&FileEntry> {
|
||||
match self.monads.get(&monad_id) {
|
||||
Some(m) => m.members.iter().filter_map(|id| self.files.get(id)).collect(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_file(db: &sled::Db, f: &FileEntry) -> Result<(), MonadDbError> {
|
||||
let tree = db.open_tree(TREE_FILES)?;
|
||||
let key = f.id.to_string();
|
||||
let val = serde_json::to_vec(f)?;
|
||||
tree.insert(key.as_bytes(), val)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn persist_monad(db: &sled::Db, m: &MonadManifest) -> Result<(), MonadDbError> {
|
||||
let tree = db.open_tree(TREE_MONADS)?;
|
||||
let key = m.id.to_string();
|
||||
let val = serde_json::to_vec(m)?;
|
||||
tree.insert(key.as_bytes(), val)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_key(k: &[u8]) -> Result<ulid::Ulid, MonadDbError> {
|
||||
let s = std::str::from_utf8(k).map_err(|_| MonadDbError::BadKey(format!("{:?}", k)))?;
|
||||
ulid::Ulid::from_string(s).map_err(|_| MonadDbError::BadKey(s.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chasqui_card::Lens;
|
||||
use ulid::Ulid;
|
||||
|
||||
fn mk_file(path: &str) -> FileEntry {
|
||||
FileEntry {
|
||||
id: FileId::from(Ulid::new()),
|
||||
path: std::path::PathBuf::from(path),
|
||||
content_hash: None,
|
||||
size: 100,
|
||||
mtime_ms: 0,
|
||||
extension: Some("rs".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ingest_and_lookup() {
|
||||
let mut db = MonadDb::new();
|
||||
let f1 = mk_file("/a/x.rs");
|
||||
let f2 = mk_file("/a/y.rs");
|
||||
let id1 = f1.id;
|
||||
db.ingest_files(vec![f1, f2]);
|
||||
assert_eq!(db.file_count(), 2);
|
||||
assert!(db.file(id1).is_some());
|
||||
assert!(!db.is_persistent());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_members_filters_missing() {
|
||||
let mut db = MonadDb::new();
|
||||
let f1 = mk_file("/x/a.rs");
|
||||
let id1 = f1.id;
|
||||
db.insert_file(f1);
|
||||
|
||||
let mut m = MonadManifest::new("test");
|
||||
m.members.insert(id1);
|
||||
m.members.insert(FileId::from(Ulid::new())); // miembro fantasma
|
||||
m.dominant_lens = Lens::Code;
|
||||
m.touch();
|
||||
|
||||
let mid = m.id;
|
||||
db.insert_monad(m);
|
||||
|
||||
let resolved = db.resolve_members(mid);
|
||||
assert_eq!(resolved.len(), 1);
|
||||
assert_eq!(resolved[0].id, id1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_monads_clears_old() {
|
||||
let mut db = MonadDb::new();
|
||||
let mut m1 = MonadManifest::new("a");
|
||||
m1.members.insert(FileId::from(Ulid::new()));
|
||||
m1.touch();
|
||||
db.insert_monad(m1);
|
||||
assert_eq!(db.monad_count(), 1);
|
||||
|
||||
let mut m2 = MonadManifest::new("b");
|
||||
m2.members.insert(FileId::from(Ulid::new()));
|
||||
m2.touch();
|
||||
db.replace_monads(vec![m2]);
|
||||
assert_eq!(db.monad_count(), 1);
|
||||
assert!(db.monads().next().unwrap().label == "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistence_roundtrip() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbpath = tmp.path().join("monads.sled");
|
||||
|
||||
// Escribimos algunos datos
|
||||
{
|
||||
let mut db = MonadDb::open(&dbpath).expect("open");
|
||||
assert!(db.is_persistent());
|
||||
let f = mk_file("/persist/a.rs");
|
||||
let fid = f.id;
|
||||
db.insert_file(f);
|
||||
|
||||
let mut m = MonadManifest::new("persist-test");
|
||||
m.members.insert(fid);
|
||||
m.dominant_lens = Lens::Code;
|
||||
m.touch();
|
||||
db.insert_monad(m);
|
||||
}
|
||||
|
||||
// Reabrimos y verificamos que están
|
||||
let db = MonadDb::open(&dbpath).expect("reopen");
|
||||
assert_eq!(db.file_count(), 1);
|
||||
assert_eq!(db.monad_count(), 1);
|
||||
let m = db.monads().next().unwrap();
|
||||
assert_eq!(m.label, "persist-test");
|
||||
assert_eq!(m.cardinality, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_monads_purges_persistent_tree() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbpath = tmp.path().join("replace.sled");
|
||||
|
||||
{
|
||||
let mut db = MonadDb::open(&dbpath).unwrap();
|
||||
let mut m1 = MonadManifest::new("old");
|
||||
m1.members.insert(FileId::from(Ulid::new()));
|
||||
m1.touch();
|
||||
db.insert_monad(m1);
|
||||
}
|
||||
|
||||
// Reabrir, replace, verificar
|
||||
{
|
||||
let mut db = MonadDb::open(&dbpath).unwrap();
|
||||
assert_eq!(db.monad_count(), 1);
|
||||
let mut m2 = MonadManifest::new("new");
|
||||
m2.members.insert(FileId::from(Ulid::new()));
|
||||
m2.touch();
|
||||
db.replace_monads(vec![m2]);
|
||||
assert_eq!(db.monad_count(), 1);
|
||||
}
|
||||
|
||||
// Tercera apertura: sólo "new" sobrevive
|
||||
let db = MonadDb::open(&dbpath).unwrap();
|
||||
assert_eq!(db.monad_count(), 1);
|
||||
assert_eq!(db.monads().next().unwrap().label, "new");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user