use crate::graph::types::*;
use anyhow::Result;
pub fn cmd_show(data: &GraphData) {
let m = &data.meta;
let layers = count_layers(&data.nodes);
let file_nodes = data.nodes.iter().filter(|n| n.node_type == "file").count();
let func_nodes = data.nodes.iter().filter(|n| n.node_type == "function").count();
let class_nodes = data.nodes.iter().filter(|n| n.node_type == "class").count();
println!("\n {}", m.project);
println!();
println!(" Languages {}", m.languages.join(", "));
if !m.frameworks.is_empty() {
println!(" Frameworks {}", m.frameworks.join(", "));
}
println!(" Files {}", file_nodes);
if func_nodes > 0 { println!(" Functions {}", func_nodes); }
if class_nodes > 0 { println!(" Classes {}", class_nodes); }
println!(" Edges {}", data.edges.len());
println!(" Analysed {}", &m.analysed_at[..19]);
println!();
println!(" Architecture");
let mut layer_list: Vec<_> = layers.iter().collect();
layer_list.sort_by(|a, b| b.1.cmp(a.1));
for (layer, count) in &layer_list {
println!(" {:<24} {} nodes", layer, count);
}
println!();
println!(" Tour ({} steps)", data.tour.len());
for step in data.tour.iter().take(5) {
println!(" {:>3}. {}", step.order, step.name);
}
if data.tour.len() > 5 {
println!(" … and {} more (run `yana-rt graph onboard`)", data.tour.len() - 5);
}
println!();
}
pub fn cmd_search(data: &GraphData, query: &str, expand: bool, limit: usize) {
let q = query.to_lowercase();
let mut hits: Vec<&Node> = data.nodes.iter().filter(|n| {
n.name.to_lowercase().contains(&q)
|| n.file_path.to_lowercase().contains(&q)
|| n.tags.iter().any(|t| t.contains(&q))
|| n.language.to_lowercase().contains(&q)
}).collect();
hits.sort_by_key(|n| {
if n.name.to_lowercase() == q { 0 }
else if n.name.to_lowercase().starts_with(&q) { 1 }
else { 2 }
});
hits.truncate(limit);
if hits.is_empty() {
println!("(no results for '{}')", query);
return;
}
println!("\n Search: {}", query);
println!(" {} result(s)\n", hits.len());
for n in &hits {
println!(" [{:<14}] {}", n.node_type, n.name);
println!(" {}", n.file_path);
if !n.summary.is_empty() {
println!(" {}", n.summary.chars().take(80).collect::<String>());
}
}
if expand && !hits.is_empty() {
let first_id = &hits[0].id;
let connected: Vec<&Node> = data.edges.iter()
.filter(|e| &e.source == first_id || &e.target == first_id)
.flat_map(|e| {
let id = if &e.source == first_id { &e.target } else { &e.source };
data.nodes.iter().find(|n| &n.id == id)
})
.take(5)
.collect();
if !connected.is_empty() {
println!("\n Connected to '{}':", hits[0].name);
for n in connected {
println!(" → {} ({})", n.name, n.file_path);
}
}
}
println!();
}
pub fn cmd_onboard(data: &GraphData, out_file: Option<&str>) -> Result<()> {
let mut md = String::new();
md.push_str(&format!("# {} — Onboarding Guide\n\n", data.meta.project));
md.push_str(&format!(
"> Generated by `yana-rt graph onboard` · {} files · {} edges\n\n",
data.nodes.iter().filter(|n| n.node_type == "file").count(),
data.edges.len()
));
md.push_str("## Project Overview\n\n");
md.push_str(&format!(
"**Languages:** {} \n**Frameworks:** {} \n**Analysed:** {}\n\n",
data.meta.languages.join(", "),
if data.meta.frameworks.is_empty() { "-".to_string() } else { data.meta.frameworks.join(", ") },
&data.meta.analysed_at[..10]
));
let layers = count_layers(&data.nodes);
md.push_str("## Architecture\n\n");
let mut lv: Vec<_> = layers.iter().collect();
lv.sort_by(|a, b| b.1.cmp(a.1));
for (layer, count) in lv {
md.push_str(&format!("- **{}**: {} nodes\n", layer, count));
}
md.push_str("\n## Guided Tour\n\n");
md.push_str("Start here to understand the codebase:\n\n");
for step in &data.tour {
md.push_str(&format!(
"### {}. `{}` ({})\n\n- Path: `{}`\n- Layer: {}\n- Reason: {}\n\n",
step.order, step.name, step.language,
step.file_path, step.layer, step.reason
));
}
let mut degree: Vec<(&str, usize)> = {
let mut map: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for e in &data.edges {
*map.entry(e.target.as_str()).or_default() += 1;
*map.entry(e.source.as_str()).or_default() += 1;
}
map.into_iter().collect()
};
degree.sort_by(|a, b| b.1.cmp(&a.1));
md.push_str("## Most Connected Files\n\n");
for (id, deg) in degree.iter().take(10) {
if let Some(n) = data.nodes.iter().find(|n| n.id == *id) {
md.push_str(&format!("- `{}` — {} connections\n", n.file_path, deg));
}
}
if let Some(path) = out_file {
std::fs::write(path, &md)?;
println!("[graph] onboarding guide → {}", path);
} else {
print!("{}", md);
}
Ok(())
}
pub fn cmd_diff(data: &GraphData, base: &str, target: &str) -> Result<()> {
use std::process::Command;
let out = Command::new("git")
.args(["diff", "--name-only", base])
.current_dir(target)
.output()?;
if !out.status.success() {
anyhow::bail!("git diff failed: {}", String::from_utf8_lossy(&out.stderr));
}
let changed: Vec<&str> = std::str::from_utf8(&out.stdout)?
.lines().collect();
if changed.is_empty() {
println!("No changed files vs {}", base);
return Ok(());
}
println!("\n Changed files (vs {}):\n", base);
for f in &changed {
println!(" ✎ {}", f);
}
let changed_ids: Vec<String> = changed.iter()
.map(|f| format!("file:{}", f)).collect();
let mut impacted: Vec<&str> = data.edges.iter()
.filter(|e| changed_ids.contains(&e.target))
.filter_map(|e| data.nodes.iter().find(|n| n.id == e.source))
.map(|n| n.file_path.as_str())
.collect();
impacted.dedup();
if !impacted.is_empty() {
println!("\n Impact (files that import changed files):\n");
for f in &impacted {
println!(" → {}", f);
}
}
println!();
Ok(())
}
fn count_layers(nodes: &[Node]) -> std::collections::HashMap<String, usize> {
let mut map: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for n in nodes {
let layer = crate::graph::types::layer_from_path(&n.file_path).to_string();
*map.entry(layer).or_default() += 1;
}
map
}