feat(nouser): Phase C — pseudo-embeddings + atracción por centroide
El "imán semántico" matemático del diseño Kairos, sin LLM. Cada
archivo se proyecta a un vector 32-d determinista derivado de sus
metadatos; cada Mónada calcula su centroide; archivos nuevos se
asignan por cosine similarity contra los centroides existentes.
Cambios:
- nouser-core dep nueva: blake3 (hash determinista de strings).
- crates/modules/nouser/core/src/embed.rs nuevo:
- EMBED_DIM = 32. Vector:
* dims 0..8: blake3(extension)
* dims 8..16: blake3(parent_dir)
* dims 16..24: blake3(file_stem)
* dims 24..28: tamaño (log + flags)
* dims 28..32: mtime (escala día + features cíclicas)
- Tip clave: hash bytes se centran a [-1, 1] (no [0, 1]). Sin
centrar, dos hashes random tendrían cosine ~0.75 espurio.
Centrados, expectativa ≈ 0 entre no-relacionados.
- APIs: embed, cosine_similarity, centroid, cohesion,
attraction_score, best_attraction. DEFAULT_ATTRACTION_THRESHOLD = 0.7.
- cluster::by_directory ahora computa el centroide de cada Mónada
y lo guarda en MonadManifest.centroid. El centroide viaja al
brahman-status vía DataFacet.centroid.
- bin nouser nuevo subcomando: attract <dir> <file>.
- Scan del dir, embedding del archivo objetivo, ranking de afinidad
contra Mónadas con centroide.
- 🧲 si la mejor supera umbral, · si es mejor pero debajo.
Validación end-to-end:
$ nouser attract crates/core crates/modules/nouser/core/src/embed.rs
🧲 0.9058 [01K..] src (ente-brain/src)
0.8984 [01K..] src (brahman-handshake/src)
...
$ nouser attract crates/core crates/modules/nouser/core/Cargo.toml
0.3427 [01K..] graph (ente-zero/src/graph)
(mejor score 0.3427 < umbral 0.7000 — no se 'pega')
7 tests nuevos en embed (determinismo, normalización, similitud
mismo-dir/mismo-ext, baja entre no-relacionados, centroide
unidad+coherente, attraction picks correctly, vacío skipeado).
Tests acumulados: 73. cargo check --workspace: 0 errores, 0 warnings.
Próximo: Phase D — nouser-nous, módulo aparte para LLM real.
Mock-nous determinista (basado en estos pseudo-embeddings) en
BRAHMAN_BROKER_CONTEXT=test; real-nous en prod. El switch lo hace
el broker via priority_contexts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use nouser_core::{
|
||||
cluster, db,
|
||||
cluster, db, embed,
|
||||
scanner::{self, ScanConfig},
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ fn main() -> ExitCode {
|
||||
"show" => cmd_show(rest),
|
||||
"json" => cmd_json(rest),
|
||||
"daemon" => cmd_daemon(rest),
|
||||
"attract" => cmd_attract(rest),
|
||||
"--help" | "-h" | "help" => {
|
||||
print_usage(&prog);
|
||||
return ExitCode::SUCCESS;
|
||||
@@ -63,6 +64,7 @@ fn print_usage(prog: &str) {
|
||||
eprintln!(" show <dir> <prefix> scan + detalle de la Mónada cuyo ID empieza con <prefix>");
|
||||
eprintln!(" json <dir> scan + dump JSON de todos los manifests");
|
||||
eprintln!(" daemon <dir> scan + sidecarea cada Mónada al Init brahman");
|
||||
eprintln!(" attract <dir> <file> dado un archivo, qué Mónada del scan lo atrae más");
|
||||
eprintln!();
|
||||
eprintln!("env:");
|
||||
eprintln!(" NOUSER_MIN_FILES mínimo de archivos por Mónada (default: 3)");
|
||||
@@ -203,6 +205,81 @@ fn cmd_daemon(args: &[String]) -> Cmd {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_attract(args: &[String]) -> Cmd {
|
||||
let dir = require_dir(args)?;
|
||||
let file_path = args.get(1).ok_or("falta argumento <file>")?;
|
||||
let file_path = std::path::PathBuf::from(file_path);
|
||||
if !file_path.exists() {
|
||||
return Err(format!("archivo no existe: {}", file_path.display()).into());
|
||||
}
|
||||
|
||||
let (db, _) = run_scan(&dir)?;
|
||||
|
||||
// Construimos un FileEntry para el archivo objetivo y sacamos su embedding.
|
||||
let metadata = std::fs::metadata(&file_path)?;
|
||||
let mtime_ms = metadata
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let target = nouser_card::FileEntry {
|
||||
id: nouser_card::FileId::from(nouser_card::ulid::Ulid::new()),
|
||||
path: file_path.clone(),
|
||||
content_hash: None,
|
||||
size: metadata.len(),
|
||||
mtime_ms,
|
||||
extension: file_path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_lowercase()),
|
||||
};
|
||||
let target_vec = embed::embed(&target);
|
||||
|
||||
// Ranking completo, no sólo el ganador — útil para entender qué
|
||||
// Mónadas son secundarias.
|
||||
let mut ranked: Vec<(&nouser_card::MonadManifest, f32)> = db
|
||||
.monads()
|
||||
.filter(|m| !m.centroid.is_empty())
|
||||
.map(|m| (m, embed::attraction_score(&target_vec, m)))
|
||||
.collect();
|
||||
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
if ranked.is_empty() {
|
||||
println!("ninguna Mónada con centroide en {}", dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("archivo: {}", file_path.display());
|
||||
println!("scan dir: {}", dir.display());
|
||||
println!("ranking de atracción (cosine similarity):");
|
||||
println!();
|
||||
for (i, (m, score)) in ranked.iter().take(5).enumerate() {
|
||||
let marker = if *score >= embed::DEFAULT_ATTRACTION_THRESHOLD && i == 0 {
|
||||
"🧲"
|
||||
} else if i == 0 {
|
||||
"·"
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
let id_short = format!("{}", m.id);
|
||||
let id_short = &id_short[..8];
|
||||
println!(
|
||||
" {} {:.4} [{}] {:30} ({})",
|
||||
marker, score, id_short, m.label, m.summary
|
||||
);
|
||||
}
|
||||
if ranked[0].1 < embed::DEFAULT_ATTRACTION_THRESHOLD {
|
||||
println!();
|
||||
println!(
|
||||
" (mejor score {:.4} < umbral {:.4} — el archivo no se 'pega' a ninguna)",
|
||||
ranked[0].1,
|
||||
embed::DEFAULT_ATTRACTION_THRESHOLD
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Card del propio engine (kind=Ente). Es el "ser" que produce y
|
||||
/// administra Mónadas; aparece en brahman-status junto a sus Mónadas.
|
||||
fn build_engine_card() -> brahman_card::Card {
|
||||
|
||||
Reference in New Issue
Block a user