use anyhow::{Context, Result};
use chrono::{DateTime, Duration, Utc};
use git2::Repository;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ValueStreamMetrics {
pub value_added_ratio: f64,
pub coding_time_hours: f64,
pub wait_time_hours: f64,
pub total_lead_time_hours: f64,
pub process_efficiency: f64,
pub bottleneck: String,
}
pub fn analyze_value_stream(repo_path: &str, days: usize) -> Result<ValueStreamMetrics> {
let repo = Repository::open(repo_path).context("Failed to open git repository")?;
let cutoff_date = Utc::now() - Duration::days(days as i64);
let mut revwalk = repo.revwalk().context("Failed to create revwalk")?;
revwalk.push_head().context("Failed to push HEAD")?;
let mut coding_time_accum = 0.0;
let mut wait_time_accum = 0.0;
let _total_commits = 0;
for oid in revwalk {
let oid = oid?;
let commit = repo.find_commit(oid)?;
let time = commit.time();
let commit_date = DateTime::<Utc>::from_timestamp(time.seconds(), 0).unwrap_or_default();
if commit_date < cutoff_date {
break;
}
let (coding_hours, wait_hours) = estimate_times(&repo, &commit)?;
coding_time_accum += coding_hours;
wait_time_accum += wait_hours;
}
let total_time = coding_time_accum + wait_time_accum;
let value_added_ratio = if total_time > 0.0 {
coding_time_accum / total_time
} else {
0.0
};
let process_efficiency = value_added_ratio;
let bottleneck = if wait_time_accum > coding_time_accum * 2.0 {
"Wait time (review/deploy)".to_string()
} else {
"Coding (implementation)".to_string()
};
Ok(ValueStreamMetrics {
value_added_ratio,
coding_time_hours: coding_time_accum,
wait_time_hours: wait_time_accum,
total_lead_time_hours: total_time,
process_efficiency,
bottleneck,
})
}
fn estimate_times(repo: &Repository, commit: &git2::Commit) -> Result<(f64, f64)> {
let mut coding_hours: f64 = 0.0;
if let Ok(parent_commit) = commit.parent(0) {
let tree = commit.tree().ok();
let parent_tree = parent_commit.tree().ok();
if let (Some(tree), Some(parent_tree)) = (tree, parent_tree) {
if let Ok(diff) = repo.diff_tree_to_tree(Some(&parent_tree), Some(&tree), None) {
let stats = diff.stats()?;
let files_changed = stats.files_changed();
coding_hours = files_changed as f64 * 0.25;
}
}
}
coding_hours = coding_hours.max(0.05_f64);
coding_hours = coding_hours.min(4.0_f64);
let is_merge = commit.parent_count() > 1;
let mut wait_hours = if is_merge {
3.0_f64
} else {
0.75_f64
};
let msg = commit.message().unwrap_or("");
if msg.contains("WIP") || msg.contains("draft") {
wait_hours *= 0.3;
} else if msg.contains("fix") || msg.contains("hotfix") {
wait_hours *= 0.5;
}
Ok((coding_hours, wait_hours))
}
pub fn generate_report(metrics: &ValueStreamMetrics) -> String {
use colored::*;
let mut report = String::new();
report.push_str(&"\n".bold());
report.push_str(&"=== VALUE STREAM ANALYSIS ===\n".bold());
report.push('\n');
report.push_str(&"Time Breakdown:\n".bold());
report.push_str(&format!(
" Coding time: {:.2} hours (active work)\n",
metrics.coding_time_hours
));
report.push_str(&format!(
" Wait time: {:.2} hours (review, test, deploy)\n",
metrics.wait_time_hours
));
report.push_str(&format!(
" Total lead time: {:.2} hours\n",
metrics.total_lead_time_hours
));
report.push_str(&"\nValue-Added Metrics:\n".bold());
report.push_str(&format!(
" Value-added ratio: {:.1}% (coding / total)\n",
metrics.value_added_ratio * 100.0
));
let ratio_status = if metrics.value_added_ratio >= 0.3 {
"✅".green()
} else if metrics.value_added_ratio >= 0.2 {
"⚠️".yellow()
} else {
"❌".red()
};
report.push_str(&format!(" Status: {} (target: >30%)\n", ratio_status));
report.push_str(&format!(
" Process efficiency: {:.1}%\n",
metrics.process_efficiency * 100.0
));
report.push_str(&"\nBottleneck Analysis:\n".bold());
report.push_str(&format!(" Primary bottleneck: {}\n", metrics.bottleneck));
report.push_str(&"\nValue Stream Assessment:\n".bold());
if metrics.value_added_ratio >= 0.3 {
report.push_str(&" • Excellent: High value-added ratio\n".green());
} else if metrics.value_added_ratio >= 0.2 {
report.push_str(&" • Fair: Moderate value-added, room for improvement\n".yellow());
} else {
report.push_str(&" • Poor: Low value-added ratio, excessive wait time\n".red());
}
report.push_str(&"\nKaizen Recommendations:\n".bold());
if metrics.value_added_ratio < 0.3 {
report
.push_str(&" • Value-added ratio below 30%. Reduce wait time in pipeline.\n".yellow());
}
if metrics.wait_time_hours > metrics.coding_time_hours * 2.0 {
report.push_str(&" • Wait time dominates. Streamline review and deployment.\n".yellow());
}
if metrics.bottleneck.contains("Wait") {
report.push_str(&" • Focus on reducing review/deployment delays.\n".yellow());
}
if metrics.value_added_ratio >= 0.3 && metrics.bottleneck.contains("Coding") {
report.push_str(&" • Value stream is efficient! Focus on coding quality.\n".green());
}
report.push_str(&"\nValue Stream Map (text):\n".bold());
report.push_str(" ┌────────────────────────────────────────────┐\n");
report.push_str(" │ Idea → Coding → Review → Test → Deploy │\n");
report.push_str(" │ └───┬───────┬───────┬──────┬───────┘ │\n");
report.push_str(" │ │ │ │ │ │\n");
report.push_str(&format!(
" │ {:.1}h {:.1}h {:.1}h {:.1}h {:.1}h │\n",
metrics.coding_time_hours * 0.1, metrics.coding_time_hours * 0.9, metrics.wait_time_hours * 0.3, metrics.wait_time_hours * 0.3, metrics.wait_time_hours * 0.4
)); report.push_str(" └────────────────────────────────────────────┘\n");
report.push('\n');
report
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value_added_ratio_bounded() {
let cases: Vec<(f64, f64)> = vec![
(1.0, 1.0),
(0.0, 1.0),
(1.0, 0.0),
(100.0, 1.0),
(0.05, 3.0),
];
for (coding, wait) in cases {
let total = coding + wait;
let ratio = if total > 0.0 { coding / total } else { 0.0 };
assert!(
(0.0..=1.0).contains(&ratio),
"ratio {ratio} out of [0,1] for coding={coding}, wait={wait}"
);
}
}
#[test]
fn test_process_efficiency_equals_value_added_ratio() {
let m = ValueStreamMetrics {
value_added_ratio: 0.42,
coding_time_hours: 4.2,
wait_time_hours: 5.8,
total_lead_time_hours: 10.0,
process_efficiency: 0.42, bottleneck: "Coding (implementation)".to_string(),
};
assert_eq!(
m.value_added_ratio, m.process_efficiency,
"process_efficiency must equal value_added_ratio"
);
}
#[test]
fn test_bottleneck_is_non_empty() {
for (coding, wait, expected_contains) in [
(1.0, 10.0, "Wait"), (5.0, 0.0, "Coding"), (3.0, 3.0, "Coding"), ] {
let bottleneck = if wait > coding * 2.0 {
"Wait time (review/deploy)".to_string()
} else {
"Coding (implementation)".to_string()
};
assert!(
!bottleneck.is_empty(),
"bottleneck must not be empty (coding={coding}, wait={wait})"
);
assert!(
bottleneck.contains(expected_contains),
"expected '{expected_contains}' in bottleneck '{bottleneck}' (coding={coding}, wait={wait})"
);
}
}
#[test]
fn test_doubling_wait_decreases_value_added_ratio() {
let coding = 2.0;
let wait_low = 1.0;
let wait_high = 2.0;
let ratio_low = coding / (coding + wait_low);
let ratio_high = coding / (coding + wait_high);
assert!(
ratio_high < ratio_low,
"doubling wait should decrease ratio: {ratio_low} → {ratio_high}"
);
}
#[test]
fn test_coding_time_minimum_clamp() {
let files_changed = 0usize;
let coding_hours = (files_changed as f64 * 0.25).max(0.05_f64);
assert_eq!(coding_hours, 0.05, "zero-file commit should clamp to 0.05h");
}
#[test]
fn test_coding_time_maximum_cap() {
let files_changed = 100usize;
let coding_hours = (files_changed as f64 * 0.25)
.max(0.05_f64)
.min(4.0_f64);
assert_eq!(coding_hours, 4.0, "large commit should cap at 4.0h");
}
#[test]
fn test_total_time_equals_coding_plus_wait() {
let coding = 3.5;
let wait = 1.2;
let total = coding + wait;
let m = ValueStreamMetrics {
value_added_ratio: coding / total,
coding_time_hours: coding,
wait_time_hours: wait,
total_lead_time_hours: total,
process_efficiency: coding / total,
bottleneck: "Coding (implementation)".to_string(),
};
let reconstructed = m.coding_time_hours + m.wait_time_hours;
assert!(
(reconstructed - m.total_lead_time_hours).abs() < 1e-9,
"total {total} != coding {coding} + wait {wait} = {reconstructed}"
);
}
}