use crate::core::graph_index::IndexEdge;
const DIRECTORY_MATCH_FACTOR: f32 = 0.9;
#[derive(Debug, Clone, serde::Serialize)]
pub struct CrossSourceHint {
pub source_uri: String,
pub relation: String,
pub weight: f32,
}
pub fn hints_for_file(
file_path: &str,
edges: &[IndexEdge],
project_root: &str,
) -> Vec<CrossSourceHint> {
hints_for_file_matching(file_path, edges, project_root, |_| true)
}
pub fn hints_for_file_matching(
file_path: &str,
edges: &[IndexEdge],
project_root: &str,
mut include: impl FnMut(&CrossSourceHint) -> bool,
) -> Vec<CrossSourceHint> {
let rel = crate::core::graph_index::graph_relative_key(file_path, project_root);
let path_weight = |edge_path: &str| -> Option<f32> {
if edge_path == file_path || edge_path == rel {
Some(1.0)
} else if !edge_path.is_empty()
&& !edge_path.contains("://")
&& rel
.replace('\\', "/")
.strip_prefix(edge_path.replace('\\', "/").as_str())
.is_some_and(|rest| rest.starts_with('/'))
{
Some(DIRECTORY_MATCH_FACTOR)
} else {
None
}
};
let mut hints: Vec<CrossSourceHint> = edges
.iter()
.filter_map(|e| {
if is_external_uri(&e.to)
&& let Some(factor) = path_weight(&e.from)
{
Some(CrossSourceHint {
source_uri: e.to.clone(),
relation: e.kind.clone(),
weight: e.weight * factor,
})
} else if is_external_uri(&e.from)
&& let Some(factor) = path_weight(&e.to)
{
Some(CrossSourceHint {
source_uri: e.from.clone(),
relation: e.kind.clone(),
weight: e.weight * factor,
})
} else {
None
}
})
.collect();
hints.retain(|hint| include(hint));
hints.sort_by(|a, b| {
b.weight
.partial_cmp(&a.weight)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.source_uri.cmp(&b.source_uri))
.then_with(|| a.relation.cmp(&b.relation))
});
let mut seen = std::collections::HashSet::new();
hints.retain(|hint| seen.insert(hint.source_uri.clone()));
hints.truncate(5);
hints
}
pub fn format_hints(hints: &[CrossSourceHint]) -> String {
if hints.is_empty() {
return String::new();
}
let mut out = String::from("\n--- Cross-Source Hints ---\n");
for hint in hints {
out.push_str(&format!(
" {} [{}] w={:.1}\n",
hint.source_uri, hint.relation, hint.weight
));
}
out
}
fn is_external_uri(path: &str) -> bool {
path.contains("://")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::graph_index::IndexEdge;
fn edge(from: &str, to: &str, kind: &str, weight: f32) -> IndexEdge {
IndexEdge {
from: from.into(),
to: to.into(),
kind: kind.into(),
weight,
}
}
const ROOT: &str = "/project";
#[test]
fn a_directory_reference_hints_files_below_it_after_exact_ones() {
let edges = vec![
edge("emspace://pages/core", "src/core", "documents", 1.0),
edge(
"emspace://pages/core/auth",
"src/core/auth.rs",
"documents",
1.0,
),
edge("emspace://pages/corelib", "src/corelib", "documents", 1.0),
];
let hints = hints_for_file("src/core/auth.rs", &edges, ROOT);
let uris: Vec<&str> = hints.iter().map(|h| h.source_uri.as_str()).collect();
assert_eq!(
uris,
vec!["emspace://pages/core/auth", "emspace://pages/core"]
);
assert_eq!(hints_for_file("src/core", &edges, ROOT).len(), 1);
}
#[test]
fn finds_hints_from_forward_edges() {
let edges = vec![
edge("src/auth.rs", "github://issues/42", "mentions", 1.0),
edge("src/auth.rs", "postgres://schemas/sessions", "queries", 1.2),
];
let hints = hints_for_file("src/auth.rs", &edges, ROOT);
assert_eq!(hints.len(), 2);
assert!(hints.iter().any(|h| h.source_uri.contains("issues/42")));
assert!(
hints
.iter()
.any(|h| h.source_uri.contains("schemas/sessions"))
);
}
#[test]
fn finds_hints_from_reverse_edges() {
let edges = vec![edge(
"github://issues/42",
"src/auth.rs",
"mentioned_in",
0.8,
)];
let hints = hints_for_file("src/auth.rs", &edges, ROOT);
assert_eq!(hints.len(), 1);
assert!(hints[0].source_uri.contains("issues/42"));
}
#[test]
fn finds_hints_with_absolute_path() {
let edges = vec![edge("src/auth.rs", "github://issues/42", "mentions", 1.0)];
let hints = hints_for_file("/project/src/auth.rs", &edges, "/project");
assert_eq!(hints.len(), 1, "absolute path should match relative edge");
}
#[test]
fn ignores_code_to_code_edges() {
let edges = vec![edge("src/auth.rs", "src/db.rs", "imports", 1.0)];
let hints = hints_for_file("src/auth.rs", &edges, ROOT);
assert!(hints.is_empty());
}
#[test]
fn deduplicates_and_limits_to_5() {
let edges: Vec<IndexEdge> = (0..10)
.map(|i| {
edge(
"src/auth.rs",
&format!("github://issues/{i}"),
"mentions",
1.0,
)
})
.collect();
let hints = hints_for_file("src/auth.rs", &edges, ROOT);
assert_eq!(hints.len(), 5);
}
#[test]
fn hint_output_is_byte_stable_for_the_same_graph_in_any_row_order() {
let edges = vec![
edge("emspace://pages/core", "src/a.rs", "documents", 0.8),
edge("src/a.rs", "emspace://pages/core", "mentioned_in", 0.64),
edge("github://issues/9", "src/a.rs", "mentions", 0.8),
edge("github://issues/3", "src/a.rs", "mentions", 0.8),
edge("emspace://pages/src", "src", "documents", 0.8),
edge("jira://tickets/A-1", "src/a.rs", "mentions", 0.5),
];
let expected = format_hints(&hints_for_file("src/a.rs", &edges, ROOT));
assert!(expected.contains("emspace://pages/core [documents]"));
assert_eq!(expected.matches("emspace://pages/core ").count(), 1);
let mut rotated = edges.clone();
for _ in 0..edges.len() {
rotated.rotate_left(1);
let mut reversed = rotated.clone();
reversed.reverse();
for order in [&rotated, &reversed] {
assert_eq!(
format_hints(&hints_for_file("src/a.rs", order, ROOT)),
expected
);
}
}
}
#[test]
fn a_source_linked_both_ways_is_hinted_once_at_its_strongest_relation() {
let edges = vec![
edge("emspace://pages/core", "src/a.rs", "documents", 0.8),
edge("github://issues/7", "src/a.rs", "mentions", 0.7),
edge("src/a.rs", "emspace://pages/core", "mentioned_in", 0.64),
];
let hints = hints_for_file("src/a.rs", &edges, ROOT);
let seen: Vec<(&str, &str)> = hints
.iter()
.map(|h| (h.source_uri.as_str(), h.relation.as_str()))
.collect();
assert_eq!(
seen,
vec![
("emspace://pages/core", "documents"),
("github://issues/7", "mentions")
]
);
}
#[test]
fn range_filter_runs_before_output_limit() {
let mut edges: Vec<IndexEdge> = (0..5)
.map(|i| {
edge(
"src/auth.rs",
&format!("health://complexity/src/auth.rs#irrelevant_{i}"),
"health_hotspot",
10.0,
)
})
.collect();
edges.push(edge(
"src/auth.rs",
"health://complexity/src/auth.rs#requested",
"health_hotspot",
1.0,
));
let hints = hints_for_file_matching("src/auth.rs", &edges, ROOT, |hint| {
hint.source_uri.ends_with("#requested")
});
assert_eq!(hints.len(), 1);
assert!(hints[0].source_uri.ends_with("#requested"));
}
#[test]
fn sorts_by_weight_descending() {
let edges = vec![
edge("src/auth.rs", "github://issues/1", "mentions", 0.5),
edge("src/auth.rs", "github://issues/2", "mentions", 1.5),
edge("src/auth.rs", "github://issues/3", "mentions", 1.0),
];
let hints = hints_for_file("src/auth.rs", &edges, ROOT);
assert_eq!(hints[0].source_uri, "github://issues/2");
assert_eq!(hints[1].source_uri, "github://issues/3");
assert_eq!(hints[2].source_uri, "github://issues/1");
}
#[test]
fn format_hints_empty_returns_empty() {
{
let found = &format_hints(&[]);
assert!(found.is_empty(), "{found:?}");
}
}
#[test]
fn format_hints_produces_readable_output() {
let hints = vec![CrossSourceHint {
source_uri: "github://issues/42".into(),
relation: "mentions".into(),
weight: 1.0,
}];
let output = format_hints(&hints);
assert!(output.contains("Cross-Source Hints"));
assert!(output.contains("github://issues/42"));
assert!(output.contains("[mentions]"));
}
}