From e0d686beaf73ed36f22b3964c521a108f448c3bd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 21:02:44 +0000 Subject: [PATCH 1/8] fix: harden parser panic paths Co-authored-by: EvalOpsBot --- src/core/diff_parser.rs | 28 ++++++++++++--- src/parsing/llm_response.rs | 50 +++++++++++++++++++++----- src/plugins/builtin/secret_scanner.rs | 4 ++- src/review/verification/parser/text.rs | 14 ++------ 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/core/diff_parser.rs b/src/core/diff_parser.rs index 24329b8..0671a2b 100644 --- a/src/core/diff_parser.rs +++ b/src/core/diff_parser.rs @@ -450,15 +450,35 @@ impl DiffParser { .captures(header) .ok_or_else(|| anyhow::anyhow!("Invalid hunk header: {}", header))?; - let old_start = caps.get(1).unwrap().as_str().parse()?; - let old_lines = caps.get(2).map_or(1, |m| m.as_str().parse().unwrap_or(1)); - let new_start = caps.get(3).unwrap().as_str().parse()?; - let new_lines = caps.get(4).map_or(1, |m| m.as_str().parse().unwrap_or(1)); + let old_start = parse_required_capture(&caps, 1, header)?; + let old_lines = parse_optional_capture(&caps, 2).unwrap_or(1); + let new_start = parse_required_capture(&caps, 3, header)?; + let new_lines = parse_optional_capture(&caps, 4).unwrap_or(1); Ok((old_start, old_lines, new_start, new_lines)) } } +fn parse_required_capture( + captures: ®ex::Captures<'_>, + group: usize, + header: &str, +) -> Result { + captures + .get(group) + .ok_or_else(|| anyhow::anyhow!("Missing hunk header capture group {}: {}", group, header))? + .as_str() + .parse() + .map_err(Into::into) +} + +fn parse_optional_capture(captures: ®ex::Captures<'_>, group: usize) -> Option { + captures + .get(group) + .and_then(|value| value.as_str().parse::().ok()) + .filter(|value| *value > 0) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/parsing/llm_response.rs b/src/parsing/llm_response.rs index 024064a..a132656 100644 --- a/src/parsing/llm_response.rs +++ b/src/parsing/llm_response.rs @@ -122,9 +122,14 @@ fn parse_primary(content: &str, file_path: &Path) -> Result Vec() { - let text = caps.get(2).unwrap().as_str().trim().to_string(); + if let Some(line_number) = capture_usize_lossy(&caps, 1) { + let Some(text) = capture_text(&caps, 2) else { + continue; + }; + let text = text.trim().to_string(); comments.push(make_raw_comment(file_path, line_number, text)); } } @@ -205,8 +213,11 @@ fn parse_markdown_bullets(content: &str, file_path: &Path) -> Vec() { - let text = caps.get(2).unwrap().as_str().trim().to_string(); + if let Some(line_number) = capture_usize_lossy(&caps, 1) { + let Some(text) = capture_text(&caps, 2) else { + continue; + }; + let text = text.trim().to_string(); comments.push(make_raw_comment(file_path, line_number, text)); } } @@ -226,8 +237,11 @@ fn parse_file_line_format(content: &str, file_path: &Path) -> Vec() { - let text = caps.get(2).unwrap().as_str().trim().to_string(); + if let Some(line_number) = capture_usize_lossy(&caps, 1) { + let Some(text) = capture_text(&caps, 2) else { + continue; + }; + let text = text.trim().to_string(); comments.push(make_raw_comment(file_path, line_number, text)); } } @@ -267,7 +281,10 @@ fn extract_json_from_code_block(content: &str) -> Option { Lazy::new(|| Regex::new(r"(?s)```(?:json)?\s*\n(.*?)```").unwrap()); for caps in CODE_BLOCK.captures_iter(content) { - let block = caps.get(1).unwrap().as_str().trim(); + let Some(block) = capture_text(&caps, 1) else { + continue; + }; + let block = block.trim(); if block.starts_with('[') || block.starts_with('{') { return Some(block.to_string()); } @@ -580,6 +597,21 @@ fn make_raw_comment( } } +fn capture_text<'a>(captures: &'a regex::Captures<'_>, group: usize) -> Option<&'a str> { + captures.get(group).map(|value| value.as_str()) +} + +fn capture_usize(captures: ®ex::Captures<'_>, group: usize) -> Result> { + capture_text(captures, group) + .map(|value| value.parse::()) + .transpose() + .map_err(Into::into) +} + +fn capture_usize_lossy(captures: ®ex::Captures<'_>, group: usize) -> Option { + capture_text(captures, group).and_then(|value| value.parse::().ok()) +} + /// Build a unified-diff-style string from original and suggested code. fn build_suggestion_diff(original: &str, suggested: &str) -> String { let mut diff = String::new(); diff --git a/src/plugins/builtin/secret_scanner.rs b/src/plugins/builtin/secret_scanner.rs index f4ad344..47c3778 100644 --- a/src/plugins/builtin/secret_scanner.rs +++ b/src/plugins/builtin/secret_scanner.rs @@ -560,7 +560,9 @@ impl SecretScanner { let re: &Regex = pattern.regex; for caps in re.captures_iter(line) { // Use the first capture group if it exists, otherwise the full match - let matched = caps.get(1).unwrap_or_else(|| caps.get(0).unwrap()); + let Some(matched) = caps.get(1).or_else(|| caps.get(0)) else { + continue; + }; let value = matched.as_str(); // False positive checks diff --git a/src/review/verification/parser/text.rs b/src/review/verification/parser/text.rs index d34d548..f307a3a 100644 --- a/src/review/verification/parser/text.rs +++ b/src/review/verification/parser/text.rs @@ -22,19 +22,11 @@ fn parse_finding_line(line: &str, comments: &[Comment]) -> Option Date: Fri, 13 Mar 2026 21:03:01 +0000 Subject: [PATCH 2/8] refactor: group config domains and trim path modules Co-authored-by: EvalOpsBot --- src/commands/eval/command.rs | 4 +- src/commands/eval/command/batch.rs | 2 +- src/commands/{git.rs => git/mod.rs} | 2 - src/commands/misc/lsp_check.rs | 8 - src/commands/misc/lsp_check/mod.rs | 5 + src/commands/{misc.rs => misc/mod.rs} | 4 - .../review/{command.rs => command/mod.rs} | 3 - src/commands/{review.rs => review/mod.rs} | 2 - src/commands/smart_review.rs | 6 - src/commands/smart_review/mod.rs | 4 + src/config.rs | 305 ++++++++++-------- .../pipeline/execution/dispatcher/context.rs | 6 +- .../pipeline/execution/dispatcher/run.rs | 2 +- .../pipeline/postprocess/verification.rs | 8 +- src/review/pipeline/services/adapter_init.rs | 4 +- src/server/api.rs | 13 +- src/server/github.rs | 21 +- 17 files changed, 220 insertions(+), 179 deletions(-) rename src/commands/{git.rs => git/mod.rs} (84%) delete mode 100644 src/commands/misc/lsp_check.rs create mode 100644 src/commands/misc/lsp_check/mod.rs rename src/commands/{misc.rs => misc/mod.rs} (63%) rename src/commands/review/{command.rs => command/mod.rs} (59%) rename src/commands/{review.rs => review/mod.rs} (69%) delete mode 100644 src/commands/smart_review.rs create mode 100644 src/commands/smart_review/mod.rs diff --git a/src/commands/eval/command.rs b/src/commands/eval/command.rs index 511c604..413172c 100644 --- a/src/commands/eval/command.rs +++ b/src/commands/eval/command.rs @@ -24,7 +24,7 @@ pub async fn eval_command( output_path: Option, options: EvalRunOptions, ) -> Result<()> { - config.verification_fail_open = true; + config.verification.fail_open = true; if options.repeat > 1 || !options.matrix_models.is_empty() { return run_eval_batch(config, &fixtures_dir, output_path.as_deref(), &options).await; } @@ -87,7 +87,7 @@ fn build_eval_run_metadata( fixture_name_filters: options.fixture_name_filters.clone(), max_fixtures: options.max_fixtures, }, - verification_fail_open: config.verification_fail_open, + verification_fail_open: config.verification.fail_open, trend_file: options .trend_file .as_ref() diff --git a/src/commands/eval/command/batch.rs b/src/commands/eval/command/batch.rs index f900846..90333ee 100644 --- a/src/commands/eval/command/batch.rs +++ b/src/commands/eval/command/batch.rs @@ -38,7 +38,7 @@ pub(super) async fn run_eval_batch( output_path: Option<&Path>, options: &EvalRunOptions, ) -> Result<()> { - config.verification_fail_open = true; + config.verification.fail_open = true; let prepared_options = prepare_eval_options(options)?; let models = matrix_models(&config, options); let repeat_total = options.repeat.max(1); diff --git a/src/commands/git.rs b/src/commands/git/mod.rs similarity index 84% rename from src/commands/git.rs rename to src/commands/git/mod.rs index 88ade18..dee6f62 100644 --- a/src/commands/git.rs +++ b/src/commands/git/mod.rs @@ -1,8 +1,6 @@ use clap::Subcommand; -#[path = "git/command.rs"] mod command; -#[path = "git/suggest.rs"] mod suggest; #[derive(Subcommand)] diff --git a/src/commands/misc/lsp_check.rs b/src/commands/misc/lsp_check.rs deleted file mode 100644 index 8d3fbad..0000000 --- a/src/commands/misc/lsp_check.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[path = "lsp_check/command.rs"] -mod command; -#[path = "lsp_check/extensions.rs"] -mod extensions; -#[path = "lsp_check/languages.rs"] -mod languages; - -pub use command::lsp_check_command; diff --git a/src/commands/misc/lsp_check/mod.rs b/src/commands/misc/lsp_check/mod.rs new file mode 100644 index 0000000..3cb9b67 --- /dev/null +++ b/src/commands/misc/lsp_check/mod.rs @@ -0,0 +1,5 @@ +mod command; +mod extensions; +mod languages; + +pub use command::lsp_check_command; diff --git a/src/commands/misc.rs b/src/commands/misc/mod.rs similarity index 63% rename from src/commands/misc.rs rename to src/commands/misc/mod.rs index b91d1c2..84909f6 100644 --- a/src/commands/misc.rs +++ b/src/commands/misc/mod.rs @@ -1,10 +1,6 @@ -#[path = "misc/changelog.rs"] mod changelog; -#[path = "misc/discussion.rs"] mod discussion; -#[path = "misc/feedback.rs"] mod feedback; -#[path = "misc/lsp_check.rs"] mod lsp_check; pub use changelog::changelog_command; diff --git a/src/commands/review/command.rs b/src/commands/review/command/mod.rs similarity index 59% rename from src/commands/review/command.rs rename to src/commands/review/command/mod.rs index 1751f22..c6cc88b 100644 --- a/src/commands/review/command.rs +++ b/src/commands/review/command/mod.rs @@ -1,8 +1,5 @@ -#[path = "command/check.rs"] mod check; -#[path = "command/compare.rs"] mod compare; -#[path = "command/review.rs"] mod review; pub use check::check_command; diff --git a/src/commands/review.rs b/src/commands/review/mod.rs similarity index 69% rename from src/commands/review.rs rename to src/commands/review/mod.rs index 8537d09..362ce30 100644 --- a/src/commands/review.rs +++ b/src/commands/review/mod.rs @@ -1,6 +1,4 @@ -#[path = "review/command.rs"] mod command; -#[path = "review/input.rs"] mod input; pub use command::{check_command, compare_command, review_command}; diff --git a/src/commands/smart_review.rs b/src/commands/smart_review.rs deleted file mode 100644 index c4182f9..0000000 --- a/src/commands/smart_review.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[path = "smart_review/command.rs"] -mod command; -#[path = "smart_review/summary.rs"] -mod summary; - -pub use command::smart_review_command; diff --git a/src/commands/smart_review/mod.rs b/src/commands/smart_review/mod.rs new file mode 100644 index 0000000..2e9b25c --- /dev/null +++ b/src/commands/smart_review/mod.rs @@ -0,0 +1,4 @@ +mod command; +mod summary; + +pub use command::smart_review_command; diff --git a/src/config.rs b/src/config.rs index 892b37a..a2f56f3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -45,6 +45,137 @@ impl Default for ProviderConfig { } } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct VaultConfig { + /// HashiCorp Vault server address (e.g., https://vault.example.com:8200). + #[serde(default, rename = "vault_addr")] + pub addr: Option, + + /// Vault authentication token. + #[serde(default, rename = "vault_token")] + pub token: Option, + + /// Secret path in Vault (e.g., "diffscope" or "ci/diffscope"). + #[serde(default, rename = "vault_path")] + pub path: Option, + + /// Key within the Vault secret to extract as the API key (default: "api_key"). + #[serde(default, rename = "vault_key")] + pub key: Option, + + /// Vault KV engine mount point (default: "secret"). + #[serde(default, rename = "vault_mount")] + pub mount: Option, + + /// Vault Enterprise namespace. + #[serde(default, rename = "vault_namespace")] + pub namespace: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct GitHubConfig { + #[serde(default, rename = "github_token")] + pub token: Option, + + /// GitHub App ID (from app settings page). + #[serde(default, rename = "github_app_id")] + pub app_id: Option, + + /// GitHub App OAuth client ID (for device flow auth). + #[serde(default, rename = "github_client_id")] + pub client_id: Option, + + /// GitHub App OAuth client secret. + #[serde(default, rename = "github_client_secret")] + pub client_secret: Option, + + /// GitHub App private key (PEM content). + #[serde(default, rename = "github_private_key")] + pub private_key: Option, + + /// Webhook secret for verifying GitHub webhook signatures. + #[serde(default, rename = "github_webhook_secret")] + pub webhook_secret: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentConfig { + /// Enable agent loop for iterative tool-calling review (default false). + #[serde(default, rename = "agent_review")] + pub enabled: bool, + + /// Maximum number of LLM round-trips in agent mode (default 10). + #[serde( + default = "default_agent_max_iterations", + rename = "agent_max_iterations" + )] + pub max_iterations: usize, + + /// Optional total token budget for agent loop. + #[serde(default, rename = "agent_max_total_tokens")] + pub max_total_tokens: Option, + + /// Which agent tools are enabled. None = all tools enabled. + #[serde(default, rename = "agent_tools_enabled")] + pub tools_enabled: Option>, +} + +impl Default for AgentConfig { + fn default() -> Self { + Self { + enabled: false, + max_iterations: default_agent_max_iterations(), + max_total_tokens: None, + tools_enabled: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VerificationConfig { + /// Whether to run the verification pass on review comments (default true). + #[serde(default = "default_true", rename = "verification_pass")] + pub enabled: bool, + + /// Which model role to use for the verification pass (default Weak). + #[serde( + default = "default_verification_model_role", + rename = "verification_model_role" + )] + pub model_role: ModelRole, + + /// Minimum verification score to keep a comment (0-10, default 5). + #[serde( + default = "default_verification_min_score", + rename = "verification_min_score" + )] + pub min_score: u8, + + /// Maximum number of comments to send through verification (default 20). + #[serde( + default = "default_verification_max_comments", + rename = "verification_max_comments" + )] + pub max_comments: usize, + + /// When true, keep original comments if the verification pass fails or + /// returns an unparseable response (default false). + #[serde(default = "default_false", rename = "verification_fail_open")] + pub fail_open: bool, +} + +impl Default for VerificationConfig { + fn default() -> Self { + Self { + enabled: true, + model_role: default_verification_model_role(), + min_score: default_verification_min_score(), + max_comments: default_verification_max_comments(), + fail_open: false, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { #[serde(default = "default_model")] @@ -195,29 +326,8 @@ pub struct Config { #[serde(default = "default_feedback_suppression_margin")] pub feedback_suppression_margin: usize, - /// HashiCorp Vault server address (e.g., https://vault.example.com:8200). - #[serde(default)] - pub vault_addr: Option, - - /// Vault authentication token. - #[serde(default)] - pub vault_token: Option, - - /// Secret path in Vault (e.g., "diffscope" or "ci/diffscope"). - #[serde(default)] - pub vault_path: Option, - - /// Key within the Vault secret to extract as the API key (default: "api_key"). - #[serde(default)] - pub vault_key: Option, - - /// Vault KV engine mount point (default: "secret"). - #[serde(default)] - pub vault_mount: Option, - - /// Vault Enterprise namespace. - #[serde(default)] - pub vault_namespace: Option, + #[serde(default, flatten)] + pub vault: VaultConfig, #[serde(default)] pub plugins: PluginConfig, @@ -246,70 +356,19 @@ pub struct Config { #[serde(default)] pub providers: HashMap, - #[serde(default)] - pub github_token: Option, - - /// GitHub App ID (from app settings page). - #[serde(default)] - pub github_app_id: Option, - - /// GitHub App OAuth client ID (for device flow auth). - #[serde(default)] - pub github_client_id: Option, - - /// GitHub App OAuth client secret. - #[serde(default)] - pub github_client_secret: Option, - - /// GitHub App private key (PEM content). - #[serde(default)] - pub github_private_key: Option, - - /// Webhook secret for verifying GitHub webhook signatures. - #[serde(default)] - pub github_webhook_secret: Option, + #[serde(default, flatten)] + pub github: GitHubConfig, /// When true, run separate specialized LLM passes for security, correctness, /// and style instead of a single monolithic review prompt. #[serde(default = "default_false")] pub multi_pass_specialized: bool, - /// Enable agent loop for iterative tool-calling review (default false). - #[serde(default)] - pub agent_review: bool, - - /// Maximum number of LLM round-trips in agent mode (default 10). - #[serde(default = "default_agent_max_iterations")] - pub agent_max_iterations: usize, - - /// Optional total token budget for agent loop. - #[serde(default)] - pub agent_max_total_tokens: Option, + #[serde(default, flatten)] + pub agent: AgentConfig, - /// Which agent tools are enabled. None = all tools enabled. - #[serde(default)] - pub agent_tools_enabled: Option>, - - /// Whether to run the verification pass on review comments (default true). - #[serde(default = "default_true")] - pub verification_pass: bool, - - /// Which model role to use for the verification pass (default Weak). - #[serde(default = "default_verification_model_role")] - pub verification_model_role: ModelRole, - - /// Minimum verification score to keep a comment (0-10, default 5). - #[serde(default = "default_verification_min_score")] - pub verification_min_score: u8, - - /// Maximum number of comments to send through verification (default 20). - #[serde(default = "default_verification_max_comments")] - pub verification_max_comments: usize, - - /// When true, keep original comments if the verification pass fails or - /// returns an unparseable response (default false). - #[serde(default = "default_false")] - pub verification_fail_open: bool, + #[serde(default, flatten)] + pub verification: VerificationConfig, /// Enable enhanced feedback loop with per-category/file-pattern tracking /// and feedback-adjusted confidence scores (default false). @@ -481,12 +540,7 @@ impl Default for Config { include_fix_suggestions: true, feedback_suppression_threshold: default_feedback_suppression_threshold(), feedback_suppression_margin: default_feedback_suppression_margin(), - vault_addr: None, - vault_token: None, - vault_path: None, - vault_key: None, - vault_mount: None, - vault_namespace: None, + vault: VaultConfig::default(), plugins: PluginConfig::default(), exclude_patterns: default_exclude_patterns(), paths: HashMap::new(), @@ -496,22 +550,10 @@ impl Default for Config { max_active_rules: default_max_active_rules(), rule_priority: Vec::new(), providers: HashMap::new(), - github_token: None, - github_app_id: None, - github_client_id: None, - github_client_secret: None, - github_private_key: None, - github_webhook_secret: None, + github: GitHubConfig::default(), multi_pass_specialized: false, - agent_review: false, - agent_max_iterations: default_agent_max_iterations(), - agent_max_total_tokens: None, - agent_tools_enabled: None, - verification_pass: true, - verification_model_role: default_verification_model_role(), - verification_min_score: default_verification_min_score(), - verification_max_comments: default_verification_max_comments(), - verification_fail_open: false, + agent: AgentConfig::default(), + verification: VerificationConfig::default(), enhanced_feedback: false, feedback_min_observations: default_feedback_min_observations(), semantic_rag: false, @@ -638,25 +680,25 @@ impl Config { self.output_language = Some(v); } if let Some(v) = cli.vault_addr { - self.vault_addr = Some(v); + self.vault.addr = Some(v); } if let Some(v) = cli.vault_path { - self.vault_path = Some(v); + self.vault.path = Some(v); } if let Some(v) = cli.vault_key { - self.vault_key = Some(v); + self.vault.key = Some(v); } if cli.agent_review { - self.agent_review = true; + self.agent.enabled = true; } if let Some(v) = cli.agent_max_iterations { - self.agent_max_iterations = v; + self.agent.max_iterations = v; } if let Some(v) = cli.agent_max_total_tokens { - self.agent_max_total_tokens = Some(v); + self.agent.max_total_tokens = Some(v); } if let Some(v) = cli.verification_pass { - self.verification_pass = v; + self.verification.enabled = v; } } @@ -675,13 +717,13 @@ impl Config { } // Env var fallbacks for GitHub integration - if self.github_token.is_none() { - self.github_token = std::env::var("GITHUB_TOKEN") + if self.github.token.is_none() { + self.github.token = std::env::var("GITHUB_TOKEN") .ok() .filter(|s| !s.trim().is_empty()); } - if self.github_webhook_secret.is_none() { - self.github_webhook_secret = std::env::var("DIFFSCOPE_WEBHOOK_SECRET") + if self.github.webhook_secret.is_none() { + self.github.webhook_secret = std::env::var("DIFFSCOPE_WEBHOOK_SECRET") .ok() .filter(|s| !s.trim().is_empty()); } @@ -987,7 +1029,10 @@ impl Config { for (pattern, config) in &self.paths { if self.path_matches(&file_path_str, pattern) { // Keep the most specific match (longest pattern) - if best_match.is_none() || pattern.len() > best_match.unwrap().0.len() { + if best_match + .as_ref() + .is_none_or(|(best_pattern, _)| pattern.len() > best_pattern.len()) + { best_match = Some((pattern, config)); } } @@ -1162,12 +1207,12 @@ impl Config { } let vault_config = crate::vault::try_build_vault_config( - self.vault_addr.as_deref(), - self.vault_token.as_deref(), - self.vault_path.as_deref(), - self.vault_key.as_deref(), - self.vault_mount.as_deref(), - self.vault_namespace.as_deref(), + self.vault.addr.as_deref(), + self.vault.token.as_deref(), + self.vault.path.as_deref(), + self.vault.key.as_deref(), + self.vault.mount.as_deref(), + self.vault.namespace.as_deref(), ); if let Some(vc) = vault_config { @@ -1992,7 +2037,7 @@ temperature: 0.3 fn test_agent_tools_enabled_default_is_none() { let config = Config::default(); assert!( - config.agent_tools_enabled.is_none(), + config.agent.tools_enabled.is_none(), "Default should be None (all tools enabled)" ); } @@ -2011,7 +2056,10 @@ temperature: 0.3 #[test] fn test_agent_tools_enabled_serialize_some() { let config = Config { - agent_tools_enabled: Some(vec!["read_file".to_string(), "search_code".to_string()]), + agent: AgentConfig { + tools_enabled: Some(vec!["read_file".to_string(), "search_code".to_string()]), + ..AgentConfig::default() + }, ..Config::default() }; let yaml = serde_yaml::to_string(&config).unwrap(); @@ -2028,7 +2076,7 @@ model: claude-opus-4-6 temperature: 0.3 "#; let config: Config = serde_yaml::from_str(yaml).unwrap(); - assert!(config.agent_tools_enabled.is_none()); + assert!(config.agent.tools_enabled.is_none()); } #[test] @@ -2041,7 +2089,7 @@ agent_tools_enabled: - list_files "#; let config: Config = serde_yaml::from_str(yaml).unwrap(); - let tools = config.agent_tools_enabled.unwrap(); + let tools = config.agent.tools_enabled.unwrap(); assert_eq!(tools.len(), 3); assert_eq!(tools[0], "read_file"); assert_eq!(tools[1], "search_code"); @@ -2055,7 +2103,7 @@ model: claude-opus-4-6 agent_tools_enabled: [] "#; let config: Config = serde_yaml::from_str(yaml).unwrap(); - let tools = config.agent_tools_enabled.unwrap(); + let tools = config.agent.tools_enabled.unwrap(); assert!( tools.is_empty(), "Empty list should deserialize as Some([])" @@ -2065,12 +2113,15 @@ agent_tools_enabled: [] #[test] fn test_agent_tools_enabled_round_trip() { let original = Config { - agent_tools_enabled: Some(vec!["read_file".to_string(), "search_code".to_string()]), + agent: AgentConfig { + tools_enabled: Some(vec!["read_file".to_string(), "search_code".to_string()]), + ..AgentConfig::default() + }, ..Config::default() }; let yaml = serde_yaml::to_string(&original).unwrap(); let restored: Config = serde_yaml::from_str(&yaml).unwrap(); - assert_eq!(original.agent_tools_enabled, restored.agent_tools_enabled); + assert_eq!(original.agent.tools_enabled, restored.agent.tools_enabled); } #[test] @@ -2078,6 +2129,6 @@ agent_tools_enabled: [] let original = Config::default(); let yaml = serde_yaml::to_string(&original).unwrap(); let restored: Config = serde_yaml::from_str(&yaml).unwrap(); - assert_eq!(original.agent_tools_enabled, restored.agent_tools_enabled); + assert_eq!(original.agent.tools_enabled, restored.agent.tools_enabled); } } diff --git a/src/review/pipeline/execution/dispatcher/context.rs b/src/review/pipeline/execution/dispatcher/context.rs index bd9bc36..d95c157 100644 --- a/src/review/pipeline/execution/dispatcher/context.rs +++ b/src/review/pipeline/execution/dispatcher/context.rs @@ -18,15 +18,15 @@ pub(super) fn build_agent_loop_config( context: &ReviewExecutionContext<'_>, ) -> core::agent_loop::AgentLoopConfig { core::agent_loop::AgentLoopConfig { - max_iterations: context.services.config.agent_max_iterations, - max_total_tokens: context.services.config.agent_max_total_tokens, + max_iterations: context.services.config.agent.max_iterations, + max_total_tokens: context.services.config.agent.max_total_tokens, } } pub(super) fn build_agent_tool_context( context: &ReviewExecutionContext<'_>, ) -> Option> { - if !(context.services.config.agent_review && context.services.adapter.supports_tools()) { + if !(context.services.config.agent.enabled && context.services.adapter.supports_tools()) { return None; } diff --git a/src/review/pipeline/execution/dispatcher/run.rs b/src/review/pipeline/execution/dispatcher/run.rs index c8b1026..3ad54ec 100644 --- a/src/review/pipeline/execution/dispatcher/run.rs +++ b/src/review/pipeline/execution/dispatcher/run.rs @@ -18,7 +18,7 @@ pub(in super::super) async fn dispatch_jobs( let agent_tool_ctx = build_agent_tool_context(context); let agent_loop_config = build_agent_loop_config(context); - let agent_tools_filter = context.services.config.agent_tools_enabled.clone(); + let agent_tools_filter = context.services.config.agent.tools_enabled.clone(); futures::stream::iter(jobs) .map(|job| { diff --git a/src/review/pipeline/postprocess/verification.rs b/src/review/pipeline/postprocess/verification.rs index b698629..7f7427e 100644 --- a/src/review/pipeline/postprocess/verification.rs +++ b/src/review/pipeline/postprocess/verification.rs @@ -19,9 +19,9 @@ pub(super) async fn apply_verification_pass( let (analyzer_comments, llm_comments): (Vec<_>, Vec<_>) = comments.into_iter().partition(is_analyzer_comment); - let (verified_llm_comments, warnings) = if services.config.verification_pass + let (verified_llm_comments, warnings) = if services.config.verification.enabled && !llm_comments.is_empty() - && llm_comments.len() <= services.config.verification_max_comments + && llm_comments.len() <= services.config.verification.max_comments { let comment_count_before = llm_comments.len(); let summary = super::super::super::verification::verify_comments( @@ -30,8 +30,8 @@ pub(super) async fn apply_verification_pass( &session.source_files, &session.verification_context, services.verification_adapter.as_ref(), - services.config.verification_min_score, - services.config.verification_fail_open, + services.config.verification.min_score, + services.config.verification.fail_open, ) .await; diff --git a/src/review/pipeline/services/adapter_init.rs b/src/review/pipeline/services/adapter_init.rs index 02189c1..078fac6 100644 --- a/src/review/pipeline/services/adapter_init.rs +++ b/src/review/pipeline/services/adapter_init.rs @@ -32,11 +32,11 @@ fn build_verification_adapter( model_config: &ModelConfig, adapter: &Arc, ) -> Result> { - let verification_config = config.to_model_config_for_role(config.verification_model_role); + let verification_config = config.to_model_config_for_role(config.verification.model_role); if verification_config.model_name != model_config.model_name { info!( "Using '{}' model '{}' for verification pass", - format!("{:?}", config.verification_model_role).to_lowercase(), + format!("{:?}", config.verification.model_role).to_lowercase(), verification_config.model_name ); Ok(Arc::from(adapters::llm::create_adapter( diff --git a/src/server/api.rs b/src/server/api.rs index 227669b..426a7cd 100644 --- a/src/server/api.rs +++ b/src/server/api.rs @@ -1210,7 +1210,7 @@ pub struct GhStatusResponse { pub async fn get_gh_status(State(state): State>) -> Json { let config = state.config.read().await; - let token = match config.github_token.as_deref() { + let token = match config.github.token.as_deref() { Some(t) if !t.is_empty() => t.to_string(), _ => { return Json(GhStatusResponse { @@ -1313,7 +1313,8 @@ pub async fn get_gh_repos( ) -> Result>, (StatusCode, String)> { let config = state.config.read().await; let token = config - .github_token + .github + .token .as_deref() .filter(|t| !t.is_empty()) .ok_or_else(|| { @@ -1568,7 +1569,8 @@ pub async fn get_gh_prs( let config = state.config.read().await; let token = config - .github_token + .github + .token .as_deref() .filter(|t| !t.is_empty()) .ok_or_else(|| { @@ -1723,7 +1725,8 @@ pub async fn start_pr_review( let config = state.config.read().await; let token = config - .github_token + .github + .token .as_deref() .filter(|t| !t.is_empty()) .ok_or_else(|| { @@ -1817,7 +1820,7 @@ async fn run_pr_review_task( let config = state.config.read().await.clone(); let repo_path = state.repo_path.clone(); - let github_token = config.github_token.clone(); + let github_token = config.github.token.clone(); let model = config.model.clone(); let provider = config.adapter.clone(); let base_url = config.base_url.clone(); diff --git a/src/server/github.rs b/src/server/github.rs index 238e5a1..e8a5b7b 100644 --- a/src/server/github.rs +++ b/src/server/github.rs @@ -35,7 +35,8 @@ pub async fn start_device_flow( ) -> Result, (StatusCode, String)> { let config = state.config.read().await; let client_id = config - .github_client_id + .github + .client_id .as_deref() .filter(|s| !s.is_empty()) .ok_or_else(|| { @@ -110,7 +111,8 @@ pub async fn poll_device_flow( ) -> Result, (StatusCode, String)> { let config = state.config.read().await; let client_id = config - .github_client_id + .github + .client_id .as_deref() .filter(|s| !s.is_empty()) .ok_or_else(|| { @@ -192,7 +194,7 @@ pub async fn poll_device_flow( // Store the token in config { let mut config = state.config.write().await; - config.github_token = Some(access_token); + config.github.token = Some(access_token); } AppState::save_config_async(&state); @@ -210,7 +212,7 @@ pub async fn poll_device_flow( pub async fn disconnect_github(State(state): State>) -> Json { { let mut config = state.config.write().await; - config.github_token = None; + config.github.token = None; } AppState::save_config_async(&state); info!("GitHub disconnected"); @@ -229,7 +231,8 @@ pub struct WebhookStatusResponse { pub async fn get_webhook_status(State(state): State>) -> Json { let config = state.config.read().await; let configured = config - .github_webhook_secret + .github + .webhook_secret .as_ref() .is_some_and(|s| !s.is_empty()); Json(WebhookStatusResponse { @@ -248,7 +251,7 @@ pub async fn handle_webhook( let config = state.config.read().await; // Verify signature if webhook secret is configured - if let Some(ref secret) = config.github_webhook_secret { + if let Some(ref secret) = config.github.webhook_secret { if !secret.is_empty() { let signature = headers .get("x-hub-signature-256") @@ -272,9 +275,9 @@ pub async fn handle_webhook( tracing::Span::current().record("event_type", event_type); - let token = config.github_token.clone(); - let github_app_id = config.github_app_id; - let private_key = config.github_private_key.clone(); + let token = config.github.token.clone(); + let github_app_id = config.github.app_id; + let private_key = config.github.private_key.clone(); drop(config); info!(event = %event_type, "Received GitHub webhook"); From 82d6417622ffa08acb38392026d65fb98d2c0d1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 21:03:23 +0000 Subject: [PATCH 3/8] fix: align release metadata with tagged version Co-authored-by: EvalOpsBot --- .github/workflows/release.yml | 33 +++++++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- charts/diffscope/Chart.yaml | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e2d4da..14f2405 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,39 @@ jobs: - name: Extract version id: get_version run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + + - name: Verify release metadata matches tag + env: + TAG_NAME: ${{ steps.get_version.outputs.VERSION }} + run: | + set -euo pipefail + expected_version="${TAG_NAME#v}" + cargo_version="$(python - <<'PY' +import tomllib +from pathlib import Path + +print(tomllib.loads(Path("Cargo.toml").read_text())["package"]["version"]) +PY +)" + chart_app_version="$(python - <<'PY' +import re +from pathlib import Path + +content = Path("charts/diffscope/Chart.yaml").read_text() +match = re.search(r'^appVersion:\s*"?(.*?)"?\s*$', content, re.MULTILINE) +if not match: + raise SystemExit("charts/diffscope/Chart.yaml is missing appVersion") +print(match.group(1)) +PY +)" + test "$cargo_version" = "$expected_version" || { + echo "Cargo.toml version ($cargo_version) does not match tag ($expected_version)" + exit 1 + } + test "$chart_app_version" = "$expected_version" || { + echo "Chart appVersion ($chart_app_version) does not match tag ($expected_version)" + exit 1 + } - name: Create Release id: create_release diff --git a/Cargo.lock b/Cargo.lock index bd61b85..2fa1d1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,7 +543,7 @@ dependencies = [ [[package]] name = "diffscope" -version = "0.5.3" +version = "0.5.26" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 08e84c7..802abdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "diffscope" -version = "0.5.3" +version = "0.5.26" edition = "2021" authors = ["Jonathan Haas "] description = "A composable code review engine with smart analysis, confidence scoring, and professional reporting" diff --git a/charts/diffscope/Chart.yaml b/charts/diffscope/Chart.yaml index 6abd2c4..66017e0 100644 --- a/charts/diffscope/Chart.yaml +++ b/charts/diffscope/Chart.yaml @@ -3,7 +3,7 @@ name: diffscope description: AI-powered code review engine with smart analysis and professional reporting type: application version: 0.1.0 -appVersion: "0.5.3" +appVersion: "0.5.26" home: https://github.com/evalops/diffscope sources: - https://github.com/evalops/diffscope From fcc9ecfa513f96fb2d619e2f84d43962cb63144c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 21:07:39 +0000 Subject: [PATCH 4/8] build: pin jsonwebtoken for Rust 1.83 Co-authored-by: EvalOpsBot --- Cargo.lock | 7 +++---- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fa1d1d..c3ec865 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,17 +1352,16 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ "base64", - "getrandom 0.2.16", "js-sys", "pem", + "ring", "serde", "serde_json", - "signature", "simple_asn1", ] diff --git a/Cargo.toml b/Cargo.toml index 802abdd..91067c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ uuid = { version = "1", features = ["v4"] } mime_guess = "2" hmac = "0.12" sha2 = "0.10" -jsonwebtoken = "10" +jsonwebtoken = "9.3" base64 = "0.22" futures = "0.3" opentelemetry = { version = "0.27", features = ["trace"], optional = true } From 147407736a1d24067618b1a3b49c7287333ce02e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 21:10:17 +0000 Subject: [PATCH 5/8] build: pin Rust toolchain for current deps Co-authored-by: EvalOpsBot --- Cargo.lock | 7 ++++--- Cargo.toml | 2 +- rust-toolchain.toml | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/Cargo.lock b/Cargo.lock index c3ec865..2fa1d1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,16 +1352,17 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.1" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64", + "getrandom 0.2.16", "js-sys", "pem", - "ring", "serde", "serde_json", + "signature", "simple_asn1", ] diff --git a/Cargo.toml b/Cargo.toml index 91067c4..802abdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ uuid = { version = "1", features = ["v4"] } mime_guess = "2" hmac = "0.12" sha2 = "0.10" -jsonwebtoken = "9.3" +jsonwebtoken = "10" base64 = "0.22" futures = "0.3" opentelemetry = { version = "0.27", features = ["trace"], optional = true } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..b475f2f --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.85.0" +components = ["rustfmt", "clippy"] From 6e3b794f83e39f22706c89b42d23496954b4fbf6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 21:11:34 +0000 Subject: [PATCH 6/8] build: raise pinned Rust toolchain to 1.88 Co-authored-by: EvalOpsBot --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b475f2f..7855e6d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.85.0" +channel = "1.88.0" components = ["rustfmt", "clippy"] From 904fa04c24cc94c7632483cd3c6a74b9ea238069 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 21:58:36 +0000 Subject: [PATCH 7/8] Preserve zero hunk line counts --- src/core/diff_parser.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/diff_parser.rs b/src/core/diff_parser.rs index 0671a2b..cf341ee 100644 --- a/src/core/diff_parser.rs +++ b/src/core/diff_parser.rs @@ -476,7 +476,6 @@ fn parse_optional_capture(captures: ®ex::Captures<'_>, group: usize) -> Optio captures .get(group) .and_then(|value| value.as_str().parse::().ok()) - .filter(|value| *value > 0) } #[cfg(test)] @@ -557,6 +556,8 @@ index 83db48f..0000000\n\ assert_eq!(diffs.len(), 1); assert!(diffs[0].is_deleted); assert!(!diffs[0].is_new); + assert_eq!(diffs[0].hunks[0].old_lines, 1); + assert_eq!(diffs[0].hunks[0].new_lines, 0); } #[test] @@ -574,6 +575,8 @@ index 0000000..f735c20\n\ assert_eq!(diffs.len(), 1); assert!(diffs[0].is_new); assert!(!diffs[0].is_deleted); + assert_eq!(diffs[0].hunks[0].old_lines, 0); + assert_eq!(diffs[0].hunks[0].new_lines, 1); } #[test] From d20cd0d056ae4d9456256e72a261ecedd2f63147 Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Sat, 14 Mar 2026 10:33:13 -0700 Subject: [PATCH 8/8] fix(ci): release.yml YAML parse + clippy uninlined_format_args - release: use python3 -c one-liners for version check (avoid heredoc that actionlint parses as YAML) - Apply clippy uninlined_format_args fixes across codebase for lint job Made-with: Cursor --- .github/workflows/release.yml | 20 +--- src/adapters/anthropic.rs | 9 +- src/adapters/ollama.rs | 20 ++-- src/adapters/openai.rs | 9 +- src/commands/doctor/command/display/config.rs | 7 +- .../doctor/command/display/endpoint.rs | 4 +- .../doctor/command/display/inference.rs | 16 +-- src/commands/doctor/command/probe.rs | 6 +- src/commands/doctor/command/recommend.rs | 2 +- src/commands/doctor/command/run/command.rs | 2 +- .../doctor/endpoint/inference/request.rs | 4 +- src/commands/doctor/system/output.rs | 9 +- src/commands/doctor/system/probes.rs | 4 +- src/commands/eval/command/batch.rs | 11 +- src/commands/eval/command/fixtures.rs | 2 +- src/commands/eval/pattern/describe.rs | 22 ++-- src/commands/eval/report/output.rs | 28 ++--- src/commands/eval/runner/execute/repro.rs | 12 +- src/commands/eval/runner/execute/result.rs | 6 +- .../eval/thresholds/evaluation/minimums.rs | 6 +- src/commands/feedback_eval/input.rs | 2 +- src/commands/git/suggest/commit.rs | 2 +- src/commands/git/suggest/pr_title.rs | 4 +- src/commands/misc/changelog/output.rs | 2 +- src/commands/misc/discussion/prompt.rs | 4 +- src/commands/misc/lsp_check/command.rs | 6 +- src/commands/misc/lsp_check/extensions.rs | 2 +- src/commands/misc/lsp_check/languages.rs | 2 +- src/commands/pr/comments/body.rs | 2 +- src/commands/pr/comments/summary.rs | 2 +- src/commands/smart_review/command.rs | 2 +- src/config.rs | 3 +- src/core/agent_loop.rs | 11 +- src/core/agent_tools.rs | 61 ++++------ src/core/changelog.rs | 10 +- src/core/code_summary.rs | 22 ++-- src/core/comment/identity.rs | 2 +- src/core/comment/suggestions.rs | 2 +- src/core/comment/summary.rs | 3 +- src/core/commit_prompt.rs | 10 +- src/core/composable_pipeline.rs | 2 +- src/core/context.rs | 14 +-- src/core/context_provenance.rs | 11 +- src/core/convention_learner.rs | 10 +- src/core/enhanced_review.rs | 2 +- src/core/eval_benchmarks.rs | 7 +- src/core/function_chunker.rs | 6 +- src/core/git_history.rs | 10 +- src/core/interactive.rs | 26 ++--- src/core/multi_pass.rs | 8 +- src/core/offline.rs | 5 +- src/core/pr_summary.rs | 13 +-- src/core/prompt.rs | 4 +- src/core/semantic.rs | 2 +- src/core/semantic/embedding.rs | 2 +- src/core/semantic/persistence.rs | 2 +- src/core/smart_review_prompt.rs | 2 +- src/core/symbol_graph.rs | 3 +- src/core/symbol_index.rs | 20 ++-- src/main.rs | 2 +- src/output/format.rs | 41 ++++--- src/parsing/llm_response.rs | 6 +- src/parsing/smart_response.rs | 6 +- src/plugins/builtin/duplicate_filter.rs | 2 +- src/plugins/builtin/eslint.rs | 11 +- src/plugins/builtin/secret_scanner.rs | 13 +-- src/plugins/builtin/supply_chain.rs | 57 +++------ src/review/compression.rs | 38 ++---- src/review/compression/summary.rs | 4 +- src/review/context_helpers/ranking.rs | 4 +- src/review/feedback.rs | 18 +-- src/review/feedback/store.rs | 2 +- src/review/filters/confidence.rs | 2 +- src/review/filters/vague.rs | 2 +- .../pipeline/context/related/callers.rs | 2 +- .../pipeline/context/related/test_files.rs | 27 ++--- src/review/pipeline/guidance/sections.rs | 9 +- src/review/pipeline/postprocess/dedup.rs | 2 +- src/review/pipeline/repo_support/diff.rs | 4 +- src/review/pipeline/services/adapter_init.rs | 2 +- src/review/pipeline/session/build.rs | 2 +- src/review/rule_helpers/reporting/files.rs | 2 +- src/review/rule_helpers/reporting/summary.rs | 2 +- src/review/rule_helpers/runtime/context.rs | 6 +- src/review/verification/prompt/render.rs | 6 +- src/review/verification/tests.rs | 2 +- src/server/api.rs | 80 ++++++------- src/server/github.rs | 108 ++++++++---------- src/server/mod.rs | 6 +- src/server/storage_json.rs | 20 ++-- src/vault.rs | 20 ++-- 91 files changed, 418 insertions(+), 592 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14f2405..5c3e3f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,24 +33,8 @@ jobs: run: | set -euo pipefail expected_version="${TAG_NAME#v}" - cargo_version="$(python - <<'PY' -import tomllib -from pathlib import Path - -print(tomllib.loads(Path("Cargo.toml").read_text())["package"]["version"]) -PY -)" - chart_app_version="$(python - <<'PY' -import re -from pathlib import Path - -content = Path("charts/diffscope/Chart.yaml").read_text() -match = re.search(r'^appVersion:\s*"?(.*?)"?\s*$', content, re.MULTILINE) -if not match: - raise SystemExit("charts/diffscope/Chart.yaml is missing appVersion") -print(match.group(1)) -PY -)" + cargo_version="$(python3 -c "import tomllib; from pathlib import Path; print(tomllib.loads(Path('Cargo.toml').read_text())['package']['version'])")" + chart_app_version="$(python3 -c "import re; from pathlib import Path; c=Path('charts/diffscope/Chart.yaml').read_text(); m=re.search(r'^appVersion:\s*\"?(.*?)\"?\s*\$', c, re.MULTILINE); (exit('Chart.yaml missing appVersion') if not m else print(m.group(1)))")" test "$cargo_version" = "$expected_version" || { echo "Cargo.toml version ($cargo_version) does not match tag ($expected_version)" exit 1 diff --git a/src/adapters/anthropic.rs b/src/adapters/anthropic.rs index b7cdfaf..7884f35 100644 --- a/src/adapters/anthropic.rs +++ b/src/adapters/anthropic.rs @@ -407,8 +407,7 @@ mod tests { let err_msg = format!("{:#}", result.unwrap_err()); assert!( err_msg.contains("401") || err_msg.contains("Unauthorized"), - "Error should mention 401 or Unauthorized, got: {}", - err_msg + "Error should mention 401 or Unauthorized, got: {err_msg}" ); mock.assert_async().await; } @@ -502,8 +501,7 @@ mod tests { let err = result.unwrap_err().to_string(); assert!( err.contains("Unsupported content type"), - "Error should mention unsupported type, got: {}", - err + "Error should mention unsupported type, got: {err}" ); } @@ -534,8 +532,7 @@ mod tests { let err = result.unwrap_err().to_string(); assert!( err.contains("empty content"), - "Error should mention empty content: {}", - err + "Error should mention empty content: {err}" ); } diff --git a/src/adapters/ollama.rs b/src/adapters/ollama.rs index f66da52..d38d0c4 100644 --- a/src/adapters/ollama.rs +++ b/src/adapters/ollama.rs @@ -211,11 +211,10 @@ mod tests { fn chat_response_body(content: &str, model: &str, done: bool) -> String { format!( r#"{{ - "message": {{"role": "assistant", "content": "{}"}}, - "model": "{}", - "done": {} - }}"#, - content, model, done + "message": {{"role": "assistant", "content": "{content}"}}, + "model": "{model}", + "done": {done} + }}"# ) } @@ -227,13 +226,12 @@ mod tests { ) -> String { format!( r#"{{ - "message": {{"role": "assistant", "content": "{}"}}, - "model": "{}", + "message": {{"role": "assistant", "content": "{content}"}}, + "model": "{model}", "done": true, - "prompt_eval_count": {}, - "eval_count": {} - }}"#, - content, model, prompt_eval, eval + "prompt_eval_count": {prompt_eval}, + "eval_count": {eval} + }}"# ) } diff --git a/src/adapters/openai.rs b/src/adapters/openai.rs index 979defd..9d74c94 100644 --- a/src/adapters/openai.rs +++ b/src/adapters/openai.rs @@ -771,8 +771,7 @@ mod tests { let err_msg = format!("{:#}", result.unwrap_err()); assert!( err_msg.contains("401") || err_msg.contains("Unauthorized"), - "Error should mention 401 or Unauthorized, got: {}", - err_msg + "Error should mention 401 or Unauthorized, got: {err_msg}" ); mock.assert_async().await; } @@ -814,8 +813,7 @@ mod tests { let err_msg = format!("{:#}", result.unwrap_err()); assert!( err_msg.contains("429") || err_msg.contains("Rate limited"), - "Error should mention rate limiting, got: {}", - err_msg + "Error should mention rate limiting, got: {err_msg}" ); mock.assert_async().await; } @@ -866,8 +864,7 @@ mod tests { let err = result.unwrap_err().to_string(); assert!( err.contains("empty choices"), - "Error should mention empty choices: {}", - err + "Error should mention empty choices: {err}" ); } diff --git a/src/commands/doctor/command/display/config.rs b/src/commands/doctor/command/display/config.rs index bfe7a98..c4583a2 100644 --- a/src/commands/doctor/command/display/config.rs +++ b/src/commands/doctor/command/display/config.rs @@ -27,17 +27,14 @@ pub(in super::super) fn print_configuration(config: &Config) { } ); if let Some(cw) = config.context_window { - println!(" Context: {} tokens", cw); + println!(" Context: {cw} tokens"); } println!(); } pub(in super::super) fn print_unreachable(base_url: &str) -> Result<()> { println!("UNREACHABLE"); - println!( - "\nCannot reach {}. Make sure your LLM server is running.", - base_url - ); + println!("\nCannot reach {base_url}. Make sure your LLM server is running."); println!("\nQuick start:"); println!(" Ollama: ollama serve"); println!(" vLLM: vllm serve "); diff --git a/src/commands/doctor/command/display/endpoint.rs b/src/commands/doctor/command/display/endpoint.rs index 962088e..2bef90e 100644 --- a/src/commands/doctor/command/display/endpoint.rs +++ b/src/commands/doctor/command/display/endpoint.rs @@ -1,7 +1,7 @@ use crate::core::offline::LocalModel; pub(in super::super) fn print_endpoint_models(endpoint_type: &str, models: &[LocalModel]) { - println!("\nEndpoint type: {}", endpoint_type); + println!("\nEndpoint type: {endpoint_type}"); println!("\nAvailable models ({}):", models.len()); if models.is_empty() { println!(" (none found)"); @@ -25,7 +25,7 @@ fn format_model_size_info(model: &LocalModel) -> String { + &model .quantization .as_ref() - .map(|quantization| format!(", {}", quantization)) + .map(|quantization| format!(", {quantization}")) .unwrap_or_default() + ")" } diff --git a/src/commands/doctor/command/display/inference.rs b/src/commands/doctor/command/display/inference.rs index 83a0137..0e7dbd4 100644 --- a/src/commands/doctor/command/display/inference.rs +++ b/src/commands/doctor/command/display/inference.rs @@ -9,13 +9,10 @@ pub(in super::super) fn print_recommended_model_summary( readiness: &ReadinessCheck, ) { println!("\nRecommended for code review: {}", recommended.name); - println!(" Estimated RAM: ~{}MB", estimated_ram_mb); + println!(" Estimated RAM: ~{estimated_ram_mb}MB"); if let Some(ctx_size) = detected_context_window { - println!( - " Context window: {} tokens (detected from model)", - ctx_size - ); + println!(" Context window: {ctx_size} tokens (detected from model)"); } if readiness.ready { @@ -23,7 +20,7 @@ pub(in super::super) fn print_recommended_model_summary( } else { println!("\nStatus: NOT READY"); for warning in &readiness.warnings { - println!(" Warning: {}", warning); + println!(" Warning: {warning}"); } } } @@ -41,14 +38,11 @@ pub(in super::super) fn print_inference_success(elapsed: Duration, tokens_per_se pub(in super::super) fn print_inference_failure(error: &impl std::fmt::Display) { println!("FAILED"); - println!(" Error: {}", error); + println!(" Error: {error}"); println!(" The model may still be loading. Try again in a moment."); } pub(in super::super) fn print_usage(base_url: &str, model_flag: &str) { println!("\nUsage:"); - println!( - " git diff | diffscope review --base-url {} --model {}", - base_url, model_flag - ); + println!(" git diff | diffscope review --base-url {base_url} --model {model_flag}"); } diff --git a/src/commands/doctor/command/probe.rs b/src/commands/doctor/command/probe.rs index a0106c8..30f1e0b 100644 --- a/src/commands/doctor/command/probe.rs +++ b/src/commands/doctor/command/probe.rs @@ -18,7 +18,7 @@ impl EndpointProbe { pub(super) fn model_flag(&self, model_name: &str) -> String { if self.is_ollama() { - format!("ollama:{}", model_name) + format!("ollama:{model_name}") } else { model_name.to_string() } @@ -49,7 +49,7 @@ pub(super) async fn probe_endpoint( } async fn probe_ollama_endpoint(client: &Client, base_url: &str) -> Result>> { - let url = format!("{}/api/tags", base_url); + let url = format!("{base_url}/api/tags"); let response = match client.get(&url).send().await { Ok(response) => response, Err(_) => return Ok(None), @@ -65,7 +65,7 @@ async fn probe_ollama_endpoint(client: &Client, base_url: &str) -> Result