Skip to main content

lean_ctx/core/
cross_source_hints.rs

1//! Cross-source hints — lateral connections between cortical columns.
2//!
3//! When `ctx_read` delivers a file, this module appends hints about related
4//! data from other sources (issues, PRs, DB schemas, wiki pages) discovered
5//! via the graph index's cross-source edges.
6//!
7//! Scientific basis: Lateral connections in V1 cortex (Stettler et al., 2002)
8//! enable feature integration across cortical columns.
9
10use crate::core::graph_index::{IndexEdge, ProjectIndex};
11
12/// A hint about related data from another source.
13#[derive(Debug, Clone, serde::Serialize)]
14pub struct CrossSourceHint {
15    pub source_uri: String,
16    pub relation: String,
17    pub weight: f32,
18}
19
20/// Find cross-source hints for a given file path by looking up
21/// edges in the graph index that connect to external URIs.
22pub 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
56/// Find cross-source hints using the full ProjectIndex.
57pub fn hints_from_index(file_path: &str, index: &ProjectIndex) -> Vec<CrossSourceHint> {
58    hints_for_file(file_path, &index.edges)
59}
60
61/// Format hints as a compact string for appending to ctx_read output.
62pub fn format_hints(hints: &[CrossSourceHint]) -> String {
63    if hints.is_empty() {
64        return String::new();
65    }
66
67    let mut out = String::from("\n--- Cross-Source Hints ---\n");
68    for hint in hints {
69        out.push_str(&format!(
70            "  {} [{}] w={:.1}\n",
71            hint.source_uri, hint.relation, hint.weight
72        ));
73    }
74    out
75}
76
77fn is_external_uri(path: &str) -> bool {
78    path.contains("://")
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::core::graph_index::IndexEdge;
85
86    fn edge(from: &str, to: &str, kind: &str, weight: f32) -> IndexEdge {
87        IndexEdge {
88            from: from.into(),
89            to: to.into(),
90            kind: kind.into(),
91            weight,
92        }
93    }
94
95    #[test]
96    fn finds_hints_from_forward_edges() {
97        let edges = vec![
98            edge("src/auth.rs", "github://issues/42", "mentions", 1.0),
99            edge("src/auth.rs", "postgres://schemas/sessions", "queries", 1.2),
100        ];
101
102        let hints = hints_for_file("src/auth.rs", &edges);
103        assert_eq!(hints.len(), 2);
104        assert!(hints.iter().any(|h| h.source_uri.contains("issues/42")));
105        assert!(hints
106            .iter()
107            .any(|h| h.source_uri.contains("schemas/sessions")));
108    }
109
110    #[test]
111    fn finds_hints_from_reverse_edges() {
112        let edges = vec![edge(
113            "github://issues/42",
114            "src/auth.rs",
115            "mentioned_in",
116            0.8,
117        )];
118
119        let hints = hints_for_file("src/auth.rs", &edges);
120        assert_eq!(hints.len(), 1);
121        assert!(hints[0].source_uri.contains("issues/42"));
122    }
123
124    #[test]
125    fn ignores_code_to_code_edges() {
126        let edges = vec![edge("src/auth.rs", "src/db.rs", "imports", 1.0)];
127
128        let hints = hints_for_file("src/auth.rs", &edges);
129        assert!(hints.is_empty());
130    }
131
132    #[test]
133    fn deduplicates_and_limits_to_5() {
134        let edges: Vec<IndexEdge> = (0..10)
135            .map(|i| {
136                edge(
137                    "src/auth.rs",
138                    &format!("github://issues/{i}"),
139                    "mentions",
140                    1.0,
141                )
142            })
143            .collect();
144
145        let hints = hints_for_file("src/auth.rs", &edges);
146        assert_eq!(hints.len(), 5);
147    }
148
149    #[test]
150    fn sorts_by_weight_descending() {
151        let edges = vec![
152            edge("src/auth.rs", "github://issues/1", "mentions", 0.5),
153            edge("src/auth.rs", "github://issues/2", "mentions", 1.5),
154            edge("src/auth.rs", "github://issues/3", "mentions", 1.0),
155        ];
156
157        let hints = hints_for_file("src/auth.rs", &edges);
158        assert_eq!(hints[0].source_uri, "github://issues/2");
159        assert_eq!(hints[1].source_uri, "github://issues/3");
160        assert_eq!(hints[2].source_uri, "github://issues/1");
161    }
162
163    #[test]
164    fn format_hints_empty_returns_empty() {
165        assert!(format_hints(&[]).is_empty());
166    }
167
168    #[test]
169    fn format_hints_produces_readable_output() {
170        let hints = vec![CrossSourceHint {
171            source_uri: "github://issues/42".into(),
172            relation: "mentions".into(),
173            weight: 1.0,
174        }];
175
176        let output = format_hints(&hints);
177        assert!(output.contains("Cross-Source Hints"));
178        assert!(output.contains("github://issues/42"));
179        assert!(output.contains("[mentions]"));
180    }
181}