Skip to content

Commit f6b2842

Browse files
committed
Add smart review summary, symbol index, and feedback loop
1 parent 5928afb commit f6b2842

File tree

9 files changed

+748
-7
lines changed

9 files changed

+748
-7
lines changed

.diffscope.yml.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ min_confidence: 0.0 # Drop comments below this confidence (0.0-1.0)
1111
review_profile: balanced # balanced | chill | assertive
1212
review_instructions: |
1313
Prioritize security and correctness issues. Avoid stylistic comments unless they impact maintainability.
14+
smart_review_summary: true # Include AI-generated PR summary in smart-review output
15+
smart_review_diagram: false # Include Mermaid diagram in PR summary (smart-review)
16+
symbol_index: true # Build repo symbol index for cross-file context
17+
symbol_index_max_files: 500
18+
symbol_index_max_bytes: 200000
19+
symbol_index_max_locations: 5
20+
feedback_path: ".diffscope.feedback.json"
1421

1522
# API configuration (optional - can use environment variables)
1623
# api_key: your-api-key-here

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,13 @@ min_confidence: 0.0 # Drop comments below this confidence (0.0-1.0)
196196
review_profile: balanced # balanced | chill | assertive
197197
review_instructions: |
198198
Prioritize security and correctness issues. Avoid stylistic comments unless they impact maintainability.
199+
smart_review_summary: true # Include AI-generated PR summary in smart-review output
200+
smart_review_diagram: false # Include Mermaid diagram in PR summary (smart-review)
201+
symbol_index: true # Build repo symbol index for cross-file context
202+
symbol_index_max_files: 500
203+
symbol_index_max_bytes: 200000
204+
symbol_index_max_locations: 5
205+
feedback_path: ".diffscope.feedback.json"
199206
system_prompt: "Focus on security vulnerabilities, performance issues, and best practices"
200207
openai_use_responses: true # Use OpenAI Responses API (recommended) instead of chat completions
201208
@@ -274,6 +281,25 @@ Apache-2.0 License. See [LICENSE](LICENSE) for details.
274281
🚨 **Critical Issues:** 1
275282
📁 **Files Analyzed:** 3
276283
284+
## 🧾 PR Summary
285+
286+
**Add auth safeguards** (Fix)
287+
288+
### Key Changes
289+
290+
- Harden auth query handling
291+
- Add route-level guards
292+
- Introduce safe defaults for user lookups
293+
294+
### Diagram
295+
296+
```mermaid
297+
flowchart TD
298+
A[Request] --> B[Auth Guard]
299+
B --> C[DB Query]
300+
C --> D[Response]
301+
```
302+
277303
## 🧭 Change Walkthrough
278304

279305
- `src/auth.py` (modified; +12, -3)
@@ -413,6 +439,9 @@ temperature: 0.1 # Low for consistent reviews
413439
max_tokens: 4000
414440
min_confidence: 0.2
415441
review_profile: assertive
442+
smart_review_summary: true
443+
smart_review_diagram: true
444+
symbol_index: true
416445

