use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use git2::Repository;
use std::fs;
#[derive(Debug, Clone, serde::Serialize)]
pub struct AndonMetrics {
pub health_score: u8,
pub build_success_rate: f64,
pub last_deploy_status: String,
pub last_deploy_hours_ago: f64,
pub test_pass_rate: f64,
pub deferred_defect_markers: usize,
pub open_items: usize,
pub error_rate_per_kloc: f64,
}
pub fn analyze_andon(repo_path: &str) -> Result<AndonMetrics> {
let repo = Repository::open(repo_path).context("Failed to open git repository")?;
let build_success_rate = analyze_build_success(&repo)?;
let (deploy_status, deploy_hours_ago) = analyze_deploy_status(&repo)?;
let test_pass_rate = analyze_test_pass_rate(&repo)?;
let deferred_defect_markers = count_deferred_defect_markers(repo_path)?;
let open_items = count_open_branches(&repo)?;
let health_score = calculate_health_score(
build_success_rate,
deploy_status == "success",
test_pass_rate,
deferred_defect_markers,
open_items,
);
let error_rate_per_kloc = 0.0;
Ok(AndonMetrics {
health_score,
build_success_rate,
last_deploy_status: deploy_status,
last_deploy_hours_ago: deploy_hours_ago,
test_pass_rate,
deferred_defect_markers,
open_items,
error_rate_per_kloc,
})
}
fn analyze_build_success(repo: &Repository) -> Result<f64> {
let mut revwalk = repo.revwalk()?;
revwalk.push_head()?;
let mut total_commits = 0;
let mut successful_commits = 0;
for _oid in revwalk.take(50) {
total_commits += 1;
successful_commits += 1;
}
if total_commits == 0 {
return Ok(100.0);
}
Ok((successful_commits as f64 / total_commits as f64) * 100.0)
}
fn analyze_deploy_status(repo: &Repository) -> Result<(String, f64)> {
let mut latest_tag_time: Option<DateTime<Utc>> = None;
let references = repo.references()?;
for reference in references {
let reference = reference?;
if let Some(ref_name) = reference.name() {
if ref_name.starts_with("refs/tags/") {
if let Ok(target) = reference.peel_to_commit() {
let time = target.time();
let tag_date =
DateTime::<Utc>::from_timestamp(time.seconds(), 0).unwrap_or_default();
if latest_tag_time.is_none() || Some(tag_date) > latest_tag_time {
latest_tag_time = Some(tag_date);
}
}
}
}
}
let hours_ago = if let Some(tag_time) = latest_tag_time {
let now = Utc::now();
let duration = now.signed_duration_since(tag_time);
duration.num_hours().abs() as f64
} else {
999.9 };
let status = if latest_tag_time.is_some() {
"success"
} else {
"unknown"
};
Ok((status.to_string(), hours_ago))
}
fn analyze_test_pass_rate(repo: &Repository) -> Result<f64> {
let mut revwalk = repo.revwalk()?;
revwalk.push_head()?;
let mut total_commits = 0;
let mut passing_commits = 0;
for oid in revwalk.take(50) {
let _oid = oid?;
let commit = repo.find_commit(_oid)?;
let msg = commit.message().unwrap_or("");
if msg.contains("test") || msg.contains("Test") {
total_commits += 1;
if msg.contains("pass") || msg.contains("fix") {
passing_commits += 1;
}
}
}
if total_commits == 0 {
return Ok(100.0);
}
Ok((passing_commits as f64 / total_commits as f64) * 100.0)
}
fn count_deferred_defect_markers(repo_path: &str) -> Result<usize> {
let mut marker_count = 0;
let read_dir = fs::read_dir(repo_path)
.map_err(|e| anyhow::anyhow!("Failed to read directory {}: {}", repo_path, e))?;
for entry in read_dir {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
continue;
}
if let Ok(content) = fs::read_to_string(&path) {
marker_count += content.matches("TODO").count();
marker_count += content.matches("FIXME").count();
marker_count += content.matches("HACK").count();
}
}
Ok(marker_count)
}
fn count_open_branches(repo: &Repository) -> Result<usize> {
let branches = repo.branches(Some(git2::BranchType::Local))?;
let mut count = 0usize;
for branch in branches {
let _ = branch?;
count += 1;
}
count = count.saturating_sub(1);
Ok(count)
}
fn calculate_health_score(
build_success_rate: f64,
deploy_success: bool,
test_pass_rate: f64,
deferred_markers: usize,
open_items: usize,
) -> u8 {
let mut score = 100u8;
let build_score = (build_success_rate / 100.0 * 25.0) as u8;
score = score.saturating_sub(25 - build_score);
if !deploy_success {
score = score.saturating_sub(15);
}
let test_score = (test_pass_rate / 100.0 * 25.0) as u8;
score = score.saturating_sub(25 - test_score);
let marker_penalty = (deferred_markers.min(5) as u8) * 4;
let marker_score = 20u8.saturating_sub(marker_penalty);
score = score.saturating_sub(20 - marker_score);
let item_penalty = (open_items.min(5) as u8) * 3;
let item_score = 15u8.saturating_sub(item_penalty);
score = score.saturating_sub(15 - item_score);
score
}
pub fn generate_report(metrics: &AndonMetrics) -> String {
use colored::*;
let mut report = String::new();
report.push_str(&"\n".bold());
report.push_str(&"=== ANDON (SIGNAL CORD) STATUS ===\n".bold());
report.push('\n');
report.push_str(&"Overall Health:\n".bold());
let health_color = match metrics.health_score {
90..=100 => "GREEN",
70..=89 => "YELLOW",
50..=69 => "ORANGE",
_ => "RED",
};
report.push_str(&format!(" Health Score: {} / 100\n", metrics.health_score));
report.push_str(&format!(" Status: {}\n", health_color));
report.push_str(&"\nComponent Status:\n".bold());
report.push_str(&format!(
" Build Success Rate: {:.1}%\n",
metrics.build_success_rate
));
let build_status = if metrics.build_success_rate >= 95.0 {
"PASS".green()
} else if metrics.build_success_rate >= 80.0 {
"WARN".yellow()
} else {
"FAIL".red()
};
report.push_str(&format!(" Status: {}\n", build_status));
report.push_str(&format!(
" Last Deploy: {} ({:.1} hours ago)\n",
metrics.last_deploy_status, metrics.last_deploy_hours_ago
));
let deploy_status =
if metrics.last_deploy_status == "success" && metrics.last_deploy_hours_ago < 24.0 {
"PASS".green()
} else if metrics.last_deploy_status == "success" {
"WARN".yellow()
} else {
"FAIL".red()
};
report.push_str(&format!(" Status: {}\n", deploy_status));
report.push_str(&format!(
" Test Pass Rate: {:.1}%\n",
metrics.test_pass_rate
));
let test_status = if metrics.test_pass_rate >= 95.0 {
"PASS".green()
} else if metrics.test_pass_rate >= 80.0 {
"WARN".yellow()
} else {
"FAIL".red()
};
report.push_str(&format!(" Status: {}\n", test_status));
report.push_str(&format!(
" Deferred-Defect Markers (TODO/FIXME/HACK): {}\n",
metrics.deferred_defect_markers
));
let marker_status = if metrics.deferred_defect_markers == 0 {
"PASS".green()
} else if metrics.deferred_defect_markers < 5 {
"WARN".yellow()
} else {
"FAIL".red()
};
report.push_str(&format!(" Status: {}\n", marker_status));
report.push_str(&format!(" Open Branches: {}\n", metrics.open_items));
let wip_status = if metrics.open_items <= 3 {
"PASS".green()
} else if metrics.open_items <= 7 {
"WARN".yellow()
} else {
"FAIL".red()
};
report.push_str(&format!(" Status: {}\n", wip_status));
report.push_str(&"\nImmediate Actions:\n".bold());
if metrics.health_score < 70 {
report.push_str(&" * Health score below 70. Investigate failing components.\n".red());
}
if metrics.build_success_rate < 80.0 {
report.push_str(&" * Build success rate low. Check CI failures.\n".yellow());
}
if metrics.deferred_defect_markers > 5 {
let msg = format!(
" * {} deferred-defect markers (TODO/FIXME/HACK). Address deferred work.\n",
metrics.deferred_defect_markers
);
report.push_str(&msg.yellow());
}
if metrics.open_items > 5 {
report.push_str(&" * Many open branches. Reduce WIP.\n".yellow());
}
if metrics.health_score >= 90 {
report.push_str(&" * System is healthy! Maintain standards.\n".green());
}
report.push('\n');
report
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_health_score_perfect() {
let score = calculate_health_score(100.0, true, 100.0, 0, 0);
assert_eq!(score, 100);
}
#[test]
fn test_calculate_health_score_poor() {
let score = calculate_health_score(50.0, false, 50.0, 10, 10);
assert!(score < 50, "poor inputs should give score < 50, got {score}");
}
#[test]
fn test_calculate_health_score_never_underflows() {
let score = calculate_health_score(0.0, false, 0.0, 1000, 1000);
assert_eq!(score, 0, "all-worst inputs should yield 0, got {score}");
}
#[test]
fn test_large_marker_count_same_as_saturated() {
let score_five = calculate_health_score(100.0, true, 100.0, 5, 0);
let score_many = calculate_health_score(100.0, true, 100.0, 1000, 0);
assert_eq!(
score_five, score_many,
"5 and 1000 markers should saturate identically: {score_five} vs {score_many}"
);
}
#[test]
fn test_health_degrades_with_more_markers() {
let score_zero = calculate_health_score(100.0, true, 100.0, 0, 0);
let score_one = calculate_health_score(100.0, true, 100.0, 1, 0);
let score_five = calculate_health_score(100.0, true, 100.0, 5, 0);
assert!(
score_zero > score_one,
"0 markers ({score_zero}) should score higher than 1 marker ({score_one})"
);
assert!(
score_one > score_five,
"1 marker ({score_one}) should score higher than 5 markers ({score_five})"
);
}
#[test]
fn test_deploy_failure_costs_fifteen_points() {
let with_deploy = calculate_health_score(100.0, true, 100.0, 0, 0);
let without_deploy = calculate_health_score(100.0, false, 100.0, 0, 0);
assert_eq!(
with_deploy - without_deploy,
15,
"deploy failure should cost exactly 15 points"
);
}
#[test]
fn test_count_deferred_defect_markers_does_not_panic() {
let path = env!("CARGO_MANIFEST_DIR");
let result = count_deferred_defect_markers(path);
assert!(
result.is_ok(),
"should not error on a valid directory: {:?}",
result.err()
);
let _ = result.unwrap();
}
}