use anyhow::{bail, Result};
use clap::Args;
use serde::Serialize;
use std::path::Path;
use crate::commands::entrypoints::EntrypointInfo;
use crate::config::workspace::WorkspaceConfig;
use crate::core::graph::Graph;
use crate::core::workspace_graph::WorkspaceGraph;
use crate::output::formatter;
use crate::output::json::JsonOutput;
use crate::Context;
#[derive(Args, Debug)]
pub struct MapArgs {
#[arg(long, default_value = "10")]
pub limit: usize,
#[arg(long, short = 'j')]
pub json: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Debug, Serialize)]
pub struct MapStats {
pub file_count: usize,
pub symbol_count: usize,
pub edge_count: usize,
pub languages: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct CoreSymbol {
pub name: String,
pub kind: String,
pub file_path: String,
pub caller_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct DirStats {
pub directory: String,
pub file_count: usize,
pub symbol_count: usize,
}
#[derive(Debug, Serialize)]
pub struct MapData {
pub stats: MapStats,
pub entrypoints: Vec<(String, Vec<EntrypointInfo>)>,
pub core_symbols: Vec<CoreSymbol>,
pub architecture: Vec<DirStats>,
}
pub fn run(args: &MapArgs, ctx: &Context) -> Result<()> {
match ctx {
Context::SingleProject { root } => run_single(args, root),
Context::Workspace {
workspace_root,
config,
..
} => run_workspace(args, workspace_root, config),
}
}
fn run_single(args: &MapArgs, project_root: &Path) -> Result<()> {
let scope_dir = project_root.join(".scope");
if !scope_dir.exists() {
bail!("No .scope/ directory found. Run 'scope init' first.");
}
let db_path = scope_dir.join("graph.db");
if !db_path.exists() {
bail!("No index found. Run 'scope index' to build one first.");
}
let graph = Graph::open(&db_path)?;
crate::commands::warn_if_stale(&graph, project_root);
let stats = MapStats {
file_count: graph.file_count()?,
symbol_count: graph.symbol_count()?,
edge_count: graph.edge_count()?,
languages: graph.get_languages()?,
};
let raw_entrypoints = graph.get_entrypoints()?;
let (ep_groups, ep_total, _ep_file_count) =
crate::commands::entrypoints::collapse_and_group(&raw_entrypoints, &graph);
let raw_core = graph.get_symbols_by_importance(args.limit)?;
let core_symbols: Vec<CoreSymbol> = raw_core
.into_iter()
.map(|(sym, count)| CoreSymbol {
name: sym.name,
kind: sym.kind,
file_path: sym.file_path,
caller_count: count,
project: None,
})
.collect();
let raw_dirs = graph.get_directory_stats()?;
let architecture: Vec<DirStats> = raw_dirs
.into_iter()
.map(|(dir, files, symbols)| DirStats {
directory: dir,
file_count: files,
symbol_count: symbols,
})
.collect();
let project_name = project_root
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
if args.json || args.compact {
let ep_data = if args.compact {
ep_groups
.into_iter()
.map(|(cat, entries)| {
let truncated: Vec<_> = entries.into_iter().take(10).collect();
(cat, truncated)
})
.collect()
} else {
ep_groups
};
let data = MapData {
stats,
entrypoints: ep_data,
core_symbols,
architecture,
};
let output = JsonOutput {
command: "map",
symbol: None,
data: &data,
truncated: false,
total: ep_total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_map(
project_name,
&stats,
&ep_groups,
&core_symbols,
&architecture,
);
}
Ok(())
}
fn run_workspace(args: &MapArgs, workspace_root: &Path, config: &WorkspaceConfig) -> Result<()> {
let members: Vec<(String, std::path::PathBuf)> = config
.workspace
.members
.iter()
.map(|entry| {
let name = WorkspaceConfig::resolve_member_name(entry);
let path = workspace_root.join(&entry.path);
(name, path)
})
.collect();
let wg = WorkspaceGraph::open(members)?;
let stats = MapStats {
file_count: wg.file_count(),
symbol_count: wg.symbol_count(),
edge_count: wg.edge_count(),
languages: wg.get_languages(),
};
let mut ep_groups: Vec<(String, Vec<EntrypointInfo>)> = Vec::new();
let mut ep_total = 0usize;
for member in wg.members() {
let raw = member.graph.get_entrypoints().unwrap_or_default();
let (member_groups, member_total, _) =
crate::commands::entrypoints::collapse_and_group(&raw, &member.graph);
ep_total += member_total;
for (group_name, mut entries) in member_groups {
for info in &mut entries {
info.name = format!("{}::{}", member.name, info.name);
}
ep_groups.push((group_name, entries));
}
}
let per_member_limit = args.limit.saturating_mul(2);
let mut all_core: Vec<CoreSymbol> = Vec::new();
for member in wg.members() {
match member.graph.get_symbols_by_importance(per_member_limit) {
Ok(symbols) => {
for (sym, count) in symbols {
all_core.push(CoreSymbol {
name: sym.name,
kind: sym.kind,
file_path: sym.file_path,
caller_count: count,
project: Some(member.name.clone()),
});
}
}
Err(e) => {
tracing::warn!("Error getting core symbols from '{}': {}", member.name, e);
}
}
}
all_core.sort_by(|a, b| b.caller_count.cmp(&a.caller_count));
all_core.truncate(args.limit);
let mut architecture: Vec<DirStats> = Vec::new();
for member in wg.members() {
match member.graph.get_directory_stats() {
Ok(dirs) => {
for (dir, files, symbols) in dirs {
architecture.push(DirStats {
directory: format!("{}/{}", member.name, dir),
file_count: files,
symbol_count: symbols,
});
}
}
Err(e) => {
tracing::warn!(
"Error getting directory stats from '{}': {}",
member.name,
e
);
}
}
}
architecture.sort_by(|a, b| b.symbol_count.cmp(&a.symbol_count));
if args.json || args.compact {
let data = MapData {
stats,
entrypoints: ep_groups,
core_symbols: all_core,
architecture,
};
let output = JsonOutput {
command: "map",
symbol: None,
data: &data,
truncated: false,
total: ep_total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_map(
&config.workspace.name,
&stats,
&ep_groups,
&all_core,
&architecture,
);
}
Ok(())
}