use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use rusqlite::{Connection, params};
#[derive(Debug, Clone)]
pub struct GraphQuery;
#[derive(Debug, Clone)]
pub struct ImpactResult {
pub root_file: String,
pub affected_files: Vec<String>,
pub weak_files: Vec<String>,
pub max_depth_reached: usize,
pub edges_traversed: usize,
}
#[derive(Debug, Clone)]
pub struct DependencyChain {
pub path: Vec<String>,
pub depth: usize,
}
pub type Served<'a> = &'a dyn Fn(&str) -> bool;
const STRUCTURAL_EDGE_KINDS: &str = "'imports','calls','references','implements','extends','exports','type_ref','tested_by','module','cochange','sibling'";
pub fn edge_weight(kind: &str) -> f64 {
match kind {
"imports" => 1.0,
"calls" | "references" => 0.8,
"exports" | "implements" | "extends" => 0.7,
"module" => 0.6,
"type_ref" => 0.5,
"tested_by" => 0.4,
"cochange" => 0.35,
"defines" => 0.3,
"sibling" => 0.25,
"changed_in" => 0.2,
_ => 0.1,
}
}
pub(super) fn dependents(conn: &Connection, file_path: &str) -> anyhow::Result<Vec<String>> {
let sql = format!(
"SELECT DISTINCT p_src.path
FROM edges e
JOIN nodes n_src ON e.source_id = n_src.id
JOIN nodes n_tgt ON e.target_id = n_tgt.id
JOIN paths p_src ON p_src.id = n_src.file_id
JOIN paths p_tgt ON p_tgt.id = n_tgt.file_id
WHERE p_tgt.path = ?1
AND p_src.path != ?1
AND e.kind IN ({STRUCTURAL_EDGE_KINDS})"
);
let mut stmt = conn.prepare(&sql)?;
let mut results: Vec<String> = stmt
.query_map(params![file_path], |row| row.get(0))?
.filter_map(std::result::Result::ok)
.collect();
results.sort();
results.dedup();
Ok(results)
}
pub(super) fn dependencies(conn: &Connection, file_path: &str) -> anyhow::Result<Vec<String>> {
let sql = format!(
"SELECT DISTINCT p_tgt.path
FROM edges e
JOIN nodes n_src ON e.source_id = n_src.id
JOIN nodes n_tgt ON e.target_id = n_tgt.id
JOIN paths p_src ON p_src.id = n_src.file_id
JOIN paths p_tgt ON p_tgt.id = n_tgt.file_id
WHERE p_src.path = ?1
AND p_tgt.path != ?1
AND e.kind IN ({STRUCTURAL_EDGE_KINDS})"
);
let mut stmt = conn.prepare(&sql)?;
let mut results: Vec<String> = stmt
.query_map(params![file_path], |row| row.get(0))?
.filter_map(std::result::Result::ok)
.collect();
results.sort();
results.dedup();
Ok(results)
}
pub(super) fn impact_analysis(
conn: &Connection,
file_path: &str,
max_depth: usize,
served: Served<'_>,
) -> anyhow::Result<ImpactResult> {
let file_path = file_path.replace('\\', "/");
let file_path = file_path.as_str();
if !served(file_path) {
return Ok(ImpactResult {
root_file: file_path.to_string(),
affected_files: Vec::new(),
weak_files: Vec::new(),
max_depth_reached: 0,
edges_traversed: 0,
});
}
let reverse_graph = build_weighted_reverse_graph(conn)?;
let all = propagate(&reverse_graph, file_path, max_depth, false, served);
let strong = propagate(&reverse_graph, file_path, max_depth, true, served);
let weak_files: Vec<String> = all.reached.difference(&strong.reached).cloned().collect();
Ok(ImpactResult {
root_file: file_path.to_string(),
affected_files: all.reached.into_iter().collect(),
weak_files,
max_depth_reached: all.max_depth_reached,
edges_traversed: all.edges_traversed,
})
}
struct Propagation {
reached: BTreeSet<String>,
max_depth_reached: usize,
edges_traversed: usize,
}
fn propagate(
reverse_graph: &ReverseGraph,
root: &str,
max_depth: usize,
strong_only: bool,
served: Served<'_>,
) -> Propagation {
const PROPAGATION_THRESHOLD: f64 = 0.1;
let mut best: HashMap<&str, f64> = HashMap::from([(root, 1.0)]);
let mut first_depth: BTreeMap<&str, usize> = BTreeMap::new();
let mut layer: BTreeMap<&str, f64> = BTreeMap::from([(root, 1.0)]);
let mut edges_traversed = 0;
for depth in 1..=max_depth {
let mut next: BTreeMap<&str, f64> = BTreeMap::new();
for (file, weight) in &layer {
for dep in reverse_graph.get(*file).into_iter().flatten() {
let Some(edge_weight) = (if strong_only {
dep.strong_weight
} else {
Some(dep.weight)
}) else {
continue;
};
if !served(dep.file.as_str()) {
continue;
}
edges_traversed += 1;
let propagated = weight * edge_weight;
if propagated < PROPAGATION_THRESHOLD
|| best
.get(dep.file.as_str())
.is_some_and(|b| *b >= propagated)
{
continue;
}
let slot = next.entry(dep.file.as_str()).or_insert(0.0);
*slot = slot.max(propagated);
}
}
if next.is_empty() {
break;
}
for (file, weight) in &next {
best.insert(file, *weight);
first_depth.entry(file).or_insert(depth);
}
layer = next;
}
first_depth.remove(root);
Propagation {
max_depth_reached: first_depth.values().copied().max().unwrap_or(0),
reached: first_depth.into_keys().map(str::to_string).collect(),
edges_traversed,
}
}
pub(super) fn dependency_chain(
conn: &Connection,
from: &str,
to: &str,
served: Served<'_>,
) -> anyhow::Result<Option<DependencyChain>> {
let from = from.replace('\\', "/");
let to = to.replace('\\', "/");
let from = from.as_str();
let to = to.as_str();
if !served(from) {
return Ok(None);
}
let forward_graph = build_forward_graph(conn)?;
let mut visited: HashSet<String> = HashSet::new();
let mut parent: HashMap<String, String> = HashMap::new();
let mut queue: VecDeque<String> = VecDeque::new();
visited.insert(from.to_string());
queue.push_back(from.to_string());
while let Some(current) = queue.pop_front() {
if current == to {
let mut path = vec![to.to_string()];
let mut cursor = to.to_string();
while let Some(prev) = parent.get(&cursor) {
path.push(prev.clone());
cursor = prev.clone();
}
path.reverse();
let depth = path.len() - 1;
return Ok(Some(DependencyChain { path, depth }));
}
if let Some(deps) = forward_graph.get(¤t) {
for dep in deps {
if !visited.contains(dep) && served(dep) && visited.insert(dep.clone()) {
parent.insert(dep.clone(), current.clone());
queue.push_back(dep.clone());
}
}
}
}
Ok(None)
}
pub fn related_files(
conn: &Connection,
file_path: &str,
limit: usize,
served: Served<'_>,
) -> anyhow::Result<Vec<(String, f64)>> {
if !served(file_path) {
return Ok(Vec::new());
}
let sql = format!(
"SELECT p_other.path, e.kind, e.metadata
FROM edges e
JOIN nodes n_self ON (e.source_id = n_self.id OR e.target_id = n_self.id)
JOIN nodes n_other ON (
(e.source_id = n_other.id AND e.target_id = n_self.id)
OR (e.target_id = n_other.id AND e.source_id = n_self.id)
)
JOIN paths p_self ON p_self.id = n_self.file_id
JOIN paths p_other ON p_other.id = n_other.file_id
WHERE p_self.path = ?1
AND p_other.path != ?1
AND e.kind IN ({STRUCTURAL_EDGE_KINDS})"
);
let mut stmt = conn.prepare(&sql)?;
let mut strongest: BTreeMap<(String, String), f64> = BTreeMap::new();
let rows = stmt.query_map(params![file_path], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
))
})?;
for row in rows {
let (path, kind, metadata) = row?;
let w = evidence_weight(&kind, metadata.as_deref());
let slot = strongest.entry((path, kind)).or_insert(0.0);
if w > *slot {
*slot = w;
}
}
let mut scores: BTreeMap<String, f64> = BTreeMap::new();
for ((path, _), w) in strongest {
*scores.entry(path).or_default() += w;
}
let mut results: Vec<(String, f64)> = scores.into_iter().collect();
results.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
Ok(results
.into_iter()
.filter(|(path, _)| served(path))
.take(limit)
.collect())
}
pub(super) fn evidence_weight(kind: &str, metadata: Option<&str>) -> f64 {
edge_weight(kind) * crate::core::semantic::EdgeEvidence::weight_factor_of(metadata)
}
pub fn file_connectivity(
conn: &Connection,
file_path: &str,
served: Served<'_>,
) -> anyhow::Result<HashMap<String, (usize, usize)>> {
let mut result: HashMap<String, (usize, usize)> = HashMap::new();
if !served(file_path) {
return Ok(result);
}
for (outgoing, own, other) in [
(true, "source_id", "target_id"),
(false, "target_id", "source_id"),
] {
let mut stmt = conn.prepare(&format!(
"SELECT e.kind, po.path
FROM edges e
JOIN nodes n ON e.{own} = n.id
JOIN paths p ON p.id = n.file_id
JOIN nodes o ON e.{other} = o.id
JOIN paths po ON po.id = o.file_id
WHERE p.path = ?1"
))?;
let rows = stmt.query_map(params![file_path], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
for row in rows {
let (kind, other_path) = row?;
if !(other_path.is_empty() || other_path == file_path || served(&other_path)) {
continue;
}
let slot = result.entry(kind).or_insert((0, 0));
if outgoing {
slot.0 += 1;
} else {
slot.1 += 1;
}
}
}
Ok(result)
}
struct Dependent {
file: String,
weight: f64,
strong_weight: Option<f64>,
}
type ReverseGraph = BTreeMap<String, Vec<Dependent>>;
fn build_weighted_reverse_graph(conn: &Connection) -> anyhow::Result<ReverseGraph> {
let sql = format!(
"SELECT p_tgt.path, p_src.path, e.kind, e.metadata
FROM edges e
JOIN nodes n_src ON e.source_id = n_src.id
JOIN nodes n_tgt ON e.target_id = n_tgt.id
JOIN paths p_src ON p_src.id = n_src.file_id
JOIN paths p_tgt ON p_tgt.id = n_tgt.file_id
WHERE e.kind IN ({STRUCTURAL_EDGE_KINDS})
AND p_src.path != p_tgt.path"
);
let mut stmt = conn.prepare(&sql)?;
let mut graph: BTreeMap<String, BTreeMap<String, (f64, Option<f64>)>> = BTreeMap::new();
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, Option<String>>(3)?,
))
})?;
for row in rows {
let (target, source, kind, metadata) = row?;
let w = evidence_weight(&kind, metadata.as_deref());
let strong = crate::core::semantic::EdgeEvidence::from_metadata(metadata.as_deref())
.is_none_or(|e| e.grade != crate::core::semantic::EvidenceGrade::HeuristicStructural);
let entry = graph
.entry(target)
.or_default()
.entry(source)
.or_insert((0.0, None));
entry.0 = entry.0.max(w);
if strong {
entry.1 = Some(entry.1.map_or(w, |s| s.max(w)));
}
}
Ok(graph
.into_iter()
.map(|(k, v)| {
(
k,
v.into_iter()
.map(|(file, (weight, strong_weight))| Dependent {
file,
weight,
strong_weight,
})
.collect(),
)
})
.collect())
}
fn build_forward_graph(conn: &Connection) -> anyhow::Result<HashMap<String, Vec<String>>> {
let sql = format!(
"SELECT DISTINCT p_src.path, p_tgt.path
FROM edges e
JOIN nodes n_src ON e.source_id = n_src.id
JOIN nodes n_tgt ON e.target_id = n_tgt.id
JOIN paths p_src ON p_src.id = n_src.file_id
JOIN paths p_tgt ON p_tgt.id = n_tgt.file_id
WHERE e.kind IN ({STRUCTURAL_EDGE_KINDS})
AND p_src.path != p_tgt.path"
);
let mut stmt = conn.prepare(&sql)?;
let mut graph: HashMap<String, Vec<String>> = HashMap::new();
let rows = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
for row in rows {
let (source, target) = row?;
graph.entry(source).or_default().push(target);
}
for deps in graph.values_mut() {
deps.sort();
deps.dedup();
}
Ok(graph)
}