use crate::error::RsGuardError;
use regex::Regex;
use std::sync::LazyLock;
const METADATA_SCAN_WINDOW: usize = 4096;
const IMPORTANT_ISSUES_THRESHOLD: u32 = 3;
const METADATA_MARKER: &str = "[RS_GUARD_VERDICT_METADATA]";
static CRITICAL_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\[Critical Bug\]|\[Critical\]").expect("critical regex is valid")
});
static SECURITY_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\[Security\]|\[Security Issue\]").expect("security regex is valid")
});
static IMPORTANT_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\[Important\]|\[Important Issue\]").expect("important regex is valid")
});
static SUGGESTION_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\[Suggestion\]|\[Suggestion Issue\]").expect("suggestion regex is valid")
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReviewState {
Approve,
RequestChanges,
Comment,
}
impl std::fmt::Display for ReviewState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReviewState::Approve => write!(f, "APPROVE"),
ReviewState::RequestChanges => write!(f, "REQUEST_CHANGES"),
ReviewState::Comment => write!(f, "COMMENT"),
}
}
}
impl ReviewState {
pub fn as_github_state(&self) -> &'static str {
match self {
ReviewState::Approve => "APPROVE",
ReviewState::RequestChanges => "REQUEST_CHANGES",
ReviewState::Comment => "COMMENT",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use = "Verdict should be used to determine a ReviewState"]
pub struct Verdict {
pub verdict: String,
pub critical_issues: u32,
pub security_issues: u32,
pub important_issues: u32,
pub suggestions: u32,
}
impl std::fmt::Display for Verdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Verdict: {}, CriticalIssues: {}, SecurityIssues: {}, ImportantIssues: {}, Suggestions: {}",
self.verdict,
self.critical_issues,
self.security_issues,
self.important_issues,
self.suggestions
)
}
}
fn extract_field<'a>(section: &'a str, label: &str) -> Option<&'a str> {
let pos = section.find(label)?;
let value = section[pos + label.len()..].trim_start();
let end = value.find(['\n', '\r']).unwrap_or(value.len());
let result = value[..end].trim();
if result.is_empty() {
None
} else {
Some(result)
}
}
pub fn parse_metadata_block(response: &str) -> Option<Verdict> {
let marker_pos = response.find(METADATA_MARKER)?;
let section_start = marker_pos + METADATA_MARKER.len();
let section = &response[section_start..];
let scan_window = §ion[..METADATA_SCAN_WINDOW.min(section.len())];
let verdict = extract_field(scan_window, "Verdict:")?.to_string();
let critical_issues: u32 = extract_field(scan_window, "CriticalIssues:")
.or_else(|| extract_field(scan_window, "CriticalBugs:"))
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let security_issues: u32 = extract_field(scan_window, "SecurityIssues:")
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let important_issues: u32 = extract_field(scan_window, "ImportantIssues:")
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let suggestions: u32 = extract_field(scan_window, "Suggestions:")
.and_then(|v| v.parse().ok())
.unwrap_or(0);
Some(Verdict {
verdict,
critical_issues,
security_issues,
important_issues,
suggestions,
})
}
pub fn evaluate_by_tags(response: &str) -> Verdict {
let critical_issues = CRITICAL_RE.find_iter(response).count() as u32;
let security_issues = SECURITY_RE.find_iter(response).count() as u32;
let important_issues = IMPORTANT_RE.find_iter(response).count() as u32;
let suggestions = SUGGESTION_RE.find_iter(response).count() as u32;
Verdict {
verdict: if critical_issues > 0 || security_issues > 0 {
"NEGATIVE".to_string()
} else {
"POSITIVE".to_string()
},
critical_issues,
security_issues,
important_issues,
suggestions,
}
}
pub fn determine_review_state(verdict: &Verdict) -> ReviewState {
if verdict.verdict == "NEGATIVE"
|| verdict.security_issues > 0
|| verdict.critical_issues > 0
|| verdict.important_issues >= IMPORTANT_ISSUES_THRESHOLD
{
ReviewState::RequestChanges
} else if verdict.important_issues > 0 {
ReviewState::Comment
} else {
ReviewState::Approve
}
}
pub fn parse_verdict(response: &str) -> Result<(Verdict, ReviewState), RsGuardError> {
if response.trim().is_empty() {
return Err(RsGuardError::VerdictParse(
"LLM response is empty or whitespace-only. Cannot determine verdict.".to_string(),
));
}
let verdict = parse_metadata_block(response).unwrap_or_else(|| evaluate_by_tags(response));
if verdict.verdict != "POSITIVE" && verdict.verdict != "NEGATIVE" {
return Err(RsGuardError::VerdictParse(format!(
"Invalid verdict value: {}. Expected POSITIVE or NEGATIVE.",
verdict.verdict
)));
}
let state = determine_review_state(&verdict);
Ok((verdict, state))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_positive() {
let response = "Some review text\n\n[RS_GUARD_VERDICT_METADATA]\nVerdict: POSITIVE\nCriticalIssues: 0\nSecurityIssues: 0\nImportantIssues: 0\nSuggestions: 0";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(verdict.verdict, "POSITIVE");
assert_eq!(verdict.critical_issues, 0);
assert_eq!(verdict.security_issues, 0);
assert_eq!(verdict.important_issues, 0);
assert_eq!(verdict.suggestions, 0);
assert_eq!(determine_review_state(&verdict), ReviewState::Approve);
}
#[test]
fn test_parse_negative() {
let response = "Some review text\n\n[RS_GUARD_VERDICT_METADATA]\nVerdict: NEGATIVE\nCriticalIssues: 0\nSecurityIssues: 0\nImportantIssues: 0\nSuggestions: 0";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(
determine_review_state(&verdict),
ReviewState::RequestChanges
);
}
#[test]
fn test_parse_critical_gt_0() {
let response =
"[RS_GUARD_VERDICT_METADATA]\nVerdict: POSITIVE\nCriticalIssues: 1\nSecurityIssues: 0\nImportantIssues: 0\nSuggestions: 0";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(
determine_review_state(&verdict),
ReviewState::RequestChanges
);
}
#[test]
fn test_parse_security_gt_0() {
let response =
"[RS_GUARD_VERDICT_METADATA]\nVerdict: POSITIVE\nCriticalIssues: 0\nSecurityIssues: 1\nImportantIssues: 0\nSuggestions: 0";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(
determine_review_state(&verdict),
ReviewState::RequestChanges
);
}
#[test]
fn test_missing_metadata_fallback_to_tags() {
let response = "Review found some issues.\n[Critical Bug] Race condition in handler\n[Security] SQL injection risk";
let verdict = evaluate_by_tags(response);
assert_eq!(verdict.critical_issues, 1);
assert_eq!(verdict.security_issues, 1);
assert_eq!(
determine_review_state(&verdict),
ReviewState::RequestChanges
);
}
#[test]
fn test_clean_tag_fallback() {
let response = "Everything looks good. No issues found.";
let verdict = evaluate_by_tags(response);
assert_eq!(verdict.critical_issues, 0);
assert_eq!(verdict.security_issues, 0);
assert_eq!(verdict.important_issues, 0);
assert_eq!(verdict.suggestions, 0);
assert_eq!(determine_review_state(&verdict), ReviewState::Approve);
}
#[test]
fn test_positive_with_important_issues_comment() {
let response =
"[RS_GUARD_VERDICT_METADATA]\nVerdict: POSITIVE\nCriticalIssues: 0\nSecurityIssues: 0\nImportantIssues: 1\nSuggestions: 0";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(determine_review_state(&verdict), ReviewState::Comment);
}
#[test]
fn test_as_github_state_request_body_values() {
assert_eq!(ReviewState::Approve.as_github_state(), "APPROVE");
assert_eq!(
ReviewState::RequestChanges.as_github_state(),
"REQUEST_CHANGES"
);
assert_eq!(ReviewState::Comment.as_github_state(), "COMMENT");
}
#[test]
fn test_metadata_block_at_end_of_large_response() {
let padding = "x".repeat(3000);
let response = format!(
"{}\n[RS_GUARD_VERDICT_METADATA]\nVerdict: POSITIVE\nCriticalIssues: 0\nSecurityIssues: 0\nImportantIssues: 0\nSuggestions: 0",
padding
);
let verdict = parse_metadata_block(&response).unwrap();
assert_eq!(verdict.verdict, "POSITIVE");
assert_eq!(verdict.critical_issues, 0);
assert_eq!(verdict.security_issues, 0);
}
#[test]
fn test_metadata_block_near_boundary() {
let padding = "x".repeat(3500);
let response = format!(
"{}\n[RS_GUARD_VERDICT_METADATA]\nVerdict: NEGATIVE\nCriticalIssues: 1\nSecurityIssues: 0\nImportantIssues: 0\nSuggestions: 0",
padding
);
let verdict = parse_metadata_block(&response).unwrap();
assert_eq!(verdict.verdict, "NEGATIVE");
assert_eq!(verdict.critical_issues, 1);
assert_eq!(verdict.security_issues, 0);
}
#[test]
fn test_metadata_block_beyond_window_fallback_to_tags() {
let padding = "x".repeat(5000);
let response = format!(
"[RS_GUARD_VERDICT_METADATA]\n{}\nVerdict: POSITIVE\nCriticalIssues: 0\nSecurityIssues: 0\nImportantIssues: 0\nSuggestions: 0",
padding
);
let verdict = parse_metadata_block(&response);
assert!(verdict.is_none());
}
#[test]
fn test_empty_response_returns_error() {
let response = "";
let result = parse_verdict(response);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("empty or whitespace-only"));
}
#[test]
fn test_whitespace_only_response_returns_error() {
let response = " \n\t \n ";
let result = parse_verdict(response);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("empty or whitespace-only"));
}
#[test]
fn test_valid_response_parses_successfully() {
let response = "Some review text\n\n[RS_GUARD_VERDICT_METADATA]\nVerdict: POSITIVE\nCriticalIssues: 0\nSecurityIssues: 0\nImportantIssues: 0\nSuggestions: 0";
let result = parse_verdict(response);
assert!(result.is_ok());
let (verdict, state) = result.unwrap();
assert_eq!(verdict.verdict, "POSITIVE");
assert_eq!(state, ReviewState::Approve);
}
#[test]
fn test_metadata_block_reversed_field_order() {
let response =
"[RS_GUARD_VERDICT_METADATA]\nSuggestions: 1\nImportantIssues: 0\nSecurityIssues: 0\nCriticalIssues: 1\nVerdict: NEGATIVE";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(verdict.verdict, "NEGATIVE");
assert_eq!(verdict.critical_issues, 1);
assert_eq!(verdict.security_issues, 0);
assert_eq!(verdict.suggestions, 1);
}
#[test]
fn test_metadata_block_fields_with_content_between() {
let response = "[RS_GUARD_VERDICT_METADATA]\nVerdict: POSITIVE\nSome extra text here\nCriticalIssues: 0\nMore text\nSecurityIssues: 0\nImportantIssues: 0\nSuggestions: 0";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(verdict.verdict, "POSITIVE");
assert_eq!(verdict.critical_issues, 0);
assert_eq!(verdict.security_issues, 0);
}
#[test]
fn test_metadata_block_random_field_order() {
let response =
"[RS_GUARD_VERDICT_METADATA]\nImportantIssues: 2\nCriticalIssues: 1\nVerdict: NEGATIVE\nSecurityIssues: 0\nSuggestions: 3";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(verdict.verdict, "NEGATIVE");
assert_eq!(verdict.critical_issues, 1);
assert_eq!(verdict.security_issues, 0);
assert_eq!(verdict.important_issues, 2);
assert_eq!(verdict.suggestions, 3);
}
#[test]
fn test_legacy_critical_issues_field_still_parses() {
let response =
"[RS_GUARD_VERDICT_METADATA]\nVerdict: NEGATIVE\nCriticalBugs: 2\nSecurityIssues: 1";
let verdict = parse_metadata_block(response).unwrap();
assert_eq!(verdict.critical_issues, 2);
assert_eq!(verdict.security_issues, 1);
assert_eq!(verdict.important_issues, 0);
assert_eq!(verdict.suggestions, 0);
}
}