use std::path::Path;
use anyhow::Result;
use serde_json::{Value, json};
use sinter_resolve::qualified_of;
use crate::graph_tool::{
affected_json, affected_options, by_file, limit, required_string, scoped_node_json,
traversal_filter,
};
use crate::lookup::{Found, ensure_snapshot, find_symbol, open_current, unique_symbol};
pub(crate) fn call(repo: &Path, name: &str, args: &Value) -> Result<Value> {
if name == "impact" {
let report = crate::impact::compute_current(repo, &required_string(args, "rev_range")?)?;
return Ok(crate::impact::to_json(&report));
}
if name == "ask" {
let limit = limit(args, 5);
let scopes = crate::corpus::ScopeSelection::from_json(
args,
crate::corpus::ScopeSelection::agent_default(),
)?;
return crate::ask::ask_response_json_current(
repo,
&required_string(args, "question")?,
limit,
&scopes,
);
}
if name == "overlap" {
let ranges: Vec<String> = args
.get("ranges")
.and_then(Value::as_array)
.map(|values| {
values
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect()
})
.unwrap_or_default();
let (maps, pairs) = crate::overlap::compute_current(repo, &ranges)?;
return Ok(crate::overlap::to_json(&maps, &pairs));
}
let store = &open_current(repo)?;
let snapshot = matches!(name, "show" | "query" | "affected" | "deps" | "path")
.then(|| ensure_snapshot(store, args.get("if_snapshot").and_then(Value::as_str)))
.transpose()?;
let mut result = match name {
"map" => map(repo, store, args),
"unresolved" => {
let optional = |key: &str| {
args.get(key)
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
};
let limit = limit(args, 50);
let refs = store.unresolved_details(optional("file"), optional("name"))?;
let root = crate::pipeline::discover_root(repo);
let classifier = crate::coverage::Classifier::new(&root, store, &refs)?;
Ok(crate::unresolved::to_json(&root, &classifier, &refs, limit))
}
"show" => {
let node = unique_symbol(store, &required_string(args, "symbol")?)?;
let scopes = store.file_scopes()?;
let edge_json = |edge: &sinter_core::Edge, other: &sinter_core::NodeId| {
json!({
"symbol": qualified_of(other.as_str()),
"relation": edge.relation.as_str(),
"evidence": edge.evidence.as_str(),
"site": crate::render::site_json(repo, edge),
})
};
let outgoing: Vec<Value> = store
.out_edges(&node.id)?
.iter()
.map(|edge| edge_json(edge, &edge.dst))
.collect();
let incoming: Vec<Value> = store
.in_edges(&node.id)?
.iter()
.map(|edge| edge_json(edge, &edge.src))
.collect();
Ok(json!({
"symbol": scoped_node_json(&node, &scopes),
"outgoing": outgoing,
"incoming": incoming,
}))
}
"query" => {
let limit = limit(args, 10);
let selection = crate::corpus::ScopeSelection::from_json(
args,
crate::corpus::ScopeSelection::all(),
)?;
let (resolution, exact, mut nodes) =
match find_symbol(store, &required_string(args, "symbol")?)? {
Found::Exact(nodes) => ("exact", true, nodes),
Found::Relocated(nodes) => ("relocated", false, nodes),
Found::Suggestions(nodes) => ("suggestions", false, nodes),
};
let scopes = store.file_scopes()?;
nodes.retain(|node| {
selection.contains(
scopes
.get(&node.file)
.copied()
.unwrap_or_else(|| sinter_core::CorpusScope::classify_path(&node.file)),
)
});
Ok(json!({
"resolution": resolution,
"exact": exact,
"scope": selection.json(),
"results": nodes.iter().take(limit).map(|node| scoped_node_json(node, &scopes)).collect::<Vec<_>>(),
}))
}
"affected" => affected(repo, store, args),
"deps" => dependencies(repo, store, args),
"path" => path(repo, store, args),
other => anyhow::bail!("unknown tool {other}"),
}?;
if let Some(snapshot) = snapshot {
if name == "affected"
&& args.get("symbols").is_some()
&& let Some(results) = result.get_mut("results").and_then(Value::as_array_mut)
{
for item in results {
item["snapshot"] = json!(snapshot);
}
}
result["snapshot"] = json!(snapshot);
}
Ok(result)
}
fn affected(repo: &Path, store: &sinter_store::Store, args: &Value) -> Result<Value> {
let filter = traversal_filter(args)?;
let depth = args.get("max_depth").and_then(Value::as_u64).unwrap_or(10) as usize;
let (limit, detail) = affected_options(args);
let one = |symbol: &str| affected_one(store, repo, symbol, &filter, depth, limit, detail);
if let Some(symbols) = args.get("symbols").and_then(Value::as_array) {
let results: Vec<Value> = symbols
.iter()
.map(|value| {
let Some(symbol) = value.as_str() else {
return json!({"symbol": value, "error": "symbols entries must be strings"});
};
one(symbol).unwrap_or_else(
|error| json!({"symbol": symbol, "error": format!("{error:#}")}),
)
})
.collect();
return Ok(json!({"results": results}));
}
one(&required_string(args, "symbol")?)
}
fn affected_one(
store: &sinter_store::Store,
repo: &Path,
symbol: &str,
filter: &sinter_store::EdgeFilter,
depth: usize,
limit: usize,
detail: bool,
) -> Result<Value> {
let node = match unique_symbol(store, symbol) {
Ok(node) => node,
Err(error) if error.is::<crate::lookup::NoMatch>() => {
let sites = crate::lookup::external_sites(store, symbol)?;
if sites.is_empty() {
return Err(error);
}
let unresolved = sites.iter().map(|site| site.refs).sum();
let mut out = json!({
"external": true,
"note": "symbol is not defined in this repo; sites reference it (dependency blast radius at the repo boundary)",
"sites": sites.iter().map(|site| json!({
"enclosing": site.enclosing,
"file": site.file,
"refs": site.refs,
})).collect::<Vec<_>>(),
});
out["coverage"] = crate::coverage::traversal_json(
repo,
store,
filter,
crate::coverage::TraversalEvidence {
unresolved,
..Default::default()
},
true,
)?;
return Ok(out);
}
Err(error) => return Err(error),
};
let reached = store.dependents(&node.id, filter, depth)?;
let scopes = store.file_scopes()?;
let unresolved = store.unresolved_named(&node.name)?;
let evidence = crate::coverage::TraversalEvidence::from_confidences(
reached.iter().map(|item| item.via.confidence),
unresolved,
);
let entries: Vec<Value> = reached
.iter()
.take(limit)
.map(|reached| {
if detail {
json!({
"node": scoped_node_json(&reached.node, &scopes),
"depth": reached.depth,
"relation": reached.via.relation.as_str(),
"evidence": reached.via.evidence.as_str(),
"confidence": match reached.via.confidence {
sinter_core::Confidence::Certain => "certain",
sinter_core::Confidence::Inferred => "possible",
},
"site": crate::render::site_json(repo, &reached.via),
})
} else {
let mut entry = json!({
"s": qualified_of(reached.node.id.as_str()),
"k": reached.node.kind.as_str(),
"f": reached.node.file,
"scope": scopes.get(&reached.node.file).copied()
.unwrap_or_else(|| sinter_core::CorpusScope::classify_path(&reached.node.file)).as_str(),
"e": format!("{}/{}", reached.via.relation.as_str(), reached.via.evidence.as_str()),
"c": match reached.via.confidence {
sinter_core::Confidence::Certain => "certain",
sinter_core::Confidence::Inferred => "possible",
},
"d": reached.depth,
});
let site = crate::render::site_json(repo, &reached.via);
if !site.is_null() {
entry["site"] = site;
}
entry
}
})
.collect();
let mut out = affected_json(
scoped_node_json(&node, &scopes),
json!(unresolved),
Some(crate::pipeline::scip_index_path(repo).is_some()),
entries,
by_file(reached.iter().map(|reached| reached.node.file.clone())),
{
let (direct, files) = sinter_store::direct_summary(&reached);
(reached.len(), direct, files)
},
limit,
);
out["coverage"] =
crate::coverage::traversal_json(repo, store, filter, evidence, !reached.is_empty())?;
Ok(out)
}
fn dependencies(repo: &Path, store: &sinter_store::Store, args: &Value) -> Result<Value> {
let filter = traversal_filter(args)?;
let depth = args.get("max_depth").and_then(Value::as_u64).unwrap_or(10) as usize;
let limit = limit(args, 50);
let node = unique_symbol(store, &required_string(args, "symbol")?)?;
let reached = store.dependencies(&node.id, &filter, depth)?;
let scopes = store.file_scopes()?;
let unresolved = store
.references_in(&node.file)?
.iter()
.filter(|reference| reference.enclosing.as_ref() == Some(&node.id))
.count();
let evidence = crate::coverage::TraversalEvidence::from_confidences(
reached.iter().map(|item| item.via.confidence),
unresolved,
);
let entries: Vec<Value> = reached
.iter()
.take(limit)
.map(|reached| {
let mut entry = json!({
"s": qualified_of(reached.node.id.as_str()),
"k": reached.node.kind.as_str(),
"f": reached.node.file,
"scope": scopes.get(&reached.node.file).copied()
.unwrap_or_else(|| sinter_core::CorpusScope::classify_path(&reached.node.file)).as_str(),
"e": format!("{}/{}", reached.via.relation.as_str(), reached.via.evidence.as_str()),
"c": match reached.via.confidence {
sinter_core::Confidence::Certain => "certain",
sinter_core::Confidence::Inferred => "possible",
},
"d": reached.depth,
});
let site = crate::render::site_json(repo, &reached.via);
if !site.is_null() {
entry["site"] = site;
}
entry
})
.collect();
let mut out = json!({
"symbol": scoped_node_json(&node, &scopes),
"total": reached.len(),
"unresolved_refs_in_symbol": unresolved,
"by_file": by_file(reached.iter().map(|reached| reached.node.file.clone())),
"dependencies": entries,
});
if reached.len() > limit {
out["truncated"] = json!(reached.len() - limit);
}
out["coverage"] =
crate::coverage::traversal_json(repo, store, &filter, evidence, !reached.is_empty())?;
Ok(out)
}
fn path(repo: &Path, store: &sinter_store::Store, args: &Value) -> Result<Value> {
let filter = traversal_filter(args)?;
let from = unique_symbol(store, &required_string(args, "from")?)?;
let to = unique_symbol(store, &required_string(args, "to")?)?;
let path = store.shortest_path(&from.id, &to.id, &filter)?;
let scopes = store.file_scopes()?;
let scope_of_id = |id: &sinter_core::NodeId| {
let file = id
.as_str()
.split_once('#')
.map_or(id.as_str(), |(file, _)| file);
scopes
.get(file)
.copied()
.unwrap_or_else(|| sinter_core::CorpusScope::classify_path(file))
};
let evidence = crate::coverage::TraversalEvidence::from_confidences(
path.iter().flatten().map(|edge| edge.confidence),
0,
);
let mut out = json!({
"found": path.is_some(),
"steps": path.iter().flatten().map(|edge| json!({
"from": qualified_of(edge.src.as_str()),
"to": qualified_of(edge.dst.as_str()),
"from_scope": scope_of_id(&edge.src).as_str(),
"to_scope": scope_of_id(&edge.dst).as_str(),
"relation": edge.relation.as_str(),
"evidence": edge.evidence.as_str(),
"confidence": match edge.confidence {
sinter_core::Confidence::Certain => "certain",
sinter_core::Confidence::Inferred => "possible",
},
"site": crate::render::site_json(repo, edge),
})).collect::<Vec<_>>(),
});
if path.is_none() {
let miss = crate::pathcmd::explain_miss(store, &from, &to, &filter)?;
out["miss"] = crate::pathcmd::miss_json(repo, &miss);
}
out["coverage"] =
crate::coverage::traversal_json(repo, store, &filter, evidence, path.is_some())?;
Ok(out)
}
fn map(repo: &Path, store: &sinter_store::Store, args: &Value) -> Result<Value> {
let selection = crate::corpus::ScopeSelection::from_json(
args,
crate::corpus::ScopeSelection::agent_default(),
)?;
crate::map::response(repo, store, &selection)
}