use crate::analyses::delivery_metrics::DeliveryMetricsRow;
use crate::analyses::health_trend::HealthTrendRow;
use crate::analyses::knowledge_islands::KnowledgeIslandRow;
use crate::analyses::release_cadence::ReleaseCadenceRow;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FactorTile {
pub name: String,
pub headline: Option<f64>,
pub band: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub series: Vec<f64>,
pub attention: bool,
pub detail: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub numbers: Vec<(String, String)>,
}
#[must_use]
pub fn xmr_attention(series: &[f64]) -> bool {
if series.len() < 4 {
return false;
}
let n = series.len();
#[allow(clippy::cast_precision_loss)]
let mean = series.iter().sum::<f64>() / n as f64;
let mr_mean = {
let sum: f64 = series.windows(2).map(|w| (w[1] - w[0]).abs()).sum();
#[allow(clippy::cast_precision_loss)]
let denom = (n - 1) as f64;
sum / denom
};
let limit = 2.66 * mr_mean;
let last = series[n - 1];
if (last - mean).abs() > limit {
return true;
}
if n >= 8 {
let run = &series[n - 8..];
let all_above = run.iter().all(|&v| v > mean);
let all_below = run.iter().all(|&v| v < mean);
if all_above || all_below {
return true;
}
}
false
}
#[must_use]
pub fn health_trend_factors(rows: &[HealthTrendRow]) -> Vec<FactorTile> {
if rows.is_empty() {
return Vec::new();
}
let last = &rows[rows.len() - 1];
let code_series: Vec<f64> = rows.iter().map(|r| r.code_health).collect();
let arch_series: Vec<f64> = rows.iter().map(|r| r.arch_health).collect();
let code_score = last.code_health;
let arch_score = last.arch_health;
vec![
FactorTile {
name: "Code".into(),
headline: Some(code_score),
band: crate::bands::health_band(code_score).to_string(),
attention: xmr_attention(&code_series),
detail: format!(
"Code health {:.1} ({}) — averaged over all files at latest sample",
code_score,
crate::bands::health_band(code_score),
),
series: code_series,
numbers: Vec::new(),
},
FactorTile {
name: "Architecture".into(),
headline: Some(arch_score),
band: crate::bands::health_band(arch_score).to_string(),
attention: xmr_attention(&arch_series),
detail: format!(
"Architecture health {:.1} ({}) — propagation cost and cycle exposure",
arch_score,
crate::bands::health_band(arch_score),
),
series: arch_series,
numbers: Vec::new(),
},
]
}
#[must_use]
pub fn knowledge_factor_from_familiarity(familiarity_pct: f64, islands_pct: f64) -> FactorTile {
let headline = 0.5 * familiarity_pct + 0.5 * (100.0 - islands_pct);
FactorTile {
name: "Knowledge".into(),
headline: Some(headline),
band: crate::bands::health_band(headline).to_string(),
series: Vec::new(),
attention: false,
detail: format!(
"Team familiarity {familiarity_pct:.1}%, knowledge islands {islands_pct:.1}% of SLOC",
),
numbers: Vec::new(),
}
}
#[must_use]
pub fn knowledge_factor_from_islands(
rows: &[KnowledgeIslandRow],
total_live_files: u64,
) -> Option<FactorTile> {
if rows.is_empty() || total_live_files == 0 {
return None;
}
let departed_islands = rows.len();
#[allow(clippy::cast_precision_loss)]
let departed_share = departed_islands as f64 / total_live_files as f64;
let headline = (100.0 * (1.0 - departed_share)).clamp(0.0, 100.0);
Some(FactorTile {
name: "Knowledge".into(),
headline: Some(headline),
band: crate::bands::health_band(headline).to_string(),
series: Vec::new(),
attention: departed_share > 0.2,
detail: format!(
"{departed_islands} of {total_live_files} live files are departed knowledge islands"
),
numbers: Vec::new(),
})
}
#[must_use]
pub fn delivery_factor_from_metrics(
delivery_rows: &[DeliveryMetricsRow],
cadence_rows: &[ReleaseCadenceRow],
) -> Option<FactorTile> {
let mut numbers: Vec<(String, String)> = Vec::new();
let rework_band = if let Some(r) = delivery_rows.iter().find(|r| r.metric == "rework_pct") {
let pct = r.p50;
let band = if pct < 9.0 {
"green"
} else if pct < 15.0 {
"yellow"
} else {
"red"
};
numbers.push(("rework %".to_string(), format!("{pct:.1}")));
band
} else {
""
};
if let Some(r) = delivery_rows
.iter()
.find(|r| r.metric == "branch_duration_hours")
{
numbers.push(("branch p75 h".to_string(), format!("{:.0}", r.p75)));
}
if let Some(days) = cadence_rows
.iter()
.find(|r| r.tag == "__summary__")
.and_then(|s| s.days_since_prev)
{
numbers.push(("cadence median d".to_string(), format!("{days:.0}")));
}
if numbers.is_empty() {
return None;
}
Some(FactorTile {
name: "Delivery".into(),
headline: None,
band: rework_band.to_string(),
series: Vec::new(),
attention: false,
detail: "Git-only proxies — not DORA metrics. Rework band: Pluralsight benchmark (correlational).".into(),
numbers,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xmr_flat_series_no_attention() {
let series = vec![70.0_f64; 10];
assert!(!xmr_attention(&series));
}
#[test]
fn xmr_step_change_signals_attention() {
let mut series = vec![70.0_f64; 9];
series.push(10.0);
assert!(xmr_attention(&series));
}
#[test]
fn xmr_short_series_returns_false() {
assert!(!xmr_attention(&[]));
assert!(!xmr_attention(&[50.0, 60.0]));
assert!(!xmr_attention(&[50.0, 60.0, 55.0]));
}
#[test]
fn xmr_eight_run_below_mean_signals_attention() {
let series = vec![
90.0, 88.0, 92.0, 91.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0,
];
assert!(xmr_attention(&series));
}
#[test]
fn xmr_eight_run_above_mean_signals_attention() {
let series = vec![
10.0, 12.0, 8.0, 9.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0,
];
assert!(xmr_attention(&series));
}
#[test]
fn xmr_exactly_four_points_eligible() {
let series = vec![70.0, 71.0, 70.5, 70.8];
assert!(!xmr_attention(&series));
}
#[test]
fn health_trend_factors_empty_returns_empty() {
assert!(health_trend_factors(&[]).is_empty());
}
#[test]
fn health_trend_factors_returns_code_and_arch() {
let row = HealthTrendRow {
date: "2026-01-01".into(),
rev: "abc123".into(),
files: 5,
arch_health: 85.0,
code_health: 62.0,
combined_health: 73.5,
arch_band: "green".into(),
code_band: "yellow".into(),
combined_band: "green".into(),
};
let tiles = health_trend_factors(&[row]);
assert_eq!(tiles.len(), 2);
assert_eq!(tiles[0].name, "Code");
assert!((tiles[0].headline.unwrap() - 62.0).abs() < 1e-9);
assert_eq!(tiles[0].band, "yellow");
assert_eq!(tiles[1].name, "Architecture");
assert!((tiles[1].headline.unwrap() - 85.0).abs() < 1e-9);
assert_eq!(tiles[1].band, "green");
}
#[test]
fn knowledge_familiarity_blends_correctly() {
let tile = knowledge_factor_from_familiarity(80.0, 20.0);
assert!((tile.headline.unwrap() - 80.0).abs() < 1e-9);
assert_eq!(tile.name, "Knowledge");
assert_eq!(tile.band, "green");
}
#[test]
fn knowledge_familiarity_high_islands_lowers_headline() {
let tile = knowledge_factor_from_familiarity(100.0, 80.0);
assert!((tile.headline.unwrap() - 60.0).abs() < 1e-9);
assert_eq!(tile.band, "yellow");
}
fn island_row(entity: &str) -> KnowledgeIslandRow {
KnowledgeIslandRow {
entity: entity.into(),
main_author: "alice".into(),
ownership_pct: 90.0,
days_since_main_active: 200,
last_main_author_commit: "abc".into(),
n_substantial_others: 0,
total_loc: 100,
}
}
#[test]
fn knowledge_islands_fallback_empty_returns_none() {
assert!(knowledge_factor_from_islands(&[], 100).is_none());
}
#[test]
fn knowledge_islands_fallback_zero_live_files_returns_none() {
let rows = vec![island_row("src/a.rs")];
assert!(knowledge_factor_from_islands(&rows, 0).is_none());
}
#[test]
fn knowledge_islands_fallback_prevalence_sets_headline() {
let rows = vec![island_row("src/a.rs"), island_row("src/b.rs")];
let tile = knowledge_factor_from_islands(&rows, 10).expect("tile");
assert!((tile.headline.unwrap() - 80.0).abs() < 1e-9);
assert_eq!(tile.band, "green");
assert!(!tile.attention);
assert_eq!(
tile.detail,
"2 of 10 live files are departed knowledge islands"
);
}
#[test]
fn knowledge_islands_fallback_high_prevalence_flags_attention() {
let rows: Vec<KnowledgeIslandRow> = (0..5)
.map(|i| island_row(&format!("src/f{i}.rs")))
.collect();
let tile = knowledge_factor_from_islands(&rows, 10).expect("tile");
assert!((tile.headline.unwrap() - 50.0).abs() < 1e-9);
assert_eq!(tile.band, "yellow");
assert!(tile.attention);
}
fn make_delivery_row(metric: &str, p50: f64, p75: f64) -> DeliveryMetricsRow {
DeliveryMetricsRow {
metric: metric.to_string(),
p50,
p75,
p90: 0.0,
n: 5,
caveat: String::new(),
}
}
fn make_cadence_summary(median_days: f64) -> ReleaseCadenceRow {
ReleaseCadenceRow {
tag: "__summary__".to_string(),
date: "iqr=3.0d".to_string(),
days_since_prev: Some(median_days),
trend: "stable".to_string(),
}
}
#[test]
fn delivery_factor_both_empty_returns_none() {
assert!(delivery_factor_from_metrics(&[], &[]).is_none());
}
#[test]
fn delivery_factor_rework_only_returns_tile() {
let delivery = vec![make_delivery_row("rework_pct", 7.0, 7.0)];
let tile = delivery_factor_from_metrics(&delivery, &[]).expect("tile");
assert_eq!(tile.name, "Delivery");
assert!(tile.headline.is_none());
assert_eq!(tile.band, "green"); assert_eq!(tile.numbers.len(), 1);
assert_eq!(tile.numbers[0].0, "rework %");
assert_eq!(tile.numbers[0].1, "7.0");
}
#[test]
fn delivery_factor_rework_yellow_band() {
let delivery = vec![make_delivery_row("rework_pct", 10.0, 10.0)];
let tile = delivery_factor_from_metrics(&delivery, &[]).expect("tile");
assert_eq!(tile.band, "yellow");
}
#[test]
fn delivery_factor_rework_red_band() {
let delivery = vec![make_delivery_row("rework_pct", 15.0, 15.0)];
let tile = delivery_factor_from_metrics(&delivery, &[]).expect("tile");
assert_eq!(tile.band, "red");
}
#[test]
fn delivery_factor_all_three_numbers_present() {
let delivery = vec![
make_delivery_row("rework_pct", 5.0, 5.0),
make_delivery_row("branch_duration_hours", 12.0, 26.0),
];
let cadence = vec![make_cadence_summary(14.0)];
let tile = delivery_factor_from_metrics(&delivery, &cadence).expect("tile");
assert_eq!(tile.numbers.len(), 3);
assert_eq!(tile.numbers[0].0, "rework %");
assert_eq!(tile.numbers[1].0, "branch p75 h");
assert_eq!(tile.numbers[1].1, "26"); assert_eq!(tile.numbers[2].0, "cadence median d");
assert_eq!(tile.numbers[2].1, "14");
}
#[test]
fn delivery_factor_no_rework_no_band() {
let cadence = vec![make_cadence_summary(7.0)];
let tile = delivery_factor_from_metrics(&[], &cadence).expect("tile");
assert_eq!(tile.band, "");
assert_eq!(tile.numbers.len(), 1);
assert_eq!(tile.numbers[0].0, "cadence median d");
}
}