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;
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/// Format hints as a compact string for appending to ctx_read output.
57pub 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}