Skip to main content

cgx_engine/docs/
label.rs

1//! Replace cgx's dominant-node-name community labels with labels derived from
2//! the common path prefix of nodes in each community. Much more readable.
3//!
4//! e.g. nodes spanning `crates/cgx-engine/src/parsers/*.rs` → label `cgx-engine/parsers`.
5
6use std::collections::HashMap;
7
8use crate::graph::Node;
9
10/// Build a per-community label keyed by community id.
11///
12/// Algorithm:
13/// 1. Collect all node paths per community.
14/// 2. Strip a common leading directory prefix (longest common one ending at `/`).
15/// 3. If the prefix is informative (>=2 segments or contains a recognisable dir),
16///    use it. Otherwise fall back to the cgx-supplied label, then `community-N`.
17pub fn build_community_labels(
18    all_nodes: &[Node],
19    cgx_labels: &HashMap<i64, String>,
20) -> HashMap<i64, String> {
21    let mut by_community: HashMap<i64, Vec<&str>> = HashMap::new();
22    for n in all_nodes {
23        if n.path.is_empty() {
24            continue;
25        }
26        by_community
27            .entry(n.community)
28            .or_default()
29            .push(n.path.as_str());
30    }
31
32    let mut out: HashMap<i64, String> = HashMap::new();
33    for (community, paths) in by_community.iter() {
34        let label = derive_label(
35            paths,
36            cgx_labels.get(community).map(|s| s.as_str()),
37            *community,
38        );
39        out.insert(*community, label);
40    }
41    out
42}
43
44fn derive_label(paths: &[&str], fallback: Option<&str>, community: i64) -> String {
45    if paths.is_empty() {
46        return fallback_label(fallback, community);
47    }
48
49    // Single-file communities: use the file basename without extension.
50    let unique: std::collections::HashSet<&&str> = paths.iter().collect();
51    if unique.len() == 1 {
52        let path = paths[0];
53        return path_to_label(path);
54    }
55
56    let prefix = common_dir_prefix(paths);
57    if !prefix.is_empty() && segment_count(&prefix) >= 2 {
58        return condense(&prefix);
59    }
60    if !prefix.is_empty() {
61        return condense(&prefix);
62    }
63    fallback_label(fallback, community)
64}
65
66fn fallback_label(fallback: Option<&str>, community: i64) -> String {
67    if let Some(s) = fallback {
68        let trimmed = s.trim();
69        if !trimmed.is_empty() && !trimmed.starts_with("File:") && !trimmed.starts_with("Function:")
70        {
71            return trimmed.to_string();
72        }
73    }
74    format!("community-{}", community)
75}
76
77fn path_to_label(path: &str) -> String {
78    // Drop file extension, dasherise.
79    let last_segment = path.rsplit('/').next().unwrap_or(path);
80    let stem = last_segment
81        .rsplit_once('.')
82        .map(|(a, _)| a)
83        .unwrap_or(last_segment);
84    let parent = path.rsplit_once('/').map(|(p, _)| p).unwrap_or("");
85    let parent_tail = parent.rsplit('/').next().unwrap_or("");
86    if parent_tail.is_empty() || parent_tail == "src" || parent_tail == "." {
87        stem.to_string()
88    } else {
89        format!("{}/{}", parent_tail, stem)
90    }
91}
92
93fn common_dir_prefix(paths: &[&str]) -> String {
94    if paths.is_empty() {
95        return String::new();
96    }
97    let first_dirs: Vec<&str> = path_dirs(paths[0]);
98    let mut common: Vec<&str> = first_dirs;
99    for p in paths.iter().skip(1) {
100        let dirs = path_dirs(p);
101        let take = common
102            .iter()
103            .zip(dirs.iter())
104            .take_while(|(a, b)| a == b)
105            .count();
106        common.truncate(take);
107        if common.is_empty() {
108            break;
109        }
110    }
111    common.join("/")
112}
113
114fn path_dirs(path: &str) -> Vec<&str> {
115    let mut parts: Vec<&str> = path.split('/').collect();
116    parts.pop(); // drop filename
117    parts
118}
119
120fn segment_count(prefix: &str) -> usize {
121    prefix.split('/').filter(|s| !s.is_empty()).count()
122}
123
124/// Condense a long prefix by trimming common boilerplate roots.
125fn condense(prefix: &str) -> String {
126    let trimmed = prefix
127        .trim_start_matches("crates/")
128        .trim_start_matches("packages/")
129        .trim_start_matches("src/")
130        .trim_start_matches("./");
131    if trimmed.is_empty() {
132        prefix.trim_start_matches('/').to_string()
133    } else {
134        trimmed.to_string()
135    }
136}