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!(
113            hints
114                .iter()
115                .any(|h| h.source_uri.contains("schemas/sessions"))
116        );
117    }
118
119    #[test]
120    fn finds_hints_from_reverse_edges() {
121        let edges = vec![edge(
122            "github://issues/42",
123            "src/auth.rs",
124            "mentioned_in",
125            0.8,
126        )];
127
128        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
129        assert_eq!(hints.len(), 1);
130        assert!(hints[0].source_uri.contains("issues/42"));
131    }
132
133    #[test]
134    fn finds_hints_with_absolute_path() {
135        let edges = vec![edge("src/auth.rs", "github://issues/42", "mentions", 1.0)];
136        let hints = hints_for_file("/project/src/auth.rs", &edges, "/project");
137        assert_eq!(hints.len(), 1, "absolute path should match relative edge");
138    }
139
140    #[test]
141    fn ignores_code_to_code_edges() {
142        let edges = vec![edge("src/auth.rs", "src/db.rs", "imports", 1.0)];
143
144        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
145        assert!(hints.is_empty());
146    }
147
148    #[test]
149    fn deduplicates_and_limits_to_5() {
150        let edges: Vec<IndexEdge> = (0..10)
151            .map(|i| {
152                edge(
153                    "src/auth.rs",
154                    &format!("github://issues/{i}"),
155                    "mentions",
156                    1.0,
157                )
158            })
159            .collect();
160
161        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
162        assert_eq!(hints.len(), 5);
163    }
164
165    #[test]
166    fn sorts_by_weight_descending() {
167        let edges = vec![
168            edge("src/auth.rs", "github://issues/1", "mentions", 0.5),
169            edge("src/auth.rs", "github://issues/2", "mentions", 1.5),
170            edge("src/auth.rs", "github://issues/3", "mentions", 1.0),
171        ];
172
173        let hints = hints_for_file("src/auth.rs", &edges, ROOT);
174        assert_eq!(hints[0].source_uri, "github://issues/2");
175        assert_eq!(hints[1].source_uri, "github://issues/3");
176        assert_eq!(hints[2].source_uri, "github://issues/1");
177    }
178
179    #[test]
180    fn format_hints_empty_returns_empty() {
181        assert!(format_hints(&[]).is_empty());
182    }
183
184    #[test]
185    fn format_hints_produces_readable_output() {
186        let hints = vec![CrossSourceHint {
187            source_uri: "github://issues/42".into(),
188            relation: "mentions".into(),
189            weight: 1.0,
190        }];
191
192        let output = format_hints(&hints);
193        assert!(output.contains("Cross-Source Hints"));
194        assert!(output.contains("github://issues/42"));
195        assert!(output.contains("[mentions]"));
196    }
197}