diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a6196a..b47dad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ ratio/diff ver `git show `. ## 2026-05-08 +### refactor(nouser): labels de Mónada con 2 componentes del path +Resuelve la fricción visual de monorepos donde múltiples Mónadas se +llamaban "src". Nueva función `label_from_path` toma los últimos hasta +2 componentes normales del path y los une con `/`. + + $ nouser scan crates/core + [01K..] brahman-admin/src card=5 + [01K..] brahman-handshake/src card=6 + [01K..] ente-brain/src card=11 + [01K..] ente-kernel/src card=4 + ... + +Tests añadidos: `label_from_root_only_one_component`, +`label_from_deep_path_takes_last_two`. Tests existentes actualizados +con los nuevos labels. + ### feat(nouser): Phase D-2 — proveedor Nous real (LLM) detrás de feature flag Cierra el ciclo del módulo Nous: existe un proveedor que produce embeddings reales con un modelo LLM, mientras que `cargo build` sin diff --git a/crates/modules/nouser/core/src/cluster.rs b/crates/modules/nouser/core/src/cluster.rs index 62c5ac6..2ddc432 100644 --- a/crates/modules/nouser/core/src/cluster.rs +++ b/crates/modules/nouser/core/src/cluster.rs @@ -46,11 +46,7 @@ pub fn by_directory(files: &[FileEntry], min_files: usize) -> Vec } fn build_monad(parent: &std::path::Path, group: &[&FileEntry]) -> MonadManifest { - let label = parent - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("unnamed") - .to_string(); + let label = label_from_path(parent); let keywords = top_extensions(group, 5); let lens = pick_lens(group); @@ -75,6 +71,27 @@ fn build_monad(parent: &std::path::Path, group: &[&FileEntry]) -> MonadManifest m } +/// Construye un label legible tomando los últimos hasta 2 componentes +/// del path. Esto desambigua `src/` repetidos en monorepos: en lugar +/// de 5 Mónadas con label "src", quedan "ente-zero/src", "ente-brain/src", +/// etc. Para directorios shallow (root o un nivel), cae al +/// `file_name()` simple. +fn label_from_path(p: &std::path::Path) -> String { + let normals: Vec<&str> = p + .components() + .filter_map(|c| match c { + std::path::Component::Normal(s) => s.to_str(), + _ => None, + }) + .collect(); + if normals.is_empty() { + return "unnamed".to_string(); + } + let take = normals.len().min(2); + let start = normals.len() - take; + normals[start..].join("/") +} + fn build_summary(parent: &std::path::Path, group: &[&FileEntry], keywords: &[String]) -> String { let path_str = parent.display(); let n = group.len(); @@ -173,8 +190,10 @@ mod tests { let monads = by_directory(&files, 3); assert_eq!(monads.len(), 2); let labels: std::collections::BTreeSet<_> = monads.iter().map(|m| &m.label).collect(); - assert!(labels.iter().any(|l| l.as_str() == "src")); - assert!(labels.iter().any(|l| l.as_str() == "docs")); + // Phase B: labels usan los últimos 2 componentes del path para + // desambiguar (proj/src vs proj/docs en lugar de src vs docs). + assert!(labels.iter().any(|l| l.as_str() == "proj/src")); + assert!(labels.iter().any(|l| l.as_str() == "proj/docs")); } #[test] @@ -188,7 +207,20 @@ mod tests { // min=3 → /proj/single solo no se promueve, /proj/sub sí. let monads = by_directory(&files, 3); assert_eq!(monads.len(), 1); - assert_eq!(monads[0].label, "sub"); + assert_eq!(monads[0].label, "proj/sub"); + } + + #[test] + fn label_from_root_only_one_component() { + // Un solo componente normal en el path → no hay "padre" útil. + let p = std::path::Path::new("/onlyone"); + assert_eq!(label_from_path(p), "onlyone"); + } + + #[test] + fn label_from_deep_path_takes_last_two() { + let p = std::path::Path::new("/a/b/c/d/e"); + assert_eq!(label_from_path(p), "d/e"); } #[test]