feat(nouser): hidratación del daemon vía sled + path_hint

El daemon ya no recomputa ciegamente al arrancar. Si la DB tiene
Mónadas previas con centroid_model válido, las publica instantáneo
y el re-scan reusa sus IDs vía path_hint.

Schema:
- MonadManifest.path_hint: Option<String> — identidad estable
  derivada del origen (para by_directory, el parent dir canónico).
  Permite reusar ULID across re-scans.

Cluster:
- Nueva fn cluster::by_directory_hydrated(files, min_files, prior).
  Con prior, busca Mónada con mismo path_hint Y mismo centroid_model;
  si la encuentra, reusa id, lineage y created_at_ms.
- by_directory queda como wrapper sin hidratación (back-compat).

Daemon (cmd_daemon):
1. Open sled si NOUSER_DB_PATH existe.
2. Publica Mónadas previas con centroid_model válido (las inválidas
   se descartan con log explícito).
3. Re-scan + by_directory_hydrated(prior=&db).
4. Sólo spawnea sidecars para Mónadas con id NUEVO. Los path_hints
   existentes preservan identidad, evitando duplicados en el broker.
5. Persiste el set actualizado.

Validación:
  $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
  # arranque 1: re-scan 102 archivos → 5 mónadas (5 nuevas)
  $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
  # arranque 2: hidratadas 5 mónadas en O(1)
  #             re-scan → 5 mónadas (0 nuevas vs hidratación)

Costo del arranque 2: ~0.06s user CPU.

Tests: 7 (card) + 24 (core) verdes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-09 00:55:05 +00:00
parent 820a1a33bf
commit 65af98da13
4 changed files with 158 additions and 17 deletions
+34 -1
View File
@@ -28,6 +28,19 @@ pub const DEFAULT_MIN_FILES_PER_MONAD: usize = 3;
/// Devuelve un `Vec<MonadManifest>` ordenado por path. Archivos en
/// directorios con menos de `min_files` no producen Mónada.
pub fn by_directory(files: &[FileEntry], min_files: usize) -> Vec<MonadManifest> {
by_directory_hydrated(files, min_files, None)
}
/// Variante con hidratación: si `prior` está presente, busca Mónadas
/// previas con el mismo `path_hint` y `centroid_model` válido, y reusa
/// su `id` y `lineage`. Esto preserva identidad across re-scans —
/// fundamental para que el daemon pueda republicar tras hidratar de
/// sled sin generar duplicados en el broker.
pub fn by_directory_hydrated(
files: &[FileEntry],
min_files: usize,
prior: Option<&crate::db::MonadDb>,
) -> Vec<MonadManifest> {
let mut by_parent: BTreeMap<PathBuf, Vec<&FileEntry>> = BTreeMap::new();
for f in files {
if let Some(parent) = f.path.parent() {
@@ -40,7 +53,23 @@ pub fn by_directory(files: &[FileEntry], min_files: usize) -> Vec<MonadManifest>
if group.len() < min_files {
continue;
}
out.push(build_monad(&parent, &group));
let mut m = build_monad(&parent, &group);
if let Some(db) = prior {
// Reusamos id si encontramos Mónada previa con mismo
// path_hint Y mismo centroid_model. Distintas hipótesis
// de modelo no comparten identidad — son objetos
// semánticos distintos, aunque parecidos.
if let Some(existing) = db.monads().find(|prev| {
prev.path_hint.as_deref() == m.path_hint.as_deref()
&& prev.centroid_model == m.centroid_model
}) {
m.id = existing.id;
m.lineage = existing.lineage;
m.created_at_ms = existing.created_at_ms;
m.touch();
}
}
out.push(m);
}
out
}
@@ -69,6 +98,10 @@ fn build_monad(parent: &std::path::Path, group: &[&FileEntry]) -> MonadManifest
// Taggeamos el centroide con su modelo. attract verifica esto
// antes de comparar para no mezclar pseudo-32d con real-384d.
m.centroid_model = Some(embed::MODEL_ID.to_string());
// path_hint = identidad estable across re-scans para
// hidratación. Display es lossy con UTF-8 inválido pero los
// paths legítimos se imprimen consistentes.
m.path_hint = Some(parent.display().to_string());
m.members = group.iter().map(|f| f.id).collect();
m.touch();
m