lean_ctx/core/
cross_source_hints.rs1use crate::core::graph_index::IndexEdge;
11
12#[derive(Debug, Clone, serde::Serialize)]
14pub struct CrossSourceHint {
15 pub source_uri: String,
16 pub relation: String,
17 pub weight: f32,
18}
19
20pub fn hints_for_file(file_path: &str, edges: &[IndexEdge]) -> Vec<CrossSourceHint> {
23 let mut hints: Vec<CrossSourceHint> = edges
24 .iter()
25 .filter(|e| {
26 (e.from == file_path && is_external_uri(&e.to))
27 || (e.to == file_path && is_external_uri(&e.from))
28 })
29 .map(|e| {
30 if e.from == file_path {
31 CrossSourceHint {
32 source_uri: e.to.clone(),
33 relation: e.kind.clone(),
34 weight: e.weight,
35 }
36 } else {
37 CrossSourceHint {
38 source_uri: e.from.clone(),
39 relation: e.kind.clone(),
40 weight: e.weight,
41 }
42 }
43 })
44 .collect();
45
46 hints.sort_by(|a, b| {
47 b.weight
48 .partial_cmp(&a.weight)
49 .unwrap_or(std::cmp::Ordering::Equal)
50 });
51 hints.dedup_by(|a, b| a.source_uri == b.source_uri);
52 hints.truncate(5);
53 hints
54}
55
56pub fn format_hints(hints: &[CrossSourceHint]) -> String {
58 if hints.is_empty() {
59 return String::new();
60 }
61
62 let mut out = String::from("\n--- Cross-Source Hints ---\n");
63 for hint in hints {
64 out.push_str(&format!(
65 " {} [{}] w={:.1}\n",
66 hint.source_uri, hint.relation, hint.weight
67 ));
68 }
69 out
70}
71
72fn is_external_uri(path: &str) -> bool {
73 path.contains("://")
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::core::graph_index::IndexEdge;
80
81 fn edge(from: &str, to: &str, kind: &str, weight: f32) -> IndexEdge {
82 IndexEdge {
83 from: from.into(),
84 to: to.into(),
85 kind: kind.into(),
86 weight,
87 }
88 }
89
90 #[test]
91 fn finds_hints_from_forward_edges() {
92 let edges = vec![
93 edge("src/auth.rs", "github://issues/42", "mentions", 1.0),
94 edge("src/auth.rs", "postgres://schemas/sessions", "queries", 1.2),
95 ];
96
97 let hints = hints_for_file("src/auth.rs", &edges);
98 assert_eq!(hints.len(), 2);
99 assert!(hints.iter().any(|h| h.source_uri.contains("issues/42")));
100 assert!(hints
101 .iter()
102 .any(|h| h.source_uri.contains("schemas/sessions")));
103 }
104
105 #[test]
106 fn finds_hints_from_reverse_edges() {
107 let edges = vec![edge(
108 "github://issues/42",
109 "src/auth.rs",
110 "mentioned_in",
111 0.8,
112 )];
113
114 let hints = hints_for_file("src/auth.rs", &edges);
115 assert_eq!(hints.len(), 1);
116 assert!(hints[0].source_uri.contains("issues/42"));
117 }
118
119 #[test]
120 fn ignores_code_to_code_edges() {
121 let edges = vec![edge("src/auth.rs", "src/db.rs", "imports", 1.0)];
122
123 let hints = hints_for_file("src/auth.rs", &edges);
124 assert!(hints.is_empty());
125 }
126
127 #[test]
128 fn deduplicates_and_limits_to_5() {
129 let edges: Vec<IndexEdge> = (0..10)
130 .map(|i| {
131 edge(
132 "src/auth.rs",
133 &format!("github://issues/{i}"),
134 "mentions",
135 1.0,
136 )
137 })
138 .collect();
139
140 let hints = hints_for_file("src/auth.rs", &edges);
141 assert_eq!(hints.len(), 5);
142 }
143
144 #[test]
145 fn sorts_by_weight_descending() {
146 let edges = vec![
147 edge("src/auth.rs", "github://issues/1", "mentions", 0.5),
148 edge("src/auth.rs", "github://issues/2", "mentions", 1.5),
149 edge("src/auth.rs", "github://issues/3", "mentions", 1.0),
150 ];
151
152 let hints = hints_for_file("src/auth.rs", &edges);
153 assert_eq!(hints[0].source_uri, "github://issues/2");
154 assert_eq!(hints[1].source_uri, "github://issues/3");
155 assert_eq!(hints[2].source_uri, "github://issues/1");
156 }
157
158 #[test]
159 fn format_hints_empty_returns_empty() {
160 assert!(format_hints(&[]).is_empty());
161 }
162
163 #[test]
164 fn format_hints_produces_readable_output() {
165 let hints = vec![CrossSourceHint {
166 source_uri: "github://issues/42".into(),
167 relation: "mentions".into(),
168 weight: 1.0,
169 }];
170
171 let output = format_hints(&hints);
172 assert!(output.contains("Cross-Source Hints"));
173 assert!(output.contains("github://issues/42"));
174 assert!(output.contains("[mentions]"));
175 }
176}