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.
22/// Matches both absolute and project-relative paths since edges
23/// store relative paths while ctx_read passes absolute ones.
24pub fn hints_for_file(
25    file_path: &str,
26    edges: &[IndexEdge],
27    project_root: &str,
28) -> Vec<CrossSourceHint> {
29    let rel = crate::core::graph_index::graph_relative_key(file_path, project_root);
30
31    let matches_path = |edge_path: &str| -> bool { edge_path == file_path || edge_path == rel };
32
33    let mut hints: Vec<CrossSourceHint> = edges
34        .iter()
35        .filter(|e| {
36            (matches_path(&e.from) && is_external_uri(&e.to))
37                || (matches_path(&e.to) && is_external_uri(&e.from))
38        })
39        .map(|e| {
40            if matches_path(&e.from) {
41                CrossSourceHint {
42                    source_uri: e.to.clone(),
43                    relation: e.kind.clone(),
44                    weight: e.weight,
45                }
46            } else {
47                CrossSourceHint {
48                    source_uri: e.from.clone(),
49                    relation: e.kind.clone(),
50                    weight: e.weight,
51                }
52            }
53        })
54        .collect();
55
56    hints.sort_by(|a, b| {
57        b.weight
58            .partial_cmp(&a.weight)
59            .unwrap_or(std::cmp::Ordering::Equal)
60    });
61    hints.dedup_by(|a, b| a.source_uri == b.source_uri);
62    hints.truncate(5);
63    hints
64}
65
66/// Format hints as a compact string for appending to ctx_read output.
67pub fn format_hints(hints: &[CrossSourceHint]) -> String {
68    if hints.is_empty() {
69        return String::new();
70    }
71
72    let mut out = String::from("\n--- Cross-Source Hints ---\n");
73    for hint in hints {
74        out.push_str(&format!(
75            "  {} [{}] w={:.1}\n",
76            hint.source_uri, hint.relation, hint.weight
77        ));
78    }
79    out
80}
81
82fn is_external_uri(path: &str) -> bool {
83    path.contains("://")
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::core::graph_index::IndexEdge;
90
91    fn edge(from: &str, to: &str, kind: &str, weight: f32) -> IndexEdge {
92        IndexEdge {
93            from: from.into(),
94            to: to.into(),
95            kind: kind.into(),
96            weight,
97        }
98    }
99
100    const ROOT: &str = "/project";
101
102    #[test]
103    fn finds_hints_from_forward_edges() {
104        let edges = vec![
105            edge("src/auth.rs", "github://issues/42", "mentions", 1.0),
106            edge("src/auth.rs", "postgres://schemas/sessions", "queries", 1.2),
107        ];
108
109        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
110        assert_eq!(hints.len(), 2);
111        assert!(hints.iter().any(|h| h.source_uri.contains("issues/42")));
112        assert!(hints
113            .iter()
114            .any(|h| h.source_uri.contains("schemas/sessions")));
115    }
116
117    #[test]
118    fn finds_hints_from_reverse_edges() {
119        let edges = vec![edge(
120            "github://issues/42",
121            "src/auth.rs",
122            "mentioned_in",
123            0.8,
124        )];
125
126        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
127        assert_eq!(hints.len(), 1);
128        assert!(hints[0].source_uri.contains("issues/42"));
129    }
130
131    #[test]
132    fn finds_hints_with_absolute_path() {
133        let edges = vec![edge("src/auth.rs", "github://issues/42", "mentions", 1.0)];
134        let hints = hints_for_file("/project/src/auth.rs", &edges, "/project");
135        assert_eq!(hints.len(), 1, "absolute path should match relative edge");
136    }
137
138    #[test]
139    fn ignores_code_to_code_edges() {
140        let edges = vec![edge("src/auth.rs", "src/db.rs", "imports", 1.0)];
141
142        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
143        assert!(hints.is_empty());
144    }
145
146    #[test]
147    fn deduplicates_and_limits_to_5() {
148        let edges: Vec<IndexEdge> = (0..10)
149            .map(|i| {
150                edge(
151                    "src/auth.rs",
152                    &format!("github://issues/{i}"),
153                    "mentions",
154                    1.0,
155                )
156            })
157            .collect();
158
159        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
160        assert_eq!(hints.len(), 5);
161    }
162
163    #[test]
164    fn sorts_by_weight_descending() {
165        let edges = vec![
166            edge("src/auth.rs", "github://issues/1", "mentions", 0.5),
167            edge("src/auth.rs", "github://issues/2", "mentions", 1.5),
168            edge("src/auth.rs", "github://issues/3", "mentions", 1.0),
169        ];
170
171        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
172        assert_eq!(hints[0].source_uri, "github://issues/2");
173        assert_eq!(hints[1].source_uri, "github://issues/3");
174        assert_eq!(hints[2].source_uri, "github://issues/1");
175    }
176
177    #[test]
178    fn format_hints_empty_returns_empty() {
179        assert!(format_hints(&[]).is_empty());
180    }
181
182    #[test]
183    fn format_hints_produces_readable_output() {
184        let hints = vec![CrossSourceHint {
185            source_uri: "github://issues/42".into(),
186            relation: "mentions".into(),
187            weight: 1.0,
188        }];
189
190        let output = format_hints(&hints);
191        assert!(output.contains("Cross-Source Hints"));
192        assert!(output.contains("github://issues/42"));
193        assert!(output.contains("[mentions]"));
194    }
195}