refactor(nouser): labels de Mónada con 2 componentes del path

Resuelve la fricción visual de monorepos donde múltiples Mónadas
quedaban con label "src" (ambiguo). Nueva función label_from_path
toma los últimos hasta 2 componentes normales del path:

  $ 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 (proj/src en lugar de src).

22 tests en nouser-core (era 20, +2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 19:28:19 +00:00
parent 11fc95629c
commit 794884a90f
2 changed files with 56 additions and 8 deletions
+40 -8
View File
@@ -46,11 +46,7 @@ pub fn by_directory(files: &[FileEntry], min_files: usize) -> Vec<MonadManifest>
}
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]