Skip to content

Commit 0377bb1

Browse files
committed
Add diagram generation and gitignore-aware symbol index
1 parent f6b2842 commit 0377bb1

File tree

7 files changed

+432
-127
lines changed

7 files changed

+432
-127
lines changed

.diffscope.yml.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ review_profile: balanced # balanced | chill | assertive
1212
review_instructions: |
1313
Prioritize security and correctness issues. Avoid stylistic comments unless they impact maintainability.
1414
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
15+
smart_review_diagram: false # Generate a Mermaid diagram in smart-review output
16+
symbol_index: true # Build repo symbol index for cross-file context (respects .gitignore)
1717
symbol_index_max_files: 500
1818
symbol_index_max_bytes: 200000
1919
symbol_index_max_locations: 5

Cargo.lock

Lines changed: 93 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ regex = "1.10"
2828
dirs = "5.0"
2929
chrono = "0.4"
3030
glob = "0.3"
31+
ignore = "0.4"
3132

3233
[dev-dependencies]
3334
tempfile = "3.8"
@@ -36,4 +37,3 @@ mockito = "1.2"
3637
[[bin]]
3738
name = "diffscope"
3839
path = "src/main.rs"
39-

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ review_profile: balanced # balanced | chill | assertive
197197
review_instructions: |
198198
Prioritize security and correctness issues. Avoid stylistic comments unless they impact maintainability.
199199
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
200+
smart_review_diagram: false # Generate a Mermaid diagram in smart-review output
201+
symbol_index: true # Build repo symbol index for cross-file context (respects .gitignore)
202202
symbol_index_max_files: 500
203203
symbol_index_max_bytes: 200000
204204
symbol_index_max_locations: 5

src/core/pr_summary.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ impl Default for SummaryOptions {
1818
}
1919

2020
impl PRSummaryGenerator {
21+
#[allow(dead_code)]
2122
pub async fn generate_summary(
2223
diffs: &[UnifiedDiff],
2324
git: &GitIntegration,
@@ -54,6 +55,37 @@ impl PRSummaryGenerator {
5455
Self::parse_summary_response(&response.content, stats)
5556
}
5657

58+
pub async fn generate_change_diagram(
59+
diffs: &[UnifiedDiff],
60+
adapter: &Box<dyn LLMAdapter>,
61+
) -> Result<Option<String>> {
62+
let stats = Self::calculate_stats(diffs);
63+
let prompt = Self::build_diagram_prompt(diffs, &stats);
64+
let request = LLMRequest {
65+
system_prompt: "You create concise Mermaid diagrams for code changes. Respond with a single mermaid diagram or 'none'.".to_string(),
66+
user_prompt: prompt,
67+
temperature: Some(0.2),
68+
max_tokens: Some(800),
69+
};
70+
71+
let response = adapter.complete(request).await?;
72+
Ok(extract_mermaid_block(&response.content))
73+
}
74+
75+
pub fn build_diagram_only_summary(diffs: &[UnifiedDiff], diagram: String) -> PRSummary {
76+
let stats = Self::calculate_stats(diffs);
77+
PRSummary {
78+
title: "Change Diagram".to_string(),
79+
description: String::new(),
80+
change_type: ChangeType::Chore,
81+
key_changes: Vec::new(),
82+
breaking_changes: None,
83+
testing_notes: String::new(),
84+
stats,
85+
visual_diff: Some(diagram),
86+
}
87+
}
88+
5789
fn calculate_stats(diffs: &[UnifiedDiff]) -> ChangeStats {
5890
let mut stats = ChangeStats::default();
5991

@@ -154,6 +186,50 @@ impl PRSummaryGenerator {
154186
prompt
155187
}
156188

189+
fn build_diagram_prompt(diffs: &[UnifiedDiff], stats: &ChangeStats) -> String {
190+
let mut prompt = String::new();
191+
prompt.push_str(
192+
"Create a single Mermaid flowchart or sequence diagram that summarizes the change.\n",
193+
);
194+
prompt.push_str(
195+
"Use only one mermaid code block. If a diagram isn't useful, reply with 'none'.\n\n",
196+
);
197+
prompt.push_str("## Statistics\n");
198+
prompt.push_str(&format!("- Files changed: {}\n", stats.files_changed));
199+
prompt.push_str(&format!("- Lines added: {}\n", stats.lines_added));
200+
prompt.push_str(&format!("- Lines removed: {}\n", stats.lines_removed));
201+
202+
prompt.push_str("\n## Files Changed\n");
203+
for diff in diffs.iter().take(20) {
204+
let path = diff.file_path.display();
205+
let added = diff
206+
.hunks
207+
.iter()
208+
.flat_map(|h| &h.changes)
209+
.filter(|c| matches!(c.change_type, crate::core::diff_parser::ChangeType::Added))
210+
.count();
211+
let removed = diff
212+
.hunks
213+
.iter()
214+
.flat_map(|h| &h.changes)
215+
.filter(|c| matches!(c.change_type, crate::core::diff_parser::ChangeType::Removed))
216+
.count();
217+
let status = if diff.is_deleted {
218+
"deleted"
219+
} else if diff.is_new {
220+
"new"
221+
} else {
222+
"modified"
223+
};
224+
prompt.push_str(&format!(
225+
"- {} ({}; +{}, -{})\n",
226+
path, status, added, removed
227+
));
228+
}
229+
230+
prompt
231+
}
232+
157233
fn get_system_prompt() -> String {
158234
r#"You are an AI assistant that generates clear, concise PR summaries.
159235
@@ -394,3 +470,48 @@ fn extract_mermaid_diagram(content: &str) -> Option<String> {
394470

395471
None
396472
}
473+
474+
fn extract_mermaid_block(content: &str) -> Option<String> {
475+
if content.to_lowercase().contains("none") {
476+
return None;
477+
}
478+
if let Some(diagram) = extract_mermaid_diagram(content) {
479+
return Some(diagram);
480+
}
481+
482+
let mut in_block = false;
483+
let mut lines = Vec::new();
484+
for line in content.lines() {
485+
let trimmed = line.trim();
486+
if trimmed.starts_with("```") && trimmed.contains("mermaid") {
487+
in_block = true;
488+
continue;
489+
}
490+
if trimmed.starts_with("```") && in_block {
491+
break;
492+
}
493+
if in_block {
494+
lines.push(line);
495+
}
496+
}
497+
let diagram = lines.join("\n").trim().to_string();
498+
if !diagram.is_empty() {
499+
return Some(diagram);
500+
}
501+
502+
let fallback = content
503+
.lines()
504+
.filter(|line| {
505+
let trimmed = line.trim();
506+
trimmed.starts_with("flowchart")
507+
|| trimmed.starts_with("graph")
508+
|| trimmed.starts_with("sequenceDiagram")
509+
})
510+
.collect::<Vec<_>>()
511+
.join("\n");
512+
if fallback.trim().is_empty() {
513+
None
514+
} else {
515+
Some(fallback)
516+
}
517+
}

0 commit comments

Comments
 (0)