use crate::workspace::Workspace;
use std::path::Path;
use super::severity::IssueSeverity;
fn parse_header_issue_format(line: &str) -> Option<&str> {
let stripped = line.trim_start_matches('#');
if stripped.len() == line.len() {
return None;
}
let stripped = stripped.trim_start();
if let Some(rest) = stripped.strip_prefix("[ ]") {
return Some(rest.trim_start());
}
if let Some(rest) = stripped
.strip_prefix("[x]")
.or_else(|| stripped.strip_prefix("[X]"))
{
return Some(rest.trim_start());
}
None
}
#[derive(Debug, Clone, Default)]
pub struct ReviewMetrics {
pub(crate) total_issues: u32,
pub(crate) critical_issues: u32,
pub(crate) high_issues: u32,
pub(crate) medium_issues: u32,
pub(crate) low_issues: u32,
pub(crate) resolved_issues: u32,
pub(crate) issues_file_found: bool,
pub(crate) no_issues_declared: bool,
}
impl ReviewMetrics {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn from_issues_content(content: &str) -> Self {
let parsed_issues: Vec<(IssueSeverity, bool)> = content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
if let Some(rest) = parse_header_issue_format(trimmed) {
return IssueSeverity::from_str(rest).map(|severity| (severity, false));
}
if trimmed.starts_with('#') {
return None;
}
let (is_resolved, rest) = if let Some(rest) = trimmed
.strip_prefix("- [x]")
.or_else(|| trimmed.strip_prefix("- [X]"))
{
(true, rest)
} else if let Some(rest) = trimmed.strip_prefix("- [ ]") {
(false, rest)
} else if let Some(rest) = trimmed.strip_prefix("-") {
(false, rest)
} else {
return None;
};
let rest = rest.trim();
IssueSeverity::from_str(rest).map(|severity| (severity, is_resolved))
})
.collect();
let issues: Vec<IssueSeverity> = parsed_issues
.iter()
.map(|(severity, _)| *severity)
.collect();
let resolved_count = parsed_issues
.iter()
.filter(|(_, is_resolved)| *is_resolved)
.count() as u32;
let critical_issues = issues
.iter()
.filter(|&s| *s == IssueSeverity::Critical)
.count() as u32;
let high_issues = issues.iter().filter(|&s| *s == IssueSeverity::High).count() as u32;
let medium_issues = issues
.iter()
.filter(|&s| *s == IssueSeverity::Medium)
.count() as u32;
let low_issues = issues.iter().filter(|&s| *s == IssueSeverity::Low).count() as u32;
let total_issues = issues.len() as u32;
let content_lower = content.to_lowercase();
let no_issues_declared = if total_issues == 0 {
fn check_line_matches(line: &str) -> bool {
let trimmed = line.trim();
let cleaned = trimmed
.trim_start_matches('-')
.trim_start_matches('*')
.trim();
cleaned == "no issues found"
|| cleaned == "no issues found."
|| cleaned == "no issues"
|| cleaned == "no issues."
|| cleaned == "all issues resolved"
|| cleaned == "all issues resolved."
|| cleaned.starts_with("all issues resolved.")
|| (cleaned.starts_with("no issues found")
&& !cleaned.contains("critical")
&& !cleaned.contains("high")
&& !cleaned.contains("medium")
&& !cleaned.contains("low"))
}
fn check_all_lines(
lines: &[&str],
idx: usize,
check_fn: &dyn Fn(&str) -> bool,
) -> bool {
if idx >= lines.len() {
return false;
}
if check_fn(lines[idx]) {
true
} else {
check_all_lines(lines, idx + 1, check_fn)
}
}
let lines: Vec<&str> = content_lower.split('\n').collect();
check_all_lines(&lines, 0, &check_line_matches)
} else {
false
};
Self {
total_issues,
critical_issues,
high_issues,
medium_issues,
low_issues,
resolved_issues: resolved_count,
issues_file_found: true,
no_issues_declared,
}
}
pub(crate) fn from_issues_file_with_workspace(
workspace: &dyn Workspace,
) -> std::io::Result<Self> {
let path = Path::new(".agent/ISSUES.md");
if !workspace.exists(path) {
return Ok(Self::new());
}
let content = workspace.read(path)?;
Ok(Self::from_issues_content(&content))
}
}