feat(brahman-card-discovery): B4 — búsqueda de Cards local + DHT

- index — CardIndex: índice en memoria con filtros (by_label
  case-insensitive substring, by_kind, providing por Capability, by_id).
- registry — scan_dir: carga toda Card *.json de un directorio,
  saltando ruido y archivos rotos.
- discovery — CardDiscovery: une el índice local con la malla P2P;
  announce_all publica las Cards locales al DHT, find_remote busca
  proveedores. Modo local-only sin DHT también soportado.

Lo consumen el card-browser de nahual-shell y agorapura.
7 tests verdes. cargo check --workspace verde.

settings.local.json: defaultMode bypassPermissions (sesión desatendida).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 15:23:16 +00:00
parent 27603c906d
commit 1e01dc27a5
7 changed files with 281 additions and 0 deletions
@@ -0,0 +1,46 @@
//! Registro local: escaneo de directorios con Cards en disco.
use crate::index::CardIndex;
use std::path::Path;
/// Escanea `dir` (no recursivo) cargando toda Card `*.json` válida.
/// Los archivos que no parsean como Card se saltan en silencio.
pub fn scan_dir(dir: &Path) -> std::io::Result<CardIndex> {
let mut index = CardIndex::new();
for entry in std::fs::read_dir(dir)? {
let path = entry?.path();
if path.extension().and_then(|e| e.to_str()) == Some("json") {
if let Ok(card) = brahman_cards::load_card_file(&path) {
index.insert(card);
}
}
}
Ok(index)
}
#[cfg(test)]
mod tests {
use super::*;
use brahman_card::Card;
#[test]
fn scans_only_valid_json_cards() {
let dir = tempfile::tempdir().unwrap();
for name in ["alpha", "beta"] {
let card = Card::new(name);
let json = serde_json::to_string(&card).unwrap();
std::fs::write(dir.path().join(format!("{name}.json")), json).unwrap();
}
// Ruido que debe ignorarse.
std::fs::write(dir.path().join("readme.txt"), "no soy una card").unwrap();
std::fs::write(dir.path().join("roto.json"), "{ no json }").unwrap();
let ix = scan_dir(dir.path()).unwrap();
assert_eq!(ix.len(), 2);
}
#[test]
fn missing_dir_is_an_error() {
assert!(scan_dir(Path::new("/no/existe/jamas")).is_err());
}
}