Conversation
Parse unified diffs to identify the most-changed files and automatically fetch surrounding code context (±50 lines around changes) before pass 1 evaluation. This gives the reviewer visibility into how changes fit into the broader codebase, reducing false negatives and the need for pass 2 deep reviews. - Add diff parser module (parse_diff_files, extract_context_window) - Add build_evaluation_prompt_with_context to include file context in prompts - Fetch up to 3 most-changed files proactively, with smart context windowing - Skip non-code files (binaries, lock files, images) - Update system prompt to reference auto-fetched context Closes #668 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
PR adds a diff module with a unified diff parser and context windowing, then wires it into pass 1 evaluation in ClaudeOrchestrator. The structure is clean, error handling is defensive (fetch failures are logged and skipped gracefully), and the 7 new tests cover the core parsing and windowing paths.
One correctness issue worth addressing before merge: take(MAX_CONTEXT_FILES) is applied to the sorted file list before the is_likely_non_code filter, so a PR that touches Cargo.lock + bun.lockb + a third lock file (all sorted to the top by change count) will exhaust all 3 slots on non-code files and return nothing — exactly the scenario this filter is meant to handle.
Full Review
Findings
Important
take(MAX_CONTEXT_FILES) before the non-code filter (claude.rs:231) — see inline comment. Lock file bumps are one of the most common PR types in this repo and the file list is sorted by change count descending, so lock files will frequently occupy the top 3 slots. The fix is a one-liner: move the filter before the take.
Suggestions
pub mod diffvspub(crate) mod diff(lib.rs:11) — the module is only consumed internally; publishing it widens the crate's API surface without benefit.max_linesdoc comment is slightly inaccurate (diff.rs:135) —...gap markers are appended outside thetake(max_lines)budget. Minor, but the doc says "limits total output tomax_lines".
What looks good
- Diff parser correctly handles the
+++ b/override for reliable path extraction (more robust thandiff --gitline splitting for paths containingb/). extract_context_windowcorrectly uses aHashSetto deduplicate overlapping windows before sorting, avoiding repeated lines.- Fetch failures are downgraded to
warn!and skipped — no impact on pass 1 when context is unavailable. build_evaluation_prompt_with_contextdelegates to the existingbuild_evaluation_promptand appends, keeping backward compatibility clean.- Test coverage for empty diff, multiple files, gap separators, and empty changed lines.
References:
Reviewed by PR / Review
| let changed_files = parse_diff_files(diff_text); | ||
| let mut contexts = Vec::new(); | ||
|
|
||
| for file in changed_files.iter().take(MAX_CONTEXT_FILES) { |
There was a problem hiding this comment.
[IMPORTANT]
Priority: Correctness
take(MAX_CONTEXT_FILES) is applied before the is_likely_non_code filter. If the top 3 most-changed files are all non-code (e.g., a PR that bumps Cargo.lock + bun.lockb + package-lock.yaml and also touches Rust source files), the take(3) exhausts the slots on the lock files and the actual code files at positions 4+ never get context. The feature silently produces nothing useful in this common case.
Fix: filter before capping.
for file in changed_files
.iter()
.filter(|f| !is_likely_non_code(&f.path))
.take(MAX_CONTEXT_FILES)
{
// ...
}|
|
||
| pub mod agents; | ||
| pub mod chat; | ||
| pub mod diff; |
There was a problem hiding this comment.
[SUGGESTION]
Priority: Architecture
diff is a pure internal module — only used by claude.rs — but it's declared pub mod diff, which exposes DiffFile, parse_diff_files, extract_context_window, and the constants as part of the crate's public API. Unless there's an intent for other crates to reuse the diff parser, pub(crate) mod diff would be more appropriate.
| let mut result = Vec::new(); | ||
| let mut prev_line: Option<usize> = None; | ||
|
|
||
| for &line_num in sorted_lines.iter().take(max_lines) { |
There was a problem hiding this comment.
[SUGGESTION]
Priority: Correctness (minor)
sorted_lines.iter().take(max_lines) limits the number of source lines emitted, but the "..." gap separators are appended on top of that count. With MAX_CONTEXT_LINES_PER_FILE = 300 and many disjoint hunks, the actual output can exceed 300 lines by the number of gap markers. Not a big deal given the LLM-context framing, but the doc comment says "limits total output to max_lines" which is slightly inaccurate.
Summary
crates/orchestrator/src/diff.rs) that extracts changed file paths and line numbers from PR diffsHow it works
parse_diff_files()extracts file paths, changed line numbers, and change counts from unified diffs, sorted by most changedextract_context_window()fetches ±50 lines around each changed line, collapsing overlapping windows and inserting...separators for gapsbuild_evaluation_prompt_with_context()appends an "auto-fetched file context" section to the pass 1 promptBenefits
Test plan
Closes #668
🤖 Generated with Claude Code