Skip to main content

semantic_diff/review/
llm.rs

1use 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
102/// Build the full prompt for a review section.
103pub 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
113/// Invoke the LLM for a single review section with a 120-second timeout.
114pub 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}