semantic_diff/review/
llm.rs1use crate::grouper::llm::{invoke_llm_text, LlmBackend};
2use crate::grouper::SemanticGroup;
3use crate::diff::DiffData;
4use super::{ReviewSection, ReviewSource};
5
6fn build_shared_context(group: &SemanticGroup, diff_data: &DiffData) -> String {
7 let mut ctx = format!(
8 "You are reviewing a group of related code changes called \"{}\".\n\
9 Group description: {}\n\n\
10 The changes in this group:\n",
11 group.label, group.description
12 );
13
14 for change in group.changes() {
15 for f in &diff_data.files {
16 let path = f.target_file.trim_start_matches("b/");
17 if path == change.file {
18 ctx.push_str(&format!("\nFILE: {}\n", path));
19 if change.hunks.is_empty() {
20 for (i, hunk) in f.hunks.iter().enumerate() {
21 let content: String = hunk.lines.iter()
22 .map(|l| l.content.as_str())
23 .collect::<Vec<_>>()
24 .join("\n");
25 ctx.push_str(&format!("HUNK {}:\n{}\n", i, content));
26 }
27 } else {
28 for &hi in &change.hunks {
29 if let Some(hunk) = f.hunks.get(hi) {
30 let content: String = hunk.lines.iter()
31 .map(|l| l.content.as_str())
32 .collect::<Vec<_>>()
33 .join("\n");
34 ctx.push_str(&format!("HUNK {}:\n{}\n", hi, content));
35 }
36 }
37 }
38 break;
39 }
40 }
41 }
42
43 ctx
44}
45
46fn build_section_prompt(section: ReviewSection, shared_context: &str, review_source: &ReviewSource) -> String {
47 let section_instruction = match section {
48 ReviewSection::Why => {
49 "Analyze the PURPOSE of these changes. Return a markdown list ranked by importance.\n\
50 Each item: one sentence explaining why this change was made.\n\
51 Focus on intent, not mechanics. Max 5 items.\n\
52 Return ONLY markdown, no code fences around the whole response.".to_string()
53 }
54 ReviewSection::What => {
55 "Describe the BEHAVIORAL CHANGES as a markdown table with columns:\n\
56 | Component | Before | After | Risk |\n\n\
57 Focus on observable behavior differences, not code structure.\n\
58 Risk is one of: none, low, medium, high.\n\
59 Omit trivial changes (formatting, imports). Max 10 rows.\n\
60 Return ONLY the markdown table.".to_string()
61 }
62 ReviewSection::How => {
63 "If these changes involve complex control flow, state transitions, or\n\
64 architectural patterns, return a mermaid diagram showing the key logic.\n\
65 Use flowchart TD or sequenceDiagram as appropriate.\n\n\
66 If the changes are straightforward (simple CRUD, config changes, etc.),\n\
67 return exactly the text: SKIP\n\n\
68 Return ONLY the mermaid code block OR the word SKIP.".to_string()
69 }
70 ReviewSection::Verdict => {
71 let skill_preamble = match review_source {
72 ReviewSource::Skill { path, .. } => {
73 std::fs::read_to_string(path).unwrap_or_default()
74 }
75 ReviewSource::BuiltIn => String::new(),
76 };
77
78 format!(
79 "{}\n\
80 Review these changes for HIGH-SEVERITY issues only. Ignore:\n\
81 - Style/formatting opinions\n\
82 - Minor naming suggestions\n\
83 - Test coverage gaps (unless security-relevant)\n\n\
84 Focus on:\n\
85 - Logic errors, off-by-one, null/None handling\n\
86 - Security: injection, auth bypass, secrets exposure\n\
87 - Concurrency: race conditions, deadlocks\n\
88 - Data: schema breaks, migration risks\n\n\
89 If no high-severity issues found, say \"No high-severity issues detected.\"\n\
90 Return markdown with ## headings per issue found. Max 3 issues.\n\
91 Prefix each issue heading with a bug number like RV-1, RV-2, etc.\n\
92 Example: ## RV-1: Potential null dereference in auth handler\n\
93 This allows users to reference specific findings when asking their AI assistant to fix them.",
94 skill_preamble
95 )
96 }
97 };
98
99 format!("{}\n\n{}", shared_context, section_instruction)
100}
101
102pub fn build_review_prompt(
104 section: ReviewSection,
105 group: &SemanticGroup,
106 diff_data: &DiffData,
107 review_source: &ReviewSource,
108) -> String {
109 let shared = build_shared_context(group, diff_data);
110 build_section_prompt(section, &shared, review_source)
111}
112
113pub async fn invoke_review_section(
115 backend: LlmBackend,
116 model: &str,
117 prompt: &str,
118) -> Result<String, String> {
119 use std::time::Duration;
120 match tokio::time::timeout(
121 Duration::from_secs(120),
122 invoke_llm_text(backend, model, prompt),
123 ).await {
124 Ok(Ok(response)) => Ok(response),
125 Ok(Err(e)) => Err(e.to_string()),
126 Err(_) => Err("LLM timed out after 120s".to_string()),
127 }
128}