417446
system_prompt: |
418447
You are reviewing Python code for a production FastAPI application.
@@ -607,6 +636,20 @@ Planned support for responding to pull request comments with interactive command
607636
@diffscope help # Show all commands
608637
```
609638

639+
### ✅ Feedback Loop (Reduce Repeated False Positives)
640+
641+
Use the feedback store to suppress comments you’ve already reviewed:
642+
643+
```bash
644+
# Reject comments from a prior JSON review output
645+
diffscope feedback --reject review.json
646+
647+
# Accept comments (keeps a record, and removes them from suppress list)
648+
diffscope feedback --accept review.json
649+
```
650+
651+
The feedback file defaults to `.diffscope.feedback.json` and can be configured in `.diffscope.yml`.
652+
610653
### 📊 PR Summary Generation
611654

612655
Generate executive summaries for pull requests:

src/config.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,27 @@ pub struct Config {
2929
#[serde(default)]
3030
pub review_instructions: Option<String>,
3131

32+
#[serde(default = "default_true")]
33+
pub smart_review_summary: bool,
34+
35+
#[serde(default)]
36+
pub smart_review_diagram: bool,
37+
38+
#[serde(default = "default_true")]
39+
pub symbol_index: bool,
40+
41+
#[serde(default = "default_symbol_index_max_files")]
42+
pub symbol_index_max_files: usize,
43+
44+
#[serde(default = "default_symbol_index_max_bytes")]
45+
pub symbol_index_max_bytes: usize,
46+
47+
#[serde(default = "default_symbol_index_max_locations")]
48+
pub symbol_index_max_locations: usize,
49+
50+
#[serde(default = "default_feedback_path")]
51+
pub feedback_path: PathBuf,
52+
3253
pub system_prompt: Option<String>,
3354
pub api_key: Option<String>,
3455
pub base_url: Option<String>,
@@ -101,6 +122,13 @@ impl Default for Config {
101122
min_confidence: default_min_confidence(),
102123
review_profile: None,
103124
review_instructions: None,
125+
smart_review_summary: true,
126+
smart_review_diagram: false,
127+
symbol_index: true,
128+
symbol_index_max_files: default_symbol_index_max_files(),
129+
symbol_index_max_bytes: default_symbol_index_max_bytes(),
130+
symbol_index_max_locations: default_symbol_index_max_locations(),
131+
feedback_path: default_feedback_path(),
104132
system_prompt: None,
105133
api_key: None,
106134
base_url: None,
@@ -166,6 +194,16 @@ impl Config {
166194
self.max_tokens = default_max_tokens();
167195
}
168196

197+
if self.symbol_index_max_files == 0 {
198+
self.symbol_index_max_files = default_symbol_index_max_files();
199+
}
200+
if self.symbol_index_max_bytes == 0 {
201+
self.symbol_index_max_bytes = default_symbol_index_max_bytes();
202+
}
203+
if self.symbol_index_max_locations == 0 {
204+
self.symbol_index_max_locations = default_symbol_index_max_locations();
205+
}
206+
169207
if !self.min_confidence.is_finite() {
170208
self.min_confidence = default_min_confidence();
171209
} else if !(0.0..=1.0).contains(&self.min_confidence) {
@@ -292,6 +330,22 @@ fn default_min_confidence() -> f32 {
292330
0.0
293331
}
294332

333+
fn default_symbol_index_max_files() -> usize {
334+
500
335+
}
336+
337+
fn default_symbol_index_max_bytes() -> usize {
338+
200_000
339+
}
340+
341+
fn default_symbol_index_max_locations() -> usize {
342+
5
343+
}
344+
345+
fn default_feedback_path() -> PathBuf {
346+
PathBuf::from(".diffscope.feedback.json")
347+
}
348+
295349
fn default_true() -> bool {
296350
true
297351
}

src/core/comment.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::path::PathBuf;
55

66
#[derive(Debug, Clone, Serialize, Deserialize)]
77
pub struct Comment {
8+
#[serde(default)]
9+
pub id: String,
810
pub file_path: PathBuf,
911
pub line_number: usize,
1012
pub content: String,
@@ -139,8 +141,10 @@ impl CommentSynthesizer {
139141
.clone()
140142
.unwrap_or_else(|| Self::determine_fix_effort(&raw.content, &category));
141143
let code_suggestion = Self::generate_code_suggestion(&raw);
144+
let id = Self::generate_comment_id(&raw.file_path, &raw.content, &category);
142145

143146
Ok(Some(Comment {
147+
id,
144148
file_path: raw.file_path,
145149
line_number: raw.line_number,
146150
content: raw.content,
@@ -154,6 +158,10 @@ impl CommentSynthesizer {
154158
}))
155159
}
156160

161+
fn generate_comment_id(file_path: &PathBuf, content: &str, category: &Category) -> String {
162+
compute_comment_id(file_path, content, category)
163+
}
164+
157165
fn determine_severity(content: &str) -> Severity {
158166
let lower = content.to_lowercase();
159167
if lower.contains("error") || lower.contains("critical") {
@@ -416,6 +424,47 @@ impl CommentSynthesizer {
416424
}
417425
}
418426

427+
pub fn compute_comment_id(file_path: &PathBuf, content: &str, category: &Category) -> String {
428+
let normalized = normalize_content(content);
429+
let key = format!("{}|{:?}|{}", file_path.display(), category, normalized);
430+
let hash = fnv1a64(key.as_bytes());
431+
format!("cmt_{:016x}", hash)
432+
}
433+
434+
fn fnv1a64(bytes: &[u8]) -> u64 {
435+
let mut hash: u64 = 0xcbf29ce484222325;
436+
for &byte in bytes {
437+
hash ^= byte as u64;
438+
hash = hash.wrapping_mul(0x100000001b3);
439+
}
440+
hash
441+
}
442+
443+
fn normalize_content(content: &str) -> String {
444+
let mut normalized = String::new();
445+
let mut last_space = false;
446+
447+
for ch in content.chars() {
448+
let ch = if ch.is_ascii_digit() {
449+
'#'
450+
} else {
451+
ch.to_ascii_lowercase()
452+
};
453+
454+
if ch.is_whitespace() {
455+
if !last_space {
456+
normalized.push(' ');
457+
last_space = true;
458+
}
459+
} else {
460+
normalized.push(ch);
461+
last_space = false;
462+
}
463+
}
464+
465+
normalized.trim().to_string()
466+
}
467+
419468
#[derive(Debug)]
420469
pub struct RawComment {
421470
pub file_path: PathBuf,

src/core/context.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::collections::HashSet;
55
use std::path::Path;
66
use std::path::PathBuf;
77

8+
use crate::core::SymbolIndex;
89
#[derive(Debug, Clone, Serialize, Deserialize)]
910
pub struct LLMContextChunk {
1011
pub file_path: PathBuf,
@@ -176,6 +177,39 @@ impl ContextFetcher {
176177

177178
Ok(chunks)
178179
}
180+
181+
pub async fn fetch_related_definitions_with_index(
182+
&self,
183+
file_path: &PathBuf,
184+
symbols: &[String],
185+
index: &SymbolIndex,
186+
max_locations: usize,
187+
) -> Result<Vec<LLMContextChunk>> {
188+
let mut chunks = Vec::new();
189+
190+
if symbols.is_empty() {
191+
return Ok(chunks);
192+
}
193+
194+
for symbol in symbols {
195+
if let Some(locations) = index.lookup(symbol) {
196+
for location in locations.iter().take(max_locations) {
197+
if &location.file_path == file_path {
198+
continue;
199+
}
200+
let snippet = truncate_with_notice(location.snippet.clone(), MAX_CONTEXT_CHARS);
201+
chunks.push(LLMContextChunk {
202+
file_path: location.file_path.clone(),
203+
content: snippet,
204+
context_type: ContextType::Definition,
205+
line_range: Some(location.line_range),
206+
});
207+
}
208+
}
209+
}
210+
211+
Ok(chunks)
212+
}
179213
}
180214

181215
fn merge_ranges(lines: &[(usize, usize)]) -> Vec<(usize, usize)> {

src/core/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ pub mod interactive;
88
pub mod pr_summary;
99
pub mod prompt;
1010
pub mod smart_review_prompt;
11+
pub mod symbol_index;
1112

1213
pub use changelog::ChangelogGenerator;
1314
pub use comment::{Comment, CommentSynthesizer};
1415
pub use commit_prompt::CommitPromptBuilder;
1516
pub use context::{ContextFetcher, ContextType, LLMContextChunk};
1617
pub use diff_parser::{DiffParser, UnifiedDiff};
1718
pub use git::GitIntegration;
18-
pub use pr_summary::PRSummaryGenerator;
19+
pub use pr_summary::{PRSummaryGenerator, SummaryOptions};
1920
pub use prompt::PromptBuilder;
2021
pub use smart_review_prompt::SmartReviewPromptBuilder;
22+
pub use symbol_index::SymbolIndex;

0 commit comments

Comments
 (0)