use std::collections::{HashMap, HashSet, VecDeque};
use crate::graph::{Edge, Node, dir_id};
use super::parent_dir;
pub(super) fn collapse_to_files(
nodes: &mut Vec<Node>,
edges: &mut Vec<Edge>,
sym_node: &HashMap<i64, (String, String)>,
) {
let sym_to_file: HashMap<&str, &str> = sym_node
.values()
.map(|(s, f)| (s.as_str(), f.as_str()))
.collect();
nodes.retain(|n| n.kind == "file" || n.kind == "external");
let mut seen: HashSet<(String, String, String)> = HashSet::new();
let mut out = Vec::new();
for e in edges.drain(..) {
if e.kind == "contains" {
continue;
}
let s = sym_to_file
.get(e.source.as_str())
.map(|s| s.to_string())
.unwrap_or(e.source);
let t = sym_to_file
.get(e.target.as_str())
.map(|s| s.to_string())
.unwrap_or(e.target);
if s == t {
continue;
}
if seen.insert((s.clone(), t.clone(), e.kind.clone())) {
out.push(Edge {
source: s,
target: t,
kind: e.kind,
confidence: e.confidence,
external: e.external,
line: e.line,
});
}
}
*edges = out;
}
pub(super) fn apply_focus(nodes: &mut Vec<Node>, edges: &mut Vec<Edge>, focus: &str, depth: u32) {
let focus_normalized = focus.replace("\\", "/");
let focus_clean = focus_normalized
.strip_prefix("./")
.unwrap_or(&focus_normalized);
let seeds: HashSet<&str> = nodes
.iter()
.filter(|n| n.label == focus_clean || n.id.contains(focus_clean))
.map(|n| n.id.as_str())
.collect();
if seeds.is_empty() {
return;
}
let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
for e in edges.iter() {
adj.entry(&e.source).or_default().push(&e.target);
adj.entry(&e.target).or_default().push(&e.source);
}
let mut keep: HashSet<String> = seeds.iter().map(|s| s.to_string()).collect();
let mut frontier: VecDeque<(String, u32)> = seeds.iter().map(|s| (s.to_string(), 0)).collect();
while let Some((id, d)) = frontier.pop_front() {
if d >= depth {
continue;
}
if let Some(neigh) = adj.get(id.as_str()) {
for n in neigh {
if keep.insert(n.to_string()) {
frontier.push_back((n.to_string(), d + 1));
}
}
}
}
nodes.retain(|n| keep.contains(&n.id));
edges.retain(|e| keep.contains(&e.source) && keep.contains(&e.target));
}
pub(super) fn prune_unreferenced_externals(
nodes: &mut Vec<Node>,
edges: &[Edge],
include_external: bool,
) {
if !include_external {
nodes.retain(|n| n.kind != "external");
return;
}
let used: HashSet<&str> = edges
.iter()
.flat_map(|e| [e.source.as_str(), e.target.as_str()])
.collect();
nodes.retain(|n| n.kind != "external" || used.contains(n.id.as_str()));
}
pub(super) fn degree_map(edges: &[Edge]) -> HashMap<String, u32> {
let mut m: HashMap<String, u32> = HashMap::new();
for e in edges {
if e.kind == "contains" {
continue;
}
*m.entry(e.source.clone()).or_default() += 1;
*m.entry(e.target.clone()).or_default() += 1;
}
m
}
pub(super) fn enforce_max_nodes(
nodes: &mut Vec<Node>,
edges: &mut Vec<Edge>,
degree: &HashMap<String, u32>,
max_nodes: usize,
) {
if max_nodes == 0 || nodes.len() <= max_nodes {
return;
}
let mut syms: Vec<&Node> = nodes.iter().filter(|n| n.id.starts_with("sym:")).collect();
let structural = nodes.len() - syms.len();
let drop_count = match max_nodes.checked_sub(structural) {
Some(budget) => syms.len().saturating_sub(budget),
None => syms.len(),
};
syms.sort_by_key(|n| degree.get(n.id.as_str()).copied().unwrap_or(0));
let doomed: HashSet<String> = syms
.into_iter()
.take(drop_count)
.map(|n| n.id.clone())
.collect();
nodes.retain(|n| !doomed.contains(&n.id));
edges.retain(|e| !doomed.contains(&e.source) && !doomed.contains(&e.target));
if !doomed.is_empty() {
eprintln!(
"graph: dropped {} low-degree symbol nodes to stay under --max-nodes {max_nodes}",
doomed.len()
);
}
}
pub(super) fn rollup_directories(nodes: &mut Vec<Node>, edges: &mut Vec<Edge>) {
let mut dirs: HashSet<String> = HashSet::new();
let mut contains: Vec<(String, String)> = Vec::new();
let file_dirs: Vec<(String, String)> = nodes
.iter()
.filter(|n| n.kind == "file")
.filter_map(|n| {
n.path
.as_deref()
.and_then(parent_dir)
.map(|d| (n.id.clone(), d))
})
.collect();
for (fid, dir) in &file_dirs {
contains.push((dir_id(dir), fid.clone()));
let mut cur = dir.clone();
while dirs.insert(cur.clone()) {
match parent_dir(&cur) {
Some(parent) => {
contains.push((dir_id(&parent), dir_id(&cur)));
cur = parent;
}
None => break,
}
}
}
for d in &dirs {
nodes.push(Node {
id: dir_id(d),
kind: "dir".into(),
label: d.clone(),
path: Some(d.clone()),
dir: parent_dir(d),
language: None,
exported: true,
lines: None,
degree: None,
community: None,
});
}
let mut seen: HashSet<(String, String)> = HashSet::new();
for (parent, child) in contains {
if seen.insert((parent.clone(), child.clone())) {
edges.push(Edge {
source: parent,
target: child,
kind: "contains".into(),
confidence: 1.0,
external: None,
line: None,
});
}
}
}