diff --git a/Cargo.toml b/Cargo.toml index f15e404..6df8cba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,19 +9,19 @@ resolver = "2" [workspace.dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -candle-core = { version = "0.8" } -candle-nn = { version = "0.8" } -candle-transformers = { version = "0.8" } +candle-core = { version = "0.10.2" } +candle-nn = { version = "0.10.2" } +candle-transformers = { version = "0.10.2" } tokenizers = { version = "0.21.0", default-features = false, features = ["fancy-regex"] } -rand = "0.9" -ratatui = "0.29" -crossterm = "0.28" +rand = "0.10.1" +ratatui = "0.30" +crossterm = "0.29" tokio = { version = "1", features = ["full"] } anyhow = "1" clap = { version = "4", features = ["derive"] } regex = "1" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "stream", "rustls-tls-native-roots"] } -indicatif = "0.17" +indicatif = "0.18.4" walkdir = "2" chrono = { version = "0.4", features = ["serde"] } lazy_static = "1" diff --git a/README.md b/README.md index 237ed1a..cddd164 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Auto-detects your OS and CPU architecture (Intel or Apple Silicon), downloads th Pin a specific version: ```bash -VERSION=v0.6.0 curl -fsSL https://github.com/thinkgrid-labs/diffmind/releases/latest/download/install.sh | bash +VERSION=v.x.x curl -fsSL https://github.com/thinkgrid-labs/diffmind/releases/latest/download/install.sh | bash ``` ### Windows diff --git a/apps/tui-cli/Cargo.toml b/apps/tui-cli/Cargo.toml index 5d8bdfa..bc7ef5e 100644 --- a/apps/tui-cli/Cargo.toml +++ b/apps/tui-cli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "diffmind" -version = "0.6.2" -edition = "2021" +version = "0.6.3" +edition = "2024" description = "Local-first AI code review agent — powered by on-device inference" [[bin]] diff --git a/apps/tui-cli/src/cli.rs b/apps/tui-cli/src/cli.rs index d3ff549..9e35662 100644 --- a/apps/tui-cli/src/cli.rs +++ b/apps/tui-cli/src/cli.rs @@ -64,6 +64,12 @@ pub struct Cli { #[arg(long)] pub debug: bool, + /// Inference device: auto (default), cpu, metal. + /// `auto` tries Metal on Apple Silicon and falls back to CPU. + /// `metal` forces GPU inference (macOS only). + #[arg(long, default_value = "auto")] + pub device: String, + /// Specific files or directories to review (optional) pub files: Vec, } diff --git a/apps/tui-cli/src/git.rs b/apps/tui-cli/src/git.rs index 3719fa3..af561aa 100644 --- a/apps/tui-cli/src/git.rs +++ b/apps/tui-cli/src/git.rs @@ -11,11 +11,7 @@ pub fn current_branch() -> Option { if output.status.success() { let branch = String::from_utf8_lossy(&output.stdout).trim().to_string(); // "HEAD" means detached state — not useful to show - if branch == "HEAD" { - None - } else { - Some(branch) - } + if branch == "HEAD" { None } else { Some(branch) } } else { None } diff --git a/apps/tui-cli/src/main.rs b/apps/tui-cli/src/main.rs index dd66be1..9c19e41 100644 --- a/apps/tui-cli/src/main.rs +++ b/apps/tui-cli/src/main.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use core_engine::{ReviewAnalyzer, ReviewFinding, Severity}; +use core_engine::{DevicePreference, ReviewAnalyzer, ReviewFinding, Severity}; use indicatif::{ProgressBar, ProgressStyle}; use std::{ collections::HashSet, @@ -83,6 +83,16 @@ async fn main() -> Result<()> { Ok(()) } +// ─── Device helpers ────────────────────────────────────────────────────────── + +fn parse_device(s: &str) -> DevicePreference { + match s.to_lowercase().as_str() { + "metal" => DevicePreference::Metal, + "cpu" => DevicePreference::Cpu, + _ => DevicePreference::Auto, + } +} + // ─── Severity helpers ──────────────────────────────────────────────────────── fn parse_severity(s: &str) -> Severity { @@ -262,14 +272,15 @@ async fn run_static( // ── RAG context ─────────────────────────────────────────────────────────── let index = Indexer::load(project_root); let mut context = String::new(); - if let Some(idx) = index { - if let Some(rag_text) = rag::get_rag_context(diff, &idx) { - context = rag_text; - } + if let Some(idx) = index + && let Some(rag_text) = rag::get_rag_context(diff, &idx) + { + context = rag_text; } // ── Build analyzer ──────────────────────────────────────────────────────── - let mut analyzer = ReviewAnalyzer::new(&model_bytes, &tokenizer_bytes) + let device_pref = parse_device(&args.device); + let mut analyzer = ReviewAnalyzer::new_with_device(&model_bytes, &tokenizer_bytes, device_pref) .map_err(|e| anyhow::anyhow!(e.to_string()))? .with_languages(langs) .with_debug(args.debug); @@ -304,7 +315,7 @@ async fn run_static( }); let pb = spinner.clone(); - let (all_findings, skipped) = analyzer + let (summary, skipped) = analyzer .analyze_diff_chunked_with_progress(diff, &context, args.max_tokens, move |done, total| { *chunk_label.lock().unwrap() = format!("chunk {}/{}", done, total); pb.set_message(format!("Analyzing chunk {}/{}...", done, total)); @@ -323,39 +334,47 @@ async fn run_static( ); } - // ── Filter to threshold ─────────────────────────────────────────────────── - let findings: Vec<&ReviewFinding> = all_findings + // ── Filter findings to threshold ────────────────────────────────────────── + let findings: Vec<&ReviewFinding> = summary + .findings .iter() .filter(|f| meets_threshold(&f.severity, &min_severity)) .collect(); - if findings.is_empty() { - if skipped > 0 { - eprintln!(" ? No parseable findings — try `--model 3b` for better output quality."); - } else { - eprintln!(" ✓ No issues found."); - } - eprintln!(); - return Ok(false); - } - match args.format { cli::OutputFormat::Json => { - // Emit a clean JSON array — pipe-friendly for CI dashboards - let json = serde_json::to_string_pretty(&findings) - .map_err(|e| anyhow::anyhow!(e.to_string()))?; + // Emit the full summary as JSON — pipe-friendly for CI dashboards + let out = serde_json::json!({ + "findings": findings, + "positives": summary.positives, + "suggestions": summary.suggestions, + }); + let json = + serde_json::to_string_pretty(&out).map_err(|e| anyhow::anyhow!(e.to_string()))?; println!("{}", json); } cli::OutputFormat::Text => { println!(); - for (i, f) in findings.iter().enumerate() { - print_finding(f, i + 1, findings.len()); + if findings.is_empty() { + if skipped > 0 { + eprintln!( + " ? No parseable findings — try `--model 3b` for better output quality." + ); + } else { + use crossterm::style::Stylize; + eprintln!(" {} No issues found.", "✓".green().bold()); + } + } else { + for (i, f) in findings.iter().enumerate() { + print_finding(f, i + 1, findings.len()); + } + print_summary(findings.len(), skipped); } - print_summary(findings.len(), skipped); + print_positives_and_suggestions(&summary.positives, &summary.suggestions); } } - Ok(true) + Ok(!findings.is_empty()) } // ─── Coloured finding renderer ──────────────────────────────────────────────── @@ -478,19 +497,45 @@ fn print_summary(count: usize, skipped: usize) { eprintln!(); } +fn print_positives_and_suggestions(positives: &[String], suggestions: &[String]) { + if positives.is_empty() && suggestions.is_empty() { + return; + } + + if !positives.is_empty() { + eprintln!(" {}", "─".repeat(62).dark_grey()); + eprintln!(" {} What looks good", "✓".green().bold()); + for p in positives { + eprintln!(" {} {}", "·".green(), p); + } + eprintln!(); + } + + if !suggestions.is_empty() { + if positives.is_empty() { + eprintln!(" {}", "─".repeat(62).dark_grey()); + } + eprintln!(" 💡 Suggestions"); + for s in suggestions { + eprintln!(" {} {}", "·".dark_yellow(), s); + } + eprintln!(); + } +} + // ─── TUI runner ─────────────────────────────────────────────────────────────── use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use ratatui::{ + Frame, Terminal, backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, - Frame, Terminal, }; struct App { @@ -543,57 +588,60 @@ async fn run_tui( res } -async fn tui_loop(terminal: &mut Terminal, app: Arc>) -> Result<()> { +async fn tui_loop(terminal: &mut Terminal, app: Arc>) -> Result<()> +where + B::Error: Send + Sync + 'static, +{ loop { { let mut app_lock = app.lock().await; terminal.draw(|f| ui(f, &mut app_lock))?; } - if event::poll(Duration::from_millis(100))? { - if let Event::Key(key) = event::read()? { - let mut app_lock = app.lock().await; - match key.code { - KeyCode::Char('q') => return Ok(()), - KeyCode::Down | KeyCode::Char('j') => { - let i = match app_lock.state.selected() { - Some(i) if !app_lock.findings.is_empty() => { - (i + 1) % app_lock.findings.len() - } - _ => 0, - }; - app_lock.state.select(Some(i)); - } - KeyCode::Up | KeyCode::Char('k') => { - let i = match app_lock.state.selected() { - Some(i) if !app_lock.findings.is_empty() => { - if i == 0 { - app_lock.findings.len() - 1 - } else { - i - 1 - } + if event::poll(Duration::from_millis(100))? + && let Event::Key(key) = event::read()? + { + let mut app_lock = app.lock().await; + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Down | KeyCode::Char('j') => { + let i = match app_lock.state.selected() { + Some(i) if !app_lock.findings.is_empty() => { + (i + 1) % app_lock.findings.len() + } + _ => 0, + }; + app_lock.state.select(Some(i)); + } + KeyCode::Up | KeyCode::Char('k') => { + let i = match app_lock.state.selected() { + Some(i) if !app_lock.findings.is_empty() => { + if i == 0 { + app_lock.findings.len() - 1 + } else { + i - 1 } - _ => 0, - }; - app_lock.state.select(Some(i)); - } - KeyCode::Char('a') => { - if !app_lock.analyzing { - app_lock.analyzing = true; - app_lock.status = "Analyzing...".to_string(); - let app_clone = Arc::clone(&app); - tokio::spawn(async move { - let app_err = Arc::clone(&app_clone); - if let Err(e) = background_analysis(app_clone).await { - let mut app = app_err.lock().await; - app.status = format!("Error: {}", e); - app.analyzing = false; - } - }); } + _ => 0, + }; + app_lock.state.select(Some(i)); + } + KeyCode::Char('a') => { + if !app_lock.analyzing { + app_lock.analyzing = true; + app_lock.status = "Analyzing...".to_string(); + let app_clone = Arc::clone(&app); + tokio::spawn(async move { + let app_err = Arc::clone(&app_clone); + if let Err(e) = background_analysis(app_clone).await { + let mut app = app_err.lock().await; + app.status = format!("Error: {}", e); + app.analyzing = false; + } + }); } - _ => {} } + _ => {} } } } @@ -619,28 +667,29 @@ async fn background_analysis(app: Arc>) -> Result<()> { let index = Indexer::load(&project_root); let mut context = String::new(); - if let Some(idx) = index { - if let Some(rag_text) = rag::get_rag_context(&diff, &idx) { - context = rag_text; - } + if let Some(idx) = index + && let Some(rag_text) = rag::get_rag_context(&diff, &idx) + { + context = rag_text; } let langs = detect_languages(&diff); - let mut analyzer = ReviewAnalyzer::new(&model_bytes, &tokenizer_bytes) - .map_err(|e| anyhow::anyhow!(e.to_string()))? - .with_languages(langs); + let mut analyzer = + ReviewAnalyzer::new_with_device(&model_bytes, &tokenizer_bytes, DevicePreference::Auto) + .map_err(|e| anyhow::anyhow!(e.to_string()))? + .with_languages(langs); if let Some(req) = ticket { analyzer = analyzer.with_requirements(req); } - let findings = analyzer + let summary = analyzer .analyze_diff_chunked(&diff, &context, 1024) .map_err(|e| anyhow::anyhow!(e.to_string()))?; let mut app_lock = app.lock().await; - let count = findings.len(); - app_lock.findings = findings; + let count = summary.findings.len(); + app_lock.findings = summary.findings; app_lock.status = format!( "Done — {} finding{}", count, diff --git a/apps/tui-cli/src/rag.rs b/apps/tui-cli/src/rag.rs index cd0d4be..21e5051 100644 --- a/apps/tui-cli/src/rag.rs +++ b/apps/tui-cli/src/rag.rs @@ -1,4 +1,4 @@ -use crate::indexer::{SymbolIndex, COMMON_KEYWORDS}; +use crate::indexer::{COMMON_KEYWORDS, SymbolIndex}; use regex::Regex; const MAX_CONTEXT_BYTES: usize = 3000; diff --git a/packages/core-engine/Cargo.toml b/packages/core-engine/Cargo.toml index fecf44e..5b930f4 100644 --- a/packages/core-engine/Cargo.toml +++ b/packages/core-engine/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "core-engine" -version = "0.6.2" -edition = "2021" +version = "0.6.3" +edition = "2024" description = "diffmind shared AI engine core" [dependencies] @@ -11,6 +11,13 @@ candle-transformers = { workspace = true } tokenizers = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -thiserror = "1.0" +thiserror = "2.0.18" log = "0.4" rand = { workspace = true } + +# On macOS, enable Metal (Apple Silicon GPU) and Accelerate (CPU BLAS). +# Cargo merges features — these are additive on top of the workspace dep. +[target.'cfg(target_os = "macos")'.dependencies] +candle-core = { version = "0.10.2", features = ["metal", "accelerate"] } +candle-nn = { version = "0.10.2", features = ["metal", "accelerate"] } +candle-transformers = { version = "0.10.2", features = ["metal"] } diff --git a/packages/core-engine/src/lib.rs b/packages/core-engine/src/lib.rs index 09d093f..dfdb21d 100644 --- a/packages/core-engine/src/lib.rs +++ b/packages/core-engine/src/lib.rs @@ -56,6 +56,22 @@ pub struct ReviewFinding { pub confidence: Option, } +/// The complete result of analyzing one or more diff chunks. +/// Always populated — even when there are no bug findings, `positives` and +/// `suggestions` let the reviewer know what looks good and what could improve. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ReviewSummary { + /// Bugs, vulnerabilities, and code quality issues (may be empty). + #[serde(default)] + pub findings: Vec, + /// Things done well in this diff — at least one entry when the code is reasonable. + #[serde(default)] + pub positives: Vec, + /// Low-priority optional improvements that are not bugs. + #[serde(default)] + pub suggestions: Vec, +} + // ─── ReviewAnalyzer ────────────────────────────────────────────────────────── pub struct ReviewAnalyzer { @@ -77,9 +93,68 @@ pub struct ReviewAnalyzer { /// candle panic or garbage output with no clean error. const MAX_CONTEXT_TOKENS: usize = 4096; +/// Which compute device to use for inference. +#[derive(Debug, Clone, Default)] +pub enum DevicePreference { + /// Try Metal on macOS, fall back to CPU everywhere else. + #[default] + Auto, + /// Force CPU inference on all platforms. + Cpu, + /// Force Metal (macOS / Apple Silicon). Returns an error on other platforms. + Metal, +} + +/// Select the best available device according to the caller's preference. +/// Prints a one-line status to stderr so the user knows what's being used. +pub fn resolve_device(pref: &DevicePreference) -> Result { + match pref { + DevicePreference::Cpu => Ok(Device::Cpu), + + DevicePreference::Metal => { + #[cfg(target_os = "macos")] + { + Device::new_metal(0) + .map_err(|e| EngineError::ModelLoadError(format!("Metal unavailable: {e}"))) + } + #[cfg(not(target_os = "macos"))] + Err(EngineError::ModelLoadError( + "Metal is only available on macOS".into(), + )) + } + + DevicePreference::Auto => { + #[cfg(target_os = "macos")] + { + match Device::new_metal(0) { + Ok(d) => { + eprintln!(" Device Metal (Apple Silicon GPU)"); + return Ok(d); + } + Err(_) => { + eprintln!(" Device CPU (Metal unavailable, using Accelerate BLAS)"); + } + } + } + #[cfg(not(target_os = "macos"))] + eprintln!(" Device CPU"); + + Ok(Device::Cpu) + } + } +} + impl ReviewAnalyzer { pub fn new(model_bytes: &[u8], tokenizer_bytes: &[u8]) -> Result { - let device = Device::Cpu; + Self::new_with_device(model_bytes, tokenizer_bytes, DevicePreference::Auto) + } + + pub fn new_with_device( + model_bytes: &[u8], + tokenizer_bytes: &[u8], + device_pref: DevicePreference, + ) -> Result { + let device = resolve_device(&device_pref)?; let tokenizer = tokenizers::Tokenizer::from_bytes(tokenizer_bytes) .map_err(|e| EngineError::TokenizerError(e.to_string()))?; @@ -136,7 +211,7 @@ impl ReviewAnalyzer { /// Like [`analyze_diff_chunked`] but calls `on_progress(done, total)` after /// each chunk completes so callers can display a live progress indicator. /// - /// Returns `(findings, skipped)` where `skipped` is the number of chunks + /// Returns `(summary, skipped)` where `skipped` is the number of chunks /// the model processed but returned unparseable JSON for — useful for /// surfacing silent failures to the user. pub fn analyze_diff_chunked_with_progress( @@ -145,13 +220,15 @@ impl ReviewAnalyzer { context: &str, max_tokens_per_chunk: u32, on_progress: F, - ) -> Result<(Vec, usize), EngineError> + ) -> Result<(ReviewSummary, usize), EngineError> where F: Fn(usize, usize), { // Run deterministic rules first — catches patterns the model reliably misses. - let mut all_findings = detect_commented_out_code(diff); - all_findings.extend(detect_removed_used_variables(diff)); + let det_findings: Vec = detect_commented_out_code(diff) + .into_iter() + .chain(detect_removed_used_variables(diff)) + .collect(); const MAX_CHUNK_LINES: usize = 300; let chunks = chunk_diff(diff, MAX_CHUNK_LINES); @@ -160,6 +237,10 @@ impl ReviewAnalyzer { let mut done = 0; let mut skipped = 0; + let mut all_findings = det_findings; + let mut all_positives: Vec = Vec::new(); + let mut all_suggestions: Vec = Vec::new(); + for chunk in chunks { if chunk.trim().is_empty() { continue; @@ -167,7 +248,11 @@ impl ReviewAnalyzer { done += 1; on_progress(done, total); match self.analyze_diff_internal(&chunk, context, max_tokens_per_chunk as usize) { - Ok(findings) => all_findings.extend(findings), + Ok(summary) => { + all_findings.extend(summary.findings); + all_positives.extend(summary.positives); + all_suggestions.extend(summary.suggestions); + } // Small models often produce malformed JSON for a single chunk. // Count skips so the caller can warn the user instead of silently // returning "no issues". @@ -176,7 +261,18 @@ impl ReviewAnalyzer { } } - Ok((all_findings, skipped)) + // Deduplicate positives/suggestions that repeat across chunks. + all_positives.dedup(); + all_suggestions.dedup(); + + Ok(( + ReviewSummary { + findings: all_findings, + positives: all_positives, + suggestions: all_suggestions, + }, + skipped, + )) } pub fn analyze_diff_chunked( @@ -184,9 +280,9 @@ impl ReviewAnalyzer { diff: &str, context: &str, max_tokens_per_chunk: u32, - ) -> Result, EngineError> { + ) -> Result { self.analyze_diff_chunked_with_progress(diff, context, max_tokens_per_chunk, |_, _| {}) - .map(|(findings, _)| findings) + .map(|(summary, _)| summary) } fn analyze_diff_internal( @@ -194,9 +290,9 @@ impl ReviewAnalyzer { diff: &str, context: &str, max_new_tokens: usize, - ) -> Result, EngineError> { + ) -> Result { if diff.trim().is_empty() { - return Ok(Vec::new()); + return Ok(ReviewSummary::default()); } let prompt = self.format_prompt(diff, context); @@ -224,28 +320,42 @@ impl ReviewAnalyzer { eprintln!("[debug] raw model output:\n{}\n", response); } - let json_start = response.find('[').unwrap_or(0); - let json_end = response - .rfind(']') - .unwrap_or_else(|| response.len().saturating_sub(1)); - - if json_end <= json_start || json_end >= response.len() { + // Try the primary format: {"findings": [...], "positives": [...], "suggestions": [...]} + if let Some(start) = response.find('{') + && let Some(end) = response.rfind('}') + && end > start + { + let slice = &response[start..=end]; if self.debug { - eprintln!("[debug] no valid JSON array found in output"); + eprintln!("[debug] extracted JSON object slice:\n{}\n", slice); + } + if let Ok(summary) = serde_json::from_str::(slice) { + return Ok(summary); } - return Ok(Vec::new()); } - let json_slice = &response[json_start..=json_end]; + // Fallback: bare array (old format / model didn't follow new schema) + if let Some(start) = response.find('[') + && let Some(end) = response.rfind(']') + && end > start + { + let slice = &response[start..=end]; + if self.debug { + eprintln!("[debug] fallback: extracted JSON array slice:\n{}\n", slice); + } + let findings: Vec = serde_json::from_str(slice) + .map_err(|e| EngineError::SerializationError(e.to_string()))?; + return Ok(ReviewSummary { + findings, + positives: Vec::new(), + suggestions: Vec::new(), + }); + } if self.debug { - eprintln!("[debug] extracted JSON slice:\n{}\n", json_slice); + eprintln!("[debug] no valid JSON found in output"); } - - let findings: Vec = serde_json::from_str(json_slice) - .map_err(|e| EngineError::SerializationError(e.to_string()))?; - - Ok(findings) + Ok(ReviewSummary::default()) } fn format_prompt(&self, diff: &str, context: &str) -> String { @@ -290,13 +400,8 @@ impl ReviewAnalyzer { }; format!( - "<|im_start|>system\nYou are an expert Senior Software Engineer and Code Reviewer. Analyze the git diff and provide a comprehensive code review for {} code.{}{} \n\nFocus on:\n1. **Security**: Vulnerabilities, exposed secrets, insecure handling, disabled auth or validation.\n2. **Quality**: Bugs, anti-patterns, logical errors, commented-out functions or blocks of code (flag as medium severity), dead code left behind.\n3. **Performance**: Bottlenecks, inefficient algorithms or queries.\n4. **Maintainability**: Hard-to-read code, poor naming, high complexity, TODOs left in production code.{}\n\nIMPORTANT: If a function, method, or block has been commented out in the diff (lines starting with // or /* that previously were code), always flag it — commented-out code is a quality issue even if the change looks small.\n\nReturn a JSON array ONLY. Format: [{{ \"file\": \"path\", \"line\": 12, \"severity\": \"high\"|\"medium\"|\"low\", \"category\": {}, \"issue\": \"description\", \"suggested_fix\": \"code\" }}]\nIf genuinely no issues exist, return [].<|im_end|>\n<|im_start|>user\nAnalyze this diff:\n{}\n<|im_end|>\n<|im_start|>assistant\n", - stack, - requirements_section, - context_section, - compliance_focus, - category_hint, - diff + "<|im_start|>system\nYou are an expert Senior Software Engineer and Code Reviewer. Analyze the git diff and provide a thorough code review for {} code.{}{}\n\nFocus on:\n1. **Security**: Vulnerabilities, exposed secrets, insecure handling, disabled auth or validation.\n2. **Quality**: Bugs, anti-patterns, logical errors, commented-out functions or blocks of code (flag as medium severity), dead code left behind.\n3. **Performance**: Bottlenecks, inefficient algorithms or queries.\n4. **Maintainability**: Hard-to-read code, poor naming, high complexity, TODOs left in production code.{}\n\nIMPORTANT: If a function, method, or block has been commented out in the diff (lines starting with // or /* that previously were code), always flag it — commented-out code is a quality issue even if the change looks small.\n\nReturn a JSON object ONLY with exactly this structure:\n{{\n \"findings\": [{{ \"file\": \"path\", \"line\": 12, \"severity\": \"high\"|\"medium\"|\"low\", \"category\": {}, \"issue\": \"description\", \"suggested_fix\": \"fix\" }}],\n \"positives\": [\"one sentence describing something done well\"],\n \"suggestions\": [\"one sentence optional improvement that is not a bug\"]\n}}\n\nRules:\n- findings: real issues only. Use [] if there are none.\n- positives: always include 1-3 things done well (clean logic, good naming, proper error handling, etc.). Never leave this empty.\n- suggestions: nice-to-have improvements (tests, docs, refactors). Use [] if nothing comes to mind.\n- Keep each positive/suggestion to one concise sentence.<|im_end|>\n<|im_start|>user\nAnalyze this diff:\n{}\n<|im_end|>\n<|im_start|>assistant\n", + stack, requirements_section, context_section, compliance_focus, category_hint, diff ) } @@ -364,7 +469,7 @@ impl ReviewAnalyzer { return Err(EngineError::ForwardError(format!( "unexpected logits shape: {:?}", logits.dims() - ))) + ))); } }; @@ -396,16 +501,19 @@ impl ReviewAnalyzer { } } -/// Returns `true` once `s` contains a syntactically-complete JSON array -/// (`[…]` with balanced brackets, respecting string literals and escapes). +/// Returns `true` once `s` contains a syntactically-complete JSON value +/// (object `{…}` or array `[…]`) with balanced brackets, respecting string +/// literals and escapes. /// -/// This lets `generate()` exit early as soon as the model has closed the -/// answer array, instead of running until the token cap. +/// This lets `generate()` exit early as soon as the model has closed its +/// answer, instead of running until the token cap. fn json_array_complete(s: &str) -> bool { let trimmed = s.trim_start(); - if !trimmed.starts_with('[') { - return false; - } + let (open, close) = match trimmed.chars().next() { + Some('{') => ('{', '}'), + Some('[') => ('[', ']'), + _ => return false, + }; let mut depth: i32 = 0; let mut in_string = false; let mut escape = false; @@ -417,8 +525,8 @@ fn json_array_complete(s: &str) -> bool { match ch { '\\' if in_string => escape = true, '"' => in_string = !in_string, - '[' if !in_string => depth += 1, - ']' if !in_string => { + c if c == open && !in_string => depth += 1, + c if c == close && !in_string => { depth -= 1; if depth == 0 { return true; @@ -591,10 +699,10 @@ pub fn detect_commented_out_code(diff: &str) -> Vec { flush(&mut hunk, &mut findings); hunk.file = current_file.clone(); // Parse the new-file start line from `@@ -a,b +c,d @@` - if let Some(new_part) = line.split('+').nth(1) { - if let Some(num) = new_part.split(',').next() { - hunk.start_line = num.trim().parse().unwrap_or(1); - } + if let Some(new_part) = line.split('+').nth(1) + && let Some(num) = new_part.split(',').next() + { + hunk.start_line = num.trim().parse().unwrap_or(1); } } else if line.starts_with('-') && !line.starts_with("---") { let code = line[1..].trim().to_string(); @@ -679,12 +787,12 @@ pub fn detect_removed_used_variables(diff: &str) -> Vec { current_file = b.trim().to_string(); } } else if line.starts_with("@@") { - if let Some(new_part) = line.split('+').nth(1) { - if let Some(num) = new_part.split(',').next() { - let hunk_start: u32 = num.trim().parse().unwrap_or(1); - if file_lines.is_empty() { - file_start_line = hunk_start; - } + if let Some(new_part) = line.split('+').nth(1) + && let Some(num) = new_part.split(',').next() + { + let hunk_start: u32 = num.trim().parse().unwrap_or(1); + if file_lines.is_empty() { + file_start_line = hunk_start; } } } else if line.starts_with('-') && !line.starts_with("---") {