Skip to content

Commit 174d002

Browse files
committed
feat: add graph-backed cross-file review context
1 parent 9955818 commit 174d002

17 files changed

Lines changed: 632 additions & 41 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: repo regression - cross file sql helper usage
2+
repo_path: graph_call_chain_repo
3+
diff: |
4+
diff --git a/routes.py b/routes.py
5+
index 1111111..2222222 100644
6+
--- a/routes.py
7+
+++ b/routes.py
8+
@@ -2,4 +2,5 @@ from auth import lookup_user
9+
10+
11+
def get_profile(request, db):
12+
- return {"ok": True}
13+
+ user = lookup_user(request.args["name"], db)
14+
+ return {"user": user}
15+
expect:
16+
must_find:
17+
- file: routes.py
18+
contains_any:
19+
- sql injection
20+
- unsafe sql
21+
- interpolates user-controlled
22+
tags_any:
23+
- sql-injection
24+
must_not_find:
25+
- contains: style
26+
min_total: 1
27+
max_total: 12
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def lookup_user(name, db):
2+
query = f"SELECT * FROM users WHERE name = '{name}'"
3+
return db.execute(query).fetchone()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from auth import lookup_user
2+
3+
4+
def get_profile(request, db):
5+
return {"ok": True}

src/core/context.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub struct LLMContextChunk {
1313
pub content: String,
1414
pub context_type: ContextType,
1515
pub line_range: Option<(usize, usize)>,
16+
#[serde(default, skip_serializing_if = "Option::is_none")]
17+
pub provenance: Option<String>,
1618
}
1719

1820
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -74,6 +76,7 @@ impl ContextFetcher {
7476
content: chunk_content,
7577
context_type: ContextType::FileContent,
7678
line_range: Some((expanded_start, expanded_end)),
79+
provenance: None,
7780
});
7881
}
7982
}
@@ -137,6 +140,7 @@ impl ContextFetcher {
137140
content: snippet,
138141
context_type: ContextType::Reference,
139142
line_range: None,
143+
provenance: None,
140144
});
141145
}
142146

@@ -185,6 +189,7 @@ impl ContextFetcher {
185189
content: definition_content,
186190
context_type: ContextType::Definition,
187191
line_range: Some((start_line + 1, end_line)),
192+
provenance: None,
188193
});
189194
}
190195
}
@@ -222,11 +227,32 @@ impl ContextFetcher {
222227
content: snippet,
223228
context_type: ContextType::Definition,
224229
line_range: Some(location.line_range),
230+
provenance: location.provenance.clone(),
225231
});
226232
}
227233
}
228234
}
229235

236+
for location in index.graph_related_locations(
237+
file_path,
238+
symbols,
239+
graph_hops,
240+
max_locations,
241+
graph_max_files,
242+
) {
243+
if &location.file_path == file_path {
244+
continue;
245+
}
246+
let snippet = truncate_with_notice(location.snippet, MAX_CONTEXT_CHARS);
247+
chunks.push(LLMContextChunk {
248+
file_path: location.file_path,
249+
content: snippet,
250+
context_type: ContextType::Definition,
251+
line_range: Some(location.line_range),
252+
provenance: location.provenance,
253+
});
254+
}
255+
230256
for location in index.multi_hop_locations(
231257
file_path,
232258
symbols,
@@ -243,6 +269,7 @@ impl ContextFetcher {
243269
content: snippet,
244270
context_type: ContextType::Reference,
245271
line_range: Some(location.line_range),
272+
provenance: location.provenance,
246273
});
247274
}
248275

@@ -373,4 +400,55 @@ mod tests {
373400
let chunk = &chunks[0];
374401
assert!(chunk.content.contains("pub fn process"));
375402
}
403+
404+
#[tokio::test]
405+
async fn test_fetch_related_definitions_with_index_uses_symbol_graph_neighbors() {
406+
let dir = tempfile::tempdir().unwrap();
407+
let src_dir = dir.path().join("src");
408+
std::fs::create_dir_all(&src_dir).unwrap();
409+
410+
std::fs::write(
411+
src_dir.join("auth.rs"),
412+
"pub fn validate_token(token: &str) -> bool {\n token.len() > 10\n}\n",
413+
)
414+
.unwrap();
415+
std::fs::write(
416+
src_dir.join("handler.rs"),
417+
"use crate::auth::validate_token;\n\npub fn handle_request(token: &str) -> bool {\n validate_token(token)\n}\n",
418+
)
419+
.unwrap();
420+
421+
let index = SymbolIndex::build(dir.path(), 20, 200_000, 10, |_| false).unwrap();
422+
let fetcher = ContextFetcher::new(dir.path().to_path_buf());
423+
424+
let chunks = fetcher
425+
.fetch_related_definitions_with_index(
426+
&PathBuf::from("src/handler.rs"),
427+
&["handle_request".to_string()],
428+
&index,
429+
10,
430+
2,
431+
5,
432+
)
433+
.await
434+
.unwrap();
435+
436+
let graph_chunk = chunks
437+
.iter()
438+
.find(|chunk| chunk.file_path == std::path::Path::new("src/auth.rs"))
439+
.expect("expected graph-related auth context");
440+
441+
assert_eq!(graph_chunk.context_type, ContextType::Definition);
442+
assert!(graph_chunk.content.contains("validate_token"));
443+
assert!(graph_chunk
444+
.provenance
445+
.as_deref()
446+
.unwrap_or_default()
447+
.contains("symbol graph"));
448+
assert!(graph_chunk
449+
.provenance
450+
.as_deref()
451+
.unwrap_or_default()
452+
.contains("calls"));
453+
}
376454
}

src/core/prompt.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,13 +441,18 @@ impl PromptBuilder {
441441

442442
for chunk in chunks {
443443
let block = format!(
444-
"\n[{:?} - {}{}]\n{}\n",
444+
"\n[{:?} - {}{}{}]\n{}\n",
445445
chunk.context_type,
446446
chunk.file_path.display(),
447447
chunk
448448
.line_range
449449
.map(|(s, e)| format!(":{}-{}", s, e))
450450
.unwrap_or_default(),
451+
chunk
452+
.provenance
453+
.as_ref()
454+
.map(|value| format!(" | {}", value))
455+
.unwrap_or_default(),
451456
chunk.content
452457
);
453458
if self.config.max_context_chars > 0

src/core/semantic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,10 @@ pub async fn semantic_context_for_diff(
452452
content,
453453
context_type: ContextType::Reference,
454454
line_range: Some(semantic_match.chunk.line_range),
455+
provenance: Some(format!(
456+
"semantic retrieval (similarity={:.2}, symbol={})",
457+
semantic_match.similarity, semantic_match.chunk.symbol_name
458+
)),
455459
});
456460
if chunks.len() >= limit {
457461
break;

src/core/smart_review_prompt.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,16 @@ TAGS: [comma-separated relevant tags]
116116
prompt.push_str("## Context Information\n\n");
117117
for chunk in context_chunks {
118118
let (start_line, end_line) = chunk.line_range.unwrap_or((1, 1));
119-
let description =
120-
format!("{} - {:?}", chunk.file_path.display(), chunk.context_type);
119+
let description = format!(
120+
"{} - {:?}{}",
121+
chunk.file_path.display(),
122+
chunk.context_type,
123+
chunk
124+
.provenance
125+
.as_ref()
126+
.map(|value| format!(" | {}", value))
127+
.unwrap_or_default()
128+
);
121129
let block = format!(
122130
"**{}** (lines {}-{}):\n```\n{}\n```\n\n",
123131
description,

0 commit comments

Comments
 (0)